refactor(desktop): route tool cards and bridge events through lookup tables (#127828)

* refactor(desktop): route tool cards through a table keyed on the card-tool names

The name-keyed if ladder in ChainToolFallback becomes TOOL_CARDS, typed
Record<CardToolName, FC>. CardToolName comes from the same list isCardTool
reads, so a card added to one list and not the other fails the build.
Behaviour is unchanged.

* refactor(desktop): dispatch desktop bridge events through a handler table

The six-branch event.type ladder becomes DESKTOP_BRIDGE_HANDLERS, one row
per event with its body and comments unchanged. Behaviour is unchanged.

* chore: retrigger CI (zero-job dispatch failure, auto-heal)
This commit is contained in:
Siddharth Balyan
2026-09-29 18:37:11 +05:30
committed by GitHub
parent 5eea87882a
commit fc042f1d67
3 changed files with 62 additions and 70 deletions

View File

@@ -7,28 +7,19 @@ import { $tipsEnabled, type ActiveTip, agentTipId, showTip } from '@/store/tips'
import type { GatewayEventContext } from './types'
/** Desktop-surface bridge events: agent terminal streaming, tips, pane
* reveal, layouts and message reactions. The read-back REQUESTS the agent
* blocks on (terminal/preview/window/tour) live in `server-requests.ts`. */
export function handleDesktopBridgeEvent(ctx: GatewayEventContext): boolean {
const { event, payload, isActiveEvent } = ctx
if (event.type === 'agent.terminal.output') {
const DESKTOP_BRIDGE_HANDLERS: Record<string, (ctx: GatewayEventContext) => void> = {
'agent.terminal.output': ({ payload }) => {
// Live chunk from a background process → its read-only agent terminal tab.
writeAgentTerminalChunk(payload?.process_id ?? '', payload?.chunk ?? '')
},
return true
}
if (event.type === 'terminal.close') {
'terminal.close': ({ payload }) => {
// Agent closed its own read-only tab via the desktop-gated close_terminal tool.
// The process is untouched — this only drops the view.
closeAgentTerminalByProc(payload?.process_id ?? '')
},
return true
}
if (event.type === 'tip.show') {
'tip.show': ({ payload, isActiveEvent }) => {
// tip tool: point the accent bubble at something and say one line about
// it. Fire-and-forget — a tip is not a question, and blocking the turn on
// one would stall the sentence the agent is in the middle of, so there is
@@ -51,11 +42,9 @@ export function handleDesktopBridgeEvent(ctx: GatewayEventContext): boolean {
title: typeof payload?.title === 'string' ? payload.title : undefined
})
}
},
return true
}
if (event.type === 'pane.reveal') {
'pane.reveal': ({ payload, isActiveEvent }) => {
// Agent revealed a pane via the desktop-gated focus_pane tool, in
// response to an explicit user request. Active session only — a
// background turn must never move the user's focus (desktop AGENTS.md:
@@ -63,11 +52,9 @@ export function handleDesktopBridgeEvent(ctx: GatewayEventContext): boolean {
if (isActiveEvent) {
revealDesktopPane(payload?.pane ?? '')
}
},
return true
}
if (event.type === 'layout.apply') {
'layout.apply': ({ payload, isActiveEvent }) => {
// Agent applied a layout preset via the desktop-gated apply_layout
// tool. Same contract as pane.reveal: active session only, and the
// preset resolves against the SAME layouts registry the picker reads,
@@ -75,11 +62,9 @@ export function handleDesktopBridgeEvent(ctx: GatewayEventContext): boolean {
if (isActiveEvent) {
applyDesktopLayoutPreset(typeof payload?.preset === 'string' ? payload.preset : '')
}
},
return true
}
if (event.type === 'message.reaction') {
'message.reaction': ({ payload }) => {
// The agent reacted to a message via the desktop-gated
// react_to_message tool. Already persisted — this only paints it now
// instead of at the next resume. Fresh ChatMessage object per change:
@@ -124,9 +109,18 @@ export function handleDesktopBridgeEvent(ctx: GatewayEventContext): boolean {
)
})
}
}
}
return true
/** Desktop-surface bridge events: agent terminal streaming, tips, pane
* reveal, layouts and message reactions. The read-back REQUESTS the agent
* blocks on (terminal/preview/window/tour) live in `server-requests.ts`. */
export function handleDesktopBridgeEvent(ctx: GatewayEventContext): boolean {
if (!Object.hasOwn(DESKTOP_BRIDGE_HANDLERS, ctx.event.type)) {
return false
}
return false
DESKTOP_BRIDGE_HANDLERS[ctx.event.type](ctx)
return true
}

