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

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.
@@ -1,13 +1,27 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Theme operations used by the Minimal Subagents widget renderer. */
4
+ export type MinimalSubagentsWidgetTheme = Pick<Theme, "fg" | "bold">;
2
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
6
+ import {
7
+ sliceByColumn,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ type Component,
11
+ type TUI,
12
+ } from "@earendil-works/pi-tui";
4
13
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
5
14
  import {
6
15
  formatSubagentDuration,
7
16
  renderSubagentStatusLabel,
8
17
  renderSubagentStatusSymbol,
9
18
  } from "./minimal-subagents-rendering.js";
10
- import type { AgentSummary, HierarchyStatusResult, TurnStatus } from "./minimal-subagents-types.js";
19
+ import type {
20
+ AgentSummary,
21
+ HierarchyStatusResult,
22
+ RuntimeProfile,
23
+ TurnStatus,
24
+ } from "./minimal-subagents-types.js";
11
25
 
12
26
  const MINIMAL_SUBAGENTS_UI_KEY = "minimal-subagents";
13
27
  const MINIMAL_SUBAGENTS_RECENT_LIMIT = 3;
@@ -20,6 +34,7 @@ export interface MinimalSubagentsWidgetRow {
20
34
  depth: number;
21
35
  status: TurnStatus | "idle" | "unavailable";
22
36
  elapsedMs?: number;
37
+ runtimeProfile: RuntimeProfile;
23
38
  task?: string;
24
39
  structural: boolean;
25
40
  }
@@ -122,6 +137,10 @@ export function buildMinimalSubagentsWidgetView(
122
137
  depth: item.depth,
123
138
  status: agentTerminalStatus(item.agent),
124
139
  elapsedMs: item.agent.elapsed_ms,
140
+ runtimeProfile: {
141
+ model: item.agent.model,
142
+ thinking_level: item.agent.thinking_level,
143
+ },
125
144
  task: structural ? undefined : item.agent.task,
126
145
  structural,
127
146
  };
@@ -135,14 +154,195 @@ export function buildMinimalSubagentsWidgetView(
135
154
  };
136
155
  }
137
156
 
