From d66d806700de9ad9fb0f3997a076ad23a815e6ea Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Mon, 15 Dec 2025 04:37:14 -0600
Subject: wip(desktop): progress

---
 packages/desktop/src/context/layout.tsx | 92 +++++++++++++++++++++++++++++++--
 1 file changed, 89 insertions(+), 3 deletions(-)

(limited to 'packages/desktop/src/context/layout.tsx')

diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 925bf4d4c..af71c6a00 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -1,5 +1,5 @@
-import { createStore } from "solid-js/store"
-import { createMemo, onMount } from "solid-js"
+import { createStore, produce } from "solid-js/store"
+import { batch, createMemo, onMount } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { makePersisted } from "@solid-primitives/storage"
 import { useGlobalSync } from "./global-sync"
@@ -22,6 +22,11 @@ export function getAvatarColors(key?: string) {
   }
 }
 
+type SessionTabs = {
+  active?: string
+  all: string[]
+}
+
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
   name: "Layout",
   init: () => {
@@ -41,9 +46,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         review: {
           state: "pane" as "pane" | "tab",
         },
+        sessionTabs: {} as Record<string, SessionTabs>,
       }),
       {
-        name: "layout.v2",
+        name: "layout.v3",
       },
     )
 
@@ -155,6 +161,86 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("review", "state", "tab")
         },
       },
+      tabs(sessionKey: string) {
+        const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
+        return {
+          tabs,
+          active: createMemo(() => tabs().active),
+          all: createMemo(() => tabs().all),
+          setActive(tab: string | undefined) {
+            if (!store.sessionTabs[sessionKey]) {
+              setStore("sessionTabs", sessionKey, { all: [], active: tab })
+            } else {
+              setStore("sessionTabs", sessionKey, "active", tab)
+            }
+          },
+          setAll(all: string[]) {
+            if (!store.sessionTabs[sessionKey]) {
+              setStore("sessionTabs", sessionKey, { all, active: undefined })
+            } else {
+              setStore("sessionTabs", sessionKey, "all", all)
+            }
+          },
+          async open(tab: string) {
+            if (tab === "chat") {
+              if (!store.sessionTabs[sessionKey]) {
+                setStore("sessionTabs", sessionKey, { all: [], active: undefined })
+              } else {
+                setStore("sessionTabs", sessionKey, "active", undefined)
+              }
+              return
+            }
+            const current = store.sessionTabs[sessionKey] ?? { all: [] }
+            if (tab !== "review") {
+              if (!current.all.includes(tab)) {
+                if (!store.sessionTabs[sessionKey]) {
+                  setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
+                } else {
+                  setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
+                  setStore("sessionTabs", sessionKey, "active", tab)
+                }
+                return
+              }
+            }
+            if (!store.sessionTabs[sessionKey]) {
+              setStore("sessionTabs", sessionKey, { all: [], active: tab })
+            } else {
+              setStore("sessionTabs", sessionKey, "active", tab)
+            }
+          },
+          close(tab: string) {
+            const current = store.sessionTabs[sessionKey]
+            if (!current) return
+            batch(() => {
+              setStore(
+                "sessionTabs",
+                sessionKey,
+                "all",
+                current.all.filter((x) => x !== tab),
+              )
+              if (current.active === tab) {
+                const index = current.all.findIndex((f) => f === tab)
+                const previous = current.all[Math.max(0, index - 1)]
+                setStore("sessionTabs", sessionKey, "active", previous)
+              }
+            })
+          },
+          move(tab: string, to: number) {
+            const current = store.sessionTabs[sessionKey]
+            if (!current) return
+            const index = current.all.findIndex((f) => f === tab)
+            if (index === -1) return
+            setStore(
+              "sessionTabs",
+              sessionKey,
+              "all",
+              produce((opened) => {
+                opened.splice(to, 0, opened.splice(index, 1)[0])
+              }),
+            )
+          },
+        }
+      },
     }
   },
 })
-- 
cgit v1.2.3


From df2ebfac7d3dca6c2262258e6ee85a3c22cc53c3 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Mon, 15 Dec 2025 06:52:54 -0600
Subject: wip(desktop): progress

---
 packages/desktop/src/context/layout.tsx     | 15 +++++++
 packages/desktop/src/pages/layout.tsx       |  6 +++
 packages/desktop/src/pages/session.tsx      | 69 ++++++++++++++++++++++++++++-
 packages/ui/src/components/session-turn.tsx | 20 +++++++--
 4 files changed, 106 insertions(+), 4 deletions(-)

