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, }), { - 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, }), { @@ -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(() => ), + }, + { + 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() { + 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(
- + @@ -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) + }} > -- 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, }), { @@ -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() { - 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