@pi-unipi/unipi 2.12.2 → 2.14.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/package.json +23 -23
  4. package/packages/ask-user/package.json +2 -2
  5. package/packages/autocomplete/package.json +1 -1
  6. package/packages/background-tasks/README.md +16 -0
  7. package/packages/background-tasks/package.json +2 -2
  8. package/packages/background-tasks/src/index.ts +8 -0
  9. package/packages/background-tasks/src/registry-shared.ts +32 -0
  10. package/packages/btw/package.json +2 -2
  11. package/packages/compactor/package.json +3 -3
  12. package/packages/core/package.json +1 -1
  13. package/packages/footer/README.md +1 -0
  14. package/packages/footer/package.json +3 -2
  15. package/packages/footer/src/index.ts +7 -5
  16. package/packages/footer/src/process-line.ts +74 -0
  17. package/packages/image/README.md +8 -1
  18. package/packages/image/package.json +2 -2
  19. package/packages/image/src/index.ts +37 -5
  20. package/packages/image/src/models.ts +24 -1
  21. package/packages/info-screen/package.json +2 -2
  22. package/packages/input-shortcuts/package.json +2 -2
  23. package/packages/kanboard/package.json +2 -2
  24. package/packages/mcp/package.json +2 -2
  25. package/packages/memory/package.json +3 -3
  26. package/packages/milestone/package.json +2 -2
  27. package/packages/notify/package.json +2 -2
  28. package/packages/ralph/package.json +3 -3
  29. package/packages/subagents/package.json +2 -2
  30. package/packages/trajectory/package.json +1 -1
  31. package/packages/unipi/bundled.js +4009 -1336
  32. package/packages/updater/package.json +2 -2
  33. package/packages/utility/package.json +2 -2
  34. package/packages/web-api/package.json +2 -2
  35. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.14.0] — 2026-08-29
10
+
11
+ ### Changed
12
+
13
+ - `image`: **`image_recognize` is now hidden while the session model has vision** — a model that accepts image input reads images natively through pi's own read tool, so the separate recognition round-trip only duplicated the ability and burned system-prompt context. The extension drops the tool from the active set on `session_start` and `model_select` when the session model declares image input (`input` includes `"image"`), and restores it automatically when a text-only model takes over mid-session. Models that do not declare input modalities are treated as non-vision and keep the tool, matching the package's existing convention. The info screen shows "Hidden (model has vision)" for the Recognize stat while suppressed.
14
+
15
+ ## [2.13.0] — 2026-08-28
16
+
17
+ ### Added
18
+
19
+ - `footer`: **bg-process one-liner above the glance footer** — a centered strip directly above the frame showing live background-task counts with status dots: green ● running, yellow ● stopped (killed), red ● failed, gray ● done. Zero-count buckets are omitted and the line hides entirely when idle. Reads `BackgroundTaskRegistry.allTasks()` directly (no events, no polling channels) and re-renders on the footer's 1s refresh timer; counts reset per session.
20
+ - `background-tasks`: **shared registry accessor** (`getSharedTaskRegistry` / `setSharedTaskRegistry` / `clearSharedTaskRegistry` in `src/registry-shared.ts`) — lets sibling extensions read the live task registry synchronously. Stored on `globalThis` under a `Symbol.for` key so the singleton survives duplicate module instances; published at extension init and `session_start`, cleared on `session_shutdown`.
21
+
22
+ ### Changed
23
+
24
+ - `footer`: new dependency on `@pi-unipi/background-tasks` for the process one-liner's direct registry reads.
25
+
9
26
  ## [2.12.0] — 2026-08-27
10
27
 
11
28
  ### Added
package/README.md CHANGED
@@ -110,7 +110,7 @@ Coexists triggers enhance behavior when packages are installed together. Workflo
110
110
  | `spawn_helper` / `get_helper_result` | subagents | Parallel agents |
111
111
  | `memory_store` / `memory_search` / `memory_delete` | memory | Memory CRUD |
112
112
  | `web_search` / `multi_web_content_read` / `web_llm_summarize` | web-api | Web research |
113
- | `image_generate` / `image_recognize` | image | Image generation and vision |
113
+ | `image_generate` / `image_recognize` | image | Image generation and vision (recognize auto-hides while the session model has vision) |
114
114
  | `notify_user` | notify | Push notifications |
115
115
  | `ask_user` | ask-user | User input |
116
116
  | `compact` / `session_recall` / `sandbox` | compactor | Context management |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.12.2",
