@pi-unipi/unipi 3.0.0-alpha.27 → 3.0.0-alpha.29

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +9 -1
  3. package/package.json +21 -21
  4. package/packages/ask-user/package.json +2 -2
  5. package/packages/autocomplete/package.json +1 -1
  6. package/packages/autocomplete/src/constants.ts +1 -1
  7. package/packages/background-tasks/package.json +2 -2
  8. package/packages/btw/package.json +2 -2
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/core/package.json +1 -1
  11. package/packages/footer/package.json +3 -3
  12. package/packages/fusion/package.json +4 -4
  13. package/packages/info-screen/package.json +2 -2
  14. package/packages/input-shortcuts/package.json +2 -2
  15. package/packages/kanboard/package.json +7 -7
  16. package/packages/long-horizon/package.json +2 -2
  17. package/packages/mcp/package.json +2 -2
  18. package/packages/memory/package.json +3 -3
  19. package/packages/notify/README.md +4 -0
  20. package/packages/notify/activity.ts +4 -2
  21. package/packages/notify/events.ts +116 -11
  22. package/packages/notify/input-needed-message.ts +27 -0
  23. package/packages/notify/package.json +2 -2
  24. package/packages/notify/settings.ts +2 -0
  25. package/packages/notify/skills/configure-notify/SKILL.md +4 -2
  26. package/packages/skill-registry/package.json +2 -2
  27. package/packages/subagents/package.json +3 -3
  28. package/packages/unipi/bundled.js +87 -23
  29. package/packages/updater/package.json +2 -2
  30. package/packages/utility/README.md +3 -2
  31. package/packages/utility/package.json +2 -2
  32. package/packages/utility/skills/summarize/SKILL.md +9 -2
  33. package/packages/utility/skills/utility/SKILL.md +1 -1
  34. package/packages/utility/src/summarize/index.ts +7 -9
  35. package/packages/watchdog/package.json +3 -3
  36. package/packages/web-api/package.json +2 -2
  37. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.0.0-alpha.29] — 2026-10-04
