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 (
+
+ )
+}
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] ?? ""
}