fix(app): terminal improvements - focus, rename, error state, CSP (#9700)
This commit is contained in:
committed by
GitHub
parent
259b2a3c2d
commit
87d91c29e2
@@ -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(
|
||||
@@ -401,6 +411,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,
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user