summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-12 07:07:54 -0600
committerAdam <[email protected]>2025-12-12 07:07:54 -0600
commitfeb8c4f3c60e1ada28bd24abd09d534375a6bc08 (patch)
treefc55e3bfe53c2707944a12c234e54287e7b1ff2f /packages/desktop/src
parent3f5cd2c4a810168ed03094cbf1d3e51a4b62f261 (diff)
downloadopencode-feb8c4f3c60e1ada28bd24abd09d534375a6bc08.tar.gz
opencode-feb8c4f3c60e1ada28bd24abd09d534375a6bc08.zip
feat(desktop): archive sessions
Diffstat (limited to 'packages/desktop/src')
-rw-r--r--packages/desktop/src/context/global-sync.tsx31
-rw-r--r--packages/desktop/src/context/layout.tsx15
-rw-r--r--packages/desktop/src/context/sync.tsx9
-rw-r--r--packages/desktop/src/pages/layout.tsx106
4 files changed, 107 insertions, 54 deletions
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 39e0fd34a..4c461a4e9 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -69,8 +69,19 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
children: {},
})
+ async function loadSessions(directory: string) {
+ globalSDK.client.session.list({ directory }).then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .filter((s) => !s.time.archived)
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .slice(0, 5)
+ setGlobalStore("children", directory, "session", sessions)
+ })
+ }
+
async function bootstrapInstance(directory: string) {
- const [store, setStore] = child(directory)
+ const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
@@ -80,14 +91,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
- session: () =>
- sdk.session.list().then((x) => {
- const sessions = (x.data ?? [])
- .slice()
- .sort((a, b) => a.id.localeCompare(b.id))
- .slice(0, store.limit)
- setStore("session", sessions)
- }),
+ session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
@@ -158,6 +162,12 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
+ if (event.properties.info.time.archived) {
+ if (result.found) {
+ setStore("session", (s) => s.filter((x) => x.id !== event.properties.info.id))
+ }
+ break
+ }
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
@@ -257,6 +267,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
},
child,
bootstrap,
+ project: {
+ loadSessions,
+ },
}
},
})
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 4ec0af601..3d5cad761 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -92,21 +92,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const enriched = createMemo(() => store.projects.flatMap(enrich))
const list = createMemo(() => enriched().flatMap(colorize))
- async function loadProjectSessions(directory: string) {
- const [, setStore] = globalSync.child(directory)
- globalSdk.client.session.list({ directory }).then((x) => {
- const sessions = (x.data ?? [])
- .slice()
- .sort((a, b) => a.id.localeCompare(b.id))
- .slice(0, 5)
- setStore("session", sessions)
- })
- }
-
onMount(() => {
Promise.all(
store.projects.map((project) => {
- return loadProjectSessions(project.worktree)
+ return globalSync.project.loadSessions(project.worktree)
}),
)
})
@@ -116,7 +105,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) return
- loadProjectSessions(directory)
+ globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
},
close(directory: string) {
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 85758c5b6..2ab54b3ae 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -65,6 +65,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
},
more: createMemo(() => store.session.length >= store.limit),
+ archive: async (sessionID: string) => {
+ await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
+ setStore(
+ produce((draft) => {
+ const match = Binary.search(draft.session, sessionID, (s) => s.id)
+ if (match.found) draft.session.splice(match.index, 1)
+ }),
+ )
+ },
},
absolute,
get directory() {
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index e7ae83162..c34904e11 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -55,6 +55,7 @@ import { showToast, Toast } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { Spinner } from "@opencode-ai/ui/spinner"
import { useNotification } from "@/context/notification"
+import { Binary } from "@opencode-ai/util/binary"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -258,7 +259,8 @@ export default function Layout(props: ParentProps) {
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const notification = useNotification()
const sortable = createSortable(props.project.worktree)
- const [projectStore] = globalSync.child(props.project.worktree)
+ const [projectStore, setProjectStore] = globalSync.child(props.project.worktree)
+ const sessions = createMemo(() => projectStore.session.filter((s) => !s.time.archived))
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const [expanded, setExpanded] = createSignal(true)
@@ -300,21 +302,33 @@ export default function Layout(props: ParentProps) {
</Button>
<Collapsible.Content>
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
- <For each={projectStore.session}>
+ <For each={sessions()}>
{(session) => {
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
const notifications = createMemo(() => notification.session.unseen(session.id))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
+ async function archive(session: Session) {
+ await globalSDK.client.session.update({
+ directory: session.directory,
+ sessionID: session.id,
+ time: { archived: Date.now() },
+ })
+ setProjectStore(
+ produce((draft) => {
+ const match = Binary.search(draft.session, session.id, (s) => s.id)
+ if (match.found) draft.session.splice(match.index, 1)
+ }),
+ )
+ }
return (
<A
- data-active={session.id === params.id}
href={`${slug()}/session/${session.id}`}
class="group/session focus:outline-none cursor-default"
>
<Tooltip placement="right" value={session.title}>
<div
- class="relative w-full pl-4 pr-2 py-1 rounded-md
- group-data-[active=true]/session:bg-surface-raised-base-hover
+ class="relative w-full pl-4 pr-1 py-1 rounded-md
+ group-[.active]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
>
@@ -322,40 +336,68 @@ export default function Layout(props: ParentProps) {
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
{session.title}
</span>
- <Switch>
- <Match when={hasError()}>
- <div class="size-1.5 shrink-0 mr-1 rounded-full bg-text-diff-delete-base" />
- </Match>
- <Match when={notifications().length > 0}>
- <div class="size-1.5 shrink-0 mr-1 rounded-full bg-text-interactive-base" />
- </Match>
- <Match when={true}>
- <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
- {Math.abs(updated().diffNow().as("seconds")) < 60
- ? "Now"
- : updated()
- .toRelative({
- style: "short",
- unit: ["days", "hours", "minutes"],
- })
- ?.replace(" ago", "")
- ?.replace(/ days?/, "d")
- ?.replace(" min.", "m")
- ?.replace(" hr.", "h")}
- </span>
- </Match>
- </Switch>
- </div>
- <div class="hidden _flex justify-between items-center self-stretch">
- <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
- <Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
+ <div class="shrink-0 group-hover/session:hidden mr-1">
+ <Switch>
+ <Match when={hasError()}>
+ <div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
+ </Match>
+ <Match when={notifications().length > 0}>
+ <div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
+ </Match>
+ <Match when={true}>
+ <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
+ {Math.abs(updated().diffNow().as("seconds")) < 60
+ ? "Now"
+ : updated()
+ .toRelative({
+ style: "short",
+ unit: ["days", "hours", "minutes"],
+ })
+ ?.replace(" ago", "")
+ ?.replace(/ days?/, "d")
+ ?.replace(" min.", "m")
+ ?.replace(" hr.", "h")}
+ </span>
+ </Match>
+ </Switch>
+ </div>
+ <div class="hidden group-hover/session:flex group-active/session:flex text-text-base gap-1">
+ {/* <IconButton icon="dot-grid" variant="ghost" /> */}
+ <Tooltip placement="right" value="Archive session">
+ <IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
+ </Tooltip>
+ </div>
</div>
+ <Show when={session.summary?.files}>
+ <div class="flex justify-between items-center self-stretch">
+ <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
+ <Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
+ </div>
+ </Show>
</div>
</Tooltip>
</A>
)
}}
</For>
+ <Show when={sessions().length === 0}>
+ <A href={`${slug()}/session`} class="group/session focus:outline-none cursor-default">
+ <Tooltip placement="right" value="New session">
+ <div
+ class="relative w-full pl-4 pr-1 py-1 rounded-md
+ group-[.active]/session:bg-surface-raised-base-hover
+ group-hover/session:bg-surface-raised-base-hover
+ group-focus/session:bg-surface-raised-base-hover"
+ >
+ <div class="flex items-center self-stretch gap-6 justify-between">
+ <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
+ New session
+ </span>
+ </div>
+ </div>
+ </Tooltip>
+ </A>
+ </Show>
</nav>
</Collapsible.Content>
</Collapsible>