@ian-pascoe/pi-minimal-subagents 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,15 +45,15 @@ Project values override global values. Run `/reload` after editing either file.
45
45
  `minimalSubagents.modelRoles` gives the parent agent advisory names for
46
46
  eligible models. The extension defines no roles itself, performs no task
47
47
  classification, and does not route launches. The parent still passes the
48
- ordinary `model` argument and chooses `thinking_level` independently.
48
+ ordinary `model` and `thinking_level` arguments separately.
49
49
 
50
50
  ```json
51
51
  {
52
52
  "minimalSubagents": {
53
53
  "modelRoles": {
54
- "budget": "opencode-go/glm-5.2",
54
+ "budget": "opencode-go/glm-5.2:low",
55
55
  "design": {
56
- "model": "opencode-go/kimi-k3",
56
+ "model": "opencode-go/kimi-k3:high",
57
57
  "hint": "UI design, visual critique, and frontend polish"
58
58
  }
59
59
  }
@@ -61,10 +61,18 @@ ordinary `model` argument and chooses `thinking_level` independently.
61
61
  }
62
62
  ```
63
63
 
64
+ A recognized final suffix (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`,
65
+ or `max`) is a preferred `thinking_level`, not part of the canonical model
66
+ passed to `subagent`. Unsuffixed roles leave thinking selection independent.
64
67
  Role names and hints are trimmed, single-line text. Names may be up to 64
65
68
  characters and hints up to 500 characters. Models use canonical
66
69
  `provider/model` IDs and must be available under the effective `enabledModels`
67
- scope. Thinking-level suffixes such as `:xhigh` are invalid here.
70
+ scope. The resolver matches the complete authored model ID first, so real
71
+ colon-bearing IDs—including IDs ending in `:high`—remain exact model IDs;
72
+ only an otherwise-unmatched recognized final suffix is treated as a thinking
73
+ preference. A thinking level pinned in `enabledModels` neither supplies nor
74
+ constrains a role preference, and normal spawn-time model-capability clamping
75
+ still applies.
68
76
 
69
77
  Global and project roles merge by name in settings order. Expanded role
70
78
  objects merge by field; a project string replaces the whole global entry. A
@@ -127,3 +135,10 @@ falls back to unlinking its session file. Each Child Agent has a persistent
127
135
  JSONL session. Append-only Root Agent Registry entries retain hierarchy and
128
136
  Delivery Evidence across reloads. Forking cancels and drains active work, then
129
137
  clones child session leaves so the fork receives an independent hierarchy.
138
+
139
+ ## Status and TUI
140
+
141
+ Visible Child Agent rows show the canonical `provider/model:thinking` Runtime
142
+ Profile. Status uses the live Runtime Profile while a runtime exists and falls
143
+ back to the immutable Launch Contract otherwise. Live changes are observational:
144
+ they do not rewrite persistence or change nested spawn defaults.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -29,10 +29,6 @@
29
29
  "access": "public",
30
30
  "provenance": true
31
31
  },
32
- "scripts": {
33
- "test": "vitest run --config ../../vitest.config.ts --root .",
34
- "typecheck": "tsc --noEmit -p tsconfig.json"
35
- },
36
32
  "peerDependencies": {
37
33
  "@earendil-works/pi-agent-core": "*",
38
34
  "@earendil-works/pi-ai": "*",
@@ -47,5 +43,9 @@
47
43
  "extensions": [
48
44
  "./src/index.ts"
49
45
  ]
46
+ },
47
+ "scripts": {
48
+ "test": "vitest run --config ../../vitest.config.ts --root .",
49
+ "typecheck": "tsc --noEmit -p tsconfig.json"
50
50
  }
51
- }
51
+ }
@@ -16,8 +16,6 @@ export const COORDINATOR_TOOL_NAMES = [
16
16
 
17
17
  const READ_TOOL_BUNDLE = ["read", "grep", "find", "ls"];
18
18
  const MODIFY_TOOL_BUNDLE = [...READ_TOOL_BUNDLE, "bash", "edit", "write"];
19
- const THINKING_SUFFIX_PATTERN = /:(?:off|minimal|low|medium|high|xhigh|max)$/;
20
-
21
19
  interface ModelReference {
22
20
  provider: string;
23
21
  id: string;
@@ -28,11 +26,6 @@ interface ScopedModelReference {
28
26
  thinkingLevel?: string;
29
27
  }
30
28
 
31
- /** Remove a recognized Pi thinking suffix without changing model IDs containing other colons. */
32
- export function stripThinkingSuffix(modelPattern: string): string {
33
- return modelPattern.replace(THINKING_SUFFIX_PATTERN, "");
34
- }
35
-
36
29
  /** Build the authenticated runtime model enum from Pi's already-resolved model scope. */
37
30
  export function buildEligibleModelIds(input: {
38
31
  availableModels: readonly ModelReference[];
@@ -47,7 +40,7 @@ export function buildEligibleModelIds(input: {
47
40
  const result: string[] = [];
48
41
 
49
42
  for (const model of source) {
50
- const canonicalId = stripThinkingSuffix(`${model.provider}/${model.id}`);
43
+ const canonicalId = `${model.provider}/${model.id}`;
51
44
  if (!seen.has(canonicalId)) {
52
45
  seen.add(canonicalId);
53
46
  result.push(canonicalId);
@@ -1,15 +1,15 @@
1
- import {
2
- DEFAULT_MAX_SUBAGENT_DEPTH,
3
- stripThinkingSuffix,
4
- } from "./minimal-subagents-capabilities.js";
1
+ import { DEFAULT_MAX_SUBAGENT_DEPTH, THINKING_LEVELS } from "./minimal-subagents-capabilities.js";
5
2
 
6
3
  const MODEL_ROLE_NAME_MAX_LENGTH = 64;
7
4
  const MODEL_ROLE_HINT_MAX_LENGTH = 500;
8
5
 
6
+ type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
7
+
9
8
  /** Describes one user-authored advisory model role shown to subagent callers. */
10
9
  export interface MinimalSubagentsModelRole {
11
10
  name: string;
12
11
  model: string;
12
+ thinkingLevel?: ModelRoleThinkingLevel;
13
13
  hint?: string;
14
14
  }
15
15
 
@@ -89,6 +89,39 @@ function mergeModelRoleEntries(
89
89
  return entries;
90
90
  }
91
91
 
92
+ interface ResolvedModelRoleReference {
93
+ model: string;
94
+ thinkingLevel?: ModelRoleThinkingLevel;
95
+ }
96
+
97
+ function resolveThinkingLevelSuffix(suffix: string): ModelRoleThinkingLevel | undefined {
98
+ return THINKING_LEVELS.find((thinkingLevel) => thinkingLevel === suffix);
99
+ }
100
+
101
+ function resolveModelRoleReference(
102
+ model: string,
103
+ eligibleModels: ReadonlySet<string>,
104
+ path: string,
105
+ warnings: string[],
106
+ ): ResolvedModelRoleReference | undefined {
107
+ if (eligibleModels.has(model)) return { model };
108
+
109
+ const separatorIndex = model.lastIndexOf(":");
110
+ if (separatorIndex >= 0) {
111
+ const prefix = model.slice(0, separatorIndex);
112
+ const suffix = model.slice(separatorIndex + 1);
113
+ if (eligibleModels.has(prefix)) {
114
+ const thinkingLevel = resolveThinkingLevelSuffix(suffix);
115
+ if (thinkingLevel !== undefined) return { model: prefix, thinkingLevel };
116
+ warnings.push(`${path}: unknown thinking level suffix: ${suffix}`);
117
+ return undefined;
118
+ }
119
+ }
120
+
121
+ warnings.push(`${path}: model is not eligible: ${model}`);
122
+ return undefined;
123
+ }
124
+
92
125
  function parseModelRoles(
93
126
  entries: ReadonlyMap<string, ScopedSettingValue>,
94
127
  eligibleModelIds: readonly string[],
@@ -125,16 +158,8 @@ function parseModelRoles(
125
158
  warnings.push(`${path}: model must be a non-empty trimmed string`);
126
159
  continue;
127
160
  }
128
- if (stripThinkingSuffix(model) !== model) {
129
- warnings.push(
130
- `${path}: thinking level suffixes are not allowed; choose thinking_level per spawn`,
131
- );
132
- continue;
133
- }
134
- if (!eligibleModels.has(model)) {
135
- warnings.push(`${path}: model is not eligible: ${model}`);
136
- continue;
137
- }
161
+ const resolvedModel = resolveModelRoleReference(model, eligibleModels, path, warnings);
162
+ if (resolvedModel === undefined) continue;
138
163
 
139
164
  const hint = isRecord(value) ? value.hint : undefined;
140
165
  if (
@@ -148,7 +173,14 @@ function parseModelRoles(
148
173
  warnings.push(`${path}.hint: expected trimmed single-line text up to 500 characters`);
149
174
  continue;
150
175
  }
151
- roles.push({ name, model, ...(hint === undefined ? {} : { hint }) });
176
+ roles.push({
177
+ name,
178
+ model: resolvedModel.model,
179
+ ...(resolvedModel.thinkingLevel === undefined
180
+ ? {}
181
+ : { thinkingLevel: resolvedModel.thinkingLevel }),
182
+ ...(hint === undefined ? {} : { hint }),
183
+ });
152
184
  }
153
185
  return roles;
154
186
  }
@@ -993,6 +993,10 @@ export class MinimalSubagentsCoordinator {
993
993
  const elapsed = agent.active_turn_started_at
994
994
  ? Math.max(0, this.now().getTime() - new Date(agent.active_turn_started_at).getTime())
995
995
  : undefined;
996
+ const runtimeProfile = this.runtimes.get(agent.agent_id)?.getRuntimeProfile() ?? {
997
+ model: agent.launch_contract.model,
998
+ thinking_level: agent.launch_contract.thinking_level,
999
+ };
996
1000
  return {
997
1001
  agent_id: agent.agent_id,
998
1002
  parent_id: agent.parent_id,
@@ -1002,8 +1006,7 @@ export class MinimalSubagentsCoordinator {
1002
1006
  latest_turn: agent.latest_result
1003
1007
  ? { turn_id: agent.latest_result.turn_id, status: agent.latest_result.status }
1004
1008
  : undefined,
1005
- model: agent.launch_contract.model,
1006
- thinking_level: agent.launch_contract.thinking_level,
1009
+ ...runtimeProfile,
1007
1010
  tools: [...agent.launch_contract.ordinary_tools],
1008
1011
  elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
1009
1012
  latest_activity_at: agent.latest_activity_at ?? agent.created_at,
@@ -37,6 +37,7 @@ import type {
37
37
  PersistedSessionIdentity,
38
38
  ProjectContextMode,
39
39
  RuntimeCreationRequest,
40
+ RuntimeProfile,
40
41
  RuntimeTurnOutcome,
41
42
  } from "./minimal-subagents-types.js";
42
43
 
@@ -329,6 +330,15 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
329
330
  this.session.dispose();
330
331
  }
331
332
 
333
+ getRuntimeProfile(): RuntimeProfile | undefined {
334
+ const model = this.session.model;
335
+ if (!model) return undefined;
336
+ return {
337
+ model: `${model.provider}/${model.id}`,
338
+ thinking_level: this.session.thinkingLevel,
339
+ };
340
+ }
341
+
332
342
  snapshotCommittedMessages(): AgentMessage[] {
333
343
  return snapshotCommittedContext(this.session.messages, this.session.isStreaming);
334
344
  }
@@ -40,12 +40,13 @@ function buildModelRolePromptGuidelines(
40
40
  modelRoles: readonly MinimalSubagentsModelRole[],
41
41
  ): string[] | undefined {
42
42
  if (modelRoles.length === 0) return undefined;
43
- const roleLines = modelRoles.map(
44
- (role) => ` - ${role.name} → ${role.model}${role.hint ? ` — ${role.hint}` : ""}`,
45
- );
43
+ const roleLines = modelRoles.map((role) => {
44
+ const thinkingGuidance = role.thinkingLevel ? `, thinking_level=${role.thinkingLevel}` : "";
45
+ return ` - ${role.name} → model=${role.model}${thinkingGuidance}${role.hint ? ` — ${role.hint}` : ""}`;
46
+ });
46
47
  return [
47
48
  ["Configured model roles are guidance, not constraints:", ...roleLines].join("\n"),
48
- "Choose a model based on the task. Choose thinking_level independently.",
49
+ "Choose a model based on the task. A listed thinking_level is a preference, not a constraint. Callers choose thinking_level independently for roles without one.",
49
50
  ];
50
51
  }
51
52
 
@@ -22,6 +22,12 @@ export type AgentAvailability = "available" | "unavailable";
22
22
  /** Classifies active and terminal persistent subagent turn outcomes. */
23
23
  export type TurnStatus = "running" | "completed" | "failed" | "cancelled" | "interrupted";
24
24
 
25
+ /** Describes the canonical model and resolved thinking level currently used by a Child Agent. */
26
+ export interface RuntimeProfile {
27
+ model: string;
28
+ thinking_level: ThinkingLevel;
29
+ }
30
+
25
31
  /** Defines the validated launch contract accepted by the subagent tool. */
26
32
  export interface SpawnParameters {
27
33
  task: string;
@@ -59,16 +65,14 @@ export interface AgentMessageResult {
59
65
  error?: string;
60
66
  }
61
67
 
62
- /** Provides bounded hierarchy and usage data for one persistent agent. */
63
- export interface AgentSummary {
68
+ /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
69
+ export interface AgentSummary extends RuntimeProfile {
64
70
  agent_id: string;
65
71
  parent_id: string;
66
72
  state: AgentState;
67
73
  availability: AgentAvailability;
68
74
  active_turn_id?: string;
69
75
  latest_turn?: Pick<TurnResult, "turn_id" | "status">;
70
- model: string;
71
- thinking_level: ThinkingLevel;
72
76
  tools: string[];
73
77
  elapsed_ms?: number;
74
78
  latest_activity?: string;
@@ -125,11 +129,9 @@ export interface DeleteResult {
125
129
  }
126
130
 
127
131
  /** Persists immutable context, model, thinking, and ordinary-tool launch choices. */
128
- export interface LaunchContract {
132
+ export interface LaunchContract extends RuntimeProfile {
129
133
  session_context: SessionContextMode;
130
134
  project_context: ProjectContextMode;
131
- model: string;
132
- thinking_level: ThinkingLevel;
133
135
  tools: ToolSelection | undefined;
134
136
  ordinary_tools: string[];
135
137
  delegation?: DelegationMode;
@@ -184,6 +186,8 @@ export interface ChildAgentRuntime {
184
186
  steerCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
185
187
  abort(): Promise<void>;
186
188
  dispose(): void;
189
+ /** Return the live Runtime Profile, or undefined when the SDK session has no model. */
190
+ getRuntimeProfile(): RuntimeProfile | undefined;
187
191
  snapshotCommittedMessages(): AgentMessage[];
188
192
  hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string): boolean;
189
193
  getUsage(): Usage | undefined;
@@ -1,13 +1,24 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
3
+ import {
4
+ sliceByColumn,
5
+ truncateToWidth,
6
+ visibleWidth,
7
+ type Component,
8
+ type TUI,
9
+ } from "@earendil-works/pi-tui";
4
10
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
5
11
  import {
6
12
  formatSubagentDuration,
7
13
  renderSubagentStatusLabel,
8
14
  renderSubagentStatusSymbol,
9
15
  } from "./minimal-subagents-rendering.js";
10
- import type { AgentSummary, HierarchyStatusResult, TurnStatus } from "./minimal-subagents-types.js";
16
+ import type {
17
+ AgentSummary,
18
+ HierarchyStatusResult,
19
+ RuntimeProfile,
20
+ TurnStatus,
21
+ } from "./minimal-subagents-types.js";
11
22
 
12
23
  const MINIMAL_SUBAGENTS_UI_KEY = "minimal-subagents";
13
24
  const MINIMAL_SUBAGENTS_RECENT_LIMIT = 3;
@@ -20,6 +31,7 @@ export interface MinimalSubagentsWidgetRow {
20
31
  depth: number;
21
32
  status: TurnStatus | "idle" | "unavailable";
22
33
  elapsedMs?: number;
34
+ runtimeProfile: RuntimeProfile;
23
35
  task?: string;
24
36
  structural: boolean;
25
37
  }
@@ -122,6 +134,10 @@ export function buildMinimalSubagentsWidgetView(
122
134
  depth: item.depth,
123
135
  status: agentTerminalStatus(item.agent),
124
136
  elapsedMs: item.agent.elapsed_ms,
137
+ runtimeProfile: {
138
+ model: item.agent.model,
139
+ thinking_level: item.agent.thinking_level,
140
+ },
125
141
  task: structural ? undefined : item.agent.task,
126
142
  structural,
127
143
  };
@@ -135,6 +151,187 @@ export function buildMinimalSubagentsWidgetView(
135
151
  };
136
152
  }
137
153
 
154
+ const MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT = " · ";
155
+ const MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS = "…";
156
+
157
+ interface MinimalSubagentsWidgetRowParts {
158
+ identity: string;
159
+ status: string;
160
+ profile: string;
161
+ task?: string;
162
+ }
163
+
164
+ function joinMinimalSubagentsWidgetRow(
165
+ parts: MinimalSubagentsWidgetRowParts,
166
+ separator: string,
167
+ ): string {
168
+ return [parts.identity, parts.status, parts.profile, parts.task]
169
+ .filter((part): part is string => part !== undefined)
170
+ .join(separator);
171
+ }
172
+
173
+ function formatMinimalSubagentsRuntimeProfile(
174
+ profile: RuntimeProfile,
175
+ maxWidth?: number,
176
+ ): string | undefined {
177
+ const suffix = `:${profile.thinking_level}`;
178
+ const fullProfile = `${profile.model}${suffix}`;
179
+ if (maxWidth === undefined || visibleWidth(fullProfile) <= maxWidth) return fullProfile;
180
+
181
+ const shortestProfile = `${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
182
+ if (maxWidth < visibleWidth(shortestProfile)) return undefined;
183
+ const modelWidth = maxWidth - visibleWidth(suffix);
184
+ const prefix = sliceByColumn(
185
+ profile.model,
186
+ 0,
187
+ Math.max(0, modelWidth - visibleWidth(MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS)),
188
+ true,
189
+ );
190
+ return `${prefix}${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
191
+ }
192
+
193
+ function renderMinimalSubagentsWidgetRowParts(
194
+ row: MinimalSubagentsWidgetRow,
195
+ task: string | undefined,
196
+ duration: string | undefined,
197
+ profile: string,
198
+ theme: Theme,
199
+ ): MinimalSubagentsWidgetRowParts {
200
+ const branch = row.depth > 0 ? `${" ".repeat(row.depth)}╰─ ` : " ";
201
+ const styledBranch = theme.fg("borderMuted", branch);
202
+ const agentId = row.structural ? theme.fg("muted", row.agentId) : theme.bold(row.agentId);
203
+ const identity = `${styledBranch}${renderSubagentStatusSymbol(theme, row.status)} ${agentId}`;
204
+ const status = `${renderSubagentStatusLabel(theme, row.status)}${
205
+ duration ? ` ${theme.fg("muted", duration)}` : ""
206
+ }`;
207
+ return {
208
+ identity,
209
+ status,
210
+ profile: theme.fg("muted", profile),
211
+ task: task ? theme.fg("muted", task) : undefined,
212
+ };
213
+ }
214
+
215
+ function minimalSubagentsWidgetRowFits(
216
+ parts: MinimalSubagentsWidgetRowParts,
217
+ separator: string,
218
+ width: number,
219
+ ): boolean {
220
+ return visibleWidth(joinMinimalSubagentsWidgetRow(parts, separator)) <= width;
221
+ }
222
+
223
+ function minimalSubagentsWidgetProfileBudget(
224
+ row: MinimalSubagentsWidgetRow,
225
+ task: string | undefined,
226
+ duration: string | undefined,
227
+ separator: string,
228
+ theme: Theme,
229
+ width: number,
230
+ ): number {
231
+ const fixedParts = renderMinimalSubagentsWidgetRowParts(row, task, duration, "", theme);
232
+ return width - visibleWidth(joinMinimalSubagentsWidgetRow(fixedParts, separator));
233
+ }
234
+
235
+ function renderMinimalSubagentsWidgetRow(
236
+ row: MinimalSubagentsWidgetRow,
237
+ width: number,
238
+ theme: Theme,
239
+ ): string {
240
+ const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
241
+ const task = row.task?.replace(/\s+/g, " ").trim() || undefined;
242
+ const duration = row.status === "unavailable" ? undefined : formatSubagentDuration(row.elapsedMs);
243
+ const fullProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile);
244
+ if (!fullProfile) return "";
245
+
246
+ const completeParts = renderMinimalSubagentsWidgetRowParts(
247
+ row,
248
+ task,
249
+ duration,
250
+ fullProfile,
251
+ theme,
252
+ );
253
+ if (minimalSubagentsWidgetRowFits(completeParts, separator, width)) {
254
+ return joinMinimalSubagentsWidgetRow(completeParts, separator);
255
+ }
256
+
257
+ const partsWithoutTask = renderMinimalSubagentsWidgetRowParts(
258
+ row,
259
+ undefined,
260
+ duration,
261
+ fullProfile,
262
+ theme,
263
+ );
264
+ if (minimalSubagentsWidgetRowFits(partsWithoutTask, separator, width)) {
265
+ return joinMinimalSubagentsWidgetRow(partsWithoutTask, separator);
266
+ }
267
+
268
+ const profileBudget = minimalSubagentsWidgetProfileBudget(
269
+ row,
270
+ undefined,
271
+ duration,
272
+ separator,
273
+ theme,
274
+ width,
275
+ );
276
+ const shortenedProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile, profileBudget);
277
+ if (shortenedProfile) {
278
+ const shortenedParts = renderMinimalSubagentsWidgetRowParts(
279
+ row,
280
+ undefined,
281
+ duration,
282
+ shortenedProfile,
283
+ theme,
284
+ );
285
+ if (minimalSubagentsWidgetRowFits(shortenedParts, separator, width)) {
286
+ return joinMinimalSubagentsWidgetRow(shortenedParts, separator);
287
+ }
288
+ }
289
+
290
+ if (duration) {
291
+ const noDurationBudget = minimalSubagentsWidgetProfileBudget(
292
+ row,
293
+ undefined,
294
+ undefined,
295
+ separator,
296
+ theme,
297
+ width,
298
+ );
299
+ const noDurationProfile = formatMinimalSubagentsRuntimeProfile(
300
+ row.runtimeProfile,
301
+ noDurationBudget,
302
+ );
303
+ if (noDurationProfile) {
304
+ const noDurationParts = renderMinimalSubagentsWidgetRowParts(
305
+ row,
306
+ undefined,
307
+ undefined,
308
+ noDurationProfile,
309
+ theme,
310
+ );
311
+ if (minimalSubagentsWidgetRowFits(noDurationParts, separator, width)) {
312
+ return joinMinimalSubagentsWidgetRow(noDurationParts, separator);
313
+ }
314
+ }
315
+ }
316
+
317
+ const shortestProfile = formatMinimalSubagentsRuntimeProfile(
318
+ row.runtimeProfile,
319
+ visibleWidth(`${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}:${row.runtimeProfile.thinking_level}`),
320
+ )!;
321
+ const lastResortParts = renderMinimalSubagentsWidgetRowParts(
322
+ row,
323
+ undefined,
324
+ undefined,
325
+ shortestProfile,
326
+ theme,
327
+ );
328
+ return truncateToWidth(
329
+ joinMinimalSubagentsWidgetRow(lastResortParts, separator),
330
+ width,
331
+ MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
332
+ );
333
+ }
334
+
138
335
  /** Render a responsive widget snapshot with ANSI-safe terminal-width truncation. */
139
336
  export function renderMinimalSubagentsWidgetLines(
140
337
  view: MinimalSubagentsWidgetView,
@@ -142,7 +339,7 @@ export function renderMinimalSubagentsWidgetLines(
142
339
  theme: Theme,
143
340
  ): string[] {
144
341
  if (width <= 0) return [];
145
- const separator = theme.fg("dim", " · ");
342
+ const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
146
343
  const activity =
147
344
  view.runningCount > 0
148
345
  ? theme.fg("accent", `${view.runningCount} running`)
@@ -159,27 +356,18 @@ export function renderMinimalSubagentsWidgetLines(
159
356
  .filter((part): part is string => Boolean(part))
160
357
  .join(separator),
161
358
  width,
162
- "…",
359
+ MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
163
360
  ),
164
361
  ];
165
- for (const row of view.rows) {
166
- const duration = formatSubagentDuration(row.elapsedMs);
167
- const task = row.task?.replace(/\s+/g, " ").trim();
168
- const branch =
169
- row.depth > 0
170
- ? theme.fg("borderMuted", `${" ".repeat(row.depth)}╰─ `)
171
- : theme.fg("borderMuted", " ");
172
- const agentId = row.structural ? theme.fg("muted", row.agentId) : theme.bold(row.agentId);
173
- const parts = [
174
- `${branch}${renderSubagentStatusSymbol(theme, row.status)} ${agentId}`,
175
- row.structural ? undefined : renderSubagentStatusLabel(theme, row.status),
176
- task ? theme.fg("muted", task) : undefined,
177
- duration ? theme.fg("muted", duration) : undefined,
178
- ].filter((part): part is string => Boolean(part));
179
- lines.push(truncateToWidth(parts.join(separator), width, "…"));
180
- }
362
+ for (const row of view.rows) lines.push(renderMinimalSubagentsWidgetRow(row, width, theme));
181
363
  if (view.overflowCount > 0) {
182
- lines.push(truncateToWidth(theme.fg("dim", ` … +${view.overflowCount} more`), width, "…"));
364
+ lines.push(
365
+ truncateToWidth(
366
+ theme.fg("dim", ` … +${view.overflowCount} more`),
367
+ width,
368
+ MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
369
+ ),
370
+ );
183
371
  }
184
372
  return lines;
185
373
  }