(limited to 'packages/desktop/src/context/layout.tsx')

diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index af71c6a00..604f7c5d1 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -46,6 +46,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         review: {
           state: "pane" as "pane" | "tab",
         },
+        steps: {
+          expanded: false,
+        },
         sessionTabs: {} as Record<string, SessionTabs>,
       }),
       {
@@ -161,6 +164,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("review", "state", "tab")
         },
       },
+      steps: {
+        expanded: createMemo(() => store.steps?.expanded ?? false),
+        toggle() {
+          setStore("steps", "expanded", (x) => !x)
+        },
+        expand() {
+          setStore("steps", "expanded", true)
+        },
+        collapse() {
+          setStore("steps", "expanded", false)
+        },
+      },
       tabs(sessionKey: string) {
         const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
         return {
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index bb2302503..53078e01b 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -156,6 +156,12 @@ export default function Layout(props: ParentProps) {
           },
         ]
       : []),
+    {
+      id: "provider.connect",
+      title: "Connect provider",
+      category: "Provider",
+      onSelect: () => connectProvider(),
+    },
     {
       id: "session.previous",
       title: "Previous session",
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index 5c74f2d2e..d0c3bf7de 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -34,6 +34,7 @@ import { Terminal } from "@/components/terminal"
 import { checksum } from "@opencode-ai/util/encode"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
+import { DialogSelectModel } from "@/components/dialog-select-model"
 import { useCommand } from "@/context/command"
 import { useNavigate, useParams } from "@solidjs/router"
 import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
@@ -70,6 +71,25 @@ export default function Page() {
     setMessageStore("messageId", message?.id)
   }
 
+  function navigateMessageByOffset(offset: number) {
+    const messages = userMessages()
+    if (messages.length === 0) return
+
+    const current = activeMessage()
+    const currentIndex = current ? messages.findIndex((m) => m.id === current.id) : -1
+
+    let targetIndex: number
+    if (currentIndex === -1) {
+      targetIndex = offset > 0 ? 0 : messages.length - 1
+    } else {
+      targetIndex = currentIndex + offset
+    }
+
+    if (targetIndex < 0 || targetIndex >= messages.length) return
+
+    setActiveMessage(messages[targetIndex])
+  }
+
   const last = createMemo(
     () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
   )
@@ -118,7 +138,7 @@ export default function Page() {
       title: "New session",
       description: "Create a new session",
       category: "Session",
-      keybind: "mod+n",
+      keybind: "mod+shift+s",
       slash: "new",
       onSelect: () => navigate(`/${params.dir}/session`),
     },
@@ -163,6 +183,49 @@ export default function Page() {
       keybind: "ctrl+shift+`",
       onSelect: () => terminal.new(),
     },
+    {
+      id: "steps.toggle",
+      title: "Toggle steps",
+      description: "Show or hide the steps",
+      category: "View",
+      keybind: "mod+e",
+      slash: "steps",
+      onSelect: () => layout.steps.toggle(),
+    },
+    {
+      id: "message.previous",
+      title: "Previous message",
+      description: "Go to the previous user message",
+      category: "Session",
+      keybind: "mod+arrowup",
+      disabled: !params.id,
+      onSelect: () => navigateMessageByOffset(-1),
+    },
+    {
+      id: "message.next",
+      title: "Next message",
+      description: "Go to the next user message",
+      category: "Session",
+      keybind: "mod+arrowdown",
+      disabled: !params.id,
+      onSelect: () => navigateMessageByOffset(1),
+    },
+    {
+      id: "model.choose",
+      title: "Choose model",
+      description: "Select a different model",
+      category: "Model",
+      slash: "model",
+      onSelect: () => dialog.replace(() => <DialogSelectModel />),
+    },
+    {
+      id: "agent.cycle",
+      title: "Cycle agent",
+      description: "Switch to the next agent",
+      category: "Agent",
+      slash: "agent",
+      onSelect: () => local.agent.move(1),
+    },
   ])
 
   // Handle keyboard events that aren't commands
