Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/engine/run-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface RunRecord {
costTotal?: number;
/** SPEC-6-1: latest context tokens (calcContextTokens(usage)) — live snapshot. */
contextTokens?: number;
/** #117: the model's context window, resolved via getModelContextWindow at the tool wiring
* and persisted here so registry-backed previews (fleet preview row) render the ctx% segment
* identically to the transcript card. */
maxContext?: number;
/** #32: context-token snapshot at the end of turn 1 (the armory substrate baseline).
* Set once on the first assistant message_end; live-only (not journaled). The widget
* compares current contextTokens against this to label the tok/ctx% segment as
Expand Down
8 changes: 8 additions & 0 deletions src/panel/fleet-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,14 @@ export class FleetPanel extends Container {
if (sel) this.startRun(sel.value);
return;
}
// #115: the fleet footer promises r:Run-new — dispatch with the first registered agent
// (agents-view order). No agents → notify instead of a dead key.
if (matchesKey(data, "r") && this.view === "fleet") {
const first = [...this.deps.registry.values()][0];
if (!first) { this.onNotify("no agents registered — add one in the agents tab", "warning"); return; }
this.startRun(first.name);
return;
}
if (matchesKey(data, "i") && this.view === "agents") {
const sel = this.list.getSelectedItem();
if (sel) { this.infoAgent = this.deps.registry.get(sel.value) ?? null; this.renderShell(); }
Expand Down
7 changes: 3 additions & 4 deletions src/panel/present.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,9 @@ export function previewLine(selectedId: string | null | undefined, src: PreviewS
const rec = src.registry?.get(selectedId);
if (rec) {
if (rec.status !== "running") return "";
// maxContext rides as an override — cardSnapshot's copy list predates the ctx%
// segment, and RunRecord carries it as an optional runtime field (not yet declared).
const maxCtx = (rec as { maxContext?: number }).maxContext;
return stateLine(cardSnapshot(rec, { maxContext: maxCtx }), now, frame);
// maxContext rides as an override — cardSnapshot's copy list predates the ctx% segment;
// the field is declared on RunRecord since #117 (persisted by the tool wiring at emitCard).
return stateLine(cardSnapshot(rec, { maxContext: rec.maxContext }), now, frame);
}
for (const b of src.bgRuns?.values() ?? []) {
if (b.runId === selectedId) {
Expand Down
13 changes: 9 additions & 4 deletions src/panel/widget-rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,15 @@ export function widgetTotalsSegments(active: WidgetRun[], _now: number = Date.no
const cost = active.reduce((acc, r) => acc + (r.costTotal ?? 0), 0);
const tok = active.reduce((acc, r) => acc + (r.contextTokens ?? 0), 0);
const segs: Segment[] = [{ text: `${spin} `, status: "running" }];
if (running > 0) segs.push({ text: `${running} running`, token: "text" });
if (cost > 0) segs.push({ text: `$${cost.toFixed(2)}`, token: "text" });
if (tok > 0) segs.push({ text: `${fmtTokens(tok)} tok`, token: "text" });
return segs.length > 1 ? segs : [{ text: `${spin} `, status: "running" }, { text: `${active.length} active`, token: "text" }];
const values: Segment[] = [];
if (running > 0) values.push({ text: `${running} running`, token: "text" });
if (cost > 0) values.push({ text: `$${cost.toFixed(2)}`, token: "text" });
if (tok > 0) values.push({ text: `${fmtTokens(tok)} tok`, token: "text" });
if (values.length === 0) values.push({ text: `${active.length} active`, token: "text" });
// #116: the mockup joins value segments with muted ` · ` — the dd9a segment refactor dropped it
// (segments rendered glued: `⣾ 1 running$0.011727K tok`).
const sep: Segment = { text: " · ", token: "muted" };
return [segs[0]!, ...values.flatMap((v, i) => (i > 0 ? [sep, v] : [v]))];
}

/** Segment form of the widget lines (same shape/order as renderWidgetLines — see above). */
Expand Down
6 changes: 5 additions & 1 deletion src/tools/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,11 @@ export function createSubagentTool(deps: SubagentToolDeps) {
if (!rec) return;
const cardOverrides: Partial<RunCardState> = {};
const maxContext = deps.getModelContextWindow?.(rec.model);
if (maxContext !== undefined) cardOverrides.maxContext = maxContext;
if (maxContext !== undefined) {
cardOverrides.maxContext = maxContext;
// #117: persist to the record so the fleet preview mirrors the card's ctx% segment.
if (rec.maxContext !== maxContext) deps.runRegistry.update(rec.runId, { maxContext });
}
// pi's updateDisplay reads result.content unconditionally (image-block pass) — a partial
// MUST carry the result envelope shape: content array + details. The card rides in details.
onUpdate({ content: [] as Array<{ type: string; text?: string }>, details: { card: cardSnapshot(rec, cardOverrides) } });
Expand Down
22 changes: 22 additions & 0 deletions test/tool-onupdate.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,25 @@ test("#104 live path: cards stream DURING the run (TDZ regression — hoisted re
ok(Array.isArray(first.content), "partial carries a content array");
ok(first.details.card.runId.startsWith("fl-"), `card carries a real runId: ${first.details.card.runId}`);
});

test("#117: emitCard persists maxContext to the registry record (fleet preview ctx%)", async () => {
const runRegistry = new RunRegistry();
const cards: Array<{ content: unknown[]; details: { card: { runId: string } } }> = [];
const deps = {
registry: new Map([["g", CARD_AGENT]]),
runRegistry,
lock: createSingleSlotLock(),
todoSync: new ArmoryTodoAdapter(),
backendRegistry: regWith(cardFactory([{ type: "turn_start" }])),
parentModel: { provider: "p", id: "m" },
parentCwd: cardTmp,
defaultModelFallback: undefined,
getModelContextWindow: () => 256_000,
};
const tool = createSubagentTool(deps as any);
const res = await tool.execute("tc3", { agent: "g", task: "t" } as never, undefined as never, (p: unknown) => { cards.push(p as never); }, {});
assert.ok(!res.isError, `not an error: ${JSON.stringify(res).slice(0, 200)}`);
assert.ok(cards.length >= 1, "at least one live card");
const runId = cards[0]!.details.card.runId;
assert.equal(runRegistry.get(runId)?.maxContext, 256_000, "registry record carries the context window");
});
14 changes: 14 additions & 0 deletions test/widget-segments.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,17 @@ test("totals strip: present only when >1 active, glyph carries running, money/to
ok(two.some((s) => s.token === "text" && /^\$\d/.test(s.text)), "money segment");
ok(two.some((s) => s.token === "text" && s.text.includes("K tok")), "tok segment");
});

test("totals strip: value segments joined with muted · separators (#116)", () => {
const two = widgetTotalsSegments([
toWidgetRun(fg({ costTotal: 0.5, contextTokens: 1300 })),
toWidgetRunFromBg(bg({ status: "queued" })),
], 2000);
const values = two.slice(1).filter((s) => s.text !== " · "); // tail after the spinner segment
const seps = two.filter((s) => s.text === " · ");
strictEqual(seps.length, values.length - 1, "exactly n-1 separators between value segments");
ok(values.length >= 2, "fixture has ≥2 value segments");
ok(seps.every((s) => s.token === "muted"), "separators carry muted token");
strictEqual(two[two.length - 1]!.text !== " · ", true, "no trailing separator");
strictEqual(two[0]!.text !== " · ", true, "no leading separator");
});
Loading