Fix active agent detection in split-pane layouts and support early hints (#4949)

* Detect active agents from title/launch hints before hooks report

* Fix active agent detection in split-pane layouts with lone background ti

* Probe runtime for manually started agents during note-send detection

- Query runtime via `terminal.isRunningAgent` to detect active agents
  before titles or status hooks have reported them.
- Extract active-agent-target resolution utilities and state selectors
  to a dedicated `active-agent-note-target.ts` file.
This commit is contained in:
Jinjing 2026-06-08 21:52:19 -07:00 committed by GitHub
parent b42b286879
commit bd0533cfe5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 437 additions and 130 deletions

View File

@ -1,7 +1,7 @@
import { detectAgentStatusFromTitle, isExplicitAgentStatusFresh } from '@/lib/agent-status'
import { migrationUnsupportedToAgentStatusEntry } from '@/lib/migration-unsupported-agent-entry'
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
import { resolveRuntimePaneTitleLeafId } from './runtime-pane-title-leaf-id'
import { resolveRuntimePaneTitleLeafId } from '@/lib/runtime-pane-title-leaf-id'
import type { AgentStatus } from '../../../../shared/agent-detection'
import type { TerminalLayoutSnapshot, TerminalTab, Worktree } from '../../../../shared/types'
import {

View File

@ -16,7 +16,7 @@ import type {
TerminalPaneLayoutNode,
TerminalTab
} from '../../../../shared/types'
import { resolveRuntimePaneTitleLeafId } from './runtime-pane-title-leaf-id'
import { resolveRuntimePaneTitleLeafId } from '@/lib/runtime-pane-title-leaf-id'
import { buildTitleDerivedAgentRows } from './worktree-title-derived-agent-rows'
function tabFromAttributedStatusEntry(entry: AgentStatusEntry): TerminalTab | null {

View File

@ -2,11 +2,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
getActiveAgentNoteTarget,
getActiveAgentRuntimeProbeDescriptor,
getActiveTerminalNoteTarget,
probeActiveAgentNoteTarget,
sendNotesToActiveAgentSession
} from './active-agent-note-send'
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
import { makePaneKey } from '../../../shared/stable-pane-id'
import type { TerminalLayoutSnapshot } from '../../../shared/types'
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222'
@ -24,6 +27,7 @@ const testState = vi.hoisted(() => ({
ptyIdsByTabId: {
'tab-1': ['pty-1']
},
runtimePaneTitlesByTabId: {},
terminalLayoutsByTabId: {
'tab-1': {
activeLeafId: '11111111-1111-4111-8111-111111111111',
@ -37,11 +41,16 @@ const testState = vi.hoisted(() => ({
activeTabType: 'terminal' | 'editor'
activeTabId: string | null
activeTabIdByWorktree: Record<string, string | null>
tabsByWorktree: Record<string, { id: string }[]>
tabsByWorktree: Record<string, { id: string; launchAgent?: string }[]>
ptyIdsByTabId: Record<string, string[]>
runtimePaneTitlesByTabId: Record<string, Record<number, string>>
terminalLayoutsByTabId: Record<
string,
{ activeLeafId: string | null; ptyIdsByLeafId?: Record<string, string | undefined> }
{
activeLeafId: string | null
root?: TerminalLayoutSnapshot['root']
ptyIdsByLeafId?: Record<string, string | undefined>
}
>
agentStatusByPaneKey: Record<string, AgentStatusEntry>
settings: Record<string, unknown>
@ -77,6 +86,7 @@ describe('active agent note send', () => {
ptyIdsByTabId: {
'tab-1': ['pty-1']
},
runtimePaneTitlesByTabId: {},
terminalLayoutsByTabId: {
'tab-1': { activeLeafId: LEAF_ID, ptyIdsByLeafId: { [LEAF_ID]: 'pty-1' } }
},
@ -119,6 +129,117 @@ describe('active agent note send', () => {
expect(getActiveAgentNoteTarget(testState.appState, 'wt-1', NOW)).toBeNull()
})
it('runtime-probes a manually started agent before title or hooks report it', async () => {
testState.callRuntimeRpc.mockImplementation(async (_target, method) => {
if (method === 'terminal.list') {
return {
terminals: [
{
handle: 'term-1',
worktreeId: 'wt-1',
worktreePath: '/repo',
branch: 'main',
tabId: 'tab-1',
leafId: LEAF_ID,
title: 'repo terminal',
connected: true,
writable: true,
lastOutputAt: 1,
preview: ''
}
],
totalCount: 1,
truncated: false
}
}
if (method === 'terminal.isRunningAgent') {
return { isRunningAgent: true }
}
throw new Error(`unexpected method ${method}`)
})
const descriptor = getActiveAgentRuntimeProbeDescriptor(testState.appState, 'wt-1')
expect(descriptor).toMatchObject({
key: `local:wt-1:tab-1:${LEAF_ID}:pty-1`,
noteTarget: { tabId: 'tab-1', leafId: LEAF_ID }
})
await expect(probeActiveAgentNoteTarget(descriptor!)).resolves.toBe(true)
})
it('offers the active terminal send target for a fresh title-detected agent before hooks report', () => {
testState.appState.runtimePaneTitlesByTabId = {
'tab-1': { 1: 'Codex' }
}
expect(getActiveAgentNoteTarget(testState.appState, 'wt-1', NOW)).toEqual({
tabId: 'tab-1',
leafId: LEAF_ID
})
})
it('offers the active terminal send target for an Orca-launched agent before hooks report', () => {
testState.appState.tabsByWorktree = {
'wt-1': [{ id: 'tab-1', launchAgent: 'codex' }]
}
expect(getActiveAgentNoteTarget(testState.appState, 'wt-1', NOW)).toEqual({
tabId: 'tab-1',
leafId: LEAF_ID
})
})
it('does not let an old launch marker override a focused shell title', () => {
testState.appState.tabsByWorktree = {
'wt-1': [{ id: 'tab-1', launchAgent: 'codex' }]
}
testState.appState.runtimePaneTitlesByTabId = {
'tab-1': { 1: 'zsh' }
}
expect(getActiveAgentNoteTarget(testState.appState, 'wt-1', NOW)).toBeNull()
})
it('does not offer the active terminal send target for another split pane title', () => {
testState.appState.runtimePaneTitlesByTabId = {
'tab-1': { 1: 'zsh', 2: 'Codex' }
}
testState.appState.terminalLayoutsByTabId = {
'tab-1': {
activeLeafId: LEAF_ID,
root: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', leafId: LEAF_ID },
second: { type: 'leaf', leafId: OTHER_LEAF_ID }
},
ptyIdsByLeafId: { [LEAF_ID]: 'pty-1' }
}
}
expect(getActiveAgentNoteTarget(testState.appState, 'wt-1', NOW)).toBeNull()
})
it('does not treat a lone background split-pane title as the focused pane', () => {
testState.appState.runtimePaneTitlesByTabId = {
'tab-1': { 2: 'Codex' }
}
testState.appState.terminalLayoutsByTabId = {
'tab-1': {
activeLeafId: LEAF_ID,
root: {
type: 'split',
direction: 'horizontal',
first: { type: 'leaf', leafId: LEAF_ID },
second: { type: 'leaf', leafId: OTHER_LEAF_ID }
},
ptyIdsByLeafId: { [LEAF_ID]: 'pty-1' }
}
}
expect(getActiveAgentNoteTarget(testState.appState, 'wt-1', NOW)).toBeNull()
})
it('offers the active terminal send target when the focused pane is a fresh agent session', () => {
const paneKey = makePaneKey('tab-1', LEAF_ID)
testState.appState.agentStatusByPaneKey = {

View File

@ -1,27 +1,19 @@
import type {
RuntimeTerminalListResult,
RuntimeTerminalSend,
RuntimeTerminalWait
} from '../../../shared/runtime-types'
import {
AGENT_STATUS_STALE_AFTER_MS,
type AgentStatusEntry
} from '../../../shared/agent-status-types'
import type { AppState } from '@/store/types'
import type { RuntimeTerminalSend, RuntimeTerminalWait } from '../../../shared/runtime-types'
import { useAppStore } from '@/store'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
import { makePaneKey, isTerminalLeafId } from '../../../shared/stable-pane-id'
import { isExplicitAgentStatusFresh } from './agent-status'
import { findActiveRuntimeTerminal, getActiveTerminalNoteTarget } from './active-agent-note-target'
export {
getActiveAgentNoteTarget,
getActiveAgentRuntimeProbeDescriptor,
getActiveTerminalNoteTarget,
probeActiveAgentNoteTarget,
useCanSendNotesToActiveTerminal,
type ActiveTerminalNoteTarget
} from './active-agent-note-target'
const ACTIVE_AGENT_SEND_TIMEOUT_MS = 8000
const ACTIVE_AGENT_SEND_RPC_TIMEOUT_MS = 15000
const ACTIVE_AGENT_TERMINAL_LIST_LIMIT = 200
export type ActiveTerminalNoteTarget = {
tabId: string
leafId: string
}
export type ActiveAgentNotesSendStatus =
| 'sent'
@ -35,69 +27,6 @@ export type ActiveAgentNotesSendResult = {
status: ActiveAgentNotesSendStatus
}
type ActiveTerminalNoteTargetState = {
activeWorktreeId: AppState['activeWorktreeId']
activeTabType: AppState['activeTabType']
activeTabId: AppState['activeTabId']
activeTabIdByWorktree: AppState['activeTabIdByWorktree']
tabsByWorktree: Record<string, readonly { id: string }[] | undefined>
ptyIdsByTabId?: Record<string, readonly string[] | undefined>
terminalLayoutsByTabId: Record<
string,
{ activeLeafId: string | null; ptyIdsByLeafId?: Record<string, string | undefined> } | undefined
>
agentStatusByPaneKey?: Record<string, AgentStatusEntry | undefined>
}
export function getActiveTerminalNoteTarget(
state: ActiveTerminalNoteTargetState,
worktreeId: string
): ActiveTerminalNoteTarget | null {
if (state.activeWorktreeId !== worktreeId) {
return null
}
const tabId =
state.activeTabType === 'terminal'
? (state.activeTabId ?? state.activeTabIdByWorktree[worktreeId])
: state.activeTabIdByWorktree[worktreeId]
if (!tabId || !(state.tabsByWorktree[worktreeId] ?? []).some((tab) => tab.id === tabId)) {
return null
}
const leafId = state.terminalLayoutsByTabId[tabId]?.activeLeafId
return leafId ? { tabId, leafId } : null
}
export function useCanSendNotesToActiveTerminal(worktreeId: string): boolean {
return useAppStore((state) => getActiveAgentNoteTarget(state, worktreeId) !== null)
}
export function getActiveAgentNoteTarget(
state: ActiveTerminalNoteTargetState,
worktreeId: string,
now = Date.now()
): ActiveTerminalNoteTarget | null {
const noteTarget = getActiveTerminalNoteTarget(state, worktreeId)
if (!noteTarget || !isTerminalLeafId(noteTarget.leafId)) {
return null
}
const activePtyId = getActivePanePtyId(state, noteTarget)
if (!activePtyId) {
return null
}
const entry = state.agentStatusByPaneKey?.[makePaneKey(noteTarget.tabId, noteTarget.leafId)]
// Why: an active terminal can be a regular shell; only explicit hook-backed
// agent state is enough to offer the destructive "submit with Enter" target.
if (!entry || !isExplicitAgentStatusFresh(entry, now, AGENT_STATUS_STALE_AFTER_MS)) {
return null
}
return noteTarget
}
export async function sendNotesToActiveAgentSession({
worktreeId,
prompt,
@ -119,7 +48,12 @@ export async function sendNotesToActiveAgentSession({
}
const runtimeTarget = getActiveRuntimeTarget(state.settings)
const terminal = await findActiveRuntimeTerminal(runtimeTarget, worktreeId, noteTarget)
const terminal = await findActiveRuntimeTerminal(
runtimeTarget,
worktreeId,
noteTarget,
ACTIVE_AGENT_SEND_RPC_TIMEOUT_MS
)
if (!terminal) {
return { status: 'no-active-terminal' }
}
@ -187,44 +121,6 @@ export function activeAgentNotesSendFailureMessage(status: ActiveAgentNotesSendS
}
}
async function findActiveRuntimeTerminal(
runtimeTarget: ReturnType<typeof getActiveRuntimeTarget>,
worktreeId: string,
noteTarget: ActiveTerminalNoteTarget
): Promise<RuntimeTerminalListResult['terminals'][number] | null> {
const { terminals } = await callRuntimeRpc<RuntimeTerminalListResult>(
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 },
{ timeoutMs: ACTIVE_AGENT_SEND_RPC_TIMEOUT_MS }
)
return (
terminals.find(
(terminal) => terminal.tabId === noteTarget.tabId && terminal.leafId === noteTarget.leafId
) ?? null
)
}
function getActivePanePtyId(
state: ActiveTerminalNoteTargetState,
noteTarget: ActiveTerminalNoteTarget
): string | null {
const livePtyIds = state.ptyIdsByTabId?.[noteTarget.tabId] ?? []
if (livePtyIds.length === 0) {
return null
}
const ptyIdsByLeafId = state.terminalLayoutsByTabId[noteTarget.tabId]?.ptyIdsByLeafId
if (ptyIdsByLeafId && Object.keys(ptyIdsByLeafId).length > 0) {
const activeLeafPtyId = ptyIdsByLeafId[noteTarget.leafId]
// Why: layout maps can survive sleep/reconnect; ptyIdsByTabId is the live
// PTY source of truth for whether submitting with Enter is currently safe.
return activeLeafPtyId && livePtyIds.includes(activeLeafPtyId) ? activeLeafPtyId : null
}
return livePtyIds[0] ?? null
}
function isRuntimeTimeout(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return message.includes('timeout')

View File

@ -0,0 +1,290 @@
import { useEffect, useState } from 'react'
import type { RuntimeTerminalListResult } from '../../../shared/runtime-types'
import {
AGENT_STATUS_STALE_AFTER_MS,
type AgentStatusEntry
} from '../../../shared/agent-status-types'
import type { AppState } from '@/store/types'
import { useAppStore } from '@/store'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id'
import type { TerminalLayoutSnapshot } from '../../../shared/types'
import {
detectAgentStatusFromTitle,
getAgentLabel,
isExplicitAgentStatusFresh
} from './agent-status'
import { resolveRuntimePaneTitleLeafId } from './runtime-pane-title-leaf-id'
const ACTIVE_AGENT_PROBE_RPC_TIMEOUT_MS = 3000
const ACTIVE_AGENT_TERMINAL_LIST_LIMIT = 200
export type ActiveTerminalNoteTarget = {
tabId: string
leafId: string
}
export type ActiveTerminalNoteTargetState = {
activeWorktreeId: AppState['activeWorktreeId']
activeTabType: AppState['activeTabType']
activeTabId: AppState['activeTabId']
activeTabIdByWorktree: AppState['activeTabIdByWorktree']
tabsByWorktree: Record<
string,
readonly { id: string; title?: string; launchAgent?: unknown }[] | undefined
>
ptyIdsByTabId?: Record<string, readonly string[] | undefined>
terminalLayoutsByTabId: Record<
string,
| {
activeLeafId: string | null
root?: TerminalLayoutSnapshot['root']
ptyIdsByLeafId?: Record<string, string | undefined>
}
| undefined
>
runtimePaneTitlesByTabId?: Record<string, Record<number, string> | undefined>
agentStatusByPaneKey?: Record<string, AgentStatusEntry | undefined>
settings: Parameters<typeof getActiveRuntimeTarget>[0]
}
type ActiveAgentRuntimeProbeDescriptor = {
key: string
worktreeId: string
runtimeTarget: ReturnType<typeof getActiveRuntimeTarget>
noteTarget: ActiveTerminalNoteTarget
}
export function getActiveTerminalNoteTarget(
state: ActiveTerminalNoteTargetState,
worktreeId: string
): ActiveTerminalNoteTarget | null {
if (state.activeWorktreeId !== worktreeId) {
return null
}
const tabId =
state.activeTabType === 'terminal'
? (state.activeTabId ?? state.activeTabIdByWorktree[worktreeId])
: state.activeTabIdByWorktree[worktreeId]
if (!tabId || !(state.tabsByWorktree[worktreeId] ?? []).some((tab) => tab.id === tabId)) {
return null
}
const leafId = state.terminalLayoutsByTabId[tabId]?.activeLeafId
return leafId ? { tabId, leafId } : null
}
export function useCanSendNotesToActiveTerminal(worktreeId: string): boolean {
const canSendFromRendererState = useAppStore(
(state) => getActiveAgentNoteTarget(state, worktreeId) !== null
)
const probeKey = useAppStore(
(state) => getActiveAgentRuntimeProbeDescriptor(state, worktreeId)?.key ?? null
)
const [runtimeProbe, setRuntimeProbe] = useState<{ key: string; canSend: boolean } | null>(null)
useEffect(() => {
if (canSendFromRendererState || !probeKey) {
return
}
const probeDescriptor = getActiveAgentRuntimeProbeDescriptor(useAppStore.getState(), worktreeId)
if (!probeDescriptor || probeDescriptor.key !== probeKey) {
return
}
let cancelled = false
void probeActiveAgentNoteTarget(probeDescriptor)
.then((canSend) => {
if (!cancelled) {
setRuntimeProbe({ key: probeKey, canSend })
}
})
.catch(() => {
if (!cancelled) {
setRuntimeProbe({ key: probeKey, canSend: false })
}
})
return () => {
cancelled = true
}
}, [canSendFromRendererState, probeKey, worktreeId])
return (
canSendFromRendererState ||
(runtimeProbe !== null && runtimeProbe.key === probeKey && runtimeProbe.canSend)
)
}
export function getActiveAgentNoteTarget(
state: ActiveTerminalNoteTargetState,
worktreeId: string,
now = Date.now()
): ActiveTerminalNoteTarget | null {
const noteTarget = getActiveTerminalNoteTarget(state, worktreeId)
if (!noteTarget || !isTerminalLeafId(noteTarget.leafId)) {
return null
}
const activePtyId = getActivePanePtyId(state, noteTarget)
if (!activePtyId) {
return null
}
const entry = state.agentStatusByPaneKey?.[makePaneKey(noteTarget.tabId, noteTarget.leafId)]
if (entry && isExplicitAgentStatusFresh(entry, now, AGENT_STATUS_STALE_AFTER_MS)) {
return noteTarget
}
// Why: freshly opened agents can be idle before their first hook event. Use
// renderer title/launch hints only to show the option; runtime still verifies
// the focused terminal is an idle agent before sending Enter.
if (!hasFocusedPaneAgentHint(state, worktreeId, noteTarget)) {
return null
}
return noteTarget
}
export function getActiveAgentRuntimeProbeDescriptor(
state: ActiveTerminalNoteTargetState,
worktreeId: string
): ActiveAgentRuntimeProbeDescriptor | null {
const noteTarget = getActiveTerminalNoteTarget(state, worktreeId)
if (!noteTarget || !isTerminalLeafId(noteTarget.leafId)) {
return null
}
const activePtyId = getActivePanePtyId(state, noteTarget)
if (!activePtyId) {
return null
}
const runtimeTarget = getActiveRuntimeTarget(state.settings)
const runtimeKey =
runtimeTarget.kind === 'environment' ? `env:${runtimeTarget.environmentId}` : 'local'
return {
key: `${runtimeKey}:${worktreeId}:${noteTarget.tabId}:${noteTarget.leafId}:${activePtyId}`,
worktreeId,
runtimeTarget,
noteTarget
}
}
export async function probeActiveAgentNoteTarget({
worktreeId,
runtimeTarget,
noteTarget
}: ActiveAgentRuntimeProbeDescriptor): Promise<boolean> {
const terminal = await findActiveRuntimeTerminal(
runtimeTarget,
worktreeId,
noteTarget,
ACTIVE_AGENT_PROBE_RPC_TIMEOUT_MS
)
if (!terminal) {
return false
}
const agentCheck = await callRuntimeRpc<{ isRunningAgent: boolean }>(
runtimeTarget,
'terminal.isRunningAgent',
{ terminal: terminal.handle },
{ timeoutMs: ACTIVE_AGENT_PROBE_RPC_TIMEOUT_MS }
)
return agentCheck.isRunningAgent
}
export async function findActiveRuntimeTerminal(
runtimeTarget: ReturnType<typeof getActiveRuntimeTarget>,
worktreeId: string,
noteTarget: ActiveTerminalNoteTarget,
timeoutMs: number
): Promise<RuntimeTerminalListResult['terminals'][number] | null> {
const { terminals } = await callRuntimeRpc<RuntimeTerminalListResult>(
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 },
{ timeoutMs }
)
return (
terminals.find(
(terminal) => terminal.tabId === noteTarget.tabId && terminal.leafId === noteTarget.leafId
) ?? null
)
}
function getActivePanePtyId(
state: ActiveTerminalNoteTargetState,
noteTarget: ActiveTerminalNoteTarget
): string | null {
const livePtyIds = state.ptyIdsByTabId?.[noteTarget.tabId] ?? []
if (livePtyIds.length === 0) {
return null
}
const ptyIdsByLeafId = state.terminalLayoutsByTabId[noteTarget.tabId]?.ptyIdsByLeafId
if (ptyIdsByLeafId && Object.keys(ptyIdsByLeafId).length > 0) {
const activeLeafPtyId = ptyIdsByLeafId[noteTarget.leafId]
// Why: layout maps can survive sleep/reconnect; ptyIdsByTabId is the live
// PTY source of truth for whether submitting with Enter is currently safe.
return activeLeafPtyId && livePtyIds.includes(activeLeafPtyId) ? activeLeafPtyId : null
}
return livePtyIds[0] ?? null
}
function hasFocusedPaneAgentHint(
state: ActiveTerminalNoteTargetState,
worktreeId: string,
noteTarget: ActiveTerminalNoteTarget
): boolean {
const tab = (state.tabsByWorktree[worktreeId] ?? []).find(
(entry) => entry.id === noteTarget.tabId
)
const runtimeTitle = getFocusedRuntimePaneTitle(state, noteTarget)
if (runtimeTitle !== null) {
return isRecognizedAgentTitle(runtimeTitle)
}
if (tab?.launchAgent) {
return true
}
return tab?.title ? isRecognizedAgentTitle(tab.title) : false
}
function getFocusedRuntimePaneTitle(
state: ActiveTerminalNoteTargetState,
noteTarget: ActiveTerminalNoteTarget
): string | null {
const paneTitles = state.runtimePaneTitlesByTabId?.[noteTarget.tabId]
if (!paneTitles || Object.keys(paneTitles).length === 0) {
return null
}
const layout = state.terminalLayoutsByTabId[noteTarget.tabId]
const titleEntries = Object.entries(paneTitles)
if (layout?.root) {
// Why: split-pane title maps can be sparse; a lone background title must not
// enable "send to active agent" for the focused shell pane.
for (const [runtimePaneId, title] of titleEntries) {
if (resolveRuntimePaneTitleLeafId(layout, runtimePaneId) === noteTarget.leafId) {
return title
}
}
return null
}
if (titleEntries.length === 1) {
return titleEntries[0][1]
}
for (const [runtimePaneId, title] of titleEntries) {
if (resolveRuntimePaneTitleLeafId(layout, runtimePaneId) === noteTarget.leafId) {
return title
}
}
return null
}
function isRecognizedAgentTitle(title: string): boolean {
return detectAgentStatusFromTitle(title) !== null && getAgentLabel(title) !== null
}

View File

@ -1,6 +1,6 @@
import { FIRST_PANE_ID } from '../../../../shared/pane-key'
import { isTerminalLeafId } from '../../../../shared/stable-pane-id'
import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types'
import { FIRST_PANE_ID } from '../../../shared/pane-key'
import { isTerminalLeafId } from '../../../shared/stable-pane-id'
import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../shared/types'
function getLeftmostLeafId(node: TerminalPaneLayoutNode): string {
return node.type === 'leaf' ? node.leafId : getLeftmostLeafId(node.first)
@ -38,7 +38,7 @@ function collectLeafIdsInReplayCreationOrder(
}
export function resolveRuntimePaneTitleLeafId(
tabLayout: TerminalLayoutSnapshot | undefined,
tabLayout: { root?: TerminalLayoutSnapshot['root'] } | undefined,
runtimePaneId: string
): string | null {
return resolveRuntimePaneTitleLeafIdFromRoot(tabLayout?.root, runtimePaneId)

View File

@ -1,6 +1,6 @@
import { detectAgentStatusFromTitle } from '@/lib/agent-status'
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
import { resolveRuntimePaneTitleLeafIdFromRoot } from '@/components/sidebar/runtime-pane-title-leaf-id'
import { resolveRuntimePaneTitleLeafIdFromRoot } from '@/lib/runtime-pane-title-leaf-id'
import type {
TerminalLayoutSnapshot,
TerminalPaneLayoutNode,