diff options
Diffstat (limited to 'packages/app/src/pages')
| -rw-r--r-- | packages/app/src/pages/session.tsx | 200 |
1 files changed, 164 insertions, 36 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 562176c1b..a75a0f0c1 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,16 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" +import { + For, + Index, + onCleanup, + onMount, + Show, + Match, + Switch, + createMemo, + createEffect, + on, + createSignal, +} from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -350,14 +362,7 @@ export default function Page() { const current = activeMessage() const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1 - - let targetIndex: number - if (currentIndex === -1) { - targetIndex = offset > 0 ? 0 : msgs.length - 1 - } else { - targetIndex = currentIndex + offset - } - + const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset if (targetIndex < 0 || targetIndex >= msgs.length) return scrollToMessage(msgs[targetIndex], "auto") @@ -381,11 +386,16 @@ export default function Page() { sync.session.sync(params.id) }) + const [autoCreated, setAutoCreated] = createSignal(false) + createEffect(() => { - if (!view().terminal.opened()) return - if (!terminal.ready()) return - if (terminal.all().length !== 0) return + if (!view().terminal.opened()) { + setAutoCreated(false) + return + } + if (!terminal.ready() || terminal.all().length !== 0 || autoCreated()) return terminal.new() + setAutoCreated(true) }) createEffect( @@ -403,6 +413,32 @@ export default function Page() { createEffect( on( + () => terminal.active(), + (activeId) => { + if (!activeId || !view().terminal.opened()) return + // Immediately remove focus + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + const wrapper = document.getElementById(`terminal-wrapper-${activeId}`) + const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement + if (!element) return + + // Find and focus the ghostty textarea (the actual input element) + const textarea = element.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + textarea.focus() + return + } + // Fallback: focus container and dispatch pointer event + element.focus() + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true })) + }, + ), + ) + + createEffect( + on( () => visibleUserMessages().at(-1)?.id, (lastId, prevLastId) => { if (lastId && prevLastId && lastId > prevLastId) { @@ -753,6 +789,9 @@ export default function Page() { return } + // Don't autofocus chat if terminal panel is open + if (view().terminal.opened()) return + if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { inputRef?.focus() } @@ -800,6 +839,23 @@ export default function Page() { const handleTerminalDragEnd = () => { setStore("activeTerminalDraggable", undefined) + const activeId = terminal.active() + if (!activeId) return + setTimeout(() => { + const wrapper = document.getElementById(`terminal-wrapper-${activeId}`) + const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement + if (!element) return + + // Find and focus the ghostty textarea (the actual input element) + const textarea = element.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + textarea.focus() + return + } + // Fallback: focus container and dispatch pointer event + element.focus() + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true })) + }, 0) } const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context")) @@ -1855,7 +1911,7 @@ export default function Page() { <Show when={isDesktop() && view().terminal.opened()}> <div - class="relative w-full flex-col shrink-0 border-t border-border-weak-base" + class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base" style={{ height: `${layout.terminal.height()}px` }} > <ResizeHandle @@ -1896,29 +1952,101 @@ export default function Page() { > <DragDropSensors /> <ConstrainDragYAxis /> - <Tabs variant="alt" value={terminal.active()} onChange={terminal.open}> - <Tabs.List class="h-10"> - <SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}> - <For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</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} /> - </TooltipKeybind> - </div> - </Tabs.List> - <For each={terminal.all()}> - {(pty) => ( - <Tabs.Content value={pty.id}> - <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} /> - </Tabs.Content> - )} - </For> - </Tabs> + <div class="flex flex-col h-full"> + <Tabs + variant="alt" + value={terminal.active()} + onChange={(id) => { + // Only switch tabs if not in the middle of starting edit mode + terminal.open(id) + }} + class="!h-auto !flex-none" + > + <Tabs.List class="h-10"> + <SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}> + <For each={terminal.all()}> + {(pty) => ( + <SortableTerminalTab + terminal={pty} + onClose={() => { + view().terminal.close() + setAutoCreated(false) + }} + /> + )} + </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} /> + </TooltipKeybind> + </div> + </Tabs.List> + </Tabs> + <div class="flex-1 min-h-0 relative"> + <For each={terminal.all()}> + {(pty) => { + const [dismissed, setDismissed] = createSignal(false) + return ( + <div + id={`terminal-wrapper-${pty.id}`} + class="absolute inset-0" + style={{ + display: terminal.active() === pty.id ? "block" : "none", + }} + > + <Terminal + pty={pty} + onCleanup={(data) => terminal.update({ ...data, id: pty.id })} + onConnectError={() => { + terminal.update({ id: pty.id, error: true }) + }} + /> + <Show when={pty.error && !dismissed()}> + <div + class="absolute inset-0 flex flex-col items-center justify-center gap-3" + style={{ "background-color": "rgba(0, 0, 0, 0.6)" }} + > + <Icon + name="circle-ban-sign" + class="w-8 h-8" + style={{ color: "rgba(239, 68, 68, 0.8)" }} + /> + <div class="text-center" style={{ color: "rgba(255, 255, 255, 0.7)" }}> + <div class="text-14-semibold mb-1">Connection Lost</div> + <div class="text-12-regular" style={{ color: "rgba(255, 255, 255, 0.5)" }}> + The terminal connection was interrupted. This can happen when the server restarts. + </div> + </div> + <button + class="mt-2 px-3 py-1.5 text-12-medium rounded-lg transition-colors" + style={{ + "background-color": "rgba(255, 255, 255, 0.1)", + color: "rgba(255, 255, 255, 0.7)", + border: "1px solid rgba(255, 255, 255, 0.2)", + }} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.15)") + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)") + } + onClick={() => setDismissed(true)} + > + Dismiss + </button> + </div> + </Show> + </div> + ) + }} + </For> + </div> + </div> <DragOverlay> <Show when={store.activeTerminalDraggable}> {(draggedId) => { |
