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.
74 kB · 2068 lines
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { describe, expect, test, vi } from "vitest";import type { AgentSessionRuntimeConfig } from "../src/core/agent-session-config.js";import type { ModelRegistry } from "../src/core/model-registry.js";import type { SessionInfo } from "../src/core/session-manager.js";import type { SettingsManager } from "../src/core/settings-manager.js";import { createAgentsViewListCommand, createAgentsViewReplyHeadline, createAgentsViewResumeConfig, createInitialAgentsViewPersistentState, createInitialAgentsViewScopeFrames, createScopeBackReturnChatOpenResult, formatAgentsViewRelativeTime, formatAgentsViewStatusLine, getAgentsViewDepth, resolveAgentsViewActiveSummaryForPath, resolveAgentsViewOpenCwd, resolveAgentsViewSessionUiServices, shouldReconnectAgentsViewDaemon,} from "../src/modes/agents-view/agents-view-mode.js";import { summaryForUnifiedRecord } from "../src/modes/agents-view/agents-view-state.js";import { type AgentsViewScopeFrame, aggregateSessionHeartbeats, buildAgentsViewRows, buildUnifiedSessionIndex, classifyAgentsViewSession, computeRecursiveRollups, createUnattachableChildOpenResult, filterUnifiedSessions, formatHeartbeatBadge, getAgentsViewSelectionKey, getUnifiedSessionAncestorSessionIds, hasUnifiedSessionChildren, reconcileUnifiedSessions, resolveAgentsViewLeftResult, resolveAgentsViewScopeFrames, resolveAgentsViewSelectionIndex, resolveAgentsViewSelectionState, type SessionSummary, scopeToSessionSubtree, sectionTitle, shouldApplyScopeResolution, shouldShowAgentsViewSession, transitionAgentsViewScope,} from "../src/modes/index.js";import { formatAgentDepthLabel } from "../src/modes/interactive/interactive-mode.js";import type { InteractiveModeUiServices } from "../src/modes/interactive/interactive-mode-services.js";import type { Theme } from "../src/modes/interactive/theme/theme.js";
function heartbeat(id: string, nextRunAt?: string, activeSessionId = "child", status: "active" | "paused" = "active") { return { job: { id, status, activeSessionId, sessionId: `${activeSessionId}-session`, sessionFile: `/tmp/${activeSessionId}.jsonl`, cwd: "/tmp", prompt: "tick", schedule: { kind: "interval" as const, expression: "5m" }, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", ...(nextRunAt ? { nextRunAt } : {}), runCount: 0, }, };}
describe("agents view state", () => { test("classifies a saved orphan with rlmDepth > 0 as a subagent", () => { const saved = { path: "/tmp/sessions/child.jsonl", id: "child", cwd: "/tmp", rlmDepth: 2, created: new Date(0), modified: new Date(0), messageCount: 1, firstMessage: "", allMessagesText: "", }; const summary = summaryForUnifiedRecord({ saved, identity: "child", identityAliases: [], section: "archived", searchableText: "", } as never); // The parent edge can be reconciliation-dropped while the depth survives; // a depth > 0 session must never be presented as top-level. expect(summary.runtimeKind).toBe("subagent"); });
test("classifies sessions by live runtime status at any depth", () => { expect(classifyAgentsViewSession(makeSummary({ isStreaming: true, activity: "working" }))).toBe("running"); expect( classifyAgentsViewSession( makeSummary({ sessionActions: { queuedCount: 1, steering: [], followUps: [] }, activity: "working" }), ), ).toBe("running"); expect(classifyAgentsViewSession(makeSummary({ activity: "working" }))).toBe("running"); expect( classifyAgentsViewSession( makeSummary({ runtimeKind: "subagent", rlmDepth: 3, activity: "working", isSessionActive: true }), ), ).toBe("running"); expect(classifyAgentsViewSession(makeSummary({ activity: "idle", messageCount: 2 }))).toBe("idle"); expect(classifyAgentsViewSession(makeSummary({ runtimeKind: "subagent", activity: "idle" }))).toBe("idle"); expect( classifyAgentsViewSession( makeSummary({ activeSessionId: undefined, runtimeKind: "subagent", rlmDepth: 3, activity: "working", isSessionActive: true, hasActiveHeartbeat: true, }), ), ).toBe("inactive"); });
test("places all non-busy resident sessions in Idle", () => { // Working is heuristic and ignores taskState. expect( classifyAgentsViewSession(makeSummary({ isStreaming: true, activity: "working", taskState: "completed" })), ).toBe("running"); // Secondary completion verdicts do not create extra sections. expect(classifyAgentsViewSession(makeSummary({ activity: "idle", taskState: "needs_input" }))).toBe("idle"); expect(classifyAgentsViewSession(makeSummary({ activity: "idle", taskState: "completed" }))).toBe("idle"); expect(classifyAgentsViewSession(makeSummary({ activity: "idle", taskState: undefined }))).toBe("idle"); });
test("armed heartbeats stay Idle between firings; only real work is Running", () => { expect(classifyAgentsViewSession(makeSummary({ activity: "working", hasActiveHeartbeat: true }))).toBe("running"); expect( classifyAgentsViewSession(makeSummary({ activity: "idle", taskState: "completed", hasActiveHeartbeat: true })), ).toBe("idle");
const [row] = buildAgentsViewRows([makeSummary({ activity: "idle", hasActiveHeartbeat: true })]); expect(row).toMatchObject({ section: "idle", statusLabel: "heartbeat active" }); const [busyRow] = buildAgentsViewRows([ makeSummary({ activity: "working", hasActiveHeartbeat: true, isStreaming: true, isRunningTools: true }), ]); expect(busyRow).toMatchObject({ section: "running", statusLabel: "running tools" }); expect(sectionTitle("idle")).toBe("Idle"); });
test("labels replied subagents with active heartbeats as heartbeat active", () => { const summaries = [ makeSummary({ id: "replied-child", activeSessionId: "replied-child", sessionId: "replied-child-session", sessionName: "Replied child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", repliedSinceTask: true, hasActiveHeartbeat: true, activity: "idle", }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session" }), ]; const collapsed = buildAgentsViewRows(summaries); const expanded = buildAgentsViewRows(summaries, new Set([collapsed[0]?.identity ?? ""]));
expect(expanded.find((row) => row.title === "Replied child")).toMatchObject({ section: "idle", statusLabel: "heartbeat active", }); });
test("defaults an idle session with no verdict to needs-input", () => { // A slow, failed, or absent classification never lingers in Working; only // an explicit completed verdict moves an idle session out of needs-input. expect(classifyAgentsViewSession(makeSummary({ activity: "idle", taskState: undefined }))).toBe("idle"); });
test("sorts rows by section and creation time", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "completed", sessionId: "completed", sessionName: "completed", activity: "idle", taskState: "completed", messageCount: 2, created: "2026-01-03T00:00:00Z", }), makeSummary({ id: "older-working", sessionId: "older-working", sessionName: "older working", activity: "working", isStreaming: true, created: "2026-01-01T00:00:00Z", }), makeSummary({ id: "newer-working", sessionId: "newer-working", sessionName: "newer working", activity: "working", isStreaming: true, created: "2026-01-02T00:00:00Z", }), makeSummary({ sessionName: "heartbeat", activity: "idle", hasActiveHeartbeat: true, modified: "2026-01-03T00:00:00Z", }), ]);
expect(rows.map((row) => row.title)).toEqual(["newer working", "older working", "completed", "heartbeat"]); expect(rows.map((row) => row.section)).toEqual(["running", "running", "idle", "idle"]); });
test("keeps row order stable when activity and modification times and daemon input order change", () => { const older = makeSummary({ id: "older", sessionId: "older", sessionName: "older", activity: "working", created: "2026-01-01T00:00:00Z", modified: "2026-01-04T00:00:00Z", lastActivityAt: "2026-01-04T00:00:00Z", }); const newer = makeSummary({ id: "newer", sessionId: "newer", sessionName: "newer", activity: "working", created: "2026-01-02T00:00:00Z", modified: "2026-01-03T00:00:00Z", lastActivityAt: "2026-01-03T00:00:00Z", });
const initialOrder = buildAgentsViewRows([older, newer]).map((row) => row.summary.sessionId); const refreshedOrder = buildAgentsViewRows([ { ...newer, modified: "2026-01-05T00:00:00Z", lastActivityAt: "2026-01-05T00:00:00Z" }, { ...older, modified: "2026-01-06T00:00:00Z", lastActivityAt: "2026-01-06T00:00:00Z" }, ]).map((row) => row.summary.sessionId);
expect(initialOrder).toEqual(["newer", "older"]); expect(refreshedOrder).toEqual(initialOrder); });
test("uses deterministic fallbacks when creation times are unavailable", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "beta-2", sessionId: "beta-2", sessionName: "beta", modified: "2026-01-03T00:00:00Z" }), makeSummary({ id: "alpha", sessionId: "alpha", sessionName: "alpha", modified: "2026-01-02T00:00:00Z" }), makeSummary({ id: "beta-1", sessionId: "beta-1", sessionName: "beta", modified: "2026-01-01T00:00:00Z" }), ]);
expect(rows.map((row) => row.summary.sessionId)).toEqual(["alpha", "beta-1", "beta-2"]); });
test("sorts idle rows by last message activity, newest first", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "created-newest", sessionId: "created-newest", sessionName: "created newest", activity: "idle", created: "2026-01-03T00:00:00Z", lastActivityAt: "2026-01-01T00:00:00Z", }), makeSummary({ id: "middle", sessionId: "middle", sessionName: "middle", activity: "idle", created: "2026-01-02T00:00:00Z", lastActivityAt: "2026-01-02T00:00:00Z", }), makeSummary({ id: "active-newest", sessionId: "active-newest", sessionName: "active newest", activity: "idle", created: "2026-01-01T00:00:00Z", lastActivityAt: "2026-01-03T00:00:00Z", }), makeSummary({ id: "running-oldest", sessionId: "running-oldest", sessionName: "running oldest", activity: "working", isStreaming: true, created: "2025-12-31T00:00:00Z", lastActivityAt: "2025-12-31T00:00:00Z", }), ]);
expect(rows.map((row) => row.summary.sessionId)).toEqual([ "running-oldest", "active-newest", "middle", "created-newest", ]); expect(rows.map((row) => row.section)).toEqual(["running", "idle", "idle", "idle"]); });
test("armed heartbeats rank first within the inactive section", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "recent", activeSessionId: undefined, sessionId: "recent", sessionName: "recent", lastActivityAt: "2026-01-03T00:00:00Z", }), makeSummary({ id: "beating", activeSessionId: undefined, sessionId: "beating", sessionName: "beating", hasActiveHeartbeat: true, lastActivityAt: "2026-01-01T00:00:00Z", }), ]);
expect(rows.map((row) => row.section)).toEqual(["inactive", "inactive"]); expect(rows.map((row) => row.summary.sessionId)).toEqual(["beating", "recent"]); });
test("demotes empty sessions to the bottom of their section except the entered-from anchor", () => { const empty = makeSummary({ id: "empty", activeSessionId: undefined, sessionId: "empty", sessionName: "empty", messageCount: 0, lastActivityAt: "2026-01-04T00:00:00Z", }); const anchor = makeSummary({ id: "anchor", activeSessionId: undefined, sessionId: "anchor", sessionName: "anchor", messageCount: 0, lastActivityAt: "2026-01-03T00:00:00Z", }); const older = makeSummary({ id: "older", activeSessionId: undefined, sessionId: "older", sessionName: "older", messageCount: 3, lastActivityAt: "2026-01-02T00:00:00Z", });
// The entered-from session keeps its recency slot even while empty. const anchored = buildAgentsViewRows( [empty, anchor, older], new Set(), new Set(), undefined, undefined, "anchor", ); expect(anchored.map((row) => row.summary.sessionId)).toEqual(["anchor", "older", "empty"]);
// Entered from elsewhere, every empty session sinks below non-empty ones. const unanchored = buildAgentsViewRows([empty, anchor, older]); expect(unanchored.map((row) => row.summary.sessionId)).toEqual(["older", "empty", "anchor"]); });
test("summarizes subagents on their parent and omits subagent rows", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", parentSessionId: "parent-session", isStreaming: true, activity: "working", }), makeSummary({ id: "second-child-active", activeSessionId: "second-child-active", sessionId: "second-child-session", sessionName: "Second child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", parentSessionId: "parent-session", hasActiveHeartbeat: true, activity: "working", }), makeSummary({ id: "completed-child-active", activeSessionId: "completed-child-active", sessionId: "completed-child-session", sessionName: "Completed child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", parentSessionId: "parent-session", activity: "idle", messageCount: 2, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", isStreaming: true, activity: "working", }), makeSummary({ id: "other-active", activeSessionId: "other-active", sessionId: "other-session", sessionName: "Other", activity: "idle", taskState: "completed", messageCount: 2, }), ]);
expect(rows.map((row) => [row.title, row.kind])).toEqual([ ["Parent", "agent"], ["2 subagents running · 1 heartbeat active", "subagent-summary"], ["Other", "agent"], ]); expect(rows.map((row) => row.runningSubagentCount)).toEqual([2, 2, 0]); expect(rows.map((row) => row.depth)).toEqual([0, 1, 0]); expect(rows.map((row) => row.selectable)).toEqual([true, true, true]); expect(rows[1]?.parentIdentity).toBe(rows[0]?.identity); expect(rows[1]?.identity).not.toBe(rows[0]?.identity); });
test("surfaces an idle retained subagent heartbeat while its parent is collapsed", () => { const summaries = [ makeSummary({ id: "heartbeat-child", activeSessionId: "heartbeat-child", sessionId: "heartbeat-child-session", sessionName: "Heartbeat child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", hasActiveHeartbeat: true, activity: "idle", taskState: "completed", messageCount: 2, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", activity: "idle", taskState: "completed", messageCount: 2, }), ];
const collapsed = buildAgentsViewRows(summaries); expect(collapsed[0]).toMatchObject({ kind: "agent", section: "idle", statusLabel: "completed", }); expect(collapsed[1]).toMatchObject({ kind: "subagent-summary", section: "idle", title: "1 subagent · 1 heartbeat active", runningSubagentCount: 0, }); const expanded = buildAgentsViewRows(summaries, new Set([collapsed[0]?.identity ?? ""])); expect(expanded[1]).toMatchObject({ kind: "subagent-summary", expanded: true }); expect(expanded[2]).toMatchObject({ kind: "subagent", section: "idle", statusLabel: "heartbeat active", }); });
test("counts a busy grandchild on every idle ancestor without promoting them", () => { const summaries = [ makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", activity: "idle", taskState: "completed", hasRunningRlmChildren: true, messageCount: 2, }), makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", activity: "idle", taskState: "completed", }), makeSummary({ id: "grandchild-active", activeSessionId: "grandchild-active", sessionId: "grandchild-session", sessionName: "Grandchild", runtimeKind: "subagent", parentActiveSessionId: "child-active", activity: "working", isSessionActive: true, isStreaming: true, }), ];
const collapsed = buildAgentsViewRows(summaries); expect(collapsed[0]).toMatchObject({ kind: "agent", section: "idle", runningSubagentCount: 1 }); expect(collapsed[0]?.statusLabel).toBe("completed"); expect(collapsed[1]).toMatchObject({ kind: "subagent-summary", section: "idle", title: "1 subagent running" });
const expanded = buildAgentsViewRows(summaries, new Set([collapsed[0]?.identity ?? ""])); const childRow = expanded.find((row) => row.title === "Child"); expect(childRow).toMatchObject({ kind: "subagent", section: "idle", runningSubagentCount: 1 }); });
test("keeps heartbeat-armed descendants out of the busy tally", () => { const summaries = [ makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", activity: "idle", taskState: "completed", messageCount: 2, }), makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", activity: "idle", taskState: "completed", }), makeSummary({ id: "grandchild-active", activeSessionId: "grandchild-active", sessionId: "grandchild-session", sessionName: "Heartbeat grandchild", runtimeKind: "subagent", parentActiveSessionId: "child-active", hasActiveHeartbeat: true, activity: "idle", }), ];
const collapsed = buildAgentsViewRows(summaries); expect(collapsed[0]?.runningSubagentCount).toBe(0); expect(collapsed[1]).toMatchObject({ kind: "subagent-summary", section: "idle", title: "1 subagent" }); const expanded = buildAgentsViewRows(summaries, new Set([collapsed[0]?.identity ?? ""])); expect(expanded.find((row) => row.title === "Child")?.runningSubagentCount).toBe(0); });
test("keeps the recursive total complete when search filters out a descendant", () => { const parent = makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Searchable parent", usage: { inputTokens: 100, outputTokens: 10, cost: 0.42 }, }); const child = makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "unrelated worker", runtimeKind: "subagent", parentActiveSessionId: "parent-active", usage: { inputTokens: 50, outputTokens: 5, cost: 0.68 }, }); const grandchild = makeSummary({ id: "grandchild-active", activeSessionId: "grandchild-active", sessionId: "grandchild-session", sessionName: "unrelated nested worker", runtimeKind: "subagent", parentActiveSessionId: "child-active", usage: { inputTokens: 20, outputTokens: 2, cost: 0.18 }, }); const records = reconcileUnifiedSessions([parent, child, grandchild], []); const rollups = computeRecursiveRollups(records); const filtered = filterUnifiedSessions(records, (text) => text.includes("Searchable"));
expect(filtered).toHaveLength(1); const rows = buildAgentsViewRows(filtered, new Set(), new Set(), undefined, rollups); expect(rows[0]?.summary.usage?.cost).toBe(0.42); expect(rows[0]?.recursiveCost).toBeCloseTo(1.28); expect(rows[0]?.descendantCount).toBe(2); });
test("keeps a parent's recursive total when a passivated child survives only as a catalog row", () => { const parent = makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionFile: "/tmp/project/parent.jsonl", usage: { inputTokens: 100, outputTokens: 10, cost: 0.42 }, }); const liveChild = makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionFile: "/tmp/project/child.jsonl", runtimeKind: "subagent", parentActiveSessionId: "parent-active", usage: { inputTokens: 50, outputTokens: 5, cost: 0.68 }, }); const before = reconcileUnifiedSessions([parent, liveChild], []); const beforeRollup = computeRecursiveRollups(before).get(before[0]!);
// After a restart the child exists only as a saved-catalog row. const after = reconcileUnifiedSessions( [parent], [ makeSessionInfo({ path: "/tmp/project/child.jsonl", id: "child-session", parentSessionPath: "/tmp/project/parent.jsonl", rlmDepth: 1, usage: { inputTokens: 50, outputTokens: 5, cost: 0.68 }, }), ], ); const afterRollup = computeRecursiveRollups(after).get(after[0]!);
expect(beforeRollup?.cost).toBeCloseTo(1.1); expect(afterRollup).toEqual(beforeRollup); expect(afterRollup?.descendantCount).toBe(1); });
test("rolls up spawned subagents but never a branched session's copied lineage", () => { const source = makeSummary({ id: "src", activeSessionId: "src", sessionId: "src-session", sessionFile: "/tmp/project/src.jsonl", usage: { inputTokens: 100, outputTokens: 10, cost: 0.4 }, }); const branch = makeSessionInfo({ path: "/tmp/project/branch.jsonl", id: "branch-session", parentSessionPath: "/tmp/project/src.jsonl", // Branch/fork headers keep the source's depth; only spawns go deeper. rlmDepth: 0, usage: { inputTokens: 100, outputTokens: 10, cost: 0.4 }, }); const child = makeSessionInfo({ path: "/tmp/project/child.jsonl", id: "child-session", parentSessionPath: "/tmp/project/src.jsonl", rlmDepth: 1, usage: { inputTokens: 20, outputTokens: 2, cost: 0.1 }, });
const branchOnly = reconcileUnifiedSessions([source], [branch]); expect(computeRecursiveRollups(branchOnly).get(branchOnly[0]!)).toEqual({ cost: 0.4, descendantCount: 0 });
const withChild = reconcileUnifiedSessions([source], [branch, child]); const rollup = computeRecursiveRollups(withChild).get(withChild[0]!); expect(rollup?.descendantCount).toBe(1); expect(rollup?.cost).toBeCloseTo(0.5);
// The tree shares that definition of "child": the branch renders as its // own top-level session while only the spawned child nests and counts. const rows = buildAgentsViewRows(withChild, new Set(), new Set(), undefined, computeRecursiveRollups(withChild)); expect(rows.find((row) => row.summary.sessionId === "branch-session")).toMatchObject({ kind: "agent", depth: 0 }); expect(rows.find((row) => row.kind === "subagent-summary")).toMatchObject({ title: "1 subagent" }); });
test("tallies a very deep child chain without overflowing the stack", () => { const summaries = [ makeSummary({ id: "chain-root", activeSessionId: "chain-root", sessionId: "chain-root-session", sessionName: "Chain root", activity: "idle", taskState: "completed", messageCount: 2, }), ]; const depth = 10_000; for (let level = 1; level <= depth; level++) { summaries.push( makeSummary({ id: `chain-${level}`, activeSessionId: `chain-${level}`, sessionId: `chain-${level}-session`, sessionName: `Chain ${level}`, runtimeKind: "subagent", parentActiveSessionId: level === 1 ? "chain-root" : `chain-${level - 1}`, ...(level === depth ? { activity: "working" as const, isSessionActive: true, isStreaming: true, usage: { inputTokens: 100, outputTokens: 10, cost: 0.5 }, } : { activity: "idle" as const, taskState: "completed" as const }), }), ); }
const rows = buildAgentsViewRows(summaries); expect(rows[0]).toMatchObject({ kind: "agent", section: "idle", runningSubagentCount: 1 }); expect(rows[0]?.recursiveCost).toBeCloseTo(0.5); });
test("ranks idle rows with busy descendants above plain idle rows", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "plain-idle", activeSessionId: "plain-idle", sessionId: "plain-session", sessionName: "Plain idle", activity: "idle", taskState: "completed", messageCount: 2, lastActivityAt: "2026-09-02T00:00:00Z", }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Busy-subtree parent", activity: "idle", taskState: "completed", hasRunningRlmChildren: true, messageCount: 2, lastActivityAt: "2026-08-01T00:00:00Z", }), makeSummary({ id: "busy-child", activeSessionId: "busy-child", sessionId: "busy-child-session", sessionName: "Busy child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", activity: "working", isSessionActive: true, isStreaming: true, }), ]);
expect(rows.filter((row) => row.kind === "agent").map((row) => [row.title, row.section])).toEqual([ ["Busy-subtree parent", "idle"], ["Plain idle", "idle"], ]); });
test("keeps idle heartbeating subagents out of the running count", () => { const heartbeatChildren = Array.from({ length: 10 }, (_, index) => makeSummary({ id: `heartbeat-child-${index}`, activeSessionId: `heartbeat-child-${index}`, sessionId: `heartbeat-child-session-${index}`, sessionName: `Heartbeat child ${index}`, runtimeKind: "subagent", parentActiveSessionId: "parent-active", hasActiveHeartbeat: true, activity: "idle", taskState: "completed", }), ); const rows = buildAgentsViewRows([ ...heartbeatChildren, makeSummary({ id: "busy-child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", activity: "idle", isSessionActive: true, isRunningTools: true, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", activity: "idle", taskState: "completed", }), ]);
expect(rows[0]).toMatchObject({ section: "idle", runningSubagentCount: 1 }); expect(rows[1]).toMatchObject({ kind: "subagent-summary", title: "1 subagent running · 10 heartbeats active", runningSubagentCount: 1, }); });
test("leaves completed ancestors idle while a descendant heartbeat is armed", () => { const summaries = [ makeSummary({ id: "heartbeat-grandchild", activeSessionId: "heartbeat-grandchild", sessionId: "heartbeat-grandchild-session", sessionName: "Heartbeat grandchild", runtimeKind: "subagent", parentActiveSessionId: "child-active", hasActiveHeartbeat: true, activity: "idle", taskState: "completed", }), makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "root-active", activity: "idle", taskState: "completed", }), makeSummary({ id: "root-active", activeSessionId: "root-active", sessionId: "root-session", sessionName: "Root", activity: "idle", taskState: "completed", }), ];
const rootIdentity = buildAgentsViewRows(summaries)[0]?.identity ?? ""; const oneLevel = buildAgentsViewRows(summaries, new Set([rootIdentity])); const child = oneLevel.find((row) => row.title === "Child"); expect(oneLevel[0]).toMatchObject({ section: "idle", statusLabel: "completed" }); expect(child).toMatchObject({ section: "idle", statusLabel: "completed" });
const childIdentity = child?.identity ?? ""; const expanded = buildAgentsViewRows(summaries, new Set([rootIdentity, childIdentity])); expect( expanded .filter((row) => row.kind === "agent" || row.kind === "subagent") .map((row) => [row.title, row.section, row.statusLabel]), ).toEqual([ ["Root", "idle", "completed"], ["Child", "idle", "completed"], ["Heartbeat grandchild", "idle", "heartbeat active"], ]); });
test("expands subagent rows for expanded parents and collapses otherwise", () => { const summaries = [ makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionFile: "/tmp/child.jsonl", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", isStreaming: true, activity: "working", }), makeSummary({ id: "completed-child-active", activeSessionId: "completed-child-active", sessionId: "completed-child-session", sessionFile: "/tmp/completed-child.jsonl", sessionName: "Completed child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", activity: "idle", taskState: "completed", messageCount: 2, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionFile: "/tmp/parent.jsonl", sessionName: "Parent", isStreaming: true, activity: "working", }), ];
const collapsed = buildAgentsViewRows(summaries); expect(collapsed.map((row) => row.kind)).toEqual(["agent", "subagent-summary"]); expect(collapsed[1]?.title).toBe("1 subagent running"); expect(collapsed[1]?.expanded).toBe(false);
const parentIdentity = collapsed[0]?.identity; const expanded = buildAgentsViewRows(summaries, new Set([parentIdentity ?? ""])); expect(expanded.map((row) => [row.title, row.kind, row.depth])).toEqual([ ["Parent", "agent", 0], ["1 subagent running", "subagent-summary", 1], ["Child", "subagent", 1], ["Completed child", "subagent", 1], ]); expect(expanded[1]?.expanded).toBe(true); expect(expanded.slice(1).every((row) => row.selectable && row.parentIdentity === parentIdentity)).toBe(true); });
test("reveals a nested subagent only after its parent is also expanded", () => { const summaries = [ makeSummary({ id: "grandchild-active", activeSessionId: "grandchild-active", sessionId: "grandchild-session", sessionName: "Grandchild", runtimeKind: "subagent", parentActiveSessionId: "child-active", parentSessionId: "child-session", isStreaming: true, activity: "working", }), makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "root-active", parentSessionId: "root-session", isStreaming: true, activity: "working", }), makeSummary({ id: "root-active", activeSessionId: "root-active", sessionId: "root-session", sessionName: "Root", isStreaming: true, activity: "working", }), ];
const rootIdentity = buildAgentsViewRows(summaries)[0]?.identity ?? ""; // Expanding only the root reveals the child but not the grandchild. const oneLevel = buildAgentsViewRows(summaries, new Set([rootIdentity])); expect(oneLevel.map((row) => [row.title, row.kind])).toEqual([ ["Root", "agent"], ["2 subagents running", "subagent-summary"], ["Child", "subagent"], ["1 subagent running", "subagent-summary"], ]);
const childIdentity = oneLevel.find((row) => row.title === "Child")?.identity ?? ""; const twoLevel = buildAgentsViewRows(summaries, new Set([rootIdentity, childIdentity])); expect(twoLevel.map((row) => [row.title, row.kind, row.depth])).toEqual([ ["Root", "agent", 0], ["2 subagents running", "subagent-summary", 1], ["Child", "subagent", 1], ["1 subagent running", "subagent-summary", 2], ["Grandchild", "subagent", 2], ]); });
test("keeps finished subagents reachable via the summary row", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "done-child", activeSessionId: "done-child", sessionId: "done-child-session", sessionFile: "/tmp/done-child.jsonl", sessionName: "Done child", runtimeKind: "subagent", parentActiveSessionId: "parent-active", activity: "idle", messageCount: 2, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionFile: "/tmp/parent.jsonl", sessionName: "Parent", activity: "idle", messageCount: 4, }), ]);
expect(rows.map((row) => [row.title, row.kind])).toEqual([ ["Parent", "agent"], ["1 subagent", "subagent-summary"], ]); expect(rows[1]?.selectable).toBe(true); });
test("treats parent-linked summaries without runtimeKind as subagents and re-roots unresolved ones", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "legacy-child", activeSessionId: "legacy-child", sessionId: "legacy-child-session", sessionName: "Legacy child", parentActiveSessionId: "parent-active", isStreaming: true, activity: "working", }), makeSummary({ id: "legacy-rlm-child", activeSessionId: "legacy-rlm-child", sessionId: "legacy-rlm-session", sessionName: "Legacy rlm child", rlmChildId: "node-1", activity: "idle", messageCount: 2, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", isStreaming: true, activity: "working", }), ]);
expect(rows.map((row) => [row.title, row.kind])).toEqual([ ["Parent", "agent"], ["1 subagent running", "subagent-summary"], ["Legacy rlm child", "agent"], ]); expect(rows[0]?.runningSubagentCount).toBe(1); });
test("groups expanded subagents by spawn code and reveals the program", () => { const codeA = "task = sleep(60)\nfor i in range(2):\n run_subagent(i, task)"; const codeB = ["a = 1", "b = 2", "c = 3", "d = 4", "e = 5", "f = 6"].join("\n"); const summaries = [ makeSummary({ id: "a1", activeSessionId: "a1", sessionId: "a1-session", sessionName: "A1", runtimeKind: "subagent", parentActiveSessionId: "parent-active", spawnCode: codeA, modified: "2026-01-01T00:00:04Z", }), makeSummary({ id: "a2", activeSessionId: "a2", sessionId: "a2-session", sessionName: "A2", runtimeKind: "subagent", parentActiveSessionId: "parent-active", spawnCode: codeA, modified: "2026-01-01T00:00:03Z", }), makeSummary({ id: "b1", activeSessionId: "b1", sessionId: "b1-session", sessionName: "B1", runtimeKind: "subagent", parentActiveSessionId: "parent-active", spawnCode: codeB, modified: "2026-01-01T00:00:02Z", }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", isStreaming: true, activity: "working", }), ];
const collapsed = buildAgentsViewRows(summaries); expect(collapsed[1]?.kind).toBe("subagent-summary"); expect(collapsed[1]?.hasSpawnCode).toBe(true);
const parentIdentity = collapsed[0]?.identity ?? ""; const expandedNoProgram = buildAgentsViewRows(summaries, new Set([parentIdentity])); // Without the program toggle, only the summary and grouped subagent rows render. expect(expandedNoProgram.map((row) => row.kind)).toEqual([ "agent", "subagent-summary", "subagent", "subagent", "subagent", ]);
const expanded = buildAgentsViewRows(summaries, new Set([parentIdentity]), new Set([parentIdentity])); // Each spawn cell renders in full, once, directly above the subagents it // launched — padded with a blank panel line above and below, no truncation. expect(expanded.map((row) => [row.kind, row.code ?? row.title])).toEqual([ ["agent", "Parent"], ["subagent-summary", "3 subagents"], ["subagent-code", ""], ["subagent-code", "task = sleep(60)"], ["subagent-code", "for i in range(2):"], ["subagent-code", " run_subagent(i, task)"], ["subagent-code", ""], ["subagent", "A1"], ["subagent", "A2"], ["subagent-code", ""], ["subagent-code", "a = 1"], ["subagent-code", "b = 2"], ["subagent-code", "c = 3"], ["subagent-code", "d = 4"], ["subagent-code", "e = 5"], ["subagent-code", "f = 6"], ["subagent-code", ""], ["subagent", "B1"], ]); expect(expanded.every((row) => row.kind !== "subagent-code" || !row.selectable)).toBe(true); });
test("caps spawn code at 10 lines with a remainder note", () => { const longCode = Array.from({ length: 25 }, (_, i) => `line_${i}`).join("\n"); const summaries = [ makeSummary({ id: "c1", activeSessionId: "c1", sessionId: "c1-session", sessionName: "C1", runtimeKind: "subagent", parentActiveSessionId: "parent-active", spawnCode: longCode, }), makeSummary({ id: "parent-active", activeSessionId: "parent-active", sessionId: "parent-session", sessionName: "Parent", isStreaming: true, activity: "working", }), ]; const parentIdentity = buildAgentsViewRows(summaries)[0]?.identity ?? ""; const rows = buildAgentsViewRows(summaries, new Set([parentIdentity]), new Set([parentIdentity])); const codeLines = rows.filter((row) => row.kind === "subagent-code").map((row) => row.code); // 10 capped body lines + the "more" note + two blank pad lines. const bodyLines = codeLines.filter((line) => line !== ""); expect(bodyLines).toHaveLength(11); expect(bodyLines.slice(0, 10)).toEqual(Array.from({ length: 10 }, (_, i) => `line_${i}`)); expect(bodyLines[10]).toBe("… +15 more lines"); });
test("re-roots live subagents when their parent is not visible", () => { const rows = buildAgentsViewRows([ makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "removed-parent-active", parentSessionId: "removed-parent-session", isStreaming: true, activity: "working", }), makeSummary({ id: "other-active", activeSessionId: "other-active", sessionId: "other-session", sessionName: "Other", activity: "idle", messageCount: 2, }), ]);
expect(rows.map((row) => [row.title, row.kind, row.depth])).toEqual([ ["Child", "agent", 0], ["Other", "agent", 0], ]); });
test("shows daemon-resident sessions only", () => { const inactiveSleep = makeSummary({ lifecycle: "archived", activity: "idle" }); delete inactiveSleep.activeSessionId;
expect(shouldShowAgentsViewSession(inactiveSleep)).toBe(false); expect(shouldShowAgentsViewSession(makeSummary({ lifecycle: "live", activity: "idle" }))).toBe(true); expect(shouldShowAgentsViewSession(makeSummary({ lifecycle: "live", activity: "idle" }), true)).toBe(false); });
test("keeps sessions of non-ready workers visible and labels them with the worker state", () => { for (const workerState of ["starting", "recovering", "stopping", "failed"] as const) { const summary = makeSummary({ lifecycle: "live", workerState }); expect(shouldShowAgentsViewSession(summary), workerState).toBe(true); expect(buildAgentsViewRows([summary])[0]?.statusLabel, workerState).toBe(workerState); } expect(buildAgentsViewRows([makeSummary({ workerState: "ready" })])[0]?.statusLabel).toBe("needs input"); });
test("does not override saved session cwd when reopening inactive agents", () => { const config: AgentSessionRuntimeConfig = { cwd: "/tmp/dashboard", agentDir: "/tmp/agents", sessionDir: "/tmp/sessions", model: "openai/gpt-5", };
const resumeConfig = createAgentsViewResumeConfig(config);
expect("cwd" in resumeConfig).toBe(false); expect(resumeConfig.agentDir).toBe("/tmp/agents"); expect(resumeConfig.sessionDir).toBe("/tmp/sessions"); expect(resumeConfig.model).toBe("openai/gpt-5"); expect(config.cwd).toBe("/tmp/dashboard"); });
test("opens an existing-cwd session in its own directory with no override or notice", () => { const dir = mkdtempSync(join(tmpdir(), "agents-view-cwd-")); try { expect(resolveAgentsViewOpenCwd(makeSummary({ cwd: dir }), "/tmp/launch")).toEqual({}); } finally { rmSync(dir, { recursive: true, force: true }); } });
test("falls back to the launch cwd and explains it when the stored cwd is gone", () => { const missing = join(tmpdir(), "agents-view-missing-worktree-does-not-exist"); const { overrideCwd, notice } = resolveAgentsViewOpenCwd(makeSummary({ cwd: missing }), "/tmp/launch"); expect(overrideCwd).toBe("/tmp/launch"); expect(notice).toContain(missing); expect(notice).toContain("/tmp/launch"); });
test("does not override when there is no fallback cwd to use", () => { const missing = join(tmpdir(), "agents-view-missing-worktree-does-not-exist"); expect(resolveAgentsViewOpenCwd(makeSummary({ cwd: missing }), undefined)).toEqual({}); });
test("passes the override cwd through the resume config when the stored cwd is missing", () => { const config: AgentSessionRuntimeConfig = { cwd: "/tmp/launch", agentDir: "/tmp/agents" }; const resumeConfig = createAgentsViewResumeConfig(config, "/tmp/launch"); expect(resumeConfig.cwd).toBe("/tmp/launch"); });
test("requests only daemon-resident sessions for the agents view refresh", () => { expect(createAgentsViewListCommand()).toEqual({ type: "list" }); });
test("resolves active summaries by session file path", () => { const activeSummary = makeSummary({ id: "active-runtime", activeSessionId: "active-runtime", sessionId: "saved-active", sessionFile: "/tmp/sessions/active.jsonl", sessionName: "Running", }); const inactiveSummary = makeSummary({ id: "inactive", activeSessionId: undefined, sessionId: "inactive", sessionFile: "/tmp/sessions/inactive.jsonl", });
expect( resolveAgentsViewActiveSummaryForPath("/tmp/sessions/active.jsonl", [inactiveSummary, activeSummary]), ).toBe(activeSummary); expect(resolveAgentsViewActiveSummaryForPath("/tmp/sessions/inactive.jsonl", [inactiveSummary])).toBeUndefined(); });
test("derives the reply headline from the first line of the latest assistant text", () => { expect(createAgentsViewReplyHeadline(" Done.\nNext step? ")).toBe("Done."); expect(createAgentsViewReplyHeadline("\n\n spread over \nlines")).toBe("spread over"); expect(createAgentsViewReplyHeadline(" \n ")).toBeUndefined(); expect(createAgentsViewReplyHeadline(undefined)).toBeUndefined(); });
test("formats relative timestamps for the reply header", () => { const now = Date.parse("2026-01-02T12:00:00Z"); expect(formatAgentsViewRelativeTime("2026-01-02T11:59:30Z", now)).toBe("30s"); expect(formatAgentsViewRelativeTime("2026-01-02T11:15:00Z", now)).toBe("45m"); expect(formatAgentsViewRelativeTime("2026-01-02T06:00:00Z", now)).toBe("6h"); expect(formatAgentsViewRelativeTime("2025-12-30T12:00:00Z", now)).toBe("3d"); expect(formatAgentsViewRelativeTime(undefined, now)).toBe(""); expect(formatAgentsViewRelativeTime("not a timestamp", now)).toBe(""); });
test("flattens multiline status messages so they fit the single-row hint slot", () => { expect(formatAgentsViewStatusLine("Failed to send reply: connection lost\nat Socket.emit\nat process")).toBe( "Failed to send reply: connection lost at Socket.emit at process", ); expect(formatAgentsViewStatusLine('Failed:\r\n\t{\r\n\t"error": "boom"\r\n}')).toBe( 'Failed: { "error": "boom" }', ); expect(formatAgentsViewStatusLine(" already flat ")).toBe("already flat"); expect(formatAgentsViewStatusLine("\n \r\n ")).toBe(""); });
test("reconnects daemon restarts and crashes but stops after an intentional shutdown", () => { expect(shouldReconnectAgentsViewDaemon("update")).toBe(true); expect(shouldReconnectAgentsViewDaemon(undefined)).toBe(true); expect(shouldReconnectAgentsViewDaemon("shutdown")).toBe(false); });
test("uses session-specific UI services when opening an agent", async () => { const dashboardServices = makeUiServices("/tmp/dashboard"); const sessionServices = makeUiServices("/tmp/project"); const summary = makeSummary({ cwd: "/tmp/project", sessionFile: "/tmp/project/session.jsonl" }); const createUiServicesForSession = vi.fn(async () => sessionServices);
await expect( resolveAgentsViewSessionUiServices( { uiServices: dashboardServices, createUiServicesForSession, }, summary, ), ).resolves.toBe(sessionServices); expect(createUiServicesForSession).toHaveBeenCalledWith(summary); });
test("reconciles canonical paths and session ids while retaining saved search text", () => { const saved = makeSessionInfo({ path: "/tmp/sessions/../sessions/merged.jsonl", id: "merged-session", name: "Durable name", allMessagesText: "a uniquely searchable transcript", agentStatus: { summary: "Investigated the lunar regression", basedOnMessageCount: 1 }, }); const daemon = makeSummary({ id: "runtime", activeSessionId: "runtime", sessionId: "merged-session", sessionFile: "/tmp/sessions/merged.jsonl", sessionName: "Live name", });
const [record] = reconcileUnifiedSessions([daemon], [saved]); expect(record).toMatchObject({ daemon, saved, identity: "file:/tmp/sessions/merged.jsonl", section: "idle" }); expect(record?.searchableText).toContain("uniquely searchable transcript"); expect(record?.searchableText).toContain("lunar regression"); expect(buildAgentsViewRows([record!])[0]).toMatchObject({ title: "Live name", record, }); });
test("retains the ancestor chain when search matches only a nested subagent", () => { const summaries = [ makeSummary({ id: "root", activeSessionId: "root", sessionId: "root-session", sessionName: "Root" }), makeSummary({ id: "child", activeSessionId: "child", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "root", }), makeSummary({ id: "match", activeSessionId: "match", sessionId: "match-session", sessionName: "Needle", runtimeKind: "subagent", parentActiveSessionId: "child", }), makeSummary({ id: "newer", activeSessionId: "newer", sessionId: "newer-session", sessionName: "Needle peer", created: "2026-02-01T00:00:00Z", }), ]; const records = reconcileUnifiedSessions(summaries, []); const filtered = filterUnifiedSessions(records, (text) => text.includes("Needle"));
expect(filtered.map((record) => record.daemon?.sessionId)).toEqual([ "root-session", "child-session", "match-session", "newer-session", ]); const rootIdentity = records[0]?.identity ?? ""; const childIdentity = records[1]?.identity ?? ""; expect( buildAgentsViewRows(filtered, new Set([rootIdentity, childIdentity])).map((row) => [ row.summary.sessionId, row.depth, ]), ).toEqual([ ["newer-session", 0], ["root-session", 0], ["root-session", 1], ["child-session", 1], ["child-session", 2], ["match-session", 2], ]); });
test("deduplicates and protects sessions across symlink aliases", () => { const root = mkdtempSync(join(tmpdir(), "session-view-alias-")); try { const real = join(root, "session.jsonl"); const alias = join(root, "alias.jsonl"); writeFileSync(real, ""); symlinkSync(real, alias); const daemon = makeSummary({ activeSessionId: "active", sessionFile: alias, sessionId: "same" }); const saved = makeSessionInfo({ id: "same", path: real }); expect(reconcileUnifiedSessions([daemon], [saved])).toHaveLength(1); expect(resolveAgentsViewActiveSummaryForPath(real, [daemon])).toBe(daemon); } finally { rmSync(root, { recursive: true, force: true }); } });
test("uses canonical path and fallback active-id keys for heartbeat ancestry", () => { const root = mkdtempSync(join(tmpdir(), "session-view-parent-alias-")); try { const path = join(root, "parent.jsonl"); const alias = join(root, "alias.jsonl"); writeFileSync(path, ""); symlinkSync(path, alias); const summaries = [ makeSummary({ id: "root", activeSessionId: undefined, sessionFile: path }), makeSummary({ id: "parent", activeSessionId: undefined, parentSessionPath: alias, runtimeKind: "subagent", }), makeSummary({ id: "child", activeSessionId: undefined, parentActiveSessionId: "parent", runtimeKind: "subagent", }), ]; const aggregates = aggregateSessionHeartbeats(summaries, [heartbeat("job", undefined, "child")]); expect(["root", "parent", "child"].map((id) => aggregates.get(id))).toEqual( Array.from({ length: 3 }, () => ({ activeCount: 1 })), ); } finally { rmSync(root, { recursive: true, force: true }); } });
test("shows a fallback heartbeat count while catalog details are unavailable", () => { const [record] = reconcileUnifiedSessions([makeSummary({ hasActiveHeartbeat: true })], []); expect(record).toMatchObject({ section: "idle", heartbeat: { activeCount: 1 } }); expect(formatHeartbeatBadge(record?.heartbeat)).toBe("♥ 1"); });
test("preserves live identity and row state when saved metadata adds a file alias", () => { const saved = makeSessionInfo({ path: "/tmp/saved.jsonl", id: "saved", allMessagesText: "transcript" }); const parent = makeSummary({ id: "parent", activeSessionId: "parent", sessionId: "saved", sessionFile: undefined, }); const child = makeSummary({ id: "child", activeSessionId: "child", sessionId: "child-session", runtimeKind: "subagent", parentActiveSessionId: "parent", spawnCode: "run_subagent()", }); const [inactive] = reconcileUnifiedSessions([], [saved]); const [live] = reconcileUnifiedSessions([parent, child], []); const enrichedRecords = reconcileUnifiedSessions([parent, child], [saved]); const enriched = enrichedRecords.find((record) => record.daemon?.sessionId === parent.sessionId); const expanded = buildAgentsViewRows(enrichedRecords, new Set([live!.identity]), new Set([live!.identity]));
expect(inactive).toMatchObject({ identity: "file:/tmp/saved.jsonl", section: "inactive" }); expect(enriched).toMatchObject({ identity: live?.identity, section: "idle", saved }); expect(enriched?.identityAliases).toContain("file:/tmp/saved.jsonl"); expect(expanded.map((row) => row.kind)).toContain("subagent-code"); expect(expanded.some((row) => row.kind === "subagent" && row.summary.sessionId === "child-session")).toBe(true); });
test("aggregates active descendant heartbeats once and formats the soonest run", () => { const parent = makeSummary({ id: "parent", activeSessionId: "parent", sessionId: "parent-session" }); const child = makeSummary({ id: "child", activeSessionId: "child", sessionId: "child-session", runtimeKind: "subagent", parentActiveSessionId: "parent", });
const aggregates = aggregateSessionHeartbeats( [parent, child], [heartbeat("one", "2026-01-01T00:10:00Z"), heartbeat("two", "2026-01-01T00:05:00Z")], ); expect(aggregates.get("child")).toEqual({ activeCount: 2, nextRunAt: "2026-01-01T00:05:00Z" }); expect(aggregates.get("parent")).toEqual({ activeCount: 2, nextRunAt: "2026-01-01T00:05:00Z" }); const rows = buildAgentsViewRows(reconcileUnifiedSessions([parent, child], [], [heartbeat("one")])); expect(rows[0]).toMatchObject({ section: "idle", heartbeat: { activeCount: 1 } }); expect(rows[1]).toMatchObject({ title: "1 subagent · 1 heartbeat active" }); expect(formatHeartbeatBadge(aggregates.get("parent"), Date.parse("2026-01-01T00:00:00Z"))).toBe("♥ 2·5m"); expect( formatHeartbeatBadge( { activeCount: 1, nextRunAt: "2026-01-01T00:10:00Z" }, Date.parse("2026-01-01T00:00:00Z"), ), ).toBe("♥ 1·10m"); expect(formatHeartbeatBadge({ activeCount: 1 })).toBe("♥ 1"); // Sub-minute countdowns show seconds instead of rounding up to 1m. expect( formatHeartbeatBadge( { activeCount: 1, nextRunAt: "2026-01-01T00:00:42Z" }, Date.parse("2026-01-01T00:00:00Z"), ), ).toBe("♥ 1·42s"); expect( formatHeartbeatBadge( { activeCount: 1, nextRunAt: "2026-01-01T00:00:59.700Z" }, Date.parse("2026-01-01T00:00:00Z"), ), ).toBe("♥ 1·1m"); expect( formatHeartbeatBadge( { activeCount: 1, nextRunAt: "2026-01-01T00:00:00Z" }, Date.parse("2026-01-01T00:00:00Z"), ), ).toBe("♥ 1·1s");
const soonRows = buildAgentsViewRows( reconcileUnifiedSessions( [parent], [], [heartbeat("one", new Date(Date.now() + 5 * 60_000).toISOString(), "parent")], ), ); expect(soonRows[0]).toMatchObject({ section: "idle", statusLabel: "heartbeat · next 5m" }); });
test("counts paused heartbeats separately without affecting the section", () => { const parent = makeSummary({ id: "parent", activeSessionId: "parent", sessionId: "parent-session" }); const paused = heartbeat("paused-job", undefined, "parent", "paused"); const aggregates = aggregateSessionHeartbeats([parent], [paused]); expect(aggregates.get("parent")).toEqual({ activeCount: 0, pausedCount: 1 }); expect(formatHeartbeatBadge(aggregates.get("parent"))).toBe("♥ 1"); const [record] = reconcileUnifiedSessions([parent], [], [paused]); expect(record).toMatchObject({ section: "idle", heartbeat: { activeCount: 0, pausedCount: 1 } }); expect(record?.daemon?.hasActiveHeartbeat).toBeUndefined(); });
describe("restores selection to the previously open session", () => { const opened = makeSummary({ id: "active-open", activeSessionId: "active-open", sessionId: "session-open", sessionFile: "/tmp/project/open.jsonl", sessionName: "open", }); const other = makeSummary({ id: "active-other", activeSessionId: "active-other", sessionId: "session-other", sessionFile: "/tmp/project/other.jsonl", sessionName: "other", }); const identity = `file:${opened.sessionFile}`; const key = getAgentsViewSelectionKey(opened);
test("re-finds the session after a section change reorders the list", () => { const rows = buildAgentsViewRows([{ ...opened, activity: "working" }, other]); const openIndex = rows.findIndex((row) => row.summary.sessionId === "session-open"); expect(openIndex).toBe(0); expect(resolveAgentsViewSelectionIndex(rows, identity, key)).toBe(openIndex); });
test("falls back to activeSessionId when the row identity changed", () => { // Selected before the session had a file, so the stored identity is active:... const rows = buildAgentsViewRows([other, opened]); const staleIdentity = "active:active-open"; expect(resolveAgentsViewSelectionIndex(rows, staleIdentity, key)).toBe( rows.findIndex((row) => row.summary.sessionId === "session-open"), ); });
test("prefers the current runtime over a stale saved-file identity", () => { const switched = { ...opened, sessionId: "session-switched", sessionFile: "/tmp/project/switched.jsonl", }; const staleSaved = makeSummary({ id: "session-open", activeSessionId: undefined, sessionId: "session-open", sessionFile: opened.sessionFile, lifecycle: "archived", }); const rows = buildAgentsViewRows([staleSaved, switched]); expect(resolveAgentsViewSelectionIndex(rows, identity, key)).toBe( rows.findIndex((row) => row.summary.sessionId === "session-switched"), ); });
test("falls back to sessionId after a daemon re-attach regenerates the active id", () => { // Re-attach gives a fresh activeSessionId, so only the sessionId still matches. const reattached = { ...opened, id: "active-open-2", activeSessionId: "active-open-2" }; const rows = buildAgentsViewRows([other, reattached]); expect(resolveAgentsViewSelectionIndex(rows, identity, key)).toBe( rows.findIndex((row) => row.summary.sessionId === "session-open"), ); });
test("keeps an unresolved source anchor while streamed rows provide a visual fallback", () => { const partialRows = buildAgentsViewRows([other]); expect(resolveAgentsViewSelectionState(partialRows, 0, identity, key)).toEqual({ index: 0, resolved: false, }); const completeRows = buildAgentsViewRows([other, opened]); expect(resolveAgentsViewSelectionState(completeRows, 0, identity, key)).toEqual({ index: completeRows.findIndex((row) => row.summary.sessionId === "session-open"), resolved: true, }); });
test("keeps a collapsed subagent summary selected across refreshes", () => { const child = makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionFile: "/tmp/project/child.jsonl", runtimeKind: "subagent", parentActiveSessionId: opened.activeSessionId, parentSessionId: opened.sessionId, }); const initialRows = buildAgentsViewRows([opened, child]); const selected = initialRows.find((row) => row.kind === "subagent-summary")!; const refreshedRows = buildAgentsViewRows([{ ...opened }, { ...child }]); const refreshedSummaryIndex = refreshedRows.findIndex((row) => row.kind === "subagent-summary"); expect( resolveAgentsViewSelectionIndex( refreshedRows, selected.identity, getAgentsViewSelectionKey(selected.summary), ), ).toBe(refreshedSummaryIndex); });
test("returns -1 when the session is gone so callers pick a default", () => { const rows = buildAgentsViewRows([other]); expect(resolveAgentsViewSelectionIndex(rows, identity, key)).toBe(-1); });
test("returns -1 with no stored selection", () => { const rows = buildAgentsViewRows([opened, other]); expect(resolveAgentsViewSelectionIndex(rows, undefined, undefined)).toBe(-1); }); });
describe("scoped navigation", () => { const rootScope = { sessionId: "root-session", activeSessionId: "root-active" }; const childScope = { sessionId: "child-session", activeSessionId: "child-active" };
test("seeds handoff scope frames with the matching return chat", () => { const chat = makeSummary({ sessionId: "root-session", activeSessionId: "root-active" }); const persistentState = createInitialAgentsViewPersistentState({ initialScopeKey: rootScope, initialSession: chat, }); const handoffFrame = persistentState.scopeFrames?.at(-1);
// The scope root is never a row of its own children view: seeding it as // the selection anchor would only arm pending-anchor for the whole scan. expect(persistentState.selectedRowIdentity).toBeUndefined(); expect(persistentState.selectedSessionKey).toBeUndefined(); expect(persistentState.backSession).toBe(chat); const unscoped = createInitialAgentsViewPersistentState({ initialSession: chat }); expect(unscoped.selectedRowIdentity).toBeDefined();
expect(handoffFrame).toEqual({ scope: rootScope, returnChat: chat }); expect(createInitialAgentsViewScopeFrames(rootScope, persistentState.backSession)).toEqual([handoffFrame]); expect(createInitialAgentsViewScopeFrames(rootScope, makeSummary({ sessionId: "stale-session" }))).toEqual([ { scope: rootScope }, ]);
const leftResult = resolveAgentsViewLeftResult(chat, [], handoffFrame?.returnChat); expect(leftResult?.type).toBe("scope_back"); if (leftResult?.type !== "scope_back") throw new Error("Expected scoped Left navigation"); expect(createScopeBackReturnChatOpenResult({ ...leftResult, hasChildren: true })).toMatchObject({ type: "open", summary: { sessionId: "root-session", activeSessionId: "root-active" }, }); });
test("pushes and pops immutable scope frames with their return chats one level at a time", () => { const initial: AgentsViewScopeFrame[] = []; const rootChat = makeSummary({ sessionId: "root-session" }); const childChat = makeSummary({ sessionId: "child-session" }); const rootFrames = transitionAgentsViewScope(initial, { type: "push", scope: rootScope, returnChat: rootChat, }); const childFrames = transitionAgentsViewScope(rootFrames, { type: "push", scope: childScope, returnChat: childChat, });
expect(initial).toEqual([]); expect(childFrames.map((frame) => [frame.scope.sessionId, frame.returnChat?.sessionId])).toEqual([ ["root-session", "root-session"], ["child-session", "child-session"], ]); expect(JSON.parse(JSON.stringify(childFrames))).toEqual(childFrames); expect(transitionAgentsViewScope(childFrames, { type: "back" })).toEqual(rootFrames); expect(transitionAgentsViewScope(rootFrames, { type: "back" })).toEqual([]); });
test("refreshes the current frame instead of duplicating the same session", () => { const frames = transitionAgentsViewScope([{ scope: rootScope }], { type: "push", scope: { ...rootScope, activeSessionId: "root-active-2" }, }); expect(frames).toEqual([{ scope: { ...rootScope, activeSessionId: "root-active-2" } }]); });
test("Left is a no-op globally and returns through a surviving scoped chat", () => { const root = makeSummary({ sessionId: "root-session", activeSessionId: "root-active" }); const recordedChat = makeSummary({ sessionId: "root-session", activeSessionId: "stale-active" });
expect(resolveAgentsViewLeftResult(undefined)).toBeUndefined(); expect(resolveAgentsViewLeftResult(root, [], recordedChat)).toMatchObject({ type: "scope_back", selection: { sessionId: "root-session" }, returnChat: { sessionId: "root-session", activeSessionId: "root-active" }, }); });
test("Left falls back to the parent agents view when the recorded chat is gone", () => { const root = makeSummary({ sessionId: "replacement-session" }); const deletedChat = makeSummary({ sessionId: "deleted-session" });
expect(resolveAgentsViewLeftResult(root, ["parent-session"], deletedChat)).toEqual({ type: "scope_back", selection: root, expandedAncestorSessionIds: ["parent-session"], }); });
test("carries expansion and child metadata when scope-back reopens its return chat", () => { const root = makeSummary({ sessionId: "root-session", activeSessionId: "root-active" });
expect( createScopeBackReturnChatOpenResult({ type: "scope_back", selection: root, returnChat: root, expandedAncestorSessionIds: ["ancestor-session", "parent-session"], hasChildren: true, }), ).toEqual({ type: "open", summary: root, expandedAncestorSessionIds: ["ancestor-session", "parent-session"], hasChildren: true, }); });
test("falls back to the nearest surviving scope frame, then global", () => { const root = makeSummary({ id: "root-active", activeSessionId: "root-active", sessionId: "root-session", }); const frames = [{ scope: rootScope }, { scope: childScope }]; const records = reconcileUnifiedSessions([root], []);
expect(resolveAgentsViewScopeFrames(records, frames)).toMatchObject({ frames: [{ scope: rootScope }], root: { daemon: { sessionId: "root-session" } }, droppedFrames: 1, }); expect(resolveAgentsViewScopeFrames([], frames)).toEqual({ frames: [], droppedFrames: 2 }); });
test("settles vanished scopes only after the saved catalog attempt finishes", () => { expect(shouldApplyScopeResolution(0, false)).toBe(true); expect(shouldApplyScopeResolution(1, false)).toBe(false); expect(shouldApplyScopeResolution(1, true)).toBe(true); }); });
describe("scoped unified records", () => { test("buildAgentsViewRows excludes the scope root", () => { const root = makeSummary({ id: "root-active", activeSessionId: "root-active", sessionId: "root-session", sessionName: "Root", }); const scope = { sessionId: "root-session", activeSessionId: "root-active" };
expect(buildAgentsViewRows([root], new Set(), new Set(), scope)).toEqual([]); });
test("buildAgentsViewRows promotes direct scope children to root agent rows", () => { const root = makeSummary({ id: "root-active", activeSessionId: "root-active", sessionId: "root-session", sessionName: "Root", }); const child = makeSummary({ id: "child-active", activeSessionId: "child-active", sessionId: "child-session", sessionName: "Child", runtimeKind: "subagent", parentActiveSessionId: "root-active", parentSessionId: "root-session", }); const scope = { sessionId: "root-session", activeSessionId: "root-active" };
expect(buildAgentsViewRows([root, child], new Set(), new Set(), scope)).toMatchObject([ { kind: "agent", depth: 0, summary: { sessionId: "child-session" } }, ]); });
test("buildAgentsViewRows re-roots a saved child whose parent file is missing", () => { const [child] = reconcileUnifiedSessions( [], [ makeSessionInfo({ id: "saved-child", path: "/tmp/project/saved-child.jsonl", parentSessionPath: "/tmp/project/missing-parent.jsonl", modified: new Date("2026-01-04T00:00:00Z"), }), ], );
expect(buildAgentsViewRows([child!])).toMatchObject([ { kind: "agent", depth: 0, summary: { sessionId: "saved-child", lastActivityAt: "2026-01-04T00:00:00.000Z" }, }, ]); });
test("gets ancestor session ids from root to immediate parent", () => { const records = reconcileUnifiedSessions( [ makeSummary({ id: "root", activeSessionId: "root", sessionId: "root-session" }), makeSummary({ id: "child", activeSessionId: "child", sessionId: "child-session", runtimeKind: "subagent", parentActiveSessionId: "root", }), makeSummary({ id: "grandchild", activeSessionId: "grandchild", sessionId: "grandchild-session", runtimeKind: "subagent", parentActiveSessionId: "child", }), ], [], ); const index = buildUnifiedSessionIndex(records);
expect( getUnifiedSessionAncestorSessionIds( records, { sessionId: "grandchild-session", activeSessionId: "grandchild" }, index, ), ).toEqual(["root-session", "child-session"]); });
test("reports no children for leaf and unresolvable scopes", () => { const records = reconcileUnifiedSessions( [makeSummary({ id: "leaf", activeSessionId: "leaf", sessionId: "leaf-session" })], [], ); const index = buildUnifiedSessionIndex(records);
expect(hasUnifiedSessionChildren(records, { sessionId: "leaf-session", activeSessionId: "leaf" }, index)).toBe( false, ); expect(hasUnifiedSessionChildren(records, { sessionId: "missing" }, index)).toBe(false); expect(scopeToSessionSubtree(records, { sessionId: "missing" }, index)).toEqual([]); }); test("includes registry-backed and saved-only passive descendants", () => { const rootPath = "/tmp/project/root.jsonl"; const root = makeSummary({ id: "root-active", activeSessionId: "root-active", sessionId: "root-session", sessionFile: rootPath, rlmDepth: 0, }); const registryChild = makeSummary({ id: "registry-child", activeSessionId: undefined, sessionId: "registry-child", sessionFile: "/tmp/project/registry-child.jsonl", runtimeKind: "subagent", parentSessionId: "root-session", rlmDepth: 1, }); const records = reconcileUnifiedSessions( [root, registryChild], [ makeSessionInfo({ path: rootPath, id: "root-session", rlmDepth: 0 }), // The catalog also lists the resident child's file: it must merge, not duplicate. makeSessionInfo({ path: "/tmp/project/registry-child.jsonl", id: "registry-child", parentSessionPath: rootPath, rlmDepth: 1, }), makeSessionInfo({ path: "/tmp/project/saved-child.jsonl", id: "saved-child", parentSessionPath: rootPath, rlmDepth: 1, }), ], ); const scope = { sessionId: "root-session", activeSessionId: "root-active" }; const scoped = scopeToSessionSubtree(records, scope); const rows = buildAgentsViewRows(scoped, new Set(), new Set(), scope);
expect(hasUnifiedSessionChildren(records, scope)).toBe(true); expect(scoped.map((record) => record.daemon?.sessionId ?? record.saved?.id)).toEqual([ "root-session", "registry-child", "saved-child", ]); expect(rows).toHaveLength(2); expect(rows.every((row) => row.kind === "agent" && row.depth === 0 && row.section === "inactive")).toBe(true); expect(rows.find((row) => row.summary.sessionId === "registry-child")?.summary).toMatchObject({ parentSessionId: "root-session", rlmDepth: 1, }); expect(rows.find((row) => row.summary.sessionId === "saved-child")?.summary).toMatchObject({ parentSessionPath: rootPath, rlmDepth: 1, }); }); });
test("unattachable-child fallback preserves child selection and explains the parent open", () => { const child = makeSummary({ id: "child", sessionId: "child-session" }); const parent = makeSummary({ id: "parent", sessionId: "parent-session" }); const result = createUnattachableChildOpenResult(child, parent, ["root-session"], true);
expect(result).toMatchObject({ type: "open", summary: { sessionId: "parent-session" }, selection: { sessionId: "child-session" }, expandedAncestorSessionIds: ["root-session"], hasChildren: true, statusMessage: "Child session is unavailable; opened its parent instead", }); expect(result.selection).not.toBe(result.summary); });
describe("stored depth display", () => { test("labels the global agents view as depth zero and scoped views at their child level", () => { expect(getAgentsViewDepth(undefined)).toBe(0); expect(getAgentsViewDepth(makeSummary({ rlmDepth: 0 }))).toBe(1); expect(getAgentsViewDepth(makeSummary({ rlmDepth: 3 }))).toBe(4); });
test("never infers a chat depth when the persisted field is absent", () => { expect(formatAgentDepthLabel(undefined, true)).toBeUndefined(); expect(formatAgentDepthLabel(0, false)).toBeUndefined(); expect(formatAgentDepthLabel(0, true)).toBe("depth 0"); expect(formatAgentDepthLabel(3, false)).toBe("depth 3"); }); });});
describe("agents view pinned sessions", () => { test("marks rows pinned by session id", () => { const rows = buildAgentsViewRows( [makeSummary({ id: "a", sessionId: "session-a" }), makeSummary({ id: "b", sessionId: "session-b" })], new Set(), new Set(), undefined, new Set(["session-b"]), ); expect(rows.find((row) => row.summary.sessionId === "session-a")?.pinned).toBe(false); expect(rows.find((row) => row.summary.sessionId === "session-b")?.pinned).toBe(true); });
test("sorts pinned rows ahead of unpinned rows in the same section", () => { const summaries = [ makeSummary({ id: "old", sessionId: "session-old", lastActivityAt: "2026-01-01T00:00:00Z" }), makeSummary({ id: "new", sessionId: "session-new", lastActivityAt: "2026-01-02T00:00:00Z" }), makeSummary({ id: "mid", sessionId: "session-mid", lastActivityAt: "2026-01-01T12:00:00Z" }), ]; const ordered = buildAgentsViewRows(summaries, new Set(), new Set(), undefined, new Set(["session-old"])); expect(ordered.map((row) => row.summary.sessionId)).toEqual(["session-old", "session-new", "session-mid"]); });
test("pinned rows never cross section boundaries", () => { const summaries = [ makeSummary({ id: "idle", sessionId: "session-idle", activity: "idle", messageCount: 2 }), makeSummary({ id: "running", sessionId: "session-running", activity: "working", isStreaming: true }), ]; const ordered = buildAgentsViewRows(summaries, new Set(), new Set(), undefined, new Set(["session-idle"])); expect(ordered.map((row) => row.summary.sessionId)).toEqual(["session-running", "session-idle"]); });});
function makeSummary(overrides: Partial<SessionSummary>): SessionSummary { return { id: "active-1", activeSessionId: "active-1", lifecycle: "live", activity: "idle", isSessionActive: false, sessionId: "session-1", cwd: "/tmp/project", isStreaming: false, isCompacting: false, attachedClients: 0, messageCount: 1, sessionActions: { queuedCount: 0, steering: [], followUps: [] }, ...overrides, };}
function makeSessionInfo(overrides: Partial<SessionInfo> & { path: string; id: string }): SessionInfo { return { path: overrides.path, id: overrides.id, cwd: overrides.cwd ?? "/tmp/project", name: overrides.name, state: overrides.state, parentSessionPath: overrides.parentSessionPath, rlmDepth: overrides.rlmDepth ?? 0, created: overrides.created ?? new Date("2026-01-01T00:00:00Z"), modified: overrides.modified ?? new Date("2026-01-01T00:00:00Z"), messageCount: overrides.messageCount ?? 1, firstMessage: overrides.firstMessage ?? "hello", allMessagesText: overrides.allMessagesText ?? "hello", agentStatus: overrides.agentStatus, usage: overrides.usage, };}
function makeUiServices(cwd: string): InteractiveModeUiServices { return { settingsManager: {} as SettingsManager, modelRegistry: {} as ModelRegistry, getInitialCwd: () => cwd, getInitialSessionName: () => undefined, getThemes: (): Theme[] => [], };}