@pi-unipi/unipi 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +6 -2
  2. package/package.json +24 -23
  3. package/packages/ask-user/ask-ui.ts +45 -31
  4. package/packages/ask-user/launcher-ui.ts +26 -22
  5. package/packages/ask-user/package.json +3 -3
  6. package/packages/ask-user/settings-tui.ts +22 -9
  7. package/packages/autocomplete/package.json +1 -1
  8. package/packages/autocomplete/src/constants.ts +5 -0
  9. package/packages/btw/package.json +2 -2
  10. package/packages/cocoindex/package.json +2 -2
  11. package/packages/compactor/package.json +4 -4
  12. package/packages/compactor/src/tui/settings-overlay.ts +2 -1
  13. package/packages/core/constants.ts +18 -0
  14. package/packages/core/index.ts +1 -0
  15. package/packages/core/package.json +1 -1
  16. package/packages/core/tui-width.ts +127 -0
  17. package/packages/footer/package.json +3 -3
  18. package/packages/footer/src/tui/settings-tui.ts +2 -1
  19. package/packages/image/README.md +82 -0
  20. package/packages/image/index.ts +1 -0
  21. package/packages/image/package.json +57 -0
  22. package/packages/image/skills/image/SKILL.md +73 -0
  23. package/packages/image/src/commands.ts +17 -0
  24. package/packages/image/src/generate.ts +201 -0
  25. package/packages/image/src/image-source.ts +204 -0
  26. package/packages/image/src/index.ts +99 -0
  27. package/packages/image/src/models.ts +290 -0
  28. package/packages/image/src/recognize.ts +223 -0
  29. package/packages/image/src/settings.ts +149 -0
  30. package/packages/image/src/tools.ts +296 -0
  31. package/packages/image/src/tui/model-selector.ts +279 -0
  32. package/packages/image/src/tui/settings-dialog.ts +236 -0
  33. package/packages/info-screen/package.json +2 -2
  34. package/packages/info-screen/tui/info-overlay.ts +3 -2
  35. package/packages/input-shortcuts/package.json +3 -3
  36. package/packages/kanboard/package.json +2 -2
  37. package/packages/mcp/package.json +2 -2
  38. package/packages/mcp/src/tui/add-overlay.ts +10 -7
  39. package/packages/mcp/src/tui/settings-overlay.ts +10 -7
  40. package/packages/memory/package.json +3 -3
  41. package/packages/milestone/package.json +2 -2
  42. package/packages/notify/README.md +4 -1
  43. package/packages/notify/events.ts +130 -81
  44. package/packages/notify/package.json +3 -3
  45. package/packages/notify/permission-prompt-message.ts +95 -0
  46. package/packages/notify/settings.ts +2 -0
  47. package/packages/notify/skills/configure-notify/SKILL.md +31 -2
  48. package/packages/notify/src/__tests__/event-bus.test.ts +3 -2
  49. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +172 -0
  50. package/packages/notify/tui/gotify-setup.ts +2 -1
  51. package/packages/notify/tui/ntfy-setup.ts +2 -1
  52. package/packages/notify/tui/recap-model-selector.ts +2 -2
  53. package/packages/notify/tui/settings-overlay.ts +2 -1
  54. package/packages/notify/tui/telegram-setup.ts +2 -1
  55. package/packages/ralph/package.json +3 -3
  56. package/packages/subagents/package.json +5 -5
  57. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +185 -0
  58. package/packages/subagents/src/index.ts +52 -10
  59. package/packages/unipi/index.ts +2 -0
  60. package/packages/updater/package.json +3 -3
  61. package/packages/updater/src/tui/changelog-overlay.ts +2 -2
  62. package/packages/updater/src/tui/readme-overlay.ts +2 -1
  63. package/packages/updater/src/tui/update-overlay.ts +2 -1
  64. package/packages/utility/package.json +2 -2
  65. package/packages/utility/src/tui/badge-settings-tui.ts +2 -2
  66. package/packages/utility/src/tui/util-settings-tui.ts +2 -2
  67. package/packages/web-api/README.md +28 -11
  68. package/packages/web-api/package.json +8 -2
  69. package/packages/web-api/skills/web/SKILL.md +36 -9
  70. package/packages/web-api/src/index.ts +15 -0
  71. package/packages/web-api/src/providers/duckduckgo.ts +71 -27
  72. package/packages/web-api/src/providers/firecrawl.ts +1 -1
  73. package/packages/web-api/src/providers/jina-reader.ts +1 -1
  74. package/packages/web-api/src/providers/jina-search.ts +1 -1
  75. package/packages/web-api/src/providers/perplexity.ts +2 -2
  76. package/packages/web-api/src/providers/serpapi.ts +1 -1
  77. package/packages/web-api/src/providers/tavily.ts +1 -1
  78. package/packages/web-api/src/providers/wigolo-client.ts +196 -0
  79. package/packages/web-api/src/providers/wigolo.ts +117 -0
  80. package/packages/web-api/src/settings.ts +1 -0
  81. package/packages/web-api/src/tools.ts +93 -36
  82. package/packages/workflow/package.json +2 -2