3
+ "version": "2.14.0",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -82,28 +82,28 @@
82
82
  "typebox": "^1.1.38"
83
83
  },
84
84
  "dependencies": {
85
- "@pi-unipi/ask-user": "2.12.0",
86
- "@pi-unipi/background-tasks": "2.12.0",
87
- "@pi-unipi/btw": "2.12.0",
88
- "@pi-unipi/command-enchantment": "2.12.0",
89
- "@pi-unipi/compactor": "2.12.0",
90
- "@pi-unipi/core": "2.12.0",
91
- "@pi-unipi/footer": "2.12.1",
92
- "@pi-unipi/image": "2.12.0",
93
- "@pi-unipi/info-screen": "2.12.0",
94
- "@pi-unipi/input-shortcuts": "2.12.0",
95
- "@pi-unipi/kanboard": "2.12.0",
96
- "@pi-unipi/mcp": "2.12.0",
97
- "@pi-unipi/memory": "2.12.0",
98
- "@pi-unipi/milestone": "2.12.0",
99
- "@pi-unipi/notify": "2.12.0",
100
- "@pi-unipi/ralph": "2.12.0",
101
- "@pi-unipi/subagents": "2.12.0",
102
- "@pi-unipi/trajectory": "2.12.0",
103
- "@pi-unipi/updater": "2.12.0",
104
- "@pi-unipi/utility": "2.12.0",
105
- "@pi-unipi/web-api": "2.12.0",
106
- "@pi-unipi/workflow": "2.12.0"
85
+ "@pi-unipi/ask-user": "2.14.0",
86
+ "@pi-unipi/background-tasks": "2.14.0",
87
+ "@pi-unipi/btw": "2.14.0",
88
+ "@pi-unipi/command-enchantment": "2.14.0",
89
+ "@pi-unipi/compactor": "2.14.0",
90
+ "@pi-unipi/core": "2.14.0",
91
+ "@pi-unipi/footer": "2.14.0",
92
+ "@pi-unipi/image": "2.14.0",
93
+ "@pi-unipi/info-screen": "2.14.0",
94
+ "@pi-unipi/input-shortcuts": "2.14.0",
95
+ "@pi-unipi/kanboard": "2.14.0",
96
+ "@pi-unipi/mcp": "2.14.0",
97
+ "@pi-unipi/memory": "2.14.0",
98
+ "@pi-unipi/milestone": "2.14.0",
99
+ "@pi-unipi/notify": "2.14.0",
100
+ "@pi-unipi/ralph": "2.14.0",
101
+ "@pi-unipi/subagents": "2.14.0",
102
+ "@pi-unipi/trajectory": "2.14.0",
103
+ "@pi-unipi/updater": "2.14.0",
104
+ "@pi-unipi/utility": "2.14.0",
105
+ "@pi-unipi/web-api": "2.14.0",
106
+ "@pi-unipi/workflow": "2.14.0"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@earendil-works/pi-agent-core": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
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.12.0"
43
+ "@pi-unipi/core": "2.14.0"
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.12.0",
3
+ "version": "2.14.0",
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",
@@ -74,6 +74,22 @@ subscription OAuth attribution, exact-match system-prompt sanitization, and
74
74
  cache-retention policy for Anthropic routes. Duplicate installed copies resolve
75
75
  ownership through an EventBus claim; later copies go inert.
76
76
 
77
+ ## Shared registry (for sibling extensions)
78
+
79
+ Other packages can read the live task registry synchronously — no events, no polling:
80
+
81
+ ```ts
82
+ import { getSharedTaskRegistry } from "@pi-unipi/background-tasks";
83
+
84
+ const tasks = getSharedTaskRegistry()?.allTasks() ?? [];
85
+ const running = tasks.filter((t) => t.status === "running").length;
86
+ ```
87
+
88
+ The registry is published on `globalThis` under a `Symbol.for` key at extension
89
+ init and `session_start`, and cleared on `session_shutdown` (counts are
90
+ per-session). Returns `undefined` when the module is disabled — callers must
91
+ treat that as "no data", e.g. the footer's glance process line does.
92
+
77
93
  ## Differences from the reference
78
94
 
79
95
  - Commands live in the `/unipi:*` namespace; env prefix is `UNIPI_BG_*`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Background tasks for UniPi — durable shell jobs, delegated agents, attested Pi runs, and fixed-purpose Fusion workflows",
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.12.0",
13
+ "@pi-unipi/core": "2.14.0",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -19,10 +19,14 @@ import {
19
19
  import { registerToolsAndCommands } from "./tools.js";
20
20
  import { registerFusionExtension } from "./fusion-extension.js";
21
21
  import { registerDelegateExtension } from "./delegate-extension.js";
22
+ import { setSharedTaskRegistry, clearSharedTaskRegistry } from "./registry-shared.js";
22
23
  import { taskDisplayName, type BgTask, type StartAttestedPiTaskOptions, type StartTaskOptions } from "./types.js";
23
24
 
24
25
  const STATUS_INTERVAL_MS = 1000;
25
26
 
27
+ // Direct synchronous access for sibling extensions (footer process one-liner).
28
+ export { getSharedTaskRegistry } from "./registry-shared.js";
29
+
26
30
  export default function backgroundTasksExtension(pi: ExtensionAPI): void {
27
31
  const { config, warnings } = loadBackgroundTasksConfig(process.cwd());
28
32
 
@@ -53,6 +57,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
53
57
  eventService.publishTerminal(task);
54
58
  },
55
59
  });
