@pi-unipi/unipi 2.13.0 → 2.14.1
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 +17 -0
- package/README.md +1 -1
- package/package.json +23 -23
- package/packages/ask-user/package.json +3 -3
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +2 -0
- package/packages/background-tasks/package.json +3 -3
- package/packages/btw/package.json +3 -3
- package/packages/compactor/package.json +4 -4
- package/packages/core/constants.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +4 -4
- package/packages/image/README.md +8 -1
- package/packages/image/package.json +2 -2
- package/packages/image/src/index.ts +37 -5
- package/packages/image/src/models.ts +24 -1
- package/packages/info-screen/index.ts +49 -10
- package/packages/info-screen/package.json +3 -3
- package/packages/info-screen/tui/info-overlay.ts +54 -22
- package/packages/input-shortcuts/package.json +3 -3
- package/packages/kanboard/package.json +3 -3
- package/packages/mcp/package.json +3 -3
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +3 -3
- package/packages/notify/package.json +3 -3
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +3 -3
- package/packages/trajectory/package.json +1 -1
- package/packages/unipi/bundled.js +4371 -1553
- package/packages/updater/package.json +3 -3
- package/packages/utility/README.md +20 -0
- package/packages/utility/package.json +3 -3
- package/packages/utility/src/commands.ts +54 -0
- package/packages/utility/src/index.ts +15 -0
- package/packages/utility/src/skill-discovery.ts +178 -0
- package/packages/web-api/package.json +2 -2
- 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.1] — 2026-08-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `utility`: **skill startup discovery toggle** (`/unipi:skills-settings`, default on) — controls whether Unipi's built-in skills are cataloged in the agent's system prompt at startup. When off (`unipi.skills.discovery: false` in pi settings.json), Unipi's bundled skills are removed from the `<available_skills>` section every turn while user-owned skills (global, project, settings-mounted, third-party packages) stay cataloged; `/skill:name` invocation keeps working since pi expands those commands by reading SKILL.md directly from disk. Filtering is anchor-based (agentskills.io tags) and applied consistently per turn, preserving provider prefix cache.
|
|
14
|
+
- `autocomplete`: registered `/unipi:skills-settings` in COMMAND_REGISTRY + COMMAND_DESCRIPTIONS so the command autosuggests.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `info-screen`: **boot splash no longer eats keystrokes or strands the session** — the auto-close boot dashboard was a capturing overlay during its 2s window: typing during boot was swallowed (worse, its vim-style keys interpreted command text as tab switches), the first-keypress-cancels-auto-close rule left it on screen forever as an unclosable input-eating zombie, and prompts appeared to never send (the agent actually responded behind the overlay). The splash is now non-capturing (typing always reaches the editor) and dismisses itself via a stack-safe `handle.hide()` that only fires while it is the topmost visible overlay entry — if the user opens any dialog during the splash window (e.g. `/unipi:skills-settings`), dismissal defers until it closes, so the overlay can no longer be orphaned with a pending interaction (previously the covered overlay vanished while its `ctx.ui.select` promise hung forever). Interactive (`bootMode: "on"`) dashboards are unchanged; splash footer now reads "auto-dismissing…" instead of keyboard hints that could never reach it.
|
|
19
|
+
|
|
20
|
+
## [2.14.0] — 2026-08-29
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `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.
|
|
25
|
+
|
|
9
26
|
## [2.13.0] — 2026-08-28
|
|
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.
|
|
3
|
+
"version": "2.14.1",
|
|
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.
|
|
86
|
-
"@pi-unipi/background-tasks": "2.
|
|
87
|
-
"@pi-unipi/btw": "2.
|
|
88
|
-
"@pi-unipi/command-enchantment": "2.
|
|
89
|
-
"@pi-unipi/compactor": "2.
|
|
90
|
-
"@pi-unipi/core": "2.
|
|
91
|
-
"@pi-unipi/footer": "2.
|
|
92
|
-
"@pi-unipi/image": "2.
|
|
93
|
-
"@pi-unipi/info-screen": "2.
|
|
94
|
-
"@pi-unipi/input-shortcuts": "2.
|
|
95
|
-
"@pi-unipi/kanboard": "2.
|
|
96
|
-
"@pi-unipi/mcp": "2.
|
|
97
|
-
"@pi-unipi/memory": "2.
|
|
98
|
-
"@pi-unipi/milestone": "2.
|
|
99
|
-
"@pi-unipi/notify": "2.
|
|
100
|
-
"@pi-unipi/ralph": "2.
|
|
101
|
-
"@pi-unipi/subagents": "2.
|
|
102
|
-
"@pi-unipi/trajectory": "2.
|
|
103
|
-
"@pi-unipi/updater": "2.
|
|
104
|
-
"@pi-unipi/utility": "2.
|
|
105
|
-
"@pi-unipi/web-api": "2.
|
|
106
|
-
"@pi-unipi/workflow": "2.
|
|
85
|
+
"@pi-unipi/ask-user": "2.14.1",
|
|
86
|
+
"@pi-unipi/background-tasks": "2.14.1",
|
|
87
|
+
"@pi-unipi/btw": "2.14.1",
|
|
88
|
+
"@pi-unipi/command-enchantment": "2.14.1",
|
|
89
|
+
"@pi-unipi/compactor": "2.14.1",
|
|
90
|
+
"@pi-unipi/core": "2.14.1",
|
|
91
|
+
"@pi-unipi/footer": "2.14.1",
|
|
92
|
+
"@pi-unipi/image": "2.14.1",
|
|
93
|
+
"@pi-unipi/info-screen": "2.14.1",
|
|
94
|
+
"@pi-unipi/input-shortcuts": "2.14.1",
|
|
95
|
+
"@pi-unipi/kanboard": "2.14.1",
|
|
96
|
+
"@pi-unipi/mcp": "2.14.1",
|
|
97
|
+
"@pi-unipi/memory": "2.14.1",
|
|
98
|
+
"@pi-unipi/milestone": "2.14.1",
|
|
99
|
+
"@pi-unipi/notify": "2.14.1",
|
|
100
|
+
"@pi-unipi/ralph": "2.14.1",
|
|
101
|
+
"@pi-unipi/subagents": "2.14.1",
|
|
102
|
+
"@pi-unipi/trajectory": "2.14.1",
|
|
103
|
+
"@pi-unipi/updater": "2.14.1",
|
|
104
|
+
"@pi-unipi/utility": "2.14.1",
|
|
105
|
+
"@pi-unipi/web-api": "2.14.1",
|
|
106
|
+
"@pi-unipi/workflow": "2.14.1"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@earendil-works/pi-agent-core": "^0.84.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Structured user input tool for Pi coding agent
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "Structured user input tool for Pi coding agent \u2014 single-select, multi-select, freeform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@pi-unipi/core": "2.
|
|
43
|
+
"@pi-unipi/core": "2.14.1"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -131,6 +131,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
131
131
|
"unipi:badge-toggle": "utility",
|
|
132
132
|
"unipi:badge-settings": "utility",
|
|
133
133
|
"unipi:util-settings": "utility",
|
|
134
|
+
"unipi:skills-settings": "utility",
|
|
134
135
|
|
|
135
136
|
// ask-user (1 command)
|
|
136
137
|
"unipi:ask-user-settings": "ask-user",
|
|
@@ -270,6 +271,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
270
271
|
"unipi:badge-toggle": "Configure badge settings (autoGen, badgeEnabled, agentTool)",
|
|
271
272
|
"unipi:badge-settings": "Configure badge settings via TUI overlay",
|
|
272
273
|
"unipi:util-settings": "Unified settings — badge + diff rendering config",
|
|
274
|
+
"unipi:skills-settings": "Toggle skill startup discovery (skills catalog in system prompt)",
|
|
273
275
|
"unipi:kanboard": "Start the kanboard visualization server",
|
|
274
276
|
"unipi:kanboard-doctor": "Diagnose and fix kanboard parser issues",
|
|
275
277
|
"unipi:trajectory": "Open/reuse trajectory; add stop/off or toggle to close it",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/background-tasks",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Background tasks for UniPi
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "Background tasks for UniPi \u2014 durable shell jobs, delegated agents, attested Pi runs, and fixed-purpose Fusion workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "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.
|
|
13
|
+
"@pi-unipi/core": "2.14.1",
|
|
14
14
|
"turndown": "^7.2.4"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/btw",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "A pi extension for parallel side conversations with /unipi:btw
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "A pi extension for parallel side conversations with /unipi:btw \u2014 part of the Unipi suite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "extensions/btw.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@earendil-works/pi-tui": "^0.84.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "2.
|
|
40
|
+
"@pi-unipi/core": "2.14.1"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Context engine for Pi
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "Context engine for Pi \u2014 zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
38
|
-
"@pi-unipi/info-screen": "2.
|
|
37
|
+
"@pi-unipi/core": "2.14.1",
|
|
38
|
+
"@pi-unipi/info-screen": "2.14.1",
|
|
39
39
|
"@earendil-works/pi-agent-core": "^0.84.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Persistent status bar for Unipi
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "Persistent status bar for Unipi \u2014 subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
36
|
-
"@pi-unipi/background-tasks": "2.
|
|
35
|
+
"@pi-unipi/core": "2.14.1",
|
|
36
|
+
"@pi-unipi/background-tasks": "2.14.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
package/packages/image/README.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "2.14.1",
|
|
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.
|
|
37
|
+
"@pi-unipi/core": "2.14.1"
|
|
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 {
|
|
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 (
|
|
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
|
-
?
|
|
97
|
-
:
|
|
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
|
*
|
|
@@ -113,10 +113,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
113
|
*/
|
|
114
114
|
function showOverlay(ctx: ExtensionContext, autoCloseMs?: number): void {
|
|
115
115
|
let overlay: InfoOverlay;
|
|
116
|
+
// Splash mode (auto-close): the overlay must NEVER take keyboard focus —
|
|
117
|
+
// it lives exactly during the window where the user starts typing their
|
|
118
|
+
// first prompt. A capturing overlay here eats those keystrokes (worse: its
|
|
119
|
+
// vim-style keys "work", so command text vanishes into tab switches), and
|
|
120
|
+
// its first-keypress-cancels-auto-close rule strands it on screen forever
|
|
121
|
+
// as an unclosable, input-eating zombie. Non-capturing + stack-safe
|
|
122
|
+
// self-dismiss (see startBootTimer) makes it pure eye-candy.
|
|
123
|
+
const splashMode = autoCloseMs !== undefined && autoCloseMs > 0;
|
|
116
124
|
ctx.ui.custom<void>(
|
|
117
125
|
(tui, theme, _keybindings, done) => {
|
|
118
126
|
overlay = new InfoOverlay();
|
|
119
127
|
overlay.setTheme(theme);
|
|
128
|
+
overlay.interactive = !splashMode;
|
|
120
129
|
overlayVisible = true;
|
|
121
130
|
overlay.onClose = () => {
|
|
122
131
|
overlayVisible = false;
|
|
@@ -127,14 +136,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
136
|
const component = {
|
|
128
137
|
render: (w: number) => overlay.render(w),
|
|
129
138
|
invalidate: () => overlay.invalidate(),
|
|
139
|
+
// In splash mode the overlay is non-capturing and never receives
|
|
140
|
+
// input; keep handleInput wired for interactive ("on") mode.
|
|
130
141
|
handleInput: (data: string) => {
|
|
131
142
|
overlay.handleInput(data);
|
|
132
143
|
tui.requestRender();
|
|
133
144
|
},
|
|
134
145
|
};
|
|
135
|
-
//
|
|
146
|
+
// Splash self-dismiss may only run while this overlay is the topmost
|
|
147
|
+
// VISIBLE entry of the TUI overlay stack. If the user opens anything
|
|
148
|
+
// during the splash window (a /unipi:… settings dialog, the updater's
|
|
149
|
+
// update prompt, …), dismissal must wait — removing a covered entry
|
|
150
|
+
// breaks the covering overlay (pi retargets focus and orphans its
|
|
151
|
+
// pending interaction, e.g. a hung ctx.ui.select promise). Re-check
|
|
152
|
+
// on every timer tick; once the stack clears we dismiss as usual.
|
|
153
|
+
const isTopmostVisible = (): boolean => {
|
|
154
|
+
try {
|
|
155
|
+
const stack = (tui as unknown as { overlayStack?: Array<{ component?: unknown; hidden?: boolean }> }).overlayStack;
|
|
156
|
+
if (!stack || stack.length === 0) return true;
|
|
157
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
158
|
+
const entry = stack[i];
|
|
159
|
+
if (entry?.hidden) continue;
|
|
160
|
+
return entry?.component === component;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return true; // Stack unreadable — assume topmost (legacy behavior).
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Boot dashboard dismisses itself (splash mode).
|
|
136
168
|
if (autoCloseMs && autoCloseMs > 0) {
|
|
137
|
-
overlay.startBootTimer(autoCloseMs);
|
|
169
|
+
overlay.startBootTimer(autoCloseMs, isTopmostVisible);
|
|
138
170
|
}
|
|
139
171
|
return component;
|
|
140
172
|
},
|
|
@@ -145,17 +177,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
177
|
minWidth: 60,
|
|
146
178
|
anchor: "center" as const,
|
|
147
179
|
margin: 2,
|
|
180
|
+
nonCapturing: splashMode,
|
|
148
181
|
},
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// until we are the focused (topmost) entry; the user can still press
|
|
156
|
-
// q/Esc to close once the covering overlay is gone.
|
|
182
|
+
// In splash mode the timer uses `selfHide` (handle.hide() splices this
|
|
183
|
+
// entry out by identity) guarded by the topmost-visible check above —
|
|
184
|
+
// never `done()`, which pops whatever is TOPMOST. `isTopmostOverlay`
|
|
185
|
+
// remains as the focused-done() fallback for interactive ("on") mode,
|
|
186
|
+
// where the user drives the dashboard directly: it is topmost while
|
|
187
|
+
// being driven, so the q/Esc → done() path is correct there.
|
|
157
188
|
onHandle: (handle) => {
|
|
158
189
|
overlay.isTopmostOverlay = () => handle.isFocused();
|
|
190
|
+
overlay.selfHide = () => {
|
|
191
|
+
overlayVisible = false;
|
|
192
|
+
if (typeof handle.hide === "function") {
|
|
193
|
+
handle.hide();
|
|
194
|
+
} else if (typeof (handle as { setHidden?: (v: boolean) => void }).setHidden === "function") {
|
|
195
|
+
(handle as { setHidden: (v: boolean) => void }).setHidden(true);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
159
198
|
},
|
|
160
199
|
}
|
|
161
200
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Dashboard and module registry for Unipi
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "Dashboard and module registry for Unipi \u2014 configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.14.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -69,6 +69,21 @@ export class InfoOverlay implements Component {
|
|
|
69
69
|
* `startBootTimer` for why.
|
|
70
70
|
*/
|
|
71
71
|
isTopmostOverlay?: () => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Stack-safe self-removal via the overlay handle (`handle.hide()` splices
|
|
74
|
+
* this entry out of the TUI stack by identity, unlike `done()` which pops
|
|
75
|
+
* whatever is TOPMOST). Set from `onHandle` in index.ts. When available,
|
|
76
|
+
* the boot auto-close timer prefers this over `done()` so a splash that
|
|
77
|
+
* lingers while another overlay opens can never dismiss that overlay
|
|
78
|
+
* instead of itself.
|
|
79
|
+
*/
|
|
80
|
+
selfHide?: () => void;
|
|
81
|
+
/**
|
|
82
|
+
* False when running as a non-capturing boot splash (auto-close): the
|
|
83
|
+
* overlay never receives keyboard input, so interactive hints like
|
|
84
|
+
* "q/Esc close" would be misleading and are replaced accordingly.
|
|
85
|
+
*/
|
|
86
|
+
interactive = true;
|
|
72
87
|
|
|
73
88
|
private overlay = new OverlayTheme();
|
|
74
89
|
|
|
@@ -232,31 +247,46 @@ export class InfoOverlay implements Component {
|
|
|
232
247
|
}
|
|
233
248
|
|
|
234
249
|
/**
|
|
235
|
-
* Auto-close the overlay after `ms
|
|
250
|
+
* Auto-close the overlay after `ms`.
|
|
236
251
|
*
|
|
237
|
-
* Used when the overlay is shown
|
|
238
|
-
* it should get out of the way on its own rather than
|
|
252
|
+
* Used when the overlay is shown as a boot splash: the dashboard is
|
|
253
|
+
* informational, so it should get out of the way on its own rather than
|
|
254
|
+
* requiring a keypress.
|
|
239
255
|
*
|
|
240
|
-
*
|
|
241
|
-
* TUI stack
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
256
|
+
* Dismissal uses `selfHide` (handle.hide() — removes THIS entry from the
|
|
257
|
+
* TUI overlay stack by identity) and only fires while `isTopmostVisible`
|
|
258
|
+
* confirms nothing is stacked above: dismissing a covered overlay breaks
|
|
259
|
+
* the covering one (pi retargets focus and orphans its pending
|
|
260
|
+
* interaction, e.g. a ctx.ui.select promise that never resolves while its
|
|
261
|
+
* overlay vanishes). If covered, the timer re-arms and retries.
|
|
262
|
+
*
|
|
263
|
+
* If `selfHide` is unavailable (older host), falls back to the guarded
|
|
264
|
+
* `onClose` (`done()`) path, which requires focus (topmost) for the same
|
|
265
|
+
* reason.
|
|
248
266
|
*/
|
|
249
|
-
startBootTimer(ms: number): void {
|
|
267
|
+
startBootTimer(ms: number, isTopmostVisible?: () => boolean): void {
|
|
250
268
|
this.cancelBootTimer();
|
|
251
269
|
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
252
270
|
const arm = (): void => {
|
|
253
271
|
this.bootTimer = setTimeout(() => {
|
|
254
272
|
this.bootTimer = null;
|
|
255
273
|
if (this._destroyed) return;
|
|
274
|
+
if (this.selfHide) {
|
|
275
|
+
if (isTopmostVisible && !isTopmostVisible()) {
|
|
276
|
+
// Something is stacked on top of us — dismissing now would break
|
|
277
|
+
// it (orphaned select promise, focus retarget). Retry shortly;
|
|
278
|
+
// once the stack clears we dismiss as usual.
|
|
279
|
+
arm();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.selfHide();
|
|
283
|
+
this.destroy();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
256
286
|
if (this.isTopmostOverlay && !this.isTopmostOverlay()) {
|
|
257
|
-
//
|
|
258
|
-
// instead of this dashboard. Retry shortly; once
|
|
259
|
-
//
|
|
287
|
+
// Fallback (no selfHide available): closing now would pop the
|
|
288
|
+
// covering overlay instead of this dashboard. Retry shortly; once
|
|
289
|
+
// we are topmost the close is safe.
|
|
260
290
|
arm();
|
|
261
291
|
return;
|
|
262
292
|
}
|
|
@@ -346,7 +376,7 @@ export class InfoOverlay implements Component {
|
|
|
346
376
|
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Modules will register groups on startup."), innerWidth));
|
|
347
377
|
for (let i = 0; i < 4; i++) lines.push(this.overlay.frameLine("", innerWidth));
|
|
348
378
|
lines.push(this.overlay.ruleLine(innerWidth));
|
|
349
|
-
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "q/Esc close · r refresh"), innerWidth));
|
|
379
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", this.interactive ? "q/Esc close · r refresh" : "auto-dismissing…"), innerWidth));
|
|
350
380
|
lines.push(this.overlay.borderLine(innerWidth, "bottom"));
|
|
351
381
|
return lines;
|
|
352
382
|
}
|
|
@@ -529,12 +559,14 @@ export class InfoOverlay implements Component {
|
|
|
529
559
|
const lastUp = infoRegistry.getLastUpdated(group?.id ?? "");
|
|
530
560
|
const age = lastUp > 0 ? humanizeAge(Date.now() - lastUp) : "loading…";
|
|
531
561
|
|
|
532
|
-
const hints =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
562
|
+
const hints = this.interactive
|
|
563
|
+
? [
|
|
564
|
+
`${this.overlay.fg("accent", "←/→")} tabs`,
|
|
565
|
+
`${this.overlay.fg("success", "↑/↓")} scroll`,
|
|
566
|
+
`${this.overlay.fg("warning", "r")} refresh`,
|
|
567
|
+
`${this.overlay.fg("error", "q/Esc")} close`,
|
|
568
|
+
]
|
|
569
|
+
: [`${this.overlay.fg("dim", "auto-dismissing…")}`];
|
|
538
570
|
|
|
539
571
|
const hintStr = hints.join(` ${this.overlay.fg("borderMuted", "•")} `);
|
|
540
572
|
|