mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 00:02:05 +00:00
fix(agent-status): keep process evidence enumerable
This commit is contained in:
@@ -30,7 +30,8 @@ export function useWorktreeActivityStatus(worktreeId: string): WorktreeStatus {
|
||||
hasLiveDone,
|
||||
hasRetainedDone,
|
||||
agentStatusPaneIdsByTabId,
|
||||
paneForegroundAgentByPaneKey
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey
|
||||
} = useAppStore(useShallow((s) => selectWorktreeAgentActivitySummary(s, worktreeId)))
|
||||
|
||||
// Why: compact and detailed cards need the same status-dot semantics:
|
||||
@@ -46,6 +47,7 @@ export function useWorktreeActivityStatus(worktreeId: string): WorktreeStatus {
|
||||
agentStatusPaneIdsByTabId,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey,
|
||||
hasPermission,
|
||||
hasLiveWorking,
|
||||
hasLiveMonitoring,
|
||||
@@ -61,6 +63,7 @@ export function useWorktreeActivityStatus(worktreeId: string): WorktreeStatus {
|
||||
agentStatusPaneIdsByTabId,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey,
|
||||
hasPermission,
|
||||
hasLiveWorking,
|
||||
hasLiveMonitoring,
|
||||
|
||||
@@ -23,7 +23,9 @@ type WorktreeActivityStatusState = Pick<
|
||||
| 'retainedAgentsByPaneKey'
|
||||
| 'runtimeAgentOrchestrationByPaneKey'
|
||||
| 'paneForegroundAgentByPaneKey'
|
||||
>
|
||||
> & {
|
||||
paneForegroundAgentObservationByPaneKey?: AppState['paneForegroundAgentObservationByPaneKey']
|
||||
}
|
||||
|
||||
export function selectWorktreeActivityStatuses(
|
||||
statusInputs: WorktreeActivityStatusState,
|
||||
@@ -39,7 +41,8 @@ export function selectWorktreeActivityStatuses(
|
||||
hasLiveDone,
|
||||
hasRetainedDone,
|
||||
agentStatusPaneIdsByTabId,
|
||||
paneForegroundAgentByPaneKey
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey
|
||||
} = selectWorktreeAgentActivitySummary(statusInputs, worktreeId)
|
||||
statuses.set(
|
||||
worktreeId,
|
||||
@@ -51,6 +54,7 @@ export function selectWorktreeActivityStatuses(
|
||||
agentStatusPaneIdsByTabId,
|
||||
terminalLayoutsByTabId: selectTerminalLayoutsForWorktree(statusInputs, worktreeId),
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey,
|
||||
hasPermission,
|
||||
hasLiveWorking,
|
||||
hasLiveMonitoring,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from './worktree-agent-freshness-selector'
|
||||
import {
|
||||
EMPTY_PANE_FOREGROUND_AGENTS,
|
||||
EMPTY_PANE_FOREGROUND_OBSERVATIONS,
|
||||
selectWorktreeAgentActivitySummary
|
||||
} from './worktree-agent-activity-summary'
|
||||
|
||||
@@ -85,6 +86,11 @@ export function useWorktreeAgentRows(worktreeId: string, active = true): Dashboa
|
||||
? selectWorktreeAgentActivitySummary(s, worktreeId).paneForegroundAgentByPaneKey
|
||||
: EMPTY_PANE_FOREGROUND_AGENTS
|
||||
)
|
||||
const paneForegroundAgentObservationByPaneKey = useAppStore((s) =>
|
||||
active
|
||||
? selectWorktreeAgentActivitySummary(s, worktreeId).paneForegroundAgentObservationByPaneKey
|
||||
: EMPTY_PANE_FOREGROUND_OBSERVATIONS
|
||||
)
|
||||
const agentFreshnessSignature = useAppStore((s) =>
|
||||
active ? selectAgentFreshness(s) : EMPTY_WORKTREE_AGENT_FRESHNESS_SIGNATURE
|
||||
)
|
||||
@@ -116,6 +122,7 @@ export function useWorktreeAgentRows(worktreeId: string, active = true): Dashboa
|
||||
terminalLayoutsByTabId,
|
||||
runtimeAgentOrchestrationByPaneKey,
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey,
|
||||
now
|
||||
})
|
||||
)
|
||||
@@ -131,6 +138,7 @@ export function useWorktreeAgentRows(worktreeId: string, active = true): Dashboa
|
||||
terminalLayoutsByTabId,
|
||||
runtimeAgentOrchestrationByPaneKey,
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey,
|
||||
agentFreshnessSignature
|
||||
])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
export function paneForegroundAgentsEqual<T>(
|
||||
previous: Record<string, T>,
|
||||
next: Record<string, T>
|
||||
): boolean {
|
||||
if (previous === next) {
|
||||
return true
|
||||
}
|
||||
const previousKeys = Object.keys(previous)
|
||||
if (previousKeys.length !== Object.keys(next).length) {
|
||||
return false
|
||||
}
|
||||
return previousKeys.every((paneKey) => previous[paneKey] === next[paneKey])
|
||||
}
|
||||
|
||||
export function agentStatusPaneIdsByTabIdEqual(
|
||||
previous: Record<string, ReadonlySet<string>>,
|
||||
next: Record<string, ReadonlySet<string>>
|
||||
): boolean {
|
||||
if (previous === next) {
|
||||
return true
|
||||
}
|
||||
const previousKeys = Object.keys(previous)
|
||||
if (previousKeys.length !== Object.keys(next).length) {
|
||||
return false
|
||||
}
|
||||
for (const tabId of previousKeys) {
|
||||
const previousPaneIds = previous[tabId]
|
||||
const nextPaneIds = next[tabId]
|
||||
if (!nextPaneIds || previousPaneIds.size !== nextPaneIds.size) {
|
||||
return false
|
||||
}
|
||||
for (const paneId of previousPaneIds) {
|
||||
if (!nextPaneIds.has(paneId)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -8,10 +8,17 @@ import {
|
||||
} from '@/lib/agent-status-worktree-attribution'
|
||||
import {
|
||||
AGENT_STATUS_STALE_AFTER_MS,
|
||||
type AgentStatusEntry,
|
||||
type AgentStatusOrchestrationContext
|
||||
} from '../../../../shared/agent-status-types'
|
||||
import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
PaneForegroundAgentEntry,
|
||||
PaneForegroundAgentObservation
|
||||
} from '@/store/slices/pane-foreground-agent'
|
||||
import {
|
||||
agentStatusPaneIdsByTabIdEqual,
|
||||
paneForegroundAgentsEqual
|
||||
} from './worktree-agent-activity-summary-equality'
|
||||
import { applyLiveAgentState } from './worktree-agent-state-flags'
|
||||
|
||||
export type WorktreeAgentActivitySummary = {
|
||||
hasPermission: boolean
|
||||
@@ -24,10 +31,12 @@ export type WorktreeAgentActivitySummary = {
|
||||
agentStatusPaneIdsByTabId: Record<string, ReadonlySet<string>>
|
||||
/** Process identity narrowed to this worktree's panes. */
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry>
|
||||
paneForegroundAgentObservationByPaneKey: Record<string, PaneForegroundAgentObservation>
|
||||
}
|
||||
|
||||
const EMPTY_AGENT_STATUS_PANE_IDS_BY_TAB_ID: Record<string, ReadonlySet<string>> = {}
|
||||
export const EMPTY_PANE_FOREGROUND_AGENTS: Record<string, PaneForegroundAgentEntry> = {}
|
||||
export const EMPTY_PANE_FOREGROUND_OBSERVATIONS: Record<string, PaneForegroundAgentObservation> = {}
|
||||
|
||||
const EMPTY_SUMMARY: WorktreeAgentActivitySummary = {
|
||||
hasPermission: false,
|
||||
@@ -37,7 +46,8 @@ const EMPTY_SUMMARY: WorktreeAgentActivitySummary = {
|
||||
hasLiveDone: false,
|
||||
hasRetainedDone: false,
|
||||
agentStatusPaneIdsByTabId: EMPTY_AGENT_STATUS_PANE_IDS_BY_TAB_ID,
|
||||
paneForegroundAgentByPaneKey: EMPTY_PANE_FOREGROUND_AGENTS
|
||||
paneForegroundAgentByPaneKey: EMPTY_PANE_FOREGROUND_AGENTS,
|
||||
paneForegroundAgentObservationByPaneKey: EMPTY_PANE_FOREGROUND_OBSERVATIONS
|
||||
}
|
||||
|
||||
type AgentActivityTabsByWorktree = Record<string, readonly { id: string }[]>
|
||||
@@ -52,6 +62,7 @@ export type AgentActivityInput = Pick<
|
||||
tabsByWorktree: AgentActivityTabsByWorktree
|
||||
runtimeAgentOrchestrationByPaneKey?: AppState['runtimeAgentOrchestrationByPaneKey']
|
||||
paneForegroundAgentByPaneKey?: AppState['paneForegroundAgentByPaneKey']
|
||||
paneForegroundAgentObservationByPaneKey?: AppState['paneForegroundAgentObservationByPaneKey']
|
||||
}
|
||||
|
||||
type AgentActivityCache = {
|
||||
@@ -61,6 +72,9 @@ type AgentActivityCache = {
|
||||
retainedAgentsByPaneKey: AppState['retainedAgentsByPaneKey']
|
||||
runtimeAgentOrchestrationByPaneKey: AppState['runtimeAgentOrchestrationByPaneKey'] | undefined
|
||||
paneForegroundAgentByPaneKey: AppState['paneForegroundAgentByPaneKey'] | undefined
|
||||
paneForegroundAgentObservationByPaneKey:
|
||||
| AppState['paneForegroundAgentObservationByPaneKey']
|
||||
| undefined
|
||||
summaries: Map<string, WorktreeAgentActivitySummary>
|
||||
}
|
||||
|
||||
@@ -78,6 +92,7 @@ function getWorktreeAgentActivitySummaries(
|
||||
): Map<string, WorktreeAgentActivitySummary> {
|
||||
const runtimeAgentOrchestrationByPaneKey = state.runtimeAgentOrchestrationByPaneKey
|
||||
const paneForegroundAgentByPaneKey = state.paneForegroundAgentByPaneKey
|
||||
const paneForegroundAgentObservationByPaneKey = state.paneForegroundAgentObservationByPaneKey
|
||||
if (
|
||||
agentActivityCache &&
|
||||
agentActivityCache.tabsByWorktree === state.tabsByWorktree &&
|
||||
@@ -85,7 +100,9 @@ function getWorktreeAgentActivitySummaries(
|
||||
agentActivityCache.migrationUnsupportedByPtyId === state.migrationUnsupportedByPtyId &&
|
||||
agentActivityCache.retainedAgentsByPaneKey === state.retainedAgentsByPaneKey &&
|
||||
agentActivityCache.runtimeAgentOrchestrationByPaneKey === runtimeAgentOrchestrationByPaneKey &&
|
||||
agentActivityCache.paneForegroundAgentByPaneKey === paneForegroundAgentByPaneKey
|
||||
agentActivityCache.paneForegroundAgentByPaneKey === paneForegroundAgentByPaneKey &&
|
||||
agentActivityCache.paneForegroundAgentObservationByPaneKey ===
|
||||
paneForegroundAgentObservationByPaneKey
|
||||
) {
|
||||
return agentActivityCache.summaries
|
||||
}
|
||||
@@ -111,7 +128,7 @@ function getWorktreeAgentActivitySummaries(
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
for (const [paneKey, entry] of Object.entries(state.agentStatusByPaneKey)) {
|
||||
for (const [paneKey, entry] of Object.entries(state.agentStatusByPaneKey ?? {})) {
|
||||
const paneIdentity = parseAgentStatusPaneIdentity(paneKey)
|
||||
if (!paneIdentity) {
|
||||
continue
|
||||
@@ -160,8 +177,13 @@ function getWorktreeAgentActivitySummaries(
|
||||
const summary = summaryForWorktree(worktreeId)
|
||||
if (summary.paneForegroundAgentByPaneKey === EMPTY_PANE_FOREGROUND_AGENTS) {
|
||||
summary.paneForegroundAgentByPaneKey = {}
|
||||
summary.paneForegroundAgentObservationByPaneKey = {}
|
||||
}
|
||||
summary.paneForegroundAgentByPaneKey[paneKey] = entry
|
||||
const observation = paneForegroundAgentObservationByPaneKey?.[paneKey]
|
||||
if (observation) {
|
||||
summary.paneForegroundAgentObservationByPaneKey[paneKey] = observation
|
||||
}
|
||||
}
|
||||
|
||||
for (const retained of Object.values(state.retainedAgentsByPaneKey ?? {})) {
|
||||
@@ -197,6 +219,7 @@ function getWorktreeAgentActivitySummaries(
|
||||
retainedAgentsByPaneKey: state.retainedAgentsByPaneKey,
|
||||
runtimeAgentOrchestrationByPaneKey,
|
||||
paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey,
|
||||
summaries
|
||||
}
|
||||
return summaries
|
||||
@@ -220,70 +243,14 @@ function summariesEqual(
|
||||
paneForegroundAgentsEqual(
|
||||
previous.paneForegroundAgentByPaneKey,
|
||||
next.paneForegroundAgentByPaneKey
|
||||
) &&
|
||||
paneForegroundAgentsEqual(
|
||||
previous.paneForegroundAgentObservationByPaneKey,
|
||||
next.paneForegroundAgentObservationByPaneKey
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function paneForegroundAgentsEqual(
|
||||
previous: Record<string, PaneForegroundAgentEntry>,
|
||||
next: Record<string, PaneForegroundAgentEntry>
|
||||
): boolean {
|
||||
if (previous === next) {
|
||||
return true
|
||||
}
|
||||
const previousKeys = Object.keys(previous)
|
||||
if (previousKeys.length !== Object.keys(next).length) {
|
||||
return false
|
||||
}
|
||||
return previousKeys.every((paneKey) => previous[paneKey] === next[paneKey])
|
||||
}
|
||||
|
||||
function agentStatusPaneIdsByTabIdEqual(
|
||||
previous: Record<string, ReadonlySet<string>>,
|
||||
next: Record<string, ReadonlySet<string>>
|
||||
): boolean {
|
||||
if (previous === next) {
|
||||
return true
|
||||
}
|
||||
const previousKeys = Object.keys(previous)
|
||||
if (previousKeys.length !== Object.keys(next).length) {
|
||||
return false
|
||||
}
|
||||
for (const tabId of previousKeys) {
|
||||
const previousPaneIds = previous[tabId]
|
||||
const nextPaneIds = next[tabId]
|
||||
if (!nextPaneIds || previousPaneIds.size !== nextPaneIds.size) {
|
||||
return false
|
||||
}
|
||||
for (const paneId of previousPaneIds) {
|
||||
if (!nextPaneIds.has(paneId)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function applyLiveAgentState(
|
||||
summary: WorktreeAgentActivitySummary,
|
||||
entry: Pick<AgentStatusEntry, 'state' | 'workingMode' | 'interrupted'>
|
||||
): void {
|
||||
if (entry.state === 'blocked' || entry.state === 'waiting') {
|
||||
summary.hasPermission = true
|
||||
} else if (entry.interrupted === true) {
|
||||
// Interrupted is encoded as done, so it must be checked first.
|
||||
summary.hasInterrupted = true
|
||||
} else if (entry.state === 'working') {
|
||||
if (entry.workingMode === 'monitoring') {
|
||||
summary.hasLiveMonitoring = true
|
||||
} else {
|
||||
summary.hasLiveWorking = true
|
||||
}
|
||||
} else if (entry.state === 'done') {
|
||||
summary.hasLiveDone = true
|
||||
}
|
||||
}
|
||||
|
||||
function addAgentStatusPaneId(
|
||||
summary: WorktreeAgentActivitySummary,
|
||||
tabId: string,
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { formatAgentTypeLabel } from '@/lib/agent-status'
|
||||
import {
|
||||
resolveFreshPaneForegroundAgent,
|
||||
type PaneForegroundAgentEntry
|
||||
type PaneForegroundAgentEntry,
|
||||
type PaneForegroundAgentObservation
|
||||
} from '@/store/slices/pane-foreground-agent'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
@@ -19,6 +20,9 @@ export function freshProcessAgentForLeaf(args: {
|
||||
layout: TerminalLayoutSnapshot | undefined
|
||||
livePtyIds: readonly string[] | undefined
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry> | undefined
|
||||
paneForegroundAgentObservationByPaneKey:
|
||||
| Record<string, PaneForegroundAgentObservation>
|
||||
| undefined
|
||||
now: number
|
||||
}): TuiAgent | null {
|
||||
if (!args.paneForegroundAgentByPaneKey || !isTerminalLeafId(args.leafId)) {
|
||||
@@ -26,6 +30,7 @@ export function freshProcessAgentForLeaf(args: {
|
||||
}
|
||||
return resolveFreshPaneForegroundAgent(
|
||||
args.paneForegroundAgentByPaneKey[makePaneKey(args.tabId, args.leafId)],
|
||||
args.paneForegroundAgentObservationByPaneKey?.[makePaneKey(args.tabId, args.leafId)],
|
||||
{
|
||||
now: args.now,
|
||||
paneBoundPtyId: args.layout?.ptyIdsByLeafId?.[args.leafId],
|
||||
@@ -40,6 +45,9 @@ export function appendProcessDerivedAgentRows(args: {
|
||||
ptyIdsByTabId: Record<string, string[]>
|
||||
terminalLayoutsByTabId: Record<string, TerminalLayoutSnapshot | undefined>
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry> | undefined
|
||||
paneForegroundAgentObservationByPaneKey:
|
||||
| Record<string, PaneForegroundAgentObservation>
|
||||
| undefined
|
||||
seenPaneKeys: Set<string>
|
||||
rows: DashboardAgentRow[]
|
||||
now: number
|
||||
@@ -62,6 +70,7 @@ export function appendProcessDerivedAgentRows(args: {
|
||||
layout: args.terminalLayoutsByTabId[tab.id],
|
||||
livePtyIds: args.ptyIdsByTabId[tab.id],
|
||||
paneForegroundAgentByPaneKey: args.paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey: args.paneForegroundAgentObservationByPaneKey,
|
||||
now: args.now
|
||||
})
|
||||
if (!processAgent) {
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from './worktree-agent-row-fallback-tab'
|
||||
import { resolveRowAgentType } from './worktree-agent-row-type'
|
||||
import { entryWithRuntimeOrchestration } from './worktree-agent-row-orchestration'
|
||||
import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
PaneForegroundAgentEntry,
|
||||
PaneForegroundAgentObservation
|
||||
} from '@/store/slices/pane-foreground-agent'
|
||||
|
||||
function countTerminalLayoutLeaves(node: TerminalPaneLayoutNode | null | undefined): number {
|
||||
if (!node) {
|
||||
@@ -146,6 +149,7 @@ export function buildWorktreeAgentRows(args: {
|
||||
terminalLayoutsByTabId?: Record<string, TerminalLayoutSnapshot | undefined>
|
||||
runtimeAgentOrchestrationByPaneKey?: Record<string, AgentStatusOrchestrationContext>
|
||||
paneForegroundAgentByPaneKey?: Record<string, PaneForegroundAgentEntry>
|
||||
paneForegroundAgentObservationByPaneKey?: Record<string, PaneForegroundAgentObservation>
|
||||
now: number
|
||||
}): DashboardAgentRow[] {
|
||||
const rows: DashboardAgentRow[] = []
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import type { WorktreeAgentActivitySummary } from './worktree-agent-activity-summary'
|
||||
|
||||
export function applyLiveAgentState(
|
||||
summary: WorktreeAgentActivitySummary,
|
||||
entry: Pick<AgentStatusEntry, 'state' | 'workingMode' | 'interrupted'>
|
||||
): void {
|
||||
if (entry.state === 'blocked' || entry.state === 'waiting') {
|
||||
summary.hasPermission = true
|
||||
} else if (entry.interrupted === true) {
|
||||
// Interrupted is encoded as done, so it must be checked first.
|
||||
summary.hasInterrupted = true
|
||||
} else if (entry.state === 'working') {
|
||||
if (entry.workingMode === 'monitoring') {
|
||||
summary.hasLiveMonitoring = true
|
||||
} else {
|
||||
summary.hasLiveWorking = true
|
||||
}
|
||||
} else if (entry.state === 'done') {
|
||||
summary.hasLiveDone = true
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ import { applyAgentRowLineage } from '@/components/dashboard/agent-row-lineage'
|
||||
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
PaneForegroundAgentEntry,
|
||||
PaneForegroundAgentObservation
|
||||
} from '@/store/slices/pane-foreground-agent'
|
||||
import { buildWorktreeAgentRows } from './worktree-agent-rows'
|
||||
|
||||
const LEAF_ID_1 = '77777777-7777-4777-8777-777777777777'
|
||||
@@ -49,10 +52,9 @@ describe('buildTitleDerivedAgentRows', () => {
|
||||
const paneKey = makePaneKey('tab-1', LEAF_ID_1)
|
||||
const foreground: PaneForegroundAgentEntry = {
|
||||
agent: 'claude',
|
||||
shellForeground: false,
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-claude'
|
||||
shellForeground: false
|
||||
}
|
||||
const observation: PaneForegroundAgentObservation = { observedAt: 1_000, ptyId: 'pty-claude' }
|
||||
const rows = buildWorktreeAgentRows({
|
||||
tabs: [makeTab('tab-1', { title: 'zsh' })],
|
||||
entries: [],
|
||||
@@ -60,6 +62,7 @@ describe('buildTitleDerivedAgentRows', () => {
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-claude'] },
|
||||
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
|
||||
paneForegroundAgentByPaneKey: { [paneKey]: foreground },
|
||||
paneForegroundAgentObservationByPaneKey: { [paneKey]: observation },
|
||||
now: 1_000 + 1_000
|
||||
})
|
||||
|
||||
@@ -79,11 +82,12 @@ describe('buildTitleDerivedAgentRows', () => {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
[paneKey]: {
|
||||
agent: 'claude',
|
||||
shellForeground: false,
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-old'
|
||||
shellForeground: false
|
||||
}
|
||||
},
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
[paneKey]: { observedAt: 1_000, ptyId: 'pty-old' }
|
||||
},
|
||||
now: 2_000
|
||||
})
|
||||
|
||||
@@ -101,11 +105,12 @@ describe('buildTitleDerivedAgentRows', () => {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
[paneKey]: {
|
||||
agent: 'claude',
|
||||
shellForeground: false,
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-claude'
|
||||
shellForeground: false
|
||||
}
|
||||
},
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
[paneKey]: { observedAt: 1_000, ptyId: 'pty-claude' }
|
||||
},
|
||||
now: 31_001
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { formatAgentTypeLabel, isClaudeManagementTitle } from '@/lib/agent-statu
|
||||
import { isCursorAgentTitle } from '../../../../shared/agent-title-core'
|
||||
import { classifyTitleActivity, resolveTitleActivityLabel } from '@/lib/pane-agent-evidence'
|
||||
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
|
||||
import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
PaneForegroundAgentEntry,
|
||||
PaneForegroundAgentObservation
|
||||
} from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
AgentStatusEntry,
|
||||
AgentStatusOrchestrationContext,
|
||||
@@ -46,6 +49,7 @@ export function buildTitleDerivedAgentRows(args: {
|
||||
terminalLayoutsByTabId?: Record<string, TerminalLayoutSnapshot | undefined>
|
||||
runtimeAgentOrchestrationByPaneKey?: Record<string, AgentStatusOrchestrationContext>
|
||||
paneForegroundAgentByPaneKey?: Record<string, PaneForegroundAgentEntry>
|
||||
paneForegroundAgentObservationByPaneKey?: Record<string, PaneForegroundAgentObservation>
|
||||
seenPaneKeys: Set<string>
|
||||
now: number
|
||||
}): DashboardAgentRow[] {
|
||||
@@ -89,6 +93,7 @@ export function buildTitleDerivedAgentRows(args: {
|
||||
layout,
|
||||
livePtyIds: ptyIdsByTabId[tab.id],
|
||||
paneForegroundAgentByPaneKey: args.paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey: args.paneForegroundAgentObservationByPaneKey,
|
||||
now: args.now
|
||||
})
|
||||
})
|
||||
@@ -118,6 +123,7 @@ export function buildTitleDerivedAgentRows(args: {
|
||||
layout,
|
||||
livePtyIds: ptyIdsByTabId[tab.id],
|
||||
paneForegroundAgentByPaneKey: args.paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey: args.paneForegroundAgentObservationByPaneKey,
|
||||
now: args.now
|
||||
})
|
||||
})
|
||||
@@ -133,6 +139,7 @@ export function buildTitleDerivedAgentRows(args: {
|
||||
ptyIdsByTabId,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey: args.paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey: args.paneForegroundAgentObservationByPaneKey,
|
||||
seenPaneKeys: args.seenPaneKeys,
|
||||
rows,
|
||||
now: args.now
|
||||
|
||||
+34
-13
@@ -272,10 +272,17 @@ describe('connectPanePty', () => {
|
||||
await advanceVisibleForegroundRead()
|
||||
|
||||
expect(foregroundReadCallsFor(ptyId)).toEqual([[ptyId]])
|
||||
expect(mockStoreState.setPaneForegroundAgent).toHaveBeenCalledWith(cacheKey, {
|
||||
agent: 'codex',
|
||||
shellForeground: false
|
||||
})
|
||||
expect(mockStoreState.setPaneForegroundAgent).toHaveBeenCalledWith(
|
||||
cacheKey,
|
||||
{
|
||||
agent: 'codex',
|
||||
shellForeground: false
|
||||
},
|
||||
{
|
||||
observedAt: expect.any(Number),
|
||||
ptyId
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('does not sample hidden restored PTYs', async () => {
|
||||
@@ -758,10 +765,17 @@ describe('connectPanePty', () => {
|
||||
|
||||
await advanceVisibleForegroundRead()
|
||||
|
||||
expect(mockStoreState.setPaneForegroundAgent).toHaveBeenCalledWith(cacheKey, {
|
||||
agent: null,
|
||||
shellForeground: true
|
||||
})
|
||||
expect(mockStoreState.setPaneForegroundAgent).toHaveBeenCalledWith(
|
||||
cacheKey,
|
||||
{
|
||||
agent: null,
|
||||
shellForeground: true
|
||||
},
|
||||
{
|
||||
observedAt: expect.any(Number),
|
||||
ptyId
|
||||
}
|
||||
)
|
||||
expect(foregroundReadCallsFor(ptyId)).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -807,11 +821,18 @@ describe('connectPanePty', () => {
|
||||
agent: null,
|
||||
shellForeground: true
|
||||
})
|
||||
expect(mockStoreState.setPaneForegroundAgent).toHaveBeenCalledWith(cacheKey, {
|
||||
agent: 'droid',
|
||||
routingTrusted: true,
|
||||
shellForeground: false
|
||||
})
|
||||
expect(mockStoreState.setPaneForegroundAgent).toHaveBeenCalledWith(
|
||||
cacheKey,
|
||||
{
|
||||
agent: 'droid',
|
||||
routingTrusted: true,
|
||||
shellForeground: false
|
||||
},
|
||||
{
|
||||
observedAt: expect.any(Number),
|
||||
ptyId
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('never probes the foreground for a visible remote/SSH restored pane', async () => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence'
|
||||
import type { TuiAgent } from '../../../../../shared/tui-agent'
|
||||
import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../../shared/tui-agent-config'
|
||||
import { createPaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
|
||||
import type { ConnectPanePtySession } from './connect-pane-pty-session'
|
||||
|
||||
@@ -159,13 +158,10 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
|
||||
// Bind observations to the PTY that was inspected; a pane key may survive
|
||||
// a rebind while the old process identity is still in the store.
|
||||
publish: (entry) =>
|
||||
useAppStore.getState().setPaneForegroundAgent(
|
||||
session.cacheKey,
|
||||
createPaneForegroundAgentEntry(entry, {
|
||||
observedAt: Date.now(),
|
||||
ptyId: session.transport.getPtyId() ?? undefined
|
||||
})
|
||||
),
|
||||
useAppStore.getState().setPaneForegroundAgent(session.cacheKey, entry, {
|
||||
observedAt: Date.now(),
|
||||
ptyId: session.transport.getPtyId() ?? undefined
|
||||
}),
|
||||
hasKnownAgentIdentity: session.paneHasKnownAgentIdentity,
|
||||
onConfirmedShellForeground: (reason) => {
|
||||
session.clearStaleAgentTabTitleOnConfirmedShell()
|
||||
@@ -199,17 +195,11 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
|
||||
if (foreground?.routingConfirmationPending !== true) {
|
||||
return
|
||||
}
|
||||
useAppStore.getState().setPaneForegroundAgent(
|
||||
session.cacheKey,
|
||||
createPaneForegroundAgentEntry(
|
||||
{
|
||||
agent: foreground.agent,
|
||||
routingRevoked: true,
|
||||
shellForeground: foreground.shellForeground
|
||||
},
|
||||
{ ptyId: session.transport.getPtyId() ?? undefined }
|
||||
)
|
||||
)
|
||||
useAppStore.getState().setPaneForegroundAgent(session.cacheKey, {
|
||||
agent: foreground.agent,
|
||||
routingRevoked: true,
|
||||
shellForeground: foreground.shellForeground
|
||||
})
|
||||
}
|
||||
})
|
||||
// Why: one command-finished policy whether the signal arrives as bytes
|
||||
@@ -303,33 +293,23 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void {
|
||||
}
|
||||
// Why: cmd.exe and Git Bash have no OSC command boundaries. Keep the icon
|
||||
// as a hint, but revoke bytes until one current provider confirmation lands.
|
||||
useAppStore
|
||||
.getState()
|
||||
.setPaneForegroundAgent(
|
||||
session.cacheKey,
|
||||
createPaneForegroundAgentEntry(
|
||||
{ agent: foreground.agent, routingRevoked: true, shellForeground: false },
|
||||
{ ptyId: session.transport.getPtyId() ?? undefined }
|
||||
)
|
||||
)
|
||||
useAppStore.getState().setPaneForegroundAgent(session.cacheKey, {
|
||||
agent: foreground.agent,
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
})
|
||||
session.visibleForegroundSamplePending = false
|
||||
session.visibleForegroundSampleSettled = false
|
||||
// Why: hook rows can suppress display-only sampling, but cannot restore
|
||||
// byte authority after this function explicitly revoked routing trust.
|
||||
session.sampleVisiblePaneForegroundAgent(true)
|
||||
if (session.paneForegroundAgentTracker.hasReadInFlight()) {
|
||||
useAppStore.getState().setPaneForegroundAgent(
|
||||
session.cacheKey,
|
||||
createPaneForegroundAgentEntry(
|
||||
{
|
||||
agent: foreground.agent,
|
||||
routingRevoked: true,
|
||||
shellForeground: false,
|
||||
routingConfirmationPending: true
|
||||
},
|
||||
{ ptyId: session.transport.getPtyId() ?? undefined }
|
||||
)
|
||||
)
|
||||
useAppStore.getState().setPaneForegroundAgent(session.cacheKey, {
|
||||
agent: foreground.agent,
|
||||
routingRevoked: true,
|
||||
shellForeground: false,
|
||||
routingConfirmationPending: true
|
||||
})
|
||||
}
|
||||
}
|
||||
session.commandLifecycle = createTerminalCommandLifecycle({
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
import type { ConnectPanePtySession } from './connect-pane-pty-session'
|
||||
|
||||
import { installCommandInferredPaneAgent } from './command-inferred-pane-agent'
|
||||
import { createPaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
|
||||
export function installSleepingRecordAccess(session: ConnectPanePtySession): void {
|
||||
session.getSleepingRecordForPane = (
|
||||
@@ -160,15 +159,10 @@ export function installSleepingRecordAccess(session: ConnectPanePtySession): voi
|
||||
if (metadata?.launchAgent) {
|
||||
// Why: daemon launch identity can outlive the process while Orca is
|
||||
// closed. Use it to request confirmation, never as current byte authority.
|
||||
useAppStore
|
||||
.getState()
|
||||
.setPaneForegroundAgent(
|
||||
session.cacheKey,
|
||||
createPaneForegroundAgentEntry(
|
||||
{ agent: metadata.launchAgent, shellForeground: false },
|
||||
{ ptyId: session.transport.getPtyId() ?? undefined }
|
||||
)
|
||||
)
|
||||
useAppStore.getState().setPaneForegroundAgent(session.cacheKey, {
|
||||
agent: metadata.launchAgent,
|
||||
shellForeground: false
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,11 +158,12 @@ describe('getWorktreeStatus', () => {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
[makePaneKey('tab-1', LEAF_ID_1)]: {
|
||||
agent: 'claude',
|
||||
shellForeground: false,
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-old'
|
||||
shellForeground: false
|
||||
}
|
||||
},
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
[makePaneKey('tab-1', LEAF_ID_1)]: { observedAt: 1_000, ptyId: 'pty-old' }
|
||||
},
|
||||
now: 2_000
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,7 +6,10 @@ import { classifyTitleActivity } from '@/lib/pane-agent-evidence'
|
||||
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
|
||||
import { resolveRuntimePaneTitleLeafIdFromRoot } from '@/lib/runtime-pane-title-leaf-id'
|
||||
import { containsAgentSpinnerGlyph } from '../../../shared/agent-title-core'
|
||||
import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
PaneForegroundAgentEntry,
|
||||
PaneForegroundAgentObservation
|
||||
} from '@/store/slices/pane-foreground-agent'
|
||||
import type {
|
||||
TerminalLayoutSnapshot,
|
||||
TerminalPaneLayoutNode,
|
||||
@@ -30,6 +33,7 @@ type WorktreeStatusHeuristicOptions = {
|
||||
terminalLayoutsByTabId?: Record<string, TerminalLayoutSnapshot | undefined>
|
||||
terminalLayoutRootsByTabId?: Record<string, TerminalPaneLayoutNode | null | undefined>
|
||||
paneForegroundAgentByPaneKey?: Record<string, PaneForegroundAgentEntry>
|
||||
paneForegroundAgentObservationByPaneKey?: Record<string, PaneForegroundAgentObservation>
|
||||
now?: number
|
||||
}
|
||||
|
||||
@@ -92,6 +96,7 @@ function tabHasStatus(
|
||||
layout: options.terminalLayoutsByTabId?.[tab.id],
|
||||
livePtyIds: ptyIdsByTabId[tab.id],
|
||||
paneForegroundAgentByPaneKey: options.paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey: options.paneForegroundAgentObservationByPaneKey,
|
||||
now: options.now ?? Date.now()
|
||||
})
|
||||
const paneTitles = runtimePaneTitlesByTabId[tab.id]
|
||||
@@ -178,6 +183,7 @@ export function resolveWorktreeStatus(args: {
|
||||
terminalLayoutsByTabId?: Record<string, TerminalLayoutSnapshot | undefined>
|
||||
terminalLayoutRootsByTabId?: Record<string, TerminalPaneLayoutNode | null | undefined>
|
||||
paneForegroundAgentByPaneKey?: Record<string, PaneForegroundAgentEntry>
|
||||
paneForegroundAgentObservationByPaneKey?: Record<string, PaneForegroundAgentObservation>
|
||||
now?: number
|
||||
hasPermission: boolean
|
||||
hasLiveWorking: boolean
|
||||
@@ -196,6 +202,7 @@ export function resolveWorktreeStatus(args: {
|
||||
terminalLayoutsByTabId: args.terminalLayoutsByTabId,
|
||||
terminalLayoutRootsByTabId: args.terminalLayoutRootsByTabId,
|
||||
paneForegroundAgentByPaneKey: args.paneForegroundAgentByPaneKey,
|
||||
paneForegroundAgentObservationByPaneKey: args.paneForegroundAgentObservationByPaneKey,
|
||||
now: args.now
|
||||
}
|
||||
)
|
||||
|
||||
@@ -286,6 +286,7 @@ export type WebSessionTabsSyncState = Pick<
|
||||
| 'automaticAgentResumeClaimsByTabId'
|
||||
| 'migrationUnsupportedByPtyId'
|
||||
| 'paneForegroundAgentByPaneKey'
|
||||
| 'paneForegroundAgentObservationByPaneKey'
|
||||
| 'pendingStartupByTabId'
|
||||
| 'recentlyClosedAgentStatusTabIds'
|
||||
| 'recentlyRetiredAgentStatusPaneKeys'
|
||||
@@ -1400,6 +1401,7 @@ function buildRetractedMirroredTabSweepPatch(
|
||||
agentStatusEpoch: agentStatusPatch?.agentStatusEpoch ?? state.agentStatusEpoch,
|
||||
migrationUnsupportedByPtyId: state.migrationUnsupportedByPtyId ?? {},
|
||||
paneForegroundAgentByPaneKey: state.paneForegroundAgentByPaneKey ?? {},
|
||||
paneForegroundAgentObservationByPaneKey: state.paneForegroundAgentObservationByPaneKey ?? {},
|
||||
recentlyClosedAgentStatusTabIds: state.recentlyClosedAgentStatusTabIds ?? {},
|
||||
recentlyRetiredAgentStatusPaneKeys: state.recentlyRetiredAgentStatusPaneKeys ?? {},
|
||||
retainedAgentsByPaneKey: state.retainedAgentsByPaneKey ?? {},
|
||||
|
||||
@@ -1675,6 +1675,10 @@ export const createAgentStatusSlice: StateCreator<AppState, [], [], AgentStatusS
|
||||
s.paneForegroundAgentByPaneKey,
|
||||
retiredPaneKeySet
|
||||
),
|
||||
paneForegroundAgentObservationByPaneKey: removePaneKeys(
|
||||
s.paneForegroundAgentObservationByPaneKey,
|
||||
retiredPaneKeySet
|
||||
),
|
||||
unreadTerminalPanes: removePaneKeys(s.unreadTerminalPanes, retiredPaneKeySet),
|
||||
unreadAgentCompletionPanes: removePaneKeys(
|
||||
s.unreadAgentCompletionPanes,
|
||||
@@ -1800,6 +1804,11 @@ export const createAgentStatusSlice: StateCreator<AppState, [], [], AgentStatusS
|
||||
),
|
||||
acknowledgedAgentsByPaneKey: movePaneKeyedRecord(s.acknowledgedAgentsByPaneKey, from, to),
|
||||
paneForegroundAgentByPaneKey: movePaneKeyedRecord(s.paneForegroundAgentByPaneKey, from, to),
|
||||
paneForegroundAgentObservationByPaneKey: movePaneKeyedRecord(
|
||||
s.paneForegroundAgentObservationByPaneKey,
|
||||
from,
|
||||
to
|
||||
),
|
||||
unreadTerminalPanes: movePaneKeyedRecord(s.unreadTerminalPanes, from, to),
|
||||
unreadAgentCompletionPanes: movePaneKeyedRecord(s.unreadAgentCompletionPanes, from, to),
|
||||
lastTerminalInputAtByPaneKey: movePaneKeyedRecord(s.lastTerminalInputAtByPaneKey, from, to),
|
||||
|
||||
@@ -16,20 +16,27 @@ function terminalTab(id: string, worktreeId: string): TerminalTab {
|
||||
}
|
||||
|
||||
describe('pane foreground agent slice', () => {
|
||||
it('keeps process freshness metadata out of the public identity shape', () => {
|
||||
it('keeps process freshness metadata in an explicit observation map', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().setPaneForegroundAgent('tab-1:leaf-1', {
|
||||
agent: 'codex',
|
||||
shellForeground: false,
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-1'
|
||||
})
|
||||
store.getState().setPaneForegroundAgent(
|
||||
'tab-1:leaf-1',
|
||||
{
|
||||
agent: 'codex',
|
||||
shellForeground: false
|
||||
},
|
||||
{
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-1'
|
||||
}
|
||||
)
|
||||
|
||||
const entry = store.getState().paneForegroundAgentByPaneKey['tab-1:leaf-1']
|
||||
expect(entry).toMatchObject({ agent: 'codex', shellForeground: false })
|
||||
expect(entry?.observedAt).toBe(1_000)
|
||||
expect(entry?.ptyId).toBe('pty-1')
|
||||
expect(Object.keys(entry ?? {})).toEqual(['agent', 'shellForeground'])
|
||||
expect(store.getState().paneForegroundAgentObservationByPaneKey['tab-1:leaf-1']).toEqual({
|
||||
observedAt: 1_000,
|
||||
ptyId: 'pty-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('sets, value-bails, and clears entries per pane key', () => {
|
||||
|
||||
@@ -21,33 +21,12 @@ export type PaneForegroundAgentEntry = {
|
||||
/** True once the foreground is proven back at the shell (OSC 133;D) —
|
||||
* process-grade launched-agent exit evidence, independent of titles. */
|
||||
shellForeground: boolean
|
||||
/** Main-clock timestamp for the process observation. */
|
||||
observedAt?: number
|
||||
/** PTY from which the process observation was read. */
|
||||
ptyId?: string
|
||||
}
|
||||
|
||||
/** Keep freshness metadata out of the long-standing identity record shape. */
|
||||
export function createPaneForegroundAgentEntry(
|
||||
entry: PaneForegroundAgentEntry,
|
||||
metadata: Pick<PaneForegroundAgentEntry, 'observedAt' | 'ptyId'> = entry
|
||||
): PaneForegroundAgentEntry {
|
||||
const { observedAt: _ignoredObservedAt, ptyId: _ignoredPtyId, ...publicEntry } = entry
|
||||
Object.defineProperties(publicEntry, {
|
||||
observedAt: {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: metadata.observedAt,
|
||||
writable: true
|
||||
},
|
||||
ptyId: {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: metadata.ptyId,
|
||||
writable: true
|
||||
}
|
||||
})
|
||||
return publicEntry
|
||||
/** PTY-bound, time-bounded process evidence kept separate from the public identity record. */
|
||||
export type PaneForegroundAgentObservation = {
|
||||
observedAt: number
|
||||
ptyId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +36,12 @@ export function createPaneForegroundAgentEntry(
|
||||
*/
|
||||
export type PaneForegroundAgentSlice = {
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry>
|
||||
setPaneForegroundAgent: (paneKey: string, entry: PaneForegroundAgentEntry) => void
|
||||
paneForegroundAgentObservationByPaneKey: Record<string, PaneForegroundAgentObservation>
|
||||
setPaneForegroundAgent: (
|
||||
paneKey: string,
|
||||
entry: PaneForegroundAgentEntry,
|
||||
observation?: PaneForegroundAgentObservation
|
||||
) => void
|
||||
refreshPaneForegroundAgentObservation: (paneKey: string, agent: TuiAgent, ptyId?: string) => void
|
||||
clearPaneForegroundAgent: (paneKey: string) => void
|
||||
/** Wholesale teardown sweeps (tab close, worktree sleep/remove) retire pane
|
||||
@@ -73,47 +57,54 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
PaneForegroundAgentSlice
|
||||
> = (set) => ({
|
||||
paneForegroundAgentByPaneKey: {},
|
||||
setPaneForegroundAgent: (paneKey, entry) => {
|
||||
paneForegroundAgentObservationByPaneKey: {},
|
||||
setPaneForegroundAgent: (paneKey, entry, observation) => {
|
||||
set((s) => {
|
||||
const normalizedEntry = createPaneForegroundAgentEntry(entry)
|
||||
const current = s.paneForegroundAgentByPaneKey[paneKey]
|
||||
if (
|
||||
current &&
|
||||
current.agent === normalizedEntry.agent &&
|
||||
current.routingTrusted === normalizedEntry.routingTrusted &&
|
||||
current.routingRevoked === normalizedEntry.routingRevoked &&
|
||||
current.routingConfirmationPending === normalizedEntry.routingConfirmationPending &&
|
||||
current.shellForeground === normalizedEntry.shellForeground &&
|
||||
current.ptyId === normalizedEntry.ptyId
|
||||
current.agent === entry.agent &&
|
||||
current.routingTrusted === entry.routingTrusted &&
|
||||
current.routingRevoked === entry.routingRevoked &&
|
||||
current.routingConfirmationPending === entry.routingConfirmationPending &&
|
||||
current.shellForeground === entry.shellForeground
|
||||
) {
|
||||
// Test/legacy callers may provide identity-only entries. Preserve the
|
||||
// value-bail semantics for those records; only observed process
|
||||
// evidence opts into the bounded freshness clock.
|
||||
if (normalizedEntry.observedAt === undefined) {
|
||||
if (!observation) {
|
||||
return s
|
||||
}
|
||||
const now = Date.now()
|
||||
const currentObservation = s.paneForegroundAgentObservationByPaneKey[paneKey]
|
||||
if (
|
||||
current.observedAt !== undefined &&
|
||||
now - current.observedAt < OBSERVATION_REFRESH_QUANTUM_MS
|
||||
currentObservation &&
|
||||
currentObservation.ptyId === observation.ptyId &&
|
||||
now - currentObservation.observedAt < OBSERVATION_REFRESH_QUANTUM_MS
|
||||
) {
|
||||
return s
|
||||
}
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
...s.paneForegroundAgentByPaneKey,
|
||||
[paneKey]: createPaneForegroundAgentEntry(current, {
|
||||
observedAt: now,
|
||||
ptyId: current.ptyId
|
||||
})
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
...s.paneForegroundAgentObservationByPaneKey,
|
||||
[paneKey]: { ...observation, observedAt: now }
|
||||
}
|
||||
}
|
||||
}
|
||||
const nextObservation = observation
|
||||
? { ...observation }
|
||||
: entry.agent !== current?.agent || entry.shellForeground
|
||||
? undefined
|
||||
: s.paneForegroundAgentObservationByPaneKey[paneKey]
|
||||
const nextObservations = { ...s.paneForegroundAgentObservationByPaneKey }
|
||||
if (nextObservation) {
|
||||
nextObservations[paneKey] = nextObservation
|
||||
} else {
|
||||
delete nextObservations[paneKey]
|
||||
}
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
...s.paneForegroundAgentByPaneKey,
|
||||
[paneKey]: normalizedEntry
|
||||
}
|
||||
[paneKey]: entry
|
||||
},
|
||||
paneForegroundAgentObservationByPaneKey: nextObservations
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -126,26 +117,25 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
...s.paneForegroundAgentByPaneKey,
|
||||
[paneKey]: createPaneForegroundAgentEntry(
|
||||
{ agent, shellForeground: false },
|
||||
{ observedAt: now, ptyId: ptyId ?? current.ptyId }
|
||||
)
|
||||
[paneKey]: { agent, shellForeground: false }
|
||||
},
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
...s.paneForegroundAgentObservationByPaneKey,
|
||||
[paneKey]: { observedAt: now, ptyId }
|
||||
}
|
||||
}
|
||||
}
|
||||
const currentObservation = s.paneForegroundAgentObservationByPaneKey[paneKey]
|
||||
if (
|
||||
current.observedAt !== undefined &&
|
||||
now - current.observedAt < OBSERVATION_REFRESH_QUANTUM_MS
|
||||
currentObservation &&
|
||||
now - currentObservation.observedAt < OBSERVATION_REFRESH_QUANTUM_MS
|
||||
) {
|
||||
return s
|
||||
}
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
...s.paneForegroundAgentByPaneKey,
|
||||
[paneKey]: createPaneForegroundAgentEntry(current, {
|
||||
observedAt: now,
|
||||
ptyId: ptyId ?? current.ptyId
|
||||
})
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
...s.paneForegroundAgentObservationByPaneKey,
|
||||
[paneKey]: { observedAt: now, ptyId: ptyId ?? currentObservation?.ptyId }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,30 +143,41 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
...s.paneForegroundAgentByPaneKey,
|
||||
[paneKey]: createPaneForegroundAgentEntry(
|
||||
{ agent, shellForeground: false },
|
||||
{ observedAt: now, ptyId }
|
||||
)
|
||||
[paneKey]: { agent, shellForeground: false }
|
||||
},
|
||||
paneForegroundAgentObservationByPaneKey: {
|
||||
...s.paneForegroundAgentObservationByPaneKey,
|
||||
[paneKey]: { observedAt: now, ptyId }
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
clearPaneForegroundAgent: (paneKey) => {
|
||||
set((s) => {
|
||||
if (!(paneKey in s.paneForegroundAgentByPaneKey)) {
|
||||
if (
|
||||
!(paneKey in s.paneForegroundAgentByPaneKey) &&
|
||||
!(paneKey in s.paneForegroundAgentObservationByPaneKey)
|
||||
) {
|
||||
return s
|
||||
}
|
||||
const next = { ...s.paneForegroundAgentByPaneKey }
|
||||
delete next[paneKey]
|
||||
return { paneForegroundAgentByPaneKey: next }
|
||||
const nextObservations = { ...s.paneForegroundAgentObservationByPaneKey }
|
||||
delete nextObservations[paneKey]
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: next,
|
||||
paneForegroundAgentObservationByPaneKey: nextObservations
|
||||
}
|
||||
})
|
||||
},
|
||||
clearPaneForegroundAgentByTabPrefix: (tabIdPrefix) => {
|
||||
set(
|
||||
(s) =>
|
||||
buildPaneForegroundAgentTabPrefixClearPatch(s.paneForegroundAgentByPaneKey, [
|
||||
`${tabIdPrefix}:`
|
||||
]) ?? s
|
||||
buildPaneForegroundAgentTabPrefixClearPatch(
|
||||
s.paneForegroundAgentByPaneKey,
|
||||
s.paneForegroundAgentObservationByPaneKey,
|
||||
[`${tabIdPrefix}:`]
|
||||
) ?? s
|
||||
)
|
||||
},
|
||||
clearPaneForegroundAgentByWorktree: (worktreeId) => {
|
||||
@@ -186,7 +187,11 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
set((s) => {
|
||||
const prefixes = (s.tabsByWorktree[worktreeId] ?? []).map((tab) => `${tab.id}:`)
|
||||
return (
|
||||
buildPaneForegroundAgentTabPrefixClearPatch(s.paneForegroundAgentByPaneKey, prefixes) ?? s
|
||||
buildPaneForegroundAgentTabPrefixClearPatch(
|
||||
s.paneForegroundAgentByPaneKey,
|
||||
s.paneForegroundAgentObservationByPaneKey,
|
||||
prefixes
|
||||
) ?? s
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -195,39 +200,55 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
/** Return process identity only while the evidence and its PTY are current. */
|
||||
export function resolveFreshPaneForegroundAgent(
|
||||
entry: PaneForegroundAgentEntry | undefined,
|
||||
observation: PaneForegroundAgentObservation | undefined,
|
||||
args: { now: number; paneBoundPtyId?: string; liveTabPtyIds?: readonly string[] }
|
||||
): TuiAgent | null {
|
||||
if (!entry?.agent || entry.shellForeground || entry.observedAt === undefined) {
|
||||
if (!entry?.agent || entry.shellForeground || !observation) {
|
||||
return null
|
||||
}
|
||||
if (args.now - entry.observedAt > PANE_FOREGROUND_AGENT_EVIDENCE_TTL_MS) {
|
||||
if (args.now - observation.observedAt > PANE_FOREGROUND_AGENT_EVIDENCE_TTL_MS) {
|
||||
return null
|
||||
}
|
||||
if (args.paneBoundPtyId !== undefined) {
|
||||
return entry.ptyId === undefined || entry.ptyId === args.paneBoundPtyId ? entry.agent : null
|
||||
return observation.ptyId === undefined || observation.ptyId === args.paneBoundPtyId
|
||||
? entry.agent
|
||||
: null
|
||||
}
|
||||
if (entry.ptyId !== undefined) {
|
||||
return args.liveTabPtyIds?.includes(entry.ptyId) === true ? entry.agent : null
|
||||
if (observation.ptyId !== undefined) {
|
||||
return args.liveTabPtyIds?.includes(observation.ptyId) === true ? entry.agent : null
|
||||
}
|
||||
return (args.liveTabPtyIds?.length ?? 0) > 0 ? entry.agent : null
|
||||
}
|
||||
|
||||
export function buildPaneForegroundAgentTabPrefixClearPatch(
|
||||
entries: Record<string, PaneForegroundAgentEntry>,
|
||||
observations: Record<string, PaneForegroundAgentObservation>,
|
||||
tabPrefixes: readonly string[]
|
||||
): Pick<PaneForegroundAgentSlice, 'paneForegroundAgentByPaneKey'> | null {
|
||||
): Pick<
|
||||
PaneForegroundAgentSlice,
|
||||
'paneForegroundAgentByPaneKey' | 'paneForegroundAgentObservationByPaneKey'
|
||||
> | null {
|
||||
if (tabPrefixes.length === 0) {
|
||||
return null
|
||||
}
|
||||
const staleKeys = Object.keys(entries).filter((paneKey) =>
|
||||
tabPrefixes.some((prefix) => paneKey.startsWith(prefix))
|
||||
)
|
||||
const staleKeys = [
|
||||
...new Set(
|
||||
[...Object.keys(entries), ...Object.keys(observations)].filter((paneKey) =>
|
||||
tabPrefixes.some((prefix) => paneKey.startsWith(prefix))
|
||||
)
|
||||
)
|
||||
]
|
||||
if (staleKeys.length === 0) {
|
||||
return null
|
||||
}
|
||||
const next = { ...entries }
|
||||
const nextObservations = { ...observations }
|
||||
for (const paneKey of staleKeys) {
|
||||
delete next[paneKey]
|
||||
delete nextObservations[paneKey]
|
||||
}
|
||||
return {
|
||||
paneForegroundAgentByPaneKey: next,
|
||||
paneForegroundAgentObservationByPaneKey: nextObservations
|
||||
}
|
||||
return { paneForegroundAgentByPaneKey: next }
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export type RetiredTerminalTabSweepActions = Pick<
|
||||
/** The state the sweep reduces over: the two store maps plus everything the
|
||||
* agent-status drop reads. Narrow so a non-store caller can pass its own view. */
|
||||
export type RetiredTerminalTabSweepState = AgentStatusTabPrefixDropState &
|
||||
Pick<AppState, 'paneForegroundAgentByPaneKey'>
|
||||
Pick<AppState, 'paneForegroundAgentByPaneKey' | 'paneForegroundAgentObservationByPaneKey'>
|
||||
|
||||
/**
|
||||
* The suppressor-aware store maps plus three module registries a retired terminal tab strands.
|
||||
@@ -77,6 +77,7 @@ export function buildRetiredTerminalTabStateSweepPatch(
|
||||
)
|
||||
const foreground = buildPaneForegroundAgentTabPrefixClearPatch(
|
||||
swept.paneForegroundAgentByPaneKey,
|
||||
swept.paneForegroundAgentObservationByPaneKey,
|
||||
[`${tabId}:`]
|
||||
)
|
||||
swept = { ...swept, ...patch, ...foreground }
|
||||
|
||||
@@ -104,6 +104,9 @@ export function buildWorktreePurgeState(
|
||||
agentLaunchConfigByPaneKey: omitByPaneKeyTabPrefix(s.agentLaunchConfigByPaneKey),
|
||||
acknowledgedAgentsByPaneKey: omitByPaneKeyTabPrefix(s.acknowledgedAgentsByPaneKey),
|
||||
paneForegroundAgentByPaneKey: omitByPaneKeyTabPrefix(s.paneForegroundAgentByPaneKey),
|
||||
paneForegroundAgentObservationByPaneKey: omitByPaneKeyTabPrefix(
|
||||
s.paneForegroundAgentObservationByPaneKey
|
||||
),
|
||||
sleepingAgentSessionsByPaneKey: omitByPaneKeyTabPrefix(s.sleepingAgentSessionsByPaneKey),
|
||||
unreadTerminalTabs: omitByTabId(s.unreadTerminalTabs),
|
||||
unreadTerminalPanes: omitByPaneKeyTabPrefix(s.unreadTerminalPanes),
|
||||
|
||||
Reference in New Issue
Block a user