60
+ setSharedTaskRegistry(registry);
61
+
56
62
  const eventService: BackgroundTaskExtensionService = installBackgroundTaskExtensionApi({
57
63
  events: pi.events,
58
64
  registry,
@@ -253,6 +259,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
253
259
 
254
260
  pi.on("session_start", async (_event, ctx) => {
255
261
  registry.setShuttingDown(false);
262
+ setSharedTaskRegistry(registry);
256
263
  currentCtx = ctx;
257
264
  await registry.ensureRuntimeDir(ctx);
258
265
  updateUi(ctx);
@@ -264,6 +271,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
264
271
 
265
272
  pi.on("session_shutdown", async (_event, ctx) => {
266
273
  registry.setShuttingDown(true);
274
+ clearSharedTaskRegistry();
267
275
  currentCtx = undefined;
268
276
  if (statusInterval) {
269
277
  clearInterval(statusInterval);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared BackgroundTaskRegistry accessor
3
+ *
4
+ * Exposes the live registry to sibling extensions (e.g. @pi-unipi/footer)
5
+ * without events or request/response channels — direct synchronous reads of
6
+ * `allTasks()`.
7
+ *
8
+ * Stored on globalThis under a `Symbol.for` key so the singleton is shared
9
+ * even if this package ends up instantiated more than once (duplicate
10
+ * node_modules copies would otherwise each hold their own module state).
11
+ */
12
+
13
+ import type { BackgroundTaskRegistry } from "./registry.js";
14
+
15
+ const SHARED_REGISTRY_KEY = Symbol.for("unipi.background-tasks.shared-registry");
16
+
17
+ /** Publish the live registry (idempotent; later calls overwrite). */
18
+ export function setSharedTaskRegistry(registry: BackgroundTaskRegistry): void {
19
+ (globalThis as unknown as Record<symbol, unknown>)[SHARED_REGISTRY_KEY] = registry;
20
+ }
21
+
22
+ /** Read the live registry, or undefined when background-tasks is not loaded. */
23
+ export function getSharedTaskRegistry(): BackgroundTaskRegistry | undefined {
24
+ return (globalThis as unknown as Record<symbol, unknown>)[SHARED_REGISTRY_KEY] as
25
+ | BackgroundTaskRegistry
26
+ | undefined;
27
+ }
28
+
29
+ /** Drop the shared reference (used on session shutdown so readers see a clean slate). */
30
+ export function clearSharedTaskRegistry(): void {
31
+ delete (globalThis as unknown as Record<symbol, unknown>)[SHARED_REGISTRY_KEY];
32
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
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.12.0"
40
+ "@pi-unipi/core": "2.14.0"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
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.12.0",
38
- "@pi-unipi/info-screen": "2.12.0",
37
+ "@pi-unipi/core": "2.14.0",
38
+ "@pi-unipi/info-screen": "2.14.0",
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.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,6 +18,7 @@ An experimental input surface, on by default and toggleable in `/unipi:footer-se
18
18
  - **Top border:** animated lolcat-gradient UNIPI brand + git branch (turns rainbow-frame animated while thinking is max/xhigh)
19
19
  - **Bottom border:** workspace · context %/window · model · thinking level
20
20
  - **Session strip:** turns/steps, wall + tool wall time, average TTFT, tok/s, cache hit % — colored per stat, honest across restarts (derived from persisted session timestamps when live hooks are unavailable; provider-reported `usage.output` anchors token counts whenever present)
21
+ - **Process line (new in 2.13):** centered one-liner directly above the frame while background work is in flight — `● 3 running ● 1 stopped ● 1 failed ● 2 done` — green ● running, yellow ● stopped (killed), red ● failed, gray ● done. Covers every task type (shell jobs, delegates, fusion workflows) via direct registry reads; zero-count buckets are omitted and the line hides when idle. Counts reset per session.
21
22
  - The classic segment status line is suppressed while glance mode is on; toggle it back for the classic footer
22
23
 
23
24
  ## Commands
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.12.1",
3
+ "version": "2.14.0",
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,7 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.12.0"
35
+ "@pi-unipi/core": "2.14.0",
36
+ "@pi-unipi/background-tasks": "2.14.0"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -29,6 +29,7 @@ import { STATUS_EXT_SEGMENTS } from "./segments/status-ext.js";
29
29
 
30
30
  import type { FooterGroup, FooterSegment } from "./types.js";
31
31
  import { tpsTracker } from "./tps-tracker.js";
32
+ import { renderProcessLine } from "./process-line.js";
32
33
 
33
34
  /** All segment groups */
34
35
  const ALL_GROUPS: FooterGroup[] = [
@@ -328,9 +329,10 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
328
329
  };
329
330
  });
330
331
 
331
- // Top row widget — classic status line (suppressed in glance mode; the
332
- // glance frame's top border already shows UNIPI branch and the bottom
333
- // border shows context/model/thinking, so a segment row would duplicate it)
332
+ // Top row widget — dual role. Classic mode: status segment line. Glance
333
+ // mode: the bg-process one-liner, rendered directly above the glance frame
334
+ // (the frame replaces the editor, so this aboveEditor slot sits right above
335
+ // the footer); the frame's own borders show branch/context/model/thinking.
334
336
  ctx.ui.setWidget("footer-top", (_tui, theme) => {
335
337
  // Update the renderer's theme-like
336
338
  const themeLike = { fg: (color: string, text: string) => theme.fg(color as any, text) };
@@ -344,8 +346,8 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
344
346
  },
345
347
  render(width: number): string[] {
346
348
  if (!state.enabled || !state.piContext || width <= 0) return [];
347
- // Glance mode replaces the classic segment line entirely.
348
- if (state.glanceMode) return [];
349
+ // Glance mode: this slot becomes the bg-process one-liner.
350
+ if (state.glanceMode) return renderProcessLine(width);
349
351
  const layout = state.renderer.computeLayout(width);
350
352
  if (!layout.topContent) return [];
351
353
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @pi-unipi/footer — Background process one-liner
3
+ *
4
+ * Glance-mode strip rendered above the footer frame: one colored dot + count
5
+ * per background-task status. Reads DIRECTLY from the
6
+ * @pi-unipi/background-tasks shared registry (no events, no polling
7
+ * channels); re-renders on the footer's existing 1s refresh timer.
8
+ *
9
+ * Dot → status mapping:
10
+ * green ● running yellow ● stopped (killed) red ● failed gray ● done (completed)
11
+ *
12
+ * Buckets with zero count are omitted; with nothing in flight the line is
13
+ * empty so the footer stays clean.
14
+ */
15
+
16
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
+ import { getSharedTaskRegistry } from "@pi-unipi/background-tasks";
18
+
19
+ const GREEN_DOT = "\x1b[38;5;82m●\x1b[0m"; // running — active work
20
+ const YELLOW_DOT = "\x1b[38;5;220m●\x1b[0m"; // stopped (killed) — needs attention
21
+ const RED_DOT = "\x1b[38;5;196m●\x1b[0m"; // failed — needs attention
22
+ const GRAY_DOT = "\x1b[38;5;245m●\x1b[0m"; // done (completed) — idle info
23
+
24
+ export interface BgProcessCounts {
25
+ running: number;
26
+ stopped: number;
27
+ failed: number;
28
+ done: number;
29
+ }
30
+
31
+ /**
32
+ * Count background tasks by display status straight from the registry.
33
+ * Returns null when background-tasks has not published a registry (module
34
+ * disabled, before first load, or after session shutdown).
35
+ */
36
+ export function countBgProcesses(): BgProcessCounts | null {
37
+ try {
38
+ const tasks = getSharedTaskRegistry()?.allTasks();
39
+ if (!tasks) return null;
40
+ const counts: BgProcessCounts = { running: 0, stopped: 0, failed: 0, done: 0 };
41
+ for (const task of tasks) {
42
+ if (task.status === "running") counts.running++;
43
+ else if (task.status === "killed") counts.stopped++;
44
+ else if (task.status === "failed") counts.failed++;
45
+ else if (task.status === "completed") counts.done++;
46
+ }
47
+ return counts;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Render the centered one-liner for the given terminal width.
55
+ * Returns [] when there is nothing to show.
56
+ */
57
+ export function renderProcessLine(width: number): string[] {
58
+ if (width <= 0) return [];
59
+ const counts = countBgProcesses();
60
+ if (!counts) return [];
61
+
62
+ const parts: string[] = [];
63
+ if (counts.running > 0) parts.push(`${GREEN_DOT} ${counts.running} running`);
64
+ if (counts.stopped > 0) parts.push(`${YELLOW_DOT} ${counts.stopped} stopped`);
65
+ if (counts.failed > 0) parts.push(`${RED_DOT} ${counts.failed} failed`);
66
+ if (counts.done > 0) parts.push(`${GRAY_DOT} ${counts.done} done`);
67
+ if (parts.length === 0) return [];
68
+
69
+ const line = parts.join(" ");
70
+ const w = visibleWidth(line);
71
+ if (w >= width) return [truncateToWidth(line, width)];
72
+ const leftPad = Math.floor((width - w) / 2);
73
+ return [" ".repeat(leftPad) + line];
74
+ }
@@ -7,7 +7,7 @@ Image generation and image recognition tools for the agent.
7
7
  | Tool | Description |
8
8
  |------|-------------|
9
9
  | `image_generate` | Generate an image from a text prompt. Returned inline and saved to disk. |
10
- | `image_recognize` | Analyze an image with a vision model. Accepts a file path, `data:` URL, or base64. |
10
+ | `image_recognize` | Analyze an image with a vision model. Accepts a file path, `data:` URL, or base64. Automatically hidden while the session model itself has vision. |
11
11
 
12
12
  ## Commands
13
13
 
@@ -63,6 +63,13 @@ works. Supported: PNG, JPEG, GIF, WebP. Remote URLs are not fetched.
63
63
  Prefer file paths — inlining base64 into the conversation is far more
64
64
  expensive in tokens.
65
65
 
66
+ **Vision models don't get this tool.** When the session's current model already
67
+ accepts image input, `image_recognize` is dropped from the active tool set —
68
+ the model reads images natively through pi's own tools, so a separate
69
+ recognition round-trip through another model would only duplicate that ability
70
+ and burn context. Switch to a text-only model (via `/model`) and the tool comes
71
+ back automatically.
72
+
66
73
  ## Configuration
67
74
 
68
75
  `~/.unipi/config/image/config.json`:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
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.12.0"
37
+ "@pi-unipi/core": "2.14.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.84.0",
@@ -20,7 +20,13 @@ import {
20
20
 
21
21
  import { registerImageCommands } from "./commands.js";
22
22
  import { registerImageTools } from "./tools.js";
23
- import { listImageGenModels, listVisionModels, type ChatModelRegistry } from "./models.js";
23
+ import {
24
+ applyRecognizeGating,
25
+ isVisionModel,
26
+ listImageGenModels,
27
+ listVisionModels,
28
+ type ChatModelRegistry,
29
+ } from "./models.js";
24
30
  import { registerRegistryImageProviders } from "./register-providers.js";
25
31
  import { loadConfig } from "./settings.js";
26
32
 
@@ -37,10 +43,27 @@ function getInfoRegistry() {
37
43
  ).__unipi_info_registry;
38
44
  }
39
45
 
46
+ /**
47
+ * Hide image_recognize when the session model can natively see images, and
48
+ * restore it when a text-only model takes over. Returns whether the tool is
49
+ * provided after gating.
50
+ */
51
+ function applyVisionGating(pi: ExtensionAPI, model: unknown): boolean {
52
+ const active = pi.getActiveTools();
53
+ const next = applyRecognizeGating(active, model, IMAGE_TOOLS.RECOGNIZE);
54
+ if (next !== active) pi.setActiveTools(next);
55
+ return next.includes(IMAGE_TOOLS.RECOGNIZE);
56
+ }
57
+
40
58
  export default function (pi: ExtensionAPI) {
41
59
  registerImageTools(pi);
42
60
  registerImageCommands(pi);
43
61
 
62
+ pi.on("model_select", (event) => {
63
+ if (!loadConfig().recognize.enabled) return;
64
+ applyVisionGating(pi, event.model);
65
+ });
66
+
44
67
  pi.on("session_start", async (_event, ctx) => {
45
68
  const config = loadConfig();
46
69
 
@@ -51,9 +74,16 @@ export default function (pi: ExtensionAPI) {
51
74
  (ctx as unknown as { modelRegistry?: ChatModelRegistry }).modelRegistry,
52
75
  ).catch(() => undefined);
53
76
 
77
+ // A vision-capable session model reads images itself, so image_recognize
78
+ // would only duplicate that ability. Drop it from the active tool set;
79
+ // the model_select handler above restores it when a text-only model is
80
+ // chosen later in the same session.
81
+ const recognizeProvided =
82
+ config.recognize.enabled && applyVisionGating(pi, ctx.model);
83
+
54
84
  const tools: string[] = [];
55
85
  if (config.generate.enabled) tools.push(IMAGE_TOOLS.GENERATE);
56
- if (config.recognize.enabled) tools.push(IMAGE_TOOLS.RECOGNIZE);
86
+ if (recognizeProvided) tools.push(IMAGE_TOOLS.RECOGNIZE);
57
87
 
58
88
  emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
59
89
  name: MODULES.IMAGE,
@@ -92,9 +122,11 @@ export default function (pi: ExtensionAPI) {
92
122
  .modelRegistry;
93
123
  const vision = chatRegistry ? listVisionModels(chatRegistry) : [];
94
124
 
95
- const recognize = current.recognize.enabled
96
- ? current.recognize.model || "Session model"
97
- : "Disabled";
125
+ const recognize = !current.recognize.enabled
126
+ ? "Disabled"
127
+ : isVisionModel(ctx.model)
128
+ ? "Hidden (model has vision)"
129
+ : current.recognize.model || "Session model";
98
130
 
99
131
  return {
100
132
  generate: { value: generate },
@@ -383,7 +383,7 @@ export function listVisionModels(registry: ChatModelRegistry): VisionModel[] {
383
383
  return models.filter(isVisionModel);
384
384
  }
385
385
 
386
- function isVisionModel(model: unknown): model is VisionModel {
386
+ export function isVisionModel(model: unknown): model is VisionModel {
387
387
  if (model === null || typeof model !== "object") return false;
388
388
  const candidate = model as Partial<VisionModel>;
389
389
  if (typeof candidate.id !== "string" || typeof candidate.provider !== "string") {
@@ -392,6 +392,29 @@ function isVisionModel(model: unknown): model is VisionModel {
392
392
  return Array.isArray(candidate.input) && candidate.input.includes("image");
393
393
  }
394
394
 
395
+ /**
396
+ * Active tool names after hiding `recognizeTool` for a vision-capable model.
397
+ *
398
+ * A model that accepts image input can read images natively (pi's own read
399
+ * tool hands it the pixels), so a separate image_recognize round-trip through
400
+ * another model only duplicates the ability and burns system-prompt context.
401
+ * Text-only models get the tool back. Models that do not declare their input
402
+ * modalities are treated as non-vision, matching `isVisionModel`.
403
+ */
404
+ export function applyRecognizeGating(
405
+ active: string[],
406
+ model: unknown,
407
+ recognizeTool: string,
408
+ ): string[] {
409
+ const vision = isVisionModel(model);
410
+ const present = active.includes(recognizeTool);
411
+ // Already correct: hidden for a vision model, or provided for a text-only one.
412
+ if (vision !== present) return active;
413
+ return vision
414
+ ? active.filter((name) => name !== recognizeTool)
415
+ : [...active, recognizeTool];
416
+ }
417
+
395
418
  /**
396
419
  * Resolve a vision-model reference, restricted to image-capable models.
397
420
  *
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
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.12.0"
36
+ "@pi-unipi/core": "2.14.0"
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.12.0",
3
+ "version": "2.14.0",
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.12.0"
36
+ "@pi-unipi/core": "2.14.0"
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.12.0",
3
+ "version": "2.14.0",
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.12.0"
42
+ "@pi-unipi/core": "2.14.0"
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.12.0",
3
+ "version": "2.14.0",
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.12.0"
33
+ "@pi-unipi/core": "2.14.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-coding-agent": "^0.84.0",