@pi-unipi/unipi 2.19.0 → 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 +14 -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 +5 -1
- package/packages/fusion/package.json +3 -3
- package/packages/fusion/src/index.ts +93 -2
- package/packages/fusion/src/sidekick-widget.ts +3 -0
- package/packages/fusion/src/tools.ts +58 -35
- 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/package.json +2 -2
- 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 +2951 -7008
- 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,20 @@ 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
|
+
|
|
9
23
|
## [2.19.0] — 2026-09-16
|
|
10
24
|
|
|
11
25
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.19.
|
|
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.19.
|
|
88
|
-
"@pi-unipi/background-tasks": "2.19.
|
|
89
|
-
"@pi-unipi/btw": "2.19.
|
|
90
|
-
"@pi-unipi/command-enchantment": "2.19.
|
|
91
|
-
"@pi-unipi/compactor": "2.19.
|
|
92
|
-
"@pi-unipi/core": "2.19.
|
|
93
|
-
"@pi-unipi/footer": "2.19.
|
|
94
|
-
"@pi-unipi/image": "2.19.
|
|
95
|
-
"@pi-unipi/fusion": "2.19.
|
|
96
|
-
"@pi-unipi/info-screen": "2.19.
|
|
97
|
-
"@pi-unipi/input-shortcuts": "2.19.
|
|
98
|
-
"@pi-unipi/kanboard": "2.19.
|
|
99
|
-
"@pi-unipi/mcp": "2.19.
|
|
100
|
-
"@pi-unipi/memory": "2.19.
|
|
101
|
-
"@pi-unipi/milestone": "2.19.
|
|
102
|
-
"@pi-unipi/notify": "2.19.
|
|
103
|
-
"@pi-unipi/ralph": "2.19.
|
|
104
|
-
"@pi-unipi/subagents": "2.19.
|
|
105
|
-
"@pi-unipi/trajectory": "2.19.
|
|
106
|
-
"@pi-unipi/updater": "2.19.
|
|
107
|
-
"@pi-unipi/utility": "2.19.
|
|
108
|
-
"@pi-unipi/web-api": "2.19.
|
|
109
|
-
"@pi-unipi/workflow": "2.19.
|
|
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.19.
|
|
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.19.
|
|
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.19.
|
|
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.19.
|
|
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.19.
|
|
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.19.
|
|
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.19.
|
|
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.19.
|
|
38
|
-
"@pi-unipi/info-screen": "2.19.
|
|
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.19.
|
|
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.19.
|
|
36
|
-
"@pi-unipi/background-tasks": "2.19.
|
|
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",
|
|
@@ -95,7 +95,11 @@ default; `block:false` returns immediately and delivers a
|
|
|
95
95
|
handoff. Background tasks keep that handoff open through their completion
|
|
96
96
|
notification and any follow-up turn, so the report is not released at an
|
|
97
97
|
intermediate checkpoint. If a prompt arrives while the child is processing,
|
|
98
|
-
Fusion retries it once with pi's `followUp` streaming behavior.
|
|
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.
|
|
99
103
|
`read_subagent({agent_id?, block?, timeout?})` reads or waits for a handoff.
|
|
100
104
|
|
|
101
105
|
The child receives `UNIPI_FUSION_CHILD=1` and `UNIPI_SUBAGENT_CHILD=1`; the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/fusion",
|
|
3
|
-
"version": "2.19.
|
|
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.19.
|
|
36
|
-
"@pi-unipi/subagents": "2.19.
|
|
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)",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import type { SidekickRuntime, HandoffProgress, HandoffReport } from "./sidekick-runtime.js";
|
|
5
|
-
import {
|
|
5
|
+
import { duration, markdownText, renderSidekickTranscript, sidekickWorkingHeader } from "./transcript.js";
|
|
6
6
|
|
|
7
7
|
const SidekickParams = Type.Object({
|
|
8
8
|
message: Type.String({ description: "A concrete implementation or verification brief for the sidekick" }),
|
|
@@ -18,10 +18,8 @@ export interface FusionToolDeps {
|
|
|
18
18
|
getRuntime: (ctx: ExtensionContext) => SidekickRuntime | undefined;
|
|
19
19
|
onReport?: (ctx: ExtensionContext, report: HandoffReport) => void;
|
|
20
20
|
onHandoffStart?: (ctx: ExtensionContext) => void;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function duration(ms: number): string {
|
|
24
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
onAttach?: (ctx: ExtensionContext) => void;
|
|
22
|
+
onDetach?: (ctx: ExtensionContext) => void;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
function firstLine(value: string): string {
|
|
@@ -100,16 +98,26 @@ function completionMessage(report: HandoffReport): { customType: string; content
|
|
|
100
98
|
|
|
101
99
|
type ThemeLike = {
|
|
102
100
|
fg: (color: string, text: string) => string;
|
|
103
|
-
bg: (color: string, text: string) => string;
|
|
104
101
|
bold: (text: string) => string;
|
|
105
102
|
};
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function
|
|
112
|
-
return
|
|
104
|
+
// Model-facing result text carries handoff ids and protocol instructions the
|
|
105
|
+
// lead needs; none of it may reach the terminal. Anything rendered as plain
|
|
106
|
+
// content goes through this first.
|
|
107
|
+
const HANDOFF_ID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
108
|
+
function displayText(text: string): string {
|
|
109
|
+
return text
|
|
110
|
+
.replace(/<\/?subagent_completion_notification[^>]*>/g, "")
|
|
111
|
+
.replace(/use `?read_subagent`?[^.\n]*\.?/gi, "")
|
|
112
|
+
.replace(HANDOFF_ID, "")
|
|
113
|
+
.replace(/agent_id[ =]?"?"?/g, "")
|
|
114
|
+
.replace(/read_subagent\([^)]*\)/g, "read_subagent")
|
|
115
|
+
.replace(/\(\s*\)/g, "")
|
|
116
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
117
|
+
.replace(/\bHandoff\s*(?=(is|was|failed|aborted|started)\b)/g, "The handoff ")
|
|
118
|
+
.replace(/ for\s*\./g, ".")
|
|
119
|
+
.replace(/ +$/gm, "")
|
|
120
|
+
.trim();
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
function contentText(content: unknown): string {
|
|
@@ -117,18 +125,29 @@ function contentText(content: unknown): string {
|
|
|
117
125
|
return content.map((part) => typeof part === "object" && part !== null && typeof (part as { text?: unknown }).text === "string" ? (part as { text: string }).text : "").filter(Boolean).join("\n");
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
function backgroundComponent(theme: ThemeLike): Component {
|
|
129
|
+
return new Text(`${theme.fg("accent", theme.bold("◆ sidekick"))} ${theme.fg("dim", "· continuing in background")}`, 0, 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type ToolDetails = Partial<HandoffReport> & { progress?: HandoffProgress; background?: boolean; id?: string };
|
|
133
|
+
|
|
134
|
+
function reportHeader(theme: ThemeLike, label: string, report: HandoffReport | undefined): string {
|
|
135
|
+
const status = report?.status ?? "done";
|
|
136
|
+
return `${theme.fg(status === "completed" ? "success" : "error", "◆")} ${theme.fg("accent", theme.bold(`${label} ${status}`))} ${theme.fg("dim", report ? `· ${String(report.toolCalls)} tool calls · ${duration(report.durationMs)} · in ${String(report.usage.input)} / out ${String(report.usage.output)} tokens` : "")}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
function renderToolTranscript(result: { content?: unknown; details?: unknown; isError?: boolean }, options: { expanded?: boolean }, theme: ThemeLike, label: "sidekick" | "read_subagent"): Component {
|
|
121
|
-
const details = result.details as
|
|
140
|
+
const details = result.details as ToolDetails | undefined;
|
|
141
|
+
if (details?.background === true) return backgroundComponent(theme);
|
|
142
|
+
const hasProgress = details !== undefined && "progress" in details;
|
|
122
143
|
const progress = details?.progress;
|
|
144
|
+
if (hasProgress && progress === undefined) return backgroundComponent(theme);
|
|
123
145
|
const report = progress ? undefined : details as HandoffReport | undefined;
|
|
124
146
|
const events = progress?.events ?? report?.events;
|
|
125
147
|
const partial = progress !== undefined;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
? `${theme.fg("accent", theme.bold(`◆ ${label} working`))} ${theme.fg("dim", `· ${String(progress.toolCalls)} tool calls · ${duration(Date.now() - progress.startedAt)}`)}`
|
|
130
|
-
: `${theme.fg(report?.status === "completed" ? "success" : "error", "◆")} ${theme.fg("accent", theme.bold(`${label} ${report?.status ?? "done"}`))} ${theme.fg("dim", report ? `· ${report.id} · ${String(report.toolCalls)} tool calls · ${duration(report.durationMs)} · in ${String(report.usage.input)} / out ${String(report.usage.output)} tokens` : "")}`;
|
|
131
|
-
return frameSidekick(theme, status, renderSidekickTranscript(transcriptTheme(theme), {
|
|
148
|
+
if (!events) return new Text(displayText(contentText(result.content)), 0, 0);
|
|
149
|
+
const header = partial ? sidekickWorkingHeader(theme, progress, label) : reportHeader(theme, label, report);
|
|
150
|
+
return renderSidekickTranscript(theme, {
|
|
132
151
|
events,
|
|
133
152
|
droppedEvents: progress?.droppedEvents,
|
|
134
153
|
header,
|
|
@@ -136,20 +155,19 @@ function renderToolTranscript(result: { content?: unknown; details?: unknown; is
|
|
|
136
155
|
isPartial: partial,
|
|
137
156
|
report,
|
|
138
157
|
renderText: markdownText,
|
|
139
|
-
})
|
|
158
|
+
});
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
function renderCompletionCard(theme: ThemeLike, report: HandoffReport | undefined): Component {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return frameSidekick(theme, status, renderSidekickTranscript(transcriptTheme(theme), {
|
|
162
|
+
if (!report) return new Text(`${theme.fg("accent", "◆")} ${theme.fg("accent", theme.bold("sidekick done"))}`, 0, 0);
|
|
163
|
+
return renderSidekickTranscript(theme, {
|
|
146
164
|
events: report.events ?? [],
|
|
147
|
-
header:
|
|
165
|
+
header: reportHeader(theme, "sidekick", report),
|
|
148
166
|
expanded: false,
|
|
149
167
|
isPartial: false,
|
|
150
168
|
report,
|
|
151
169
|
renderText: markdownText,
|
|
152
|
-
})
|
|
170
|
+
});
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): void {
|
|
@@ -161,7 +179,7 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
|
|
|
161
179
|
description: "Hand off work to your persistent sidekick subagent (one per session; context and shells persist across handoffs; runs on the same machine). block:true (default) waits and returns the report. block:false returns immediately and the report arrives later as a <subagent_completion_notification>. Calling again while a handoff is running injects the message as an interrupt rather than starting a second sidekick.",
|
|
162
180
|
parameters: SidekickParams,
|
|
163
181
|
renderShell: "self",
|
|
164
|
-
renderCall: (args, theme) =>
|
|
182
|
+
renderCall: (args, theme) => new Text(`${theme.fg("toolTitle", theme.bold("◆ sidekick"))} ${theme.fg("dim", firstLine(String(args.message)).slice(0, 100))}`, 0, 0),
|
|
165
183
|
renderResult: (result, options, theme) => renderToolTranscript(result, options, theme as unknown as ThemeLike, "sidekick"),
|
|
166
184
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
167
185
|
const runtime = deps.getRuntime(ctx);
|
|
@@ -174,17 +192,20 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
|
|
|
174
192
|
deps.onReport?.(ctx, report);
|
|
175
193
|
pi.sendMessage(completionMessage(report) as never, { deliverAs: "followUp", triggerTurn: true } as never);
|
|
176
194
|
}).catch(() => undefined);
|
|
177
|
-
|
|
195
|
+
deps.onDetach?.(ctx);
|
|
196
|
+
return result(`Handoff ${handoff.id} started in the background. You will receive a <subagent_completion_notification agent_id="${handoff.id}"> when it finishes; use read_subagent to wait.`, { background: true, id: handoff.id });
|
|
178
197
|
}
|
|
198
|
+
deps.onAttach?.(ctx);
|
|
179
199
|
const waited = await waitForReport(runtime, handoff.id, handoff.done, signal, ctx, onUpdate ? (update) => onUpdate(update as never) : undefined);
|
|
180
200
|
if (waited.report) {
|
|
181
201
|
deps.onReport?.(ctx, waited.report);
|
|
182
202
|
return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
|
|
183
203
|
}
|
|
204
|
+
deps.onDetach?.(ctx);
|
|
184
205
|
if (waited.error) return result(`Handoff ${handoff.id} failed: ${waited.error}`, undefined, true);
|
|
185
206
|
if (waited.aborted) return result(`${progressText(runtime, handoff.id)}\nHandoff ${handoff.id} aborted.`, undefined, true);
|
|
186
|
-
if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${handoff.id}) was working. The handoff continues in the background. Act on the user's message first, then call read_subagent({agent_id:"${handoff.id}", block:true}) to collect the report or sidekick({message}) to redirect it.\n${waited.interrupted}
|
|
187
|
-
return result(`Handoff ${handoff.id} is still running.\n${progressText(runtime, handoff.id)}
|
|
207
|
+
if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${handoff.id}) was working. The handoff continues in the background. Act on the user's message first, then call read_subagent({agent_id:"${handoff.id}", block:true}) to collect the report or sidekick({message}) to redirect it.\n${waited.interrupted}`, { progress: runtime.progress(handoff.id), id: handoff.id });
|
|
208
|
+
return result(`Handoff ${handoff.id} is still running.\n${progressText(runtime, handoff.id)}`, { progress: runtime.progress(handoff.id), id: handoff.id });
|
|
188
209
|
},
|
|
189
210
|
});
|
|
190
211
|
|
|
@@ -194,7 +215,7 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
|
|
|
194
215
|
description: "Read a sidekick handoff report by agent_id (omit for the latest). block:true waits for completion (default timeout 2700s when omitted); block:false returns the current progress snapshot immediately.",
|
|
195
216
|
parameters: ReadSubagentParams,
|
|
196
217
|
renderShell: "self",
|
|
197
|
-
renderCall: (args, theme) =>
|
|
218
|
+
renderCall: (args, theme) => new Text(`${theme.fg("toolTitle", theme.bold("◆ read_subagent"))} ${theme.fg("dim", args.block === false ? "· snapshot" : "· waiting")}`, 0, 0),
|
|
198
219
|
renderResult: (result, options, theme) => renderToolTranscript(result, options, theme as unknown as ThemeLike, "read_subagent"),
|
|
199
220
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
200
221
|
const runtime = deps.getRuntime(ctx);
|
|
@@ -205,17 +226,19 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
|
|
|
205
226
|
const selected = runtime.reports.get(id);
|
|
206
227
|
if (selected) return result(reportText(selected), selected, selected.status !== "completed");
|
|
207
228
|
if (id !== latest.id) return result(`No sidekick handoff found for ${id}.`, undefined, true);
|
|
208
|
-
if (params.block !== true) return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}
|
|
229
|
+
if (params.block !== true) return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`, { progress: runtime.progress(id), id });
|
|
230
|
+
deps.onAttach?.(ctx);
|
|
209
231
|
const timeoutMs = (params.timeout ?? 2700) * 1000;
|
|
210
232
|
const waited = await waitForReport(runtime, id, latest.done, signal, ctx, onUpdate ? (update) => onUpdate(update as never) : undefined, timeoutMs);
|
|
211
233
|
if (waited.report) {
|
|
212
234
|
deps.onReport?.(ctx, waited.report);
|
|
213
235
|
return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
|
|
214
236
|
}
|
|
237
|
+
deps.onDetach?.(ctx);
|
|
215
238
|
if (waited.error) return result(`Handoff ${id} failed: ${waited.error}`, undefined, true);
|
|
216
239
|
if (waited.aborted) return result(`Handoff ${id} aborted.`, undefined, true);
|
|
217
|
-
if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${id}) was working.\n${waited.interrupted}
|
|
218
|
-
return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}
|
|
240
|
+
if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${id}) was working.\n${waited.interrupted}`, { progress: runtime.progress(id), id });
|
|
241
|
+
return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`, { progress: runtime.progress(id), id });
|
|
219
242
|
},
|
|
220
243
|
});
|
|
221
244
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import { Box, Container, Text, type Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import
|
|
1
|
+
import { Box, Container, Markdown, Text, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { HandoffProgress, SidekickEvent } from "./sidekick-runtime.js";
|
|
3
4
|
|
|
4
5
|
export interface ThemeLike {
|
|
5
6
|
fg: (color: string, text: string) => string;
|
|
6
7
|
bold: (text: string) => string;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
export function duration(ms: number): string {
|
|
11
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function markdownText(markdown: string): Component {
|
|
15
|
+
return new Markdown(markdown, 0, 0, getMarkdownTheme());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sidekickWorkingHeader(theme: ThemeLike, progress: Pick<HandoffProgress, "toolCalls" | "startedAt">, label = "sidekick"): string {
|
|
19
|
+
return `${theme.fg("accent", theme.bold(`◆ ${label} working`))} ${theme.fg("dim", `· ${String(progress.toolCalls)} tool calls · ${duration(Date.now() - progress.startedAt)}`)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export class RailComponent implements Component {
|
|
10
23
|
constructor(private readonly inner: Component, private readonly rail: string) {}
|
|
11
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.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.19.
|
|
37
|
+
"@pi-unipi/core": "2.19.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.19.
|
|
36
|
+
"@pi-unipi/core": "2.19.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/input-shortcuts",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.19.
|
|
36
|
+
"@pi-unipi/core": "2.19.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|