From 96c57418f39bbf10e4538a6e0652baff7f0932fb Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:13:02 -0600 Subject: [PATCH] feat(desktop): review flow --- .../desktop/src/components/prompt-input.tsx | 1 + packages/desktop/src/context/local.tsx | 17 +- packages/desktop/src/context/session.tsx | 8 +- packages/desktop/src/context/sync.tsx | 59 +++- packages/desktop/src/pages/layout.tsx | 4 +- packages/desktop/src/pages/session.tsx | 301 +++++++++++++++--- packages/ui/src/components/accordion.tsx | 11 +- packages/ui/src/components/button.css | 5 +- packages/ui/src/components/diff-changes.tsx | 7 +- packages/ui/src/components/icon.tsx | 3 + packages/ui/src/components/tooltip.tsx | 2 +- 11 files changed, 341 insertions(+), 77 deletions(-) diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 74443d6a..15bc54c4 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -338,6 +338,7 @@ export const PromptInput: Component = (props) => { // session.layout.copyTabs("", session.id) } session.layout.setActiveTab(undefined) + session.messages.setActive(undefined) const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path)) const attachments = session.prompt.current().filter((part) => part.type === "file") diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 9dacc710..1785bdf0 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -464,9 +464,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ opened: true, width: 240, }, + review: { + state: "closed" as "open" | "closed" | "tab", + }, }), { - name: "layout", + name: "default-layout", }, ) @@ -487,6 +490,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ setStore("sidebar", "width", width) }, }, + review: { + state: createMemo(() => store.review?.state ?? "closed"), + open() { + setStore("review", "state", "open") + }, + close() { + setStore("review", "state", "closed") + }, + tab() { + setStore("review", "state", "tab") + }, + }, } })() diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 77ab3bc2..61fed945 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -80,6 +80,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex const model = createMemo(() => last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined, ) + const diffs = createMemo(() => (props.sessionId ? (sync.data.session_diff[props.sessionId] ?? []) : [])) const tokens = createMemo(() => { if (!last()) return @@ -98,6 +99,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex id: props.sessionId, info, working, + diffs, prompt: { current: createMemo(() => store.prompt), cursor: createMemo(() => store.cursorPosition), @@ -139,8 +141,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex if (tab.startsWith("file://")) { await local.file.open(tab.replace("file://", "")) } - if (!store.tabs.opened.includes(tab)) { - setStore("tabs", "opened", [...store.tabs.opened, tab]) + if (tab !== "review") { + if (!store.tabs.opened.includes(tab)) { + setStore("tabs", "opened", [...store.tabs.opened, tab]) + } } setStore("tabs", "active", tab) }, diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 1e960397..c5b169a3 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -1,4 +1,17 @@ -import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode, Project } from "@opencode-ai/sdk" +import type { + Message, + Agent, + Provider, + Session, + Part, + Config, + Path, + File, + FileNode, + Project, + FileDiff, + Todo, +} from "@opencode-ai/sdk" import { createStore, produce, reconcile } from "solid-js/store" import { createMemo } from "solid-js" import { Binary } from "@/utils/binary" @@ -16,8 +29,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ config: Config path: Path session: Session[] + session_diff: { + [sessionID: string]: FileDiff[] + } + todo: { + [sessionID: string]: Todo[] + } limit: number - more: boolean message: { [sessionID: string]: Message[] } @@ -34,8 +52,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ agent: [], provider: [], session: [], + session_diff: {}, + todo: {}, limit: 10, - more: false, message: {}, part: {}, node: [], @@ -60,6 +79,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ ) break } + case "session.diff": + setStore("session_diff", event.properties.sessionID, event.properties.diff) + break + case "todo.updated": + setStore("todo", event.properties.sessionID, event.properties.todos) + break case "message.updated": { const messages = store.message[event.properties.info.sessionID] if (!messages) { @@ -116,7 +141,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ .sort((a, b) => a.id.localeCompare(b.id)) .slice(0, store.limit) setStore("session", sessions) - setStore("more", sessions.length === store.limit) }), config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)), changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)), @@ -161,22 +185,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (match.found) return store.session[match.index] return undefined }, - async sync(sessionID: string, isRetry = false) { - const [session, messages] = await Promise.all([ - sdk.client.session.get({ path: { id: sessionID } }), + async sync(sessionID: string, _isRetry = false) { + const [session, messages, todo, diff] = await Promise.all([ + sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }), sdk.client.session.messages({ path: { id: sessionID } }), + sdk.client.session.todo({ path: { id: sessionID } }), + sdk.client.session.diff({ path: { id: sessionID } }), ]) - - // If no messages and this might be a new session, retry after a delay - if (!isRetry && messages.data!.length === 0) { - setTimeout(() => this.sync(sessionID, true), 500) - return - } - setStore( produce((draft) => { const match = Binary.search(draft.session, sessionID, (s) => s.id) - draft.session[match.index] = session.data! + if (match.found) draft.session[match.index] = session.data! + if (!match.found) draft.session.splice(match.index, 0, session.data!) + draft.todo[sessionID] = todo.data ?? [] draft.message[sessionID] = messages .data!.map((x) => x.info) .slice() @@ -187,13 +208,21 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ .map(sanitizePart) .sort((a, b) => a.id.localeCompare(b.id)) } + draft.session_diff[sessionID] = diff.data ?? [] }), ) + + // If no messages and this might be a new session, retry after a delay + // if (!isRetry && messages.data!.length === 0) { + // setTimeout(() => this.sync(sessionID, true), 500) + // return + // } }, fetch: async (count = 10) => { setStore("limit", (x) => x + count) await load.session() }, + more: createMemo(() => store.session.length === store.limit), }, load, absolute, diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index c5957a2d..d8856400 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -80,7 +80,7 @@ export default function Layout(props: ParentProps) { }} - + + + +
1}>
+ } + > + +