package/README.md CHANGED
@@ -21,7 +21,9 @@ pi install npm:@pi-unipi/unipi
21
21
 
22
22
  **[Subagents](./packages/subagents/README.md)** — Parallel execution with file locking. Spawn background agents to research, fix, or build while the main agent keeps going.
23
23
 
24
- **[Web API](./packages/web-api/README.md)** — Web search, page reading, content summarization. Smart-fetch engine with browser-grade TLS fingerprintingfree, no API key. Paid providers as fallbacks.
24
+ **[Web API](./packages/web-api/README.md)** — Web search, page reading, content summarization. Defaults to [wigolo](https://github.com/KnockOutEZ/wigolo), a local-first engine with multi-engine search and on-device reranking $0/query, no API key. Plus a smart-fetch engine with browser-grade TLS fingerprinting. Paid providers as fallbacks, and auto-selection falls through when a provider is unavailable.
25
+
26
+ **[Image](./packages/image/README.md)** — Generate images from a text prompt and analyze existing ones. 34 image models, plus vision analysis on any model that accepts image input, with a customizable system prompt.
25
27
 
26
28
  **[MCP](./packages/mcp/README.md)** — Browse 7,800+ MCP servers, add them interactively. Tools from servers register automatically as Pi tools.
27
29
 
@@ -101,7 +103,8 @@ Coexists triggers enhance behavior when packages are installed together. Workflo
101
103
  | `ralph_start` / `ralph_done` | ralph | Loop control |
102
104
  | `spawn_helper` / `get_helper_result` | subagents | Parallel agents |
103
105
  | `memory_store` / `memory_search` / `memory_delete` | memory | Memory CRUD |
104
- | `web_search` / `web_read` / `web_llm_summarize` | web-api | Web research |
106
+ | `web_search` / `multi_web_content_read` / `web_llm_summarize` | web-api | Web research |
107
+ | `image_generate` / `image_recognize` | image | Image generation and vision |
105
108
  | `notify_user` | notify | Push notifications |
106
109
  | `ask_user` | ask-user | User input |
107
110
  | `compact` / `session_recall` / `sandbox` | compactor | Context management |
@@ -129,6 +132,7 @@ unipi/
129
132
  │ ├── cocoindex/ # Project indexing and semantic search
130
133
  │ ├── subagents/ # Parallel execution
131
134
  │ ├── web-api/ # Web research
135
+ │ ├── image/ # Image generation and vision
132
136
  │ ├── mcp/ # MCP server integration
133
137
  │ ├── notify/ # Push notifications
134
138
  │ ├── footer/ # Status bar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "typecheck": "npx tsc --noEmit --skipLibCheck",
27
- "test": "node --test tests/*.test.js && npm test --workspaces --if-present",
27
+ "test": "npx tsx --test tests/*.test.js && npm test --workspaces --if-present",
28
28
  "publish:all": "npm publish --workspaces --access public"
29
29
  },
30
30
  "files": [
@@ -55,7 +55,8 @@
55
55
  "packages/milestone/skills",
56
56
  "packages/kanboard/skills",
57
57
  "packages/updater/skills",
58
- "packages/cocoindex/skills"
58
+ "packages/cocoindex/skills",
59
+ "packages/image/skills"
59
60
  ]
