diff options
| author | Halil Tezcan KARABULUT <[email protected]> | 2026-01-21 15:49:46 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-21 06:49:46 -0600 |
| commit | 87d91c29e23d136436342542f83a20bfc4a3d0bf (patch) | |
| tree | 390b43083bec0c662d1df2d490a7b46bc9102680 /packages/app/src/components | |
| parent | 259b2a3c2dbb785dd46c83e243f20436a05e3021 (diff) | |
| download | opencode-87d91c29e23d136436342542f83a20bfc4a3d0bf.tar.gz opencode-87d91c29e23d136436342542f83a20bfc4a3d0bf.zip | |
fix(app): terminal improvements - focus, rename, error state, CSP (#9700)
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/session/session-sortable-terminal-tab.tsx | 136 | ||||
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 12 |
2 files changed, 139 insertions, 9 deletions
diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx index 0e387b9fb..63efa54a8 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -1,14 +1,22 @@ import type { JSX } from "solid-js" +import { createSignal, Show } from "solid-js" import { createSortable } from "@thisbeyond/solid-dnd" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLanguage } from "@/context/language" -export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element { +export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => void }): JSX.Element { const terminal = useTerminal() const language = useLanguage() const sortable = createSortable(props.terminal.id) + const [editing, setEditing] = createSignal(false) + const [title, setTitle] = createSignal(props.terminal.title) + const [menuOpen, setMenuOpen] = createSignal(false) + const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 }) + const [blurEnabled, setBlurEnabled] = createSignal(false) const label = () => { language.locale() @@ -19,20 +27,138 @@ export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element if (props.terminal.title) return props.terminal.title return language.t("terminal.title") } + + const close = () => { + const count = terminal.all().length + terminal.close(props.terminal.id) + if (count === 1) { + props.onClose?.() + } + } + + const focus = () => { + if (editing()) return + + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + const wrapper = document.getElementById(`terminal-wrapper-${props.terminal.id}`) + const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement + if (!element) return + + const textarea = element.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + textarea.focus() + return + } + element.focus() + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true })) + } + + const edit = (e?: Event) => { + if (e) { + e.stopPropagation() + e.preventDefault() + } + + setBlurEnabled(false) + setTitle(props.terminal.title) + setEditing(true) + setTimeout(() => { + const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement + if (!input) return + input.focus() + input.select() + setTimeout(() => setBlurEnabled(true), 100) + }, 10) + } + + const save = () => { + if (!blurEnabled()) return + + const value = title().trim() + if (value && value !== props.terminal.title) { + terminal.update({ id: props.terminal.id, title: value }) + } + setEditing(false) + } + + const keydown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault() + save() + return + } + if (e.key === "Escape") { + e.preventDefault() + setEditing(false) + } + } + + const menu = (e: MouseEvent) => { + e.preventDefault() + setMenuPosition({ x: e.clientX, y: e.clientY }) + setMenuOpen(true) + } + return ( // @ts-ignore <div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}> <div class="relative h-full"> <Tabs.Trigger value={props.terminal.id} + onClick={focus} + onMouseDown={(e) => e.preventDefault()} + onContextMenu={menu} closeButton={ - terminal.all().length > 1 && ( - <IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} /> - ) + <IconButton + icon="close" + variant="ghost" + onClick={(e) => { + e.stopPropagation() + close() + }} + /> } > - {label()} + <span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}> + {label()} + </span> </Tabs.Trigger> + <Show when={editing()}> + <div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto"> + <input + id={`terminal-title-input-${props.terminal.id}`} + type="text" + value={title()} + onInput={(e) => setTitle(e.currentTarget.value)} + onBlur={save} + onKeyDown={keydown} + onMouseDown={(e) => e.stopPropagation()} + class="bg-transparent border-none outline-none text-sm min-w-0 flex-1" + /> + </div> + </Show> + <DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}> + <DropdownMenu.Portal> + <DropdownMenu.Content + style={{ + position: "fixed", + left: `${menuPosition().x}px`, + top: `${menuPosition().y}px`, + }} + > + <DropdownMenu.Item onSelect={edit}> + <Icon name="edit" class="w-4 h-4 mr-2" /> + Rename + </DropdownMenu.Item> + <DropdownMenu.Item onSelect={close}> + <Icon name="close" class="w-4 h-4 mr-2" /> + Close + </DropdownMenu.Item> + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenu> </div> </div> ) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index f19366b8a..1ab171898 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -241,7 +241,6 @@ export const Terminal = (props: TerminalProps) => { // console.log("Scroll position:", ydisp) // }) socket.addEventListener("open", () => { - console.log("WebSocket connected") sdk.client.pty .update({ ptyID: local.pty.id, @@ -257,10 +256,14 @@ export const Terminal = (props: TerminalProps) => { }) socket.addEventListener("error", (error) => { console.error("WebSocket error:", error) - props.onConnectError?.(error) + local.onConnectError?.(error) }) - socket.addEventListener("close", () => { - console.log("WebSocket disconnected") + socket.addEventListener("close", (event) => { + // Normal closure (code 1000) means PTY process exited - server event handles cleanup + // For other codes (network issues, server restart), trigger error handler + if (event.code !== 1000) { + local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`)) + } }) }) @@ -293,6 +296,7 @@ export const Terminal = (props: TerminalProps) => { ref={container} data-component="terminal" data-prevent-autofocus + tabIndex={-1} style={{ "background-color": terminalColors().background }} classList={{ ...(local.classList ?? {}), |
