feat(desktop): new layout

This commit is contained in:
Adam
2025-12-20 19:52:12 -06:00
parent c81506b28d
commit 182630e0d7
2 changed files with 205 additions and 292 deletions

View File

@@ -46,8 +46,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
opened: false, opened: false,
height: 280, height: 280,
}, },
review: { session: {
state: "pane" as "pane" | "tab", width: 600,
}, },
sessionTabs: {} as Record<string, SessionTabs>, sessionTabs: {} as Record<string, SessionTabs>,
}), }),
@@ -156,13 +156,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "height", height) setStore("terminal", "height", height)
}, },
}, },
review: { session: {
state: createMemo(() => store.review?.state ?? "closed"), width: createMemo(() => store.session?.width ?? 600),
pane() { resize(width: number) {
setStore("review", "state", "pane") if (!store.session) {
}, setStore("session", { width })
tab() { } else {
setStore("review", "state", "tab") setStore("session", "width", width)
}
}, },
}, },
tabs(sessionKey: string) { tabs(sessionKey: string) {
@@ -186,14 +187,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
}, },
async open(tab: string) { 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: [] } const current = store.sessionTabs[sessionKey] ?? { all: [] }
if (tab !== "review") { if (tab !== "review") {
if (!current.all.includes(tab)) { if (!current.all.includes(tab)) {

View File

@@ -22,7 +22,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes" 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 { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code" 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 { DialogSelectModel } from "@/components/dialog-select-model"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router" 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 { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt" import { extractPromptFromParts } from "@/utils/prompt"
@@ -118,27 +117,8 @@ export default function Page() {
setActiveMessage(msgs[targetIndex]) 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 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({ const [store, setStore] = createStore({
clickTimer: undefined as number | undefined, clickTimer: undefined as number | undefined,
activeDraggable: undefined as string | undefined, activeDraggable: undefined as string | undefined,
@@ -551,95 +531,17 @@ export default function Page() {
) )
} }
const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length) const showTabs = createMemo(() => diffs().length > 0 || tabs().all().length > 0)
return ( return (
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col"> <div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
<div class="min-h-0 grow w-full"> <div class="min-h-0 grow w-full flex">
<DragDropProvider {/* Session pane - always visible */}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<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>
</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>
</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>
</div>
</Tabs.List>
</div>
<Tabs.Content
value="chat"
class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden contain-strict"
>
<div <div
classList={{ class="relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
"w-full flex-1 min-h-0": true, style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
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(),
}}
> >
<div class="flex-1 min-h-0 overflow-hidden">
<Switch> <Switch>
<Match when={params.id}> <Match when={params.id}>
<div class="flex items-start justify-start h-full min-h-0"> <div class="flex items-start justify-start h-full min-h-0">
@@ -647,7 +549,7 @@ export default function Page() {
messages={visibleUserMessages()} messages={visibleUserMessages()}
current={activeMessage()} current={activeMessage()}
onMessageSelect={setActiveMessage} onMessageSelect={setActiveMessage}
wide={wide()} wide={!showTabs()}
/> />
<Show when={activeMessage()}> <Show when={activeMessage()}>
<SessionTurn <SessionTurn
@@ -661,7 +563,7 @@ export default function Page() {
content: "pb-20", content: "pb-20",
container: container:
"w-full " + "w-full " +
(wide() (!showTabs()
? "max-w-200 mx-auto px-6" ? "max-w-200 mx-auto px-6"
: visibleUserMessages().length > 1 : visibleUserMessages().length > 1
? "pr-6 pl-18" ? "pr-6 pl-18"
@@ -697,6 +599,7 @@ export default function Page() {
</div> </div>
</Match> </Match>
</Switch> </Switch>
</div>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50"> <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"> <div class="w-full max-w-200 px-6">
<PromptInput <PromptInput
@@ -706,44 +609,68 @@ export default function Page() {
/> />
</div> </div>
</div> </div>
<Show when={showTabs()}>
<ResizeHandle
direction="horizontal"
size={layout.session.width()}
min={450}
max={window.innerWidth * 0.45}
onResize={layout.session.resize}
/>
</Show>
</div> </div>
<Show when={layout.review.state() === "pane" && diffs().length}>
<div {/* Tabs pane - visible when there are diffs or file tabs */}
classList={{ <Show when={showTabs()}>
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base contain-strict": true, <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}
> >
<SessionReview <DragDropSensors />
classes={{ <ConstrainDragYAxis />
root: "pb-20", <Tabs value={tabs().active() ?? "review"} onChange={tabs().open}>
header: "px-6", <div class="sticky top-0 shrink-0 flex">
container: "px-6", <Tabs.List>
}} <Show when={diffs().length}>
diffs={diffs()} <Tabs.Trigger value="review">
actions={ <div class="flex items-center gap-3">
<Tooltip value="Open in tab"> <Show when={diffs()}>
<IconButton <DiffChanges changes={diffs()} variant="bars" />
icon="expand" </Show>
variant="ghost" <div class="flex items-center gap-1.5">
onClick={() => { <div>Review</div>
layout.review.tab() <Show when={info()?.summary?.files}>
tabs().setActive("review") <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}
/>
</Tooltip>
}
/>
</div> </div>
</Show> </Show>
</div> </div>
</Tabs.Content> </div>
<Show when={layout.review.state() === "tab" && diffs().length}> </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"> <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
<div <div class="relative pt-3 flex-1 min-h-0 overflow-hidden">
classList={{
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
}}
>
<SessionReview <SessionReview
classes={{ classes={{
root: "pb-40", root: "pb-40",
@@ -811,13 +738,6 @@ export default function Page() {
</Show> </Show>
</DragOverlay> </DragOverlay>
</DragDropProvider> </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
}}
/>
</div> </div>
</Show> </Show>
</div> </div>