diff options
| author | Adam <[email protected]> | 2025-12-20 19:52:12 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-20 19:52:12 -0600 |
| commit | 182630e0d7d49d8c9842781f08b19388346d8e5c (patch) | |
| tree | 2b6c1139c7f4a22204893fcd1a8a469d792614e7 /packages | |
| parent | c81506b28d78c944da72bad33899bb0ef00ff816 (diff) | |
| download | opencode-182630e0d7d49d8c9842781f08b19388346d8e5c.tar.gz opencode-182630e0d7d49d8c9842781f08b19388346d8e5c.zip | |
feat(desktop): new layout
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 27 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 464 |
2 files changed, 202 insertions, 289 deletions
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 0d3de5683..8bfc8aa21 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -46,8 +46,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( opened: false, height: 280, }, - review: { - state: "pane" as "pane" | "tab", + session: { + width: 600, }, sessionTabs: {} as Record<string, SessionTabs>, }), @@ -156,13 +156,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("terminal", "height", height) }, }, - review: { - state: createMemo(() => store.review?.state ?? "closed"), - pane() { - setStore("review", "state", "pane") - }, - tab() { - setStore("review", "state", "tab") + session: { + width: createMemo(() => store.session?.width ?? 600), + resize(width: number) { + if (!store.session) { + setStore("session", { width }) + } else { + setStore("session", "width", width) + } }, }, tabs(sessionKey: string) { @@ -186,14 +187,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } }, 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)) { diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 6e993ff8f..796b5b0c7 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -22,7 +22,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DiffChanges } from "@opencode-ai/ui/diff-changes" -import { ProgressCircle } from "@opencode-ai/ui/progress-circle" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Tabs } from "@opencode-ai/ui/tabs" import { useCodeComponent } from "@opencode-ai/ui/context/code" @@ -50,7 +49,7 @@ 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" +import { UserMessage } from "@opencode-ai/sdk/v2" import { useSDK } from "@/context/sdk" import { usePrompt } from "@/context/prompt" import { extractPromptFromParts } from "@/utils/prompt" @@ -118,27 +117,8 @@ export default function Page() { setActiveMessage(msgs[targetIndex]) } - 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, @@ -551,273 +531,213 @@ export default function Page() { ) } - const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length) + const showTabs = createMemo(() => diffs().length > 0 || tabs().all().length > 0) return ( <div class="relative bg-background-base size-full overflow-x-hidden flex flex-col"> - <div class="min-h-0 grow w-full"> - <DragDropProvider - onDragStart={handleDragStart} - onDragEnd={handleDragEnd} - onDragOver={handleDragOver} - collisionDetector={closestCenter} + <div class="min-h-0 grow w-full flex"> + {/* Session pane - always visible */} + <div + class="relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger" + style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }} > - <DragDropSensors /> - <ConstrainDragYAxis /> - <Tabs value={tabs().active() ?? "chat"} onChange={tabs().open}> - <div class="sticky top-0 shrink-0 flex"> - <Tabs.List> - <Tabs.Trigger value="chat"> - <div class="flex gap-x-[17px] items-center"> - <div>Session</div> - <Tooltip - value={`${new Intl.NumberFormat("en-US", { - notation: "compact", - compactDisplay: "short", - }).format(tokens() ?? 0)} Tokens`} - class="flex items-center gap-1.5" - > - <ProgressCircle percentage={context() ?? 0} /> - <div class="text-14-regular text-text-weak text-left w-7">{context() ?? 0}%</div> - </Tooltip> + <div class="flex-1 min-h-0 overflow-hidden"> + <Switch> + <Match when={params.id}> + <div class="flex items-start justify-start h-full min-h-0"> + <SessionMessageRail + messages={visibleUserMessages()} + current={activeMessage()} + onMessageSelect={setActiveMessage} + wide={!showTabs()} + /> + <Show when={activeMessage()}> + <SessionTurn + sessionID={params.id!} + messageID={activeMessage()!.id} + stepsExpanded={store.stepsExpanded} + onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)} + onUserInteracted={() => setStore("userInteracted", true)} + classes={{ + root: "pb-20 flex-1 min-w-0", + content: "pb-20", + container: + "w-full " + + (!showTabs() + ? "max-w-200 mx-auto px-6" + : visibleUserMessages().length > 1 + ? "pr-6 pl-18" + : "px-6"), + }} + /> + </Show> + </div> + </Match> + <Match when={true}> + <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"> + <div class="text-20-medium text-text-weaker">New session</div> + <div class="flex justify-center items-center gap-3"> + <Icon name="folder" size="small" /> + <div class="text-12-medium text-text-weak"> + {getDirectory(sync.data.path.directory)} + <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span> + </div> </div> - </Tabs.Trigger> - <Show when={layout.review.state() === "tab" && diffs().length}> - <Tabs.Trigger - value="review" - closeButton={ - <Tooltip value="Close tab" placement="bottom"> - <IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} /> - </Tooltip> - } - > - <div class="flex items-center gap-3"> - <Show when={diffs()}> - <DiffChanges changes={diffs()} variant="bars" /> - </Show> - <div class="flex items-center gap-1.5"> - <div>Review</div> - <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"> - {info()?.summary?.files ?? 0} - </div> - </Show> + <Show when={sync.project}> + {(project) => ( + <div class="flex justify-center items-center gap-3"> + <Icon name="pencil-line" size="small" /> + <div class="text-12-medium text-text-weak"> + Last modified + <span class="text-text-strong"> + {DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()} + </span> + </div> </div> - </div> - </Tabs.Trigger> - </Show> - <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"> - <Tooltip value="Open file" class="flex items-center"> - <IconButton - icon="plus-small" - variant="ghost" - iconSize="large" - onClick={() => dialog.show(() => <DialogSelectFile />)} - /> - </Tooltip> + )} + </Show> </div> - </Tabs.List> + </Match> + </Switch> + </div> + <div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50"> + <div class="w-full max-w-200 px-6"> + <PromptInput + ref={(el) => { + inputRef = el + }} + /> </div> - <Tabs.Content - value="chat" - class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden contain-strict" + </div> + <Show when={showTabs()}> + <ResizeHandle + direction="horizontal" + size={layout.session.width()} + min={450} + max={window.innerWidth * 0.45} + onResize={layout.session.resize} + /> + </Show> + </div> + + {/* Tabs pane - visible when there are diffs or file tabs */} + <Show when={showTabs()}> + <div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base"> + <DragDropProvider + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + onDragOver={handleDragOver} + collisionDetector={closestCenter} > - <div - classList={{ - "w-full flex-1 min-h-0": true, - grid: layout.review.state() === "tab", - flex: layout.review.state() === "pane", - }} - > - <div - classList={{ - "relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true, - "max-w-146 mx-auto": !wide(), - }} - > - <Switch> - <Match when={params.id}> - <div class="flex items-start justify-start h-full min-h-0"> - <SessionMessageRail - messages={visibleUserMessages()} - current={activeMessage()} - onMessageSelect={setActiveMessage} - wide={wide()} - /> - <Show when={activeMessage()}> - <SessionTurn - sessionID={params.id!} - messageID={activeMessage()!.id} - stepsExpanded={store.stepsExpanded} - onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)} - onUserInteracted={() => setStore("userInteracted", true)} - classes={{ - root: "pb-20 flex-1 min-w-0", - content: "pb-20", - container: - "w-full " + - (wide() - ? "max-w-200 mx-auto px-6" - : visibleUserMessages().length > 1 - ? "pr-6 pl-18" - : "px-6"), - }} - /> - </Show> - </div> - </Match> - <Match when={true}> - <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"> - <div class="text-20-medium text-text-weaker">New session</div> - <div class="flex justify-center items-center gap-3"> - <Icon name="folder" size="small" /> - <div class="text-12-medium text-text-weak"> - {getDirectory(sync.data.path.directory)} - <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span> + <DragDropSensors /> + <ConstrainDragYAxis /> + <Tabs value={tabs().active() ?? "review"} onChange={tabs().open}> + <div class="sticky top-0 shrink-0 flex"> + <Tabs.List> + <Show when={diffs().length}> + <Tabs.Trigger value="review"> + <div class="flex items-center gap-3"> + <Show when={diffs()}> + <DiffChanges changes={diffs()} variant="bars" /> + </Show> + <div class="flex items-center gap-1.5"> + <div>Review</div> + <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"> + {info()?.summary?.files ?? 0} + </div> + </Show> </div> </div> - <Show when={sync.project}> - {(project) => ( - <div class="flex justify-center items-center gap-3"> - <Icon name="pencil-line" size="small" /> - <div class="text-12-medium text-text-weak"> - Last modified - <span class="text-text-strong"> - {DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()} - </span> - </div> - </div> - )} - </Show> - </div> - </Match> - </Switch> - <div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50"> - <div class="w-full max-w-200 px-6"> - <PromptInput - ref={(el) => { - inputRef = el + </Tabs.Trigger> + </Show> + <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"> + <Tooltip value="Open file" class="flex items-center"> + <IconButton + icon="plus-small" + variant="ghost" + iconSize="large" + onClick={() => dialog.show(() => <DialogSelectFile />)} + /> + </Tooltip> + </div> + </Tabs.List> + </div> + <Show when={diffs().length}> + <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict"> + <div class="relative pt-3 flex-1 min-h-0 overflow-hidden"> + <SessionReview + classes={{ + root: "pb-40", + header: "px-6", + container: "px-6", }} + diffs={diffs()} + split /> </div> - </div> - </div> - <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 contain-strict": true, - }} - > - <SessionReview - classes={{ - root: "pb-20", - header: "px-6", - container: "px-6", - }} - diffs={diffs()} - actions={ - <Tooltip value="Open in tab"> - <IconButton - icon="expand" - variant="ghost" - onClick={() => { - layout.review.tab() - tabs().setActive("review") - }} - /> - </Tooltip> - } - /> - </div> + </Tabs.Content> </Show> - </div> - </Tabs.Content> - <Show when={layout.review.state() === "tab" && diffs().length}> - <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict"> - <div - classList={{ - "relative pt-3 flex-1 min-h-0 overflow-hidden": true, + <For each={tabs().all()}> + {(tab) => { + const [file] = createResource( + () => tab, + async (tab) => { + if (tab.startsWith("file://")) { + return local.file.node(tab.replace("file://", "")) + } + return undefined + }, + ) + return ( + <Tabs.Content value={tab} class="select-text mt-3"> + <Switch> + <Match when={file()}> + {(f) => ( + <Dynamic + component={codeComponent} + file={{ + name: f().path, + contents: f().content?.content ?? "", + cacheKey: checksum(f().content?.content ?? ""), + }} + overflow="scroll" + class="pb-40" + /> + )} + </Match> + </Switch> + </Tabs.Content> + ) }} - > - <SessionReview - classes={{ - root: "pb-40", - header: "px-6", - container: "px-6", - }} - diffs={diffs()} - split - /> - </div> - </Tabs.Content> - </Show> - <For each={tabs().all()}> - {(tab) => { - const [file] = createResource( - () => tab, - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, - ) - return ( - <Tabs.Content value={tab} class="select-text mt-3"> - <Switch> - <Match when={file()}> - {(f) => ( - <Dynamic - component={codeComponent} - file={{ - name: f().path, - contents: f().content?.content ?? "", - cacheKey: checksum(f().content?.content ?? ""), - }} - overflow="scroll" - class="pb-40" - /> - )} - </Match> - </Switch> - </Tabs.Content> - ) - }} - </For> - </Tabs> - <DragOverlay> - <Show when={store.activeDraggable}> - {(draggedFile) => { - const [file] = createResource( - () => draggedFile(), - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, - ) - return ( - <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent"> - <Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show> - </div> - ) - }} - </Show> - </DragOverlay> - </DragDropProvider> - <Show when={tabs().active()}> - <div class="absolute inset-x-0 px-6 max-w-200 flex flex-col justify-center items-center z-50 mx-auto bottom-8"> - <PromptInput - ref={(el) => { - inputRef = el - }} - /> + </For> + </Tabs> + <DragOverlay> + <Show when={store.activeDraggable}> + {(draggedFile) => { + const [file] = createResource( + () => draggedFile(), + async (tab) => { + if (tab.startsWith("file://")) { + return local.file.node(tab.replace("file://", "")) + } + return undefined + }, + ) + return ( + <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent"> + <Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show> + </div> + ) + }} + </Show> + </DragOverlay> + </DragDropProvider> </div> </Show> </div> |