60
61
  },
61
62
  "peerDependencies": {
@@ -65,26 +66,26 @@
65
66
  "typebox": "^1.1.38"
66
67
  },
67
68
  "dependencies": {
68
- "@pi-unipi/ask-user": "2.1.2",
69
- "@pi-unipi/btw": "2.1.2",
70
- "@pi-unipi/compactor": "2.1.2",
71
- "@pi-unipi/notify": "2.1.2",
72
- "@pi-unipi/command-enchantment": "2.1.2",
73
- "@pi-unipi/core": "2.1.2",
74
- "@pi-unipi/info-screen": "2.1.2",
75
- "@pi-unipi/mcp": "2.1.2",
76
- "@pi-unipi/memory": "2.1.2",
77
- "@pi-unipi/ralph": "2.1.2",
78
- "@pi-unipi/subagents": "2.1.2",
79
- "@pi-unipi/utility": "2.1.2",
80
- "@pi-unipi/milestone": "2.1.2",
81
- "@pi-unipi/kanboard": "2.1.2",
82
- "@pi-unipi/web-api": "2.1.2",
83
- "@pi-unipi/workflow": "2.1.2",
84
- "@pi-unipi/footer": "2.1.2",
85
- "@pi-unipi/updater": "2.1.2",
86
- "@pi-unipi/input-shortcuts": "2.1.2",
87
- "@pi-unipi/cocoindex": "2.1.2"
69
+ "@pi-unipi/ask-user": "2.2.0",
70
+ "@pi-unipi/btw": "2.2.0",
71
+ "@pi-unipi/compactor": "2.2.0",
72
+ "@pi-unipi/notify": "2.2.0",
73
+ "@pi-unipi/command-enchantment": "2.2.0",
74
+ "@pi-unipi/core": "2.2.0",
75
+ "@pi-unipi/info-screen": "2.2.0",
76
+ "@pi-unipi/mcp": "2.2.0",
77
+ "@pi-unipi/memory": "2.2.0",
78
+ "@pi-unipi/ralph": "2.2.0",
79
+ "@pi-unipi/subagents": "2.2.0",
80
+ "@pi-unipi/utility": "2.2.0",
81
+ "@pi-unipi/milestone": "2.2.0",
82
+ "@pi-unipi/kanboard": "2.2.0",
83
+ "@pi-unipi/web-api": "2.2.0",
84
+ "@pi-unipi/workflow": "2.2.0",
85
+ "@pi-unipi/footer": "2.2.0",
86
+ "@pi-unipi/updater": "2.2.0",
87
+ "@pi-unipi/input-shortcuts": "2.2.0",
88
+ "@pi-unipi/cocoindex": "2.2.0"
88
89
  },