@@ -492,6 +555,10 @@ export default function Page() {
                           <SessionTurn
                             sessionID={params.id!}
                             messageID={activeMessage()?.id!}
+                            stepsExpanded={layout.steps.expanded()}
+                            onStepsExpandedChange={(expanded) =>
+                              expanded ? layout.steps.expand() : layout.steps.collapse()
+                            }
                             classes={{
                               root: "pb-20 flex-1 min-w-0",
                               content: "pb-20",
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 54dd01091..807092d03 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -24,6 +24,8 @@ export function SessionTurn(
   props: ParentProps<{
     sessionID: string
     messageID: string
+    stepsExpanded?: boolean
+    onStepsExpandedChange?: (expanded: boolean) => void
     classes?: {
       root?: string
       content?: string
@@ -222,10 +224,17 @@ export function SessionTurn(
 
               const [store, setStore] = createStore({
                 status: rawStatus(),
-                stepsExpanded: working(),
+                stepsExpanded: props.stepsExpanded ?? working(),
                 duration: duration(),
               })
 
+              // Sync with controlled prop
+              createEffect(() => {
+                if (props.stepsExpanded !== undefined) {
+                  setStore("stepsExpanded", props.stepsExpanded)
+                }
+              })
+
               createEffect(() => {
                 const timer = setInterval(() => {
                   setStore("duration", duration())
@@ -262,6 +271,7 @@ export function SessionTurn(
                 const isWorking = working()
                 if (prev && !isWorking && !state.userScrolled) {
                   setStore("stepsExpanded", false)
+                  props.onStepsExpandedChange?.(false)
                 }
                 return isWorking
               }, working())
@@ -278,7 +288,7 @@ export function SessionTurn(
                     <div data-slot="session-turn-message-header">
                       <div data-slot="session-turn-message-title">
                         <Switch>
-                          <Match when={working()}>
+                          <Match when={working() && message().id === userMessages().at(-1)?.id}>
                             <Typewriter as="h1" text={message().summary?.title} data-slot="session-turn-typewriter" />
                           </Match>
                           <Match when={true}>
@@ -298,7 +308,11 @@ export function SessionTurn(
                       data-slot="session-turn-collapsible-trigger-content"
                       variant="ghost"
                       size="small"
-                      onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
+                      onClick={() => {
+                        const next = !store.stepsExpanded
+                        setStore("stepsExpanded", next)
+                        props.onStepsExpandedChange?.(next)
+                      }}
                     >
                       <Show when={working()}>
                         <Spinner />
-- 
cgit v1.2.3


From 5e37a902ce0ad209cedb0a85e997f1964064424a Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Mon, 15 Dec 2025 06:59:01 -0600
Subject: wip(desktop): progress

---
 packages/desktop/src/context/layout.tsx | 15 ---------------
 packages/desktop/src/pages/session.tsx  | 10 +++++-----
 2 files changed, 5 insertions(+), 20 deletions(-)

(limited to 'packages/desktop/src/context/layout.tsx')

diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 604f7c5d1..af71c6a00 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -46,9 +46,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         review: {
           state: "pane" as "pane" | "tab",
         },
-        steps: {
-          expanded: false,
-        },
         sessionTabs: {} as Record<string, SessionTabs>,
       }),
       {
@@ -164,18 +161,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("review", "state", "tab")
         },
       },
-      steps: {
-        expanded: createMemo(() => store.steps?.expanded ?? false),
-        toggle() {
-          setStore("steps", "expanded", (x) => !x)
-        },
-        expand() {
-          setStore("steps", "expanded", true)
-        },
-        collapse() {
-          setStore("steps", "expanded", false)
-        },
-      },
       tabs(sessionKey: string) {
         const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
         return {
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index d0c3bf7de..d49779587 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -115,6 +115,7 @@ export default function Page() {
     clickTimer: undefined as number | undefined,
     activeDraggable: undefined as string | undefined,
     activeTerminalDraggable: undefined as string | undefined,
+    stepsExpanded: false,
   })
   let inputRef!: HTMLDivElement
 
@@ -190,7 +191,8 @@ export default function Page() {
       category: "View",
       keybind: "mod+e",
       slash: "steps",
-      onSelect: () => layout.steps.toggle(),
+      disabled: !params.id,
+      onSelect: () => setStore("stepsExpanded", (x) => !x),
     },
     {
       id: "message.previous",
@@ -555,10 +557,8 @@ export default function Page() {
                           <SessionTurn
                             sessionID={params.id!}
                             messageID={activeMessage()?.id!}
-                            stepsExpanded={layout.steps.expanded()}
-                            onStepsExpandedChange={(expanded) =>
-                              expanded ? layout.steps.expand() : layout.steps.collapse()
-                            }
+                            stepsExpanded={store.stepsExpanded}
+                            onStepsExpandedChange={(expanded) => setStore("stepsExpanded", expanded)}
                             classes={{
                               root: "pb-20 flex-1 min-w-0",
                               content: "pb-20",
-- 
cgit v1.2.3

