From d9c96d7014fa78559ceaf6b276afed80c81f08f9 Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Fri, 2 Jan 2026 10:51:15 -0400 Subject: [PATCH 01/10] feat(app): add view archived sessions to sidebar menu - Add dialog to list and restore archived sessions - Update server to support archived filter in session list - Add archived parameter to SDK session.list method --- .../dialog-view-archived-sessions.tsx | 76 +++++++++++++++++++ packages/app/src/pages/layout.tsx | 6 ++ packages/opencode/src/server/server.ts | 19 ++++- packages/sdk/js/src/v2/gen/sdk.gen.ts | 13 +++- packages/sdk/openapi.json | 7 ++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 packages/app/src/components/dialog-view-archived-sessions.tsx diff --git a/packages/app/src/components/dialog-view-archived-sessions.tsx b/packages/app/src/components/dialog-view-archived-sessions.tsx new file mode 100644 index 00000000000..fe7aa65267a --- /dev/null +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -0,0 +1,76 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { Icon } from "@opencode-ai/ui/icon" +import { Spinner } from "@opencode-ai/ui/spinner" +import { createResource, For, Show } from "solid-js" +import { useGlobalSDK } from "@/context/global-sdk" +import { type LocalProject } from "@/context/layout" +import { base64Encode } from "@opencode-ai/util/encode" +import { DateTime } from "luxon" +import { useNavigate } from "@solidjs/router" + +export function DialogViewArchivedSessions(props: { project: LocalProject }) { + const dialog = useDialog() + const globalSDK = useGlobalSDK() + const navigate = useNavigate() + + const [archivedSessions] = createResource(async () => { + const result = await globalSDK.client.session.list({ + directory: props.project.worktree, + archived: true, + }) + return result.data ?? [] + }) + + async function restoreSession(sessionID: string) { + await globalSDK.client.session.update({ + directory: props.project.worktree, + sessionID, + time: { archived: undefined }, + }) + navigate(`/${base64Encode(props.project.worktree)}/session/${sessionID}`) + dialog.close() + } + + return ( + +
+ +
No archived sessions
+
+ } + > +
+ +
+ + 0}> +
+ + {(session) => ( + + )} + +
+
+
+
+ ) +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index e237d21845d..9898f3bf4a1 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -52,6 +52,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogEditProject } from "@/components/dialog-edit-project" +import { DialogViewArchivedSessions } from "@/components/dialog-view-archived-sessions" import { DialogSelectServer } from "@/components/dialog-select-server" import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" @@ -787,6 +788,11 @@ export default function Layout(props: ParentProps) { + dialog.show(() => )} + > + View archived sessions + dialog.show(() => )} > diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 4c6dac415bb..d9bb8654d78 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -651,14 +651,25 @@ export namespace Server { }, }, }), + validator( + "query", + z.object({ + archived: z.boolean().optional(), + }), + ), async (c) => { + const { archived } = c.req.valid("query") const sessions = await Array.fromAsync(Session.list()) - pipe( - await Array.fromAsync(Session.list()), - filter((s) => !s.time.archived), + const filtered = pipe( + sessions, + archived === true + ? filter((s) => s.time.archived) + : archived === false + ? filter((s) => !s.time.archived) + : filter((s) => !s.time.archived), sortBy((s) => s.time.updated), ) - return c.json(sessions) + return c.json(filtered) }, ) .get( diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f56e8367795..8a13b4489a7 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -710,10 +710,21 @@ export class Session extends HeyApiClient { public list( parameters?: { directory?: string + archived?: boolean }, options?: Options, ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "archived" }, + ], + }, + ], + ) return (options?.client ?? this.client).get({ url: "/session", ...options, diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3393d1c8618..ee92358fe51 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -894,6 +894,13 @@ "schema": { "type": "string" } + }, + { + "in": "query", + "name": "archived", + "schema": { + "type": "boolean" + } } ], "summary": "List sessions", From 485bab469a4029a010d555f798ca0ee8fd5a74ce Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 08:48:48 -0400 Subject: [PATCH 02/10] fix: updated zod to coerce boolean from string in server.ts --- packages/opencode/src/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index d9bb8654d78..dee00f4bcec 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -654,7 +654,7 @@ export namespace Server { validator( "query", z.object({ - archived: z.boolean().optional(), + archived: z.coerce.boolean().optional(), }), ), async (c) => { From a604fdad0b255b399f78b660434b066531f670c3 Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 09:09:12 -0400 Subject: [PATCH 03/10] fix: replace path references with actual TypeScript declarations in custom-elements.d.ts --- packages/app/src/custom-elements.d.ts | 18 +++++++++++++++++- packages/enterprise/src/custom-elements.d.ts | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts index e4ea0d6cebd..49ec4449fa2 120000 --- a/packages/app/src/custom-elements.d.ts +++ b/packages/app/src/custom-elements.d.ts @@ -1 +1,17 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +import { DIFFS_TAG_NAME } from "@pierre/diffs" + +/** + * TypeScript declaration for the custom element. + * This tells TypeScript that is a valid JSX element in SolidJS. + * Required for using the @pierre/diffs web component in .tsx files. + */ + +declare module "solid-js" { + namespace JSX { + interface IntrinsicElements { + [DIFFS_TAG_NAME]: HTMLAttributes + } + } +} + +export {} diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts index e4ea0d6cebd..49ec4449fa2 120000 --- a/packages/enterprise/src/custom-elements.d.ts +++ b/packages/enterprise/src/custom-elements.d.ts @@ -1 +1,17 @@ -../../ui/src/custom-elements.d.ts \ No newline at end of file +import { DIFFS_TAG_NAME } from "@pierre/diffs" + +/** + * TypeScript declaration for the custom element. + * This tells TypeScript that is a valid JSX element in SolidJS. + * Required for using the @pierre/diffs web component in .tsx files. + */ + +declare module "solid-js" { + namespace JSX { + interface IntrinsicElements { + [DIFFS_TAG_NAME]: HTMLAttributes + } + } +} + +export {} From 60f86bda6851e9871daf16215e88a4bb2039233e Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 14:11:31 -0400 Subject: [PATCH 04/10] fix: remove unsupported size prop from Spinner and use chevron-right icon --- packages/app/src/components/dialog-view-archived-sessions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/dialog-view-archived-sessions.tsx b/packages/app/src/components/dialog-view-archived-sessions.tsx index fe7aa65267a..c11d1622d5f 100644 --- a/packages/app/src/components/dialog-view-archived-sessions.tsx +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -44,7 +44,7 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { } >
- +
0}> @@ -64,7 +64,7 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { )} - + )} From 16379f5d70b9c7ee13c8f357dfb6ee1ffb5ab227 Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 15:24:24 -0400 Subject: [PATCH 05/10] fix: properly filter archived sessions by checking for undefined instead of truthiness --- packages/opencode/src/server/server.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index dee00f4bcec..3c989881601 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -662,11 +662,13 @@ export namespace Server { const sessions = await Array.fromAsync(Session.list()) const filtered = pipe( sessions, - archived === true - ? filter((s) => s.time.archived) - : archived === false - ? filter((s) => !s.time.archived) - : filter((s) => !s.time.archived), + filter((s) => + archived === true + ? s.time.archived !== undefined + : archived === false + ? s.time.archived === undefined + : s.time.archived === undefined, + ), sortBy((s) => s.time.updated), ) return c.json(filtered) From fb9f7492583a6ea6ccc7e3f918106fd724054e5f Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 18:11:21 -0400 Subject: [PATCH 06/10] refactor(app): use List component in archived sessions dialog - Add search functionality via List component's built-in search - Simplify code by removing manual loading/empty state handling - Enable keyboard navigation and fuzzy search for archived sessions --- .../dialog-view-archived-sessions.tsx | 76 ++++++++----------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/packages/app/src/components/dialog-view-archived-sessions.tsx b/packages/app/src/components/dialog-view-archived-sessions.tsx index c11d1622d5f..8e5206895b9 100644 --- a/packages/app/src/components/dialog-view-archived-sessions.tsx +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -1,8 +1,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { Icon } from "@opencode-ai/ui/icon" -import { Spinner } from "@opencode-ai/ui/spinner" -import { createResource, For, Show } from "solid-js" +import { List } from "@opencode-ai/ui/list" import { useGlobalSDK } from "@/context/global-sdk" import { type LocalProject } from "@/context/layout" import { base64Encode } from "@opencode-ai/util/encode" @@ -14,14 +13,6 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { const globalSDK = useGlobalSDK() const navigate = useNavigate() - const [archivedSessions] = createResource(async () => { - const result = await globalSDK.client.session.list({ - directory: props.project.worktree, - archived: true, - }) - return result.data ?? [] - }) - async function restoreSession(sessionID: string) { await globalSDK.client.session.update({ directory: props.project.worktree, @@ -34,43 +25,36 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { return ( -
- -
No archived sessions
-
- } - > -
- -
- - 0}> -
- - {(session) => ( - - )} - + { + const result = await globalSDK.client.session.list({ + directory: props.project.worktree, + archived: true, + }) + return result.data ?? [] + }} + key={(x) => x.id} + onSelect={(session) => { + if (session) restoreSession(session.id) + }} + > + {(session) => ( +
+ +
+
{session.title}
+
+ {DateTime.fromMillis(session.time.archived ?? session.time.updated).toLocaleString( + DateTime.DATETIME_MED, + )} +
+
+
- -
+ )} +
) } From 54f8c5405a09e42823fbd5571f87f18e09df5b4c Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 18:20:46 -0400 Subject: [PATCH 07/10] fix(app): make archived sessions search work by adding filterKeys and accepting filter param --- packages/app/src/components/dialog-view-archived-sessions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-view-archived-sessions.tsx b/packages/app/src/components/dialog-view-archived-sessions.tsx index 8e5206895b9..258851a17f6 100644 --- a/packages/app/src/components/dialog-view-archived-sessions.tsx +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -28,13 +28,14 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { { + items={async (filter: string) => { const result = await globalSDK.client.session.list({ directory: props.project.worktree, archived: true, }) return result.data ?? [] }} + filterKeys={["title"]} key={(x) => x.id} onSelect={(session) => { if (session) restoreSession(session.id) From c3a2d87cfa2cfbf93a140d2df7b29cc605c5ae7b Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sat, 3 Jan 2026 20:35:30 -0400 Subject: [PATCH 08/10] style(app): match archived sessions list styling to file selector --- .../dialog-view-archived-sessions.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/dialog-view-archived-sessions.tsx b/packages/app/src/components/dialog-view-archived-sessions.tsx index 258851a17f6..fa6231fe1a9 100644 --- a/packages/app/src/components/dialog-view-archived-sessions.tsx +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -5,7 +5,6 @@ import { List } from "@opencode-ai/ui/list" import { useGlobalSDK } from "@/context/global-sdk" import { type LocalProject } from "@/context/layout" import { base64Encode } from "@opencode-ai/util/encode" -import { DateTime } from "luxon" import { useNavigate } from "@solidjs/router" export function DialogViewArchivedSessions(props: { project: LocalProject }) { @@ -42,17 +41,15 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { }} > {(session) => ( -
- -
-
{session.title}
-
- {DateTime.fromMillis(session.time.archived ?? session.time.updated).toLocaleString( - DateTime.DATETIME_MED, - )} +
+
+ +
+ + {session.title} +
-
)} From e4d0377d4fb32b75f666e6972fb80c26019e82e6 Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Sun, 4 Jan 2026 08:10:29 -0400 Subject: [PATCH 09/10] fix(app): add overflow-hidden for proper text truncation --- .../app/src/components/dialog-view-archived-sessions.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/dialog-view-archived-sessions.tsx b/packages/app/src/components/dialog-view-archived-sessions.tsx index fa6231fe1a9..fc3177ff737 100644 --- a/packages/app/src/components/dialog-view-archived-sessions.tsx +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -41,10 +41,10 @@ export function DialogViewArchivedSessions(props: { project: LocalProject }) { }} > {(session) => ( -
-
+
+
-
+
{session.title} From 3cdae64ee2a6ce211ed9e65f790c269ef366fa82 Mon Sep 17 00:00:00 2001 From: aj <0xajka@gmail.com> Date: Tue, 6 Jan 2026 02:23:17 -0400 Subject: [PATCH 10/10] fix: combined archive and other session attrib to fix merge conflict in server.ts --- packages/opencode/src/server/server.ts | 99 ++++++++++++++++---------- 1 file changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 397351698d6..0b269a329ab 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -684,46 +684,69 @@ export namespace Server { }) }, ) - .get( - "/session", - describeRoute({ - summary: "List sessions", - description: "Get a list of all OpenCode sessions, sorted by most recently updated.", - operationId: "session.list", - responses: { - 200: { - description: "List of sessions", - content: { - "application/json": { - schema: resolver(Session.Info.array()), - }, - }, - }, +.get( + "/session", + describeRoute({ + summary: "List sessions", + description: "Get a list of all OpenCode sessions, sorted by most recently updated.", + operationId: "session.list", + responses: { + 200: { + description: "List of sessions", + content: { + "application/json": { + schema: resolver(Session.Info.array()), }, - }), - validator( - "query", - z.object({ - archived: z.coerce.boolean().optional(), - }), - ), - async (c) => { - const { archived } = c.req.valid("query") - const sessions = await Array.fromAsync(Session.list()) - const filtered = pipe( - sessions, - filter((s) => - archived === true - ? s.time.archived !== undefined - : archived === false - ? s.time.archived === undefined - : s.time.archived === undefined, - ), - sortBy((s) => s.time.updated), - ) - return c.json(filtered) }, - ) + }, + }, + }), + validator( + "query", + z.object({ + archived: z.coerce.boolean().optional(), + start: z.coerce + .number() + .optional() + .meta({ + description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)", + }), + search: z.string().optional().meta({ + description: "Filter sessions by title (case-insensitive)", + }), + limit: z.coerce.number().optional().meta({ + description: "Maximum number of sessions to return", + }), + }), + ), + async (c) => { + const query = c.req.valid("query") + const term = query.search?.toLowerCase() + const sessions: Session.Info[] = [] + + for await (const session of Session.list()) { + // archived filtering + if (query.archived === true && session.time.archived === undefined) continue + if (query.archived === false && session.time.archived !== undefined) continue + if (query.archived === undefined && session.time.archived !== undefined) continue + + // updated timestamp filter + if (query.start !== undefined && session.time.updated < query.start) continue + + // title search + if (term !== undefined && !session.title.toLowerCase().includes(term)) continue + + sessions.push(session) + + if (query.limit !== undefined && sessions.length >= query.limit) break + } + + sessions.sort((a, b) => b.time.updated - a.time.updated) + + return c.json(sessions) + }, +) + .get( "/session/status", describeRoute({