diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 27ce3903cdc..2f0f7db1f68 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -7,15 +7,11 @@ import { createMemo, createSignal, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { useGlobalSDK } from "@/context/global-sdk" import { type LocalProject, getAvatarColors } from "@/context/layout" +import { getFilename } from "@opencode-ai/util/path" import { Avatar } from "@opencode-ai/ui/avatar" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const -function getFilename(input: string) { - const parts = input.split("/") - return parts[parts.length - 1] || input -} - export function DialogEditProject(props: { project: LocalProject }) { const dialog = useDialog() const globalSDK = useGlobalSDK() 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..fc3177ff737 --- /dev/null +++ b/packages/app/src/components/dialog-view-archived-sessions.tsx @@ -0,0 +1,58 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { Icon } from "@opencode-ai/ui/icon" +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 { useNavigate } from "@solidjs/router" + +export function DialogViewArchivedSessions(props: { project: LocalProject }) { + const dialog = useDialog() + const globalSDK = useGlobalSDK() + const navigate = useNavigate() + + 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 ( + + { + 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) + }} + > + {(session) => ( +
+
+ +
+ + {session.title} + +
+
+
+ )} +
+
+ ) +} 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/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 9320267944d..153b1fbf131 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" @@ -800,6 +801,11 @@ export default function Layout(props: ParentProps) { + dialog.show(() => )} + > + View archived sessions + dialog.show(() => )} > @@ -868,7 +874,7 @@ export default function Layout(props: ParentProps) { - + 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 {} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 04ec4673ec4..1f4148721b2 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -684,47 +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({ - 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()) { - if (query.start !== undefined && session.time.updated < query.start) continue - if (term !== undefined && !session.title.toLowerCase().includes(term)) continue - sessions.push(session) - if (query.limit !== undefined && sessions.length >= query.limit) break - } - return c.json(sessions) }, - ) + }, + }, + }), + 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({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index a26cefb176f..1c14c412bbd 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -775,6 +775,7 @@ export class Session extends HeyApiClient { public list( parameters?: { directory?: string + archived?: boolean start?: number search?: string limit?: number @@ -787,6 +788,7 @@ export class Session extends HeyApiClient { { args: [ { in: "query", key: "directory" }, + { in: "query", key: "archived" }, { in: "query", key: "start" }, { in: "query", key: "search" }, { in: "query", key: "limit" }, diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index c3af722451d..a6b4bef0cd0 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -985,6 +985,12 @@ }, { "in": "query", + "name": "archived", + "schema": { + "type": "boolean" + }, + { + "in": "query", "name": "start", "schema": { "type": "number" diff --git a/packages/util/src/path.ts b/packages/util/src/path.ts index f7c46d4eff8..2da8028b46a 100644 --- a/packages/util/src/path.ts +++ b/packages/util/src/path.ts @@ -1,7 +1,7 @@ export function getFilename(path: string | undefined) { if (!path) return "" - const trimmed = path.replace(/[\/]+$/, "") - const parts = trimmed.split("/") + const trimmed = path.replace(/[\/\\]+$/, "") + const parts = trimmed.split(/[\/\\]/) return parts[parts.length - 1] ?? "" }