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
7 changes: 2 additions & 5 deletions src/renderer/src/components/terminal/TerminalInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ interface Props {
active: boolean
mode: TerminalAttachMode
onActivate?: () => void
autoHold?: boolean
onAutoHoldStart?: () => Promise<void> | void
onAutoHoldRelease?: (flush: boolean) => Promise<void> | void
}

export function TerminalInstance({ agentName, projectId, visible, active, mode, onActivate, autoHold, onAutoHoldStart, onAutoHoldRelease }: Props): React.ReactNode {
export function TerminalInstance({ agentName, projectId, visible, active, mode, onActivate }: Props): React.ReactNode {
const containerRef = useRef<HTMLDivElement>(null)
useTerminal(containerRef, agentName, projectId, visible, active, mode, autoHold, onAutoHoldStart, onAutoHoldRelease)
useTerminal(containerRef, agentName, projectId, visible, active, mode)

return (
<div
Expand Down
64 changes: 3 additions & 61 deletions src/renderer/src/components/terminal/TerminalPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,19 +172,13 @@ interface TerminalProjectProps {
visible: boolean
active: boolean
onActivate: () => void
autoHold?: boolean
onAutoHoldStart?: () => Promise<void> | void
onAutoHoldRelease?: (flush: boolean) => Promise<void> | void
}

function TerminalProject({
agent,
visible,
active,
onActivate,
autoHold,
onAutoHoldStart,
onAutoHoldRelease
onActivate
}: TerminalProjectProps): React.ReactNode {
const terminalMode = getTerminalMode(agent)

Expand All @@ -198,9 +192,6 @@ function TerminalProject({
active={active}
mode={terminalMode}
onActivate={onActivate}
autoHold={autoHold}
onAutoHoldStart={onAutoHoldStart}
onAutoHoldRelease={onAutoHoldRelease}
/>
</div>
</div>
Expand All @@ -216,9 +207,6 @@ interface SplitTerminalTileProps {
onActivate: () => void
onDeliveryModeChange: (agent: Agent, mode: QueueDeliveryMode) => void
onOpenBurn: (agent: Agent) => void
autoHold?: boolean
onAutoHoldStart?: () => Promise<void> | void
onAutoHoldRelease?: (flush: boolean) => Promise<void> | void
}

function SplitTerminalTile({
Expand All @@ -229,10 +217,7 @@ function SplitTerminalTile({
className = '',
onActivate,
onDeliveryModeChange,
onOpenBurn,
autoHold,
onAutoHoldStart,
onAutoHoldRelease
onOpenBurn
}: SplitTerminalTileProps): React.ReactNode {
const typing = useIsAgentTyping(agent)
return (
Expand Down Expand Up @@ -284,9 +269,6 @@ function SplitTerminalTile({
visible={visible}
active={active}
onActivate={onActivate}
autoHold={autoHold}
onAutoHoldStart={onAutoHoldStart}
onAutoHoldRelease={onAutoHoldRelease}
/>
</div>
</div>
Expand Down Expand Up @@ -364,11 +346,6 @@ interface SplitTerminalPageProps {
onActivateAgent: (key: string) => void
onDeliveryModeChange: (agent: Agent, mode: QueueDeliveryMode) => void
onOpenBurn: (agent: Agent) => void
autoHold: boolean
makeAutoHoldHandlers: (agent: Agent) => {
onAutoHoldStart: () => Promise<void>
onAutoHoldRelease: (flush: boolean) => Promise<void>
}
}

function SplitTerminalPage({
Expand All @@ -378,16 +355,13 @@ function SplitTerminalPage({
activeAgentKey,
onActivateAgent,
onDeliveryModeChange,
onOpenBurn,
autoHold,
makeAutoHoldHandlers
onOpenBurn
}: SplitTerminalPageProps): React.ReactNode {
return (
<div className={`grid h-full gap-1 p-1 ${getSplitPageGridClass(agents.length)}`}>
{agents.map((agent, index) => {
const agentKey = getAgentKeyForAgent(agent)
const active = visible && agentKey === activeAgentKey
const { onAutoHoldStart, onAutoHoldRelease } = makeAutoHoldHandlers(agent)
return (
<SplitTerminalTile
key={agentKey}
Expand All @@ -399,9 +373,6 @@ function SplitTerminalPage({
onActivate={() => onActivateAgent(agentKey)}
onDeliveryModeChange={onDeliveryModeChange}
onOpenBurn={onOpenBurn}
autoHold={autoHold}
onAutoHoldStart={onAutoHoldStart}
onAutoHoldRelease={onAutoHoldRelease}
/>
)
})}
Expand Down Expand Up @@ -524,31 +495,6 @@ export function TerminalPane(): React.ReactNode {
}
}

const makeAutoHoldHandlers = (agent: Agent): {
onAutoHoldStart: () => Promise<void>
onAutoHoldRelease: (flush: boolean) => Promise<void>
} => ({
onAutoHoldStart: async () => {
await handleDeliveryModeChange(agent, 'drive')
},
onAutoHoldRelease: async (flush: boolean) => {
let flushError: unknown
if (flush) {
try {
await pear.broker.flushPending(agent.projectId, agent.name)
} catch (err) {
flushError = err
setSpawnError(err instanceof Error ? err.message : String(err))
}
}
await handleDeliveryModeChange(agent, 'auto')
if (flushError) throw flushError
}
})

const runningAgentCount = agents.filter((a) => a.status === 'running').length
const autoHold = runningAgentCount > 1

const goToSplitPage = (page: number): void => {
const clampedPage = Math.max(0, Math.min(page, splitPageCount - 1))
setSplitPage(clampedPage)
Expand Down Expand Up @@ -1020,8 +966,6 @@ export function TerminalPane(): React.ReactNode {
onActivateAgent={setActiveAgentKey}
onDeliveryModeChange={(agent, mode) => void handleDeliveryModeChange(agent, mode)}
onOpenBurn={openBurnDetails}
autoHold={autoHold}
makeAutoHoldHandlers={makeAutoHoldHandlers}
/>
</div>
)
Expand Down Expand Up @@ -1087,8 +1031,6 @@ export function TerminalPane(): React.ReactNode {
visible={active}
active={active}
onActivate={() => setActiveAgentKey(agentKey)}
autoHold={autoHold}
{...makeAutoHoldHandlers(agent)}
/>
</div>
)
Expand Down
55 changes: 3 additions & 52 deletions src/renderer/src/hooks/use-terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,29 +144,17 @@ export function useTerminal(
projectId: string | undefined,
visible: boolean,
active: boolean = visible,
terminalMode: TerminalAttachMode = 'drive',
autoHold = false,
onAutoHoldStart?: () => Promise<void> | void,
onAutoHoldRelease?: (flush: boolean) => Promise<void> | void
terminalMode: TerminalAttachMode = 'drive'
): Terminal | null {
const termRef = useRef<Terminal | null>(null)
const fitAddonRef = useRef<FitAddon | null>(null)
const predictiveEchoRef = useRef<PredictiveEcho | null>(null)
const writtenChunksRef = useRef<number>(0)
const activeRef = useRef(active)
const terminalModeRef = useRef<TerminalAttachMode>(terminalMode)
const autoHoldRef = useRef(autoHold)
const onAutoHoldStartRef = useRef(onAutoHoldStart)
const onAutoHoldReleaseRef = useRef(onAutoHoldRelease)
const typingActiveRef = useRef(false)
const inputQueueRef = useRef<Promise<void>>(Promise.resolve())
const theme = useUIStore((s) => s.theme)
const activeDialog = useUIStore((s) => s.activeDialog)

useEffect(() => { autoHoldRef.current = autoHold }, [autoHold])
useEffect(() => { onAutoHoldStartRef.current = onAutoHoldStart }, [onAutoHoldStart])
useEffect(() => { onAutoHoldReleaseRef.current = onAutoHoldRelease }, [onAutoHoldRelease])

useEffect(() => {
activeRef.current = active
}, [active])
Expand All @@ -182,44 +170,16 @@ export function useTerminal(
})
}, [agentName, projectId, terminalMode])

const sendInputNow = useCallback(async (data: string): Promise<void> => {
const sendInput = useCallback((data: string): void => {
if (!agentName || terminalModeRef.current === 'view') return

// Auto-hold: on first keystroke with multiple agents running, switch to
// drive mode so input is queued rather than immediately injected.
const holdInput = autoHoldRef.current && typingActiveRef.current
if (autoHoldRef.current && !typingActiveRef.current && terminalModeRef.current === 'passthrough') {
typingActiveRef.current = true
await onAutoHoldStartRef.current?.()
}

// Optimistically echo before the round trip; the engine reconciles
// against authoritative output and stays dormant on fast local links.
predictiveEchoRef.current?.onUserInput(data)
recordKeystrokeSent(data)
if (holdInput || typingActiveRef.current) {
await pear.broker.sendInput(projectId, agentName, data).catch((err) => {
console.warn('[terminal] held input failed:', err)
throw err
})
} else {
pear.broker.sendInputFast(projectId, agentName, data)
}

// On Enter, flush the queued input and return to live mode.
if (typingActiveRef.current && data.includes('\r')) {
typingActiveRef.current = false
await onAutoHoldReleaseRef.current?.(true)
}
pear.broker.sendInputFast(projectId, agentName, data)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since pear.broker.sendInputFast is an IPC call that may return a Promise (or throw synchronously if the broker is disconnected), it is safer to handle potential errors or rejections to avoid unhandled promise rejections or uncaught exceptions in the renderer.

Suggested change
pear.broker.sendInputFast(projectId, agentName, data)
pear.broker.sendInputFast(projectId, agentName, data).catch((err) => { console.warn('[terminal] sendInputFast failed:', err); })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable: pear.broker.sendInputFast is typed => void (fire-and-forget ipcRenderer.send in the preload, src/shared/types/ipc.ts:869) — chaining .catch() on it would be a type error. This line restores the exact pre-auto-hold call shape; broker-disconnect handling lives on the main-process side of the channel.

}, [agentName, projectId])

const sendInput = useCallback((data: string): void => {
const next = inputQueueRef.current.then(() => sendInputNow(data))
inputQueueRef.current = next.catch((err) => {
console.warn('[terminal] input failed:', err)
})
}, [sendInputNow])

useEffect(() => {
if (!containerRef.current || !agentName) return

Expand Down Expand Up @@ -460,13 +420,6 @@ export function useTerminal(
focusTerminal()
}

const handleBlur = (): void => {
if (typingActiveRef.current) {
typingActiveRef.current = false
void onAutoHoldReleaseRef.current?.(false)
}
}

const handleKeyDown = (event: KeyboardEvent): void => {
if (event.isComposing || event.target === term?.textarea) {
return
Expand Down Expand Up @@ -498,7 +451,6 @@ export function useTerminal(
container.addEventListener('pointerdown', handlePointerDown)
container.addEventListener('keydown', handleKeyDown)
container.addEventListener('paste', handlePaste)
container.addEventListener('blur', handleBlur, true)

return () => {
disposed = true
Expand All @@ -507,7 +459,6 @@ export function useTerminal(
container.removeEventListener('pointerdown', handlePointerDown)
container.removeEventListener('keydown', handleKeyDown)
container.removeEventListener('paste', handlePaste)
container.removeEventListener('blur', handleBlur, true)
resizeObserver?.disconnect()
if (srttPoll) clearInterval(srttPoll)
disposePredictiveEcho?.()
Expand Down
Loading