Files
orca/src/renderer/src/components/activity/activity-event-builder.ts
T
Brennan Benson 3aec926fe5 Chat renames are saved as the chat's name, so Session History shows them after the tab closes (#31602)
* Chat renames are saved as the chat's name, so Session History shows them after the tab closes

Renaming a native chat tab only changed that window's tab. Session History, phones and other windows read the host's saved name, so the rename vanished from History once the tab closed or Orca restarted.

A rename now goes to the host that owns the chat (agentSession.rename) and is saved as the chat's one name, the field History, tabs, sidebars and phones already read. The record remembers that the user chose it, so automatic naming never replaces it. A window's pre-upgrade tab rename is uploaded once and only fills a name Orca chose. Hosts without the new capability keep the old tab-only rename. Every native agent's History row now shows the chat's name once it has one.

* Keep saved chat names consistent across readers and reconnects

* Carry qualified chat names into cached readers

* Reuse the history recorder context across platforms

* Use recorded chat ownership for legacy name transfer

* Transfer legacy chat names only from known catalog ownership

* Correct recorder path matching and History subscription order

* Require complete evidence for bridged parity exclusions

* Preserve sparse array lengths in complete recording comparisons

* Initialize History observation before loading workspace context

* Integrate measured History recordings and scoped parity evidence

* fix chat naming retries and certify both replay modes

* fix ambiguous chat rename labels after navigation

* preserve chat rename intent across tab updates

* type dashboard snapshot fixtures without assertions

* fix: align chat name observation with store contracts

* test: update chat name consumer fixtures

* test: distinguish sender naming metadata from status

* Preserve saved Activity chat names across tab focus

* Clarify Activity tab projection identity

* Remove package-manager lockfile churn from main merge

* fix(runtime): validate tab property edits before saving chat names

* fix: refresh History ownership after conversation clear

* fix: commit History request identity before refresh effects

* Fix History scan scope typing and integration test placement

* test: cover saved chat name reader contracts

* test: make chat naming fixtures deterministic
2026-10-25 16:20:29 -07:00

154 lines
5.5 KiB
TypeScript

