mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
perf(runtime): gate terminal.list visual layouts and stop the false writable claim
visualLayouts is ~31% of a large terminal.list payload (44,208 B of 137,412 B on a live 134-terminal remote runtime) and has exactly one consumer: the human-readable CLI formatter. Gate it behind an includeVisualLayouts request param that defaults to included, so pre-flag clients are unaffected, and have every --json/internal caller opt out. Also drop the record-backed builder's writable, which was a verbatim copy of connected. terminal.show now states writability explicitly as exactly what terminal.send's PTY gate enforces.
This commit is contained in:
@@ -1693,7 +1693,8 @@ export default function SessionScreen() {
|
||||
|
||||
try {
|
||||
const response = await client.sendRequest('terminal.list', {
|
||||
worktree: `id:${worktreeId}`
|
||||
worktree: `id:${worktreeId}`,
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
if (response.ok) {
|
||||
const result = (response as RpcSuccess).result as { terminals: Terminal[] }
|
||||
|
||||
@@ -55,7 +55,9 @@ export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
|
||||
'terminal list': async ({ flags, client, cwd, json }) => {
|
||||
const result = await client.call<RuntimeTerminalListResult>('terminal.list', {
|
||||
worktree: await getOptionalWorktreeSelector(flags, 'worktree', cwd, client),
|
||||
limit: getOptionalPositiveIntegerFlag(flags, 'limit')
|
||||
limit: getOptionalPositiveIntegerFlag(flags, 'limit'),
|
||||
// Why: only the human formatter renders layouts; --json ships and discards them.
|
||||
includeVisualLayouts: !json
|
||||
})
|
||||
printResult(result, json, formatTerminalList)
|
||||
},
|
||||
|
||||
+19
-1
@@ -3702,10 +3702,28 @@ describe('orca cli worktree awareness', () => {
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.list', {
|
||||
worktree: 'id:repo::/tmp/repo/feature',
|
||||
limit: undefined
|
||||
limit: undefined,
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
})
|
||||
|
||||
it('requests visual layouts only for the human-readable terminal list', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
|
||||
okFixture('req_term', { terminals: [], totalCount: 0, truncated: false })
|
||||
)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(['terminal', 'list', '--worktree', 'active'], '/tmp/repo/feature/src')
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'terminal.list',
|
||||
expect.objectContaining({ includeVisualLayouts: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects implicit remote terminal create instead of resolving from client cwd', async () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
@@ -20759,6 +20759,51 @@ describe('OrcaRuntimeService', () => {
|
||||
).rejects.toThrow('terminal_topology_conflict')
|
||||
})
|
||||
|
||||
it('never claims writable for an orphaned listing entry, and keeps show aligned with the send gate', async () => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: { [TEST_WORKTREE_ID]: [] }
|
||||
})
|
||||
const runtime = new OrcaRuntimeService({ ...runtimeStore, flushOrThrow: vi.fn() } as never)
|
||||
const writes: [string, string][] = []
|
||||
runtime.setPtyController({
|
||||
write: (ptyId: string, data: string) => {
|
||||
writes.push([ptyId, data])
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => [
|
||||
{
|
||||
id: 'pty-orphan',
|
||||
incarnationId: 'inc-orphan',
|
||||
terminalHandle: 'term_orphan',
|
||||
title: 'shell',
|
||||
cwd: TEST_WORKTREE_PATH,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
wslDistro: null
|
||||
}
|
||||
]
|
||||
} as never)
|
||||
runtime.registerPty('pty-orphan', TEST_WORKTREE_ID)
|
||||
runtime.onPtySpawned('pty-orphan', 'inc-orphan', { awaitsRegistration: false })
|
||||
|
||||
const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)
|
||||
const entry = listed.terminals.find((terminal) => terminal.ptyId === 'pty-orphan')
|
||||
expect(entry).toMatchObject({ orphaned: true, connected: true })
|
||||
// The listing makes no writability claim it cannot back: `writable` would
|
||||
// only restate `connected` here.
|
||||
expect(entry?.writable).toBeUndefined()
|
||||
|
||||
// ...but the orphan is genuinely writable, so show and send must agree.
|
||||
const shown = await runtime.showTerminal(entry!.handle)
|
||||
expect(shown.writable).toBe(true)
|
||||
await expect(runtime.sendTerminal(entry!.handle, { text: 'hi' })).resolves.toMatchObject({
|
||||
accepted: true
|
||||
})
|
||||
expect(writes).toEqual([['pty-orphan', 'hi']])
|
||||
})
|
||||
|
||||
it('rejects connection mismatch and reused handles while allowing a WSL-owned orphan', async () => {
|
||||
const makeRuntime = (): OrcaRuntimeService => {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
|
||||
|
||||
@@ -14851,7 +14851,11 @@ export class OrcaRuntimeService {
|
||||
async listTerminals(
|
||||
worktreeSelector?: string,
|
||||
limit = DEFAULT_TERMINAL_LIST_LIMIT,
|
||||
opts: { handles?: readonly string[]; requireFreshPtyLiveness?: boolean } = {}
|
||||
opts: {
|
||||
handles?: readonly string[]
|
||||
requireFreshPtyLiveness?: boolean
|
||||
includeVisualLayouts?: boolean
|
||||
} = {}
|
||||
): Promise<RuntimeTerminalListResult> {
|
||||
if (!Number.isInteger(limit) || limit <= 0) {
|
||||
throw new Error('invalid_limit')
|
||||
@@ -14968,11 +14972,12 @@ export class OrcaRuntimeService {
|
||||
? terminals.filter((terminal) => requestedHandles.has(terminal.handle))
|
||||
: terminals
|
||||
const listedTerminals = matchingTerminals.slice(0, limit)
|
||||
const visualLayouts = this.buildTerminalVisualLayouts(
|
||||
listedTerminals,
|
||||
worktreesById,
|
||||
targetWorktreeId
|
||||
)
|
||||
// Why: undefined (pre-flag client) must still get layouts; only an explicit
|
||||
// `false` opts out.
|
||||
const visualLayouts =
|
||||
opts.includeVisualLayouts === false
|
||||
? []
|
||||
: this.buildTerminalVisualLayouts(listedTerminals, worktreesById, targetWorktreeId)
|
||||
|
||||
return {
|
||||
terminals: listedTerminals,
|
||||
@@ -15963,6 +15968,9 @@ export class OrcaRuntimeService {
|
||||
return {
|
||||
...summary,
|
||||
preview,
|
||||
// Why: mirrors terminal.send's PTY gate exactly (connected only —
|
||||
// orphaned handles still accept writes and adoption re-attaches them).
|
||||
writable: pty.pty.connected,
|
||||
tabId: pty.pty.tabId ?? pty.record.tabId,
|
||||
leafId: parsePaneKey(pty.pty.paneKey ?? '')?.leafId ?? pty.record.leafId,
|
||||
paneRuntimeId: -1,
|
||||
@@ -15985,6 +15993,7 @@ export class OrcaRuntimeService {
|
||||
return {
|
||||
...summary,
|
||||
preview,
|
||||
writable: leaf.writable,
|
||||
paneRuntimeId: leaf.paneRuntimeId,
|
||||
ptyId: leaf.ptyId,
|
||||
rendererGraphEpoch: this.rendererGraphEpoch
|
||||
@@ -30582,7 +30591,9 @@ export class OrcaRuntimeService {
|
||||
leafId: orphaned ? `pty:${pty.ptyId}` : pane.leafId,
|
||||
title: getLatestPtyTitle(pty),
|
||||
connected: pty.connected,
|
||||
writable: pty.connected,
|
||||
// Why: no `writable` — for a PTY record it would only restate `connected`,
|
||||
// which is exactly what terminal.send gates on. Callers needing the claim
|
||||
// use terminal.show.
|
||||
lastOutputAt: pty.lastOutputAt,
|
||||
preview: pty.preview
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export type CoordinatorRuntime = {
|
||||
worktreeSelector?: string,
|
||||
limit?: number
|
||||
): Promise<{
|
||||
terminals: { handle: string; worktreeId: string; connected: boolean; writable: boolean }[]
|
||||
terminals: { handle: string; worktreeId: string; connected: boolean; writable?: boolean }[]
|
||||
}>
|
||||
createTerminal(
|
||||
worktreeSelector?: string,
|
||||
@@ -501,7 +501,9 @@ export class Coordinator {
|
||||
t.handle !== this.opts.coordinatorHandle &&
|
||||
!busyHandles.has(t.handle) &&
|
||||
t.connected &&
|
||||
t.writable
|
||||
// Why: absent means the runtime makes no claim beyond `connected`
|
||||
// (record-backed entry); only an explicit `false` disqualifies.
|
||||
t.writable !== false
|
||||
)
|
||||
.map((t) => t.handle)
|
||||
} catch {
|
||||
|
||||
@@ -813,7 +813,10 @@ const TerminalListParams = z.object({
|
||||
.array(requiredString('Missing terminal handle').pipe(z.string().max(256)))
|
||||
.max(64)
|
||||
.optional(),
|
||||
requireFreshPtyLiveness: z.boolean().optional()
|
||||
requireFreshPtyLiveness: z.boolean().optional(),
|
||||
// Why: layouts are ~31% of a large listing and only the human CLI formatter
|
||||
// reads them. Absent means "include" so pre-flag clients keep rendering them.
|
||||
includeVisualLayouts: z.boolean().optional()
|
||||
})
|
||||
|
||||
const TerminalResolveActive = z.object({
|
||||
@@ -1108,7 +1111,8 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
|
||||
handler: async (params, { runtime }) =>
|
||||
runtime.listTerminals(params.worktree, params.limit, {
|
||||
handles: params.handles,
|
||||
requireFreshPtyLiveness: params.requireFreshPtyLiveness
|
||||
requireFreshPtyLiveness: params.requireFreshPtyLiveness,
|
||||
includeVisualLayouts: params.includeVisualLayouts
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
|
||||
@@ -3999,6 +3999,32 @@ describe('OrcaRuntimeRpcServer', () => {
|
||||
}
|
||||
])
|
||||
|
||||
// Pins the opt-out half of the compat contract: the request above omits
|
||||
// the flag and still gets layouts; only an explicit `false` drops them.
|
||||
const optedOutResponse = await sendRequest(metadata!.transports[0]!.endpoint, {
|
||||
id: 'req_list_layout_opt_out',
|
||||
authToken: metadata!.authToken,
|
||||
method: 'terminal.list',
|
||||
params: { worktree: `id:${worktreeId}`, includeVisualLayouts: false }
|
||||
})
|
||||
const optedOut = optedOutResponse.result as {
|
||||
visualLayouts?: unknown[]
|
||||
terminals: unknown[]
|
||||
}
|
||||
expect(optedOutResponse).toMatchObject({ id: 'req_list_layout_opt_out', ok: true })
|
||||
expect(optedOut.visualLayouts).toBeUndefined()
|
||||
expect(optedOut.terminals).toHaveLength(result.terminals.length)
|
||||
|
||||
const explicitIncludeResponse = await sendRequest(metadata!.transports[0]!.endpoint, {
|
||||
id: 'req_list_layout_opt_in',
|
||||
authToken: metadata!.authToken,
|
||||
method: 'terminal.list',
|
||||
params: { worktree: `id:${worktreeId}`, includeVisualLayouts: true }
|
||||
})
|
||||
expect(
|
||||
(explicitIncludeResponse.result as { visualLayouts?: unknown[] }).visualLayouts
|
||||
).toHaveLength(1)
|
||||
|
||||
const resolvePaneResponse = await sendRequest(metadata!.transports[0]!.endpoint, {
|
||||
id: 'req_resolve_pane',
|
||||
authToken: metadata!.authToken,
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
// Measures the real `terminal.list` wire payload for a large listing, so the
|
||||
// visualLayouts opt-out keeps paying for itself. Sized against a live
|
||||
// 134-terminal remote runtime (137,412 B on the wire, visualLayouts 44,208 B).
|
||||
const TERMINAL_COUNT = 134
|
||||
const PANES_PER_TAB = 2
|
||||
const TAB_COUNT = TERMINAL_COUNT / PANES_PER_TAB
|
||||
const REPO_ID = 'repo-7f3a91c2e4b85d60'
|
||||
const WORKTREE_PATH = '/Users/dev/orca/workspaces/orca/perf-terminal-list-diet'
|
||||
const WORKTREE_ID = `${REPO_ID}::${WORKTREE_PATH}`
|
||||
|
||||
const uuid = (n: number): string => {
|
||||
const hex = n.toString(16).padStart(12, '0')
|
||||
return `11111111-1111-4111-8111-${hex}`
|
||||
}
|
||||
|
||||
const leafIdFor = (index: number): string => uuid(index)
|
||||
const tabIdFor = (tab: number): string => `tab-${uuid(1_000 + tab)}`
|
||||
const ptyIdFor = (index: number): string => `pty-${uuid(2_000 + index)}`
|
||||
const groupIdFor = (tab: number): string => `group-${uuid(3_000 + (tab % 2))}`
|
||||
const titleFor = (index: number): string => `claude — orca/perf-terminal-list-diet #${index}`
|
||||
|
||||
const makeStore = () => ({
|
||||
getRepo: (id: string) => makeStore().getRepos()[0] ?? (id as never),
|
||||
getRepos: () => [
|
||||
{ id: REPO_ID, path: '/tmp/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1 }
|
||||
],
|
||||
addRepo: () => {},
|
||||
updateRepo: (id: string) => makeStore().getRepo(id) as never,
|
||||
getAllWorktreeMeta: () => ({
|
||||
[WORKTREE_ID]: {
|
||||
displayName: 'foo',
|
||||
comment: '',
|
||||
linkedIssue: 123,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
}
|
||||
}),
|
||||
getWorktreeMeta: (worktreeId: string) =>
|
||||
(makeStore().getAllWorktreeMeta() as Record<string, unknown>)[worktreeId] as never,
|
||||
setWorktreeMeta: () => ({}) as never,
|
||||
removeWorktreeMeta: () => {},
|
||||
getSettings: () => ({
|
||||
workspaceDir: '/tmp/workspaces',
|
||||
nestWorkspaces: false,
|
||||
branchPrefix: 'none',
|
||||
branchPrefixCustom: ''
|
||||
})
|
||||
})
|
||||
|
||||
function buildLoadedRuntime(): OrcaRuntimeService {
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
const tabs = []
|
||||
const leaves = []
|
||||
const mobileTabs = []
|
||||
|
||||
for (let tab = 0; tab < TAB_COUNT; tab += 1) {
|
||||
const tabId = tabIdFor(tab)
|
||||
const first = leafIdFor(tab * PANES_PER_TAB)
|
||||
const second = leafIdFor(tab * PANES_PER_TAB + 1)
|
||||
const root = {
|
||||
type: 'split' as const,
|
||||
direction: 'vertical' as const,
|
||||
first: { type: 'leaf' as const, leafId: first },
|
||||
second: { type: 'leaf' as const, leafId: second }
|
||||
}
|
||||
tabs.push({
|
||||
tabId,
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: `Tab ${tab}`,
|
||||
activeLeafId: second,
|
||||
layout: root
|
||||
})
|
||||
for (let pane = 0; pane < PANES_PER_TAB; pane += 1) {
|
||||
const index = tab * PANES_PER_TAB + pane
|
||||
const leafId = leafIdFor(index)
|
||||
leaves.push({
|
||||
tabId,
|
||||
worktreeId: WORKTREE_ID,
|
||||
leafId,
|
||||
paneRuntimeId: pane + 1,
|
||||
ptyId: ptyIdFor(index),
|
||||
title: titleFor(index)
|
||||
})
|
||||
mobileTabs.push({
|
||||
type: 'terminal' as const,
|
||||
id: `${tabId}::${leafId}`,
|
||||
title: titleFor(index),
|
||||
parentTabId: tabId,
|
||||
leafId,
|
||||
ptyId: ptyIdFor(index),
|
||||
parentLayout: {
|
||||
root,
|
||||
activeLeafId: second,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: {
|
||||
[first]: ptyIdFor(tab * PANES_PER_TAB),
|
||||
[second]: ptyIdFor(tab * PANES_PER_TAB + 1)
|
||||
}
|
||||
},
|
||||
isActive: index === 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const groupIds = [groupIdFor(0), groupIdFor(1)]
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs,
|
||||
leaves,
|
||||
mobileSessionTabs: [
|
||||
{
|
||||
worktree: WORKTREE_ID,
|
||||
publicationEpoch: 'test',
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: groupIds[0],
|
||||
activeTabId: mobileTabs[0]!.id,
|
||||
activeTabType: 'terminal',
|
||||
tabGroups: groupIds.map((id, groupIndex) => ({
|
||||
id,
|
||||
activeTabId: tabIdFor(groupIndex),
|
||||
tabOrder: tabs
|
||||
.filter((_tab, tabIndex) => tabIndex % 2 === groupIndex)
|
||||
.map((tab) => tab.tabId)
|
||||
})),
|
||||
tabGroupLayout: {
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', groupId: groupIds[0] },
|
||||
second: { type: 'leaf', groupId: groupIds[1] }
|
||||
},
|
||||
tabs: mobileTabs
|
||||
}
|
||||
]
|
||||
} as never)
|
||||
|
||||
// Realistic previews: the live sample carried ~27 B of tail text per terminal.
|
||||
for (let index = 0; index < TERMINAL_COUNT; index += 1) {
|
||||
const ptyId = ptyIdFor(index)
|
||||
runtime.registerPty(ptyId, WORKTREE_ID)
|
||||
runtime.onPtySpawned(ptyId, `inc-${uuid(4_000 + index)}`, { awaitsRegistration: false })
|
||||
runtime.onPtyData(ptyId, `esc to interrupt · ${index}\n`, 1)
|
||||
}
|
||||
return runtime
|
||||
}
|
||||
|
||||
describe('terminal.list payload size', () => {
|
||||
it('drops ~30% of the wire payload when a caller opts out of visualLayouts', async () => {
|
||||
const runtime = buildLoadedRuntime()
|
||||
|
||||
const withLayouts = await runtime.listTerminals(`id:${WORKTREE_ID}`, 10_000)
|
||||
const withoutLayouts = await runtime.listTerminals(`id:${WORKTREE_ID}`, 10_000, {
|
||||
includeVisualLayouts: false
|
||||
})
|
||||
|
||||
expect(withLayouts.terminals).toHaveLength(TERMINAL_COUNT)
|
||||
expect(withoutLayouts.terminals).toHaveLength(TERMINAL_COUNT)
|
||||
expect(withLayouts.visualLayouts).toHaveLength(1)
|
||||
expect(withoutLayouts.visualLayouts).toBeUndefined()
|
||||
|
||||
const beforeBytes = Buffer.byteLength(JSON.stringify(withLayouts), 'utf8')
|
||||
const afterBytes = Buffer.byteLength(JSON.stringify(withoutLayouts), 'utf8')
|
||||
const layoutBytes = Buffer.byteLength(JSON.stringify(withLayouts.visualLayouts), 'utf8')
|
||||
const reduction = (beforeBytes - afterBytes) / beforeBytes
|
||||
|
||||
console.log(
|
||||
`terminal.list ${TERMINAL_COUNT} terminals: ${beforeBytes} B -> ${afterBytes} B ` +
|
||||
`(-${(reduction * 100).toFixed(1)}%); visualLayouts alone ${layoutBytes} B`
|
||||
)
|
||||
|
||||
expect(beforeBytes).toBeGreaterThan(100_000)
|
||||
expect(reduction).toBeGreaterThan(0.25)
|
||||
})
|
||||
})
|
||||
@@ -188,7 +188,9 @@ async function dispatchRemoteCli(
|
||||
case 'terminal list':
|
||||
return await call(dispatcher, 'terminal.list', {
|
||||
worktree: optionalRemoteCliString(parsed.flags, 'worktree'),
|
||||
limit: optionalRemoteCliNumber(parsed.flags, 'limit')
|
||||
limit: optionalRemoteCliNumber(parsed.flags, 'limit'),
|
||||
// Why: only the human formatter renders layouts; --json ships and discards them.
|
||||
includeVisualLayouts: !parsed.flags.has('json')
|
||||
})
|
||||
case 'orchestration send': {
|
||||
const type = optionalRemoteCliString(parsed.flags, 'type')
|
||||
|
||||
@@ -347,7 +347,7 @@ describe('active agent note send', () => {
|
||||
expect(testState.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'local' },
|
||||
'terminal.list',
|
||||
{ worktree: 'id:wt-1', limit: 200 },
|
||||
{ worktree: 'id:wt-1', limit: 200, includeVisualLayouts: false },
|
||||
{ timeoutMs: 15000 }
|
||||
)
|
||||
expect(testState.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
|
||||
@@ -167,7 +167,11 @@ export async function findActiveRuntimeTerminal(
|
||||
runtimeTarget,
|
||||
'terminal.list',
|
||||
// Why: worktree ids can look like branch names or paths; keep the lookup unambiguous.
|
||||
{ worktree: toRuntimeWorktreeSelector(worktreeId), limit: ACTIVE_AGENT_TERMINAL_LIST_LIMIT },
|
||||
{
|
||||
worktree: toRuntimeWorktreeSelector(worktreeId),
|
||||
limit: ACTIVE_AGENT_TERMINAL_LIST_LIMIT,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
{ timeoutMs }
|
||||
)
|
||||
return (
|
||||
|
||||
@@ -116,7 +116,8 @@ async function collectRuntimePtyLiveness(state: AppState): Promise<RuntimePtyLiv
|
||||
{
|
||||
worktree: toRuntimeWorktreeSelector(worktreeId),
|
||||
limit: 10_000,
|
||||
requireFreshPtyLiveness: true
|
||||
requireFreshPtyLiveness: true,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
{ timeoutMs: 10_000 }
|
||||
)
|
||||
|
||||
@@ -108,7 +108,8 @@ async function waitForLegacySleepConvergence(
|
||||
{
|
||||
worktree: worktreeSelector,
|
||||
limit: 10_000,
|
||||
requireFreshPtyLiveness: true
|
||||
requireFreshPtyLiveness: true,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
{ timeoutMs: listTimeoutMs }
|
||||
)
|
||||
|
||||
@@ -91,7 +91,8 @@ async function recoverTerminalOrphans(
|
||||
params: {
|
||||
worktree: toRuntimeWorktreeSelector(snapshot.worktree),
|
||||
handles: [...candidateHandles],
|
||||
requireFreshPtyLiveness: true
|
||||
requireFreshPtyLiveness: true,
|
||||
includeVisualLayouts: false
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
|
||||
@@ -448,7 +448,9 @@ export type RuntimeTerminalSummary = {
|
||||
leafId: string
|
||||
title: string | null
|
||||
connected: boolean
|
||||
writable: boolean
|
||||
// Why: only graph-backed entries can claim writability independently of
|
||||
// `connected`; record-backed (PTY) entries omit it rather than restate it.
|
||||
writable?: boolean
|
||||
lastOutputAt: number | null
|
||||
preview: string
|
||||
}
|
||||
@@ -575,6 +577,9 @@ export type RuntimeWorktreeTerminalSleepResult = {
|
||||
)
|
||||
|
||||
export type RuntimeTerminalShow = RuntimeTerminalSummary & {
|
||||
// Why: `terminal show` always states writability — exactly what terminal.send's
|
||||
// gate enforces, so the two can never disagree.
|
||||
writable: boolean
|
||||
paneRuntimeId: number
|
||||
ptyId: string | null
|
||||
rendererGraphEpoch: number
|
||||
|
||||
Reference in New Issue
Block a user