10
+
11
+ ### Fixed
12
+ - `notify`: prompts that send no event of their own (third-party `ask_user` tools, the permission prompt, the plan review) can notify through the new **Input Needed** event (`input_needed`, off by default); closing any prompt now stops its reminders. (GitHub #38)
13
+
14
+ ## [3.0.0-alpha.28] — 2026-10-04
15
+
16
+ ### Changed
17
+ - `utility`: **`/unipi:summarize` re-summarizes the last reply**, with the session as context. The command sends only `/skill:summarize`, plus any text you add, which reaches the skill as your question ("what would it change?" now asks about the last reply). It no longer wraps your text in a "summarize this session" prompt.
18
+
9
19
  ## [3.0.0-alpha.27] — 2026-10-04
10
20
 
11
21
  ### Added
package/README.md CHANGED
@@ -27,9 +27,16 @@
27
27
  ## Install
28
28
 
29
29
  ```bash
30
- pi install npm:@pi-unipi/unipi
30
+ pi install npm:@pi-unipi/unipi@alpha
31
31
  ```
32
32
 
33
+ > [!NOTE]
34
+ > **UniPi 3 is in alpha.** This README describes v3, published under the npm
35
+ > `alpha` tag. Plain `pi install npm:@pi-unipi/unipi` still installs the stable
36
+ > 2.x line ([2.x README](https://github.com/Neuron-Mr-White/unipi/tree/main#readme)).
37
+ > To update an alpha install: `pi update npm:@pi-unipi/unipi@alpha`. To go back
38
+ > to stable: `pi install npm:@pi-unipi/unipi@latest`.
39
+
33
40
  UniPi needs [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
34
41
  `0.87.1` or later. This command installs 21 extension packages. Each package
35
42
  also works alone.
@@ -242,6 +249,7 @@ The recent releases, in short. The [changelog](CHANGELOG.md) has the details.
242
249
 
243
250
  | Release | What changed for you |
244
251
  |---|---|
252
+ | alpha.29 | Plan mode plans with you: it asks before it submits, blocks only edits outside the plan, and the review shows a summary beside the steps. Notify can alert on any prompt the agent waits on (Input Needed). |
245
253
  | alpha.27 | The footer shows tokens in and out and the session cost under the input. It fits narrow and short windows. Each part of the stats line, the frame badges and the rainbow can be turned off in `/unipi:settings` → Footer. The classic footer is gone. |
246
254
  | alpha.25 | The Unicrab splash at startup. A new `/unipi:info`: this session, the context bucket, compaction savings and `s`/`p`/`g` scope tags. Prompts that UniPi sends for you (goal, summarize, answer) show as a UniPi panel, not as your own message. |
247
255
  | alpha.24 | The `summarize` skill and `/unipi:summarize [focus]`. Long work ends with a short recap: the answer first, then findings, open items and questions. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -83,26 +83,26 @@
83
83
  "typebox": "^1.1.38"
84
84
  },
85
85
  "dependencies": {
86
- "@pi-unipi/ask-user": "3.0.0-alpha.27",
87
- "@pi-unipi/background-tasks": "3.0.0-alpha.27",
88
- "@pi-unipi/btw": "3.0.0-alpha.27",
89
- "@pi-unipi/command-enchantment": "3.0.0-alpha.27",
90
- "@pi-unipi/compactor": "3.0.0-alpha.27",
91
- "@pi-unipi/core": "3.0.0-alpha.27",
92
- "@pi-unipi/footer": "3.0.0-alpha.27",
93
- "@pi-unipi/fusion": "3.0.0-alpha.27",
94
- "@pi-unipi/info-screen": "3.0.0-alpha.27",
95
- "@pi-unipi/input-shortcuts": "3.0.0-alpha.27",
96
- "@pi-unipi/kanboard": "3.0.0-alpha.27",
97
- "@pi-unipi/mcp": "3.0.0-alpha.27",
98
- "@pi-unipi/memory": "3.0.0-alpha.27",
99
- "@pi-unipi/notify": "3.0.0-alpha.27",
100
- "@pi-unipi/skill-registry": "3.0.0-alpha.27",
101
- "@pi-unipi/subagents": "3.0.0-alpha.27",
102
- "@pi-unipi/updater": "3.0.0-alpha.27",
103
- "@pi-unipi/utility": "3.0.0-alpha.27",
104
- "@pi-unipi/web-api": "3.0.0-alpha.27",
105
- "@pi-unipi/workflow": "3.0.0-alpha.27"
86
+ "@pi-unipi/ask-user": "3.0.0-alpha.29",
87
+ "@pi-unipi/background-tasks": "3.0.0-alpha.29",
88
+ "@pi-unipi/btw": "3.0.0-alpha.29",
89
+ "@pi-unipi/command-enchantment": "3.0.0-alpha.29",
90
+ "@pi-unipi/compactor": "3.0.0-alpha.29",
91
+ "@pi-unipi/core": "3.0.0-alpha.29",
92
+ "@pi-unipi/footer": "3.0.0-alpha.29",
93
+ "@pi-unipi/fusion": "3.0.0-alpha.29",
94
+ "@pi-unipi/info-screen": "3.0.0-alpha.29",
95
+ "@pi-unipi/input-shortcuts": "3.0.0-alpha.29",
96
+ "@pi-unipi/kanboard": "3.0.0-alpha.29",
97
+ "@pi-unipi/mcp": "3.0.0-alpha.29",
98
+ "@pi-unipi/memory": "3.0.0-alpha.29",
99
+ "@pi-unipi/notify": "3.0.0-alpha.29",
100
+ "@pi-unipi/skill-registry": "3.0.0-alpha.29",
101
+ "@pi-unipi/subagents": "3.0.0-alpha.29",
102
+ "@pi-unipi/updater": "3.0.0-alpha.29",
103
+ "@pi-unipi/utility": "3.0.0-alpha.29",
104
+ "@pi-unipi/web-api": "3.0.0-alpha.29",
105
+ "@pi-unipi/workflow": "3.0.0-alpha.29"
106
106
  },
107
107
  "devDependencies": {
108
108
  "@earendil-works/pi-agent-core": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27"
43
+ "@pi-unipi/core": "3.0.0-alpha.29"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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",
@@ -160,7 +160,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
160
160
  "unipi:cleanup": "Remove stale UniPi temp files and leftovers (preview first)",
161
161
  "unipi:doctor": "Check UniPi's runtime: config, model cache, Decision Model, skills",
162
162
  "unipi:answer": "Answer the last reply — fixed input under the scrollable reply, per question, or web form",
163
- "unipi:summarize": "Summarize this session — answer first, then findings, open items, questions; add text to set a focus",
163
+ "unipi:summarize": "Summarize your last reply again (answer first, then findings, open items, questions). Add text to ask about it",
164
164
  "unipi:skills": "Manage skills — on/off per project, listed or not, the skill vault",
165
165
 
166
166
  "unipi:kanboard": "Kanboard — capture tasks, run them, open the board",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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.87.1",
13
- "@pi-unipi/core": "3.0.0-alpha.27",
13
+ "@pi-unipi/core": "3.0.0-alpha.29",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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.87.1"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.27"
40
+ "@pi-unipi/core": "3.0.0-alpha.29"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Compaction for Pi — lossless zero-LLM or model summaries, rebuilt from full history, with session recall",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,8 +33,8 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.27",
37
- "@pi-unipi/info-screen": "3.0.0-alpha.27",
36
+ "@pi-unipi/core": "3.0.0-alpha.29",
37
+ "@pi-unipi/info-screen": "3.0.0-alpha.29",
38
38
  "@earendil-works/pi-agent-core": "^0.87.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27",
36
- "@pi-unipi/background-tasks": "3.0.0-alpha.27"
35
+ "@pi-unipi/core": "3.0.0-alpha.29",
36
+ "@pi-unipi/background-tasks": "3.0.0-alpha.29"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/fusion",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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,9 +32,9 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.27",
36
- "@pi-unipi/subagents": "3.0.0-alpha.27",
37
- "@pi-unipi/utility": "3.0.0-alpha.27"
35
+ "@pi-unipi/core": "3.0.0-alpha.29",
36
+ "@pi-unipi/subagents": "3.0.0-alpha.29",
37
+ "@pi-unipi/utility": "3.0.0-alpha.29"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Unicrab startup splash and the /unipi:info dashboard — this session, context, usage, compaction savings",
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": "3.0.0-alpha.27"
36
+ "@pi-unipi/core": "3.0.0-alpha.29"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27"
36
+ "@pi-unipi/core": "3.0.0-alpha.29"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Kanboard v3 — deferred-work board: CLI/daemon bridge, task runner and skill",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "3.0.0-alpha.27"
34
+ "@pi-unipi/core": "3.0.0-alpha.29"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -48,10 +48,10 @@
48
48
  "themes": []
49
49
  },
50
50
  "optionalDependencies": {
51
- "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.27",
52
- "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.27",
53
- "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.27",
54
- "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.27",
55
- "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.27"
51
+ "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.29",
52
+ "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.29",
53
+ "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.29",
54
+ "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.29",
55
+ "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.29"
56
56
  }
57
57
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/long-horizon",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Long-horizon execution for Pi — mode-gated /goal /ralph /swarm /graph orchestration with a TypeSafe prompt judge",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.27"
40
+ "@pi-unipi/core": "3.0.0-alpha.29"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27"
33
+ "@pi-unipi/core": "3.0.0-alpha.29"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Persistent cross-session memory on MemPalace (auto-installed): daemon-routed writes, shared palace with other agents, for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -42,8 +42,8 @@
42
42
  "README.md"
43
43
  ],
44
44
  "dependencies": {
45
- "@pi-unipi/core": "3.0.0-alpha.27",
46
- "@pi-unipi/info-screen": "3.0.0-alpha.27",
45
+ "@pi-unipi/core": "3.0.0-alpha.29",
46
+ "@pi-unipi/info-screen": "3.0.0-alpha.29",
47
47
  "js-yaml": "^4.1.0"
48
48
  },
49
49
  "peerDependencies": {
@@ -71,9 +71,12 @@ notify_user({ title: "Build failed", message: "tsc found 12 errors.", priority:
71
71
  | `session_shutdown` | off | The session ends. |
72
72
  | `ask_user_prompt` | off | The agent asks you a question and waits. |
73
73
  | `permission_request` | off | A permission prompt opens. This needs [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system). |
74
+ | `input_needed` | off | The agent waits on any prompt while it runs. Covers prompts that send no event of their own — third-party `ask_user` tools, the permission prompt, the plan review. |
74
75
 
75
76
  `ask_user_prompt` and `permission_request` are blocking events. The agent stops until you answer.
76
77
 
78
+ `input_needed` is the catch-all for the same situation: Pi fires it around every blocking prompt, including ones that emit no event of their own. It only fires while the agent is running — prompts you open yourself while the agent is idle (for example `/unipi:settings`) stay quiet — and a prompt already announced as `ask_user_prompt` or `permission_request` is not announced twice. Closing any prompt stops its reminders.
79
+
77
80
  ## Settings
78
81
 
79
82
  Open `/unipi:settings` → **Notify**. The file is `~/.unipi/config/notify/config.json`. A project file at `.unipi/config/notify/config.json` overrides it.
@@ -104,6 +107,7 @@ A reminder has the text `(still waiting)` in its title and `high` priority. Remi
104
107
  - You press a key.
105
108
  - herdr reports that the agent is not blocked.
106
109
  - The agent starts a new turn.
110
+ - The prompt closes.
107
111
  - The session ends.
108
112
 
109
113
  Only one reminder runs at a time. Silence after input does not apply to blocking events.
@@ -26,6 +26,7 @@ const VALID_PLATFORMS: ReadonlySet<NotifyPlatform> = new Set([
26
26
  const BLOCKING_EVENTS: ReadonlySet<string> = new Set([
27
27
  "ask_user_prompt",
28
28
  "permission_request",
29
+ "input_needed",
29
30
  ]);
30
31
 
31
32
  /** Whether an event type is a human-blocking prompt. */
@@ -51,8 +52,9 @@ export function resetInputActivity(): void {
51
52
  * Empty `platforms` (while enabled) silences all incoming channels, matching
52
53
  * `events.*.platforms: []` → all enabled.
53
54
  *
54
- * Human-blocking events (`ask_user_prompt`, `permission_request`) bypass the
55
- * filter entirely — the keypress that triggered them must not mute them.
55
+ * Human-blocking events (`ask_user_prompt`, `permission_request`,
56
+ * `input_needed`) bypass the filter entirely — the keypress that triggered
57
+ * them must not mute them.
56
58
  */
57
59
  export function filterPlatformsAfterInput(
58
60
  platforms: NotifyPlatform[],
@@ -15,6 +15,7 @@ import { sendTelegramNotification } from "./platforms/telegram.js";
15
15
  import { sendNtfyNotification } from "./platforms/ntfy.js";
16
16
  import { buildAskUserPromptMessage } from "./ask-user-prompt-message.js";
17
17
  import { buildPermissionPromptMessage } from "./permission-prompt-message.js";
18
+ import { buildInputNeededMessage } from "./input-needed-message.js";
18
19
  import { summarizeLastMessage } from "./summarize.js";
19
20
  import { filterPlatformsAfterInput, isBlockingEvent } from "./activity.js";
20
21
 
@@ -75,6 +76,22 @@ const unsubs: Array<() => void> = [];
75
76
  /** Pending re-notify interval for an unanswered blocking prompt. */
76
77
  let renotifyTimer: ReturnType<typeof setInterval> | undefined;
77
78
 
79
+ /**
80
+ * A `ui_prompt_start` within this window of an ask_user/permission alert is
81
+ * the same prompt — pi emits the bus event and the lifecycle event back to
82
+ * back. Exported for tests.
83
+ */
84
+ export const PROMPT_DEDUP_MS = 2000;
85
+
86
+ /** Whether an agent run is in progress — `input_needed` fires only while it is. */
87
+ let agentRunning = false;
88
+
89
+ /** Open blocking UI prompts (`ui_prompt_start`/`ui_prompt_end` pairing). */
90
+ let openPrompts = 0;
91
+
92
+ /** When the last ask_user/permission alert went out, for ui_prompt de-dup. */
93
+ let lastBlockingAlertAt = 0;
94
+
78
95
  /** Cancel any pending re-notify timer. Safe to call at any time. */
79
96
  export function disarmRenotify(): void {
80
97
  const timer = renotifyTimer;
@@ -128,9 +145,33 @@ function armRenotify(
128
145
  renotifyTimer = timer;
129
146
  }
130
147
 
148
+ /**
149
+ * Dispatch a human-blocking prompt alert: high priority plus the reminder
150
+ * loop. Non-`input_needed` alerts also stamp `lastBlockingAlertAt` so the
151
+ * back-to-back `ui_prompt_start` for the same prompt is seen as announced.
152
+ */
153
+ function notifyBlocking(
154
+ pi: ExtensionAPI,
155
+ eventKey: string,
156
+ title: string,
157
+ message: string,
158
+ platforms: NotifyPlatform[],
159
+ config: NotifyConfig,
160
+ cwd: string,
161
+ dispatch: DispatchNotification,
162
+ ): void {
163
+ if (eventKey !== "input_needed") lastBlockingAlertAt = Date.now();
164
+ dispatch(pi, title, message, platforms, eventKey, config, cwd, "high").catch(() => {
165
+ // Silently ignore — background notification failure is non-blocking.
166
+ });
167
+ armRenotify(pi, title, message, platforms, eventKey, config, cwd, dispatch);
168
+ }
169
+
131
170
  /** Unregister all previously registered pi.events.on() listeners. */
132
171
  function unregisterAll(): void {
133
172
  disarmRenotify();
173
+ agentRunning = false;
174
+ openPrompts = 0;
134
175
  for (const unsub of unsubs) {
135
176
  try { unsub(); } catch { /* ignore */ }
136
177
  }
@@ -161,6 +202,7 @@ export const BUILTIN_EVENTS: Record<
161
202
  session_shutdown: { hook: "session_shutdown", label: "Session End" },
162
203
  ask_user_prompt: { hook: UNIPI_EVENTS.ASK_USER_PROMPT, label: "Question Asked" },
163
204
  permission_request: { hook: PERMISSION_UI_PROMPT_EVENT, label: "Permission Request" },
205
+ input_needed: { hook: "ui_prompt_start", label: "Input Needed" },
164
206
  };
165
207
 
166
208
  /**
@@ -184,7 +226,9 @@ export function registerEventListeners(
184
226
  unregisterAll();
185
227
  // Register built-in events (except agent lifecycle notifications which have custom logic)
186
228
  for (const [eventKey, def] of Object.entries(BUILTIN_EVENTS)) {
187
- if (isAgentNotificationEvent(eventKey)) continue; // handled separately below
229
+ // Agent notifications have custom logic; input_needed has its own
230
+ // ui_prompt_start registration below (with agent-running + de-dup guards).
231
+ if (isAgentNotificationEvent(eventKey) || eventKey === "input_needed") continue;
188
232
 
189
233
  const eventConfig = config.events[eventKey];
190
234
  if (!eventConfig?.enabled) continue;
@@ -192,6 +236,12 @@ export function registerEventListeners(
192
236
  const handler = (payload: unknown) => {
193
237
  const title = `Pi — ${def.label}`;
194
238
  const message = buildEventMessage(eventKey, payload);
239
+ // Human-blocking prompts: high priority, reminder loop, and a stamp
240
+ // that keeps the follow-up ui_prompt_start from double-firing.
241
+ if (isBlockingEvent(eventKey)) {
242
+ notifyBlocking(pi, eventKey, title, message, eventConfig.platforms, config, cwd, dispatch);
243
+ return;
244
+ }
195
245
  // Fire-and-forget: don't block the event emitter
196
246
  dispatch(
197
247
  pi,
@@ -205,9 +255,6 @@ export function registerEventListeners(
205
255
  ).catch(() => {
206
256
  // Silently ignore — background notification failure is non-blocking.
207
257
  });
208
- if (isBlockingEvent(eventKey)) {
209
- armRenotify(pi, title, message, eventConfig.platforms, eventKey, config, cwd, dispatch);
210
- }
211
258
  };
212
259
 
213
260
  // Pi lifecycle events are dispatched via ExtensionRunner — must use
@@ -225,14 +272,16 @@ export function registerEventListeners(
225
272
  const askUserConfig = config.events["ask_user_prompt"];
226
273
  if (askUserConfig?.enabled) {
227
274
  unsubs.push(pi.events.on(ASK_USER_PROMPT_EVENT, (payload: unknown) => {
228
- const title = `Pi — ${BUILTIN_EVENTS.ask_user_prompt.label}`;
229
- const message = buildAskUserPromptMessage(payload);
230
- dispatch(pi, title, message, askUserConfig.platforms, "ask_user_prompt", config, cwd, "high").catch(
231
- () => {
232
- // Silently ignore — background notification failure is non-blocking.
233
- }
275
+ notifyBlocking(
276
+ pi,
277
+ "ask_user_prompt",
278
+ `Pi — ${BUILTIN_EVENTS.ask_user_prompt.label}`,
279
+ buildAskUserPromptMessage(payload),
280
+ askUserConfig.platforms,
281
+ config,
282
+ cwd,
283
+ dispatch,
234
284
  );
235
- armRenotify(pi, title, message, askUserConfig.platforms, "ask_user_prompt", config, cwd, dispatch);
236
285
  }));
237
286
  }
238
287
 
@@ -242,11 +291,67 @@ export function registerEventListeners(
242
291
  if ((payload as { active?: unknown } | null)?.active === false) disarmRenotify();
243
292
  }));
244
293
  (pi as any).on("agent_start", () => {
294
+ agentRunning = true;
245
295
  disarmRenotify();
246
296
  });
247
297
 
248
298
  registerAgentNotification(pi, "agent_end", config, cwd, dispatch);
249
299
  registerAgentNotification(pi, "agent_settled", config, cwd, dispatch);
300
+
301
+ // Keep the agent-running flag in sync. Registered after the agent
302
+ // notification handlers above so the first agent_end handler stays the
303
+ // notification path (pi.on handlers fire in registration order).
304
+ (pi as any).on("agent_end", () => {
305
+ agentRunning = false;
306
+ });
307
+
308
+ registerInputNeeded(pi, config, cwd, dispatch);
309
+ }
310
+
311
+ /**
312
+ * pi ≥0.84.4 fires `ui_prompt_start`/`ui_prompt_end` around every blocking
313
+ * user-facing prompt — including ones that emit no bus event of their own
314
+ * (third-party ask_user tools, the permission prompt, the plan review).
315
+ *
316
+ * The start/end pair is registered whenever we are not a subagent child,
317
+ * independent of the `input_needed` flag: the open-prompt counter must stay
318
+ * paired so a prompt closing also disarms ask_user/permission reminders.
319
+ * Only the notification itself respects the flag, and it fires only while
320
+ * the agent is running — a `ui_prompt_start` while idle is the user's own
321
+ * overlay (e.g. /unipi:settings).
322
+ */
323
+ function registerInputNeeded(
324
+ pi: ExtensionAPI,
325
+ config: NotifyConfig,
326
+ cwd: string,
327
+ dispatch: DispatchNotification,
328
+ ): void {
329
+ // Subagent children never own a user-facing prompt.
330
+ if (process.env.UNIPI_SUBAGENT_CHILD === "1") return;
331
+
332
+ (pi as any).on("ui_prompt_start", (payload: unknown) => {
333
+ openPrompts += 1;
334
+ const eventConfig = config.events["input_needed"];
335
+ if (!eventConfig?.enabled) return;
336
+ if (!agentRunning) return;
337
+ // Same prompt already announced as ask_user/permission_request.
338
+ if (Date.now() - lastBlockingAlertAt < PROMPT_DEDUP_MS) return;
339
+ notifyBlocking(
340
+ pi,
341
+ "input_needed",
342
+ `Pi — ${BUILTIN_EVENTS.input_needed.label}`,
343
+ buildInputNeededMessage(payload),
344
+ eventConfig.platforms,
345
+ config,
346
+ cwd,
347
+ dispatch,
348
+ );
349
+ });
350
+
351
+ (pi as any).on("ui_prompt_end", () => {
352
+ openPrompts = Math.max(0, openPrompts - 1);
353
+ if (openPrompts === 0) disarmRenotify();
354
+ });
250
355
  }
251
356
 
252
357
  /** Get all platforms that are currently enabled in config */
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @pi-unipi/notify — Internal helper: build notification message from pi's
3
+ * `ui_prompt_start` lifecycle payload.
4
+ *
5
+ * @internal — not part of the public API. Shared by the event listener and tests.
6
+ */
7
+
8
+ /** Longest prompt title kept verbatim before an ellipsis is appended. */
9
+ export const INPUT_TITLE_MAX_CHARS = 120;
10
+
11
+ function cleanKind(value: unknown): string {
12
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : "prompt";
13
+ }
14
+
15
+ /** Truncate by code points (Array.from) so surrogate pairs are never split. */
16
+ function truncate(text: string, max: number): string {
17
+ const chars = Array.from(text);
18
+ return chars.length <= max ? text : `${chars.slice(0, max).join("")}…`;
19
+ }
20
+
21
+ /** Build a human-readable notification message from a ui_prompt_start payload. */
22
+ export function buildInputNeededMessage(payload: unknown): string {
23
+ const p = (payload ?? {}) as { title?: unknown; kind?: unknown };
24
+ const title = typeof p.title === "string" ? p.title.replace(/\s+/g, " ").trim() : "";
25
+ if (title.length === 0) return `Waiting for your input (${cleanKind(p.kind)})`;
26
+ return `Waiting for your input: ${truncate(title, INPUT_TITLE_MAX_CHARS)}`;
27
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27",
37
+ "@pi-unipi/core": "3.0.0-alpha.29",
38
38
  "node-notifier": "^10.0.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -30,6 +30,7 @@ export const DEFAULT_CONFIG: NotifyConfig = {
30
30
  session_shutdown: { enabled: false, platforms: [] },
31
31
  ask_user_prompt: { enabled: false, platforms: [] },
32
32
  permission_request: { enabled: false, platforms: [] },
33
+ input_needed: { enabled: false, platforms: [] },
33
34
  },
34
35
  native: {
35
36
  enabled: true,
@@ -79,6 +80,7 @@ const EVENT_PHRASES: Record<string, string> = {
79
80
  session_shutdown: "the session shuts down.",
80
81
  ask_user_prompt: "the agent asks you a question.",
81
82
  permission_request: "a permission prompt opens.",
83
+ input_needed: "the agent is waiting on any prompt (covers tools that send no event of their own).",
82
84
  };
83
85
 
84
86
  /** One section per known event: enable + platform routing. */
@@ -38,7 +38,8 @@ Help users configure the `@pi-unipi/notify` notification system.
38
38
  "memory_consolidated": { "enabled": false, "platforms": [] },
39
39
  "session_shutdown": { "enabled": false, "platforms": [] },
40
40
  "ask_user_prompt": { "enabled": false, "platforms": [] },
41
- "permission_request": { "enabled": false, "platforms": [] }
41
+ "permission_request": { "enabled": false, "platforms": [] },
42
+ "input_needed": { "enabled": false, "platforms": [] }
42
43
  },
43
44
  "native": {
44
45
  "enabled": true,
@@ -207,10 +208,11 @@ ntfy uses dedicated `ntfy.json` files at both global and project scope, with ful
207
208
  | `session_shutdown` | Off | Session ends |
208
209
  | `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
209
210
  | `permission_request` | Off | A permission prompt is about to be shown |
211
+ | `input_needed` | Off | The agent waits on any prompt while it runs (covers tools that send no event of their own) |
210
212
 
211
213
  Each event can override `platforms` — empty array means use `defaultPlatforms`.
212
214
 
213
- `ask_user_prompt` and `permission_request` are **blocking** events: while one is unanswered the agent is parked, so notify re-sends it periodically (see the Re-notify unanswered prompts section under Platforms).
215
+ `ask_user_prompt`, `permission_request` and `input_needed` are **blocking** events: while one is unanswered the agent is parked, so notify re-sends it periodically (see the Re-notify unanswered prompts section under Platforms).
214
216
 
215
217
  ### `permission_request`
216
218
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/skill-registry",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Skill registry for pi — per-project skill on/off, a skill vault, and jev-judged skill exposure",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "main": "index.ts",
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.27"
36
+ "@pi-unipi/core": "3.0.0-alpha.29"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/subagents",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Subagents for UniPi — Devin-model independent child agents",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -9,8 +9,8 @@
9
9
  "test": "npx tsx --test src/__tests__/*.test.ts"
10
10
  },
11
11
  "dependencies": {
12
- "@pi-unipi/core": "3.0.0-alpha.27",
13
- "@pi-unipi/utility": "3.0.0-alpha.27",
12
+ "@pi-unipi/core": "3.0.0-alpha.29",
13
+ "@pi-unipi/utility": "3.0.0-alpha.29",
14
14
  "@earendil-works/pi-agent-core": "^0.87.1",
15
15
  "acorn": "8.18.0"
16
16
  },
@@ -10735,8 +10735,7 @@ init_core();
10735
10735
  var SUMMARIZE_SKILL = "summarize";
10736
10736
  function summarizePrompt(focus) {
10737
10737
  const f = focus.trim();
10738
- const scope = f ? `Summarize this session for me, with the focus on: ${f}` : "Summarize this session for me: what we did, why, and where it stands now.";
10739
- return `/skill:${SUMMARIZE_SKILL} ${scope} Use only what happened in this session. Do not call tools.`;
10738
+ return f ? `/skill:${SUMMARIZE_SKILL} ${f}` : `/skill:${SUMMARIZE_SKILL}`;
10740
10739
  }
10741
10740
  function hasSummarizeSkill(pi) {
10742
10741
  try {
@@ -10761,7 +10760,7 @@ function registerSummarizeCommand(pi) {
10761
10760
  sendHarnessUserMessage(
10762
10761
  pi,
10763
10762
  summarizePrompt(focus),
10764
- { source: "Utility", title: "Summarize", synopsis: focus ? `focus: ${focus}` : "whole session" },
10763
+ { source: "Utility", title: "Summarize", synopsis: focus ? `ask: ${focus}` : "last reply" },
10765
10764
  { expandPromptTemplates: true }
10766
10765
  );
10767
10766
  await new Promise((r) => setTimeout(r, 0));
@@ -25337,10 +25336,10 @@ import { dirname as dirname20, join as join37 } from "node:path";
25337
25336
  import { randomUUID as randomUUID6 } from "node:crypto";
25338
25337
  function primaryArg(name, args) {
25339
25338
  if (!args) return "";
25340
- const truncate2 = (v, max) => v.length > max ? `${v.slice(0, Math.max(0, max - 1))}\u2026` : v;
25339
+ const truncate3 = (v, max) => v.length > max ? `${v.slice(0, Math.max(0, max - 1))}\u2026` : v;
25341
25340
  const firstLine3 = (v) => v.split("\n", 1)[0] ?? "";
25342
25341
  const value = name === "bash" ? args.command : name === "read" || name === "edit" || name === "write" ? args.path ?? args.file_path ?? args.filePath : Object.values(args).find((entry) => typeof entry === "string");
25343
- return typeof value === "string" ? truncate2(firstLine3(value), 100) : "";
25342
+ return typeof value === "string" ? truncate3(firstLine3(value), 100) : "";
25344
25343
  }
25345
25344
  var DENY_WITH_NOTE = "Deny with note\u2026";
25346
25345
  var BACKGROUND_DENY_NOTE = "Permission denied for this tool: this agent is running in the background, where tools that would require approval are automatically denied. Do not retry variants of this call; finish what you can without it and say in your report what needs approval.";
@@ -36578,8 +36577,8 @@ function renderMcpSettingsOverlay(params) {
36578
36577
  const selected = i === state2.selectedIndex;
36579
36578
  const statusIcon = server.status === "running" ? "\u25CF" : server.status === "error" ? "\u2717" : server.enabled ? "\u25CB" : "\u25CB";
36580
36579
  const label = `${statusIcon} ${server.enabled ? "" : "disabled \xB7 "}${server.name}`;
36581
- const tools = server.status === "running" && server.toolCount > 0 ? `${server.toolCount} tools` : server.status === "error" && server.error ? truncate2(server.error, 20) : "stopped";
36582
- const value = `${truncate2(server.command, 24)} \xB7 ${tools} \xB7 [${server.source}]`;
36580
+ const tools = server.status === "running" && server.toolCount > 0 ? `${server.toolCount} tools` : server.status === "error" && server.error ? truncate3(server.error, 20) : "stopped";
36581
+ const value = `${truncate3(server.command, 24)} \xB7 ${tools} \xB7 [${server.source}]`;
36583
36582
  body.push(hubExactRow(hubRowColumns({
36584
36583
  inner,
36585
36584
  selected,
@@ -36601,7 +36600,7 @@ function renderMcpSettingsOverlay(params) {
36601
36600
  title: hubBoldText(hubFrameTitle("mcp", [], `\u2014 ${state2.viewScope}`)),
36602
36601
  borderFg: (t) => hubTheme.fg("borderMuted", t)
36603
36602
  });
36604
- function truncate2(text, n) {
36603
+ function truncate3(text, n) {
36605
36604
  return text.length > n ? text.slice(0, n - 1) + "\u2026" : text;
36606
36605
  }
36607
36606
  }
@@ -36880,7 +36879,8 @@ var VALID_PLATFORMS = /* @__PURE__ */ new Set([
36880
36879
  ]);
36881
36880
  var BLOCKING_EVENTS = /* @__PURE__ */ new Set([
36882
36881
  "ask_user_prompt",
36883
- "permission_request"
36882
+ "permission_request",
36883
+ "input_needed"
36884
36884
  ]);
36885
36885
  function isBlockingEvent(eventType) {
36886
36886
  return eventType !== void 0 && BLOCKING_EVENTS.has(eventType);
@@ -36949,7 +36949,8 @@ var DEFAULT_CONFIG5 = {
36949
36949
  memory_consolidated: { enabled: false, platforms: [] },
36950
36950
  session_shutdown: { enabled: false, platforms: [] },
36951
36951
  ask_user_prompt: { enabled: false, platforms: [] },
36952
- permission_request: { enabled: false, platforms: [] }
36952
+ permission_request: { enabled: false, platforms: [] },
36953
+ input_needed: { enabled: false, platforms: [] }
36953
36954
  },
36954
36955
  native: {
36955
36956
  enabled: true,
@@ -36993,7 +36994,8 @@ var EVENT_PHRASES = {
36993
36994
  memory_consolidated: "a memory consolidation completes.",
36994
36995
  session_shutdown: "the session shuts down.",
36995
36996
  ask_user_prompt: "the agent asks you a question.",
36996
- permission_request: "a permission prompt opens."
36997
+ permission_request: "a permission prompt opens.",
36998
+ input_needed: "the agent is waiting on any prompt (covers tools that send no event of their own)."
36997
36999
  };
36998
37000
  var EVENT_SECTIONS = Object.keys(DEFAULT_CONFIG5.events).map((event) => ({
36999
37001
  title: event,
@@ -37559,6 +37561,22 @@ function buildPermissionPromptMessage(payload) {
37559
37561
  return parts.join(" ");
37560
37562
  }
37561
37563
 
37564
+ // packages/notify/input-needed-message.ts
37565
+ var INPUT_TITLE_MAX_CHARS = 120;
37566
+ function cleanKind(value) {
37567
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : "prompt";
37568
+ }
37569
+ function truncate2(text, max) {
37570
+ const chars = Array.from(text);
37571
+ return chars.length <= max ? text : `${chars.slice(0, max).join("")}\u2026`;
37572
+ }
37573
+ function buildInputNeededMessage(payload) {
37574
+ const p = payload ?? {};
37575
+ const title = typeof p.title === "string" ? p.title.replace(/\s+/g, " ").trim() : "";
37576
+ if (title.length === 0) return `Waiting for your input (${cleanKind(p.kind)})`;
37577
+ return `Waiting for your input: ${truncate2(title, INPUT_TITLE_MAX_CHARS)}`;
37578
+ }
37579
+
37562
37580
  // packages/notify/summarize.ts
37563
37581
  var SYSTEM_PROMPT2 = "Summarize this in one concise sentence for a push notification. Reply with ONLY the summary.";
37564
37582
  var MAX_INPUT_CHARS = 2e3;
@@ -37676,6 +37694,10 @@ function defaultEventPriority(eventKey) {
37676
37694
  var sessionCtx = null;
37677
37695
  var unsubs = [];
37678
37696
  var renotifyTimer;
37697
+ var PROMPT_DEDUP_MS = 2e3;
37698
+ var agentRunning = false;
37699
+ var openPrompts = 0;
37700
+ var lastBlockingAlertAt = 0;
37679
37701
  function disarmRenotify() {
37680
37702
  const timer = renotifyTimer;
37681
37703
  renotifyTimer = void 0;
@@ -37708,8 +37730,16 @@ function armRenotify(pi, title, message, platforms, eventType, config, cwd, disp
37708
37730
  timer.unref?.();
37709
37731
  renotifyTimer = timer;
37710
37732
  }
37733
+ function notifyBlocking(pi, eventKey, title, message, platforms, config, cwd, dispatch) {
37734
+ if (eventKey !== "input_needed") lastBlockingAlertAt = Date.now();
37735
+ dispatch(pi, title, message, platforms, eventKey, config, cwd, "high").catch(() => {
37736
+ });
37737
+ armRenotify(pi, title, message, platforms, eventKey, config, cwd, dispatch);
37738
+ }
37711
37739
  function unregisterAll() {
37712
37740
  disarmRenotify();
37741
+ agentRunning = false;
37742
+ openPrompts = 0;
37713
37743
  for (const unsub of unsubs) {
37714
37744
  try {
37715
37745
  unsub();
@@ -37733,18 +37763,23 @@ var BUILTIN_EVENTS = {
37733
37763
  memory_consolidated: { hook: UNIPI_EVENTS.MEMORY_CONSOLIDATED, label: "Memory Saved" },
37734
37764
  session_shutdown: { hook: "session_shutdown", label: "Session End" },
37735
37765
  ask_user_prompt: { hook: UNIPI_EVENTS.ASK_USER_PROMPT, label: "Question Asked" },
37736
- permission_request: { hook: PERMISSION_UI_PROMPT_EVENT, label: "Permission Request" }
37766
+ permission_request: { hook: PERMISSION_UI_PROMPT_EVENT, label: "Permission Request" },
37767
+ input_needed: { hook: "ui_prompt_start", label: "Input Needed" }
37737
37768
  };
37738
37769
  var LIFECYCLE_EVENTS = /* @__PURE__ */ new Set(["agent_end", "agent_settled", "session_shutdown"]);
37739
37770
  function registerEventListeners(pi, config, cwd, dispatch = dispatchNotification) {
37740
37771
  unregisterAll();
37741
37772
  for (const [eventKey, def] of Object.entries(BUILTIN_EVENTS)) {
37742
- if (isAgentNotificationEvent(eventKey)) continue;
37773
+ if (isAgentNotificationEvent(eventKey) || eventKey === "input_needed") continue;
37743
37774
  const eventConfig = config.events[eventKey];
37744
37775
  if (!eventConfig?.enabled) continue;
37745
37776
  const handler = (payload) => {
37746
37777
  const title = `Pi \u2014 ${def.label}`;
37747
37778
  const message = buildEventMessage(eventKey, payload);
37779
+ if (isBlockingEvent(eventKey)) {
37780
+ notifyBlocking(pi, eventKey, title, message, eventConfig.platforms, config, cwd, dispatch);
37781
+ return;
37782
+ }
37748
37783
  dispatch(
37749
37784
  pi,
37750
37785
  title,
@@ -37756,9 +37791,6 @@ function registerEventListeners(pi, config, cwd, dispatch = dispatchNotification
37756
37791
  defaultEventPriority(eventKey)
37757
37792
  ).catch(() => {
37758
37793
  });
37759
- if (isBlockingEvent(eventKey)) {
37760
- armRenotify(pi, title, message, eventConfig.platforms, eventKey, config, cwd, dispatch);
37761
- }
37762
37794
  };
37763
37795
  if (LIFECYCLE_EVENTS.has(eventKey)) {
37764
37796
  pi.on(def.hook, handler);
@@ -37769,23 +37801,55 @@ function registerEventListeners(pi, config, cwd, dispatch = dispatchNotification
37769
37801
  const askUserConfig = config.events["ask_user_prompt"];
37770
37802
  if (askUserConfig?.enabled) {
37771
37803
  unsubs.push(pi.events.on(ASK_USER_PROMPT_EVENT, (payload) => {
37772
- const title = `Pi \u2014 ${BUILTIN_EVENTS.ask_user_prompt.label}`;
37773
- const message = buildAskUserPromptMessage(payload);
37774
- dispatch(pi, title, message, askUserConfig.platforms, "ask_user_prompt", config, cwd, "high").catch(
37775
- () => {
37776
- }
37804
+ notifyBlocking(
37805
+ pi,
37806
+ "ask_user_prompt",
37807
+ `Pi \u2014 ${BUILTIN_EVENTS.ask_user_prompt.label}`,
37808
+ buildAskUserPromptMessage(payload),
37809
+ askUserConfig.platforms,
37810
+ config,
37811
+ cwd,
37812
+ dispatch
37777
37813
  );
37778
- armRenotify(pi, title, message, askUserConfig.platforms, "ask_user_prompt", config, cwd, dispatch);
37779
37814
  }));
37780
37815
  }
37781
37816
  unsubs.push(pi.events.on("herdr:blocked", (payload) => {
37782
37817
  if (payload?.active === false) disarmRenotify();
37783
37818
  }));
37784
37819
  pi.on("agent_start", () => {
37820
+ agentRunning = true;
37785
37821
  disarmRenotify();
37786
37822
  });
37787
37823
  registerAgentNotification(pi, "agent_end", config, cwd, dispatch);
37788
37824
  registerAgentNotification(pi, "agent_settled", config, cwd, dispatch);
37825
+ pi.on("agent_end", () => {
37826
+ agentRunning = false;
37827
+ });
37828
+ registerInputNeeded(pi, config, cwd, dispatch);
37829
+ }
37830
+ function registerInputNeeded(pi, config, cwd, dispatch) {
37831
+ if (process.env.UNIPI_SUBAGENT_CHILD === "1") return;
37832
+ pi.on("ui_prompt_start", (payload) => {
37833
+ openPrompts += 1;
37834
+ const eventConfig = config.events["input_needed"];
37835
+ if (!eventConfig?.enabled) return;
37836
+ if (!agentRunning) return;
37837
+ if (Date.now() - lastBlockingAlertAt < PROMPT_DEDUP_MS) return;
37838
+ notifyBlocking(
37839
+ pi,
37840
+ "input_needed",
37841
+ `Pi \u2014 ${BUILTIN_EVENTS.input_needed.label}`,
37842
+ buildInputNeededMessage(payload),
37843
+ eventConfig.platforms,
37844
+ config,
37845
+ cwd,
37846
+ dispatch
37847
+ );
37848
+ });
37849
+ pi.on("ui_prompt_end", () => {
37850
+ openPrompts = Math.max(0, openPrompts - 1);
37851
+ if (openPrompts === 0) disarmRenotify();
37852
+ });
37789
37853
  }
37790
37854
  function getEnabledPlatforms(config, ntfyEnabled) {
37791
37855
  const enabled = [];
@@ -42174,7 +42238,7 @@ var COMMAND_DESCRIPTIONS = {
42174
42238
  "unipi:cleanup": "Remove stale UniPi temp files and leftovers (preview first)",
42175
42239
  "unipi:doctor": "Check UniPi's runtime: config, model cache, Decision Model, skills",
42176
42240
  "unipi:answer": "Answer the last reply \u2014 fixed input under the scrollable reply, per question, or web form",
42177
- "unipi:summarize": "Summarize this session \u2014 answer first, then findings, open items, questions; add text to set a focus",
42241
+ "unipi:summarize": "Summarize your last reply again (answer first, then findings, open items, questions). Add text to ask about it",
42178
42242
  "unipi:skills": "Manage skills \u2014 on/off per project, listed or not, the skill vault",
42179
42243
  "unipi:kanboard": "Kanboard \u2014 capture tasks, run them, open the board",
42180
42244
  "unipi:kanboard-add": "Kanboard \u2014 capture a task into Backlog",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27"
37
+ "@pi-unipi/core": "3.0.0-alpha.29"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -35,7 +35,7 @@ pi install npm:@pi-unipi/utility
35
35
  | `/unipi:cleanup` | Lists stale UniPi files, then asks before it removes them. `--dry-run` only lists. `--yes` does not ask. |
36
36
  | `/unipi:doctor` | Checks folders, config files, Node, the model cache, the Decision Model and skill exposure. |
37
37
  | `/unipi:answer [reply\|questions\|web]` | Opens a screen to answer the last agent reply. |
38
- | `/unipi:summarize [focus]` | Asks the agent for a summary of the session. Add text to set a focus. |
38
+ | `/unipi:summarize [focus]` | Re-summarizes the last reply (the `summarize` skill). Any text is passed to the skill as your question. |
39
39
 
40
40
  The skill manager is in [Skill Registry](../skill-registry/README.md) (`/unipi:skills`).
41
41
 
@@ -51,7 +51,8 @@ final reply after work:
51
51
  5. Questions for you. Each question has a type: pick one, pick any or your answer.
52
52
 
53
53
  The agent uses the skill for every final reply, unless you ask for full detail
54
- or raw output. `/unipi:summarize` asks for this summary at any time.
54
+ or raw output. `/unipi:summarize` runs this summary again on the last reply.
55
+ Any text you add goes to the skill as your question.
55
56
 
56
57
  The skill is "must show" by default, so exposure judging does not hide it. To
57
58
  turn this off, open `/unipi:skills` and clear **M** on `summarize`. Clear **E**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/utility",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27"
39
+ "@pi-unipi/core": "3.0.0-alpha.29"
40
40
  },
41
41
  "devDependencies": {},
42
42
  "peerDependencies": {
@@ -11,8 +11,15 @@ description: |
11
11
 
12
12
  Use this skill for the final reply after work (a task, an investigation, a
13
13
  fix), and when the user runs /unipi:summarize. "The work" below means the
14
- session, or the part of it you are summarizing: the user's messages, your
15
- tool calls and their results, and your replies.
14
+ latest piece of work: the user's last request, the tool calls and results
15
+ that answered it, and your last reply. Use the rest of the session only as
16
+ context to explain it.
17
+
18
+ When the user runs /unipi:summarize, summarize your last reply again in
19
+ this shape. Do not call tools. If text follows this skill, it is the
20
+ user's question or focus. Answer it about your last reply. Words like "it"
21
+ or "this" refer to what your last reply covered. Use earlier parts of the
22
+ session only when the user's text asks about them.
16
23
 
17
24
  ## When not to use it
18
25
 
@@ -17,7 +17,7 @@ description: |
17
17
  | `/unipi:continue` / `/unipi:retry` | Take another turn without new text |
18
18
  | `/unipi:cleanup` | Remove stale UniPi temp files (previews and asks first; `--dry-run`, `--yes`) |
19
19
  | `/unipi:doctor` | Check config, model cache, Decision Model key, skill exposure |
20
- | `/unipi:summarize [focus]` | Ask the agent for a session summary (uses the `summarize` skill) |
20
+ | `/unipi:summarize [focus]` | Summarize the last reply again (runs the `summarize` skill); any text is passed to the skill as the user's question |
21
21
 
22
22
  ## Notes for the agent
23
23
 
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * @pi-unipi/utility — /unipi:summarize [focus]
3
3
  *
4
- * Asks the agent for a summary of the session in the shape the bundled
5
- * `summarize` skill sets. The command sends `/skill:summarize`, so pi
6
- * inlines the skill file into the turn, and the agent cannot skip it.
4
+ * Runs the bundled `summarize` skill again on the last reply. The command
5
+ * sends `/skill:summarize` (plus the user's own text, if any), so pi inlines
6
+ * the skill file into the turn, the agent cannot skip it, and the command
7
+ * adds no wording of its own.
7
8
  */
8
9
 
9
10
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
@@ -11,13 +12,10 @@ import { sendHarnessUserMessage, UNIPI_PREFIX, UTILITY_COMMANDS } from "@pi-unip
11
12
 
12
13
  export const SUMMARIZE_SKILL = "summarize";
13
14
 
14
- /** The prompt the command sends. `focus` narrows the summary; empty means the whole session. */
15
+ /** The text the command sends: the skill, plus the user's own text (if any) as-is. */
15
16
  export function summarizePrompt(focus: string): string {
16
17
  const f = focus.trim();
17
- const scope = f
18
- ? `Summarize this session for me, with the focus on: ${f}`
19
- : "Summarize this session for me: what we did, why, and where it stands now.";
20
- return `/skill:${SUMMARIZE_SKILL} ${scope} Use only what happened in this session. Do not call tools.`;
18
+ return f ? `/skill:${SUMMARIZE_SKILL} ${f}` : `/skill:${SUMMARIZE_SKILL}`;
21
19
  }
22
20
 
23
21
  /** True when pi can expand `/skill:summarize` (the skill is loaded and on). */
@@ -45,7 +43,7 @@ export function registerSummarizeCommand(pi: ExtensionAPI): void {
45
43
  sendHarnessUserMessage(
46
44
  pi,
47
45
  summarizePrompt(focus),
48
- { source: "Utility", title: "Summarize", synopsis: focus ? `focus: ${focus}` : "whole session" },
46
+ { source: "Utility", title: "Summarize", synopsis: focus ? `ask: ${focus}` : "last reply" },
49
47
  { expandPromptTemplates: true },
50
48
  );
51
49
  // sendUserMessage does not wait. Print mode (-p) exits when the handler
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/watchdog",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Jev watchdog for long-running tool calls and background tasks — judges stuck work via TypeSafe System One and kills or warns",
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/background-tasks": "3.0.0-alpha.27",
36
- "@pi-unipi/core": "3.0.0-alpha.27"
35
+ "@pi-unipi/background-tasks": "3.0.0-alpha.29",
36
+ "@pi-unipi/core": "3.0.0-alpha.29"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
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": "3.0.0-alpha.27",
34
+ "@pi-unipi/core": "3.0.0-alpha.29",
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": "3.0.0-alpha.27",
3
+ "version": "3.0.0-alpha.29",
4
4
  "description": "Plan mode and permission modes for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@pi-unipi/core": "3.0.0-alpha.27"
30
+ "@pi-unipi/core": "3.0.0-alpha.29"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-ai": "^0.87.1",