From d525fbf82940442d8d47265b7d7d0a9af8c282bc Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 5 Nov 2025 11:55:31 -0600
Subject: feat(desktop): session router, interrupt agent, visual cleanup
---
packages/desktop/src/context/sync.tsx | 14 +-------------
1 file changed, 1 insertion(+), 13 deletions(-)
(limited to 'packages/desktop/src/context/sync.tsx')
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 06fc05677..c60206b0b 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -1,16 +1,4 @@
-import type {
- Message,
- Agent,
- Provider,
- Session,
- Part,
- Config,
- Path,
- File,
- FileNode,
- Project,
- Command,
-} from "@opencode-ai/sdk"
+import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode, Project } from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@/utils/binary"
--
cgit v1.2.3
From e006e3355cff3de25e023edcee0b59985e7db66b Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:32:05 -0600
Subject: feat(desktop): incrementally load sessions in side nav
---
packages/desktop/src/context/sync.tsx | 22 +++++---
packages/desktop/src/pages/layout.tsx | 95 ++++++++++++++++++++---------------
2 files changed, 70 insertions(+), 47 deletions(-)
(limited to 'packages/desktop/src/context/sync.tsx')
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index c60206b0b..1e960397b 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -16,6 +16,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
config: Config
path: Path
session: Session[]
+ limit: number
+ more: boolean
message: {
[sessionID: string]: Message[]
}
@@ -32,6 +34,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
agent: [],
provider: [],
session: [],
+ limit: 10,
+ more: false,
message: {},
part: {},
node: [],
@@ -106,12 +110,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
session: () =>
- sdk.client.session.list().then((x) =>
- setStore(
- "session",
- (x.data ?? []).slice().sort((a, b) => a.id.localeCompare(b.id)),
- ),
- ),
+ sdk.client.session.list().then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .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!)),
node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
@@ -184,6 +190,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}),
)
},
+ fetch: async (count = 10) => {
+ setStore("limit", (x) => x + count)
+ await load.session()
+ },
},
load,
absolute,
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 22f0a50de..1d9669604 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -1,5 +1,5 @@
import { Button, Tooltip, DiffChanges } from "@opencode-ai/ui"
-import { createMemo, ParentProps, Show } from "solid-js"
+import { createMemo, For, ParentProps, Show } from "solid-js"
import { getFilename } from "@/utils"
import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
@@ -9,61 +9,74 @@ import { A, useParams } from "@solidjs/router"
export default function Layout(props: ParentProps) {
const params = useParams()
const sync = useSync()
+
return (
-
{props.children}
--
cgit v1.2.3
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: feat(desktop): review flow
---
packages/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 | 299 +++++++++++++++++++----
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, 340 insertions(+), 76 deletions(-)
(limited to 'packages/desktop/src/context/sync.tsx')
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index 74443d6a5..15bc54c49 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 9dacc7100..1785bdf0c 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 77ab3bc25..61fed945e 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 1e960397b..c5b169a38 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 c5957a2d2..d88564007 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) {
}}
-
+
-
-
- New session
-
-
-
- {getDirectory(sync.data.path.directory)}
- {getFilename(sync.data.path.directory)}
-
+
+ New session
+
+
+
+ {getDirectory(sync.data.path.directory)}
+ {getFilename(sync.data.path.directory)}
-
-
-
- Last modified
-
- {DateTime.fromMillis(sync.data.project.time.created).toRelative()}
-
-
+
+
+
+
+ Last modified
+
+ {DateTime.fromMillis(sync.data.project.time.created).toRelative()}
+
- }
- >
- {(_) => {
- return (
-
-
+
+ }
+ >
+ {(_) => {
+ return (
+
+
+
+
+
+
+
+
1}>
{(message) => {
@@ -351,11 +397,43 @@ export default function Page() {
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const working = createMemo(() => !message.summary?.body && !error())
+ const handleClick = () => session.messages.setActive(message.id)
+
return (
- -
+
-
+
+
+ {message.summary?.title}
+
+ }
+ >
+
+
+ )
+ }}
+
- {/*
*/}
+
+
+
+
+
{(tab) => {
const [file] = createResource(
diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx
index 535d38e3d..02f00b7be 100644
--- a/packages/ui/src/components/accordion.tsx
+++ b/packages/ui/src/components/accordion.tsx
@@ -1,9 +1,11 @@
import { Accordion as Kobalte } from "@kobalte/core/accordion"
-import { splitProps } from "solid-js"
+import { createSignal, splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface AccordionProps extends ComponentProps {}
-export interface AccordionItemProps extends ComponentProps {}
+export interface AccordionItemProps extends ComponentProps {
+ defaultOpen?: boolean
+}
export interface AccordionHeaderProps extends ComponentProps {}
export interface AccordionTriggerProps extends ComponentProps {}
export interface AccordionContentProps extends ComponentProps {}
@@ -23,11 +25,14 @@ function AccordionRoot(props: AccordionProps) {
}
function AccordionItem(props: AccordionItemProps) {
- const [split, rest] = splitProps(props, ["class", "classList"])
+ const [split, rest] = splitProps(props, ["class", "classList", "defaultOpen"])
+ const [open, setOpen] = createSignal(split.defaultOpen ?? false)
return (
0 : true}>
-
+