import { BrowserDialogPresenter } from './classes.browserdialogpresenter.js'; import { BrowserDevToolsClient } from './classes.browserdevtools.js'; import './elements.codexconnections.js'; import './elements.authswitch.js'; import { AglAccounts } from './elements.accounts.js'; import { AglAccountLimits, type IAccountLimitsCloseDetail, type IAccountLimitsOpenRequestDetail, } from './elements.accountlimits.js'; import * as plugins from './plugins.js'; import * as interfaces from '../ts_interfaces/index.js'; import { commitinfo } from './00_commitinfo_data.js'; import { ControllerSocketClient } from './classes.controllersocketclient.js'; import { appendErrorJournalEntry, formatErrorJournalReport, type IErrorJournalEntry, } from './functions.errorjournal.js'; import { ControllerBrowserOperationError, ControllerBrowserViewTransportClient, } from './classes.browserviewtransport.js'; import { browserScreencastRows, browserStatisticsIntervalMs, browserViewStatisticsEqual, formatBrowserViewStatistics, sampleBrowserViewStatistics, type IBrowserStatisticsSample, type IBrowserViewStatistics, } from './browserstatistics.js'; import { SessionDraftSync, type ISessionDraftSubmission, type ISessionDraftSyncState, } from './classes.sessiondraftsync.js'; import { appendPendingTerminalOutput, emptyPendingTerminalOutput, maxPendingTerminalOutputBytes, type IPendingTerminalRestoreGrid, } from './functions.terminaloutputbuffer.js'; type TAppAuthState = interfaces.TControllerAuthState | 'loading'; type TBrowserHarnessId = 'opencode' | 'flex' | 'codex' | 'controller'; type TBrowserSessionHarnessId = Exclude; type TProviderManagementState = 'loading' | 'available' | 'unavailable' | 'error'; export interface IControllerBrowserRuntimeId { harnessId: TBrowserHarnessId; nativeId: string; } type TBrowserSessionId = IControllerBrowserRuntimeId & { harnessId: TBrowserSessionHarnessId; }; type TBrowserOpenCodeSessionId = TBrowserSessionId & { harnessId: 'opencode' }; type TSubtaskAccess = 'managed' | 'scoped'; type TBrowserTerminalId = IControllerBrowserRuntimeId & { harnessId: 'controller'; }; interface IFormDataEventDetail { data?: Record; } interface IHarnessSessionEventDetail { session?: { id?: unknown; }; } interface ILocalSessionTurnMarker { projectId: string; sessionId: TBrowserSessionId; sessionKey: string; revision: number; hadFinished: boolean; hadError: boolean; hadOptimisticWorking: boolean; } interface IComposerFocusRequest { id: number; projectId: string; mode: 'draft' | 'session'; sessionId?: TBrowserSessionId; ready: boolean; } interface IDraftMaterializationResult { sessionId: TBrowserSessionId; focusRequestId: number; draftText: string; draftRevision: number; } interface ILiveToolOverlay { projectId: string; execution: interfaces.IControllerToolExecution; order: interfaces.IControllerTranscriptOrder; bytes: number; } interface ILiveToolHydrationFloor { sessionId: TBrowserSessionId; cursor: interfaces.IControllerToolStreamCursor; } interface ILiveMessageHydrationFloor { sessionId: TBrowserSessionId; cursor: interfaces.IControllerMessageStreamCursor; } interface ISubtaskPreviewEntry { key: string; projectId: string; ownerSessionId: TBrowserSessionId; childSessionId: TBrowserSessionId; access: TSubtaskAccess; scopeGeneration?: string; scopeSequence?: number; generation: number; stream: plugins.deesCatalog.IHarnessSubtaskStream; statusAuthoritative: boolean; queued: boolean; refreshing: boolean; refreshRequested: boolean; refreshTimer?: ReturnType; abortController?: AbortController; } interface ISubtaskPreviewCandidate { key: string; projectId: string; ownerSessionId: TBrowserSessionId; childSessionId: TBrowserSessionId; access: TSubtaskAccess; call: interfaces.IControllerToolCall; createdAt: number; } interface IProviderOpenCodeSwitch { token: symbol; connectionId: string; generation: number; } interface IControllerSessionRenderDetail extends Omit< interfaces.IControllerSessionDetail, 'messagePage' > { messages: interfaces.IControllerMessage[]; sessionMetrics: interfaces.IControllerSessionMetrics; sessionIntelligenceEnabled: boolean; sessionIntelligenceAvailabilityStatus: | interfaces.TControllerSessionIntelligenceAvailabilityStatus | 'checking'; sessionIntelligenceUnavailableReason: string; } interface IDetailLoadOwnership { generation: number; connectionGeneration: number; projectId: string; sessionId: TBrowserSessionId; toolStreamEpoch: number; messageStreamEpoch: number; } interface ICodexSteerSubmission { projectId: string; sessionId: TBrowserSessionId; turnId: string; draftGeneration: number; draft: ISessionDraftSubmission; } interface ILiveMessageBaseline< TUpdate extends interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate, > { update: TUpdate; textUtf8Bytes: number; bytes: number; } interface ICanonicalReasoningTarget { message: plugins.deesCatalog.IHarnessMessage; part: plugins.deesCatalog.IHarnessReasoningPart; } interface ICanonicalChatProjection { key: string; sourceDetail: IControllerSessionRenderDetail | undefined; messages: plugins.deesCatalog.IHarnessMessage[]; messagesById: Map; messagesByOrder: Map; reasoningById: Map; } interface ILiveMessageDeltaBlock { sessionId: TBrowserSessionId; streamEpoch: number; } interface ISlashCatalogOwnership { key: string; projectId: string; sessionId: TBrowserSessionId; harnessId: TBrowserSessionHarnessId; connectionGeneration: number; requestId: number; menuEpisode: number; } interface ISlashCatalogState extends ISlashCatalogOwnership { commands: interfaces.IControllerSlashCommandDescriptor[]; reversion: interfaces.IControllerSessionReversionInfo; } interface ISlashCatalogRequest extends ISlashCatalogOwnership { abortController: AbortController; promise: Promise; } type TDetailHistoryStatus = 'idle' | 'backfilling' | 'complete' | 'partial'; interface IDetailHistoryLimits { truncated: boolean; historyLimited: boolean; unavailable: boolean; } const emptyDetailHistoryLimits = (): IDetailHistoryLimits => ({ truncated: false, historyLimited: false, unavailable: false, }); export const detailHistoryStatusText = ( statusArg: TDetailHistoryStatus, limitsArg: IDetailHistoryLimits, ): string => { if (statusArg === 'backfilling') return 'Loading earlier messages...'; if (statusArg !== 'partial') return ''; if (limitsArg.unavailable) return 'Earlier history is unavailable'; if (limitsArg.truncated && limitsArg.historyLimited) { return 'Earlier messages were removed by the provider and additional history was limited by the controller'; } if (limitsArg.truncated) return 'Earlier messages were removed by the provider'; if (limitsArg.historyLimited) return 'Earlier history is limited by the controller'; return 'Earlier history is unavailable'; }; const isNonEmptyString = (valueArg: unknown): valueArg is string => ( typeof valueArg === 'string' && valueArg.trim().length > 0 ); const toControllerDraftAttachments = ( valueArg: unknown, ): interfaces.IControllerDraftAttachment[] | undefined => { if (!Array.isArray(valueArg)) return undefined; const attachments: interfaces.IControllerDraftAttachment[] = []; for (const value of valueArg) { if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined; const attachment = value as Partial; if ( !isNonEmptyString(attachment.id) || !isNonEmptyString(attachment.name) || !isNonEmptyString(attachment.mediaType) || !Number.isSafeInteger(attachment.size) || (attachment.size ?? -1) < 0 || !['image', 'text', 'binary'].includes(attachment.kind ?? '') || typeof attachment.dataBase64 !== 'string' ) return undefined; attachments.push({ id: attachment.id, name: attachment.name, mediaType: attachment.mediaType, size: attachment.size!, kind: attachment.kind!, dataBase64: attachment.dataBase64, }); } return attachments; }; const isControllerRuntimeId = (valueArg: unknown): valueArg is IControllerBrowserRuntimeId => { if (!valueArg || typeof valueArg !== 'object') return false; const candidate = valueArg as Partial; return ( Object.keys(candidate).length === 2 && (candidate.harnessId === 'opencode' || candidate.harnessId === 'flex' || candidate.harnessId === 'codex' || candidate.harnessId === 'controller') && typeof candidate.nativeId === 'string' && candidate.nativeId.length > 0 && new TextEncoder().encode(candidate.nativeId).byteLength <= 512 ); }; const isSessionRuntimeId = (valueArg: unknown): valueArg is TBrowserSessionId => ( isControllerRuntimeId(valueArg) && valueArg.harnessId !== 'controller' ); const isOpenCodeSessionRuntimeId = ( valueArg: unknown, ): valueArg is TBrowserOpenCodeSessionId => ( isSessionRuntimeId(valueArg) && valueArg.harnessId === 'opencode' ); const isTerminalRuntimeId = (valueArg: unknown): valueArg is TBrowserTerminalId => ( isControllerRuntimeId(valueArg) && valueArg.harnessId === 'controller' ); const hasSessionRuntimeId = ( sessionArg: interfaces.IControllerSession, ): sessionArg is interfaces.IControllerSession & { id: TBrowserSessionId } => ( isSessionRuntimeId(sessionArg.id) ); const hasTerminalRuntimeId = ( terminalArg: interfaces.IControllerTerminal, ): terminalArg is interfaces.IControllerTerminal & { id: TBrowserTerminalId } => ( isTerminalRuntimeId(terminalArg.id) ); // Runtime ID objects are reconciled for identity across refreshes, so their UI // keys are memoized per object instead of being re-serialized on every lookup // and comparison in render paths. const controllerRuntimeIdUiKeys = new WeakMap< IControllerBrowserRuntimeId, { harnessId: string; nativeId: string; key: string } >(); export const controllerRuntimeIdToUiKey = (idArg: IControllerBrowserRuntimeId): string => { if (!isControllerRuntimeId(idArg)) throw new Error('Invalid qualified runtime ID.'); const cached = controllerRuntimeIdUiKeys.get(idArg); if (cached && cached.harnessId === idArg.harnessId && cached.nativeId === idArg.nativeId) { return cached.key; } const key = JSON.stringify([idArg.harnessId, idArg.nativeId]); controllerRuntimeIdUiKeys.set(idArg, { harnessId: idArg.harnessId, nativeId: idArg.nativeId, key }); return key; }; /** * Sidebar identity of one conversation. The list spans projects and a native conversation id is * only unique inside its own project, so the project is part of the key. */ export const conversationUiKey = ( projectIdArg: string, sessionIdArg: IControllerBrowserRuntimeId, ): string => `${projectIdArg}|${controllerRuntimeIdToUiKey(sessionIdArg)}`; /** Inverse of `conversationUiKey`; returns undefined for a key this client did not mint. */ export const parseConversationUiKey = ( keyArg: string, ): { projectId: string; sessionId: TBrowserSessionId } | undefined => { const separator = keyArg.indexOf('|'); if (separator <= 0) return undefined; const projectId = keyArg.slice(0, separator); let parsed: unknown; try { parsed = JSON.parse(keyArg.slice(separator + 1)); } catch { return undefined; } if (!Array.isArray(parsed) || parsed.length !== 2) return undefined; const [harnessId, nativeId] = parsed; const sessionId = { harnessId, nativeId }; return isSessionRuntimeId(sessionId) ? { projectId, sessionId } : undefined; }; export const isTrackedConversation = ( valueArg: interfaces.IControllerTrackedConversation, ): boolean => isNonEmptyString(valueArg?.projectId) && hasSessionRuntimeId(valueArg.session); /** * The workspace's view of a tracked conversation. `archivedAt` carries AGL's own archive: the * harness archive is a separate thing the sidebar deliberately does not act on. */ export const trackedConversationToSession = ( conversationArg: interfaces.IControllerTrackedConversation, ): interfaces.IControllerSession => { const { archivedAt: _harnessArchivedAt, ...session } = conversationArg.session; return conversationArg.archivedAt === undefined ? session : { ...session, archivedAt: conversationArg.archivedAt }; }; export const controllerRuntimeIdsEqual = ( leftArg: IControllerBrowserRuntimeId | undefined, rightArg: IControllerBrowserRuntimeId | undefined, ): boolean => { if (leftArg === undefined) return rightArg === undefined; if (rightArg === undefined) return false; if (!isControllerRuntimeId(leftArg) || !isControllerRuntimeId(rightArg)) { throw new Error('Invalid qualified runtime ID.'); } return leftArg.harnessId === rightArg.harnessId && leftArg.nativeId === rightArg.nativeId; }; // One JSON equality for the whole controller surface, shared with the live-coverage rule that // needs the same semantics on the server. const controllerJsonValuesEqual = interfaces.controllerJsonValuesEqual; const reconcileKeyedJsonArray = ( currentArg: readonly TValue[], nextArg: readonly TValue[], keyForArg: (valueArg: TValue, indexArg: number) => string, ): TValue[] => { const currentByKey = new Map( currentArg.map((value, index) => [keyForArg(value, index), value]), ); const seenKeys = new Set(); let unchanged = currentArg.length === nextArg.length; const reconciled = nextArg.map((nextValue, index) => { const key = keyForArg(nextValue, index); const currentValue = seenKeys.has(key) ? undefined : currentByKey.get(key); seenKeys.add(key); const value = currentValue !== undefined && controllerJsonValuesEqual(currentValue, nextValue) ? currentValue : nextValue; unchanged &&= value === currentArg[index]; return value; }); return unchanged ? currentArg as TValue[] : reconciled; }; const browserTextEncoder = new TextEncoder(); // Shared empty values keep child component property identities stable across // unrelated app renders; a fresh literal per render would invalidate them. const emptyHarnessUsage: plugins.deesCatalog.IHarnessUsage = {}; const emptyHarnessSessionMetrics: plugins.deesCatalog.IHarnessSessionMetrics = {}; const harnessModeOptions = ['Ask', 'Yolo']; const emptyHarnessQuestions: plugins.deesCatalog.IHarnessQuestionRequest[] = []; const emptyHarnessAttachments: plugins.deesCatalog.IHarnessAttachment[] = []; const latestHarnessUsage = ( messagesArg: readonly plugins.deesCatalog.IHarnessMessage[], ): plugins.deesCatalog.IHarnessUsage => { for (let index = messagesArg.length - 1; index >= 0; index -= 1) { const usage = messagesArg[index]!.usage; if (usage) return usage; } return emptyHarnessUsage; }; const draftStatesEqualExceptText = ( leftArg: ISessionDraftSyncState, rightArg: ISessionDraftSyncState, ): boolean => ( leftArg.projectId === rightArg.projectId && controllerRuntimeIdsEqual(leftArg.sessionId, rightArg.sessionId) && leftArg.revision === rightArg.revision && leftArg.loading === rightArg.loading && leftArg.saving === rightArg.saving && leftArg.error === rightArg.error && controllerJsonValuesEqual(leftArg.attachments, rightArg.attachments) ); const controllerTextAfterAppendUtf8Bytes = ( textArg: string, textUtf8BytesArg: number, deltaArg: string, ): number | undefined => { const previousCodeUnit = textArg.charCodeAt(textArg.length - 1); const nextCodeUnit = deltaArg.charCodeAt(0); // Separately encoded surrogate halves cost six bytes; the joined pair costs four. const boundaryCorrection = textArg.length > 0 && deltaArg.length > 0 && previousCodeUnit >= 0xD800 && previousCodeUnit <= 0xDBFF && nextCodeUnit >= 0xDC00 && nextCodeUnit <= 0xDFFF ? -2 : 0; const result = textUtf8BytesArg + browserTextEncoder.encode(deltaArg).byteLength + boundaryCorrection; return Number.isSafeInteger(result) && result >= 0 ? result : undefined; }; const transcriptOrderKey = (orderArg: interfaces.IControllerTranscriptOrder): string => ( `${orderArg.messageIndex}\0${orderArg.partIndex}` ); const controllerNormalizeBrowserAddress = (addressArg: string): string => { const address = addressArg.trim(); if (!address) throw new Error('Enter a browser address.'); const explicitScheme = address.match(/^([a-z][a-z\d+.-]*):\/\//i)?.[1]?.toLowerCase(); if (explicitScheme !== undefined && explicitScheme !== 'http' && explicitScheme !== 'https') { throw new Error('Enter a valid HTTP or HTTPS address.'); } const hasNumericHostPort = /^[^/?#\s]+:\d+(?:[/?#]|$)/.test(address); if ( /^[a-z][a-z\d+.-]*:/i.test(address) && explicitScheme === undefined && !hasNumericHostPort ) { throw new Error('Enter a valid HTTP or HTTPS address.'); } const candidate = explicitScheme === undefined ? `https://${address}` : address; let url: URL; try { url = new URL(candidate); } catch { throw new Error('Enter a valid HTTP or HTTPS address.'); } if ((url.protocol !== 'http:' && url.protocol !== 'https:') || !url.hostname) { throw new Error('Enter a valid HTTP or HTTPS address.'); } return url.href; }; const runtimeIdsByUiKey = ( idsArg: Iterable, ): Map => new Map( [...idsArg].map((idArg) => [controllerRuntimeIdToUiKey(idArg), idArg]), ); const isSessionHarnessId = (valueArg: unknown): valueArg is TBrowserSessionHarnessId => ( valueArg === 'opencode' || valueArg === 'flex' || valueArg === 'codex' ); const sessionHarnessLabel = (harnessIdArg: TBrowserSessionHarnessId): string => ( harnessIdArg === 'codex' ? 'Codex' : harnessIdArg === 'flex' ? 'Flex' : 'OpenCode' ); /** A resource the New menu creates directly, in menu order. */ interface INewMenuResource { /** The menu entry's label. */ readonly name: string; /** The same resource in running text, for the project dialogs the menu may need. */ readonly subject: string; readonly iconName: string; readonly kind: interfaces.TControllerResourceKind; readonly agent?: interfaces.TControllerTerminalAgentKind; } const newMenuResources: readonly INewMenuResource[] = [ { name: 'Terminal', subject: 'terminal', iconName: 'lucide:Terminal', kind: 'terminal' }, { name: 'Claude terminal', subject: 'Claude terminal', iconName: 'lucide:Bot', kind: 'terminal', agent: 'claude', }, { name: 'Browser', subject: 'browser', iconName: 'lucide:Globe2', kind: 'browser' }, ]; const runtimeIdDisplay = (idArg: IControllerBrowserRuntimeId): string => ( `${idArg.harnessId === 'controller' ? 'Controller' : sessionHarnessLabel(idArg.harnessId)}:${idArg.nativeId}` ); const modelHarnessId = (choiceArg: interfaces.TControllerModelChoice): TBrowserSessionHarnessId => { const harnessId = (choiceArg as interfaces.TControllerModelChoice & { harnessId?: unknown }).harnessId; if (harnessId !== 'opencode' && harnessId !== 'flex' && harnessId !== 'codex') { throw new Error('Invalid model harness ID.'); } return harnessId; }; const modelOptionLabel = (choiceArg: interfaces.TControllerModelChoice): string => { return [ sessionHarnessLabel(modelHarnessId(choiceArg)), `${choiceArg.providerID}/${choiceArg.modelID}`, ].join(' · '); }; const sameStringArray = (leftArg: readonly string[], rightArg: readonly string[]): boolean => ( leftArg.length === rightArg.length && leftArg.every((value, index) => value === rightArg[index]) ); const sameComposerOptions = ( leftArg: readonly plugins.deesCatalog.IHarnessComposerOption[], rightArg: readonly plugins.deesCatalog.IHarnessComposerOption[], ): boolean => ( leftArg.length === rightArg.length && leftArg.every((value, index) => ( value.label === rightArg[index]?.label && value.value === rightArg[index]?.value )) ); // Override-map key for a chat that exists only in this tab until its first // message creates the real harness session. Never a canonical runtime ID key. const draftSessionKey = (harnessIdArg: TBrowserSessionHarnessId): string => `draft:${harnessIdArg}`; const systemMetricsRefreshMs = 2_000; const highCpuThresholdPercent = 50; const highCpuSustainMs = 10_000; const maximumContinuousCpuSampleGapMs = systemMetricsRefreshMs * 2.5; const legalDisclosureHideDelayMs = 500; const maxLiveToolOverlayEntries = 128; const maxLiveToolOverlayBytes = 4 * 1024 * 1024; const maxSubtaskPreviewEntries = 16; const maxSubtaskPreviewBundles = 40; const maxConcurrentSubtaskPreviewHydrations = 4; const subtaskPreviewRefreshDelayMs = 400; const subtaskPreviewUnavailableNotice = 'Live preview is unavailable. Open the subagent chat to view its transcript.'; const unmanagedChildTranscriptNotice = 'Child transcript preview and drill-in are unavailable because parent/child relationships do not grant managed access.'; const limitedChildAttentionNotice = 'Some direct-child permission or question requests were omitted by controller safety limits. Refresh to reconcile the current requests.'; const detailHistoryPageLimit = 50; const sidebarWidthStorageKey = 'harnessControllerSidebarWidth'; const browserSidebarStorageKey = 'harnessControllerBrowserSidebar'; /** The browser diagnostics panel offers exactly one section; the shell keeps it open. */ const browserSidebarSections: plugins.deesCatalog.IHarnessSidebarSection[] = [ { key: 'screencast', label: 'Screencast', icon: 'lucide:MonitorPlay' }, ]; const sidebarWidthDefault = 292; const sidebarWidthMin = 260; const sidebarWidthMax = 520; const sidebarKeyboardStep = 8; const layoutRequestTimeoutMs = 10_000; const providerLoginPollTimeoutMs = 15 * 60 * 1000; const providerRefreshPollTimeoutMs = 5 * 60 * 1000; const providerOpenCodeSwitchTimeoutMs = 6 * 60 * 1000; const browserRendererLifecycleTimeoutMs = 1_000; const maximumSlashSuggestions = 24; const maximumWorkspaceNoticeCharacters = 500; const codexModeOutcomeUnknownNotice = 'The Codex mode change outcome is unknown. Send, Steer, Queue, slash commands, and model changes are unavailable for this conversation.'; export const controllerClampSidebarWidth = (widthArg: number): number => Math.min( sidebarWidthMax, Math.max(sidebarWidthMin, Math.round(widthArg)), ); // Message bundles are reconciled for identity, so an unchanged bundle object is // measured once instead of being re-serialized on every refresh. const controllerMessageBundleByteSizes = new WeakMap(); const controllerMessageBundleBytes = (bundleArg: interfaces.IControllerMessageBundle): number => { const cached = controllerMessageBundleByteSizes.get(bundleArg); if (cached !== undefined) return cached; const bytes = browserTextEncoder.encode(JSON.stringify(bundleArg)).byteLength; controllerMessageBundleByteSizes.set(bundleArg, bytes); return bytes; }; export const boundedNewestMessageBundles = ( bundlesArg: readonly interfaces.IControllerMessageBundle[], ): { bundles: interfaces.IControllerMessageBundle[]; limited: boolean } => { const bundles: interfaces.IControllerMessageBundle[] = []; let bytes = 2; for (let index = bundlesArg.length - 1; index >= 0; index -= 1) { const bundle = bundlesArg[index]; const bundleBytes = controllerMessageBundleBytes(bundle); const separatorBytes = bundles.length === 0 ? 0 : 1; if ( bundles.length >= interfaces.controllerMaximumMessageBundles || bytes + separatorBytes + bundleBytes > interfaces.controllerMaximumTranscriptBytes ) break; bundles.unshift(bundle); bytes += separatorBytes + bundleBytes; } return { bundles, limited: bundles.length !== bundlesArg.length }; }; const presentationRefKey = ( refArg: plugins.deesCatalog.IHarnessSessionListItemRef, ): string => `${refArg.kind}:${refArg.id}`; const layoutItemRefKey = (itemRefArg: interfaces.TControllerLayoutItemRef): string => itemRefArg.kind === 'session' ? `${itemRefArg.projectId}:session:${controllerRuntimeIdToUiKey(itemRefArg.id)}` : `${itemRefArg.projectId}:resource:${itemRefArg.id}`; interface ILayoutOrderableItem { ref: interfaces.TControllerLayoutItemRef; sortAt: number; sourceIndex: number; } /** * Conversations and resources are peers in one order. Unknown members fall back to the same * recency order the sidebar itself derives, so an item that has never been placed explicitly * still lands where the user expects it. */ const controllerOrderableLayoutItems = ( conversationsArg: interfaces.IControllerTrackedConversation[], resourcesArg: interfaces.TControllerResource[], projectIdArg: string, ): ILayoutOrderableItem[] => { const items: ILayoutOrderableItem[] = []; let sourceIndex = 0; // Conversations come from every project; resources belong to the project in context. for (const conversation of conversationsArg) { if ( conversation.archivedAt !== undefined || !hasSessionRuntimeId(conversation.session) ) continue; items.push({ ref: { kind: 'session', id: conversation.session.id, projectId: conversation.projectId, }, sortAt: conversation.session.updatedAt, sourceIndex: sourceIndex += 1, }); } for (const resource of resourcesArg) { if (resource.lifecycle === 'retired') continue; items.push({ ref: { kind: 'resource', id: resource.id, projectId: projectIdArg }, sortAt: resource.updatedAt || resource.createdAt, sourceIndex: sourceIndex += 1, }); } return items; }; export const controllerEffectiveUngroupedLayoutItemIds = ( conversationsArg: interfaces.IControllerTrackedConversation[], resourcesArg: interfaces.TControllerResource[], groupsArg: interfaces.IControllerSessionGroup[], explicitIdsArg: interfaces.TControllerLayoutItemRef[], projectIdArg: string, ): interfaces.TControllerLayoutItemRef[] => { const groupedIds = new Set(groupsArg.flatMap((group) => group.itemIds.map(layoutItemRefKey))); const available = controllerOrderableLayoutItems(conversationsArg, resourcesArg, projectIdArg) .filter((item) => !groupedIds.has(layoutItemRefKey(item.ref))) .sort((left, right) => right.sortAt - left.sortAt || left.sourceIndex - right.sourceIndex); const availableById = new Map(available.map((item) => [layoutItemRefKey(item.ref), item])); const result: interfaces.TControllerLayoutItemRef[] = []; for (const layoutItemId of explicitIdsArg) { const layoutItemKey = layoutItemRefKey(layoutItemId); if (!availableById.has(layoutItemKey)) continue; result.push(layoutItemId); availableById.delete(layoutItemKey); } result.push(...available .filter((item) => availableById.has(layoutItemRefKey(item.ref))) .map((item) => item.ref)); return result; }; const controllerMutableUngroupedLayoutItemIds = ( conversationsArg: interfaces.IControllerTrackedConversation[], resourcesArg: interfaces.TControllerResource[], groupsArg: interfaces.IControllerSessionGroup[], explicitIdsArg: interfaces.TControllerLayoutItemRef[], projectIdArg: string, ): interfaces.TControllerLayoutItemRef[] => { const groupedKeys = new Set(groupsArg.flatMap((group) => group.itemIds.map(layoutItemRefKey))); const archivedKeys = new Set( conversationsArg .filter((conversation) => ( conversation.archivedAt !== undefined && hasSessionRuntimeId(conversation.session) )) .map((conversation) => `${conversation.projectId}:session:${controllerRuntimeIdToUiKey( conversation.session.id, )}`), ); const retiredResourceKeys = new Set( resourcesArg .filter((resource) => resource.lifecycle === 'retired') .map((resource) => `${projectIdArg}:resource:${resource.id}`), ); const retained: interfaces.TControllerLayoutItemRef[] = []; const retainedKeys = new Set(); for (const layoutItemId of explicitIdsArg) { const key = layoutItemRefKey(layoutItemId); const keep = !groupedKeys.has(key) && !retainedKeys.has(key) && !retiredResourceKeys.has(key) && !archivedKeys.has(key); if (!keep) continue; retained.push(layoutItemId); retainedKeys.add(key); } for (const layoutItemId of controllerEffectiveUngroupedLayoutItemIds( conversationsArg, resourcesArg, groupsArg, explicitIdsArg, projectIdArg, )) { const key = layoutItemRefKey(layoutItemId); if (retainedKeys.has(key)) continue; retained.push(layoutItemId); retainedKeys.add(key); } return retained; }; const cloneLayoutItemRef = ( itemRefArg: interfaces.TControllerLayoutItemRef, ): interfaces.TControllerLayoutItemRef => ( itemRefArg.kind === 'session' ? { kind: 'session', id: { ...itemRefArg.id }, projectId: itemRefArg.projectId } : { kind: 'resource', id: itemRefArg.id, projectId: itemRefArg.projectId } ); /** * Presentation refs use the sidebar's own opaque key space. The layout ref already carries its * project, so making the sidebar cross-project is a change to this one mapping rather than to * every call site. */ const layoutItemRefToPresentation = ( itemRefArg: interfaces.TControllerLayoutItemRef, ): plugins.deesCatalog.IHarnessSessionListItemRef => ( itemRefArg.kind === 'session' ? { kind: 'session', id: conversationUiKey(itemRefArg.projectId, itemRefArg.id) } : { kind: 'resource', id: itemRefArg.id } ); const cloneSessionLayout = ( layoutArg: interfaces.IControllerSessionLayout, ): interfaces.IControllerSessionLayout => ({ groups: layoutArg.groups.map((group) => ({ id: group.id, name: group.name, itemIds: group.itemIds.map(cloneLayoutItemRef), })), ungroupedItemIds: layoutArg.ungroupedItemIds.map(cloneLayoutItemRef), revision: layoutArg.revision, }); const openCodeBuiltinSlashCommands: ReadonlyArray<{ name: interfaces.TControllerBuiltinCommand; description: string; }> = [ { name: 'compact', description: 'Summarize the conversation and compact its context' }, { name: 'undo', description: 'Revert the last message and its changes' }, { name: 'redo', description: 'Restore previously reverted messages' }, { name: 'init', description: 'Analyze the project and create AGENTS.md' }, ]; const flexBuiltinSlashCommands: typeof openCodeBuiltinSlashCommands = [ { name: 'compact', description: 'Summarize the conversation and compact its context' }, { name: 'init', description: 'Analyze the project and create AGENTS.md' }, { name: 'undo', description: 'Revert the last message and its changes' }, { name: 'redo', description: 'Restore previously reverted messages' }, ]; const localBuiltinSlashCommands = ( harnessIdArg: TBrowserSessionHarnessId, ): typeof openCodeBuiltinSlashCommands => ( harnessIdArg === 'codex' ? [] : harnessIdArg === 'flex' ? flexBuiltinSlashCommands : openCodeBuiltinSlashCommands ); const bytesToBase64 = (bytesArg: Uint8Array): string => { let binary = ''; const chunkSize = 0x8000; for (let index = 0; index < bytesArg.length; index += chunkSize) { binary += String.fromCharCode(...bytesArg.subarray(index, index + chunkSize)); } return btoa(binary); }; const base64ToBytes = (base64Arg: string): Uint8Array => { try { const binary = atob(base64Arg); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; } catch { return new Uint8Array(0); } }; /** Joins the chunks of one reconstructed terminal screen state into the payload `restore()` takes. */ const concatBytes = (chunksArg: readonly Uint8Array[]): Uint8Array => { const total = chunksArg.reduce((sum, chunk) => sum + chunk.byteLength, 0); const bytes = new Uint8Array(total); let offset = 0; for (const chunk of chunksArg) { bytes.set(chunk, offset); offset += chunk.byteLength; } return bytes; }; const errorMessage = (errorArg: unknown): string => { if (errorArg instanceof Error && errorArg.message) { return errorArg.message; } return String(errorArg || 'The operation failed.'); }; /** * The controller's opaque reference for a failure whose wire text stays generic. It is the only * way back to the cause, which never leaves the controller process on its own. */ const controllerFailureReference = (errorArg: unknown): string | undefined => { if (!(errorArg instanceof plugins.typedrequest.TypedResponseError)) return undefined; if (errorArg.errorData?.code !== interfaces.controllerOperationFailedErrorCode) return undefined; const reference = errorArg.errorData?.reference; return typeof reference === 'string' && reference.length > 0 ? reference : undefined; }; /** * AGL-authored text for a rejected catalog action, keyed on the reported source * alone. The rejected value is never shown: it can carry controller-internal * detail. A source the catalog adds later falls back to the generic sentence. */ const actionErrorText = (sourceArg: string | undefined): string => { const subject = sourceArg === 'modal-menu' ? 'dialog action' : sourceArg?.startsWith('table-') ? 'table action' : 'action'; return `The ${subject} failed. Its result is unknown; check the view before retrying.`; }; const waitForBrowserRendererLifecycle = async ( operationArg: Promise, actionArg: string, ): Promise => { let timeout: ReturnType | undefined; try { await Promise.race([ operationArg, new Promise((_resolve, reject) => { timeout = setTimeout(() => { reject(new Error(`Browser renderer ${actionArg} timed out.`)); }, browserRendererLifecycleTimeoutMs); }), ]); } finally { if (timeout) clearTimeout(timeout); } }; const formatPercentage = (valueArg: number | null | undefined): string => ( typeof valueArg === 'number' && Number.isFinite(valueArg) ? `${Math.round(valueArg)}%` : '--' ); const formatBytes = (bytesArg: number | null | undefined): string => { if (typeof bytesArg !== 'number' || !Number.isFinite(bytesArg) || bytesArg < 0) return '--'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytesArg; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } const decimals = value >= 100 || unitIndex === 0 ? 0 : 1; return `${value.toFixed(decimals)} ${units[unitIndex]}`; }; const formatByteRate = (bytesArg: number | null | undefined): string => { if (typeof bytesArg !== 'number' || !Number.isFinite(bytesArg) || bytesArg < 0) return '--'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytesArg; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } const decimals = value >= 100 || unitIndex === 0 ? 0 : 1; return `${value.toFixed(decimals)}${units[unitIndex]}/s`; }; const formatMetricTime = (timestampArg: number): string => new Date(timestampArg) .toLocaleTimeString(undefined, { hour12: false }); interface IConversationActivityPresentation { text: string; title: string; ariaText: string; dateTime?: string; } const conversationActivityFormatter = new Intl.DateTimeFormat(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); const conversationActivityDetailFormatter = new Intl.DateTimeFormat(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'long', }); const formatConversationActivity = ( timestampArg: number, ): IConversationActivityPresentation => { const date = new Date(timestampArg); if ( !Number.isFinite(timestampArg) || timestampArg <= 0 || !Number.isFinite(date.getTime()) ) { return { text: 'Unknown', title: 'Last activity time is unavailable.', ariaText: 'last activity unknown', }; } const detail = conversationActivityDetailFormatter.format(date); return { text: conversationActivityFormatter.format(date), title: `Last active ${detail}`, ariaText: `last active ${detail}`, dateTime: date.toISOString(), }; }; const formatFooterMount = (mountPointArg: string): string => { if (mountPointArg === '/') return 'DISK'; const leaf = mountPointArg.split('/').filter(Boolean).at(-1) ?? mountPointArg; return leaf.length <= 4 ? `…/${leaf}` : `…${leaf.slice(-5)}`; }; /** Busy share from which a chip's IO figure takes the warning style. */ const diskBusyWarningPercent = 80; /** Busy share from which a chip's IO figure takes the critical style. */ const diskBusyCriticalPercent = 95; const resourceStrainThresholdPercent = 90; const diskBusyCaveat = 'Busy is the share of time the disk had I/O in flight; SSDs and NVMe can read 100% before they are saturated.'; const formatPressure = (valueArg: number): string => `${valueArg.toFixed(1)}%`; const formatIops = (valueArg: number): string => `${Math.round(valueArg)} IOPS`; type TSystemMetricSelection = | { kind: 'cpu'; label: string } | { kind: 'memory'; label: string } | { kind: 'network'; label: string } | { kind: 'diskCapacity'; label: string; mountPoint: string } | { kind: 'diskIo'; label: string; mountPoint: string }; interface ISystemMetricHistoryWindow { points: interfaces.IControllerSystemMetricsHistoryPoint[]; pointsByBucket: Map; buckets: number[]; status: string; latestSampledAt?: number; } const systemMetricHistoryWindow = ( pointsArg: interfaces.IControllerSystemMetricsHistoryPoint[], nowArg = Date.now(), ): ISystemMetricHistoryWindow => { const resolutionMs = 10_000; const windowMs = 60 * 60 * 1_000; const points = pointsArg .filter((pointArg) => pointArg.sampledAt >= nowArg - windowMs && pointArg.sampledAt <= nowArg) .sort((leftArg, rightArg) => leftArg.sampledAt - rightArg.sampledAt); const pointsByBucket = new Map(points.map((pointArg) => [ Math.floor(pointArg.sampledAt / resolutionMs), pointArg, ] as const)); const firstBucket = Math.floor((nowArg - windowMs) / resolutionMs); const lastBucket = Math.floor(nowArg / resolutionMs); const buckets = Array.from({ length: lastBucket - firstBucket + 1 }, (_, indexArg) => firstBucket + indexArg); if (points.length === 0) { return { points, pointsByBucket, buckets, status: 'No recorded readings in the last hour yet.', }; } const partial = points[0].sampledAt > nowArg - 59 * 60 * 1_000; return { points, pointsByBucket, buckets, status: `${points.length} readings in the last hour · first ${formatMetricTime(points[0].sampledAt)}${partial ? ' (partial hour)' : ''}`, latestSampledAt: points[points.length - 1].sampledAt, }; }; const systemMetricHistorySeries = ( historyArg: ISystemMetricHistoryWindow, nameArg: string, readArg: (pointArg: interfaces.IControllerSystemMetricsHistoryPoint) => number | null, colorArg: string, ): plugins.deesCatalog.ChartSeriesConfig[number] => ({ name: nameArg, color: colorArg, data: historyArg.buckets.map((bucketArg) => { const point = historyArg.pointsByBucket.get(bucketArg); const value = point ? readArg(point) : null; return { x: bucketArg * 10_000, y: value !== null && Number.isFinite(value) ? value : null, }; }), }); /** The detail panel owns its own small render cycle; sampling never rerenders the workspace. */ @plugins.deesElement.customElement('agl-system-stats-panel') class AglSystemStatsPanel extends plugins.deesElement.DeesElement { @plugins.deesElement.property({ attribute: false }) accessor metrics: interfaces.IControllerSystemMetrics | undefined; @plugins.deesElement.property({ type: Boolean }) accessor highCpuWarning = false; @plugins.deesElement.state() accessor historyStatus = 'Loading the last hour of readings…'; private historyLatestSampledAt: number | undefined; private cpuRamSeries: plugins.deesCatalog.ChartSeriesConfig = []; private networkSeries: plugins.deesCatalog.ChartSeriesConfig = []; private diskCapacitySeries: plugins.deesCatalog.ChartSeriesConfig = []; private diskBusySeries: plugins.deesCatalog.ChartSeriesConfig = []; @plugins.deesElement.state() accessor now = Date.now(); private ageTimer: ReturnType | undefined; private chartColors(): string[] { return this.goBright ? ['#007aff', '#af52de', '#187432', '#9d4400', '#bf1825'] : ['#0a84ff', '#bf5af2', '#30d158', '#ff9f0a', '#ff746b']; } protected themeChanged(): void { const colors = this.chartColors(); const recolor = (seriesArg: plugins.deesCatalog.ChartSeriesConfig, colorIndexArg: (indexArg: number) => number) => seriesArg.map((entryArg, indexArg) => ({ ...entryArg, color: colors[colorIndexArg(indexArg)] })); this.cpuRamSeries = recolor(this.cpuRamSeries, (indexArg) => indexArg); this.networkSeries = recolor(this.networkSeries, (indexArg) => indexArg === 0 ? 0 : 3); this.diskCapacitySeries = recolor(this.diskCapacitySeries, (indexArg) => indexArg % colors.length); this.diskBusySeries = recolor(this.diskBusySeries, (indexArg) => indexArg % colors.length); this.requestUpdate(); } public static styles = [ plugins.deesCatalog.themeDefaultStyles, plugins.deesElement.cssManager.defaultStyles, plugins.deesElement.css` :host { display: block; position: fixed; box-sizing: border-box; width: min(610px, calc(100vw - 16px)); max-height: calc(100vh - 16px); overflow: auto; padding: 14px; border: 1px solid var(--dees-color-border-default); border-radius: var(--dees-radius-md); background: var(--dees-material-thick-bg-opaque); color: var(--dees-color-text-primary); box-shadow: var(--dees-shadow-md); font: 12px/1.45 var(--dees-font-family); } .heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } h2 { margin: 0; font-size: 14px; font-weight: 700; } .freshness { margin-top: 3px; color: var(--dees-color-text-muted); font-size: 11px; } .freshness.stale { color: var(--dees-color-text-warning); } .close { flex: 0 0 auto; border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-sm); padding: 3px 8px; background: transparent; color: inherit; font: inherit; cursor: pointer; } .close:hover, .close:focus-visible { background: var(--dees-color-bg-secondary); } .close:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: 2px; } .summary { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; margin-top: 13px; } .summary.stale .stat { border-style: dashed; } .summary.stale .statLabel, .summary.stale .note, .filesystems.stale .details, .filesystems.stale .note { color: var(--dees-color-text-primary); } .summaryState { margin: 7px 0 0; color: var(--dees-color-text-primary); font-size: 11px; } .stat { padding: 8px 10px; border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-sm); } .statLabel { display: block; color: var(--dees-color-text-muted); font-size: 10px; font-weight: 700; letter-spacing: .05em; } .statValue { display: block; margin-top: 2px; font: 700 15px var(--dees-font-family-mono); } .note { color: var(--dees-color-text-secondary); } .warning { color: var(--dees-color-text-warning); } .critical { color: var(--dees-color-text-error); font-weight: 700; } .historyStatus { margin: 13px 0 0; color: var(--dees-color-text-muted); font-size: 11px; } .historyStatus.error { color: var(--dees-color-text-warning); } dees-chart-area { display: block; height: 245px; margin: 8px 0; } .historyCharts { margin-top: 5px; } .historyHeading { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } .historyGap { color: var(--dees-color-text-primary); font-size: 11px; text-align: right; } h3 { margin: 16px 0 7px; font-size: 11px; color: var(--dees-color-text-muted); text-transform: uppercase; letter-spacing: .05em; } .filesystem { padding: 9px 0; border-top: 1px solid var(--dees-color-border-subtle); } .filesystem:first-of-type { border-top: 0; } .filesystemTop { display: flex; justify-content: space-between; gap: 10px; } .mount { min-width: 0; overflow-wrap: anywhere; font: 700 12px var(--dees-font-family-mono); } .capacity { flex: 0 0 auto; font: 700 12px var(--dees-font-family-mono); } .details { margin-top: 3px; color: var(--dees-color-text-secondary); overflow-wrap: anywhere; } .caveat { margin: 11px 0 0; color: var(--dees-color-text-muted); font-size: 11px; } @media (max-width: 400px) { .summary { grid-template-columns: 1fr; } } `, ]; public async connectedCallback(): Promise { await super.connectedCallback(); if (!this.isConnected || this.ageTimer) return; this.ageTimer = setInterval(() => { this.now = Date.now(); }, 1_000); } public async disconnectedCallback(): Promise { if (this.ageTimer) clearInterval(this.ageTimer); this.ageTimer = undefined; await super.disconnectedCallback(); } public focusClose(): void { this.shadowRoot?.querySelector('.close')?.focus({ preventScroll: true }); } private readonly requestClose = (): void => { this.dispatchEvent(new CustomEvent('stats-close', { bubbles: true, composed: true })); }; public setHistory(pointsArg: interfaces.IControllerSystemMetricsHistoryPoint[]): void { const history = systemMetricHistoryWindow(pointsArg); const points = history.points; const chartColors = this.chartColors(); const series = ( nameArg: string, readArg: (pointArg: interfaces.IControllerSystemMetricsHistoryPoint) => number | null, colorArg: string, ): plugins.deesCatalog.ChartSeriesConfig[number] => systemMetricHistorySeries( history, nameArg, readArg, colorArg, ); this.cpuRamSeries = [ series('CPU', (pointArg) => pointArg.cpuUsagePercent, chartColors[0]), series('RAM', (pointArg) => pointArg.memoryUsedPercent, chartColors[1]), ]; this.networkSeries = [ series('Receive', (pointArg) => pointArg.networkReceiveBytesPerSecond, chartColors[0]), series('Transmit', (pointArg) => pointArg.networkTransmitBytesPerSecond, chartColors[3]), ]; const mountPoints = [...new Set(points.flatMap((pointArg) => pointArg.disks.map( (diskArg) => diskArg.mountPoint, )))].sort((leftArg, rightArg) => leftArg.localeCompare(rightArg)); const diskReading = ( pointArg: interfaces.IControllerSystemMetricsHistoryPoint, mountPointArg: string, metricArg: 'usedPercent' | 'busyPercent', ): number | null => pointArg.disks.find((diskArg) => diskArg.mountPoint === mountPointArg)?.[metricArg] ?? null; const observedMounts = new Set([this.metrics?.mainDisk?.mountPoint, ...(this.metrics?.volumes.map((volumeArg) => volumeArg.mountPoint) ?? [])]); this.diskCapacitySeries = mountPoints.map((mountPointArg, indexArg) => series( observedMounts.has(mountPointArg) ? mountPointArg : `${mountPointArg} · history only`, (pointArg) => diskReading(pointArg, mountPointArg, 'usedPercent'), chartColors[indexArg % chartColors.length], )); this.diskBusySeries = mountPoints.map((mountPointArg, indexArg) => series( observedMounts.has(mountPointArg) ? mountPointArg : `${mountPointArg} · history only`, (pointArg) => diskReading(pointArg, mountPointArg, 'busyPercent'), chartColors[indexArg % chartColors.length], )); this.historyStatus = history.status; this.historyLatestSampledAt = history.latestSampledAt; } public setHistoryUnavailable(): void { this.historyStatus = 'History is unavailable; live readings remain above.'; this.historyLatestSampledAt = undefined; this.cpuRamSeries = []; this.networkSeries = []; this.diskCapacitySeries = []; this.diskBusySeries = []; } private renderFilesystem( filesystemArg: interfaces.IControllerFilesystemUsage | null | undefined, fallbackArg: string, isMainArg: boolean, ): plugins.deesElement.TemplateResult { if (!filesystemArg) return plugins.deesElement.html`
${fallbackArg}
`; const io = filesystemArg.io; const roundedBusy = io ? Math.round(io.busyPercent) : null; const ioClass = roundedBusy !== null && roundedBusy >= diskBusyCriticalPercent ? 'critical' : roundedBusy !== null && roundedBusy >= diskBusyWarningPercent ? 'warning' : ''; return plugins.deesElement.html`
${filesystemArg.mountPoint} ${formatPercentage(filesystemArg.usedPercent)} used
${filesystemArg.unavailableReason ?? `${formatBytes(filesystemArg.usedBytes)} of ${formatBytes(filesystemArg.totalBytes)} · ${filesystemArg.fsType}`}
${io ? `IO ${formatPercentage(roundedBusy)} busy · ${io.devices.join(', ')}` : `IO -- · ${filesystemArg.ioUnavailableReason ?? 'Disk activity unavailable.'}`}
${io ? plugins.deesElement.html`
read ${formatBytes(io.readBytesPerSecond)}/s · ${formatIops(io.readsPerSecond)}
write ${formatBytes(io.writeBytesPerSecond)}/s · ${formatIops(io.writesPerSecond)}
` : ''} ${isMainArg && this.metrics?.ioPressure ? plugins.deesElement.html`
System I/O pressure (10 s): some ${formatPressure(this.metrics.ioPressure.someAvg10)} · full ${formatPressure(this.metrics.ioPressure.fullAvg10)}
System I/O pressure (60 s): some ${formatPressure(this.metrics.ioPressure.someAvg60)} · full ${formatPressure(this.metrics.ioPressure.fullAvg60)}
` : ''}
`; } public render(): plugins.deesElement.TemplateResult { const metrics = this.metrics; const memoryPercent = metrics?.memoryUsedBytes !== null && metrics?.memoryUsedBytes !== undefined && metrics.memoryTotalBytes !== null && metrics.memoryTotalBytes !== undefined && metrics.memoryTotalBytes > 0 ? (metrics.memoryUsedBytes / metrics.memoryTotalBytes) * 100 : null; const ageSeconds = metrics ? Math.max(0, Math.floor((this.now - metrics.sampledAt) / 1_000)) : null; const stale = ageSeconds === null || ageSeconds > 6; const historyAgeSeconds = this.historyLatestSampledAt === undefined ? null : Math.max(0, Math.floor((this.now - this.historyLatestSampledAt) / 1_000)); return plugins.deesElement.html`

Host statistics

${ageSeconds === null ? 'Live sample unavailable' : `Live sample ${formatMetricTime(metrics!.sampledAt)} · ${ageSeconds}s old${stale ? ' · stale' : ''}`}
CPU · whole system${formatPercentage(metrics?.cpuUsagePercent)} ${this.highCpuWarning ? plugins.deesElement.html`Above 50% for at least 10 seconds` : ''}
RAM · active use${formatPercentage(memoryPercent)} ${formatBytes(metrics?.memoryUsedBytes)} of ${formatBytes(metrics?.memoryTotalBytes)}
NETWORK · aggregate receive${formatByteRate(metrics?.networkReceiveBytesPerSecond)}
NETWORK · aggregate transmit${formatByteRate(metrics?.networkTransmitBytesPerSecond)}
${stale ? plugins.deesElement.html`

${metrics ? 'Summary values are from the last live sample; no current sample is available.' : 'No live sample is available.'}

` : ''}

Last 1 hour

${historyAgeSeconds !== null ? plugins.deesElement.html`History ends ${formatMetricTime(this.historyLatestSampledAt!)} · ${historyAgeSeconds}s old${historyAgeSeconds > 30 ? ' · stale' : ''}` : ''}
${this.historyStatus}

${metrics ? stale ? 'Main disk · last observed' : 'Main disk' : 'Main disk · unavailable'}

${this.renderFilesystem(metrics?.mainDisk, 'Main disk usage unavailable', true)}

${metrics ? stale ? `Mounted at last observation · ${metrics.volumes.length}` : `Mounted now · ${metrics.volumes.length}` : 'Mounted now · unavailable'}

${metrics?.volumes.length ? metrics.volumes.map((volumeArg) => this.renderFilesystem(volumeArg, 'Volume usage unavailable', false)) : plugins.deesElement.html`
${metrics ? 'No additional mounted volumes reported.' : 'Mounted volumes unavailable.'}
`}

${diskBusyCaveat}

`; } } @plugins.deesElement.customElement('agl-system-metric-popover') class AglSystemMetricPopover extends plugins.deesElement.DeesElement { @plugins.deesElement.property({ attribute: false }) accessor selection: TSystemMetricSelection | undefined; @plugins.deesElement.property({ attribute: false }) accessor metrics: interfaces.IControllerSystemMetrics | undefined; @plugins.deesElement.state() accessor historyStatus = 'Loading the last hour of readings…'; @plugins.deesElement.state() accessor historyLatestSampledAt: number | undefined; private historyPoints: interfaces.IControllerSystemMetricsHistoryPoint[] | undefined; private series: plugins.deesCatalog.ChartSeriesConfig = []; public static styles = [ plugins.deesCatalog.themeDefaultStyles, plugins.deesElement.cssManager.defaultStyles, plugins.deesElement.css` :host { box-sizing: border-box; display: block; min-width: 0; padding: 10px 12px; color: var(--dees-color-text-primary); font: 12px/1.35 var(--dees-font-family); } .heading { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; min-width: 0; } h2 { min-width: 0; margin: 0; overflow: hidden; font-size: 12px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } .live { flex: 0 0 auto; font: 700 12px var(--dees-font-family-mono); font-variant-numeric: tabular-nums; } .status { min-height: 15px; margin-top: 2px; color: var(--dees-color-text-muted); font-size: 10px; overflow-wrap: anywhere; } .status.error { color: var(--dees-color-text-warning); } dees-chart-area { display: block; height: 116px; margin-top: 5px; } `, ]; protected themeChanged(): void { if (this.historyPoints) this.setHistory(this.historyPoints); } public setLoading(): void { this.historyPoints = undefined; this.series = []; this.historyStatus = 'Loading the last hour of readings…'; this.historyLatestSampledAt = undefined; this.requestUpdate(); } public setHistory(pointsArg: interfaces.IControllerSystemMetricsHistoryPoint[]): void { this.historyPoints = pointsArg; const selection = this.selection; if (!selection) return; const history = systemMetricHistoryWindow(pointsArg); const colors = this.goBright ? ['#007aff', '#af52de', '#9d4400'] : ['#0a84ff', '#bf5af2', '#ff9f0a']; const diskReading = ( pointArg: interfaces.IControllerSystemMetricsHistoryPoint, metricArg: 'usedPercent' | 'busyPercent', ): number | null => pointArg.disks.find( (diskArg) => diskArg.mountPoint === ('mountPoint' in selection ? selection.mountPoint : ''), )?.[metricArg] ?? null; switch (selection.kind) { case 'cpu': this.series = [systemMetricHistorySeries( history, 'CPU', (pointArg) => pointArg.cpuUsagePercent, colors[0], )]; break; case 'memory': this.series = [systemMetricHistorySeries( history, 'RAM', (pointArg) => pointArg.memoryUsedPercent, colors[1], )]; break; case 'network': this.series = [ systemMetricHistorySeries( history, 'Receive', (pointArg) => pointArg.networkReceiveBytesPerSecond, colors[0], ), systemMetricHistorySeries( history, 'Transmit', (pointArg) => pointArg.networkTransmitBytesPerSecond, colors[2], ), ]; break; case 'diskCapacity': this.series = [systemMetricHistorySeries( history, `${selection.mountPoint} used`, (pointArg) => diskReading(pointArg, 'usedPercent'), colors[0], )]; break; case 'diskIo': this.series = [systemMetricHistorySeries( history, `${selection.mountPoint} busy`, (pointArg) => diskReading(pointArg, 'busyPercent'), colors[2], )]; break; } this.historyStatus = history.status; this.historyLatestSampledAt = history.latestSampledAt; this.requestUpdate(); } public setHistoryUnavailable(): void { this.historyPoints = undefined; this.series = []; this.historyStatus = 'History is unavailable.'; this.historyLatestSampledAt = undefined; this.requestUpdate(); } private filesystem(): interfaces.IControllerFilesystemUsage | null | undefined { const selection = this.selection; if (!selection || !('mountPoint' in selection)) return undefined; return [this.metrics?.mainDisk, ...(this.metrics?.volumes ?? [])].find( (filesystemArg) => filesystemArg?.mountPoint === selection.mountPoint, ); } private liveValue(): string { const selection = this.selection; if (!selection) return '--'; switch (selection.kind) { case 'cpu': return formatPercentage(this.metrics?.cpuUsagePercent); case 'memory': { const used = this.metrics?.memoryUsedBytes; const total = this.metrics?.memoryTotalBytes; return typeof used === 'number' && typeof total === 'number' && total > 0 ? formatPercentage((used / total) * 100) : '--'; } case 'network': return `${formatByteRate(this.metrics?.networkReceiveBytesPerSecond)} in · ${formatByteRate(this.metrics?.networkTransmitBytesPerSecond)} out`; case 'diskCapacity': return formatPercentage(this.filesystem()?.usedPercent); case 'diskIo': return formatPercentage(this.filesystem()?.io?.busyPercent); } } public render(): plugins.deesElement.TemplateResult { const selection = this.selection; const percentage = selection?.kind !== 'network'; const latestAge = this.historyLatestSampledAt === undefined ? undefined : Math.max(0, Math.floor((Date.now() - this.historyLatestSampledAt) / 1_000)); const status = latestAge === undefined ? this.historyStatus : `${this.historyStatus} · latest ${latestAge}s ago${latestAge > 30 ? ' · stale' : ''}`; return plugins.deesElement.html`

${selection?.label ?? 'Host metric'} · last 1 hour

${this.liveValue()}
${status}
`; } } const formatDuration = (secondsArg: number): string => { if (!Number.isFinite(secondsArg) || secondsArg < 0) return '--'; if (secondsArg < 60) return `${Math.ceil(secondsArg)}s`; if (secondsArg < 3_600) return `${Math.ceil(secondsArg / 60)}m`; if (secondsArg < 86_400) return `${Math.ceil(secondsArg / 3_600)}h`; return `${Math.ceil(secondsArg / 86_400)}d`; }; export const controllerBundleReloadDecision = ( bundleVersionArg: string, backendVersionArg: string, previousTargetArg: string | null, ): 'match' | 'reload' | 'stale' => { if (backendVersionArg === bundleVersionArg) return 'match'; return previousTargetArg === `${bundleVersionArg}->${backendVersionArg}` ? 'stale' : 'reload'; }; @plugins.deesElement.customElement('harness-controller-app') export class HarnessControllerApp extends plugins.deesElement.DeesElement { private readonly socketClient: ControllerSocketClient; private readonly sessionDraftSync: SessionDraftSync; private componentConnected = false; private lifecycleGeneration = 0; private connectionGeneration = 0; private startupCheckTimer: ReturnType | undefined; private sessionsRequestId = 0; private detailRequestId = 0; private sessionYoloRequestId = 0; private detailEnrichmentGeneration = 0; private detailRequestAbortController: AbortController | undefined; private detailAuxiliaryAbortController: AbortController | undefined; private detailHistoryAbortController: AbortController | undefined; private detailRefreshInFlight: Promise | undefined; private detailRefreshPending = false; private detailRefreshRestartEnrichment = false; private detailHistoryOwnership: IDetailLoadOwnership | undefined; private detailHistoryCursor: string | undefined; private detailHistorySeenCursors = new Set(); private detailBundles: interfaces.IControllerMessageBundle[] = []; private detailBundleKeys = new Set(); private detailCoreBundleKeys = new Set(); private detailProvisionalBundleKeys = new Set(); private detailHistoryLimits = emptyDetailHistoryLimits(); private activeCeremonyId: symbol | undefined; private activeMutationId: symbol | undefined; private mutationSequence = 0; private refreshTimer: ReturnType | undefined; private immediateRefreshTimer: ReturnType | undefined; private immediateRefreshPending = false; private refreshInFlight: Promise | undefined; private systemMetricsTimer: ReturnType | undefined; private footerAgeTimer: ReturnType | undefined; private statsPanelLayer: plugins.deesCatalog.DeesWindowLayer | undefined; private statsPanel: AglSystemStatsPanel | undefined; private statsPanelListeners: AbortController | undefined; private statsPanelActivator: HTMLButtonElement | undefined; private statsPanelGeneration = 0; private footerOverlayGeneration = 0; @plugins.deesElement.state() private accessor accountLimitsOpen = false; private statsHistoryTimer: ReturnType | undefined; private metricPopoverGeneration = 0; private metricHistoryTimer: ReturnType | undefined; @plugins.deesElement.state() private accessor metricPopoverSelection: TSystemMetricSelection | undefined; @plugins.deesElement.state() private accessor metricPopoverAnchor: HTMLElement | null = null; private codexDiffModalRef: InstanceType | undefined; private codexDiffModalOpenTask: Promise | undefined; @plugins.deesElement.state() private accessor codexModeMutationSessionKey = ''; @plugins.deesElement.state() private accessor codexModeUnconfirmedSessionKey = ''; private footerDiskMounts = ['/']; private footerMetricsScroller: HTMLElement | undefined; private footerMetricsResizeObserver: ResizeObserver | undefined; private legalDisclosureHideTimer: ReturnType | undefined; private highCpuStartedAt: number | undefined; private lastCpuSampleAt: number | undefined; private refreshPending = false; private refreshRelatedPending = false; private projectsRequestId = 0; private modelCatalogRequestId = 0; @plugins.deesElement.state() private accessor codexComposer: interfaces.IControllerCodexContext | undefined; private codexComposerKey = ''; private layoutRequestId = 0; private terminalRequestId = 0; private resourceRequestId = 0; private workspaceSelectionGeneration = 0; private resourceRefreshInFlight: Promise | undefined; private resourceRefreshPending = false; private resourceRefreshReportErrors = false; private browserViewGeneration = 0; private browserNavigationRequestId = 0; private browserViewOpenAbortController: AbortController | undefined; private browserNavigationAbortController: AbortController | undefined; private browserViewRecoveryAbortController: AbortController | undefined; private browserViewId = ''; private browserViewConnectionGeneration = -1; private browserViewStreamGeneration = -1; private browserViewActivated = false; private browserViewPendingClose: { viewId: string; streamGeneration: number; connectionGeneration: number; attempts: number; } | undefined; private browserViewCloseTask: Promise | undefined; private browserViewCloseRetryTimer: ReturnType | undefined; private browserViewResourceId = ''; private browserViewAttachmentRevision = -1; /** Browser resource whose view the controller closed for a resource change; reopened after refresh. */ private browserViewReopenResourceId = ''; private browserViewReopenAttempts = 0; private browserViewReopenWindowStartedAt = 0; private readonly browserViewListeners = new Set(); private browserViewTransport?: ControllerBrowserViewTransportClient; private browserViewTransportRecoveryTask?: Promise; private browserRenderer?: plugins.LiveBrowserVideoRenderer; private readonly browserDialogs = new BrowserDialogPresenter(); private browserDevTools?: { client: BrowserDevToolsClient; response: interfaces.IReq_ControllerBrowserDevToolsOpen['response']; connectionGeneration: number; }; private browserDevToolsOpenAbort?: AbortController; @plugins.deesElement.state() private accessor browserDevToolsVisible = false; @plugins.deesElement.state() private accessor browserDevToolsError = ''; private browserDevToolsFailedId?: string; /** The diagnostics panel beside the live browser; a view preference, remembered per browser. */ @plugins.deesElement.state() private accessor browserSidebarVisible = false; /** Page-lifetime preference: detailed screencast statistics stay in the sidebar by default. */ @plugins.deesElement.state() private accessor browserStatisticsOverlayVisible = false; @plugins.deesElement.state() private accessor browserVideoFailed = false; @plugins.deesElement.state() private accessor browserWebsiteErrors: Record = {}; private browserStatisticsTimer?: ReturnType; private browserStatisticsSample?: IBrowserStatisticsSample; private browserAddressDraft = ''; private browserAddressEditing = false; private suggestRequestId = 0; private suggestDebounce: ReturnType | undefined; private controllerStatus: interfaces.IControllerStatus | undefined; private providers: interfaces.IControllerProvider[] = []; private providerConnections: interfaces.IControllerProviderConnection[] = []; private providerManagementState: TProviderManagementState = 'loading'; private providerManagementRefreshRequested = false; private providerManagementTask: Promise | undefined; private activeProviderLogin: interfaces.IControllerProviderLogin | undefined; private providerLoginBeginTask: Promise | undefined; private providerLoginPollTimer: ReturnType | undefined; private providerLoginPollDeadline: number | undefined; private providerLoginPollTask: Promise | undefined; private providerLoginPollTaskKey: string | undefined; private readonly providerRefreshPollTimers = new Map>(); private readonly providerRefreshJobs = new Map(); private readonly providerRefreshPollDeadlines = new Map(); private readonly providerRefreshStartTokens = new Map(); private readonly providerRefreshStatuses = new Map(); private readonly providerRateLimits = new Map< string, interfaces.IControllerProviderAccountRateLimits >(); private readonly providerRateLimitErrors = new Map(); private readonly providerRateLimitTasks = new Map>(); private providerOpenCodeSwitch: IProviderOpenCodeSwitch | undefined; private providerOpenCodeSwitchStatus = ''; private providerLoginStatusRoot: HTMLElement | undefined; private providerLoginStatusError = ''; private existingConversationModalRef: InstanceType< typeof plugins.deesCatalog.DeesModal > | undefined; private existingConversationModalGeneration = 0; private existingConversationModalOpenTask: Promise | undefined; private existingConversationSearchTimer: ReturnType | undefined; private existingConversationSearchAbortController: AbortController | undefined; /** Numbers journal entries within this tab so list keys stay stable across renders. */ private errorJournalSequence = 0; private errorJournalModalRef: InstanceType | undefined; private errorJournalModalOpenTask: Promise | undefined; private errorJournalDetailTask: Promise | undefined; private errorJournalDetailRerunRequested = false; /** Projects section of the settings modal; it applies each change immediately. */ private settingsStandardDirectoryDraft = ''; private settingsProjectsError = ''; private settingsModalRef: InstanceType | undefined; private settingsModalGeneration = 0; private settingsModalOpenTask: Promise | undefined; private settingsModalContentRenderer: (() => plugins.deesElement.TemplateResult) | undefined; private settingsModalSidebarRenderer: (() => plugins.deesElement.TemplateResult) | undefined; private settingsDraftHarnessId: TBrowserSessionHarnessId = 'opencode'; private settingsDraftModelString = ''; private settingsDraftEffortString = ''; private settingsDraftAutoAccept = false; private settingsDraftBrowserVideoBackend: 'chromium' | 'native' = 'chromium'; private browserVideoBackend: 'chromium' | 'native' = 'chromium'; private activeBrowserVideoBackend: 'chromium' | 'native' = 'chromium'; private autoAcceptPermissions = false; private readonly modelChoicesByString = new Map(); private readonly modelVariantsByString = new Map(); private readonly flexModelAvailabilityByString = new Map< string, interfaces.IControllerFlexModelAvailability[] >(); private defaultModelStrings: Record = { opencode: '', flex: '', codex: '', }; private defaultEffortStrings: Record = { opencode: '', flex: '', codex: '', }; private defaultModelChoices: Partial< Record > = {}; // Per-tab, per-session model overrides; the persisted default applies // whenever a session has no override. private readonly sessionModelOverrides = new Map(); private readonly sessionEffortOverrides = new Map(); private readonly sessionAccountOverrides = new Map(); private readonly sessionModelSaveTokens = new Map(); private flexAccountOptionsCache: plugins.deesCatalog.IHarnessComposerOption[] = []; private readonly modelOptionsCache = new Map(); private readonly effortOptionsCache = new Map(); private slashCatalogRequestId = 0; private slashMenuEpisode = 0; private slashMenuEligible = false; private slashCatalogState: ISlashCatalogState | undefined; private slashCatalogRequest: ISlashCatalogRequest | undefined; private sessionsLoadedForProjectId = ''; private terminalsLoadedForProjectId = ''; private resourcesLoadedForProjectId = ''; // Output that arrives before the terminal view renders is flushed in updated(). private pendingTerminalOutput = emptyPendingTerminalOutput(); /** * Highest stream offset already painted, so a frame re-sent after an unacknowledged delivery * is discarded instead of written twice. `undefined` means nothing is anchored yet and only a * reconstructed screen state can establish a baseline. */ private selectedTerminalAppliedEnd: number | undefined; /** * The reconstructed screen state being collected, or the last one applied. Frames of one state * share its stream offset and are ordered by `index`, so both facts are kept until a state at * another offset replaces them. */ private terminalSnapshot: { offset: number; /** Index the next frame of this state must carry; anything lower is a retry. */ nextIndex: number; /** The final frame arrived and the state is on screen. */ complete: boolean; /** Grid the state was serialized at, which is the grid it is restored into. */ grid: IPendingTerminalRestoreGrid; chunks: Uint8Array[]; /** Bytes collected so far, so an unterminated collection cannot grow without a bound. */ bytes: number; } | undefined; /** What the client believes about its own attachment, so re-selecting a dead one re-attaches. */ private selectedTerminalAttached = false; private terminalAttachGeneration = 0; private terminalAttachPending = false; // Answered cards persist inline for the currently viewed session even after // OpenCode drops them from its pending lists; cleared on session switch. private readonly answeredQuestionCards = new Map(); private readonly answeredPermissionCards = new Map(); // Multi-question requests answer card-by-card; the reply fires once all // cards of the request are in. private readonly pendingQuestionBatchAnswers = new Map>(); private readonly authoritativeStatusesByProject = new Map< string, Map >(); private readonly finishedSessionIdsByProject = new Map>(); private readonly stableRenderValues = new Map(); private sidebarDragWidth?: number; private readonly errorSessionIdsByProject = new Map>(); private readonly optimisticWorkingIdsByProject = new Map>(); private readonly sessionEventRevisionByProject = new Map(); // One layout per controller: the sidebar orders every project's conversations and resources. private confirmedSessionLayout?: interfaces.IControllerSessionLayout; private pendingSessionLayout?: interfaces.IControllerSessionLayout; private layoutSavePromise?: Promise; private pendingLayoutReload = false; private groupsLoaded = false; private readonly staleDraftSessionsToDelete = new Map< string, Map >(); private readonly staleDraftSessionCleanupPromises = new Map>(); private readonly staleDraftSessionCleanupAttempts = new Map(); private sidebarResizePointerId: number | undefined; private sidebarResizeStartX = 0; private sidebarResizeBaseline: number | undefined; private composerFocusRequestId = 0; private composerFocusRequest: IComposerFocusRequest | undefined; private composerFocusInFlight: Promise | undefined; private sessionDraftActivationGeneration = 0; private draftSessionGeneration = 0; private draftSessionDetailReadyId: TBrowserSessionId | undefined; private readonly scratchpadSaveIdsBySession = new Map(); private readonly scratchpadErrorsBySession = new Map(); private readonly intelligenceAskIdsBySession = new Map(); private readonly intelligenceErrorsBySession = new Map(); private readonly liveToolOverlays = new Map(); private readonly liveReasoningUpdates = new Map(); private readonly liveTextUpdates = new Map(); private readonly liveReasoningBaselines = new Map< string, ILiveMessageBaseline >(); private readonly liveTextBaselines = new Map< string, ILiveMessageBaseline >(); private readonly liveReasoningTextUtf8Bytes = new Map(); private readonly liveTextTextUtf8Bytes = new Map(); private readonly liveReasoningStoredBytes = new Map(); private readonly liveTextStoredBytes = new Map(); private readonly liveToolHydrationFloors = new Map(); private readonly liveMessageHydrationFloors = new Map(); private readonly liveMessageDeltaBlocks = new Map(); private readonly liveHistoryBarriers = new Map(); private liveToolOverlayBytes = 0; private liveMessageOverlayBytes = 0; private readonly liveToolStreamEpochs = new Map(); private readonly liveMessageStreamEpochs = new Map(); private canonicalChatProjection: ICanonicalChatProjection | undefined; private canonicalChatProjectionDirty = false; private canonicalTimelineRefreshPending = false; private readonly canonicalMessageRefreshIds = new Set(); private readonly subtaskPreviewEntries = new Map(); private readonly pendingChildEventsByScopeGeneration = new Map< string, interfaces.IControllerChildEvent >(); private subtaskPreviewGeneration = 0; private subtaskPreviewHydrationQueue: string[] = []; private activeSubtaskPreviewHydrations = 0; @plugins.deesElement.state() accessor connectionStatus: plugins.typedsocket.TConnectionStatus = 'new'; @plugins.deesElement.state() accessor authState: TAppAuthState = 'loading'; @plugins.deesElement.state() accessor authenticated = false; @plugins.deesElement.state() accessor setupCodeExpired = false; @plugins.deesElement.state() accessor connectionError = ''; @plugins.deesElement.state() accessor setupError = ''; @plugins.deesElement.state() accessor showTempPasswordForm = false; @plugins.deesElement.state() accessor tempPasswordError = ''; /** Failed workspace operations, oldest first, bounded by `maxErrorJournalEntries`. */ @plugins.deesElement.state() accessor errorJournal: IErrorJournalEntry[] = []; /** Why the Errors dialog could not read a cause or reach the clipboard, shown inside it. */ @plugins.deesElement.state() accessor errorJournalNotice = ''; /** What the composer needs before it can send, stated at the composer itself. */ @plugins.deesElement.state() accessor composerNotice = ''; @plugins.deesElement.state() accessor sessionLayoutError = ''; /** * What a sidebar action needs before it can run, stated in the sidebar. A sidebar action can be * taken while a terminal, a browser or the empty workspace is shown, where no composer exists. */ @plugins.deesElement.state() accessor sidebarNotice = ''; @plugins.deesElement.state() accessor workspaceNotice = ''; @plugins.deesElement.state() accessor ceremonyBusy = false; @plugins.deesElement.state() accessor projects: interfaces.IControllerProject[] = []; /** Pending project removals the controller stopped retrying; only an operator can resume them. */ @plugins.deesElement.state() accessor blockedProjectRemovals: interfaces.IControllerBlockedProjectRemoval[] = []; @plugins.deesElement.state() accessor selectedProjectId = ''; @plugins.deesElement.state() accessor projectsRoot = ''; @plugins.deesElement.state() accessor pathSuggestions: interfaces.IControllerPathSuggestion[] = []; @plugins.deesElement.state() accessor slashSuggestions: plugins.deesCatalog.IHarnessComposerSuggestion[] = []; @plugins.deesElement.state() accessor slashReversion: interfaces.IControllerSessionReversionInfo | undefined; @plugins.deesElement.state() accessor modelOptionStrings: string[] = []; @plugins.deesElement.state() accessor backendVersion = ''; @plugins.deesElement.state() accessor upgradeStatus: interfaces.IControllerUpgradeStatus | undefined; @plugins.deesElement.state() accessor staleBundleVersion = ''; public systemMetrics: interfaces.IControllerSystemMetrics | undefined; @plugins.deesElement.state() accessor highCpuWarning = false; @plugins.deesElement.state() accessor legalDisclosureVisible = false; @plugins.deesElement.state() accessor sessions: interfaces.IControllerSession[] = []; /** Every conversation AGL tracks, across every project: the sidebar's own source. */ @plugins.deesElement.state() accessor conversations: interfaces.IControllerTrackedConversation[] = []; @plugins.deesElement.state() accessor selectedSessionId: TBrowserSessionId | undefined; @plugins.deesElement.state() accessor sessionDetail: IControllerSessionRenderDetail | undefined; @plugins.deesElement.state() accessor sessionsLoading = false; @plugins.deesElement.state() accessor detailLoading = false; @plugins.deesElement.state() accessor detailHistoryStatus: TDetailHistoryStatus = 'idle'; @plugins.deesElement.state() accessor mutationPending = false; @plugins.deesElement.state() accessor codexSteerPending = false; @plugins.deesElement.state() accessor sessionModelPending = false; @plugins.deesElement.state() accessor draftSessionActive = false; @plugins.deesElement.state() accessor draftHarnessId: TBrowserSessionHarnessId = 'opencode'; /** Absolute directories configured in settings; the new-conversation box offers them. */ @plugins.deesElement.state() accessor standardProjectDirectories: string[] = []; /** * Harness of the conversation started last, read from the controller settings. Absent means * the new-conversation box preselects nothing and the harness is an explicit choice. */ @plugins.deesElement.state() accessor lastSessionHarnessId: TBrowserSessionHarnessId | undefined; /** * The new-conversation box. The header's New button, the chat view's new-session button and * the empty workspace all render this one box, so its state lives on the element. */ @plugins.deesElement.state() accessor newConversationProjectId = ''; @plugins.deesElement.state() accessor newConversationDirectory = ''; @plugins.deesElement.state() accessor newConversationHarnessId: TBrowserSessionHarnessId | undefined; @plugins.deesElement.state() accessor newConversationModelString = ''; @plugins.deesElement.state() accessor newConversationError = ''; @plugins.deesElement.state() accessor sessionDraftState: ISessionDraftSyncState | undefined; @plugins.deesElement.state() accessor localDraftText = ''; @plugins.deesElement.state() accessor localDraftAttachments: interfaces.IControllerDraftAttachment[] = []; @plugins.deesElement.state() accessor sessionGroups: interfaces.IControllerSessionGroup[] = []; @plugins.deesElement.state() accessor ungroupedItemIds: interfaces.TControllerLayoutItemRef[] = []; @plugins.deesElement.state() accessor sessionCardStateRevision = 0; @plugins.deesElement.state() accessor sidebarWidth = sidebarWidthDefault; @plugins.deesElement.state() accessor sidebarResizing = false; @plugins.deesElement.state() accessor conversationSidebarVisible = true; @plugins.deesElement.state() accessor terminals: interfaces.IControllerTerminal[] = []; @plugins.deesElement.state() accessor resources: interfaces.TControllerResource[] = []; @plugins.deesElement.state() accessor selectedResourceId = ''; @plugins.deesElement.state() accessor browserViewLoading = false; /** * Set for the whole of an open attempt, including the close of the view it * replaces. `browserViewLoading` cannot cover that window because the close * clears it. */ @plugins.deesElement.state() accessor browserViewOpening = false; @plugins.deesElement.state() accessor browserViewStatistics: IBrowserViewStatistics | undefined; @plugins.deesElement.state() accessor browserViewState: interfaces.IControllerBrowserViewState | undefined; @plugins.deesElement.state() accessor selectedTerminalId: TBrowserTerminalId | undefined; @plugins.deesElement.state() accessor selectedTerminalEnded = false; constructor() { super(); // The CSP forbids CDN scripts, so xterm and highlight.js ship in this // bundle and are handed to the catalog's lib loader before any terminal // or code view mounts. const libLoader = plugins.deesCatalog.DeesServiceLibLoader.getInstance(); libLoader.provideXtermModules({ xterm: { Terminal: plugins.XtermTerminal }, fitAddon: { FitAddon: plugins.XtermFitAddon }, }); libLoader.provideHighlightJs(plugins.highlightJs); this.socketClient = new ControllerSocketClient({ onStatus: (statusArg) => this.handleConnectionStatus(statusArg), onControllerEvent: (eventArg) => this.handleControllerEvent(eventArg), onChildEvent: (eventArg) => this.handleChildEvent(eventArg), onTerminalOutput: (outputArg) => this.handleTerminalOutput(outputArg), onTerminalDetached: (detachedArg) => this.handleTerminalDetached(detachedArg), onTransportClose: (closeCodeArg, reasonArg) => { this.recordTransportClose(closeCodeArg, reasonArg); console.error(`Controller transport closed (code ${closeCodeArg}${reasonArg ? `: ${reasonArg}` : ''}).`); if (!this.componentConnected) return; this.connectionError = closeCodeArg === 1009 ? 'The connection was closed because a response exceeded the transport frame limit. Reconnecting…' : `The connection was closed by the transport (code ${closeCodeArg}${reasonArg ? `: ${reasonArg}` : ''}). Reconnecting…`; }, onReconnectExhausted: (attemptsArg, endpointArg) => { if (!this.componentConnected) return; this.connectionError = `Automatic reconnection stopped after ${attemptsArg} attempts to ${endpointArg}.`; void this.socketClient.stop().finally(() => { if (this.componentConnected) this.connectionStatus = 'disconnected'; }); }, }); this.sessionDraftSync = new SessionDraftSync({ transport: { getDraft: async (projectIdArg, sessionIdArg) => { const response = await this.socketClient.fire( 'controller.session.draft.get', { projectId: projectIdArg, sessionId: sessionIdArg }, { maxRetries: 0 }, ); return response.draft; }, updateDraft: async ( projectIdArg, sessionIdArg, expectedRevisionArg, patchArg, ) => { const response = await this.socketClient.fire( 'controller.session.draft.update', { projectId: projectIdArg, sessionId: sessionIdArg, expectedRevision: expectedRevisionArg, ...patchArg, }, { maxRetries: 0 }, ); return response.draft; }, isConflict: (errorArg) => ( errorArg instanceof plugins.typedrequest.TypedResponseError && errorArg.errorData?.code === 'concurrent_change' ), }, onState: (stateArg) => { const previousState = this.sessionDraftState; this.sessionDraftState = stateArg; if ( stateArg && this.draftSessionActive && controllerRuntimeIdsEqual(this.selectedSessionId, stateArg.sessionId) ) { this.localDraftText = stateArg.text; this.localDraftAttachments = stateArg.attachments.map((attachment) => ({ ...attachment })); } if ( stateArg && previousState?.text !== stateArg.text && stateArg.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(stateArg.sessionId, this.selectedSessionId) ) this.updateSlashSuggestions(stateArg.text); }, onError: (errorArg) => { if (this.authenticated && this.socketClient.isConnected) { this.reportWorkspaceError('Sync the composer draft', errorArg); } }, }); } public static styles = [ plugins.deesCatalog.themeDefaultStyles, plugins.deesElement.cssManager.defaultStyles, plugins.deesElement.css` :host { position: fixed; inset: 0; display: block; min-width: 0; min-height: 0; overflow: hidden; color: var(--dees-color-text-primary); background: radial-gradient(circle at 12% -12%, color-mix(in srgb, var(--dees-color-accent-primary) 13%, transparent), transparent 34%), var(--dees-color-bg-canvas); font-family: var(--dees-font-family); } * { box-sizing: border-box; } .surfaceShell { display: grid; grid-template-rows: minmax(0, 1fr) auto; width: 100%; height: 100%; } .centerSurface { min-height: 0; display: flex; align-items: center; justify-content: center; overflow-y: auto; padding: var(--dees-spacing-2xl) var(--dees-spacing-xl); } .authSurface { position: relative; min-height: 0; } .authSurface dees-simple-login { width: 100%; height: 100%; } .tempPasswordSection { position: absolute; left: 50%; bottom: var(--dees-spacing-2xl); transform: translateX(-50%); display: flex; flex-direction: column; gap: var(--dees-spacing-md); width: min(100%, 360px); z-index: 1; } .setupCard > .tempPasswordSection { position: static; width: 100%; margin-top: var(--dees-spacing-lg); transform: none; } .tempPasswordToggle { appearance: none; border: none; background: transparent; padding: var(--dees-spacing-sm); color: var(--dees-color-text-secondary); font: inherit; font-size: var(--dees-font-control-size-sm); text-decoration: underline; cursor: pointer; } .tempPasswordToggle:hover { color: var(--dees-color-text-primary); } .tempPasswordForm { display: flex; flex-direction: column; gap: var(--dees-spacing-md); padding: var(--dees-spacing-lg); border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-2xl); corner-shape: var(--dees-corner-shape); background: var(--dees-material-thick-bg-opaque); box-shadow: var(--dees-shadow-lg); } .setupCard, .statusCard, .emptyWorkspace { width: min(100%, 430px); padding: var(--dees-spacing-2xl); border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-2xl); corner-shape: var(--dees-corner-shape); background: var(--dees-material-thick-bg-opaque); box-shadow: var(--dees-shadow-lg); } .statusCard, .emptyWorkspace { display: flex; flex-direction: column; align-items: center; gap: var(--dees-spacing-lg); text-align: center; } .statusIcon, .setupIcon { display: grid; place-items: center; width: 48px; height: 48px; border-radius: var(--dees-radius-xl); background: var(--dees-color-badge-default-bg); color: var(--dees-color-accent-primary); } .setupIcon { margin-bottom: var(--dees-spacing-xl); } .title { margin: 0; color: var(--dees-color-text-primary); font-size: var(--dees-font-title2-size); font-weight: 650; letter-spacing: -0.025em; } .description { margin: var(--dees-spacing-sm) 0 0; color: var(--dees-color-text-secondary); font-size: var(--dees-font-control-size); line-height: 1.55; } .setupForm { display: flex; flex-direction: column; gap: var(--dees-spacing-lg); margin-top: var(--dees-spacing-xl); } .notice, .errorBanner { display: flex; align-items: flex-start; gap: var(--dees-spacing-sm); padding: var(--dees-spacing-sm) var(--dees-spacing-md); border-radius: var(--dees-radius-lg); font-size: var(--dees-font-control-size-sm); line-height: 1.45; } .notice { margin-top: var(--dees-spacing-lg); color: var(--dees-color-text-warning); background: var(--dees-color-badge-warning-bg); } .errorBanner { color: var(--dees-color-text-error); background: var(--dees-color-badge-error-bg); } .workspaceShell { display: grid; grid-template-rows: 56px minmax(0, 1fr) auto; width: 100%; height: 100%; background: var(--dees-color-bg-canvas); } .appHeader { display: flex; align-items: center; gap: var(--dees-spacing-md); min-width: 0; padding: 0 var(--dees-spacing-lg); border-bottom: 1px solid var(--dees-color-border-subtle); background: var(--dees-material-thick-bg); backdrop-filter: var(--dees-material-thick-filter); } .brandMark { display: grid; place-items: center; width: 30px; height: 30px; flex: 0 0 auto; border-radius: var(--dees-radius-lg); color: var(--dees-color-on-accent); background: var(--dees-color-accent-primary); } .brandCopy { min-width: 0; } .brandTitle { overflow: hidden; color: var(--dees-color-text-primary); font-size: var(--dees-font-control-size); font-weight: 650; text-overflow: ellipsis; white-space: nowrap; } .brandSubtitle { overflow: hidden; margin-top: 1px; color: var(--dees-color-text-muted); font-family: var(--dees-font-family-mono); font-size: var(--dees-font-caption2-size); text-overflow: ellipsis; white-space: nowrap; } .headerSpacer { flex: 1; } .headerActions { display: flex; align-items: center; gap: var(--dees-spacing-xs); } .workspaceGrid { display: grid; grid-template-columns: var(--sidebar-width, 292px) 6px minmax(0, 1fr); /* the single row must fill the container, otherwise short transcripts collapse the row and the composer floats above the viewport bottom */ grid-template-rows: minmax(0, 1fr); min-width: 0; min-height: 0; } .workspaceGrid.sidebarHidden { grid-template-columns: minmax(0, 1fr); } .workspaceGrid.sidebarHidden .sessionSidebar, .workspaceGrid.sidebarHidden .sidebarSplitter { display: none; } .sessionSidebar { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--dees-color-bg-secondary); } .sidebarSplitter { position: relative; z-index: 2; width: 6px; min-height: 0; border: 0; border-right: 1px solid var(--dees-color-border-subtle); border-left: 1px solid transparent; background: transparent; cursor: col-resize; touch-action: none; transition: background var(--dees-transition-fast) var(--dees-ease-standard); } .sidebarSplitter:hover, .sidebarSplitter.active { background: color-mix(in srgb, var(--dees-color-accent-primary) 18%, transparent); } .sidebarSplitter:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: -2px; background: color-mix(in srgb, var(--dees-color-accent-primary) 14%, transparent); } .sidebarSectionLabel { padding: var(--dees-spacing-sm) var(--dees-spacing-md) 2px; color: var(--dees-color-text-muted); font-size: 10px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } .projectRow { display: flex; align-items: center; gap: var(--dees-spacing-xs); padding: var(--dees-spacing-xs) var(--dees-spacing-sm) var(--dees-spacing-sm); } .projectRow dees-input-dropdown { flex: 1; min-width: 0; margin: 0; } .newProjectRow { display: flex; align-items: center; gap: var(--dees-spacing-xs); padding: var(--dees-spacing-sm); border-bottom: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-primary); } .newProjectRow dees-input-text { flex: 1; min-width: 0; margin: 0; } .pathSuggestionHint { padding: 4px var(--dees-spacing-sm); color: var(--dees-color-text-muted); font-size: var(--dees-font-caption2-size); line-height: 1.4; } .pathSuggestionList { display: grid; max-height: 190px; overflow-y: auto; padding: var(--dees-spacing-xs); gap: 2px; border-bottom: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-primary); } .pathSuggestion { display: flex; align-items: center; gap: var(--dees-spacing-sm); width: 100%; padding: 6px var(--dees-spacing-sm); border: none; border-radius: var(--dees-radius-md); background: transparent; color: var(--dees-color-text-primary); font-family: var(--dees-font-family-mono); font-size: var(--dees-font-caption1-size); text-align: left; cursor: pointer; } .pathSuggestion:hover { background: var(--dees-color-bg-secondary); } .pathSuggestion span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .projectsRootHint { padding: var(--dees-spacing-xs) var(--dees-spacing-sm) 0; color: var(--dees-color-text-secondary); font-size: var(--dees-font-control-size-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .projectsRootHint code { font-family: var(--dees-font-family-mono); color: var(--dees-color-text-primary); } .projectCreateError { display: flex; align-items: flex-start; gap: var(--dees-spacing-xs); margin: 0 var(--dees-spacing-sm); padding: var(--dees-spacing-xs) var(--dees-spacing-sm); border-radius: var(--dees-radius-md); color: var(--dees-color-text-error); background: var(--dees-color-badge-error-bg); font-size: var(--dees-font-control-size-sm); line-height: 1.4; } .sessionLayoutError { flex: 0 0 auto; margin: 0 var(--dees-spacing-sm) var(--dees-spacing-xs); } /* In the sidebar flow under its header, so a sidebar action states what it needs where it was taken and never covers the view the workspace is showing. */ .sidebarNotice { flex: 0 0 auto; margin: 0 var(--dees-spacing-sm) var(--dees-spacing-xs); } .blockedRemoval { flex: 0 0 auto; align-items: center; margin: 0 var(--dees-spacing-sm) var(--dees-spacing-xs); } .blockedRemovalCopy { flex: 1 1 auto; min-width: 0; } .blockedRemovalTitle { font-weight: 600; } .sidebarHeader { display: flex; align-items: center; gap: var(--dees-spacing-xs); padding: var(--dees-spacing-sm) var(--dees-spacing-sm) var(--dees-spacing-xs); } .sidebarLabel { flex: 1; min-width: 0; padding-left: var(--dees-spacing-xs); color: var(--dees-color-text-muted); font-size: 10px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } .sessionSidebar dees-harness-session-list { flex: 1; min-height: 0; } .chatPane { position: relative; min-width: 0; min-height: 0; background: var(--dees-color-bg-primary); } .chatPane dees-harness-chat { width: 100%; height: 100%; } .chatStack { /* flex, not grid: the permission dock is conditionally rendered, and a lone chat child must still take the full remaining height */ display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; } .chatStack dees-harness-chat { flex: 1; min-height: 0; height: auto; } .historyStatus { position: absolute; z-index: 2; top: 10px; left: 50%; transform: translateX(-50%); padding: 4px 10px; border: 1px solid var(--dees-color-border-subtle); border-radius: 999px; background: var(--dees-color-bg-secondary); color: var(--dees-color-text-secondary); font-size: 11px; line-height: 1.3; pointer-events: none; } .codexActivity { display: flex; align-items: center; flex-wrap: wrap; min-width: 0; overflow-wrap: anywhere; gap: 6px var(--dees-spacing-sm); padding: 6px var(--dees-spacing-md); border-top: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-secondary); color: var(--dees-color-text-secondary); font-size: 12px; line-height: 1.35; } .codexActivityNotice { flex: 1 1 220px; min-width: 0; } .codexActivityControls, .codexCollaborationMode { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--dees-spacing-xs); } .codexActivityControls { flex: 0 1 auto; } .codexActivityControls:not(:has(.codexCollaborationMode)) { margin-left: auto; } .codexCollaborationModeLabel { font-weight: 650; color: var(--dees-color-text-primary); } .codexCollaborationModeButtons { display: inline-flex; min-width: 150px; padding: 2px; border-radius: var(--dees-radius-lg); background: var(--dees-color-fill-tertiary); } .codexCollaborationModeButton { flex: 1 1 0; min-height: 24px; padding: 1px 10px; border: 0; border-radius: calc(var(--dees-radius-lg) - 2px); background: transparent; color: var(--dees-color-text-secondary); font: 600 12px/1.35 var(--dees-font-family); cursor: pointer; } .codexCollaborationModeButton[aria-pressed='true'] { background: var(--dees-color-control-thumb); color: var(--dees-color-text-primary); box-shadow: var(--dees-shadow-xs); } .codexCollaborationModeButton.plan[aria-pressed='true'] { background: color-mix(in srgb, var(--dees-color-accent-primary) 14%, var(--dees-color-control-thumb)); color: var(--dees-color-accent-primary); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dees-color-accent-primary) 45%, transparent); } .codexCollaborationModeButton:focus-visible { outline: 2px solid var(--dees-color-focus-ring); outline-offset: 1px; } .codexCollaborationModeButton:disabled { opacity: .5; cursor: default; } .codexCollaborationModeButton[aria-pressed='true']:disabled { opacity: 1; } @media (pointer: coarse) { .codexCollaborationModeButton { min-height: 44px; } } .codexDiffSummary { display: inline-flex; align-items: center; flex: 0 1 auto; flex-wrap: wrap; gap: var(--dees-spacing-xs); } .codexDiffAdded, .codexDiffRemoved { font-family: var(--dees-font-family-mono); font-weight: 700; font-variant-numeric: tabular-nums; } .codexDiffAdded { color: var(--dees-color-text-success); } .codexDiffRemoved { color: var(--dees-color-text-error); } .codexDiffQualifier { color: var(--dees-color-text-warning); } .childAccessNotice { flex: 0 0 auto; min-width: 0; overflow: hidden; padding: 5px var(--dees-spacing-md); border-bottom: 1px solid var(--dees-color-border-subtle); color: var(--dees-color-text-warning); background: var(--dees-color-badge-warning-bg); font-size: var(--dees-font-caption1-size); line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; } .workspaceReversion { display: flex; flex: 0 0 auto; align-items: center; gap: var(--dees-spacing-xs); min-width: 0; padding: 5px var(--dees-spacing-md); border-bottom: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-secondary); color: var(--dees-color-text-secondary); font-size: var(--dees-font-caption1-size); } .workspaceReversionLabel { flex: 0 0 auto; color: var(--dees-color-text-muted); font-weight: 650; } .workspaceRepositories { display: flex; flex: 1; gap: 4px; min-width: 0; overflow: hidden; } .workspaceRepository, .workspaceReversionFlag { overflow: hidden; padding: 1px 6px; border-radius: var(--dees-radius-full); background: var(--dees-color-badge-default-bg); text-overflow: ellipsis; white-space: nowrap; } .workspaceRepository { max-width: 220px; font-family: var(--dees-font-family-mono); } .workspaceReversionFlag { flex: 0 0 auto; color: var(--dees-color-text-warning); background: var(--dees-color-badge-warning-bg); } .terminalStack { display: flex; flex-direction: column; min-height: 0; height: 100%; } .terminalHeader { display: flex; flex: 0 0 auto; align-items: center; gap: var(--dees-spacing-sm); padding: var(--dees-spacing-sm) var(--dees-spacing-md); border-bottom: 1px solid var(--dees-color-border-subtle); color: var(--dees-color-text-primary); } .terminalHeading { min-width: 0; } .terminalTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--dees-font-control-size-sm); font-weight: 650; } .terminalSubtitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dees-color-text-secondary); font-family: var(--dees-font-family-mono); font-size: var(--dees-font-caption1-size); } .terminalEndedBadge { margin-left: auto; padding: 1px 8px; border-radius: 999px; background: var(--dees-color-badge-warning-bg); color: var(--dees-color-badge-warning-fg); font-size: var(--dees-font-caption1-size); font-weight: 650; } .terminalStack dees-terminal-view { flex: 1; min-height: 0; } .browserPane { display: flex; min-width: 0; min-height: 0; height: 100%; } .browserStack { display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; min-height: 0; height: 100%; } .browserSidebar { flex: 0 0 auto; width: 260px; min-height: 0; height: 100%; border-left: 1px solid var(--dees-color-border-subtle); } .browserSidebarRow { display: flex; align-items: baseline; justify-content: space-between; gap: var(--dees-spacing-md); color: var(--dees-color-text-secondary); } .browserSidebarRow + .browserSidebarRow { margin-top: 6px; } .browserSidebarValue { color: var(--dees-color-text-primary); font-family: var(--dees-font-family-mono); font-variant-numeric: tabular-nums; } .browserSidebarEmpty { color: var(--dees-color-text-secondary); } .browserStatisticsToggle { display: block; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--dees-color-border-subtle); } .browserToolbar { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto auto; align-items: center; gap: var(--dees-spacing-xs); padding: var(--dees-spacing-sm) var(--dees-spacing-md); border-bottom: 1px solid var(--dees-color-border-subtle); } .browserToolbar input { min-width: 0; padding: 7px 10px; border: 1px solid var(--dees-color-border-default); border-radius: var(--dees-radius-sm); background: var(--dees-color-bg-primary); color: var(--dees-color-text-primary); font: inherit; } .browserDevTools { flex: 0 0 42%; min-height: 180px; display: flex; flex-direction: column; border-top: 1px solid var(--dees-color-border-default); } .browserDevTools iframe { flex: 1; min-height: 0; width: 100%; border: 0; background: #202124; } .browserWebsiteError { display: flex; align-items: center; gap: 8px; padding: 5px 12px; border-bottom: 1px solid var(--dees-color-border-default); font-size: 12px; } .browserWebsiteError span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .browserDevToolsError { padding: 8px 12px; font-size: 12px; } .browserVideoSurface { position: relative; flex: 1; min-height: 0; overflow: hidden; background: #111315; outline: none; } .browserVideoSurface video { display: block; width: 100%; height: 100%; } .browserStatistics { position: absolute; right: var(--dees-spacing-sm); bottom: var(--dees-spacing-sm); max-width: calc(100% - (2 * var(--dees-spacing-sm))); padding: 2px 8px; border-radius: var(--dees-radius-sm); background: rgba(17, 19, 21, 0.72); color: rgba(255, 255, 255, 0.82); font-size: 11px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; pointer-events: none; user-select: none; } .browserLoading { display: grid; place-items: center; height: 100%; } .workspaceNotice { position: absolute; top: var(--dees-spacing-sm); left: 50%; z-index: 2; width: min(620px, calc(100% - 24px)); margin-top: 0; transform: translateX(-50%); box-shadow: var(--dees-shadow-md); color: var(--dees-color-text-primary); background: var(--dees-color-bg-secondary); } /* Below the composer and in the flow, so it states what the send needs right where the owner is typing without ever covering the transcript. */ .composerNotice { flex: 0 0 auto; margin: 0 var(--dees-spacing-md) var(--dees-spacing-md); } .emptyWorkspaceWrap { display: grid; place-items: center; width: 100%; height: 100%; padding: var(--dees-spacing-xl); overflow-y: auto; } .newConversationBox { display: flex; flex-direction: column; gap: var(--dees-spacing-md); width: min(100%, 460px); padding: var(--dees-spacing-2xl); border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-2xl); corner-shape: var(--dees-corner-shape); background: var(--dees-material-thick-bg-opaque); box-shadow: var(--dees-shadow-lg); } .newConversationHeading { display: flex; align-items: center; gap: var(--dees-spacing-md); } .newConversationField { display: flex; flex-direction: column; gap: var(--dees-spacing-xs); } .newConversationLabel { color: var(--dees-color-text-muted); font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } .newConversationHarnesses, .newConversationSecondary { display: flex; flex-wrap: wrap; gap: var(--dees-spacing-xs); } .newConversationHint { color: var(--dees-color-text-muted); font-size: 13px; } .newConversationStandardDirs { display: flex; flex-wrap: wrap; gap: var(--dees-spacing-xs); } .newConversationStandardDir { display: flex; align-items: center; gap: var(--dees-spacing-xs); padding: 3px 8px; border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-lg); background: transparent; color: var(--dees-color-text-muted); font: inherit; font-size: 12px; cursor: pointer; } .statusBar { position: relative; display: flex; align-items: center; gap: 0; min-width: 0; min-height: 30px; padding: 0; border-top: 1px solid var(--dees-color-border-subtle); color: var(--dees-color-text-muted); background: var(--dees-color-bg-secondary); font-family: var(--dees-font-family-mono); font-size: var(--dees-font-caption2-size); } .statusConnection, .statusMetrics, .statusMetric, .legalDisclosure { display: flex; align-items: center; } .statusConnection { flex: 0 0 auto; gap: 6px; padding: 0 8px; white-space: nowrap; } .statusConnectionLabel { display: inline-block; width: 13ch; } .statusDot { width: 7px; height: 7px; border-radius: 50%; background: var(--dees-color-text-muted); } .statusDot.connected { background: var(--dees-color-accent-success); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dees-color-accent-success) 18%, transparent); } .statusDot.pending { background: var(--dees-color-accent-warning); } .statusMetrics { align-self: stretch; flex: 1 1 auto; min-width: 0; margin-left: 0; } .metricsScroller { display: flex; align-self: stretch; flex: 1 1 auto; min-width: 0; overflow-x: auto; overflow-y: hidden; white-space: nowrap; scrollbar-width: none; scroll-snap-type: x proximity; } .metricsEndSpacer { flex: 0 0 0px; align-self: stretch; } .metricsScroller::-webkit-scrollbar { display: none; } .metricsScroller:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: -2px; } .statsTrigger { display: flex; align-items: stretch; gap: 4px; flex: 0 0 auto; min-width: max-content; height: 100%; padding: 0 8px; border: 0; border-left: 1px solid var(--dees-color-border-subtle); background: transparent; color: inherit; font: inherit; cursor: pointer; } .statsTrigger:hover, .statsTrigger[aria-expanded='true'] { background: var(--dees-color-bg-tertiary); } .statsTrigger:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: -2px; } .metricPopoverTrigger { padding: 0; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; } .metricPopoverTrigger:hover, .metricPopoverTrigger[aria-expanded='true'] { background-color: var(--dees-color-bg-tertiary); } .metricPopoverTrigger:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: -2px; } .statsTriggerLabel, .statsAlert { display: flex; align-items: center; padding: 0; font-weight: 700; } .statsAlert { color: var(--dees-color-text-warning); } .statsAlert[hidden] { display: none; } .statsSampleCue { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 12px; color: var(--dees-color-text-primary); font-weight: 700; } .statusMetric { align-self: stretch; display: grid; align-items: center; flex: 0 0 auto; box-sizing: border-box; padding: 0 8px; border-left: 1px solid var(--dees-color-border-subtle); color: var(--dees-color-text-secondary); font-variant-numeric: tabular-nums; scroll-snap-align: start; } button.statusMetric { border-top: 0; border-right: 0; border-bottom: 0; background: transparent; font: inherit; cursor: pointer; } .cpuMetric, .memoryMetric { grid-template-columns: 1fr 4ch; column-gap: 4px; width: 78px; } .networkMetric { grid-template-columns: auto auto auto; column-gap: 14px; width: 220px; } .diskMetric { grid-template-columns: 43px 4ch auto; column-gap: 4px; width: 156px; } .diskCapacityTrigger { align-self: stretch; display: grid; grid-column: 1 / 3; grid-template-columns: 43px 4ch; align-items: center; column-gap: 4px; } .diskMount { display: inline-block; width: 43px; overflow: hidden; text-overflow: ellipsis; } .metricValue { display: inline-block; width: 4ch; text-align: right; } .diskIo { display: grid; align-self: stretch; grid-template-columns: auto 4ch; align-items: center; column-gap: 4px; margin-left: 10px; } .ioValue { display: inline-block; width: 4ch; text-align: right; } .diskIo.ioBusyWarning { color: var(--dees-color-text-warning); background: var(--dees-color-badge-warning-bg); } .diskIo.ioBusyCritical { color: var(--dees-color-text-error); background: var(--dees-color-badge-error-bg); font-weight: 700; } .diskIo.ioBusyWarning .metricLabel, .diskIo.ioBusyCritical .metricLabel { color: inherit; } .networkReceive, .networkTransmit { display: grid; grid-template-columns: auto 8ch; align-items: center; column-gap: 4px; } .networkRate { display: inline-block; width: 8ch; text-align: right; } .metricLabel.edgeClipped, .metricValue.edgeClipped, .ioValue.edgeClipped, .networkRate.edgeClipped { opacity: 0; } .diskIo.edgeClipped, .networkReceive.edgeClipped, .networkTransmit.edgeClipped { opacity: 0; } .statusMetric .strainedValue { font-weight: 700; } .metricsRailControl { align-self: stretch; flex: 0 0 auto; padding: 0; border: 0; border-left: 1px solid var(--dees-color-border-subtle); background: transparent; color: var(--dees-color-text-secondary); font: 700 11px var(--dees-font-family-mono); cursor: pointer; } .metricsRailControl { width: 38px; } .metricsRailControl.hasStrain { color: var(--dees-color-text-error); } .metricsRailControl.inactive { display: none; } .metricsRailControl:focus-visible { outline: 2px solid var(--dees-color-accent-primary); outline-offset: -2px; } @keyframes resourceStrainFlash { 0%, 100% { background: color-mix(in srgb, var(--dees-color-text-error) 8%, transparent); } 50% { background: color-mix(in srgb, var(--dees-color-text-error) 34%, transparent); } } .statusMetric.strained { color: var(--dees-color-text-primary); border-left-color: var(--dees-color-border-strong); box-shadow: inset 1px 0 0 color-mix(in srgb, var(--dees-color-text-primary) 40%, transparent); animation: resourceStrainFlash 900ms ease-in-out infinite; } .statusMetric.strained .metricLabel, .statusMetric.strained .diskIo.ioBusyWarning, .statusMetric.strained .diskIo.ioBusyCritical { color: inherit; } .statusMetric.strained .diskIo.ioBusyWarning, .statusMetric.strained .diskIo.ioBusyCritical { background: transparent; } @keyframes highCpuFlash { 0%, 100% { background: transparent; } 50% { background: color-mix(in srgb, var(--dees-color-text-error) 34%, transparent); color: var(--dees-color-text-primary); } } .statsTrigger.highCpuWarning { animation: highCpuFlash 900ms ease-in-out infinite; } .statsTrigger.highCpuWarning .metricLabel { color: inherit; } .metricLabel { color: var(--dees-color-text-muted); font-size: 9px; font-weight: 700; letter-spacing: 0.05em; text-align: left; } .legalDisclosure { align-self: stretch; flex: 0 0 auto; margin-left: auto; scroll-snap-align: start; justify-content: center; border-left: 1px solid var(--dees-color-border-subtle); border-top: 0; border-right: 0; border-bottom: 0; padding: 0 8px; background: transparent; color: var(--dees-color-text-secondary); font: inherit; cursor: default; user-select: none; } .legalDetails { position: absolute; right: 0; bottom: calc(100% + 7px); z-index: ${plugins.deesCatalog.zIndexLayers.overlay.contextMenu}; width: max-content; max-width: min(360px, calc(100vw - 24px)); padding: var(--dees-spacing-sm) var(--dees-spacing-md); border: 1px solid var(--dees-color-border-default); border-radius: var(--dees-radius-md); background: var(--dees-material-thick-bg-opaque); box-shadow: var(--dees-shadow-md); font-family: var(--dees-font-family); line-height: 1.4; opacity: 0; transform: translateY(4px); transition: opacity 140ms ease, transform 140ms ease; pointer-events: none; } .legalDetails.visible, .legalDetails:focus-within { opacity: 1; transform: translateY(0); pointer-events: auto; } .legalDisclosure:focus-within { outline: 1px solid var(--dees-color-accent-primary); outline-offset: -1px; } .legalDetails a { margin-left: 4px; color: inherit; text-decoration: none; } .legalDetails a:hover, .legalDetails a:focus-visible { color: var(--dees-color-text-primary); text-decoration: underline; } @media (max-width: 760px) { .appHeader { padding: 0 var(--dees-spacing-sm); } .brandSubtitle { display: none; } .workspaceGrid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(150px, 30vh) minmax(0, 1fr); } .workspaceGrid.sidebarHidden { grid-template-rows: minmax(0, 1fr); } .sidebarSplitter { display: none; } .sessionSidebar { border-right: none; border-bottom: 1px solid var(--dees-color-border-subtle); } .centerSurface { align-items: flex-start; padding: var(--dees-spacing-xl) var(--dees-spacing-md); } .setupCard, .statusCard { padding: var(--dees-spacing-xl); } .statusBar { gap: 0; padding: 0; } .statusConnection { padding: 0 6px; } .statusConnectionLabel { display: none; } .statusMetric { padding: 0 6px; } .cpuMetric, .memoryMetric { width: 66px; } .diskMetric { grid-template-columns: 37px 4ch auto; width: 128px; } .diskMount { width: 37px; } .diskIo { margin-left: 4px; } .networkMetric { column-gap: 8px; width: 208px; } .statsTrigger { padding: 0 6px; } .metricLabel { font-size: 8px; } .legalDisclosure { padding: 0 6px; } } @media (prefers-reduced-motion: reduce) { .statsTrigger.highCpuWarning { animation: none; background: color-mix(in srgb, var(--dees-color-text-error) 34%, transparent); color: var(--dees-color-text-primary); } .statusMetric.strained { animation: none; background: color-mix(in srgb, var(--dees-color-text-error) 34%, transparent); color: var(--dees-color-text-primary); } } `, ]; public async connectedCallback(): Promise { const lifecycleGeneration = ++this.lifecycleGeneration; document.addEventListener('focusin', this.handleDocumentFocusIn, true); document.addEventListener(plugins.deesCatalog.actionErrorEventName, this.handleActionError); await super.connectedCallback(); if (lifecycleGeneration !== this.lifecycleGeneration || !this.isConnected) { // A newer connection may already own the deduplicated listener. if (!this.isConnected) { document.removeEventListener('focusin', this.handleDocumentFocusIn, true); document.removeEventListener(plugins.deesCatalog.actionErrorEventName, this.handleActionError); } return; } this.componentConnected = true; this.footerAgeTimer = setInterval(() => this.updateSystemMetricsDom(), 1_000); this.restoreSidebarWidth(); this.restoreBrowserSidebarVisible(); this.restoreUpgradeStatus(); void this.connectSocket(lifecycleGeneration); } public async disconnectedCallback(): Promise { this.lifecycleGeneration += 1; this.componentConnected = false; if (this.footerAgeTimer) clearInterval(this.footerAgeTimer); this.footerAgeTimer = undefined; this.footerMetricsResizeObserver?.disconnect(); this.footerMetricsResizeObserver = undefined; this.footerMetricsScroller = undefined; document.removeEventListener('focusin', this.handleDocumentFocusIn, true); document.removeEventListener(plugins.deesCatalog.actionErrorEventName, this.handleActionError); this.closeMetricPopover(); await this.closeStatsPanel(false); await this.destroyCodexDiffModal(); await this.destroyExistingConversationModal(); await this.destroyErrorJournalModal(); this.cancelComposerFocus(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.sessionDraftSync.deactivate(); await this.closeBrowserView(); this.cancelSidebarResize(); this.invalidateConnectionState(); this.clearProviderManagementSnapshot(); this.hideLegalDisclosure(); this.stopProviderPolling(); await this.socketClient.stop(); await super.disconnectedCallback(); } public render(): plugins.deesElement.TemplateResult { if (this.connectionStatus !== 'connected') { return this.renderConnectionState(); } if (this.staleBundleVersion) { return this.renderStaleBundleState(); } if (this.authState === 'loading') { return this.renderLoadingState(this.controllerStatus?.lifecycleState === 'starting' ? 'Waiting for the controller to finish starting…' : 'Checking authentication…'); } if (this.authState === 'setupRequired' && !this.authenticated) { return this.renderSetup(); } if (!this.authenticated) { return this.renderLogin(); } return this.renderWorkspace(); } private async connectSocket(lifecycleGenerationArg = this.lifecycleGeneration): Promise { if (!this.isCurrentLifecycle(lifecycleGenerationArg)) { return; } this.connectionError = ''; try { await this.socketClient.start(); } catch (error) { if (!this.isCurrentLifecycle(lifecycleGenerationArg)) { return; } this.connectionStatus = 'disconnected'; this.connectionError = errorMessage(error); } } private isCurrentLifecycle(lifecycleGenerationArg: number): boolean { return ( this.componentConnected && this.isConnected && this.lifecycleGeneration === lifecycleGenerationArg ); } private handleConnectionStatus(statusArg: plugins.typedsocket.TConnectionStatus): void { if (!this.componentConnected) { return; } this.connectionStatus = statusArg; this.connectionGeneration += 1; this.clearStartupCheck(); if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer); this.browserViewCloseRetryTimer = undefined; if (this.browserViewPendingClose?.connectionGeneration !== this.connectionGeneration) { this.browserViewPendingClose = undefined; } this.invalidateAuthenticatedWorkspace(); this.cancelCeremony(); if (statusArg === 'connected') { this.connectionError = ''; this.authState = 'loading'; const generation = this.connectionGeneration; void this.reconcileAuthState(generation); } else { this.authState = 'loading'; } } private invalidateConnectionState(): void { this.connectionGeneration += 1; this.clearStartupCheck(); if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer); this.browserViewCloseRetryTimer = undefined; if (this.browserViewPendingClose?.connectionGeneration !== this.connectionGeneration) { this.browserViewPendingClose = undefined; } this.cancelCeremony(); this.invalidateAuthenticatedWorkspace(); if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = undefined; } } private invalidateAuthenticatedWorkspace(): void { this.cancelComposerFocus(); this.sessionDraftSync.deactivate(); void this.closeBrowserView(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.authenticated = false; this.refreshPending = false; this.refreshRelatedPending = false; this.detailRefreshPending = false; this.projects = []; this.sessions = []; this.terminals = []; this.resources = []; this.selectedResourceId = ''; this.sessionGroups = []; this.ungroupedItemIds = []; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; this.sessionsLoading = false; this.detailLoading = false; this.mutationPending = false; this.sessionModelPending = false; this.sessionModelOverrides.clear(); this.sessionEffortOverrides.clear(); this.sessionAccountOverrides.clear(); this.sessionModelSaveTokens.clear(); this.scratchpadSaveIdsBySession.clear(); this.scratchpadErrorsBySession.clear(); this.intelligenceAskIdsBySession.clear(); this.intelligenceErrorsBySession.clear(); this.clearLiveToolState(); this.clearLiveMessageState(); this.liveHistoryBarriers.clear(); this.clearSubtaskPreviewState(); this.pendingChildEventsByScopeGeneration.clear(); this.liveToolStreamEpochs.clear(); this.liveMessageStreamEpochs.clear(); this.draftSessionActive = false; this.activeMutationId = undefined; this.sessionLayoutError = ''; this.sidebarNotice = ''; this.workspaceNotice = ''; this.composerNotice = ''; // The error journal survives on purpose: it is the tab's record of what went wrong, and a // reconnection is often exactly what the owner wants to report about. Clear empties it. this.clearAnsweredCardCaches(); this.clearSubagentModalState(); this.sessionsLoadedForProjectId = ''; this.groupsLoaded = false; this.terminalsLoadedForProjectId = ''; this.selectedTerminalId = undefined; this.selectedTerminalEnded = false; this.selectedTerminalAttached = false; this.terminalAttachGeneration += 1; this.terminalAttachPending = false; this.resetTerminalStream(); this.sessionsRequestId += 1; this.projectsRequestId += 1; this.modelCatalogRequestId += 1; this.layoutRequestId += 1; this.terminalRequestId += 1; // selectedProjectId survives as a preference so a reconnect returns to // the same project once the registry reloads. if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = undefined; } if (this.immediateRefreshTimer) { clearTimeout(this.immediateRefreshTimer); this.immediateRefreshTimer = undefined; } this.immediateRefreshPending = false; if (this.systemMetricsTimer) { clearTimeout(this.systemMetricsTimer); this.systemMetricsTimer = undefined; } this.systemMetrics = undefined; void this.closeStatsPanel(false); this.authoritativeStatusesByProject.clear(); this.finishedSessionIdsByProject.clear(); this.errorSessionIdsByProject.clear(); this.optimisticWorkingIdsByProject.clear(); this.sessionEventRevisionByProject.clear(); this.confirmedSessionLayout = undefined; this.pendingSessionLayout = undefined; this.layoutSavePromise = undefined; this.pendingLayoutReload = false; this.groupsLoaded = false; this.providerManagementState = 'loading'; this.providerRateLimits.clear(); this.providerRateLimitErrors.clear(); this.providerRateLimitTasks.clear(); this.providerOpenCodeSwitch = undefined; this.providerOpenCodeSwitchStatus = ''; this.activeProviderLogin = undefined; this.providerManagementRefreshRequested = false; this.destroySettingsModal(); this.stopProviderPolling(); this.resetHighCpuWarning(); } private cancelCeremony(): void { this.activeCeremonyId = undefined; this.ceremonyBusy = false; plugins.simpleWebAuthnBrowser.WebAuthnAbortService.cancelCeremony(); } private async reconcileAuthState(generationArg: number): Promise { try { if (await this.reconcileBackendVersion(generationArg) === false) return; const response = await this.socketClient.fire( 'controller.auth.state', {}, ); if (!this.isCurrentConnection(generationArg)) { return; } if (response.state !== 'setupRequired' && response.state !== 'ready') { throw new Error('The controller returned an invalid authentication state.'); } this.authState = response.state; this.setupCodeExpired = response.setupCodeExpired === true; this.authenticated = response.authenticated === true; this.connectionError = ''; if (!this.authenticated) { // A reconnect starts unauthenticated; a stored resume token // re-authenticates without changing global passkey setup state. await this.tryResumeAuthentication(generationArg); } if (this.authenticated) { await this.loadWorkspaceData(); } else { this.clearProviderManagementSnapshot(); } } catch (error) { if (!this.isCurrentConnection(generationArg)) { return; } this.connectionError = errorMessage(error); this.authState = 'loading'; } } private static readonly resumeTokenStorageKey = 'harnessControllerResumeToken'; private static readonly closeLogStorageKey = 'harnessControllerCloseLog'; private static readonly upgradeStorageKey = 'harnessControllerUpgradeStatus'; private static readonly reloadFenceStorageKey = 'harnessControllerUpgradeReloadFence'; private restoreSidebarWidth(): void { try { const stored = Number(globalThis.localStorage?.getItem(sidebarWidthStorageKey)); if (Number.isFinite(stored) && stored > 0) { this.sidebarWidth = controllerClampSidebarWidth(stored); } } catch { // Sidebar sizing remains usable when browser storage is unavailable. } } private persistSidebarWidth(): void { try { globalThis.localStorage?.setItem(sidebarWidthStorageKey, String(this.sidebarWidth)); } catch { // Persistence is optional; resizing itself must still work. } } /** * The diagnostics panel is a view preference like the sidebar width, so it is remembered the * same way: per browser profile, and never at the cost of the toggle itself working. */ private restoreBrowserSidebarVisible(): void { try { this.browserSidebarVisible = globalThis.localStorage?.getItem(browserSidebarStorageKey) === 'open'; } catch { // The panel stays usable when browser storage is unavailable. } } private readonly toggleBrowserSidebar = (): void => { this.browserSidebarVisible = !this.browserSidebarVisible; try { globalThis.localStorage?.setItem( browserSidebarStorageKey, this.browserSidebarVisible ? 'open' : 'closed', ); } catch { // Persistence is optional; toggling itself must still work. } }; private releaseSidebarPointerCapture(pointerIdArg: number | undefined): void { const splitter = this.shadowRoot?.querySelector('.sidebarSplitter'); if (pointerIdArg === undefined || !splitter?.hasPointerCapture(pointerIdArg)) return; splitter.releasePointerCapture(pointerIdArg); } private workspaceGridElement(): HTMLElement | null { return this.shadowRoot?.querySelector('.workspaceGrid') ?? null; } /** * Pointer drags update the grid's CSS variable directly so every pointer * move paints immediately without a full application render; the reactive * width is committed once when the drag ends. */ private applySidebarDragWidth(widthArg: number): void { this.sidebarDragWidth = widthArg; this.workspaceGridElement()?.style.setProperty('--sidebar-width', `${widthArg}px`); } private commitSidebarDragWidth(widthArg: number): void { this.sidebarDragWidth = undefined; // The template binding only re-applies the variable when the reactive // width changes, so the inline value is always aligned explicitly. this.workspaceGridElement()?.style.setProperty('--sidebar-width', `${widthArg}px`); this.sidebarWidth = widthArg; } private finishSidebarResize(): void { const pointerId = this.sidebarResizePointerId; this.sidebarResizePointerId = undefined; this.releaseSidebarPointerCapture(pointerId); this.sidebarResizeBaseline = undefined; if (this.sidebarDragWidth !== undefined) this.commitSidebarDragWidth(this.sidebarDragWidth); this.sidebarResizing = false; this.persistSidebarWidth(); } private cancelSidebarResize(): void { if (this.sidebarResizeBaseline !== undefined) { this.commitSidebarDragWidth(this.sidebarResizeBaseline); } else if (this.sidebarDragWidth !== undefined) { this.commitSidebarDragWidth(this.sidebarWidth); } const pointerId = this.sidebarResizePointerId; this.sidebarResizePointerId = undefined; this.releaseSidebarPointerCapture(pointerId); this.sidebarResizeBaseline = undefined; this.sidebarResizing = false; this.persistSidebarWidth(); } private readonly handleSidebarPointerDown = (eventArg: PointerEvent): void => { if (eventArg.button !== 0) return; const splitter = eventArg.currentTarget; if (!(splitter instanceof HTMLElement)) return; eventArg.preventDefault(); this.sidebarResizePointerId = eventArg.pointerId; this.sidebarResizeStartX = eventArg.clientX; this.sidebarResizeBaseline = this.sidebarWidth; this.sidebarResizing = true; splitter.setPointerCapture(eventArg.pointerId); splitter.focus(); }; private readonly handleSidebarPointerMove = (eventArg: PointerEvent): void => { if ( eventArg.pointerId !== this.sidebarResizePointerId || this.sidebarResizeBaseline === undefined ) return; this.applySidebarDragWidth(controllerClampSidebarWidth( this.sidebarResizeBaseline + eventArg.clientX - this.sidebarResizeStartX, )); }; private readonly handleSidebarPointerEnd = (eventArg: PointerEvent): void => { if (eventArg.pointerId !== this.sidebarResizePointerId) return; this.finishSidebarResize(); }; private readonly handleSidebarPointerCancel = (eventArg: PointerEvent): void => { if (eventArg.pointerId !== this.sidebarResizePointerId) return; this.cancelSidebarResize(); }; private readonly handleSidebarLostPointerCapture = (eventArg: PointerEvent): void => { if (eventArg.pointerId === this.sidebarResizePointerId) { this.cancelSidebarResize(); } }; private readonly handleSidebarKeyDown = (eventArg: KeyboardEvent): void => { if (eventArg.key === 'Escape' && this.sidebarResizeBaseline !== undefined) { eventArg.preventDefault(); this.cancelSidebarResize(); return; } let nextWidth: number | undefined; if (eventArg.key === 'ArrowLeft') nextWidth = this.sidebarWidth - sidebarKeyboardStep; if (eventArg.key === 'ArrowRight') nextWidth = this.sidebarWidth + sidebarKeyboardStep; if (eventArg.key === 'Home') nextWidth = sidebarWidthMin; if (eventArg.key === 'End') nextWidth = sidebarWidthMax; if (nextWidth === undefined) return; eventArg.preventDefault(); this.sidebarResizeBaseline ??= this.sidebarWidth; this.sidebarWidth = controllerClampSidebarWidth(nextWidth); }; private readonly handleSidebarKeyUp = (eventArg: KeyboardEvent): void => { if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(eventArg.key)) { this.persistSidebarWidth(); } }; private readonly handleSidebarBlur = (): void => { if (this.sidebarResizePointerId !== undefined) return; this.sidebarResizeBaseline = undefined; this.persistSidebarWidth(); }; private readonly toggleConversationSidebar = (): void => { if (this.conversationSidebarVisible) this.cancelSidebarResize(); this.conversationSidebarVisible = !this.conversationSidebarVisible; }; private storeResumeToken(tokenArg: string): void { try { globalThis.sessionStorage?.setItem(HarnessControllerApp.resumeTokenStorageKey, tokenArg); } catch { // Storage may be unavailable (privacy mode); resume is best-effort. } } private clearResumeToken(): void { try { globalThis.sessionStorage?.removeItem(HarnessControllerApp.resumeTokenStorageKey); } catch { // ignore } } private async tryResumeAuthentication(generationArg: number): Promise { let token: string | null = null; try { token = globalThis.sessionStorage?.getItem(HarnessControllerApp.resumeTokenStorageKey) ?? null; } catch { return; } if (!token) return; try { const response = await this.socketClient.fire( 'controller.auth.resume', { token }, ); if (!this.isCurrentConnection(generationArg)) return; this.storeResumeToken(response.resumeToken); this.authenticated = response.authenticated === true; } catch { // Invalid or expired token: fall back to the normal login flow. if (!this.isCurrentConnection(generationArg)) return; this.clearResumeToken(); } } /** Ring buffer of recent transport closes, for diagnosing forced logouts. */ private recordTransportClose(closeCodeArg: number, reasonArg: string): void { try { const storage = globalThis.localStorage; if (!storage) return; const raw = storage.getItem(HarnessControllerApp.closeLogStorageKey); const entries = raw ? JSON.parse(raw) as unknown[] : []; const log = Array.isArray(entries) ? entries : []; log.push({ at: new Date().toISOString(), code: closeCodeArg, reason: reasonArg }); storage.setItem( HarnessControllerApp.closeLogStorageKey, JSON.stringify(log.slice(-20)), ); } catch { // Diagnostics must never break the app. } } private clearStartupCheck(): void { if (this.startupCheckTimer !== undefined) clearTimeout(this.startupCheckTimer); this.startupCheckTimer = undefined; } private async reconcileBackendVersion(generationArg: number): Promise { const status = await this.socketClient.fire( 'controller.status', {}, ); if (!this.isCurrentConnection(generationArg)) { return false; } this.clearStartupCheck(); this.controllerStatus = status; this.applyAuthoritativeUpgradeStatus(status.upgrade); // The listener serves status while startup is still restoring resources. // Authentication and bundle replacement must wait for operation admission. if (status.lifecycleState !== 'ready') { this.startupCheckTimer = setTimeout(() => { this.startupCheckTimer = undefined; if (this.isCurrentConnection(generationArg)) void this.reconcileAuthState(generationArg); }, 500); return false; } const version = typeof status.packageVersion === 'string' ? status.packageVersion : ''; if (!version) { return true; } if (version !== commitinfo.version) { const target = `${commitinfo.version}->${version}`; let previousTarget: string | null = null; try { previousTarget = globalThis.sessionStorage?.getItem( HarnessControllerApp.reloadFenceStorageKey, ) ?? null; } catch { // A storage-disabled browser falls through to one best-effort reload. } if (!previousTarget) { const attemptedTarget = new URL(globalThis.location.href).searchParams.get('_hcon_bundle'); if (attemptedTarget === version) previousTarget = target; } if (controllerBundleReloadDecision(commitinfo.version, version, previousTarget) === 'stale') { this.staleBundleVersion = version; this.backendVersion = version; return false; } try { globalThis.sessionStorage?.setItem(HarnessControllerApp.reloadFenceStorageKey, target); } catch { // Reload still has a chance to refresh the bundle without the loop fence. } const reloadUrl = new URL(globalThis.location.href); reloadUrl.searchParams.set('_hcon_bundle', version); globalThis.location.replace(reloadUrl.href); return false; } this.staleBundleVersion = ''; try { globalThis.sessionStorage?.removeItem(HarnessControllerApp.reloadFenceStorageKey); } catch { // Storage cleanup is best-effort. } this.backendVersion = version; return true; } private storeUpgradeStatus(statusArg: interfaces.IControllerUpgradeStatus): void { this.upgradeStatus = { ...statusArg }; try { globalThis.sessionStorage?.setItem( HarnessControllerApp.upgradeStorageKey, JSON.stringify(statusArg), ); } catch { // Upgrade display remains available in memory when storage is unavailable. } } private applyAuthoritativeUpgradeStatus( statusArg: interfaces.IControllerUpgradeStatus | undefined, ): void { if (statusArg) { this.storeUpgradeStatus(statusArg); return; } this.upgradeStatus = undefined; try { globalThis.sessionStorage?.removeItem(HarnessControllerApp.upgradeStorageKey); } catch { // Authoritative in-memory status still wins when storage is unavailable. } } private restoreUpgradeStatus(): void { if (this.upgradeStatus) return; try { const raw = globalThis.sessionStorage?.getItem(HarnessControllerApp.upgradeStorageKey); if (!raw) return; const value = JSON.parse(raw) as Partial; if ( typeof value.fromVersion === 'string' && typeof value.toVersion === 'string' && ['preparing', 'pausing', 'installing', 'restarting', 'continuing', 'completed', 'failed'] .includes(value.phase ?? '') ) this.upgradeStatus = value as interfaces.IControllerUpgradeStatus; } catch { // Malformed or unavailable browser storage is ignored. } } private async reconcileControllerStatus(): Promise { const generation = this.connectionGeneration; try { const status = await this.socketClient.fire( 'controller.status', {}, ); if (this.isCurrentConnection(generation)) { this.controllerStatus = status; this.applyAuthoritativeUpgradeStatus(status.upgrade); await this.refreshSettingsModalContent(); } } catch { // Session projections remain usable even when one harness is unavailable. } } private async loadWorkspaceData(): Promise { this.scheduleSystemMetricsRefresh(0); await Promise.all([ this.refreshProjects(), this.refreshProviderManagement(), this.reconcileControllerStatus(), ]); void this.retryStaleDraftSessionCleanup(); void this.refreshModelCatalog(); await this.refreshSessions(); } private scheduleSystemMetricsRefresh(delayMsArg: number): void { if (this.systemMetricsTimer) clearTimeout(this.systemMetricsTimer); this.systemMetricsTimer = undefined; if (!this.authenticated || !this.socketClient.isConnected) return; const generation = this.connectionGeneration; this.systemMetricsTimer = setTimeout(() => { this.systemMetricsTimer = undefined; void this.refreshSystemMetrics(generation); }, delayMsArg); } private async refreshSystemMetrics(generationArg: number): Promise { if (!this.authenticated || !this.isCurrentConnection(generationArg)) return; try { const metrics = await this.socketClient.fire( 'controller.system.metrics', {}, { timeoutMs: 5_000, maxRetries: 0 }, ); if (this.authenticated && this.isCurrentConnection(generationArg)) { this.applySystemMetrics(metrics); } } catch { // Telemetry is supplemental; transient sampling failures must not affect the workspace. this.resetHighCpuWarning(); } finally { if (this.authenticated && this.isCurrentConnection(generationArg)) { this.scheduleSystemMetricsRefresh(systemMetricsRefreshMs); } } } private applySystemMetrics(metricsArg: interfaces.IControllerSystemMetrics): void { const cpuUsage = metricsArg.cpuUsagePercent; const sampledAt = metricsArg.sampledAt; const continuousSample = ( this.lastCpuSampleAt === undefined || ( sampledAt > this.lastCpuSampleAt && sampledAt - this.lastCpuSampleAt <= maximumContinuousCpuSampleGapMs ) ); if ( cpuUsage === null || cpuUsage <= highCpuThresholdPercent || !continuousSample ) { this.highCpuStartedAt = cpuUsage !== null && cpuUsage > highCpuThresholdPercent ? sampledAt : undefined; this.highCpuWarning = false; } else if (this.highCpuStartedAt === undefined) { this.highCpuStartedAt = sampledAt; } else if (sampledAt - this.highCpuStartedAt >= highCpuSustainMs) { this.highCpuWarning = true; } this.lastCpuSampleAt = sampledAt; this.systemMetrics = metricsArg; const newMounts = [metricsArg.mainDisk, ...metricsArg.volumes] .filter((diskArg): diskArg is interfaces.IControllerFilesystemUsage => diskArg !== null) .map((diskArg) => diskArg.mountPoint) .filter((mountPointArg) => !this.footerDiskMounts.includes(mountPointArg)) .sort((leftArg, rightArg) => leftArg.localeCompare(rightArg)); if (newMounts.length > 0) { this.footerDiskMounts = [...this.footerDiskMounts, ...newMounts]; this.requestUpdate(); } this.updateSystemMetricsDom(); } private resetHighCpuWarning(): void { this.highCpuStartedAt = undefined; this.lastCpuSampleAt = undefined; this.highCpuWarning = false; this.updateSystemMetricsDom(); } private updateSystemMetricsDom(): void { const metrics = this.systemMetrics; const ageSeconds = metrics ? Math.max(0, Math.floor((Date.now() - metrics.sampledAt) / 1_000)) : null; const visibleMetrics = metrics && Date.now() - metrics.sampledAt <= 6_000 ? metrics : undefined; const memoryPercent = visibleMetrics?.memoryUsedBytes !== null && visibleMetrics?.memoryUsedBytes !== undefined && visibleMetrics.memoryTotalBytes !== null && visibleMetrics.memoryTotalBytes !== undefined && visibleMetrics.memoryTotalBytes > 0 ? (visibleMetrics.memoryUsedBytes / visibleMetrics.memoryTotalBytes) * 100 : null; const trigger = this.shadowRoot?.querySelector('.statsTrigger'); const sampleCue = trigger?.querySelector('.statsSampleCue'); const cpu = this.shadowRoot?.querySelector('.cpuMetric'); const memory = this.shadowRoot?.querySelector('.memoryMetric'); const network = this.shadowRoot?.querySelector('.networkMetric'); const cpuValue = cpu?.querySelector('.metricValue'); const memoryValue = memory?.querySelector('.metricValue'); const networkReceive = network?.querySelector('.networkReceive .networkRate'); const networkTransmit = network?.querySelector('.networkTransmit .networkRate'); if (trigger) { trigger.classList.toggle('highCpuWarning', this.highCpuWarning); trigger.title = metrics && ageSeconds !== null ? `Host statistics · live sample ${formatMetricTime(metrics.sampledAt)} · ${ageSeconds}s old${visibleMetrics ? '' : ' · stale'}` : 'Host statistics · live sample unavailable'; trigger.setAttribute('aria-label', trigger.title); } if (sampleCue) { sampleCue.textContent = visibleMetrics ? '▴' : metrics ? '!' : '?'; } if (cpu) { cpu.title = this.highCpuWarning ? 'Whole-system CPU usage has remained above 50% for at least 10 seconds' : 'Whole-system CPU usage'; } if (cpuValue) cpuValue.textContent = formatPercentage(visibleMetrics?.cpuUsagePercent); if (memory) { memory.classList.toggle('strained', memoryPercent !== null && memoryPercent >= resourceStrainThresholdPercent); memory.title = visibleMetrics?.memoryUsedBytes !== null && visibleMetrics?.memoryUsedBytes !== undefined && visibleMetrics.memoryTotalBytes !== null && visibleMetrics.memoryTotalBytes !== undefined ? `${formatBytes(visibleMetrics.memoryUsedBytes)} used of ${formatBytes(visibleMetrics.memoryTotalBytes)}${memoryPercent !== null && memoryPercent >= resourceStrainThresholdPercent ? ' · RAM at or above 90% active use' : ''}` : 'Memory usage unavailable or stale'; } if (memoryValue) { memoryValue.textContent = formatPercentage(memoryPercent); memoryValue.classList.toggle('strainedValue', memoryPercent !== null && memoryPercent >= resourceStrainThresholdPercent); } if (networkReceive) { networkReceive.textContent = formatByteRate(visibleMetrics?.networkReceiveBytesPerSecond); } if (networkTransmit) { networkTransmit.textContent = formatByteRate(visibleMetrics?.networkTransmitBytesPerSecond); } const disks = [visibleMetrics?.mainDisk, ...(visibleMetrics?.volumes ?? [])].filter( (diskArg): diskArg is interfaces.IControllerFilesystemUsage => diskArg !== null && diskArg !== undefined, ); for (const diskElement of this.shadowRoot?.querySelectorAll('.diskMetric') ?? []) { const disk = disks.find((candidateArg) => candidateArg.mountPoint === diskElement.dataset.mountPoint); const capacityStrained = disk?.usedPercent !== null && disk?.usedPercent !== undefined && disk.usedPercent >= resourceStrainThresholdPercent; const busyStrained = disk?.io !== null && disk?.io !== undefined && disk.io.busyPercent >= resourceStrainThresholdPercent; diskElement.classList.toggle('strained', capacityStrained || busyStrained); const reason = [ capacityStrained ? `capacity ${formatPercentage(disk?.usedPercent)} used` : null, busyStrained ? `I/O ${formatPercentage(disk?.io?.busyPercent)} busy` : null, ].filter((valueArg): valueArg is string => valueArg !== null).join(' and '); diskElement.title = disk ? `${disk.mountPoint}: ${formatPercentage(disk.usedPercent)} used${disk.io ? ` · ${formatPercentage(disk.io.busyPercent)} I/O busy` : ''}${reason ? ` · strained: ${reason}` : ''}` : `${diskElement.dataset.mountPoint ?? 'Disk'}: unavailable or stale`; const capacityTrigger = diskElement.querySelector('.diskCapacityTrigger'); if (capacityTrigger) { capacityTrigger.title = disk ? `${disk.mountPoint}: ${formatPercentage(disk.usedPercent)} used` : `${diskElement.dataset.mountPoint ?? 'Disk'}: capacity unavailable or stale`; } const valueElement = diskElement.querySelector('.metricValue'); if (valueElement) { valueElement.textContent = formatPercentage(disk?.usedPercent); valueElement.classList.toggle('strainedValue', capacityStrained); } const io = diskElement.querySelector('.diskIo'); if (io instanceof HTMLButtonElement) { io.title = disk?.io ? `${disk.mountPoint}: ${formatPercentage(disk.io.busyPercent)} I/O busy` : `${diskElement.dataset.mountPoint ?? 'Disk'}: I/O unavailable or stale`; } const roundedBusy = disk?.io ? Math.round(disk.io.busyPercent) : null; io?.classList.toggle('ioBusyCritical', roundedBusy !== null && roundedBusy >= diskBusyCriticalPercent); io?.classList.toggle('ioBusyWarning', roundedBusy !== null && roundedBusy >= diskBusyWarningPercent && roundedBusy < diskBusyCriticalPercent); const ioValue = io?.querySelector('.ioValue'); if (ioValue) { ioValue.textContent = formatPercentage(roundedBusy); ioValue.classList.toggle('strainedValue', busyStrained); } } if (this.statsPanel) { this.statsPanel.metrics = metrics; this.statsPanel.highCpuWarning = this.highCpuWarning; } const metricPopover = this.shadowRoot?.querySelector( 'agl-system-metric-popover', ); if (metricPopover) metricPopover.metrics = metrics; this.updateFooterScrollState(); } private updateFooterScrollState(): void { const metricsGroup = this.shadowRoot?.querySelector('.statusMetrics'); const statsTrigger = this.shadowRoot?.querySelector('.statsTrigger'); const accountLimits = this.shadowRoot?.querySelector('agl-account-limits'); const scroller = this.shadowRoot?.querySelector('.metricsScroller'); const control = this.shadowRoot?.querySelector('.metricsRailControl'); const endSpacer = scroller?.querySelector('.metricsEndSpacer'); if (!metricsGroup || !statsTrigger || !scroller || !control || !endSpacer) return; const children = [...scroller.querySelectorAll('.statusMetric, .legalDisclosure')]; const widths = children.map((childArg) => childArg.getBoundingClientRect().width); const contentWidth = widths.reduce((sumArg, widthArg) => sumArg + widthArg, 0); const availableWithoutControl = Math.max( 0, metricsGroup.clientWidth - statsTrigger.getBoundingClientRect().width - (accountLimits?.getBoundingClientRect().width ?? 0), ); const needsControl = contentWidth - availableWithoutControl > 1; control.classList.toggle('inactive', !needsControl); control.disabled = !needsControl; const naturalRemaining = contentWidth - scroller.clientWidth; let endPadding = 0; if (needsControl && naturalRemaining > 1) { let offset = 0; for (const width of widths) { if (offset >= naturalRemaining - 1) { endPadding = Math.ceil(offset - naturalRemaining); break; } offset += width; } } endSpacer.style.flexBasis = `${Math.max(0, endPadding)}px`; const remaining = Math.max(0, scroller.scrollWidth - scroller.clientWidth); const moreLeft = scroller.scrollLeft > 1; const moreRight = remaining - scroller.scrollLeft > 1; scroller.classList.toggle('hasMoreLeft', moreLeft); scroller.classList.toggle('hasMoreRight', moreRight); const scrollerRect = scroller.getBoundingClientRect(); const wholeValueLeft = scrollerRect.left; const wholeValueRight = scrollerRect.right; for (const value of scroller.querySelectorAll('.metricValue, .ioValue, .networkRate')) { const rect = value.getBoundingClientRect(); const edgeClipped = rect.left < wholeValueLeft - 1 || rect.right > wholeValueRight + 1; value.classList.toggle('edgeClipped', edgeClipped); } for (const pair of scroller.querySelectorAll('.diskIo, .networkReceive, .networkTransmit')) { const rect = pair.getBoundingClientRect(); pair.classList.toggle( 'edgeClipped', rect.left < wholeValueLeft - 1 || rect.right > wholeValueRight + 1, ); } for (const label of scroller.querySelectorAll('.metricLabel')) { const rect = label.getBoundingClientRect(); label.classList.toggle( 'edgeClipped', rect.left < wholeValueLeft - 1 || rect.right > wholeValueRight + 1, ); } const offscreen = [...scroller.querySelectorAll('.statusMetric.strained')] .filter((metricArg) => { const rect = metricArg.getBoundingClientRect(); return rect.left < scrollerRect.left - 1 || rect.right > scrollerRect.right + 1 || Boolean(metricArg.querySelector('.metricValue.edgeClipped, .ioValue.edgeClipped, .networkRate.edgeClipped')); }); control.classList.toggle('hasStrain', offscreen.length > 0); const direction = moreLeft && moreRight ? '↔' : moreRight ? '›' : '‹'; const count = offscreen.length > 0 ? `▲${new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 0 }).format(offscreen.length)}` : ''; control.textContent = `${count}${direction}`; const strainDescription = `${offscreen.length} strained cell${offscreen.length === 1 ? '' : 's'} with clipped or offscreen values`; const actionDescription = moreRight ? 'scroll metrics right' : 'scroll metrics left'; control.title = `${strainDescription} · ${actionDescription}`; control.setAttribute('aria-label', `${strainDescription}; ${actionDescription}`); } private readonly handleFooterMetricsScroll = (): void => this.updateFooterScrollState(); private readonly scrollFooterMetrics = (): void => { const scroller = this.shadowRoot?.querySelector('.metricsScroller'); if (!scroller) return; const remaining = Math.max(0, scroller.scrollWidth - scroller.clientWidth); const direction = remaining - scroller.scrollLeft > 1 ? 1 : -1; scroller.scrollBy({ left: direction * Math.max(100, scroller.clientWidth * .7), behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth', }); }; private readonly toggleStatsPanel = (eventArg: Event): void => { const button = eventArg.currentTarget; if (!(button instanceof HTMLButtonElement)) return; const footerOverlayGeneration = ++this.footerOverlayGeneration; if (this.statsPanel) void this.closeStatsPanel(true, button); else { this.accountLimitsOpen = false; this.closeMetricPopover(); void this.openStatsPanel(button, footerOverlayGeneration); } }; private readonly handleAccountLimitsOpenRequest = ( _eventArg: CustomEvent, ): void => { const generation = ++this.footerOverlayGeneration; this.closeMetricPopover(); void (async (): Promise => { if (this.statsPanel) await this.closeStatsPanel(false); if (generation !== this.footerOverlayGeneration || !this.authenticated) return; this.accountLimitsOpen = true; })(); }; private readonly handleAccountLimitsClose = ( _eventArg: CustomEvent, ): void => { this.footerOverlayGeneration += 1; this.accountLimitsOpen = false; }; private accountLimitsContext(): interfaces.TControllerAuthSwitchLimitsContext | undefined { const sessionId = this.selectedSessionId; const detail = this.sessionDetail; return isSessionRuntimeId(sessionId) && controllerRuntimeIdsEqual(detail?.session.id, sessionId) ? detail?.authSwitchLimitsContext : undefined; } private accountLimitsContextKey(): string { const sessionId = this.selectedSessionId; return isSessionRuntimeId(sessionId) ? this.sessionOperationKey(this.selectedProjectId, sessionId) : `standalone:${this.selectedProjectId}:${this.selectedResourceId || 'none'}`; } private handleAuthSwitchChanged(): void { this.scheduleRefresh(); void this.refreshModelCatalog(); this.shadowRoot?.querySelector('agl-account-limits')?.invalidate(); } private systemMetricSelectionsEqual( leftArg: TSystemMetricSelection | undefined, rightArg: TSystemMetricSelection, ): boolean { return leftArg?.kind === rightArg.kind && (!('mountPoint' in rightArg) || (leftArg !== undefined && 'mountPoint' in leftArg && leftArg.mountPoint === rightArg.mountPoint)); } private async toggleMetricPopover( eventArg: Event, selectionArg: TSystemMetricSelection, ): Promise { const anchor = eventArg.currentTarget; if (!(anchor instanceof HTMLButtonElement)) return; if ( this.metricPopoverAnchor === anchor && this.systemMetricSelectionsEqual(this.metricPopoverSelection, selectionArg) ) { const popover = this.shadowRoot?.querySelector( 'dees-popover.metricPopover', ); if (popover) await popover.close(); else this.closeMetricPopover(); return; } const footerOverlayGeneration = ++this.footerOverlayGeneration; this.accountLimitsOpen = false; if (this.statsPanel) await this.closeStatsPanel(false); if (footerOverlayGeneration !== this.footerOverlayGeneration) return; if (this.metricHistoryTimer) clearTimeout(this.metricHistoryTimer); this.metricHistoryTimer = undefined; const generation = ++this.metricPopoverGeneration; this.metricPopoverAnchor = anchor; this.metricPopoverSelection = selectionArg; await this.updateComplete; if ( generation !== this.metricPopoverGeneration || footerOverlayGeneration !== this.footerOverlayGeneration || !anchor.isConnected || !this.authenticated ) return; this.shadowRoot?.querySelector('agl-system-metric-popover') ?.setLoading(); void this.refreshMetricPopoverHistory(generation); } private readonly handleMetricPopoverClose = (): void => { this.closeMetricPopover(); }; private closeMetricPopover(): void { this.metricPopoverGeneration += 1; if (this.metricHistoryTimer) clearTimeout(this.metricHistoryTimer); this.metricHistoryTimer = undefined; this.metricPopoverSelection = undefined; this.metricPopoverAnchor = null; } private async refreshMetricPopoverHistory(generationArg: number): Promise { if ( generationArg !== this.metricPopoverGeneration || !this.metricPopoverSelection || !this.authenticated ) return; const content = this.shadowRoot?.querySelector( 'agl-system-metric-popover', ); if (!content) return; try { const response = await this.socketClient.fire( 'controller.system.metrics.history', {}, { timeoutMs: 5_000, maxRetries: 0 }, ); if (generationArg === this.metricPopoverGeneration) content.setHistory(response.points); } catch { if (generationArg === this.metricPopoverGeneration) content.setHistoryUnavailable(); } finally { if (generationArg === this.metricPopoverGeneration && this.metricPopoverSelection) { this.metricHistoryTimer = setTimeout(() => { this.metricHistoryTimer = undefined; void this.refreshMetricPopoverHistory(generationArg); }, 10_000); } } } private statsPanelTriggers(): HTMLButtonElement[] { return [...(this.shadowRoot?.querySelectorAll('.statsPanelTrigger') ?? [])]; } private positionStatsPanel(): void { const button = this.statsPanelActivator?.isConnected ? this.statsPanelActivator : this.shadowRoot?.querySelector('.statsTrigger'); const panel = this.statsPanel; if (!button || !panel) return; const anchor = button.getBoundingClientRect(); panel.style.maxHeight = `${Math.max(120, anchor.top - 16)}px`; const size = panel.getBoundingClientRect(); const left = Math.max(8, Math.min(anchor.right - size.width, window.innerWidth - size.width - 8)); const top = Math.max(8, Math.min(anchor.top - size.height - 8, window.innerHeight - size.height - 8)); panel.style.left = `${left}px`; panel.style.top = `${top}px`; } private async openStatsPanel( buttonArg: HTMLButtonElement, footerOverlayGenerationArg = this.footerOverlayGeneration, ): Promise { const generation = ++this.statsPanelGeneration; this.statsPanelActivator = buttonArg; const layer = await plugins.deesCatalog.DeesWindowLayer.createAndShow({ ownerElement: buttonArg, dimmed: false, blocking: false, dismissOnOutsidePress: true, }); if (generation !== this.statsPanelGeneration || footerOverlayGenerationArg !== this.footerOverlayGeneration || !this.isConnected || !this.authenticated || !buttonArg.isConnected) { if (generation === this.statsPanelGeneration) this.statsPanelActivator = undefined; await layer.destroy(); return; } const panel = new AglSystemStatsPanel(); panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'Host statistics'); panel.metrics = this.systemMetrics; panel.highCpuWarning = this.highCpuWarning; layer.protectedElements = [...this.statsPanelTriggers(), panel]; layer.append(panel); this.statsPanelLayer = layer; this.statsPanel = panel; void this.refreshStatsHistory(generation); for (const trigger of this.statsPanelTriggers()) trigger.setAttribute('aria-expanded', 'true'); const listeners = new AbortController(); this.statsPanelListeners = listeners; layer.addEventListener('clicked', () => { void this.closeStatsPanel(false); }, { signal: listeners.signal }); layer.addEventListener('click', (eventArg) => { if (eventArg.target === layer) void this.closeStatsPanel(false); }, { signal: listeners.signal }); panel.addEventListener('stats-close', () => { void this.closeStatsPanel(true); }, { signal: listeners.signal }); document.addEventListener('keydown', (eventArg) => { if (eventArg.key !== 'Escape') return; eventArg.preventDefault(); eventArg.stopPropagation(); void this.closeStatsPanel(true); }, { capture: true, signal: listeners.signal }); window.addEventListener('resize', () => this.positionStatsPanel(), { signal: listeners.signal }); window.addEventListener('scroll', () => this.positionStatsPanel(), { capture: true, signal: listeners.signal }); await panel.updateComplete; if ( generation !== this.statsPanelGeneration || footerOverlayGenerationArg !== this.footerOverlayGeneration || !panel.isConnected ) { if (generation === this.statsPanelGeneration) await this.closeStatsPanel(false); return; } this.positionStatsPanel(); panel.focusClose(); } private async closeStatsPanel( restoreFocusArg: boolean, focusTargetArg = this.statsPanelActivator, ): Promise { this.statsPanelGeneration += 1; if (this.statsHistoryTimer) clearTimeout(this.statsHistoryTimer); this.statsHistoryTimer = undefined; this.statsPanelListeners?.abort(); this.statsPanelListeners = undefined; const panel = this.statsPanel; const layer = this.statsPanelLayer; this.statsPanel = undefined; this.statsPanelLayer = undefined; this.statsPanelActivator = undefined; panel?.remove(); for (const trigger of this.statsPanelTriggers()) trigger.setAttribute('aria-expanded', 'false'); if (restoreFocusArg && focusTargetArg?.isConnected) focusTargetArg.focus({ preventScroll: true }); if (layer) await layer.destroy(); } private async refreshStatsHistory(generationArg: number): Promise { if (generationArg !== this.statsPanelGeneration || !this.statsPanel) return; try { const response = await this.socketClient.fire( 'controller.system.metrics.history', {}, { timeoutMs: 5_000, maxRetries: 0 }, ); if (generationArg === this.statsPanelGeneration) this.statsPanel?.setHistory(response.points); } catch { if (generationArg === this.statsPanelGeneration) this.statsPanel?.setHistoryUnavailable(); } finally { if (generationArg === this.statsPanelGeneration && this.statsPanel) { this.statsHistoryTimer = setTimeout(() => { this.statsHistoryTimer = undefined; void this.refreshStatsHistory(generationArg); }, 10_000); } } } private readonly showLegalDisclosure = (): void => { if (this.legalDisclosureHideTimer) { clearTimeout(this.legalDisclosureHideTimer); this.legalDisclosureHideTimer = undefined; } this.legalDisclosureVisible = true; }; private readonly openLegalDisclosure = async (): Promise => { this.showLegalDisclosure(); await this.updateComplete; this.shadowRoot?.querySelector('.legalDetails a')?.focus(); }; private readonly scheduleLegalDisclosureHide = (): void => { if (this.legalDisclosureHideTimer) clearTimeout(this.legalDisclosureHideTimer); this.legalDisclosureHideTimer = setTimeout(() => { this.legalDisclosureHideTimer = undefined; this.legalDisclosureVisible = false; }, legalDisclosureHideDelayMs); }; private hideLegalDisclosure(): void { if (this.legalDisclosureHideTimer) { clearTimeout(this.legalDisclosureHideTimer); this.legalDisclosureHideTimer = undefined; } this.legalDisclosureVisible = false; } private async refreshProjects(): Promise { if (!this.authenticated || !this.socketClient.isConnected) { return; } const requestId = ++this.projectsRequestId; const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.project.list', {}, ); if (requestId !== this.projectsRequestId || !this.isCurrentConnection(generation)) { return; } this.projects = Array.isArray(response.projects) ? response.projects : []; this.blockedProjectRemovals = Array.isArray(response.blockedRemovals) ? response.blockedRemovals : []; this.pruneProjectScopedState(new Set(this.projects.map((project) => project.id))); const selectionStillExists = this.projects.some( (projectArg) => projectArg.id === this.selectedProjectId, ); if (!selectionStillExists) { this.cancelComposerFocus(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.draftSessionActive = false; this.selectedProjectId = this.projects[0]?.id ?? ''; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; this.sessionLayoutError = ''; } } catch (error) { if (requestId === this.projectsRequestId && this.isCurrentConnection(generation)) { this.reportWorkspaceError('Load projects', error); } } } private pruneProjectScopedState(activeProjectIdsArg: ReadonlySet): void { for (const projectId of this.authoritativeStatusesByProject.keys()) { if (!activeProjectIdsArg.has(projectId)) this.authoritativeStatusesByProject.delete(projectId); } for (const projectId of this.finishedSessionIdsByProject.keys()) { if (!activeProjectIdsArg.has(projectId)) this.finishedSessionIdsByProject.delete(projectId); } for (const projectId of this.errorSessionIdsByProject.keys()) { if (!activeProjectIdsArg.has(projectId)) this.errorSessionIdsByProject.delete(projectId); } for (const projectId of this.optimisticWorkingIdsByProject.keys()) { if (!activeProjectIdsArg.has(projectId)) this.optimisticWorkingIdsByProject.delete(projectId); } for (const projectId of this.sessionEventRevisionByProject.keys()) { if (!activeProjectIdsArg.has(projectId)) this.sessionEventRevisionByProject.delete(projectId); } // The layout is controller-wide; the server prunes a removed project's rows and pushes // the change, so the client keeps no per-project layout state to prune here. } private async refreshModelCatalog(): Promise { if (!this.authenticated || !this.socketClient.isConnected) { return; } const requestId = ++this.modelCatalogRequestId; const generation = this.connectionGeneration; const projectId = this.selectedProjectId; const sessionId = this.selectedSessionId; const contextKey = this.currentCodexComposerKey(); if (contextKey !== this.codexComposerKey) this.codexComposer = undefined; try { const [modelsResponse, settingsResponse] = await Promise.all([ this.socketClient.fire('controller.model.list', { ...(projectId ? { projectId, ...(sessionId ? { sessionId } : {}) } : {}), }), this.socketClient.fire('controller.settings.get', {}), ]); if (requestId !== this.modelCatalogRequestId || !this.isCurrentConnection(generation) || contextKey !== this.currentCodexComposerKey()) { return; } this.codexComposer = modelsResponse.codexContext; this.codexComposerKey = contextKey; this.modelChoicesByString.clear(); this.modelVariantsByString.clear(); this.flexModelAvailabilityByString.clear(); this.modelOptionsCache.clear(); this.effortOptionsCache.clear(); const optionStrings: string[] = []; for (const model of modelsResponse.models ?? []) { const optionString = modelOptionLabel(model); if (this.modelChoicesByString.has(optionString)) continue; const choice: interfaces.TControllerModelChoice = model.harnessId === 'codex' ? { harnessId: 'codex', providerID: 'codex', modelID: model.modelID } : model.harnessId === 'flex' ? { harnessId: 'flex', providerID: model.providerID, modelID: model.modelID, } : { harnessId: 'opencode', providerID: model.providerID, modelID: model.modelID, }; this.modelChoicesByString.set(optionString, choice); this.modelVariantsByString.set( optionString, Array.isArray(model.variants) ? model.variants.filter(isNonEmptyString) : [], ); if (model.harnessId === 'flex') { this.flexModelAvailabilityByString.set( optionString, Array.isArray(model.availability) ? model.availability.flatMap((availability) => ( isNonEmptyString(availability.providerConnectionId) ? [{ providerConnectionId: availability.providerConnectionId, variants: Array.isArray(availability.variants) ? availability.variants.filter(isNonEmptyString) : [], isDefault: availability.isDefault === true, }] : [] )) : [], ); } optionStrings.push(optionString); } this.modelOptionStrings = optionStrings; this.projectsRoot = typeof settingsResponse.projectsRoot === 'string' ? settingsResponse.projectsRoot : ''; const defaultModels = settingsResponse.settings?.defaultModels ?? []; this.defaultModelStrings = { opencode: '', flex: '', codex: '' }; this.defaultEffortStrings = { opencode: '', flex: '', codex: '' }; this.defaultModelChoices = {}; for (const defaultModel of defaultModels) { this.defaultModelChoices[defaultModel.harnessId] = { ...defaultModel }; const optionString = modelOptionLabel(defaultModel); if (!this.modelChoicesByString.has(optionString)) continue; this.defaultModelStrings[defaultModel.harnessId] = optionString; this.defaultEffortStrings[defaultModel.harnessId] = isNonEmptyString(defaultModel.variant) ? defaultModel.variant : ''; } this.autoAcceptPermissions = settingsResponse.settings?.autoAcceptPermissions === true; this.standardProjectDirectories = Array.isArray( settingsResponse.settings?.standardProjectDirectories, ) ? settingsResponse.settings.standardProjectDirectories.filter(isNonEmptyString) : []; this.lastSessionHarnessId = isSessionHarnessId(settingsResponse.settings?.lastSessionHarnessId) ? settingsResponse.settings.lastSessionHarnessId : undefined; this.browserVideoBackend = settingsResponse.settings?.browserVideoBackend ?? 'chromium'; this.activeBrowserVideoBackend = settingsResponse.settings?.activeBrowserVideoBackend ?? 'chromium'; } catch { if (requestId === this.modelCatalogRequestId && this.isCurrentConnection(generation)) this.codexComposer = undefined; } } private isCurrentConnection(generationArg: number): boolean { return ( this.componentConnected && this.connectionGeneration === generationArg && this.socketClient.isConnected ); } private readonly handleSetupSubmit = async ( eventArg: CustomEvent, ): Promise => { const setupCodeValue = eventArg.detail?.data?.setupCode; if (!isNonEmptyString(setupCodeValue) || this.ceremonyBusy) { if (!isNonEmptyString(setupCodeValue)) { this.setupError = 'Enter the setup code printed by the CLI.'; } return; } const form = eventArg.currentTarget; if (!(form instanceof plugins.deesCatalog.DeesForm)) { this.setupError = 'The setup form could not be read.'; return; } const setupCode = setupCodeValue.trim(); const generation = this.connectionGeneration; const ceremonyId = Symbol('setupCeremony'); this.activeCeremonyId = ceremonyId; this.ceremonyBusy = true; this.setupError = ''; form.setStatus('pending', 'Creating passkey…'); try { const beginResponse = await this.socketClient.fire( 'controller.auth.setup.begin', { setupCode }, ); this.assertActiveCeremony(ceremonyId, generation); const registrationResponse = await plugins.simpleWebAuthnBrowser.startRegistration({ optionsJSON: beginResponse.options, }); this.assertActiveCeremony(ceremonyId, generation); const setupResult = await this.socketClient.fire( 'controller.auth.setup.finish', { ceremonyId: beginResponse.ceremonyId, setupCode, response: registrationResponse, }, ); this.assertActiveCeremony(ceremonyId, generation); this.storeResumeToken(setupResult.resumeToken); this.authState = 'ready'; this.authenticated = true; await this.loadWorkspaceData(); } catch (error) { if (this.activeCeremonyId === ceremonyId && this.isCurrentConnection(generation)) { this.setupError = errorMessage(error); form.setStatus('normal', 'Create passkey'); } } finally { if (this.activeCeremonyId === ceremonyId) { this.activeCeremonyId = undefined; this.ceremonyBusy = false; } } }; private readonly handleTempPasswordSubmit = async ( eventArg: CustomEvent, ): Promise => { const passwordValue = eventArg.detail?.data?.tempPassword; const form = eventArg.currentTarget; if (!(form instanceof plugins.deesCatalog.DeesForm)) { return; } if (!isNonEmptyString(passwordValue)) { this.tempPasswordError = 'Enter the temporary password printed by the CLI.'; return; } const generation = this.connectionGeneration; this.tempPasswordError = ''; form.setStatus('pending', 'Signing in…'); try { const loginResult = await this.socketClient.fire( 'controller.auth.temppassword.login', { password: passwordValue.trim() }, ); if (!this.isCurrentConnection(generation)) { return; } this.storeResumeToken(loginResult.resumeToken); this.authenticated = true; this.showTempPasswordForm = false; await this.loadWorkspaceData(); } catch (error) { if (this.isCurrentConnection(generation)) { this.tempPasswordError = errorMessage(error); form.setStatus('normal', 'Sign in'); } } }; private readonly passkeyLoginHandler: plugins.deesCatalog.TDeesLoginPasskeyHandler = async ( contextArg, ): Promise => { if (contextArg.intent !== 'authenticate' || contextArg.mediation !== 'optional') { throw new Error('Only explicit passkey authentication is supported.'); } if (this.ceremonyBusy) { throw new Error('A passkey ceremony is already running.'); } const generation = this.connectionGeneration; const ceremonyId = Symbol('authenticationCeremony'); this.activeCeremonyId = ceremonyId; this.ceremonyBusy = true; try { this.assertActiveCeremony(ceremonyId, generation, contextArg.signal); const beginResponse = await this.socketClient.fire( 'controller.auth.authentication.begin', {}, ); this.assertActiveCeremony(ceremonyId, generation, contextArg.signal); const authenticationResponse = await plugins.simpleWebAuthnBrowser.startAuthentication({ optionsJSON: beginResponse.options, }); this.assertActiveCeremony(ceremonyId, generation, contextArg.signal); const authenticationResult = await this.socketClient.fire( 'controller.auth.authentication.finish', { ceremonyId: beginResponse.ceremonyId, response: authenticationResponse, }, ); this.assertActiveCeremony(ceremonyId, generation, contextArg.signal); this.storeResumeToken(authenticationResult.resumeToken); this.authenticated = true; await this.loadWorkspaceData(); } finally { if (this.activeCeremonyId === ceremonyId) { this.activeCeremonyId = undefined; this.ceremonyBusy = false; } } }; private assertActiveCeremony( ceremonyIdArg: symbol, generationArg: number, signalArg?: AbortSignal, ): void { if ( signalArg?.aborted || this.activeCeremonyId !== ceremonyIdArg || !this.isCurrentConnection(generationArg) ) { throw new DOMException('The passkey ceremony was cancelled.', 'AbortError'); } } private refreshSessions(includeRelatedArg = true): Promise { if (!this.authenticated || !this.socketClient.isConnected) { return Promise.resolve(); } this.refreshPending = true; this.refreshRelatedPending ||= includeRelatedArg; if (!this.refreshInFlight) { this.refreshInFlight = this.runRefreshQueue(); } return this.refreshInFlight; } private projectSessionIds( mapArg: Map>, projectIdArg: string, ): Set { let sessionIds = mapArg.get(projectIdArg); if (!sessionIds) { sessionIds = new Set(); mapArg.set(projectIdArg, sessionIds); } return sessionIds; } private projectStatuses(projectIdArg: string): Map { let statuses = this.authoritativeStatusesByProject.get(projectIdArg); if (!statuses) { statuses = new Map(); this.authoritativeStatusesByProject.set(projectIdArg, statuses); } return statuses; } /** * Returns the previously rendered value when the freshly computed one is * structurally identical. Child components such as the transcript and the * session list invalidate on property identity, so handing them a new but * equal array or object on every app render would rebuild their timelines * and re-run their follow-scroll logic without any visible change. */ private stableRenderValue(keyArg: string, valueArg: T): T { const previous = this.stableRenderValues.get(keyArg); if (previous !== undefined && controllerJsonValuesEqual(previous, valueArg)) return previous as T; this.stableRenderValues.set(keyArg, valueArg); return valueArg; } /** * Text-only draft changes skip application renders (see shouldUpdate), which * leaves Lit's committed composerValue behind the composer's live text. After * every render the mounted chat's composer value is aligned with the selected * session's draft, so a session switch can never carry typed text across * sessions through a stale binding. */ private syncMountedComposerValue(chatArg: { composerValue: string }): void { const sessionId = this.selectedSessionId; if (!isSessionRuntimeId(sessionId)) return; const draftState = this.sessionDraftState?.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(this.sessionDraftState.sessionId, sessionId) ? this.sessionDraftState : undefined; const expected = draftState?.text ?? ''; if (chatArg.composerValue !== expected) chatArg.composerValue = expected; } public shouldUpdate(changedProperties: Map): boolean { // Composer keystrokes only change the draft text, which the composer // already displays; re-rendering the whole application per keystroke // would rebuild the transcript and re-pin its scroll position. if (changedProperties.size === 1 && changedProperties.has('sessionDraftState')) { const previous = changedProperties.get('sessionDraftState') as ISessionDraftSyncState | undefined; const next = this.sessionDraftState; if (previous && next && draftStatesEqualExceptText(previous, next)) return false; } return super.shouldUpdate(changedProperties); } private touchSessionCardState(projectIdArg: string): void { if (projectIdArg === this.selectedProjectId) this.sessionCardStateRevision += 1; } private bumpSessionEventRevision(projectIdArg: string): void { this.sessionEventRevisionByProject.set( projectIdArg, (this.sessionEventRevisionByProject.get(projectIdArg) ?? 0) + 1, ); } private liveToolSessionKey(projectIdArg: string, sessionIdArg: IControllerBrowserRuntimeId): string { return JSON.stringify([projectIdArg, controllerRuntimeIdToUiKey(sessionIdArg)]); } private liveToolOverlayKey( projectIdArg: string, executionArg: interfaces.IControllerToolExecution, ): string { return JSON.stringify([ projectIdArg, controllerRuntimeIdToUiKey(executionArg.sessionId), controllerRuntimeIdToUiKey(executionArg.partId), ]); } private deleteLiveToolOverlay(keyArg: string): void { const existing = this.liveToolOverlays.get(keyArg); if (!existing) return; this.liveToolOverlayBytes = Math.max(0, this.liveToolOverlayBytes - existing.bytes); this.liveToolOverlays.delete(keyArg); } private clearLiveToolState(harnessIdArg?: TBrowserSessionHarnessId): void { if (harnessIdArg === undefined) { this.liveToolOverlays.clear(); this.liveToolHydrationFloors.clear(); this.liveToolOverlayBytes = 0; return; } for (const [key, overlay] of this.liveToolOverlays) { if (overlay.execution.sessionId.harnessId === harnessIdArg) { this.deleteLiveToolOverlay(key); } } for (const [key, floor] of this.liveToolHydrationFloors) { if (floor.sessionId.harnessId === harnessIdArg) this.liveToolHydrationFloors.delete(key); } } private clearLiveMessageState(harnessIdArg?: TBrowserSessionHarnessId): void { if (harnessIdArg === undefined) { this.liveReasoningUpdates.clear(); this.liveTextUpdates.clear(); this.liveReasoningBaselines.clear(); this.liveTextBaselines.clear(); this.liveReasoningTextUtf8Bytes.clear(); this.liveTextTextUtf8Bytes.clear(); this.liveReasoningStoredBytes.clear(); this.liveTextStoredBytes.clear(); this.liveMessageHydrationFloors.clear(); this.liveMessageDeltaBlocks.clear(); this.liveMessageOverlayBytes = 0; this.canonicalChatProjectionDirty = true; return; } for (const [key, floor] of this.liveMessageHydrationFloors) { if (floor.sessionId.harnessId === harnessIdArg) this.liveMessageHydrationFloors.delete(key); } for (const [key, update] of this.liveReasoningUpdates) { if (update.sessionId.harnessId === harnessIdArg) this.deleteLiveReasoningUpdate(key); } for (const [key, update] of this.liveTextUpdates) { if (update.sessionId.harnessId === harnessIdArg) this.deleteLiveTextUpdate(key); } for (const [key, baseline] of this.liveReasoningBaselines) { if (baseline.update.sessionId.harnessId === harnessIdArg) { this.deleteLiveReasoningBaseline(key); } } for (const [key, baseline] of this.liveTextBaselines) { if (baseline.update.sessionId.harnessId === harnessIdArg) this.deleteLiveTextBaseline(key); } for (const [key, block] of this.liveMessageDeltaBlocks) { if (block.sessionId.harnessId === harnessIdArg) this.liveMessageDeltaBlocks.delete(key); } this.canonicalChatProjectionDirty = true; } private clearLiveToolSession( projectIdArg: string, sessionIdArg: IControllerBrowserRuntimeId, ): void { for (const [key, overlay] of this.liveToolOverlays) { if ( overlay.projectId === projectIdArg && controllerRuntimeIdsEqual(overlay.execution.sessionId, sessionIdArg) ) this.deleteLiveToolOverlay(key); } for (const [key, update] of this.liveReasoningUpdates) { if ( key.startsWith(`${projectIdArg}\0`) && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) ) this.deleteLiveReasoningUpdate(key); } for (const [key, update] of this.liveTextUpdates) { if ( key.startsWith(`${projectIdArg}\0`) && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) ) this.deleteLiveTextUpdate(key); } for (const [key, baseline] of this.liveReasoningBaselines) { if ( key.startsWith(`${projectIdArg}\0`) && controllerRuntimeIdsEqual(baseline.update.sessionId, sessionIdArg) ) this.deleteLiveReasoningBaseline(key); } for (const [key, baseline] of this.liveTextBaselines) { if ( key.startsWith(`${projectIdArg}\0`) && controllerRuntimeIdsEqual(baseline.update.sessionId, sessionIdArg) ) this.deleteLiveTextBaseline(key); } this.canonicalChatProjectionDirty = true; // Keep both hydration floors: they continue fencing snapshots already // represented by the last authoritative detail until its replacement lands. } private liveMessageUpdateBytes(updateArg: unknown): number { return browserTextEncoder.encode(JSON.stringify(updateArg)).byteLength; } private deleteLiveReasoningUpdate(keyArg: string): void { const update = this.liveReasoningUpdates.get(keyArg); if (!update) return; this.liveReasoningUpdates.delete(keyArg); this.liveReasoningTextUtf8Bytes.delete(keyArg); const bytes = this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(update); this.liveReasoningStoredBytes.delete(keyArg); this.liveMessageOverlayBytes = Math.max( 0, this.liveMessageOverlayBytes - bytes, ); } private deleteLiveTextUpdate(keyArg: string): void { const update = this.liveTextUpdates.get(keyArg); if (!update) return; this.liveTextUpdates.delete(keyArg); this.liveTextTextUtf8Bytes.delete(keyArg); const bytes = this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(update); this.liveTextStoredBytes.delete(keyArg); this.liveMessageOverlayBytes = Math.max( 0, this.liveMessageOverlayBytes - bytes, ); } private deleteLiveReasoningBaseline(keyArg: string): void { const baseline = this.liveReasoningBaselines.get(keyArg); if (!baseline) return; this.liveReasoningBaselines.delete(keyArg); this.liveMessageOverlayBytes = Math.max(0, this.liveMessageOverlayBytes - baseline.bytes); } private deleteLiveTextBaseline(keyArg: string): void { const baseline = this.liveTextBaselines.get(keyArg); if (!baseline) return; this.liveTextBaselines.delete(keyArg); this.liveMessageOverlayBytes = Math.max(0, this.liveMessageOverlayBytes - baseline.bytes); } private liveMessageEntryCount(): number { return this.liveReasoningUpdates.size + this.liveTextUpdates.size + this.liveReasoningBaselines.size + this.liveTextBaselines.size; } private storeLiveReasoningUpdate( keyArg: string, updateArg: interfaces.IControllerReasoningUpdate, ): boolean { const bytes = this.liveMessageUpdateBytes(updateArg); const existing = this.liveReasoningUpdates.get(keyArg); const baseline = this.liveReasoningBaselines.get(keyArg); const existingBytes = existing ? this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(existing) : baseline?.bytes ?? 0; if ( (!existing && !baseline && this.liveMessageEntryCount() >= maxLiveToolOverlayEntries) || this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes ) { return false; } if (existing) this.deleteLiveReasoningUpdate(keyArg); if (baseline) this.deleteLiveReasoningBaseline(keyArg); this.liveReasoningUpdates.set(keyArg, updateArg); this.liveReasoningTextUtf8Bytes.set( keyArg, browserTextEncoder.encode(updateArg.text).byteLength, ); this.liveReasoningStoredBytes.set(keyArg, bytes); this.liveMessageOverlayBytes += bytes; return true; } private storeLiveTextUpdate( keyArg: string, updateArg: interfaces.IControllerTextUpdate, ): boolean { const bytes = this.liveMessageUpdateBytes(updateArg); const existing = this.liveTextUpdates.get(keyArg); const baseline = this.liveTextBaselines.get(keyArg); const existingBytes = existing ? this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(existing) : baseline?.bytes ?? 0; if ( (!existing && !baseline && this.liveMessageEntryCount() >= maxLiveToolOverlayEntries) || this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes ) { return false; } if (existing) this.deleteLiveTextUpdate(keyArg); if (baseline) this.deleteLiveTextBaseline(keyArg); this.liveTextUpdates.set(keyArg, updateArg); this.liveTextTextUtf8Bytes.set(keyArg, browserTextEncoder.encode(updateArg.text).byteLength); this.liveTextStoredBytes.set(keyArg, bytes); this.liveMessageOverlayBytes += bytes; return true; } private retireLiveReasoningUpdateToBaseline( keyArg: string, updateArg: interfaces.IControllerReasoningUpdate, textArg: string, cursorArg: interfaces.IControllerMessageStreamCursor, ): void { const update = { ...updateArg, text: textArg, revision: cursorArg.revision }; const bytes = this.liveMessageUpdateBytes(update); const existingBytes = this.liveReasoningStoredBytes.get(keyArg) ?? this.liveReasoningBaselines.get(keyArg)?.bytes ?? this.liveMessageUpdateBytes(updateArg); if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) { this.deleteLiveReasoningUpdate(keyArg); this.deleteLiveReasoningBaseline(keyArg); return; } this.deleteLiveReasoningUpdate(keyArg); this.deleteLiveReasoningBaseline(keyArg); this.liveReasoningBaselines.set(keyArg, { update, textUtf8Bytes: browserTextEncoder.encode(textArg).byteLength, bytes, }); this.liveMessageOverlayBytes += bytes; } private retireLiveTextUpdateToBaseline( keyArg: string, updateArg: interfaces.IControllerTextUpdate, textArg: string, cursorArg: interfaces.IControllerMessageStreamCursor, ): void { const update = { ...updateArg, text: textArg, revision: cursorArg.revision }; const bytes = this.liveMessageUpdateBytes(update); const existingBytes = this.liveTextStoredBytes.get(keyArg) ?? this.liveTextBaselines.get(keyArg)?.bytes ?? this.liveMessageUpdateBytes(updateArg); if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) { this.deleteLiveTextUpdate(keyArg); this.deleteLiveTextBaseline(keyArg); return; } this.deleteLiveTextUpdate(keyArg); this.deleteLiveTextBaseline(keyArg); this.liveTextBaselines.set(keyArg, { update, textUtf8Bytes: browserTextEncoder.encode(textArg).byteLength, bytes, }); this.liveMessageOverlayBytes += bytes; } private storeLiveReasoningDeltaState( keyArg: string, updateArg: interfaces.IControllerReasoningUpdate, textUtf8BytesArg: number, deltaArg: string, ): boolean { const active = this.liveReasoningUpdates.get(keyArg); const baseline = this.liveReasoningBaselines.get(keyArg); const existingBytes = active ? this.liveReasoningStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(active) : baseline?.bytes ?? 0; const bytes = existingBytes + Math.max( browserTextEncoder.encode(deltaArg).byteLength, deltaArg.length * 6, ); if (!active && !baseline) return false; if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) return false; this.deleteLiveReasoningUpdate(keyArg); this.deleteLiveReasoningBaseline(keyArg); this.liveReasoningUpdates.set(keyArg, updateArg); this.liveReasoningTextUtf8Bytes.set(keyArg, textUtf8BytesArg); this.liveReasoningStoredBytes.set(keyArg, bytes); this.liveMessageOverlayBytes += bytes; return true; } private storeLiveTextDeltaState( keyArg: string, updateArg: interfaces.IControllerTextUpdate, textUtf8BytesArg: number, deltaArg: string, ): boolean { const active = this.liveTextUpdates.get(keyArg); const baseline = this.liveTextBaselines.get(keyArg); const existingBytes = active ? this.liveTextStoredBytes.get(keyArg) ?? this.liveMessageUpdateBytes(active) : baseline?.bytes ?? 0; const bytes = existingBytes + Math.max( browserTextEncoder.encode(deltaArg).byteLength, deltaArg.length * 6, ); if (!active && !baseline) return false; if (this.liveMessageOverlayBytes - existingBytes + bytes > maxLiveToolOverlayBytes) return false; this.deleteLiveTextUpdate(keyArg); this.deleteLiveTextBaseline(keyArg); this.liveTextUpdates.set(keyArg, updateArg); this.liveTextTextUtf8Bytes.set(keyArg, textUtf8BytesArg); this.liveTextStoredBytes.set(keyArg, bytes); this.liveMessageOverlayBytes += bytes; return true; } private liveMessageUpdateKey( projectIdArg: string, updateArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate | interfaces.IControllerReasoningDelta | interfaces.IControllerTextDelta, ): string { return `${projectIdArg}\0${controllerRuntimeIdToUiKey(updateArg.sessionId)}\0${controllerRuntimeIdToUiKey(updateArg.partId)}`; } private blockLiveMessageDeltas( projectIdArg: string, sessionIdArg: TBrowserSessionId, streamEpochArg: number, refreshArg = true, ): void { const sessionKey = this.sessionOperationKey(projectIdArg, sessionIdArg); if (this.liveMessageDeltaBlocks.get(sessionKey)?.streamEpoch === streamEpochArg) return; this.liveMessageDeltaBlocks.set(sessionKey, { sessionId: sessionIdArg, streamEpoch: streamEpochArg, }); if ( refreshArg && projectIdArg === this.selectedProjectId && controllerRuntimeIdsEqual(sessionIdArg, this.selectedSessionId) ) void this.refreshSelectedSessionDetail(); } private liveMessageDeltasBlocked( projectIdArg: string, sessionIdArg: TBrowserSessionId, streamEpochArg: number, ): boolean { return this.liveMessageDeltaBlocks.get( this.sessionOperationKey(projectIdArg, sessionIdArg), )?.streamEpoch === streamEpochArg; } private acceptLiveToolEpoch( harnessIdArg: TBrowserSessionHarnessId, epochArg: number, handleBarrierArg = true, ): boolean { const currentEpoch = this.liveToolStreamEpochs.get(harnessIdArg) ?? 0; if (epochArg < currentEpoch) return false; if (epochArg > currentEpoch) { this.liveToolStreamEpochs.set(harnessIdArg, epochArg); this.clearLiveToolState(harnessIdArg); if (this.subagentModalSessionId?.harnessId === harnessIdArg) { this.subagentModalHistoryAbortController?.abort( new Error('Subagent history tool stream epoch advanced.'), ); } if (handleBarrierArg) { if (this.selectedSessionId?.harnessId === harnessIdArg) { this.invalidateDetailEnrichment(); void this.refreshSelectedSessionDetail(); } this.scheduleRefresh(); } } return true; } private acceptLiveMessageEpoch( harnessIdArg: TBrowserSessionHarnessId, epochArg: number, handleBarrierArg = true, refreshSessionListArg = true, ): boolean { const currentEpoch = this.liveMessageStreamEpochs.get(harnessIdArg) ?? 0; if (epochArg < currentEpoch) return false; if (epochArg > currentEpoch) { this.liveMessageStreamEpochs.set(harnessIdArg, epochArg); this.clearLiveMessageState(harnessIdArg); if (this.subagentModalSessionId?.harnessId === harnessIdArg) { this.subagentModalHistoryAbortController?.abort( new Error('Subagent history message stream epoch advanced.'), ); } if (handleBarrierArg) { if (this.selectedSessionId?.harnessId === harnessIdArg) { this.invalidateDetailEnrichment(); void this.refreshSelectedSessionDetail(); } if (refreshSessionListArg) this.refreshSessionsNow(); } } return true; } private authoritativeToolCall( executionArg: interfaces.IControllerToolExecution, detailArg = this.sessionDetail, ): interfaces.IControllerToolCall | undefined { if (!detailArg || !controllerRuntimeIdsEqual(detailArg.session.id, executionArg.sessionId)) { return undefined; } return detailArg.messages.find((messageArg) => ( controllerRuntimeIdsEqual(messageArg.id, executionArg.partId) ))?.toolCall; } private authoritativeToolCallCovers( executionArg: interfaces.IControllerToolExecution, toolCallArg: interfaces.IControllerToolCall | undefined, ): boolean { return interfaces.controllerLiveToolExecutionIsCovered(executionArg, toolCallArg); } private hydrationCursorCovers( cursorArg: { streamEpoch: number; revision: number }, snapshotArg: { streamEpoch: number; revision: number }, ): boolean { return cursorArg.streamEpoch === snapshotArg.streamEpoch && snapshotArg.revision <= cursorArg.revision; } private authoritativeReasoningUpdateCovers( updateArg: interfaces.IControllerReasoningUpdate, detailArg: IControllerSessionRenderDetail, ): boolean { const target = this.authoritativeReasoningTarget(updateArg, detailArg); return target !== undefined && (target.part.text === updateArg.text || target.part.text.startsWith(updateArg.text)) && ( updateArg.status === 'running' || target.part.endedAt !== undefined || target.message.streaming !== true ); } private authoritativeReasoningTarget( updateArg: interfaces.IControllerReasoningUpdate, detailArg: IControllerSessionRenderDetail, ): { message: interfaces.IControllerMessage; part: interfaces.IControllerReasoningPart } | undefined { const message = detailArg.messages.find((messageArg) => ( messageArg.role === 'assistant' && ( messageArg.reasoning?.some((partArg) => ( controllerRuntimeIdsEqual(partArg.id, updateArg.partId) )) || ( updateArg.order !== undefined && messageArg.order?.messageIndex === updateArg.order.messageIndex && messageArg.order.partIndex === updateArg.order.partIndex ) ) )); const part = message?.reasoning?.find((partArg) => ( controllerRuntimeIdsEqual(partArg.id, updateArg.partId) )); return message && part ? { message, part } : undefined; } private authoritativeTextUpdateCovers( updateArg: interfaces.IControllerTextUpdate, detailArg: IControllerSessionRenderDetail, ): boolean { const message = this.authoritativeTextTarget(updateArg, detailArg); return message !== undefined && (message.text === updateArg.text || message.text.startsWith(updateArg.text)) && (updateArg.status === 'running' || message.streaming !== true); } private authoritativeTextTarget( updateArg: interfaces.IControllerTextUpdate, detailArg: IControllerSessionRenderDetail, ): interfaces.IControllerMessage | undefined { return detailArg.messages.find((messageArg) => ( messageArg.role === 'assistant' && ( controllerRuntimeIdsEqual(messageArg.id, updateArg.partId) || ( updateArg.order !== undefined && messageArg.order?.messageIndex === updateArg.order.messageIndex && messageArg.order.partIndex === updateArg.order.partIndex ) ) )); } private liveToolSnapshotIsNewer( executionArg: interfaces.IControllerToolExecution, currentArg: interfaces.IControllerToolCall | interfaces.IControllerToolExecution | undefined, ): boolean { if (!currentArg) return true; const currentExecution = 'revision' in currentArg ? currentArg : undefined; if (currentExecution) { if (executionArg.streamEpoch < currentExecution.streamEpoch) return false; if ( executionArg.streamEpoch === currentExecution.streamEpoch && executionArg.revision <= currentExecution.revision ) return false; } const currentTerminal = currentArg.status === 'completed' || currentArg.status === 'error' || currentArg.status === 'stopped'; const nextTerminal = executionArg.status === 'completed' || executionArg.status === 'error' || executionArg.status === 'stopped'; if (currentTerminal && !nextTerminal) return false; if (currentExecution && currentArg.status === executionArg.status) return true; const currentSourceUpdatedAt = currentArg.finishedAt ?? currentArg.startedAt ?? 0; if (executionArg.sourceUpdatedAt < currentSourceUpdatedAt) return false; if (executionArg.sourceUpdatedAt > currentSourceUpdatedAt) return true; if (currentTerminal) return false; if (nextTerminal) return true; if (currentArg.status === executionArg.status) return false; return currentArg.status === 'pending' && executionArg.status === 'running'; } private coveredTerminalToolSupplement( executionArg: interfaces.IControllerToolExecution, toolCallArg: interfaces.IControllerToolCall | undefined, ): interfaces.IControllerToolExecution | undefined { const executionTerminal = executionArg.status === 'completed' || executionArg.status === 'error' || executionArg.status === 'stopped'; const durableTerminal = toolCallArg?.status === 'completed' || toolCallArg?.status === 'error' || toolCallArg?.status === 'stopped'; if ( !toolCallArg || !executionTerminal || !durableTerminal || !controllerRuntimeIdsEqual(toolCallArg.id, executionArg.callId) || toolCallArg.name !== executionArg.toolName || toolCallArg.status !== executionArg.status || ( executionArg.finishedAt !== undefined && ( toolCallArg.finishedAt === undefined || toolCallArg.finishedAt < executionArg.finishedAt ) ) ) return undefined; // The snapshot's truncation flags are dropped here and re-derived below: a spread can add a // key but never remove one, and a flag that outlived the live prefix a durable payload // replaced would mark a complete text as cut. const { outputTruncated: _outputTruncated, errorTextTruncated: _errorTextTruncated, ...snapshot } = executionArg; const merged: interfaces.IControllerToolExecution = { ...snapshot, ...this.supplementedToolTruncationFlags(executionArg, toolCallArg), ...(toolCallArg.input === undefined ? {} : { input: toolCallArg.input }), ...(toolCallArg.output === undefined ? {} : { output: toolCallArg.output }), ...(toolCallArg.errorText === undefined ? {} : { errorText: toolCallArg.errorText }), ...(toolCallArg.exitCode === undefined ? {} : { exitCode: toolCallArg.exitCode }), ...(toolCallArg.startedAt === undefined ? {} : { startedAt: toolCallArg.startedAt }), ...(toolCallArg.finishedAt === undefined ? {} : { finishedAt: toolCallArg.finishedAt }), ...(toolCallArg.childSessionId === undefined ? {} : { childSessionId: toolCallArg.childSessionId }), ...(toolCallArg.model === undefined ? {} : { model: toolCallArg.model }), }; return this.authoritativeToolCallCovers(merged, toolCallArg) ? undefined : merged; } /** * The truncation flags a supplemented snapshot carries. A bounded text and the flag describing * it are one unit, so each flag comes from the call that supplies the text: a durable payload * is complete unless the durable call was itself filled from a bounded live snapshot, and a * live prefix the transcript has not caught up with keeps the flag it arrived with. */ private supplementedToolTruncationFlags( executionArg: interfaces.IControllerToolExecution, toolCallArg: interfaces.IControllerToolCall, ): Pick { const outputTruncated = toolCallArg.output === undefined ? executionArg.outputTruncated : toolCallArg.outputTruncated; const errorTextTruncated = toolCallArg.errorText === undefined ? executionArg.errorTextTruncated : toolCallArg.errorTextTruncated; return { ...(outputTruncated ? { outputTruncated: true as const } : {}), ...(errorTextTruncated ? { errorTextTruncated: true as const } : {}), }; } private ensureCanonicalChatProjection( detailArg: IControllerSessionRenderDetail, projectIdArg: string, sessionIdArg: TBrowserSessionId, ): ICanonicalChatProjection { const key = this.sessionOperationKey(projectIdArg, sessionIdArg); let projection = this.canonicalChatProjection; if (!projection || projection.key !== key) { projection = { key, sourceDetail: undefined, messages: [], messagesById: new Map(), messagesByOrder: new Map(), reasoningById: new Map(), }; this.canonicalChatProjection = projection; this.canonicalChatProjectionDirty = true; } if (projection.sourceDetail === detailArg && !this.canonicalChatProjectionDirty) { return projection; } const messages = detailArg.messages .filter((messageArg) => messageArg.toolCall?.name !== 'question') .map((messageArg) => this.toHarnessMessage(messageArg, projectIdArg, sessionIdArg)); const visibleMessageIds = new Set(messages.map((message) => message.id)); for (const prompt of detailArg.pendingPrompts) { const id = controllerRuntimeIdToUiKey(prompt.id); if (visibleMessageIds.has(id)) continue; visibleMessageIds.add(id); messages.push({ id, role: 'user', text: prompt.text, createdAt: prompt.createdAt, markdown: false, attachments: prompt.attachments.map((attachment) => ({ ...attachment })), }); } this.mergeLiveReasoningMessages(messages, projectIdArg, sessionIdArg); this.mergeLiveTextMessages(messages, projectIdArg, sessionIdArg); this.mergeLiveToolMessages(messages, projectIdArg, sessionIdArg); const structureChanged = this.canonicalHarnessStructureChanged( projection.messages, messages, ); this.reconcileCanonicalHarnessMessages(projection, messages); this.canonicalTimelineRefreshPending ||= structureChanged; projection.sourceDetail = detailArg; this.canonicalChatProjectionDirty = false; return projection; } private canonicalHarnessStructureChanged( currentArg: readonly plugins.deesCatalog.IHarnessMessage[], nextArg: readonly plugins.deesCatalog.IHarnessMessage[], ): boolean { return currentArg.length !== nextArg.length || currentArg.some((current, index) => { const next = nextArg[index]; return !next || current.id !== next.id || current.role !== next.role || current.toolCall?.id !== next.toolCall?.id || current.toolCall?.name !== next.toolCall?.name || !this.liveMessageOrdersEqual(current.order, next.order); }); } private reconcileCanonicalHarnessMessages( projectionArg: ICanonicalChatProjection, nextMessagesArg: plugins.deesCatalog.IHarnessMessage[], ): void { const currentById = new Map( projectionArg.messages.map((message) => [message.id, message]), ); const reconciled = nextMessagesArg.map((nextMessage) => { const current = currentById.get(nextMessage.id); if (!current) return nextMessage; if (!controllerJsonValuesEqual(current, nextMessage)) { this.canonicalMessageRefreshIds.add(current.id); } this.reconcileCanonicalHarnessMessage(current, nextMessage); return current; }); projectionArg.messages.splice(0, projectionArg.messages.length, ...reconciled); this.rebuildCanonicalChatIndexes(projectionArg); } private reconcileCanonicalHarnessMessage( targetArg: plugins.deesCatalog.IHarnessMessage, sourceArg: plugins.deesCatalog.IHarnessMessage, ): void { const targetReasoning = targetArg.reasoning; const sourceReasoning = sourceArg.reasoning; const targetToolCall = targetArg.toolCall; const sourceToolCall = sourceArg.toolCall; const target = targetArg as unknown as Record; const source = sourceArg as unknown as Record; for (const key of Object.keys(target)) { if (!(key in source)) delete target[key]; } for (const [key, value] of Object.entries(source)) { if (key !== 'reasoning' && key !== 'toolCall') target[key] = value; } if (sourceReasoning) { const currentById = new Map((targetReasoning ?? []).map((part) => [part.id, part])); const reasoning = sourceReasoning.map((sourcePart) => { const currentPart = currentById.get(sourcePart.id); if (!currentPart) return sourcePart; const current = currentPart as unknown as Record; const next = sourcePart as unknown as Record; for (const key of Object.keys(current)) { if (!(key in next)) delete current[key]; } Object.assign(current, next); return currentPart; }); if (targetReasoning) targetReasoning.splice(0, targetReasoning.length, ...reasoning); else targetArg.reasoning = reasoning; } if (sourceToolCall) { if (targetToolCall?.id === sourceToolCall.id) { const current = targetToolCall as unknown as Record; const next = sourceToolCall as unknown as Record; for (const key of Object.keys(current)) { if (!(key in next)) delete current[key]; } Object.assign(current, next); targetArg.toolCall = targetToolCall; } else { targetArg.toolCall = sourceToolCall; } } } private rebuildCanonicalChatIndexes(projectionArg: ICanonicalChatProjection): void { projectionArg.messagesById.clear(); projectionArg.messagesByOrder.clear(); projectionArg.reasoningById.clear(); for (const message of projectionArg.messages) { projectionArg.messagesById.set(message.id, message); if (message.order && !projectionArg.messagesByOrder.has(transcriptOrderKey(message.order))) { projectionArg.messagesByOrder.set(transcriptOrderKey(message.order), message); } for (const part of message.reasoning ?? []) { projectionArg.reasoningById.set(part.id, { message, part }); } } } private selectedCanonicalChatProjection( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): ICanonicalChatProjection | undefined { const detail = this.sessionDetail; if ( projectIdArg !== this.selectedProjectId || !detail || !controllerRuntimeIdsEqual(detail.session.id, sessionIdArg) || !controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg) ) return undefined; return this.ensureCanonicalChatProjection(detail, projectIdArg, sessionIdArg); } private canonicalTextTarget( projectionArg: ICanonicalChatProjection, updateArg: interfaces.IControllerTextUpdate | interfaces.IControllerTextDelta, ): plugins.deesCatalog.IHarnessMessage | undefined { return projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.messageId)) ?? projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.partId)) ?? (updateArg.order ? projectionArg.messagesByOrder.get(transcriptOrderKey(updateArg.order)) : undefined); } private canonicalReasoningTarget( projectionArg: ICanonicalChatProjection, updateArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerReasoningDelta, ): ICanonicalReasoningTarget | undefined { const reasoningId = controllerRuntimeIdToUiKey(updateArg.partId); const partTarget = projectionArg.reasoningById.get(reasoningId); const message = projectionArg.messagesById.get(controllerRuntimeIdToUiKey(updateArg.messageId)) ?? partTarget?.message ?? (updateArg.order ? projectionArg.messagesByOrder.get(transcriptOrderKey(updateArg.order)) : undefined); if (!message) return undefined; const part = partTarget?.message === message ? partTarget.part : message.reasoning?.find((candidate) => candidate.id === reasoningId); return part ? { message, part } : undefined; } private mountedCanonicalChat( projectionArg: ICanonicalChatProjection, ): InstanceType | undefined { const chat = this.shadowRoot?.querySelector('dees-harness-chat'); return chat instanceof plugins.deesCatalog.DeesHarnessChat && chat.messages === projectionArg.messages && chat.transcriptKey === projectionArg.key ? chat : undefined; } private applyCanonicalTextDelta( projectionArg: ICanonicalChatProjection, messageArg: plugins.deesCatalog.IHarnessMessage, deltaArg: string, ): boolean { const previousText = messageArg.text; const expectedText = `${previousText}${deltaArg}`; const chat = this.mountedCanonicalChat(projectionArg); chat?.applyDelta({ type: 'text', messageId: messageArg.id, delta: deltaArg }); if (messageArg.text === previousText) { messageArg.text = expectedText; messageArg.streaming = true; } if (messageArg.text === expectedText) return true; messageArg.text = previousText; return false; } private applyCanonicalReasoningDelta( projectionArg: ICanonicalChatProjection, targetArg: ICanonicalReasoningTarget, deltaArg: string, ): boolean { const previousText = targetArg.part.text; const expectedText = `${previousText}${deltaArg}`; const chat = this.mountedCanonicalChat(projectionArg); chat?.applyDelta({ type: 'reasoning', messageId: targetArg.message.id, partId: targetArg.part.id, delta: deltaArg, }); if (targetArg.part.text === previousText) targetArg.part.text = expectedText; if (targetArg.part.text === expectedText) return true; targetArg.part.text = previousText; return false; } private requestCanonicalChatReconciliation(structureMayChangeArg = true): void { this.canonicalChatProjectionDirty = true; this.canonicalTimelineRefreshPending ||= structureMayChangeArg; this.requestUpdate(); } private applyLiveToolExecution( projectIdArg: string, executionArg: interfaces.IControllerToolExecution, ): void { if (!isSessionRuntimeId(executionArg.sessionId)) return; if ( executionArg.sourceUpdatedAt <= (this.liveHistoryBarriers.get( this.liveToolSessionKey(projectIdArg, executionArg.sessionId), ) ?? Number.NEGATIVE_INFINITY) ) return; if (!this.acceptLiveToolEpoch(executionArg.sessionId.harnessId, executionArg.streamEpoch)) return; if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(executionArg.sessionId, this.selectedSessionId) ) return; let execution = executionArg; const sessionKey = this.liveToolSessionKey(projectIdArg, executionArg.sessionId); const hydrationFloor = this.liveToolHydrationFloors.get(sessionKey)?.cursor; const authoritativeCall = this.authoritativeToolCall(executionArg); let coveredSupplement = false; if ( hydrationFloor && hydrationFloor.streamEpoch === executionArg.streamEpoch && executionArg.revision <= hydrationFloor.revision ) { if (this.authoritativeToolCallCovers(executionArg, authoritativeCall)) return; const supplement = this.coveredTerminalToolSupplement(executionArg, authoritativeCall); if (!supplement) return; execution = supplement; coveredSupplement = true; } const key = this.liveToolOverlayKey(projectIdArg, execution); const existing = this.liveToolOverlays.get(key); if ( existing && existing.execution.streamEpoch === execution.streamEpoch && execution.revision < existing.execution.revision ) return; if ( coveredSupplement ? existing && !this.liveToolSnapshotIsNewer(execution, existing.execution) : !this.liveToolSnapshotIsNewer(execution, existing?.execution ?? authoritativeCall) ) return; if (execution.sessionId.harnessId === 'flex' && !execution.order) return; const order = execution.order ?? existing?.order ?? this.allocateLiveToolOrder(projectIdArg, execution); const bytes = new TextEncoder().encode(JSON.stringify({ execution, order })).byteLength; if (existing) this.deleteLiveToolOverlay(key); if ( this.liveToolOverlays.size >= maxLiveToolOverlayEntries || this.liveToolOverlayBytes + bytes > maxLiveToolOverlayBytes ) { this.clearLiveToolState(executionArg.sessionId.harnessId); if ( this.liveToolOverlays.size >= maxLiveToolOverlayEntries || this.liveToolOverlayBytes + bytes > maxLiveToolOverlayBytes ) this.clearLiveToolState(); this.scheduleRefresh(); } if (bytes <= maxLiveToolOverlayBytes) { this.liveToolOverlays.set(key, { projectId: projectIdArg, execution, order, bytes, }); this.liveToolOverlayBytes += bytes; } this.reconcileSubtaskPreviews(projectIdArg, executionArg.sessionId); const projection = this.selectedCanonicalChatProjection(projectIdArg, executionArg.sessionId); if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate(); const messageId = controllerRuntimeIdToUiKey(execution.partId); const harnessToolCall = this.toHarnessLiveToolCall(execution, projectIdArg); const message = projection?.messagesById.get(messageId); if (projection && message) { message.toolCall = harnessToolCall; message.streaming = execution.status === 'pending' || execution.status === 'running'; message.updatedAt = execution.finishedAt ?? execution.sourceUpdatedAt; this.mountedCanonicalChat(projection)?.applyDelta({ type: 'tool-update', messageId, tool: harnessToolCall, }); } else { this.requestCanonicalChatReconciliation(); } } private reconcileLiveToolHydration( projectIdArg: string, sessionIdArg: TBrowserSessionId, cursorArg: interfaces.IControllerToolStreamCursor, detailArg: IControllerSessionRenderDetail, ): boolean { const harnessId = sessionIdArg.harnessId; if (cursorArg.streamEpoch < (this.liveToolStreamEpochs.get(harnessId) ?? 0)) return false; if (!this.acceptLiveToolEpoch(harnessId, cursorArg.streamEpoch, false)) return false; const sessionKey = this.liveToolSessionKey(projectIdArg, sessionIdArg); this.liveToolHydrationFloors.set(sessionKey, { sessionId: sessionIdArg, cursor: cursorArg }); for (const [key, overlay] of this.liveToolOverlays) { if ( overlay.projectId === projectIdArg && controllerRuntimeIdsEqual(overlay.execution.sessionId, sessionIdArg) && this.hydrationCursorCovers(cursorArg, overlay.execution) && this.authoritativeToolCallCovers( overlay.execution, this.authoritativeToolCall(overlay.execution, detailArg), ) ) this.deleteLiveToolOverlay(key); } return true; } private reconcileLiveMessageHydration( projectIdArg: string, sessionIdArg: TBrowserSessionId, cursorArg: interfaces.IControllerMessageStreamCursor, detailArg: IControllerSessionRenderDetail, ): boolean { const harnessId = sessionIdArg.harnessId; if (cursorArg.streamEpoch < (this.liveMessageStreamEpochs.get(harnessId) ?? 0)) return false; if (!this.acceptLiveMessageEpoch(harnessId, cursorArg.streamEpoch, false)) return false; const sessionKey = this.liveToolSessionKey(projectIdArg, sessionIdArg); const deltaBlockKey = this.sessionOperationKey(projectIdArg, sessionIdArg); const recoveringDeltaGap = this.liveMessageDeltaBlocks.get(deltaBlockKey)?.streamEpoch === cursorArg.streamEpoch; this.liveMessageHydrationFloors.set(sessionKey, { sessionId: sessionIdArg, cursor: cursorArg }); for (const [key, update] of this.liveReasoningUpdates) { const target = this.authoritativeReasoningTarget(update, detailArg); if ( key.startsWith(`${projectIdArg}\0`) && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) && this.hydrationCursorCovers(cursorArg, update) && target !== undefined && (recoveringDeltaGap || this.authoritativeReasoningUpdateCovers(update, detailArg)) ) this.retireLiveReasoningUpdateToBaseline(key, update, target.part.text, cursorArg); } for (const [key, update] of this.liveTextUpdates) { const target = this.authoritativeTextTarget(update, detailArg); if ( key.startsWith(`${projectIdArg}\0`) && controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) && this.hydrationCursorCovers(cursorArg, update) && target !== undefined && (recoveringDeltaGap || this.authoritativeTextUpdateCovers(update, detailArg)) ) this.retireLiveTextUpdateToBaseline(key, update, target.text, cursorArg); } for (const [key, baseline] of [...this.liveReasoningBaselines]) { const update = baseline.update; if ( !key.startsWith(`${projectIdArg}\0`) || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) || !this.hydrationCursorCovers(cursorArg, update) ) continue; const target = this.authoritativeReasoningTarget(update, detailArg); if (target) { this.retireLiveReasoningUpdateToBaseline(key, update, target.part.text, cursorArg); } else if (recoveringDeltaGap) { this.deleteLiveReasoningBaseline(key); } } for (const [key, baseline] of [...this.liveTextBaselines]) { const update = baseline.update; if ( !key.startsWith(`${projectIdArg}\0`) || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) || !this.hydrationCursorCovers(cursorArg, update) ) continue; const target = this.authoritativeTextTarget(update, detailArg); if (target) { this.retireLiveTextUpdateToBaseline(key, update, target.text, cursorArg); } else if (recoveringDeltaGap) { this.deleteLiveTextBaseline(key); } } this.liveMessageDeltaBlocks.delete(deltaBlockKey); return true; } private pruneLiveToolStateToSelection(): void { const projectId = this.selectedProjectId; const sessionId = this.selectedSessionId; for (const [key, overlay] of this.liveToolOverlays) { if ( projectId !== overlay.projectId || !isSessionRuntimeId(sessionId) || !controllerRuntimeIdsEqual(overlay.execution.sessionId, sessionId) ) this.deleteLiveToolOverlay(key); } for (const key of this.liveToolHydrationFloors.keys()) { if ( !projectId || !isSessionRuntimeId(sessionId) || key !== this.liveToolSessionKey(projectId, sessionId) ) this.liveToolHydrationFloors.delete(key); } for (const key of this.liveMessageHydrationFloors.keys()) { if ( !projectId || !isSessionRuntimeId(sessionId) || key !== this.liveToolSessionKey(projectId, sessionId) ) this.liveMessageHydrationFloors.delete(key); } for (const [key, update] of this.liveReasoningUpdates) { if ( !projectId || !isSessionRuntimeId(sessionId) || !key.startsWith(`${projectId}\0`) || !controllerRuntimeIdsEqual(update.sessionId, sessionId) ) this.deleteLiveReasoningUpdate(key); } for (const [key, update] of this.liveTextUpdates) { if ( !projectId || !isSessionRuntimeId(sessionId) || !key.startsWith(`${projectId}\0`) || !controllerRuntimeIdsEqual(update.sessionId, sessionId) ) this.deleteLiveTextUpdate(key); } for (const [key, baseline] of this.liveReasoningBaselines) { if ( !projectId || !isSessionRuntimeId(sessionId) || !key.startsWith(`${projectId}\0`) || !controllerRuntimeIdsEqual(baseline.update.sessionId, sessionId) ) this.deleteLiveReasoningBaseline(key); } for (const [key, baseline] of this.liveTextBaselines) { if ( !projectId || !isSessionRuntimeId(sessionId) || !key.startsWith(`${projectId}\0`) || !controllerRuntimeIdsEqual(baseline.update.sessionId, sessionId) ) this.deleteLiveTextBaseline(key); } for (const [key, block] of this.liveMessageDeltaBlocks) { if ( !projectId || !isSessionRuntimeId(sessionId) || key !== this.sessionOperationKey(projectId, sessionId) || !controllerRuntimeIdsEqual(block.sessionId, sessionId) ) this.liveMessageDeltaBlocks.delete(key); } } private childSessionHasIndependentManagedMembership( projectIdArg: string, childSessionIdArg: TBrowserSessionId, ): boolean { if (projectIdArg !== this.selectedProjectId) return false; // The normal session list contains only independent managed memberships; // provider-declared relationships never extend that authority. return this.sessions.some((sessionArg) => ( isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, childSessionIdArg) )); } private childSessionScopedAccessIsSupported( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, childSessionIdArg: TBrowserSessionId, ): boolean { return projectIdArg === this.selectedProjectId && controllerRuntimeIdsEqual(ownerSessionIdArg, this.selectedSessionId) && isOpenCodeSessionRuntimeId(ownerSessionIdArg) && isOpenCodeSessionRuntimeId(childSessionIdArg) && !controllerRuntimeIdsEqual(ownerSessionIdArg, childSessionIdArg); } private childSessionAccess( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, childSessionIdArg: TBrowserSessionId, ): TSubtaskAccess | undefined { if (this.childSessionHasIndependentManagedMembership(projectIdArg, childSessionIdArg)) { return 'managed'; } return this.childSessionScopedAccessIsSupported( projectIdArg, ownerSessionIdArg, childSessionIdArg, ) ? 'scoped' : undefined; } private hasUnmanagedChildSessionLink( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, ): boolean { const unavailable = (childSessionIdArg: interfaces.IControllerRuntimeId | undefined) => ( isSessionRuntimeId(childSessionIdArg) && this.childSessionAccess(projectIdArg, ownerSessionIdArg, childSessionIdArg) === undefined ); const detail = this.sessionDetail; if ( detail && controllerRuntimeIdsEqual(detail.session.id, ownerSessionIdArg) && detail.messages.some((messageArg) => unavailable(messageArg.toolCall?.childSessionId)) ) return true; return [...this.liveToolOverlays.values()].some((overlayArg) => ( overlayArg.projectId === projectIdArg && controllerRuntimeIdsEqual(overlayArg.execution.sessionId, ownerSessionIdArg) && unavailable(overlayArg.execution.childSessionId) )); } private subtaskPreviewKey( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, childSessionIdArg: TBrowserSessionId, ): string { return JSON.stringify([ projectIdArg, controllerRuntimeIdToUiKey(ownerSessionIdArg), controllerRuntimeIdToUiKey(childSessionIdArg), ]); } private subtaskStatusFromToolCall( callArg: interfaces.IControllerToolCall, ): plugins.deesCatalog.IHarnessStatus { if (callArg.status === 'pending' || callArg.status === 'running') { return { type: 'busy' }; } if (callArg.status === 'error') { return { type: 'error', message: callArg.errorText || 'The subagent reported an error.' }; } return { type: 'idle' }; } private subtaskStatusForCandidate( candidateArg: ISubtaskPreviewCandidate, ): { status: plugins.deesCatalog.IHarnessStatus; authoritative: boolean } { const childKey = controllerRuntimeIdToUiKey(candidateArg.childSessionId); if (this.errorSessionIdsByProject.get(candidateArg.projectId)?.has(childKey)) { return { status: { type: 'error', message: 'The subagent session reported an error.' }, authoritative: true, }; } const controllerStatus = this.authoritativeStatusesByProject .get(candidateArg.projectId) ?.get(childKey); if (controllerStatus === 'busy' || controllerStatus === 'retry') { return { status: { type: 'busy' }, authoritative: true }; } if (controllerStatus === 'error') { return { status: { type: 'error', message: 'The subagent session reported an error.' }, authoritative: true, }; } if (controllerStatus === 'idle') { return { status: { type: 'idle' }, authoritative: true }; } return { status: this.subtaskStatusFromToolCall(candidateArg.call), authoritative: false }; } private subtaskStatusFromSessionEvent( projectIdArg: string, eventArg: interfaces.IControllerEvent, ): plugins.deesCatalog.IHarnessStatus | undefined { const sessionKey = isSessionRuntimeId(eventArg.sessionId) ? controllerRuntimeIdToUiKey(eventArg.sessionId) : undefined; if ( eventArg.sessionError === true || eventArg.sessionStatus === 'error' || (sessionKey !== undefined && this.errorSessionIdsByProject.get(projectIdArg)?.has(sessionKey)) ) { return { type: 'error', message: 'The subagent session reported an error.' }; } if (eventArg.sessionStatus === 'busy' || eventArg.sessionStatus === 'retry') { return { type: 'busy' }; } if (eventArg.sessionStatus === 'idle') { return { type: 'idle' }; } return undefined; } private withSubtaskPreviewStatus( streamArg: plugins.deesCatalog.IHarnessSubtaskStream, statusArg: plugins.deesCatalog.IHarnessStatus, ): plugins.deesCatalog.IHarnessSubtaskStream { if ( streamArg.status.type === statusArg.type && streamArg.status.message === statusArg.message ) return streamArg; return { ...streamArg, status: statusArg }; } private collectSubtaskPreviewCandidates( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, ): ISubtaskPreviewCandidate[] { const candidates = new Map(); const addCandidate = ( callArg: interfaces.IControllerToolCall, createdAtArg: number, ): void => { if (!isSessionRuntimeId(callArg.childSessionId)) return; const access = this.childSessionAccess( projectIdArg, ownerSessionIdArg, callArg.childSessionId, ); if (!access) return; const key = this.subtaskPreviewKey(projectIdArg, ownerSessionIdArg, callArg.childSessionId); candidates.set(key, { key, projectId: projectIdArg, ownerSessionId: ownerSessionIdArg, childSessionId: callArg.childSessionId, access, call: callArg, createdAt: createdAtArg, }); }; const detail = this.sessionDetail; if (detail && controllerRuntimeIdsEqual(detail.session.id, ownerSessionIdArg)) { for (const message of detail.messages) { if (message.toolCall) addCandidate(message.toolCall, message.createdAt); } } for (const overlay of this.liveToolOverlays.values()) { const execution = overlay.execution; if ( overlay.projectId !== projectIdArg || !controllerRuntimeIdsEqual(execution.sessionId, ownerSessionIdArg) ) continue; addCandidate({ id: execution.callId, name: execution.toolName, status: execution.status, ...(execution.input === undefined ? {} : { input: execution.input }), ...(execution.output === undefined ? {} : { output: execution.output }), ...(execution.exitCode === undefined ? {} : { exitCode: execution.exitCode }), ...(execution.errorText === undefined ? {} : { errorText: execution.errorText }), ...(execution.childSessionId === undefined ? {} : { childSessionId: execution.childSessionId }), ...(execution.model === undefined ? {} : { model: execution.model }), ...(execution.startedAt === undefined ? {} : { startedAt: execution.startedAt }), ...(execution.finishedAt === undefined ? {} : { finishedAt: execution.finishedAt }), }, execution.startedAt ?? execution.sourceUpdatedAt); } return [...candidates.values()] .sort((left, right) => { const leftEntry = this.subtaskPreviewEntries.get(left.key); const rightEntry = this.subtaskPreviewEntries.get(right.key); const leftActive = (leftEntry?.statusAuthoritative ? leftEntry.stream.status : this.subtaskStatusForCandidate(left).status).type === 'busy'; const rightActive = (rightEntry?.statusAuthoritative ? rightEntry.stream.status : this.subtaskStatusForCandidate(right).status).type === 'busy'; if (leftActive !== rightActive) return leftActive ? -1 : 1; if (left.createdAt !== right.createdAt) return right.createdAt - left.createdAt; return left.key.localeCompare(right.key); }) .slice(0, maxSubtaskPreviewEntries); } private reconcileSubtaskPreviews( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, ): void { if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(ownerSessionIdArg, this.selectedSessionId) ) return; const candidates = this.collectSubtaskPreviewCandidates(projectIdArg, ownerSessionIdArg); const retainedKeys = new Set(candidates.map((candidate) => candidate.key)); let changed = false; for (const [key, entry] of this.subtaskPreviewEntries) { if (retainedKeys.has(key)) continue; if (entry.refreshTimer) clearTimeout(entry.refreshTimer); entry.abortController?.abort(new Error('Subtask preview removed.')); this.subtaskPreviewEntries.delete(key); changed = true; } for (const candidate of candidates) { const existing = this.subtaskPreviewEntries.get(candidate.key); const candidateStatus = this.subtaskStatusForCandidate(candidate); if (existing) { if (existing.access !== candidate.access) { this.cancelSubtaskPreviewHydration(existing); existing.access = candidate.access; existing.scopeGeneration = undefined; existing.scopeSequence = undefined; } const wasBusy = existing.stream.status.type === 'busy'; if (!existing.statusAuthoritative || candidateStatus.authoritative) { const stream = this.withSubtaskPreviewStatus(existing.stream, candidateStatus.status); if (stream !== existing.stream) { existing.stream = stream; changed = true; } existing.statusAuthoritative = candidateStatus.authoritative; } const isBusy = existing.stream.status.type === 'busy'; if (!isBusy) this.cancelSubtaskPreviewHydration(existing); else if (!wasBusy) this.queueSubtaskPreviewHydration(existing); continue; } const isBusy = candidateStatus.status.type === 'busy'; const entry: ISubtaskPreviewEntry = { key: candidate.key, projectId: candidate.projectId, ownerSessionId: candidate.ownerSessionId, childSessionId: candidate.childSessionId, access: candidate.access, generation: this.subtaskPreviewGeneration, stream: isBusy ? { sessionId: controllerRuntimeIdToUiKey(candidate.childSessionId), previewState: 'loading', status: candidateStatus.status, } : { sessionId: controllerRuntimeIdToUiKey(candidate.childSessionId), previewState: 'unavailable', status: candidateStatus.status, notice: subtaskPreviewUnavailableNotice, }, statusAuthoritative: candidateStatus.authoritative, queued: false, refreshing: false, refreshRequested: false, }; this.subtaskPreviewEntries.set(candidate.key, entry); if (isBusy) this.queueSubtaskPreviewHydration(entry); changed = true; } if (changed) this.requestCanonicalChatReconciliation(); } private queueSubtaskPreviewHydration(entryArg: ISubtaskPreviewEntry): void { if ( this.subtaskPreviewEntries.get(entryArg.key) !== entryArg || entryArg.generation !== this.subtaskPreviewGeneration || entryArg.stream.status.type !== 'busy' ) return; if (entryArg.refreshing) { entryArg.refreshRequested = true; return; } if (entryArg.queued) return; entryArg.queued = true; this.subtaskPreviewHydrationQueue.push(entryArg.key); this.pumpSubtaskPreviewHydrations(); } private cancelSubtaskPreviewHydration(entryArg: ISubtaskPreviewEntry): void { if (entryArg.refreshTimer) { clearTimeout(entryArg.refreshTimer); entryArg.refreshTimer = undefined; } if (entryArg.queued) { entryArg.queued = false; this.subtaskPreviewHydrationQueue = this.subtaskPreviewHydrationQueue.filter( (key) => key !== entryArg.key, ); } entryArg.refreshRequested = false; entryArg.abortController?.abort(new Error('Subtask preview is no longer active.')); entryArg.abortController = undefined; entryArg.refreshing = false; if (entryArg.stream.previewState === 'loading') { entryArg.stream = { sessionId: controllerRuntimeIdToUiKey(entryArg.childSessionId), previewState: 'unavailable', status: entryArg.stream.status, notice: subtaskPreviewUnavailableNotice, }; } } private pumpSubtaskPreviewHydrations(): void { while ( this.activeSubtaskPreviewHydrations < maxConcurrentSubtaskPreviewHydrations && this.subtaskPreviewHydrationQueue.length > 0 ) { const key = this.subtaskPreviewHydrationQueue.shift()!; const entry = this.subtaskPreviewEntries.get(key); if ( !entry || !entry.queued || entry.refreshing || entry.generation !== this.subtaskPreviewGeneration || entry.stream.status.type !== 'busy' ) continue; entry.queued = false; entry.refreshing = true; const abortController = new AbortController(); entry.abortController = abortController; this.activeSubtaskPreviewHydrations += 1; void this.hydrateSubtaskPreview(entry, abortController).finally(() => { this.activeSubtaskPreviewHydrations -= 1; if (entry.abortController !== abortController) { this.pumpSubtaskPreviewHydrations(); return; } entry.abortController = undefined; entry.refreshing = false; if ( entry.refreshRequested && this.subtaskPreviewEntries.get(entry.key) === entry && entry.generation === this.subtaskPreviewGeneration ) { entry.refreshRequested = false; this.queueSubtaskPreviewHydration(entry); } this.pumpSubtaskPreviewHydrations(); }); } } private subtaskPreviewEntryIsCurrent(entryArg: ISubtaskPreviewEntry): boolean { return ( this.subtaskPreviewEntries.get(entryArg.key) === entryArg && entryArg.generation === this.subtaskPreviewGeneration && entryArg.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(entryArg.ownerSessionId, this.selectedSessionId) && this.childSessionAccess( entryArg.projectId, entryArg.ownerSessionId, entryArg.childSessionId, ) === entryArg.access && this.authenticated && this.socketClient.isConnected ); } private async hydrateSubtaskPreview( entryArg: ISubtaskPreviewEntry, abortControllerArg: AbortController, ): Promise { try { let page: interfaces.IControllerMessagePage; let scopedDetail: interfaces.IControllerChildSessionDetail | undefined; if (entryArg.access === 'managed') { page = await this.socketClient.fire( 'controller.session.messages.page', { projectId: entryArg.projectId, sessionId: entryArg.childSessionId, limit: maxSubtaskPreviewBundles, }, { maxRetries: 0, abortSignal: abortControllerArg.signal }, ); } else { if ( !isOpenCodeSessionRuntimeId(entryArg.ownerSessionId) || !isOpenCodeSessionRuntimeId(entryArg.childSessionId) ) return; scopedDetail = await this.socketClient.fire( 'controller.session.child.get', { projectId: entryArg.projectId, parentSessionId: entryArg.ownerSessionId, childSessionId: entryArg.childSessionId, }, { maxRetries: 0, abortSignal: abortControllerArg.signal }, ); page = scopedDetail.messagePage; } if ( entryArg.abortController !== abortControllerArg || !this.subtaskPreviewEntryIsCurrent(entryArg) ) return; if (scopedDetail) { entryArg.scopeGeneration = scopedDetail.scopeGeneration; entryArg.scopeSequence = scopedDetail.sequence; this.applyPendingChildEvent(scopedDetail.scopeGeneration, scopedDetail.sequence); } const messages = page.bundles .flatMap((bundle) => bundle.messages) .filter((message) => message.toolCall?.name !== 'question') .map((message) => this.toHarnessMessage( message, entryArg.projectId, entryArg.childSessionId, )); entryArg.stream = { sessionId: controllerRuntimeIdToUiKey(entryArg.childSessionId), previewState: 'ready', status: scopedDetail ? this.toHarnessStatus(scopedDetail.session) : entryArg.stream.status, messages, ...((page.truncated === true || page.nextCursor !== undefined) ? { truncated: true } : {}), }; this.requestCanonicalChatReconciliation(); } catch { if (abortControllerArg.signal.aborted) return; if (!this.subtaskPreviewEntryIsCurrent(entryArg)) return; entryArg.stream = entryArg.stream.previewState === 'ready' ? { ...entryArg.stream, notice: subtaskPreviewUnavailableNotice } : { sessionId: controllerRuntimeIdToUiKey(entryArg.childSessionId), previewState: 'unavailable', status: entryArg.stream.status, notice: subtaskPreviewUnavailableNotice, }; this.requestCanonicalChatReconciliation(); } } private scheduleSubtaskPreviewRefresh( projectIdArg: string, childSessionIdArg: TBrowserSessionId, statusArg?: plugins.deesCatalog.IHarnessStatus, ): void { let changed = false; for (const entry of this.subtaskPreviewEntries.values()) { if ( entry.projectId !== projectIdArg || !controllerRuntimeIdsEqual(entry.childSessionId, childSessionIdArg) ) continue; if (statusArg) { const stream = this.withSubtaskPreviewStatus(entry.stream, statusArg); if (stream !== entry.stream) { entry.stream = stream; changed = true; } entry.statusAuthoritative = true; } if (entry.stream.status.type !== 'busy') { this.cancelSubtaskPreviewHydration(entry); continue; } if (entry.refreshTimer) clearTimeout(entry.refreshTimer); entry.refreshTimer = setTimeout(() => { entry.refreshTimer = undefined; this.queueSubtaskPreviewHydration(entry); }, subtaskPreviewRefreshDelayMs); } if (changed) this.requestCanonicalChatReconciliation(); } private clearSubtaskPreviewState(): void { this.subtaskPreviewGeneration += 1; for (const entry of this.subtaskPreviewEntries.values()) { if (entry.refreshTimer) clearTimeout(entry.refreshTimer); entry.abortController?.abort(new Error('Subtask preview state cleared.')); } this.subtaskPreviewEntries.clear(); this.subtaskPreviewHydrationQueue = []; this.canonicalChatProjectionDirty = true; } private applySessionEventState(eventArg: interfaces.IControllerEvent): void { if (eventArg.type !== 'session.changed' || !isSessionRuntimeId(eventArg.sessionId)) return; const projectId = eventArg.projectId ?? this.selectedProjectId; if (!isNonEmptyString(projectId)) return; this.bumpSessionEventRevision(projectId); if (eventArg.sessionError !== true && eventArg.sessionStatus === undefined) return; const sessionKey = controllerRuntimeIdToUiKey(eventArg.sessionId); const statuses = this.projectStatuses(projectId); const finished = this.projectSessionIds(this.finishedSessionIdsByProject, projectId); const errors = this.projectSessionIds(this.errorSessionIdsByProject, projectId); const optimistic = this.projectSessionIds(this.optimisticWorkingIdsByProject, projectId); const cardStateBefore = this.sessionCardStateSignature( sessionKey, statuses, finished, errors, optimistic, ); if (eventArg.sessionError === true) { statuses.set(sessionKey, 'error'); errors.add(sessionKey); finished.delete(sessionKey); optimistic.delete(sessionKey); } else if (eventArg.sessionStatus !== undefined) { const previousStatus = statuses.get(sessionKey); const wasWorking = previousStatus === 'busy' || previousStatus === 'retry' || optimistic.has(sessionKey); statuses.set(sessionKey, eventArg.sessionStatus); if (eventArg.sessionStatus === 'busy' || eventArg.sessionStatus === 'retry') { errors.delete(sessionKey); finished.delete(sessionKey); optimistic.delete(sessionKey); } else if (eventArg.sessionStatus === 'idle') { optimistic.delete(sessionKey); if (wasWorking && !errors.has(sessionKey)) finished.add(sessionKey); } else if (eventArg.sessionStatus === 'error') { errors.add(sessionKey); finished.delete(sessionKey); optimistic.delete(sessionKey); } } // Repeated status events for an unchanged card state must not re-render the // whole application; busy sessions emit them continuously. const cardStateAfter = this.sessionCardStateSignature( sessionKey, statuses, finished, errors, optimistic, ); if (cardStateAfter !== cardStateBefore) this.touchSessionCardState(projectId); } private sessionCardStateSignature( sessionKeyArg: string, statusesArg: ReadonlyMap, finishedArg: ReadonlySet, errorsArg: ReadonlySet, optimisticArg: ReadonlySet, ): string { return `${statusesArg.get(sessionKeyArg) ?? ''}|${finishedArg.has(sessionKeyArg) ? 1 : 0}` + `${errorsArg.has(sessionKeyArg) ? 1 : 0}${optimisticArg.has(sessionKeyArg) ? 1 : 0}`; } private applySessionSnapshot( projectIdArg: string, sessionsArg: interfaces.IControllerSession[], requestEventRevisionArg: number, ): void { const statuses = this.projectStatuses(projectIdArg); const currentRevision = this.sessionEventRevisionByProject.get(projectIdArg) ?? 0; if (currentRevision !== requestEventRevisionArg) { for (const session of sessionsArg) { if (!isSessionRuntimeId(session.id)) continue; const sessionKey = controllerRuntimeIdToUiKey(session.id); if (!statuses.has(sessionKey)) statuses.set(sessionKey, session.status); } this.touchSessionCardState(projectIdArg); return; } const finished = this.projectSessionIds(this.finishedSessionIdsByProject, projectIdArg); const errors = this.projectSessionIds(this.errorSessionIdsByProject, projectIdArg); const optimistic = this.projectSessionIds(this.optimisticWorkingIdsByProject, projectIdArg); const activeIds = new Set(); for (const session of sessionsArg) { if (session.archivedAt !== undefined || !isSessionRuntimeId(session.id)) continue; const sessionKey = controllerRuntimeIdToUiKey(session.id); activeIds.add(sessionKey); const previousStatus = statuses.get(sessionKey); statuses.set(sessionKey, session.status); if (session.status === 'busy' || session.status === 'retry') { errors.delete(sessionKey); finished.delete(sessionKey); optimistic.delete(sessionKey); } else if ( session.status === 'idle' && (previousStatus === 'busy' || previousStatus === 'retry') && !errors.has(sessionKey) ) { finished.add(sessionKey); } else if (session.status === 'error') { errors.add(sessionKey); finished.delete(sessionKey); optimistic.delete(sessionKey); } } for (const sessionId of [...statuses.keys()]) { if (!activeIds.has(sessionId)) statuses.delete(sessionId); } for (const sessionIds of [finished, errors, optimistic]) { for (const sessionId of [...sessionIds]) { if (!activeIds.has(sessionId)) sessionIds.delete(sessionId); } } this.touchSessionCardState(projectIdArg); } private beginLocalSessionTurn( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): ILocalSessionTurnMarker { const finished = this.projectSessionIds(this.finishedSessionIdsByProject, projectIdArg); const errors = this.projectSessionIds(this.errorSessionIdsByProject, projectIdArg); const optimistic = this.projectSessionIds(this.optimisticWorkingIdsByProject, projectIdArg); const sessionKey = controllerRuntimeIdToUiKey(sessionIdArg); const marker: ILocalSessionTurnMarker = { projectId: projectIdArg, sessionId: sessionIdArg, sessionKey, revision: 0, hadFinished: finished.has(sessionKey), hadError: errors.has(sessionKey), hadOptimisticWorking: optimistic.has(sessionKey), }; finished.delete(sessionKey); errors.delete(sessionKey); optimistic.add(sessionKey); this.bumpSessionEventRevision(projectIdArg); marker.revision = this.sessionEventRevisionByProject.get(projectIdArg) ?? 0; this.touchSessionCardState(projectIdArg); return marker; } private rollbackLocalSessionTurn(markerArg: ILocalSessionTurnMarker | undefined): void { if ( !markerArg || (this.sessionEventRevisionByProject.get(markerArg.projectId) ?? 0) !== markerArg.revision ) return; const finished = this.projectSessionIds(this.finishedSessionIdsByProject, markerArg.projectId); const errors = this.projectSessionIds(this.errorSessionIdsByProject, markerArg.projectId); const optimistic = this.projectSessionIds(this.optimisticWorkingIdsByProject, markerArg.projectId); if (markerArg.hadFinished) finished.add(markerArg.sessionKey); else finished.delete(markerArg.sessionKey); if (markerArg.hadError) errors.add(markerArg.sessionKey); else errors.delete(markerArg.sessionKey); if (markerArg.hadOptimisticWorking) optimistic.add(markerArg.sessionKey); else optimistic.delete(markerArg.sessionKey); this.bumpSessionEventRevision(markerArg.projectId); this.touchSessionCardState(markerArg.projectId); } private localTurnHasNoNewEvent(markerArg: ILocalSessionTurnMarker | undefined): boolean { return markerArg !== undefined && (this.sessionEventRevisionByProject.get(markerArg.projectId) ?? 0) === markerArg.revision; } private async runRefreshQueue(): Promise { try { while ( this.refreshPending && this.authenticated && this.socketClient.isConnected ) { this.refreshPending = false; const includeRelated = this.refreshRelatedPending; this.refreshRelatedPending = false; await this.performSessionRefresh(includeRelated); } } finally { this.refreshInFlight = undefined; } } private refreshSelectedSessionDetail(restartEnrichmentArg = true): Promise { this.detailRefreshPending = true; this.detailRefreshRestartEnrichment ||= restartEnrichmentArg; if (!this.detailRefreshInFlight) { const task = this.runDetailRefreshQueue(); this.detailRefreshInFlight = task; void task.finally(() => { if (this.detailRefreshInFlight !== task) return; this.detailRefreshInFlight = undefined; if ( this.detailRefreshPending && this.authenticated && this.socketClient.isConnected ) void this.refreshSelectedSessionDetail(this.detailRefreshRestartEnrichment); }); } return this.detailRefreshInFlight; } private async runDetailRefreshQueue(): Promise { while ( this.detailRefreshPending && this.authenticated && this.socketClient.isConnected ) { this.detailRefreshPending = false; const sessionId = this.selectedSessionId; const restartEnrichment = this.detailRefreshRestartEnrichment; this.detailRefreshRestartEnrichment = false; if (isSessionRuntimeId(sessionId)) await this.loadSessionDetail(sessionId, restartEnrichment); } } /** * Reads the tracked conversations of every project. The sidebar spans projects, so its rows * exist even while no project is selected; only the workspace below it is project-scoped. */ private async refreshTrackedConversations(): Promise { const requestId = ++this.sessionsRequestId; const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.conversation.list', {}, ); if ( requestId !== this.sessionsRequestId || !this.isCurrentConnection(generation) || !this.authenticated ) return; this.conversations = reconcileKeyedJsonArray( this.conversations, Array.isArray(response.conversations) ? response.conversations.filter(isTrackedConversation) : [], (conversationArg) => conversationUiKey( conversationArg.projectId, conversationArg.session.id, ), ); } catch { // The list is re-read on the next event or refresh; a failure keeps the last rows. } } private async performSessionRefresh(includeRelatedArg = true): Promise { const projectId = this.selectedProjectId; if (!isNonEmptyString(projectId)) { this.sessions = []; this.invalidateDetailLoad(); this.sessionDetail = undefined; this.sessionsLoadedForProjectId = ''; await this.refreshTrackedConversations(); if (includeRelatedArg) void this.loadSessionGroups(); return; } const requestId = ++this.sessionsRequestId; const generation = this.connectionGeneration; const requestEventRevision = this.sessionEventRevisionByProject.get(projectId) ?? 0; const selectedSessionAtStart = isSessionRuntimeId(this.selectedSessionId) ? this.selectedSessionId : undefined; const selectedSessionWasArchived = selectedSessionAtStart !== undefined && this.sessions.some((sessionArg) => ( isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, selectedSessionAtStart) && sessionArg.archivedAt !== undefined )); if (includeRelatedArg && selectedSessionAtStart) { void this.refreshSelectedSessionDetail(false); } if (includeRelatedArg) { void this.loadSessionGroups(); void this.loadTerminals(); void this.loadResources(); } // The loading state is for a list we cannot show yet; background refreshes // must not flip it, or the sidebar flashes on every pushed event. const initialLoad = this.sessionsLoadedForProjectId !== projectId; if (initialLoad) { this.sessions = []; this.sessionsLoading = true; } try { const response = await this.socketClient.fire( 'controller.conversation.list', {}, ); if ( requestId !== this.sessionsRequestId || !this.isCurrentConnection(generation) || !this.authenticated || this.selectedProjectId !== projectId ) { return; } const conversations = Array.isArray(response.conversations) ? response.conversations.filter(isTrackedConversation) : []; this.conversations = reconcileKeyedJsonArray( this.conversations, conversations, (conversationArg) => conversationUiKey( conversationArg.projectId, conversationArg.session.id, ), ); // The workspace still works inside one project at a time; the sidebar spans them. const nextSessions = conversations .filter((conversationArg) => conversationArg.projectId === projectId) .map(trackedConversationToSession); this.applySessionSnapshot(projectId, nextSessions, requestEventRevision); const reconciledSessions = reconcileKeyedJsonArray( this.sessions, nextSessions, (session) => controllerRuntimeIdToUiKey(session.id), ); if (reconciledSessions !== this.sessions) { this.sessions = reconciledSessions; const selectedSessionId = this.selectedSessionId; if (isSessionRuntimeId(selectedSessionId)) { this.reconcileSubtaskPreviews(projectId, selectedSessionId); } const modalSessionId = this.subagentModalSessionId; if ( isSessionRuntimeId(modalSessionId) && this.subagentModalAccess === 'managed' && !this.childSessionHasIndependentManagedMembership(projectId, modalSessionId) ) this.clearSubagentModalState(); this.requestCanonicalChatReconciliation(); } const selectedSessionBecameArchived = selectedSessionAtStart !== undefined && !selectedSessionWasArchived && this.sessions.some((sessionArg) => ( isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, selectedSessionAtStart) && sessionArg.archivedAt !== undefined )); if (selectedSessionBecameArchived) { this.applyArchivedSessionState(projectId, selectedSessionAtStart!); } // Composer overrides for sessions that no longer exist would otherwise // accumulate for the lifetime of the tab. const knownSessionIds = new Set( nextSessions .flatMap((sessionArg) => (isSessionRuntimeId(sessionArg.id) ? [this.sessionOperationKey(projectId, sessionArg.id)] : [])), ); for (const overrideKey of [...this.sessionModelOverrides.keys()]) { if (overrideKey.startsWith(`${projectId}\u0000`) && !knownSessionIds.has(overrideKey)) { this.sessionModelOverrides.delete(overrideKey); } } for (const overrideKey of [...this.sessionEffortOverrides.keys()]) { if (overrideKey.startsWith(`${projectId}\u0000`) && !knownSessionIds.has(overrideKey)) { this.sessionEffortOverrides.delete(overrideKey); } } for (const overrideKey of [...this.sessionAccountOverrides.keys()]) { if (overrideKey.startsWith(`${projectId}\u0000`) && !knownSessionIds.has(overrideKey)) { this.sessionAccountOverrides.delete(overrideKey); } } this.sessionsLoadedForProjectId = projectId; const selectedStillExists = this.sessions.some( (sessionArg) => isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, this.selectedSessionId), ); const standaloneResourceSelected = this.selectedSessionId === undefined && isNonEmptyString(this.selectedResourceId); if ( !selectedStillExists && !this.draftSessionActive && !selectedSessionBecameArchived && !standaloneResourceSelected ) { // Archived sessions never grab the selection implicitly, and an open // draft or standalone resource keeps its own view instead of jumping to an existing chat. const nextSessionId = this.sessions .filter(hasSessionRuntimeId) .find((sessionArg) => sessionArg.archivedAt === undefined)?.id; if (!controllerRuntimeIdsEqual(nextSessionId, this.selectedSessionId)) { this.cancelComposerFocus(); this.invalidateSlashCatalog(); } this.selectedSessionId = nextSessionId; } if (this.selectedSessionId) { void this.loadSlashCatalog(); if (!controllerRuntimeIdsEqual(this.selectedSessionId, selectedSessionAtStart)) { void this.refreshSelectedSessionDetail(); } } else { this.invalidateDetailLoad(); this.sessionDetail = undefined; } } catch (error) { if (requestId === this.sessionsRequestId && this.isCurrentConnection(generation)) { this.reportWorkspaceError('Load conversations', error); } } finally { if (requestId === this.sessionsRequestId) { this.sessionsLoading = false; } } } private reconcileControllerMessages( currentArg: readonly interfaces.IControllerMessage[], nextArg: readonly interfaces.IControllerMessage[], ): interfaces.IControllerMessage[] { return reconcileKeyedJsonArray( currentArg, nextArg, (message) => controllerRuntimeIdToUiKey(message.id), ); } private reconcileControllerMessageBundles( currentArg: readonly interfaces.IControllerMessageBundle[], nextArg: readonly interfaces.IControllerMessageBundle[], ): interfaces.IControllerMessageBundle[] { const currentByKey = new Map(currentArg.map((bundle) => [ controllerRuntimeIdToUiKey(bundle.sourceMessageId), bundle, ])); let unchanged = currentArg.length === nextArg.length; const bundles = nextArg.map((nextBundle, index) => { const current = currentByKey.get(controllerRuntimeIdToUiKey(nextBundle.sourceMessageId)); if (!current) { unchanged = false; return nextBundle; } const messages = this.reconcileControllerMessages(current.messages, nextBundle.messages); const bundle = current.structuralDigest === nextBundle.structuralDigest && messages === current.messages && controllerRuntimeIdsEqual(current.sourceMessageId, nextBundle.sourceMessageId) ? current : { ...nextBundle, messages }; unchanged &&= bundle === currentArg[index]; return bundle; }); return unchanged ? currentArg as interfaces.IControllerMessageBundle[] : bundles; } private reconcileSessionRenderDetail( currentArg: IControllerSessionRenderDetail | undefined, nextArg: IControllerSessionRenderDetail, ): IControllerSessionRenderDetail { if (!currentArg || !controllerRuntimeIdsEqual(currentArg.session.id, nextArg.session.id)) { return nextArg; } const reuse = (currentValueArg: TValue, nextValueArg: TValue): TValue => ( controllerJsonValuesEqual(currentValueArg, nextValueArg) ? currentValueArg : nextValueArg ); const messages = this.reconcileControllerMessages(currentArg.messages, nextArg.messages); const pendingPrompts = reconcileKeyedJsonArray( currentArg.pendingPrompts, nextArg.pendingPrompts, (prompt) => controllerRuntimeIdToUiKey(prompt.id), ); const permissions = reconcileKeyedJsonArray( currentArg.permissions, nextArg.permissions, (permission) => controllerRuntimeIdToUiKey(permission.id), ); const questions = reconcileKeyedJsonArray( currentArg.questions, nextArg.questions, (question) => controllerRuntimeIdToUiKey(question.id), ); const todos = reconcileKeyedJsonArray( currentArg.todos, nextArg.todos, (todo, index) => todo.id ? controllerRuntimeIdToUiKey(todo.id) : `index:${index}`, ); const intelligenceExchanges = reconcileKeyedJsonArray( currentArg.intelligenceExchanges, nextArg.intelligenceExchanges, (exchange) => exchange.id, ); const detail: IControllerSessionRenderDetail = { ...nextArg, session: reuse(currentArg.session, nextArg.session), messages, pendingPrompts, permissions, questions, todos, scratchpad: reuse(currentArg.scratchpad, nextArg.scratchpad), intelligenceExchanges, sessionMetrics: reuse(currentArg.sessionMetrics, nextArg.sessionMetrics), toolStreamCursor: reuse(currentArg.toolStreamCursor, nextArg.toolStreamCursor), messageStreamCursor: reuse(currentArg.messageStreamCursor, nextArg.messageStreamCursor), ...(nextArg.childAttention === undefined ? {} : { childAttention: reuse(currentArg.childAttention, nextArg.childAttention) }), ...(nextArg.modelChoice === undefined ? {} : { modelChoice: reuse(currentArg.modelChoice, nextArg.modelChoice) }), }; const current = currentArg as unknown as Record; const reconciled = detail as unknown as Record; const currentKeys = Object.keys(current); const nextKeys = Object.keys(reconciled); return currentKeys.length === nextKeys.length && nextKeys.every((key) => Object.is(current[key], reconciled[key])) ? currentArg : detail; } private async loadSessionDetail( sessionIdArg: TBrowserSessionId, restartEnrichmentArg = true, ): Promise { const projectId = this.selectedProjectId; if ( !isSessionRuntimeId(sessionIdArg) || !isNonEmptyString(projectId) || !this.authenticated || !this.socketClient.isConnected ) { return; } this.ensureSessionDraftActive(projectId, sessionIdArg); this.detailRequestAbortController?.abort(new Error('Session detail request superseded.')); const abortController = new AbortController(); this.detailRequestAbortController = abortController; const requestId = ++this.detailRequestId; const generation = this.connectionGeneration; // detailLoading disables the composer, so background refreshes of the // already-displayed session must not toggle it. if (!controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg)) { this.detailLoading = true; } try { const response = await this.socketClient.fire( 'controller.session.get', { projectId, sessionId: sessionIdArg }, { abortSignal: abortController.signal }, ); if ( requestId !== this.detailRequestId || !this.isCurrentConnection(generation) || !controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg) ) { return; } if (!response.session || !controllerRuntimeIdsEqual(response.session.id, sessionIdArg)) { throw new Error('The controller returned an invalid session.'); } const choiceKey = this.sessionOperationKey(projectId, sessionIdArg); if (!this.sessionModelSaveTokens.has(choiceKey)) { this.sessionModelOverrides.delete(choiceKey); this.sessionEffortOverrides.delete(choiceKey); this.sessionAccountOverrides.delete(choiceKey); } if (!response.toolStreamCursor) { throw new Error('The controller omitted the tool stream cursor.'); } if (!response.messageStreamCursor) { throw new Error('The controller omitted the message stream cursor.'); } // An outcome-unknown mode mutation synchronously starts this replacement detail request // after setting the local fence. loadSessionDetail aborts and increments detailRequestId // before its first await, so an older pre-mutation response cannot reach this accepted path. // The backend hides cached collaborationMode while its authority marker is pending or // unconfirmed; this exact/no-authority pair therefore proves a later native settings event. if ( this.codexModeUnconfirmedSessionKey === choiceKey && response.codexActivity?.collaborationMode !== undefined && response.codexActivity.collaborationModeAuthority === undefined ) { this.codexModeUnconfirmedSessionKey = ''; } const toolEpochAdvanced = response.toolStreamCursor.streamEpoch > (this.liveToolStreamEpochs.get(sessionIdArg.harnessId) ?? 0); const messageEpochAdvanced = response.messageStreamCursor.streamEpoch > (this.liveMessageStreamEpochs.get(sessionIdArg.harnessId) ?? 0); const bounded = boundedNewestMessageBundles(response.messagePage.bundles); const currentDetail = this.sessionDetail; const sameSession = controllerRuntimeIdsEqual(currentDetail?.session.id, sessionIdArg); const nextCoreBundleKeys = bounded.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), ); const coreBoundaryChanged = sameSession && !sameStringArray( [...this.detailCoreBundleKeys], nextCoreBundleKeys, ); const historyBecameTerminal = sameSession && this.detailHistoryCursor !== undefined && response.messagePage.nextCursor === undefined; const sessionBecameSettled = sameSession && (currentDetail!.session.status === 'busy' || currentDetail!.session.status === 'retry') && response.session.status !== 'busy' && response.session.status !== 'retry'; const restartEnrichment = restartEnrichmentArg || coreBoundaryChanged || historyBecameTerminal || sessionBecameSettled || toolEpochAdvanced || messageEpochAdvanced; const nextCoreBundleKeySet = new Set(nextCoreBundleKeys); const provisionalBundleKeys = sameSession && response.messagePage.nextCursor !== undefined && restartEnrichment ? new Set(this.detailBundles .map((bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId)) .filter((key) => !nextCoreBundleKeySet.has(key))) : new Set(); const retainedOlderBundles = sameSession ? this.detailBundles.filter((bundle) => { const key = controllerRuntimeIdToUiKey(bundle.sourceMessageId); if (nextCoreBundleKeySet.has(key)) return false; if (!restartEnrichment) return !this.detailCoreBundleKeys.has(key); if (response.messagePage.nextCursor !== undefined) { return provisionalBundleKeys.has(key); } return !this.detailCoreBundleKeys.has(key) && !this.detailProvisionalBundleKeys.has(key); }) : []; const accumulated = boundedNewestMessageBundles([ ...retainedOlderBundles, ...bounded.bundles, ]); if ( restartEnrichment && (bounded.limited || accumulated.limited) && provisionalBundleKeys.size > 0 ) { accumulated.bundles = accumulated.bundles.filter((bundle) => ( !provisionalBundleKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); provisionalBundleKeys.clear(); } accumulated.bundles = this.reconcileControllerMessageBundles( this.detailBundles, accumulated.bundles, ); const { messagePage, ...coreDetail } = response; const renderDetail: IControllerSessionRenderDetail = { ...coreDetail, messages: accumulated.bundles.flatMap((bundle) => bundle.messages), sessionMetrics: sameSession ? currentDetail!.sessionMetrics : {}, sessionIntelligenceEnabled: sameSession ? currentDetail!.sessionIntelligenceEnabled : true, sessionIntelligenceAvailabilityStatus: sameSession && !restartEnrichment ? currentDetail!.sessionIntelligenceAvailabilityStatus : 'checking', sessionIntelligenceUnavailableReason: sameSession && !restartEnrichment ? currentDetail!.sessionIntelligenceUnavailableReason : '', }; if (!this.reconcileLiveToolHydration( projectId, sessionIdArg, response.toolStreamCursor, renderDetail, )) { this.scheduleRefresh(); return; } if (!this.reconcileLiveMessageHydration( projectId, sessionIdArg, response.messageStreamCursor, renderDetail, )) { this.refreshSessionsNow(); return; } // Until the first turn is accepted, the materialized draft remains the // source of truth for its composer text and attachments. if ( this.draftSessionActive && !controllerRuntimeIdsEqual(this.draftSessionDetailReadyId, sessionIdArg) ) { return; } // Stable identity for unchanged transcripts keeps the chat from // re-rendering (and its composer from flickering) on no-op refreshes. this.detailBundles = accumulated.bundles; this.detailBundleKeys = new Set( accumulated.bundles.map((bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId)), ); this.detailCoreBundleKeys = new Set( nextCoreBundleKeys, ); const coreHistoryLimits: IDetailHistoryLimits = { truncated: response.messagePage.truncated === true, historyLimited: response.messagePage.historyLimited === true || bounded.limited || accumulated.limited, unavailable: false, }; if (restartEnrichment) { this.invalidateDetailEnrichment(); this.detailProvisionalBundleKeys = provisionalBundleKeys; this.detailHistoryLimits = coreHistoryLimits; this.detailHistoryCursor = bounded.limited || accumulated.limited ? undefined : response.messagePage.nextCursor; if (this.detailHistoryCursor !== undefined) { this.detailHistorySeenCursors.add(this.detailHistoryCursor); } this.detailHistoryStatus = this.detailHistoryCursor ? 'idle' : coreHistoryLimits.truncated || coreHistoryLimits.historyLimited ? 'partial' : 'complete'; } else { this.detailHistoryLimits = { truncated: this.detailHistoryLimits.truncated || coreHistoryLimits.truncated, historyLimited: this.detailHistoryLimits.historyLimited || coreHistoryLimits.historyLimited, unavailable: this.detailHistoryLimits.unavailable, }; } const reconciledDetail = this.reconcileSessionRenderDetail(this.sessionDetail, renderDetail); if (reconciledDetail !== this.sessionDetail) this.sessionDetail = reconciledDetail; for (const attention of reconciledDetail.childAttention ?? []) { this.applyPendingChildEvent(attention.scopeGeneration, attention.sequence); } this.ensureCanonicalChatProjection(reconciledDetail, projectId, sessionIdArg); this.reconcileSubtaskPreviews(projectId, sessionIdArg); // A draft that materialized keeps its placeholder view until this first // real detail arrives; now the actual chat takes over. this.draftSessionActive = false; this.draftSessionDetailReadyId = undefined; const ownership: IDetailLoadOwnership = !restartEnrichment && this.detailHistoryOwnership ? this.detailHistoryOwnership : { generation: this.detailEnrichmentGeneration, connectionGeneration: generation, projectId, sessionId: sessionIdArg, toolStreamEpoch: response.toolStreamCursor.streamEpoch, messageStreamEpoch: response.messageStreamCursor.streamEpoch, }; this.detailHistoryOwnership = ownership; if (restartEnrichment) void this.loadSessionAuxiliary(ownership); } catch (error) { if (abortController.signal.aborted) return; if (requestId === this.detailRequestId && this.isCurrentConnection(generation)) { this.reportWorkspaceError('Load the conversation', error); if (!controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg)) { this.cancelComposerFocusForSession(projectId, sessionIdArg); } } } finally { if (this.detailRequestAbortController === abortController) { this.detailRequestAbortController = undefined; } if (requestId === this.detailRequestId) { this.detailLoading = false; } } } private isCurrentDetailLoad(ownershipArg: IDetailLoadOwnership): boolean { return ownershipArg.generation === this.detailEnrichmentGeneration && this.isCurrentConnection(ownershipArg.connectionGeneration) && this.authenticated && this.socketClient.isConnected && this.selectedProjectId === ownershipArg.projectId && controllerRuntimeIdsEqual(this.selectedSessionId, ownershipArg.sessionId) && (this.liveToolStreamEpochs.get(ownershipArg.sessionId.harnessId) ?? 0) === ownershipArg.toolStreamEpoch && (this.liveMessageStreamEpochs.get(ownershipArg.sessionId.harnessId) ?? 0) === ownershipArg.messageStreamEpoch; } private invalidateDetailLoad(): void { this.detailRequestAbortController?.abort(new Error('Session detail invalidated.')); this.detailRequestAbortController = undefined; this.detailRequestId += 1; this.invalidateDetailEnrichment(); this.detailBundles = []; this.detailBundleKeys.clear(); this.detailCoreBundleKeys.clear(); this.detailProvisionalBundleKeys.clear(); this.detailHistoryCursor = undefined; this.detailHistorySeenCursors.clear(); this.detailHistoryOwnership = undefined; this.detailHistoryLimits = emptyDetailHistoryLimits(); this.detailHistoryStatus = 'idle'; this.canonicalChatProjection = undefined; this.canonicalChatProjectionDirty = false; this.canonicalTimelineRefreshPending = false; this.canonicalMessageRefreshIds.clear(); } private invalidateDetailEnrichment(): void { this.detailAuxiliaryAbortController?.abort(new Error('Session auxiliary request superseded.')); this.detailHistoryAbortController?.abort(new Error('Session history request superseded.')); this.detailAuxiliaryAbortController = undefined; this.detailHistoryAbortController = undefined; this.detailEnrichmentGeneration += 1; this.detailHistoryOwnership = undefined; this.detailHistoryCursor = undefined; this.detailHistorySeenCursors.clear(); if (this.detailHistoryStatus === 'backfilling') this.detailHistoryStatus = 'idle'; } private async loadSessionAuxiliary(ownershipArg: IDetailLoadOwnership): Promise { this.detailAuxiliaryAbortController?.abort(new Error('Session auxiliary request superseded.')); const abortController = new AbortController(); this.detailAuxiliaryAbortController = abortController; try { const auxiliary = await this.socketClient.fire( 'controller.session.auxiliary.get', { projectId: ownershipArg.projectId, sessionId: ownershipArg.sessionId }, { maxRetries: 0, abortSignal: abortController.signal }, ); if ( this.detailAuxiliaryAbortController !== abortController || !this.isCurrentDetailLoad(ownershipArg) ) return; const detail = this.sessionDetail; if (!detail || !controllerRuntimeIdsEqual(detail.session.id, ownershipArg.sessionId)) return; this.sessionDetail = { ...detail, sessionMetrics: auxiliary.sessionMetrics, sessionIntelligenceEnabled: auxiliary.sessionIntelligenceEnabled, sessionIntelligenceAvailabilityStatus: auxiliary.sessionIntelligenceAvailabilityStatus, sessionIntelligenceUnavailableReason: auxiliary.sessionIntelligenceUnavailableReason, }; } catch { if ( abortController.signal.aborted || this.detailAuxiliaryAbortController !== abortController || !this.isCurrentDetailLoad(ownershipArg) ) return; const detail = this.sessionDetail; if (!detail || !controllerRuntimeIdsEqual(detail.session.id, ownershipArg.sessionId)) return; this.sessionDetail = { ...detail, sessionIntelligenceEnabled: true, sessionIntelligenceAvailabilityStatus: 'unavailable', sessionIntelligenceUnavailableReason: 'Session Intelligence is temporarily unavailable.', }; } finally { if (this.detailAuxiliaryAbortController === abortController) { this.detailAuxiliaryAbortController = undefined; } } } private readonly handleLoadEarlier = (): void => { void this.loadEarlierSessionHistory(); }; private async loadEarlierSessionHistory(): Promise { const ownership = this.detailHistoryOwnership; const before = this.detailHistoryCursor; if ( !ownership || !before || this.detailHistoryAbortController || !this.isCurrentDetailLoad(ownership) ) return; const abortController = new AbortController(); this.detailHistoryAbortController = abortController; this.detailHistoryStatus = 'backfilling'; try { const page = await this.socketClient.fire( 'controller.session.messages.page', { projectId: ownership.projectId, sessionId: ownership.sessionId, limit: detailHistoryPageLimit, before, }, { maxRetries: 0, abortSignal: abortController.signal }, ); if ( this.detailHistoryAbortController !== abortController || this.detailHistoryOwnership !== ownership || this.detailHistoryCursor !== before || !this.isCurrentDetailLoad(ownership) ) return; const historyLimits: IDetailHistoryLimits = { truncated: this.detailHistoryLimits.truncated || page.truncated === true, historyLimited: this.detailHistoryLimits.historyLimited || page.historyLimited === true, unavailable: false, }; const nextBundles = [...this.detailBundles]; let provisionalContentChanged = false; for (const bundle of page.bundles) { const key = controllerRuntimeIdToUiKey(bundle.sourceMessageId); if (!this.detailProvisionalBundleKeys.delete(key)) continue; const retainedIndex = nextBundles.findIndex((candidate) => ( controllerRuntimeIdToUiKey(candidate.sourceMessageId) === key )); if (retainedIndex >= 0) { nextBundles[retainedIndex] = bundle; provisionalContentChanged = true; } } const unseen = page.bundles.filter((bundle) => ( !this.detailBundleKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); const previousKeys = nextBundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), ); const bounded = boundedNewestMessageBundles([...unseen, ...nextBundles]); const retainedKeys = new Set(bounded.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), )); const retainedUnseen = unseen.some((bundle) => ( retainedKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); let nextCursor = page.nextCursor; if ( bounded.limited || (unseen.length > 0 && !retainedUnseen) || (unseen.length === 0 && !provisionalContentChanged && nextCursor !== undefined) || (nextCursor !== undefined && this.detailHistorySeenCursors.has(nextCursor)) ) { historyLimits.historyLimited = true; nextCursor = undefined; } if (nextCursor === undefined && this.detailProvisionalBundleKeys.size > 0) { bounded.bundles = bounded.bundles.filter((bundle) => ( !this.detailProvisionalBundleKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); this.detailProvisionalBundleKeys.clear(); } const nextKeys = bounded.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), ); if (provisionalContentChanged || !sameStringArray(previousKeys, nextKeys)) { this.detailBundles = this.reconcileControllerMessageBundles( this.detailBundles, bounded.bundles, ); this.detailBundleKeys = new Set(nextKeys); const detail = this.sessionDetail; if (!detail || !controllerRuntimeIdsEqual(detail.session.id, ownership.sessionId)) return; const nextDetail = this.reconcileSessionRenderDetail(detail, { ...detail, messages: this.detailBundles.flatMap((bundle) => bundle.messages), }); if (nextDetail !== detail) this.sessionDetail = nextDetail; this.ensureCanonicalChatProjection(nextDetail, ownership.projectId, ownership.sessionId); } this.detailHistoryCursor = nextCursor; if (nextCursor !== undefined) this.detailHistorySeenCursors.add(nextCursor); this.detailHistoryLimits = historyLimits; this.detailHistoryStatus = nextCursor ? 'idle' : historyLimits.truncated || historyLimits.historyLimited ? 'partial' : 'complete'; await this.updateComplete; } catch { if ( !abortController.signal.aborted && this.detailHistoryAbortController === abortController && this.detailHistoryOwnership === ownership && this.detailHistoryCursor === before && this.isCurrentDetailLoad(ownership) ) { this.detailHistoryLimits = { ...this.detailHistoryLimits, unavailable: true }; this.detailHistoryStatus = 'partial'; } } finally { if (this.detailHistoryAbortController === abortController) { this.detailHistoryAbortController = undefined; if (this.detailHistoryStatus === 'backfilling') this.detailHistoryStatus = 'idle'; } } } private ensureSessionDraftActive(projectIdArg: string, sessionIdArg: TBrowserSessionId): void { const state = this.sessionDraftSync.state; if ( state?.projectId === projectIdArg && controllerRuntimeIdsEqual(state.sessionId, sessionIdArg) ) return; this.sessionDraftActivationGeneration += 1; void this.sessionDraftSync.activate(projectIdArg, sessionIdArg).catch((errorArg) => { if ( this.selectedProjectId === projectIdArg && controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg) ) this.reportWorkspaceError('Open the conversation draft', errorArg); }); } private reconcileMaterializedDraftComposer(sessionIdArg: TBrowserSessionId): void { const state = this.sessionDraftSync.state; if ( state?.projectId !== this.selectedProjectId || !controllerRuntimeIdsEqual(state.sessionId, sessionIdArg) ) return; this.localDraftText = state.text; this.localDraftAttachments = state.attachments.map((attachment) => ({ ...attachment })); } private readonly handleSessionSelect = (eventArg: CustomEvent): void => { const uiKey = eventArg.detail?.session?.id; if (!isNonEmptyString(uiKey)) { return; } const terminalId = runtimeIdsByUiKey( this.terminals.map((terminalArg) => terminalArg.id).filter(isTerminalRuntimeId), ).get(uiKey); if (terminalId) { this.openTerminalById(terminalId); return; } const conversation = this.trackedConversationByUiKey(uiKey); if (conversation) this.openTrackedConversation(conversation.projectId, conversation.sessionId); }; /** Resolves a sidebar row key to its tracked conversation, in whichever project it lives. */ private trackedConversationByUiKey( uiKeyArg: string, ): { projectId: string; sessionId: TBrowserSessionId } | undefined { const parsed = parseConversationUiKey(uiKeyArg); if (!parsed) return undefined; return this.conversations.some((conversationArg) => ( conversationArg.projectId === parsed.projectId && hasSessionRuntimeId(conversationArg.session) && controllerRuntimeIdsEqual(conversationArg.session.id, parsed.sessionId) )) ? parsed : undefined; } private readonly handleResourceSelect = ( eventArg: CustomEvent, ): void => { const resourceId = eventArg.detail?.resource?.id; const resource = this.resources.find((candidate) => candidate.id === resourceId); if (resource) this.selectResource(resource, eventArg.detail.targets ?? []); }; private selectResource( resource: interfaces.TControllerResource, targetsArg: readonly plugins.deesCatalog.IHarnessSessionListItemRef[] = [], ): void { if (!resource || resource.lifecycle !== 'active') return; if ( resource.kind === 'browser' && this.selectedResourceId === resource.id && ( this.browserViewLoading || ( this.browserViewResourceId === resource.id && this.browserViewAttachmentRevision === resource.attachment.revision && this.browserViewId !== '' ) ) ) return; this.beginWorkspaceSelection(); this.invalidateSlashCatalog(); this.browserViewReopenResourceId = ''; this.browserViewReopenAttempts = 0; this.browserViewReopenWindowStartedAt = 0; this.selectedResourceId = resource.id; const selectedConversationKey = this.selectedSessionId ? conversationUiKey(this.selectedProjectId, this.selectedSessionId) : ''; const preserveAttachedSession = selectedConversationKey !== '' && targetsArg.some( (targetArg) => targetArg.kind === 'session' && targetArg.id === selectedConversationKey, ); if (!preserveAttachedSession) this.deselectConversationForStandaloneResource(); if (resource.kind === 'browser') { this.closeTerminalView(); if (resource.browserRuntimeState !== 'available') { this.reportWorkspaceError('Open the browser', 'BrowserRuntime is unavailable on this host.'); return; } void this.openBrowserView(resource); return; } void this.closeBrowserView(); if (resource.processState !== 'running') { this.closeTerminalView(); return; } this.openTerminalById( { harnessId: 'controller', nativeId: resource.id }, preserveAttachedSession, ); } private deselectConversationForStandaloneResource(): void { this.cancelComposerFocus(); this.sessionDraftSync.deactivate(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.draftSessionActive = false; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; this.detailLoading = false; this.clearAnsweredCardCaches(); this.clearSubagentModalState(); void this.destroyCodexDiffModal(); } private async openBrowserView(resourceArg: interfaces.IControllerBrowserResource): Promise { const generation = ++this.browserViewGeneration; this.browserViewReopenResourceId = ''; const connectionGeneration = this.connectionGeneration; this.browserViewOpening = true; try { await this.openBrowserViewInternal(resourceArg, generation, connectionGeneration); } finally { if (generation === this.browserViewGeneration) this.browserViewOpening = false; } } private async openBrowserViewInternal( resourceArg: interfaces.IControllerBrowserResource, generation: number, connectionGeneration: number, ): Promise { await this.closeBrowserView(false); if ( this.browserRenderer !== undefined || generation !== this.browserViewGeneration || connectionGeneration !== this.connectionGeneration || this.selectedResourceId !== resourceArg.id ) return; const openAbortController = new AbortController(); this.browserViewOpenAbortController = openAbortController; this.browserViewLoading = true; let openedViewId = ''; let openedStreamGeneration = -1; try { const response = await this.socketClient.fire( 'controller.browser.view.open', { projectId: this.selectedProjectId, resourceId: resourceArg.id, expectedAttachmentRevision: resourceArg.attachment.revision, }, { maxRetries: 0, abortSignal: openAbortController.signal }, ); openedViewId = response.viewId; openedStreamGeneration = response.streamGeneration; if ( generation !== this.browserViewGeneration || connectionGeneration !== this.connectionGeneration || this.selectedResourceId !== resourceArg.id || response.resourceId !== resourceArg.id || response.attachmentRevision !== resourceArg.attachment.revision ) { void this.requestBrowserViewClose( response.viewId, response.streamGeneration, connectionGeneration, ); return; } if ( this.browserViewPendingClose?.connectionGeneration === connectionGeneration && this.browserViewPendingClose.viewId !== response.viewId ) this.browserViewPendingClose = undefined; this.browserViewId = response.viewId; this.browserViewConnectionGeneration = connectionGeneration; this.browserViewStreamGeneration = response.streamGeneration; this.browserViewActivated = false; this.browserViewResourceId = response.resourceId; this.browserViewAttachmentRevision = response.attachmentRevision; this.applyBrowserViewState(response.state); const transport = this.createBrowserViewTransportClient( response.streamGeneration, response.events, response.operations, generation, ); this.browserViewTransport = transport; const transportStart = transport.start(); this.browserViewLoading = false; this.requestUpdate(); await Promise.all([this.updateComplete, transportStart]); const rendererReady = await this.mountBrowserRenderer(generation); if ( !rendererReady || generation !== this.browserViewGeneration || connectionGeneration !== this.connectionGeneration || response.viewId !== this.browserViewId || response.streamGeneration !== this.browserViewStreamGeneration ) return; await this.socketClient.fire( 'controller.browser.view.activate', { viewId: response.viewId, streamGeneration: response.streamGeneration }, { maxRetries: 0, abortSignal: openAbortController.signal }, ); if ( generation === this.browserViewGeneration && response.viewId === this.browserViewId && response.streamGeneration === this.browserViewStreamGeneration ) this.browserViewActivated = true; } catch (errorArg) { if (openedViewId && openedViewId === this.browserViewId) { await this.closeBrowserView(false); } else if (openedViewId && this.socketClient.isConnected) { await this.requestBrowserViewClose( openedViewId, openedStreamGeneration, connectionGeneration, ); } if ( generation === this.browserViewGeneration && connectionGeneration === this.connectionGeneration && this.selectedResourceId === resourceArg.id ) { this.browserViewLoading = false; this.reportWorkspaceError('Open the browser view', errorArg); } } finally { if (this.browserViewOpenAbortController === openAbortController) { this.browserViewOpenAbortController = undefined; } } } private async closeBrowserView(incrementGenerationArg = true): Promise { this.closeBrowserDevTools(); this.browserWebsiteErrors = {}; if (incrementGenerationArg) { this.browserViewGeneration += 1; // A bumped generation orphans any open still in flight, and that open // will not clear the flag it no longer owns. The internal close an open // performs on the view it replaces does not bump, so it is unaffected. this.browserViewOpening = false; } this.browserViewOpenAbortController?.abort(new Error('Browser view changed.')); this.browserViewOpenAbortController = undefined; this.browserNavigationAbortController?.abort(new Error('Browser view changed.')); this.browserNavigationAbortController = undefined; this.browserViewRecoveryAbortController?.abort(new Error('Browser view changed.')); this.browserViewRecoveryAbortController = undefined; const renderer = this.browserRenderer; const transport = this.browserViewTransport; const viewId = this.browserViewId; const viewStreamGeneration = this.browserViewStreamGeneration; const viewConnectionGeneration = this.browserViewConnectionGeneration; this.browserViewId = ''; this.browserViewConnectionGeneration = -1; this.browserViewStreamGeneration = -1; this.browserViewActivated = false; this.browserViewResourceId = ''; this.browserViewAttachmentRevision = -1; this.browserViewState = undefined; this.browserVideoFailed = false; this.browserDialogs.clear(); this.browserAddressDraft = ''; this.browserAddressEditing = false; this.browserViewLoading = false; this.stopBrowserStatistics(); this.browserViewListeners.clear(); // The renderer owns a lease-bound signaling adapter, so it can close its // video peer after the UI identity is retired. Keep that transport alive // until renderer cleanup settles or its bounded deadline forces lease closure. if (renderer) await this.stopBrowserRenderer(renderer); const remoteCloseTask = ( viewId && viewStreamGeneration > 0 && viewConnectionGeneration === this.connectionGeneration ) ? this.requestBrowserViewClose(viewId, viewStreamGeneration, viewConnectionGeneration) : this.flushPendingBrowserViewClose(); const transportStopTask = transport ? transport.stop(new Error('Browser view changed.')).then(() => { if (this.browserViewTransport === transport) this.browserViewTransport = undefined; }) : Promise.resolve(); await Promise.all([remoteCloseTask, transportStopTask]); } private async stopBrowserRenderer(rendererArg: plugins.LiveBrowserVideoRenderer): Promise { let stopTask: Promise; try { stopTask = rendererArg.stop(); } catch (errorArg) { if (this.browserRenderer === rendererArg) { this.reportWorkspaceError( 'Stop the browser renderer', errorArg, `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`, ); } return; } void stopTask.then(() => { if (this.browserRenderer === rendererArg) this.browserRenderer = undefined; }, () => undefined); try { await waitForBrowserRendererLifecycle(stopTask, 'stop'); } catch (errorArg) { if (this.browserRenderer === rendererArg) { this.reportWorkspaceError( 'Stop the browser renderer', errorArg, `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`, ); } } } private async requestBrowserViewClose( viewIdArg: string, streamGenerationArg: number, connectionGenerationArg: number, ): Promise { if (connectionGenerationArg !== this.connectionGeneration) return; const pendingClose = { viewId: viewIdArg, streamGeneration: streamGenerationArg, connectionGeneration: connectionGenerationArg, attempts: 0, }; this.browserViewPendingClose = pendingClose; await this.flushPendingBrowserViewClose(); } private async flushPendingBrowserViewClose(): Promise { const pendingClose = this.browserViewPendingClose; if ( !pendingClose || pendingClose.connectionGeneration !== this.connectionGeneration || !this.socketClient.isConnected ) return; if (this.browserViewCloseTask) return this.browserViewCloseTask; pendingClose.attempts += 1; const closeTask = this.socketClient.fire( 'controller.browser.view.close', { viewId: pendingClose.viewId, expectedStreamGeneration: pendingClose.streamGeneration, }, { maxRetries: 0 }, ).then(() => { if (this.browserViewPendingClose === pendingClose) { this.browserViewPendingClose = undefined; } }).catch(() => { if ( this.browserViewPendingClose === pendingClose && pendingClose.attempts >= 3 ) { this.browserViewPendingClose = undefined; this.reportWorkspaceError( 'Close the browser view', 'The browser view close request failed after three attempts.', ); return; } if ( this.browserViewPendingClose !== pendingClose || pendingClose.connectionGeneration !== this.connectionGeneration ) return; if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer); this.browserViewCloseRetryTimer = setTimeout(() => { this.browserViewCloseRetryTimer = undefined; void this.flushPendingBrowserViewClose(); }, 250 * pendingClose.attempts); }).finally(() => { if (this.browserViewCloseTask === closeTask) this.browserViewCloseTask = undefined; if ( this.browserViewPendingClose && this.browserViewPendingClose !== pendingClose ) void this.flushPendingBrowserViewClose(); }); this.browserViewCloseTask = closeTask; return closeTask; } private createBrowserViewTransportClient( streamGenerationArg: number, eventsArg: plugins.typedrequestInterfaces.TVirtualStream<'receive'>, operationsArg: plugins.typedrequestInterfaces.TVirtualStream<'send'>, viewGenerationArg: number, ): ControllerBrowserViewTransportClient { return new ControllerBrowserViewTransportClient({ streamGeneration: streamGenerationArg, events: eventsArg, operations: operationsArg, onEvent: (eventArg) => { if ( viewGenerationArg !== this.browserViewGeneration || streamGenerationArg !== this.browserViewStreamGeneration ) return; this.handleBrowserViewEvent(eventArg); }, onFailure: (errorArg, recoverableArg) => { this.handleBrowserTransportFailure( viewGenerationArg, streamGenerationArg, errorArg, recoverableArg, ); }, }); } private handleBrowserViewEvent(eventArg: interfaces.TControllerBrowserViewEvent): void { if ( eventArg.type === 'error' && eventArg.error.fatal && interfaces.isControllerBrowserViewCloseCode(eventArg.error.code) ) { // These are AGL lifecycle announcements, handled by onFailure after // onEvent returns. The canvas renderer treats every error it receives // as a browser fault, including an otherwise intentional view close. return; } if (eventArg.type === 'state') this.applyBrowserViewState(eventArg.state); if (eventArg.type === 'error' && eventArg.error.fatal) { this.reportWorkspaceError('Run the browser view', eventArg.error.message); } for (const listener of this.browserViewListeners) listener(eventArg); } private applyBrowserViewState(stateArg: interfaces.IControllerBrowserViewState): boolean { if (this.browserViewState && stateArg.revision <= this.browserViewState.revision) return false; if (this.browserViewState?.activeTabId !== stateArg.activeTabId) this.closeBrowserDevTools(); this.browserWebsiteErrors = Object.fromEntries(Object.entries(this.browserWebsiteErrors) .filter(([id]) => stateArg.tabs.some(tab => tab.id === id))); this.browserViewState = stateArg; const transport = this.browserViewTransport; this.browserDialogs.update(this.browserViewId, stateArg, async input => { if (!transport || this.browserViewTransport !== transport || !this.browserViewActivated) { throw new Error('The browser view is unavailable.'); } await transport.operate({ type: 'respondToDialog', input }); }); if (!this.browserAddressEditing) { this.browserAddressDraft = stateArg.tabs.find((tabArg) => tabArg.active)?.url ?? ''; } return true; } /** * Spends one reopen attempt. The reopen cycle drives itself, so without a * budget a resource that revokes every view it hands out keeps the client * closing and reopening at network speed with nothing shown to the user. * Attempts more than a window apart cost nothing. */ private noteBrowserViewReopenAttempt(): boolean { const now = Date.now(); if (now - this.browserViewReopenWindowStartedAt > interfaces.controllerBrowserViewReopenWindowMs) { this.browserViewReopenWindowStartedAt = now; this.browserViewReopenAttempts = 0; } if (this.browserViewReopenAttempts >= interfaces.controllerBrowserViewMaximumReopenAttempts) return false; this.browserViewReopenAttempts += 1; return true; } private handleBrowserTransportFailure( viewGenerationArg: number, streamGenerationArg: number, errorArg: unknown, recoverableArg: boolean, ): void { if ( viewGenerationArg !== this.browserViewGeneration || streamGenerationArg !== this.browserViewStreamGeneration || !this.browserViewId ) return; this.closeBrowserDevTools(); if (errorArg instanceof ControllerBrowserOperationError) { // The controller announced a close it made itself, so this is not a // transport fault and spends no recovery attempt. `resource_changed` // closes quietly and reopens from the refreshed resource list while the // resource stays active and selected; `view_closed` is intentional and // stays closed. Anything else falls through and is reported. if (errorArg.code === interfaces.controllerBrowserViewResourceChangedErrorCode) { const reopening = this.noteBrowserViewReopenAttempt(); this.browserViewReopenResourceId = reopening ? this.browserViewResourceId : ''; if (!reopening) { this.reportWorkspaceError( 'Keep the browser view open', errorArg, `The browser view kept closing: ${errorMessage(errorArg)}`, ); } void this.closeBrowserView(); void this.loadResources(true); return; } if (errorArg.code === interfaces.controllerBrowserViewClosedErrorCode) { this.browserViewReopenResourceId = ''; void this.closeBrowserView(); void this.loadResources(true); return; } } if (!recoverableArg || !this.browserViewActivated || !this.browserRenderer) { this.reportWorkspaceError( 'Stream the browser view', errorArg, `Browser transport failed: ${errorMessage(errorArg)}`, ); void this.closeBrowserView(); return; } if (this.browserViewTransportRecoveryTask) return; const recovery = this.recoverBrowserViewTransport( viewGenerationArg, streamGenerationArg, errorArg, ).finally(() => { if (this.browserViewTransportRecoveryTask === recovery) { this.browserViewTransportRecoveryTask = undefined; } }); this.browserViewTransportRecoveryTask = recovery; void recovery.catch(() => undefined); } private async recoverBrowserViewTransport( viewGenerationArg: number, streamGenerationArg: number, reasonArg: unknown, ): Promise { const viewId = this.browserViewId; const connectionGeneration = this.connectionGeneration; const oldTransport = this.browserViewTransport; const renderer = this.browserRenderer; if (!viewId || !oldTransport || !renderer) return; const recoveryAbortController = new AbortController(); this.browserViewRecoveryAbortController = recoveryAbortController; this.browserViewActivated = false; try { await waitForBrowserRendererLifecycle(renderer.suspend(), 'suspension'); await oldTransport.stop(reasonArg); const response = await this.socketClient.fire( 'controller.browser.view.recover', { viewId, expectedStreamGeneration: streamGenerationArg }, { maxRetries: 0, abortSignal: recoveryAbortController.signal }, ); if ( viewGenerationArg !== this.browserViewGeneration || connectionGeneration !== this.connectionGeneration || viewId !== this.browserViewId || streamGenerationArg !== this.browserViewStreamGeneration || response.viewId !== viewId || response.resourceId !== this.browserViewResourceId || response.attachmentRevision !== this.browserViewAttachmentRevision || response.streamGeneration !== streamGenerationArg + 1 ) throw new Error('The recovered browser stream generation is stale.'); const transport = this.createBrowserViewTransportClient( response.streamGeneration, response.events, response.operations, viewGenerationArg, ); this.browserViewTransport = transport; this.browserViewStreamGeneration = response.streamGeneration; this.applyBrowserViewState(response.state); await transport.start(); await waitForBrowserRendererLifecycle(renderer.resume(), 'resume'); await this.socketClient.fire( 'controller.browser.view.activate', { viewId, streamGeneration: response.streamGeneration }, { maxRetries: 0, abortSignal: recoveryAbortController.signal }, ); if ( viewGenerationArg === this.browserViewGeneration && viewId === this.browserViewId && response.streamGeneration === this.browserViewStreamGeneration ) { this.browserViewActivated = true; } } catch (errorArg) { if ( viewGenerationArg === this.browserViewGeneration && connectionGeneration === this.connectionGeneration && viewId === this.browserViewId ) { this.reportWorkspaceError( 'Recover the browser transport', errorArg, `Browser transport recovery failed: ${errorMessage(errorArg)}`, ); await this.closeBrowserView(); } } finally { if (this.browserViewRecoveryAbortController === recoveryAbortController) { this.browserViewRecoveryAbortController = undefined; } } } private async browserViewOperation( operationArg: interfaces.TControllerBrowserViewOperation, publishStateArg = true, abortSignalArg?: AbortSignal, ): Promise { const viewId = this.browserViewId; const transport = this.browserViewTransport; if (!viewId || !transport) throw new Error('The browser view is unavailable.'); const result = await transport.operate(operationArg, abortSignalArg); if ( result.status === 'succeeded' && result.state && viewId === this.browserViewId ) { if (!publishStateArg) return result.state; this.applyBrowserViewState(result.state); } if (!this.browserViewState) throw new Error('The browser view state is unavailable.'); return this.browserViewState; } private async navigateBrowserAddress(formArg: HTMLFormElement): Promise { const requestId = ++this.browserNavigationRequestId; const viewGeneration = this.browserViewGeneration; const viewId = this.browserViewId; this.browserNavigationAbortController?.abort(new Error('A newer navigation was requested.')); const navigationAbortController = new AbortController(); this.browserNavigationAbortController = navigationAbortController; const input = formArg.querySelector('input'); try { const url = controllerNormalizeBrowserAddress(input?.value ?? ''); this.browserAddressDraft = input?.value ?? ''; const state = await this.browserViewOperation( { type: 'navigate', url }, false, navigationAbortController.signal, ); if ( requestId === this.browserNavigationRequestId && viewGeneration === this.browserViewGeneration && viewId === this.browserViewId ) this.applyBrowserViewState(state); } catch (errorArg) { if ( requestId === this.browserNavigationRequestId && viewGeneration === this.browserViewGeneration && viewId === this.browserViewId ) this.reportWorkspaceError('Navigate the browser', errorArg); } finally { if (this.browserNavigationAbortController === navigationAbortController) { this.browserNavigationAbortController = undefined; } } } private readonly handleBrowserAddressNavigation = (eventArg: Event): void => { if (eventArg.type === 'submit') eventArg.preventDefault(); const currentTarget = eventArg.currentTarget; const form = currentTarget instanceof HTMLFormElement ? currentTarget : currentTarget instanceof HTMLElement ? currentTarget.closest('form') : null; if (form) void this.navigateBrowserAddress(form); }; private readonly handleBrowserAddressFocus = (eventArg: Event): void => { const input = eventArg.currentTarget; if (!(input instanceof HTMLInputElement)) return; this.browserAddressEditing = true; this.browserAddressDraft = input.value; }; private readonly handleBrowserAddressInput = (eventArg: Event): void => { const input = eventArg.currentTarget; if (input instanceof HTMLInputElement) this.browserAddressDraft = input.value; }; private readonly handleBrowserAddressBlur = (): void => { this.browserAddressEditing = false; this.requestUpdate(); }; private browserVideoClient(): plugins.ILiveBrowserVideoClient { const viewId = this.browserViewId; let ownedTransport = this.browserViewTransport; const getTransport = (closing = false) => { if (this.browserViewId === viewId && this.browserViewTransport) ownedTransport = this.browserViewTransport; else if (!closing) throw new Error('The browser view is unavailable.'); if (!ownedTransport) throw new Error('The browser signaling transport is unavailable.'); return ownedTransport; }; return { getState: () => { if (this.browserViewId !== viewId || !this.browserViewState) throw new Error('The browser view state is unavailable.'); return this.browserViewState; }, onEvent: (listenerArg) => { const listener: Parameters[0] = (event) => { if (this.browserViewId === viewId) listenerArg(event); }; this.browserViewListeners.add(listener); return () => this.browserViewListeners.delete(listener); }, openVideoPeer: async (options) => { const transport = getTransport(); const result = await transport.operate({ type: 'openVideoPeer' }, options.signal); if (result.status !== 'succeeded' || !result.videoOffer) throw new Error('The browser did not provide a video offer.'); return result.videoOffer; }, answerVideoPeer: async (negotiationId, description, options) => { const result = await getTransport().operate({ type: 'answerVideoPeer', negotiationId, description }, options.signal); if (result.status !== 'succeeded') throw new Error('The video answer was not accepted.'); }, closeVideoPeer: async (options) => { const result = await getTransport(true).operate({ type: 'closeVideoPeer' }, options.signal); if (result.status !== 'succeeded') throw new Error('The video peer was not closed.'); }, getVideoStatistics: async (options) => { const transport = getTransport(); const result = await transport.operate({ type: 'getVideoStatistics' }, options.signal); if (result.status !== 'succeeded' || !result.videoStatistics) throw new Error('The browser did not provide video statistics.'); return result.videoStatistics; }, setViewport: async (viewport, options) => { const transport = getTransport(); const result = await transport.operate({ type: 'setViewport', viewport }, options.signal); if (result.status !== 'succeeded' || !result.viewportResult) { throw new Error('The browser did not confirm its shared viewport.'); } return result.viewportResult; }, dispatchMouse: async (input, options) => { const result = await getTransport().operate({ type: 'dispatchMouse', input }, options.signal); if (result.status !== 'succeeded' && !(input.type === 'move' && result.status === 'superseded')) throw new Error('Browser input was not accepted.'); }, dispatchWheel: async (input, options) => { const result = await getTransport().operate({ type: 'dispatchWheel', input }, options.signal); if (result.status !== 'succeeded') throw new Error('Browser input was not accepted.'); }, dispatchKey: async (input, options) => { const result = await getTransport().operate({ type: 'dispatchKey', input }, options.signal); if (result.status !== 'succeeded') throw new Error('Browser input was not accepted.'); }, insertText: async (input, options) => { const result = await getTransport().operate({ type: 'insertText', input }, options.signal); if (result.status !== 'succeeded') throw new Error('Browser input was not accepted.'); }, }; } private closeBrowserDevTools(): void { this.browserDevToolsOpenAbort?.abort(new Error('The inspected view changed.')); this.browserDevToolsOpenAbort = undefined; const inspector = this.browserDevTools; this.browserDevTools = undefined; this.browserDevToolsVisible = false; this.browserDevToolsError = ''; this.browserDevToolsFailedId = undefined; if (!inspector) return; inspector.client.stop(); void this.requestBrowserDevToolsClose(inspector.response, inspector.connectionGeneration); } private async requestBrowserDevToolsClose(response: interfaces.IReq_ControllerBrowserDevToolsOpen['response'], connectionGeneration: number, reportFailure = false): Promise { for (const stream of [response.commands, response.events]) { void stream.abort(new Error('DevTools closed.')).catch(() => undefined); } if (connectionGeneration !== this.connectionGeneration) return; try { const result = await this.socketClient.fire('controller.browser.devtools.close', { viewId: response.viewId, devToolsId: response.devToolsId, streamGeneration: response.streamGeneration, }, { maxRetries: 0 }); if (reportFailure && result.reason && connectionGeneration === this.connectionGeneration && this.browserViewId === response.viewId && this.browserDevToolsVisible && this.browserDevToolsFailedId === response.devToolsId && !this.browserDevTools) { this.browserDevToolsError = result.reason; } } catch (error) { if (connectionGeneration === this.connectionGeneration && this.browserViewId === response.viewId && this.browserDevToolsVisible && this.browserDevToolsFailedId === response.devToolsId && !this.browserDevTools) { this.browserDevToolsError = `Inspector cleanup failed: ${errorMessage(error)}`; } } } private async openBrowserDevTools(panel: plugins.TDevToolsPanel = 'elements'): Promise { if (this.browserDevTools) { this.browserDevTools.client.showPanel(panel); return; } if (this.browserDevToolsOpenAbort || !this.browserViewActivated) return; const viewId = this.browserViewId; const tabId = this.browserViewState?.activeTabId; if (!viewId || !tabId) return; const streamGeneration = this.browserViewStreamGeneration; const connectionGeneration = this.connectionGeneration; const controller = new AbortController(); this.browserDevToolsOpenAbort = controller; this.browserDevToolsVisible = true; this.browserDevToolsError = ''; this.browserDevToolsFailedId = undefined; let response: interfaces.IReq_ControllerBrowserDevToolsOpen['response'] | undefined; try { await this.updateComplete; const iframe = this.shadowRoot?.querySelector('.browserDevTools iframe'); if (!iframe) throw new Error('The inspector pane is unavailable.'); response = await this.socketClient.fire( 'controller.browser.devtools.open', { viewId, tabId, streamGeneration }, { maxRetries: 0, abortSignal: controller.signal }); if (controller.signal.aborted || this.browserDevToolsOpenAbort !== controller || this.browserViewId !== viewId || this.browserViewStreamGeneration !== streamGeneration || this.connectionGeneration !== connectionGeneration || this.browserViewState?.activeTabId !== tabId) { await this.requestBrowserDevToolsClose(response, connectionGeneration); return; } const client = new BrowserDevToolsClient(response, iframe, reason => { if (this.browserDevTools?.client !== client) return; this.browserDevTools = undefined; this.browserDevToolsError = reason; this.browserDevToolsFailedId = response!.devToolsId; void this.requestBrowserDevToolsClose(response!, connectionGeneration, true); }); this.browserDevTools = { client, response, connectionGeneration }; await client.start(panel); } catch (error) { if (response) await this.requestBrowserDevToolsClose(response, connectionGeneration); if (this.browserDevToolsOpenAbort === controller && !controller.signal.aborted) { this.browserDevTools?.client.stop(); this.browserDevTools = undefined; this.browserDevToolsError = errorMessage(error); } } finally { if (this.browserDevToolsOpenAbort === controller) this.browserDevToolsOpenAbort = undefined; } } private async mountBrowserRenderer(generationArg: number): Promise { if (generationArg !== this.browserViewGeneration || !this.browserViewId) return false; const surface = this.shadowRoot?.querySelector('.browserVideoSurface'); const video = surface?.querySelector('video'); if (!surface || !video) throw new Error('The browser renderer surface is unavailable.'); const renderer = new plugins.LiveBrowserVideoRenderer({ video, client: this.browserVideoClient(), focusTarget: surface, resizeTarget: surface, onError: (errorArg) => { if (generationArg !== this.browserViewGeneration) return; if (errorArg.code === 'remote_page_error') { const tabId = errorArg.tabId ?? this.browserViewState?.activeTabId; if (tabId && this.browserViewState?.tabs.some(tab => tab.id === tabId)) { this.browserWebsiteErrors = { ...this.browserWebsiteErrors, [tabId]: errorArg.message.slice(0, 512) }; } return; } this.reportWorkspaceError('Run the browser video', errorArg.message); if (errorArg.code === 'video_negotiation_failed' || errorArg.code === 'video_connection_failed') { this.browserVideoFailed = true; } }, }); this.browserRenderer = renderer; await this.startBrowserRenderer(renderer); const mounted = generationArg === this.browserViewGeneration && this.browserRenderer === renderer; if (mounted) this.startBrowserStatistics(renderer); return mounted; } /** * Samples the mounted renderer once per period for the view readout. The * state changes only when a sampled value differs, so an idle view causes * no application re-render; the timer retires itself once the renderer is * replaced. */ private startBrowserStatistics(rendererArg: plugins.LiveBrowserVideoRenderer): void { this.stopBrowserStatistics(); this.browserStatisticsSample = sampleBrowserViewStatistics( rendererArg.getStatistics(), undefined, performance.now(), ).sample; this.browserStatisticsTimer = setInterval(() => { if (this.browserRenderer !== rendererArg) { this.stopBrowserStatistics(); return; } const result = sampleBrowserViewStatistics( rendererArg.getStatistics(), this.browserStatisticsSample, performance.now(), ); this.browserStatisticsSample = result.sample; if (browserViewStatisticsEqual(this.browserViewStatistics, result.statistics)) return; this.browserViewStatistics = result.statistics; }, browserStatisticsIntervalMs); } private stopBrowserStatistics(): void { if (this.browserStatisticsTimer !== undefined) { clearInterval(this.browserStatisticsTimer); this.browserStatisticsTimer = undefined; } this.browserStatisticsSample = undefined; if (this.browserViewStatistics !== undefined) this.browserViewStatistics = undefined; } private startBrowserRenderer(rendererArg: plugins.LiveBrowserVideoRenderer): Promise { return waitForBrowserRendererLifecycle(rendererArg.start(), 'start'); } /** The conversations a resource is attached to, as sidebar item refs. */ private resourceAssociationTargets( resourceArg: interfaces.TControllerResource, ): plugins.deesCatalog.IHarnessSessionListItemRef[] { return resourceArg.attachment.entries .filter((entry) => entry.kind === 'session') .map((entry) => ({ kind: 'session' as const, id: conversationUiKey( entry.projectId, entry.id as interfaces.TControllerSessionId, ), })); } /** * Resolves a sidebar conversation ref back to the qualified session id the server needs. A * destination must be a conversation the sidebar currently offers. */ private associationTargetSessionId( targetArg: plugins.deesCatalog.IHarnessSessionListItemRef, ): TBrowserSessionId | undefined { if (targetArg.kind !== 'session') return undefined; return this.projectConversationIdFromUiKey(targetArg.id); } /** * Resolves the membership a Detach or Move acts on from the resource's own attachment set. * The conversation may be archived in AGL and therefore absent from the list; its attachment * exists regardless, and refusing to act on it would strand the resource. */ private attachedAssociationSessionId( resourceArg: interfaces.TControllerResource, targetArg: plugins.deesCatalog.IHarnessSessionListItemRef, ): TBrowserSessionId | undefined { if (targetArg.kind !== 'session') return undefined; const parsed = parseConversationUiKey(targetArg.id); if (!parsed) return undefined; const entry = resourceArg.attachment.entries.find((candidateArg) => ( candidateArg.kind === 'session' && candidateArg.projectId === parsed.projectId && controllerRuntimeIdsEqual( candidateArg.id as interfaces.TControllerSessionId, parsed.sessionId, ) )); return entry === undefined ? undefined : parsed.sessionId; } /** * The list validates the request and names the mode, so this only has to translate it. `move` * and `detach` always carry the single membership they act on, which is why a resource attached * to several conversations is never guessed at: the host disambiguates through the picker * before the request is made. */ private readonly handleResourceAssociate = ( eventArg: CustomEvent, ): void => { const detail = eventArg.detail; const resource = this.resources.find((candidate) => candidate.id === detail?.resource?.id); if (!detail || !resource || this.mutationPending) return; if (detail.mode === 'detach') { // Resolved from the attachment set, not from the list: a conversation that is archived // in AGL is not a row any more, and its attachment must still be removable. const from = this.attachedAssociationSessionId(resource, detail.fromTarget); if (!from) return; void this.changeResourceAssociation(resource, { op: 'detach', from }); return; } // Only conversation targets are attachable today; a resource target needs the server // contract that carries it, so it is refused rather than silently attached to nothing. const to = this.associationTargetSessionId(detail.toTarget); if (!to) return; if (detail.mode === 'move') { const from = this.attachedAssociationSessionId(resource, detail.fromTarget); if (!from) return; void this.changeResourceAssociation(resource, { op: 'move', to, from }); return; } void this.changeResourceAssociation(resource, { op: 'attach', to }); }; private async changeResourceAssociation( resourceArg: interfaces.TControllerResource, intentArg: | { op: 'attach'; to: TBrowserSessionId } | { op: 'move'; to: TBrowserSessionId; from: TBrowserSessionId } | { op: 'detach'; from: TBrowserSessionId }, ): Promise { const mutationId = this.beginMutation(); if (!mutationId) return; const generation = this.connectionGeneration; const projectId = this.selectedProjectId; try { if (intentArg.op === 'detach') { // Removes exactly the membership the action named. A UI action never clears the whole // set: the resource may be attached to subjects this sidebar does not present, and // Detach must not take those away silently. await this.socketClient.fire( 'controller.resource.detach', { projectId, resourceId: resourceArg.id, entry: { kind: 'session' as const, sessionId: intentArg.from }, expectedAttachmentRevision: resourceArg.attachment.revision, }, ); } else { // A Move is one request, so the resource is never momentarily unattached. await this.socketClient.fire( 'controller.resource.attach', { projectId, resourceId: resourceArg.id, sessionId: intentArg.to, ...(intentArg.op === 'move' ? { replace: { kind: 'session' as const, sessionId: intentArg.from } } : {}), expectedAttachmentRevision: resourceArg.attachment.revision, }, ); } if (this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId) { await this.loadResources(true); } } catch (errorArg) { if (this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId) { this.reportWorkspaceError('Change the resource attachment', errorArg); await this.loadResources(true); } } finally { this.finishMutation(mutationId); } } /** Conversation rows for the picker; status belongs to the sidebar, not to a chooser. */ /** * Rows the picker can show. Active tracked conversations of every project, plus the exact * archived ones a Detach or Move has to disambiguate: their attachment still exists, so * hiding them would leave the resource attached with no way to say so. */ private pickerSessionMetas( includeTargetsArg: readonly plugins.deesCatalog.IHarnessSessionListItemRef[] = [], ): plugins.deesCatalog.IHarnessSessionMeta[] { const includedKeys = new Set( includeTargetsArg.flatMap((targetArg) => ( targetArg.kind === 'session' ? [targetArg.id] : [] )), ); return this.conversations .filter((conversationArg) => hasSessionRuntimeId(conversationArg.session)) .flatMap((conversationArg) => { const sessionId = conversationArg.session.id as TBrowserSessionId; const key = conversationUiKey(conversationArg.projectId, sessionId); const archived = conversationArg.archivedAt !== undefined; if (archived && !includedKeys.has(key)) return []; return [{ id: key, title: conversationArg.session.title, createdAt: conversationArg.session.createdAt, updatedAt: conversationArg.session.updatedAt, harness: { id: sessionId.harnessId, name: sessionHarnessLabel(sessionId.harnessId), }, projectLabel: conversationArg.projectName, ...(archived ? { statusLabel: 'Archived in AGL' } : {}), }]; }); } /** The same grouping the sidebar shows, so a picker row reads with its group name. */ private pickerGroups(): plugins.deesCatalog.IHarnessSessionGroup[] { return this.sessionGroups.map((groupArg) => ({ id: groupArg.id, name: groupArg.name, itemIds: groupArg.itemIds.map(layoutItemRefToPresentation), })); } private async pickAssociationTarget(optionsArg: { heading: string; subheading?: string; eligibleTargets: plugins.deesCatalog.IHarnessSessionListItemRef[]; emptyText: string; }): Promise { return plugins.deesCatalog.DeesHarnessConversationPicker.pick({ sessions: this.pickerSessionMetas(optionsArg.eligibleTargets), groups: this.pickerGroups(), eligibleTargets: optionsArg.eligibleTargets, heading: optionsArg.heading, ...(optionsArg.subheading === undefined ? {} : { subheading: optionsArg.subheading }), emptyText: optionsArg.emptyText, }); } /** * Which existing attachment an action means. With one attachment there is nothing to ask, so * Move and Detach stay one click; with several, the choice is made explicitly rather than * guessed, which is exactly why the component refuses a modeless request in that case. */ private async pickAttachedAssociation( targetsArg: plugins.deesCatalog.IHarnessSessionListItemRef[], headingArg: string, ): Promise { if (targetsArg.length === 1) return targetsArg[0] ?? null; return this.pickAssociationTarget({ heading: headingArg, subheading: 'This resource is attached to several conversations.', eligibleTargets: targetsArg, emptyText: 'This resource has no attachment to choose.', }); } /** * Attaching is an explicit action now that a drop on a row's edge stays positional. Every entry * routes through the sidebar's own validated request, so the component stays the single source * of truth for what an association may become, and every "…" resolves its target through the * picker rather than listing every conversation in the menu. */ private resourceAssociationMenuItems( detailArg: plugins.deesCatalog.IHarnessResourceContextDetail, ): Array< | { name: string; iconName: string; action: () => Promise } | { divider: true } > { const items: Array< | { name: string; iconName: string; action: () => Promise } | { divider: true } > = []; const attachedTargets = detailArg.targets; const attachedKeys = new Set(attachedTargets.map(presentationRefKey)); // A picker never offers a no-op, so an attachment that already exists is not attachable. const attachableTargets = this.eligibleAssociationTargets() .filter((target) => !attachedKeys.has(presentationRefKey(target))) .filter((target) => this.associationTargetTitle(target) !== undefined); // A Detach or Move names an existing membership, which may belong to a conversation that is // archived in AGL; the picker is given its row explicitly rather than dropping the target. const isAttached = attachedTargets.length > 0; if (attachableTargets.length > 0) { const heading = isAttached ? 'Additionally attach to…' : 'Attach to…'; items.push({ name: heading, iconName: 'lucide:Link', action: async () => { const toTarget = await this.pickAssociationTarget({ heading, eligibleTargets: attachableTargets, emptyText: 'No other conversation is available.', }); if (!toTarget) return; detailArg.requestAssociation( toTarget, isAttached ? 'attach-additional' : 'attach', ); }, }); if (isAttached) { items.push({ name: 'Move to…', iconName: 'lucide:ArrowRightLeft', action: async () => { const fromTarget = await this.pickAttachedAssociation( attachedTargets, 'Move which attachment?', ); if (!fromTarget) return; const toTarget = await this.pickAssociationTarget({ heading: 'Move to…', eligibleTargets: attachableTargets, emptyText: 'No other conversation is available.', }); if (!toTarget) return; detailArg.requestAssociation(toTarget, 'move', fromTarget); }, }); } } if (isAttached) { items.push({ name: 'Detach', iconName: 'lucide:Unlink', action: async () => { const fromTarget = await this.pickAttachedAssociation( attachedTargets, 'Detach which attachment?', ); if (!fromTarget) return; detailArg.requestAssociation(null, 'detach', fromTarget); }, }); } if (items.length > 0) items.push({ divider: true }); return items; } /** * An association may target any other list item. Only conversations are attachable today; * the list is already a ref list, so a terminal target slots in without changing the callers. */ private eligibleAssociationTargets(): plugins.deesCatalog.IHarnessSessionListItemRef[] { return this.sessions .filter((session) => session.archivedAt === undefined) .filter(hasSessionRuntimeId) .map((session) => ({ kind: 'session' as const, id: conversationUiKey(this.selectedProjectId, session.id), })); } /** A sidebar row key that belongs to the project the resources are scoped to. */ private projectConversationIdFromUiKey(uiKeyArg: string): TBrowserSessionId | undefined { const parsed = parseConversationUiKey(uiKeyArg); if (!parsed || parsed.projectId !== this.selectedProjectId) return undefined; return this.sessions.some((sessionArg) => ( isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, parsed.sessionId) )) ? parsed.sessionId : undefined; } private associationTargetTitle( targetArg: plugins.deesCatalog.IHarnessSessionListItemRef, ): string | undefined { if (targetArg.kind === 'resource') { return this.resources.find((resource) => resource.id === targetArg.id)?.title; } // Read from the tracked set rather than the current project's list: a membership may name // a conversation that is archived in AGL and therefore not a row. return this.conversations .filter((conversationArg) => hasSessionRuntimeId(conversationArg.session)) .find((conversationArg) => ( conversationUiKey(conversationArg.projectId, conversationArg.session.id) === targetArg.id )) ?.session.title; } private readonly handleResourceContext = ( eventArg: CustomEvent, ): void => { const detail = eventArg.detail; const resource = this.resources.find((candidate) => candidate.id === detail?.resource?.id); if (!resource || !detail?.originalEvent || this.mutationPending) return; const menuEvent = detail.originalEvent instanceof MouseEvent ? detail.originalEvent : new MouseEvent('contextmenu', { clientX: detail.clientX, clientY: detail.clientY }); void plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(menuEvent, [ { name: 'Rename', iconName: 'lucide:Pencil', action: async () => this.showTextPromptModal({ heading: `Rename ${resource.kind}`, label: 'Title', initialValue: resource.title, submitLabel: 'Rename', onSubmit: async (titleArg) => this.renameResource(resource.id, titleArg), }), }, ...(resource.kind === 'terminal' ? [{ name: resource.processState === 'running' ? 'Stop' : 'Start', iconName: resource.processState === 'running' ? 'lucide:Square' : 'lucide:Play', action: async () => this.setResourceRunning(resource, resource.processState !== 'running'), }] : []), { divider: true }, ...this.resourceAssociationMenuItems(detail), { name: 'Retire', iconName: 'lucide:Trash2', action: async () => this.showResourceRetirementConfirmation(resource), }, ]).then((menuArg) => { if (!menuArg) { detail.close(); return; } const observer = new MutationObserver(() => { if (menuArg.isConnected) return; observer.disconnect(); detail.close(); }); observer.observe(document.body, { childList: true }); }).catch(() => detail.close()); }; private async renameResource(resourceIdArg: string, titleArg: string): Promise { await this.runResourceMutation(async () => { await this.socketClient.fire( 'controller.resource.rename', { projectId: this.selectedProjectId, resourceId: resourceIdArg, title: titleArg }, ); }); } private async setResourceRunning( resourceArg: interfaces.TControllerResource, runningArg: boolean, ): Promise { await this.runResourceMutation(async () => { if (runningArg) { await this.socketClient.fire( 'controller.resource.start', { projectId: this.selectedProjectId, resourceId: resourceArg.id }, ); } else { await this.socketClient.fire( 'controller.resource.stop', { projectId: this.selectedProjectId, resourceId: resourceArg.id }, ); } }); if (this.selectedResourceId !== resourceArg.id) return; const currentResource = this.resources.find((candidate) => candidate.id === resourceArg.id); if (currentResource?.kind !== 'terminal') return; if (runningArg && currentResource.processState === 'running') { this.openTerminalById({ harnessId: 'controller', nativeId: currentResource.id }); } else if (!runningArg && currentResource.processState === 'stopped') { this.closeTerminalView(); } } private showResourceRetirementConfirmation(resourceArg: interfaces.TControllerResource): void { void plugins.deesCatalog.DeesModal.createAndShow({ heading: `Retire ${resourceArg.kind}?`, width: 'small', content: plugins.deesElement.html`

“${resourceArg.title}” and its resource-owned runtime state will be removed.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy() }, { name: 'Retire', action: async (modalArg) => { await modalArg?.destroy(); await this.runResourceMutation(async () => { await this.socketClient.fire( 'controller.resource.retire', { projectId: this.selectedProjectId, resourceId: resourceArg.id }, ); if (this.selectedResourceId === resourceArg.id) { this.selectedResourceId = ''; this.closeTerminalView(); await this.closeBrowserView(); } }); }, }, ], }); } private async runResourceMutation(operationArg: () => Promise): Promise { const mutationId = this.beginMutation(); if (!mutationId) return; const generation = this.connectionGeneration; const projectId = this.selectedProjectId; try { await operationArg(); if (this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId) { await Promise.all([this.loadResources(true), this.loadTerminals(true)]); } } catch (errorArg) { if (this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId) { this.reportWorkspaceError('Change the resource', errorArg); await this.loadResources(true); } } finally { this.finishMutation(mutationId); } } /** * Opens a new thing in the workspace: work started for the previous selection is superseded, * and the composer notice goes with it — it was guidance for a composer no longer on screen. */ private beginWorkspaceSelection(): void { this.workspaceSelectionGeneration += 1; this.composerNotice = ''; } private openSessionById(sessionIdArg: TBrowserSessionId): void { if (!this.sessions.some((sessionArg) => ( isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, sessionIdArg) ))) { return; } this.beginWorkspaceSelection(); this.selectedResourceId = ''; void this.closeBrowserView(); this.closeTerminalView(); // Picking an existing chat abandons an unsent draft. this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.draftSessionActive = false; this.clearAnsweredCardCaches(); this.clearSubagentModalState(); this.invalidateSlashCatalog(); this.selectedSessionId = sessionIdArg; this.invalidateDetailLoad(); this.sessionDetail = undefined; this.ensureSessionDraftActive(this.selectedProjectId, sessionIdArg); this.requestComposerFocus('session', sessionIdArg); void this.loadSlashCatalog(); void this.loadSessionDetail(sessionIdArg); } /** * AGL's own archive: every tracked conversation archived here, across every project. The * harness archive is a different thing and is deliberately not shown. */ private readonly showArchivedModal = (): void => { const archivedConversations = this.conversations .filter((conversationArg) => ( conversationArg.archivedAt !== undefined && hasSessionRuntimeId(conversationArg.session) )) .slice() .sort((leftArg, rightArg) => (rightArg.archivedAt ?? 0) - (leftArg.archivedAt ?? 0)); let modalRef: plugins.deesCatalog.DeesModal | undefined; void plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Archived conversations', width: 'small', content: plugins.deesElement.html`
${archivedConversations.length === 0 ? plugins.deesElement.html`
No conversations archived in AGL.
` : archivedConversations.map((conversationArg) => plugins.deesElement.html` `)}
`, menuOptions: [ { name: 'Close', action: async (modalArg) => modalArg?.destroy(), }, ], }).then((modalArg) => { modalRef = modalArg; }); }; /** Replaces the tracked conversation the controller just returned, keyed by project and id. */ private applyTrackedConversation( conversationArg: interfaces.IControllerTrackedConversation, ): void { const key = conversationUiKey(conversationArg.projectId, conversationArg.session.id); const index = this.conversations.findIndex((candidateArg) => ( conversationUiKey(candidateArg.projectId, candidateArg.session.id) === key )); this.conversations = index < 0 ? [...this.conversations, conversationArg] : this.conversations.map((candidateArg, candidateIndex) => ( candidateIndex === index ? conversationArg : candidateArg )); } /** Returns an AGL-archived conversation to the active list and opens it. */ private async reopenTrackedConversation( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): Promise { const mutationId = this.beginMutation(); if (!mutationId) return; const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.conversation.reopen', { projectId: projectIdArg, sessionId: sessionIdArg }, ); if ( !isTrackedConversation(response.conversation) || response.conversation.archivedAt !== undefined ) { throw new Error('The controller returned an invalid reopened conversation.'); } if (!this.isCurrentMutation(mutationId, generation)) return; this.applyTrackedConversation(response.conversation); } catch (errorArg) { if (this.isCurrentMutation(mutationId, generation)) { this.reportWorkspaceError('Reopen the conversation', errorArg); } return; } finally { this.finishMutation(mutationId); } this.layoutRequestId += 1; this.groupsLoaded = false; await this.refreshSessions(); this.openTrackedConversation(projectIdArg, sessionIdArg); } /** * Every new-conversation entry point lands on the same box: the New menu's chat entry, the * chat view's new-session button and the empty workspace. Nothing starts a harness until the * box is submitted, which is what keeps AGL from silently choosing OpenCode. */ private readonly handleNewSession = (): void => { this.beginNewConversation(); }; /** * The sidebar's New button is a menu, not one action. A chat session opens the * new-conversation box, while a terminal, a Claude terminal and a browser are created * directly. The menu hangs under the button rather than at the pointer, so a click and a * keyboard activation anchor it identically, and dismissing it returns focus to the button. */ private readonly openNewMenu = (eventArg: Event): void => { const button = eventArg.currentTarget; if (!(button instanceof HTMLElement)) return; const rect = button.getBoundingClientRect(); void plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions( new MouseEvent('click', { clientX: rect.left, clientY: rect.bottom + 6 }), [ { name: 'Chat session', iconName: 'lucide:MessageSquarePlus', action: async () => this.handleNewSession(), }, { divider: true }, ...newMenuResources.map((resourceArg) => ({ name: resourceArg.name, iconName: resourceArg.iconName, action: async () => this.createResourceInProject(resourceArg), })), ], { ownerElement: button }, ); }; /** * Terminals and browsers live in a project, so the New menu resolves one instead of dropping * the action: the selected project is used, a single registered project is used without * asking, several are picked from, and with none registered the box that registers one opens. */ private createResourceInProject(resourceArg: INewMenuResource): void { const create = (projectIdArg: string): void => { // The picker is a dialog, so its project can be deregistered while it is open. Resolving // again is what keeps a create out of a project that no longer exists. if (!this.projects.some((projectArg) => projectArg.id === projectIdArg)) { this.createResourceInProject(resourceArg); return; } if (projectIdArg !== this.selectedProjectId) this.enterProjectContext(projectIdArg); void this.createResource(resourceArg.kind, resourceArg.agent); }; if (this.projects.some((projectArg) => projectArg.id === this.selectedProjectId)) { create(this.selectedProjectId); return; } const [firstProject, ...furtherProjects] = this.projects; if (!firstProject) { this.showResourceProjectRegistrationModal(resourceArg); return; } if (furtherProjects.length === 0) { create(firstProject.id); return; } this.showResourceProjectChoiceModal(resourceArg, create); } /** Asks which project the resource runs in. Nothing is created until one is picked. */ private showResourceProjectChoiceModal( resourceArg: INewMenuResource, onPickArg: (projectIdArg: string) => void, ): void { const options = this.projects.map((projectArg) => ({ option: projectArg.name, key: projectArg.id, })); let pickedProjectId = options[0]?.key ?? ''; void plugins.deesCatalog.DeesModal.createAndShow({ heading: `New ${resourceArg.subject}`, width: 'small', content: plugins.deesElement.html`

Choose the project this ${resourceArg.subject} runs in.

) => { const key = selectionArg.detail?.key; if (isNonEmptyString(key)) pickedProjectId = key; }} > `, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy(), }, { name: `Create ${resourceArg.subject}`, action: async (modalArg) => { if (!isNonEmptyString(pickedProjectId)) return; await modalArg?.destroy(); onPickArg(pickedProjectId); }, }, ], }); } /** With no project registered there is nothing to create in, so the flow that registers one opens. */ private showResourceProjectRegistrationModal(resourceArg: INewMenuResource): void { void plugins.deesCatalog.DeesModal.createAndShow({ heading: `New ${resourceArg.subject}`, width: 'small', content: plugins.deesElement.html`

A ${resourceArg.subject} runs in a project, and none is registered yet. Start a conversation in a directory to register it, then create the ${resourceArg.subject}.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy(), }, { name: 'Register a project', action: async (modalArg) => { await modalArg?.destroy(); this.handleNewSession(); }, }, ], }); } private beginNewConversation(): void { this.beginWorkspaceSelection(); this.selectedResourceId = ''; void this.closeBrowserView(); this.closeTerminalView(); this.cancelComposerFocus(); this.sessionDraftSync.deactivate(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.draftSessionActive = false; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; this.detailLoading = false; this.clearAnsweredCardCaches(); this.clearSubagentModalState(); this.newConversationError = ''; this.newConversationProjectId = this.selectedProjectId; this.newConversationDirectory = ''; this.newConversationHarnessId = this.lastSessionHarnessId; this.newConversationModelString = this.newConversationHarnessId === undefined ? '' : this.defaultModelStrings[this.newConversationHarnessId]; } private selectNewConversationHarness(harnessIdArg: TBrowserSessionHarnessId): void { this.newConversationHarnessId = harnessIdArg; this.newConversationModelString = this.defaultModelStrings[harnessIdArg]; this.newConversationError = ''; } /** * Starts the conversation the box describes. A directory that is not a registered project yet * is registered first, so a standard directory needs no separate project step. */ private async startNewConversation(): Promise { if (this.mutationPending) return; const harnessId = this.newConversationHarnessId; if (harnessId === undefined) { this.newConversationError = 'Choose the harness for this conversation.'; return; } const directory = this.newConversationDirectory.trim().replace(/(?<=.)\/+$/, ''); let projectId = this.newConversationProjectId; if (directory) { const registeredProjectId = await this.registerProjectDirectory(directory); if (registeredProjectId === undefined) return; projectId = registeredProjectId; } if (!isNonEmptyString(projectId)) { this.newConversationError = 'Choose the project this conversation belongs to.'; return; } if (projectId !== this.selectedProjectId) { this.switchProjectContext(projectId); await this.refreshSessions(); } const modelString = this.newConversationModelString; this.startDraftSession(harnessId); if ( modelString && modelString !== this.defaultModelStrings[harnessId] && this.modelChoicesByString.has(modelString) ) { // The draft carries the chosen model until its first message creates the session. this.sessionModelOverrides.set(draftSessionKey(harnessId), modelString); } void this.rememberSessionHarness(harnessId); } /** Registers an existing directory as a project; returns undefined when it was refused. */ private async registerProjectDirectory(directoryArg: string): Promise { const mutationId = this.beginMutation(); if (!mutationId) return undefined; const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.project.create', { path: directoryArg }, ); if (!isNonEmptyString(response.project?.id)) { throw new Error('The controller returned an invalid project.'); } if (!this.isCurrentMutation(mutationId, generation)) return undefined; await this.refreshProjects(); return response.project.id; } catch (errorArg) { if (this.isCurrentMutation(mutationId, generation)) { this.newConversationError = errorMessage(errorArg); } return undefined; } finally { this.finishMutation(mutationId); } } /** * Remembers the harness in the controller settings, so the next new-conversation box * preselects it. A failure is silent: it costs a preselection, never a conversation. */ private async rememberSessionHarness(harnessIdArg: TBrowserSessionHarnessId): Promise { if (this.lastSessionHarnessId === harnessIdArg || !this.authenticated) return; this.lastSessionHarnessId = harnessIdArg; try { await this.socketClient.fire( 'controller.settings.update', { lastSessionHarnessId: harnessIdArg }, ); } catch { // The preselection is a convenience; the conversation itself already started. } } private startDraftSession(harnessIdArg: TBrowserSessionHarnessId): void { if (!isNonEmptyString(this.selectedProjectId)) { return; } this.beginWorkspaceSelection(); this.selectedResourceId = ''; void this.closeBrowserView(); this.closeTerminalView(); // Nothing is persisted yet: the session is created by the first message. this.sessionModelOverrides.delete(draftSessionKey('opencode')); this.sessionModelOverrides.delete(draftSessionKey('flex')); this.sessionModelOverrides.delete(draftSessionKey('codex')); this.sessionEffortOverrides.delete(draftSessionKey('opencode')); this.sessionEffortOverrides.delete(draftSessionKey('flex')); this.sessionEffortOverrides.delete(draftSessionKey('codex')); this.sessionAccountOverrides.delete(draftSessionKey('opencode')); this.sessionAccountOverrides.delete(draftSessionKey('flex')); this.sessionAccountOverrides.delete(draftSessionKey('codex')); this.draftHarnessId = harnessIdArg; this.sessionDraftSync.deactivate(); this.localDraftText = ''; this.localDraftAttachments = []; this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.draftSessionActive = true; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; this.detailLoading = false; this.requestComposerFocus('draft', undefined); } private async showExistingConversationModal(): Promise { const existingOpenTask = this.existingConversationModalOpenTask; if (existingOpenTask) { await existingOpenTask; return; } if (this.existingConversationModalRef) return; const connectionGeneration = this.connectionGeneration; const lifecycleGeneration = this.lifecycleGeneration; if ( !this.authenticated || !this.socketClient.isConnected || !this.isCurrentLifecycle(lifecycleGeneration) ) return; const modalGeneration = ++this.existingConversationModalGeneration; const task = this.openExistingConversationModal( connectionGeneration, lifecycleGeneration, modalGeneration, ); this.existingConversationModalOpenTask = task; try { await task; } finally { if (this.existingConversationModalOpenTask === task) { this.existingConversationModalOpenTask = undefined; } } } /** * Open searches the harnesses themselves, across every registered project and the immediate * subdirectories of the configured standard directories. A result AGL already tracks is shown * as such rather than hidden, so picking it is an explicit no-op instead of a mystery. */ private async openExistingConversationModal( connectionGenerationArg: number, lifecycleGenerationArg: number, modalGenerationArg: number, ): Promise { let modal: plugins.deesCatalog.DeesModal | undefined; let query = ''; let loading = false; let error = ''; let results: interfaces.IControllerConversationSearchResult[] = []; let truncated = false; let requestId = 0; let closed = false; const renderContent = () => { const visibleResults = results.filter( (resultArg) => hasSessionRuntimeId(resultArg.session), ); const trimmedQuery = query.trim(); const statusText = error ? error : loading ? 'Searching…' : !trimmedQuery ? 'Searches titles across every harness and configured project location.' : truncated ? visibleResults.length === 0 ? 'No matches found. Some conversations could not be searched. Refine the title or retry.' : `${visibleResults.length} conversation${visibleResults.length === 1 ? '' : 's'} shown. Search incomplete — refine the title or retry.` : visibleResults.length === 0 ? `No conversations match “${trimmedQuery}”.` : `${visibleResults.length} conversation${visibleResults.length === 1 ? '' : 's'}`; return plugins.deesElement.html`
{ if (!modal || closed || this.existingConversationModalRef !== modal) return; this.clearExistingConversationSearch(); const target = eventArg.currentTarget as HTMLElement & { value?: string }; query = target.value ?? ''; error = ''; results = []; truncated = false; requestId += 1; if (!query.trim()) { loading = false; void refreshContent(); return; } loading = true; void refreshContent(); const scheduledRequestId = requestId; const scheduledQuery = query.trim(); const abortController = new AbortController(); this.existingConversationSearchAbortController = abortController; const timer = setTimeout(() => { if ( closed || this.existingConversationModalRef !== modal || this.existingConversationSearchTimer !== timer || this.existingConversationSearchAbortController !== abortController ) return; this.existingConversationSearchTimer = undefined; void search(scheduledQuery, scheduledRequestId, abortController); }, 250); this.existingConversationSearchTimer = timer; }} >
${statusText} ${visibleResults.length > 0 ? plugins.deesElement.html`Last active` : plugins.deesElement.html``}
0 ? 'hasResults' : ''}`}> ${visibleResults.map((resultArg) => { const harnessLabel = sessionHarnessLabel( (resultArg.session.id as TBrowserSessionId).harnessId, ); const title = resultArg.session.title || runtimeIdDisplay(resultArg.session.id); const activity = formatConversationActivity(resultArg.session.updatedAt); const ariaLabel = [ title, harnessLabel, resultArg.projectName, resultArg.projectDirectory, activity.ariaText, resultArg.tracked ? 'already open' : '', ].filter(Boolean).join(', '); return plugins.deesElement.html` `; })}
`; }; const refreshContent = async () => { if ( !modal || closed || this.existingConversationModalRef !== modal || !modal.isConnected ) return; modal.content = renderContent(); await modal.updateComplete; }; const search = async ( queryArg: string, scheduledRequestIdArg: number, abortControllerArg: AbortController, ) => { const isCurrentSearch = (): boolean => ( !closed && modal !== undefined && this.existingConversationModalRef === modal && this.existingConversationSearchAbortController === abortControllerArg && scheduledRequestIdArg === requestId && this.authenticated && this.isCurrentConnection(connectionGenerationArg) ); try { const response = await this.socketClient.fire( 'controller.conversation.search', { query: queryArg }, { abortSignal: abortControllerArg.signal }, ); if (!isCurrentSearch()) return; results = Array.isArray(response.results) ? response.results.filter((resultArg) => hasSessionRuntimeId(resultArg.session)) : []; truncated = response.truncated === true; } catch (errorArg) { if (abortControllerArg.signal.aborted || !isCurrentSearch()) return; error = errorMessage(errorArg); results = []; } finally { const refresh = isCurrentSearch() && !abortControllerArg.signal.aborted; if (this.existingConversationSearchAbortController === abortControllerArg) { this.existingConversationSearchAbortController = undefined; } if (refresh) { loading = false; await refreshContent(); } } }; modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Open a conversation', width: 520, content: renderContent(), menuOptions: [{ name: 'Close', action: async (modalArg) => modalArg?.destroy() }], }); const modalArg = modal; const destroy = modalArg.destroy.bind(modalArg); let destroyTask: Promise | undefined; modal.destroy = async () => { if (destroyTask) return destroyTask; closed = true; requestId += 1; this.clearExistingConversationModalReference(modalArg); const task = destroy(); destroyTask = task; await task; if (destroyTask === task) modalArg.destroy = async () => undefined; }; if ( modalGenerationArg !== this.existingConversationModalGeneration || !this.isCurrentLifecycle(lifecycleGenerationArg) || !this.authenticated || !this.isCurrentConnection(connectionGenerationArg) ) { await modalArg.destroy().catch(() => undefined); return; } this.existingConversationModalRef = modalArg; const input = modalArg.shadowRoot?.querySelector('dees-input-text'); if ( input instanceof plugins.deesCatalog.DeesInputText && this.existingConversationModalRef === modalArg && modalArg.isConnected && this.authenticated && this.isCurrentLifecycle(lifecycleGenerationArg) && this.isCurrentConnection(connectionGenerationArg) ) { await input.focus(); } } private clearExistingConversationSearch(): void { if (this.existingConversationSearchTimer) { clearTimeout(this.existingConversationSearchTimer); this.existingConversationSearchTimer = undefined; } const abortController = this.existingConversationSearchAbortController; this.existingConversationSearchAbortController = undefined; abortController?.abort(); } private clearExistingConversationModalReference( modalArg: InstanceType, ): void { if (this.existingConversationModalRef !== modalArg) return; this.existingConversationModalRef = undefined; this.existingConversationModalGeneration += 1; this.clearExistingConversationSearch(); } private async destroyExistingConversationModal(): Promise { this.existingConversationModalGeneration += 1; this.existingConversationModalOpenTask = undefined; this.clearExistingConversationSearch(); const modal = this.existingConversationModalRef; if (modal) await modal.destroy().catch(() => undefined); } private async openSearchedConversation( modalArg: plugins.deesCatalog.DeesModal | undefined, connectionGenerationArg: number, resultArg: interfaces.IControllerConversationSearchResult, ): Promise { if (!isSessionRuntimeId(resultArg.session.id) || resultArg.tracked) return; const sessionId = { ...resultArg.session.id }; const mutationId = this.beginMutation(); if (!mutationId) return; let opened: interfaces.IControllerTrackedConversation | undefined; try { const response = await this.socketClient.fire( 'controller.conversation.open', { projectDirectory: resultArg.projectDirectory, sessionId }, ); if ( !isTrackedConversation(response.conversation) || !controllerRuntimeIdsEqual(response.conversation.session.id, sessionId) ) throw new Error('The controller returned an invalid opened conversation.'); opened = response.conversation; if (!this.isCurrentMutation(mutationId, connectionGenerationArg)) return; await modalArg?.destroy(); } catch (errorArg) { if (this.isCurrentMutation(mutationId, connectionGenerationArg)) { this.reportWorkspaceError('Open the found conversation', errorArg); } return; } finally { this.finishMutation(mutationId); } // Opening a conversation is an explicit intent to work in it, including its project. if (opened && isSessionRuntimeId(opened.session.id)) { this.openTrackedConversation(opened.projectId, opened.session.id); } void this.refreshSessions(); } private async materializeDraftSession(): Promise { const mutationId = this.beginMutation(); if (!mutationId || !this.authenticated) { this.finishMutation(mutationId); return undefined; } const generation = this.connectionGeneration; const projectId = this.selectedProjectId; const draftGeneration = this.draftSessionGeneration; const draftHarnessId = this.draftHarnessId; const focusRequestId = this.requestComposerFocus('draft', undefined, projectId, false); if (focusRequestId === undefined) { this.finishMutation(mutationId); return undefined; } let createdSessionId: TBrowserSessionId | undefined; try { const initialModel = this.resolveInitialDraftModelChoice(); const providerConnectionId = initialModel?.harnessId === 'flex' ? this.resolveSessionAccount() : undefined; const response = await this.socketClient.fire( 'controller.session.create', { projectId, harnessId: draftHarnessId, ...(initialModel ? { model: initialModel } : {}), ...(providerConnectionId ? { providerConnectionId } : {}), }, ); if ( !response.session || !isSessionRuntimeId(response.session.id) || response.session.id.harnessId !== draftHarnessId ) { throw new Error('The controller returned an invalid new session.'); } createdSessionId = response.session.id; if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId || this.draftSessionGeneration !== draftGeneration || !this.draftSessionActive || this.selectedSessionId !== undefined || this.selectedTerminalId !== undefined ) { this.cancelComposerFocusById(focusRequestId); this.discardStaleDraftSession(projectId, response.session.id); return undefined; } const sessionId = response.session.id; const sessionKey = this.sessionOperationKey(projectId, sessionId); const draftKey = draftSessionKey(draftHarnessId); // Composer choices made while drafting follow the session they become. const draftModel = this.sessionModelOverrides.get(draftKey); if (draftModel !== undefined) { this.sessionModelOverrides.set(sessionKey, draftModel); this.sessionModelOverrides.delete(draftKey); } const draftEffort = this.sessionEffortOverrides.get(draftKey); if (draftEffort !== undefined) { this.sessionEffortOverrides.set(sessionKey, draftEffort); this.sessionEffortOverrides.delete(draftKey); } const draftAccount = this.sessionAccountOverrides.get(draftKey); if (draftAccount !== undefined) { this.sessionAccountOverrides.set(sessionKey, draftAccount); this.sessionAccountOverrides.delete(draftKey); } this.sessionDraftActivationGeneration += 1; await this.sessionDraftSync.activate(projectId, sessionId); this.sessionDraftSync.setText(this.localDraftText); this.sessionDraftSync.setAttachments(this.localDraftAttachments); const materializedDraft = await this.sessionDraftSync.flush(); if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId || this.draftSessionGeneration !== draftGeneration || !this.draftSessionActive || this.selectedSessionId !== undefined || this.selectedTerminalId !== undefined ) { this.sessionDraftSync.deactivate(); this.cancelComposerFocusById(focusRequestId); this.discardStaleDraftSession(projectId, sessionId); return undefined; } // The draft view keeps rendering until the real detail arrives, so the // chat never flashes through the empty-workspace state. this.selectedSessionId = sessionId; this.invalidateSlashCatalog(); void this.loadSlashCatalog(); this.retargetComposerFocus(focusRequestId, 'draft', sessionId, false); if (response.creationPending) { // Both surfaces: the controller call did fail, so it is journaled, and the owner has to // send again, so the instruction stands at the composer where that happens. const creationPendingMessage = 'This Codex conversation was created, but its initial settings are unfinished. Your draft is retained. Send again to finish setup and send it.'; this.reportWorkspaceError('Create the conversation', creationPendingMessage); this.noteComposer(creationPendingMessage); this.retargetComposerFocus(focusRequestId, 'draft', sessionId, true); return undefined; } return { sessionId, focusRequestId, draftText: materializedDraft.text, draftRevision: materializedDraft.revision, }; } catch (error) { if (createdSessionId && this.selectedSessionId === undefined) { this.sessionDraftSync.deactivate(); this.discardStaleDraftSession(projectId, createdSessionId); } if ( this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId && this.draftSessionGeneration === draftGeneration && this.draftSessionActive && this.selectedSessionId === undefined && this.selectedTerminalId === undefined ) { this.reportWorkspaceError('Create the conversation', error); this.retargetComposerFocus(focusRequestId, 'draft', undefined, true); } return undefined; } finally { this.finishMutation(mutationId); } } private discardStaleDraftSession(projectIdArg: string, sessionIdArg: TBrowserSessionId): void { let sessionIds = this.staleDraftSessionsToDelete.get(projectIdArg); if (!sessionIds) { sessionIds = new Map(); this.staleDraftSessionsToDelete.set(projectIdArg, sessionIds); } const sessionKey = controllerRuntimeIdToUiKey(sessionIdArg); sessionIds.set(sessionKey, sessionIdArg); const cleanupKey = `${projectIdArg}\u0000${sessionKey}`; this.staleDraftSessionCleanupAttempts.set(cleanupKey, 0); void this.deleteStaleDraftSession(projectIdArg, sessionIdArg); } private forgetStaleDraftSession(projectIdArg: string, sessionIdArg: TBrowserSessionId): void { const sessionKey = controllerRuntimeIdToUiKey(sessionIdArg); const cleanupKey = `${projectIdArg}\u0000${sessionKey}`; this.staleDraftSessionCleanupAttempts.delete(cleanupKey); const sessionIds = this.staleDraftSessionsToDelete.get(projectIdArg); sessionIds?.delete(sessionKey); if (sessionIds?.size === 0) this.staleDraftSessionsToDelete.delete(projectIdArg); } private deleteStaleDraftSession( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): Promise { const cleanupKey = `${projectIdArg}\u0000${controllerRuntimeIdToUiKey(sessionIdArg)}`; const existing = this.staleDraftSessionCleanupPromises.get(cleanupKey); if (existing) return existing; const attempt = (this.staleDraftSessionCleanupAttempts.get(cleanupKey) ?? 0) + 1; if (attempt > 2) { this.forgetStaleDraftSession(projectIdArg, sessionIdArg); return Promise.resolve(); } this.staleDraftSessionCleanupAttempts.set(cleanupKey, attempt); const cleanupOperation = (async (): Promise => { try { const response = await this.socketClient.fire( 'controller.session.discard-empty', { projectId: projectIdArg, sessionId: sessionIdArg }, { timeoutMs: 10_000, maxRetries: 0 }, ); if (response.discarded || !response.retryable || attempt >= 2) { this.forgetStaleDraftSession(projectIdArg, sessionIdArg); } } catch { if (attempt >= 2) this.forgetStaleDraftSession(projectIdArg, sessionIdArg); } })(); const cleanupPromise = cleanupOperation.finally(() => { if (this.staleDraftSessionCleanupPromises.get(cleanupKey) === cleanupPromise) { this.staleDraftSessionCleanupPromises.delete(cleanupKey); } }); this.staleDraftSessionCleanupPromises.set(cleanupKey, cleanupPromise); return cleanupPromise; } private async retryStaleDraftSessionCleanup(): Promise { const sessions: Array<{ projectId: string; sessionId: TBrowserSessionId }> = []; for (const [projectId, sessionIds] of this.staleDraftSessionsToDelete) { for (const sessionId of sessionIds.values()) { sessions.push({ projectId, sessionId }); } } await Promise.all(sessions.map(async ({ projectId, sessionId }) => { const sessionKey = controllerRuntimeIdToUiKey(sessionId); const cleanupKey = `${projectId}\u0000${sessionKey}`; await this.staleDraftSessionCleanupPromises.get(cleanupKey); if (this.staleDraftSessionsToDelete.get(projectId)?.has(sessionKey)) { await this.deleteStaleDraftSession(projectId, sessionId); } })); } private readonly handleSend = ( eventArg: CustomEvent, ): void => { this.closeSlashMenu(true); this.workspaceNotice = ''; // Every send re-evaluates what the composer needs; the checks below restate it if it still does. this.composerNotice = ''; const text = eventArg.detail?.text; const attachments = eventArg.detail?.attachments; if (!isNonEmptyString(text) || !Array.isArray(attachments)) { return; } const chat = eventArg.currentTarget; if (!(chat instanceof plugins.deesCatalog.DeesHarnessChat)) { return; } if (this.blockForUnknownCodexMode()) return; const targetHarnessId = this.selectedSessionId?.harnessId ?? this.draftHarnessId; if ( targetHarnessId === 'flex' && !text.startsWith('/') && !this.resolveComposerModelChoice(this.selectedSessionId) ) { this.noteComposer('Choose an OpenAI account and model before sending this Flex message.'); this.requestCurrentComposerFocus(); return; } const draftAttachments = toControllerDraftAttachments(attachments); if (!draftAttachments) { this.noteComposer('One or more attachments are incomplete.'); this.requestCurrentComposerFocus(); return; } const slashInput = interfaces.classifyControllerSlashInput(text); if (this.draftSessionActive && this.selectedSessionId === undefined) { this.localDraftText = text; this.localDraftAttachments = draftAttachments; void this.materializeDraftSession().then((resultArg) => { if (resultArg) { this.dispatchComposerText( resultArg.draftText, resultArg.draftRevision, resultArg.focusRequestId, ); } }); return; } if (slashInput.type !== 'ordinary') { void this.submitSlashCommand(text, draftAttachments); return; } this.sessionDraftSync.setText(text); this.sessionDraftSync.setAttachments(draftAttachments); void this.sessionDraftSync.flush().then((draftArg) => { this.dispatchComposerText(draftArg.text, draftArg.revision); }).catch((errorArg) => { this.reportWorkspaceError('Send the message', errorArg); this.requestCurrentComposerFocus(); }); }; private readonly handleSteerInput = (eventArg: CustomEvent): void => { void this.submitCodexSteer(eventArg); }; private async submitCodexSteer( eventArg: CustomEvent, ): Promise { const sessionId = this.selectedSessionId; const chat = eventArg.currentTarget; const activity = this.sessionDetail?.codexActivity; if (this.blockForUnknownCodexMode(sessionId)) return; if ( sessionId?.harnessId !== 'codex' || !(chat instanceof plugins.deesCatalog.DeesHarnessChat) || this.shadowRoot?.querySelector('dees-harness-chat') !== chat || !controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionId) || this.codexSteerPending || activity?.writer !== 'agl' || !isNonEmptyString(activity.turnId) ) return; const attachments = toControllerDraftAttachments(eventArg.detail.attachments); if (!isNonEmptyString(eventArg.detail.text) || !attachments) return; if (interfaces.classifyControllerSlashInput(eventArg.detail.text).type !== 'ordinary') { void this.submitSlashCommand(eventArg.detail.text, attachments); return; } const draftState = this.sessionDraftSync.state; if ( !draftState || draftState.loading || draftState.projectId !== this.selectedProjectId || !controllerRuntimeIdsEqual(draftState.sessionId, sessionId) ) return; const ownership = { projectId: this.selectedProjectId, sessionId: { ...sessionId }, turnId: activity.turnId, draftGeneration: this.sessionDraftActivationGeneration, }; this.codexSteerPending = true; try { const draft = await this.sessionDraftSync.prepareSubmission(eventArg.detail.text, attachments); const submission: ICodexSteerSubmission = { ...ownership, draft }; if (!this.codexSteerSubmissionIsCurrent(submission)) { await this.sessionDraftSync.settleSubmission( draft, false, this.codexSteerDraftWritesAllowed(submission), ); return; } await this.sendMessage(draft.revision, undefined, submission); } catch (errorArg) { this.reportWorkspaceError('Steer the running turn', errorArg); this.requestCurrentComposerFocus(); } finally { this.codexSteerPending = false; } } private codexSteerSubmissionIsCurrent(submissionArg: ICodexSteerSubmission): boolean { const draftState = this.sessionDraftSync.state; return ( this.sessionDraftActivationGeneration === submissionArg.draftGeneration && this.selectedProjectId === submissionArg.projectId && controllerRuntimeIdsEqual(this.selectedSessionId, submissionArg.sessionId) && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, submissionArg.sessionId) && this.sessionDetail?.codexActivity?.writer === 'agl' && this.sessionDetail.codexActivity.turnId === submissionArg.turnId && draftState?.projectId === submissionArg.projectId && draftState.loading === false && controllerRuntimeIdsEqual(draftState.sessionId, submissionArg.sessionId) ); } private codexSteerDraftWritesAllowed(submissionArg: ICodexSteerSubmission): boolean { const selectedSubmission = this.selectedProjectId === submissionArg.projectId && controllerRuntimeIdsEqual(this.selectedSessionId, submissionArg.sessionId) && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, submissionArg.sessionId); return !selectedSubmission || this.sessionDetail?.codexActivity?.writer !== 'external'; } private async updateCodexWriter(actionArg: 'release' | 'resume' | 'resumeQueue'): Promise { const sessionId = this.selectedSessionId; if (sessionId?.harnessId !== 'codex') return; if (actionArg === 'resumeQueue' && this.blockForUnknownCodexMode(sessionId)) return; const mutation = this.beginMutation(); if (!mutation) return; try { await this.socketClient.fire('controller.codex.writer.update', { projectId: this.selectedProjectId, sessionId, action: actionArg }, { maxRetries: 0 }); this.scheduleRefresh(); } catch (error) { this.reportWorkspaceError('Change the Codex writer', error); } finally { this.finishMutation(mutation); } } private updateCodexCollaborationMode( requestedMode: interfaces.TControllerCodexCollaborationMode, ): void { const sessionId = this.selectedSessionId; const detail = this.sessionDetail; const activity = detail?.codexActivity; if ( sessionId?.harnessId !== 'codex' || !detail || !controllerRuntimeIdsEqual(detail.session.id, sessionId) || activity?.collaborationMode === undefined || this.codexModeDispatchIsFenced(sessionId) || activity.collaborationMode.mode === requestedMode || activity.writer !== 'idle' || detail.session.status === 'busy' || detail.session.status === 'retry' || !this.codexHarnessIsReady() ) return; const codexSessionId = { ...sessionId, harnessId: 'codex' as const }; const mutationId = this.beginMutation(); if (!mutationId) return; const generation = this.connectionGeneration; const projectId = this.selectedProjectId; const modeMutationSessionKey = this.sessionOperationKey(projectId, sessionId); this.codexModeMutationSessionKey = modeMutationSessionKey; void (async () => { try { const response = await this.socketClient.fire( 'controller.codex.mode.update', { projectId, sessionId: codexSessionId, mode: requestedMode }, { maxRetries: 0 }, ); if ( !this.isCurrentMutation(mutationId, generation, sessionId) || this.selectedProjectId !== projectId || !controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionId) || !this.sessionDetail?.codexActivity ) return; this.sessionDetail = { ...this.sessionDetail, codexActivity: { ...this.sessionDetail.codexActivity, collaborationMode: { ...response.collaborationMode }, }, }; this.codexModeUnconfirmedSessionKey = ''; } catch (errorArg) { if (this.isCurrentMutation(mutationId, generation, sessionId)) { if (this.isOutcomeUnknownError(errorArg)) { this.codexModeUnconfirmedSessionKey = this.sessionOperationKey(projectId, sessionId); void this.loadSessionDetail(sessionId); } this.reportWorkspaceError('Change the Codex mode', errorArg); } } finally { if (this.codexModeMutationSessionKey === modeMutationSessionKey) { this.codexModeMutationSessionKey = ''; } this.finishMutation(mutationId); } })(); } /** * Renders only what the session header cannot already say. The header carries the model and its * effort, so repeating them here would push the chat down to state the obvious; the strip is for * the exceptions — someone else holds the writer, the authoritative Codex collaboration mode, * a turn was rerouted to another model, the queue is paused, a turn produced a diff, or a * running turn makes Steer and Queue next use different models. Writer ownership itself is acted * on from the conversation's actions menu, so no inert control sits above the chat. */ private renderCodexActivity( detailArg: IControllerSessionRenderDetail, ): plugins.deesElement.TemplateResult | '' { const activity = detailArg.codexActivity; if (!activity) return ''; const turnRunning = detailArg.session.status === 'busy' || detailArg.session.status === 'retry'; const modeSessionKey = isSessionRuntimeId(detailArg.session.id) ? this.sessionOperationKey(this.selectedProjectId, detailArg.session.id) : ''; const modeAuthority = activity.collaborationModeAuthority; const modeUnconfirmed = modeAuthority?.status === 'unconfirmed' || (modeSessionKey !== '' && this.codexModeUnconfirmedSessionKey === modeSessionKey); const modeChanging = !modeUnconfirmed && ( modeAuthority?.status === 'pending' || ( modeSessionKey !== '' && this.mutationPending && this.codexModeMutationSessionKey === modeSessionKey ) ); const modeControlVisible = activity.collaborationMode !== undefined || modeAuthority !== undefined || modeUnconfirmed || modeChanging; const modeDispatchFenced = isSessionRuntimeId(detailArg.session.id) && this.codexModeDispatchIsFenced(detailArg.session.id); const steeringHint = activity.writer === 'agl' && turnRunning && !modeDispatchFenced && !modeUnconfirmed; const writerNotice = activity.writer === 'external' || activity.writer === 'released'; // A turn whose changes net out reports an empty diff, and so does a diff notification without // a payload: one predicate decides both whether the strip appears and what it renders. const turnDiff = isNonEmptyString(activity.diff) ? activity.diff : undefined; if ( !writerNotice && !steeringHint && !activity.reroute && activity.queuePaused !== true && turnDiff === undefined && !modeControlVisible ) return ''; const diffSummary = turnDiff === undefined ? undefined : plugins.deesCatalog.parseUnifiedDiffDocument(turnDiff, { truncated: activity.diffTruncated === true, }).summary; const diffStats = diffSummary?.type === 'exact' || diffSummary?.type === 'partial' ? diffSummary.stats : undefined; const diffQualifier = diffSummary?.type === 'partial' ? `${diffStats ? 'Partial counts' : 'Counts unavailable'}${diffSummary.binaryFiles > 0 ? ' · binary changes' : ''}` : diffSummary?.type === 'unparseable' ? `Counts unavailable${diffSummary.binaryFiles > 0 ? ' · binary changes' : ''}` : diffSummary?.binaryFiles ? diffStats ? 'Includes binary changes' : 'Binary changes' : ''; return plugins.deesElement.html`
${modeControlVisible || turnDiff ? plugins.deesElement.html`
${modeControlVisible ? plugins.deesElement.html`
Mode
${(['default', 'plan'] as const).map((modeArg) => plugins.deesElement.html` `)}
` : ''} ${turnDiff ? plugins.deesElement.html`
${diffStats ? plugins.deesElement.html` +${diffStats.added} −${diffStats.removed} ` : ''} ${diffQualifier ? plugins.deesElement.html`${diffQualifier}` : ''} this.showCodexDiffModal( activity.turnId, turnDiff, activity.diffTruncated === true, )} >
` : ''}
` : ''} ${modeChanging ? plugins.deesElement.html`
Switching mode…
` : ''} ${modeUnconfirmed ? plugins.deesElement.html`
${codexModeOutcomeUnknownNotice}
` : ''} ${activity.reroute ? plugins.deesElement.html`
Codex switched from ${activity.reroute.fromModel} to ${activity.reroute.toModel} for this turn.
` : ''} ${activity.writer === 'external' ? plugins.deesElement.html`
Another Codex client owns this turn. Input and steering are unavailable here until it finishes.
` : ''} ${activity.writer === 'released' ? plugins.deesElement.html`
AGL is no longer following this conversation. Resume it from the conversation's actions menu to see live updates.
` : ''} ${activity.queuePaused ? plugins.deesElement.html`
The prompt queue is paused. Its undispatched prompts are retained.
void this.updateCodexWriter('resumeQueue')}>
` : ''} ${steeringHint ? plugins.deesElement.html`
Steer now uses the running turn’s model. Queue next uses your selected model.
` : ''}
`; } private readonly showCodexDiffModal = ( turnIdArg: string | undefined, diffArg: string, truncatedArg: boolean, ): void => { if (this.codexDiffModalRef?.isConnected || this.codexDiffModalOpenTask) return; const snapshot = { turnId: turnIdArg, diff: diffArg, truncated: truncatedArg }; const task = this.openCodexDiffModal(snapshot).catch((errorArg) => { this.reportWorkspaceError('Open turn edits', errorArg); }).finally(() => { if (this.codexDiffModalOpenTask === task) this.codexDiffModalOpenTask = undefined; }); this.codexDiffModalOpenTask = task; }; private async openCodexDiffModal(snapshotArg: { turnId: string | undefined; diff: string; truncated: boolean; }): Promise { const content = document.createElement('div'); content.dataset.turnId = snapshotArg.turnId ?? ''; content.style.height = 'min(70vh, 720px)'; content.style.minHeight = '280px'; const codebox = document.createElement('dees-dataview-codebox'); codebox.unifiedDiff = snapshotArg.diff; codebox.unifiedDiffTruncated = snapshotArg.truncated; codebox.diffView = 'inline'; codebox.style.display = 'block'; codebox.style.height = '100%'; content.append(codebox); const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Turn edits', subheading: 'Snapshot of this turn’s edits.', width: 'large', content: plugins.deesElement.html`${content}`, menuOptions: [{ name: 'Close', action: async (modalArg) => { await modalArg?.destroy(); }, }], }); const openedModal = modal; const destroy = openedModal.destroy.bind(openedModal); let destroyTask: Promise | undefined; openedModal.destroy = async () => { if (destroyTask) return destroyTask; if (this.codexDiffModalRef === openedModal) this.codexDiffModalRef = undefined; const task = destroy(); destroyTask = task; await task; if (destroyTask === task) openedModal.destroy = async () => undefined; }; if (!this.isConnected) { await openedModal.destroy(); return; } this.codexDiffModalRef = openedModal; } private async destroyCodexDiffModal(): Promise { const modal = this.codexDiffModalRef; this.codexDiffModalRef = undefined; if (modal) await modal.destroy(); } /** * The writer hand-over, offered only when it is actionable: AGL stops following the * conversation — whether it is quiet or another client is running a turn in it — or picks a * released conversation back up. While AGL owns a running turn there is nothing to hand over, * and a pending prompt queue must drain first. */ private codexWriterAction( sessionIdArg: TBrowserSessionId, ): { name: string; iconName: string; action: 'release' | 'resume' } | undefined { if ( sessionIdArg.harnessId !== 'codex' || !this.selectedSessionId || controllerRuntimeIdToUiKey(this.selectedSessionId) !== controllerRuntimeIdToUiKey(sessionIdArg) || this.sessionDetail === undefined || this.sessionDetail.pendingPrompts.length > 0 ) return undefined; const writer = this.sessionDetail.codexActivity?.writer; if (writer === 'idle' || writer === 'external') { return { name: 'Stop following in AGL', iconName: 'lucide:PauseCircle', action: 'release' }; } if (writer === 'released') { return { name: 'Resume in AGL', iconName: 'lucide:Play', action: 'resume' }; } return undefined; } private dispatchComposerText( draftTextArg: string, draftRevisionArg: number, focusRequestIdArg?: number, ): void { if ( isSessionRuntimeId(this.selectedSessionId) && interfaces.classifyControllerSlashInput(draftTextArg).type !== 'ordinary' ) { const state = this.sessionDraftSync.state; if ( state && state.revision === draftRevisionArg && controllerRuntimeIdsEqual(state.sessionId, this.selectedSessionId) ) { void this.submitSlashCommand(draftTextArg, state.attachments, focusRequestIdArg); } else { void this.executeSlashCommand(draftTextArg, draftRevisionArg, focusRequestIdArg); } return; } void this.sendMessage(draftRevisionArg, focusRequestIdArg); } private async submitSlashCommand( textArg: string, attachmentsArg: readonly interfaces.IControllerDraftAttachment[], focusRequestIdArg?: number, ): Promise { const sessionId = this.selectedSessionId; if (this.blockForUnknownCodexMode(sessionId)) return; const state = this.sessionDraftSync.state; if ( !isSessionRuntimeId(sessionId) || !state || state.loading || state.projectId !== this.selectedProjectId || !controllerRuntimeIdsEqual(state.sessionId, sessionId) ) return; try { const submission = await this.sessionDraftSync.prepareSubmission(textArg, attachmentsArg); await this.executeSlashCommand(textArg, submission.revision, focusRequestIdArg, submission); } catch (errorArg) { this.reportWorkspaceError('Run the slash command', errorArg); this.requestCurrentComposerFocus(); } } private async sendMessage( draftRevisionArg: number, focusRequestIdArg?: number, steerSubmissionArg?: ICodexSteerSubmission, ): Promise { if (steerSubmissionArg && !this.codexSteerSubmissionIsCurrent(steerSubmissionArg)) { await this.sessionDraftSync.settleSubmission( steerSubmissionArg.draft, false, this.codexSteerDraftWritesAllowed(steerSubmissionArg), ); return; } const sessionId = steerSubmissionArg?.sessionId ?? this.selectedSessionId; if (this.codexModeDispatchIsFenced(sessionId)) { if (steerSubmissionArg) { await this.sessionDraftSync.settleSubmission( steerSubmissionArg.draft, false, this.codexSteerDraftWritesAllowed(steerSubmissionArg), ); } this.blockForUnknownCodexMode(sessionId); this.cancelComposerFocusById(focusRequestIdArg); return; } const mutationId = this.beginMutation(); if (!isSessionRuntimeId(sessionId) || !mutationId) { if (steerSubmissionArg) { await this.sessionDraftSync.settleSubmission( steerSubmissionArg.draft, false, this.codexSteerDraftWritesAllowed(steerSubmissionArg), ); } this.cancelComposerFocusById(focusRequestIdArg); this.finishMutation(mutationId); return; } const generation = this.connectionGeneration; const projectId = steerSubmissionArg?.projectId ?? this.selectedProjectId; const wasDraft = this.draftSessionActive && this.sessionDetail === undefined; const focusRequestId = this.prepareSendComposerFocus( focusRequestIdArg, projectId, sessionId, wasDraft, ); const turnMarker = this.beginLocalSessionTurn(projectId, sessionId); this.workspaceNotice = ''; let steerAccepted = false; let steerSettled = false; try { const explicitChoice = this.resolveExplicitModelChoice(sessionId); const providerConnectionId = explicitChoice?.harnessId === 'flex' ? this.resolveSessionAccount(sessionId) : undefined; await this.socketClient.fire( 'controller.session.send', { projectId, sessionId, draftRevision: draftRevisionArg, ...(steerSubmissionArg ? { delivery: 'steer' as const, expectedTurnId: steerSubmissionArg.turnId } : {}), ...(explicitChoice ? { model: explicitChoice } : {}), ...(providerConnectionId ? { providerConnectionId } : {}), }, ); steerAccepted = steerSubmissionArg !== undefined; if (steerSubmissionArg) { await this.sessionDraftSync.settleSubmission( steerSubmissionArg.draft, true, this.codexSteerDraftWritesAllowed(steerSubmissionArg), ); steerSettled = true; } if (!this.isCurrentMutation(mutationId, generation, sessionId)) { return; } if (!steerSubmissionArg) await this.sessionDraftSync.refresh().catch(() => undefined); if (wasDraft) { this.reconcileMaterializedDraftComposer(sessionId); this.draftSessionDetailReadyId = sessionId; } this.retargetComposerFocus(focusRequestId, 'session', sessionId, true); const currentDetail = this.sessionDetail; if ( this.localTurnHasNoNewEvent(turnMarker) && currentDetail && controllerRuntimeIdsEqual(currentDetail.session.id, sessionId) ) { this.sessionDetail = { ...currentDetail, session: { ...currentDetail.session, status: 'busy' }, }; } this.scheduleRefresh(); } catch (error) { this.rollbackLocalSessionTurn(turnMarker); if (this.isCurrentMutation(mutationId, generation, sessionId)) { this.reportWorkspaceError('Send the message', error); } } finally { if (steerSubmissionArg && !steerSettled) { await this.sessionDraftSync.settleSubmission( steerSubmissionArg.draft, steerAccepted, this.codexSteerDraftWritesAllowed(steerSubmissionArg), ); } this.finishMutation(mutationId); } } private readonly handleAttachmentsChange = ( eventArg: CustomEvent<{ attachments?: unknown }>, ): void => { const attachments = toControllerDraftAttachments(eventArg.detail?.attachments); if (!attachments) return; if (this.draftSessionActive && this.selectedSessionId === undefined) { this.localDraftAttachments = attachments; } else { this.sessionDraftSync.setAttachments(attachments); } }; private sessionOperationKey(projectIdArg: string, sessionIdArg: TBrowserSessionId): string { return `${projectIdArg}\u0000${controllerRuntimeIdToUiKey(sessionIdArg)}`; } private codexModeDispatchIsFenced( sessionIdArg: TBrowserSessionId | undefined = this.selectedSessionId, ): boolean { if (sessionIdArg?.harnessId !== 'codex') return false; const sessionKey = this.sessionOperationKey(this.selectedProjectId, sessionIdArg); if ( this.codexModeMutationSessionKey === sessionKey || this.codexModeUnconfirmedSessionKey === sessionKey ) return true; return controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg) && this.sessionDetail?.codexActivity?.collaborationModeAuthority !== undefined; } private blockForUnknownCodexMode( sessionIdArg: TBrowserSessionId | undefined = this.selectedSessionId, ): boolean { if (!this.codexModeDispatchIsFenced(sessionIdArg)) return false; const sessionKey = sessionIdArg ? this.sessionOperationKey(this.selectedProjectId, sessionIdArg) : ''; const pending = this.codexModeMutationSessionKey === sessionKey || ( controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg) && this.sessionDetail?.codexActivity?.collaborationModeAuthority?.status === 'pending' ); this.noteComposer(pending ? 'The Codex mode change is waiting for native confirmation. Send, Steer, Queue, slash commands, and model changes are unavailable for this conversation.' : codexModeOutcomeUnknownNotice); this.requestCurrentComposerFocus(); return true; } /** * Keeps one contextual error next to the session it belongs to, and journals it as well: the * inline banner explains the view, the journal is where the owner copies a report from. */ private setSessionOperationError( mapArg: Map, keyArg: string, operationArg: string, errorArg: unknown, ): void { const message = errorMessage(errorArg); mapArg.delete(keyArg); mapArg.set(keyArg, message); while (mapArg.size > 512) { const oldestKey = mapArg.keys().next().value; if (oldestKey === undefined) break; mapArg.delete(oldestKey); } this.recordErrorJournalEntry(operationArg, message, errorArg); } /** * Reports a failed workspace operation into the journal the header surfaces. `operationArg` * names what the owner was doing; `messageArg` overrides the sentence where the UI phrases the * failure itself, while `errorArg` still carries the controller reference the cause is read * back with. */ private reportWorkspaceError(operationArg: string, errorArg: unknown, messageArg?: string): void { this.recordErrorJournalEntry(operationArg, messageArg ?? errorMessage(errorArg), errorArg); } /** * States a sentence at the composer, which is where the owner is looking when a send needs * something first. It is guidance, not a record of what the system did, so it stays out of the * error journal unless the caller also reports a failure that actually happened. */ private noteComposer(messageArg: string): void { this.composerNotice = messageArg; } private renderComposerNotice(): plugins.deesElement.TemplateResult | '' { if (!this.composerNotice) return ''; return plugins.deesElement.html`
${this.composerNotice}
`; } /** * States a sentence in the sidebar, which is where the owner clicked when a sidebar action needs * something first. The composer is not available for this: a conversation menu is reachable while * a terminal, a browser or the empty workspace is shown, and a composer notice would be dropped. * Like the composer notice it is guidance, not a record of what the system did, so it stays out * of the error journal. */ private noteSidebar(messageArg: string): void { this.sidebarNotice = messageArg; } private renderSidebarNotice(): plugins.deesElement.TemplateResult | '' { if (!this.sidebarNotice) return ''; return plugins.deesElement.html`
${this.sidebarNotice}
`; } private recordErrorJournalEntry( operationArg: string, messageArg: string, errorArg: unknown, ): void { const reference = controllerFailureReference(errorArg); this.errorJournalSequence += 1; this.errorJournal = appendErrorJournalEntry(this.errorJournal, { id: `${this.errorJournalSequence}`, at: Date.now(), operation: operationArg, message: messageArg, ...(reference === undefined ? {} : { reference }), seen: false, }); // A failure that happens while the dialog is open belongs in it, cause included. if (this.errorJournalModalRef?.isConnected) { this.refreshErrorJournalModal(); if (reference !== undefined) void this.loadErrorJournalDetails(); } } /** Newest first: the dialog and every copied report read in the order failures arrived last. */ private errorJournalNewestFirst(): IErrorJournalEntry[] { return [...this.errorJournal].reverse(); } private markErrorJournalSeen(entryIdsArg: ReadonlySet): void { if (this.errorJournal.every((entry) => entry.seen || !entryIdsArg.has(entry.id))) return; this.errorJournal = this.errorJournal.map( (entry) => (entry.seen || !entryIdsArg.has(entry.id) ? entry : { ...entry, seen: true }), ); } private readonly showErrorJournalModal = (): void => { if (this.errorJournalModalRef?.isConnected || this.errorJournalModalOpenTask) return; const task = this.openErrorJournalModal().catch((errorArg) => { // The dialog is where a failure would be shown, so a dialog that cannot open is journaled // instead of disappearing into an unhandled rejection. this.recordErrorJournalEntry('Open the Errors dialog', errorMessage(errorArg), errorArg); }).finally(() => { if (this.errorJournalModalOpenTask === task) this.errorJournalModalOpenTask = undefined; }); this.errorJournalModalOpenTask = task; }; private async openErrorJournalModal(): Promise { this.errorJournalNotice = ''; const openingEntryIds = new Set(this.errorJournal.map((entry) => entry.id)); const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Errors', subheading: 'Operations that failed since this tab was opened.', width: 'large', content: this.renderErrorJournalContent(), menuOptions: [ { name: 'Copy all', action: async () => { await this.copyErrorJournalReport(this.errorJournalNewestFirst()); }, }, { name: 'Clear', action: async () => { this.errorJournal = []; this.errorJournalNotice = ''; this.refreshErrorJournalModal(); }, }, { name: 'Close', action: async (modalArg) => { await modalArg?.destroy(); }, }, ], }); // Own-property override, so every dismissal — the Close action, the exit button, Escape and // the backdrop all call `destroy()` — drops the reference instead of leaving a closed dialog // behind for the next failure to refresh. const openedModal = modal; const destroy = openedModal.destroy.bind(openedModal); let destroyTask: Promise | undefined; openedModal.destroy = async () => { if (destroyTask) return destroyTask; this.clearErrorJournalModalReference(openedModal); const task = destroy(); destroyTask = task; await task; if (destroyTask === task) openedModal.destroy = async () => undefined; }; if (!this.isConnected) { await openedModal.destroy(); return; } this.errorJournalModalRef = openedModal; // Only entries present when opening began count as read. A failure arriving while the modal // was being created was absent from its initial content and keeps its unread header count. this.markErrorJournalSeen(openingEntryIds); this.refreshErrorJournalModal(); await this.loadErrorJournalDetails(); } private clearErrorJournalModalReference( modalArg: InstanceType, ): void { if (this.errorJournalModalRef !== modalArg) return; this.errorJournalModalRef = undefined; this.errorJournalNotice = ''; } private async destroyErrorJournalModal(): Promise { this.errorJournalModalOpenTask = undefined; const modal = this.errorJournalModalRef; if (modal) await modal.destroy().catch(() => undefined); } private refreshErrorJournalModal(): void { const modal = this.errorJournalModalRef; if (!modal?.isConnected) { this.errorJournalModalRef = undefined; return; } modal.content = this.renderErrorJournalContent(); } /** * One detail read at a time. Failures can arrive in bursts while the dialog is open, and each * one asks for the causes again; a read requested during another is satisfied by a single * re-run afterwards, which sees every reference collected in the meantime. */ private loadErrorJournalDetails(): Promise { if (this.errorJournalDetailTask) { this.errorJournalDetailRerunRequested = true; return this.errorJournalDetailTask; } const task = this.readErrorJournalDetails().finally(() => { this.errorJournalDetailTask = undefined; if (!this.errorJournalDetailRerunRequested) return; this.errorJournalDetailRerunRequested = false; void this.loadErrorJournalDetails(); }); this.errorJournalDetailTask = task; return task; } /** Reads the causes the controller kept for the references the dialog does not have yet. */ private async readErrorJournalDetails(): Promise { const references = [...new Set(this.errorJournal.flatMap((entry) => ( entry.reference !== undefined && entry.detail === undefined ? [entry.reference] : [] )))].slice(-interfaces.controllerFailureDetailRequestLimit); if (references.length === 0) return; if (!this.authenticated || !this.socketClient.isConnected) { this.errorJournalNotice = 'The controller is not connected, so the causes behind these references cannot be read.'; this.refreshErrorJournalModal(); return; } try { const response = await this.socketClient.fire( 'controller.failure.detail.get', { references }, { maxRetries: 0 }, ); const detailsByReference = new Map( response.failures.map((failure) => [failure.reference, failure]), ); this.errorJournal = this.errorJournal.map((entry) => { const detail = entry.reference === undefined ? undefined : detailsByReference.get(entry.reference); return detail === undefined || entry.detail !== undefined ? entry : { ...entry, detail }; }); } catch (errorArg) { // Stated in the dialog only: journaling this read would extend the list the owner opened. this.errorJournalNotice = `The controller-side causes could not be read: ${errorMessage(errorArg)}`; } this.refreshErrorJournalModal(); } private async copyErrorJournalReport(entriesArg: readonly IErrorJournalEntry[]): Promise { const report = formatErrorJournalReport({ version: this.backendVersion, generatedAt: Date.now(), entries: entriesArg, }); try { await navigator.clipboard.writeText(report); this.errorJournalNotice = entriesArg.length === 1 ? 'The report was copied.' : `${entriesArg.length} reports were copied.`; } catch (errorArg) { this.errorJournalNotice = `The clipboard refused the report: ${errorMessage(errorArg)}`; } this.refreshErrorJournalModal(); } private renderErrorJournalContent(): plugins.deesElement.TemplateResult { const entries = this.errorJournalNewestFirst(); return plugins.deesElement.html` ${this.errorJournalNotice ? plugins.deesElement.html`
${this.errorJournalNotice}
` : ''} ${entries.length === 0 ? plugins.deesElement.html`
No errors were recorded in this tab.
` : plugins.deesElement.html`
${entries.map((entry) => plugins.deesElement.html`
${entry.operation} ${new Date(entry.at).toLocaleTimeString()} { void this.copyErrorJournalReport([entry]); }} >
${entry.message}
${entry.reference === undefined ? '' : plugins.deesElement.html`
reference ${entry.reference}
${entry.detail === undefined
                      ? 'No controller cause is available for this reference.'
                      : `${entry.detail.operation}
${entry.detail.name}: ${entry.detail.message}
${entry.detail.stack}`}
`}
`)}
`} `; } private readonly handleScratchpadSave = ( eventArg: CustomEvent, ): void => { const detail = eventArg.detail; if ( typeof detail?.text !== 'string' || !Number.isSafeInteger(detail.expectedRevision) || detail.expectedRevision < 0 ) return; void this.saveSessionScratchpad(detail.text, detail.expectedRevision); }; private async saveSessionScratchpad(textArg: string, expectedRevisionArg: number): Promise { const projectId = this.selectedProjectId; const sessionId = this.selectedSessionId; if ( !isNonEmptyString(projectId) || !isSessionRuntimeId(sessionId) || !controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionId) || !this.authenticated || !this.socketClient.isConnected ) return; const operationKey = this.sessionOperationKey(projectId, sessionId); if (this.scratchpadSaveIdsBySession.has(operationKey)) return; const operationId = Symbol('scratchpadSave'); const generation = this.connectionGeneration; this.scratchpadSaveIdsBySession.set(operationKey, operationId); this.scratchpadErrorsBySession.delete(operationKey); this.requestUpdate(); try { const response = await this.socketClient.fire( 'controller.session.scratchpad.save', { projectId, sessionId, text: textArg, expectedRevision: expectedRevisionArg }, { maxRetries: 0 }, ); if (this.scratchpadSaveIdsBySession.get(operationKey) !== operationId) return; this.scratchpadErrorsBySession.delete(operationKey); if ( this.isCurrentConnection(generation) && this.selectedProjectId === projectId && controllerRuntimeIdsEqual(this.selectedSessionId, sessionId) && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionId) ) { // Supersede any older background detail read before publishing the CAS result. this.detailRequestId += 1; this.detailLoading = false; this.sessionDetail = { ...this.sessionDetail!, scratchpad: response.scratchpad }; } } catch (errorArg) { if (this.scratchpadSaveIdsBySession.get(operationKey) !== operationId) return; this.setSessionOperationError( this.scratchpadErrorsBySession, operationKey, 'Save the scratchpad', errorArg, ); if ( this.isCurrentConnection(generation) && this.selectedProjectId === projectId && controllerRuntimeIdsEqual(this.selectedSessionId, sessionId) ) { await this.loadSessionDetail(sessionId); } } finally { if (this.scratchpadSaveIdsBySession.get(operationKey) === operationId) { this.scratchpadSaveIdsBySession.delete(operationKey); this.requestUpdate(); } } } private readonly handleSessionIntelligenceAsk = ( eventArg: CustomEvent, ): void => { const question = eventArg.detail?.question?.trim(); if (!isNonEmptyString(question)) return; void this.askSessionIntelligence(question); }; private async askSessionIntelligence(questionArg: string): Promise { const projectId = this.selectedProjectId; const sessionId = this.selectedSessionId; const detail = this.sessionDetail; if ( !isNonEmptyString(projectId) || !isSessionRuntimeId(sessionId) || !controllerRuntimeIdsEqual(detail?.session.id, sessionId) || detail?.sessionIntelligenceEnabled !== true || detail.sessionIntelligenceAvailabilityStatus !== 'available' || detail.intelligenceExchanges.some((exchange) => exchange.status === 'running') || !this.authenticated || !this.socketClient.isConnected ) return; const operationKey = this.sessionOperationKey(projectId, sessionId); if (this.intelligenceAskIdsBySession.has(operationKey)) return; const operationId = Symbol('sessionIntelligenceAsk'); const generation = this.connectionGeneration; this.intelligenceAskIdsBySession.set(operationKey, operationId); this.intelligenceErrorsBySession.delete(operationKey); this.requestUpdate(); try { const response = await this.socketClient.fire( 'controller.session.intelligence.ask', { projectId, sessionId, question: questionArg }, { maxRetries: 0 }, ); if (this.intelligenceAskIdsBySession.get(operationKey) !== operationId) return; this.intelligenceErrorsBySession.delete(operationKey); const currentDetail = this.sessionDetail; if ( this.isCurrentConnection(generation) && this.selectedProjectId === projectId && controllerRuntimeIdsEqual(this.selectedSessionId, sessionId) && controllerRuntimeIdsEqual(currentDetail?.session.id, sessionId) ) { const existingExchange = currentDetail!.intelligenceExchanges.find( (exchange) => exchange.id === response.exchange.id, ); this.sessionDetail = { ...currentDetail!, intelligenceExchanges: existingExchange === undefined ? [...currentDetail!.intelligenceExchanges, response.exchange] : currentDetail!.intelligenceExchanges.map((exchange) => ( exchange.id === response.exchange.id && exchange.status === 'running' ? response.exchange : exchange )), }; } } catch (errorArg) { if (this.intelligenceAskIdsBySession.get(operationKey) !== operationId) return; this.setSessionOperationError( this.intelligenceErrorsBySession, operationKey, 'Ask session intelligence', errorArg, ); if ( this.isCurrentConnection(generation) && this.selectedProjectId === projectId && controllerRuntimeIdsEqual(this.selectedSessionId, sessionId) ) { await this.loadSessionDetail(sessionId); } } finally { if (this.intelligenceAskIdsBySession.get(operationKey) === operationId) { this.intelligenceAskIdsBySession.delete(operationKey); this.requestUpdate(); } } } private readonly handleAbort = (): void => { void this.abortSession(); }; private async abortSession(): Promise { const sessionId = this.selectedSessionId; const codexActivity = sessionId?.harnessId === 'codex' ? this.sessionDetail?.codexActivity : undefined; const expectedTurnId = codexActivity?.canInterrupt === true && isNonEmptyString(codexActivity.turnId) ? codexActivity.turnId : undefined; // A Codex Stop is bound to the exact observed turn. A stale or synthetic event must not turn // into an unscoped request that could interrupt work which started after this UI rendered. if (sessionId?.harnessId === 'codex' && expectedTurnId === undefined) return; const mutationId = this.beginMutation(); if (!isSessionRuntimeId(sessionId) || !mutationId) { this.finishMutation(mutationId); return; } const generation = this.connectionGeneration; const projectId = this.selectedProjectId; try { await this.socketClient.fire( 'controller.session.abort', { projectId, sessionId, ...(expectedTurnId ? { expectedTurnId } : {}) }, ); if (this.isCurrentMutation(mutationId, generation, sessionId)) { await this.loadSessionDetail(sessionId); } } catch (error) { if (this.isCurrentMutation(mutationId, generation, sessionId)) { this.reportWorkspaceError('Stop the conversation', error); } } finally { this.finishMutation(mutationId); } } private async replyToPermission( requestIdArg: interfaces.IControllerRuntimeId, replyArg: interfaces.TControllerPermissionReply, childAttentionArg?: interfaces.IControllerChildAttention, ): Promise { const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; const sessionId = this.selectedSessionId; try { if (childAttentionArg) { await this.socketClient.fire( 'controller.session.child.permission.reply', { projectId: childAttentionArg.projectId, parentSessionId: childAttentionArg.parentSessionId, childSessionId: childAttentionArg.childSessionId, scopeGeneration: childAttentionArg.scopeGeneration, requestId: requestIdArg as interfaces.IControllerRuntimeId & { harnessId: 'opencode' }, reply: replyArg, }, ); } else { await this.socketClient.fire( 'controller.permission.reply', { projectId: this.selectedProjectId, sessionId: sessionId!, requestId: requestIdArg, reply: replyArg, }, ); } if (this.isCurrentMutation(mutationId, generation, sessionId) && sessionId) { await this.loadSessionDetail(sessionId); } } catch (error) { if (this.isCurrentMutation(mutationId, generation, sessionId)) { this.reportWorkspaceError('Answer the permission request', error); } } finally { this.finishMutation(mutationId); } } private readonly handleQuestionResponse = ( eventArg: CustomEvent, ): void => { const detail = eventArg.detail; const cardId = detail?.requestId; const answers = Array.isArray(detail?.answers) ? detail.answers.filter((answerArg): answerArg is string => typeof answerArg === 'string' && answerArg.trim().length > 0) : []; if (!isNonEmptyString(cardId) || answers.length === 0) { return; } let request: interfaces.IControllerQuestion | undefined; let childAttention: interfaces.IControllerChildAttention | undefined; let questionIndex = 0; const questionSources: Array<{ questions: interfaces.IControllerQuestion[]; childAttention?: interfaces.IControllerChildAttention; }> = [ { questions: this.sessionDetail?.questions ?? [] }, ...(this.sessionDetail?.childAttention ?? []).map((attentionArg) => ({ questions: attentionArg.questions, childAttention: attentionArg, })), ]; for (const sourceArg of questionSources) { for (const candidateArg of sourceArg.questions) { const requestKey = controllerRuntimeIdToUiKey(candidateArg.id); const matchedIndex = candidateArg.questions.findIndex((_questionArg, indexArg) => ( cardId === (candidateArg.questions.length > 1 ? `${requestKey}#${indexArg}` : requestKey) )); if (matchedIndex >= 0) { request = candidateArg; childAttention = sourceArg.childAttention; questionIndex = matchedIndex; break; } } if (request) break; } if (!request) { return; } const requestKey = controllerRuntimeIdToUiKey(request.id); // The card persists inline showing what was chosen. this.answeredQuestionCards.set(cardId, { ...detail.request, response: answers, respondedAt: Date.now(), }); this.requestUpdate(); if (request.questions.length <= 1) { void this.submitQuestionReply(request.id, [answers], childAttention); return; } let byIndex = this.pendingQuestionBatchAnswers.get(requestKey); if (!byIndex) { byIndex = new Map(); this.pendingQuestionBatchAnswers.set(requestKey, byIndex); } byIndex.set(questionIndex, answers); if (byIndex.size >= request.questions.length) { const assembled = request.questions.map( (_questionArg, indexArg) => byIndex.get(indexArg) ?? [], ); this.pendingQuestionBatchAnswers.delete(requestKey); void this.submitQuestionReply(request.id, assembled, childAttention); } }; private readonly handlePermissionResponse = ( eventArg: CustomEvent, ): void => { const detail = eventArg.detail; if (!detail || !isNonEmptyString(detail.requestId)) { return; } this.answeredPermissionCards.set(detail.requestId, { ...detail.request, response: detail.response, respondedAt: Date.now(), }); this.requestUpdate(); // Persistent broad grants stay unsupported: 'always' grants this request // once and OpenCode will simply ask again next time. const reply = detail.response === 'reject' ? 'reject' : 'once'; let request = this.sessionDetail?.permissions.find( (candidateArg) => controllerRuntimeIdToUiKey(candidateArg.id) === detail.requestId, ); let childAttention: interfaces.IControllerChildAttention | undefined; if (!request) { for (const attentionArg of this.sessionDetail?.childAttention ?? []) { request = attentionArg.permissions.find( (candidateArg) => controllerRuntimeIdToUiKey(candidateArg.id) === detail.requestId, ); if (request) { childAttention = attentionArg; break; } } } if (request) void this.replyToPermission(request.id, reply, childAttention); }; // The open subagent drill-in modal; refreshed on that child's session events. private subagentModalSessionId: TBrowserSessionId | undefined; private subagentModalParentSessionId: TBrowserSessionId | undefined; private subagentModalAccess: TSubtaskAccess | undefined; private subagentModalScopeGeneration: string | undefined; private subagentModalScopeSequence: number | undefined; private subagentModalList: plugins.deesCatalog.DeesHarnessMessageList | undefined; private subagentModalRef: InstanceType | undefined; private subagentModalObserver: MutationObserver | undefined; private subagentModalGeneration = 0; private subagentModalRefreshInFlight: Promise | undefined; private subagentModalRefreshPending = false; private subagentModalRefreshTimer: ReturnType | undefined; private subagentModalDetailAbortController: AbortController | undefined; private subagentModalHistoryAbortController: AbortController | undefined; private subagentModalBundles: interfaces.IControllerMessageBundle[] = []; private subagentModalBundleKeys = new Set(); private subagentModalCoreBundleKeys = new Set(); private subagentModalProvisionalBundleKeys = new Set(); private subagentModalHistoryCursor: string | undefined; private subagentModalHistorySeenCursors = new Set(); private subagentModalHistoryScopeGeneration: string | undefined; private subagentModalHistoryToolStreamEpoch: number | undefined; private subagentModalHistoryMessageStreamEpoch: number | undefined; private subagentModalDetailLoaded = false; private subagentModalHistoryStatus: TDetailHistoryStatus = 'idle'; private subagentModalHistoryLimits = emptyDetailHistoryLimits(); private subagentModalHistoryStatusElement: HTMLElement | undefined; private resolveSubtaskSessionId(sessionKeyArg: string): TBrowserSessionId | undefined { const childSessionIds = [ ...(this.sessionDetail?.messages ?? []) .map((message) => message.toolCall?.childSessionId) .filter(isSessionRuntimeId), ...[...this.subtaskPreviewEntries.values()] .filter((entry) => ( entry.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(entry.ownerSessionId, this.selectedSessionId) )) .map((entry) => entry.childSessionId), ...[...this.liveToolOverlays.values()] .filter((overlay) => ( overlay.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(overlay.execution.sessionId, this.selectedSessionId) && isSessionRuntimeId(overlay.execution.childSessionId) )) .map((overlay) => overlay.execution.childSessionId) .filter(isSessionRuntimeId), ]; const sessionId = runtimeIdsByUiKey(childSessionIds).get(sessionKeyArg); return sessionId && isSessionRuntimeId(this.selectedSessionId) && this.childSessionAccess( this.selectedProjectId, this.selectedSessionId, sessionId, ) !== undefined ? sessionId : undefined; } private readonly handleSubtaskOpen = ( eventArg: CustomEvent, ): void => { const sessionKey = eventArg.detail?.sessionId; if (!isNonEmptyString(sessionKey)) { return; } const sessionId = this.resolveSubtaskSessionId(sessionKey); if (!sessionId) return; this.clearSubagentModalState(); const modalGeneration = this.subagentModalGeneration; const projectId = this.selectedProjectId; const ownerSessionId = this.selectedSessionId; if (!isSessionRuntimeId(ownerSessionId)) return; const access = this.childSessionAccess(projectId, ownerSessionId, sessionId); if (!access) return; const call = eventArg.detail.call; const input = call?.input as { description?: unknown } | undefined; const description = typeof input?.description === 'string' && input.description.trim() ? input.description.trim() : 'Subagent chat'; // A subagent frequently runs on a different model than its parent chat, // so the drill-in names it up front. const heading = call?.model ? `${description} — ${call.model}` : description; const content = document.createElement('div'); content.style.position = 'relative'; content.style.height = '65vh'; const list = document.createElement('dees-harness-message-list'); list.style.height = '100%'; list.style.display = 'block'; list.transcriptKey = `${projectId}\u0000${controllerRuntimeIdToUiKey(sessionId)}`; list.addEventListener('harness-load-earlier', this.handleSubagentModalLoadEarlier); const historyStatus = document.createElement('div'); historyStatus.style.position = 'absolute'; historyStatus.style.zIndex = '2'; historyStatus.style.top = '10px'; historyStatus.style.left = '50%'; historyStatus.style.transform = 'translateX(-50%)'; historyStatus.style.padding = '4px 10px'; historyStatus.style.border = '1px solid var(--dees-color-border-subtle)'; historyStatus.style.borderRadius = '999px'; historyStatus.style.background = 'var(--dees-color-bg-secondary)'; historyStatus.style.color = 'var(--dees-color-text-secondary)'; historyStatus.style.fontSize = '11px'; historyStatus.style.lineHeight = '1.3'; historyStatus.style.pointerEvents = 'none'; historyStatus.hidden = true; content.append(list, historyStatus); this.subagentModalSessionId = sessionId; this.subagentModalParentSessionId = ownerSessionId; this.subagentModalAccess = access; this.subagentModalList = list; this.subagentModalHistoryStatusElement = historyStatus; this.setSubagentModalHistoryStatus('idle'); void plugins.deesCatalog.DeesModal.createAndShow({ heading, width: 'large', content: plugins.deesElement.html`${content}`, menuOptions: [ { name: 'Close', action: async (modalArg) => { this.clearSubagentModalState(false); await modalArg?.destroy(); }, }, ], }).then(async (modalArg) => { if ( modalGeneration !== this.subagentModalGeneration || !this.isConnected || projectId !== this.selectedProjectId || !controllerRuntimeIdsEqual(ownerSessionId, this.selectedSessionId) ) { await modalArg.destroy(); return; } this.subagentModalRef = modalArg; const observer = new MutationObserver(() => { if (modalArg.isConnected) return; observer.disconnect(); if (this.subagentModalRef === modalArg) this.clearSubagentModalState(false); }); observer.observe(document.body, { childList: true }); this.subagentModalObserver = observer; void this.refreshSubagentModal(); }).catch(() => { if (modalGeneration === this.subagentModalGeneration) { this.clearSubagentModalState(false); } }); }; private refreshSubagentModal(): Promise { if (this.subagentModalRefreshInFlight) { this.subagentModalRefreshPending = true; this.subagentModalDetailAbortController?.abort( new Error('Subagent detail request superseded.'), ); return this.subagentModalRefreshInFlight; } const task = this.performSubagentModalRefresh(); this.subagentModalRefreshInFlight = task; void task.finally(() => { if (this.subagentModalRefreshInFlight !== task) return; this.subagentModalRefreshInFlight = undefined; if (!this.subagentModalRefreshPending) return; this.subagentModalRefreshPending = false; void this.refreshSubagentModal(); }); return task; } private scheduleSubagentModalRefresh(): void { if (this.subagentModalRefreshTimer) clearTimeout(this.subagentModalRefreshTimer); this.subagentModalRefreshTimer = setTimeout(() => { this.subagentModalRefreshTimer = undefined; void this.refreshSubagentModal(); }, subtaskPreviewRefreshDelayMs); } private setSubagentModalHistoryStatus( statusArg: TDetailHistoryStatus, limitsArg = this.subagentModalHistoryLimits, ): void { this.subagentModalHistoryStatus = statusArg; this.subagentModalHistoryLimits = limitsArg; if (this.subagentModalList) { this.subagentModalList.hasEarlierMessages = this.subagentModalHistoryCursor !== undefined; this.subagentModalList.loadingEarlier = statusArg === 'backfilling'; } const element = this.subagentModalHistoryStatusElement; if (!element) return; element.hidden = statusArg === 'idle' || statusArg === 'complete'; element.textContent = detailHistoryStatusText(statusArg, limitsArg); } private async performSubagentModalRefresh(): Promise { const sessionId = this.subagentModalSessionId; let parentSessionId = this.subagentModalParentSessionId ?? (isSessionRuntimeId(this.selectedSessionId) ? this.selectedSessionId : undefined); let access = this.subagentModalAccess; if ( !access && isSessionRuntimeId(sessionId) && this.childSessionHasIndependentManagedMembership(this.selectedProjectId, sessionId) ) access = 'managed'; access ??= isSessionRuntimeId(sessionId) && isSessionRuntimeId(parentSessionId) ? this.childSessionAccess(this.selectedProjectId, parentSessionId, sessionId) : undefined; if (access === 'managed' && !isSessionRuntimeId(parentSessionId)) parentSessionId = sessionId; const list = this.subagentModalList; const modalGeneration = this.subagentModalGeneration; const projectId = this.selectedProjectId; if (!isSessionRuntimeId(sessionId) || !isSessionRuntimeId(parentSessionId) || !access || !list) { return; } if ( this.childSessionAccess(projectId, parentSessionId, sessionId) !== access || (access === 'scoped' && ( !isOpenCodeSessionRuntimeId(parentSessionId) || !isOpenCodeSessionRuntimeId(sessionId) )) ) { this.clearSubagentModalState(); return; } // dismissed by outside click: stop tracking without keeping references if (this.subagentModalRef && !this.subagentModalRef.isConnected) { this.clearSubagentModalState(false); return; } const isCurrentModalTarget = (): boolean => ( modalGeneration === this.subagentModalGeneration && projectId === this.selectedProjectId && controllerRuntimeIdsEqual(this.subagentModalSessionId, sessionId) && (this.subagentModalParentSessionId === undefined || controllerRuntimeIdsEqual(this.subagentModalParentSessionId, parentSessionId)) && (this.subagentModalAccess === undefined || this.subagentModalAccess === access) && (this.subagentModalRef === undefined || this.subagentModalRef.isConnected) ); const abortController = new AbortController(); this.subagentModalDetailAbortController = abortController; try { const readDetail = (): Promise< interfaces.IControllerSessionDetail | interfaces.IControllerChildSessionDetail > => access === 'managed' ? this.socketClient.fire( 'controller.session.get', { projectId, sessionId }, { abortSignal: abortController.signal }, ) : this.socketClient.fire( 'controller.session.child.get', { projectId, parentSessionId: parentSessionId as TBrowserOpenCodeSessionId, childSessionId: sessionId as TBrowserOpenCodeSessionId, }, { maxRetries: 0, abortSignal: abortController.signal }, ); let detail: interfaces.IControllerSessionDetail | interfaces.IControllerChildSessionDetail; try { detail = await readDetail(); } catch { if (abortController.signal.aborted || !isCurrentModalTarget()) return; detail = await readDetail(); } if ( this.subagentModalDetailAbortController !== abortController || !isCurrentModalTarget() ) return; if (!detail.toolStreamCursor || !detail.messageStreamCursor) { throw new Error('The controller omitted a live stream cursor.'); } const scopedDetail = access === 'scoped' ? detail as interfaces.IControllerChildSessionDetail : undefined; const scopedGenerationChanged = scopedDetail !== undefined && this.subagentModalScopeGeneration !== undefined && this.subagentModalScopeGeneration !== scopedDetail.scopeGeneration; if ( scopedDetail && this.subagentModalScopeGeneration === scopedDetail.scopeGeneration && (this.subagentModalScopeSequence ?? -1) > scopedDetail.sequence ) return; const toolEpochAdvanced = scopedDetail ? scopedGenerationChanged || scopedDetail.sequence > (this.subagentModalScopeSequence ?? -1) : detail.toolStreamCursor.streamEpoch > (this.liveToolStreamEpochs.get(sessionId.harnessId) ?? 0); const messageEpochAdvanced = scopedDetail ? toolEpochAdvanced : detail.messageStreamCursor.streamEpoch > (this.liveMessageStreamEpochs.get(sessionId.harnessId) ?? 0); if (!scopedDetail && ( !this.acceptLiveToolEpoch(sessionId.harnessId, detail.toolStreamCursor.streamEpoch, false) || !this.acceptLiveMessageEpoch(sessionId.harnessId, detail.messageStreamCursor.streamEpoch, false) )) return; if (scopedDetail) { this.subagentModalScopeGeneration = scopedDetail.scopeGeneration; this.subagentModalScopeSequence = scopedDetail.sequence; this.applyPendingChildEvent(scopedDetail.scopeGeneration, scopedDetail.sequence); } const toolStreamEpoch = detail.toolStreamCursor.streamEpoch; const messageStreamEpoch = detail.messageStreamCursor.streamEpoch; const isCurrentModalLoad = (): boolean => ( this.subagentModalDetailAbortController === abortController && isCurrentModalTarget() && (scopedDetail ? this.subagentModalScopeGeneration === scopedDetail.scopeGeneration && this.subagentModalScopeSequence === scopedDetail.sequence : (this.liveToolStreamEpochs.get(sessionId.harnessId) ?? 0) === toolStreamEpoch && (this.liveMessageStreamEpochs.get(sessionId.harnessId) ?? 0) === messageStreamEpoch) ); if (!isCurrentModalLoad()) return; const core = boundedNewestMessageBundles(detail.messagePage.bundles); const coreKeys = core.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), ); const boundaryChanged = !this.subagentModalDetailLoaded || !sameStringArray([...this.subagentModalCoreBundleKeys], coreKeys); const coreKeySet = new Set(coreKeys); const historyEpochChanged = scopedDetail ? this.subagentModalHistoryScopeGeneration !== undefined && ( this.subagentModalHistoryScopeGeneration !== scopedDetail.scopeGeneration || this.subagentModalHistoryToolStreamEpoch !== scopedDetail.sequence || this.subagentModalHistoryMessageStreamEpoch !== scopedDetail.sequence ) : ( this.subagentModalHistoryToolStreamEpoch !== undefined && this.subagentModalHistoryToolStreamEpoch !== toolStreamEpoch ) || ( this.subagentModalHistoryMessageStreamEpoch !== undefined && this.subagentModalHistoryMessageStreamEpoch !== messageStreamEpoch ); const historyBecameTerminal = this.subagentModalDetailLoaded && this.subagentModalHistoryCursor !== undefined && detail.messagePage.nextCursor === undefined; const restartHistory = boundaryChanged || toolEpochAdvanced || messageEpochAdvanced || historyEpochChanged || historyBecameTerminal; const provisionalBundleKeys = this.subagentModalDetailLoaded && detail.messagePage.nextCursor !== undefined && restartHistory ? new Set(this.subagentModalBundles .map((bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId)) .filter((key) => !coreKeySet.has(key))) : new Set(); const retainedOlderBundles = this.subagentModalDetailLoaded ? this.subagentModalBundles.filter((bundle) => { const key = controllerRuntimeIdToUiKey(bundle.sourceMessageId); if (coreKeySet.has(key)) return false; if (!restartHistory) return !this.subagentModalCoreBundleKeys.has(key); if (detail.messagePage.nextCursor !== undefined) { return provisionalBundleKeys.has(key); } return !this.subagentModalCoreBundleKeys.has(key) && !this.subagentModalProvisionalBundleKeys.has(key); }) : []; const accumulated = boundedNewestMessageBundles([ ...retainedOlderBundles, ...core.bundles, ]); if ( restartHistory && (core.limited || accumulated.limited) && provisionalBundleKeys.size > 0 ) { accumulated.bundles = accumulated.bundles.filter((bundle) => ( !provisionalBundleKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); provisionalBundleKeys.clear(); } const coreHistoryLimits: IDetailHistoryLimits = { truncated: detail.messagePage.truncated === true, historyLimited: detail.messagePage.historyLimited === true || core.limited || accumulated.limited, unavailable: false, }; this.subagentModalBundles = accumulated.bundles; this.subagentModalBundleKeys = new Set(accumulated.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), )); this.subagentModalCoreBundleKeys = new Set(coreKeys); if (restartHistory) { this.subagentModalHistoryAbortController?.abort( new Error('Subagent history request superseded.'), ); this.subagentModalHistoryAbortController = undefined; this.subagentModalProvisionalBundleKeys = provisionalBundleKeys; this.subagentModalHistoryCursor = accumulated.limited ? undefined : detail.messagePage.nextCursor; this.subagentModalHistorySeenCursors.clear(); if (this.subagentModalHistoryCursor !== undefined) { this.subagentModalHistorySeenCursors.add(this.subagentModalHistoryCursor); } this.subagentModalHistoryScopeGeneration = this.subagentModalHistoryCursor === undefined ? undefined : scopedDetail?.scopeGeneration; this.subagentModalHistoryToolStreamEpoch = this.subagentModalHistoryCursor === undefined ? undefined : (scopedDetail?.sequence ?? toolStreamEpoch); this.subagentModalHistoryMessageStreamEpoch = this.subagentModalHistoryCursor === undefined ? undefined : (scopedDetail?.sequence ?? messageStreamEpoch); this.subagentModalHistoryLimits = coreHistoryLimits; } else { this.subagentModalHistoryLimits = { truncated: this.subagentModalHistoryLimits.truncated || coreHistoryLimits.truncated, historyLimited: this.subagentModalHistoryLimits.historyLimited || coreHistoryLimits.historyLimited, unavailable: this.subagentModalHistoryLimits.unavailable, }; } list.messages = accumulated.bundles .flatMap((bundle) => bundle.messages) .filter((messageArg) => messageArg.toolCall?.name !== 'question') .map((messageArg) => this.toHarnessMessage(messageArg, projectId, sessionId)); list.status = this.toHarnessStatus(detail.session); this.subagentModalDetailLoaded = true; this.setSubagentModalHistoryStatus( this.subagentModalHistoryAbortController ? 'backfilling' : this.subagentModalHistoryCursor ? 'idle' : this.subagentModalHistoryLimits.truncated || this.subagentModalHistoryLimits.historyLimited ? 'partial' : 'complete', ); } catch { if (abortController.signal.aborted || !isCurrentModalTarget()) return; if (this.subagentModalHistoryAbortController) { this.setSubagentModalHistoryStatus('backfilling'); return; } this.setSubagentModalHistoryStatus('partial', { ...this.subagentModalHistoryLimits, unavailable: true, }); // The drill-in view is best effort; the parent chat stays authoritative. } finally { if (this.subagentModalDetailAbortController === abortController) { this.subagentModalDetailAbortController = undefined; } } } private readonly handleSubagentModalLoadEarlier = (): void => { void this.loadEarlierSubagentModalHistory(); }; private async loadEarlierSubagentModalHistory(): Promise { const sessionId = this.subagentModalSessionId; let parentSessionId = this.subagentModalParentSessionId ?? (isSessionRuntimeId(this.selectedSessionId) ? this.selectedSessionId : undefined); let access = this.subagentModalAccess; if ( !access && isSessionRuntimeId(sessionId) && this.childSessionHasIndependentManagedMembership(this.selectedProjectId, sessionId) ) access = 'managed'; access ??= isSessionRuntimeId(sessionId) && isSessionRuntimeId(parentSessionId) ? this.childSessionAccess(this.selectedProjectId, parentSessionId, sessionId) : undefined; if (access === 'managed' && !isSessionRuntimeId(parentSessionId)) parentSessionId = sessionId; const scopeGeneration = this.subagentModalScopeGeneration; const scopeSequence = this.subagentModalScopeSequence; const list = this.subagentModalList; const projectId = this.selectedProjectId; const modalGeneration = this.subagentModalGeneration; const before = this.subagentModalHistoryCursor; const historyScopeGeneration = this.subagentModalHistoryScopeGeneration; const toolStreamEpoch = this.subagentModalHistoryToolStreamEpoch; const messageStreamEpoch = this.subagentModalHistoryMessageStreamEpoch; if ( !isSessionRuntimeId(sessionId) || !isSessionRuntimeId(parentSessionId) || !access || !list || !before || toolStreamEpoch === undefined || messageStreamEpoch === undefined || this.subagentModalHistoryAbortController || this.childSessionAccess(projectId, parentSessionId, sessionId) !== access || (access === 'scoped' && ( !isOpenCodeSessionRuntimeId(parentSessionId) || !isOpenCodeSessionRuntimeId(sessionId) || scopeGeneration === undefined || scopeSequence === undefined || historyScopeGeneration !== scopeGeneration )) ) return; if ( (access === 'managed' && ( (this.liveToolStreamEpochs.get(sessionId.harnessId) ?? 0) !== toolStreamEpoch || (this.liveMessageStreamEpochs.get(sessionId.harnessId) ?? 0) !== messageStreamEpoch )) || (access === 'scoped' && ( scopeSequence !== toolStreamEpoch || scopeSequence !== messageStreamEpoch )) ) { this.subagentModalHistoryCursor = undefined; this.subagentModalHistorySeenCursors.clear(); this.subagentModalHistoryScopeGeneration = undefined; this.subagentModalHistoryToolStreamEpoch = undefined; this.subagentModalHistoryMessageStreamEpoch = undefined; this.subagentModalDetailLoaded = false; if (this.subagentModalProvisionalBundleKeys.size > 0) { this.subagentModalBundles = this.subagentModalBundles.filter((bundle) => ( !this.subagentModalProvisionalBundleKeys.has( controllerRuntimeIdToUiKey(bundle.sourceMessageId), ) )); this.subagentModalProvisionalBundleKeys.clear(); this.subagentModalBundleKeys = new Set(this.subagentModalBundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), )); list.messages = this.subagentModalBundles .flatMap((bundle) => bundle.messages) .filter((messageArg) => messageArg.toolCall?.name !== 'question') .map((messageArg) => this.toHarnessMessage(messageArg, projectId, sessionId)); } this.setSubagentModalHistoryStatus('idle'); void this.refreshSubagentModal(); return; } const abortController = new AbortController(); this.subagentModalHistoryAbortController = abortController; const isCurrentLoad = (): boolean => ( this.subagentModalHistoryAbortController === abortController && modalGeneration === this.subagentModalGeneration && projectId === this.selectedProjectId && this.subagentModalList === list && controllerRuntimeIdsEqual(this.subagentModalSessionId, sessionId) && (this.subagentModalParentSessionId === undefined || controllerRuntimeIdsEqual(this.subagentModalParentSessionId, parentSessionId)) && (this.subagentModalAccess === undefined || this.subagentModalAccess === access) && this.subagentModalHistoryCursor === before && (access === 'managed' ? (this.liveToolStreamEpochs.get(sessionId.harnessId) ?? 0) === toolStreamEpoch && (this.liveMessageStreamEpochs.get(sessionId.harnessId) ?? 0) === messageStreamEpoch : this.subagentModalScopeGeneration === scopeGeneration && this.subagentModalScopeSequence === scopeSequence) && (this.subagentModalRef === undefined || this.subagentModalRef.isConnected) ); this.setSubagentModalHistoryStatus('backfilling'); try { const page = access === 'managed' ? await this.socketClient.fire( 'controller.session.messages.page', { projectId, sessionId, limit: detailHistoryPageLimit, before }, { maxRetries: 0, abortSignal: abortController.signal }, ) : await this.socketClient.fire( 'controller.session.child.messages.page', { projectId, parentSessionId: parentSessionId as TBrowserOpenCodeSessionId, childSessionId: sessionId as TBrowserOpenCodeSessionId, scopeGeneration: scopeGeneration!, limit: detailHistoryPageLimit, before, }, { maxRetries: 0, abortSignal: abortController.signal }, ); if (!isCurrentLoad()) return; const historyLimits: IDetailHistoryLimits = { truncated: this.subagentModalHistoryLimits.truncated || page.truncated === true, historyLimited: this.subagentModalHistoryLimits.historyLimited || page.historyLimited === true, unavailable: false, }; const nextBundles = [...this.subagentModalBundles]; let contentChanged = false; let provisionalConfirmed = false; for (const bundle of page.bundles) { const key = controllerRuntimeIdToUiKey(bundle.sourceMessageId); if (!this.subagentModalProvisionalBundleKeys.delete(key)) continue; provisionalConfirmed = true; const retainedIndex = nextBundles.findIndex((candidate) => ( controllerRuntimeIdToUiKey(candidate.sourceMessageId) === key )); if (retainedIndex < 0) continue; if (JSON.stringify(nextBundles[retainedIndex]) !== JSON.stringify(bundle)) { nextBundles[retainedIndex] = bundle; contentChanged = true; } } const unseen = page.bundles.filter((bundle) => ( !this.subagentModalBundleKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); const bounded = boundedNewestMessageBundles([...unseen, ...nextBundles]); const retainedKeys = new Set(bounded.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), )); const retainedUnseen = unseen.some((bundle) => ( retainedKeys.has(controllerRuntimeIdToUiKey(bundle.sourceMessageId)) )); let nextCursor = page.nextCursor; if ( bounded.limited || (unseen.length > 0 && !retainedUnseen) || (unseen.length === 0 && !provisionalConfirmed && nextCursor !== undefined) || (nextCursor !== undefined && this.subagentModalHistorySeenCursors.has(nextCursor)) ) { historyLimits.historyLimited = true; nextCursor = undefined; } if (nextCursor === undefined && this.subagentModalProvisionalBundleKeys.size > 0) { bounded.bundles = bounded.bundles.filter((bundle) => ( !this.subagentModalProvisionalBundleKeys.has( controllerRuntimeIdToUiKey(bundle.sourceMessageId), ) )); this.subagentModalProvisionalBundleKeys.clear(); contentChanged = true; } const previousKeys = this.subagentModalBundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), ); const nextKeys = bounded.bundles.map( (bundle) => controllerRuntimeIdToUiKey(bundle.sourceMessageId), ); if (contentChanged || !sameStringArray(previousKeys, nextKeys)) { this.subagentModalBundles = bounded.bundles; this.subagentModalBundleKeys = new Set(nextKeys); list.messages = bounded.bundles .flatMap((bundle) => bundle.messages) .filter((messageArg) => messageArg.toolCall?.name !== 'question') .map((messageArg) => this.toHarnessMessage(messageArg, projectId, sessionId)); } this.subagentModalHistoryCursor = nextCursor; if (nextCursor !== undefined) this.subagentModalHistorySeenCursors.add(nextCursor); if (nextCursor === undefined) { this.subagentModalHistoryScopeGeneration = undefined; this.subagentModalHistoryToolStreamEpoch = undefined; this.subagentModalHistoryMessageStreamEpoch = undefined; } this.setSubagentModalHistoryStatus( nextCursor ? 'idle' : historyLimits.truncated || historyLimits.historyLimited ? 'partial' : 'complete', historyLimits, ); await list.updateComplete; } catch { if (!abortController.signal.aborted && isCurrentLoad()) { this.setSubagentModalHistoryStatus('partial', { ...this.subagentModalHistoryLimits, unavailable: true, }); } } finally { if (this.subagentModalHistoryAbortController === abortController) { this.subagentModalHistoryAbortController = undefined; if (this.subagentModalHistoryStatus === 'backfilling') { this.setSubagentModalHistoryStatus('idle'); } } } } private clearSubagentModalState(destroyArg = true): void { this.subagentModalGeneration += 1; const modal = this.subagentModalRef; const list = this.subagentModalList; list?.removeEventListener('harness-load-earlier', this.handleSubagentModalLoadEarlier); this.subagentModalDetailAbortController?.abort(new Error('Subagent modal closed.')); this.subagentModalHistoryAbortController?.abort(new Error('Subagent modal closed.')); this.subagentModalDetailAbortController = undefined; this.subagentModalHistoryAbortController = undefined; this.subagentModalObserver?.disconnect(); this.subagentModalObserver = undefined; if (this.subagentModalRefreshTimer) clearTimeout(this.subagentModalRefreshTimer); this.subagentModalRefreshTimer = undefined; this.subagentModalRefreshInFlight = undefined; this.subagentModalRefreshPending = false; this.subagentModalSessionId = undefined; this.subagentModalParentSessionId = undefined; this.subagentModalAccess = undefined; this.subagentModalScopeGeneration = undefined; this.subagentModalScopeSequence = undefined; this.subagentModalList = undefined; this.subagentModalBundles = []; this.subagentModalBundleKeys.clear(); this.subagentModalCoreBundleKeys.clear(); this.subagentModalProvisionalBundleKeys.clear(); this.subagentModalHistoryCursor = undefined; this.subagentModalHistorySeenCursors.clear(); this.subagentModalHistoryScopeGeneration = undefined; this.subagentModalHistoryToolStreamEpoch = undefined; this.subagentModalHistoryMessageStreamEpoch = undefined; this.subagentModalDetailLoaded = false; this.subagentModalHistoryStatus = 'idle'; this.subagentModalHistoryLimits = emptyDetailHistoryLimits(); this.subagentModalHistoryStatusElement = undefined; this.subagentModalRef = undefined; if (destroyArg && modal?.isConnected) void modal.destroy(); } private readonly handleModeChange = ( eventArg: CustomEvent<{ mode?: unknown }>, ): void => { const mode = eventArg.detail?.mode; const sessionId = this.selectedSessionId; if ( (mode !== 'Ask' && mode !== 'Yolo') || !isSessionRuntimeId(sessionId) ) { return; } const enabled = mode === 'Yolo'; const requestId = ++this.sessionYoloRequestId; const generation = this.connectionGeneration; const projectId = this.selectedProjectId; void (async () => { try { const response = await this.socketClient.fire( 'controller.session.yolo', { projectId, sessionId, enabled }, ); if ( requestId !== this.sessionYoloRequestId || !this.isCurrentConnection(generation) || projectId !== this.selectedProjectId || !controllerRuntimeIdsEqual(this.selectedSessionId, sessionId) ) return; const currentDetail = this.sessionDetail; if (currentDetail && controllerRuntimeIdsEqual(currentDetail.session.id, sessionId)) { this.sessionDetail = { ...currentDetail, ...(response.enabled ? { autoAcceptPermissions: true } : {}), ...(response.enabled ? {} : { autoAcceptPermissions: undefined }), }; } } catch (error) { if (requestId !== this.sessionYoloRequestId) return; this.reportWorkspaceError('Change the conversation mode', error); if (controllerRuntimeIdsEqual(this.selectedSessionId, sessionId)) { await this.loadSessionDetail(sessionId); } } })(); }; private clearAnsweredCardCaches(): void { this.answeredQuestionCards.clear(); this.answeredPermissionCards.clear(); this.pendingQuestionBatchAnswers.clear(); } private async submitQuestionReply( requestIdArg: interfaces.IControllerRuntimeId, answersArg: string[][], childAttentionArg?: interfaces.IControllerChildAttention, ): Promise { const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; const sessionId = this.selectedSessionId; try { if (childAttentionArg) { await this.socketClient.fire( 'controller.session.child.question.reply', { projectId: childAttentionArg.projectId, parentSessionId: childAttentionArg.parentSessionId, childSessionId: childAttentionArg.childSessionId, scopeGeneration: childAttentionArg.scopeGeneration, requestId: requestIdArg as interfaces.IControllerRuntimeId & { harnessId: 'opencode' }, answers: answersArg, }, ); } else { await this.socketClient.fire( 'controller.question.reply', { projectId: this.selectedProjectId, sessionId: sessionId!, requestId: requestIdArg, answers: answersArg, }, ); } if (this.isCurrentMutation(mutationId, generation, sessionId) && sessionId) { await this.loadSessionDetail(sessionId); } } catch (error) { if (this.isCurrentMutation(mutationId, generation, sessionId)) { this.reportWorkspaceError('Answer the question', error); } } finally { this.finishMutation(mutationId); } } private readonly showDeleteConfirmation = (): void => { const sessionId = this.selectedSessionId; const sessionTitle = this.sessionDetail?.session.title || (sessionId ? runtimeIdDisplay(sessionId) : ''); if (!isSessionRuntimeId(sessionId)) { return; } void this.showDeleteConfirmationFor(sessionId, sessionTitle); }; private async showDeleteConfirmationFor( sessionIdArg: TBrowserSessionId, sessionTitleArg: string, ): Promise { if (this.mutationPending) { return false; } const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Delete conversation?', width: 'small', content: plugins.deesElement.html`

${sessionIdArg.harnessId === 'flex' ? plugins.deesElement.html` The managed Flex conversation “${sessionTitleArg || runtimeIdDisplay(sessionIdArg)}” and its exact captured descendant subtree will be deleted from FlexHarness. ` : plugins.deesElement.html` The selected ${sessionHarnessLabel(sessionIdArg.harnessId)} conversation “${sessionTitleArg || runtimeIdDisplay(sessionIdArg)}” will be deleted from ${sessionHarnessLabel(sessionIdArg.harnessId)}. `} This action cannot be undone.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy(), }, { name: 'Delete', action: async (modalArg) => { await modalArg?.destroy(); await this.deleteSession(sessionIdArg); }, }, ], }); return modal.isConnected; } private async deleteSession(sessionIdArg: TBrowserSessionId): Promise { const mutationId = this.beginMutation(); if (!isSessionRuntimeId(sessionIdArg) || !mutationId) { this.finishMutation(mutationId); return; } const generation = this.connectionGeneration; const projectId = this.selectedProjectId; let refreshAfterDelete = false; try { const response = await this.socketClient.fire( 'controller.session.delete', { projectId, sessionId: sessionIdArg }, ); if (response.success !== true) { throw new Error('The controller did not confirm conversation deletion.'); } if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId ) { return; } this.sessions = this.sessions.filter((sessionArg) => ( !isSessionRuntimeId(sessionArg.id) || !controllerRuntimeIdsEqual(sessionArg.id, sessionIdArg) )); const keepSessionItem = ( candidateArg: interfaces.TControllerLayoutItemRef, ): boolean => candidateArg.kind !== 'session' || !controllerRuntimeIdsEqual(candidateArg.id, sessionIdArg); this.sessionGroups = this.sessionGroups.map((group) => ({ ...group, itemIds: group.itemIds.filter(keepSessionItem), })); this.ungroupedItemIds = this.ungroupedItemIds.filter(keepSessionItem); const choiceKey = this.sessionOperationKey(projectId, sessionIdArg); this.sessionModelOverrides.delete(choiceKey); this.sessionEffortOverrides.delete(choiceKey); this.sessionAccountOverrides.delete(choiceKey); if (controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg)) { this.cancelComposerFocusForSession(projectId, sessionIdArg); this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; } refreshAfterDelete = true; } catch (error) { if ( this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId ) { this.reportWorkspaceError('Delete the conversation', error); } } finally { this.finishMutation(mutationId); } if (refreshAfterDelete) void this.refreshSessions(); } private async loadTerminals(forceArg = false): Promise { const projectId = this.selectedProjectId; const generation = this.connectionGeneration; if ( !isNonEmptyString(projectId) || !this.authenticated || !this.socketClient.isConnected || (!forceArg && this.terminalsLoadedForProjectId === projectId) ) { return; } const requestId = ++this.terminalRequestId; try { const response = await this.socketClient.fire( 'controller.terminal.list', { projectId }, ); if ( requestId !== this.terminalRequestId || !this.authenticated || !this.isCurrentConnection(generation) || this.selectedProjectId !== projectId || !this.projects.some((project) => project.id === projectId) ) { return; } const terminals = Array.isArray(response.terminals) ? response.terminals.filter(hasTerminalRuntimeId) : []; const reconciledTerminals = reconcileKeyedJsonArray( this.terminals, terminals, (terminal) => controllerRuntimeIdToUiKey(terminal.id), ); if (reconciledTerminals !== this.terminals) this.terminals = reconciledTerminals; this.terminalsLoadedForProjectId = projectId; if ( this.selectedTerminalId !== undefined && !terminals.some((terminalArg) => ( isTerminalRuntimeId(terminalArg.id) && controllerRuntimeIdsEqual(terminalArg.id, this.selectedTerminalId) )) && !this.selectedTerminalEnded ) { // The viewed terminal disappeared (removed elsewhere). this.closeTerminalView(); } } catch { // Terminals are optional sidebar entries; chats work without them. } } private loadResources(forceArg = false): Promise { const projectId = this.selectedProjectId; if ( !isNonEmptyString(projectId) || !this.authenticated || !this.socketClient.isConnected || (!forceArg && this.resourcesLoadedForProjectId === projectId) ) return Promise.resolve(); this.resourceRefreshPending = true; this.resourceRefreshReportErrors ||= forceArg; if (this.resourceRefreshInFlight) return this.resourceRefreshInFlight; const task = Promise.resolve().then(() => this.runResourceRefreshQueue()); this.resourceRefreshInFlight = task; return task; } private async runResourceRefreshQueue(): Promise { try { while (this.resourceRefreshPending) { const reportErrors = this.resourceRefreshReportErrors; this.resourceRefreshPending = false; this.resourceRefreshReportErrors = false; if (!this.authenticated || !this.socketClient.isConnected) return; await this.performResourceRefresh(reportErrors); } } finally { this.resourceRefreshInFlight = undefined; } } private async performResourceRefresh(reportErrorsArg: boolean): Promise { const projectId = this.selectedProjectId; const generation = this.connectionGeneration; if (!isNonEmptyString(projectId)) return; const requestId = ++this.resourceRequestId; try { const response = await this.socketClient.fire( 'controller.resource.list', { projectId }, ); if ( requestId !== this.resourceRequestId || !this.authenticated || !this.isCurrentConnection(generation) || this.selectedProjectId !== projectId || !this.projects.some((project) => project.id === projectId) ) return; this.resources = Array.isArray(response.resources) ? response.resources : []; this.resourcesLoadedForProjectId = projectId; const viewedBrowserResource = this.resources.find((resource): resource is interfaces.IControllerBrowserResource => ( resource.id === this.browserViewResourceId && resource.kind === 'browser' )); if ( this.browserViewId && ( !viewedBrowserResource || viewedBrowserResource.lifecycle !== 'active' || viewedBrowserResource.attachment.revision !== this.browserViewAttachmentRevision ) ) { const reopen = Boolean( viewedBrowserResource && viewedBrowserResource.lifecycle === 'active' && viewedBrowserResource.browserRuntimeState === 'available' && this.selectedResourceId === viewedBrowserResource.id, ); await this.closeBrowserView(); if ( reopen && viewedBrowserResource && this.selectedResourceId === viewedBrowserResource.id && this.selectedProjectId === projectId && this.isCurrentConnection(generation) ) await this.openBrowserView(viewedBrowserResource); } const reopenResourceId = this.browserViewReopenResourceId; if (reopenResourceId && !this.browserViewId && !this.browserViewLoading) { const reopenResource = this.resources.find((resource): resource is interfaces.IControllerBrowserResource => ( resource.id === reopenResourceId && resource.kind === 'browser' )); if ( reopenResource && reopenResource.lifecycle === 'active' && reopenResource.browserRuntimeState === 'available' && this.selectedResourceId === reopenResource.id && this.selectedProjectId === projectId && this.isCurrentConnection(generation) ) { await this.openBrowserView(reopenResource); } else if ( !reopenResource || reopenResource.lifecycle !== 'active' || this.selectedResourceId !== reopenResource.id ) { this.browserViewReopenResourceId = ''; } } if ( this.selectedResourceId && !this.resources.some((resource) => resource.id === this.selectedResourceId) ) { this.selectedResourceId = ''; this.closeTerminalView(); } } catch (errorArg) { if ( reportErrorsArg && requestId === this.resourceRequestId && this.isCurrentConnection(generation) && this.selectedProjectId === projectId ) this.reportWorkspaceError('Load resources', errorArg); } } private terminalViewElement(): plugins.deesCatalog.DeesTerminalView | undefined { const element = this.shadowRoot?.querySelector('dees-terminal-view'); return element instanceof plugins.deesCatalog.DeesTerminalView ? element : undefined; } /** * Frames carry their absolute stream offset because the controller retries an output frame * whose delivery was not acknowledged. That makes three cases distinguishable that used to be * indistinguishable: an already painted range (discard), a contiguous range (paint), and a * lost window (re-attach rather than silently corrupt the screen). * * Every attachment opens with the reconstructed screen state instead of a raw scrollback * replay, so the anchor those three cases are judged against is established by a snapshot. */ private handleTerminalOutput( outputArg: interfaces.IReq_ControllerTerminalOutput['request'], ): void { const terminalId = this.selectedTerminalId; if ( !isTerminalRuntimeId(terminalId) || !controllerRuntimeIdsEqual(outputArg.terminalId, terminalId) ) { return; } const bytes = base64ToBytes(outputArg.dataBase64); const ended = outputArg.ended === true; if (outputArg.snapshot) { this.collectTerminalSnapshotFrame(terminalId, outputArg.snapshot, outputArg.offset, bytes); return; } const appliedEnd = this.selectedTerminalAppliedEnd; if (appliedEnd === undefined) { // Nothing is anchored, which a pre-mount overflow leaves behind after discarding bytes this // frame may continue from. Painting it would paint over that hole, so the client asks for a // state it can place instead. this.requestTerminalReattach(terminalId); return; } if (bytes.byteLength === 0) { if (ended) this.applyTerminalFrame({ offset: outputArg.offset, bytes, ended: true }); return; } const frameEnd = outputArg.offset + bytes.byteLength; if (frameEnd <= appliedEnd) { // Re-sent after an unacknowledged delivery; these bytes are already on screen. if (ended) { this.applyTerminalFrame({ offset: appliedEnd, bytes: new Uint8Array(0), ended: true }); } return; } if (outputArg.offset > appliedEnd) { this.requestTerminalReattach(terminalId); return; } // Partial overlap: paint only what is not on screen yet. const fresh = outputArg.offset === appliedEnd ? bytes : bytes.subarray(appliedEnd - outputArg.offset); this.selectedTerminalAppliedEnd = frameEnd; this.applyTerminalFrame({ offset: appliedEnd, bytes: fresh, ended }); } /** * Collects the frames of one reconstructed screen state and applies it as a whole. * * Every frame of a state carries the stream offset that state is exact at, so the offset rule * raw frames are deduplicated by cannot order them; `index` orders them within that offset. A * frame repeating an index already collected, and any frame of a state already applied, is a * retry: applying it again would collect a chunk twice or drop the live output painted since, * so it is ignored. A gap in the indices means a frame was lost and only a whole state can * recover it. A hand-over the controller makes at another stream position restarts at index 0 * and replaces what is on screen. A collection that passes the client's in-flight byte bound is * dropped and re-attached for the same reason the pre-mount buffer overflows: what cannot be * applied faithfully is not worth holding. */ private collectTerminalSnapshotFrame( terminalIdArg: TBrowserTerminalId, frameArg: interfaces.IControllerTerminalSnapshotFrame, offsetArg: number, bytesArg: Uint8Array, ): void { let pending = this.terminalSnapshot; if (pending && pending.offset === offsetArg) { // Re-sent after an unacknowledged delivery: this chunk is already collected or applied. if (frameArg.index < pending.nextIndex) return; if (frameArg.index > pending.nextIndex || pending.complete) { this.requestTerminalReattach(terminalIdArg); return; } } else if (frameArg.index === 0) { // This state replaces the screen, so a collection left half-finished at another position // goes with it, and so does the pre-mount buffer: a view mounting between this frame and // the last one would otherwise flush the previous screen for a moment before the state // lands on it. The completed state supersedes that buffer in any case. this.pendingTerminalOutput = emptyPendingTerminalOutput(); pending = { offset: offsetArg, nextIndex: 0, complete: false, grid: { cols: frameArg.cols, rows: frameArg.rows }, chunks: [], bytes: 0, }; this.terminalSnapshot = pending; } else { // A continuation of a state whose opening frame never arrived. this.requestTerminalReattach(terminalIdArg); return; } if (pending.bytes + bytesArg.byteLength > maxPendingTerminalOutputBytes) { // The client holds at most one terminal's in-flight bytes, whether they are buffered live // output or a state being collected, so both are bounded by the same constant. A state is // larger than the reconstructed history the controller bounds at 512 KiB — it carries the // positioning and SGR sequences that reproduce it — but a collection that passes this cap // is one no `last` frame is going to close, and holding it would grow until the tab dies. this.terminalSnapshot = undefined; this.requestTerminalReattach(terminalIdArg); return; } pending.chunks.push(bytesArg); pending.bytes += bytesArg.byteLength; pending.nextIndex = frameArg.index + 1; if (!frameArg.last) return; const state = concatBytes(pending.chunks); // Collected chunks are only ever needed until the state is applied: a later frame of it is a // retry, which is ignored, and a gap re-attaches for a fresh state. pending.chunks = []; pending.bytes = 0; pending.complete = true; // The peer's cursor is exactly here once the state is complete, so the next raw frame is // contiguous with it. this.selectedTerminalAppliedEnd = offsetArg; this.selectedTerminalAttached = true; this.applyTerminalFrame({ offset: offsetArg, bytes: state, restore: pending.grid }); } private applyTerminalFrame(frameArg: { offset: number; bytes: Uint8Array; /** The bytes are a reconstructed screen state: restore them into this grid, never write them. */ restore?: IPendingTerminalRestoreGrid; ended?: boolean; }): void { const view = this.terminalViewElement(); if (!view) { this.pendingTerminalOutput = appendPendingTerminalOutput(this.pendingTerminalOutput, { offset: frameArg.offset, bytes: frameArg.bytes, ...(frameArg.restore ? { restore: frameArg.restore } : {}), ...(frameArg.ended ? { ended: true } : {}), }); if (this.pendingTerminalOutput.overflowed) { // The buffer can no longer be flushed faithfully, and truncating it would paint a // silent gap. A fresh attachment, which opens with the terminal's current state, is the // only honest recovery. // // Dropping the anchor is what makes that recovery reliable. These bytes were discarded, // so the anchor must not keep claiming them: the re-attach below is refused while an // attach is already in flight, which is exactly the pre-mount window this branch runs // in. Without clearing it, the next frame would satisfy `offset === appliedEnd` and // paint contiguously over the hole. With no anchor, every later raw frame takes the // "no anchor -> re-attach" path until one of those re-attaches is admitted — which is // the one way a raw frame can arrive with nothing anchored, now that every attachment // opens with a state. this.selectedTerminalAppliedEnd = undefined; const terminalId = this.selectedTerminalId; if (isTerminalRuntimeId(terminalId)) this.requestTerminalReattach(terminalId); return; } this.requestUpdate(); return; } if (frameArg.restore) { // The view paints the state invisibly at the grid it was serialized at and fits it // afterwards, which is what makes the following `terminal-resize` the real PTY size. void view.restore(frameArg.bytes, frameArg.restore); } else if (frameArg.bytes.byteLength > 0) { view.write(frameArg.bytes); } if (frameArg.ended) { this.selectedTerminalEnded = true; } } private handleTerminalDetached( detachedArg: interfaces.IReq_ControllerTerminalDetached['request'], ): void { const terminalId = this.selectedTerminalId; if ( !isTerminalRuntimeId(terminalId) || !controllerRuntimeIdsEqual(detachedArg.terminalId, terminalId) ) { return; } this.requestTerminalReattach(terminalId); } /** Re-attaches at most once per lost attachment; an attachment always opens with a snapshot. */ private requestTerminalReattach(terminalIdArg: TBrowserTerminalId): void { if (!controllerRuntimeIdsEqual(this.selectedTerminalId, terminalIdArg)) return; if (this.terminalAttachPending) return; this.selectedTerminalAttached = false; this.resetTerminalStream(); this.attachSelectedTerminal(terminalIdArg); } private resetTerminalStream(): void { this.selectedTerminalAppliedEnd = undefined; this.terminalSnapshot = undefined; this.pendingTerminalOutput = emptyPendingTerminalOutput(); } private requestComposerFocus( modeArg: IComposerFocusRequest['mode'], sessionIdArg: TBrowserSessionId | undefined, projectIdArg = this.selectedProjectId, readyArg = true, ): number | undefined { if (!isNonEmptyString(projectIdArg)) return undefined; const request: IComposerFocusRequest = { id: ++this.composerFocusRequestId, projectId: projectIdArg, mode: modeArg, sessionId: sessionIdArg, ready: readyArg, }; this.composerFocusRequest = request; this.requestUpdate(); return request.id; } private cancelComposerFocus(): void { this.composerFocusRequestId += 1; this.composerFocusRequest = undefined; } /** * A rejected modal or table action is announced by the catalog, which shows * the rejected value's own message in a toast unless the host cancels the * event. AGL always cancels it: an action can reject with controller-internal * detail (backend text, ports, paths, hostnames), so only AGL's own sanitized * sentence reaches the UI. The raw error stays in the browser console. * * The event is dispatched on the modal, which lives in `document.body`, so the * listener belongs on `document` rather than on this element. The toast is * mounted without an owner element on purpose: the catalog then resolves the * open dialog from the focus scope, so a `modal-menu` failure is shown inside * that dialog's top layer instead of behind it. */ private readonly handleActionError = (eventArg: Event): void => { const { detail } = eventArg as CustomEvent; eventArg.preventDefault(); console.error('A catalog action failed.', detail?.source, detail?.error); void plugins.deesCatalog.DeesToast.show({ message: actionErrorText(detail?.source), type: 'error', }); }; private readonly handleDocumentFocusIn = (eventArg: FocusEvent): void => { if (!this.composerFocusRequest) return; const focusStayedInComposer = eventArg.composedPath().some( (targetArg) => targetArg instanceof HTMLElement && targetArg.localName === 'dees-harness-composer', ); if (!focusStayedInComposer) this.cancelComposerFocus(); }; private cancelComposerFocusById(requestIdArg: number | undefined): void { if (requestIdArg !== undefined && this.composerFocusRequest?.id === requestIdArg) { this.cancelComposerFocus(); } } private cancelComposerFocusForSession( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { const request = this.composerFocusRequest; if ( request?.mode === 'session' && request.projectId === projectIdArg && controllerRuntimeIdsEqual(request.sessionId, sessionIdArg) ) { this.cancelComposerFocus(); } } private retargetComposerFocus( requestIdArg: number | undefined, modeArg: IComposerFocusRequest['mode'], sessionIdArg: TBrowserSessionId | undefined, readyArg: boolean, ): void { const request = this.composerFocusRequest; if (requestIdArg === undefined || request?.id !== requestIdArg) return; this.composerFocusRequest = { ...request, mode: modeArg, sessionId: sessionIdArg, ready: readyArg, }; this.requestUpdate(); } private prepareSendComposerFocus( requestIdArg: number | undefined, projectIdArg: string, sessionIdArg: TBrowserSessionId, wasDraftArg: boolean, ): number | undefined { if (requestIdArg !== undefined) { if (this.composerFocusRequest?.id !== requestIdArg) return undefined; this.retargetComposerFocus( requestIdArg, wasDraftArg ? 'draft' : 'session', sessionIdArg, true, ); return requestIdArg; } return this.requestComposerFocus( wasDraftArg ? 'draft' : 'session', sessionIdArg, projectIdArg, ); } private requestCurrentComposerFocus(): number | undefined { if (this.draftSessionActive && this.sessionDetail === undefined) { return this.requestComposerFocus('draft', this.selectedSessionId); } const sessionId = this.sessionDetail?.session.id; return isSessionRuntimeId(sessionId) ? this.requestComposerFocus('session', sessionId) : undefined; } private composerFocusRequestMatches(requestArg: IComposerFocusRequest): boolean { if ( !requestArg.ready || !this.authenticated || this.selectedProjectId !== requestArg.projectId || this.selectedTerminalId !== undefined || this.mutationPending || this.detailLoading ) return false; if (requestArg.mode === 'draft') { return ( this.draftSessionActive && this.sessionDetail === undefined && controllerRuntimeIdsEqual(this.selectedSessionId, requestArg.sessionId) ); } return ( !this.draftSessionActive && controllerRuntimeIdsEqual(this.selectedSessionId, requestArg.sessionId) && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, requestArg.sessionId) ); } private flushComposerFocus(): void { const request = this.composerFocusRequest; if ( !request || this.composerFocusInFlight || !this.composerFocusRequestMatches(request) ) return; const chat = this.shadowRoot?.querySelector('dees-harness-chat'); if (!(chat instanceof plugins.deesCatalog.DeesHarnessChat)) return; const focusPromise = (async (): Promise => { await chat.updateComplete; const current = this.composerFocusRequest; if ( current?.id !== request.id || !this.composerFocusRequestMatches(current) || this.shadowRoot?.querySelector('dees-harness-chat') !== chat ) return; await chat.focusComposer(); if (this.composerFocusRequest?.id === request.id) { this.composerFocusRequest = undefined; } })(); this.composerFocusInFlight = focusPromise; void focusPromise.catch(() => { this.cancelComposerFocusById(request.id); }).finally(() => { if (this.composerFocusInFlight === focusPromise) { this.composerFocusInFlight = undefined; } if (this.composerFocusRequest) { this.requestUpdate(); } }); } public updated(changedPropertiesArg: Map): void { super.updated(changedPropertiesArg); if (this.authenticated) this.updateSystemMetricsDom(); const scroller = this.shadowRoot?.querySelector('.metricsScroller'); if (scroller !== this.footerMetricsScroller) { this.footerMetricsResizeObserver?.disconnect(); this.footerMetricsScroller = scroller ?? undefined; if (scroller && typeof ResizeObserver !== 'undefined') { this.footerMetricsResizeObserver = new ResizeObserver(() => this.updateFooterScrollState()); this.footerMetricsResizeObserver.observe(scroller); } else { this.footerMetricsResizeObserver = undefined; } } this.updateFooterScrollState(); if (this.statsPanel && (!this.authenticated || !this.shadowRoot?.querySelector('.statsTrigger'))) { void this.closeStatsPanel(false); } if ( this.metricPopoverSelection && (!this.authenticated || !this.metricPopoverAnchor?.isConnected) ) this.closeMetricPopover(); if ( this.accountLimitsOpen && (!this.authenticated || !this.shadowRoot?.querySelector('agl-account-limits')) ) this.accountLimitsOpen = false; if ( (changedPropertiesArg.has('connectionStatus') || changedPropertiesArg.has('authenticated')) && this.connectionStatus === 'connected' && this.authenticated ) { const accountLimits = this.shadowRoot?.querySelector('agl-account-limits'); accountLimits?.invalidate(); void accountLimits?.refresh(); } this.flushComposerFocus(); const projection = this.canonicalChatProjection; const chat = projection ? this.mountedCanonicalChat(projection) : undefined; if (chat) this.syncMountedComposerValue(chat); if (this.canonicalTimelineRefreshPending) { this.canonicalTimelineRefreshPending = false; this.canonicalMessageRefreshIds.clear(); chat?.refreshMessages(); } else if (this.canonicalMessageRefreshIds.size > 0) { const messageIds = [...this.canonicalMessageRefreshIds]; this.canonicalMessageRefreshIds.clear(); chat?.refreshMessages(messageIds); } if ( changedPropertiesArg.has('selectedProjectId') || changedPropertiesArg.has('selectedSessionId') ) { this.clearSubtaskPreviewState(); this.pruneLiveToolStateToSelection(); } if (['selectedProjectId', 'selectedSessionId', 'draftHarnessId', 'draftSessionActive'].some(key => changedPropertiesArg.has(key)) && (this.selectedSessionId?.harnessId === 'codex' || (this.draftSessionActive && this.draftHarnessId === 'codex'))) { void this.refreshModelCatalog(); } if (this.pendingTerminalOutput.entries.length === 0 && !this.pendingTerminalOutput.ended) { return; } const view = this.terminalViewElement(); if (!view) { return; } const pending = this.pendingTerminalOutput; this.pendingTerminalOutput = emptyPendingTerminalOutput(); for (const item of pending.entries) { if (item.restore) void view.restore(item.bytes, item.restore); else if (item.bytes.byteLength > 0) view.write(item.bytes); } if (pending.ended) this.selectedTerminalEnded = true; } private openTerminalById( terminalIdArg: TBrowserTerminalId, preserveSessionSelection = false, ): void { if (!this.terminals.some((terminalArg) => ( isTerminalRuntimeId(terminalArg.id) && controllerRuntimeIdsEqual(terminalArg.id, terminalIdArg) ))) { return; } if (controllerRuntimeIdsEqual(this.selectedTerminalId, terminalIdArg)) { // Re-selecting the open terminal is the user's manual recovery when the client believes it // lost its attachment. A healthy one is left alone: re-attaching restarts the stream. if (!this.selectedTerminalAttached) this.requestTerminalReattach(terminalIdArg); this.focusSelectedTerminal(terminalIdArg); return; } this.beginWorkspaceSelection(); this.selectedResourceId = terminalIdArg.nativeId; void this.closeBrowserView(); this.closeTerminalView(); if (!preserveSessionSelection) this.deselectConversationForStandaloneResource(); this.selectedTerminalId = terminalIdArg; this.selectedTerminalEnded = false; this.resetTerminalStream(); this.attachSelectedTerminal(terminalIdArg); this.focusSelectedTerminal(terminalIdArg); } /** * Puts the caret on the terminal the user just selected, so the next keystroke reaches the pty * instead of whatever the page focused last. The view is rendered by the next update and * `DeesTerminalView` remembers a `focus()` that arrives before xterm exists, so this waits for * the element to mount but never for the terminal to come up. * * It is armed only by an explicit selection, never by a reattach or by output, so a reconnect * cannot take the caret back from wherever the user has moved on to. A newer selection during * the wait cancels it. */ private focusSelectedTerminal(terminalIdArg: TBrowserTerminalId): void { const selectionGeneration = this.workspaceSelectionGeneration; void (async () => { await this.updateComplete; if (this.workspaceSelectionGeneration !== selectionGeneration) return; if (!controllerRuntimeIdsEqual(this.selectedTerminalId, terminalIdArg)) return; this.terminalViewElement()?.focus(); })(); } private attachSelectedTerminal(terminalIdArg: TBrowserTerminalId): void { const generation = ++this.terminalAttachGeneration; this.terminalAttachPending = true; this.selectedTerminalAttached = true; void (async () => { try { await this.socketClient.fire( 'controller.terminal.attach', { projectId: this.selectedProjectId, terminalId: terminalIdArg }, ); // Attach now returns once the peer is admitted rather than once the whole scrollback has // been replayed, so the view is not guaranteed to be mounted yet. Waiting for the render // is what keeps the initial PTY size negotiation from being skipped. await this.updateComplete; if (generation !== this.terminalAttachGeneration) return; const size = this.terminalViewElement()?.currentSize; if (size && controllerRuntimeIdsEqual(this.selectedTerminalId, terminalIdArg)) { await this.fireTerminalResize(terminalIdArg, size.rows, size.cols); } } catch (error) { if (generation !== this.terminalAttachGeneration) return; this.selectedTerminalAttached = false; if (controllerRuntimeIdsEqual(this.selectedTerminalId, terminalIdArg)) { this.reportWorkspaceError('Attach the terminal', error); } } finally { if (generation === this.terminalAttachGeneration) this.terminalAttachPending = false; } })(); } private closeTerminalView(): void { const terminalId = this.selectedTerminalId; if (!isTerminalRuntimeId(terminalId)) { return; } this.selectedTerminalId = undefined; this.selectedTerminalEnded = false; this.selectedTerminalAttached = false; this.terminalAttachGeneration += 1; this.terminalAttachPending = false; this.resetTerminalStream(); void this.socketClient.fire( 'controller.terminal.detach', { projectId: this.selectedProjectId, terminalId }, ).catch(() => undefined); } private async fireTerminalResize( terminalIdArg: TBrowserTerminalId, rowsArg: number, colsArg: number, ): Promise { await this.socketClient.fire( 'controller.terminal.resize', { projectId: this.selectedProjectId, terminalId: terminalIdArg, rows: rowsArg, cols: colsArg, }, ).catch(() => undefined); } private readonly handleTerminalInput = ( eventArg: CustomEvent<{ data?: unknown }>, ): void => { const data = eventArg.detail?.data; const terminalId = this.selectedTerminalId; if (typeof data !== 'string' || data.length === 0 || !isTerminalRuntimeId(terminalId)) { return; } void this.socketClient.fire( 'controller.terminal.input', { projectId: this.selectedProjectId, terminalId, dataBase64: bytesToBase64(new TextEncoder().encode(data)), }, ).catch(() => undefined); }; private readonly handleTerminalResize = ( eventArg: CustomEvent<{ rows?: unknown; cols?: unknown }>, ): void => { const rows = eventArg.detail?.rows; const cols = eventArg.detail?.cols; const terminalId = this.selectedTerminalId; if ( typeof rows !== 'number' || typeof cols !== 'number' || !isTerminalRuntimeId(terminalId) ) { return; } void this.fireTerminalResize(terminalId, rows, cols); }; private async createResource( kindArg: interfaces.TControllerResourceKind, agentArg?: interfaces.TControllerTerminalAgentKind, ): Promise { const mutationId = this.beginMutation(); if (!mutationId || !isNonEmptyString(this.selectedProjectId)) { this.finishMutation(mutationId); return; } const generation = this.connectionGeneration; const projectId = this.selectedProjectId; const selectionGeneration = this.workspaceSelectionGeneration; try { const response = await this.socketClient.fire( 'controller.resource.create', { projectId, kind: kindArg, ...(agentArg === undefined ? {} : { agent: agentArg }) }, ); if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId || !response.resource ) { return; } await this.loadResources(true); await this.loadTerminals(true); if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId || this.workspaceSelectionGeneration !== selectionGeneration ) return; const resource = this.resources.find((candidate) => ( candidate.id === response.resource.id && candidate.projectId === projectId && candidate.kind === kindArg && candidate.lifecycle === 'active' )); if (resource) this.selectResource(resource); return; } catch (error) { if (this.isCurrentMutation(mutationId, generation)) { this.reportWorkspaceError('Create the resource', error); } } finally { this.finishMutation(mutationId); } } private async renameTerminal(terminalIdArg: TBrowserTerminalId, titleArg: string): Promise { const title = titleArg.trim(); if (!isNonEmptyString(title)) { return; } const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; try { await this.socketClient.fire( 'controller.terminal.rename', { projectId: this.selectedProjectId, terminalId: terminalIdArg, title }, ); if (this.isCurrentMutation(mutationId, generation)) { await this.loadTerminals(true); } } catch (error) { if (this.isCurrentMutation(mutationId, generation)) { this.reportWorkspaceError('Rename the terminal', error); } } finally { this.finishMutation(mutationId); } } private async removeTerminal(terminalIdArg: TBrowserTerminalId): Promise { const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; const projectId = this.selectedProjectId; try { await this.socketClient.fire( 'controller.terminal.remove', { projectId, terminalId: terminalIdArg }, ); if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId ) { return; } if (controllerRuntimeIdsEqual(this.selectedTerminalId, terminalIdArg)) { this.selectedTerminalId = undefined; this.selectedTerminalEnded = false; this.selectedTerminalAttached = false; this.terminalAttachGeneration += 1; this.terminalAttachPending = false; this.resetTerminalStream(); } await this.loadTerminals(true); } catch (error) { if ( this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId ) { this.reportWorkspaceError('Remove the terminal', error); } } finally { this.finishMutation(mutationId); } } private async loadSessionGroups(forceArg = false): Promise { const generation = this.connectionGeneration; if (this.pendingSessionLayout !== undefined || this.layoutSavePromise !== undefined) { if (forceArg) this.pendingLayoutReload = true; return; } if ( !this.authenticated || !this.socketClient.isConnected || (!forceArg && this.groupsLoaded) ) { return; } const requestId = ++this.layoutRequestId; try { const response = await this.socketClient.fire( 'controller.sessiongroups.get', {}, { timeoutMs: layoutRequestTimeoutMs, maxRetries: 0 }, ); if ( requestId !== this.layoutRequestId || !this.authenticated || !this.isCurrentConnection(generation) ) { return; } const layout = cloneSessionLayout({ groups: Array.isArray(response.groups) ? response.groups : [], ungroupedItemIds: Array.isArray(response.ungroupedItemIds) ? response.ungroupedItemIds : [], revision: Number.isSafeInteger(response.revision) ? response.revision : 0, }); const confirmedRevision = this.confirmedSessionLayout?.revision; if (confirmedRevision !== undefined && layout.revision < confirmedRevision) return; this.confirmedSessionLayout = layout; this.applySessionLayout(layout); this.groupsLoaded = true; this.sessionLayoutError = ''; } catch (error) { if ( requestId === this.layoutRequestId && this.authenticated && this.isCurrentConnection(generation) ) { this.sessionLayoutError = `Conversation groups could not be loaded: ${errorMessage(error)}`; } } } private applySessionLayout(layoutArg: interfaces.IControllerSessionLayout): void { const layout = cloneSessionLayout(layoutArg); // A conversation archived in AGL leaves the ordered list; the archive view keeps it. const archivedKeys = new Set( this.conversations .filter((conversationArg) => conversationArg.archivedAt !== undefined) .map((conversationArg) => conversationUiKey( conversationArg.projectId, conversationArg.session.id, )), ); const keepItem = (itemRefArg: interfaces.TControllerLayoutItemRef): boolean => ( itemRefArg.kind === 'resource' ? true : isSessionRuntimeId(itemRefArg.id) && !archivedKeys.has(conversationUiKey(itemRefArg.projectId, itemRefArg.id)) ); this.sessionGroups = layout.groups.map((groupArg) => ({ ...groupArg, itemIds: groupArg.itemIds.filter(keepItem), })); this.ungroupedItemIds = layout.ungroupedItemIds.filter(keepItem); } private persistSessionLayout( nextGroupsArg: interfaces.IControllerSessionGroup[], nextUngroupedItemIdsArg = this.ungroupedItemIds, ): void { const layout = cloneSessionLayout({ groups: nextGroupsArg, ungroupedItemIds: nextUngroupedItemIdsArg, revision: this.confirmedSessionLayout?.revision ?? 0, }); this.layoutRequestId += 1; this.applySessionLayout(layout); this.pendingSessionLayout = layout; if (this.layoutSavePromise !== undefined) return; const generation = this.connectionGeneration; const savePromise = this.flushSessionLayoutSaves(generation); this.layoutSavePromise = savePromise; void savePromise.finally(() => { if (this.layoutSavePromise === savePromise) this.layoutSavePromise = undefined; if (!this.authenticated || !this.isCurrentConnection(generation)) return; if (this.pendingLayoutReload) { this.pendingLayoutReload = false; void this.loadSessionGroups(true); } }); } /** Reading through a method keeps the queued layout a value, not a narrowed field. */ private takePendingSessionLayout(): interfaces.IControllerSessionLayout | undefined { const pending = this.pendingSessionLayout; this.pendingSessionLayout = undefined; return pending; } private peekPendingSessionLayout(): interfaces.IControllerSessionLayout | undefined { return this.pendingSessionLayout; } private async flushSessionLayoutSaves(generationArg: number): Promise { while (this.authenticated && this.isCurrentConnection(generationArg)) { const layout = this.takePendingSessionLayout(); if (layout === undefined) return; try { const response = await this.socketClient.fire( 'controller.sessiongroups.update', { groups: layout.groups, ungroupedItemIds: layout.ungroupedItemIds, expectedRevision: layout.revision, }, { timeoutMs: layoutRequestTimeoutMs, maxRetries: 0 }, ); if (!this.authenticated || !this.isCurrentConnection(generationArg)) return; const confirmed = cloneSessionLayout({ groups: response.groups, ungroupedItemIds: response.ungroupedItemIds, revision: response.revision, }); this.confirmedSessionLayout = confirmed; const pending = this.peekPendingSessionLayout(); if (pending !== undefined && pending.revision === layout.revision) { this.pendingSessionLayout = { ...pending, revision: confirmed.revision }; } if (this.peekPendingSessionLayout() === undefined) this.applySessionLayout(confirmed); } catch (error) { if (!this.authenticated || !this.isCurrentConnection(generationArg)) return; let latest = this.confirmedSessionLayout; try { const response = await this.socketClient.fire( 'controller.sessiongroups.get', {}, { timeoutMs: layoutRequestTimeoutMs, maxRetries: 0 }, ); if (!this.authenticated || !this.isCurrentConnection(generationArg)) return; const recovered = cloneSessionLayout(response); if (!latest || recovered.revision >= latest.revision) { latest = recovered; this.confirmedSessionLayout = latest; } } catch { // Keep the last confirmed layout if the transport is also unavailable. } if (this.authenticated) { if (latest && this.peekPendingSessionLayout() === undefined) { this.applySessionLayout(latest); } this.reportWorkspaceError('Save the conversation layout', error); } } } } /** * One ordered list: a conversation and a resource move the same way. A move is purely * positional — it never changes an association. */ private readonly handleItemMove = ( eventArg: CustomEvent, ): void => { const detail = eventArg.detail; if (!detail?.item || !isNonEmptyString(detail.item.id)) return; const { toGroupId, beforeItem } = detail; const movedItem = this.presentationRefToLayoutItemRef(detail.item); if (!movedItem) return; if (toGroupId !== null && !this.sessionGroups.some((group) => group.id === toGroupId)) return; const movedKey = layoutItemRefKey(movedItem); const next = this.sessionGroups.map((group) => ({ id: group.id, name: group.name, itemIds: group.itemIds.filter((itemRef) => layoutItemRefKey(itemRef) !== movedKey), })); const nextUngrouped = controllerMutableUngroupedLayoutItemIds( this.conversations, this.resources, this.sessionGroups, this.ungroupedItemIds, this.selectedProjectId, ).filter((itemRef) => layoutItemRefKey(itemRef) !== movedKey); const beforeKey = beforeItem === null ? undefined : layoutItemRefKey(this.presentationRefToLayoutItemRef(beforeItem) ?? movedItem); if (toGroupId !== null) { const target = next.find((group) => group.id === toGroupId); if (!target) return; const insertAt = beforeKey === undefined ? -1 : target.itemIds.findIndex((candidateArg) => layoutItemRefKey(candidateArg) === beforeKey); if (insertAt >= 0) target.itemIds.splice(insertAt, 0, movedItem); else target.itemIds.push(movedItem); } else { const insertAt = beforeKey === undefined ? -1 : nextUngrouped.findIndex((candidateArg) => layoutItemRefKey(candidateArg) === beforeKey); if (insertAt >= 0) nextUngrouped.splice(insertAt, 0, movedItem); else nextUngrouped.push(movedItem); } this.persistSessionLayout(next, nextUngrouped); }; /** Resolves a sidebar presentation ref back to a layout ref it can persist. */ private presentationRefToLayoutItemRef( refArg: plugins.deesCatalog.IHarnessSessionListItemRef, ): interfaces.TControllerLayoutItemRef | undefined { if (refArg.kind === 'resource') { return this.resources.some((resource) => ( resource.id === refArg.id && resource.lifecycle !== 'retired' )) ? { kind: 'resource', id: refArg.id, projectId: this.selectedProjectId } : undefined; } // The key carries the project, so a row of another project keeps its own identity. const parsed = parseConversationUiKey(refArg.id); if (!parsed) return undefined; const tracked = this.conversations.some((conversationArg) => ( conversationArg.projectId === parsed.projectId && conversationArg.archivedAt === undefined && hasSessionRuntimeId(conversationArg.session) && controllerRuntimeIdsEqual(conversationArg.session.id, parsed.sessionId) )); return tracked ? { kind: 'session', id: parsed.sessionId, projectId: parsed.projectId } : undefined; } private readonly handleGroupCreateRequest = (): void => { this.showTextPromptModal({ heading: 'New group', label: 'Group name', submitLabel: 'Create', onSubmit: async (nameArg) => { const idAlphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; const id = 'grp-' + Array.from( crypto.getRandomValues(new Uint8Array(12)), (byteArg) => idAlphabet[byteArg % idAlphabet.length], ).join(''); this.persistSessionLayout([ ...this.sessionGroups, { id, name: nameArg, itemIds: [] }, ]); }, }); }; private readonly handleGroupContext = ( eventArg: CustomEvent, ): void => { const presentationGroup = eventArg.detail?.group; const originalEvent = eventArg.detail?.originalEvent; const group = this.sessionGroups.find((candidateArg) => candidateArg.id === presentationGroup?.id); if (!group || !originalEvent || this.mutationPending) { return; } void plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(originalEvent, [ { name: 'Rename group', iconName: 'lucide:Pencil', action: async () => this.showRenameGroupModal(group.id, group.name), }, { divider: true }, { name: 'Delete group', iconName: 'lucide:Trash2', action: async () => this.deleteSessionGroup(group.id), }, ]); }; private deleteSessionGroup(groupIdArg: string): void { const group = this.sessionGroups.find((candidate) => candidate.id === groupIdArg); if (!group) return; const nextGroups = this.sessionGroups.filter((candidate) => candidate.id !== groupIdArg); const remainingGroupedKeys = new Set( nextGroups.flatMap((candidate) => candidate.itemIds.map(layoutItemRefKey)), ); const retainedGroupIds = controllerMutableUngroupedLayoutItemIds( this.conversations, this.resources, [], group.itemIds, this.selectedProjectId, ); const retainedGroupKeys = new Set(retainedGroupIds.map(layoutItemRefKey)); const nextUngrouped = controllerMutableUngroupedLayoutItemIds( this.conversations, this.resources, this.sessionGroups, this.ungroupedItemIds, this.selectedProjectId, ); const ungroupedKeys = new Set(nextUngrouped.map(layoutItemRefKey)); for (const layoutItemId of group.itemIds) { const key = layoutItemRefKey(layoutItemId); if ( remainingGroupedKeys.has(key) || !retainedGroupKeys.has(key) || ungroupedKeys.has(key) ) continue; nextUngrouped.push(layoutItemId); ungroupedKeys.add(key); } this.persistSessionLayout(nextGroups, nextUngrouped); } private showRenameGroupModal(groupIdArg: string, currentNameArg: string): void { this.showTextPromptModal({ heading: 'Rename group', label: 'Group name', initialValue: currentNameArg, submitLabel: 'Rename', onSubmit: async (nameArg) => { this.persistSessionLayout( this.sessionGroups.map((candidate) => candidate.id === groupIdArg ? { ...candidate, name: nameArg } : candidate, ), ); }, }); } private readonly handleSessionContext = ( eventArg: CustomEvent, ): void => { const session = eventArg.detail?.session; const originalEvent = eventArg.detail?.originalEvent; if (!session || !isNonEmptyString(session.id) || !originalEvent || this.mutationPending) { return; } const sessionTitle = session.title || session.id; const terminalId = runtimeIdsByUiKey( this.terminals.map((terminalArg) => terminalArg.id).filter(isTerminalRuntimeId), ).get(session.id); if (terminalId) { void plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(originalEvent, [ { name: 'Rename', iconName: 'lucide:Pencil', action: async () => { this.showTextPromptModal({ heading: 'Rename terminal', label: 'Title', initialValue: sessionTitle, submitLabel: 'Rename', onSubmit: async (valueArg) => this.renameTerminal(terminalId, valueArg), }); }, }, { divider: true }, { name: 'Terminate', iconName: 'lucide:Trash2', action: async () => { void plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Terminate terminal?', width: 'small', content: plugins.deesElement.html`

“${sessionTitle}” and its running process will be terminated. This cannot be undone.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy(), }, { name: 'Terminate', action: async (modalArg) => { await modalArg?.destroy(); await this.removeTerminal(terminalId); }, }, ], }); }, }, ]); return; } // The row key carries its project, so a row of another project resolves to its own // conversation instead of silently missing. const conversation = this.trackedConversationByUiKey(session.id); if (!conversation) return; const { projectId, sessionId } = conversation; const harnessLabel = sessionHarnessLabel(sessionId.harnessId); const writerAction = this.codexWriterAction(sessionId); void plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(originalEvent, [ { name: `Rename in ${harnessLabel}`, iconName: 'lucide:Pencil', action: async () => { if (projectId !== this.selectedProjectId) this.switchProjectContext(projectId); this.showRenameSessionModal(sessionId, sessionTitle); }, }, ...(writerAction ? [{ name: writerAction.name, iconName: writerAction.iconName, // Offered only for the conversation that is open, which is always in the selected // project, so no project switch runs here — it would clear the selection the action // acts on. action: async () => { await this.updateCodexWriter(writerAction.action); }, }] : []), { // AGL's own archive: the conversation leaves the list and the Archive view reopens it. name: 'Archive', iconName: 'lucide:Archive', action: async () => { if (projectId !== this.selectedProjectId) this.switchProjectContext(projectId); await this.archiveSession(sessionId); }, }, { divider: true }, { name: 'Delete', iconName: 'lucide:Trash2', action: async () => { if (projectId !== this.selectedProjectId) this.switchProjectContext(projectId); void this.showDeleteConfirmationFor(sessionId, sessionTitle); }, }, ]); }; /** * Single-field prompt used by every rename/create dialog: the input is * focused with its text selected, Enter submits, Escape/Cancel closes. */ private showTextPromptModal(optionsArg: { heading: string; label: string; initialValue?: string; submitLabel: string; onSubmit: (valueArg: string) => Promise | void; }): void { let modalRef: InstanceType | undefined; const readInput = (): plugins.deesCatalog.DeesInputText | undefined => { const input = modalRef?.shadowRoot?.querySelector('dees-input-text.textPromptInput') ?? modalRef?.querySelector('dees-input-text.textPromptInput'); return input instanceof plugins.deesCatalog.DeesInputText ? input : undefined; }; const submit = async (): Promise => { // The catalog types the value as `string | number | null`; only a `number` // input ever holds a non-string, and this prompt is always a text field. const enteredValue = readInput()?.value; const value = (typeof enteredValue === 'string' ? enteredValue : '').trim(); await modalRef?.destroy(); if (isNonEmptyString(value)) { await optionsArg.onSubmit(value); } }; void plugins.deesCatalog.DeesModal.createAndShow({ heading: optionsArg.heading, width: 'small', content: plugins.deesElement.html` { if (eventArg.key === 'Enter') { eventArg.preventDefault(); void submit(); } }} > `, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy(), }, { name: optionsArg.submitLabel, action: async () => submit(), }, ], }).then((modalArg) => { modalRef = modalArg; const focusInput = (attemptArg: number): void => { const nativeInput = readInput()?.shadowRoot?.querySelector('input'); if (nativeInput) { nativeInput.focus(); nativeInput.select(); return; } if (attemptArg < 10) { setTimeout(() => focusInput(attemptArg + 1), 50); } }; focusInput(0); }); } private showRenameSessionModal( sessionIdArg: TBrowserSessionId, currentTitleArg: string, ): void { if (this.mutationPending) { return; } this.showTextPromptModal({ heading: `Rename ${sessionHarnessLabel(sessionIdArg.harnessId)} conversation`, label: 'Title', initialValue: currentTitleArg, submitLabel: 'Rename', onSubmit: async (valueArg) => this.renameSession(sessionIdArg, valueArg), }); } private async renameSession(sessionIdArg: TBrowserSessionId, titleArg: string): Promise { const title = titleArg.trim(); if (!isNonEmptyString(title)) { return; } const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; try { await this.socketClient.fire( 'controller.session.rename', { projectId: this.selectedProjectId, sessionId: sessionIdArg, title }, ); if (this.isCurrentMutation(mutationId, generation)) { await this.refreshSessions(); } } catch (error) { if (this.isCurrentMutation(mutationId, generation)) { this.reportWorkspaceError('Rename the conversation', error); } } finally { this.finishMutation(mutationId); } } private applyArchivedSessionState( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { const keepArchivedItem = ( candidateArg: interfaces.TControllerLayoutItemRef, ): boolean => candidateArg.kind !== 'session' || candidateArg.projectId !== projectIdArg || !controllerRuntimeIdsEqual(candidateArg.id, sessionIdArg); const pruneLayout = ( layoutArg: interfaces.IControllerSessionLayout, ): interfaces.IControllerSessionLayout => ({ ...layoutArg, groups: layoutArg.groups.map((groupArg) => ({ ...groupArg, itemIds: groupArg.itemIds.filter(keepArchivedItem), })), ungroupedItemIds: layoutArg.ungroupedItemIds.filter(keepArchivedItem), }); const confirmedLayout = this.confirmedSessionLayout; if (confirmedLayout) this.confirmedSessionLayout = pruneLayout(confirmedLayout); if (this.pendingSessionLayout) { this.pendingSessionLayout = pruneLayout(this.pendingSessionLayout); } this.applySessionLayout(pruneLayout({ groups: this.sessionGroups, ungroupedItemIds: this.ungroupedItemIds, revision: confirmedLayout?.revision ?? 0, })); this.cancelComposerFocusForSession(projectIdArg, sessionIdArg); const draftState = this.sessionDraftSync.state; if ( draftState?.projectId === projectIdArg && controllerRuntimeIdsEqual(draftState.sessionId, sessionIdArg) ) { this.sessionDraftSync.deactivate(); } if (!controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg)) return; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.detailLoading = false; this.selectedSessionId = undefined; this.sessionDetail = undefined; this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; if (this.draftSessionActive) { this.localDraftText = ''; this.localDraftAttachments = []; } this.draftSessionActive = false; this.clearAnsweredCardCaches(); this.clearSubagentModalState(); } /** * Archives the conversation in AGL. The harness conversation is untouched: this is AGL's own * archive, which the Archive view reopens from. */ private async archiveSession(sessionIdArg: TBrowserSessionId): Promise { if (!isSessionRuntimeId(sessionIdArg)) return; const mutationId = this.beginMutation(); if (!mutationId) { // Nothing failed — another change simply holds the workspace — so this is stated where the // owner clicked rather than recorded as a failure of the system. The sidebar is the only // surface that is always there: archiving is offered by the conversation menu while a // terminal, a browser or the empty workspace is shown, and none of those renders a composer. if (this.authenticated && this.socketClient.isConnected) { this.noteSidebar('Another change is already in progress.'); } return; } const generation = this.connectionGeneration; const mutationSequence = this.mutationSequence; const projectId = this.selectedProjectId; let refreshAfterArchive = false; let archiveError = ''; try { const response = await this.socketClient.fire( 'controller.conversation.archive', { projectId, sessionId: sessionIdArg }, ); if ( !this.isCurrentMutation(mutationId, generation) || this.selectedProjectId !== projectId ) { return; } if ( !isTrackedConversation(response.conversation) || !controllerRuntimeIdsEqual(response.conversation.session.id, sessionIdArg) || !Number.isFinite(response.conversation.archivedAt) || response.conversation.archivedAt! <= 0 ) { throw new Error('The controller returned an invalid archived conversation.'); } this.applyTrackedConversation(response.conversation); const archivedSession = trackedConversationToSession(response.conversation); // Older list/layout responses must not restore the pre-archive sidebar. this.sessionsRequestId += 1; this.sessionsLoading = false; this.sessionsLoadedForProjectId = projectId; this.layoutRequestId += 1; this.groupsLoaded = false; const archivedSessionIndex = this.sessions.findIndex((sessionArg) => ( isSessionRuntimeId(sessionArg.id) && controllerRuntimeIdsEqual(sessionArg.id, sessionIdArg) )); this.sessions = archivedSessionIndex < 0 ? [...this.sessions, archivedSession] : this.sessions.map((sessionArg, index) => ( index === archivedSessionIndex ? archivedSession : sessionArg )); this.applyArchivedSessionState(projectId, sessionIdArg); } catch (error) { if ( this.isCurrentMutation(mutationId, generation) && this.selectedProjectId === projectId ) { archiveError = errorMessage(error); this.reportWorkspaceError('Archive the conversation', error); refreshAfterArchive = true; } } finally { this.finishMutation(mutationId); } if (refreshAfterArchive) { void this.refreshSessions() .catch((error) => { if ( !archiveError && mutationSequence === this.mutationSequence && this.isCurrentConnection(generation) && this.selectedProjectId === projectId ) { this.reportWorkspaceError('Reload conversations', error); } }) .finally(() => { if ( archiveError && mutationSequence === this.mutationSequence && this.isCurrentConnection(generation) && this.selectedProjectId === projectId ) { const archivedSession = this.sessions.find((sessionArg) => ( hasSessionRuntimeId(sessionArg) && controllerRuntimeIdsEqual(sessionArg.id, sessionIdArg) && Number.isFinite(sessionArg.archivedAt) && sessionArg.archivedAt! > 0 )); // The reload can still show the archive as done. The failure stays in the journal, // because the request did fail; the workspace follows what the controller now reports. if (archivedSession) this.applyArchivedSessionState(projectId, sessionIdArg); } }); } } private beginMutation(): symbol | undefined { if (this.mutationPending || !this.authenticated || !this.socketClient.isConnected) { return undefined; } const mutationId = Symbol('controllerMutation'); this.mutationSequence += 1; this.activeMutationId = mutationId; this.mutationPending = true; return mutationId; } private isCurrentMutation( mutationIdArg: symbol, generationArg: number, sessionIdArg?: TBrowserSessionId, ): boolean { return ( this.activeMutationId === mutationIdArg && this.isCurrentConnection(generationArg) && this.authenticated && (sessionIdArg === undefined || controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg)) ); } private finishMutation(mutationIdArg: symbol | undefined): void { if (mutationIdArg && this.activeMutationId === mutationIdArg) { this.activeMutationId = undefined; this.mutationPending = false; // The sidebar notice only ever says that this change holds the workspace, so it stops being // true the moment the change is done and the action can simply be taken again. this.sidebarNotice = ''; } } private liveMessageOrdersEqual( leftArg: interfaces.IControllerTranscriptOrder | undefined, rightArg: interfaces.IControllerTranscriptOrder | undefined, ): boolean { return leftArg === undefined ? rightArg === undefined : rightArg !== undefined && leftArg.messageIndex === rightArg.messageIndex && leftArg.partIndex === rightArg.partIndex; } private liveMessageIdentityMatches( previousArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate, deltaArg: interfaces.IControllerReasoningDelta | interfaces.IControllerTextDelta, ): boolean { return controllerRuntimeIdsEqual(previousArg.sessionId, deltaArg.sessionId) && controllerRuntimeIdsEqual(previousArg.messageId, deltaArg.messageId) && controllerRuntimeIdsEqual(previousArg.partId, deltaArg.partId) && this.liveMessageOrdersEqual(previousArg.order, deltaArg.order); } private liveMessageEventPassesBarriers( projectIdArg: string, updateArg: interfaces.IControllerReasoningUpdate | interfaces.IControllerTextUpdate | interfaces.IControllerReasoningDelta | interfaces.IControllerTextDelta, ): { accepted: boolean; epochAdvanced: boolean } { if (!isSessionRuntimeId(updateArg.sessionId)) { return { accepted: false, epochAdvanced: false }; } if ( updateArg.sourceUpdatedAt <= (this.liveHistoryBarriers.get( this.liveToolSessionKey(projectIdArg, updateArg.sessionId), ) ?? Number.NEGATIVE_INFINITY) ) return { accepted: false, epochAdvanced: false }; const previousEpoch = this.liveMessageStreamEpochs.get(updateArg.sessionId.harnessId) ?? 0; const accepted = this.acceptLiveMessageEpoch( updateArg.sessionId.harnessId, updateArg.streamEpoch, true, false, ); return { accepted, epochAdvanced: accepted && updateArg.streamEpoch > previousEpoch }; } private refreshAfterTerminalLiveMessage( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(sessionIdArg, this.selectedSessionId) ) return; void this.refreshSelectedSessionDetail(); void this.refreshSessions(false); } private applyLiveReasoningUpdate( projectIdArg: string, updateArg: interfaces.IControllerReasoningUpdate, ): void { if (!isSessionRuntimeId(updateArg.sessionId)) return; const sessionId = updateArg.sessionId; const barrier = this.liveMessageEventPassesBarriers(projectIdArg, updateArg); if (!barrier.accepted) return; if (controllerRuntimeIdsEqual(updateArg.sessionId, this.subagentModalSessionId)) { this.scheduleSubagentModalRefresh(); } if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(updateArg.sessionId, this.selectedSessionId) ) return; const floor = this.liveMessageHydrationFloors.get( this.liveToolSessionKey(projectIdArg, updateArg.sessionId), )?.cursor; if ( floor && floor.streamEpoch === updateArg.streamEpoch && updateArg.revision <= floor.revision ) return; const key = this.liveMessageUpdateKey(projectIdArg, updateArg); const previous = this.liveReasoningUpdates.get(key) ?? this.liveReasoningBaselines.get(key)?.update; if ( previous && previous.streamEpoch === updateArg.streamEpoch && updateArg.revision <= previous.revision ) return; if (!this.storeLiveReasoningUpdate(key, updateArg)) { this.blockLiveMessageDeltas( projectIdArg, sessionId, updateArg.streamEpoch, !barrier.epochAdvanced, ); return; } const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId); if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate(); const target = projection ? this.canonicalReasoningTarget(projection, updateArg) : undefined; if (!projection || !target) { this.requestCanonicalChatReconciliation(); } else if (!this.harnessMessageIsTerminal(target.message)) { const previousStreaming = target.message.streaming === true; const previousText = target.part.text; let correction = !updateArg.text.startsWith(previousText); if (!correction && updateArg.text.length > previousText.length) { correction = !this.applyCanonicalReasoningDelta( projection, target, updateArg.text.slice(previousText.length), ); } if (correction) target.part.text = updateArg.text; target.part.startedAt = updateArg.sourceUpdatedAt; if (updateArg.status === 'running') delete target.part.endedAt; target.message.streaming = updateArg.status === 'running' || (updateArg.sessionId.harnessId === 'opencode' && previousStreaming); if ( !correction && updateArg.status === 'running' && !previousStreaming && updateArg.text === previousText ) { this.mountedCanonicalChat(projection)?.applyDelta({ type: 'reasoning', messageId: target.message.id, partId: target.part.id, delta: '', }); } if (updateArg.status !== 'running') { this.mountedCanonicalChat(projection)?.applyDelta({ type: 'reasoning-end', messageId: target.message.id, partId: target.part.id, endedAt: updateArg.sourceUpdatedAt, }); target.part.endedAt = updateArg.sourceUpdatedAt; } if (correction) { this.canonicalMessageRefreshIds.add(target.message.id); this.requestCanonicalChatReconciliation(false); } } if (updateArg.status !== 'running') { this.refreshAfterTerminalLiveMessage(projectIdArg, sessionId); } } private applyLiveTextUpdate( projectIdArg: string, updateArg: interfaces.IControllerTextUpdate, ): void { if (!isSessionRuntimeId(updateArg.sessionId)) return; const sessionId = updateArg.sessionId; const barrier = this.liveMessageEventPassesBarriers(projectIdArg, updateArg); if (!barrier.accepted) return; if (controllerRuntimeIdsEqual(updateArg.sessionId, this.subagentModalSessionId)) { this.scheduleSubagentModalRefresh(); } if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(updateArg.sessionId, this.selectedSessionId) ) return; const floor = this.liveMessageHydrationFloors.get( this.liveToolSessionKey(projectIdArg, updateArg.sessionId), )?.cursor; if ( floor && floor.streamEpoch === updateArg.streamEpoch && updateArg.revision <= floor.revision ) return; const key = this.liveMessageUpdateKey(projectIdArg, updateArg); const previous = this.liveTextUpdates.get(key) ?? this.liveTextBaselines.get(key)?.update; if ( previous && previous.streamEpoch === updateArg.streamEpoch && updateArg.revision <= previous.revision ) return; if (!this.storeLiveTextUpdate(key, updateArg)) { this.blockLiveMessageDeltas( projectIdArg, sessionId, updateArg.streamEpoch, !barrier.epochAdvanced, ); return; } const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId); if (projection && this.canonicalTimelineRefreshPending) this.requestUpdate(); const target = projection ? this.canonicalTextTarget(projection, updateArg) : undefined; if (!projection || !target) { this.requestCanonicalChatReconciliation(); } else if (!this.harnessMessageIsTerminal(target)) { const previousStreaming = target.streaming === true; const previousText = target.text; let correction = !updateArg.text.startsWith(previousText); if (!correction && updateArg.text.length > previousText.length) { correction = !this.applyCanonicalTextDelta( projection, target, updateArg.text.slice(previousText.length), ); } if (correction) target.text = updateArg.text; if (updateArg.status === 'running') { target.streaming = true; if (!correction && !previousStreaming && updateArg.text === previousText) { this.mountedCanonicalChat(projection)?.applyDelta({ type: 'text', messageId: target.id, delta: '', }); } } else { this.mountedCanonicalChat(projection)?.applyDelta({ type: 'message-end', messageId: target.id, }); target.streaming = false; target.updatedAt = updateArg.sourceUpdatedAt; } if (correction) { this.canonicalMessageRefreshIds.add(target.id); this.requestCanonicalChatReconciliation(false); } } if (updateArg.status === 'completed') { this.refreshAfterTerminalLiveMessage(projectIdArg, sessionId); } } private applyLiveReasoningDelta( projectIdArg: string, deltaArg: interfaces.IControllerReasoningDelta, ): void { if (!isSessionRuntimeId(deltaArg.sessionId)) return; const sessionId = deltaArg.sessionId; const barrier = this.liveMessageEventPassesBarriers(projectIdArg, deltaArg); if (!barrier.accepted) return; if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(deltaArg.sessionId, this.selectedSessionId) || this.liveMessageDeltasBlocked(projectIdArg, sessionId, deltaArg.streamEpoch) ) return; const floor = this.liveMessageHydrationFloors.get( this.liveToolSessionKey(projectIdArg, deltaArg.sessionId), )?.cursor; if ( floor && floor.streamEpoch === deltaArg.streamEpoch && deltaArg.revision <= floor.revision ) return; const key = this.liveMessageUpdateKey(projectIdArg, deltaArg); const active = this.liveReasoningUpdates.get(key); const baseline = this.liveReasoningBaselines.get(key); const previous = active ?? baseline?.update; if ( previous && previous.streamEpoch === deltaArg.streamEpoch && deltaArg.revision <= previous.revision ) return; const textUtf8Bytes = active ? this.liveReasoningTextUtf8Bytes.get(key) ?? browserTextEncoder.encode(active.text).byteLength : baseline?.textUtf8Bytes; const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId); const target = projection ? this.canonicalReasoningTarget(projection, deltaArg) : undefined; const nextTextUtf8Bytes = previous && textUtf8Bytes !== undefined ? controllerTextAfterAppendUtf8Bytes(previous.text, textUtf8Bytes, deltaArg.delta) : undefined; if ( !previous || previous.status !== 'running' || previous.streamEpoch !== deltaArg.streamEpoch || deltaArg.revision <= previous.revision || deltaArg.sourceUpdatedAt < previous.sourceUpdatedAt || !this.liveMessageIdentityMatches(previous, deltaArg) || textUtf8Bytes !== deltaArg.baseTextUtf8Bytes || nextTextUtf8Bytes !== deltaArg.textUtf8Bytes || !projection || !target || this.harnessMessageIsTerminal(target.message) || target.part.text !== previous.text || (deltaArg.order !== undefined && (!target.message.order || transcriptOrderKey(target.message.order) !== transcriptOrderKey(deltaArg.order))) ) { this.blockLiveMessageDeltas( projectIdArg, sessionId, deltaArg.streamEpoch, !barrier.epochAdvanced, ); return; } if (!this.applyCanonicalReasoningDelta(projection, target, deltaArg.delta)) { this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch); return; } target.message.streaming = true; const update: interfaces.IControllerReasoningUpdate = { ...previous, text: `${previous.text}${deltaArg.delta}`, sourceUpdatedAt: deltaArg.sourceUpdatedAt, revision: deltaArg.revision, streamEpoch: deltaArg.streamEpoch, }; if (!this.storeLiveReasoningDeltaState( key, update, deltaArg.textUtf8Bytes, deltaArg.delta, )) { target.part.text = previous.text; this.requestCanonicalChatReconciliation(); this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch); } } private applyLiveTextDelta( projectIdArg: string, deltaArg: interfaces.IControllerTextDelta, ): void { if (!isSessionRuntimeId(deltaArg.sessionId)) return; const sessionId = deltaArg.sessionId; const barrier = this.liveMessageEventPassesBarriers(projectIdArg, deltaArg); if (!barrier.accepted) return; if ( projectIdArg !== this.selectedProjectId || !controllerRuntimeIdsEqual(deltaArg.sessionId, this.selectedSessionId) || this.liveMessageDeltasBlocked(projectIdArg, sessionId, deltaArg.streamEpoch) ) return; const floor = this.liveMessageHydrationFloors.get( this.liveToolSessionKey(projectIdArg, deltaArg.sessionId), )?.cursor; if ( floor && floor.streamEpoch === deltaArg.streamEpoch && deltaArg.revision <= floor.revision ) return; const key = this.liveMessageUpdateKey(projectIdArg, deltaArg); const active = this.liveTextUpdates.get(key); const baseline = this.liveTextBaselines.get(key); const previous = active ?? baseline?.update; if ( previous && previous.streamEpoch === deltaArg.streamEpoch && deltaArg.revision <= previous.revision ) return; const textUtf8Bytes = active ? this.liveTextTextUtf8Bytes.get(key) ?? browserTextEncoder.encode(active.text).byteLength : baseline?.textUtf8Bytes; const projection = this.selectedCanonicalChatProjection(projectIdArg, sessionId); const target = projection ? this.canonicalTextTarget(projection, deltaArg) : undefined; const nextTextUtf8Bytes = previous && textUtf8Bytes !== undefined ? controllerTextAfterAppendUtf8Bytes(previous.text, textUtf8Bytes, deltaArg.delta) : undefined; if ( !previous || previous.status !== 'running' || previous.streamEpoch !== deltaArg.streamEpoch || deltaArg.revision <= previous.revision || deltaArg.sourceUpdatedAt < previous.sourceUpdatedAt || !this.liveMessageIdentityMatches(previous, deltaArg) || textUtf8Bytes !== deltaArg.baseTextUtf8Bytes || nextTextUtf8Bytes !== deltaArg.textUtf8Bytes || !projection || !target || this.harnessMessageIsTerminal(target) || target.text !== previous.text || (deltaArg.order !== undefined && (!target.order || transcriptOrderKey(target.order) !== transcriptOrderKey(deltaArg.order))) ) { this.blockLiveMessageDeltas( projectIdArg, sessionId, deltaArg.streamEpoch, !barrier.epochAdvanced, ); return; } if (!this.applyCanonicalTextDelta(projection, target, deltaArg.delta)) { this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch); return; } const update: interfaces.IControllerTextUpdate = { ...previous, text: `${previous.text}${deltaArg.delta}`, sourceUpdatedAt: deltaArg.sourceUpdatedAt, revision: deltaArg.revision, streamEpoch: deltaArg.streamEpoch, }; if (!this.storeLiveTextDeltaState( key, update, deltaArg.textUtf8Bytes, deltaArg.delta, )) { target.text = previous.text; this.requestCanonicalChatReconciliation(); this.blockLiveMessageDeltas(projectIdArg, sessionId, deltaArg.streamEpoch); } } private childEventMatchesRelationship( eventArg: interfaces.IControllerChildEvent, projectIdArg: string, parentSessionIdArg: TBrowserSessionId, childSessionIdArg: TBrowserSessionId, ): boolean { return eventArg.projectId === projectIdArg && controllerRuntimeIdsEqual(eventArg.parentSessionId, parentSessionIdArg) && controllerRuntimeIdsEqual(eventArg.childSessionId, childSessionIdArg); } private applyPendingChildEvent(scopeGenerationArg: string, coveredSequenceArg: number): void { const pending = this.pendingChildEventsByScopeGeneration.get(scopeGenerationArg); if (!pending) return; this.pendingChildEventsByScopeGeneration.delete(scopeGenerationArg); if (pending.sequence <= coveredSequenceArg) return; void this.handleChildEvent(pending); } private async handleChildEvent(eventArg: interfaces.IControllerChildEvent): Promise { if (!this.authenticated) return; let handled = false; for (const entry of this.subtaskPreviewEntries.values()) { if ( entry.access !== 'scoped' || !this.childEventMatchesRelationship( eventArg, entry.projectId, entry.ownerSessionId, entry.childSessionId, ) ) continue; if (entry.scopeGeneration === undefined) { continue; } if (entry.scopeGeneration !== eventArg.scopeGeneration) continue; handled = true; if (eventArg.sequence <= (entry.scopeSequence ?? -1)) continue; if (eventArg.kind === 'scope.revoked') { entry.scopeGeneration = undefined; entry.scopeSequence = undefined; } else { entry.scopeSequence = eventArg.sequence; } if (entry.stream.status.type === 'busy') this.queueSubtaskPreviewHydration(entry); } if ( this.subagentModalAccess === 'scoped' && isSessionRuntimeId(this.subagentModalParentSessionId) && isSessionRuntimeId(this.subagentModalSessionId) && this.childEventMatchesRelationship( eventArg, this.selectedProjectId, this.subagentModalParentSessionId, this.subagentModalSessionId, ) ) { if (this.subagentModalScopeGeneration === eventArg.scopeGeneration) { handled = true; if (eventArg.sequence > (this.subagentModalScopeSequence ?? -1)) { if (eventArg.kind === 'scope.revoked') { this.clearSubagentModalState(); } else { this.subagentModalScopeSequence = eventArg.sequence; this.scheduleSubagentModalRefresh(); } } } } const currentDetail = this.sessionDetail; if ( currentDetail && eventArg.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(currentDetail.session.id, eventArg.parentSessionId) ) { if (eventArg.kind === 'attention.changed') this.scheduleRefresh(); const attentionIndex = (currentDetail.childAttention ?? []).findIndex((attentionArg) => ( attentionArg.scopeGeneration === eventArg.scopeGeneration && this.childEventMatchesRelationship( eventArg, attentionArg.projectId, attentionArg.parentSessionId, attentionArg.childSessionId, ) )); const attention = currentDetail.childAttention?.[attentionIndex]; if (attention && eventArg.sequence > attention.sequence) { handled = true; const childAttention = [...(currentDetail.childAttention ?? [])]; if (eventArg.kind === 'scope.revoked') childAttention.splice(attentionIndex, 1); else childAttention[attentionIndex] = { ...attention, sequence: eventArg.sequence }; this.sessionDetail = { ...currentDetail, ...(childAttention.length === 0 ? { childAttention: undefined } : { childAttention }), }; this.scheduleRefresh(); } } if (!handled) { const previous = this.pendingChildEventsByScopeGeneration.get(eventArg.scopeGeneration); if (!previous || eventArg.sequence > previous.sequence) { this.pendingChildEventsByScopeGeneration.delete(eventArg.scopeGeneration); this.pendingChildEventsByScopeGeneration.set(eventArg.scopeGeneration, eventArg); } while (this.pendingChildEventsByScopeGeneration.size > 64) { const oldest = this.pendingChildEventsByScopeGeneration.keys().next().value; if (oldest === undefined) break; this.pendingChildEventsByScopeGeneration.delete(oldest); } } } private async handleControllerEvent(eventArg: interfaces.IControllerEvent): Promise { if (!this.authenticated) { return; } if (eventArg.type === 'upgrade.changed' && eventArg.upgrade) { this.storeUpgradeStatus(eventArg.upgrade); await this.updateComplete; if (document.visibilityState === 'visible') { await new Promise((resolve) => globalThis.requestAnimationFrame(() => resolve())); } return; } if ( eventArg.type === 'session.draft.changed' && typeof eventArg.projectId === 'string' && isSessionRuntimeId(eventArg.sessionId) && eventArg.sessionDraftUpdate ) { this.sessionDraftSync.applyRemoteUpdate( eventArg.projectId, eventArg.sessionId, eventArg.sessionDraftUpdate, ); return; } if (eventArg.type === 'session.history.changed' && isSessionRuntimeId(eventArg.sessionId)) { const projectId = eventArg.projectId ?? this.selectedProjectId; const historyKey = this.liveToolSessionKey(projectId, eventArg.sessionId); const barrier = Math.max( this.liveHistoryBarriers.get(historyKey) ?? Number.NEGATIVE_INFINITY, eventArg.timestamp, ); this.liveHistoryBarriers.delete(historyKey); this.liveHistoryBarriers.set(historyKey, barrier); while (this.liveHistoryBarriers.size > 512) { const oldestKey = this.liveHistoryBarriers.keys().next().value; if (oldestKey === undefined) break; this.liveHistoryBarriers.delete(oldestKey); } const selectedHistoryChanged = projectId === this.selectedProjectId && controllerRuntimeIdsEqual(eventArg.sessionId, this.selectedSessionId); if (selectedHistoryChanged) { this.detailRefreshPending = false; this.detailRefreshRestartEnrichment = false; this.invalidateDetailLoad(); this.clearLiveToolSession(projectId, eventArg.sessionId); if (eventArg.sessionId.harnessId === 'flex') { this.liveToolStreamEpochs.delete('flex'); this.liveMessageStreamEpochs.delete('flex'); } this.clearSubtaskPreviewState(); this.sessionDetail = undefined; this.detailLoading = false; this.workspaceNotice = ''; this.invalidateSlashCatalog(); void this.refreshSelectedSessionDetail(true); void this.loadSlashCatalog(); } if (projectId === this.selectedProjectId) void this.refreshSessions(); return; } let modalStreamBarrierAdvanced = false; if ( eventArg.toolStreamEpoch !== undefined && (eventArg.harnessId === 'opencode' || eventArg.harnessId === 'flex' || eventArg.harnessId === 'codex') ) { const previousEpoch = this.liveToolStreamEpochs.get(eventArg.harnessId) ?? 0; if (this.acceptLiveToolEpoch(eventArg.harnessId, eventArg.toolStreamEpoch)) { modalStreamBarrierAdvanced ||= eventArg.toolStreamEpoch > previousEpoch; } } if ( eventArg.messageStreamEpoch !== undefined && (eventArg.harnessId === 'opencode' || eventArg.harnessId === 'flex' || eventArg.harnessId === 'codex') ) { const previousEpoch = this.liveMessageStreamEpochs.get(eventArg.harnessId) ?? 0; if (this.acceptLiveMessageEpoch(eventArg.harnessId, eventArg.messageStreamEpoch)) { modalStreamBarrierAdvanced ||= eventArg.messageStreamEpoch > previousEpoch; } } if ( eventArg.type === 'harness.changed' && modalStreamBarrierAdvanced && this.subagentModalSessionId?.harnessId === eventArg.harnessId ) this.scheduleSubagentModalRefresh(); if (eventArg.type === 'session.tool.updated' && eventArg.toolExecution) { if (typeof eventArg.projectId === 'string') { this.applyLiveToolExecution(eventArg.projectId, eventArg.toolExecution); if (isSessionRuntimeId(eventArg.toolExecution.sessionId)) { this.scheduleSubtaskPreviewRefresh( eventArg.projectId, eventArg.toolExecution.sessionId, eventArg.toolExecution.status === 'pending' || eventArg.toolExecution.status === 'running' ? { type: 'busy' } : undefined, ); } } if ( eventArg.toolExecution.status === 'completed' || eventArg.toolExecution.status === 'error' || eventArg.toolExecution.status === 'stopped' ) this.scheduleRefresh(); return; } if (eventArg.type === 'session.reasoning.updated' && eventArg.reasoningUpdate) { if (typeof eventArg.projectId !== 'string') return; if (!isSessionRuntimeId(eventArg.reasoningUpdate.sessionId)) return; this.applyLiveReasoningUpdate(eventArg.projectId, eventArg.reasoningUpdate); return; } if (eventArg.type === 'session.reasoning.delta' && eventArg.reasoningDelta) { if (typeof eventArg.projectId !== 'string') return; if (!isSessionRuntimeId(eventArg.reasoningDelta.sessionId)) return; this.applyLiveReasoningDelta(eventArg.projectId, eventArg.reasoningDelta); return; } if (eventArg.type === 'session.text.updated' && eventArg.textUpdate) { if (typeof eventArg.projectId !== 'string') return; if (!isSessionRuntimeId(eventArg.textUpdate.sessionId)) return; this.applyLiveTextUpdate(eventArg.projectId, eventArg.textUpdate); return; } if (eventArg.type === 'session.text.delta' && eventArg.textDelta) { if (typeof eventArg.projectId !== 'string') return; if (!isSessionRuntimeId(eventArg.textDelta.sessionId)) return; this.applyLiveTextDelta(eventArg.projectId, eventArg.textDelta); return; } this.applySessionEventState(eventArg); const eventProjectId = eventArg.projectId ?? this.selectedProjectId; if (isNonEmptyString(eventProjectId) && isSessionRuntimeId(eventArg.sessionId)) { this.scheduleSubtaskPreviewRefresh( eventProjectId, eventArg.sessionId, this.subtaskStatusFromSessionEvent(eventProjectId, eventArg), ); } if ( isSessionRuntimeId(this.subagentModalSessionId) && controllerRuntimeIdsEqual(eventArg.sessionId, this.subagentModalSessionId) ) { this.scheduleSubagentModalRefresh(); } if (eventArg.type === 'projects.changed') { void this.refreshProjects().then(() => this.refreshSessions()); return; } if (eventArg.type === 'settings.changed') { void this.refreshModelCatalog(); return; } if (eventArg.type === 'accounts.changed') { // The controller owns the account view and announces every change of it. The screen re-reads // on this event and on nothing else, which is why it needs no poll and no timer. void this.settingsModalRef?.shadowRoot ?.querySelector('agl-accounts')?.refresh(); return; } if (eventArg.type === 'harness.changed') { const limitsContext = this.accountLimitsContext(); const limitsHarnessId = limitsContext?.available ? limitsContext.harnessId : this.selectedSessionId?.harnessId; if (eventArg.harnessId === undefined || eventArg.harnessId === limitsHarnessId) { this.shadowRoot?.querySelector('agl-account-limits')?.invalidate(); } if ( eventArg.harnessId === undefined || eventArg.harnessId === this.selectedSessionId?.harnessId ) { this.invalidateSlashCatalog(); void this.loadSlashCatalog(); } void this.reconcileControllerStatus(); void this.refreshModelCatalog(); void this.refreshProviderManagement(); if (this.activeProviderLogin?.status === 'pending') { void this.pollProviderLogin(); } } if (eventArg.type === 'sessiongroups.changed') { // One layout per controller: every change concerns this client, whichever project moved. void this.loadSessionGroups(true); return; } if (eventArg.type === 'terminals.changed') { if (eventArg.projectId === undefined || eventArg.projectId === this.selectedProjectId) { void this.loadTerminals(true); } return; } if (eventArg.type === 'resources.changed') { if (eventArg.projectId === undefined || eventArg.projectId === this.selectedProjectId) { void this.loadResources(true); void this.loadTerminals(true); } return; } if ( eventArg.projectId !== undefined && this.selectedProjectId && eventArg.projectId !== this.selectedProjectId ) { // Another project's activity leaves the open conversation alone, but the sidebar spans // projects now, so its rows still have to follow. if (eventArg.type === 'sessions.changed' || eventArg.type === 'session.changed') { void this.refreshSessions(false); } return; } this.scheduleRefresh(); } /** * The sidebar spans projects, so selecting a conversation of another project moves the whole * workspace there: its resources, terminals, drafts and composer state are project-scoped. */ private openTrackedConversation( projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { if (projectIdArg !== this.selectedProjectId) { this.switchProjectContext(projectIdArg); // The tracked set already holds that project's conversations, so the chat opens now // rather than after the refresh the project switch starts. this.sessions = this.conversations .filter((conversationArg) => ( conversationArg.projectId === projectIdArg && conversationArg.archivedAt === undefined && hasSessionRuntimeId(conversationArg.session) )) .map(trackedConversationToSession); this.openSessionById(sessionIdArg); void this.refreshSessions(); return; } this.openSessionById(sessionIdArg); } private readonly handleProjectSelected = ( eventArg: CustomEvent<{ key?: unknown }>, ): void => { const projectId = eventArg.detail?.key; if ( !isNonEmptyString(projectId) || projectId === this.selectedProjectId || !this.projects.some((projectArg) => projectArg.id === projectId) ) { return; } this.enterProjectContext(projectId); }; /** Makes a project the workspace context and reloads what that context shows. */ private enterProjectContext(projectIdArg: string): void { this.switchProjectContext(projectIdArg); void this.refreshSessions(); } private switchProjectContext(projectIdArg: string): void { const projectId = projectIdArg; this.cancelComposerFocus(); this.sessionDraftSync.deactivate(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.closeTerminalView(); this.clearAnsweredCardCaches(); this.clearSubagentModalState(); void this.closeBrowserView(); this.selectedProjectId = projectId; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.sessionDetail = undefined; this.sessionLayoutError = ''; this.terminals = []; this.resources = []; this.selectedResourceId = ''; this.terminalsLoadedForProjectId = ''; this.resourcesLoadedForProjectId = ''; this.terminalRequestId += 1; this.resourceRequestId += 1; this.draftSessionActive = false; this.sessionModelOverrides.delete(draftSessionKey('opencode')); this.sessionModelOverrides.delete(draftSessionKey('flex')); this.sessionModelOverrides.delete(draftSessionKey('codex')); this.sessionEffortOverrides.delete(draftSessionKey('opencode')); this.sessionEffortOverrides.delete(draftSessionKey('flex')); this.sessionEffortOverrides.delete(draftSessionKey('codex')); this.sessionAccountOverrides.delete(draftSessionKey('opencode')); this.sessionAccountOverrides.delete(draftSessionKey('flex')); this.sessionAccountOverrides.delete(draftSessionKey('codex')); this.workspaceNotice = ''; this.invalidateSlashCatalog(); } private readonly showRemoveProjectConfirmation = (projectIdArg?: string): void => { const projectId = projectIdArg ?? this.selectedProjectId; const project = this.projects.find((projectArg) => projectArg.id === projectId); if (!project || this.mutationPending) { return; } void plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Deregister project?', width: 'small', content: plugins.deesElement.html`

AGL will deregister “${project.name}”, retire its controller-managed state, and clean its managed Flex roots. Controller-managed resources must be retired first. Working-tree files at ${project.directory} stay untouched; this is not a list-only action.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy(), }, { name: 'Deregister project', action: async (modalArg) => { await modalArg?.destroy(); await this.removeProject(project.id); }, }, ], }); }; private async removeProject(projectIdArg: string): Promise { const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; try { await this.socketClient.fire( 'controller.project.remove', { projectId: projectIdArg }, ); if (!this.isCurrentMutation(mutationId, generation)) { return; } if (this.selectedProjectId === projectIdArg) { this.cancelComposerFocus(); this.draftSessionGeneration += 1; this.draftSessionDetailReadyId = undefined; this.draftSessionActive = false; this.selectedProjectId = ''; this.sessionLayoutError = ''; this.selectedSessionId = undefined; this.invalidateDetailLoad(); this.invalidateSlashCatalog(); this.sessionDetail = undefined; } await this.refreshProjects(); await this.refreshSessions(); } catch (error) { if (this.isCurrentMutation(mutationId, generation)) { this.reportWorkspaceError('Remove the project', error); } } finally { this.finishMutation(mutationId); } } /** Debounced directory suggestions for the new-conversation box. */ private readonly handleNewConversationDirectoryInput = (eventArg: Event): void => { const target = eventArg.currentTarget as HTMLElement & { value?: unknown }; const value = typeof target.value === 'string' ? target.value : ''; this.newConversationDirectory = value; this.newConversationError = ''; if (this.suggestDebounce) { clearTimeout(this.suggestDebounce); } this.suggestDebounce = setTimeout(() => { this.suggestDebounce = undefined; void this.fireSuggest(value.trim()); }, 250); }; private async fireSuggest(queryArg: string): Promise { if (!this.authenticated || !this.socketClient.isConnected) { return; } const requestId = ++this.suggestRequestId; const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.project.suggest', { query: queryArg }, ); if ( requestId !== this.suggestRequestId || !this.isCurrentConnection(generation) ) { return; } this.pathSuggestions = Array.isArray(response.suggestions) ? response.suggestions : []; } catch { // Suggestions are best-effort; typing continues without them, but a // stale list must not suggest the previous query still matches. if (requestId === this.suggestRequestId && this.isCurrentConnection(generation)) { this.pathSuggestions = []; } } } private readonly handlePathSuggestionClick = (suggestionArg: interfaces.IControllerPathSuggestion): void => { this.newConversationDirectory = suggestionArg.path; this.newConversationError = ''; void this.fireSuggest(`${suggestionArg.path}/`); }; private readonly handleModelChange = ( eventArg: CustomEvent<{ model?: unknown; reasoningEffort?: unknown }>, ): void => { const sessionId = this.selectedSessionId; if (this.blockForUnknownCodexMode(sessionId)) return; const harnessId = sessionId?.harnessId ?? this.draftHarnessId; const sessionKey = this.composerChoiceKey(sessionId); const modelString = eventArg.detail?.model; const modelChoice = isNonEmptyString(modelString) ? this.modelChoicesByString.get(modelString) : undefined; if (isNonEmptyString(modelString) && modelChoice?.harnessId === harnessId) { if (modelChoice.harnessId === 'flex') { const currentAccount = this.resolveSessionAccount(sessionId); if (!this.flexModelSupportsAccount(modelString, currentAccount)) { const availableAccounts = this.activeFlexAccountsForModel(modelString); this.sessionAccountOverrides.set( sessionKey, availableAccounts.length === 1 ? availableAccounts[0] : '', ); } } this.sessionModelOverrides.set(sessionKey, modelString); const variants = this.modelVariantsForSession(modelString, sessionId); const effortOverride = this.sessionEffortOverrides.get(sessionKey); if (effortOverride !== undefined && !variants.includes(effortOverride)) { this.sessionEffortOverrides.delete(sessionKey); } } const effortString = eventArg.detail?.reasoningEffort; const effectiveModelString = this.resolveSessionModelString(sessionId); const availableVariants = this.modelVariantsByString.get(effectiveModelString) ?? []; if (isNonEmptyString(effortString) && availableVariants.includes(effortString)) { this.sessionEffortOverrides.set(sessionKey, effortString); } else if (effortString === 'default' && !availableVariants.includes('default')) { this.sessionEffortOverrides.set(sessionKey, ''); } this.requestUpdate(); if (sessionId) void this.persistSessionModelChoice(sessionId); }; private readonly handleAccountChange = ( eventArg: CustomEvent, ): void => { const sessionId = this.selectedSessionId; const harnessId = sessionId?.harnessId ?? this.draftHarnessId; const account = eventArg.detail?.account; if ( harnessId !== 'flex' || !isNonEmptyString(account) || !this.providerConnections.some((connection) => ( connection.id === account && connection.status === 'active' )) ) return; const sessionKey = this.composerChoiceKey(sessionId); const priorModel = this.resolveSessionModelString(sessionId); const priorEffort = this.resolveSessionEffortString(sessionId); this.sessionAccountOverrides.set(sessionKey, account); const nextModel = this.flexModelSupportsAccount(priorModel, account) ? priorModel : this.safeFlexDefaultModelString(account, false); if (nextModel) this.sessionModelOverrides.set(sessionKey, nextModel); else this.sessionModelOverrides.delete(sessionKey); const nextVariants = this.modelVariantsForSession(nextModel, sessionId); if (priorEffort && nextVariants.includes(priorEffort)) { this.sessionEffortOverrides.set(sessionKey, priorEffort); } else { this.sessionEffortOverrides.delete(sessionKey); } this.requestUpdate(); if (sessionId && nextModel) void this.persistSessionModelChoice(sessionId); }; private composerChoiceKey(sessionIdArg?: TBrowserSessionId): string { return sessionIdArg ? this.sessionOperationKey(this.selectedProjectId, sessionIdArg) : draftSessionKey(this.draftHarnessId); } private flexAccountOptions(): plugins.deesCatalog.IHarnessComposerOption[] { const options = this.providerConnections .filter((connection) => connection.status === 'active') .map((connection) => { const account = connection.account.email || connection.account.accountId || connection.account.plan || 'OpenAI account'; const plan = connection.account.plan && connection.account.plan !== account ? ` · ${connection.account.plan}` : ''; return { label: `${account}${plan}`, value: connection.id, }; }); if (!sameComposerOptions(this.flexAccountOptionsCache, options)) { this.flexAccountOptionsCache = options; this.modelOptionsCache.clear(); } return this.flexAccountOptionsCache; } private safeFlexDefaultModelString( accountIdArg?: string, requireSoleAccountArg = true, ): string { const activeAccountIds = this.activeFlexAccountIds(); if (requireSoleAccountArg && activeAccountIds.length !== 1) return ''; const accountId = accountIdArg ?? activeAccountIds[0]; if (!accountId || !activeAccountIds.includes(accountId)) return ''; const defaults = this.modelOptionStrings.filter((optionString) => ( this.flexModelAvailabilityByString.get(optionString)?.some((availability) => ( availability.providerConnectionId === accountId && availability.isDefault )) === true )); return defaults.length === 1 ? defaults[0] : ''; } private activeFlexAccountIds(): string[] { return this.providerConnections .filter((connection) => connection.status === 'active') .map((connection) => connection.id); } private activeFlexAccountsForModel(modelStringArg: string): string[] { const activeAccounts = new Set(this.activeFlexAccountIds()); return (this.flexModelAvailabilityByString.get(modelStringArg) ?? []) .map((availability) => availability.providerConnectionId) .filter((providerConnectionId) => activeAccounts.has(providerConnectionId)); } private flexModelSupportsAccount(modelStringArg: string, accountIdArg: string): boolean { return isNonEmptyString(modelStringArg) && isNonEmptyString(accountIdArg) && this.flexModelAvailabilityByString.get(modelStringArg)?.some((availability) => ( availability.providerConnectionId === accountIdArg )) === true; } private modelVariantsForSession( modelStringArg: string, sessionIdArg?: TBrowserSessionId, ): string[] { const choice = this.modelChoicesByString.get(modelStringArg); if (choice?.harnessId !== 'flex') return this.modelVariantsByString.get(modelStringArg) ?? []; const accountId = this.resolveSessionAccount(sessionIdArg); if (!accountId) return this.modelVariantsByString.get(modelStringArg) ?? []; return this.flexModelAvailabilityByString.get(modelStringArg)?.find((availability) => ( availability.providerConnectionId === accountId ))?.variants ?? []; } private resolveSessionAccount(sessionIdArg?: TBrowserSessionId): string { const harnessId = sessionIdArg?.harnessId ?? this.draftHarnessId; if (harnessId !== 'flex') return ''; const sessionKey = this.composerChoiceKey(sessionIdArg); if (this.sessionAccountOverrides.has(sessionKey)) { const override = this.sessionAccountOverrides.get(sessionKey) ?? ''; return this.activeFlexAccountIds().includes(override) ? override : ''; } if ( sessionIdArg && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg) && isNonEmptyString(this.sessionDetail?.providerConnectionId) && this.activeFlexAccountIds().includes(this.sessionDetail.providerConnectionId) ) return this.sessionDetail.providerConnectionId; const persistedModel = sessionIdArg && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg) && this.sessionDetail?.modelChoice?.harnessId === 'flex' ? modelOptionLabel(this.sessionDetail.modelChoice) : ''; const modelString = this.sessionModelOverrides.get(sessionKey) || persistedModel || this.defaultModelStrings.flex; const capableAccounts = this.activeFlexAccountsForModel(modelString); return capableAccounts.length === 1 ? capableAccounts[0] : ''; } private resolveSessionModelString(sessionIdArg?: TBrowserSessionId): string { const harnessId = sessionIdArg?.harnessId ?? this.draftHarnessId; const sessionKey = this.composerChoiceKey(sessionIdArg); const account = harnessId === 'flex' ? this.resolveSessionAccount(sessionIdArg) : ''; const isAvailable = (modelStringArg: string): boolean => { const choice = this.modelChoicesByString.get(modelStringArg); if (choice?.harnessId !== harnessId) return false; return choice.harnessId !== 'flex' || (account ? this.flexModelSupportsAccount(modelStringArg, account) : this.activeFlexAccountsForModel(modelStringArg).length > 0); }; const override = this.sessionModelOverrides.get(sessionKey); if (override && isAvailable(override)) return override; if ( sessionIdArg && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg) && this.sessionDetail?.modelChoice?.harnessId === harnessId ) { const persisted = modelOptionLabel(this.sessionDetail.modelChoice); if (isAvailable(persisted)) return persisted; } const configuredDefault = this.defaultModelStrings[harnessId]; if (configuredDefault && isAvailable(configuredDefault)) return configuredDefault; return harnessId === 'flex' ? this.safeFlexDefaultModelString(account || undefined, account === '') : ''; } private resolveSessionEffortString(sessionIdArg?: TBrowserSessionId): string { const harnessId = sessionIdArg?.harnessId ?? this.draftHarnessId; const sessionKey = this.composerChoiceKey(sessionIdArg); const modelString = this.resolveSessionModelString(sessionIdArg); const variants = this.modelVariantsForSession(modelString, sessionIdArg); const override = this.sessionEffortOverrides.get(sessionKey); const persistedVariant = sessionIdArg && controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg) && this.sessionDetail?.modelChoice?.harnessId === harnessId && modelOptionLabel(this.sessionDetail.modelChoice) === modelString && isNonEmptyString(this.sessionDetail.modelChoice.variant) ? this.sessionDetail.modelChoice.variant : undefined; const candidate = override ?? persistedVariant ?? (modelString === this.defaultModelStrings[harnessId] ? this.defaultEffortStrings[harnessId] : ''); return variants.includes(candidate) ? candidate : ''; } private resolveSessionEffortOptions(sessionIdArg?: TBrowserSessionId): string[] { const modelString = this.resolveSessionModelString(sessionIdArg); const variants = this.modelVariantsForSession(modelString, sessionIdArg); const options = variants.length === 0 ? [] : variants.includes('default') ? variants : ['default', ...variants]; const cacheKey = `${modelString}\0${this.resolveSessionAccount(sessionIdArg)}`; const cached = this.effortOptionsCache.get(cacheKey); if (cached && sameStringArray(cached, options)) return cached; this.effortOptionsCache.set(cacheKey, options); return options; } /** * The request omits the model whenever the persisted default fully covers * this chat; any per-chat override makes the choice explicit end-to-end. */ private resolveExplicitModelChoice( sessionIdArg: TBrowserSessionId, ): interfaces.TControllerModelChoice | undefined { const sessionKey = this.composerChoiceKey(sessionIdArg); if ( !this.sessionModelOverrides.has(sessionKey) && !this.sessionEffortOverrides.has(sessionKey) && !this.sessionAccountOverrides.has(sessionKey) ) { return undefined; } return this.resolveComposerModelChoice(sessionIdArg); } private resolveComposerModelChoice( sessionIdArg?: TBrowserSessionId, ): interfaces.TControllerModelChoice | undefined { const harnessId = sessionIdArg?.harnessId ?? this.draftHarnessId; const choice = this.modelChoicesByString.get(this.resolveSessionModelString(sessionIdArg)); if (!choice || choice.harnessId !== harnessId) return undefined; if ( choice.harnessId === 'flex' && !this.flexModelSupportsAccount( modelOptionLabel(choice), this.resolveSessionAccount(sessionIdArg), ) ) { return undefined; } const effort = this.resolveSessionEffortString(sessionIdArg); return { ...choice, ...(effort ? { variant: effort } : {}) }; } private resolveInitialDraftModelChoice(): interfaces.TControllerModelChoice | undefined { const key = draftSessionKey(this.draftHarnessId); if ( this.sessionModelOverrides.has(key) || this.sessionEffortOverrides.has(key) || this.sessionAccountOverrides.has(key) || (this.draftHarnessId === 'flex' && this.safeFlexDefaultModelString() !== '') ) return this.resolveComposerModelChoice(); return undefined; } private async persistSessionModelChoice(sessionIdArg: TBrowserSessionId): Promise { if (this.blockForUnknownCodexMode(sessionIdArg)) return; const model = this.resolveComposerModelChoice(sessionIdArg); if (!model || !isNonEmptyString(this.selectedProjectId)) return; const providerConnectionId = model.harnessId === 'flex' ? this.resolveSessionAccount(sessionIdArg) : undefined; if (model.harnessId === 'flex' && !providerConnectionId) return; const projectId = this.selectedProjectId; const key = this.sessionOperationKey(projectId, sessionIdArg); const token = Symbol('sessionModelSave'); this.sessionModelSaveTokens.set(key, token); this.sessionModelPending = true; try { const response = await this.socketClient.fire( 'controller.session.model.update', { projectId, sessionId: sessionIdArg, model, ...(providerConnectionId ? { providerConnectionId } : {}), }, ); if (this.sessionModelSaveTokens.get(key) !== token) return; if (controllerRuntimeIdsEqual(this.sessionDetail?.session.id, sessionIdArg)) { this.sessionDetail = { ...this.sessionDetail!, modelChoice: { ...response.model }, ...(response.providerConnectionId ? { providerConnectionId: response.providerConnectionId } : { providerConnectionId: undefined }), }; } this.sessionModelOverrides.delete(key); this.sessionEffortOverrides.delete(key); this.sessionAccountOverrides.delete(key); } catch (errorArg) { if (this.sessionModelSaveTokens.get(key) !== token) return; this.sessionModelOverrides.delete(key); this.sessionEffortOverrides.delete(key); this.sessionAccountOverrides.delete(key); this.reportWorkspaceError('Change the conversation model', errorArg); if (controllerRuntimeIdsEqual(this.selectedSessionId, sessionIdArg)) { void this.loadSessionDetail(sessionIdArg); } } finally { if (this.sessionModelSaveTokens.get(key) === token) { this.sessionModelSaveTokens.delete(key); } this.sessionModelPending = this.sessionModelSaveTokens.size > 0; } } private settingsEffortDropdownOptions( modelStringArg: string, ): Array<{ option: string; key: string }> { const variants = this.modelVariantsByString.get(modelStringArg) ?? []; return [ { option: 'model default', key: '' }, ...variants.map((variant) => ({ option: variant, key: variant })), ]; } private modelOptionsForHarness( harnessIdArg: TBrowserSessionHarnessId, accountIdArg = '', ): string[] { const cacheKey = `${harnessIdArg}\0${accountIdArg}`; const options = this.modelOptionStrings.filter( (optionArg) => { const choice = this.modelChoicesByString.get(optionArg); return choice?.harnessId === harnessIdArg && (choice.harnessId !== 'flex' || !accountIdArg || this.flexModelSupportsAccount(optionArg, accountIdArg)); }, ); const cached = this.modelOptionsCache.get(cacheKey); if (cached && sameStringArray(cached, options)) return cached; this.modelOptionsCache.set(cacheKey, options); return options; } private stopProviderPolling(): void { if (this.providerLoginPollTimer) clearTimeout(this.providerLoginPollTimer); for (const timer of this.providerRefreshPollTimers.values()) clearTimeout(timer); this.providerLoginPollTimer = undefined; this.providerLoginPollDeadline = undefined; this.providerRefreshPollTimers.clear(); this.providerRefreshJobs.clear(); this.providerRefreshPollDeadlines.clear(); this.providerRefreshStartTokens.clear(); this.providerRefreshStatuses.clear(); this.providerLoginStatusRoot = undefined; this.providerLoginStatusError = ''; } private clearProviderManagementSnapshot(): void { this.providers = []; this.providerConnections = []; this.providerManagementState = 'loading'; } private isCurrentProviderGeneration(generationArg: number): boolean { return ( this.authenticated && this.socketClient.isConnected && this.connectionGeneration === generationArg ); } private refreshProviderManagement(): Promise { this.providerManagementRefreshRequested = true; if (this.providerManagementTask) return this.providerManagementTask; const task = this.drainProviderManagementRefreshes().finally(() => { if (this.providerManagementTask === task) this.providerManagementTask = undefined; }); this.providerManagementTask = task; return task; } private async drainProviderManagementRefreshes(): Promise { let acceptedCurrentSnapshot = false; while (this.providerManagementRefreshRequested) { this.providerManagementRefreshRequested = false; acceptedCurrentSnapshot = false; if (!this.authenticated || !this.socketClient.isConnected) return false; const generation = this.connectionGeneration; try { const [providersResponse, connectionsResponse] = await Promise.all([ this.socketClient.fire( 'controller.provider.list', {}, ), this.socketClient.fire( 'controller.provider.connection.list', {}, ), ]); if (!this.isCurrentProviderGeneration(generation)) continue; if (typeof connectionsResponse.runtimeAvailable !== 'boolean') { throw new Error('The controller returned an invalid provider runtime status.'); } this.providers = Array.isArray(providersResponse.providers) ? providersResponse.providers : []; this.providerConnections = Array.isArray(connectionsResponse.connections) ? connectionsResponse.connections : []; this.providerManagementState = connectionsResponse.runtimeAvailable ? 'available' : 'unavailable'; acceptedCurrentSnapshot = true; await this.refreshSettingsModalContent(); } catch { if (!this.isCurrentProviderGeneration(generation)) continue; this.providerManagementState = this.flexHarnessIsReady() ? 'error' : 'unavailable'; await this.refreshSettingsModalContent(); } } return acceptedCurrentSnapshot; } private updateProviderLoginStatus( loginArg: interfaces.IControllerProviderLogin | undefined, errorArg = '', ): void { this.providerLoginStatusError = errorArg; const root = this.providerLoginStatusRoot; if (!root?.isConnected) return; const message = root.querySelector('.providerLoginMessage'); const verificationLink = root.querySelector('.providerVerificationLink'); const userCode = root.querySelector('.providerUserCode'); const loginPending = loginArg?.status === 'pending'; if (message) { const account = loginArg?.account; const accountLabel = account?.email || account?.accountId || account?.plan; message.textContent = errorArg || (loginArg ? `${loginArg.status}${accountLabel ? ` · ${accountLabel}` : ''}` : 'No OpenAI login is active.'); } if (verificationLink) { let safeUrl = ''; try { const parsed = loginPending ? new URL(loginArg.verificationUrl) : undefined; if (parsed && (parsed.protocol === 'https:' || parsed.protocol === 'http:')) { safeUrl = parsed.href; } } catch { safeUrl = ''; } verificationLink.hidden = !safeUrl; verificationLink.href = safeUrl; verificationLink.textContent = safeUrl ? 'Open verification page' : ''; } if (userCode) { userCode.hidden = !loginPending || !loginArg.userCode; userCode.textContent = loginPending && loginArg.userCode ? `Code: ${loginArg.userCode}` : ''; } } private beginProviderLogin(): Promise { if (!this.providerOperationsAvailable()) return Promise.resolve(); if (this.activeProviderLogin?.status === 'pending') { this.updateProviderLoginStatus(this.activeProviderLogin, 'An OpenAI login is already active.'); return Promise.resolve(); } if (this.providerLoginBeginTask) return this.providerLoginBeginTask; const task = this.beginProviderLoginOnce().finally(() => { if (this.providerLoginBeginTask === task) this.providerLoginBeginTask = undefined; }); this.providerLoginBeginTask = task; return task; } private async beginProviderLoginOnce(): Promise { if (!this.providerOperationsAvailable()) return; const openAi = this.providers.find((providerArg) => providerArg.id === 'openai'); if (openAi && !openAi.loginFlows.includes('device')) { this.updateProviderLoginStatus(undefined, 'OpenAI device login is unavailable.'); return; } const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.provider.login.begin', { providerID: 'openai' }, ); if ( !isControllerRuntimeId(response.login?.id) || response.login.id.harnessId !== 'flex' || !isNonEmptyString(response.login.verificationUrl) || !isNonEmptyString(response.login.userCode) ) { throw new Error('The controller returned an invalid public login status.'); } if (!this.isCurrentProviderGeneration(generation)) { if (this.socketClient.isConnected) { await this.socketClient.fire( 'controller.provider.login.cancel', { loginId: response.login.id }, ).catch(() => undefined); } return; } this.activeProviderLogin = response.login; this.providerLoginPollDeadline = Date.now() + providerLoginPollTimeoutMs; this.updateProviderLoginStatus(response.login); this.scheduleProviderLoginPoll(); } catch (errorArg) { this.updateProviderLoginStatus( undefined, `OpenAI login could not be started: ${errorMessage(errorArg)}`, ); } } private scheduleProviderLoginPoll(): void { if (this.providerLoginPollTimer) clearTimeout(this.providerLoginPollTimer); if (this.activeProviderLogin?.status !== 'pending') return; if ( this.providerLoginPollDeadline !== undefined && Date.now() >= this.providerLoginPollDeadline ) { this.updateProviderLoginStatus( this.activeProviderLogin, 'Login status polling expired. Cancel this login or start a new one.', ); return; } this.providerLoginPollTimer = setTimeout(() => { this.providerLoginPollTimer = undefined; void this.pollProviderLogin(); }, 1_500); } private pollProviderLogin(): Promise { const login = this.activeProviderLogin; const taskKey = login ? JSON.stringify([this.connectionGeneration, interfaces.controllerRuntimeIdKey(login.id)]) : ''; if (this.providerLoginPollTask) { if (this.providerLoginPollTaskKey === taskKey) return this.providerLoginPollTask; return this.providerLoginPollTask.then(() => this.pollProviderLogin()); } const task = this.pollProviderLoginOnce().finally(() => { if (this.providerLoginPollTask === task) { this.providerLoginPollTask = undefined; this.providerLoginPollTaskKey = undefined; } }); this.providerLoginPollTask = task; this.providerLoginPollTaskKey = taskKey; return task; } private async pollProviderLoginOnce(): Promise { const login = this.activeProviderLogin; if ( !login || login.status !== 'pending' || !this.authenticated || !this.providerOperationsAvailable() ) return; const generation = this.connectionGeneration; try { const response = await this.socketClient.fire( 'controller.provider.login.get', { loginId: login.id }, ); if (!controllerRuntimeIdsEqual(response.login.id, login.id)) return; if ( !this.isCurrentProviderGeneration(generation) || !this.activeProviderLogin || this.activeProviderLogin.status !== 'pending' || !controllerRuntimeIdsEqual(this.activeProviderLogin.id, login.id) ) return; this.activeProviderLogin = response.login; this.updateProviderLoginStatus(response.login); if (response.login.status === 'pending') { this.scheduleProviderLoginPoll(); } else { this.providerLoginPollDeadline = undefined; const managementAccepted = await this.refreshProviderManagement(); if ( response.login.status !== 'succeeded' || !managementAccepted || !this.isCurrentProviderGeneration(generation) || !this.activeProviderLogin || !controllerRuntimeIdsEqual(this.activeProviderLogin.id, login.id) ) return; const connection = this.providerConnections.find((connectionArg) => ( connectionArg.id === login.id.nativeId && connectionArg.status === 'active' )); if (!connection) { this.updateProviderLoginStatus( response.login, 'OpenAI connected, but its provider connection is not available yet.', ); return; } await this.refreshProviderCatalog(connection); } } catch { this.updateProviderLoginStatus(login, 'Login status could not be refreshed. Retrying.'); this.scheduleProviderLoginPoll(); } } private async cancelProviderLogin(): Promise { if (!this.providerOperationsAvailable()) return; const login = this.activeProviderLogin; if (!login || login.status !== 'pending') return; try { const response = await this.socketClient.fire( 'controller.provider.login.cancel', { loginId: login.id }, ); if (!response.cancelled) { await this.pollProviderLogin(); return; } this.activeProviderLogin = { ...login, status: 'cancelled' }; this.providerLoginPollDeadline = undefined; this.updateProviderLoginStatus(this.activeProviderLogin); } catch { this.updateProviderLoginStatus(login, 'The login could not be cancelled.'); } } private async logoutProviderConnection( connectionArg: interfaces.IControllerProviderConnection, ): Promise { if (!this.providerOperationsAvailable()) return; try { await this.socketClient.fire( 'controller.provider.connection.logout', { providerConnectionId: connectionArg.id }, ); this.providerConnections = this.providerConnections.filter( (candidateArg) => candidateArg.id !== connectionArg.id, ); this.clearProviderRefreshState(connectionArg.id); this.providerRateLimits.delete(connectionArg.id); this.providerRateLimitErrors.delete(connectionArg.id); await this.refreshSettingsModalContent(); await this.refreshModelCatalog(); } catch (errorArg) { this.updateProviderLoginStatus( this.activeProviderLogin, `The provider connection could not be logged out: ${errorMessage(errorArg)}`, ); } } private isCurrentProviderOpenCodeSwitch(switchArg: IProviderOpenCodeSwitch): boolean { return ( this.providerOpenCodeSwitch?.token === switchArg.token && this.providerOpenCodeSwitch.generation === switchArg.generation && this.isCurrentProviderGeneration(switchArg.generation) ); } private async activateProviderConnectionForOpenCode( connectionArg: interfaces.IControllerProviderConnection, ): Promise { if ( !this.providerOperationsAvailable() || connectionArg.status !== 'active' || connectionArg.selectedForOpenCode === true || this.providerOpenCodeSwitch ) return; const switchState: IProviderOpenCodeSwitch = { token: Symbol(connectionArg.id), connectionId: connectionArg.id, generation: this.connectionGeneration, }; this.providerOpenCodeSwitch = switchState; this.providerOpenCodeSwitchStatus = ''; try { await this.refreshSettingsModalContent(); const response = await this.socketClient.fire< interfaces.IReq_ControllerProviderConnectionActivateOpenCode >( 'controller.provider.connection.activate-opencode', { providerConnectionId: connectionArg.id }, { timeoutMs: providerOpenCodeSwitchTimeoutMs, maxRetries: 0 }, ); if (!this.isCurrentProviderOpenCodeSwitch(switchState)) return; await this.refreshProviderManagement(); if (!this.isCurrentProviderOpenCodeSwitch(switchState)) return; this.providerOpenCodeSwitchStatus = [ 'OpenCode switched.', `Paused sessions: ${response.pausedSessions}.`, `Continued sessions: ${response.continuedSessions}.`, ].join(' '); } catch (errorArg) { if (!this.isCurrentProviderOpenCodeSwitch(switchState)) return; globalThis.alert(`OpenCode account could not be switched: ${errorMessage(errorArg)}`); } finally { if (this.isCurrentProviderOpenCodeSwitch(switchState)) { this.providerOpenCodeSwitch = undefined; await this.refreshSettingsModalContent(); } } } private refreshProviderRateLimits( connectionArg: interfaces.IControllerProviderConnection, ): Promise { if (!this.providerOperationsAvailable()) return Promise.resolve(); const existing = this.providerRateLimitTasks.get(connectionArg.id); if (existing) return existing; const generation = this.connectionGeneration; const task = (async () => { if (connectionArg.status !== 'active') { this.providerRateLimits.delete(connectionArg.id); return; } try { const response = await this.socketClient.fire< interfaces.IReq_ControllerProviderConnectionRateLimitsGet >( 'controller.provider.connection.ratelimits.get', { providerConnectionId: connectionArg.id }, ); if (!this.isCurrentProviderGeneration(generation)) return; this.providerRateLimits.set(connectionArg.id, response.rateLimits); this.providerRateLimitErrors.delete(connectionArg.id); } catch (errorArg) { if (!this.isCurrentProviderGeneration(generation)) return; this.providerRateLimitErrors.set(connectionArg.id, errorMessage(errorArg)); } })().finally(() => { if (this.providerRateLimitTasks.get(connectionArg.id) === task) { this.providerRateLimitTasks.delete(connectionArg.id); } }); this.providerRateLimitTasks.set(connectionArg.id, task); return task; } private renderProviderRateLimits( connectionArg: interfaces.IControllerProviderConnection, ): plugins.deesElement.TemplateResult { const rateLimits = this.providerRateLimits.get(connectionArg.id); const error = this.providerRateLimitErrors.get(connectionArg.id); if (!rateLimits) { return plugins.deesElement.html`
${error ? `Usage unavailable: ${error}` : 'Usage has not been loaded.'}
`; } const windows: Array<{ label: string; window: interfaces.IControllerProviderRateLimitWindow; }> = []; if (rateLimits.rateLimit?.primaryWindow) { windows.push({ label: 'Primary', window: rateLimits.rateLimit.primaryWindow }); } if (rateLimits.rateLimit?.secondaryWindow) { windows.push({ label: 'Secondary', window: rateLimits.rateLimit.secondaryWindow }); } for (const additional of rateLimits.additionalRateLimits) { if (additional.rateLimit?.primaryWindow) { windows.push({ label: additional.limitName, window: additional.rateLimit.primaryWindow }); } } return plugins.deesElement.html`
Usage · ${rateLimits.plan || connectionArg.account.plan || 'unknown plan'} · observed ${new Date(rateLimits.observedAt).toLocaleString()}
${windows.length === 0 ? plugins.deesElement.html`
No metered windows were reported.
` : windows.map(({ label, window }) => plugins.deesElement.html`
${label}: ${formatPercentage(window.usedPercent)} used · resets in ${formatDuration(window.resetAfterSeconds)}
`)} ${error ? plugins.deesElement.html`
Latest refresh failed: ${error}
` : ''}
`; } private async refreshProviderCatalog( connectionArg: interfaces.IControllerProviderConnection, ): Promise { if (!this.providerOperationsAvailable()) return; if (connectionArg.status !== 'active') { this.providerRefreshStatuses.set( connectionArg.id, 'Reauthentication required. Log out and connect OpenAI again.', ); await this.refreshSettingsModalContent(); return; } if ( this.providerRefreshStartTokens.has(connectionArg.id) || this.providerRefreshJobs.has(connectionArg.id) ) return; const token = Symbol(connectionArg.id); const generation = this.connectionGeneration; this.providerRefreshStartTokens.set(connectionArg.id, token); this.providerRefreshStatuses.set(connectionArg.id, 'starting'); await this.refreshSettingsModalContent(); try { const response = await this.socketClient.fire( 'controller.provider.model.refresh.begin', { providerConnectionId: connectionArg.id }, ); if ( !this.isCurrentProviderGeneration(generation) || this.providerRefreshStartTokens.get(connectionArg.id) !== token ) return; this.providerRefreshJobs.set(connectionArg.id, response.job.id); this.providerRefreshPollDeadlines.set( connectionArg.id, Date.now() + providerRefreshPollTimeoutMs, ); this.providerRefreshStatuses.set(connectionArg.id, response.job.status); await this.refreshSettingsModalContent(); this.scheduleProviderCatalogPoll(connectionArg.id, response.job.id); } catch (errorArg) { if (this.providerRefreshStartTokens.get(connectionArg.id) === token) { this.providerRefreshStatuses.set( connectionArg.id, `Model refresh could not be started: ${errorMessage(errorArg)}`, ); await this.refreshSettingsModalContent(); } } finally { if (this.providerRefreshStartTokens.get(connectionArg.id) === token) { this.providerRefreshStartTokens.delete(connectionArg.id); } } } private scheduleProviderCatalogPoll( connectionIdArg: string, jobIdArg: interfaces.IControllerRuntimeId, ): void { const previousTimer = this.providerRefreshPollTimers.get(connectionIdArg); if (previousTimer) clearTimeout(previousTimer); const timer = setTimeout(() => { if (this.providerRefreshPollTimers.get(connectionIdArg) === timer) { this.providerRefreshPollTimers.delete(connectionIdArg); } void this.pollProviderCatalog(connectionIdArg, jobIdArg); }, 1_500); this.providerRefreshPollTimers.set(connectionIdArg, timer); } private async pollProviderCatalog( connectionIdArg: string, jobIdArg: interfaces.IControllerRuntimeId, ): Promise { const currentJob = this.providerRefreshJobs.get(connectionIdArg); if (!currentJob || !controllerRuntimeIdsEqual(currentJob, jobIdArg)) return; const deadline = this.providerRefreshPollDeadlines.get(connectionIdArg); if (deadline !== undefined && Date.now() >= deadline) { this.providerRefreshJobs.delete(connectionIdArg); this.providerRefreshPollDeadlines.delete(connectionIdArg); this.providerRefreshStatuses.set(connectionIdArg, 'Model refresh status polling expired.'); await this.refreshSettingsModalContent(); return; } try { const response = await this.socketClient.fire( 'controller.provider.model.refresh.get', { jobId: jobIdArg }, ); const latestJob = this.providerRefreshJobs.get(connectionIdArg); if (!latestJob || !controllerRuntimeIdsEqual(latestJob, jobIdArg)) return; this.providerRefreshStatuses.set( connectionIdArg, response.job.modelCount === undefined ? response.job.status : `${response.job.status} · ${response.job.modelCount} models`, ); await this.refreshSettingsModalContent(); if (response.job.status === 'pending' || response.job.status === 'running') { this.scheduleProviderCatalogPoll(connectionIdArg, jobIdArg); return; } this.providerRefreshJobs.delete(connectionIdArg); this.providerRefreshPollDeadlines.delete(connectionIdArg); if (response.job.status === 'completed') await this.refreshModelCatalog(); } catch { const latestJob = this.providerRefreshJobs.get(connectionIdArg); if (!latestJob || !controllerRuntimeIdsEqual(latestJob, jobIdArg)) return; this.providerRefreshJobs.delete(connectionIdArg); this.providerRefreshPollDeadlines.delete(connectionIdArg); this.providerRefreshStatuses.set(connectionIdArg, 'Model refresh status is unavailable.'); await this.refreshSettingsModalContent(); } } private clearProviderRefreshState(connectionIdArg: string): void { const timer = this.providerRefreshPollTimers.get(connectionIdArg); if (timer) clearTimeout(timer); this.providerRefreshPollTimers.delete(connectionIdArg); this.providerRefreshJobs.delete(connectionIdArg); this.providerRefreshPollDeadlines.delete(connectionIdArg); this.providerRefreshStartTokens.delete(connectionIdArg); this.providerRefreshStatuses.delete(connectionIdArg); } private readonly showSettingsModal = (): void => { if (this.settingsModalRef?.isConnected || this.settingsModalOpenTask) return; const task = this.openSettingsModal().finally(() => { if (this.settingsModalOpenTask === task) this.settingsModalOpenTask = undefined; }); this.settingsModalOpenTask = task; }; private async openSettingsModal(): Promise { const connectionGeneration = this.connectionGeneration; await this.refreshProviderManagement(); if (!this.isCurrentProviderGeneration(connectionGeneration)) return; await Promise.all(this.providerConnections.map((connection) => ( this.refreshProviderRateLimits(connection) ))); if (!this.isCurrentProviderGeneration(connectionGeneration)) return; this.settingsDraftHarnessId = this.selectedSessionId?.harnessId ?? this.draftHarnessId; this.settingsDraftModelString = this.defaultModelStrings[this.settingsDraftHarnessId]; this.settingsDraftEffortString = this.defaultEffortStrings[this.settingsDraftHarnessId]; this.settingsDraftAutoAccept = this.autoAcceptPermissions; this.settingsDraftBrowserVideoBackend = this.browserVideoBackend; const modalGeneration = ++this.settingsModalGeneration; const sections = [ { id: 'general', title: 'General', description: 'Default models and permissions for new messages.', icon: 'lucide:Settings' }, { id: 'projects', title: 'Projects', description: 'Registered projects and the directories new conversations can use.', icon: 'lucide:FolderTree' }, { id: 'accounts', title: 'Accounts', description: 'Saved logins, subscription status, and usage.', icon: 'lucide:Users' }, { id: 'codex', title: 'Codex connections', description: 'Codex servers available to this project.', icon: 'lucide:Terminal' }, { id: 'flex', title: 'Flex connections', description: 'Provider logins for Flex and OpenCode.', icon: 'lucide:Plug' }, ] as const; let activeSection: (typeof sections)[number]['id'] = 'general'; const renderSidebar = (): plugins.deesElement.TemplateResult => plugins.deesElement.html`
Settings
`; const renderContent = (): plugins.deesElement.TemplateResult => { const providerOperationsAvailable = this.providerOperationsAvailable(); const options = this.modelOptionsForHarness(this.settingsDraftHarnessId).map((optionString) => ({ option: optionString, key: optionString, })); const selected = this.settingsDraftModelString ? { option: this.settingsDraftModelString, key: this.settingsDraftModelString } : null; const effortOptions = this.settingsEffortDropdownOptions(this.settingsDraftModelString); const effortSelected = this.settingsDraftEffortString ? { option: this.settingsDraftEffortString, key: this.settingsDraftEffortString } : { option: 'model default', key: '' }; return plugins.deesElement.html`
Harness
) => { const harnessId = eventArg.detail?.value; if (harnessId !== 'opencode' && harnessId !== 'flex' && harnessId !== 'codex') return; this.settingsDraftHarnessId = harnessId; this.settingsDraftModelString = this.defaultModelStrings[harnessId]; this.settingsDraftEffortString = this.defaultEffortStrings[harnessId]; const root = (eventArg.currentTarget as HTMLElement | null) ?.closest('div[style*="grid"]'); const modelDropdown = root?.querySelector('dees-input-dropdown.settingsModelSelect') as | (HTMLElement & { options: Array<{ option: string; key: string }>; selectedOption: { option: string; key: string } | null; }) | null; const effortDropdown = root?.querySelector('dees-input-dropdown.settingsEffortSelect') as | (HTMLElement & { options: Array<{ option: string; key: string }>; selectedOption: { option: string; key: string } | null; }) | null; if (modelDropdown) { modelDropdown.options = this.modelOptionsForHarness(harnessId).map((option) => ({ option, key: option, })); modelDropdown.selectedOption = this.settingsDraftModelString ? { option: this.settingsDraftModelString, key: this.settingsDraftModelString } : null; } if (effortDropdown) { effortDropdown.options = this.settingsEffortDropdownOptions( this.settingsDraftModelString, ); effortDropdown.selectedOption = this.settingsDraftEffortString ? { option: this.settingsDraftEffortString, key: this.settingsDraftEffortString } : { option: 'model default', key: '' }; } }} >
Default model
Used for every new message in all projects unless a chat overrides it from the composer.
) => { const key = eventArg.detail?.key; if (!isNonEmptyString(key)) { return; } this.settingsDraftModelString = key; this.settingsDraftEffortString = ''; // Keep the dependent dropdown in sync during this input event. const modelDropdown = eventArg.currentTarget as HTMLElement | null; const effortDropdown = modelDropdown ?.closest('div[style*="grid"]') ?.querySelector('dees-input-dropdown.settingsEffortSelect'); if (effortDropdown) { const dropdown = effortDropdown as unknown as { options: Array<{ option: string; key: string }>; selectedOption: { option: string; key: string } | null; }; dropdown.options = this.settingsEffortDropdownOptions(key); dropdown.selectedOption = { option: 'model default', key: '' }; } }} >
Default reasoning effort
Only models that advertise effort variants offer choices here; 'model default' sends no explicit effort.
) => { const key = eventArg.detail?.key; if (typeof key === 'string') { this.settingsDraftEffortString = key; } }} >
Browser video
) => { const key = event.detail?.key; if (key === 'chromium' || key === 'native') this.settingsDraftBrowserVideoBackend = key; }}>
Active: ${this.activeBrowserVideoBackend === 'native' ? 'NVIDIA GPU' : 'Chromium'}. Restart AGL after saving to change the video backend. Native capture requires Linux with NVIDIA graphics and NVENC.
Global auto-accept (yolo)
The controller approves every permission request in all projects on its own. Agents run tools without asking; only enable this when you trust every registered project.
) => { this.settingsDraftAutoAccept = eventArg.detail === true; }} >
Registered projects
A project is registered the first time a conversation is created or opened in its directory. Deregistering keeps the files on disk.
${this.projects.length === 0 ? plugins.deesElement.html`
No projects registered yet.
` : this.projects.map((projectArg) => plugins.deesElement.html`
${projectArg.name}
${projectArg.directory}
this.showRemoveProjectConfirmation(projectArg.id)} >Deregister
`)}
Standard project directories
Absolute existing directories AGL offers as project locations. Their immediate subdirectories are searched when you open a conversation, and offered when you start one. Nothing is registered until you do.
${this.standardProjectDirectories.length === 0 ? plugins.deesElement.html`
None configured; conversations are opened by typing a directory.
` : this.standardProjectDirectories.map((directoryArg) => plugins.deesElement.html`
${directoryArg}
void this.updateStandardProjectDirectories( this.standardProjectDirectories.filter( (candidateArg) => candidateArg !== directoryArg, ), )} >Remove
`)}
{ const target = eventArg.currentTarget as HTMLElement & { value?: unknown }; this.settingsStandardDirectoryDraft = typeof target.value === 'string' ? target.value : ''; }} > void this.addStandardProjectDirectory()} >Add
${this.settingsProjectsError ? plugins.deesElement.html` ` : ''}

project.id === this.selectedProjectId)} .status=${this.controllerStatus?.harnesses.find(entry => entry.harnessId === 'codex')} @codex-connection-changed=${() => { this.scheduleRefresh(); void this.refreshModelCatalog(); }}>
OpenAI sign-in uses the provider device flow. This UI never accepts or displays credentials, access tokens, API keys, or raw provider responses. Choose which connected account OpenCode uses; active sessions pause while it restarts.
${this.providerManagementState === 'unavailable' ? plugins.deesElement.html`
Flex is unavailable and reconnecting. Saved accounts are shown read-only.
` : this.providerManagementState === 'error' ? plugins.deesElement.html`
Provider connections could not be refreshed. Previously loaded accounts are shown read-only.
` : this.providerManagementState === 'loading' ? plugins.deesElement.html`
Refreshing provider connections...
` : ''} ${this.providerConnections.length === 0 ? plugins.deesElement.html`
${this.providerManagementState === 'available' ? 'No provider connections.' : 'Saved provider connections are not available.'}
` : this.providerConnections.map((connectionArg) => { const switchTargetsConnection = this.providerOpenCodeSwitch?.connectionId === connectionArg.id; const switchInProgress = this.providerOpenCodeSwitch !== undefined; const switchLabel = switchTargetsConnection ? 'Switching OpenCode...' : connectionArg.status !== 'active' ? 'Reauthenticate for OpenCode' : connectionArg.selectedForOpenCode ? 'Used by OpenCode' : 'Use for OpenCode'; return plugins.deesElement.html`
${connectionArg.providerID} ${connectionArg.account.email ? ` · ${connectionArg.account.email}` : connectionArg.account.accountId ? ` · ${connectionArg.account.accountId}` : ''} ${connectionArg.account.plan ? ` · ${connectionArg.account.plan}` : ''} · ${connectionArg.status === 'active' ? 'Connected' : 'Reauthentication required'}
${this.renderProviderRateLimits(connectionArg)}
void this.activateProviderConnectionForOpenCode( connectionArg, )} > ${connectionArg.status === 'active' ? plugins.deesElement.html` void this.refreshProviderCatalog(connectionArg)} >Refresh models void this.refreshProviderRateLimits(connectionArg) .then(() => this.refreshSettingsModalContent())} >Refresh usage ` : plugins.deesElement.html` Log out, then connect OpenAI again. `} void this.logoutProviderConnection(connectionArg)} >Log out ${this.providerRefreshStatuses.get(connectionArg.id) ?? ''}
`; })}
${this.providerOpenCodeSwitchStatus}
void this.beginProviderLogin()} > Connect OpenAI void this.cancelProviderLogin()} > Cancel login
`; }; this.settingsModalContentRenderer = renderContent; this.settingsModalSidebarRenderer = renderSidebar; const modalArg = await plugins.deesCatalog.DeesModal.createAndShow({ heading: sections[0].title, subheading: sections[0].description, sidebar: renderSidebar(), width: 1000, height: 680, mobileFullscreen: true, content: renderContent(), menuOptions: [ { name: 'Cancel', action: async (menuModalArg) => { this.clearSettingsModalReference(menuModalArg); await menuModalArg?.destroy(); }, }, { name: 'Save settings', action: async (menuModalArg) => { this.clearSettingsModalReference(menuModalArg); await menuModalArg?.destroy(); await this.persistSettings( this.settingsDraftModelString, this.settingsDraftEffortString, this.settingsDraftAutoAccept, this.settingsDraftBrowserVideoBackend, ); }, }, ], }); if ( modalGeneration !== this.settingsModalGeneration || !this.authenticated || !this.socketClient.isConnected ) { if (modalArg.isConnected) await modalArg.destroy().catch(() => undefined); return; } this.settingsModalRef = modalArg; const destroyModal = modalArg.destroy.bind(modalArg); modalArg.destroy = async () => { this.clearSettingsModalReference(modalArg); await destroyModal(); }; await this.refreshSettingsModalContent(); } /** * Adds the typed directory to the standard project directories. The controller validates it * (absolute, existing, distinct) and its message is shown as-is, because it is the only place * that can decide whether the path really is a directory on the controller's disk. */ private async addStandardProjectDirectory(): Promise { const directory = this.settingsStandardDirectoryDraft.trim().replace(/(?<=.)\/+$/, ''); if (!directory) { this.settingsProjectsError = 'Enter the absolute path of an existing directory.'; await this.refreshSettingsModalContent(); return; } if (this.standardProjectDirectories.includes(directory)) { this.settingsProjectsError = 'That directory is already a standard project directory.'; await this.refreshSettingsModalContent(); return; } await this.updateStandardProjectDirectories([ ...this.standardProjectDirectories, directory, ]); } private async updateStandardProjectDirectories(directoriesArg: string[]): Promise { const mutationId = this.beginMutation(); if (!mutationId) return; const generation = this.connectionGeneration; this.settingsProjectsError = ''; try { const response = await this.socketClient.fire( 'controller.settings.update', { standardProjectDirectories: directoriesArg }, ); if (!this.isCurrentMutation(mutationId, generation)) return; this.standardProjectDirectories = Array.isArray( response.settings?.standardProjectDirectories, ) ? response.settings.standardProjectDirectories.filter(isNonEmptyString) : []; this.settingsStandardDirectoryDraft = ''; } catch (errorArg) { if (this.isCurrentMutation(mutationId, generation)) { this.settingsProjectsError = errorMessage(errorArg); } } finally { this.finishMutation(mutationId); } await this.refreshSettingsModalContent(); } private clearSettingsModalReference( modalArg?: InstanceType, ): void { if (modalArg && this.settingsModalRef !== modalArg) return; this.settingsModalRef = undefined; this.settingsModalContentRenderer = undefined; this.settingsModalSidebarRenderer = undefined; this.providerLoginStatusRoot = undefined; this.settingsModalGeneration += 1; } private destroySettingsModal(): void { const modal = this.settingsModalRef; this.clearSettingsModalReference(modal); if (modal?.isConnected) void modal.destroy().catch(() => undefined); } private async refreshSettingsModalContent(): Promise { const modal = this.settingsModalRef; const renderContent = this.settingsModalContentRenderer; const generation = this.settingsModalGeneration; if (!modal || !renderContent) return; if (!modal.isConnected) { this.clearSettingsModalReference(modal); return; } if (!this.authenticated || !this.socketClient.isConnected) return; modal.content = renderContent(); modal.sidebar = this.settingsModalSidebarRenderer?.(); await modal.updateComplete; if ( this.settingsModalRef !== modal || this.settingsModalContentRenderer !== renderContent || this.settingsModalGeneration !== generation || !modal.isConnected || !this.authenticated || !this.socketClient.isConnected ) return; const root = modal.shadowRoot?.querySelector('.providerLoginStatus') ?? modal.querySelector('.providerLoginStatus'); this.providerLoginStatusRoot = root ?? undefined; this.updateProviderLoginStatus(this.activeProviderLogin, this.providerLoginStatusError); } private async persistSettings( modelStringArg: string, effortStringArg: string, autoAcceptArg: boolean, browserVideoBackendArg: 'chromium' | 'native' = this.browserVideoBackend, ): Promise { const harnessId = this.settingsDraftHarnessId; const choiceCandidate = this.modelChoicesByString.get(modelStringArg); const choice = choiceCandidate?.harnessId === harnessId ? choiceCandidate : undefined; const variants = this.modelVariantsByString.get(modelStringArg) ?? []; const effort = choice && variants.includes(effortStringArg) ? effortStringArg : ''; const previousModelStrings = { ...this.defaultModelStrings }; const previousEffortStrings = { ...this.defaultEffortStrings }; const previousModelChoices = { ...this.defaultModelChoices }; const previousAutoAccept = this.autoAcceptPermissions; if (choice) { this.defaultModelStrings = { ...this.defaultModelStrings, [harnessId]: modelStringArg }; this.defaultEffortStrings = { ...this.defaultEffortStrings, [harnessId]: effort }; this.defaultModelChoices = { ...this.defaultModelChoices, [harnessId]: { ...choice, ...(effort ? { variant: effort } : {}) }, }; } this.autoAcceptPermissions = autoAcceptArg; try { const response = await this.socketClient.fire( 'controller.settings.update', { // A missing model choice leaves the stored default untouched instead // of blocking the toggle from saving. ...(choice ? { defaultModels: (['opencode', 'flex', 'codex'] as const).flatMap((defaultHarnessId) => ( this.defaultModelChoices[defaultHarnessId] ? [{ ...this.defaultModelChoices[defaultHarnessId] }] : [] )), } : {}), autoAcceptPermissions: autoAcceptArg, ...(browserVideoBackendArg === this.browserVideoBackend ? {} : { browserVideoBackend: browserVideoBackendArg }), }, ); this.autoAcceptPermissions = response.settings.autoAcceptPermissions === true; this.browserVideoBackend = response.settings.browserVideoBackend ?? 'chromium'; this.activeBrowserVideoBackend = response.settings.activeBrowserVideoBackend ?? 'chromium'; } catch (error) { this.defaultModelStrings = previousModelStrings; this.defaultEffortStrings = previousEffortStrings; this.defaultModelChoices = previousModelChoices; this.autoAcceptPermissions = previousAutoAccept; this.reportWorkspaceError('Save settings', error); } } private slashCatalogKey( projectIdArg: string, harnessIdArg: TBrowserSessionHarnessId, sessionIdArg?: TBrowserSessionId, ): string { return JSON.stringify([ projectIdArg, harnessIdArg, sessionIdArg ? controllerRuntimeIdToUiKey(sessionIdArg) : 'draft', ]); } private invalidateSlashCatalog(): void { this.slashCatalogRequestId += 1; this.slashMenuEpisode += 1; this.slashMenuEligible = false; this.slashCatalogState = undefined; this.slashCatalogRequest?.abortController.abort(new DOMException( 'The slash catalog request is no longer current.', 'AbortError', )); this.slashCatalogRequest = undefined; this.slashSuggestions = []; this.slashReversion = undefined; } private closeSlashMenu(forceArg = false): void { if (forceArg || this.slashMenuEligible) { this.slashCatalogRequestId += 1; this.slashMenuEpisode += 1; this.slashCatalogRequest?.abortController.abort(new DOMException( 'The slash menu closed.', 'AbortError', )); this.slashCatalogRequest = undefined; } this.slashMenuEligible = false; if (this.slashSuggestions.length > 0) this.slashSuggestions = []; } private slashInputEligible(valueArg: string): boolean { // eslint-disable-next-line no-control-regex return /^\/[^\s/\\\x00-\x1f\x7f]*$/u.test(valueArg); } private currentSlashCatalogKey(): string { const harnessId = this.selectedSessionId?.harnessId ?? this.draftHarnessId; return this.slashCatalogKey(this.selectedProjectId, harnessId, this.selectedSessionId); } private updateSlashSuggestions(valueArg: string): void { if (!this.slashInputEligible(valueArg)) { this.closeSlashMenu(); return; } this.slashMenuEligible = true; const harnessId = this.selectedSessionId?.harnessId ?? this.draftHarnessId; const catalog = this.slashCatalogState; const serverCommands = catalog && catalog.key === this.currentSlashCatalogKey() && catalog.connectionGeneration === this.connectionGeneration ? catalog.commands : []; const serverByExactName = new Map(); for (const command of serverCommands) { if (!serverByExactName.has(command.name)) serverByExactName.set(command.name, command); } const merged: Array<{ name: string; description: string }> = []; const seenNames = new Set(); for (const builtin of localBuiltinSlashCommands(harnessId)) { const server = serverByExactName.get(builtin.name); const description = server?.description || builtin.description; merged.push({ name: builtin.name, description: server?.available === false ? `${description}${description ? ' ' : ''}Unavailable: ${server.unavailableReason || 'not available for this session.'}` : description, }); seenNames.add(builtin.name); } for (const command of serverCommands) { if (seenNames.has(command.name)) continue; seenNames.add(command.name); merged.push({ name: command.name, description: command.available === false ? `${command.description}${command.description ? ' ' : ''}Unavailable: ${command.unavailableReason || 'not available for this session.'}` : command.description, }); } const prefix = valueArg.slice(1).toLowerCase(); this.slashSuggestions = merged .filter((command) => command.name.toLowerCase().startsWith(prefix)) .slice(0, maximumSlashSuggestions) .map((command): plugins.deesCatalog.IHarnessComposerSuggestion => ({ label: `/${command.name}`, value: `/${command.name} `, ...(command.description ? { description: command.description } : {}), })); if (this.selectedSessionId) void this.loadSlashCatalog(); } private async fetchSlashCatalog( ownershipArg: ISlashCatalogOwnership, signalArg: AbortSignal, ): Promise { try { const response = await this.socketClient.fire( 'controller.slash.list', { projectId: ownershipArg.projectId, sessionId: ownershipArg.sessionId }, { maxRetries: 0, abortSignal: signalArg }, ); if (!this.slashCatalogOwnershipIsCurrent(ownershipArg)) return; const state: ISlashCatalogState = { ...ownershipArg, commands: Array.isArray(response.commands) ? response.commands : [], reversion: response.reversion, }; this.slashCatalogState = state; this.slashReversion = response.reversion; const draftText = this.sessionDraftState?.projectId === ownershipArg.projectId && controllerRuntimeIdsEqual(this.sessionDraftState.sessionId, ownershipArg.sessionId) ? this.sessionDraftState.text : ''; if (this.slashMenuEligible) this.updateSlashSuggestions(draftText); } catch { // Listing is passive. Local built-ins remain available without surfacing an error. } } private slashCatalogOwnershipIsCurrent(ownershipArg: ISlashCatalogOwnership): boolean { return ownershipArg.requestId === this.slashCatalogRequestId && ownershipArg.menuEpisode === this.slashMenuEpisode && ownershipArg.connectionGeneration === this.connectionGeneration && this.isCurrentConnection(ownershipArg.connectionGeneration) && this.selectedProjectId === ownershipArg.projectId && this.selectedSessionId?.harnessId === ownershipArg.harnessId && controllerRuntimeIdsEqual(this.selectedSessionId, ownershipArg.sessionId) && ownershipArg.key === this.currentSlashCatalogKey(); } private loadSlashCatalog(forceArg = false): Promise { const projectId = this.selectedProjectId; const sessionId = this.selectedSessionId; if ( !isNonEmptyString(projectId) || !isSessionRuntimeId(sessionId) || !this.authenticated || !this.socketClient.isConnected ) return Promise.resolve(); const key = this.slashCatalogKey(projectId, sessionId.harnessId, sessionId); const pending = this.slashCatalogRequest; if ( pending && pending.key === key && pending.connectionGeneration === this.connectionGeneration && pending.menuEpisode === this.slashMenuEpisode ) return pending.promise; pending?.abortController.abort(new DOMException( 'A newer slash catalog request superseded this request.', 'AbortError', )); if ( !forceArg && this.slashCatalogState?.key === key && this.slashCatalogState.connectionGeneration === this.connectionGeneration ) return Promise.resolve(); const ownership: ISlashCatalogOwnership = { key, projectId, sessionId: { ...sessionId }, harnessId: sessionId.harnessId, connectionGeneration: this.connectionGeneration, requestId: ++this.slashCatalogRequestId, menuEpisode: this.slashMenuEpisode, }; const abortController = new AbortController(); const promise = this.fetchSlashCatalog(ownership, abortController.signal); const request: ISlashCatalogRequest = { ...ownership, abortController, promise }; this.slashCatalogRequest = request; void promise.finally(() => { if (this.slashCatalogRequest === request) this.slashCatalogRequest = undefined; }); return promise; } private readonly handleComposerInput = ( eventArg: CustomEvent<{ value?: unknown }>, ): void => { const value = eventArg.detail?.value; if (typeof value !== 'string') return; if (new TextEncoder().encode(value).byteLength > interfaces.controllerMaxDraftTextBytes) { this.noteComposer('The composer text exceeds the 64 KiB UTF-8 limit.'); return; } // An accepted keystroke answers whatever the composer was last asked for. this.composerNotice = ''; if (this.draftSessionActive && this.selectedSessionId === undefined) { this.localDraftText = value; } else { this.sessionDraftSync.setText(value); } this.updateSlashSuggestions(value); }; private isOutcomeUnknownError(errorArg: unknown): boolean { return errorArg instanceof plugins.typedrequest.TypedResponseError && errorArg.errorData?.code === 'outcome_unknown'; } private slashExecutionErrorMessage(errorArg: unknown): string { if (this.isOutcomeUnknownError(errorArg)) { return 'The slash command outcome is unknown. Do not retry it; refresh the conversation before taking further action.'; } return errorMessage(errorArg); } private slashUnavailableReason(textArg: string): string | undefined { const parsed = interfaces.classifyControllerSlashInput(textArg); if (parsed.type !== 'command') return undefined; const sessionId = this.selectedSessionId; const catalog = this.slashCatalogState; if ( !isSessionRuntimeId(sessionId) || catalog?.key !== this.slashCatalogKey(this.selectedProjectId, sessionId.harnessId, sessionId) || catalog.connectionGeneration !== this.connectionGeneration ) return undefined; const descriptor = catalog.commands.find((commandArg) => commandArg.name === parsed.name); return descriptor?.available === false ? descriptor.unavailableReason || 'This command is unavailable for the current conversation.' : undefined; } private slashExecutionNotice(resultArg: interfaces.TControllerSlashExecuteResult): string { let notice: string; if (resultArg.type === 'handler-result') { let renderedResult = ''; try { renderedResult = typeof resultArg.result === 'string' ? resultArg.result : JSON.stringify(resultArg.result); } catch { renderedResult = 'completed'; } notice = `/${resultArg.name}: ${renderedResult || 'completed'}`; } else if (resultArg.type === 'prompt-admission') { notice = `/${resultArg.name} was accepted by the harness.`; } else if (resultArg.type === 'literal-prompt') { notice = `/${resultArg.name} was handled as a prompt.`; } else if (resultArg.type === 'client-action') { notice = `/${resultArg.name} opened.`; } else if (resultArg.type === 'mode-updated') { notice = `/${resultArg.name} changed Codex to ${resultArg.collaborationMode.mode === 'plan' ? 'Plan' : 'Default'} mode.`; } else if (resultArg.type === 'session-created') { notice = `/${resultArg.name} created and opened the new conversation.`; } else { notice = `/${resultArg.name} started.`; } return notice.length > maximumWorkspaceNoticeCharacters ? `${notice.slice(0, maximumWorkspaceNoticeCharacters - 3)}...` : notice; } private slashResultStartsTurn(resultArg: interfaces.TControllerSlashExecuteResult): boolean { if (resultArg.type === 'prompt-admission' || resultArg.type === 'literal-prompt') return true; return resultArg.type === 'operation' && resultArg.name !== 'undo' && resultArg.name !== 'redo' && resultArg.name !== 'rename'; } private async showSlashStatus(): Promise { const detail = this.sessionDetail; const sessionId = this.selectedSessionId; if (!detail || !isSessionRuntimeId(sessionId)) return false; const project = this.projects.find((candidateArg) => candidateArg.id === this.selectedProjectId); const latestUsage = [...detail.messages].reverse().find((messageArg) => ( messageArg.role === 'assistant' && messageArg.usage !== undefined ))?.usage ?? emptyHarnessUsage; const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Conversation status', subheading: detail.session.title || runtimeIdDisplay(sessionId), width: 'small', content: plugins.deesElement.html`
Harness
${sessionHarnessLabel(sessionId.harnessId)}
Status
${detail.session.status}
Model
${detail.model || 'Not reported'}
Effort
${detail.effort || 'Default'}
Project
${project?.directory || this.selectedProjectId}
`, menuOptions: [{ name: 'Close', action: async (modalArg) => modalArg?.destroy() }], }); return modal.isConnected; } private async copyLatestCompletedResponse(): Promise { const message = [...(this.sessionDetail?.messages ?? [])].reverse().find((candidateArg) => ( candidateArg.role === 'assistant' && candidateArg.streaming !== true && !candidateArg.error && isNonEmptyString(candidateArg.text) )); if (!message) throw new Error('No completed response is available to copy.'); try { await navigator.clipboard.writeText(message.text); return true; } catch (errorArg) { throw new Error(`The clipboard refused the response: ${errorMessage(errorArg)}`); } } private async showArchiveConfirmationFor( sessionIdArg: TBrowserSessionId, sessionTitleArg: string, ): Promise { if (this.mutationPending) return false; const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Archive conversation?', width: 'small', content: plugins.deesElement.html`

“${sessionTitleArg || runtimeIdDisplay(sessionIdArg)}” will leave the conversation list. Its ${sessionHarnessLabel(sessionIdArg.harnessId)} conversation remains available from Archive.

`, menuOptions: [ { name: 'Cancel', action: async (modalArg) => modalArg?.destroy() }, { name: 'Archive', action: async (modalArg) => { await modalArg?.destroy(); await this.archiveSession(sessionIdArg); }, }, ], }); return modal.isConnected; } private async performSlashClientAction( actionArg: interfaces.TControllerSlashClientAction, ): Promise { const chat = this.shadowRoot?.querySelector( 'dees-harness-chat', ); const sessionId = this.selectedSessionId; const sessionTitle = this.sessionDetail?.session.title || (sessionId ? runtimeIdDisplay(sessionId) : ''); switch (actionArg) { case 'model': return Boolean(await chat?.openModelSelector()); case 'usage': return Boolean(await chat?.openSessionSidebar('metrics')); case 'subagents': return Boolean(await chat?.openSessionSidebar('activity')); case 'new': case 'clear': this.beginNewConversation(); await this.updateComplete; return this.shadowRoot?.querySelector('.newConversationBox') !== null; case 'resume': await this.showExistingConversationModal(); return this.existingConversationModalRef?.isConnected === true; case 'copy': return this.copyLatestCompletedResponse(); case 'status': return this.showSlashStatus(); case 'archive-confirm': return isSessionRuntimeId(sessionId) ? this.showArchiveConfirmationFor(sessionId, sessionTitle) : false; case 'delete-confirm': return isSessionRuntimeId(sessionId) ? this.showDeleteConfirmationFor(sessionId, sessionTitle) : false; } } private async executeSlashCommand( draftTextArg: string, draftRevisionArg: number, focusRequestIdArg?: number, submissionArg?: ISessionDraftSubmission, ): Promise { const sessionId = this.selectedSessionId; if (this.codexModeDispatchIsFenced(sessionId)) { if (submissionArg) await this.sessionDraftSync.settleSubmission(submissionArg, false); this.blockForUnknownCodexMode(sessionId); this.cancelComposerFocusById(focusRequestIdArg); return; } const unavailableReason = this.slashUnavailableReason(draftTextArg); if (unavailableReason) { if (submissionArg) await this.sessionDraftSync.settleSubmission(submissionArg, false); this.reportWorkspaceError('Run the slash command', unavailableReason); this.requestCurrentComposerFocus(); return; } const mutationId = this.beginMutation(); if (!isSessionRuntimeId(sessionId) || !mutationId) { if (submissionArg) await this.sessionDraftSync.settleSubmission(submissionArg, false); this.cancelComposerFocusById(focusRequestIdArg); this.finishMutation(mutationId); return; } const generation = this.connectionGeneration; const projectId = this.selectedProjectId; const workspaceSelectionGeneration = this.workspaceSelectionGeneration; const wasDraft = this.draftSessionActive && this.sessionDetail === undefined; const focusRequestId = this.prepareSendComposerFocus( focusRequestIdArg, projectId, sessionId, wasDraft, ); this.workspaceNotice = ''; const workspaceOwnershipIsCurrent = (): boolean => ( this.connectionGeneration === generation && this.selectedProjectId === projectId && controllerRuntimeIdsEqual(this.selectedSessionId, sessionId) && this.workspaceSelectionGeneration === workspaceSelectionGeneration ); const executionOwnershipIsCurrent = (): boolean => ( this.isCurrentMutation(mutationId, generation, sessionId) && workspaceOwnershipIsCurrent() ); let mutationFinished = false; let submissionSettled = false; try { const explicitChoice = this.resolveExplicitModelChoice(sessionId); const providerConnectionId = explicitChoice?.harnessId === 'flex' ? this.resolveSessionAccount(sessionId) : undefined; const response = await this.socketClient.fire( 'controller.slash.execute', { projectId, sessionId, draftRevision: draftRevisionArg, ...(explicitChoice ? { model: explicitChoice } : {}), ...(providerConnectionId ? { providerConnectionId } : {}), }, { maxRetries: 0 }, ); if (response.result.type === 'session-created') { const createdSession = response.result.session; if ( !isSessionRuntimeId(createdSession.id) || createdSession.id.harnessId !== 'codex' || controllerRuntimeIdsEqual(createdSession.id, sessionId) ) throw new Error('Codex returned an invalid forked conversation.'); if (submissionArg) { // The fork is durable before this response. Clear exactly the clicked command and drain // post-click edits to the source before navigation can activate the fork's draft. submissionSettled = true; await this.sessionDraftSync.consumeSubmission(submissionArg, true); } else { await this.sessionDraftSync.refresh().catch(() => undefined); } if (!workspaceOwnershipIsCurrent()) { void this.refreshSessions(false); return; } const existingIndex = this.sessions.findIndex((candidateArg) => ( isSessionRuntimeId(candidateArg.id) && controllerRuntimeIdsEqual(candidateArg.id, createdSession.id) )); this.sessions = existingIndex < 0 ? [...this.sessions, createdSession] : this.sessions.map((candidateArg, candidateIndex) => ( candidateIndex === existingIndex ? createdSession : candidateArg )); this.openSessionById(createdSession.id); this.workspaceNotice = this.slashExecutionNotice(response.result); if (response.result.creationPending) { this.noteComposer('The fork was created, but Codex has not finished its initial settings. The new conversation may need to be reopened after refresh.'); } void this.refreshSessions(false); return; } if (!executionOwnershipIsCurrent()) { return; } if (response.result.type === 'client-action') { this.finishMutation(mutationId); mutationFinished = true; if (!workspaceOwnershipIsCurrent()) return; const opened = await this.performSlashClientAction(response.result.action); if (!opened) { throw new Error(`/${response.result.name} is unavailable in the current view.`); } if (submissionArg) { // consumeSubmission always settles its prepared token, including when the exact-revision // clear conflicts. Mark it before awaiting so the outer error path cannot settle twice. submissionSettled = true; await this.sessionDraftSync.consumeSubmission(submissionArg); } else { await this.sessionDraftSync.refresh().catch(() => undefined); } } else if (submissionArg) { await this.sessionDraftSync.settleSubmission(submissionArg, true); submissionSettled = true; } else { await this.sessionDraftSync.refresh().catch(() => undefined); } if (!workspaceOwnershipIsCurrent()) return; if (wasDraft) { this.reconcileMaterializedDraftComposer(sessionId); this.draftSessionDetailReadyId = sessionId; } if (response.result.type !== 'client-action') { this.retargetComposerFocus(focusRequestId, 'session', sessionId, true); } this.workspaceNotice = this.slashExecutionNotice(response.result); const currentDetail = this.sessionDetail; if ( response.result.type === 'mode-updated' && currentDetail?.codexActivity && controllerRuntimeIdsEqual(currentDetail.session.id, sessionId) ) { this.sessionDetail = { ...currentDetail, codexActivity: { ...currentDetail.codexActivity, collaborationMode: { ...response.result.collaborationMode }, }, }; this.codexModeUnconfirmedSessionKey = ''; } if ( this.sessionDetail && controllerRuntimeIdsEqual(this.sessionDetail.session.id, sessionId) && ( this.slashResultStartsTurn(response.result) ) ) { this.sessionDetail = { ...this.sessionDetail, session: { ...this.sessionDetail.session, status: 'busy' }, }; } if (response.result.type !== 'client-action') { void this.loadSlashCatalog(true); this.scheduleRefresh(); } } catch (error) { if (submissionArg && !submissionSettled) { await this.sessionDraftSync.settleSubmission(submissionArg, false); submissionSettled = true; } else if (!submissionArg) { await this.sessionDraftSync.refresh().catch(() => undefined); } if (workspaceOwnershipIsCurrent()) { const parsedCommand = interfaces.classifyControllerSlashInput(draftTextArg); if ( sessionId.harnessId === 'codex' && parsedCommand.type === 'command' && parsedCommand.name === 'plan' && this.isOutcomeUnknownError(error) ) { this.codexModeUnconfirmedSessionKey = this.sessionOperationKey(projectId, sessionId); void this.loadSessionDetail(sessionId); } this.reportWorkspaceError( 'Run the slash command', error, this.slashExecutionErrorMessage(error), ); this.retargetComposerFocus(focusRequestId, wasDraft ? 'draft' : 'session', sessionId, true); } } finally { if (submissionArg && !submissionSettled) { await this.sessionDraftSync.settleSubmission(submissionArg, false); } if (!mutationFinished) this.finishMutation(mutationId); } } private scheduleRefresh(): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); } this.refreshTimer = setTimeout(() => { this.refreshTimer = undefined; if (this.authenticated && this.socketClient.isConnected) { void this.refreshSessions(); } }, 400); } private refreshSessionsNow(): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = undefined; } if (!this.authenticated || !this.socketClient.isConnected) return; if (this.immediateRefreshTimer) { this.immediateRefreshPending = true; return; } void this.refreshSessions(); this.immediateRefreshTimer = setTimeout(() => { this.immediateRefreshTimer = undefined; if (!this.immediateRefreshPending) return; this.immediateRefreshPending = false; this.refreshSessionsNow(); }, 400); } private renderConnectionState(): plugins.deesElement.TemplateResult { const reconnecting = this.connectionStatus === 'reconnecting'; const disconnected = this.connectionStatus === 'disconnected'; return plugins.deesElement.html`
${!disconnected || reconnecting ? plugins.deesElement.html`` : ''}

${reconnecting ? 'Reconnecting' : disconnected ? 'Controller unavailable' : 'Connecting'}

${this.upgradeStatus && this.upgradeStatus.phase !== 'completed' && this.upgradeStatus.phase !== 'failed' ? `Upgrading v${this.upgradeStatus.fromVersion} to v${this.upgradeStatus.toVersion}. The page will refresh when the controller returns.` : reconnecting ? 'The browser session was cleared. Authenticate again after the connection returns.' : disconnected ? 'The local controller socket could not be reached.' : 'Opening a private TypedSocket connection to the controller…'}

${this.connectionError ? plugins.deesElement.html` void this.connectSocket()}> Retry connection ` : ''}
${this.renderStatusBar()}
`; } private renderStaleBundleState(): plugins.deesElement.TemplateResult { return plugins.deesElement.html`

Browser update required

Browser bundle v${commitinfo.version} does not match controller v${this.staleBundleVersion}. Clear this site's cached data and reload.

${this.renderStatusBar()}
`; } private renderLoadingState(messageArg: string): plugins.deesElement.TemplateResult { return plugins.deesElement.html`

AGL - Agent Gateway Layer

${messageArg}

${this.connectionError ? plugins.deesElement.html` { const generation = this.connectionGeneration; void this.reconcileAuthState(generation); }}> Retry authentication check ` : ''}
${this.renderStatusBar()}
`; } private renderSetup(): plugins.deesElement.TemplateResult { const passkeyUnavailable = !globalThis.isSecureContext || !('PublicKeyCredential' in globalThis); return plugins.deesElement.html`

Secure this controller

Enter the one-time setup code printed by the CLI, then create the passkey used to control OpenCode from this browser.

${this.setupCodeExpired ? plugins.deesElement.html`
The setup code expired. Restart the controller to print a fresh code.
` : ''} ${passkeyUnavailable ? plugins.deesElement.html`
Passkeys require HTTPS, except when this UI is opened on localhost.
` : ''} ${this.setupError ? plugins.deesElement.html` ` : ''} ${this.renderTempPasswordSection()}
${this.renderStatusBar()}
`; } private renderTempPasswordSection(): plugins.deesElement.TemplateResult { return plugins.deesElement.html`
${this.showTempPasswordForm ? plugins.deesElement.html` ${this.tempPasswordError ? plugins.deesElement.html` ` : ''} ` : plugins.deesElement.html` `}
`; } private renderLogin(): plugins.deesElement.TemplateResult { return plugins.deesElement.html`
${this.renderTempPasswordSection()}
${this.renderStatusBar()}
`; } private renderWorkspace(): plugins.deesElement.TemplateResult { // Archived sessions stay out of the sidebar; the archive dialog lists them. const statuses = this.authoritativeStatusesByProject.get(this.selectedProjectId); const finishedSessionIds = this.finishedSessionIdsByProject.get(this.selectedProjectId); const errorSessionIds = this.errorSessionIdsByProject.get(this.selectedProjectId); const optimisticWorkingIds = this.optimisticWorkingIdsByProject.get(this.selectedProjectId); const flexReadOnly = this.controllerStatus !== undefined && !this.flexHarnessIsReady(); // The sidebar spans projects: every tracked conversation is a row, and the live status // overlays are per project, so they are read with the row's own project. const chatMetas: plugins.deesCatalog.IHarnessSessionMeta[] = this.conversations .filter((conversationArg) => ( conversationArg.archivedAt === undefined && hasSessionRuntimeId(conversationArg.session) )) .map((conversationArg) => { const sessionArg = trackedConversationToSession(conversationArg) as interfaces.IControllerSession & { id: TBrowserSessionId }; const conversationProjectId = conversationArg.projectId; const sessionKey = controllerRuntimeIdToUiKey(sessionArg.id); const projectStatuses = this.authoritativeStatusesByProject.get(conversationProjectId); const projectFinishedIds = this.finishedSessionIdsByProject.get(conversationProjectId); const projectErrorIds = this.errorSessionIdsByProject.get(conversationProjectId); const projectOptimisticIds = this.optimisticWorkingIdsByProject.get(conversationProjectId); const status = projectStatuses?.get(sessionKey) ?? sessionArg.status; const working = status === 'busy' || status === 'retry' || projectOptimisticIds?.has(sessionKey) === true; const external = sessionArg.id.harnessId === 'codex' && sessionArg.codexActivity?.writer === 'external'; const state: plugins.deesCatalog.THarnessSessionCardState = ( status === 'error' || projectErrorIds?.has(sessionKey) ? 'error' : sessionArg.attention === true ? 'attention' : external ? 'external' : working ? 'working' : projectFinishedIds?.has(sessionKey) ? 'finished' : 'normal' ); const harnessLabel = sessionHarnessLabel(sessionArg.id.harnessId); const readOnly = sessionArg.id.harnessId === 'flex' && flexReadOnly; // The card renders statusLabel as its own chip and already derives one from `state` // (Working, Needs attention, Error, Finished, Idle), so only the conditions that // derivation cannot express are overridden here. const statusLabel = readOnly ? 'Read-only' : external ? undefined : status === 'retry' ? 'Retrying' : undefined; // preview is shown only on an expanded card, so it carries the explanation rather than // repeating the harness name and status that the collapsed row already renders. const preview = readOnly ? `${harnessLabel} is unavailable. This is the saved projection.` : state === 'error' ? 'The session reported an error.' : state === 'attention' ? 'A permission or answer is required.' : external ? 'A Codex turn is running in another client.' : status === 'retry' ? 'Retrying after a failure.' : undefined; return { id: conversationUiKey(conversationProjectId, sessionArg.id), title: sessionArg.title, createdAt: sessionArg.createdAt, updatedAt: sessionArg.updatedAt, state, working: external ? false : working, // Identity belongs in the card's own identity row, where it is also indexed for search. harness: { id: sessionArg.id.harnessId, name: harnessLabel }, projectLabel: conversationArg.projectName, ...(statusLabel === undefined ? {} : { statusLabel }), ...(preview === undefined ? {} : { preview }), }; }); const sessionMetas = chatMetas; const resourceMetas: plugins.deesCatalog.IHarnessResourceMeta[] = this.resources .filter((resource) => resource.lifecycle !== 'retired') .map((resource) => ({ id: resource.id, kind: resource.kind, title: resource.title, icon: resource.kind === 'terminal' ? (resource.agent ? 'lucide:Bot' : 'lucide:Terminal') : 'lucide:Globe2', preview: resource.kind === 'terminal' ? (resource.agent ? `Claude · ${resource.processState}${resource.agent.lastFailure ? ` · ${resource.agent.lastFailure}` : ''}` : `${resource.processState} · ${resource.command}`) : `${resource.browserRuntimeState} · ${resource.agentAvailability}`, createdAt: resource.createdAt, updatedAt: resource.updatedAt, state: resource.lifecycle, })); // Every conversation the resource is attached to, so the row renders one marker per // attachment and Move and Detach can name which one they mean. Terminal memberships are // deliberately not projected: the sidebar offers no way to create one, and presenting one as // a target would make a drag read as a Move of the terminal rather than an additional attach. const resourceAssociations: plugins.deesCatalog.IHarnessResourceAssociation[] = this.resources .flatMap((resource) => { const targets = this.resourceAssociationTargets(resource); if (targets.length === 0) return []; return [{ resourceId: resource.id, targets }]; }); // An association target is any other list item. Only conversations are offered today; the // shape already carries the kind, so a terminal target slots in without changing the wiring. const eligibleTargets = this.eligibleAssociationTargets(); const eligibleTargetsByResourceId = Object.fromEntries( this.resources.map((resource) => [resource.id, eligibleTargets]), ); const archivedCount = this.conversations.filter( (conversationArg) => conversationArg.archivedAt !== undefined, ).length; const presentationGroups: plugins.deesCatalog.IHarnessSessionGroup[] = this.sessionGroups.map( (groupArg) => ({ id: groupArg.id, name: groupArg.name, itemIds: groupArg.itemIds.map(layoutItemRefToPresentation), }), ); // The sidebar renders exactly the order the client would persist, so an item that has never // been placed explicitly still appears where a later reorder would put it. const presentationUngroupedIds = controllerMutableUngroupedLayoutItemIds( this.conversations, this.resources, this.sessionGroups, this.ungroupedItemIds, this.selectedProjectId, ).map(layoutItemRefToPresentation); const selectedUiKey = this.selectedSessionId && isNonEmptyString(this.selectedProjectId) ? conversationUiKey(this.selectedProjectId, this.selectedSessionId) : ''; const stableSessionMetas = this.stableRenderValue('workspace.sessionMetas', sessionMetas); const stableResourceMetas = this.stableRenderValue('workspace.resourceMetas', resourceMetas); const stableResourceAssociations = this.stableRenderValue( 'workspace.resourceAssociations', resourceAssociations, ); const stableEligibleTargetsByResourceId = this.stableRenderValue( 'workspace.eligibleTargetsByResourceId', eligibleTargetsByResourceId, ); const stablePresentationGroups = this.stableRenderValue( 'workspace.presentationGroups', presentationGroups, ); const stablePresentationUngroupedIds = this.stableRenderValue( 'workspace.presentationUngroupedIds', presentationUngroupedIds, ); const unseenErrorCount = this.errorJournal.reduce( (count, entry) => (entry.seen ? count : count + 1), 0, ); return plugins.deesElement.html`
AGL - Agent Gateway Layer
${this.upgradeStatus && this.upgradeStatus.phase !== 'completed' && this.upgradeStatus.phase !== 'failed' ? `upgrading v${this.upgradeStatus.fromVersion} to v${this.upgradeStatus.toVersion}` : `local harnesses${this.backendVersion ? ` · v${this.backendVersion}` : ''}`} · authenticated
${this.errorJournal.length === 0 ? '' : plugins.deesElement.html` 0 ? 'lucide:OctagonAlert' : 'lucide:TriangleAlert'} .text=${unseenErrorCount > 0 ? `Errors (${unseenErrorCount})` : 'Errors'} title=${unseenErrorCount > 0 ? `${unseenErrorCount} unread error${unseenErrorCount === 1 ? '' : 's'}` : 'Errors recorded in this tab'} @clicked=${this.showErrorJournalModal} >`} Settings
${this.workspaceNotice ? plugins.deesElement.html`
${this.workspaceNotice}
` : ''} ${this.selectedTerminalId !== undefined || ( this.selectedResourceId && this.resources.some((resource) => ( resource.id === this.selectedResourceId && resource.kind === 'terminal' )) ) ? this.renderTerminal() : this.selectedResourceId && this.resources.some((resource) => ( resource.id === this.selectedResourceId && resource.kind === 'browser' )) ? this.renderBrowser() : this.sessionDetail ? this.renderChat(this.sessionDetail) : this.draftSessionActive ? this.renderDraftChat() : this.detailLoading ? plugins.deesElement.html`

Loading conversation

Reading the latest harness messages…

` : plugins.deesElement.html`
${this.renderNewConversationBox()}
`}
${this.renderStatusBar()}
`; } /** * Pending project removals the controller stopped retrying. The project is already gone from the * workspace, so this row is the only place that names why its removal stopped and offers to * resume it once the cause is resolved. */ private renderBlockedRemovals(): plugins.deesElement.TemplateResult | '' { if (this.blockedProjectRemovals.length === 0) return ''; return plugins.deesElement.html` ${this.blockedProjectRemovals.map((removalArg) => plugins.deesElement.html` `)} `; } private async retryBlockedProjectRemoval(projectIdArg: string): Promise { const mutationId = this.beginMutation(); if (!mutationId) { return; } const generation = this.connectionGeneration; try { await this.socketClient.fire( 'controller.project.removal.retry', { projectId: projectIdArg }, ); if (!this.isCurrentMutation(mutationId, generation)) { return; } await this.refreshProjects(); } catch (error) { if (this.isCurrentMutation(mutationId, generation)) { this.reportWorkspaceError('Retry the project removal', error); } } finally { this.finishMutation(mutationId); } } /** * The one new-conversation box. The New menu's chat entry, the chat view's new-session button * and the empty workspace all render exactly this, so the options never drift apart: where the * conversation lives, which harness runs it, and which model it starts on. Terminals and * browsers are created from the New menu; this box configures a conversation only. */ private renderNewConversationBox(): plugins.deesElement.TemplateResult { const harnessId = this.newConversationHarnessId; const directory = this.newConversationDirectory.trim(); const selectedProject = this.projects.find( (projectArg) => projectArg.id === this.newConversationProjectId, ); const modelOptions = harnessId === undefined ? [] : this.modelOptionsForHarness(harnessId).map((optionArg) => ({ option: optionArg, key: optionArg, })); const archivedCount = this.conversations.filter( (conversationArg) => conversationArg.archivedAt !== undefined, ).length; const canStart = harnessId !== undefined && (directory !== '' || selectedProject !== undefined) && !this.mutationPending; return plugins.deesElement.html`

New conversation

A conversation lives in a project and runs on one harness.

${directory !== '' && this.pathSuggestions.length > 0 ? plugins.deesElement.html`
${this.pathSuggestions.map((suggestionArg) => plugins.deesElement.html` `)}
` : ''} ${this.standardProjectDirectories.length > 0 ? plugins.deesElement.html`
${this.standardProjectDirectories.map((directoryArg) => plugins.deesElement.html` `)}
` : ''}
Harness
${(['opencode', 'flex', 'codex'] as const).map((candidateArg) => plugins.deesElement.html` this.selectNewConversationHarness(candidateArg)} >${sessionHarnessLabel(candidateArg)} `)}
${harnessId === undefined ? plugins.deesElement.html`
Pick the harness explicitly; AGL never starts one you did not choose.
` : plugins.deesElement.html` `} ${this.newConversationError ? plugins.deesElement.html` ` : ''} void this.startNewConversation()} >Start conversation
void this.showExistingConversationModal()} > Open existing ${archivedCount > 0 ? `Archive (${archivedCount})` : 'Archive'}
`; } private renderTerminal(): plugins.deesElement.TemplateResult { const resource = this.resources.find((candidate): candidate is interfaces.IControllerTerminalResource => ( candidate.id === this.selectedResourceId && candidate.kind === 'terminal' )); if (resource?.processState === 'stopped') { const agent = resource.agent; const stoppedDescription = agent ? (agent.lastFailureMessage ? `This Claude chat could not start: ${agent.lastFailureMessage} Its conversation is kept; start it again from the resource Actions menu.` : `This Claude chat is stopped. Start it from the resource Actions menu to resume conversation ${agent.sessionId.slice(0, 8)}.`) : 'This terminal is stopped. Start it from the resource Actions menu.'; return plugins.deesElement.html`

${resource.title}

${stoppedDescription}

`; } const terminal = this.terminals.find( (terminalArg) => controllerRuntimeIdsEqual(terminalArg.id, this.selectedTerminalId), ); return plugins.deesElement.html`
${resource?.title || terminal?.title || (this.selectedTerminalId ? runtimeIdDisplay(this.selectedTerminalId) : '')}
${resource?.agent ? `claude · ${resource.agent.sessionId.slice(0, 8)}${resource.agent.launchMode === 'resume' ? ' · resumed' : ''} · ${resource.cwd}` : resource?.cwd ?? terminal?.cwd ?? ''}
${this.selectedTerminalEnded ? plugins.deesElement.html`ended` : ''}
${plugins.deesElement.directives.keyed( this.selectedResourceId, plugins.deesElement.html` `, )}
`; } private renderBrowser(): plugins.deesElement.TemplateResult { const resource = this.resources.find((candidate): candidate is interfaces.IControllerBrowserResource => ( candidate.id === this.selectedResourceId && candidate.kind === 'browser' )); const activeTab = this.browserViewState?.tabs.find((tab) => tab.active); return plugins.deesElement.html`
Go this.browserDevToolsVisible ? this.closeBrowserDevTools() : void this.openBrowserDevTools()} >DevTools
${activeTab && this.browserWebsiteErrors[activeTab.id] ? plugins.deesElement.html`
Website error${this.browserWebsiteErrors[activeTab.id]} void this.openBrowserDevTools('console')}>Console { const errors = { ...this.browserWebsiteErrors }; delete errors[activeTab.id]; this.browserWebsiteErrors = errors; }}>Dismiss
` : ''} ${this.browserViewLoading || !this.browserViewId ? plugins.deesElement.html`
${resource?.browserRuntimeState !== 'available' ? 'BrowserRuntime unavailable' : this.browserViewLoading || this.browserViewOpening ? plugins.deesElement.html`` : 'The browser view is closed.'}
` : plugins.deesElement.html`
${this.browserStatisticsOverlayVisible && this.browserViewStatistics ? plugins.deesElement.html`` : ''}
`} ${this.browserVideoFailed ? plugins.deesElement.html`
Video connection interrupted. { const renderer = this.browserRenderer; if (!renderer) return; this.browserVideoFailed = false; try { await renderer.suspend(); await renderer.resume(); } catch (error) { this.browserVideoFailed = true; this.reportWorkspaceError('Start the browser video', error); } }}>Reconnect video
` : ''} ${this.browserDevToolsVisible ? plugins.deesElement.html`
${this.browserDevToolsError ? plugins.deesElement.html`
${this.browserDevToolsError}
` : ''}
` : ''}
${this.browserSidebarVisible ? this.renderBrowserSidebar() : ''}
`; } /** * The screencast diagnostics, off the video surface: they used to ride along as its tooltip, * which put them under the pointer instead of where they can be read while the page is used. * The corner readout stays the glanceable line; this panel is the full set behind it. */ private renderBrowserSidebar(): plugins.deesElement.TemplateResult { const rows = browserScreencastRows( this.browserViewStatistics, this.browserViewState?.videoAcceleration, ); return plugins.deesElement.html` `; } private renderDraftChat(): plugins.deesElement.TemplateResult { const project = this.projects.find( (projectArg) => projectArg.id === this.selectedProjectId, ); const directory = project?.directory ?? ''; const codexUnavailable = this.draftHarnessId === 'codex' && !this.codexHarnessIsReady(); const draftStatus: plugins.deesCatalog.IHarnessStatus = codexUnavailable ? { type: 'error', message: 'Codex is unavailable. Check the AGL server configuration.' } : { type: 'idle' }; const account = this.resolveSessionAccount(); return plugins.deesElement.html`
${this.renderComposerNotice()}
`; } private renderFlexWorkspaceReversion( sessionIdArg: TBrowserSessionId, ): plugins.deesElement.TemplateResult | '' { if ( sessionIdArg.harnessId !== 'flex' || !this.slashReversion || this.slashCatalogState?.key !== this.slashCatalogKey( this.selectedProjectId, sessionIdArg.harnessId, sessionIdArg, ) ) return ''; const repositories = new Map(); let truncated = false; let barrier = false; for (const group of this.slashReversion.groups) { truncated ||= group.affectedWorkspacesTruncated; barrier ||= group.kind === 'barrier'; for (const workspace of group.affectedWorkspaces) { if (!repositories.has(workspace.id)) repositories.set(workspace.id, workspace); } } if (repositories.size === 0 && !truncated && !barrier) return ''; return plugins.deesElement.html`
Affected Git ${[...repositories.values()].map((workspace) => plugins.deesElement.html` ${workspace.label || workspace.id} `)} ${truncated ? plugins.deesElement.html` more ` : ''} ${barrier ? plugins.deesElement.html` barrier · non-revertible ` : ''}
`; } private renderChat( detailArg: IControllerSessionRenderDetail, ): plugins.deesElement.TemplateResult { if (!isSessionRuntimeId(detailArg.session.id)) { return plugins.deesElement.html`
Invalid harness session ID.
`; } const sessionId = detailArg.session.id; const isOpenCode = sessionId.harnessId === 'opencode'; const readOnly = this.sessionIsReadOnly(sessionId); // The canonical array and all delta targets remain stable across unrelated app renders. const messages = this.ensureCanonicalChatProjection( detailArg, this.selectedProjectId, sessionId, ).messages; // Status, usage, permissions, questions, and todos keep their identity while // structurally unchanged; the transcript rebuilds its timeline and re-pins // its scroll position whenever any of these properties change identity. const status = this.stableRenderValue('chat.status', this.toHarnessStatus( detailArg.session, this.activeSubagentCount(this.selectedProjectId, sessionId, detailArg.messages), readOnly, )); const usage = this.stableRenderValue('chat.usage', latestHarnessUsage(messages)); const account = this.resolveSessionAccount(sessionId); const hasUnavailableChildTranscript = this.hasUnmanagedChildSessionLink( this.selectedProjectId, sessionId, ); const draftState = this.sessionDraftState?.projectId === this.selectedProjectId && controllerRuntimeIdsEqual(this.sessionDraftState.sessionId, sessionId) ? this.sessionDraftState : undefined; // Pending cards come from OpenCode's question list; answered ones come // from completed question tool calls in the transcript (so they survive // reloads), with the local cache bridging the moment in between. const harnessQuestions: plugins.deesCatalog.IHarnessQuestionRequest[] = []; const answeredQuestionTexts = new Set(); const pendingToolCallIds = new Set( detailArg.questions .map((requestArg) => requestArg.toolCallId) .filter(isControllerRuntimeId) .map(controllerRuntimeIdToUiKey), ); for (const messageArg of detailArg.messages) { const call = messageArg.toolCall; if (call?.name !== 'question') continue; // Running with a live pending request → the interactive card covers it. // Running WITHOUT one is an orphaned ask from an earlier run: show it // as expired instead of hiding the conversation turn entirely. const orphaned = (call.status === 'running' || call.status === 'pending') && !pendingToolCallIds.has(controllerRuntimeIdToUiKey(call.id)); const dismissed = call.status === 'error'; if (call.status !== 'completed' && !orphaned && !dismissed) continue; const callInput = call.input as { questions?: unknown } | undefined; const callOutput = call.output as { answers?: unknown } | undefined; const askedQuestions = Array.isArray(callInput?.questions) ? callInput.questions : []; const answers = call.status === 'completed' && Array.isArray(callOutput?.answers) ? callOutput.answers : []; // Completed calls usually carry the model-facing string // `… "question"="answer, answer" …` instead of structured answers. const parsedAnswerPairs = new Map(); if (call.status === 'completed' && typeof call.output === 'string') { for (const match of call.output.matchAll(/"([^"]+)"="([^"]*)"/g)) { parsedAnswerPairs.set(match[1], match[2]); } } askedQuestions.forEach((questionArg, questionIndex) => { if (!questionArg || typeof questionArg !== 'object') return; const question = questionArg as { question?: unknown; options?: unknown; multiple?: unknown; custom?: unknown; }; if (typeof question.question !== 'string') return; const structuredAnswer = Array.isArray(answers[questionIndex]) ? (answers[questionIndex] as unknown[]).filter( (entryArg): entryArg is string => typeof entryArg === 'string', ) : []; const parsedAnswer = parsedAnswerPairs.get(question.question); const cachedAnswer = [...this.answeredQuestionCards.values()].find( (cardArg) => cardArg.question === question.question && cardArg.response?.length, )?.response; const answer = structuredAnswer.length > 0 ? structuredAnswer : parsedAnswer !== undefined && parsedAnswer !== 'Unanswered' ? [parsedAnswer] : cachedAnswer ?? []; const response = call.status === 'completed' ? answer : dismissed ? ['(dismissed)'] : ['(expired — asked in an earlier run)']; answeredQuestionTexts.add(question.question); harnessQuestions.push({ id: `${controllerRuntimeIdToUiKey(messageArg.id)}#${questionIndex}`, question: question.question, options: Array.isArray(question.options) ? (question.options as Array<{ label?: unknown; description?: unknown }>) .filter((optionArg) => typeof optionArg?.label === 'string') .map((optionArg) => ({ label: optionArg.label as string, ...(typeof optionArg.description === 'string' ? { description: optionArg.description } : {}), })) : [], multiSelect: question.multiple === true, allowCustom: question.custom !== false, createdAt: messageArg.createdAt, response, respondedAt: messageArg.updatedAt ?? messageArg.createdAt, }); }); } const pendingQuestionCardIds = new Set(); const pendingQuestions = [ ...detailArg.questions, ...(isOpenCode ? (detailArg.childAttention ?? []).flatMap((attentionArg) => attentionArg.questions) : []), ]; for (const requestArg of pendingQuestions) { requestArg.questions.forEach((questionArg, questionIndex) => { const requestKey = controllerRuntimeIdToUiKey(requestArg.id); const cardId = requestArg.questions.length > 1 ? `${requestKey}#${questionIndex}` : requestKey; pendingQuestionCardIds.add(cardId); harnessQuestions.push( this.answeredQuestionCards.get(cardId) ?? { id: cardId, sessionId: controllerRuntimeIdToUiKey(requestArg.sessionId), question: questionArg.question, options: questionArg.options.map((optionArg) => ({ label: optionArg.label, description: optionArg.description, })), multiSelect: questionArg.multiple === true, allowCustom: questionArg.custom !== false, }, ); }); } for (const [cardId, card] of this.answeredQuestionCards) { // The transcript version of a just-answered question replaces the // locally cached one as soon as the completed tool call arrives. if (!pendingQuestionCardIds.has(cardId) && !answeredQuestionTexts.has(card.question)) { harnessQuestions.push(card); } } const harnessPermissions: plugins.deesCatalog.IHarnessPermissionRequest[] = []; const pendingPermissionIds = new Set(); const pendingPermissions = [ ...detailArg.permissions, ...(detailArg.childAttention ?? []).flatMap((attentionArg) => attentionArg.permissions), ]; for (const permissionArg of pendingPermissions) { const permissionId = controllerRuntimeIdToUiKey(permissionArg.id); pendingPermissionIds.add(permissionId); harnessPermissions.push( this.answeredPermissionCards.get(permissionId) ?? { id: permissionId, sessionId: controllerRuntimeIdToUiKey(permissionArg.sessionId), createdAt: permissionArg.createdAt, title: permissionArg.title || 'Permission requested', type: permissionArg.type, metadata: { ...(permissionArg.patterns.length > 0 ? { patterns: permissionArg.patterns } : {}), ...permissionArg.metadata, }, }, ); } for (const [permissionId, card] of this.answeredPermissionCards) { if (!pendingPermissionIds.has(permissionId)) harnessPermissions.push(card); } const operationKey = this.sessionOperationKey(this.selectedProjectId, sessionId); const scratchpadError = this.scratchpadErrorsBySession.get(operationKey) ?? ''; const intelligenceConflict = [...detailArg.intelligenceExchanges].reverse().find( (exchange) => exchange.scratchpadConflict === true, ); const intelligenceError = this.intelligenceErrorsBySession.get(operationKey) ?? (intelligenceConflict ? 'The answer completed, but its scratchpad update was not applied because a newer revision exists.' : ''); const codexActivity = sessionId.harnessId === 'codex' ? detailArg.codexActivity : undefined; const codexExternalWriter = codexActivity?.writer === 'external'; const codexModeDispatchFenced = this.codexModeDispatchIsFenced(sessionId); const codexSteerEnabled = codexActivity?.writer === 'agl' && isNonEmptyString(codexActivity.turnId) && !codexModeDispatchFenced && !this.codexSteerPending; const codexAbortEnabled = codexActivity?.canInterrupt === true && isNonEmptyString(codexActivity.turnId); return plugins.deesElement.html`
${this.detailHistoryStatus === 'backfilling' ? plugins.deesElement.html`
Loading earlier messages...
` : this.detailHistoryStatus === 'partial' ? plugins.deesElement.html`
${detailHistoryStatusText( this.detailHistoryStatus, this.detailHistoryLimits, )}
` : ''} ${this.renderFlexWorkspaceReversion(sessionId)} ${hasUnavailableChildTranscript ? plugins.deesElement.html`
${unmanagedChildTranscriptNotice}
` : ''} ${detailArg.childAttentionLimited === true ? plugins.deesElement.html`
${limitedChildAttentionNotice}
` : ''} exchange.status === 'running')} .intelligenceError=${intelligenceError} .todos=${this.stableRenderValue('chat.todos', detailArg.todos.map((todoArg) => ({ ...todoArg, ...(todoArg.id ? { id: controllerRuntimeIdToUiKey(todoArg.id) } : {}), })))} .todosAuthoritative=${true} .busy=${detailArg.session.status === 'busy' || detailArg.session.status === 'retry'} .queuedCount=${detailArg.pendingPrompts.length} .steeringEnabled=${false} .queueingEnabled=${sessionId.harnessId !== 'flex' && !codexModeDispatchFenced && !this.codexSteerPending} .inputSteeringEnabled=${codexSteerEnabled} .inputLocked=${codexExternalWriter || codexModeDispatchFenced} .abortEnabled=${sessionId.harnessId !== 'codex' || codexAbortEnabled} .abortDisabledReason=${codexExternalWriter ? 'This Codex turn cannot be stopped from AGL.' : 'This Codex turn is no longer interruptible.'} @harness-steer-input=${this.handleSteerInput} .disabled=${this.detailLoading || (this.mutationPending && !this.codexSteerPending) || this.sessionModelPending || draftState?.loading === true || readOnly} .composerValue=${draftState?.text ?? ''} .attachments=${this.stableRenderValue( 'chat.attachments', draftState?.attachments ?? emptyHarnessAttachments, )} .suggestions=${this.slashSuggestions} .maxAttachmentCount=${this.localAttachmentsAvailable(sessionId.harnessId) ? interfaces.controllerMaxDraftAttachments : 0} .maxAttachmentBytes=${interfaces.controllerMaxDraftAttachmentBytes} .maxTotalAttachmentBytes=${interfaces.controllerMaxDraftAttachmentTotalBytes} .account=${account} .accountOptions=${sessionId.harnessId === 'flex' ? this.flexAccountOptions() : []} .model=${this.resolveSessionModelString(sessionId)} .modelOptions=${this.modelOptionsForHarness(sessionId.harnessId, account)} .reasoningEffort=${this.resolveSessionEffortString(sessionId) || 'default'} .effortOptions=${this.resolveSessionEffortOptions(sessionId)} .showTodosPanel=${true} .markdownWhileStreaming=${false} heading=${detailArg.session.title || `${sessionHarnessLabel(sessionId.harnessId)} session`} subheading=${readOnly ? `${sessionId.harnessId === 'codex' ? 'Codex unavailable' : 'Flex unavailable · saved projection'} · read-only · ${runtimeIdDisplay(sessionId)}` : detailArg.model ? `${sessionHarnessLabel(sessionId.harnessId)} · ${detailArg.model}${detailArg.effort ? ` · ${detailArg.effort}` : ''} · ${sessionId.nativeId}` : `${sessionHarnessLabel(sessionId.harnessId)} · ${sessionId.nativeId}`} @harness-send=${this.handleSend} @harness-abort=${this.handleAbort} @harness-new-session=${this.handleNewSession} @harness-open-sessions=${this.focusSessionSearch} @harness-scratchpad-save=${this.handleScratchpadSave} @harness-session-intelligence-ask=${this.handleSessionIntelligenceAsk} @harness-attachments-change=${this.handleAttachmentsChange} @harness-account-change=${this.handleAccountChange} @harness-model-change=${this.handleModelChange} @harness-input=${this.handleComposerInput} > ${sessionId.harnessId === 'codex' ? this.renderCodexActivity(detailArg) : ''} ${this.renderComposerNotice()}
`; } private toHarnessMessage( messageArg: interfaces.IControllerMessage, projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, ): plugins.deesCatalog.IHarnessMessage { const { id, reasoning, toolCall, ...message } = messageArg; return { ...message, id: controllerRuntimeIdToUiKey(id), markdown: messageArg.role === 'assistant', ...(reasoning ? { reasoning: reasoning.map((reasoningArg) => ({ ...reasoningArg, id: controllerRuntimeIdToUiKey(reasoningArg.id), })), } : {}), ...(toolCall ? { toolCall: this.toHarnessToolCall(toolCall, projectIdArg, ownerSessionIdArg), } : {}), }; } private toHarnessToolCall( toolCallArg: interfaces.IControllerToolCall, projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, ): plugins.deesCatalog.IHarnessToolCall { const { id, childSessionId, ...call } = toolCallArg; const accessibleChildSessionId = isSessionRuntimeId(childSessionId) && this.childSessionAccess(projectIdArg, ownerSessionIdArg, childSessionId) !== undefined ? childSessionId : undefined; const subtask = accessibleChildSessionId ? this.subtaskPreviewEntries.get( this.subtaskPreviewKey(projectIdArg, ownerSessionIdArg, accessibleChildSessionId), )?.stream : undefined; return { ...call, id: controllerRuntimeIdToUiKey(id), ...(accessibleChildSessionId ? { childSessionId: controllerRuntimeIdToUiKey(accessibleChildSessionId) } : {}), ...(subtask ? { subtask } : {}), }; } private toHarnessLiveToolCall( executionArg: interfaces.IControllerToolExecution, projectIdArg: string, ): plugins.deesCatalog.IHarnessToolCall { if (!isSessionRuntimeId(executionArg.sessionId)) { throw new Error('Invalid live tool session ID.'); } const { callId, toolName, sessionId: _sessionId, messageId: _messageId, partId: _partId, order: _order, sourceUpdatedAt: _sourceUpdatedAt, revision: _revision, streamEpoch: _streamEpoch, ...state } = executionArg; return this.toHarnessToolCall( { id: callId, name: toolName, ...state }, projectIdArg, executionArg.sessionId, ); } private mergeLiveToolMessages( messagesArg: plugins.deesCatalog.IHarnessMessage[], projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { for (const overlay of this.liveToolOverlays.values()) { const execution = overlay.execution; if ( overlay.projectId !== projectIdArg || !controllerRuntimeIdsEqual(execution.sessionId, sessionIdArg) ) continue; const messageId = controllerRuntimeIdToUiKey(execution.partId); const toolCall = this.toHarnessLiveToolCall(execution, projectIdArg); const existing = messagesArg.find((messageArg) => messageArg.id === messageId); if (existing) { existing.toolCall = toolCall; existing.streaming = execution.status === 'pending' || execution.status === 'running'; if (execution.order) existing.order = execution.order; } else { messagesArg.push({ id: messageId, role: 'tool', text: '', markdown: false, createdAt: execution.startedAt ?? execution.sourceUpdatedAt, order: overlay.order, updatedAt: execution.finishedAt, streaming: execution.status === 'pending' || execution.status === 'running', toolCall, }); } } } private mergeLiveReasoningMessages( messagesArg: plugins.deesCatalog.IHarnessMessage[], projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { for (const [key, update] of this.liveReasoningUpdates) { if ( !key.startsWith(`${projectIdArg}\0`) || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg) ) continue; const reasoningId = controllerRuntimeIdToUiKey(update.partId); const sourceMessageId = controllerRuntimeIdToUiKey(update.messageId); let message = messagesArg.find((candidate) => ( candidate.id === sourceMessageId || candidate.reasoning?.some((part) => part.id === reasoningId) || ( update.order !== undefined && candidate.order?.messageIndex === update.order.messageIndex && candidate.order.partIndex === update.order.partIndex ) )); if (message && this.harnessMessageIsTerminal(message)) continue; if (!message) { message = { id: update.sessionId.harnessId === 'opencode' ? sourceMessageId : reasoningId, role: 'assistant', text: '', createdAt: update.sourceUpdatedAt, ...(update.order === undefined ? {} : { order: update.order }), markdown: true, reasoning: [], }; messagesArg.push(message); } const reasoning = message.reasoning ?? []; const index = reasoning.findIndex((part) => part.id === reasoningId); const part = { id: reasoningId, text: update.text, startedAt: update.sourceUpdatedAt, ...(update.status === 'running' ? {} : { endedAt: update.sourceUpdatedAt }), }; if (index >= 0) reasoning[index] = part; else reasoning.push(part); message.reasoning = reasoning; message.streaming = update.status === 'running' || (update.sessionId.harnessId === 'opencode' && message.streaming === true); } } private mergeLiveTextMessages( messagesArg: plugins.deesCatalog.IHarnessMessage[], projectIdArg: string, sessionIdArg: TBrowserSessionId, ): void { for (const [key, update] of this.liveTextUpdates) { if (!key.startsWith(`${projectIdArg}\0`) || !controllerRuntimeIdsEqual(update.sessionId, sessionIdArg)) { continue; } const id = controllerRuntimeIdToUiKey(update.partId); const sourceMessageId = controllerRuntimeIdToUiKey(update.messageId); const partMessage = messagesArg.find((message) => message.id === id); const sourceMessage = messagesArg.find((message) => message.id === sourceMessageId); const orderedMessage = update.order === undefined ? undefined : messagesArg.find((message) => ( message.order?.messageIndex === update.order!.messageIndex && message.order.partIndex === update.order!.partIndex )); const existing = sourceMessage ?? partMessage ?? orderedMessage; if (existing) { if (this.harnessMessageIsTerminal(existing)) continue; existing.text = update.text; existing.streaming = update.status === 'running' || (update.sessionId.harnessId === 'opencode' && existing.streaming === true); } else { messagesArg.push({ id: update.sessionId.harnessId === 'opencode' ? sourceMessageId : id, role: 'assistant', text: update.text, createdAt: update.sourceUpdatedAt, ...(update.order === undefined ? {} : { order: update.order }), markdown: true, streaming: update.status === 'running', }); } } } private harnessMessageIsTerminal(messageArg: plugins.deesCatalog.IHarnessMessage): boolean { return messageArg.streaming !== true && ( messageArg.updatedAt !== undefined || messageArg.error !== undefined || messageArg.usage !== undefined ); } private allocateLiveToolOrder( projectIdArg: string, executionArg: interfaces.IControllerToolExecution, ): interfaces.IControllerTranscriptOrder { const sessionKey = controllerRuntimeIdToUiKey(executionArg.sessionId); const sourceMessageKey = controllerRuntimeIdToUiKey(executionArg.messageId); let maximumMessageIndex = -1; let sourceMessageIndex: number | undefined; let maximumPartIndex = -1; if ( this.sessionDetail && controllerRuntimeIdToUiKey(this.sessionDetail.session.id) === sessionKey ) { for (const message of this.sessionDetail.messages) { if (!message.order) continue; maximumMessageIndex = Math.max(maximumMessageIndex, message.order.messageIndex); } } for (const overlay of this.liveToolOverlays.values()) { if ( overlay.projectId !== projectIdArg || controllerRuntimeIdToUiKey(overlay.execution.sessionId) !== sessionKey ) continue; maximumMessageIndex = Math.max(maximumMessageIndex, overlay.order.messageIndex); if (controllerRuntimeIdToUiKey(overlay.execution.messageId) !== sourceMessageKey) continue; sourceMessageIndex ??= overlay.order.messageIndex; maximumPartIndex = Math.max(maximumPartIndex, overlay.order.partIndex); } return { messageIndex: sourceMessageIndex ?? maximumMessageIndex + 1, partIndex: maximumPartIndex + 1, }; } private flexHarnessIsReady(): boolean { if (!this.controllerStatus) return false; const status = this.controllerStatus.harnesses.find((entryArg) => entryArg.harnessId === 'flex'); return status?.healthy === true && status.state === 'ready'; } private providerOperationsAvailable(): boolean { return this.providerManagementState === 'available' && this.flexHarnessIsReady(); } private currentCodexComposerKey(): string { return JSON.stringify([this.connectionGeneration, this.selectedProjectId, this.selectedSessionId]); } private codexHarnessIsReady(): boolean { const status = this.codexComposerKey === this.currentCodexComposerKey() ? this.codexComposer?.status : undefined; return status?.healthy === true && status.state === 'ready'; } private localAttachmentsAvailable(harnessIdArg: TBrowserSessionHarnessId): boolean { return harnessIdArg !== 'codex' || (this.codexComposerKey === this.currentCodexComposerKey() && this.codexComposer?.status.supportsLocalAttachments === true); } private sessionIsReadOnly(sessionIdArg: TBrowserSessionId): boolean { if (sessionIdArg.harnessId === 'codex') return !this.codexHarnessIsReady(); if (sessionIdArg.harnessId !== 'flex' || !this.controllerStatus) return false; return !this.flexHarnessIsReady(); } private toHarnessStatus( sessionArg: interfaces.IControllerSession, activeSubagentCountArg = 0, readOnlyArg?: boolean, ): plugins.deesCatalog.IHarnessStatus { if (!isSessionRuntimeId(sessionArg.id)) { return { type: 'error', message: 'Invalid harness session ID.' }; } const harnessLabel = sessionHarnessLabel(sessionArg.id.harnessId); if (readOnlyArg ?? this.sessionIsReadOnly(sessionArg.id)) { return { type: 'idle', message: sessionArg.id.harnessId === 'codex' ? 'Codex is unavailable.' : 'Flex unavailable. Showing a saved read-only projection.' }; } switch (sessionArg.status) { case 'busy': return { type: 'busy', message: `${harnessLabel} is working…${activeSubagentCountArg > 0 ? ` · ${activeSubagentCountArg} ${activeSubagentCountArg === 1 ? 'subagent' : 'subagents'} active` : ''}`, }; case 'retry': return { type: 'busy', message: `${harnessLabel} is retrying…` }; case 'error': return { type: 'error', message: `The ${harnessLabel} session reported an error.` }; default: return { type: 'idle' }; } } private activeSubagentCount( projectIdArg: string, ownerSessionIdArg: TBrowserSessionId, messagesArg: readonly interfaces.IControllerMessage[], ): number { const childCalls = new Map< string, { sessionId: TBrowserSessionId; status: interfaces.IControllerToolCall['status'] } >(); const applyCall = (callArg: interfaces.IControllerToolCall): void => { if (!isSessionRuntimeId(callArg.childSessionId)) return; childCalls.set(controllerRuntimeIdToUiKey(callArg.childSessionId), { sessionId: callArg.childSessionId, status: callArg.status, }); }; for (const messageArg of messagesArg) { if (messageArg.toolCall) applyCall(messageArg.toolCall); } for (const overlayArg of this.liveToolOverlays.values()) { if ( overlayArg.projectId !== projectIdArg || !controllerRuntimeIdsEqual(overlayArg.execution.sessionId, ownerSessionIdArg) ) continue; applyCall({ id: overlayArg.execution.callId, name: overlayArg.execution.toolName, status: overlayArg.execution.status, ...(overlayArg.execution.childSessionId === undefined ? {} : { childSessionId: overlayArg.execution.childSessionId }), }); } return [...childCalls.values()].filter((entryArg) => { const preview = this.subtaskPreviewEntries.get( this.subtaskPreviewKey(projectIdArg, ownerSessionIdArg, entryArg.sessionId), ); return preview ? preview.stream.status.type === 'busy' : entryArg.status === 'pending' || entryArg.status === 'running'; }).length; } private readonly focusSessionSearch = (): void => { void this.openConversationSidebarAndFocusSearch(); }; private async openConversationSidebarAndFocusSearch(): Promise { if (!this.conversationSidebarVisible) { this.conversationSidebarVisible = true; await this.updateComplete; } const sessionList = this.shadowRoot?.querySelector('dees-harness-session-list'); if (sessionList instanceof plugins.deesCatalog.DeesHarnessSessionList) { sessionList.focusSearch(); } } private renderStatusBar(): plugins.deesElement.TemplateResult { const connected = this.connectionStatus === 'connected'; const pending = this.connectionStatus === 'connecting' || this.connectionStatus === 'reconnecting'; const connectionLabel = connected ? (this.authenticated ? 'authenticated' : 'connected') : pending ? 'connecting' : 'offline'; const metrics = this.systemMetrics; const memoryTitle = metrics?.memoryUsedBytes !== null && metrics?.memoryUsedBytes !== undefined && metrics.memoryTotalBytes !== null && metrics.memoryTotalBytes !== undefined ? `${formatBytes(metrics.memoryUsedBytes)} used of ${formatBytes(metrics.memoryTotalBytes)}` : 'Memory usage unavailable'; return plugins.deesElement.html` `; } } declare global { interface HTMLElementTagNameMap { 'harness-controller-app': HarnessControllerApp; } }