@nredd/pi-subagents 0.22.0-nredd.7 → 0.22.0-nredd.8

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.
Files changed (46) hide show
  1. package/README.md +124 -37
  2. package/package.json +8 -8
  3. package/src/agent-file-toggle.ts +6 -4
  4. package/src/agent-manager.ts +131 -28
  5. package/src/agent-runner.ts +155 -62
  6. package/src/cross-extension-rpc.ts +11 -36
  7. package/src/custom-agents.ts +9 -5
  8. package/src/default-agents.ts +2 -4
  9. package/src/enabled-models.ts +8 -7
  10. package/src/enterprise-local.ts +5 -5
  11. package/src/home-paths.ts +27 -0
  12. package/src/index.ts +191 -90
  13. package/src/invocation-config.ts +3 -16
  14. package/src/memory.ts +9 -9
  15. package/src/mention-clone.ts +9 -5
  16. package/src/model-resolver.ts +41 -11
  17. package/src/model-scope.ts +2 -1
  18. package/src/model-selection.ts +159 -0
  19. package/src/model-tiers.ts +171 -0
  20. package/src/nested-tools.ts +22 -26
  21. package/src/output-file.ts +23 -1
  22. package/src/package-resources.ts +25 -10
  23. package/src/process-budget.ts +6 -3
  24. package/src/quota-admission.ts +21 -9
  25. package/src/resource-loader.ts +51 -0
  26. package/src/schedule.ts +36 -18
  27. package/src/settings.ts +42 -12
  28. package/src/skill-loader.ts +8 -7
  29. package/src/stale-ctx.ts +16 -0
  30. package/src/types.ts +27 -0
  31. package/src/ui/agent-row.ts +13 -3
  32. package/src/ui/agent-widget.ts +15 -4
  33. package/src/ui/conversation-viewer.ts +6 -3
  34. package/src/ui/fallback-flag.ts +90 -0
  35. package/src/ui/fleet-list.ts +363 -53
  36. package/src/ui/fleet-state.ts +154 -0
  37. package/src/ui/notification-view.ts +18 -2
  38. package/src/ui/workflow-card.ts +5 -1
  39. package/src/ui/workflow-dialog.ts +10 -0
  40. package/src/workflow/host.ts +43 -45
  41. package/src/workflow/journal.ts +13 -1
  42. package/src/workflow/progress.ts +4 -0
  43. package/src/workflow/runtime.ts +25 -8
  44. package/src/workflow/saved.ts +19 -17
  45. package/src/workflow/task.ts +10 -0
  46. package/src/workflow/worker-source.ts +156 -38
@@ -25,7 +25,7 @@ import type { Dirent } from "node:fs";
25
25
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
26
26
  import { homedir } from "node:os";
27
27
  import { dirname, join } from "node:path";
28
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
28
+ import { resolveAgentDir } from "./home-paths.js";
29
29
  import { isSymlink, isUnsafeName, safeReadFile } from "./memory.js";
30
30
 
31
31
  export interface PreloadedSkill {
@@ -33,21 +33,22 @@ export interface PreloadedSkill {
33
33
  content: string;
34
34
  }
35
35
 
36
- export function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[] {
37
- return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
36
+ export function preloadSkills(skillNames: string[], cwd: string, homeDir = homedir()): PreloadedSkill[] {
37
+ return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd, homeDir) }));
38
38
  }
39
39
 
