Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/e2e/utils/mock-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export interface MockServerConfig {
onInboxChange?: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" | "queue" }) => void
onBackground?: (input: { sessionID: string }) => void
// Serves the session-scoped persistent PTY routes and their connect WebSockets (`e2e/utils/persistent-pty.ts`).
// Without it, those routes answer 503 like a server whose opencode-pty daemon cannot run.
// Without it, the server has a working daemon and no terminals; `capability: false` makes the routes answer 503.
persistentPty?: MockPersistentPtyConfig
// Answers 500 InvalidDirectory when a request names a directory this server does not own.
strictDirectory?: boolean
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/runtime/plugin/side-region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,9 @@ export function PaneTabs(props: {
selectionEvent.target instanceof Element &&
selectionEvent.target.closest('[role="tab"]')
) {
props.region.newTabs(props.pane.id).forEach((entry) => props.region.dismissNewTab(entry.id))
// Select first: dismissing the only New tab with nothing selected lets Kobalte pick the first tab meanwhile.
props.region.select(value)
props.region.newTabs(props.pane.id).forEach((entry) => props.region.dismissNewTab(entry.id))
}
}}
>
Expand Down
6 changes: 5 additions & 1 deletion packages/gui-plugins/src/terminal/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ export function createTerminalModel(input: {

if (supported) refreshServer(server)
})
.catch((error) => console.error("Failed to read server terminal support", error))
.catch((error) => {
// Like the TUI: unknown support offers no terminals until the next connection checks again.
setState("capabilities", server.id, false)
console.error("Failed to read server terminal support", error)
})

// A restarted server replaces its data under the same ref, so reads that track `server.data` subscribe again.
const watch = (server: ServerRef) => {
Expand Down
9 changes: 6 additions & 3 deletions packages/gui-plugins/src/terminal/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,18 @@ export default function TerminalPanel(props: {

return opened() && id && terminal().focusRequested(id) ? { id } : undefined
},
(request) =>
requestAnimationFrame(() => {
(request) => {
const focusFrame = requestAnimationFrame(() => {
if (!opened() || active() !== request.id || !terminal().focusRequested(request.id)) return

focusTerminalById(request.id)
const input = document.getElementById(`terminal-wrapper-${request.id}`)?.querySelector("textarea")

if (input === document.activeElement) terminal().consumeFocus(request.id)
}),
})

onCleanup(() => cancelAnimationFrame(focusFrame))
},
)

// Terminals whose tabs the user moved to another pane render there, so this pane lets their screens go.
Expand Down
15 changes: 13 additions & 2 deletions packages/gui-plugins/src/terminal/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export function TerminalTab(props: {
}

// Ending the edit unmounts the focused field, whose blur would save again, or save a cancelled title.
// The field renders inside the strip's tab trigger, so it keeps its keys, pointer and focus from the trigger: the
// tab list returns focus that enters it from outside to the trigger.
const isolate = (event: Event) => event.stopPropagation()

const save = () => {
if (!store.editing || !store.blurEnabled) return

Expand Down Expand Up @@ -141,8 +145,15 @@ export function TerminalTab(props: {
value={store.title}
onInput={(e) => setStore("title", e.currentTarget.value)}
onBlur={save}
onKeyDown={keydown}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
isolate(e)
keydown(e)
}}
onKeyUp={isolate}
onFocusIn={isolate}
onPointerDown={isolate}
onMouseDown={isolate}
onClick={isolate}
class="bg-transparent border-none outline-none min-w-0 flex-1 p-0 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:440] [font-variation-settings:'slnt'_0] [font-variant-numeric:tabular-nums]"
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/gui-plugins/src/terminal/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ export const Terminal = (props: TerminalProps) => {
// A tab's rename field keeps focus, before and after a tick; it may take it before the terminal mounts. Other
// fields, such as the command palette that created the terminal, give focus to it.
const focus = () => {
if (disposed) return

const current = document.activeElement

if (current instanceof HTMLInputElement && current.closest('[role="tablist"]')) return
Expand Down
Loading