89
90
  "devDependencies": {
90
91
  "@earendil-works/pi-agent-core": "^0.80.0",
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, type TUI, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
9
  import type { Theme, AgentToolResult } from "@earendil-works/pi-coding-agent";
10
+ import { adaptiveInnerWidth, contentWidth, normalizeWidth, safeRepeat, shouldRenderBorder, WidthKeyedCache } from "@pi-unipi/core";
10
11
  import type { NormalizedOption, AskUserResponse } from "./types.js";
11
12
 
12
13
  /** Result returned by the ask UI */
@@ -60,7 +61,8 @@ export function renderAskUI(params: {
60
61
  let optionIndex = 0;
61
62
  let editMode = false;
62
63
  let editTarget: "freeform" | number = "freeform"; // which option is being edited
63
- let cachedLines: string[] | undefined;
64
+ // Width-keyed so a terminal resize can never serve stale, over-wide lines.
65
+ const lineCache = new WidthKeyedCache();
64
66
  const selected = new Set<string>();
65
67
  let customText: string | null = null; // Store custom text (global freeform)
66
68
  const optionCustomTexts = new Map<string, string>(); // Per-option custom text for allowCustom
@@ -157,7 +159,7 @@ export function renderAskUI(params: {
157
159
  }
158
160
 
159
161
  function refresh() {
160
- cachedLines = undefined;
162
+ lineCache.clear();
161
163
  tui.requestRender();
162
164
  }
163
165
 
@@ -363,29 +365,39 @@ export function renderAskUI(params: {
363
365
  }
364
366
  }
365
367
 
366
- function render(width: number): string[] {
367
- if (cachedLines) return cachedLines;
368
+ function render(rawWidth: number): string[] {
369
+ const width = normalizeWidth(rawWidth);
370
+ const cached = lineCache.get(width);
371
+ if (cached) return cached;
368
372
 
369
373
  const lines: string[] = [];
370
- const innerWidth = Math.max(40, width - 2);
374
+ // Never exceed the terminal width — pi-tui throws on over-wide lines.
375
+ // On very narrow terminals the box border is dropped so the little
376
+ // width available all goes to content.
377
+ const innerWidth = adaptiveInnerWidth(width);
378
+ const bordered = shouldRenderBorder(width);
371
379
  const border = (s: string) => theme.fg("accent", s);
372
380
 
373
381
  function padVisible(content: string, targetWidth: number): string {
374
382
  const vw = visibleWidth(content);
375
- const pad = Math.max(0, targetWidth - vw);
376
- return content + " ".repeat(pad);
383
+ return content + safeRepeat(" ", targetWidth - vw);
377
384
  }
378
385
 
379
- const add = (s: string) => lines.push(border("│") + padVisible(truncateToWidth(s, innerWidth), innerWidth) + border("│"));
386
+ const frame = (content: string) => {
387
+ const body = padVisible(truncateToWidth(content, innerWidth), innerWidth);
388
+ return bordered ? border("│") + body + border("│") : body;
389
+ };
390
+
391
+ const add = (s: string) => lines.push(frame(s));
380
392
  const addWrapped = (s: string) => {
381
393
  for (const line of wrapTextWithAnsi(s, innerWidth)) {
382
- lines.push(border("│") + padVisible(truncateToWidth(line, innerWidth), innerWidth) + border("│"));
394
+ lines.push(frame(line));
383
395
  }
384
396
  };
385
- const addEmpty = () => lines.push(border("") + " ".repeat(innerWidth) + border("│"));
397
+ const addEmpty = () => lines.push(frame(""));
386
398
 
387
399
  // Top border
388
- lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
400
+ if (bordered) lines.push(border(`╭${safeRepeat("─", innerWidth)}╮`));
389
401
 
390
402
  // Context
391
403
  if (context) {
@@ -438,10 +450,9 @@ export function renderAskUI(params: {
438
450
  }
439
451
 
440
452
  // Bottom border
441
- lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
453
+ if (bordered) lines.push(border(`╰${safeRepeat("─", innerWidth)}╯`));
442
454
 
443
- cachedLines = lines;
444
- return lines;
455
+ return lineCache.set(width, lines);
445
456
  }
446
457
 
447
458
  function renderOptions(
@@ -452,16 +463,28 @@ export function renderAskUI(params: {
452
463
  ) {
453
464
  const addWrappedOptionLine = (prefix: string, content: string) => {
454
465
  const prefixWidth = visibleWidth(prefix);
455
- const contentWidth = Math.max(1, width - prefixWidth);
456
- const continuationPrefix = " ".repeat(prefixWidth);
457
- const wrapped = wrapTextWithAnsi(content, contentWidth);
466
+ const wrapWidth = contentWidth(width, prefixWidth);
467
+ const continuationPrefix = safeRepeat(" ", prefixWidth);
468
+ const wrapped = wrapTextWithAnsi(content, wrapWidth);
458
469
  for (let lineIndex = 0; lineIndex < wrapped.length; lineIndex++) {
459
470
  add((lineIndex === 0 ? prefix : continuationPrefix) + wrapped[lineIndex]);
460
471
  }
461
472
  };
462
473
 
474
+ // Indent descriptions under the option label, but give up the indent
475
+ // entirely when the terminal is too narrow to afford it.
476
+ const descriptionIndent = safeRepeat(" ", width > 10 ? 5 : 0);
463
477
  const addWrappedDescription = (description: string) => {
464
- addWrappedOptionLine(" ", theme.fg("muted", description));
478
+ addWrappedOptionLine(descriptionIndent, theme.fg("muted", description));
479
+ };
480
+
481
+ // Inline editor, inset by 3 columns where there is room for it.
482
+ const editorIndent = safeRepeat(" ", width > 8 ? 3 : 0);
483
+ const addInlineEditor = () => {
484
+ add(`${editorIndent}${theme.fg("muted", "Type your response:")}`);
485
+ for (const line of editor.render(contentWidth(width, visibleWidth(editorIndent) + 1))) {
486
+ add(`${editorIndent}${line}`);
487
+ }
465
488
  };
466
489
 
467
490
  for (let i = 0; i < displayOptions.length; i++) {
@@ -488,10 +511,7 @@ export function renderAskUI(params: {
488
511
 
489
512
  // Show edit indicator if in edit mode for this option
490
513
  if (editMode && editTarget === "freeform" && isSelected) {
491
- add(` ${theme.fg("muted", "Type your response:")}`);
492
- for (const line of editor.render(width - 4)) {
493
- add(` ${line}`);
494
- }
514
+ addInlineEditor();
495
515
  }
496
516
  } else if (allowMultiple) {
497
517
  // Multi-select: show checkbox
@@ -512,10 +532,7 @@ export function renderAskUI(params: {
512
532
 
513
533
  // Show edit indicator if in edit mode for this option
514
534
  if (editMode && editTarget === i && isSelected) {
515
- add(` ${theme.fg("muted", "Type your response:")}`);
516
- for (const line of editor.render(width - 4)) {
517
- add(` ${line}`);
518
- }
535
+ addInlineEditor();
519
536
  }
520
537
  } else {
521
538
  // Single-select: option
@@ -544,10 +561,7 @@ export function renderAskUI(params: {
544
561
 
545
562
  // Show edit indicator if in edit mode for this option
546
563
  if (editMode && editTarget === i && isSelected) {
547
- add(` ${theme.fg("muted", "Type your response:")}`);
548
- for (const line of editor.render(width - 4)) {
549
- add(` ${line}`);
550
- }
564
+ addInlineEditor();
551
565
  }
552
566
  }
553
567
 
@@ -561,7 +575,7 @@ export function renderAskUI(params: {
561
575
  return {
562
576
  render,
563
577
  invalidate: () => {
564
- cachedLines = undefined;
578
+ lineCache.clear();
565
579
  },
566
580
  handleInput,
567
581
  };
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { Key, matchesKey, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
9
9
  import type { Theme, KeybindingsManager } from "@earendil-works/pi-coding-agent";
10
+ import { adaptiveInnerWidth, contentWidth, normalizeWidth, safeRepeat, shouldRenderBorder, WidthKeyedCache } from "@pi-unipi/core";
10
11
  import type { SessionLauncherResult } from "./types.js";
11
12
 
12
13
  /** Launcher option definition */
@@ -45,10 +46,11 @@ export function renderLauncherUI(params: {
45
46
 
46
47
  // State
47
48
  let optionIndex = 0;
48
- let cachedLines: string[] | undefined;
49
+ // Width-keyed so a terminal resize can never serve stale, over-wide lines.
50
+ const lineCache = new WidthKeyedCache();
49
51
 
50
52
  function refresh() {
51
- cachedLines = undefined;
53
+ lineCache.clear();
52
54
  _tui.requestRender();
53
55
  }
54
56
 
@@ -79,34 +81,37 @@ export function renderLauncherUI(params: {
79
81
  }
80
82
  }
81
83
 
82
- function render(width: number): string[] {
83
- if (cachedLines) return cachedLines;
84
+ function render(rawWidth: number): string[] {
85
+ const width = normalizeWidth(rawWidth);
86
+ const cached = lineCache.get(width);
87
+ if (cached) return cached;
84
88
 
85
89
  const lines: string[] = [];
86
- const innerWidth = Math.max(40, width - 2);
90
+ // Never exceed the terminal width — pi-tui throws on over-wide lines.
91
+ const innerWidth = adaptiveInnerWidth(width);
92
+ const bordered = shouldRenderBorder(width);
87
93
  const border = (s: string) => theme.fg("accent", s);
88
94
 
89
95
  function padVisible(content: string, targetWidth: number): string {
90
96
  const vw = visibleWidth(content);
91
- const pad = Math.max(0, targetWidth - vw);
92
- return content + " ".repeat(pad);
97
+ return content + safeRepeat(" ", targetWidth - vw);
93
98
  }
94
99
 
95
- const add = (s: string) =>
96
- lines.push(
97
- border("│") +
98
- padVisible(truncateToWidth(s, innerWidth), innerWidth) +
99
- border("│"),
100
- );
101
- const addEmpty = () =>
102
- lines.push(border("│") + " ".repeat(innerWidth) + border("│"));
100
+ const frame = (content: string) => {
101
+ const body = padVisible(truncateToWidth(content, innerWidth), innerWidth);
102
+ return bordered ? border("│") + body + border("│") : body;
103
+ };
104
+
105
+ const add = (s: string) => lines.push(frame(s));
106
+ const addEmpty = () => lines.push(frame(""));
103
107
 
104
108
  // Top border
105
- lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
109
+ if (bordered) lines.push(border(`╭${safeRepeat("─", innerWidth)}╮`));
106
110
 
107
- // Header: show prefill command (truncated)
111
+ // Header: show prefill command (truncated).
112
+ // visibleWidth, not .length — the prefix holds an astral emoji.
108
113
  const headerPrefix = " 🚀 ";
109
- const maxPrefillWidth = innerWidth - headerPrefix.length - 1;
114
+ const maxPrefillWidth = contentWidth(innerWidth, visibleWidth(headerPrefix) + 1);
110
115
  const truncatedPrefill = truncateToWidth(prefill || "(no command)", maxPrefillWidth);
111
116
  add(theme.fg("accent", headerPrefix) + theme.fg("text", truncatedPrefill));
112
117
  addEmpty();
@@ -126,16 +131,15 @@ export function renderLauncherUI(params: {
126
131
  add(theme.fg("dim", " ↑↓ navigate • Enter select • Esc cancel"));
127
132
 
128
133
  // Bottom border
129
- lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
134
+ if (bordered) lines.push(border(`╰${safeRepeat("─", innerWidth)}╯`));
130
135
 
131
- cachedLines = lines;
132
- return lines;
136
+ return lineCache.set(width, lines);
133
137
  }
134
138
 
135
139
  return {
136
140
  render,
137
141
  invalidate: () => {
138
- cachedLines = undefined;
142
+ lineCache.clear();
139
143
  },
140
144
  handleInput,
141
145
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.1.2",
3
+ "version": "2.2.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",
@@ -34,13 +34,13 @@
34
34
  "README.md"
35
35
  ],
36
36
  "scripts": {
37
- "test": "node --experimental-strip-types --test tests/**/*.test.ts"
37
+ "test": "npx tsx --test tests/**/*.test.ts"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "2.1.2"
43
+ "@pi-unipi/core": "2.2.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
9
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { adaptiveInnerWidth, normalizeWidth, safeRepeat, shouldRenderBorder } from "@pi-unipi/core";
10
11
  import { getAskUserSettings, saveAskUserSettings, type AskUserSettings } from "./config.js";
11
12
 
12
13
  /** ANSI escape codes */
@@ -140,27 +141,39 @@ export class AskUserSettingsOverlay implements Component {
140
141
  /**
141
142
  * Render the overlay.
142
143
  */
143
- render(width: number): string[] {
144
+ render(rawWidth: number): string[] {
145
+ const width = normalizeWidth(rawWidth);
144
146
  const lines: string[] = [];
145
- const innerWidth = Math.max(40, width - 2);
147
+ // Never exceed the given width — pi-tui throws on over-wide lines.
148
+ const innerWidth = adaptiveInnerWidth(width);
149
+ const bordered = shouldRenderBorder(width);
146
150
 
147
151
  function padVisible(content: string, targetWidth: number): string {
148
152
  const vw = visibleWidth(content);
149
- const pad = Math.max(0, targetWidth - vw);
150
- return content + " ".repeat(pad);
153
+ return content + safeRepeat(" ", targetWidth - vw);
151
154
  }
152
155
 
153
- const add = (s: string) => lines.push(`${ansi.cyan}│${ansi.reset}` + padVisible(truncateToWidth(s, innerWidth), innerWidth) + `${ansi.cyan}│${ansi.reset}`);
154
- const addEmpty = () => lines.push(`${ansi.cyan}│${ansi.reset}` + " ".repeat(innerWidth) + `${ansi.cyan}│${ansi.reset}`);
156
+ const frame = (content: string) => {
157
+ const body = padVisible(truncateToWidth(content, innerWidth), innerWidth);
158
+ return bordered
159
+ ? `${ansi.cyan}│${ansi.reset}` + body + `${ansi.cyan}│${ansi.reset}`
160
+ : body;
161
+ };
162
+
163
+ const add = (s: string) => lines.push(frame(s));
164
+ const addEmpty = () => lines.push(frame(""));
155
165
 
156
166
  // Top border
157
- lines.push(`${ansi.cyan}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
167
+ if (bordered) lines.push(`${ansi.cyan}╭${safeRepeat("─", innerWidth)}╮${ansi.reset}`);
158
168
 
159
169
  // Header
160
170
  add(`${ansi.bold}${ansi.cyan}Ask User Settings${ansi.reset}`);
161
171
  add(`${ansi.dim}Configure how the agent can ask you questions${ansi.reset}`);
162
172
  addEmpty();
163
173
 
174
+ // Indent descriptions, unless the terminal is too narrow to afford it.
175
+ const descIndent = safeRepeat(" ", width > 10 ? 3 : 0);
176
+
164
177
  // Settings list
165
178
  for (let i = 0; i < SETTINGS.length; i++) {
166
179
  const item = SETTINGS[i];
@@ -171,7 +184,7 @@ export class AskUserSettingsOverlay implements Component {
171
184
  const descColor = ansi.gray;
172
185
 
173
186
  add(`${isSelected ? ansi.cyan + "▸" + ansi.reset : " "} ${toggle} ${labelColor}${item.label}${ansi.reset}`);
174
- add(` ${descColor}${item.description}${ansi.reset}`);
187
+ add(`${descIndent}${descColor}${item.description}${ansi.reset}`);
175
188
  }
176
189
 
177
190
  // Footer
@@ -179,7 +192,7 @@ export class AskUserSettingsOverlay implements Component {
179
192
  add(`${ansi.dim}↑↓ navigate • Space toggle • Enter save • Esc cancel${ansi.reset}`);
180
193
 
181
194
  // Bottom border
182
- lines.push(`${ansi.cyan}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
195
+ if (bordered) lines.push(`${ansi.cyan}╰${safeRepeat("─", innerWidth)}╯${ansi.reset}`);
183
196
 
184
197
  return lines;
185
198
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.1.2",
3
+ "version": "2.2.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",
@@ -34,6 +34,7 @@ export const PACKAGE_ORDER: string[] = [
34
34
  "updater",
35
35
  "input-shortcuts",
36
36
  "cocoindex",
37
+ "image",
37
38
  ];
38
39
 
39
40
  // ─── Package Colors ──────────────────────────────────────────────────
@@ -56,6 +57,7 @@ export const PACKAGE_COLORS: Record<string, string> = {
56
57
  updater: `${ESC}[93m`, // Bright Yellow
57
58
  "input-shortcuts": `${ESC}[95m`, // Bright Magenta
58
59
  cocoindex: `${ESC}[97m`, // Bright White
60
+ image: `${ESC}[35m`, // Magenta
59
61
  };
60
62
 
61
63
  // ─── Command Registry ────────────────────────────────────────────────
@@ -135,6 +137,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
135
137
  // web-api (2 commands)
136
138
  "unipi:web-settings": "web-api",
137
139
  "unipi:web-cache-clear": "web-api",
140
+ "unipi:image-settings": "image",
138
141
 
139
142
  // compact (9 commands)
140
143
  "unipi:lossless-compact": "compact",
@@ -254,6 +257,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
254
257
 
255
258
  "unipi:web-settings": "Configure web search settings",
256
259
  "unipi:web-cache-clear": "Clear web search cache",
260
+ "unipi:image-settings": "Configure image generation and recognition models",
257
261
 
258
262
  "unipi:lossless-compact": "Immediate zero-LLM compaction",
259
263
  "unipi:compact": "(DEPRECATED) Use /unipi:lossless-compact instead",
@@ -311,4 +315,5 @@ export const PACKAGE_LABELS: Record<string, string> = {
311
315
  updater: "updater",
312
316
  "input-shortcuts": "input-shortcuts",
313
317
  cocoindex: "cocoindex",
318
+ image: "image",
314
319
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "2.1.2",
3
+ "version": "2.2.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.80.0"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "2.1.2"
40
+ "@pi-unipi/core": "2.2.0"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/cocoindex",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "CocoIndex integration for Pi — AST-aware content indexing, semantic vector search, and incremental pipeline management",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -25,7 +25,7 @@
25
25
  "typebox": "^1.1.38"
26
26
  },
27
27
  "dependencies": {
28
- "@pi-unipi/core": "2.1.2"
28
+ "@pi-unipi/core": "2.2.0"
29
29
  },
30
30
  "optionalDependencies": {
31
31
  "@lancedb/lancedb": "^0.21.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.1.2",
3
+ "version": "2.2.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",
@@ -22,7 +22,7 @@
22
22
  "fts5"
23
23
  ],
24
24
  "scripts": {
25
- "test": "bun test tests"
25
+ "test": "npx bun test tests"
26
26
  },
27
27
  "files": [
28
28
  "index.ts",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.1.2",
38
- "@pi-unipi/info-screen": "2.1.2",
37
+ "@pi-unipi/core": "2.2.0",
38
+ "@pi-unipi/info-screen": "2.2.0",
39
39
  "@earendil-works/pi-agent-core": "^0.80.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -15,6 +15,7 @@ import { applyPreset, detectPreset } from "../config/presets.js";
15
15
  import type { CompactorPreset } from "../types.js";
16
16
  import type { CompactorConfig } from "../types.js";
17
17
  import { existsSync, unlinkSync } from "node:fs";
18
+ import { boxInnerWidth } from "@pi-unipi/core";
18
19
 
19
20
  // ─── Section types ─────────────────────────────────────────────────────
20
21
 
@@ -577,7 +578,7 @@ export class CompactorSettingsOverlay implements Component {
577
578
  // ─── Render ────────────────────────────────────────────────────────
578
579
 
579
580
  render(width: number): string[] {
580
- const innerWidth = Math.max(22, width - 2);
581
+ const innerWidth = boxInnerWidth(width);
581
582
  const lines: string[] = [];
582
583
 
583
584
  // Header
@@ -39,6 +39,7 @@ export const MODULES = {
39
39
  UPDATER: "@pi-unipi/updater",
40
40
  INPUT_SHORTCUTS: "@pi-unipi/input-shortcuts",
41
41
  COCOINDEX: "@pi-unipi/cocoindex",
42
+ IMAGE: "@pi-unipi/image",
42
43
  } as const;
43
44
 
44
45
  /** Workflow command names */
@@ -192,6 +193,23 @@ export const ASK_USER_TOOLS = {
192
193
  ASK: "ask_user",
193
194
  } as const;
194
195
 
196
+ /** Image tool names */
197
+ export const IMAGE_TOOLS = {
198
+ GENERATE: "image_generate",
199
+ RECOGNIZE: "image_recognize",
200
+ } as const;
201
+
202
+ /** Image command names */
203
+ export const IMAGE_COMMANDS = {
204
+ SETTINGS: "image-settings",
205
+ } as const;
206
+
207
+ /** Image directory paths */
208
+ export const IMAGE_DIRS = {
209
+ CONFIG: "~/.unipi/config/image",
210
+ OUTPUT: "~/.unipi/images",
211
+ } as const;
212
+
195
213
  /** MCP directory paths */
196
214
  export const MCP_DIRS = {
197
215
  GLOBAL_CONFIG: "~/.unipi/config/mcp",
@@ -9,3 +9,4 @@ export * from "./events.js";
9
9
  export * from "./sandbox.js";
10
10
  export * from "./utils.js";
11
11
  export * from "./model-cache.js";
12
+ export * from "./tui-width.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",