Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions packages/app/src/components/dialog-edit-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
58 changes: 58 additions & 0 deletions packages/app/src/components/dialog-view-archived-sessions.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog title="Archived Sessions">
<List
search={{ placeholder: "Search archived sessions", autofocus: true }}
emptyMessage="No archived sessions"
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)
}}
>
{(session) => (
<div class="w-full flex items-center justify-between rounded-md overflow-hidden">
<div class="flex items-center gap-x-3 grow min-w-0 overflow-hidden">
<Icon name="archive" size="small" class="text-text-weak shrink-0" />
<div class="flex items-center text-14-regular overflow-hidden">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{session.title}
</span>
</div>
</div>
</div>
)}
</List>
</Dialog>
)
}
18 changes: 17 additions & 1 deletion packages/app/src/custom-elements.d.ts
8 changes: 7 additions & 1 deletion packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -800,6 +801,11 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogViewArchivedSessions project={props.project} />)}
>
<DropdownMenu.ItemLabel>View archived sessions</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}
>
Expand Down Expand Up @@ -868,7 +874,7 @@ export default function Layout(props: ParentProps) {
</Collapsible>
</Match>
<Match when={true}>
<Tooltip placement="right" value={props.project.worktree}>
<Tooltip placement="right" value={getFilename(props.project.worktree)}>
<ProjectVisual project={props.project} />
</Tooltip>
</Match>
Expand Down
18 changes: 17 additions & 1 deletion packages/enterprise/src/custom-elements.d.ts
100 changes: 61 additions & 39 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ export class Session extends HeyApiClient {
public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
archived?: boolean
start?: number
search?: string
limit?: number
Expand All @@ -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" },
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,12 @@
},
{
"in": "query",
"name": "archived",
"schema": {
"type": "boolean"
},
{
"in": "query",
"name": "start",
"schema": {
"type": "number"
Expand Down
4 changes: 2 additions & 2 deletions packages/util/src/path.ts
Original file line number Diff line number Diff line change
@@ -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] ?? ""
}

Expand Down