diff options
| author | Adam <[email protected]> | 2026-03-10 07:33:54 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-10 13:00:14 -0500 |
| commit | 85afaaa13d693f400d8ec8e257fec086a58b68c1 (patch) | |
| tree | 8a94e8046ac5f76d08b36c2bdbdbb8f5e33ce5ec /packages/app/src | |
| parent | 490615169e6717daab641fed827c21b11ef84a85 (diff) | |
| download | opencode-85afaaa13d693f400d8ec8e257fec086a58b68c1.tar.gz opencode-85afaaa13d693f400d8ec8e257fec086a58b68c1.zip | |
fix(app): terminal focus issues and jank
Diffstat (limited to 'packages/app/src')
| -rw-r--r-- | packages/app/src/pages/session/terminal-panel.tsx | 70 |
1 files changed, 49 insertions, 21 deletions
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 8fd652e90..19a656b53 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,4 +1,4 @@ -import { For, Show, createEffect, createMemo, on } from "solid-js" +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" @@ -17,7 +17,7 @@ import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useTerminal, type LocalPTY } from "@/context/terminal" import { terminalTabLabel } from "@/pages/session/terminal-label" -import { createPresence, createSizing, focusTerminalById } from "@/pages/session/helpers" +import { createSizing, focusTerminalById } from "@/pages/session/helpers" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" export function TerminalPanel() { @@ -33,7 +33,6 @@ export function TerminalPanel() { const opened = createMemo(() => view().terminal.opened()) const open = createMemo(() => isDesktop() && opened()) - const panel = createPresence(open) const size = createSizing() const height = createMemo(() => layout.terminal.height()) const close = () => view().terminal.close() @@ -66,21 +65,42 @@ export function TerminalPanel() { ), ) + const focus = (id: string) => { + focusTerminalById(id) + + const frame = requestAnimationFrame(() => { + if (!open()) return + if (terminal.active() !== id) return + focusTerminalById(id) + }) + + const timers = [120, 240].map((ms) => + window.setTimeout(() => { + if (!open()) return + if (terminal.active() !== id) return + focusTerminalById(id) + }, ms), + ) + + return () => { + cancelAnimationFrame(frame) + for (const timer of timers) clearTimeout(timer) + } + } + createEffect( on( - () => terminal.active(), - (activeId) => { - if (!activeId || !panel.open()) return - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur() - } - setTimeout(() => focusTerminalById(activeId), 0) + () => [open(), terminal.active()] as const, + ([next, id]) => { + if (!next || !id) return + const stop = focus(id) + onCleanup(stop) }, ), ) createEffect(() => { - if (panel.open()) return + if (open()) return const active = document.activeElement if (!(active instanceof HTMLElement)) return if (!root?.contains(active)) return @@ -138,30 +158,38 @@ export function TerminalPanel() { const activeId = terminal.active() if (!activeId) return - setTimeout(() => { + requestAnimationFrame(() => { + if (terminal.active() !== activeId) return focusTerminalById(activeId) - }, 0) + }) } return ( - <Show when={panel.show()}> + <Show when={isDesktop()}> <div ref={root} id="terminal-panel" role="region" aria-label={language.t("terminal.title")} - aria-hidden={!panel.open()} - inert={!panel.open()} + aria-hidden={!open()} + inert={!open()} class="relative w-full shrink-0 overflow-hidden" classList={{ - "opacity-100": panel.open(), - "opacity-0 pointer-events-none": !panel.open(), - "transition-[height,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[height] motion-reduce:transition-none": + "transition-[height] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[height] motion-reduce:transition-none": !size.active(), }} - style={{ height: panel.open() ? `${height()}px` : "0px" }} + style={{ height: open() ? `${height()}px` : "0px" }} > - <div class="size-full flex flex-col border-t border-border-weak-base"> + <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" |
