summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-26 18:23:04 -0600
committerGitHub <[email protected]>2026-02-26 18:23:04 -0600
commitfc52e4b2d3a41efde772e6de8fb2e01f27821701 (patch)
treecf23af294a00a10e55f230232585344c111f0bb9 /packages/app/src/context
parent9a6bfeb782766099d4ce3a98bb9e7b4e79f8bfe6 (diff)
downloadopencode-fc52e4b2d3a41efde772e6de8fb2e01f27821701.tar.gz
opencode-fc52e4b2d3a41efde772e6de8fb2e01f27821701.zip
feat(app): better diff/code comments (#14621)
Co-authored-by: adamelmore <[email protected]> Co-authored-by: David Hill <[email protected]>
Diffstat (limited to 'packages/app/src/context')
-rw-r--r--packages/app/src/context/comments.test.ts33
-rw-r--r--packages/app/src/context/comments.tsx55
-rw-r--r--packages/app/src/context/file/view-cache.ts2
-rw-r--r--packages/app/src/context/layout-scroll.test.ts20
-rw-r--r--packages/app/src/context/layout-scroll.ts12
-rw-r--r--packages/app/src/context/prompt.tsx28
6 files changed, 147 insertions, 3 deletions
diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts
index bee5c7871..82fa170f2 100644
--- a/packages/app/src/context/comments.test.ts
+++ b/packages/app/src/context/comments.test.ts
@@ -150,4 +150,37 @@ describe("comments session indexing", () => {
dispose()
})
})
+
+ test("update changes only the targeted comment body", () => {
+ createRoot((dispose) => {
+ const comments = createCommentSessionForTest({
+ "a.ts": [line("a.ts", "a1", 10), line("a.ts", "a2", 20)],
+ })
+
+ comments.update("a.ts", "a2", "edited")
+
+ expect(comments.list("a.ts").map((item) => item.comment)).toEqual(["a1", "edited"])
+
+ dispose()
+ })
+ })
+
+ test("replace swaps comment state and clears focus state", () => {
+ createRoot((dispose) => {
+ const comments = createCommentSessionForTest({
+ "a.ts": [line("a.ts", "a1", 10)],
+ })
+
+ comments.setFocus({ file: "a.ts", id: "a1" })
+ comments.setActive({ file: "a.ts", id: "a1" })
+ comments.replace([line("b.ts", "b1", 30)])
+
+ expect(comments.list("a.ts")).toEqual([])
+ expect(comments.list("b.ts").map((item) => item.id)).toEqual(["b1"])
+ expect(comments.focus()).toBeNull()
+ expect(comments.active()).toBeNull()
+
+ dispose()
+ })
+ })
})
diff --git a/packages/app/src/context/comments.tsx b/packages/app/src/context/comments.tsx
index ecf63e45b..a97010c0a 100644
--- a/packages/app/src/context/comments.tsx
+++ b/packages/app/src/context/comments.tsx
@@ -44,6 +44,37 @@ function aggregate(comments: Record<string, LineComment[]>) {
.sort((a, b) => a.time - b.time)
}
+function cloneSelection(selection: SelectedLineRange): SelectedLineRange {
+ const next: SelectedLineRange = {
+ start: selection.start,
+ end: selection.end,
+ }
+
+ if (selection.side) next.side = selection.side
+ if (selection.endSide) next.endSide = selection.endSide
+ return next
+}
+
+function cloneComment(comment: LineComment): LineComment {
+ return {
+ ...comment,
+ selection: cloneSelection(comment.selection),
+ }
+}
+
+function group(comments: LineComment[]) {
+ return comments.reduce<Record<string, LineComment[]>>((acc, comment) => {
+ const list = acc[comment.file]
+ const next = cloneComment(comment)
+ if (list) {
+ list.push(next)
+ return acc
+ }
+ acc[comment.file] = [next]
+ return acc
+ }, {})
+}
+
function createCommentSessionState(store: Store<CommentStore>, setStore: SetStoreFunction<CommentStore>) {
const [state, setState] = createStore({
focus: null as CommentFocus | null,
@@ -70,6 +101,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
id: uuid(),
time: Date.now(),
...input,
+ selection: cloneSelection(input.selection),
}
batch(() => {
@@ -87,6 +119,23 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
})
}
+ const update = (file: string, id: string, comment: string) => {
+ setStore("comments", file, (items) =>
+ (items ?? []).map((item) => {
+ if (item.id !== id) return item
+ return { ...item, comment }
+ }),
+ )
+ }
+
+ const replace = (comments: LineComment[]) => {
+ batch(() => {
+ setStore("comments", reconcile(group(comments)))
+ setFocus(null)
+ setActive(null)
+ })
+ }
+
const clear = () => {
batch(() => {
setStore("comments", reconcile({}))
@@ -100,6 +149,8 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
all,
add,
remove,
+ update,
+ replace,
clear,
focus: () => state.focus,
setFocus,
@@ -132,6 +183,8 @@ function createCommentSession(dir: string, id: string | undefined) {
all: session.all,
add: session.add,
remove: session.remove,
+ update: session.update,
+ replace: session.replace,
clear: session.clear,
focus: session.focus,
setFocus: session.setFocus,
@@ -176,6 +229,8 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
all: () => session().all(),
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
remove: (file: string, id: string) => session().remove(file, id),
+ update: (file: string, id: string, comment: string) => session().update(file, id, comment),
+ replace: (comments: LineComment[]) => session().replace(comments),
clear: () => session().clear(),
focus: () => session().focus(),
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
diff --git a/packages/app/src/context/file/view-cache.ts b/packages/app/src/context/file/view-cache.ts
index 6e8ddf62d..4c060174a 100644
--- a/packages/app/src/context/file/view-cache.ts
+++ b/packages/app/src/context/file/view-cache.ts
@@ -9,7 +9,7 @@ const MAX_FILE_VIEW_SESSIONS = 20
const MAX_VIEW_FILES = 500
function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
- if (range.start <= range.end) return range
+ if (range.start <= range.end) return { ...range }
const startSide = range.side
const endSide = range.endSide ?? startSide
diff --git a/packages/app/src/context/layout-scroll.test.ts b/packages/app/src/context/layout-scroll.test.ts
index 2a13e4020..483be150f 100644
--- a/packages/app/src/context/layout-scroll.test.ts
+++ b/packages/app/src/context/layout-scroll.test.ts
@@ -41,4 +41,24 @@ describe("createScrollPersistence", () => {
vi.useRealTimers()
}
})
+
+ test("reseeds empty cache after persisted snapshot loads", () => {
+ const snapshot = {
+ session: {},
+ } as Record<string, Record<string, { x: number; y: number }>>
+
+ const scroll = createScrollPersistence({
+ getSnapshot: (sessionKey) => snapshot[sessionKey],
+ onFlush: () => {},
+ })
+
+ expect(scroll.scroll("session", "review")).toBeUndefined()
+
+ snapshot.session = {
+ review: { x: 12, y: 34 },
+ }
+
+ expect(scroll.scroll("session", "review")).toEqual({ x: 12, y: 34 })
+ scroll.dispose()
+ })
})
diff --git a/packages/app/src/context/layout-scroll.ts b/packages/app/src/context/layout-scroll.ts
index 30b0f6904..ef66eccd9 100644
--- a/packages/app/src/context/layout-scroll.ts
+++ b/packages/app/src/context/layout-scroll.ts
@@ -33,8 +33,16 @@ export function createScrollPersistence(opts: Options) {
}
function seed(sessionKey: string) {
- if (cache[sessionKey]) return
- setCache(sessionKey, clone(opts.getSnapshot(sessionKey)))
+ const next = clone(opts.getSnapshot(sessionKey))
+ const current = cache[sessionKey]
+ if (!current) {
+ setCache(sessionKey, next)
+ return
+ }
+
+ if (Object.keys(current).length > 0) return
+ if (Object.keys(next).length === 0) return
+ setCache(sessionKey, next)
}
function scroll(sessionKey: string, tab: string) {
diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx
index 064892105..fb8226559 100644
--- a/packages/app/src/context/prompt.tsx
+++ b/packages/app/src/context/prompt.tsx
@@ -116,6 +116,10 @@ function contextItemKey(item: ContextItem) {
return `${key}:c=${digest.slice(0, 8)}`
}
+function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
+ return item.type === "file" && !!item.comment?.trim()
+}
+
function createPromptActions(
setStore: SetStoreFunction<{
prompt: Prompt
@@ -189,6 +193,26 @@ function createPromptSession(dir: string, id: string | undefined) {
remove(key: string) {
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
},
+ removeComment(path: string, commentID: string) {
+ setStore("context", "items", (items) =>
+ items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
+ )
+ },
+ updateComment(path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) {
+ setStore("context", "items", (items) =>
+ items.map((item) => {
+ if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
+ const value = { ...item, ...next }
+ return { ...value, key: contextItemKey(value) }
+ }),
+ )
+ },
+ replaceComments(items: FileContextItem[]) {
+ setStore("context", "items", (current) => [
+ ...current.filter((item) => !isCommentItem(item)),
+ ...items.map((item) => ({ ...item, key: contextItemKey(item) })),
+ ])
+ },
},
set: actions.set,
reset: actions.reset,
@@ -251,6 +275,10 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
items: () => session().context.items(),
add: (item: ContextItem) => session().context.add(item),
remove: (key: string) => session().context.remove(key),
+ removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID),
+ updateComment: (path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) =>
+ session().context.updateComment(path, commentID, next),
+ replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
},
set: (prompt: Prompt, cursorPosition?: number) => session().set(prompt, cursorPosition),
reset: () => session().reset(),