Browse Source

Merge branch 'dev' of https://github.com/sst/opencode into dev

David Hill 2 months ago
parent
commit
9b77246246
36 changed files with 602 additions and 237 deletions
  1. 1 0
      STATS.md
  2. 6 0
      bun.lock
  3. 1 1
      nix/hashes.json
  4. 8 1
      packages/console/app/src/routes/download/index.tsx
  5. 2 2
      packages/desktop/index.html
  6. 1 0
      packages/desktop/package.json
  7. 22 19
      packages/desktop/src/app.tsx
  8. 113 0
      packages/desktop/src/components/header.tsx
  9. 51 27
      packages/desktop/src/context/global-sync.tsx
  10. 4 20
      packages/desktop/src/context/layout.tsx
  11. 106 0
      packages/desktop/src/context/notification.tsx
  12. 9 0
      packages/desktop/src/context/sync.tsx
  13. 3 2
      packages/desktop/src/pages/home.tsx
  14. 157 150
      packages/desktop/src/pages/layout.tsx
  15. 1 1
      packages/desktop/src/pages/session.tsx
  16. 2 0
      packages/opencode/src/acp/agent.ts
  17. 1 1
      packages/opencode/src/cli/cmd/tui/util/clipboard.ts
  18. 1 1
      packages/opencode/src/provider/transform.ts
  19. 2 2
      packages/tauri/index.html
  20. 1 0
      packages/tauri/package.json
  21. 84 0
      packages/tauri/src-tauri/Cargo.lock
  22. 1 0
      packages/tauri/src-tauri/Cargo.toml
  23. 2 1
      packages/tauri/src-tauri/capabilities/default.json
  24. 5 1
      packages/tauri/src-tauri/src/lib.rs
  25. 4 0
      packages/tauri/src/index.tsx
  26. 2 1
      packages/ui/package.json
  27. BIN
      packages/ui/src/assets/audio/staplebops-01.aac
  28. BIN
      packages/ui/src/assets/audio/staplebops-02.aac
  29. BIN
      packages/ui/src/assets/audio/staplebops-03.aac
  30. BIN
      packages/ui/src/assets/audio/staplebops-04.aac
  31. BIN
      packages/ui/src/assets/audio/staplebops-05.aac
  32. BIN
      packages/ui/src/assets/audio/staplebops-06.aac
  33. BIN
      packages/ui/src/assets/audio/staplebops-07.aac
  34. 2 1
      packages/ui/src/components/icon.tsx
  35. 8 6
      packages/ui/src/components/toast.css
  36. 2 0
      packages/web/src/content/docs/zen.mdx

+ 1 - 0
STATS.md

@@ -167,3 +167,4 @@
 | 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773)   | 1,985,410 (+27,363) |
 | 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773)   | 1,985,410 (+27,363) |
 | 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786)   | 2,017,599 (+32,189) |
 | 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786)   | 2,017,599 (+32,189) |
 | 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
 | 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
+| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |

+ 6 - 0
bun.lock

@@ -131,6 +131,7 @@
         "@opencode-ai/util": "workspace:*",
         "@opencode-ai/util": "workspace:*",
         "@shikijs/transformers": "3.9.2",
         "@shikijs/transformers": "3.9.2",
         "@solid-primitives/active-element": "2.1.3",
         "@solid-primitives/active-element": "2.1.3",
+        "@solid-primitives/audio": "1.4.2",
         "@solid-primitives/event-bus": "1.1.2",
         "@solid-primitives/event-bus": "1.1.2",
         "@solid-primitives/resize-observer": "2.1.3",
         "@solid-primitives/resize-observer": "2.1.3",
         "@solid-primitives/scroll": "2.1.3",
         "@solid-primitives/scroll": "2.1.3",
@@ -355,6 +356,7 @@
         "@tauri-apps/api": "^2",
         "@tauri-apps/api": "^2",
         "@tauri-apps/plugin-dialog": "~2",
         "@tauri-apps/plugin-dialog": "~2",
         "@tauri-apps/plugin-opener": "^2",
         "@tauri-apps/plugin-opener": "^2",
+        "@tauri-apps/plugin-os": "~2",
         "@tauri-apps/plugin-process": "~2",
         "@tauri-apps/plugin-process": "~2",
         "@tauri-apps/plugin-shell": "~2",
         "@tauri-apps/plugin-shell": "~2",
         "@tauri-apps/plugin-store": "~2",
         "@tauri-apps/plugin-store": "~2",
@@ -1548,6 +1550,8 @@
 
 
     "@solid-primitives/active-element": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
     "@solid-primitives/active-element": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
 
 
+    "@solid-primitives/audio": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
+
     "@solid-primitives/event-bus": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
     "@solid-primitives/event-bus": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
 
 
     "@solid-primitives/event-listener": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
     "@solid-primitives/event-listener": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
@@ -1660,6 +1664,8 @@
 
 
     "@tauri-apps/plugin-opener": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
     "@tauri-apps/plugin-opener": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
 
 
+    "@tauri-apps/plugin-os": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
+
     "@tauri-apps/plugin-process": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="],
     "@tauri-apps/plugin-process": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="],
 
 
     "@tauri-apps/plugin-shell": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
     "@tauri-apps/plugin-shell": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
 {
-  "nodeModules": "sha256-b6AEbARiEcI/Pu1g0LbRfH1Oo5rClncW44Ug0d4oP0w="
+  "nodeModules": "sha256-3CG0wAMQp2E6ghPUXbYaYifJorp9b1WvCtHD+o8Nhck="
 }
 }

+ 8 - 1
packages/console/app/src/routes/download/index.tsx

