diff options
| author | Adam <[email protected]> | 2025-12-15 04:37:14 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-15 10:20:16 -0600 |
| commit | d66d806700de9ad9fb0f3997a076ad23a815e6ea (patch) | |
| tree | ba651bd6ffa5a6f9d222326d2af627be1cf7d9f4 /packages/desktop/src/pages | |
| parent | e9b95b2e9199bfd94d6ad2f67a12ef5ae60f4f21 (diff) | |
| download | opencode-d66d806700de9ad9fb0f3997a076ad23a815e6ea.tar.gz opencode-d66d806700de9ad9fb0f3997a076ad23a815e6ea.zip | |
wip(desktop): progress
Diffstat (limited to 'packages/desktop/src/pages')
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 184 |
1 files changed, 124 insertions, 60 deletions
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index e3cac4842..48e01239c 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -27,22 +27,91 @@ import { import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" import type { JSX } from "solid-js" import { useSync } from "@/context/sync" -import { useSession, type LocalPTY } from "@/context/session" +import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLayout } from "@/context/layout" +import { usePrompt } from "@/context/prompt" import { getDirectory, getFilename } from "@opencode-ai/util/path" 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 { useCommand } from "@/context/command" +import { useParams } from "@solidjs/router" +import { pipe, sumBy } from "remeda" +import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2" export default function Page() { const layout = useLayout() const local = useLocal() const sync = useSync() - const session = useSession() + const terminal = useTerminal() + const prompt = usePrompt() const dialog = useDialog() const command = useCommand() + const params = useParams() + + // Session-specific derived state + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const tabs = createMemo(() => layout.tabs(sessionKey())) + + const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) + const userMessages = createMemo(() => + messages() + .filter((m) => m.role === "user") + .sort((a, b) => a.id.localeCompare(b.id)), + ) + const lastUserMessage = createMemo(() => userMessages()?.at(-1)) + + const [messageStore, setMessageStore] = createStore<{ messageId?: string }>({}) + const activeMessage = createMemo(() => { + if (!messageStore.messageId) return lastUserMessage() + return userMessages()?.find((m) => m.id === messageStore.messageId) + }) + const setActiveMessage = (message: UserMessage | undefined) => { + setMessageStore("messageId", message?.id) + } + + const status = createMemo( + () => + sync.data.session_status[params.id ?? ""] ?? { + type: "idle", + }, + ) + const working = createMemo(() => status()?.type !== "idle") + + const cost = createMemo(() => { + const total = pipe( + messages(), + sumBy((x) => (x.role === "assistant" ? x.cost : 0)), + ) + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(total) + }) + + const last = createMemo( + () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage, + ) + const model = createMemo(() => + last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined, + ) + const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) + + const tokens = createMemo(() => { + if (!last()) return + const t = last().tokens + return t.input + t.output + t.reasoning + t.cache.read + t.cache.write + }) + + const context = createMemo(() => { + const total = tokens() + const limit = model()?.limit.context + if (!total || !limit) return 0 + return Math.round((total / limit) * 100) + }) + const [store, setStore] = createStore({ clickTimer: undefined as number | undefined, activeDraggable: undefined as string | undefined, @@ -51,9 +120,14 @@ export default function Page() { let inputRef!: HTMLDivElement createEffect(() => { + if (!params.id) return + sync.session.sync(params.id) + }) + + createEffect(() => { if (layout.terminal.opened()) { - if (session.terminal.all().length === 0) { - session.terminal.new() + if (terminal.all().length === 0) { + terminal.new() } } }) @@ -99,7 +173,7 @@ export default function Page() { description: "Create a new terminal tab", category: "Terminal", keybind: "ctrl+shift+`", - onSelect: () => session.terminal.new(), + onSelect: () => terminal.new(), }, ]) @@ -166,11 +240,11 @@ export default function Page() { const handleDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (draggable && droppable) { - const currentTabs = session.layout.tabs.all + const currentTabs = tabs().all() const fromIndex = currentTabs?.indexOf(draggable.id.toString()) const toIndex = currentTabs?.indexOf(droppable.id.toString()) if (fromIndex !== toIndex && toIndex !== undefined) { - session.layout.moveTab(draggable.id.toString(), toIndex) + tabs().move(draggable.id.toString(), toIndex) } } } @@ -188,11 +262,11 @@ export default function Page() { const handleTerminalDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (draggable && droppable) { - const terminals = session.terminal.all() - const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString()) - const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString()) + const terminals = terminal.all() + const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString()) + const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString()) if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { - session.terminal.move(draggable.id.toString(), toIndex) + terminal.move(draggable.id.toString(), toIndex) } } } @@ -210,8 +284,8 @@ export default function Page() { <Tabs.Trigger value={props.terminal.id} closeButton={ - session.terminal.all().length > 1 && ( - <IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(props.terminal.id)} /> + terminal.all().length > 1 && ( + <IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} /> ) } > @@ -326,7 +400,7 @@ export default function Page() { return typeof draggable.id === "string" ? draggable.id : undefined } - const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length) + const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length) return ( <div class="relative bg-background-base size-full overflow-x-hidden flex flex-col"> @@ -339,7 +413,7 @@ export default function Page() { > <DragDropSensors /> <ConstrainDragYAxis /> - <Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}> + <Tabs value={tabs().active() ?? "chat"} onChange={tabs().open}> <div class="sticky top-0 shrink-0 flex"> <Tabs.List> <Tabs.Trigger value="chat"> @@ -349,15 +423,15 @@ export default function Page() { value={`${new Intl.NumberFormat("en-US", { notation: "compact", compactDisplay: "short", - }).format(session.usage.tokens() ?? 0)} Tokens`} + }).format(tokens() ?? 0)} Tokens`} class="flex items-center gap-1.5" > - <ProgressCircle percentage={session.usage.context() ?? 0} /> - <div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div> + <ProgressCircle percentage={context() ?? 0} /> + <div class="text-14-regular text-text-weak text-left w-7">{context() ?? 0}%</div> </Tooltip> </div> </Tabs.Trigger> - <Show when={layout.review.state() === "tab" && session.diffs().length}> + <Show when={layout.review.state() === "tab" && diffs().length}> <Tabs.Trigger value="review" closeButton={ @@ -367,25 +441,23 @@ export default function Page() { } > <div class="flex items-center gap-3"> - <Show when={session.diffs()}> - <DiffChanges changes={session.diffs()} variant="bars" /> + <Show when={diffs()}> + <DiffChanges changes={diffs()} variant="bars" /> </Show> <div class="flex items-center gap-1.5"> <div>Review</div> - <Show when={session.info()?.summary?.files}> + <Show when={info()?.summary?.files}> <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> - {session.info()?.summary?.files ?? 0} + {info()?.summary?.files ?? 0} </div> </Show> </div> </div> </Tabs.Trigger> </Show> - <SortableProvider ids={session.layout.tabs.all ?? []}> - <For each={session.layout.tabs.all ?? []}> - {(tab) => ( - <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} /> - )} + <SortableProvider ids={tabs().all() ?? []}> + <For each={tabs().all() ?? []}> + {(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />} </For> </SortableProvider> <div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3"> @@ -415,27 +487,23 @@ export default function Page() { }} > <Switch> - <Match when={session.id}> + <Match when={params.id}> <div class="flex items-start justify-start h-full min-h-0"> <SessionMessageRail - messages={session.messages.user()} - current={session.messages.active()} - onMessageSelect={session.messages.setActive} + messages={userMessages()} + current={activeMessage()} + onMessageSelect={setActiveMessage} wide={wide()} /> <SessionTurn - sessionID={session.id!} - messageID={session.messages.active()?.id!} + sessionID={params.id!} + messageID={activeMessage()?.id!} classes={{ root: "pb-20 flex-1 min-w-0", content: "pb-20", container: "w-full " + - (wide() - ? "max-w-146 mx-auto px-6" - : session.messages.user().length > 1 - ? "pr-6 pl-18" - : "px-6"), + (wide() ? "max-w-146 mx-auto px-6" : userMessages().length > 1 ? "pr-6 pl-18" : "px-6"), }} /> </div> @@ -476,7 +544,7 @@ export default function Page() { </div> </div> </div> - <Show when={layout.review.state() === "pane" && session.diffs().length}> + <Show when={layout.review.state() === "pane" && diffs().length}> <div classList={{ "relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true, @@ -488,7 +556,7 @@ export default function Page() { header: "px-6", container: "px-6", }} - diffs={session.diffs()} + diffs={diffs()} actions={ <Tooltip value="Open in tab"> <IconButton @@ -496,7 +564,7 @@ export default function Page() { variant="ghost" onClick={() => { layout.review.tab() - session.layout.setActiveTab("review") + tabs().setActive("review") }} /> </Tooltip> @@ -506,7 +574,7 @@ export default function Page() { </Show> </div> </Tabs.Content> - <Show when={layout.review.state() === "tab" && session.diffs().length}> + <Show when={layout.review.state() === "tab" && diffs().length}> <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden"> <div classList={{ @@ -519,13 +587,13 @@ export default function Page() { header: "px-6", container: "px-6", }} - diffs={session.diffs()} + diffs={diffs()} split /> </div> </Tabs.Content> </Show> - <For each={session.layout.tabs.all}> + <For each={tabs().all()}> {(tab) => { const [file] = createResource( () => tab, @@ -579,7 +647,7 @@ export default function Page() { </Show> </DragOverlay> </DragDropProvider> - <Show when={session.layout.tabs.active}> + <Show when={tabs().active()}> <div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8"> <PromptInput ref={(el) => { @@ -639,25 +707,21 @@ export default function Page() { > <DragDropSensors /> <ConstrainDragYAxis /> - <Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}> + <Tabs variant="alt" value={terminal.active()} onChange={terminal.open}> <Tabs.List class="h-10"> - <SortableProvider ids={session.terminal.all().map((t) => t.id)}> - <For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For> + <SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}> + <For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For> </SortableProvider> <div class="h-full flex items-center justify-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={terminal.new} /> </Tooltip> </div> </Tabs.List> - <For each={session.terminal.all()}> - {(terminal) => ( - <Tabs.Content value={terminal.id}> - <Terminal - pty={terminal} - onCleanup={session.terminal.update} - onConnectError={() => session.terminal.clone(terminal.id)} - /> + <For each={terminal.all()}> + {(pty) => ( + <Tabs.Content value={pty.id}> + <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} /> </Tabs.Content> )} </For> @@ -665,9 +729,9 @@ export default function Page() { <DragOverlay> <Show when={store.activeTerminalDraggable}> {(draggedId) => { - const terminal = createMemo(() => session.terminal.all().find((t) => t.id === draggedId())) + const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId())) return ( - <Show when={terminal()}> + <Show when={pty()}> {(t) => ( <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular"> {t().title} |
