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) + }} >