diff options
| author | Adam <[email protected]> | 2026-03-10 11:29:57 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-10 13:00:14 -0500 |
| commit | 835a27cf517fae5d9952c30989de8be8f760d7a5 (patch) | |
| tree | 94c5ec763e09517d42ce86a960264854c139f2f3 /packages/app/src/pages/session | |
| parent | 85afaaa13d693f400d8ec8e257fec086a58b68c1 (diff) | |
| download | opencode-835a27cf517fae5d9952c30989de8be8f760d7a5.tar.gz opencode-835a27cf517fae5d9952c30989de8be8f760d7a5.zip | |
fix(app): terminal jank
Diffstat (limited to 'packages/app/src/pages/session')
| -rw-r--r-- | packages/app/src/pages/session/terminal-panel.tsx | 292 |
1 files changed, 153 insertions, 139 deletions
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 19a656b53..a6c3929c1 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,6 +1,5 @@ import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" -import { createMediaQuery } from "@solid-primitives/media" import { useParams } from "@solidjs/router" import { Tabs } from "@opencode-ai/ui/tabs" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" @@ -27,12 +26,10 @@ export function TerminalPanel() { const language = useLanguage() const command = useCommand() - const isDesktop = createMediaQuery("(min-width: 768px)") const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const view = createMemo(() => layout.view(sessionKey)) const opened = createMemo(() => view().terminal.opened()) - const open = createMemo(() => isDesktop() && opened()) const size = createSizing() const height = createMemo(() => layout.terminal.height()) const close = () => view().terminal.close() @@ -41,6 +38,25 @@ export function TerminalPanel() { const [store, setStore] = createStore({ autoCreated: false, activeDraggable: undefined as string | undefined, + view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), + }) + + const max = () => store.view * 0.6 + const pane = () => Math.min(height(), max()) + + createEffect(() => { + if (typeof window === "undefined") return + + const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) + const port = window.visualViewport + + sync() + window.addEventListener("resize", sync) + port?.addEventListener("resize", sync) + onCleanup(() => { + window.removeEventListener("resize", sync) + port?.removeEventListener("resize", sync) + }) }) createEffect(() => { @@ -69,14 +85,14 @@ export function TerminalPanel() { focusTerminalById(id) const frame = requestAnimationFrame(() => { - if (!open()) return + if (!opened()) return if (terminal.active() !== id) return focusTerminalById(id) }) const timers = [120, 240].map((ms) => window.setTimeout(() => { - if (!open()) return + if (!opened()) return if (terminal.active() !== id) return focusTerminalById(id) }, ms), @@ -90,7 +106,7 @@ export function TerminalPanel() { createEffect( on( - () => [open(), terminal.active()] as const, + () => [opened(), terminal.active()] as const, ([next, id]) => { if (!next || !id) return const stop = focus(id) @@ -100,7 +116,7 @@ export function TerminalPanel() { ) createEffect(() => { - if (open()) return + if (opened()) return const active = document.activeElement if (!(active instanceof HTMLElement)) return if (!root?.contains(active)) return @@ -165,151 +181,149 @@ export function TerminalPanel() { } return ( - <Show when={isDesktop()}> + <div + ref={root} + id="terminal-panel" + role="region" + aria-label={language.t("terminal.title")} + aria-hidden={!opened()} + inert={!opened()} + class="relative w-full shrink-0 overflow-hidden bg-background-stronger" + classList={{ + "border-t border-border-weak-base": opened(), + "transition-[height] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[height] motion-reduce:transition-none": + !size.active(), + }} + style={{ height: opened() ? `${pane()}px` : "0px" }} + > <div - ref={root} - id="terminal-panel" - role="region" - aria-label={language.t("terminal.title")} - aria-hidden={!open()} - inert={!open()} - class="relative w-full shrink-0 overflow-hidden" + class="absolute inset-x-0 top-0 flex flex-col" classList={{ - "transition-[height] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[height] motion-reduce:transition-none": + "translate-y-0": opened(), + "translate-y-full pointer-events-none": !opened(), + "transition-transform duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none": !size.active(), }} - style={{ height: open() ? `${height()}px` : "0px" }} + style={{ height: `${pane()}px` }} > - <div - class="absolute inset-x-0 top-0 flex flex-col border-t border-border-weak-base" - classList={{ - "translate-y-0 opacity-100": open(), - "translate-y-full opacity-0 pointer-events-none": !open(), - "transition-[transform,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[transform,opacity] motion-reduce:transition-none": - !size.active(), - }} - style={{ height: `${height()}px` }} - > - <div onPointerDown={() => size.start()}> - <ResizeHandle - direction="vertical" - size={height()} - min={100} - max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6} - collapseThreshold={50} - onResize={(next) => { - size.touch() - layout.terminal.resize(next) - }} - onCollapse={close} - /> - </div> - <Show - when={terminal.ready()} - fallback={ - <div class="flex flex-col h-full pointer-events-none"> - <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden"> - <For each={handoff()}> - {(title) => ( - <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40"> - {title} - </div> - )} - </For> - <div class="flex-1" /> - <div class="text-text-weak pr-2"> - {language.t("common.loading")} - {language.t("common.loading.ellipsis")} - </div> - </div> - <div class="flex-1 flex items-center justify-center text-text-weak"> - {language.t("terminal.loading")} - </div> - </div> - } - > - <DragDropProvider - onDragStart={handleTerminalDragStart} - onDragEnd={handleTerminalDragEnd} - onDragOver={handleTerminalDragOver} - collisionDetector={closestCenter} - > - <DragDropSensors /> - <ConstrainDragYAxis /> - <div class="flex flex-col h-full"> - <Tabs - variant="alt" - value={terminal.active()} - onChange={(id) => terminal.open(id)} - class="!h-auto !flex-none" - > - <Tabs.List class="h-10 border-b border-border-weaker-base"> - <SortableProvider ids={ids()}> - <For each={ids()}> - {(id) => ( - <Show when={byId().get(id)}> - {(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />} - </Show> - )} - </For> - </SortableProvider> - <div class="h-full flex items-center justify-center"> - <TooltipKeybind - title={language.t("command.terminal.new")} - keybind={command.keybind("terminal.new")} - class="flex items-center" - > - <IconButton - icon="plus-small" - variant="ghost" - iconSize="large" - onClick={terminal.new} - aria-label={language.t("command.terminal.new")} - /> - </TooltipKeybind> + <div class="hidden md:block" onPointerDown={() => size.start()}> + <ResizeHandle + direction="vertical" + size={pane()} + min={100} + max={max()} + collapseThreshold={50} + onResize={(next) => { + size.touch() + layout.terminal.resize(next) + }} + onCollapse={close} + /> + </div> + <Show + when={terminal.ready()} + fallback={ + <div class="flex flex-col h-full pointer-events-none"> + <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden"> + <For each={handoff()}> + {(title) => ( + <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40"> + {title} </div> - </Tabs.List> - </Tabs> - <div class="flex-1 min-h-0 relative"> - <Show when={terminal.active()} keyed> - {(id) => ( - <Show when={byId().get(id)}> - {(pty) => ( - <div id={`terminal-wrapper-${id}`} class="absolute inset-0"> - <Terminal - pty={pty()} - onConnect={() => terminal.trim(id)} - onCleanup={terminal.update} - onConnectError={() => terminal.clone(id)} - /> - </div> - )} - </Show> - )} - </Show> + )} + </For> + <div class="flex-1" /> + <div class="text-text-weak pr-2"> + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} </div> </div> - <DragOverlay> - <Show when={store.activeDraggable}> - {(draggedId) => ( - <Show when={byId().get(draggedId())}> - {(t) => ( - <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular"> - {terminalTabLabel({ - title: t().title, - titleNumber: t().titleNumber, - t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string, - })} + <div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div> + </div> + } + > + <DragDropProvider + onDragStart={handleTerminalDragStart} + onDragEnd={handleTerminalDragEnd} + onDragOver={handleTerminalDragOver} + collisionDetector={closestCenter} + > + <DragDropSensors /> + <ConstrainDragYAxis /> + <div class="flex flex-col h-full"> + <Tabs + variant="alt" + value={terminal.active()} + onChange={(id) => terminal.open(id)} + class="!h-auto !flex-none" + > + <Tabs.List class="h-10 border-b border-border-weaker-base"> + <SortableProvider ids={ids()}> + <For each={ids()}> + {(id) => ( + <Show when={byId().get(id)}> + {(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />} + </Show> + )} + </For> + </SortableProvider> + <div class="h-full flex items-center justify-center"> + <TooltipKeybind + title={language.t("command.terminal.new")} + keybind={command.keybind("terminal.new")} + class="flex items-center" + > + <IconButton + icon="plus-small" + variant="ghost" + iconSize="large" + onClick={terminal.new} + aria-label={language.t("command.terminal.new")} + /> + </TooltipKeybind> + </div> + </Tabs.List> + </Tabs> + <div class="flex-1 min-h-0 relative"> + <Show when={terminal.active()} keyed> + {(id) => ( + <Show when={byId().get(id)}> + {(pty) => ( + <div id={`terminal-wrapper-${id}`} class="absolute inset-0"> + <Terminal + pty={pty()} + autoFocus={opened()} + onConnect={() => terminal.trim(id)} + onCleanup={terminal.update} + onConnectError={() => terminal.clone(id)} + /> </div> )} </Show> )} </Show> - </DragOverlay> - </DragDropProvider> - </Show> - </div> + </div> + </div> + <DragOverlay> + <Show when={store.activeDraggable}> + {(draggedId) => ( + <Show when={byId().get(draggedId())}> + {(t) => ( + <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular"> + {terminalTabLabel({ + title: t().title, + titleNumber: t().titleNumber, + t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string, + })} + </div> + )} + </Show> + )} + </Show> + </DragOverlay> + </DragDropProvider> + </Show> </div> - </Show> + </div> ) } |
