Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 17c4da9

Browse files
committed
Merge PR tintinweb#168: show effective agent runtime model
1 parent d96cc00 commit 17c4da9

10 files changed

Lines changed: 355 additions & 33 deletions

src/agent-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ export class AgentManager {
311311
},
312312
onSessionCreated: (session) => {
313313
record.session = session;
314+
record.invocation ??= {};
315+
record.invocation.modelName = session.model
316+
? `${session.model.provider}/${session.model.id}`
317+
: undefined;
318+
record.invocation.thinking = session.thinkingLevel;
314319
// Flush any steers that arrived before the session was ready
315320
if (record.pendingSteers?.length) {
316321
for (const msg of record.pendingSteers) {

src/index.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
formatTurns,
4747
getDisplayName,
4848
getPromptModeLabel,
49+
prepareModelNameForDisplay,
4950
SPINNER,
5051
type Theme,
5152
type UICtx,
@@ -179,6 +180,16 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number): stri
179180
].filter(Boolean).join('\n');
180181
}
181182

183+
/** Read current runtime model/thinking, falling back to the pre-session invocation snapshot. */
184+
function getRuntimeInvocation(record: Pick<AgentRecord, "invocation" | "session"> | undefined): AgentInvocation | undefined {
185+
if (!record?.session?.model) return record?.invocation;
186+
return {
187+
...record.invocation,
188+
modelName: `${record.session.model.provider}/${record.session.model.id}`,
189+
thinking: record.session.thinkingLevel,
190+
};
191+
}
192+
182193
/** Build AgentDetails from a base + record-specific fields. */
183194
function buildDetails(
184195
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
@@ -976,10 +987,11 @@ Terse command-style prompts produce shallow, generic work.
976987
return new Text(text, 0, 0);
977988
}
978989

