mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
fix(agent-dashboard): admit main renderer to terminal-preview IPC for in-window dialog
The terminalPreview:* handlers gated every channel to the pop-out
renderer, so the in-window overlay's terminal dialog (running in the
main renderer) got { snapshot: null } from connect and falsely showed
"No live terminal — this agent's pane has closed." for live agents.
Accept the trusted UI renderer too — it already has full PTY access
through the regular terminal channels, so this adds no reach.
This commit is contained in:
@@ -15,6 +15,9 @@ vi.mock('electron', () => ({ ipcMain: ipcMainMock }))
|
||||
vi.mock('../window/dashboard-popout-window', () => ({
|
||||
isDashboardPopoutRenderer: () => true
|
||||
}))
|
||||
vi.mock('./ui', () => ({
|
||||
isTrustedUIRenderer: () => false
|
||||
}))
|
||||
|
||||
import {
|
||||
registerTerminalPreviewHandlers,
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { handlers, ipcMainMock, isDashboardPopoutRendererMock } = vi.hoisted(() => {
|
||||
const map = new Map<string, (...args: unknown[]) => unknown>()
|
||||
return {
|
||||
handlers: map,
|
||||
ipcMainMock: {
|
||||
removeHandler: vi.fn(),
|
||||
handle: (channel: string, fn: (...args: unknown[]) => unknown) => map.set(channel, fn)
|
||||
},
|
||||
isDashboardPopoutRendererMock: vi.fn(() => true)
|
||||
}
|
||||
})
|
||||
const { handlers, ipcMainMock, isDashboardPopoutRendererMock, isTrustedUIRendererMock } =
|
||||
vi.hoisted(() => {
|
||||
const map = new Map<string, (...args: unknown[]) => unknown>()
|
||||
return {
|
||||
handlers: map,
|
||||
ipcMainMock: {
|
||||
removeHandler: vi.fn(),
|
||||
handle: (channel: string, fn: (...args: unknown[]) => unknown) => map.set(channel, fn)
|
||||
},
|
||||
isDashboardPopoutRendererMock: vi.fn(() => true),
|
||||
isTrustedUIRendererMock: vi.fn(() => false)
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('electron', () => ({ ipcMain: ipcMainMock }))
|
||||
vi.mock('../window/dashboard-popout-window', () => ({
|
||||
isDashboardPopoutRenderer: isDashboardPopoutRendererMock
|
||||
}))
|
||||
vi.mock('./ui', () => ({
|
||||
isTrustedUIRenderer: isTrustedUIRendererMock
|
||||
}))
|
||||
|
||||
import { registerTerminalPreviewHandlers } from './terminal-preview'
|
||||
|
||||
@@ -82,6 +87,7 @@ describe('registerTerminalPreviewHandlers', () => {
|
||||
beforeEach(() => {
|
||||
handlers.clear()
|
||||
isDashboardPopoutRendererMock.mockReturnValue(true)
|
||||
isTrustedUIRendererMock.mockReturnValue(false)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -281,6 +287,27 @@ describe('registerTerminalPreviewHandlers', () => {
|
||||
expect(runtime.writeTerminalPreviewInput).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// The in-window dashboard overlay hosts the preview dialog from the main
|
||||
// renderer, which is trusted but is not the popout window.
|
||||
it('admits the trusted main renderer when it is not the popout', async () => {
|
||||
const runtime = makeRuntime()
|
||||
registerTerminalPreviewHandlers(runtime as never)
|
||||
const sender = makeSender()
|
||||
isDashboardPopoutRendererMock.mockReturnValue(false)
|
||||
isTrustedUIRendererMock.mockReturnValue(true)
|
||||
|
||||
await expect(
|
||||
handlers.get('terminalPreview:connect')!(eventFor(sender), { ptyId: 'p1' })
|
||||
).resolves.toEqual({
|
||||
snapshot: { data: 'screen', cols: 80, rows: 20, seq: 5 },
|
||||
replay: []
|
||||
})
|
||||
await expect(
|
||||
handlers.get('terminalPreview:input')!(eventFor(sender), { ptyId: 'p1', data: 'x' })
|
||||
).resolves.toBe(true)
|
||||
expect(runtime.writeTerminalPreviewInput).toHaveBeenCalledWith('p1', 'x')
|
||||
})
|
||||
|
||||
it('pushes a resync only when the PTY grid dimensions change', async () => {
|
||||
const runtime = makeRuntime()
|
||||
registerTerminalPreviewHandlers(runtime as never)
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
} from '../../shared/terminal-preview'
|
||||
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
import { isDashboardPopoutRenderer } from '../window/dashboard-popout-window'
|
||||
import { isTrustedUIRenderer } from './ui'
|
||||
import {
|
||||
TERMINAL_PREVIEW_OUTPUT_BATCH_MAX_BYTES,
|
||||
TerminalPreviewOutputStream
|
||||
@@ -18,6 +19,13 @@ function isValidPtyId(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.length > 0 && value.length <= PREVIEW_ID_MAX_LENGTH
|
||||
}
|
||||
|
||||
// Why: the preview dialog has two hosts — the pop-out window and the main
|
||||
// renderer's in-window overlay. The trusted UI renderer already has full PTY
|
||||
// access through the regular terminal channels, so admitting it adds no reach.
|
||||
function isTerminalPreviewRenderer(sender: WebContents): boolean {
|
||||
return isDashboardPopoutRenderer(sender) || isTrustedUIRenderer(sender)
|
||||
}
|
||||
|
||||
function canRetainPreviewEntry<T>(
|
||||
registry: Map<number, Map<string, T>>,
|
||||
contentsId: number,
|
||||
@@ -48,7 +56,7 @@ export function registerTerminalPreviewHandlers(runtime: OrcaRuntimeService): vo
|
||||
const subscriptionsByContents = new Map<number, Map<string, TerminalPreviewOutputStream>>()
|
||||
// Why: the preview dialog claims the PTY grid through the remote-desktop
|
||||
// viewer registry so the main-window pane parks and later reclaims its own
|
||||
// geometry. Claims are tracked per popout webContents so an explicit
|
||||
// geometry. Claims are tracked per viewer webContents so an explicit
|
||||
// unsubscribe or a destroyed window always releases the size floor.
|
||||
const fitClaimsByContents = new Map<number, Map<string, symbol>>()
|
||||
|
||||
@@ -104,7 +112,7 @@ export function registerTerminalPreviewHandlers(runtime: OrcaRuntimeService): vo
|
||||
event,
|
||||
args: { ptyId?: unknown; opts?: { scrollbackRows?: unknown } }
|
||||
): Promise<TerminalPreviewConnectResult> => {
|
||||
if (!isDashboardPopoutRenderer(event.sender) || !isValidPtyId(args?.ptyId)) {
|
||||
if (!isTerminalPreviewRenderer(event.sender) || !isValidPtyId(args?.ptyId)) {
|
||||
return { snapshot: null, replay: [] }
|
||||
}
|
||||
const ptyId = args.ptyId
|
||||
@@ -183,7 +191,7 @@ export function registerTerminalPreviewHandlers(runtime: OrcaRuntimeService): vo
|
||||
'terminalPreview:input',
|
||||
(event, args: { ptyId?: unknown; data?: unknown }): Promise<boolean> => {
|
||||
if (
|
||||
!isDashboardPopoutRenderer(event.sender) ||
|
||||
!isTerminalPreviewRenderer(event.sender) ||
|
||||
!isValidPtyId(args?.ptyId) ||
|
||||
typeof args.data !== 'string'
|
||||
) {
|
||||
@@ -197,7 +205,7 @@ export function registerTerminalPreviewHandlers(runtime: OrcaRuntimeService): vo
|
||||
'terminalPreview:ack',
|
||||
(event, args: { ptyId?: unknown; bytes?: unknown }): void => {
|
||||
if (
|
||||
!isDashboardPopoutRenderer(event.sender) ||
|
||||
!isTerminalPreviewRenderer(event.sender) ||
|
||||
!isValidPtyId(args?.ptyId) ||
|
||||
typeof args.bytes !== 'number' ||
|
||||
!Number.isFinite(args.bytes) ||
|
||||
@@ -221,7 +229,7 @@ export function registerTerminalPreviewHandlers(runtime: OrcaRuntimeService): vo
|
||||
args: { ptyId?: unknown; cols?: unknown; rows?: unknown }
|
||||
): Promise<{ cols: number; rows: number } | null> => {
|
||||
if (
|
||||
!isDashboardPopoutRenderer(event.sender) ||
|
||||
!isTerminalPreviewRenderer(event.sender) ||
|
||||
!isValidPtyId(args?.ptyId) ||
|
||||
typeof args.cols !== 'number' ||
|
||||
typeof args.rows !== 'number' ||
|
||||
@@ -271,7 +279,7 @@ export function registerTerminalPreviewHandlers(runtime: OrcaRuntimeService): vo
|
||||
)
|
||||
|
||||
ipcMain.handle('terminalPreview:unsubscribe', (event, args: { ptyId?: unknown }): void => {
|
||||
if (!isDashboardPopoutRenderer(event.sender) || !isValidPtyId(args?.ptyId)) {
|
||||
if (!isTerminalPreviewRenderer(event.sender) || !isValidPtyId(args?.ptyId)) {
|
||||
return
|
||||
}
|
||||
subscriptionsByContents.get(event.sender.id)?.get(args.ptyId)?.dispose()
|
||||
|
||||
Reference in New Issue
Block a user