import { freshActivityLiveAgentState } from './activity-event-state'
import type { RetainedAgentEntry } from '@/store/slices/agent-status'
import type {
AgentStatusEntry,
AgentStatusOrchestrationContext,
MigrationUnsupportedPtyEntry
} from '../../../../shared/agent-status-types'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { StructuredChatTabNames } from '@/runtime/structured-chat-tab-name'
import type { Repo } from '../../../../shared/repo-types'
import { parsePaneKey } from '../../../../shared/stable-pane-id'
import type { Tab } from '../../../../shared/tab-types'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import type { Worktree } from '../../../../shared/worktree/types'
import type { ActivityEvent, ActivityLiveAgentSnapshot } from './activity-thread-types'
import { capActivityEvents } from './activity-event-cap'
import { newestActivityHistoryEntries } from './activity-pane-events'
import {
createActivityEventBuildCache,
resolvePaneBuild,
type ActivityEventBuildCache
} from './activity-event-build-cache'
import { appendUnsupportedAndRetainedEvents } from './activity-event-builder-sources'
import {
attributedActivityTabContext,
buildActivityTabContext,
buildActivityTabHostIndex,
resolveActivityEventOwner,
type ActivityEventOwner
} from './activity-event-builder-context'
export { createActivityEventBuildCache, type ActivityEventBuildCache, newestActivityHistoryEntries }
export type BuildActivityEventsArgs = {
chatNames: StructuredChatTabNames
agentStatusByPaneKey: Record<string, AgentStatusEntry>
runtimeAgentOrchestrationByPaneKey?: Record<string, AgentStatusOrchestrationContext>
migrationUnsupportedByPtyId?: Record<string, MigrationUnsupportedPtyEntry>
retainedAgentsByPaneKey: Record<string, RetainedAgentEntry>
tabsByWorktree: Record<string, TerminalTab[]>
unifiedTabsByWorktree?: Record<string, Tab[]>
worktreeMap: Map<string, Worktree>
repoMap: Map<string, Repo>
repos?: readonly Repo[]
resolveWorktree?: (worktreeId: string, executionHostId?: ExecutionHostId) => Worktree | undefined
acknowledgedAgentsByPaneKey: Record<string, number>
/** Per-pane "Clear completed" cutoffs; events stamped at or before the cutoff are hidden. */
activityClearedAtByPaneKey?: Record<string, number>
now: number
}
export function buildActivityEvents(
args: BuildActivityEventsArgs,
cache?: ActivityEventBuildCache
): {
events: ActivityEvent[]
liveAgentByPaneKey: Record<string, ActivityLiveAgentSnapshot>
paneEntryByPaneKey: Record<string, AgentStatusEntry>
} {
const events: ActivityEvent[] = []
const paneEntryByPaneKey: Record<string, AgentStatusEntry> = {}
const seenEventIds = new Set<string>()
const tabContext = buildActivityTabContext(
args.tabsByWorktree,
args.unifiedTabsByWorktree,
args.chatNames
)
const tabHostIndex = buildActivityTabHostIndex(args.unifiedTabsByWorktree)
const ownerCache = new Map<string, ActivityEventOwner>()
const liveAgentByPaneKey: Record<string, ActivityLiveAgentSnapshot> = {}
const seenCacheKeys = cache ? new Set<string>() : null
const pushPaneEvents = (paneEvents: ActivityEvent[], rowEntry: AgentStatusEntry): void => {
// Live/retained sources can overlap; keep globally unique event IDs with first-source precedence.
paneEntryByPaneKey[rowEntry.paneKey] ??= rowEntry
for (const event of paneEvents) {
if (seenEventIds.has(event.id)) {
continue
}
seenEventIds.add(event.id)
events.push(event)
}
}
for (const [paneKey, entry] of Object.entries(args.agentStatusByPaneKey)) {
const parsed = parsePaneKey(paneKey)
if (!parsed) {
continue
}
const context = tabContext.get(parsed.tabId) ?? attributedActivityTabContext(entry)
if (!context) {
continue
}
const owner = resolveActivityEventOwner(
args,
context,
entry,
context.tab.ptyId,
tabHostIndex,
ownerCache
)
const orchestration = args.runtimeAgentOrchestrationByPaneKey?.[paneKey]
// Only fresh raw live state proves working; history and orchestration enrichment cannot establish liveness.
const liveState = freshActivityLiveAgentState(entry, args.now)
const {
events: paneEvents,
live,
rowEntry
} = resolvePaneBuild(
{
cacheKey: `live:${paneKey}`,
source: entry,
entry,
orchestration,
worktree: owner.worktree,
repo: owner.repo,
tab: context.tab,
agentType: entry.agentType ?? 'unknown',
agentAlive: true,
acknowledgedAt: args.acknowledgedAgentsByPaneKey[paneKey] ?? 0,
clearedAt: args.activityClearedAtByPaneKey?.[paneKey] ?? 0,
liveState
},
cache,
seenCacheKeys
)
if (live) {
liveAgentByPaneKey[paneKey] = live
}
pushPaneEvents(paneEvents, rowEntry)
}
appendUnsupportedAndRetainedEvents({
args,
cache,
seenCacheKeys,
liveAgentByPaneKey,
tabContext,
resolveOwner: (context, entry, terminalPtyId) =>
resolveActivityEventOwner(args, context, entry, terminalPtyId, tabHostIndex, ownerCache),
pushPaneEvents
})
// Evict panes absent from every source so caches cannot outgrow live maps.
if (cache && seenCacheKeys) {
for (const cacheKey of cache.panes.keys()) {
if (!seenCacheKeys.has(cacheKey)) {
cache.panes.delete(cacheKey)
}
}
}
return { events: capActivityEvents(events), liveAgentByPaneKey, paneEntryByPaneKey }
}