feat(desktop): archive sessions
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user