View File

@@ -29,7 +29,7 @@ import { mcpTargets, toolLabels } from '@/lib/connector-tools'
import { generatedImageFromResult } from '@/lib/generated-images'
import { separateGluedReasoningBlocks } from '@/lib/reasoning-blocks'
import { isTodoToolName } from '@/lib/todos'
import { isCardTool } from '@/lib/tool-render-class'
import { type CardToolName, isCardTool, isCardToolName } from '@/lib/tool-render-class'
import { useEnterAnimation } from '@/lib/use-enter-animation'
import { cn } from '@/lib/utils'
import { $reasoningCollapsedByDefault, $showReasoning } from '@/store/reasoning-disclosure'
@@ -77,6 +77,33 @@ const DelegateToolPart: FC<TimelineToolCallProps> = props => {
)
}
const ClarifyToolPart: FC<TimelineToolCallProps> = props => {
// Stopped on this question, never answered: history. ClarifyTool reads
// the session's live clarify request, so a later turn's question would
// otherwise paint onto this row as a second live card.
if (settledWithoutResult(props)) {
return <ToolFallback {...props} />
}
return (
<>
<TimelineTimestamp className="mb-0.5 block" completedAt={props.completedAt} timestamp={props.timestamp} />
<ClarifyTool {...props} />
</>
)
}
const ConnectionsToolPart: FC<TimelineToolCallProps> = props =>
mcpTargets(props.toolName, props.args).length > 0 ? <McpSetupTool {...props} /> : <ConnectorTool {...props} />
const TOOL_CARDS: Record<CardToolName, FC<TimelineToolCallProps>> = {
clarify: ClarifyToolPart,
delegate_task: DelegateToolPart,
image_generate: ImageGenerateTool,
manage_catalog: CatalogInstallTool,
manage_connections: ConnectionsToolPart
}
// A failure the user still has to see. The gateway's tool.complete carries the
// failure inside `result`, never as the top-level error that sets isError, so
// this reads the body like the run summary does. A non-zero exit_code counts
@@ -115,40 +142,10 @@ const ChainToolFallback: FC<TimelineToolCallProps> = props => {
return null
}
if (props.toolName === 'delegate_task') {
return <DelegateToolPart {...props} />
}
if (isCardToolName(props.toolName)) {
const ToolCard = TOOL_CARDS[props.toolName]
if (props.toolName === 'image_generate') {
return <ImageGenerateTool {...props} />
}
if (props.toolName === 'clarify') {
// Stopped on this question, never answered: history. ClarifyTool reads
// the session's live clarify request, so a later turn's question would
// otherwise paint onto this row as a second live card.
if (settledWithoutResult(props)) {
return <ToolFallback {...props} />
}
return (
<>
<TimelineTimestamp className="mb-0.5 block" completedAt={props.completedAt} timestamp={props.timestamp} />
<ClarifyTool {...props} />
</>
)
}
if (props.toolName === 'manage_catalog') {
return <CatalogInstallTool {...props} />
}
if (mcpTargets(props.toolName, props.args).length > 0) {
return <McpSetupTool {...props} />
}
if (props.toolName === 'manage_connections') {
return <ConnectorTool {...props} />
return <ToolCard {...props} />
}
if (toolLabels(props.args).length > 0) {

View File

@@ -28,18 +28,19 @@ export function isFileEditTool(toolName: string): boolean {
//
// Everything else is ephemeral activity — reads, searches, commands — which is
// what a run summarizes and what the live ticker cycles through.
const CARD_TOOL_NAMES = new Set(['clarify', 'delegate_task', 'image_generate', 'manage_catalog'])
const CARD_TOOL_NAMES = ['clarify', 'delegate_task', 'image_generate', 'manage_catalog', 'manage_connections'] as const
export type CardToolName = (typeof CARD_TOOL_NAMES)[number]
export function isCardToolName(toolName: string): toolName is CardToolName {
return (CARD_TOOL_NAMES as readonly string[]).includes(toolName)
}
// Name the run splitter uses for a manage_connections part it has classified as a card.
export const CONNECTION_CARD_KEY = 'manage_connections:card'
export function isCardTool(toolName: string): boolean {
return (
CARD_TOOL_NAMES.has(toolName) ||
toolName === CONNECTION_CARD_KEY ||
isFileEditTool(toolName) ||
toolName === 'manage_connections'
)
return isCardToolName(toolName) || toolName === CONNECTION_CARD_KEY || isFileEditTool(toolName)
}
// Activity tools that render nothing at all: `todo` parts are hoisted to a