40
- function loadSkillContent(name: string, cwd: string): string {
40
+ function loadSkillContent(name: string, cwd: string, homeDir: string): string {
41
41
  if (isUnsafeName(name)) {
42
42
  return `(Skill "${name}" skipped: name contains path traversal characters)`;
43
43
  }
44
44
  // `trusted`: user-owned root, symlinks followed. Project roots stay strict.
45
+ const agentDir = resolveAgentDir(homeDir);
45
46
  const roots = [
46
47
  { path: join(cwd, ".pi", "skills"), trusted: false }, // project — Pi standard
47
48
  { path: join(cwd, ".agents", "skills"), trusted: false }, // project — Agent Skills spec
48
- { path: join(getAgentDir(), "skills"), trusted: true }, // user — Pi standard
49
- { path: join(homedir(), ".agents", "skills"), trusted: true }, // user — Agent Skills spec
50
- { path: join(homedir(), ".pi", "skills"), trusted: true }, // legacy global, pre-Pi
49
+ { path: join(agentDir, "skills"), trusted: true }, // user — Pi standard
50
+ { path: join(homeDir, ".agents", "skills"), trusted: true }, // user — Agent Skills spec
51
+ { path: join(homeDir, ".pi", "skills"), trusted: true }, // legacy global, pre-Pi
51
52
  ];
52
53
  for (const root of roots) {
53
54
  const content = findInRoot(root.path, name, root.trusted);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * stale-ctx.ts -- recognising pi's "extension ctx is stale" failure.
3
+ *
4
+ * pi invalidates an extension runner on reload and on session replacement
5
+ * (/new, /fork, /resume). Anything that still reaches the old runner or its
6
+ * `pi` throws this message (#145).
7
+ */
8
+
9
+ /** Matched on the stable prefix of core's message. */
10
+ export const STALE_CTX_RE = /extension ctx is stale after session replacement or reload/i;
11
+
12
+ /** Whether `error` (an `Error` or a message string) is the stale-ctx failure. */
13
+ export function isStaleCtxError(error: unknown): boolean {
14
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
15
+ return STALE_CTX_RE.test(message);
16
+ }
package/src/types.ts CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  import type { ThinkingLevel } from "@earendil-works/pi-ai";
6
6
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
7
+ import type { ModelFallback } from "./model-resolver.js";
8
+ import type { ModelSelection } from "./model-selection.js";
9
+ import type { TierDisclosure } from "./model-tiers.js";
7
10
  import type { BudgetLease } from "./process-budget.js";
8
11
  import type { LifetimeUsage } from "./usage.js";
9
12
 
@@ -147,6 +150,9 @@ export interface AgentTombstone {
147
150
  description: string;
148
151
  /** Always set — a record with no session file is never tombstoned. */
149
152
  sessionFile: string;
153
+ /** The evicted run's tier and fallback, handed back when `@handle` reopens it. */
154
+ modelTier?: TierDisclosure;
155
+ modelFallback?: ModelFallback;
150
156
  completedAt: number;
151
157
  }
152
158
 
@@ -270,6 +276,24 @@ export interface AgentRecord {
270
276
  isBackground?: boolean;
271
277
  /** Resolved spawn params, captured for UI display. Fixed at spawn time. */
272
278
  invocation?: AgentInvocation;
279
+ /**
280
+ * Set when a configured model did not resolve and the agent runs on the parent
281
+ * model. The single source for every disclosure surface; a resume reuses it
282
+ * as is, never recomputes it from the resuming call's config.
283
+ */
284
+ modelFallback?: ModelFallback;
285
+ /**
286
+ * This record's selection, retained across live resumes and physical relabelling.
287
+ * An evicted-session reopen selects anew; current fallback disclosure always
288
+ * comes from that selection.
289
+ */
290
+ modelSelection?: ModelSelection;
291
+ /** Actual run's tier provenance, preserved through eviction and replay. */
292
+ modelTier?: TierDisclosure;
293
+ /** Prior evicted run's outcome, never the continuation's selection or disclosure. */
294
+ previousModelTier?: TierDisclosure;
295
+ /** Prior evicted run's fallback, retained separately from the continuation's current disclosure. */
296
+ previousModelFallback?: ModelFallback;
273
297
  /** Nesting depth: top-level subagent = 1. */
274
298
  depth?: number;
275
299
  /**
@@ -350,6 +374,9 @@ export interface NotificationDetails {
350
374
  durationMs: number;
351
375
  outputFile?: string;
352
376
  error?: string;
377
+ /** The `Note:` line when the agent ran on the parent model because its configured one did not resolve. */
378
+ modelNote?: string;
379
+ modelTier?: TierDisclosure;
353
380
  resultPreview: string;
354
381
  /** Additional agents in a group notification. */
355
382
  others?: NotificationDetails[];
@@ -14,6 +14,7 @@ import { pathToFileURL } from "node:url";
14
14
  import { type Component, hyperlink, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
15
15
  import { type Clock, realClock, type TimerHandle } from "../clock.js";
16
16
  import { formatClock } from "../model-call.js";
17
+ import { modelSelectionNote } from "../model-selection.js";
17
18
  import type { AgentRecord } from "../types.js";
18
19
  import { getLifetimeCost, getLifetimeTotal } from "../usage.js";
19
20
  import {
@@ -26,6 +27,7 @@ import {
26
27
  SPINNER,
27
28
  type Theme,
28
29
  } from "./agent-widget.js";
30
+ import { fallbackFlag, withLeadingFlag } from "./fallback-flag.js";
29
31
 
30
32
  /** Lines of the result shown when expanded; the rest is one `get_subagent_result` away. */
31
33
  const EXPANDED_RESULT_LINES = 50;
@@ -89,6 +91,7 @@ export function agentRowSummary(
89
91
  live: AgentRowLive,
90
92
  theme: Theme,
91
93
  now: number = Date.now(),
94
+ width?: number,
92
95
  ): string {
93
96
  const record = details.agentId ? live.getRecord(details.agentId) : undefined;
94
97
  const activity = details.agentId ? live.getActivity(details.agentId) : undefined;
@@ -106,7 +109,10 @@ export function agentRowSummary(
106
109
  if (tokenText) parts.push(tokenText);
107
110
  if (cost) parts.push(cost);
108
111
  if (!finished && details.backgroundHint && elapsed >= 2000) parts.push("(ctrl+b to run in background)");
109
- return `${icon} ${parts.map(p => fgPreservingNestedStyles(theme, "dim", p)).join(` ${theme.fg("dim", "·")} `)}`;
112
+ // Leading, ahead of the status text, which may carry a 300-char error.
113
+ const flag = fallbackFlag(theme, record?.modelFallback || record?.modelTier ? record : details);
114
+ const body = parts.map(p => fgPreservingNestedStyles(theme, "dim", p)).join(` ${theme.fg("dim", "·")} `);
115
+ return width === undefined ? `${icon} ${flag ? `${flag} ` : ""}${body}` : withLeadingFlag(`${icon} `, flag, body, width);
110
116
  }
111
117
 
112
118
  /**
@@ -116,7 +122,9 @@ export function agentRowSummary(
116
122
  */
117
123
  function tagsLine(details: AgentDetails, theme: Theme): string | undefined {
118
124
  const parts = [...(details.modelName ? [details.modelName] : []), ...(details.tags ?? [])];
119
- return parts.length > 0 ? theme.fg("dim", `↳ ${parts.join(" · ")}`) : undefined;
125
+ const flag = fallbackFlag(theme, details);
126
+ if (parts.length === 0 && !flag) return undefined;
127
+ return `${theme.fg("dim", "↳")} ${flag ? `${flag} ` : ""}${theme.fg("dim", parts.join(" · "))}`;
120
128
  }
121
129
 
122
130
  /** The transcript as an OSC 8 hyperlink, so a click opens it. */
@@ -189,12 +197,14 @@ export class AgentResultView implements Component {
189
197
  ) {}
190
198
 
191
199
  render(width: number): string[] {
192
- const lines = [truncateToWidth(agentRowSummary(this.details, this.live, this.theme, this.clock.now()), width)];
200
+ const lines = [agentRowSummary(this.details, this.live, this.theme, this.clock.now(), width)];
193
201
  if (!this.expanded) return lines;
194
202
 
195
203
  const record = this.details.agentId ? this.live.getRecord(this.details.agentId) : undefined;
196
204
  const tags = tagsLine(this.details, this.theme);
197
205
  if (tags) lines.push(truncateToWidth(tags, width));
206
+ const note = modelSelectionNote(record) || this.details.modelNote;
207
+ if (note) lines.push(...wrapTextWithAnsi(this.theme.fg("warning", note), width));
198
208
  const transcript = record?.outputFile ?? this.details.outputFile;
199
209
  if (transcript) lines.push(truncateToWidth(transcriptLink(transcript, this.theme), width));
200
210
 
@@ -11,8 +11,11 @@ import { type AgentManager, isTopLevelAgent } from "../agent-manager.js";
11
11
  import { getConfig } from "../agent-types.js";
12
12
  import { type Clock, realClock, type TimerHandle } from "../clock.js";
13
13
  import { describeModelCall, type ModelCallState } from "../model-call.js";
14
+ import type { ModelFallback } from "../model-resolver.js";
15
+ import type { TierDisclosure } from "../model-tiers.js";
14
16
  import type { AgentInvocation, SubagentType, WidgetMode } from "../types.js";
15
17
  import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
18
+ import { fallbackFlag } from "./fallback-flag.js";
16
19
 
17
20
  // ---- Constants ----
18
21
 
@@ -83,6 +86,11 @@ export interface AgentDetails {
83
86
  backgroundHint?: boolean;
84
87
  /** Short label for the model the run used, e.g. "haiku 4.5". */
85
88
  modelName?: string;
89
+ /** Set when a configured model did not resolve and the run fell back to the parent model. */
90
+ modelNote?: string;
91
+ /** The structured form of {@link modelNote}, for the one-line label. */
92
+ modelFallback?: ModelFallback;
93
+ modelTier?: TierDisclosure;
86
94
  /** Notable config tags (e.g. ["thinking: high", "isolated"]). */
87
95
  tags?: string[];
88
96
  /** Current turn count. */
@@ -389,7 +397,7 @@ export class AgentWidget {
389
397
  }
390
398
 
391
399
  /** Render a finished agent line. */
392
- private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string; lifetimeUsage?: LifetimeUsage }, theme: Theme): string {
400
+ private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string; lifetimeUsage?: LifetimeUsage; modelFallback?: ModelFallback; modelTier?: TierDisclosure }, theme: Theme): string {
393
401
  const modeLabel = getPromptModeLabel(a.type);
394
402
  const duration = formatMs((a.completedAt ?? this.clock.now()) - a.startedAt);
395
403
 
@@ -418,6 +426,7 @@ export class AgentWidget {
418
426
  const activity = this.agentActivity.get(a.id);
419
427
  if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
420
428
  if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
429
+ // Not gated by `showModel`: a substituted model is never a cosmetic detail.
421
430
  // From the record, not the activity tracker: that entry is deleted the
422
431
  // moment an agent finishes, and "what did it cost" is a question asked
423
432
  // about finished agents.
@@ -426,7 +435,8 @@ export class AgentWidget {
426
435
  parts.push(duration);
427
436
 
428
437
  const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
429
- return `${icon} ${renderAgentName(a.type, theme, { fallbackColor: "dim" })}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
438
+ const flag = fallbackFlag(theme, a) ? `${fallbackFlag(theme, a)} ` : "";
439
+ return `${icon} ${flag}${renderAgentName(a.type, theme, { fallbackColor: "dim" })}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
430
440
  }
431
441
 
432
442
  /**
@@ -466,6 +476,7 @@ export class AgentWidget {
466
476
  for (const a of running) {
467
477
  const modeLabel = getPromptModeLabel(a.type);
468
478
  const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
479
+ const flag = fallbackFlag(theme, a) ? `${fallbackFlag(theme, a)} ` : "";
469
480
  const elapsed = formatMs(this.clock.now() - a.startedAt);
470
481
 
471
482
  const bg = this.agentActivity.get(a.id);
@@ -489,7 +500,7 @@ export class AgentWidget {
489
500
  const thinkingTag = tags.find(tag => tag.startsWith("thinking: "));
490
501
  if (thinkingTag) parts.push(thinkingTag);
491
502
  }
492
- if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
503
+ if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
493
504
  if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
494
505
  if (tokenText) parts.push(tokenText);
495
506
  if (costText) parts.push(costText);
@@ -499,7 +510,7 @@ export class AgentWidget {
499
510
  const activity = bg ? describeActivity(bg.activeTools, bg.responseText, bg.modelCall, this.clock.now()) : "thinking…";
500
511
 
501
512
  runningLines.push([
502
- truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
513
+ truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${flag}${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
503
514
  truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
504
515
  ]);
505
516
  }
@@ -14,6 +14,7 @@ import type { AgentRecord, ViewerMarkdownMode } from "../types.js";
14
14
  import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent } from "../usage.js";
15
15
  import type { Theme } from "./agent-widget.js";
16
16
  import { type AgentActivity, buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatCost, formatDuration, formatSessionTokens, getPromptModeLabel } from "./agent-widget.js";
17
+ import { fallbackFlag } from "./fallback-flag.js";
17
18
  import { createViewerKeys, type ViewerKeybindings, type ViewerKeys } from "./viewer-keys.js";
18
19
 
19
20
  /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
@@ -727,9 +728,11 @@ export class ConversationViewer implements Component, Focusable {
727
728
  // models whose short names read alike.
728
729
  const { modelName, modelId, tags } = buildInvocationTags(this.record.invocation);
729
730
  const model = modelId ?? modelName;
730
- const parts = model ? [model, ...tags] : tags;
731
- if (parts.length === 0) return undefined;
732
- return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
731
+ const flag = fallbackFlag(this.theme, this.record);
732
+ const parts = [...(model ? [model] : []), ...tags];
733
+ if (parts.length === 0 && !flag) return undefined;
734
+ // The flag leads the (possibly very long) canonical model id.
735
+ return `${this.theme.fg("dim", " ↳")} ${flag ? `${flag} ` : ""}${this.theme.fg("dim", parts.join(" · "))}`;
733
736
  }
734
737
 
735
738
  private buildContentLines(width: number): string[] {
@@ -0,0 +1,90 @@
1
+ /**
2
+ * fallback-flag.ts -- the ONE compact indicator for "this agent ran on another
3
+ * model than configured", shared by every renderer that shows model info.
4
+ *
5
+ * Rows are `label + stats` clamped from the right, so anything appended after
6
+ * unbounded text (a description, an error, a canonical model id) is the first
7
+ * thing a narrow terminal cuts. The flag is therefore always rendered FIRST and
8
+ * the rest is truncated around it. Renderers must use these helpers rather than
9
+ * building their own marker; `test/fallback-flag-renderers.test.ts` walks every
10
+ * renderer and fails the ones that do not.
11
+ */
12
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
13
+ import type { TierDisclosure } from "../model-tiers.js";
14
+
15
+ /** Minimal theme surface, so this module has no dependency on the widgets that use it. */
16
+ interface FlagTheme {
17
+ fg(color: string, text: string): string;
18
+ }
19
+
20
+ /** Anything that can carry the fallback: a record, details or notice details. */
21
+ export interface FallbackCarrier {
22
+ modelFallback?: unknown;
23
+ modelNote?: string;
24
+ modelTier?: TierDisclosure;
25
+ }
26
+
27
+ /** Whether this tier's answer differs from the member/profile admitted for it. */
28
+ function switchedWithinTier(tier: TierDisclosure): boolean {
29
+ if (tier.kind !== "routed" && tier.kind !== "static") return false;
30
+ const baseline = tier.kind === "routed" ? tier.admitted : tier.member;
31
+ return baseline !== undefined && tier.answered !== undefined && tier.answered !== baseline;
32
+ }
33
+
34
+ /** Whether `carrier` needs an actionable fallback warning, not merely tier identity. */
35
+ export const hasFallback = (carrier: FallbackCarrier | undefined): boolean => {
36
+ if (carrier?.modelFallback !== undefined) return true;
37
+ const tier = carrier?.modelTier;
38
+ if (!tier) return carrier?.modelNote !== undefined;
39
+ return tier.kind === "unmapped" || tier.kind === "unavailable" || switchedWithinTier(tier);
40
+ };
41
+
42
+ /** Plain tier identity/outcome flag, or `[fallback]`; group counts retain the same identity. */
43
+ export function fallbackFlagText(count = 1, carrier?: FallbackCarrier): string {
44
+ const tier = carrier?.modelTier;
45
+ const suffix = count > 1 ? ` x${count}` : "";
46
+ return tier ? `[@${tier.tier}${tier.kind !== "routed" && tier.kind !== "static" ? ` ${tier.kind}` : ""}${suffix}]`
47
+ : `[fallback${suffix}]`;
48
+ }
49
+
50
+ /** The themed tier identity or actionable fallback flag for `carrier`. */
51
+ export function fallbackFlag(theme: FlagTheme, carrier: FallbackCarrier | undefined, count = 1): string {
52
+ if (carrier?.modelTier) return theme.fg(hasFallback(carrier) ? "warning" : "dim", fallbackFlagText(count, carrier));
53
+ return hasFallback(carrier) ? theme.fg("warning", fallbackFlagText(count, carrier)) : "";
54
+ }
55
+
56
+ /** What a flag degrades to when even it does not fit; plain so it cannot depend on a theme. */
57
+ const MINIMAL_FLAGS = ["[fb]", "!"] as const;
58
+
59
+ /**
60
+ * `prefix + flag + rest`, never wider than `width`, and the flag is never cut.
61
+ *
62
+ * Normal case: the prefix and the flag are reserved first and `rest` (a string,
63
+ * or a function of the width left for it) is truncated to what remains.
64
+ *
65
+ * Narrow case, when `prefix + flag + " "` exceeds `width`: the prefix and then
66
+ * the rest are dropped before the flag is. If the flag alone still does not
67
+ * fit, it degrades to a plain `[fb]`, then `!`. Only a zero width renders
68
+ * nothing.
69
+ */
70
+ export function withLeadingFlag(
71
+ prefix: string,
72
+ flag: string,
73
+ rest: string | ((inner: number) => string),
74
+ width: number,
75
+ ): string {
76
+ const lead = flag ? `${prefix}${flag} ` : prefix;
77
+ if (!flag || visibleWidth(lead) <= width) {
78
+ const inner = Math.max(0, width - visibleWidth(lead));
79
+ const body = typeof rest === "function" ? rest(inner) : truncateToWidth(rest, inner);
80
+ return truncateToWidth(lead + body, width);
81
+ }
82
+ const flagWidth = visibleWidth(flag);
83
+ if (flagWidth <= width) {
84
+ const inner = width - flagWidth - 1;
85
+ if (inner < 1) return flag;
86
+ const body = typeof rest === "function" ? rest(inner) : truncateToWidth(rest, inner);
87
+ return truncateToWidth(`${flag} ${body}`, width);
88
+ }
89
+ return MINIMAL_FLAGS.find(m => visibleWidth(m) <= width) ?? "";
90
+ }