Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
104 kB · 2814 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815import { homedir } from "node:os";import * as path from "node:path";import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";import type { Api, Model, ServiceTier } from "@earendil-works/pi-ai";import { type AutocompleteProvider, CombinedAutocompleteProvider, Container, type TUI, visibleWidth,} from "@earendil-works/pi-tui";import stripAnsi from "strip-ansi";import { beforeAll, describe, expect, it, test, vi } from "vitest";import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.js";import { AuthStorage } from "../src/core/auth-storage.js";import type { AgentCronJob } from "../src/core/cron-jobs.js";import type { AutocompleteProviderFactory } from "../src/core/extensions/types.js";import { emptyGoalState, type GoalState } from "../src/core/goals.js";import { createSessionSlashCommandMessage, createSessionSlashCommandResultMessage } from "../src/core/messages.js";import type { ModelRegistry } from "../src/core/model-registry.js";import type { PromptCacheSnapshot } from "../src/core/prompt-cache.js";import { InProcessAgentConnection } from "../src/modes/agent-connection/in-process-agent-connection.js";import type { AgentConnectionExtensionUiRequest, AgentConnectionExtensionUiResponse, AgentConnectionHeartbeat, AgentConnectionModel, AgentConnectionModelCatalog, AgentConnectionResourceDiagnostic, AgentConnectionResourceSnapshot, AgentConnectionSessionContext, AgentConnectionSnapshot, AgentConnectionSourceInfo, AgentConnectionState,} from "../src/modes/agent-connection/types.js";import { AgentMessageComponent } from "../src/modes/interactive/components/agent-message.js";import type { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js";import { BrandSplashHeader, InteractiveMode, truncatePathMiddle } from "../src/modes/interactive/interactive-mode.js";import { QueueSelection } from "../src/modes/interactive/queue-selection.js";import { initTheme } from "../src/modes/interactive/theme/theme.js";
function renderAll(container: Container, width = 120): string { return container.children.flatMap((child) => child.render(width)).join("\n");}
function normalizeRenderedOutput(container: Container, width = 220): string { return renderAll(container, width) .replace(/\u001b\[[0-9;]*m/g, "") .replace(/\\/g, "/") .split("\n") .map((line) => line.replace(/\s+$/g, "")) .join("\n") .trim();}
type ExtensionFixture = { path: string; sourceInfo?: AgentConnectionSourceInfo;};
function createDeferred<T>(): { promise: Promise<T>; resolve(value: T): void; reject(error: unknown): void;} { let resolve!: (value: T) => void; let reject!: (error: unknown) => void; const promise = new Promise<T>((nextResolve, nextReject) => { resolve = nextResolve; reject = nextReject; }); return { promise, resolve, reject };}
function createConnectionState(overrides: Partial<AgentConnectionState> = {}): AgentConnectionState { return { activeSessionId: "active-1", cwd: "/tmp/project", thinkingLevel: "medium", serviceTier: "default", availableThinkingLevels: ["minimal", "low", "medium", "high", "xhigh"], isStreaming: false, isCompacting: false, isBashRunning: false, retryAttempt: 0, steeringMode: "all", followUpMode: "all", sessionId: "session-1", leafId: null, autoCompactionEnabled: true, messageCount: 0, sessionActions: { queuedCount: 0, steering: [], followUps: [] }, compactionCount: 0, goal: emptyGoalState(), scopedModels: [], activeToolNames: ["ipython"], contextUsage: undefined, ...overrides, };}
type RenderSessionContextHarness = { pendingTools: Map<string, ToolExecutionComponent>; ipythonToolComponents: Map<string, unknown>; lateIpythonSentAgentMessages: Map<string, unknown[]>; toolOutputExpanded: boolean; chatContainer: Container; editor: { addToHistory?: (text: string) => void }; footer: { invalidate: () => void }; updateEditorBorderColor: () => void; resetPendingToolState: () => void; preloadToolDefinitions: (toolNames: string[]) => Promise<void>; settingsManager: { getShowImages: () => boolean }; getCachedToolDefinition: () => undefined; getCurrentCwd: () => string; getRetryAttempt: () => number; ui: { requestRender: () => void }; addMessageToChat: (message: AgentMessage, options?: { populateHistory?: boolean }) => void; showWarning: (warningMessage: string) => void; connectionState?: AgentConnectionState;};
type RenderSessionContextOptions = { updateFooter?: boolean; populateHistory?: boolean; clearChat?: boolean; limitTranscript?: boolean;};
const renderSessionContext = ( InteractiveMode.prototype as unknown as { renderSessionContext( this: RenderSessionContextHarness, sessionContext: AgentConnectionSessionContext, options?: RenderSessionContextOptions, ): Promise<void>; }).renderSessionContext;
function createRenderSessionContextHarness(overrides: Partial<RenderSessionContextHarness> = {}): { harness: RenderSessionContextHarness; chatContainer: Container; addMessageToChat: ReturnType<typeof vi.fn>; addToHistory: ReturnType<typeof vi.fn>;} { const chatContainer = overrides.chatContainer ?? new Container(); const addMessageToChat = vi.fn(() => { chatContainer.addChild({ render: () => ["assistant"], invalidate: () => {} }); }); const addToHistory = vi.fn(); const harness: RenderSessionContextHarness = { pendingTools: new Map<string, ToolExecutionComponent>(), ipythonToolComponents: new Map<string, unknown>(), lateIpythonSentAgentMessages: new Map<string, unknown[]>(), toolOutputExpanded: false, chatContainer, editor: { addToHistory }, footer: { invalidate: vi.fn() }, updateEditorBorderColor: vi.fn(), resetPendingToolState: vi.fn(), preloadToolDefinitions: vi.fn(async () => {}), settingsManager: { getShowImages: () => true }, getCachedToolDefinition: () => undefined, getCurrentCwd: () => process.cwd(), getRetryAttempt: () => 0, ui: { requestRender: vi.fn() }, addMessageToChat, showWarning: vi.fn(), ...overrides, }; Object.setPrototypeOf(harness, InteractiveMode.prototype); return { harness, chatContainer, addMessageToChat, addToHistory };}
function userMessage(content: string, timestamp: number): Extract<AgentMessage, { role: "user" }> { return { role: "user", content, timestamp };}
async function renderMessages( harness: RenderSessionContextHarness, messages: AgentMessage[], options?: RenderSessionContextOptions,): Promise<void> { await renderSessionContext.call( harness, { messages, thinkingLevel: "medium", serviceTier: "default", model: null }, options, );}
describe("InteractiveMode.renderSessionContext", () => { beforeAll(() => { initTheme("dark"); });
test("renders only the recent tail for very long initial transcripts", async () => { const { harness, chatContainer, addMessageToChat } = createRenderSessionContextHarness(); const messages = Array.from({ length: 405 }, (_, index) => userMessage(`message ${index}`, index));
await renderMessages(harness, messages, { limitTranscript: true });
expect(addMessageToChat).toHaveBeenCalledTimes(400); expect(addMessageToChat.mock.calls[0]?.[0]).toMatchObject({ content: "message 5" }); expect(renderAll(chatContainer)).toContain("Showing latest 400 of 405 messages for faster open."); });
test("keeps equal-timestamp legacy messages after the compaction summary", async () => { const { harness, addMessageToChat } = createRenderSessionContextHarness(); const earlier = userMessage("earlier", 4); const summary = { role: "compactionSummary", summary: "legacy summary", tokensBefore: 123, timestamp: 5, } as AgentMessage; const equalLater = userMessage("equal later", 5);
await renderMessages(harness, [summary, earlier, equalLater]); expect(addMessageToChat.mock.calls.map((call) => call[0])).toEqual([earlier, summary, equalLater]); });
test("orders the compaction summary at its exact boundary before bounding the initial transcript", async () => { const { harness, addMessageToChat } = createRenderSessionContextHarness(); const retained = Array.from({ length: 5 }, (_, index) => userMessage(`retained ${index}`, 5)); const summary = { role: "compactionSummary", summary: "summary", tokensBefore: 123, retainedMessageCount: retained.length, timestamp: 5, } as AgentMessage; const later = Array.from({ length: 399 }, (_, index) => userMessage(`later ${index}`, 5));
await renderMessages(harness, [summary, ...retained, ...later], { limitTranscript: true });
expect(addMessageToChat).toHaveBeenCalledTimes(400); expect(addMessageToChat.mock.calls[0]?.[0]).toBe(summary); expect(addMessageToChat.mock.calls[1]?.[0]).toMatchObject({ content: "later 0" }); });
test("preserves the full transcript when rebuilding a cleared transcript", async () => { const { harness, chatContainer, addMessageToChat } = createRenderSessionContextHarness(); chatContainer.addChild({ render: () => ["old transcript"], invalidate: () => {} }); const messages = Array.from({ length: 405 }, (_, index) => userMessage(`message ${index}`, index));
await renderMessages(harness, messages, { clearChat: true });
expect(addMessageToChat).toHaveBeenCalledTimes(405); expect(addMessageToChat.mock.calls[0]?.[0]).toMatchObject({ content: "message 0" }); expect(renderAll(chatContainer)).not.toContain("old transcript"); expect(renderAll(chatContainer)).not.toContain("for faster open"); });
test("populates editor history from the full transcript when initial rendering is capped", async () => { const { harness, addMessageToChat, addToHistory } = createRenderSessionContextHarness(); const messages = Array.from({ length: 405 }, (_, index) => userMessage(`message ${index}`, index));
await renderMessages(harness, messages, { populateHistory: true, limitTranscript: true });
expect(addMessageToChat).toHaveBeenCalledTimes(400); expect(addToHistory).toHaveBeenCalledTimes(405); expect(addToHistory.mock.calls[0]?.[0]).toBe("message 0"); expect(addToHistory.mock.calls.at(-1)?.[0]).toBe("message 404"); });});
describe("InteractiveMode connection events", () => { type ConnectionEventListener = (event: any) => Promise<void> | void;
function createSubscribeHarness(overrides: Record<string, any> = {}): { fakeThis: Record<string, any>; emit: ConnectionEventListener; } { let listener: ConnectionEventListener | undefined; const fakeThis: Record<string, any> = { agentConnection: { subscribe: vi.fn((callback: ConnectionEventListener) => { listener = callback; return vi.fn(); }), }, sessionEventQueue: Promise.resolve(), sessionEventGeneration: 0, handleEvent: vi.fn(async () => {}), refreshCommandCatalogForCurrentSession: vi.fn(async () => {}), renderResyncedSession: vi.fn(async () => {}), resetSideQuestion: vi.fn(), resetExtensionUI: vi.fn(), applyConnectionStateSnapshot: vi.fn(), resetCurrentSessionRenderState: vi.fn(), rebindCurrentSession: vi.fn(async () => {}), renderInitialMessages: vi.fn(async () => {}), ui: { requestRender: vi.fn() }, handleConnectionExtensionUiRequest: vi.fn(), showError: vi.fn(), ...overrides, }; (InteractiveMode.prototype as unknown as { subscribeToAgent(this: unknown): void }).subscribeToAgent.call( fakeThis, ); return { fakeThis, emit: (event) => listener?.(event) }; }
const callOrder = (mock: unknown): number => (mock as { mock: { invocationCallOrder: number[] } }).mock.invocationCallOrder[0] as number;
const renderResyncedSession = ( InteractiveMode.prototype as unknown as { renderResyncedSession(this: unknown, snapshot: AgentConnectionSnapshot): Promise<void>; } ).renderResyncedSession;
function createResyncHarness(overrides: Record<string, any> = {}): Record<string, any> { const fakeThis: Record<string, any> = { applyConnectionStateSnapshot: vi.fn(), refreshQueueSelectionFromState: vi.fn(), restoreTurnStartFromMessages: vi.fn(), replaceSubagentSummary: vi.fn(), updateWorkingLoaderMessage: vi.fn(), getSessionContextFromConnectionSnapshot: vi.fn(() => ({ messages: [], thinkingLevel: "medium", model: null, })), renderSessionContext: vi.fn(async () => {}), restoreStreamingMessageFromSnapshot: vi.fn(), updatePendingMessagesDisplay: vi.fn(), flushPendingBashComponents: vi.fn(), updateTerminalTitle: vi.fn(), refreshTopBarCost: vi.fn(), setGoalAnnouncementBaseline: vi.fn(), syncGoalTray: vi.fn(), syncWorkingLoader: vi.fn(), getGoalState: () => emptyGoalState(), isAgentCompacting: () => false, isBashRunning: () => false, isAgentStreaming: () => false, ...overrides, }; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); return fakeThis; }
test("degrades heartbeat refresh failures while updating the pending display during rebind", async () => { const rebindCurrentSession = ( InteractiveMode.prototype as unknown as { rebindCurrentSession(this: InteractiveMode): Promise<void> } ).rebindCurrentSession; const updatePendingMessagesDisplay = vi.fn(); const subscribeToAgent = vi.fn(); const subscribeToRosterBar = vi.fn(async () => {}); const getState = vi.fn(async () => createConnectionState()); const harness = { unsubscribe: undefined, localSessionHost: undefined, toolDefinitionCache: { clear: vi.fn() }, applyRuntimeSettings: vi.fn(), bindLocalSessionExtensions: true, bindCurrentSessionExtensions: vi.fn(async () => {}), subscribeToAgent, subscribeToRosterBar, agentConnection: { getState }, patchConnectionState: vi.fn(), refreshQueueSelectionFromState: vi.fn(), updatePendingMessagesDisplay, refreshHeartbeatCatalog: vi.fn(async () => { throw new Error("heartbeat unavailable"); }), updateAvailableProviderCount: vi.fn(async () => {}), updateEditorBorderColor: vi.fn(), updateTerminalTitle: vi.fn(), refreshTopBarCost: vi.fn(), setGoalAnnouncementBaseline: vi.fn(), syncGoalTray: vi.fn(), syncWorkingLoader: vi.fn(), getGoalState: () => emptyGoalState(), } as unknown as InteractiveMode;
await expect(rebindCurrentSession.call(harness)).resolves.toBeUndefined(); expect(updatePendingMessagesDisplay).toHaveBeenCalledOnce(); // The queue re-sync must run after subscribing, or updates in the gap are lost. expect(getState.mock.invocationCallOrder[0]).toBeGreaterThan( subscribeToAgent.mock.invocationCallOrder[0] as number, ); });
test("clears extension UI and rebinds in order when a connection-backed session is replaced", async () => { const { fakeThis, emit } = createSubscribeHarness(); const state = createConnectionState();
await emit({ type: "session_replaced", state, messages: [] });
expect(callOrder(fakeThis.resetExtensionUI)).toBeLessThan(callOrder(fakeThis.applyConnectionStateSnapshot)); expect(callOrder(fakeThis.applyConnectionStateSnapshot)).toBeLessThan( callOrder(fakeThis.resetCurrentSessionRenderState), ); expect(callOrder(fakeThis.resetCurrentSessionRenderState)).toBeLessThan(callOrder(fakeThis.rebindCurrentSession)); expect(callOrder(fakeThis.rebindCurrentSession)).toBeLessThan(callOrder(fakeThis.renderInitialMessages)); expect(fakeThis.applyConnectionStateSnapshot).toHaveBeenCalledWith(state); });
test("resynchronizes transcript state without destructive session teardown", async () => { const { fakeThis, emit } = createSubscribeHarness(); const snapshot = { state: createConnectionState(), messages: [] as [] };
await emit({ type: "session_resynced", snapshot });
expect(fakeThis.renderResyncedSession).toHaveBeenCalledWith(snapshot); // The command catalog must be current before the resynced transcript renders. expect(callOrder(fakeThis.refreshCommandCatalogForCurrentSession)).toBeLessThan( callOrder(fakeThis.renderResyncedSession), ); expect(fakeThis.resetSideQuestion).not.toHaveBeenCalled(); expect(fakeThis.resetExtensionUI).not.toHaveBeenCalled(); expect(fakeThis.resetCurrentSessionRenderState).not.toHaveBeenCalled(); expect(fakeThis.rebindCurrentSession).not.toHaveBeenCalled(); });
test("drops a resync superseded while its command catalog refreshes", async () => { const catalogStarted = createDeferred<void>(); const catalog = createDeferred<void>(); const { fakeThis, emit } = createSubscribeHarness({ refreshCommandCatalogForCurrentSession: vi.fn(() => { catalogStarted.resolve(); return catalog.promise; }), });
const resync = emit({ type: "session_resynced", snapshot: { state: createConnectionState(), messages: [] } }); await catalogStarted.promise; expect(fakeThis.refreshCommandCatalogForCurrentSession).toHaveBeenCalledOnce(); const replacement = emit({ type: "session_replaced", state: createConnectionState(), messages: [] }); catalog.resolve(); await Promise.all([resync, replacement]);
expect(fakeThis.renderResyncedSession).not.toHaveBeenCalled(); expect(fakeThis.renderInitialMessages).toHaveBeenCalledOnce(); });
test("drops a queued source event after the session is replaced", async () => { let releaseQueue: (() => void) | undefined; const blocked = new Promise<void>((resolve) => { releaseQueue = resolve; }); const { fakeThis, emit } = createSubscribeHarness({ sessionEventQueue: blocked });
const staleEvent = emit({ type: "session_event", event: { type: "message_update" } }); const replacement = emit({ type: "session_replaced", state: createConnectionState() }); releaseQueue?.(); await Promise.all([staleEvent, replacement]);
expect(fakeThis.handleEvent).not.toHaveBeenCalled(); expect(fakeThis.renderInitialMessages).toHaveBeenCalledOnce(); });
test("exits stale queue browsing when a resync replaces the queue snapshot", async () => { const queueSelection = new QueueSelection(); let editorText = "draft"; queueSelection.move({ steering: [], followUp: ["queued"] }, editorText, -1); editorText = "queued"; const snapshot: AgentConnectionSnapshot = { state: createConnectionState({ sessionActions: { queuedCount: 0, steering: [], followUps: [] } }), messages: [], }; const fakeThis = createResyncHarness({ connectionState: createConnectionState({ sessionActions: { queuedCount: 1, steering: [], followUps: ["queued"] }, }), queueSelection, pendingQueueEdit: undefined, pendingQueueMove: false, isApplyingQueueSelectionText: false, editor: { getText: () => editorText, setText: (text: string) => { editorText = text; }, }, }); fakeThis.applyConnectionStateSnapshot.mockImplementation((state: AgentConnectionState) => { fakeThis.connectionState = state; }); // The real queue re-sync must decide that the browsed entry is gone. delete fakeThis.refreshQueueSelectionFromState;
await renderResyncedSession.call(fakeThis, snapshot);
expect(queueSelection.isBrowsing).toBe(false); expect(editorText).toBe("draft"); });
test("preserves client-local work while rendering a resynchronized snapshot", async () => { const sideQuestion = { id: "side-1", status: "running" }; const extensionRequests = new Map([["request-1", { cancelLocal: vi.fn() }]]); const activeBashComponent = {}; const streamingMessage = { role: "assistant", content: [{ type: "thinking", thinking: "Still reasoning" }], } as AgentConnectionSnapshot["streamingMessage"]; const snapshot: AgentConnectionSnapshot = { state: createConnectionState({ isCompacting: true, isBashRunning: true, isStreaming: true }), messages: [userMessage("Still working", 100)], streamingMessage, }; const startAssistantStreamingMessage = vi.fn(); const fakeThis = createResyncHarness({ turnStartedAt: 1, workingStartedAt: 1, sideQuestionEvent: sideQuestion, activeConnectionExtensionUiRequests: extensionRequests, activeBashComponent, isAgentCompacting: () => true, isBashRunning: () => true, isAgentStreaming: () => true, streamingComponent: {}, streamingMessage: {}, getSessionContextFromConnectionSnapshot: vi.fn(() => ({ messages: snapshot.messages, thinkingLevel: "medium", model: null, })), restoreStreamingMessageFromSnapshot: vi.fn((message: AgentConnectionSnapshot["streamingMessage"]) => { if (message?.role === "assistant") startAssistantStreamingMessage(message); }), refreshCommandCatalogForCurrentSession: vi.fn(async () => {}), }); // The real turn-start restore must derive the timers from the snapshot messages. delete fakeThis.restoreTurnStartFromMessages;
await renderResyncedSession.call(fakeThis, snapshot);
expect(fakeThis.sideQuestionEvent).toBe(sideQuestion); expect(fakeThis.activeConnectionExtensionUiRequests).toBe(extensionRequests); expect(fakeThis.activeBashComponent).toBe(activeBashComponent); expect(fakeThis.renderSessionContext).toHaveBeenCalledWith(expect.anything(), { clearChat: true, updateFooter: true, limitTranscript: true, }); expect(startAssistantStreamingMessage).toHaveBeenCalledWith(streamingMessage); expect(fakeThis.turnStartedAt).toBe(100); expect(fakeThis.workingStartedAt).toBe(100); expect(fakeThis.updateWorkingLoaderMessage).toHaveBeenCalledOnce(); // A resync render must not re-refresh the catalog the resync handler already refreshed. expect(fakeThis.refreshCommandCatalogForCurrentSession).not.toHaveBeenCalled(); });
test("finishes local bash UI when a resync proves the operation ended", async () => { const bashComponent = { setComplete: vi.fn() }; const snapshot: AgentConnectionSnapshot = { state: createConnectionState({ isCompacting: false, isBashRunning: false, isStreaming: false }), messages: [], }; const fakeThis = createResyncHarness({ activeBashComponent: bashComponent, streamingComponent: {}, streamingMessage: {}, isAgentCompacting: () => true, isBashRunning: () => true, });
await renderResyncedSession.call(fakeThis, snapshot);
expect(bashComponent.setComplete).toHaveBeenCalledWith(undefined, false); expect(fakeThis.activeBashComponent).toBeUndefined(); expect(fakeThis.flushPendingBashComponents).toHaveBeenCalledOnce(); });
test("renderCurrentSessionState waits for replacement handling before rendering", async () => { const calls: string[] = []; const fakeThis = { sessionEventQueue: Promise.resolve().then(() => { calls.push("replacement"); }), resetCurrentSessionRenderState: () => calls.push("reset"), renderInitialMessages: async () => calls.push("messages"), updatePendingMessagesDisplay: () => calls.push("display"), syncWorkingLoader: () => calls.push("loader"), };
await ( InteractiveMode.prototype as unknown as { renderCurrentSessionState(this: typeof fakeThis): Promise<void>; } ).renderCurrentSessionState.call(fakeThis);
expect(calls).toEqual(["replacement", "reset", "messages", "display", "loader"]); });});
describe("InteractiveMode connection extension UI", () => { type ActiveConnectionExtensionUiRequest = { cancelLocal(): void };
type ConnectionExtensionUiCancelHarness = { activeConnectionExtensionUiRequests: Map<string, ActiveConnectionExtensionUiRequest>; agentConnection: { respondToExtensionUiRequest(requestId: string, response: AgentConnectionExtensionUiResponse): Promise<void>; }; showError(message: string): void; cancelActiveConnectionExtensionUiRequests(): void; };
type ConnectionExtensionUiHandlerHarness = ConnectionExtensionUiCancelHarness & { resolveConnectionExtensionUiRequest( request: AgentConnectionExtensionUiRequest, ): Promise<AgentConnectionExtensionUiResponse | undefined>; handleConnectionExtensionUiRequest(request: AgentConnectionExtensionUiRequest): Promise<void>; };
const prototype = InteractiveMode.prototype as unknown as ConnectionExtensionUiHandlerHarness;
test("reset cancellation responds to active connection UI requests", async () => { const cancelLocal = vi.fn(); const fakeThis = Object.create(InteractiveMode.prototype) as ConnectionExtensionUiCancelHarness; fakeThis.activeConnectionExtensionUiRequests = new Map([["request-1", { cancelLocal }]]); fakeThis.agentConnection = { respondToExtensionUiRequest: vi.fn(async () => {}), }; fakeThis.showError = vi.fn();
prototype.cancelActiveConnectionExtensionUiRequests.call(fakeThis); await Promise.resolve();
expect(fakeThis.activeConnectionExtensionUiRequests.size).toBe(0); expect(cancelLocal).toHaveBeenCalledTimes(1); expect(fakeThis.agentConnection.respondToExtensionUiRequest).toHaveBeenCalledWith("request-1", { cancelled: true, }); expect(fakeThis.showError).not.toHaveBeenCalled(); });
test("connection UI handler does not double respond after reset cancellation", async () => { const fakeThis = Object.create(InteractiveMode.prototype) as ConnectionExtensionUiHandlerHarness; fakeThis.activeConnectionExtensionUiRequests = new Map(); fakeThis.agentConnection = { respondToExtensionUiRequest: vi.fn(async () => {}), }; fakeThis.showError = vi.fn(); fakeThis.resolveConnectionExtensionUiRequest = vi.fn( () => new Promise<AgentConnectionExtensionUiResponse | undefined>(() => { // Intentionally left pending until cancellation wins the race. }), );
const request: AgentConnectionExtensionUiRequest = { id: "request-1", method: "select", payload: {}, }; const handling = prototype.handleConnectionExtensionUiRequest.call(fakeThis, request); expect(fakeThis.activeConnectionExtensionUiRequests.size).toBe(1);
prototype.cancelActiveConnectionExtensionUiRequests.call(fakeThis); await handling;
expect(fakeThis.agentConnection.respondToExtensionUiRequest).toHaveBeenCalledTimes(1); expect(fakeThis.agentConnection.respondToExtensionUiRequest).toHaveBeenCalledWith("request-1", { cancelled: true, }); expect(fakeThis.activeConnectionExtensionUiRequests.size).toBe(0); });});
function createFakeConnectionSession(commandName: string): AgentSessionRuntime["session"] { return { extensionRunner: { getRegisteredCommands: () => [] }, promptTemplates: [ { name: commandName, sourceInfo: { path: `/${commandName}.md`, source: `/${commandName}.md`, scope: "project", origin: "top-level", }, }, ], resourceLoader: { getSkills: () => ({ skills: [] }), getPrompts: () => ({ prompts: [] }), getThemes: () => ({ themes: [] }), getExtensions: () => ({ extensions: [], errors: [] }), getAgentsFiles: () => ({ agentsFiles: [] }), }, modelRegistry: { refreshModelCatalog: async () => ({ models: [], configuredProviders: [] }) }, refreshModelMetadata: vi.fn(), sessionManager: { getCwd: () => "/tmp/project", getSessionDir: () => "/tmp/sessions", getLeafId: () => null, getEntries: () => [], countEntriesOfType: () => 0, }, getAvailableThinkingLevels: () => ["medium"], getActiveToolNames: () => [], getContextUsage: () => undefined, thinkingLevel: "medium", serviceTier: "default", isStreaming: false, isCompacting: false, isBashRunning: false, retryAttempt: 0, steeringMode: "all", followUpMode: "all", sessionId: commandName, autoCompactionEnabled: true, sessionActions: { queuedCount: 0, steering: [], followUps: [] }, getSessionActionSnapshot: () => ({ queuedCount: 0, steering: [], followUps: [] }), goalState: emptyGoalState(), scopedModels: [], subscribe: () => () => {}, messages: [], } as unknown as AgentSessionRuntime["session"];}
class EventEmittingReplacementRuntime { private rebindSession: Parameters<AgentSessionRuntime["setRebindSession"]>[0];
constructor( public session: AgentSessionRuntime["session"], private readonly replacement: AgentSessionRuntime["session"], ) {}
setRebindSession(callback?: Parameters<AgentSessionRuntime["setRebindSession"]>[0]): void { this.rebindSession = callback; }
setBeforeSessionInvalidate(): void {}
newSession(): Promise<{ cancelled: boolean }> { return this.replaceSession(); }
switchSession(): Promise<{ cancelled: boolean }> { return this.replaceSession(); }
async fork(): Promise<{ cancelled: boolean; selectedText?: string }> { return this.replaceSession(); }
private async replaceSession(): Promise<{ cancelled: boolean }> { this.session = this.replacement; await this.rebindSession?.(this.replacement); return { cancelled: false }; }}
describe("InteractiveMode session switch command catalog", () => { test.each(["switchSession", "newSession", "fork"] as const)( "refreshes the command catalog for an event-emitting in-process %s replacement exactly once before replay", async (operation) => { const sourceSession = createFakeConnectionSession("source-command"); const targetSession = createFakeConnectionSession("target-command"); const runtime = new EventEmittingReplacementRuntime(sourceSession, targetSession); const connection = new InProcessAgentConnection(runtime as unknown as AgentSessionRuntime); const getCommands = vi.spyOn(connection, "getCommands"); const calls: string[] = []; const fakeThis = { agentConnection: connection, sessionEventQueue: Promise.resolve(), sessionEventGeneration: 0, connectionCommands: await connection.getCommands(), connectionModelsRefreshVersion: 0, bindLocalSessionExtensions: false, uiServices: { getThemes: () => [] }, toolDefinitionCache: { clear: vi.fn() }, applyRuntimeSettings: vi.fn(), applyConnectionModelCatalog: vi.fn(), showLoadedResources: vi.fn(), refreshHeartbeatCatalog: vi.fn(async () => {}), updateAvailableProviderCount: vi.fn(async () => {}), updateEditorBorderColor: vi.fn(), updateTerminalTitle: vi.fn(), refreshTopBarCost: vi.fn(), setGoalAnnouncementBaseline: vi.fn(), syncGoalTray: vi.fn(), getGoalState: () => emptyGoalState(), resetSideQuestion: vi.fn(), resetExtensionUI: vi.fn(), applyConnectionStateSnapshot: vi.fn(), resetCurrentSessionRenderState: vi.fn(() => calls.push("reset")), queueSelection: { selected: undefined }, setupAutocompleteProvider: vi.fn(() => calls.push("catalog")), renderInitialMessages: vi.fn(async () => { calls.push("render"); expect(fakeThis.connectionCommands.map((command) => command.name)).toEqual(["target-command"]); }), updatePendingMessagesDisplay: vi.fn(), syncWorkingLoader: vi.fn(), ui: { requestRender: vi.fn() }, handleEvent: vi.fn(), handleConnectionExtensionUiRequest: vi.fn(), showError: vi.fn(), }; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); const interactiveHarness = fakeThis as typeof fakeThis & { subscribeToAgent(): void; renderCurrentSessionState(): Promise<void>; }; interactiveHarness.subscribeToAgent();
if (operation === "switchSession") await connection.switchSession("/target/session.jsonl"); else if (operation === "newSession") await connection.newSession(); else await connection.fork("entry-1"); expect(fakeThis.showError).not.toHaveBeenCalled(); await interactiveHarness.renderCurrentSessionState();
expect(calls).toEqual(["reset", "catalog", "render", "reset", "render"]); expect(getCommands).toHaveBeenCalledTimes(2); // initial catalog + one replacement refresh expect(targetSession.refreshModelMetadata).toHaveBeenCalledOnce(); }, );
test("keeps catalog refresh failures nonfatal and drops stale dynamic commands", async () => { const setupAutocompleteProvider = vi.fn(); const fakeThis = Object.create(InteractiveMode.prototype) as { agentConnection: { getCommands(): Promise<never> }; connectionCommands: unknown[]; setupAutocompleteProvider(): void; refreshCommandCatalogForCurrentSession(): Promise<void>; }; fakeThis.agentConnection = { getCommands: vi.fn(async () => { throw new Error("catalog unavailable"); }), }; fakeThis.connectionCommands = [{ name: "stale-command" }]; fakeThis.setupAutocompleteProvider = setupAutocompleteProvider;
await expect(fakeThis.refreshCommandCatalogForCurrentSession()).resolves.toBeUndefined();
expect(fakeThis.connectionCommands).toEqual([]); expect(setupAutocompleteProvider).toHaveBeenCalledOnce(); });});
describe("InteractiveMode model catalog staleness", () => { type CatalogHarness = { agentConnection: { getModelCatalog: () => Promise<AgentConnectionModelCatalog>; getState: () => Promise<AgentConnectionState>; getCommands: () => Promise<unknown[]>; getResourceSnapshot: () => Promise<unknown>; }; connectionCommands: unknown[]; connectionResourceSnapshot: unknown; connectionModelCatalog: AgentConnectionModel[]; connectionConfiguredProviders: Set<string>; connectionModelsFetchedAt: number; connectionModelsRefreshVersion: number; connectionModelsRefreshInFlight: { version: number; promise: Promise<AgentConnectionModel[]> } | undefined; uiServices: { modelRegistry: ModelRegistry }; getScopedModelState(): AgentConnectionState["scopedModels"]; applyConnectionStateSnapshot(state: AgentConnectionState): void; getCachedModelCandidates(): AgentConnectionModel[]; getAvailableConnectionModels(): AgentConnectionModel[]; getConnectionAvailableModels(): Promise<AgentConnectionModel[]>; findExactModelMatch(searchTerm: string): Promise<AgentConnectionModel | undefined>; refreshConnectionCatalog(): Promise<void>; };
const createModel = (provider: string, id: string): AgentConnectionModel => ({ provider, id, name: id, cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 }, }) as AgentConnectionModel;
function createCatalogHarness( options: { connectionModels?: AgentConnectionModel[]; catalogModels?: AgentConnectionModel[]; registryModels?: AgentConnectionModel[]; fetched?: boolean; scopedModels?: AgentConnectionState["scopedModels"]; getModelCatalog?: () => Promise<AgentConnectionModelCatalog>; getCommands?: () => Promise<unknown[]>; } = {}, ): CatalogHarness { const connectionModels = options.connectionModels ?? []; const catalogModels = options.catalogModels ?? connectionModels; const registryModels = options.registryModels ?? []; const modelRegistry = { authStorage: AuthStorage.inMemory(), refresh: vi.fn(), getError: vi.fn(() => undefined), getAvailable: vi.fn(() => registryModels), hasConfiguredAuth: vi.fn(() => false), getProviderAuthStatus: vi.fn(() => ({ configured: false })), find: vi.fn((provider: string, modelId: string) => registryModels.find((model) => model.provider === provider && model.id === modelId), ), } as unknown as ModelRegistry; const fakeThis = Object.create(InteractiveMode.prototype) as CatalogHarness; fakeThis.agentConnection = { getModelCatalog: options.getModelCatalog ?? vi.fn(async () => ({ models: catalogModels, configuredProviders: catalogModels.map((model) => model.provider), })), getState: vi.fn(async () => createConnectionState()), getCommands: options.getCommands ?? vi.fn(async () => []), getResourceSnapshot: vi.fn(async () => ({})), setModel: vi.fn(async () => {}), } as never; fakeThis.connectionCommands = []; fakeThis.connectionResourceSnapshot = undefined; fakeThis.connectionModelCatalog = [...connectionModels]; fakeThis.connectionConfiguredProviders = new Set(connectionModels.map((model) => model.provider)); fakeThis.connectionModelsFetchedAt = options.fetched ? Date.now() : 0; fakeThis.connectionModelsRefreshVersion = 0; fakeThis.connectionModelsRefreshInFlight = undefined; fakeThis.uiServices = { modelRegistry }; fakeThis.getScopedModelState = vi.fn(() => options.scopedModels ?? []); fakeThis.applyConnectionStateSnapshot = vi.fn(); return fakeThis; }
test.each([ { label: "refreshes cached candidates for an exact model miss", cached: ["alpha"], fetched: true, registry: [] as string[], catalog: ["beta"], search: "beta", expected: "beta" as string | undefined, }, { label: "does not exact-match a local fallback before the daemon catalog loads", cached: [] as string[], fetched: false, registry: ["local-only"], catalog: [] as string[], search: "local-only", expected: undefined as string | undefined, }, ])("$label", async ({ cached, fetched, registry, catalog, search, expected }) => { const harness = createCatalogHarness({ connectionModels: cached.map((id) => createModel("openai", id)), catalogModels: catalog.map((id) => createModel("openai", id)), registryModels: registry.map((id) => createModel("openai", id)), fetched, });
await expect(harness.findExactModelMatch(search)).resolves.toEqual( expected === undefined ? undefined : expect.objectContaining({ id: expected }), ); expect(harness.agentConnection.getModelCatalog).toHaveBeenCalledTimes(1); });
test("keeps local fallback models out of the cached candidates", () => { const harness = createCatalogHarness({ registryModels: [createModel("openai", "local-only")] });
expect(harness.getCachedModelCandidates()).toEqual([]); });
test("keeps cached daemon models visible with scoped models without refetching", async () => { const scoped = createModel("openai", "scoped"); const cached = createModel("anthropic", "catalog"); const harness = createCatalogHarness({ connectionModels: [cached], fetched: true, scopedModels: [{ model: scoped }], });
expect(harness.getCachedModelCandidates()).toEqual([scoped, cached]); await expect(harness.findExactModelMatch("catalog")).resolves.toEqual(cached); expect(harness.agentConnection.getModelCatalog).not.toHaveBeenCalled(); });
test("does not let a stale model refresh overwrite a newer catalog refresh", async () => { const oldModel = createModel("openai", "old"); const freshModel = createModel("openai", "fresh"); const oldModels = createDeferred<AgentConnectionModelCatalog>(); const getModelCatalog = vi .fn<() => Promise<AgentConnectionModelCatalog>>() .mockImplementationOnce(() => oldModels.promise) .mockImplementationOnce(async () => ({ models: [freshModel], configuredProviders: [freshModel.provider] })); const harness = createCatalogHarness({ getModelCatalog });
const staleRefresh = harness.getConnectionAvailableModels(); await harness.refreshConnectionCatalog(); oldModels.resolve({ models: [oldModel], configuredProviders: [oldModel.provider] });
await expect(staleRefresh).resolves.toEqual([freshModel]); expect(harness.getAvailableConnectionModels()).toEqual([freshModel]); });
test("keeps the refreshed model catalog when the command catalog fails", async () => { const cachedModel = createModel("openai", "cached"); const freshModel = createModel("openai", "fresh"); const harness = createCatalogHarness({ connectionModels: [cachedModel], catalogModels: [freshModel], fetched: true, getCommands: vi.fn(async () => { throw new Error("commands unavailable"); }), });
await expect(harness.refreshConnectionCatalog()).resolves.toBeUndefined();
expect(harness.connectionCommands).toEqual([]); expect(harness.getAvailableConnectionModels()).toEqual([expect.objectContaining({ id: "fresh" })]); expect(harness.connectionModelsFetchedAt).toBeGreaterThan(0); });});
describe("InteractiveMode live context usage", () => { type LiveContextHarness = { connectionState: Pick<AgentConnectionState, "contextUsage"> | undefined; activityTracker: { getStatus(): { tokens: number } }; isAgentStreaming(): boolean; contextUsageTokenBaseline: number; getConnectionContextUsage(): AgentConnectionState["contextUsage"]; }; const prototype = InteractiveMode.prototype as unknown as LiveContextHarness;
function createHarness( opts: { streaming?: boolean; inFlight?: number; baseline?: number } = {}, ): LiveContextHarness { const fakeThis = Object.create(InteractiveMode.prototype) as LiveContextHarness; fakeThis.connectionState = { contextUsage: undefined }; fakeThis.activityTracker = { getStatus: () => ({ tokens: opts.inFlight ?? 0 }) }; fakeThis.isAgentStreaming = () => opts.streaming ?? false; fakeThis.contextUsageTokenBaseline = opts.baseline ?? 0; return fakeThis; }
const SNAPSHOT = { contextWindow: 100_000, tokens: 42_000, percent: 42 };
test.each([ // Idle sessions report the daemon snapshot verbatim. { label: "returns the snapshot verbatim when idle", opts: {}, snapshot: SNAPSHOT, expected: SNAPSHOT }, { label: "adds in-flight streaming output to the snapshot baseline while streaming", opts: { streaming: true, inFlight: 3_000 }, snapshot: SNAPSHOT, expected: { contextWindow: 100_000, tokens: 45_000, percent: 45 }, }, { // Tracker holds 5k from a failed attempt (baseline), now 6k after 1k of the retry: // only the 1k generated since the refresh counts as in-flight. label: "only adds output beyond the refresh baseline (auto-retry does not double count)", opts: { streaming: true, inFlight: 6_000, baseline: 5_000 }, snapshot: SNAPSHOT, expected: { contextWindow: 100_000, tokens: 43_000, percent: 43 }, }, { label: "does not add in-flight output when not streaming", opts: { streaming: false, inFlight: 3_000 }, snapshot: SNAPSHOT, expected: SNAPSHOT, }, { label: "passes through an unknown (post-compaction) snapshot without inflating it", opts: { streaming: true, inFlight: 3_000 }, snapshot: { contextWindow: 100_000, tokens: null, percent: null }, expected: { contextWindow: 100_000, tokens: null, percent: null }, }, { label: "returns undefined when there is no snapshot yet", opts: { streaming: true, inFlight: 3_000 }, snapshot: undefined, expected: undefined, }, ])("$label", ({ opts, snapshot, expected }) => { const fakeThis = createHarness(opts); fakeThis.connectionState = { contextUsage: snapshot as AgentConnectionState["contextUsage"] };
expect(prototype.getConnectionContextUsage.call(fakeThis)).toEqual(expected); });
type RefreshHarness = { agentConnection: { getSessionStats(): Promise<{ contextUsage: unknown } | undefined> }; connectionState: { sessionId?: string; contextUsage?: unknown }; activityTracker: { getStatus(): { tokens: number } }; contextUsageTokenBaseline: number; contextUsageRefresh: { generation: number; lastSuccessGeneration: number }; patchConnectionState(patch: Record<string, unknown>): void; refreshConnectionContextUsage(): Promise<void>; }; const refresh = (InteractiveMode.prototype as unknown as RefreshHarness).refreshConnectionContextUsage;
function createRefreshHarness( getSessionStats: RefreshHarness["agentConnection"]["getSessionStats"], ): RefreshHarness & { patched: Record<string, unknown>[] } { const fakeThis = Object.create(InteractiveMode.prototype) as RefreshHarness & { patched: Record<string, unknown>[]; }; fakeThis.patched = []; fakeThis.activityTracker = { getStatus: () => ({ tokens: 0 }) }; fakeThis.contextUsageTokenBaseline = 0; fakeThis.contextUsageRefresh = { generation: 0, lastSuccessGeneration: 0 }; fakeThis.connectionState = { sessionId: "session-A", contextUsage: undefined }; fakeThis.patchConnectionState = (patch) => fakeThis.patched.push(patch); fakeThis.agentConnection = { getSessionStats }; return fakeThis; }
test("refreshConnectionContextUsage drops stale stats after a session switch and applies the next refresh", async () => { const stale = createDeferred<{ contextUsage: unknown }>(); const freshContextUsage = { contextWindow: 200_000, tokens: 20_000, percent: 10 }; const getSessionStats = vi .fn<RefreshHarness["agentConnection"]["getSessionStats"]>() .mockImplementationOnce(() => stale.promise) .mockResolvedValueOnce({ contextUsage: freshContextUsage }); const fakeThis = createRefreshHarness(getSessionStats);
const staleRefresh = refresh.call(fakeThis); await Promise.resolve(); fakeThis.connectionState = { sessionId: "session-B", contextUsage: undefined }; stale.resolve({ contextUsage: { contextWindow: 100_000, tokens: 50_000, percent: 50 } }); await staleRefresh;
// Stats belonged to session-A; the next refresh for session-B must still apply normally. expect(fakeThis.patched).toEqual([]); await refresh.call(fakeThis); expect(fakeThis.patched).toEqual([{ contextUsage: freshContextUsage }]); expect(getSessionStats).toHaveBeenCalledTimes(2); });
test("refreshConnectionContextUsage keeps a newer successful same-session response", async () => { const first = createDeferred<{ contextUsage: unknown }>(); const second = createDeferred<{ contextUsage: unknown }>(); let request = 0; const fakeThis = createRefreshHarness(() => (++request === 1 ? first.promise : second.promise));
const olderRefresh = refresh.call(fakeThis); const newerRefresh = refresh.call(fakeThis); second.resolve({ contextUsage: { tokens: 10, contextWindow: 100, percent: 10 } }); await newerRefresh; first.resolve({ contextUsage: { tokens: 90, contextWindow: 100, percent: 90 } }); await olderRefresh;
expect(fakeThis.patched).toEqual([{ contextUsage: { tokens: 10, contextWindow: 100, percent: 10 } }]); });
test.each(["failure", "no stats"] as const)( "refreshConnectionContextUsage lets an older successful response survive a newer %s", async (newerResult) => { const first = createDeferred<{ contextUsage: unknown }>(); let request = 0; const fakeThis = createRefreshHarness(() => { if (++request === 1) return first.promise; return newerResult === "failure" ? Promise.reject(new Error("stats unavailable")) : Promise.resolve(undefined); });
const olderRefresh = refresh.call(fakeThis); await refresh.call(fakeThis); first.resolve({ contextUsage: { tokens: 90, contextWindow: 100, percent: 90 } }); await olderRefresh;
expect(fakeThis.patched).toEqual([{ contextUsage: { tokens: 90, contextWindow: 100, percent: 90 } }]); }, );});
describe("InteractiveMode Fast mode concurrency", () => { type FastCommandContext = { connectionState?: { sessionId: string; serviceTier: ServiceTier; thinkingLevel: ThinkingLevel }; fastModeToggleQueue: Promise<void>; agentConnection: { setServiceTier: (serviceTier: ServiceTier) => Promise<void>; getState: () => Promise<{ sessionId: string; serviceTier: ServiceTier }>; }; footer: { invalidate: () => void }; subagentSummaryLine: { invalidate: () => void }; showStatus: (message: string) => void; showError: (message: string) => void; patchConnectionState: (patch: Record<string, unknown>) => void; getCurrentModel: () => Model<Api> | undefined; currentModelSupportsFastMode: () => boolean; getConnectionContextUsage: () => undefined; };
type FastInteractiveModePrototype = { currentModelSupportsFastMode(this: FastCommandContext): boolean; handleFastCommand(this: FastCommandContext): void; getModelContextLabel(this: FastCommandContext, maxWidth: number): string; };
const fastInteractiveModePrototype = InteractiveMode.prototype as unknown as FastInteractiveModePrototype;
function testModel(provider: string, id: string, api: Api): Model<Api> { return { id, name: id, api, provider, baseUrl: "https://example.com", reasoning: true, input: ["text"], cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000, maxTokens: 100, }; }
function makeFastContext(model: Model<Api> = testModel("openai-codex", "gpt-5.5", "openai-codex-responses")) { const context: FastCommandContext = { connectionState: { sessionId: "session-1", serviceTier: "default", thinkingLevel: "high" }, fastModeToggleQueue: Promise.resolve(), agentConnection: undefined as never, footer: { invalidate: vi.fn() }, subagentSummaryLine: { invalidate: vi.fn() }, showStatus: vi.fn(), showError: vi.fn(), patchConnectionState: vi.fn((patch: Record<string, unknown>) => { context.connectionState = { ...context.connectionState, ...patch } as FastCommandContext["connectionState"]; }), getCurrentModel: () => model, getConnectionContextUsage: () => undefined, currentModelSupportsFastMode: () => fastInteractiveModePrototype.currentModelSupportsFastMode.call(context), }; context.agentConnection = { setServiceTier: vi.fn(async (serviceTier) => { context.connectionState = { ...context.connectionState!, serviceTier }; }), getState: vi.fn(async () => ({ sessionId: context.connectionState!.sessionId, serviceTier: context.connectionState!.serviceTier, })), }; return context; }
test("serializes rapid toggles and applies both results in order", async () => { const context = makeFastContext(); const firstToggle = createDeferred<void>(); let toggleCall = 0; context.agentConnection.setServiceTier = vi.fn(async (serviceTier) => { toggleCall += 1; if (toggleCall === 1) await firstToggle.promise; context.connectionState = { ...context.connectionState!, serviceTier }; });
fastInteractiveModePrototype.handleFastCommand.call(context); fastInteractiveModePrototype.handleFastCommand.call(context); await Promise.resolve();
expect(context.agentConnection.setServiceTier).toHaveBeenCalledOnce(); expect(context.agentConnection.setServiceTier).toHaveBeenCalledWith("priority");
firstToggle.resolve(); await context.fastModeToggleQueue;
expect(context.agentConnection.setServiceTier).toHaveBeenNthCalledWith(1, "priority"); expect(context.agentConnection.setServiceTier).toHaveBeenNthCalledWith(2, "default"); expect(context.patchConnectionState).toHaveBeenNthCalledWith(1, { serviceTier: "priority" }); expect(context.patchConnectionState).toHaveBeenNthCalledWith(2, { serviceTier: "default" }); expect(context.showStatus).toHaveBeenNthCalledWith(1, "Fast mode: on"); expect(context.showStatus).toHaveBeenNthCalledWith(2, "Fast mode: off"); expect(context.connectionState?.serviceTier).toBe("default"); }); test("drops a queued toggle after switching sessions", async () => { let releaseQueue!: () => void; const context = makeFastContext(); const originalConnection = context.agentConnection; context.fastModeToggleQueue = new Promise<void>((resolve) => { releaseQueue = resolve; });
fastInteractiveModePrototype.handleFastCommand.call(context); context.agentConnection = { setServiceTier: vi.fn(async () => {}), getState: vi.fn( async (): Promise<{ sessionId: string; serviceTier: ServiceTier }> => ({ sessionId: "session-2", serviceTier: "default", }), ), }; context.connectionState = { sessionId: "session-2", serviceTier: "default", thinkingLevel: "high" }; releaseQueue(); await context.fastModeToggleQueue;
expect(originalConnection.setServiceTier).not.toHaveBeenCalled(); expect(context.agentConnection.setServiceTier).not.toHaveBeenCalled(); }); test("does not apply an in-flight toggle result to a replacement session", async () => { const toggleStarted = createDeferred<void>(); const finishToggle = createDeferred<void>(); const context = makeFastContext(); const originalConnection = context.agentConnection; originalConnection.setServiceTier = vi.fn(() => { toggleStarted.resolve(); return finishToggle.promise; });
fastInteractiveModePrototype.handleFastCommand.call(context); await toggleStarted.promise; expect(originalConnection.setServiceTier).toHaveBeenCalledWith("priority");
context.agentConnection = { setServiceTier: vi.fn(async () => {}), getState: vi.fn( async (): Promise<{ sessionId: string; serviceTier: ServiceTier }> => ({ sessionId: "session-2", serviceTier: "default", }), ), }; context.connectionState = { sessionId: "session-2", serviceTier: "default", thinkingLevel: "high" }; finishToggle.resolve(); await context.fastModeToggleQueue;
expect(context.patchConnectionState).not.toHaveBeenCalled(); expect(context.showStatus).not.toHaveBeenCalled(); });});
describe("remote host indicators", () => { function createMode(remoteHost: string | undefined) { const mode = { options: { sessionDepth: undefined, sessionHasChildren: false, returnToAgentsView: false }, editor: {}, editorContainer: { children: [] }, ui: { hasOverlay: () => false }, subagentSnapshots: undefined, connectionState: { model: { name: "test-model", reasoning: true }, thinkingLevel: "high" as const, messageCount: 0, isStreaming: false, remoteHost, }, }; Object.setPrototypeOf(mode, InteractiveMode.prototype); return mode; }
it("shows the remote host in the tray location label", () => { const mode = createMode("chernobog"); const label = Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode) as string;
expect(stripAnsi(label)).toContain("on chernobog"); });
it("omits the host label when no remote host is set", () => { const mode = createMode(undefined); const label = Reflect.get(InteractiveMode.prototype, "getTrayLocationLabel").call(mode) as string;
expect(stripAnsi(label)).not.toContain("on "); });
it("renders remote host in splash header metadata when provided", () => { const header = new BrandSplashHeader("0.0.0", () => "/tmp/project", undefined, { topPadding: true, getExtraMetadata: () => [{ label: "host", value: "chernobog" }], });
const output = stripAnsi(header.render(120).join("\n"));
expect(output).toContain("host"); expect(output).toContain("chernobog"); });});
describe("InteractiveMode tray goal label", () => { type TrayUsage = { contextWindow: number; tokens: number | null; percent: number | null; promptCache?: PromptCacheSnapshot; }; type TrayLabelHarness = { heartbeatCatalog: AgentConnectionHeartbeat[]; subagentSnapshots: Map<string, never>; connectionState: { activeSessionId: string; sessionId: string; goal?: GoalState; heartbeat?: AgentCronJob | null; contextUsage: TrayUsage | undefined; }; uiServices: { getContextUsage(): TrayUsage | undefined }; isAgentStreaming?(): boolean; getCurrentModel?(): { id: string; provider: string; reasoning?: boolean } | undefined; getTrayContextLabel(): string | undefined; }; const getTrayContextLabel = (InteractiveMode.prototype as unknown as TrayLabelHarness).getTrayContextLabel;
function createHeartbeat(status: AgentCronJob["status"]): AgentCronJob { return { id: "heartbeat-1", status, source: "heartbeat", activeSessionId: "active-1", sessionId: "session-1", sessionFile: "/tmp/session.jsonl", cwd: "/tmp/project", prompt: "check whether there is follow-up work", schedule: { kind: "interval", expression: "every 5m", intervalMs: 300_000 }, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", nextRunAt: "2026-01-01T00:05:00.000Z", runCount: 0, }; }
test("shows active goals in the lower tray without an objective", () => { const fakeThis = Object.create(InteractiveMode.prototype) as TrayLabelHarness; fakeThis.heartbeatCatalog = []; fakeThis.subagentSnapshots = new Map<string, never>(); fakeThis.connectionState = { activeSessionId: "active-1", sessionId: "session-1", goal: { active: true, status: "active", objective: "a long objective that should not render in the tray", tokensUsed: 0, timeUsedSeconds: 65, continuationsUsed: 1, } satisfies GoalState, contextUsage: undefined, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s)"); });
test("combines active goals with token/context usage in one lower-tray label", () => { const fakeThis = Object.create(InteractiveMode.prototype) as TrayLabelHarness; fakeThis.heartbeatCatalog = []; fakeThis.subagentSnapshots = new Map<string, never>(); fakeThis.connectionState = { activeSessionId: "active-1", sessionId: "session-1", goal: { active: true, status: "active", objective: "finish the task", tokensUsed: 0, timeUsedSeconds: 65, continuationsUsed: 1, } satisfies GoalState, contextUsage: { contextWindow: 100_000, tokens: 75_000, percent: 75 }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s) · 75k (75%)"); });
test("formats cached tokens as slash ratio when promptCache is warm", () => { const fakeThis = Object.create(InteractiveMode.prototype) as TrayLabelHarness; fakeThis.heartbeatCatalog = []; fakeThis.subagentSnapshots = new Map<string, never>(); fakeThis.isAgentStreaming = () => false; fakeThis.connectionState = { activeSessionId: "active-1", sessionId: "session-1", contextUsage: { contextWindow: 100_000, tokens: 75_000, percent: 75, promptCache: { status: "warm", cachedTokens: 50_000, ttlSeconds: 300, // Two seconds of slack: the label floors to whole seconds, so a render // that lands a millisecond later reads 3m 59s instead of 4m. expiresAt: Date.now() + 242_000, deterministic: true, }, }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toMatch(/^50k\/75k cached \(75%\) · ttl 4m(?: \d+s)?$/); });
test("retains cached ratio and ttl label when agent is streaming", () => { const fakeThis = Object.create(InteractiveMode.prototype) as TrayLabelHarness; fakeThis.heartbeatCatalog = []; fakeThis.subagentSnapshots = new Map<string, never>(); fakeThis.isAgentStreaming = () => true; fakeThis.connectionState = { activeSessionId: "active-1", sessionId: "session-1", contextUsage: { contextWindow: 100_000, tokens: 75_000, percent: 75, promptCache: { status: "warm", cachedTokens: 50_000, ttlSeconds: 300, expiresAt: Date.now() + 240_000, deterministic: true, }, }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
// The ttl text is derived from the clock, so a slow machine can land a second // past the four-minute mark; the ratio and the minute are what matter here. expect(getTrayContextLabel.call(fakeThis)).toMatch(/^50k\/75k cached \(75%\) · ttl 4m(?: \d+s)?$/); });
test("combines active goals, active heartbeats, and context usage in one lower-tray label", () => { const fakeThis = Object.create(InteractiveMode.prototype) as TrayLabelHarness; fakeThis.heartbeatCatalog = [{ job: createHeartbeat("active") }]; fakeThis.subagentSnapshots = new Map<string, never>(); fakeThis.connectionState = { activeSessionId: "active-1", sessionId: "session-1", goal: { active: true, status: "active", objective: "finish the task", tokensUsed: 0, timeUsedSeconds: 65, continuationsUsed: 1, } satisfies GoalState, heartbeat: createHeartbeat("active"), contextUsage: { contextWindow: 100_000, tokens: 75_000, percent: 75 }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s) · 1 heartbeat · 75k (75%)"); });
test("omits the usage segment when token count is unknown", () => { const fakeThis = Object.create(InteractiveMode.prototype) as TrayLabelHarness; fakeThis.heartbeatCatalog = []; fakeThis.subagentSnapshots = new Map<string, never>(); fakeThis.connectionState = { activeSessionId: "active-1", sessionId: "session-1", goal: { active: true, status: "active", objective: "finish the task", tokensUsed: 0, timeUsedSeconds: 65, continuationsUsed: 1, } satisfies GoalState, contextUsage: { contextWindow: 100_000, tokens: null, percent: null }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s)"); });});
describe("InteractiveMode live context usage", () => { type LiveContextHarness = { connectionState: Pick<AgentConnectionState, "contextUsage"> | undefined; activityTracker: { getStatus(): { tokens: number } }; isAgentStreaming(): boolean; contextUsageTokenBaseline: number; getConnectionContextUsage(): AgentConnectionState["contextUsage"]; }; const prototype = InteractiveMode.prototype as unknown as LiveContextHarness;
function createHarness( opts: { streaming?: boolean; inFlight?: number; baseline?: number } = {}, ): LiveContextHarness { const fakeThis = Object.create(InteractiveMode.prototype) as LiveContextHarness; fakeThis.connectionState = { contextUsage: undefined }; fakeThis.activityTracker = { getStatus: () => ({ tokens: opts.inFlight ?? 0 }) }; fakeThis.isAgentStreaming = () => opts.streaming ?? false; fakeThis.contextUsageTokenBaseline = opts.baseline ?? 0; return fakeThis; }
test("returns the snapshot verbatim when idle", () => { const fakeThis = createHarness(); fakeThis.connectionState = { contextUsage: { contextWindow: 100_000, tokens: 42_000, percent: 42 } };
expect(prototype.getConnectionContextUsage.call(fakeThis)).toEqual({ contextWindow: 100_000, tokens: 42_000, percent: 42, }); });
test("adds in-flight streaming output to the snapshot baseline while streaming", () => { const fakeThis = createHarness({ streaming: true, inFlight: 3_000 }); fakeThis.connectionState = { contextUsage: { contextWindow: 100_000, tokens: 42_000, percent: 42 } };
// 42k baseline + 3k in-flight = 45k; ticks up live as the model writes. expect(prototype.getConnectionContextUsage.call(fakeThis)).toMatchObject({ tokens: 45_000, percent: 45 }); });
test("only adds output beyond the refresh baseline (auto-retry does not double count)", () => { // Tracker holds 5k from a failed attempt (baseline), now 6k after 1k of the retry. const fakeThis = createHarness({ streaming: true, inFlight: 6_000, baseline: 5_000 }); fakeThis.connectionState = { contextUsage: { contextWindow: 100_000, tokens: 42_000, percent: 42 } };
// Only the 1k generated since the refresh is in-flight, not the failed attempt's 5k. expect(prototype.getConnectionContextUsage.call(fakeThis)).toMatchObject({ tokens: 43_000 }); });
test("does not add in-flight output when not streaming", () => { const fakeThis = createHarness({ streaming: false, inFlight: 3_000 }); fakeThis.connectionState = { contextUsage: { contextWindow: 100_000, tokens: 42_000, percent: 42 } };
expect(prototype.getConnectionContextUsage.call(fakeThis)).toMatchObject({ tokens: 42_000 }); });
test("passes through an unknown (post-compaction) snapshot without inflating it", () => { const fakeThis = createHarness({ streaming: true, inFlight: 3_000 }); fakeThis.connectionState = { contextUsage: { contextWindow: 100_000, tokens: null, percent: null } };
expect(prototype.getConnectionContextUsage.call(fakeThis)).toMatchObject({ tokens: null, percent: null }); });
test("returns undefined when there is no snapshot yet", () => { const fakeThis = createHarness({ streaming: true, inFlight: 3_000 }); fakeThis.connectionState = { contextUsage: undefined };
expect(prototype.getConnectionContextUsage.call(fakeThis)).toBeUndefined(); });
test("incorporates streaming message cache and input usage in-flight", () => { const fakeThis = createHarness({ streaming: true, inFlight: 500 }) as LiveContextHarness & { streamingMessage?: unknown; getCurrentModel?: () => unknown; }; fakeThis.connectionState = { contextUsage: { contextWindow: 100_000, tokens: 10_000, percent: 10 } }; fakeThis.streamingMessage = { provider: "anthropic", model: "claude-3-5-sonnet", usage: { input: 20_000, cacheRead: 15_000, cacheWrite: 0, output: 0, }, }; fakeThis.getCurrentModel = () => ({ provider: "anthropic", id: "claude-3-5-sonnet", contextWindow: 100_000 });
const usage = prototype.getConnectionContextUsage.call(fakeThis); expect(usage).toMatchObject({ tokens: 35_500, percent: 35.5, promptCache: expect.objectContaining({ status: "warm", cachedTokens: 15_000, }), }); });
type RefreshHarness = { agentConnection: { getSessionStats(): Promise<{ contextUsage: unknown } | undefined> }; connectionState: { sessionId?: string; contextUsage?: unknown }; activityTracker: { getStatus(): { tokens: number } }; contextUsageTokenBaseline: number; contextUsageRefresh: { generation: number; lastSuccessGeneration: number }; patchConnectionState(patch: Record<string, unknown>): void; refreshConnectionContextUsage(): Promise<void>; }; const refresh = (InteractiveMode.prototype as unknown as RefreshHarness).refreshConnectionContextUsage;
function createRefreshHarness( getSessionStats: RefreshHarness["agentConnection"]["getSessionStats"], ): RefreshHarness & { patched: Record<string, unknown>[] } { const fakeThis = Object.create(InteractiveMode.prototype) as RefreshHarness & { patched: Record<string, unknown>[]; }; fakeThis.patched = []; fakeThis.activityTracker = { getStatus: () => ({ tokens: 0 }) }; fakeThis.contextUsageTokenBaseline = 0; fakeThis.contextUsageRefresh = { generation: 0, lastSuccessGeneration: 0 }; fakeThis.connectionState = { sessionId: "session-A", contextUsage: undefined }; fakeThis.patchConnectionState = (patch) => fakeThis.patched.push(patch); fakeThis.agentConnection = { getSessionStats }; return fakeThis; }
test("refreshConnectionContextUsage drops stale stats after a session switch", async () => { let fakeThis: ReturnType<typeof createRefreshHarness>; fakeThis = createRefreshHarness(async () => { // User switches sessions while the stats call is in flight. fakeThis.connectionState = { sessionId: "session-B", contextUsage: undefined }; return { contextUsage: { contextWindow: 100_000, tokens: 50_000, percent: 50 } }; });
await refresh.call(fakeThis);
// Stats belonged to session-A; must not overwrite session-B. expect(fakeThis.patched).toHaveLength(0); });
test("refreshConnectionContextUsage keeps a newer successful same-session response", async () => { const first = createDeferred<{ contextUsage: unknown }>(); const second = createDeferred<{ contextUsage: unknown }>(); let request = 0; const fakeThis = createRefreshHarness(() => (++request === 1 ? first.promise : second.promise));
const olderRefresh = refresh.call(fakeThis); const newerRefresh = refresh.call(fakeThis); second.resolve({ contextUsage: { tokens: 10, contextWindow: 100, percent: 10 } }); await newerRefresh; first.resolve({ contextUsage: { tokens: 90, contextWindow: 100, percent: 90 } }); await olderRefresh;
expect(fakeThis.patched).toEqual([{ contextUsage: { tokens: 10, contextWindow: 100, percent: 10 } }]); });
test.each(["failure", "no stats"] as const)( "refreshConnectionContextUsage lets an older successful response survive a newer %s", async (newerResult) => { const first = createDeferred<{ contextUsage: unknown }>(); let request = 0; const fakeThis = createRefreshHarness(() => { if (++request === 1) return first.promise; return newerResult === "failure" ? Promise.reject(new Error("stats unavailable")) : Promise.resolve(undefined); });
const olderRefresh = refresh.call(fakeThis); await refresh.call(fakeThis); first.resolve({ contextUsage: { tokens: 90, contextWindow: 100, percent: 90 } }); await olderRefresh;
expect(fakeThis.patched).toEqual([{ contextUsage: { tokens: 90, contextWindow: 100, percent: 90 } }]); }, );
test("harvestAssistantMessageUsage updates tokens and prompt cache", () => { type HarvestHarness = { connectionState?: { contextUsage?: unknown }; activityTracker?: { getStatus(): { tokens: number } }; contextUsageTokenBaseline?: number; getCurrentModel?: () => unknown; patchConnectionState(patch: Record<string, unknown>): void; harvestAssistantMessageUsage(message: unknown): void; }; const harvest = (InteractiveMode.prototype as unknown as HarvestHarness).harvestAssistantMessageUsage; const patched: Record<string, unknown>[] = []; const fakeThis: HarvestHarness = { connectionState: { contextUsage: { contextWindow: 100_000, tokens: 5_000, percent: 5 } }, activityTracker: { getStatus: () => ({ tokens: 100 }) }, contextUsageTokenBaseline: 0, getCurrentModel: () => ({ provider: "anthropic", id: "claude-3-5-sonnet", contextWindow: 100_000 }), patchConnectionState: (patch) => patched.push(patch), harvestAssistantMessageUsage: harvest, }; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype);
harvest.call(fakeThis, { role: "assistant", provider: "anthropic", model: "claude-3-5-sonnet", usage: { input: 20_000, cacheRead: 10_000, cacheWrite: 0, output: 200, }, });
expect(patched).toHaveLength(1); expect(patched[0]).toMatchObject({ contextUsage: { tokens: 30_200, percent: 30.2, promptCache: expect.objectContaining({ status: "warm", cachedTokens: 10_000, }), }, }); });});
describe("truncatePathMiddle", () => { test("does not duplicate the home prefix for short tilde paths", () => { const value = truncatePathMiddle("~/myproject", 8);
expect(value).toMatch(/^~\//); expect(value).not.toContain("/~/"); expect(visibleWidth(value)).toBeLessThanOrEqual(8); });});
describe("InteractiveMode.setToolsExpanded", () => { function createExpansionFakeThis(chatChildren: unknown[]): any { const fakeThis: any = { toolOutputExpanded: false, agentMessagesExpanded: false, editDiffsExpanded: false, pendingBashComponents: [], activeBashComponent: undefined, sideQuestionComponent: undefined, customHeader: undefined, builtInHeader: { setExpanded: vi.fn() }, chatContainer: { children: chatChildren }, ui: { requestRender: vi.fn(), requestRenderPreservingViewport: vi.fn(), isFullscreen: vi.fn().mockReturnValue(false), }, }; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); return fakeThis; }
test("applies expansion state to the active header and chat entries", () => { const chatChild = { setExpanded: vi.fn() }; const fakeThis = createExpansionFakeThis([chatChild]);
fakeThis.setToolsExpanded(true);
expect(fakeThis.toolOutputExpanded).toBe(true); expect(fakeThis.builtInHeader.setExpanded).toHaveBeenCalledWith(true); expect(chatChild.setExpanded).toHaveBeenCalledWith(true); // Expansion keeps the user anchored, so it uses the viewport-preserving path. expect(fakeThis.ui.requestRenderPreservingViewport).toHaveBeenCalledTimes(1); });
test("toggles agent messages separately from tools", () => { const toolChild = { setExpanded: vi.fn() }; const ipythonChild = { setExpanded: vi.fn(), setAgentMessagesExpanded: vi.fn(), setEditDiffsExpanded: vi.fn() }; const messageChild = new AgentMessageComponent({ role: "custom", customType: "agent_message", content: "Ping.", display: true, details: { id: "agentmsg_split", message: "Ping.", from: null }, timestamp: 123, } as any); const messageSetExpanded = vi.spyOn(messageChild, "setExpanded"); const fakeThis = createExpansionFakeThis([toolChild, ipythonChild, messageChild]);
fakeThis.toggleAgentMessageExpansion();
expect(fakeThis.agentMessagesExpanded).toBe(true); expect(fakeThis.toolOutputExpanded).toBe(false); expect(messageSetExpanded).toHaveBeenCalledWith(true); expect(toolChild.setExpanded).toHaveBeenCalledWith(false); expect(ipythonChild.setExpanded).toHaveBeenCalledWith(false); expect(ipythonChild.setAgentMessagesExpanded).toHaveBeenCalledWith(true);
fakeThis.setToolsExpanded(true);
expect(toolChild.setExpanded).toHaveBeenCalledWith(true); expect(messageSetExpanded).toHaveBeenLastCalledWith(true); expect(ipythonChild.setExpanded).toHaveBeenCalledWith(true); expect(ipythonChild.setAgentMessagesExpanded).toHaveBeenLastCalledWith(true); expect(fakeThis.agentMessagesExpanded).toBe(true); });
test("toggles edit diffs separately from tools and agent messages", () => { const child = { setExpanded: vi.fn(), setAgentMessagesExpanded: vi.fn(), setEditDiffsExpanded: vi.fn() }; const fakeThis = createExpansionFakeThis([child]);
fakeThis.toggleEditDiffExpansion();
expect(fakeThis.editDiffsExpanded).toBe(true); expect(fakeThis.toolOutputExpanded).toBe(false); expect(fakeThis.agentMessagesExpanded).toBe(false); expect(child.setEditDiffsExpanded).toHaveBeenCalledWith(true); expect(child.setExpanded).toHaveBeenCalledWith(false); expect(child.setAgentMessagesExpanded).toHaveBeenCalledWith(false);
fakeThis.setToolsExpanded(true);
expect(fakeThis.editDiffsExpanded).toBe(true); expect(child.setEditDiffsExpanded).toHaveBeenLastCalledWith(true); expect(child.setExpanded).toHaveBeenLastCalledWith(true); });});
describe("InteractiveMode.createExtensionUIContext setTheme", () => { test("persists theme changes to settings manager", () => { initTheme("dark");
let currentTheme = "dark"; const settingsManager = { getTheme: vi.fn(() => currentTheme), setTheme: vi.fn((theme: string) => { currentTheme = theme; }), }; const fakeThis: any = { session: { settingsManager }, settingsManager, ui: { requestRender: vi.fn() }, };
const uiContext = (InteractiveMode as any).prototype.createExtensionUIContext.call(fakeThis); const result = uiContext.setTheme("light");
expect(result.success).toBe(true); expect(settingsManager.setTheme).toHaveBeenCalledWith("light"); expect(currentTheme).toBe("light"); expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); });
test("does not persist invalid theme names", () => { initTheme("dark");
const settingsManager = { getTheme: vi.fn(() => "dark"), setTheme: vi.fn(), }; const fakeThis: any = { session: { settingsManager }, settingsManager, ui: { requestRender: vi.fn() }, };
const uiContext = (InteractiveMode as any).prototype.createExtensionUIContext.call(fakeThis); const result = uiContext.setTheme("__missing_theme__");
expect(result.success).toBe(false); expect(settingsManager.setTheme).not.toHaveBeenCalled(); expect(fakeThis.ui.requestRender).not.toHaveBeenCalled(); });});
describe("InteractiveMode.createExtensionUIContext addAutocompleteProvider", () => { test("stores wrapper factories and rebuilds autocomplete immediately", () => { const wrapper: AutocompleteProviderFactory = (current) => current; const fakeThis = { autocompleteProviderWrappers: [] as AutocompleteProviderFactory[], setupAutocompleteProvider: vi.fn(), };
const uiContext = (InteractiveMode as any).prototype.createExtensionUIContext.call(fakeThis); uiContext.addAutocompleteProvider(wrapper);
expect(fakeThis.autocompleteProviderWrappers).toEqual([wrapper]); expect(fakeThis.setupAutocompleteProvider).toHaveBeenCalledTimes(1); });});
describe("InteractiveMode.setupAutocompleteProvider", () => { test("stacks wrapper factories over a fresh base provider", () => { const defaultEditor = { setAutocompleteProvider: vi.fn() }; const customEditor = { setAutocompleteProvider: vi.fn() }; const calls: string[] = [];
const wrap1: AutocompleteProviderFactory = (current): AutocompleteProvider => ({ async getSuggestions(lines, cursorLine, cursorCol, options) { calls.push("getSuggestions:wrap1"); return current.getSuggestions(lines, cursorLine, cursorCol, options); }, applyCompletion(lines, cursorLine, cursorCol, item, prefix) { calls.push("applyCompletion:wrap1"); return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix); }, shouldTriggerFileCompletion(lines, cursorLine, cursorCol) { calls.push("shouldTrigger:wrap1"); return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true; }, }); const wrap2: AutocompleteProviderFactory = (current): AutocompleteProvider => ({ async getSuggestions(lines, cursorLine, cursorCol, options) { calls.push("getSuggestions:wrap2"); return current.getSuggestions(lines, cursorLine, cursorCol, options); }, applyCompletion(lines, cursorLine, cursorCol, item, prefix) { calls.push("applyCompletion:wrap2"); return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix); }, shouldTriggerFileCompletion(lines, cursorLine, cursorCol) { calls.push("shouldTrigger:wrap2"); return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true; }, });
const fakeThis = { createBaseAutocompleteProvider: () => new CombinedAutocompleteProvider([], "/tmp/project", undefined), defaultEditor, editor: customEditor, autocompleteProviderWrappers: [wrap1, wrap2], };
(InteractiveMode as any).prototype.setupAutocompleteProvider.call(fakeThis);
expect(defaultEditor.setAutocompleteProvider).toHaveBeenCalledTimes(1); expect(customEditor.setAutocompleteProvider).toHaveBeenCalledTimes(1); const provider = defaultEditor.setAutocompleteProvider.mock.calls[0]?.[0] as AutocompleteProvider; expect(provider).toBe(customEditor.setAutocompleteProvider.mock.calls[0]?.[0]); expect(provider.shouldTriggerFileCompletion?.(["foo"], 0, 3)).toBe(true); expect(calls).toEqual(["shouldTrigger:wrap2", "shouldTrigger:wrap1"]); });});
describe("InteractiveMode.showLoadedResources", () => { beforeAll(() => { initTheme("dark"); });
function createShowLoadedResourcesThis(options: { quietStartup: boolean; verbose?: boolean; toolOutputExpanded?: boolean; cwd?: string; contextFiles?: Array<{ path: string; content?: string }>; extensions?: ExtensionFixture[]; skills?: Array<{ filePath: string; name: string }>; skillDiagnostics?: AgentConnectionResourceDiagnostic[]; useRealScopeGroups?: boolean; useRealDiagnostics?: boolean; }) { const connectionResourceSnapshot: AgentConnectionResourceSnapshot = { contextFiles: (options.contextFiles ?? []).map((contextFile) => ({ path: contextFile.path })), skills: options.skills ?? [], prompts: [], extensions: options.extensions ?? [], themes: [], diagnostics: { skills: options.skillDiagnostics ?? [], prompts: [], extensions: [], themes: [], }, }; const extensionRunner = { getCommandDiagnostics: () => [], getShortcutDiagnostics: () => [], }; const fakeThis: any = { options: { verbose: options.verbose ?? false }, toolOutputExpanded: options.toolOutputExpanded ?? false, chatContainer: new Container(), settingsManager: { getQuietStartup: () => options.quietStartup, }, sessionManager: { getCwd: () => options.cwd ?? "/tmp/project", }, connectionResourceSnapshot, extensionRunner, formatDisplayPath: (p: string) => (InteractiveMode as any).prototype.formatDisplayPath.call(fakeThis, p), formatExtensionDisplayPath: (p: string) => (InteractiveMode as any).prototype.formatExtensionDisplayPath.call(fakeThis, p), formatContextPath: (p: string) => (InteractiveMode as any).prototype.formatContextPath.call(fakeThis, p), getCurrentCwd: () => options.cwd ?? "/tmp/project", getStartupExpansionState: () => (InteractiveMode as any).prototype.getStartupExpansionState.call(fakeThis), buildScopeGroups: () => [], formatScopeGroups: () => "resource-list", isPackageSource: (sourceInfo?: AgentConnectionSourceInfo) => (InteractiveMode as any).prototype.isPackageSource.call(fakeThis, sourceInfo), getShortPath: (p: string, sourceInfo?: AgentConnectionSourceInfo) => (InteractiveMode as any).prototype.getShortPath.call(fakeThis, p, sourceInfo), getCompactPathLabel: (p: string, sourceInfo?: AgentConnectionSourceInfo) => (InteractiveMode as any).prototype.getCompactPathLabel.call(fakeThis, p, sourceInfo), getCompactPackageSourceLabel: (sourceInfo?: AgentConnectionSourceInfo) => (InteractiveMode as any).prototype.getCompactPackageSourceLabel.call(fakeThis, sourceInfo), getCompactExtensionLabel: (p: string, sourceInfo?: AgentConnectionSourceInfo) => (InteractiveMode as any).prototype.getCompactExtensionLabel.call(fakeThis, p, sourceInfo), getCompactDisplayPathSegments: (p: string) => (InteractiveMode as any).prototype.getCompactDisplayPathSegments.call(fakeThis, p), getCompactNonPackageExtensionLabel: ( p: string, index: number, allPaths: Array<{ path: string; segments: string[] }>, ) => (InteractiveMode as any).prototype.getCompactNonPackageExtensionLabel.call(fakeThis, p, index, allPaths), getCompactExtensionLabels: (extensions: ExtensionFixture[]) => (InteractiveMode as any).prototype.getCompactExtensionLabels.call(fakeThis, extensions), formatDiagnostics: () => "diagnostics", getBuiltInCommandConflictDiagnostics: () => [], };
if (options.useRealDiagnostics) { fakeThis.formatDiagnostics = ( diagnostics: readonly AgentConnectionResourceDiagnostic[], sourceInfos: Map<string, AgentConnectionSourceInfo>, ) => (InteractiveMode as any).prototype.formatDiagnostics.call(fakeThis, diagnostics, sourceInfos); }
if (options.useRealScopeGroups) { fakeThis.getScopeGroup = (sourceInfo?: AgentConnectionSourceInfo) => (InteractiveMode as any).prototype.getScopeGroup.call(fakeThis, sourceInfo); fakeThis.buildScopeGroups = (items: Array<{ path: string; sourceInfo?: AgentConnectionSourceInfo }>) => (InteractiveMode as any).prototype.buildScopeGroups.call(fakeThis, items); fakeThis.formatScopeGroups = (groups: unknown, formatOptions: unknown) => (InteractiveMode as any).prototype.formatScopeGroups.call(fakeThis, groups, formatOptions); }
return fakeThis; }
function createSourceInfo( filePath: string, options: { source: string; scope: "user" | "project" | "temporary"; origin: "package" | "top-level"; baseDir?: string; }, ): AgentConnectionSourceInfo { return { path: filePath, source: options.source, scope: options.scope, origin: options.origin, baseDir: options.baseDir, }; }
function createExtensionFixtures(): ExtensionFixture[] { return [ { path: "/tmp/project/.pi/extensions/answer.ts", sourceInfo: createSourceInfo("/tmp/project/.pi/extensions/answer.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/project/.pi/extensions", }), }, { path: "/tmp/project/.pi/extensions/local-index/index.ts", sourceInfo: createSourceInfo("/tmp/project/.pi/extensions/local-index/index.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/project/.pi/extensions", }), }, { path: "/tmp/agent/extensions/user-index/index.ts", sourceInfo: createSourceInfo("/tmp/agent/extensions/user-index/index.ts", { source: "local", scope: "user", origin: "top-level", baseDir: "/tmp/agent/extensions", }), }, { path: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", { source: "npm:pi-markdown-preview", scope: "project", origin: "package", baseDir: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview", }), }, { path: "/tmp/project/.pi/npm/node_modules/@scope/pi-scoped/extensions/index.ts", sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/@scope/pi-scoped/extensions/index.ts", { source: "npm:@scope/pi-scoped", scope: "project", origin: "package", baseDir: "/tmp/project/.pi/npm/node_modules/@scope/pi-scoped", }), }, { path: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/index.ts", sourceInfo: createSourceInfo( "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/index.ts", { source: "git:github.com/HazAT/pi-interactive-subagents", scope: "project", origin: "package", baseDir: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents", }, ), }, { path: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/subagents/index.ts", sourceInfo: createSourceInfo( "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents/extensions/subagents/index.ts", { source: "git:github.com/HazAT/pi-interactive-subagents", scope: "project", origin: "package", baseDir: "/tmp/project/.pi/git/github.com/HazAT/pi-interactive-subagents", }, ), }, { path: "/tmp/temp/cli-extension.ts", sourceInfo: createSourceInfo("/tmp/temp/cli-extension.ts", { source: "cli", scope: "temporary", origin: "top-level", baseDir: "/tmp/temp", }), }, ]; }
test("does not show resource listing by default", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: false, });
expect(fakeThis.chatContainer.children).toHaveLength(0); });
test("shows a compact resource listing when forced", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
const output = renderAll(fakeThis.chatContainer); expect(output).toContain("[Skills]"); expect(output).toContain("commit"); expect(output).not.toContain("resource-list"); });
test("shows full resource listing when expanded", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, toolOutputExpanded: true, skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
const output = renderAll(fakeThis.chatContainer); expect(output).toContain("[Skills]"); expect(output).toContain("resource-list"); expect(output).not.toContain("commit"); });
test("shows full resource listing on verbose startup even when tool output is collapsed", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, verbose: true, toolOutputExpanded: false, skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: false, });
const output = renderAll(fakeThis.chatContainer); expect(output).toContain("[Skills]"); expect(output).toContain("resource-list"); expect(output).not.toContain("commit"); });
test("abbreviates extensions in compact listing", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions: [{ path: "/tmp/extensions/answer.ts" }, { path: "/tmp/extensions/btw.ts" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
const output = renderAll(fakeThis.chatContainer); expect(output).toContain("[Extensions]"); expect(output).toContain("answer.ts, btw.ts"); expect(output).not.toContain("extensions/answer.ts"); });
test("captures mixed extension layouts in compact output", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions: createExtensionFixtures(), useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`); });
test("adds more parent folders until local extension labels are unique", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/alpha/one/index.ts", sourceInfo: createSourceInfo("/tmp/alpha/one/index.ts", { source: "cli", scope: "temporary", origin: "top-level", baseDir: "/tmp/alpha", }), }, { path: "/tmp/beta/one/index.ts", sourceInfo: createSourceInfo("/tmp/beta/one/index.ts", { source: "cli", scope: "temporary", origin: "top-level", baseDir: "/tmp/beta", }), }, { path: "/tmp/gamma/one/index.ts", sourceInfo: createSourceInfo("/tmp/gamma/one/index.ts", { source: "cli", scope: "temporary", origin: "top-level", baseDir: "/tmp/gamma", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] alpha/one, beta/one, gamma/one"`); });
test("strips index.ts from local extension label, showing parent dir", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/extensions/plan-mode/index.ts", sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] plan-mode"`); });
test("strips index.js from local extension label, showing parent dir", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/extensions/plan-mode/index.js", sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.js", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] plan-mode"`); });
test("mixed single-file and subdirectory index.ts extensions strip index.ts", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/extensions/webfetch.ts", sourceInfo: createSourceInfo("/tmp/extensions/webfetch.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, { path: "/tmp/extensions/plan-mode/index.ts", sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] plan-mode, webfetch.ts"`); });
test("multiple index.ts with unique parent dirs need no disambiguation", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/extensions/foo/index.ts", sourceInfo: createSourceInfo("/tmp/extensions/foo/index.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, { path: "/tmp/extensions/bar/index.ts", sourceInfo: createSourceInfo("/tmp/extensions/bar/index.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] bar, foo"`); });
test("multiple index.ts with same parent dir name disambiguated with grandparent", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/alpha/tools/index.ts", sourceInfo: createSourceInfo("/tmp/alpha/tools/index.ts", { source: "cli", scope: "temporary", origin: "top-level", baseDir: "/tmp/alpha", }), }, { path: "/tmp/beta/tools/index.ts", sourceInfo: createSourceInfo("/tmp/beta/tools/index.ts", { source: "cli", scope: "temporary", origin: "top-level", baseDir: "/tmp/beta", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] alpha/tools, beta/tools"`); });
test("non-index file in subdirectory stays as filename", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/extensions/my-ext/main.ts", sourceInfo: createSourceInfo("/tmp/extensions/my-ext/main.ts", { source: "local", scope: "project", origin: "top-level", baseDir: "/tmp/extensions", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] main.ts"`); });
test("package extensions still strip index.ts correctly (regression guard)", () => { const extensions: ExtensionFixture[] = [ { path: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", { source: "npm:pi-markdown-preview", scope: "project", origin: "package", baseDir: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview", }), }, ];
const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, extensions, useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] pi-markdown-preview"`); }); test("captures mixed extension layouts in expanded output", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, toolOutputExpanded: true, extensions: createExtensionFixtures(), useRealScopeGroups: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(`"[Extensions] project /tmp/project/.pi/extensions/answer.ts /tmp/project/.pi/extensions/local-index git:github.com/HazAT/pi-interactive-subagents extensions extensions/subagents npm:@scope/pi-scoped extensions npm:pi-markdown-preview extensions user /tmp/agent/extensions/user-index path /tmp/temp/cli-extension.ts"`); });
test("shows context paths relative to cwd while preserving full external paths", () => { const home = homedir(); const cwd = path.join(home, "Development", "pi-mono"); const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, cwd, contextFiles: [{ path: path.join(home, ".pi", "agent", "AGENTS.md") }, { path: path.join(cwd, "AGENTS.md") }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); expect(output).toContain("[Context]"); expect(output).toContain("~/.pi/agent/AGENTS.md, AGENTS.md"); expect(output).not.toContain(`${cwd.replace(/\\/g, "/")}/AGENTS.md`); });
test("shows full context paths when expanded", () => { const home = homedir(); const cwd = path.join(home, "Development", "pi-mono"); const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, toolOutputExpanded: true, cwd, contextFiles: [{ path: path.join(home, ".pi", "agent", "AGENTS.md") }, { path: path.join(cwd, "AGENTS.md") }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: true, });
const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); expect(output).toContain("[Context]"); expect(output).toContain("~/.pi/agent/AGENTS.md"); expect(output).toContain("~/Development/pi-mono/AGENTS.md"); expect(output).not.toContain("~/.pi/agent/AGENTS.md, AGENTS.md"); });
test("does not show verbose listing on quiet startup during reload", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { extensions: [{ path: "/tmp/ext/index.ts" }], force: false, showDiagnosticsWhenQuiet: true, });
expect(fakeThis.chatContainer.children).toHaveLength(0); });
test("still shows diagnostics on quiet startup when requested", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], skillDiagnostics: [{ type: "warning", message: "duplicate skill name" }], });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: false, showDiagnosticsWhenQuiet: true, });
const output = renderAll(fakeThis.chatContainer); expect(output).toContain("[Skill warning]"); expect(output).not.toContain("[Skills]"); });
test("formats a multi-line skill warning without path noise", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, skillDiagnostics: [ { type: "warning", message: "first line of the warning.\nsecond line with guidance.\n\n indented detail", }, ], useRealDiagnostics: true, });
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { force: false, showDiagnosticsWhenQuiet: true, });
const output = normalizeRenderedOutput(fakeThis.chatContainer, 100); expect(output).toMatchInlineSnapshot(`"[Skill warning] first line of the warning. second line with guidance.
indented detail"`); expect(output).not.toContain("[Skill conflicts]"); });});
test("shows a refine loader from the /refine command message until its result row settles it", () => { initTheme("dark"); const statusContainer = new Container(); const fakeThis = { ui: { requestRender: vi.fn() } as unknown as TUI, statusContainer, refineLoader: undefined, stopWorkingLoader: vi.fn(), syncWorkingLoader: vi.fn(), } as unknown as InteractiveMode; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); const prototype = InteractiveMode.prototype as unknown as { startRefineLoader(this: InteractiveMode): void; stopRefineLoader(this: InteractiveMode): void; };
prototype.startRefineLoader.call(fakeThis); expect(statusContainer.children.length).toBe(1);
prototype.stopRefineLoader.call(fakeThis); expect(statusContainer.children.length).toBe(0); expect((fakeThis as unknown as { syncWorkingLoader: () => void }).syncWorkingLoader).toHaveBeenCalled();});
test("syncWorkingLoader remounts a refine loader that a compaction cleared", () => { initTheme("dark"); const statusContainer = new Container(); const fakeThis = { ui: { requestRender: vi.fn() } as unknown as TUI, statusContainer, refineLoader: undefined, autoCompactionLoader: undefined, retryLoader: undefined, stopWorkingLoader: vi.fn(), isAgentCompacting: () => false, } as unknown as InteractiveMode; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); const prototype = InteractiveMode.prototype as unknown as { startRefineLoader(this: InteractiveMode): void; syncWorkingLoader(this: InteractiveMode): void; };
prototype.startRefineLoader.call(fakeThis); statusContainer.clear(); expect(statusContainer.children.length).toBe(0);
prototype.syncWorkingLoader.call(fakeThis); expect(statusContainer.children.length).toBe(1);});
test("only the queued user /refine settlement stops its loader", async () => { initTheme("dark"); const statusContainer = new Container(); const fakeThis = { ui: { requestRender: vi.fn() } as unknown as TUI, statusContainer, refineLoader: undefined, stopWorkingLoader: vi.fn(), syncWorkingLoader: vi.fn(), addMessageToChat: vi.fn(), showError: vi.fn(), isInitialized: true, footer: { invalidate: vi.fn() }, updateConnectionStateFromEvent: vi.fn(), prepareFeatureHintRun: vi.fn(), activityTracker: { handleEvent: vi.fn(), reset: vi.fn() }, updateWorkingLoaderMessage: vi.fn(), renderRecap: vi.fn(), } as unknown as InteractiveMode; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); const prototype = InteractiveMode.prototype as unknown as { handleEvent(this: InteractiveMode, event: unknown): Promise<void>; }; const command = { name: "refine", args: "", text: "/refine" } as const;
await prototype.handleEvent.call(fakeThis, { type: "message_start", message: createSessionSlashCommandMessage(command), }); expect(statusContainer.children.length).toBe(1);
// The waited-on agent/auto refinement settles first; the user /refine is still running. await prototype.handleEvent.call(fakeThis, { type: "refine_complete", result: {} }); expect(statusContainer.children.length).toBe(1); await prototype.handleEvent.call(fakeThis, { type: "refine_failed", error: "background failure" }); expect(statusContainer.children.length).toBe(1);
await prototype.handleEvent.call(fakeThis, { type: "message_start", message: createSessionSlashCommandResultMessage( "Refined continual harness state: 0 edits applied.", { command, success: true, severity: "info" }, false, ), }); expect(statusContainer.children.length).toBe(0);});
test("session teardown removes a running refine loader without remounting anything", () => { initTheme("dark"); const statusContainer = new Container(); const fakeThis = { ui: { requestRender: vi.fn() } as unknown as TUI, statusContainer, refineLoader: undefined, stopWorkingLoader: vi.fn(), syncWorkingLoader: vi.fn(), } as unknown as InteractiveMode; Object.setPrototypeOf(fakeThis, InteractiveMode.prototype); const prototype = InteractiveMode.prototype as unknown as { startRefineLoader(this: InteractiveMode): void; discardRefineLoader(this: InteractiveMode): void; };
prototype.startRefineLoader.call(fakeThis); const loader = (fakeThis as unknown as { refineLoader?: { stop(): void } }).refineLoader; const stopSpy = vi.spyOn(loader!, "stop");
prototype.discardRefineLoader.call(fakeThis); expect(stopSpy).toHaveBeenCalled(); expect(statusContainer.children).toHaveLength(0); expect((fakeThis as unknown as { refineLoader?: unknown }).refineLoader).toBeUndefined(); expect((fakeThis as unknown as { syncWorkingLoader: () => void }).syncWorkingLoader).not.toHaveBeenCalled();});