refactor: extract helpers that agent-status files need room for (#30260)

No behavior change. Moves cohesive pieces out of files at their line limit
so the OSC 7501 program-status work on top reads as additions:

- pane-agent-identity: the confirmed-shell tab-title reset
- SortableTab: its activity-status selector, into useTerminalTabActivity
- agent-status-event-applicator: the Pi provider-session partial update
- terminal-derived-agent-facts: resolving a PTY's status target panes
- server-row-ownership: notifying row-mutation listeners
This commit is contained in:
Neil
2026-10-18 20:44:24 -07:00
committed by GitHub
parent 5ee118a908
commit 8908fdeb0c
8 changed files with 154 additions and 101 deletions
@@ -104,10 +104,14 @@ export abstract class AgentHookServerRowOwnership extends AgentHookServerListene
if (!emit || semanticRowsEqual(before, after)) {
return false
}
const mutation: AgentHookStatusRowMutation = {
this.notifyStatusRowMutationListeners({
before: toMutationIdentity(before),
after: toMutationIdentity(after)
}
})
return true
}
private notifyStatusRowMutationListeners(mutation: AgentHookStatusRowMutation): void {
for (const listener of this.statusRowMutationListeners) {
try {
listener(mutation)
@@ -115,6 +119,5 @@ export abstract class AgentHookServerRowOwnership extends AgentHookServerListene
console.error('[agent-hooks] status-row mutation listener threw', error)
}
}
return true
}
}
@@ -10,6 +10,12 @@ import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id'
import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path'
import type { ProcessedAgentStatusChunk } from '../../../../shared/agent-status-osc'
import { mapExplicitAgentStateToRuntimeTerminalStatus } from '../../runtime-worktree-status-projection'
import type { RuntimeTerminalAgentStatusEvent } from '../../runtime-terminal-contracts'
type TerminalAgentStatusTarget = Pick<
RuntimeTerminalAgentStatusEvent,
'source' | 'paneKey' | 'tabId' | 'worktreeId' | 'connectionId' | 'terminalHandle'
>
export abstract class OrcaRuntimeWithCreateTerminalSideEffectCommandCodeDetector extends OrcaRuntimeWithApplyTrackedPtyTitle {
abstract getAgentStatusTerminalHandleForPaneKey(paneKey: string): string | undefined
@@ -94,17 +100,41 @@ export abstract class OrcaRuntimeWithCreateTerminalSideEffectCommandCodeDetector
if (chunk.payloads.length === 0) {
return
}
const targets = new Map<
string,
{
source: 'mounted-leaf' | 'pty-record'
paneKey: string
tabId?: string
worktreeId?: string
connectionId?: string | null
terminalHandle?: string
const targets = this.resolveTerminalAgentStatusTargets(ptyId)
for (const payload of chunk.payloads) {
// Why not gated on a listener: the prompt lifecycle is main's own state, read by
// terminal waits that run with no status consumer attached.
this.recordAgentPromptLifecycleState(
ptyId,
mapExplicitAgentStateToRuntimeTerminalStatus(payload.state)
)
for (const target of targets) {
if (!this.onTerminalAgentStatus) {
continue
}
try {
this.onTerminalAgentStatus({
ptyId,
...target,
payload,
...provenance
})
} catch (err) {
console.error('[runtime] terminal agent status listener threw', {
ptyId,
paneKey: target.paneKey,
state: payload.state,
agentType: payload.agentType,
err
})
}
}
>()
}
}
/** The panes a PTY's in-band status belongs to: its mounted leaves, else its own pane record. */
private resolveTerminalAgentStatusTargets(ptyId: string): TerminalAgentStatusTarget[] {
const targets = new Map<string, TerminalAgentStatusTarget>()
const pty = this.ptysById.get(ptyId)
const connectionId = pty?.connectionId ?? null
for (const leaf of this.getLeavesForPty(ptyId)) {
@@ -136,34 +166,6 @@ export abstract class OrcaRuntimeWithCreateTerminalSideEffectCommandCodeDetector
}
}
}
for (const payload of chunk.payloads) {
// Why not gated on a listener: the prompt lifecycle is main's own state, read by
// terminal waits that run with no status consumer attached.
this.recordAgentPromptLifecycleState(
ptyId,
mapExplicitAgentStateToRuntimeTerminalStatus(payload.state)
)
for (const target of targets.values()) {
if (!this.onTerminalAgentStatus) {
continue
}
try {
this.onTerminalAgentStatus({
ptyId,
...target,
payload,
...provenance
})
} catch (err) {
console.error('[runtime] terminal agent status listener threw', {
ptyId,
paneKey: target.paneKey,
state: payload.state,
agentType: payload.agentType,
err
})
}
}
}
return [...targets.values()]
}
}
@@ -29,9 +29,9 @@ import { useTabStripPointerActivation } from './tab-strip-pointer-activation'
import { TerminalTabLeadingIcon } from './TerminalTabLeadingIcon'
import {
isTerminalTabActivityLive,
resolveTerminalTabActivityStatus,
terminalTabHasUnreadActivity
} from './terminal-tab-activity-status'
import { useTerminalTabActivity } from './use-terminal-tab-activity'
import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/close-all-context-menus'
import type { NativeChatTabSession } from './tab-session-history-switch'
@@ -115,17 +115,7 @@ export default function SortableTab({
unreadAgentCompletionPanes: s.unreadAgentCompletionPanes
})
)
// Why: resolver returns a primitive so unrelated agent updates can't repaint this tab (pane bucketing memoized per snapshot).
const activityStatus = useAppStore((s) =>
resolveTerminalTabActivityStatus({
tab,
agentStatusByPaneKey: s.agentStatusByPaneKey,
agentStatusEpoch: s.agentStatusEpoch,
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId,
ptyIdsByTabId: s.ptyIdsByTabId,
terminalLayout: s.terminalLayoutsByTabId?.[tab.id]
})
)
const { activityStatus } = useTerminalTabActivity(tab)
// Why: shellOverride is stamped at create time, so changing the default shell later won't repaint existing tabs.
const shellForIcon = tab.shellOverride
@@ -0,0 +1,28 @@
import { useAppStore } from '../../store'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import {
resolveTerminalTabActivityStatus,
type TerminalTabActivityStatus
} from './terminal-tab-activity-status'
type TerminalTabActivity = {
activityStatus: TerminalTabActivityStatus
}
/** A terminal tab's status glyph, as a primitive so unrelated agent updates can't repaint it. */
export function useTerminalTabActivity(
tab: Pick<TerminalTab, 'id' | 'title' | 'launchAgent'>
): TerminalTabActivity {
// Pane bucketing is memoized per snapshot, so every mounted tab shares one pass.
const activityStatus = useAppStore((s) =>
resolveTerminalTabActivityStatus({
tab,
agentStatusByPaneKey: s.agentStatusByPaneKey,
agentStatusEpoch: s.agentStatusEpoch,
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId,
ptyIdsByTabId: s.ptyIdsByTabId,
terminalLayout: s.terminalLayoutsByTabId?.[tab.id]
})
)
return { activityStatus }
}
@@ -0,0 +1,26 @@
import { useAppStore } from '@/store'
import { titleAfterConfirmedShell } from '../../../../../shared/terminal-pane-title-reset'
import type { ConnectPanePtySession } from './connect-pane-pty-session'
// Why: a plain `codex`/`grok` sets its OSC title and the shell never repaints
// it on exit, so a confirmed return-to-shell must clear a title that still
// names an agent — otherwise the tab reads "grok" over a bare prompt. Only
// reset an agent-named title; user/shell-set titles are left untouched.
export function clearStaleAgentTabTitleOnConfirmedShell(session: ConnectPanePtySession): void {
const state = useAppStore.getState()
const currentTitle = state.runtimePaneTitlesByTabId?.[session.deps.tabId]?.[session.pane.id]
const tab = (state.tabsByWorktree[session.deps.worktreeId] ?? []).find(
(entry) => entry.id === session.deps.tabId
)
const neutralTitle = titleAfterConfirmedShell(
currentTitle ?? tab?.title,
session.neutralTerminalTitle()
)
if (neutralTitle === null) {
return
}
session.deps.setRuntimePaneTitle(session.deps.tabId, session.pane.id, neutralTitle)
if (session.manager.getActivePane()?.id === session.pane.id) {
session.deps.updateTabTitle(session.deps.tabId, neutralTitle)
}
}
@@ -13,7 +13,6 @@ import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspec
import { dispatchTerminalCommandFinishedEvent } from '@/hooks/terminal-command-finished-event'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import type { TuiAgent } from '../../../../../shared/tui-agent'
import { titleAfterConfirmedShell } from '../../../../../shared/terminal-pane-title-reset'
import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../../shared/tui-agent-config'
import {
@@ -22,6 +21,7 @@ import {
type PaneLaunchAgentPaneSlice
} from './pane-launch-agent-candidate'
import type { ConnectPanePtySession } from './connect-pane-pty-session'
import { clearStaleAgentTabTitleOnConfirmedShell } from './confirmed-shell-agent-title'
/** Pane agent identity, foreground-agent sampling, and command lifecycle handling. */
export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
@@ -71,28 +71,8 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
isTuiAgent(registeredLaunchAgent)
)
}
// Why: a plain `codex`/`grok` sets its OSC title and the shell never repaints
// it on exit, so a confirmed return-to-shell must clear a title that still
// names an agent — otherwise the tab reads "grok" over a bare prompt. Only
// reset an agent-named title; user/shell-set titles are left untouched.
session.clearStaleAgentTabTitleOnConfirmedShell = (): void => {
const state = useAppStore.getState()
const currentTitle = state.runtimePaneTitlesByTabId?.[session.deps.tabId]?.[session.pane.id]
const tab = (state.tabsByWorktree[session.deps.worktreeId] ?? []).find(
(entry) => entry.id === session.deps.tabId
)
const neutralTitle = titleAfterConfirmedShell(
currentTitle ?? tab?.title,
session.neutralTerminalTitle()
)
if (neutralTitle === null) {
return
}
session.deps.setRuntimePaneTitle(session.deps.tabId, session.pane.id, neutralTitle)
if (session.manager.getActivePane()?.id === session.pane.id) {
session.deps.updateTabTitle(session.deps.tabId, neutralTitle)
}
}
session.clearStaleAgentTabTitleOnConfirmedShell = () =>
clearStaleAgentTabTitleOnConfirmedShell(session)
session.deferredCommandFinishedStatusDrop = null
session.deferredConfirmedShellReconcile = null
/** The pane's foreground was proven to be a shell, so its agent exited. Unlike a user dismissal,
@@ -10,7 +10,7 @@ import { parsePaneKey } from '../../../../shared/stable-pane-id'
import { resolveAgentStatusTerminalTitle } from '@/lib/agent-status-terminal-title'
import { track } from '@/lib/telemetry'
import { resolveAgentPaneAuthorityKey } from '@/store/slices/agent-pane-authority'
import type { AgentStatusBatchUpdate, AgentStatusUpdate } from '@/store/slices/agent-status'
import type { AgentStatusUpdate } from '@/store/slices/agent-status'
import { observeAgentHookCompletionForNotification } from '../agent-hook-completion-notifications'
import { useAppStore } from '../../store'
import {
@@ -35,6 +35,7 @@ import {
normalizeAgentStatusEvent,
normalizeAgentStatusMetadata
} from './normalize-agent-status-event'
import { applyProviderSessionOnlyUpdate } from './agent-status-partial-updates'
export function createAgentStatusEventApplicator(args: {
pendingAgentStatusEvents: PendingAgentStatusEvent[]
@@ -154,34 +155,17 @@ export function createAgentStatusEventApplicator(args: {
return 'dropped'
}
if (data.providerSessionOnly) {
if (!data.providerSession || data.agentType !== 'pi') {
return 'dropped'
}
const providerSessionUpdate: AgentStatusBatchUpdate = {
kind: 'providerSession',
return applyProviderSessionOnlyUpdate({
store,
batch: options?.batch,
data,
paneKey,
agent: 'pi',
providerSession: data.providerSession,
timing: { updatedAt: data.receivedAt },
routing: {
tabId: ownerTabId,
worktreeId: data.worktreeId ?? owningWorktreeId,
...(ownershipConnectionId !== undefined ? { connectionId: ownershipConnectionId } : {})
},
metadata: data.launchToken ? { launchToken: data.launchToken } : undefined
}
if (options?.batch) {
return options.batch.transaction.apply(providerSessionUpdate) ? 'applied' : 'dropped'
}
store.recordAgentProviderSession(
providerSessionUpdate.paneKey,
providerSessionUpdate.agent,
providerSessionUpdate.providerSession,
providerSessionUpdate.timing,
providerSessionUpdate.routing,
providerSessionUpdate.metadata
)
return 'applied'
}
})
}
const resolvedPayload = resolveHookPayloadAgentType(payload, identityTitle ?? title)
const statusPayload = data.orchestration
@@ -0,0 +1,40 @@
import type { AgentStatusIpcPayload } from '../../../../shared/agent-status-types'
import type { AppState } from '@/store/types'
import type { AgentProviderSessionUpdate } from '@/store/slices/agent-status'
import type { AgentStatusApplyResult, AgentStatusBatchContext } from './agent-status-bridge-types'
// Events that patch one aspect of an already-routed row; they never create, re-time or notify one.
export function applyProviderSessionOnlyUpdate(args: {
store: AppState
batch: AgentStatusBatchContext | undefined
data: AgentStatusIpcPayload
paneKey: string
routing: NonNullable<AgentProviderSessionUpdate['routing']>
}): AgentStatusApplyResult {
const { store, batch, data, paneKey, routing } = args
if (!data.providerSession || data.agentType !== 'pi') {
return 'dropped'
}
const update: AgentProviderSessionUpdate = {
kind: 'providerSession',
paneKey,
agent: 'pi',
providerSession: data.providerSession,
timing: { updatedAt: data.receivedAt },
routing,
metadata: data.launchToken ? { launchToken: data.launchToken } : undefined
}
if (batch) {
return batch.transaction.apply(update) ? 'applied' : 'dropped'
}
store.recordAgentProviderSession(
update.paneKey,
update.agent,
update.providerSession,
update.timing,
update.routing,
update.metadata
)
return 'applied'
}