157
+ const MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT = " · ";
158
+ const MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS = "…";
159
+
160
+ interface MinimalSubagentsWidgetRowParts {
161
+ identity: string;
162
+ status: string;
163
+ profile: string;
164
+ task?: string;
165
+ }
166
+
167
+ function joinMinimalSubagentsWidgetRow(
168
+ parts: MinimalSubagentsWidgetRowParts,
169
+ separator: string,
170
+ ): string {
171
+ return [parts.identity, parts.status, parts.profile, parts.task]
172
+ .filter((part): part is string => part !== undefined)
173
+ .join(separator);
174
+ }
175
+
176
+ function formatMinimalSubagentsRuntimeProfile(
177
+ profile: RuntimeProfile,
178
+ maxWidth?: number,
179
+ ): string | undefined {
180
+ const suffix = `:${profile.thinking_level}`;
181
+ const fullProfile = `${profile.model}${suffix}`;
182
+ if (maxWidth === undefined || visibleWidth(fullProfile) <= maxWidth) return fullProfile;
183
+
184
+ const shortestProfile = `${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
185
+ if (maxWidth < visibleWidth(shortestProfile)) return undefined;
186
+ const modelWidth = maxWidth - visibleWidth(suffix);
187
+ const prefix = sliceByColumn(
188
+ profile.model,
189
+ 0,
190
+ Math.max(0, modelWidth - visibleWidth(MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS)),
191
+ true,
192
+ );
193
+ return `${prefix}${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
194
+ }
195
+
196
+ function renderMinimalSubagentsWidgetRowParts(
197
+ row: MinimalSubagentsWidgetRow,
198
+ task: string | undefined,
199
+ duration: string | undefined,
200
+ profile: string,
201
+ theme: MinimalSubagentsWidgetTheme,
202
+ ): MinimalSubagentsWidgetRowParts {
203
+ const branch = row.depth > 0 ? `${" ".repeat(row.depth)}╰─ ` : " ";
204
+ const styledBranch = theme.fg("borderMuted", branch);
205
+ const agentId = row.structural ? theme.fg("muted", row.agentId) : theme.bold(row.agentId);
206
+ const identity = `${styledBranch}${renderSubagentStatusSymbol(theme, row.status)} ${agentId}`;
207
+ const status = `${renderSubagentStatusLabel(theme, row.status)}${
208
+ duration ? ` ${theme.fg("muted", duration)}` : ""
209
+ }`;
210
+ return {
211
+ identity,
212
+ status,
213
+ profile: theme.fg("muted", profile),
214
+ task: task ? theme.fg("muted", task) : undefined,
215
+ };
216
+ }
217
+
218
+ function minimalSubagentsWidgetRowFits(
219
+ parts: MinimalSubagentsWidgetRowParts,
220
+ separator: string,
221
+ width: number,
222
+ ): boolean {
223
+ return visibleWidth(joinMinimalSubagentsWidgetRow(parts, separator)) <= width;
224
+ }
225
+
226
+ function minimalSubagentsWidgetProfileBudget(
227
+ row: MinimalSubagentsWidgetRow,
228
+ task: string | undefined,
229
+ duration: string | undefined,
230
+ separator: string,
231
+ theme: MinimalSubagentsWidgetTheme,
232
+ width: number,
233
+ ): number {
234
+ const fixedParts = renderMinimalSubagentsWidgetRowParts(row, task, duration, "", theme);
235
+ return width - visibleWidth(joinMinimalSubagentsWidgetRow(fixedParts, separator));
236
+ }
237
+
238
+ function renderMinimalSubagentsWidgetRow(
239
+ row: MinimalSubagentsWidgetRow,
240
+ width: number,
241
+ theme: MinimalSubagentsWidgetTheme,
242
+ ): string {
243
+ const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
244
+ const task = row.task?.replace(/\s+/g, " ").trim() || undefined;
245
+ const duration = row.status === "unavailable" ? undefined : formatSubagentDuration(row.elapsedMs);
246
+ const fullProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile);
247
+ if (!fullProfile) return "";
248
+
249
+ const completeParts = renderMinimalSubagentsWidgetRowParts(
250
+ row,
251
+ task,
252
+ duration,
253
+ fullProfile,
254
+ theme,
255
+ );
256
+ if (minimalSubagentsWidgetRowFits(completeParts, separator, width)) {
257
+ return joinMinimalSubagentsWidgetRow(completeParts, separator);
258
+ }
259
+
260
+ const partsWithoutTask = renderMinimalSubagentsWidgetRowParts(
261
+ row,
262
+ undefined,
263
+ duration,
264
+ fullProfile,
265
+ theme,
266
+ );
267
+ if (minimalSubagentsWidgetRowFits(partsWithoutTask, separator, width)) {
268
+ return joinMinimalSubagentsWidgetRow(partsWithoutTask, separator);
269
+ }
270
+
271
+ const profileBudget = minimalSubagentsWidgetProfileBudget(
272
+ row,
273
+ undefined,
274
+ duration,
275
+ separator,
276
+ theme,
277
+ width,
278
+ );
279
+ const shortenedProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile, profileBudget);
280
+ if (shortenedProfile) {
281
+ const shortenedParts = renderMinimalSubagentsWidgetRowParts(
282
+ row,
283
+ undefined,
284
+ duration,
285
+ shortenedProfile,
286
+ theme,
287
+ );
288
+ if (minimalSubagentsWidgetRowFits(shortenedParts, separator, width)) {
289
+ return joinMinimalSubagentsWidgetRow(shortenedParts, separator);
290
+ }
291
+ }
292
+
293
+ if (duration) {
294
+ const noDurationBudget = minimalSubagentsWidgetProfileBudget(
295
+ row,
296
+ undefined,
297
+ undefined,
298
+ separator,
299
+ theme,
300
+ width,
301
+ );
302
+ const noDurationProfile = formatMinimalSubagentsRuntimeProfile(
303
+ row.runtimeProfile,
304
+ noDurationBudget,
305
+ );
306
+ if (noDurationProfile) {
307
+ const noDurationParts = renderMinimalSubagentsWidgetRowParts(
308
+ row,
309
+ undefined,
310
+ undefined,
311
+ noDurationProfile,
312
+ theme,
313
+ );
314
+ if (minimalSubagentsWidgetRowFits(noDurationParts, separator, width)) {
315
+ return joinMinimalSubagentsWidgetRow(noDurationParts, separator);
316
+ }
317
+ }
318
+ }
319
+
320
+ const shortestProfile = formatMinimalSubagentsRuntimeProfile(
321
+ row.runtimeProfile,
322
+ visibleWidth(`${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}:${row.runtimeProfile.thinking_level}`),
323
+ )!;
324
+ const lastResortParts = renderMinimalSubagentsWidgetRowParts(
325
+ row,
326
+ undefined,
327
+ undefined,
328
+ shortestProfile,
329
+ theme,
330
+ );
331
+ return truncateToWidth(
332
+ joinMinimalSubagentsWidgetRow(lastResortParts, separator),
333
+ width,
334
+ MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
335
+ );
336
+ }
337
+
138
338
  /** Render a responsive widget snapshot with ANSI-safe terminal-width truncation. */
139
339
  export function renderMinimalSubagentsWidgetLines(
140
340
  view: MinimalSubagentsWidgetView,
141
341
  width: number,
142
- theme: Theme,
342
+ theme: MinimalSubagentsWidgetTheme,
143
343
  ): string[] {
144
344
  if (width <= 0) return [];
145
- const separator = theme.fg("dim", " · ");
345
+ const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
146
346
  const activity =
147
347
  view.runningCount > 0
148
348
  ? theme.fg("accent", `${view.runningCount} running`)
@@ -159,27 +359,18 @@ export function renderMinimalSubagentsWidgetLines(
159
359
  .filter((part): part is string => Boolean(part))
160
360
  .join(separator),
161
361
  width,
162
- "…",
362
+ MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
163
363
  ),
164
364
  ];
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
- }
365
+ for (const row of view.rows) lines.push(renderMinimalSubagentsWidgetRow(row, width, theme));
181
366
  if (view.overflowCount > 0) {
182
- lines.push(truncateToWidth(theme.fg("dim", ` … +${view.overflowCount} more`), width, "…"));
367
+ lines.push(
368
+ truncateToWidth(
369
+ theme.fg("dim", ` … +${view.overflowCount} more`),
370
+ width,
371
+ MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
372
+ ),
373
+ );
183
374
  }
184
375
  return lines;
185
376
  }
@@ -188,7 +379,7 @@ class MinimalSubagentsWidgetComponent implements Component {
188
379
  constructor(
189
380
  private view: MinimalSubagentsWidgetView,
190
381
  private readonly tui: TUI,
191
- private readonly theme: Theme,
382
+ private readonly theme: MinimalSubagentsWidgetTheme,
192
383
  ) {}
193
384
 
194
385
  update(view: MinimalSubagentsWidgetView): void {