@pi-unipi/unipi 2.18.1 → 2.19.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 +32 -0
- package/package.json +24 -24
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/background-tasks/package.json +2 -2
- package/packages/btw/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +3 -3
- package/packages/fusion/README.md +9 -2
- package/packages/fusion/package.json +3 -3
- package/packages/fusion/src/index.ts +93 -2
- package/packages/fusion/src/sidekick-runtime.ts +62 -19
- package/packages/fusion/src/sidekick-widget.ts +3 -0
- package/packages/fusion/src/tools.ts +69 -38
- package/packages/fusion/src/transcript.ts +15 -2
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +21 -1
- package/packages/notify/activity.ts +21 -0
- package/packages/notify/events.ts +137 -14
- package/packages/notify/index.ts +4 -0
- package/packages/notify/package.json +2 -2
- package/packages/notify/settings.ts +31 -1
- package/packages/notify/skills/configure-notify/SKILL.md +29 -0
- package/packages/notify/tui/settings-overlay.ts +104 -3
- package/packages/notify/types.ts +12 -0
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +2 -2
- package/packages/trajectory/package.json +1 -1
- package/packages/unipi/bundled.js +3547 -1339
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.19.1] — 2026-09-16
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `fusion`: **sidekick work renders as ordinary tool activity — the two-agent chrome is gone.** `sidekick`/`read_subagent` results no longer wrap output in the `▍` rail + `customMessageBg` frame; during a blocking wait the sidekick's tool calls and messages render inline like the lead's own blocks, matching Devin's single-agent presentation. Report headers dropped the handoff id (tool-call count, duration, and token usage remain), and `read_subagent`'s call line shows `· waiting`/`· snapshot` instead of the id.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `fusion`: **handoff internals no longer leak into the terminal.** Model-facing result text still carries ids and protocol instructions the lead needs, but a new `displayText()` sanitizer strips handoff uuids, `agent_id` markers, `<subagent_completion_notification>` tags, and `use read_subagent` instructions from anything rendered as plain content, rewriting `Handoff <uuid> failed` to `The handoff failed`. The `block:false` result renders as a one-line `◆ sidekick · continuing in background` instead of the raw "Handoff <uuid> started… use read_subagent to wait" text.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `fusion`: **detached handoffs stay visible in a live widget above the editor.** A `block:false` handoff previously left the terminal showing only "started in the background" while the sidekick kept working — a real session looked idle for minutes mid-task. A `fusion-sidekick` widget (`ctx.ui.setWidget`, `aboveEditor`) now streams the same transcript until the completion card arrives; it is suppressed while a blocking wait is attached (`shouldShowSidekickWidget` in `sidekick-widget.ts`) and cleared when the report lands.
|
|
22
|
+
|
|
23
|
+
## [2.19.0] — 2026-09-16
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `notify`: **unanswered blocking prompts are re-sent until someone acts** — `ask_user_prompt` and `permission_request` were fire-once, so a missed push left the agent parked with no further signal. New `renotify` config block (default on: `intervalMs: 120000`, `maxRepeats: 3`) re-dispatches the same title and message with a `(still waiting)` suffix at priority `high`; the loop is cancelled by any terminal keypress, `herdr:blocked` with `active: false`, `agent_start`, or session end, and re-arming replaces the pending timer (`armRenotify`/`disarmRenotify` in `events.ts`). New **Re-notify** tab in `/unipi:notify-settings` (Space toggles, `+`/`−` adjust interval and repeats). `maxRepeats: 0` sends the initial notification only.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `notify`: **blocking prompts now dispatch at `high` priority** (`ask_user_prompt`/`permission_request` → ntfy 5, Gotify 8, breaking through DND) while `agent_end`/`agent_settled` dispatch at `low`. `dispatchNotification` already accepted a priority — the event path just never passed one; an explicit `notify_user` priority still wins.
|
|
32
|
+
- `fusion`: **a sidekick handoff now stays open while its background tasks run.** `SidekickRuntime` counts in-flight `bg_run` calls (`openBgTasks`, only when `notifyOnCompletion`/`triggerOnCompletion` are not `false`), and on `agent_settled` with tasks still running it holds the handoff instead of collecting the report at an intermediate checkpoint. The background-task completion message decrements the count; with none left and the agent settled, a `settleGraceMs` timer (new spawn-config option, default 3000, `unref`'d) waits for the follow-up turn before requesting the last assistant text, and `agent_start` clears it.
|
|
33
|
+
- `fusion`: **a sidekick prompt rejected as "already processing" is retried once** as a pi `followUp` (`streamingBehavior: "followUp"`, guarded by `retriedPrompt`) instead of failing the handoff; a second rejection finishes it as an error. `PendingHandoff` is now resolve-only.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `notify`: **no duplicate "agent finished" notification while a background task is still running.** `agent_end`/`agent_settled` notified for the intermediate turn that launched a `triggerOnCompletion` task and again when the wake started its own turn, making the session look finished when it wasn't. The lifecycle handlers now skip dispatch while the background-tasks shared registry reports a running wake-pending task; the registry is read off the `Symbol.for("unipi.background-tasks.shared-registry")` global, so there is no dependency or load-order coupling (synchronous, never blocks the lifecycle hook).
|
|
38
|
+
- `notify`: **`silenceAfterInput` no longer mutes blocking prompts.** Submitting a prompt with a keypress could silence the permission request that arrived seconds later — precisely the notification that must not be missed. `ask_user_prompt`/`permission_request` now bypass the quiet window, threaded through the existing `eventType` argument (`isBlockingEvent` in `activity.ts`); no new config.
|
|
39
|
+
- `fusion`: **a failed sidekick handoff is reported as a structured error result.** `waitForReport` races the handoff rejection into an `{ error }` outcome and `sidekick`/`read_subagent` return `Handoff <id> failed: <message>` with `isError`, instead of the rejection escaping through the race.
|
|
40
|
+
|
|
9
41
|
## [2.18.1] — 2026-09-16
|
|
10
42
|
|
|
11
43
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,29 +84,29 @@
|
|
|
84
84
|
"typebox": "^1.1.38"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@pi-unipi/ask-user": "2.
|
|
88
|
-
"@pi-unipi/background-tasks": "2.
|
|
89
|
-
"@pi-unipi/btw": "2.
|
|
90
|
-
"@pi-unipi/command-enchantment": "2.
|
|
91
|
-
"@pi-unipi/compactor": "2.
|
|
92
|
-
"@pi-unipi/core": "2.
|
|
93
|
-
"@pi-unipi/footer": "2.
|
|
94
|
-
"@pi-unipi/image": "2.
|
|
95
|
-
"@pi-unipi/fusion": "2.
|
|
96
|
-
"@pi-unipi/info-screen": "2.
|
|
97
|
-
"@pi-unipi/input-shortcuts": "2.
|
|
98
|
-
"@pi-unipi/kanboard": "2.
|
|
99
|
-
"@pi-unipi/mcp": "2.
|
|
100
|
-
"@pi-unipi/memory": "2.
|
|
101
|
-
"@pi-unipi/milestone": "2.
|
|
102
|
-
"@pi-unipi/notify": "2.
|
|
103
|
-
"@pi-unipi/ralph": "2.
|
|
104
|
-
"@pi-unipi/subagents": "2.
|
|
105
|
-
"@pi-unipi/trajectory": "2.
|
|
106
|
-
"@pi-unipi/updater": "2.
|
|
107
|
-
"@pi-unipi/utility": "2.
|
|
108
|
-
"@pi-unipi/web-api": "2.
|
|
109
|
-
"@pi-unipi/workflow": "2.
|
|
87
|
+
"@pi-unipi/ask-user": "2.19.1",
|
|
88
|
+
"@pi-unipi/background-tasks": "2.19.1",
|
|
89
|
+
"@pi-unipi/btw": "2.19.1",
|
|
90
|
+
"@pi-unipi/command-enchantment": "2.19.1",
|
|
91
|
+
"@pi-unipi/compactor": "2.19.1",
|
|
92
|
+
"@pi-unipi/core": "2.19.1",
|
|
93
|
+
"@pi-unipi/footer": "2.19.1",
|
|
94
|
+
"@pi-unipi/image": "2.19.1",
|
|
95
|
+
"@pi-unipi/fusion": "2.19.1",
|
|
96
|
+
"@pi-unipi/info-screen": "2.19.1",
|
|
97
|
+
"@pi-unipi/input-shortcuts": "2.19.1",
|
|
98
|
+
"@pi-unipi/kanboard": "2.19.1",
|
|
99
|
+
"@pi-unipi/mcp": "2.19.1",
|
|
100
|
+
"@pi-unipi/memory": "2.19.1",
|
|
101
|
+
"@pi-unipi/milestone": "2.19.1",
|
|
102
|
+
"@pi-unipi/notify": "2.19.1",
|
|
103
|
+
"@pi-unipi/ralph": "2.19.1",
|
|
104
|
+
"@pi-unipi/subagents": "2.19.1",
|
|
105
|
+
"@pi-unipi/trajectory": "2.19.1",
|
|
106
|
+
"@pi-unipi/updater": "2.19.1",
|
|
107
|
+
"@pi-unipi/utility": "2.19.1",
|
|
108
|
+
"@pi-unipi/web-api": "2.19.1",
|
|
109
|
+
"@pi-unipi/workflow": "2.19.1"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@earendil-works/pi-agent-core": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@pi-unipi/core": "2.
|
|
43
|
+
"@pi-unipi/core": "2.19.1"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/background-tasks",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
13
|
-
"@pi-unipi/core": "2.
|
|
13
|
+
"@pi-unipi/core": "2.19.1",
|
|
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": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "extensions/btw.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@earendil-works/pi-tui": "^0.84.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "2.
|
|
40
|
+
"@pi-unipi/core": "2.19.1"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
38
|
-
"@pi-unipi/info-screen": "2.
|
|
37
|
+
"@pi-unipi/core": "2.19.1",
|
|
38
|
+
"@pi-unipi/info-screen": "2.19.1",
|
|
39
39
|
"@earendil-works/pi-agent-core": "^0.84.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
36
|
-
"@pi-unipi/background-tasks": "2.
|
|
35
|
+
"@pi-unipi/core": "2.19.1",
|
|
36
|
+
"@pi-unipi/background-tasks": "2.19.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -92,8 +92,15 @@ persist across handoffs. The sidekick's context compacts independently of the
|
|
|
92
92
|
lead's (it is its own pi session). `sidekick({message, block:true})` waits by
|
|
93
93
|
default; `block:false` returns immediately and delivers a
|
|
94
94
|
`<subagent_completion_notification>`. Calling it while busy steers the same
|
|
95
|
-
handoff.
|
|
96
|
-
|
|
95
|
+
handoff. Background tasks keep that handoff open through their completion
|
|
96
|
+
notification and any follow-up turn, so the report is not released at an
|
|
97
|
+
intermediate checkpoint. If a prompt arrives while the child is processing,
|
|
98
|
+
Fusion retries it once with pi's `followUp` streaming behavior. Sidekick work
|
|
99
|
+
renders as ordinary tool activity — handoff ids, `agent_id`, and
|
|
100
|
+
`read_subagent` protocol text never reach the terminal. Detached
|
|
101
|
+
handoffs stay visible in a live sidekick widget above the editor until their
|
|
102
|
+
completion card arrives.
|
|
103
|
+
`read_subagent({agent_id?, block?, timeout?})` reads or waits for a handoff.
|
|
97
104
|
|
|
98
105
|
The child receives `UNIPI_FUSION_CHILD=1` and `UNIPI_SUBAGENT_CHILD=1`; the
|
|
99
106
|
Fusion extension guard prevents child processes from registering Fusion tools,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/fusion",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
36
|
-
"@pi-unipi/subagents": "2.
|
|
35
|
+
"@pi-unipi/core": "2.19.1",
|
|
36
|
+
"@pi-unipi/subagents": "2.19.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -40,6 +40,8 @@ import { estimateSavings } from "./savings.js";
|
|
|
40
40
|
import { EDIT_NUDGE, bashNudge, leadPolicy, sidekickSystemPrompt, type FusionIdentity } from "./prompts.js";
|
|
41
41
|
import { isTrivialShell, BASH_NUDGE_EVERY } from "./nudge.js";
|
|
42
42
|
import { registerFusionTools } from "./tools.js";
|
|
43
|
+
import { shouldShowSidekickWidget } from "./sidekick-widget.js";
|
|
44
|
+
import { frameSidekick, markdownText, renderSidekickTranscript, sidekickWorkingHeader, type ThemeLike } from "./transcript.js";
|
|
43
45
|
|
|
44
46
|
export const MODEL_COMMAND = `${UNIPI_PREFIX}model`;
|
|
45
47
|
export const PRESET_COMMAND = `${UNIPI_PREFIX}fusion-preset`;
|
|
@@ -118,6 +120,10 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
118
120
|
let leadToolCalls = 0;
|
|
119
121
|
let editNudgedThisTurn = false;
|
|
120
122
|
let bashStreak = 0;
|
|
123
|
+
let attached = false;
|
|
124
|
+
let widgetTimer: NodeJS.Timeout | undefined;
|
|
125
|
+
let widgetTimerKind: "publish" | "clear" | undefined;
|
|
126
|
+
let widgetLastAt = 0;
|
|
121
127
|
|
|
122
128
|
function identity(ctx: ExtensionContext): FusionIdentity {
|
|
123
129
|
const reg = registryOf(ctx);
|
|
@@ -161,8 +167,79 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
function clearSidekickWidget(): void {
|
|
171
|
+
if (widgetTimer !== undefined) {
|
|
172
|
+
clearTimeout(widgetTimer);
|
|
173
|
+
widgetTimer = undefined;
|
|
174
|
+
widgetTimerKind = undefined;
|
|
175
|
+
}
|
|
176
|
+
const ctx = lastCtx;
|
|
177
|
+
if (!ctx?.hasUI || typeof ctx.ui.setWidget !== "function") return;
|
|
178
|
+
const clear = () => {
|
|
179
|
+
widgetTimer = undefined;
|
|
180
|
+
widgetTimerKind = undefined;
|
|
181
|
+
widgetLastAt = Date.now();
|
|
182
|
+
ctx.ui.setWidget("fusion-sidekick", undefined);
|
|
183
|
+
};
|
|
184
|
+
const wait = Math.max(0, 150 - (Date.now() - widgetLastAt));
|
|
185
|
+
if (wait === 0) clear();
|
|
186
|
+
else {
|
|
187
|
+
widgetTimerKind = "clear";
|
|
188
|
+
widgetTimer = setTimeout(clear, wait);
|
|
189
|
+
widgetTimer.unref();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function publishSidekickWidget(): void {
|
|
194
|
+
const ctx = lastCtx;
|
|
195
|
+
if (!ctx?.hasUI || typeof ctx.ui.setWidget !== "function") return;
|
|
196
|
+
const progress = runtime?.progress();
|
|
197
|
+
if (!runtime || !shouldShowSidekickWidget(runtime.isBusy(), attached) || !progress) {
|
|
198
|
+
ctx.ui.setWidget("fusion-sidekick", undefined);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
ctx.ui.setWidget("fusion-sidekick", (_tui, theme) => {
|
|
202
|
+
const themeLike = theme as unknown as ThemeLike & { bg: (color: string, text: string) => string };
|
|
203
|
+
return frameSidekick(themeLike, "working", renderSidekickTranscript(themeLike, {
|
|
204
|
+
events: progress.events,
|
|
205
|
+
droppedEvents: progress.droppedEvents,
|
|
206
|
+
header: sidekickWorkingHeader(themeLike, progress),
|
|
207
|
+
expanded: false,
|
|
208
|
+
isPartial: true,
|
|
209
|
+
renderText: markdownText,
|
|
210
|
+
}));
|
|
211
|
+
}, { placement: "aboveEditor" });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function publishSidekickWidgetLater(): void {
|
|
215
|
+
const ctx = lastCtx;
|
|
216
|
+
if (!ctx?.hasUI || typeof ctx.ui.setWidget !== "function") return;
|
|
217
|
+
if (widgetTimer !== undefined && widgetTimerKind === "publish") return;
|
|
218
|
+
if (widgetTimer !== undefined) {
|
|
219
|
+
clearTimeout(widgetTimer);
|
|
220
|
+
widgetTimer = undefined;
|
|
221
|
+
widgetTimerKind = undefined;
|
|
222
|
+
}
|
|
223
|
+
const wait = Math.max(0, 150 - (Date.now() - widgetLastAt));
|
|
224
|
+
const publish = () => {
|
|
225
|
+
widgetTimer = undefined;
|
|
226
|
+
widgetTimerKind = undefined;
|
|
227
|
+
widgetLastAt = Date.now();
|
|
228
|
+
publishSidekickWidget();
|
|
229
|
+
};
|
|
230
|
+
if (wait === 0) publish();
|
|
231
|
+
else {
|
|
232
|
+
widgetTimerKind = "publish";
|
|
233
|
+
widgetTimer = setTimeout(publish, wait);
|
|
234
|
+
widgetTimer.unref();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
164
238
|
function publishStatusLater(): void {
|
|
165
|
-
if (lastCtx)
|
|
239
|
+
if (lastCtx) {
|
|
240
|
+
publishStatus(lastCtx);
|
|
241
|
+
publishSidekickWidgetLater();
|
|
242
|
+
}
|
|
166
243
|
}
|
|
167
244
|
|
|
168
245
|
function leadSessionId(ctx: ExtensionContext): string {
|
|
@@ -186,6 +263,8 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
186
263
|
}
|
|
187
264
|
|
|
188
265
|
function stopRuntime(): void {
|
|
266
|
+
clearSidekickWidget();
|
|
267
|
+
attached = false;
|
|
189
268
|
runtime?.kill();
|
|
190
269
|
runtime = undefined;
|
|
191
270
|
leadToolCalls = 0;
|
|
@@ -206,8 +285,20 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
206
285
|
|
|
207
286
|
registerFusionTools(pi, {
|
|
208
287
|
getRuntime,
|
|
209
|
-
onReport: (ctx) =>
|
|
288
|
+
onReport: (ctx) => {
|
|
289
|
+
attached = false;
|
|
290
|
+
clearSidekickWidget();
|
|
291
|
+
publishStatus(ctx);
|
|
292
|
+
},
|
|
210
293
|
onHandoffStart: (ctx) => publishStatus(ctx),
|
|
294
|
+
onAttach: () => {
|
|
295
|
+
attached = true;
|
|
296
|
+
clearSidekickWidget();
|
|
297
|
+
},
|
|
298
|
+
onDetach: () => {
|
|
299
|
+
attached = false;
|
|
300
|
+
publishSidekickWidgetLater();
|
|
301
|
+
},
|
|
211
302
|
});
|
|
212
303
|
pi.registerCommand("unipi:fusion-stats", {
|
|
213
304
|
description: "Estimated Fusion savings (sidekick tokens priced at lead rates)",
|
|
@@ -15,6 +15,7 @@ export interface SidekickSpawnConfig {
|
|
|
15
15
|
spawn?: typeof defaultSpawn;
|
|
16
16
|
command?: { command: string; args: string[] };
|
|
17
17
|
onProgress?: () => void;
|
|
18
|
+
settleGraceMs?: number;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface SidekickUsage {
|
|
@@ -54,11 +55,15 @@ export interface HandoffReport {
|
|
|
54
55
|
|
|
55
56
|
interface PendingHandoff {
|
|
56
57
|
id: string;
|
|
58
|
+
message: string;
|
|
57
59
|
startedAt: number;
|
|
58
60
|
usage: SidekickUsage;
|
|
59
61
|
progress: HandoffProgress;
|
|
62
|
+
retriedPrompt: boolean;
|
|
63
|
+
openBgTasks: number;
|
|
64
|
+
settled: boolean;
|
|
65
|
+
settleTimer?: NodeJS.Timeout;
|
|
60
66
|
resolve: (report: HandoffReport) => void;
|
|
61
|
-
reject: (error: Error) => void;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
const emptyUsage = (): SidekickUsage => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
@@ -193,16 +198,35 @@ export class SidekickRuntime {
|
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
|
|
201
|
+
private requestLastAssistantText(): void {
|
|
202
|
+
const current = this.pending;
|
|
203
|
+
if (current === undefined) return;
|
|
204
|
+
this.responseText = (text) => this.finish(this.abortRequested ? "aborted" : this.pendingError === undefined ? "completed" : "error", text, this.pendingError);
|
|
205
|
+
this.responseError = (error) => this.finish("error", undefined, error.message);
|
|
206
|
+
try {
|
|
207
|
+
this.send({ type: "get_last_assistant_text" });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.finish("error", undefined, error instanceof Error ? error.message : String(error));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
private handleMessage(message: Record<string, unknown>): void {
|
|
197
214
|
if (message.type === "response") {
|
|
198
215
|
const command = message.command;
|
|
199
216
|
if (command === "prompt" && message.success === false) {
|
|
200
|
-
const
|
|
217
|
+
const errorText = String(message.error ?? "Sidekick prompt rejected");
|
|
201
218
|
const current = this.pending;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
if (current === undefined) return;
|
|
220
|
+
if (/already processing/i.test(errorText) && !current.retriedPrompt) {
|
|
221
|
+
current.retriedPrompt = true;
|
|
222
|
+
try {
|
|
223
|
+
this.send({ id: current.id, type: "prompt", message: current.message, streamingBehavior: "followUp" });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.finish("error", undefined, error instanceof Error ? error.message : String(error));
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
this.finish("error", undefined, errorText);
|
|
229
|
+
}
|
|
206
230
|
} else if (command === "get_last_assistant_text") {
|
|
207
231
|
const data = message.data as Record<string, unknown> | undefined;
|
|
208
232
|
const text = typeof data?.text === "string" ? data.text : this.pending?.progress.textTail ?? "";
|
|
@@ -224,6 +248,7 @@ export class SidekickRuntime {
|
|
|
224
248
|
this.closeOpenText();
|
|
225
249
|
this.pending.progress.toolCalls += 1;
|
|
226
250
|
const args = message.args !== undefined && typeof message.args === "object" && message.args !== null ? message.args as Record<string, unknown> : undefined;
|
|
251
|
+
if (message.toolName === "bg_run" && args?.notifyOnCompletion !== false && args?.triggerOnCompletion !== false) this.pending.openBgTasks += 1;
|
|
227
252
|
const argsText = args === undefined ? "" : JSON.stringify(args).replace(/\s+/gu, " ");
|
|
228
253
|
const summary = `${String(message.toolName ?? "tool")}(${argsText})`.slice(0, 40);
|
|
229
254
|
this.pending.progress.recentTools = [...this.pending.progress.recentTools, summary].slice(-6);
|
|
@@ -237,6 +262,7 @@ export class SidekickRuntime {
|
|
|
237
262
|
event.endedAt = Date.now();
|
|
238
263
|
event.isError = message.isError === true;
|
|
239
264
|
event.output = this.toolOutput(message.result);
|
|
265
|
+
if (event.name === "bg_run" && event.isError) this.pending.openBgTasks = Math.max(0, this.pending.openBgTasks - 1);
|
|
240
266
|
}
|
|
241
267
|
this.notifyProgress();
|
|
242
268
|
} else if (message.type === "message_update") {
|
|
@@ -251,7 +277,19 @@ export class SidekickRuntime {
|
|
|
251
277
|
}
|
|
252
278
|
} else if (message.type === "message_end") {
|
|
253
279
|
const msg = message.message as Record<string, unknown> | undefined;
|
|
254
|
-
if (msg?.role === "
|
|
280
|
+
if (msg?.role === "custom" && msg.customType === "background-task-notification") {
|
|
281
|
+
this.pending.openBgTasks = Math.max(0, this.pending.openBgTasks - 1);
|
|
282
|
+
if (this.pending.openBgTasks === 0 && this.pending.settled) {
|
|
283
|
+
clearTimeout(this.pending.settleTimer);
|
|
284
|
+
this.pending.settleTimer = setTimeout(() => {
|
|
285
|
+
const current = this.pending;
|
|
286
|
+
if (current === undefined || !current.settled || current.openBgTasks > 0) return;
|
|
287
|
+
this.requestLastAssistantText();
|
|
288
|
+
}, this.cfg.settleGraceMs ?? 3000);
|
|
289
|
+
this.pending.settleTimer.unref();
|
|
290
|
+
}
|
|
291
|
+
this.notifyProgress();
|
|
292
|
+
} else if (msg?.role === "assistant") {
|
|
255
293
|
this.closeOpenText();
|
|
256
294
|
this.notifyProgress();
|
|
257
295
|
if (msg.stopReason === "error" && typeof msg.errorMessage === "string") this.pendingError = msg.errorMessage;
|
|
@@ -267,16 +305,18 @@ export class SidekickRuntime {
|
|
|
267
305
|
});
|
|
268
306
|
}
|
|
269
307
|
}
|
|
308
|
+
} else if (message.type === "agent_start") {
|
|
309
|
+
clearTimeout(this.pending.settleTimer);
|
|
310
|
+
this.pending.settleTimer = undefined;
|
|
311
|
+
this.pending.settled = false;
|
|
312
|
+
this.notifyProgress();
|
|
270
313
|
} else if (message.type === "agent_settled") {
|
|
271
|
-
this.
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
this.send({ type: "get_last_assistant_text" });
|
|
277
|
-
} catch (error) {
|
|
278
|
-
this.finish("error", undefined, error instanceof Error ? error.message : String(error));
|
|
314
|
+
this.pending.settled = true;
|
|
315
|
+
if (this.pending.openBgTasks > 0) {
|
|
316
|
+
this.notifyProgress();
|
|
317
|
+
return;
|
|
279
318
|
}
|
|
319
|
+
this.requestLastAssistantText();
|
|
280
320
|
}
|
|
281
321
|
}
|
|
282
322
|
|
|
@@ -293,6 +333,8 @@ export class SidekickRuntime {
|
|
|
293
333
|
private finish(status: HandoffReport["status"], text?: string, error?: string): void {
|
|
294
334
|
const current = this.pending;
|
|
295
335
|
if (current === undefined) return;
|
|
336
|
+
clearTimeout(current.settleTimer);
|
|
337
|
+
current.settleTimer = undefined;
|
|
296
338
|
this.pending = undefined;
|
|
297
339
|
this.responseText = undefined;
|
|
298
340
|
this.responseError = undefined;
|
|
@@ -323,19 +365,20 @@ export class SidekickRuntime {
|
|
|
323
365
|
const id = randomUUID();
|
|
324
366
|
const startedAt = Date.now();
|
|
325
367
|
let resolve!: (report: HandoffReport) => void;
|
|
326
|
-
|
|
327
|
-
const done = new Promise<HandoffReport>((res, rej) => {
|
|
368
|
+
const done = new Promise<HandoffReport>((res) => {
|
|
328
369
|
resolve = res;
|
|
329
|
-
reject = rej;
|
|
330
370
|
});
|
|
331
371
|
this.pendingError = undefined;
|
|
332
372
|
this.pending = {
|
|
333
373
|
id,
|
|
374
|
+
message,
|
|
334
375
|
startedAt,
|
|
335
376
|
usage: emptyUsage(),
|
|
336
377
|
progress: { toolCalls: 0, recentTools: [], textTail: "", startedAt, events: [], droppedEvents: 0 },
|
|
378
|
+
retriedPrompt: false,
|
|
379
|
+
openBgTasks: 0,
|
|
380
|
+
settled: false,
|
|
337
381
|
resolve,
|
|
338
|
-
reject,
|
|
339
382
|
};
|
|
340
383
|
this.latestHandoff = { id, done };
|
|
341
384
|
try {
|