979-
// Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
990+
// Helper: build "model · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
980991
const stats = (d: AgentDetails) => {
981992
const parts: string[] = [];
982-
if (d.modelName) parts.push(d.modelName);
993+
const modelName = prepareModelNameForDisplay(d.modelName);
994+
if (modelName) parts.push(modelName);
983995
if (d.tags) parts.push(...d.tags);
984996
if (d.turnCount != null && d.turnCount > 0) {
985997
parts.push(formatTurns(d.turnCount, d.maxTurns));
@@ -998,7 +1010,9 @@ Terse command-style prompts produce shallow, generic work.
9981010

9991011
// ---- Background agent launched ----
10001012
if (details.status === "background") {
1001-
return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
1013+
const s = stats(details);
1014+
const line = (s ? `${s}\n` : "") + theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`);
1015+
return new Text(line, 0, 0);
10021016
}
10031017

10041018
// ---- Completed / Steered ----
@@ -1134,11 +1148,7 @@ Terse command-style prompts produce shallow, generic work.
11341148
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
11351149
};
11361150

1137-
const parentModelId = ctx.model?.id;
1138-
const effectiveModelId = model?.id;
1139-
const modelName = effectiveModelId && effectiveModelId !== parentModelId
1140-
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
1141-
: undefined;
1151+
const modelName = model ? `${model.provider}/${model.id}` : undefined;
11421152
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
11431153
const agentInvocation: AgentInvocation = {
11441154
modelName,
@@ -1219,14 +1229,22 @@ Terse command-style prompts produce shallow, generic work.
12191229
if (!record) {
12201230
return textResult(`Failed to resume agent "${params.resume}".`);
12211231
}
1232+
const resumedInvocation = buildInvocationTags(getRuntimeInvocation(record));
1233+
const resumedDetails = {
1234+
displayName: getDisplayName(record.type),
1235+
description: record.description,
1236+
subagentType: record.type,
1237+
modelName: resumedInvocation.modelName,
1238+
tags: resumedInvocation.tags.length > 0 ? resumedInvocation.tags : undefined,
1239+
};
12221240
// A failed resume surfaces the error, plus any partial output THIS
12231241
// resume produced (never the previous turn's answer, #144).
12241242
if (record.status === "error") {
1225-
return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBase, record));
1243+
return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(resumedDetails, record));
12261244
}
12271245
return textResult(
12281246
record.result?.trim() || "No output.",
1229-
buildDetails(detailBase, record),
1247+
buildDetails(resumedDetails, record),
12301248
);
12311249
}
12321250

@@ -1301,6 +1319,12 @@ Terse command-style prompts produce shallow, generic work.
13011319
});
13021320

13031321
const isQueued = record?.status === "queued";
1322+
const runtimeInvocation = buildInvocationTags(getRuntimeInvocation(record));
1323+
const backgroundDetails = {
1324+
...detailBase,
1325+
modelName: runtimeInvocation.modelName ?? detailBase.modelName,
1326+
tags: runtimeInvocation.tags.length > 0 ? runtimeInvocation.tags : detailBase.tags,
1327+
};
13041328
return textResult(
13051329
`${fallbackNote}Agent ${isQueued ? "queued" : "started"} in background.\n` +
13061330
`Agent ID: ${id}\n` +
@@ -1311,7 +1335,7 @@ Terse command-style prompts produce shallow, generic work.
13111335
`\nYou will be notified when this agent completes.\n` +
13121336
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
13131337
`Do not duplicate this agent's work.`,
1314-
{ ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
1338+
{ ...backgroundDetails, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
13151339
);
13161340
}
13171341

@@ -1321,8 +1345,11 @@ Terse command-style prompts produce shallow, generic work.
13211345
let fgId: string | undefined;
13221346

13231347
const streamUpdate = () => {
1348+
const runtimeInvocation = buildInvocationTags(getRuntimeInvocation(fgId ? manager.getRecord(fgId) : undefined));
13241349
const details: AgentDetails = {
13251350
...detailBase,
1351+
modelName: runtimeInvocation.modelName ?? detailBase.modelName,
1352+
tags: runtimeInvocation.tags.length > 0 ? runtimeInvocation.tags : detailBase.tags,
13261353
toolUses: fgState.toolUses,
13271354
tokens: formatLifetimeTokens(fgState),
13281355
turnCount: fgState.turnCount,
@@ -1411,7 +1438,13 @@ Terse command-style prompts produce shallow, generic work.
14111438
// Get final token count
14121439
const tokenText = formatLifetimeTokens(fgState);
14131440

1414-
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
1441+
const runtimeInvocation = buildInvocationTags(getRuntimeInvocation(record));
1442+
const runtimeDetails = {
1443+
...detailBase,
1444+
modelName: runtimeInvocation.modelName ?? detailBase.modelName,
1445+
tags: runtimeInvocation.tags.length > 0 ? runtimeInvocation.tags : detailBase.tags,
1446+
};
1447+
const details = buildDetails(runtimeDetails, record, fgState, { tokens: tokenText });
14151448

14161449
if (record.status === "error") {
14171450
// Error headline + any partial output the run produced before failing.

src/schedule.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,14 @@ export class SubagentScheduler {
256256
isolated: job.isolated,
257257
thinkingLevel: job.thinking,
258258
isolation: job.isolation,
259+
invocation: {
260+
modelName: resolvedModel ? `${resolvedModel.provider}/${resolvedModel.id}` : undefined,
261+
thinking: job.thinking,
262+
maxTurns: job.max_turns,
263+
isolated: job.isolated,
264+
runInBackground: true,
265+
isolation: job.isolation,
266+
},
259267
});
260268
} catch (err) {
261269
const error = err instanceof Error ? err.message : String(err);

src/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,10 @@ export interface AgentRecord {
148148
}
149149

150150
export interface AgentInvocation {
151-
/** Short display name, e.g. "haiku" — only set when different from parent. */
151+
/** Canonical runtime model identifier (`provider/modelId`). */
152152
modelName?: string;
153-
thinking?: ThinkingLevel;
153+
/** Effective runtime thinking level after Pi applies defaults and model clamping. */
154+
thinking?: string;
154155
maxTurns?: number;
155156
isolated?: boolean;
156157
inheritContext?: boolean;

src/ui/agent-widget.ts

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Uses the callback form of setWidget for themed rendering.
66
*/
77

8+
import { stripVTControlCharacters } from "node:util";
89
import { truncateToWidth } from "@earendil-works/pi-tui";
910
import type { AgentManager } from "../agent-manager.js";
1011
import { getConfig } from "../agent-types.js";
@@ -76,7 +77,7 @@ export interface AgentDetails {
7677
activity?: string;
7778
/** Current spinner frame index (for animated running indicator). */
7879
spinnerFrame?: number;
79-
/** Short model name if different from parent (e.g. "haiku", "sonnet"). */
80+
/** Effective model display name used by this run. */
8081
modelName?: string;
8182
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
8283
tags?: string[];
@@ -160,6 +161,12 @@ export function getPromptModeLabel(type: SubagentType): string | undefined {
160161
return config.promptMode === "append" ? "twin" : undefined;
161162
}
162163

164+
/** Make a model name safe for single-line terminal display. */
165+
export function prepareModelNameForDisplay(modelName: string | undefined): string | undefined {
166+
if (!modelName) return undefined;
167+
return stripVTControlCharacters(modelName).replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
168+
}
169+
163170
/** Mode label is not included — callers add it where they want it. */
164171
export function buildInvocationTags(
165172
invocation: AgentInvocation | undefined,
@@ -172,7 +179,7 @@ export function buildInvocationTags(
172179
if (invocation.inheritContext) tags.push("inherit context");
173180
if (invocation.runInBackground) tags.push("background");
174181
if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
175-
return { modelName: invocation.modelName, tags };
182+
return { modelName: prepareModelNameForDisplay(invocation.modelName), tags };
176183
}
177184

178185
/** Truncate text to a single line, max `len` chars. */
@@ -305,8 +312,43 @@ export class AgentWidget {
305312
}
306313
}
307314

315+
/** Model and thinking metadata paired for the current run. */
316+
private invocationStats(
317+
a: {
318+
invocation?: AgentInvocation;
319+
session?: { model?: { provider: string; id: string }; thinkingLevel?: string };
320+
},
321+
theme: Theme,
322+
): string | undefined {
323+
const runtimeInvocation = a.session?.model
324+
? {
325+
...a.invocation,
326+
modelName: `${a.session.model.provider}/${a.session.model.id}`,
327+
thinking: a.session.thinkingLevel,
328+
}
329+
: a.invocation;
330+
const { modelName, tags } = buildInvocationTags(runtimeInvocation);
331+
const safeModelName = prepareModelNameForDisplay(modelName);
332+
const parts = safeModelName ? [safeModelName, ...tags.filter(tag => tag.startsWith("thinking: "))] : [];
333+
return parts.length > 0 ? theme.fg("dim", parts.join(" · ")) : undefined;
334+
}
335+
308336
/** Render a finished agent line. */
309-
private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
337+
private renderFinishedLine(
338+
a: {
339+
id: string;
340+
type: SubagentType;
341+
status: string;
342+
description: string;
343+
toolUses: number;
344+
startedAt: number;
345+
completedAt?: number;
346+
error?: string;
347+
invocation?: AgentInvocation;
348+
session?: { model?: { provider: string; id: string }; thinkingLevel?: string };
349+
},
350+
theme: Theme,
351+
): string {
310352
const name = getDisplayName(a.type);
311353
const modeLabel = getPromptModeLabel(a.type);
312354
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
@@ -333,6 +375,8 @@ export class AgentWidget {
333375
}
334376

335377
const parts: string[] = [];
378+
const invocationStats = this.invocationStats(a, theme);
379+
if (invocationStats) parts.push(invocationStats);
336380
const activity = this.agentActivity.get(a.id);
337381
if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
338382
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
@@ -389,6 +433,8 @@ export class AgentWidget {
389433
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
390434

391435
const parts: string[] = [];
436+
const invocationStats = this.invocationStats(a, theme);
437+
if (invocationStats) parts.push(invocationStats);
392438
if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
393439
if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
394440
if (tokenText) parts.push(tokenText);
@@ -403,29 +449,35 @@ export class AgentWidget {
403449
]);
404450
}
405451

406-
const queuedLine = queued.length > 0
407-
? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
408-
: undefined;
452+
const queuedLines = queued.map(a => {
453+
const invocationStats = this.invocationStats(a, theme);
454+
const suffix = invocationStats ? ` · ${invocationStats}` : "";
455+
return truncate(
456+
theme.fg("dim", "├─") +
457+
` ${theme.fg("muted", "◦")} ${theme.bold(getDisplayName(a.type))} ${theme.fg("muted", a.description)}` +
458+
theme.fg("dim", suffix),
459+
);
460+
});
409461

410462
// Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
411463
const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
412-
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
464+
const totalBody = finishedLines.length + runningLines.length * 2 + queuedLines.length;
413465

414466
const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
415467

416468
if (totalBody <= maxBody) {
417469
// Everything fits — add all lines and fix up connectors for the last item.
418470
lines.push(...finishedLines);
419471
for (const pair of runningLines) lines.push(...pair);
420-
if (queuedLine) lines.push(queuedLine);
472+
lines.push(...queuedLines);
421473

422474
// Fix last connector: swap ├─ → └─ and │ → space for activity lines.
423475
if (lines.length > 1) {
424476
const last = lines.length - 1;
425477
lines[last] = lines[last].replace("├─", "└─");
426478
// If last item is a running agent activity line, fix indent of that line
427479
// and fix the header line above it.
428-
if (runningLines.length > 0 && !queuedLine) {
480+
if (runningLines.length > 0 && queuedLines.length === 0) {
429481
// The last two lines are the last running agent's header + activity.
430482
if (last >= 2) {
431483
lines[last - 1] = lines[last - 1].replace("├─", "└─");
@@ -438,6 +490,7 @@ export class AgentWidget {
438490
// Reserve 1 line for overflow indicator.
439491
let budget = maxBody - 1;
440492
let hiddenRunning = 0;
493+
let hiddenQueued = 0;
441494
let hiddenFinished = 0;
442495

443496
// 1. Running agents (2 lines each)
@@ -450,10 +503,14 @@ export class AgentWidget {
450503
}
451504
}
452505

453-
// 2. Queued line
454-
if (queuedLine && budget >= 1) {
455-
lines.push(queuedLine);
456-
budget--;
506+
// 2. Queued agents
507+
for (const line of queuedLines) {
508+
if (budget >= 1) {
509+
lines.push(line);
510+
budget--;
511+
} else {
512+
hiddenQueued++;
513+
}
457514
}
458515

459516
// 3. Finished agents
@@ -469,9 +526,10 @@ export class AgentWidget {
469526
// Overflow summary
470527
const overflowParts: string[] = [];
471528
if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
529+
if (hiddenQueued > 0) overflowParts.push(`${hiddenQueued} queued`);
472530
if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
473531
const overflowText = overflowParts.join(", ");
474-
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`)
532+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenQueued + hiddenFinished} more (${overflowText})`)}`)
475533
);
476534
}
477535

src/ui/conversation-viewer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,14 @@ export class ConversationViewer implements Component {
274274
}
275275

276276
private invocationLine(): string | undefined {
277-
const { modelName, tags } = buildInvocationTags(this.record.invocation);
277+
const runtimeInvocation = this.session.model
278+
? {
279+
...this.record.invocation,
280+
modelName: `${this.session.model.provider}/${this.session.model.id}`,
281+
thinking: this.session.thinkingLevel,
282+
}
283+
: this.record.invocation;
284+
const { modelName, tags } = buildInvocationTags(runtimeInvocation);
278285
const parts = modelName ? [modelName, ...tags] : tags;
279286
if (parts.length === 0) return undefined;
280287
return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);

0 commit comments

Comments
 (0)