@pi-unipi/unipi 2.20.2 → 2.20.4

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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.20.4] — 2026-09-19
10
+
11
+ ### Changed
12
+
13
+ - `fusion`: **the `/unipi:model` picker name column now grows with the terminal.** `NAME_COL` was a fixed 24 columns, so every name past 23 characters was ellipsized even on a wide terminal and the thinking-effort control sat close to the name. `packages/fusion/src/picker.ts` now computes the column per render from the widest row label (`NAME_COL_MIN = 24`, capped to the available width) and keeps the effort bar right-anchored, so full names fit before the effort control.
14
+
15
+ ### Fixed
16
+
17
+ - `fusion`: **model rows that look like a `provider/id` key are no longer ambiguous.** Rows label with `model.name || model.id` and never showed the provider, so an OpenRouter model named `deepseek/deepseek-v4.1-flash` was indistinguishable from a model actually served by provider `deepseek`. A dim `provider · ` prefix is now added only when the display name contains `/` or the same name is offered by more than one provider; the prefix is never truncated (the friendly name truncates instead). The highlighted row's price panel now spells out the exact registry key (`Model key openrouter/deepseek/deepseek-v4.1-flash`, both lead and sidekick keys on the Fusion row).
18
+
19
+ ## [2.20.3] — 2026-09-17
20
+
21
+ ### Fixed
22
+
23
+ - `background-tasks`: **distinguish persistent dev servers from finite jobs in `bg_run` prompt guidelines.** The prompt guidance previously grouped dev servers and test suites under the same "do not set `triggerOnCompletion:false`" rule. For persistent servers and watchers (e.g. `npm run dev`, `vite`), `triggerOnCompletion: true` caused the UI to show an indefinite "waiting on 1 bg task" spinner and caused `SidekickRuntime` in Fusion to hang waiting for `openBgTasks` to reach zero. `BgRunParams` and `promptGuidelines` in `packages/background-tasks/src/tools.ts` now explicitly instruct agents to set `triggerOnCompletion: false` for long-running servers/daemons so neither the harness nor sidekicks wait indefinitely for exit.
24
+
9
25
  ## [2.20.2] — 2026-09-17
10
26
 
11
27
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -84,29 +84,29 @@
84
84
  "typebox": "^1.1.38"
85
85
  },
86
86
  "dependencies": {
87
- "@pi-unipi/ask-user": "2.20.2",
88
- "@pi-unipi/background-tasks": "2.20.2",
89
- "@pi-unipi/btw": "2.20.2",
90
- "@pi-unipi/command-enchantment": "2.20.2",
91
- "@pi-unipi/compactor": "2.20.2",
92
- "@pi-unipi/core": "2.20.2",
93
- "@pi-unipi/footer": "2.20.2",
94
- "@pi-unipi/image": "2.20.2",
95
- "@pi-unipi/fusion": "2.20.2",
96
- "@pi-unipi/info-screen": "2.20.2",
97
- "@pi-unipi/input-shortcuts": "2.20.2",
98
- "@pi-unipi/kanboard": "2.20.2",
99
- "@pi-unipi/mcp": "2.20.2",
100
- "@pi-unipi/memory": "2.20.2",
101
- "@pi-unipi/milestone": "2.20.2",
102
- "@pi-unipi/notify": "2.20.2",
103
- "@pi-unipi/ralph": "2.20.2",
104
- "@pi-unipi/subagents": "2.20.2",
105
- "@pi-unipi/trajectory": "2.20.2",
106
- "@pi-unipi/updater": "2.20.2",
107
- "@pi-unipi/utility": "2.20.2",
108
- "@pi-unipi/web-api": "2.20.2",
109
- "@pi-unipi/workflow": "2.20.2"
87
+ "@pi-unipi/ask-user": "2.20.4",
88
+ "@pi-unipi/background-tasks": "2.20.4",
89
+ "@pi-unipi/btw": "2.20.4",
90
+ "@pi-unipi/command-enchantment": "2.20.4",
91
+ "@pi-unipi/compactor": "2.20.4",
92
+ "@pi-unipi/core": "2.20.4",
93
+ "@pi-unipi/footer": "2.20.4",
94
+ "@pi-unipi/image": "2.20.4",
95
+ "@pi-unipi/fusion": "2.20.4",
96
+ "@pi-unipi/info-screen": "2.20.4",
97
+ "@pi-unipi/input-shortcuts": "2.20.4",
98
+ "@pi-unipi/kanboard": "2.20.4",
99
+ "@pi-unipi/mcp": "2.20.4",
100
+ "@pi-unipi/memory": "2.20.4",
101
+ "@pi-unipi/milestone": "2.20.4",
102
+ "@pi-unipi/notify": "2.20.4",
103
+ "@pi-unipi/ralph": "2.20.4",
104
+ "@pi-unipi/subagents": "2.20.4",
105
+ "@pi-unipi/trajectory": "2.20.4",
106
+ "@pi-unipi/updater": "2.20.4",
107
+ "@pi-unipi/utility": "2.20.4",
108
+ "@pi-unipi/web-api": "2.20.4",
109
+ "@pi-unipi/workflow": "2.20.4"
110
110
  },