@@ -10,7 +10,14 @@ import { Legal } from "~/component/legal"
 import { config } from "~/config"
 import { config } from "~/config"
 
 
 const getLatestRelease = query(async () => {
 const getLatestRelease = query(async () => {
-  const response = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
+  "use server"
+  const response = await fetch("https://api.github.com/repos/sst/opencode/releases/latest", {
+    headers: {
+      "User-Agent":
+        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
+    },
+  })
+
   if (!response.ok) return null
   if (!response.ok) return null
   const data = await response.json()
   const data = await response.json()
   return data.tag_name as string
   return data.tag_name as string

+ 2 - 2
packages/desktop/index.html

@@ -14,7 +14,7 @@
     <meta property="og:image" content="/social-share.png" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
   </head>
   </head>
-  <body class="antialiased overscroll-none select-none text-12-regular">
+  <body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
     <script>
     <script>
       ;(function () {
       ;(function () {
         const savedTheme = localStorage.getItem("theme") || "oc-1"
         const savedTheme = localStorage.getItem("theme") || "oc-1"
@@ -22,7 +22,7 @@
       })()
       })()
     </script>
     </script>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root"></div>
+    <div id="root" class="flex flex-col h-screen"></div>
     <script src="/src/entry.tsx" type="module"></script>
     <script src="/src/entry.tsx" type="module"></script>
   </body>
   </body>
 </html>
 </html>

+ 1 - 0
packages/desktop/package.json

@@ -35,6 +35,7 @@
     "@opencode-ai/util": "workspace:*",
     "@opencode-ai/util": "workspace:*",
     "@shikijs/transformers": "3.9.2",
     "@shikijs/transformers": "3.9.2",
     "@solid-primitives/active-element": "2.1.3",
     "@solid-primitives/active-element": "2.1.3",
+    "@solid-primitives/audio": "1.4.2",
     "@solid-primitives/event-bus": "1.1.2",
     "@solid-primitives/event-bus": "1.1.2",
     "@solid-primitives/resize-observer": "2.1.3",
     "@solid-primitives/resize-observer": "2.1.3",
     "@solid-primitives/scroll": "2.1.3",
     "@solid-primitives/scroll": "2.1.3",

+ 22 - 19
packages/desktop/src/app.tsx

@@ -14,6 +14,7 @@ import { LayoutProvider } from "./context/layout"
 import { GlobalSDKProvider } from "./context/global-sdk"
 import { GlobalSDKProvider } from "./context/global-sdk"
 import { SessionProvider } from "./context/session"
 import { SessionProvider } from "./context/session"
 import { Show } from "solid-js"
 import { Show } from "solid-js"
+import { NotificationProvider } from "./context/notification"
 
 
 declare global {
 declare global {
   interface Window {
   interface Window {
@@ -37,25 +38,27 @@ export function App() {
         <GlobalSDKProvider url={url}>
         <GlobalSDKProvider url={url}>
           <GlobalSyncProvider>
           <GlobalSyncProvider>
             <LayoutProvider>
             <LayoutProvider>
-              <MetaProvider>
-                <Font />
-                <Router root={Layout}>
-                  <Route path="/" component={Home} />
-                  <Route path="/:dir" component={DirectoryLayout}>
-                    <Route path="/" component={() => <Navigate href="session" />} />
-                    <Route
-                      path="/session/:id?"
-                      component={(p) => (
-                        <Show when={p.params.id || true} keyed>
-                          <SessionProvider>
-                            <Session />
-                          </SessionProvider>
-                        </Show>
-                      )}
-                    />
-                  </Route>
-                </Router>
-              </MetaProvider>
+              <NotificationProvider>
+                <MetaProvider>
+                  <Font />
+                  <Router root={Layout}>
+                    <Route path="/" component={Home} />
+                    <Route path="/:dir" component={DirectoryLayout}>
+                      <Route path="/" component={() => <Navigate href="session" />} />
+                      <Route
+                        path="/session/:id?"
+                        component={(p) => (
+                          <Show when={p.params.id || true} keyed>
+                            <SessionProvider>
+                              <Session />
+                            </SessionProvider>
+                          </Show>
+                        )}
+                      />
+                    </Route>
+                  </Router>
+                </MetaProvider>
+              </NotificationProvider>
             </LayoutProvider>
             </LayoutProvider>
           </GlobalSyncProvider>
           </GlobalSyncProvider>
         </GlobalSDKProvider>
         </GlobalSDKProvider>

+ 113 - 0
packages/desktop/src/components/header.tsx

@@ -0,0 +1,113 @@
+import { useGlobalSync } from "@/context/global-sync"
+import { useLayout } from "@/context/layout"
+import { Session } from "@opencode-ai/sdk/v2/client"
+import { Button } from "@opencode-ai/ui/button"
+import { Icon } from "@opencode-ai/ui/icon"
+import { Mark } from "@opencode-ai/ui/logo"
+import { Select } from "@opencode-ai/ui/select"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { base64Decode } from "@opencode-ai/util/encode"
+import { getFilename } from "@opencode-ai/util/path"
+import { A, useParams } from "@solidjs/router"
+import { createMemo, Show } from "solid-js"
+
+export function Header(props: {
+  navigateToProject: (directory: string) => void
+  navigateToSession: (session: Session | undefined) => void
+}) {
+  const globalSync = useGlobalSync()
+  const layout = useLayout()
+  const params = useParams()
+  const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+  const store = createMemo(() => globalSync.child(currentDirectory())[0])
+  const sessions = createMemo(() => store().session ?? [])
+  const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+
+  return (
+    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
+      <A
+        href="/"
+        classList={{
+          "w-12 shrink-0 px-4 py-3.5": true,
+          "flex items-center justify-start self-stretch": true,
+          "border-r border-border-weak-base": true,
+        }}
+        style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
+        data-tauri-drag-region
+      >
+        <Mark class="shrink-0" />
+      </A>
+      <div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
+        <Show when={params.dir && layout.projects.list().length > 0}>
+          <div class="flex items-center gap-3">
+            <div class="flex items-center gap-2">
+              <Select
+                options={layout.projects.list().map((project) => project.worktree)}
+                current={currentDirectory()}
+                label={(x) => getFilename(x)}
+                onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
+                class="text-14-regular text-text-base"
+                variant="ghost"
+              >
+                {/* @ts-ignore */}
+                {(i) => (
+                  <div class="flex items-center gap-2">
+                    <Icon name="folder" size="small" />
+                    <div class="text-text-strong">{getFilename(i)}</div>
+                  </div>
+                )}
+              </Select>
+              <div class="text-text-weaker">/</div>
+              <Select
+                options={sessions()}
+                current={currentSession()}
+                placeholder="New session"
+                label={(x) => x.title}
+                value={(x) => x.id}
+                onSelect={props.navigateToSession}
+                class="text-14-regular text-text-base max-w-md"
+                variant="ghost"
+              />
+            </div>
+            <Show when={currentSession()}>
+              <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
+                New session
+              </Button>
+            </Show>
+          </div>
+          <div class="flex items-center gap-4">
+            <Tooltip
+              class="shrink-0"
+              value={
+                <div class="flex items-center gap-2">
+                  <span>Toggle terminal</span>
+                  <span class="text-icon-base text-12-medium">Ctrl `</span>
+                </div>
+              }
+            >
+              <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
+                <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+                  <Icon
+                    size="small"
+                    name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+                    class="group-hover/terminal-toggle:hidden"
+                  />
+                  <Icon
+                    size="small"
+                    name="layout-bottom-partial"
+                    class="hidden group-hover/terminal-toggle:inline-block"
+                  />
+                  <Icon
+                    size="small"
+                    name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+                    class="hidden group-active/terminal-toggle:inline-block"
+                  />
+                </div>
+              </Button>
+            </Tooltip>
+          </div>
+        </Show>
+      </div>
+    </header>
+  )
+}

+ 51 - 27
packages/desktop/src/context/global-sync.tsx

@@ -55,45 +55,20 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
     const globalSDK = useGlobalSDK()
     const globalSDK = useGlobalSDK()
     const [globalStore, setGlobalStore] = createStore<{
     const [globalStore, setGlobalStore] = createStore<{
       ready: boolean
       ready: boolean
+      path: Path
       project: Project[]
       project: Project[]
       provider: ProviderListResponse
       provider: ProviderListResponse
       provider_auth: ProviderAuthResponse
       provider_auth: ProviderAuthResponse
       children: Record<string, State>
       children: Record<string, State>
     }>({
     }>({
       ready: false,
       ready: false,
+      path: { state: "", config: "", worktree: "", directory: "", home: "" },
       project: [],
       project: [],
       provider: { all: [], connected: [], default: {} },
       provider: { all: [], connected: [], default: {} },
       provider_auth: {},
       provider_auth: {},
       children: {},
       children: {},
     })
     })
 
 
-    async function bootstrapInstance(directory: string) {
-      const [store, setStore] = child(directory)
-      const sdk = createOpencodeClient({
-        baseUrl: globalSDK.url,
-        directory,
-      })
-      const load = {
-        project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
-        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)
-          }),
-        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!)),
-        node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
-      }
-      await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
-    }
-
     const children: Record<string, ReturnType<typeof createStore<State>>> = {}
     const children: Record<string, ReturnType<typeof createStore<State>>> = {}
     function child(directory: string) {
     function child(directory: string) {
       if (!children[directory]) {
       if (!children[directory]) {
@@ -120,6 +95,38 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       return children[directory]
       return children[directory]
     }
     }
 
 
+    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)
+        const [, setStore] = child(directory)
+        setStore("session", sessions)
+      })
+    }
+
+    async function bootstrapInstance(directory: string) {
+      const [, setStore] = child(directory)
+      const sdk = createOpencodeClient({
+        baseUrl: globalSDK.url,
+        directory,
+      })
+      const load = {
+        project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
+        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: () => 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!)),
+        node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
+      }
+      await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
+    }
+
     globalSDK.event.listen((e) => {
     globalSDK.event.listen((e) => {
       const directory = e.name
       const directory = e.name
       const event = e.details
       const event = e.details
@@ -156,6 +163,17 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
         }
         }
         case "session.updated": {
         case "session.updated": {
           const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
           const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
+          if (event.properties.info.time.archived) {
+            if (result.found) {
+              setStore(
+                "session",
+                produce((draft) => {
+                  draft.splice(result.index, 1)
+                }),
+              )
+            }
+            break
+          }
           if (result.found) {
           if (result.found) {
             setStore("session", result.index, reconcile(event.properties.info))
             setStore("session", result.index, reconcile(event.properties.info))
             break
             break
@@ -224,6 +242,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
 
 
     async function bootstrap() {
     async function bootstrap() {
       return Promise.all([
       return Promise.all([
+        globalSDK.client.path.get().then((x) => {
+          setGlobalStore("path", x.data!)
+        }),
         globalSDK.client.project.list().then(async (x) => {
         globalSDK.client.project.list().then(async (x) => {
           setGlobalStore(
           setGlobalStore(
             "project",
             "project",
@@ -252,6 +273,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       },
       },
       child,
       child,
       bootstrap,
       bootstrap,
+      project: {
+        loadSessions,
+      },
     }
     }
   },
   },
 })
 })

+ 4 - 20
packages/desktop/src/context/layout.tsx

@@ -7,15 +7,10 @@ import { useGlobalSDK } from "./global-sdk"
 import { Project } from "@opencode-ai/sdk/v2"
 import { Project } from "@opencode-ai/sdk/v2"
 
 
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
-
 export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
 export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
 
 
-export function isAvatarColorKey(value: string): value is AvatarColorKey {
-  return AVATAR_COLOR_KEYS.includes(value as AvatarColorKey)
-}
-
 export function getAvatarColors(key?: string) {
 export function getAvatarColors(key?: string) {
-  if (key && isAvatarColorKey(key)) {
+  if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
     return {
     return {
       background: `var(--avatar-background-${key})`,
       background: `var(--avatar-background-${key})`,
       foreground: `var(--avatar-text-${key})`,
       foreground: `var(--avatar-text-${key})`,
@@ -50,7 +45,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
         },
       }),
       }),
       {
       {
-        name: "default-layout.v7",
+        name: "layout.v1",
       },
       },
     )
     )
     const [ephemeral, setEphemeral] = createStore<{
     const [ephemeral, setEphemeral] = createStore<{
@@ -97,21 +92,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     const enriched = createMemo(() => store.projects.flatMap(enrich))
     const enriched = createMemo(() => store.projects.flatMap(enrich))
     const list = createMemo(() => enriched().flatMap(colorize))
     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(() => {
     onMount(() => {
       Promise.all(
       Promise.all(
         store.projects.map((project) => {
         store.projects.map((project) => {
-          return loadProjectSessions(project.worktree)
+          return globalSync.project.loadSessions(project.worktree)
         }),
         }),
       )
       )
     })
     })
@@ -121,7 +105,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         list,
         list,
         open(directory: string) {
         open(directory: string) {
           if (store.projects.find((x) => x.worktree === directory)) return
           if (store.projects.find((x) => x.worktree === directory)) return
-          loadProjectSessions(directory)
+          globalSync.project.loadSessions(directory)
           setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
           setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
         },
         },
         close(directory: string) {
         close(directory: string) {

+ 106 - 0
packages/desktop/src/context/notification.tsx

@@ -0,0 +1,106 @@
+import { createStore } from "solid-js/store"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { makePersisted } from "@solid-primitives/storage"
+import { useGlobalSDK } from "./global-sdk"
+import { EventSessionError } from "@opencode-ai/sdk/v2"
+import { makeAudioPlayer } from "@solid-primitives/audio"
+import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
+
+type NotificationBase = {
+  directory?: string
+  session?: string
+  metadata?: any
+  time: number
+  viewed: boolean
+}
+
+type TurnCompleteNotification = NotificationBase & {
+  type: "turn-complete"
+}
+
+type ErrorNotification = NotificationBase & {
+  type: "error"
+  error: EventSessionError["properties"]["error"]
+}
+
+export type Notification = TurnCompleteNotification | ErrorNotification
+
+export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
+  name: "Notification",
+  init: () => {
+    const idlePlayer = makeAudioPlayer(idleSound)
+    const globalSDK = useGlobalSDK()
+
+    const [store, setStore] = makePersisted(
+      createStore({
+        list: [] as Notification[],
+      }),
+      {
+        name: "notification.v1",
+      },
+    )
+
+    // onMount(() => {
+    //   const daysToKeep = 7
+    //   // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
+    // })
+
+    globalSDK.event.listen((e) => {
+      const directory = e.name
+      const event = e.details
+      const base = {
+        directory,
+        time: Date.now(),
+        viewed: false,
+      }
+      switch (event.type) {
+        case "session.idle": {
+          idlePlayer.play()
+          const session = event.properties.sessionID
+          setStore("list", store.list.length, {
+            ...base,
+            type: "turn-complete",
+            session,
+          })
+          break
+        }
+        case "session.error": {
+          const session = event.properties.sessionID ?? "global"
+          // errorPlayer.play()
+          setStore("list", store.list.length, {
+            ...base,
+            type: "error",
+            session,
+            error: "error" in event.properties ? event.properties.error : undefined,
+          })
+          break
+        }
+      }
+    })
+
+    return {
+      session: {
+        all(session: string) {
+          return store.list.filter((n) => n.session === session)
+        },
+        unseen(session: string) {
+          return store.list.filter((n) => n.session === session && !n.viewed)
+        },
+        markViewed(session: string) {
+          setStore("list", (n) => n.session === session, "viewed", true)
+        },
+      },
+      project: {
+        all(directory: string) {
+          return store.list.filter((n) => n.directory === directory)
+        },
+        unseen(directory: string) {
+          return store.list.filter((n) => n.directory === directory && !n.viewed)
+        },
+        markViewed(directory: string) {
+          setStore("list", (n) => n.directory === directory, "viewed", true)
+        },
+      },
+    }
+  },
+})

+ 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),
         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,
       absolute,
       get directory() {
       get directory() {

+ 3 - 2
packages/desktop/src/pages/home.tsx

@@ -1,5 +1,5 @@
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
-import { For, Match, Show, Switch } from "solid-js"
+import { createMemo, For, Match, Show, Switch } from "solid-js"
 import { Button } from "@opencode-ai/ui/button"
 import { Button } from "@opencode-ai/ui/button"
 import { Logo } from "@opencode-ai/ui/logo"
 import { Logo } from "@opencode-ai/ui/logo"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
@@ -14,6 +14,7 @@ export default function Home() {
   const layout = useLayout()
   const layout = useLayout()
   const platform = usePlatform()
   const platform = usePlatform()
   const navigate = useNavigate()
   const navigate = useNavigate()
+  const homedir = createMemo(() => sync.data.path.home)
 
 
   function openProject(directory: string) {
   function openProject(directory: string) {
     layout.projects.open(directory)
     layout.projects.open(directory)
@@ -61,7 +62,7 @@ export default function Home() {
                     class="text-14-mono text-left justify-between px-3"
                     class="text-14-mono text-left justify-between px-3"
                     onClick={() => openProject(project.worktree)}
                     onClick={() => openProject(project.worktree)}
                   >
                   >
-                    {project.worktree}
+                    {project.worktree.replace(homedir(), "~")}
                     <div class="text-14-regular text-text-weak">
                     <div class="text-14-regular text-text-weak">
                       {DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
                       {DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
                     </div>
                     </div>

+ 157 - 150
packages/desktop/src/pages/layout.tsx

@@ -1,10 +1,21 @@
-import { createEffect, createMemo, For, Match, onCleanup, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
+import {
+  createEffect,
+  createMemo,
+  createSignal,
+  For,
+  Match,
+  onCleanup,
+  onMount,
+  ParentProps,
+  Show,
+  Switch,
+  type JSX,
+} from "solid-js"
 import { DateTime } from "luxon"
 import { DateTime } from "luxon"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { useLayout, getAvatarColors } from "@/context/layout"
 import { useLayout, getAvatarColors } from "@/context/layout"
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
 import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
-import { Mark } from "@opencode-ai/ui/logo"
 import { Avatar } from "@opencode-ai/ui/avatar"
 import { Avatar } from "@opencode-ai/ui/avatar"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Button } from "@opencode-ai/ui/button"
 import { Button } from "@opencode-ai/ui/button"
@@ -15,7 +26,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
 import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { getFilename } from "@opencode-ai/util/path"
 import { getFilename } from "@opencode-ai/util/path"
-import { Select } from "@opencode-ai/ui/select"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
 import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
 import { usePlatform } from "@/context/platform"
 import { usePlatform } from "@/context/platform"
@@ -42,6 +52,9 @@ import { TextField } from "@opencode-ai/ui/text-field"
 import { showToast, Toast } from "@opencode-ai/ui/toast"
 import { showToast, Toast } from "@opencode-ai/ui/toast"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { Spinner } from "@opencode-ai/ui/spinner"
 import { Spinner } from "@opencode-ai/ui/spinner"
+import { useNotification } from "@/context/notification"
+import { Binary } from "@opencode-ai/util/binary"
+import { Header } from "@/components/header"
 
 
 export default function Layout(props: ParentProps) {
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
@@ -54,10 +67,8 @@ export default function Layout(props: ParentProps) {
   const globalSync = useGlobalSync()
   const globalSync = useGlobalSync()
   const layout = useLayout()
   const layout = useLayout()
   const platform = usePlatform()
   const platform = usePlatform()
+  const notification = useNotification()
   const navigate = useNavigate()
   const navigate = useNavigate()
-  const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
-  const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
-  const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
   const providers = useProviders()
   const providers = useProviders()
 
 
   function navigateToProject(directory: string | undefined) {
   function navigateToProject(directory: string | undefined) {
@@ -77,9 +88,11 @@ export default function Layout(props: ParentProps) {
   }
   }
 
 
   function closeProject(directory: string) {
   function closeProject(directory: string) {
+    const index = layout.projects.list().findIndex((x) => x.worktree === directory)
+    const next = layout.projects.list()[index + 1]
     layout.projects.close(directory)
     layout.projects.close(directory)
-    // TODO: more intelligent navigation
-    navigate("/")
+    if (next) navigateToProject(next.worktree)
+    else navigate("/")
   }
   }
 
 
   async function chooseProject() {
   async function chooseProject() {
@@ -105,6 +118,7 @@ export default function Layout(props: ParentProps) {
     if (!params.dir || !params.id) return
     if (!params.dir || !params.id) return
     const directory = base64Decode(params.dir)
     const directory = base64Decode(params.dir)
     setStore("lastSession", directory, params.id)
     setStore("lastSession", directory, params.id)
+    notification.session.markViewed(params.id)
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
@@ -164,8 +178,51 @@ export default function Layout(props: ParentProps) {
     return <></>
     return <></>
   }
   }
 
 
+  const ProjectAvatar = (props: {
+    project: Project
+    class?: string
+    expandable?: boolean
+    notify?: boolean
+  }): JSX.Element => {
+    const notification = useNotification()
+    const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
+    const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
+    const name = createMemo(() => getFilename(props.project.worktree))
+    const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
+    return (
+      <div class="relative size-6 shrink-0">
+        <Avatar
+          fallback={name()}
+          src={props.project.icon?.url}
+          {...getAvatarColors(props.project.icon?.color)}
+          class={`size-full ${props.class ?? ""}`}
+          style={
+            notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
+          }
+        />
+        <Show when={props.expandable}>
+          <Icon
+            name="chevron-right"
+            size="large"
+            class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
+          />
+        </Show>
+        <Show when={notifications().length > 0 && props.notify}>
+          <div
+            classList={{
+              "absolute -top-0.5 -right-0.5 size-1.5 rounded-full": true,
+              "bg-icon-critical-base": hasError(),
+              "bg-text-interactive-base": !hasError(),
+            }}
+          />
+        </Show>
+      </div>
+    )
+  }
+
   const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
   const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
     const name = createMemo(() => getFilename(props.project.worktree))
     const name = createMemo(() => getFilename(props.project.worktree))
+    const current = createMemo(() => base64Decode(params.dir ?? ""))
     return (
     return (
       <Switch>
       <Switch>
         <Match when={layout.sidebar.opened()}>
         <Match when={layout.sidebar.opened()}>
@@ -176,14 +233,7 @@ export default function Layout(props: ParentProps) {
             class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
             class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
           >
           >
             <div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
             <div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
-              <div class="size-6 shrink-0">
-                <Avatar
-                  fallback={name()}
-                  src={props.project.icon?.url}
-                  {...getAvatarColors(props.project.icon?.color)}
-                  class="size-full"
-                />
-              </div>
+              <ProjectAvatar project={props.project} />
               <span class="truncate text-14-medium text-text-strong">{name()}</span>
               <span class="truncate text-14-medium text-text-strong">{name()}</span>
             </div>
             </div>
           </Button>
           </Button>
@@ -193,17 +243,10 @@ export default function Layout(props: ParentProps) {
             variant="ghost"
             variant="ghost"
             size="large"
             size="large"
             class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
             class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
-            data-selected={props.project.worktree === currentDirectory()}
+            data-selected={props.project.worktree === current()}
             onClick={() => navigateToProject(props.project.worktree)}
             onClick={() => navigateToProject(props.project.worktree)}
           >
           >
-            <div class="size-6 shrink-0">
-              <Avatar
-                fallback={name()}
-                src={props.project.icon?.url}
-                {...getAvatarColors(props.project.icon?.color)}
-                class="size-full"
-              />
-            </div>
+            <ProjectAvatar project={props.project} notify />
           </Button>
           </Button>
         </Match>
         </Match>
       </Switch>
       </Switch>
@@ -211,35 +254,31 @@ export default function Layout(props: ParentProps) {
   }
   }
 
 
   const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
   const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
+    const notification = useNotification()
     const sortable = createSortable(props.project.worktree)
     const sortable = createSortable(props.project.worktree)
-    const [projectStore] = globalSync.child(props.project.worktree)
     const slug = createMemo(() => base64Encode(props.project.worktree))
     const slug = createMemo(() => base64Encode(props.project.worktree))
     const name = createMemo(() => getFilename(props.project.worktree))
     const name = createMemo(() => getFilename(props.project.worktree))
+    const [store, setStore] = globalSync.child(props.project.worktree)
+    const sessions = createMemo(() => store.session ?? [])
+    const [expanded, setExpanded] = createSignal(true)
     return (
     return (
       // @ts-ignore
       // @ts-ignore
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
         <Switch>
         <Switch>
           <Match when={layout.sidebar.opened()}>
           <Match when={layout.sidebar.opened()}>
-            <Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
+            <Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0" onOpenChange={setExpanded}>
               <Button
               <Button
                 as={"div"}
                 as={"div"}
                 variant="ghost"
                 variant="ghost"
                 class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
                 class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
               >
               >
                 <Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
                 <Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
-                  <div class="size-6 shrink-0">
-                    <Avatar
-                      fallback={name()}
-                      src={props.project.icon?.url}
-                      {...getAvatarColors(props.project.icon?.color)}
-                      class="size-full group-hover/session:hidden"
-                    />
-                    <Icon
-                      name="chevron-right"
-                      size="large"
-                      class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
-                    />
-                  </div>
+                  <ProjectAvatar
+                    project={props.project}
+                    class="group-hover/session:hidden"
+                    expandable
+                    notify={!expanded()}
+                  />
                   <span class="truncate text-14-medium text-text-strong">{name()}</span>
                   <span class="truncate text-14-medium text-text-strong">{name()}</span>
                 </Collapsible.Trigger>
                 </Collapsible.Trigger>
                 <div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
                 <div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
@@ -260,50 +299,102 @@ export default function Layout(props: ParentProps) {
               </Button>
               </Button>
               <Collapsible.Content>
               <Collapsible.Content>
                 <nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
                 <nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
-                  <For each={projectStore.session}>
+                  <For each={sessions()}>
                     {(session) => {
                     {(session) => {
                       const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
                       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() },
+                        })
+                        setStore(
+                          produce((draft) => {
+                            const match = Binary.search(draft.session, session.id, (s) => s.id)
+                            if (match.found) draft.session.splice(match.index, 1)
+                          }),
+                        )
+                      }
                       return (
                       return (
                         <A
                         <A
-                          data-active={session.id === params.id}
                           href={`${slug()}/session/${session.id}`}
                           href={`${slug()}/session/${session.id}`}
                           class="group/session focus:outline-none cursor-default"
                           class="group/session focus:outline-none cursor-default"
                         >
                         >
                           <Tooltip placement="right" value={session.title}>
                           <Tooltip placement="right" value={session.title}>
                             <div
                             <div
-                              class="w-full pl-4 pr-2 py-1 rounded-md
-                                   group-data-[active=true]/session:bg-surface-raised-base-hover
-                                   group-hover/session:bg-surface-raised-base-hover
-                                   group-focus/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"
                             >
                             >
                               <div class="flex items-center self-stretch gap-6 justify-between">
                               <div class="flex items-center self-stretch gap-6 justify-between">
                                 <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
                                 <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
                                   {session.title}
                                   {session.title}
                                 </span>
                                 </span>
-                                <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>
-                              </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>
                               </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>
                             </div>
                           </Tooltip>
                           </Tooltip>
                         </A>
                         </A>
                       )
                       )
                     }}
                     }}
                   </For>
                   </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>
                 </nav>
               </Collapsible.Content>
               </Collapsible.Content>
             </Collapsible>
             </Collapsible>
@@ -332,93 +423,9 @@ export default function Layout(props: ParentProps) {
   }
   }
 
 
   return (
   return (
-    <div class="relative h-screen flex flex-col">
-      <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
-        <A
-          href="/"
-          classList={{
-            "w-12 shrink-0 px-4 py-3.5": true,
-            "flex items-center justify-start self-stretch": true,
-            "border-r border-border-weak-base": true,
-          }}
-          style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
-          data-tauri-drag-region
-        >
-          <Mark class="shrink-0" />
-        </A>
-        <div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
-          <Show when={params.dir && layout.projects.list().length > 0}>
-            <div class="flex items-center gap-3">
-              <div class="flex items-center gap-2">
-                <Select
-                  options={layout.projects.list().map((project) => project.worktree)}
-                  current={currentDirectory()}
-                  label={(x) => getFilename(x)}
-                  onSelect={(x) => (x ? navigateToProject(x) : undefined)}
-                  class="text-14-regular text-text-base"
-                  variant="ghost"
-                >
-                  {/* @ts-ignore */}
-                  {(i) => (
-                    <div class="flex items-center gap-2">
-                      <Icon name="folder" size="small" />
-                      <div class="text-text-strong">{getFilename(i)}</div>
-                    </div>
-                  )}
-                </Select>
-                <div class="text-text-weaker">/</div>
-                <Select
-                  options={sessions()}
-                  current={currentSession()}
-                  placeholder="New session"
-                  label={(x) => x.title}
-                  value={(x) => x.id}
-                  onSelect={navigateToSession}
-                  class="text-14-regular text-text-base max-w-md"
-                  variant="ghost"
-                />
-              </div>
-              <Show when={currentSession()}>
-                <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
-                  New session
-                </Button>
-              </Show>
-            </div>
-            <div class="flex items-center gap-4">
-              <Tooltip
-                class="shrink-0"
-                value={
-                  <div class="flex items-center gap-2">
-                    <span>Toggle terminal</span>
-                    <span class="text-icon-base text-12-medium">Ctrl `</span>
-                  </div>
-                }
-              >
-                <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
-                  <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
-                    <Icon
-                      size="small"
-                      name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
-                      class="group-hover/terminal-toggle:hidden"
-                    />
-                    <Icon
-                      size="small"
-                      name="layout-bottom-partial"
-                      class="hidden group-hover/terminal-toggle:inline-block"
-                    />
-                    <Icon
-                      size="small"
-                      name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
-                      class="hidden group-active/terminal-toggle:inline-block"
-                    />
-                  </div>
-                </Button>
-              </Tooltip>
-            </div>
-          </Show>
-        </div>
-      </header>
-      <div class="h-[calc(100vh-3rem)] flex">
+    <div class="relative flex-1 min-h-0 flex flex-col">
+      <Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
+      <div class="flex-1 min-h-0 flex">
         <div
         <div
           classList={{
           classList={{
             "relative @container w-12 pb-5 shrink-0 bg-background-base": true,
             "relative @container w-12 pb-5 shrink-0 bg-background-base": true,

+ 1 - 1
packages/desktop/src/pages/session.tsx

@@ -675,7 +675,7 @@ export default function Page() {
                   <For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
                   <For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
                 </SortableProvider>
                 </SortableProvider>
                 <div class="h-full flex items-center justify-center">
                 <div class="h-full flex items-center justify-center">
-                  <Tooltip value="Open file" class="flex items-center">
+                  <Tooltip value="New Terminal" class="flex items-center">
                     <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
                     <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
                   </Tooltip>
                   </Tooltip>
                 </div>
                 </div>

+ 2 - 0
packages/opencode/src/acp/agent.ts

@@ -914,6 +914,8 @@ export namespace ACP {
             {
             {
               sessionID,
               sessionID,
               directory,
               directory,
+              providerID: model.providerID,
+              modelID: model.modelID,
             },
             },
             { throwOnError: true },
             { throwOnError: true },
           )
           )

+ 1 - 1
packages/opencode/src/cli/cmd/tui/util/clipboard.ts

@@ -61,7 +61,7 @@ export namespace Clipboard {
   const getCopyMethod = lazy(() => {
   const getCopyMethod = lazy(() => {
     const os = platform()
     const os = platform()
 
 
-    if (os === "darwin" && Bun.which("oascript")) {
+    if (os === "darwin" && Bun.which("osascript")) {
       console.log("clipboard: using osascript")
       console.log("clipboard: using osascript")
       return async (text: string) => {
       return async (text: string) => {
         const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
         const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')

+ 1 - 1
packages/opencode/src/provider/transform.ts

@@ -272,7 +272,7 @@ export namespace ProviderTransform {
     const options: Record<string, any> = {}
     const options: Record<string, any> = {}
 
 
     if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
     if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
-      if (model.api.id.includes("5.1")) {
+      if (model.api.id.includes("5.")) {
         options["reasoningEffort"] = "low"
         options["reasoningEffort"] = "low"
       } else {
       } else {
         options["reasoningEffort"] = "minimal"
         options["reasoningEffort"] = "minimal"

+ 2 - 2
packages/tauri/index.html

@@ -14,7 +14,7 @@
     <meta property="og:image" content="/social-share.png" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
   </head>
   </head>
-  <body class="antialiased overscroll-none select-none text-12-regular">
+  <body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
     <script>
     <script>
       ;(function () {
       ;(function () {
         const savedTheme = localStorage.getItem("theme") || "oc-1"
         const savedTheme = localStorage.getItem("theme") || "oc-1"
@@ -22,7 +22,7 @@
       })()
       })()
     </script>
     </script>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root"></div>
+    <div id="root" class="flex flex-col h-screen"></div>
     <script src="/src/index.tsx" type="module"></script>
     <script src="/src/index.tsx" type="module"></script>
   </body>
   </body>
 </html>
 </html>

+ 1 - 0
packages/tauri/package.json

@@ -16,6 +16,7 @@
     "@tauri-apps/api": "^2",
     "@tauri-apps/api": "^2",
     "@tauri-apps/plugin-dialog": "~2",
     "@tauri-apps/plugin-dialog": "~2",
     "@tauri-apps/plugin-opener": "^2",
     "@tauri-apps/plugin-opener": "^2",
+    "@tauri-apps/plugin-os": "~2",
     "@tauri-apps/plugin-process": "~2",
     "@tauri-apps/plugin-process": "~2",
     "@tauri-apps/plugin-shell": "~2",
     "@tauri-apps/plugin-shell": "~2",
     "@tauri-apps/plugin-store": "~2",
     "@tauri-apps/plugin-store": "~2",

+ 84 - 0
packages/tauri/src-tauri/Cargo.lock

@@ -1256,6 +1256,16 @@ dependencies = [
  "version_check",
  "version_check",
 ]
 ]
 
 
+[[package]]
+name = "gethostname"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
+dependencies = [
+ "rustix",
+ "windows-link 0.2.1",
+]
+
 [[package]]
 [[package]]
 name = "getrandom"
 name = "getrandom"
 version = "0.1.16"
 version = "0.1.16"
@@ -2309,6 +2319,16 @@ dependencies = [
  "objc2-foundation 0.3.2",
  "objc2-foundation 0.3.2",
 ]
 ]
 
 
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2 0.6.3",
+ "objc2-foundation 0.3.2",
+]
+
 [[package]]
 [[package]]
 name = "objc2-core-text"
 name = "objc2-core-text"
 version = "0.3.2"
 version = "0.3.2"
@@ -2440,6 +2460,7 @@ checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
 dependencies = [
 dependencies = [
  "bitflags 2.10.0",
  "bitflags 2.10.0",
  "objc2 0.6.3",
  "objc2 0.6.3",
+ "objc2-core-foundation",
  "objc2-foundation 0.3.2",
  "objc2-foundation 0.3.2",
 ]
 ]
 
 
@@ -2461,8 +2482,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
 checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
 dependencies = [
 dependencies = [
  "bitflags 2.10.0",
  "bitflags 2.10.0",
+ "block2 0.6.2",
  "objc2 0.6.3",
  "objc2 0.6.3",
+ "objc2-cloud-kit",
+ "objc2-core-data",
  "objc2-core-foundation",
  "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-core-text",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
+dependencies = [
+ "objc2 0.6.3",
  "objc2-foundation 0.3.2",
  "objc2-foundation 0.3.2",
 ]
 ]
 
 
@@ -2511,6 +2551,7 @@ dependencies = [
  "tauri-build",
  "tauri-build",
  "tauri-plugin-dialog",
  "tauri-plugin-dialog",
  "tauri-plugin-opener",
  "tauri-plugin-opener",
+ "tauri-plugin-os",
  "tauri-plugin-process",
  "tauri-plugin-process",
  "tauri-plugin-shell",
  "tauri-plugin-shell",
  "tauri-plugin-store",
  "tauri-plugin-store",
@@ -2535,6 +2576,22 @@ dependencies = [
  "pin-project-lite",
  "pin-project-lite",
 ]
 ]
 
 
+[[package]]
+name = "os_info"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c39b5918402d564846d5aba164c09a66cc88d232179dfd3e3c619a25a268392"
+dependencies = [
+ "android_system_properties",
+ "log",
+ "nix",
+ "objc2 0.6.3",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit",
+ "serde",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 [[package]]
 name = "os_pipe"
 name = "os_pipe"
 version = "1.2.3"
 version = "1.2.3"
@@ -3872,6 +3929,15 @@ dependencies = [
  "syn 2.0.110",
  "syn 2.0.110",
 ]
 ]
 
 
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 [[package]]
 name = "system-deps"
 name = "system-deps"
 version = "6.2.2"
 version = "6.2.2"
@@ -4146,6 +4212,24 @@ dependencies = [
  "zbus",
  "zbus",
 ]
 ]
 
 
+[[package]]
+name = "tauri-plugin-os"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
+dependencies = [
+ "gethostname",
+ "log",
+ "os_info",
+ "serde",
+ "serde_json",
+ "serialize-to-javascript",
+ "sys-locale",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.17",
+]
+
 [[package]]
 [[package]]
 name = "tauri-plugin-process"
 name = "tauri-plugin-process"
 version = "2.3.1"
 version = "2.3.1"

+ 1 - 0
packages/tauri/src-tauri/Cargo.toml

@@ -31,3 +31,4 @@ serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 serde_json = "1"
 tokio = "1.48.0"
 tokio = "1.48.0"
 listeners = "0.3"
 listeners = "0.3"
+tauri-plugin-os = "2"

+ 2 - 1
packages/tauri/src-tauri/capabilities/default.json

@@ -13,6 +13,7 @@
     "dialog:default",
     "dialog:default",
     "process:default",
     "process:default",
     "store:default",
     "store:default",
-    "window-state:default"
+    "window-state:default",
+    "os:default"
   ]
   ]
 }
 }

+ 5 - 1
packages/tauri/src-tauri/src/lib.rs

@@ -4,7 +4,9 @@ use std::{
     sync::{Arc, Mutex},
     sync::{Arc, Mutex},
     time::{Duration, Instant},
     time::{Duration, Instant},
 };
 };
-use tauri::{AppHandle, LogicalSize, Manager, Monitor, RunEvent, WebviewUrl, WebviewWindow};
+use tauri::{
+    AppHandle, LogicalSize, Manager, Monitor, RunEvent, TitleBarStyle, WebviewUrl, WebviewWindow,
+};
 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
 use tauri_plugin_shell::process::{CommandChild, CommandEvent};
 use tauri_plugin_shell::process::{CommandChild, CommandEvent};
 use tauri_plugin_shell::ShellExt;
 use tauri_plugin_shell::ShellExt;
@@ -107,6 +109,7 @@ pub fn run() {
     let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
     let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
 
 
     let mut builder = tauri::Builder::default()
     let mut builder = tauri::Builder::default()
+        .plugin(tauri_plugin_os::init())
         .plugin(tauri_plugin_window_state::Builder::new().build())
         .plugin(tauri_plugin_window_state::Builder::new().build())
         .plugin(tauri_plugin_store::Builder::new().build())
         .plugin(tauri_plugin_store::Builder::new().build())
         .plugin(tauri_plugin_dialog::init())
         .plugin(tauri_plugin_dialog::init())
@@ -180,6 +183,7 @@ pub fn run() {
                         .inner_size(size.width as f64, size.height as f64)
                         .inner_size(size.width as f64, size.height as f64)
                         .decorations(true)
                         .decorations(true)
                         .zoom_hotkeys_enabled(true)
                         .zoom_hotkeys_enabled(true)
+                        .title_bar_style(TitleBarStyle::Overlay)
                         .initialization_script(format!(
                         .initialization_script(format!(
                             r#"
                             r#"
                           window.__OPENCODE__ ??= {{}};
                           window.__OPENCODE__ ??= {{}};

+ 4 - 0
packages/tauri/src/index.tsx

@@ -5,6 +5,7 @@ import { runUpdater } from "./updater"
 import { onMount } from "solid-js"
 import { onMount } from "solid-js"
 import { open, save } from "@tauri-apps/plugin-dialog"
 import { open, save } from "@tauri-apps/plugin-dialog"
 import { open as shellOpen } from "@tauri-apps/plugin-shell"
 import { open as shellOpen } from "@tauri-apps/plugin-shell"
+import { type as ostype } from "@tauri-apps/plugin-os"
 
 
 const root = document.getElementById("root")
 const root = document.getElementById("root")
 if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
 if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -54,6 +55,9 @@ render(() => {
 
 
   return (
   return (
     <PlatformProvider value={platform}>
     <PlatformProvider value={platform}>
+      {ostype() === "macos" && (
+        <div class="bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
+      )}
       <App />
       <App />
     </PlatformProvider>
     </PlatformProvider>
   )
   )

+ 2 - 1
packages/ui/package.json

@@ -12,7 +12,8 @@
     "./styles/tailwind": "./src/styles/tailwind/index.css",
     "./styles/tailwind": "./src/styles/tailwind/index.css",
     "./icons/provider": "./src/components/provider-icons/types.ts",
     "./icons/provider": "./src/components/provider-icons/types.ts",
     "./icons/file-type": "./src/components/file-icons/types.ts",
     "./icons/file-type": "./src/components/file-icons/types.ts",
-    "./fonts/*": "./src/assets/fonts/*"
+    "./fonts/*": "./src/assets/fonts/*",
+    "./audio/*": "./src/assets/audio/*"
   },
   },
   "scripts": {
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "typecheck": "tsgo --noEmit",

BIN
packages/ui/src/assets/audio/staplebops-01.aac


BIN
packages/ui/src/assets/audio/staplebops-02.aac


BIN
packages/ui/src/assets/audio/staplebops-03.aac


BIN
packages/ui/src/assets/audio/staplebops-04.aac


BIN
packages/ui/src/assets/audio/staplebops-05.aac


BIN
packages/ui/src/assets/audio/staplebops-06.aac


BIN
packages/ui/src/assets/audio/staplebops-07.aac


+ 2 - 1
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"/>`,
   "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-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"/>`,
   "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"/>`,
   "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"/>`,
   "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"/>`,
   "check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
@@ -23,7 +24,7 @@ const icons = {
   folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
   folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
   "magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
   "magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
   "plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
   "plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
-  "plus": `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
+  plus: `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
   "pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
   "pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
   mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
   mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
   glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
   glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,

+ 8 - 6
packages/ui/src/components/toast.css

@@ -31,7 +31,7 @@
   border-radius: var(--radius-lg);
   border-radius: var(--radius-lg);
   border: 1px solid var(--border-weak-base);
   border: 1px solid var(--border-weak-base);
   background: var(--surface-float-base);
   background: var(--surface-float-base);
-  color: var(--text-inverted-base);
+  color: var(--text-invert-base);
   box-shadow: var(--shadow-md);
   box-shadow: var(--shadow-md);
 
 
   [data-slot="toast-inner"] {
   [data-slot="toast-inner"] {
@@ -80,7 +80,8 @@
     justify-content: center;
     justify-content: center;
 
 
     [data-component="icon"] {
     [data-component="icon"] {
-      color: rgba(253, 252, 252, 0.94);
+      color: var(--text-invert-stronger);
+      /* color: var(--icon-invert-base); */
     }
     }
   }
   }
 
 
@@ -93,7 +94,7 @@
   }
   }
 
 
   [data-slot="toast-title"] {
   [data-slot="toast-title"] {
-    color: var(--text-inverted-strong);
+    color: var(--text-invert-strong);
 
 
     /* text-14-medium */
     /* text-14-medium */
     font-family: var(--font-family-sans);
     font-family: var(--font-family-sans);
@@ -107,7 +108,8 @@
   }
   }
 
 
   [data-slot="toast-description"] {
   [data-slot="toast-description"] {
-    color: var(--text-inverted-base);
+    color: var(--text-invert-base);
+    text-wrap-style: pretty;
 
 
     /* text-14-regular */
     /* text-14-regular */
     font-family: var(--font-family-sans);
     font-family: var(--font-family-sans);
@@ -132,7 +134,7 @@
     padding: 0;
     padding: 0;
     cursor: pointer;
     cursor: pointer;
 
 
-    color: var(--text-inverted-strong);
+    color: var(--text-invert-strong);
     font-family: var(--font-family-sans);
     font-family: var(--font-family-sans);
     font-size: var(--font-size-base);
     font-size: var(--font-size-base);
     font-weight: var(--font-weight-medium);
     font-weight: var(--font-weight-medium);
@@ -144,7 +146,7 @@
     }
     }
 
 
     &:last-child {
     &:last-child {
-      color: var(--text-inverted-weak);
+      color: var(--text-invert-weak);
     }
     }
   }
   }
 
 

+ 2 - 0
packages/web/src/content/docs/zen.mdx

@@ -64,6 +64,7 @@ You can also access our models through the following API endpoints.
 
 
 | Model             | Model ID          | Endpoint                                         | AI SDK Package              |
 | Model             | Model ID          | Endpoint                                         | AI SDK Package              |
 | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- |
 | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- |
+| GPT 5.2           | gpt-5.2           | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
 | GPT 5.1           | gpt-5.1           | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
 | GPT 5.1           | gpt-5.1           | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
 | GPT 5.1 Codex     | gpt-5.1-codex     | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
 | GPT 5.1 Codex     | gpt-5.1-codex     | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
 | GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
 | GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
@@ -122,6 +123,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
 | Claude Opus 4.1                   | $15.00 | $75.00 | $1.50       | $18.75       |
 | Claude Opus 4.1                   | $15.00 | $75.00 | $1.50       | $18.75       |
 | Gemini 3 Pro (≤ 200K tokens)      | $2.00  | $12.00 | $0.20       | -            |
 | Gemini 3 Pro (≤ 200K tokens)      | $2.00  | $12.00 | $0.20       | -            |
 | Gemini 3 Pro (> 200K tokens)      | $4.00  | $18.00 | $0.40       | -            |
 | Gemini 3 Pro (> 200K tokens)      | $4.00  | $18.00 | $0.40       | -            |
+| GPT 5.2                           | $1.75  | $14.00 | $0.175      | -            |
 | GPT 5.1                           | $1.07  | $8.50  | $0.107      | -            |
 | GPT 5.1                           | $1.07  | $8.50  | $0.107      | -            |
 | GPT 5.1 Codex                     | $1.07  | $8.50  | $0.107      | -            |
 | GPT 5.1 Codex                     | $1.07  | $8.50  | $0.107      | -            |
 | GPT 5.1 Codex Max                 | $1.25  | $10.00 | $0.125      | -            |
 | GPT 5.1 Codex Max                 | $1.25  | $10.00 | $0.125      | -            |