فهرست منبع

feat(desktop): archive sessions

Adam 2 ماه پیش
والد
کامیت
feb8c4f3c6

+ 22 - 9
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,
+      },
     }
   },
 })

+ 2 - 13
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) {

+ 9 - 0
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() {

+ 74 - 32
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>

+ 1 - 0
packages/ui/src/components/icon.tsx

@@ -4,6 +4,7 @@ const icons = {
   "align-right": `<path d="M12.292 6.04167L16.2503 9.99998L12.292 13.9583M2.91699 9.99998H15.6253M17.0837 3.75V16.25" stroke="currentColor" stroke-linecap="square"/>`,
   "arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
   "arrow-left": `<path d="M8.33464 4.58398L2.91797 10.0007L8.33464 15.4173M3.33464 10.0007H17.0846" stroke="currentColor" stroke-linecap="square"/>`,
+  archive: `<path d="M16.8747 6.24935H17.3747V5.74935H16.8747V6.24935ZM16.8747 16.8743V17.3743H17.3747V16.8743H16.8747ZM3.12467 16.8743H2.62467V17.3743H3.12467V16.8743ZM3.12467 6.24935V5.74935H2.62467V6.24935H3.12467ZM2.08301 2.91602V2.41602H1.58301V2.91602H2.08301ZM17.9163 2.91602H18.4163V2.41602H17.9163V2.91602ZM17.9163 6.24935V6.74935H18.4163V6.24935H17.9163ZM2.08301 6.24935H1.58301V6.74935H2.08301V6.24935ZM8.33301 9.08268H7.83301V10.0827H8.33301V9.58268V9.08268ZM11.6663 10.0827H12.1663V9.08268H11.6663V9.58268V10.0827ZM16.8747 6.24935H16.3747V16.8743H16.8747H17.3747V6.24935H16.8747ZM16.8747 16.8743V16.3743H3.12467V16.8743V17.3743H16.8747V16.8743ZM3.12467 16.8743H3.62467V6.24935H3.12467H2.62467V16.8743H3.12467ZM3.12467 6.24935V6.74935H16.8747V6.24935V5.74935H3.12467V6.24935ZM2.08301 2.91602V3.41602H17.9163V2.91602V2.41602H2.08301V2.91602ZM17.9163 2.91602H17.4163V6.24935H17.9163H18.4163V2.91602H17.9163ZM17.9163 6.24935V5.74935H2.08301V6.24935V6.74935H17.9163V6.24935ZM2.08301 6.24935H2.58301V2.91602H2.08301H1.58301V6.24935H2.08301ZM8.33301 9.58268V10.0827H11.6663V9.58268V9.08268H8.33301V9.58268Z" fill="currentColor"/>`,
   "bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
   "bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
   "check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,