111
111
  "devDependencies": {
112
112
  "@earendil-works/pi-agent-core": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "2.20.2"
43
+ "@pi-unipi/core": "2.20.4"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-ai": "^0.84.0",
13
- "@pi-unipi/core": "2.20.2",
13
+ "@pi-unipi/core": "2.20.4",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -52,13 +52,13 @@ export const BgRunParams = Type.Object({
52
52
  notifyOnCompletion: Type.Optional(
53
53
  Type.Boolean({
54
54
  description:
55
- "Deliver a durable terminal notification when the task finishes. Default true. Do not disable unless opting out of completion handling.",
55
+ "Deliver a durable terminal notification when the task finishes. Default true for finite tasks. Set false or pair with triggerOnCompletion:false for persistent servers/daemons/watchers.",
56
56
  }),
57
57
  ),
58
58
  triggerOnCompletion: Type.Optional(
59
59
  Type.Boolean({
60
60
  description:
61
- "Start a follow-up agent turn on terminal notification. Default true for bg_run. Requires notifyOnCompletion.",
61
+ "Start a follow-up agent turn on terminal notification. Default true for finite tasks (tests, builds, exports). MUST set false for persistent background services/servers (e.g. dev servers, watchers) so the harness and subagents do not wait indefinitely for the server to finish. Requires notifyOnCompletion.",
62
62
  }),
63
63
  ),
64
64
  });
@@ -185,14 +185,14 @@ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void
185
185
  promptSnippet:
186
186
  "Start a named long-running shell command; default terminal notification wakes a follow-up turn, so yield instead of polling",
187
187
  promptGuidelines: [
188
- "Use bg_run instead of bash for commands expected to run for a long time, such as test suites, dev servers, watchers, or builds.",
188
+ "Use bg_run instead of bash for commands expected to run in the background (test suites, builds, dev servers, watchers).",
189
+ "For FINITE tasks (tests, builds, exports, scripts): leave triggerOnCompletion:true and notifyOnCompletion:true (defaults) to automatically wake the agent when finished.",
190
+ "For PERSISTENT servers/daemons/watchers (e.g. 'npm run dev', vite, live dev servers, background listeners): MUST set triggerOnCompletion:false (and optionally notifyOnCompletion:false) so the agent and sidekick do not wait for the server to exit.",
189
191
  "Set isAgent:true only when the background task launches an LLM/agent process; false for scripts, tests, dev servers, sleeps.",
190
192
  "Always set name to a concise 2-6 word human-readable label; do not use the raw command as the name.",
191
- "bg_run returns immediately. With notifyOnCompletion:true and triggerOnCompletion:true (both defaults), completed, failed, or killed terminal state is delivered as <background-task-notification> and automatically starts a follow-up agent turn.",
192
- "After a default bg_run launch, continue only independent useful work that does not merely wait for the task; otherwise briefly acknowledge it if useful, then end the current turn. Do not call sleep, bg_status, or bg_logs merely to wait; the terminal notification will wake you.",
193
+ "bg_run returns immediately. For finite tasks with default wake-up, continue only independent useful work that does not merely wait for the task; otherwise end the current turn. Do not call sleep, bg_status, or bg_logs merely to wait; the terminal notification will wake you.",
193
194
  "A running result is not an instruction to poll again.",
194
195
  "Treat <background-task-notification> as durable terminal truth. Do not call bg_status to reconfirm it.",
195
- "Do not set notifyOnCompletion:false or triggerOnCompletion:false unless intentionally opting out.",
196
196
  ],
197
197
  parameters: BgRunParams,
198
198
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.84.0"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "2.20.2"
40
+ "@pi-unipi/core": "2.20.4"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.20.2",
38
- "@pi-unipi/info-screen": "2.20.2",
37
+ "@pi-unipi/core": "2.20.4",
38
+ "@pi-unipi/info-screen": "2.20.4",
39
39
  "@earendil-works/pi-agent-core": "^0.84.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.20.2",
36
- "@pi-unipi/background-tasks": "2.20.2"
35
+ "@pi-unipi/core": "2.20.4",
36
+ "@pi-unipi/background-tasks": "2.20.4"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -10,10 +10,9 @@ pi's extension and RPC APIs natively.
10
10
  ╭ Model ───────────────────────────────────────────────────────────────╮
11
11
  │/ Type to search │
12
12
  │───────────────────────────────────────────────────────────────────────│
13
- │❭ Fusion ← ▰▰▰▱▱ → Medium Lead Claude Fable… ▾
14
- │· GLM-5.3 Flash ✱ ▰▰▰▰▰ Max
15
- │· Claude Fable 5.1 ▰▰▰▱▱ Medium
16
- │· DeepSeek V4.1 Flash ✱ ▰▱▱▱▱ High │
13
+ │❭ Fusion ← ▰▰▰▱▱ → Medium Lead Fable… ▾
14
+ │· GLM-5.3 Flash ✱ ▰▰▰▰▰ Max
15
+ │· openrouter · deepseek/deepseek-v4.1-flash ▰▰▱▱▱ High
17
16
  │ ↓ more below │
18
17
  │ │
19
18
  │ ━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
@@ -21,6 +20,7 @@ pi's extension and RPC APIs natively.
21
20
  │ $10 / 1M $0.25 / 1M $50 / 1M $0.2 / 1M │
22
21
  │ Sidekick cached input Sidekick output │
23
22
  │ $0.02 / 1M $1.2 / 1M │
23
+ │ Model key openrouter/deepseek/deepseek-v4.1-flash │
24
24
  │ │
25
25
  │ ✱ New ✱ Promotion ✱ Beta · Pairs frontier intelligence with cost-efficient execution │
26
26
  │↑↓ select · tab lead · ←→ effort · ↵ confirm · esc cancel │
@@ -44,7 +44,16 @@ pi's extension and RPC APIs natively.
44
44
  - **Price panel**: the highlighted model's blended price is marked on a
45
45
  logarithmic red→orange→yellow→green→cyan→blue→violet slider. Fusion shows
46
46
  `Input`, `Cached input`, `Output`, `Sidekick input`, `Sidekick cached input`,
47
- and `Sidekick output`.
47
+ and `Sidekick output`, and the exact registry key of the highlighted row is
48
+ spelled out (`Model key openrouter/deepseek/deepseek-v4.1-flash`; both the
49
+ lead and sidekick keys on the Fusion row).
50
+ - **Name column & provider**: the name column sizes itself to the widest row
51
+ label (minimum 24 columns) up to the terminal width, and the effort control
52
+ stays right-anchored — so wide terminals show full names. A row only gets a
53
+ dim `provider · ` prefix when its display name is ambiguous: it contains a
54
+ `/` (`deepseek/deepseek-v4.1-flash` reads like a key) or the same friendly
55
+ name is offered by more than one provider. Unique names stay clean, and the
56
+ prefix is never truncated.
48
57
  - **Confirm** applies: `pi.setModel`, `pi.setThinkingLevel(effort)`, updates
49
58
  MRU + persisted active selection, and shows `Fusion · Lead ◆ Sidekick` in
50
59
  the footer. Switching the model through pi's own `/model`/Ctrl+P drops
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/fusion",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.20.2",
36
- "@pi-unipi/subagents": "2.20.2"
35
+ "@pi-unipi/core": "2.20.4",
36
+ "@pi-unipi/subagents": "2.20.4"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-ai": "^0.84.0",
@@ -109,9 +109,39 @@ type Row = { kind: "fusion" } | { kind: "model"; key: ModelKey };
109
109
  type FusionFocus = "effort" | "lead" | "sidekick";
110
110
 
111
111
  const DEFAULT_VISIBLE_ROWS = 10;
112
- const NAME_COL = 24;
112
+ /** Minimum width of the name column (the pre-autosize default). */
113
+ const NAME_COL_MIN = 24;
113
114
  const MARKER_COL = 2;
114
115
  const BAR_SEGMENTS = 5;
116
+ /** Visible columns outside the name: pointer + marker prefix (4) + effort block (19). */
117
+ const FIXED_COLS = 23;
118
+
119
+ /**
120
+ * A model needs a provider prefix when its display name alone is ambiguous:
121
+ * it contains a "/" (so it reads like a `provider/id` key), or the same name
122
+ * is offered by more than one provider.
123
+ */
124
+ function ambiguousNamesOf(models: readonly PickerModel[]): Set<string> {
125
+ const providersByName = new Map<string, Set<string>>();
126
+ for (const m of models) {
127
+ const set = providersByName.get(m.name) ?? new Set<string>();
128
+ set.add(m.provider);
129
+ providersByName.set(m.name, set);
130
+ }
131
+ const out = new Set<string>();
132
+ for (const [name, set] of providersByName) {
133
+ if (set.size > 1) out.add(name);
134
+ }
135
+ return out;
136
+ }
137
+
138
+ function needsProvider(m: PickerModel, ambiguous: ReadonlySet<string>): boolean {
139
+ return m.name.includes("/") || ambiguous.has(m.name);
140
+ }
141
+
142
+ function labelOf(m: PickerModel, ambiguous: ReadonlySet<string>): string {
143
+ return needsProvider(m, ambiguous) ? `${m.provider} \u00b7 ${m.name}` : m.name;
144
+ }
115
145
 
116
146
  function printable(data: string): string | undefined {
117
147
  if (data.length !== 1) return undefined;
@@ -146,6 +176,9 @@ export class ModelPicker {
146
176
  private readonly modelsByKey: Map<ModelKey, PickerModel>;
147
177
  private readonly state: PickerState;
148
178
  private readonly priceRange: { min: number; max: number };
179
+ private readonly ambiguousNames: Set<string>;
180
+ /** Name column sized to the widest row label; capped to the available width at render time. */
181
+ private readonly naturalNameCol: number;
149
182
 
150
183
  private effort: Record<ModelKey, EffortLevel>;
151
184
  /** Fusion-row efforts — deliberately NOT stored in the per-model map. */
@@ -166,6 +199,12 @@ export class ModelPicker {
166
199
  this.onRenderRequest = options.onRenderRequest;
167
200
  this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
168
201
  this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
202
+ this.ambiguousNames = ambiguousNamesOf(options.state.models);
203
+ const widestLabel = options.state.models.reduce(
204
+ (max, m) => Math.max(max, visibleWidth(labelOf(m, this.ambiguousNames))),
205
+ 0,
206
+ );
207
+ this.naturalNameCol = Math.max(NAME_COL_MIN, widestLabel + 2);
169
208
  const prices = options.state.models.map((m) => (hasPricing(m.cost) ? blendedPrice(m.cost) : undefined)).filter((p): p is number => p !== undefined && p > 0);
170
209
  this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
171
210
  this.effort = { ...options.state.effort };
@@ -416,7 +455,11 @@ export class ModelPicker {
416
455
  return shortName(m?.name ?? key, max);
417
456
  }
418
457
 
419
- private renderRow(row: Row, highlighted: boolean, width: number): string {
458
+ private isAmbiguous(model: PickerModel): boolean {
459
+ return needsProvider(model, this.ambiguousNames);
460
+ }
461
+
462
+ private renderRow(row: Row, highlighted: boolean, width: number, nameCol: number): string {
420
463
  const t = this.theme;
421
464
  const pointer = highlighted ? t.fg("accent", "❭") : t.fg("dim", "·");
422
465
  const disabledFusion = row.kind === "fusion" && !this.fusionAvailable();
@@ -429,8 +472,14 @@ export class ModelPicker {
429
472
  : " "
430
473
  : this.markerFor(row.key);
431
474
  const working = row.kind === "model" && row.key === this.state.currentModelKey;
432
- const nameRaw = row.kind === "fusion" ? "Fusion" : this.nameOf(row.key, NAME_COL - 1);
433
- const name =
475
+ const model = row.kind === "model" ? this.modelsByKey.get(row.key) : undefined;
476
+ // Only ambiguous rows (slash-y names, or names shared across providers) pay
477
+ // the width cost of a dim provider prefix. The name is truncated, never the
478
+ // prefix, so `openrouter · …` always survives.
479
+ const providerPrefix = model !== undefined && this.isAmbiguous(model) ? t.fg("dim", `${model.provider} · `) : "";
480
+ const nameBudget = Math.max(1, nameCol - visibleWidth(providerPrefix) - 1);
481
+ const nameRaw = row.kind === "fusion" ? "Fusion" : this.nameOf(row.key, nameBudget);
482
+ const styled =
434
483
  row.kind === "fusion"
435
484
  ? disabledFusion
436
485
  ? highlighted
@@ -444,7 +493,7 @@ export class ModelPicker {
444
493
  : highlighted
445
494
  ? t.fg("accent", nameRaw)
446
495
  : t.fg("text", nameRaw);
447
- const model = row.kind === "model" ? this.modelsByKey.get(row.key) : undefined;
496
+ const name = `${providerPrefix}${styled}`;
448
497
  const badge = model?.badge;
449
498
  const badgeGlyph = badge === undefined ? "" : ` ${t.fg(badge === "new" ? "success" : badge === "promotion" ? "accent" : "warning", "✱")}`;
450
499
 
@@ -458,7 +507,7 @@ export class ModelPicker {
458
507
  ? t.fg("accent", effortLabel(level))
459
508
  : t.fg("muted", effortLabel(level));
460
509
 
461
- let line = `${pointer} ${marker} ${pad(`${name}${badgeGlyph}`, NAME_COL)} ${left} ${this.bar(level, !disabledFusion && highlighted)} ${right} ${pad(label, 8)}`;
510
+ let line = `${pointer} ${marker} ${pad(`${name}${badgeGlyph}`, nameCol)} ${left} ${this.bar(level, !disabledFusion && highlighted)} ${right} ${pad(label, 8)}`;
462
511
 
463
512
  if (row.kind === "fusion") {
464
513
  if (disabledFusion) {
@@ -480,10 +529,10 @@ export class ModelPicker {
480
529
  return truncateToWidth(line, Math.max(1, width - 1));
481
530
  }
482
531
 
483
- private renderDropdown(width: number): string[] {
532
+ private renderDropdown(width: number, nameCol: number): string[] {
484
533
  const t = this.theme;
485
534
  const items = this.dropdownItems();
486
- const indent = " ".repeat(MARKER_COL + NAME_COL + 5);
535
+ const indent = " ".repeat(MARKER_COL + nameCol + 5);
487
536
  if (items.length === 0) {
488
537
  return [`${indent}${t.fg("warning", `no ${this.focus} models in preset — run /unipi:fusion-preset`)}`];
489
538
  }
@@ -547,7 +596,23 @@ export class ModelPicker {
547
596
  : !hasPricing(primaryCost);
548
597
  const pricing = !disabledFusion && noPricing ? t.fg("dim", " · no pricing data from provider") : "";
549
598
  const description = `${badges}${badges.length > 0 ? " " : ""}${desc}${pricing}`;
550
- return [truncateToWidth(` ${head}`, width - 1), truncateToWidth(` ${vals}`, width - 1), truncateToWidth(` ${description}`, width - 1)];
599
+ // The row label may be a friendly name; spell out the exact registry key so
600
+ // the highlighted model is unambiguous (`openrouter/deepseek/...`).
601
+ const keys = row.kind === "fusion"
602
+ ? [this.lead, this.sidekick].filter((k): k is ModelKey => k !== undefined)
603
+ : primaryKey === undefined
604
+ ? []
605
+ : [primaryKey];
606
+ const out = [
607
+ truncateToWidth(` ${head}`, width - 1),
608
+ truncateToWidth(` ${vals}`, width - 1),
609
+ ];
610
+ if (keys.length > 0) {
611
+ const keyText = keys.map((k) => t.fg("text", k)).join(t.fg("dim", " · "));
612
+ out.push(truncateToWidth(` ${t.fg("dim", "Model key")} ${keyText}`, width - 1));
613
+ }
614
+ out.push(truncateToWidth(` ${description}`, width - 1));
615
+ return out;
551
616
  }
552
617
 
553
618
  private hintLine(row: Row | undefined): string {
@@ -572,6 +637,10 @@ export class ModelPicker {
572
637
  private renderBody(width: number): string[] {
573
638
  const t = this.theme;
574
639
  const rows = this.rows();
640
+ // Give the name column every spare column (the effort control stays
641
+ // right-anchored) but never less than the historical 24.
642
+ const maxNameCol = Math.max(NAME_COL_MIN, width - FIXED_COLS - 1);
643
+ const nameCol = Math.min(this.naturalNameCol, maxNameCol);
575
644
  if (this.selected >= rows.length) this.selected = Math.max(0, rows.length - 1);
576
645
  const row = rows[this.selected];
577
646
  const lines: string[] = [];
@@ -591,9 +660,9 @@ export class ModelPicker {
591
660
  const r = rows[i];
592
661
  if (r === undefined) continue;
593
662
  const highlighted = i === this.selected;
594
- lines.push(this.renderRow(r, highlighted, width));
663
+ lines.push(this.renderRow(r, highlighted, width, nameCol));
595
664
  if (highlighted && r.kind === "fusion" && this.focus !== "effort") {
596
- lines.push(...this.renderDropdown(width));
665
+ lines.push(...this.renderDropdown(width, nameCol));
597
666
  }
598
667
  }
599
668
  if (end < rows.length) lines.push(t.fg("dim", ` ↓ more below (${String(rows.length - end)})`));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Image generation and image recognition tools for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.20.2"
37
+ "@pi-unipi/core": "2.20.4"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.20.2"
36
+ "@pi-unipi/core": "2.20.4"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.20.2"
36
+ "@pi-unipi/core": "2.20.4"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.20.2"
42
+ "@pi-unipi/core": "2.20.4"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "test": "npx tsx --test tests/**/*.test.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@pi-unipi/core": "2.20.2"
33
+ "@pi-unipi/core": "2.20.4"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,8 +39,8 @@
39
39
  "README.md"
40
40
  ],
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.20.2",
43
- "@pi-unipi/info-screen": "2.20.2",
42
+ "@pi-unipi/core": "2.20.4",
43
+ "@pi-unipi/info-screen": "2.20.4",
44
44
  "js-yaml": "^4.1.0"
45
45
  },
46
46
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/milestone",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.20.2"
35
+ "@pi-unipi/core": "2.20.4"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.20.2",
37
+ "@pi-unipi/core": "2.20.4",
38
38
  "node-notifier": "^10.0.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ralph",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Long-running iterative development loops for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,8 +31,8 @@
31
31
  "test": "npx tsx --test reminder.test.ts"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.20.2",
35
- "@pi-unipi/info-screen": "2.20.2"
34
+ "@pi-unipi/core": "2.20.4",
35
+ "@pi-unipi/info-screen": "2.20.4"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-ai": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/subagents",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Subagents for UniPi — parallel execution, file locking, workflow integration",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -9,7 +9,7 @@
9
9
  "test": "npx tsx --test src/__tests__/*.test.ts"
10
10
  },
11
11
  "dependencies": {
12
- "@pi-unipi/core": "2.20.2",
12
+ "@pi-unipi/core": "2.20.4",
13
13
  "@earendil-works/pi-agent-core": "^0.84.0",
14
14
  "acorn": "8.18.0"
15
15
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/trajectory",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Live UniPi trajectory inspector for Pi sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -22204,12 +22204,12 @@ var BgRunParams = Type4.Object({
22204
22204
  ),
22205
22205
  notifyOnCompletion: Type4.Optional(
22206
22206
  Type4.Boolean({
22207
- description: "Deliver a durable terminal notification when the task finishes. Default true. Do not disable unless opting out of completion handling."
22207
+ description: "Deliver a durable terminal notification when the task finishes. Default true for finite tasks. Set false or pair with triggerOnCompletion:false for persistent servers/daemons/watchers."
22208
22208
  })
22209
22209
  ),
22210
22210
  triggerOnCompletion: Type4.Optional(
22211
22211
  Type4.Boolean({
22212
- description: "Start a follow-up agent turn on terminal notification. Default true for bg_run. Requires notifyOnCompletion."
22212
+ description: "Start a follow-up agent turn on terminal notification. Default true for finite tasks (tests, builds, exports). MUST set false for persistent background services/servers (e.g. dev servers, watchers) so the harness and subagents do not wait indefinitely for the server to finish. Requires notifyOnCompletion."
22213
22213
  })
22214
22214
  )
22215
22215
  });
@@ -22299,14 +22299,14 @@ Command: ${task.command}`,
22299
22299
  description: `Start a named long-running shell command in the background and return immediately with a task ID and output path. By default, terminal state is delivered automatically as <background-task-notification> and starts a follow-up agent turn; do not sleep or poll merely to wait. Output is written under the OS temp root and model-visible logs are bounded.`,
22300
22300
  promptSnippet: "Start a named long-running shell command; default terminal notification wakes a follow-up turn, so yield instead of polling",
22301
22301
  promptGuidelines: [
22302
- "Use bg_run instead of bash for commands expected to run for a long time, such as test suites, dev servers, watchers, or builds.",
22302
+ "Use bg_run instead of bash for commands expected to run in the background (test suites, builds, dev servers, watchers).",
22303
+ "For FINITE tasks (tests, builds, exports, scripts): leave triggerOnCompletion:true and notifyOnCompletion:true (defaults) to automatically wake the agent when finished.",
22304
+ "For PERSISTENT servers/daemons/watchers (e.g. 'npm run dev', vite, live dev servers, background listeners): MUST set triggerOnCompletion:false (and optionally notifyOnCompletion:false) so the agent and sidekick do not wait for the server to exit.",
22303
22305
  "Set isAgent:true only when the background task launches an LLM/agent process; false for scripts, tests, dev servers, sleeps.",
22304
22306
  "Always set name to a concise 2-6 word human-readable label; do not use the raw command as the name.",
22305
- "bg_run returns immediately. With notifyOnCompletion:true and triggerOnCompletion:true (both defaults), completed, failed, or killed terminal state is delivered as <background-task-notification> and automatically starts a follow-up agent turn.",
22306
- "After a default bg_run launch, continue only independent useful work that does not merely wait for the task; otherwise briefly acknowledge it if useful, then end the current turn. Do not call sleep, bg_status, or bg_logs merely to wait; the terminal notification will wake you.",
22307
+ "bg_run returns immediately. For finite tasks with default wake-up, continue only independent useful work that does not merely wait for the task; otherwise end the current turn. Do not call sleep, bg_status, or bg_logs merely to wait; the terminal notification will wake you.",
22307
22308
  "A running result is not an instruction to poll again.",
22308
- "Treat <background-task-notification> as durable terminal truth. Do not call bg_status to reconfirm it.",
22309
- "Do not set notifyOnCompletion:false or triggerOnCompletion:false unless intentionally opting out."
22309
+ "Treat <background-task-notification> as durable terminal truth. Do not call bg_status to reconfirm it."
22310
22310
  ],
22311
22311
  parameters: BgRunParams,
22312
22312
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -50836,9 +50836,29 @@ function renderSlider(cells, markerIndex) {
50836
50836
 
50837
50837
  // packages/fusion/src/picker.ts
50838
50838
  var DEFAULT_VISIBLE_ROWS = 10;
50839
- var NAME_COL = 24;
50839
+ var NAME_COL_MIN = 24;
50840
50840
  var MARKER_COL = 2;
50841
50841
  var BAR_SEGMENTS = 5;
50842
+ var FIXED_COLS = 23;
50843
+ function ambiguousNamesOf(models) {
50844
+ const providersByName = /* @__PURE__ */ new Map();
50845
+ for (const m of models) {
50846
+ const set = providersByName.get(m.name) ?? /* @__PURE__ */ new Set();
50847
+ set.add(m.provider);
50848
+ providersByName.set(m.name, set);
50849
+ }
50850
+ const out = /* @__PURE__ */ new Set();
50851
+ for (const [name, set] of providersByName) {
50852
+ if (set.size > 1) out.add(name);
50853
+ }
50854
+ return out;
50855
+ }
50856
+ function needsProvider(m, ambiguous) {
50857
+ return m.name.includes("/") || ambiguous.has(m.name);
50858
+ }
50859
+ function labelOf(m, ambiguous) {
50860
+ return needsProvider(m, ambiguous) ? `${m.provider} \xB7 ${m.name}` : m.name;
50861
+ }
50842
50862
  function printable(data) {
50843
50863
  if (data.length !== 1) return void 0;
50844
50864
  const code = data.charCodeAt(0);
@@ -50867,6 +50887,9 @@ var ModelPicker = class {
50867
50887
  modelsByKey;
50868
50888
  state;
50869
50889
  priceRange;
50890
+ ambiguousNames;
50891
+ /** Name column sized to the widest row label; capped to the available width at render time. */
50892
+ naturalNameCol;
50870
50893
  effort;
50871
50894
  /** Fusion-row efforts — deliberately NOT stored in the per-model map. */
50872
50895
  fusionLeadEffort;
@@ -50885,6 +50908,12 @@ var ModelPicker = class {
50885
50908
  this.onRenderRequest = options.onRenderRequest;
50886
50909
  this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
50887
50910
  this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
50911
+ this.ambiguousNames = ambiguousNamesOf(options.state.models);
50912
+ const widestLabel = options.state.models.reduce(
50913
+ (max, m) => Math.max(max, visibleWidth23(labelOf(m, this.ambiguousNames))),
50914
+ 0
50915
+ );
50916
+ this.naturalNameCol = Math.max(NAME_COL_MIN, widestLabel + 2);
50888
50917
  const prices = options.state.models.map((m) => hasPricing(m.cost) ? blendedPrice(m.cost) : void 0).filter((p) => p !== void 0 && p > 0);
50889
50918
  this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
50890
50919
  this.effort = { ...options.state.effort };
@@ -51086,15 +51115,21 @@ var ModelPicker = class {
51086
51115
  const m = this.modelsByKey.get(key);
51087
51116
  return shortName(m?.name ?? key, max);
51088
51117
  }
51089
- renderRow(row, highlighted, width) {
51118
+ isAmbiguous(model) {
51119
+ return needsProvider(model, this.ambiguousNames);
51120
+ }
51121
+ renderRow(row, highlighted, width, nameCol) {
51090
51122
  const t = this.theme;
51091
51123
  const pointer = highlighted ? t.fg("accent", "\u276D") : t.fg("dim", "\xB7");
51092
51124
  const disabledFusion = row.kind === "fusion" && !this.fusionAvailable();
51093
51125
  const marker = row.kind === "fusion" ? !disabledFusion && this.state.active?.kind === "fusion" ? t.fg("success", "\u2713") : " " : this.markerFor(row.key);
51094
51126
  const working = row.kind === "model" && row.key === this.state.currentModelKey;
51095
- const nameRaw = row.kind === "fusion" ? "Fusion" : this.nameOf(row.key, NAME_COL - 1);
51096
- const name = row.kind === "fusion" ? disabledFusion ? highlighted ? t.fg("muted", t.bold(nameRaw)) : t.fg("dim", nameRaw) : highlighted ? t.fg("accent", t.bold(nameRaw)) : t.fg("text", nameRaw) : working ? t.fg("accent", t.bold(nameRaw)) : highlighted ? t.fg("accent", nameRaw) : t.fg("text", nameRaw);
51097
51127
  const model = row.kind === "model" ? this.modelsByKey.get(row.key) : void 0;
51128
+ const providerPrefix = model !== void 0 && this.isAmbiguous(model) ? t.fg("dim", `${model.provider} \xB7 `) : "";
51129
+ const nameBudget = Math.max(1, nameCol - visibleWidth23(providerPrefix) - 1);
51130
+ const nameRaw = row.kind === "fusion" ? "Fusion" : this.nameOf(row.key, nameBudget);
51131
+ const styled = row.kind === "fusion" ? disabledFusion ? highlighted ? t.fg("muted", t.bold(nameRaw)) : t.fg("dim", nameRaw) : highlighted ? t.fg("accent", t.bold(nameRaw)) : t.fg("text", nameRaw) : working ? t.fg("accent", t.bold(nameRaw)) : highlighted ? t.fg("accent", nameRaw) : t.fg("text", nameRaw);
51132
+ const name = `${providerPrefix}${styled}`;
51098
51133
  const badge = model?.badge;
51099
51134
  const badgeGlyph = badge === void 0 ? "" : ` ${t.fg(badge === "new" ? "success" : badge === "promotion" ? "accent" : "warning", "\u2731")}`;
51100
51135
  const level = row.kind === "fusion" ? this.fusionLeadEffort : this.effortFor(row.key);
@@ -51102,7 +51137,7 @@ var ModelPicker = class {
51102
51137
  const left = arrowsOn ? t.fg("accent", "\u2190") : " ";
51103
51138
  const right = arrowsOn ? t.fg("accent", "\u2192") : " ";
51104
51139
  const label = disabledFusion ? t.fg("dim", effortLabel(level)) : highlighted ? t.fg("accent", effortLabel(level)) : t.fg("muted", effortLabel(level));
51105
- let line = `${pointer} ${marker} ${pad(`${name}${badgeGlyph}`, NAME_COL)} ${left} ${this.bar(level, !disabledFusion && highlighted)} ${right} ${pad(label, 8)}`;
51140
+ let line = `${pointer} ${marker} ${pad(`${name}${badgeGlyph}`, nameCol)} ${left} ${this.bar(level, !disabledFusion && highlighted)} ${right} ${pad(label, 8)}`;
51106
51141
  if (row.kind === "fusion") {
51107
51142
  if (disabledFusion) {
51108
51143
  line += ` ${t.fg("dim", "not configured \u2014 run /unipi:fusion-preset")}`;
@@ -51118,10 +51153,10 @@ var ModelPicker = class {
51118
51153
  }
51119
51154
  return truncateToWidth26(line, Math.max(1, width - 1));
51120
51155
  }
51121
- renderDropdown(width) {
51156
+ renderDropdown(width, nameCol) {
51122
51157
  const t = this.theme;
51123
51158
  const items = this.dropdownItems();
51124
- const indent = " ".repeat(MARKER_COL + NAME_COL + 5);
51159
+ const indent = " ".repeat(MARKER_COL + nameCol + 5);
51125
51160
  if (items.length === 0) {
51126
51161
  return [`${indent}${t.fg("warning", `no ${this.focus} models in preset \u2014 run /unipi:fusion-preset`)}`];
51127
51162
  }
@@ -51173,7 +51208,17 @@ var ModelPicker = class {
51173
51208
  const noPricing = row.kind === "fusion" ? !hasPricing(primaryCost) || !hasPricing(sideCost) : !hasPricing(primaryCost);
51174
51209
  const pricing = !disabledFusion && noPricing ? t.fg("dim", " \xB7 no pricing data from provider") : "";
51175
51210
  const description = `${badges}${badges.length > 0 ? " " : ""}${desc}${pricing}`;
51176
- return [truncateToWidth26(` ${head}`, width - 1), truncateToWidth26(` ${vals}`, width - 1), truncateToWidth26(` ${description}`, width - 1)];
51211
+ const keys = row.kind === "fusion" ? [this.lead, this.sidekick].filter((k) => k !== void 0) : primaryKey === void 0 ? [] : [primaryKey];
51212
+ const out = [
51213
+ truncateToWidth26(` ${head}`, width - 1),
51214
+ truncateToWidth26(` ${vals}`, width - 1)
51215
+ ];
51216
+ if (keys.length > 0) {
51217
+ const keyText = keys.map((k) => t.fg("text", k)).join(t.fg("dim", " \xB7 "));
51218
+ out.push(truncateToWidth26(` ${t.fg("dim", "Model key")} ${keyText}`, width - 1));
51219
+ }
51220
+ out.push(truncateToWidth26(` ${description}`, width - 1));
51221
+ return out;
51177
51222
  }
51178
51223
  hintLine(row) {
51179
51224
  const t = this.theme;
@@ -51195,6 +51240,8 @@ var ModelPicker = class {
51195
51240
  renderBody(width) {
51196
51241
  const t = this.theme;
51197
51242
  const rows = this.rows();
51243
+ const maxNameCol = Math.max(NAME_COL_MIN, width - FIXED_COLS - 1);
51244
+ const nameCol = Math.min(this.naturalNameCol, maxNameCol);
51198
51245
  if (this.selected >= rows.length) this.selected = Math.max(0, rows.length - 1);
51199
51246
  const row = rows[this.selected];
51200
51247
  const lines = [];
@@ -51212,9 +51259,9 @@ var ModelPicker = class {
51212
51259
  const r = rows[i];
51213
51260
  if (r === void 0) continue;
51214
51261
  const highlighted = i === this.selected;
51215
- lines.push(this.renderRow(r, highlighted, width));
51262
+ lines.push(this.renderRow(r, highlighted, width, nameCol));
51216
51263
  if (highlighted && r.kind === "fusion" && this.focus !== "effort") {
51217
- lines.push(...this.renderDropdown(width));
51264
+ lines.push(...this.renderDropdown(width, nameCol));
51218
51265
  }
51219
51266
  }
51220
51267
  if (end < rows.length) lines.push(t.fg("dim", ` \u2193 more below (${String(rows.length - end)})`));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Auto-updater, changelog browser, and readme browser for Unipi — checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.20.2"
37
+ "@pi-unipi/core": "2.20.4"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/utility",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Utility commands and tools for Pi coding agent — lifecycle, diagnostics, cache, analytics, display, batch execution",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@pi-unipi/core": "2.20.2"
39
+ "@pi-unipi/core": "2.20.4"
40
40
  },
41
41
  "devDependencies": {},
42
42
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.20.2",
34
+ "@pi-unipi/core": "2.20.4",
35
35
  "defuddle": "^0.18.1",
36
36
  "linkedom": "^0.18.12",
37
37
  "wreq-js": "^2.3.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/workflow",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Structured development workflow commands for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -30,7 +30,7 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@pi-unipi/core": "2.20.2"
33
+ "@pi-unipi/core": "2.20.4"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-ai": "^0.84.0",