@pi-unipi/unipi 3.0.0-alpha.40 → 3.0.0-alpha.42
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 +23 -0
- package/package.json +23 -23
- package/packages/app-bridge/package.json +2 -2
- package/packages/app-bridge/src/bridge.ts +40 -7
- package/packages/app-bridge/src/snapshot.ts +26 -0
- package/packages/app-bridge/src/wire.ts +24 -2
- package/packages/app-bridge/src/work.ts +1 -0
- 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/bus.ts +15 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/skill-conditions.ts +68 -0
- package/packages/core/src/work/index.ts +21 -0
- package/packages/core/src/work/tray.ts +70 -9
- package/packages/dream/package.json +2 -2
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/index.ts +1 -1
- package/packages/fusion/package.json +4 -4
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +7 -7
- package/packages/long-horizon/README.md +11 -3
- package/packages/long-horizon/index.ts +198 -18
- package/packages/long-horizon/package.json +2 -2
- package/packages/long-horizon/src/commands.ts +25 -7
- package/packages/long-horizon/src/engine/continuation.ts +166 -4
- package/packages/long-horizon/src/engine/goal-state.ts +69 -3
- package/packages/long-horizon/src/engine/ralph.ts +74 -4
- package/packages/long-horizon/src/engine/task-gate.ts +109 -0
- package/packages/long-horizon/src/gate.ts +1 -2
- package/packages/long-horizon/src/graph/engine.ts +929 -0
- package/packages/long-horizon/src/graph/runner.ts +49 -0
- package/packages/long-horizon/src/modes.ts +2 -2
- package/packages/long-horizon/src/progress-snapshot.ts +40 -64
- package/packages/long-horizon/src/runtime.ts +13 -0
- package/packages/long-horizon/src/tools/graph.ts +285 -332
- package/packages/long-horizon/src/tools/ralph.ts +1 -1
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/index.ts +16 -4
- package/packages/skill-registry/package.json +2 -2
- package/packages/subagents/package.json +3 -3
- package/packages/subagents/src/index.ts +54 -2
- package/packages/subagents/src/manager-shared.ts +31 -0
- package/packages/unipi/bundled.js +2603 -1311
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/browser/SKILL.md +73 -0
- package/packages/utility/src/browser/state.ts +218 -0
- package/packages/utility/src/index.ts +24 -0
- package/packages/watchdog/package.json +3 -3
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +0 -238
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ 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.42] — 2026-10-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `skill:browser`: the agent uses the PC's browser, the one you see in the UniPi app, with `agent-browser`. While the browser is on, UniPi points agent-browser at it (`AGENT_BROWSER_CDP`), so `agent-browser open <url>` just works, with no start or connect step. The browser is CloakBrowser, a stealthy Chromium shown only in the app; the agent's clicks and typing look human, and you can take over. Needs UniPi host 0.5.0. (UNI-295, UNI-303)
|
|
13
|
+
- The agent sees `skill:browser` and a one-line note only while the PC's browser is on. Turning it off hides both from the next prompt, and `/skill:browser` explains how to turn it on. Any skill can now be shown only when a condition holds (`registerSkillCondition`). (UNI-300)
|
|
14
|
+
- The work tray has a hint again: while it has items, one dim line under the input reads, for example, "Background tasks 1 running · Subagents 2 · ↓ open". (UNI-291)
|
|
15
|
+
- App: "Start from here" on one of your messages puts that message, with its images, back in the app's input, like `/tree` in the terminal. (UNI-290)
|
|
16
|
+
|
|
17
|
+
## [3.0.0-alpha.41] — 2026-10-13
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Graph and swarm now follow Maka's agent graph. The model adds work with dependencies, and pi runs each step as a background subagent. Before, the model had to start every subagent and report back by hand, and weaker models skipped it. The model yields while steps run, and pi wakes it once at each checkpoint (a failure, nothing left to run, or everything settled). A graph closes only when the model finishes it with the results it chose. Swarm is the same graph with Maka's swarm instructions. New tools: `update_agent_graph` (`add_work` / `stop` / `finish`), `view_agent_graph`, `yield_agent_graph`, `agent_output`, `agent_swarm_status`. Removed: `swarm_report`, `swarm_status`, `swarm_yield`, `graph_output`, and the declare-once `update_agent_graph`. (UNI-279, UNI-280, UNI-281)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- The agent graph is saved in the project's state folder. It survives restarts: steps that were running when pi closed resume once in their own subagent session. `/unipi:graph history` and `/unipi:swarm history` list past graphs, and a new graph can reuse results a finished one selected. (UNI-279, UNI-280)
|
|
24
|
+
- The UniPi app's Progress sheet shows finished and past graphs with their chart. It adds each step's details, its full result and its subagent session, plus Stop graph and a picker for earlier runs. The app can also open any subagent's transcript. (UNI-282)
|
|
25
|
+
- Goal: the next continuation is saved before it is sent. If pi crashes, the next start sends it again, up to 2 times, then pauses the goal. A completion claim while the todo list still has open items gets a task reminder first, up to 2 times. (UNI-283)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `/unipi:ralph start` from an empty prompt started the loop but never ran the first iteration. (UNI-283)
|
|
29
|
+
- After a restart, `ralph_done` failed with "no active loop". Prompts and tools also pointed the model at the wrong task-file path. (UNI-283)
|
|
30
|
+
- Stopping a ralph loop with `/unipi:regular` left its goal active, so the next `/unipi:goal` resumed the old checklist. (UNI-283)
|
|
31
|
+
|
|
9
32
|
## [3.0.0-alpha.40] — 2026-10-12
|
|
10
33
|
|
|
11
34
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.42",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,28 +80,28 @@
|
|
|
80
80
|
"typebox": "^1.1.38"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@pi-unipi/app-bridge": "3.0.0-alpha.
|
|
84
|
-
"@pi-unipi/ask-user": "3.0.0-alpha.
|
|
85
|
-
"@pi-unipi/background-tasks": "3.0.0-alpha.
|
|
86
|
-
"@pi-unipi/btw": "3.0.0-alpha.
|
|
87
|
-
"@pi-unipi/command-enchantment": "3.0.0-alpha.
|
|
88
|
-
"@pi-unipi/compactor": "3.0.0-alpha.
|
|
89
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
90
|
-
"@pi-unipi/dream": "3.0.0-alpha.
|
|
91
|
-
"@pi-unipi/footer": "3.0.0-alpha.
|
|
92
|
-
"@pi-unipi/fusion": "3.0.0-alpha.
|
|
93
|
-
"@pi-unipi/info-screen": "3.0.0-alpha.
|
|
94
|
-
"@pi-unipi/input-shortcuts": "3.0.0-alpha.
|
|
95
|
-
"@pi-unipi/kanboard": "3.0.0-alpha.
|
|
96
|
-
"@pi-unipi/mcp": "3.0.0-alpha.
|
|
97
|
-
"@pi-unipi/memory": "3.0.0-alpha.
|
|
98
|
-
"@pi-unipi/notify": "3.0.0-alpha.
|
|
99
|
-
"@pi-unipi/skill-registry": "3.0.0-alpha.
|
|
100
|
-
"@pi-unipi/subagents": "3.0.0-alpha.
|
|
101
|
-
"@pi-unipi/updater": "3.0.0-alpha.
|
|
102
|
-
"@pi-unipi/utility": "3.0.0-alpha.
|
|
103
|
-
"@pi-unipi/web-api": "3.0.0-alpha.
|
|
104
|
-
"@pi-unipi/workflow": "3.0.0-alpha.
|
|
83
|
+
"@pi-unipi/app-bridge": "3.0.0-alpha.42",
|
|
84
|
+
"@pi-unipi/ask-user": "3.0.0-alpha.42",
|
|
85
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.42",
|
|
86
|
+
"@pi-unipi/btw": "3.0.0-alpha.42",
|
|
87
|
+
"@pi-unipi/command-enchantment": "3.0.0-alpha.42",
|
|
88
|
+
"@pi-unipi/compactor": "3.0.0-alpha.42",
|
|
89
|
+
"@pi-unipi/core": "3.0.0-alpha.42",
|
|
90
|
+
"@pi-unipi/dream": "3.0.0-alpha.42",
|
|
91
|
+
"@pi-unipi/footer": "3.0.0-alpha.42",
|
|
92
|
+
"@pi-unipi/fusion": "3.0.0-alpha.42",
|
|
93
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.42",
|
|
94
|
+
"@pi-unipi/input-shortcuts": "3.0.0-alpha.42",
|
|
95
|
+
"@pi-unipi/kanboard": "3.0.0-alpha.42",
|
|
96
|
+
"@pi-unipi/mcp": "3.0.0-alpha.42",
|
|
97
|
+
"@pi-unipi/memory": "3.0.0-alpha.42",
|
|
98
|
+
"@pi-unipi/notify": "3.0.0-alpha.42",
|
|
99
|
+
"@pi-unipi/skill-registry": "3.0.0-alpha.42",
|
|
100
|
+
"@pi-unipi/subagents": "3.0.0-alpha.42",
|
|
101
|
+
"@pi-unipi/updater": "3.0.0-alpha.42",
|
|
102
|
+
"@pi-unipi/utility": "3.0.0-alpha.42",
|
|
103
|
+
"@pi-unipi/web-api": "3.0.0-alpha.42",
|
|
104
|
+
"@pi-unipi/workflow": "3.0.0-alpha.42",
|
|
105
105
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
106
106
|
"beautiful-mermaid": "^1.1.3"
|
|
107
107
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/app-bridge",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.42",
|
|
4
4
|
"description": "Live bridge between a pi TUI session and the UniPi phone app — streaming chat, prompts, steering and dialogs from the phone",
|
|
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.
|
|
34
|
+
"@pi-unipi/core": "3.0.0-alpha.42"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -14,7 +14,7 @@ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join, relative, resolve as resolvePath } from "node:path";
|
|
16
16
|
import { DialogHub, wrapUi } from "./dialogs.js";
|
|
17
|
-
import { ENTRIES_BUDGET, HELLO_ENTRIES_BUDGET, LINE_BUDGET, fitLine, historyPage, jsonBytes, phoneSafe, snapshotEntries, clipText, editedAway, wantedEntry } from "./snapshot.js";
|
|
17
|
+
import { ENTRIES_BUDGET, HELLO_ENTRIES_BUDGET, LINE_BUDGET, fitLine, historyPage, jsonBytes, phoneSafe, snapshotEntries, clipText, editedAway, wantedEntry, treeGoRestore, type TreeGoRestore } from "./snapshot.js";
|
|
18
18
|
import { searchSession } from "./search.js";
|
|
19
19
|
import {
|
|
20
20
|
BRIDGE_PROTOCOL,
|
|
@@ -44,7 +44,7 @@ import { setRemoteDialogs } from "./remote.js";
|
|
|
44
44
|
import { fileSuggestions } from "./files.js";
|
|
45
45
|
import { statPaths } from "./paths.js";
|
|
46
46
|
import { registerPath, resolveMedia } from "./media.js";
|
|
47
|
-
import { listWorkItems, stopWorkItem, backgroundWorkItem, workLogPage } from "./work.js";
|
|
47
|
+
import { listWorkItems, stopWorkItem, backgroundWorkItem, workLogPage, workTranscript } from "./work.js";
|
|
48
48
|
import { bus, callCommandRunner, commandArgCompleters, pendingWorkLabel, silenceCommandEcho, subscribePendingWork, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
49
49
|
import type { BtwListPage } from "./wire.js";
|
|
50
50
|
|
|
@@ -62,6 +62,8 @@ export function recordLhMode(state: LhModeInfo | undefined): { lhMode: string |
|
|
|
62
62
|
|
|
63
63
|
/** UNI-269: long-horizon's command runner behind `set_lh_mode` (packages/long-horizon/src/commands.ts). */
|
|
64
64
|
export const LH_MODE_RUNNER = "unipi:lh-mode";
|
|
65
|
+
/** UNI-282: long-horizon's agent graph ops runner (packages/long-horizon/index.ts). */
|
|
66
|
+
export const LH_GRAPH_RUNNER = "unipi:lh-graph";
|
|
65
67
|
/** UNI-118: dream_status pushes coalesce within this window. */
|
|
66
68
|
export const DREAM_COALESCE_MS = 250;
|
|
67
69
|
|
|
@@ -1508,11 +1510,18 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1508
1510
|
case "tree_go": {
|
|
1509
1511
|
if (!(await ensureIdle(msg.force))) return write(sock, { t: "error", message: "pi is busy. Stop the current run first.", code: "busy", ref: msg.ref });
|
|
1510
1512
|
try {
|
|
1513
|
+
let restore: TreeGoRestore | undefined;
|
|
1511
1514
|
await runCommandOp(async (cctx) => {
|
|
1515
|
+
// Read before navigating: the target is a user message → pi moves
|
|
1516
|
+
// the leaf to just before it, and (like /tree) its text goes back
|
|
1517
|
+
// to the composer (UNI-290).
|
|
1518
|
+
const target = cctx.sessionManager.getEntry?.(msg.id);
|
|
1512
1519
|
const result = await cctx.navigateTree(msg.id, { summarize: msg.summarize });
|
|
1513
1520
|
if (result.cancelled) throw new Error("Cancelled");
|
|
1521
|
+
const leaf = cctx.sessionManager.getLeafId?.() ?? null;
|
|
1522
|
+
restore = treeGoRestore(target, (result as { editorText?: unknown }).editorText, leaf);
|
|
1514
1523
|
});
|
|
1515
|
-
ack();
|
|
1524
|
+
write(sock, { t: "ack", ref: msg.ref, ...(restore ?? {}) });
|
|
1516
1525
|
} catch (error) {
|
|
1517
1526
|
fail(error instanceof Error ? error.message : String(error));
|
|
1518
1527
|
}
|
|
@@ -1581,6 +1590,26 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1581
1590
|
}
|
|
1582
1591
|
return;
|
|
1583
1592
|
}
|
|
1593
|
+
case "lh_graph": {
|
|
1594
|
+
try {
|
|
1595
|
+
const out = await callCommandRunner<{ ok: boolean; reason?: string; output?: { text?: string }; runs?: unknown[] }>(LH_GRAPH_RUNNER, ctx, { op: msg.op, id: msg.id, graphId: msg.graphId });
|
|
1596
|
+
if (!out.found) return fail("The agent graph needs a newer unipi on this PC.");
|
|
1597
|
+
if (!out.result?.ok) return fail(out.result?.reason ?? "The graph op failed.");
|
|
1598
|
+
if (msg.op === "stop") {
|
|
1599
|
+
ack();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (msg.op === "output") {
|
|
1603
|
+
const o = out.result.output ?? {};
|
|
1604
|
+
write(sock, { t: "lh_graph", op: "output", output: { ...o, ...(typeof o.text === "string" ? { text: clipText(o.text, 256 * 1024) } : {}) }, ref: msg.ref });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
write(sock, { t: "lh_graph", op: "history", runs: out.result.runs ?? [], ref: msg.ref });
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1584
1613
|
case "dream_get":
|
|
1585
1614
|
write(sock, { t: "dream_status", status: dreamStatus(), ref: msg.ref });
|
|
1586
1615
|
return;
|
|
@@ -1640,10 +1669,14 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1640
1669
|
return;
|
|
1641
1670
|
}
|
|
1642
1671
|
case "work_transcript": {
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1672
|
+
// UNI-282: a subagent's mini-transcript (work id `agent-<id>` or a
|
|
1673
|
+
// graph step's operator id) from @pi-unipi/subagents' builder.
|
|
1674
|
+
const out = workTranscript(msg.id);
|
|
1675
|
+
if ("error" in out) return fail(out.error);
|
|
1676
|
+
const items = out.items.slice(-400).map((item) =>
|
|
1677
|
+
item.kind === "tool" ? { ...item, output: clipText(item.output, 8 * 1024) } : { ...item, text: clipText(item.text, 32 * 1024) },
|
|
1678
|
+
);
|
|
1679
|
+
write(sock, { t: "work_transcript", id: msg.id, items, ...(out.report !== undefined ? { report: clipText(out.report, 256 * 1024) } : {}), ref: msg.ref });
|
|
1647
1680
|
return;
|
|
1648
1681
|
}
|
|
1649
1682
|
case "work_stop": {
|
|
@@ -273,3 +273,29 @@ export function historyPage(branch: readonly unknown[], before: string, budget =
|
|
|
273
273
|
}
|
|
274
274
|
return { entries: out.reverse(), more };
|
|
275
275
|
}
|
|
276
|
+
|
|
277
|
+
/** UNI-290: what goes back into the app's composer after `tree_go` to a user
|
|
278
|
+
* message — its text (pi's own `editorText` when the runtime passes it on,
|
|
279
|
+
* else read off the entry, joined the same way pi does) and its images as
|
|
280
|
+
* the usual phone-safe refs (inline thumbnail or a `mediaRef`). */
|
|
281
|
+
export type TreeGoRestore = { restore: { text: string; images?: Array<{ mime: string; data?: string; mediaRef?: string; bytes?: number }> } };
|
|
282
|
+
|
|
283
|
+
/** The ack extras for a `tree_go` that landed: only when `target` is a user
|
|
284
|
+
* message and pi really moved the leaf off it (to its parent) — a no-op
|
|
285
|
+
* navigation (already there) restores nothing. */
|
|
286
|
+
export function treeGoRestore(target: unknown, editorText: unknown, leafAfter: string | null): TreeGoRestore | undefined {
|
|
287
|
+
const e = target as { id?: string; type?: string; message?: { role?: string; content?: unknown } } | null | undefined;
|
|
288
|
+
if (!e || e.type !== "message" || e.message?.role !== "user") return undefined;
|
|
289
|
+
if (leafAfter !== null && leafAfter === e.id) return undefined;
|
|
290
|
+
const content = e.message.content;
|
|
291
|
+
const parts = Array.isArray(content) ? (content as Array<{ type?: string; text?: unknown }>) : [];
|
|
292
|
+
const fromEntry = typeof content === "string" ? content : parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text as string).join("");
|
|
293
|
+
const text = typeof editorText === "string" ? editorText : fromEntry;
|
|
294
|
+
const images = parts
|
|
295
|
+
.filter((p) => p?.type === "image")
|
|
296
|
+
.map((p) => phoneSafe(p) as { mime?: string; data?: string; mediaRef?: string; bytes?: number } | null)
|
|
297
|
+
.filter((p): p is { mime?: string; data?: string; mediaRef?: string; bytes?: number } => !!p && (typeof p.data === "string" || typeof p.mediaRef === "string"))
|
|
298
|
+
.map((p) => ({ mime: p.mime ?? "image/*", ...(p.data ? { data: p.data } : {}), ...(p.mediaRef ? { mediaRef: p.mediaRef } : {}), ...(typeof p.bytes === "number" ? { bytes: p.bytes } : {}) }));
|
|
299
|
+
if (!text && !images.length) return undefined;
|
|
300
|
+
return { restore: { text: clipText(text, TEXT_CLIP), ...(images.length ? { images } : {}) } };
|
|
301
|
+
}
|
|
@@ -275,8 +275,11 @@ export type OutMsg =
|
|
|
275
275
|
| { t: "btw_list"; pages: BtwListPage[]; complete?: true; ref?: string }
|
|
276
276
|
/** `code: "busy"`: new/resume/fork/tree_go while pi is running — the phone may retry with `force:true`. */
|
|
277
277
|
| { t: "error"; message: string; ref?: string; code?: "busy" }
|
|
278
|
-
/** `as: "command"`: the prompt ran an extension command (no chat message follows).
|
|
279
|
-
|
|
278
|
+
/** `as: "command"`: the prompt ran an extension command (no chat message follows).
|
|
279
|
+
* `restore` (UNI-290, `tree_go` only): the target was a user message, so pi
|
|
280
|
+
* moved the leaf to just before it — its text (and images, phone-safe refs)
|
|
281
|
+
* go back into the phone's composer, like /tree puts it in the TUI editor. */
|
|
282
|
+
| { t: "ack"; ref?: string; as?: "command"; restore?: { text: string; images?: Array<{ mime: string; data?: string; mediaRef?: string; bytes?: number }> } }
|
|
280
283
|
/** The session is being replaced (new/resume/fork/reload): reconnect to get the new session's hello. */
|
|
281
284
|
| { t: "reconnect"; reason: string }
|
|
282
285
|
/** A page of older entries (oldest first) ending right before `before`. */
|
|
@@ -325,6 +328,8 @@ export type OutMsg =
|
|
|
325
328
|
| { t: "lh_progress"; progress: LhProgressInfo | undefined; ref?: string }
|
|
326
329
|
/** UNI-269: the working mode changed (pushed to every client, coalesced). */
|
|
327
330
|
| { t: "lh_mode"; mode: LhModeInfo | undefined }
|
|
331
|
+
/** UNI-282: reply to `lh_graph` (stop replies `ack` instead). */
|
|
332
|
+
| { t: "lh_graph"; op: "output" | "history"; output?: unknown; runs?: unknown[]; ref?: string }
|
|
328
333
|
/** UNI-118: Dream status changed (pushed to every client, coalesced), or the
|
|
329
334
|
* reply to `dream_get` (`ref`). `status` undefined = the dream module isn't loaded. */
|
|
330
335
|
| { t: "dream_status"; status: DreamStatusInfo | undefined; ref?: string }
|
|
@@ -464,6 +469,11 @@ export type InMsg =
|
|
|
464
469
|
/** UNI-269: switch the working mode (no prompt); `none` = /unipi:regular.
|
|
465
470
|
* Replies `ack` (then an `lh_mode` push) or `error{message}`. */
|
|
466
471
|
| { t: "set_lh_mode"; mode: LhModeId; ref?: string }
|
|
472
|
+
/** UNI-282: agent graph ops for the Progress sheet. `output` = one step's
|
|
473
|
+
* full committed result (`id` = work id or result id, `graphId?` for a past
|
|
474
|
+
* graph); `stop` = stop the running graph; `history` = past graphs as runs.
|
|
475
|
+
* Replies `lh_graph{op, output? | runs?, ref}` or `error`. */
|
|
476
|
+
| { t: "lh_graph"; op: "output" | "stop" | "history"; id?: string; graphId?: string; ref?: string }
|
|
467
477
|
/** UNI-118: the current Dream status; replies `dream_status{status, ref}`. */
|
|
468
478
|
| { t: "dream_get"; ref?: string }
|
|
469
479
|
/** UNI-118: one run's report + trajectory; replies `dream_detail` (or `error`). */
|
|
@@ -510,6 +520,7 @@ const IN_TYPES = new Set([
|
|
|
510
520
|
"work_background",
|
|
511
521
|
"lh_progress_get",
|
|
512
522
|
"set_lh_mode",
|
|
523
|
+
"lh_graph",
|
|
513
524
|
"dream_get",
|
|
514
525
|
"dream_detail",
|
|
515
526
|
"dream_action",
|
|
@@ -654,6 +665,17 @@ export function parseIn(line: string): InMsg | { bad: string; ref?: string } | u
|
|
|
654
665
|
return { bad: "set_lh_mode.mode must be goal|ralph|swarm|graph|none", ref };
|
|
655
666
|
}
|
|
656
667
|
return { t: "set_lh_mode", mode: m.mode as LhModeId, ref };
|
|
668
|
+
case "lh_graph": {
|
|
669
|
+
if (m.op !== "output" && m.op !== "stop" && m.op !== "history") return { bad: "lh_graph.op must be output|stop|history", ref };
|
|
670
|
+
if (m.op === "output" && (typeof m.id !== "string" || !m.id)) return { bad: "lh_graph output needs id", ref };
|
|
671
|
+
return {
|
|
672
|
+
t: "lh_graph",
|
|
673
|
+
op: m.op,
|
|
674
|
+
...(typeof m.id === "string" && m.id ? { id: m.id } : {}),
|
|
675
|
+
...(typeof m.graphId === "string" && m.graphId ? { graphId: m.graphId } : {}),
|
|
676
|
+
ref,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
657
679
|
case "dream_get":
|
|
658
680
|
return { t: "dream_get", ref };
|
|
659
681
|
case "dream_detail":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.42",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
45
|
+
"@pi-unipi/core": "3.0.0-alpha.42"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/background-tasks",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.42",
|
|
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.
|
|
13
|
+
"@pi-unipi/core": "3.0.0-alpha.42",
|
|
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.
|
|
3
|
+
"version": "3.0.0-alpha.42",
|
|
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.
|
|
40
|
+
"@pi-unipi/core": "3.0.0-alpha.42"
|
|
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.
|
|
3
|
+
"version": "3.0.0-alpha.42",
|
|
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.
|
|
37
|
-
"@pi-unipi/info-screen": "3.0.0-alpha.
|
|
36
|
+
"@pi-unipi/core": "3.0.0-alpha.42",
|
|
37
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.42",
|
|
38
38
|
"@earendil-works/pi-agent-core": "^0.87.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/packages/core/bus.ts
CHANGED
|
@@ -58,6 +58,17 @@ export interface LhProgressItem {
|
|
|
58
58
|
/** The committed result / failure note, clipped. */
|
|
59
59
|
summary?: string;
|
|
60
60
|
attempts?: number;
|
|
61
|
+
/** UNI-282 (graph/swarm): the subagent that runs/ran it (opens its transcript). */
|
|
62
|
+
operatorId?: string;
|
|
63
|
+
/** Committed result id (`r-<id>`) — read the full text with the graph output op. */
|
|
64
|
+
resultId?: string;
|
|
65
|
+
/** Subagent profile (graph/swarm). */
|
|
66
|
+
profile?: string;
|
|
67
|
+
startedAt?: number;
|
|
68
|
+
endedAt?: number;
|
|
69
|
+
toolCalls?: number;
|
|
70
|
+
/** Input + output tokens of the child run (settled). */
|
|
71
|
+
tokens?: number;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
/** One long-horizon run (the current one or the last finished one). */
|
|
@@ -73,6 +84,10 @@ export interface LhProgressRun {
|
|
|
73
84
|
ralph?: { name: string; iteration: number; maxIterations: number; checked: number; total: number };
|
|
74
85
|
goal?: { objective: string; status: string; turn: number; maxTurns: number; percent?: number; summary?: string; estimatedAt?: number };
|
|
75
86
|
endedAt?: number;
|
|
87
|
+
/** UNI-282 (graph/swarm): the durable graph id (history / output ops). */
|
|
88
|
+
graphId?: string;
|
|
89
|
+
/** Graph/swarm: result ids the supervisor's finish selected. */
|
|
90
|
+
selected?: string[];
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
/** One line of the shared progress log, generated from state transitions (never hand-written). */
|
package/packages/core/index.ts
CHANGED
|
@@ -54,3 +54,4 @@ export * from "./src/hints/store.js";
|
|
|
54
54
|
export { CRAB_14_LINES_256, CRAB_14_LINES_TRUECOLOR, CRAB_22_LINES_256, CRAB_22_LINES_TRUECOLOR } from "./src/hints/crab-data.js";
|
|
55
55
|
export * from "./harness-messages.js";
|
|
56
56
|
export * from "./remote-dialogs.js";
|
|
57
|
+
export * from "./src/skill-conditions.js";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/core — skill visibility conditions (UNI-300)
|
|
3
|
+
*
|
|
4
|
+
* A generic, tiny gate any module can use to hide one of its bundled
|
|
5
|
+
* skills from the model when some runtime fact says it can't be used
|
|
6
|
+
* right now (e.g. the PC's browser is off). Skill-registry (and anything
|
|
7
|
+
* else shaping `options.skills`) calls `skillConditionsPass` per skill
|
|
8
|
+
* name; a module that owns the fact calls `registerSkillCondition` once
|
|
9
|
+
* at load time with a cheap, synchronous predicate.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately process-global like the rest of core's small registries
|
|
12
|
+
* (command-runner, settings): modules load once per process, and this
|
|
13
|
+
* needs no session/ctx threading. A condition must never throw or block;
|
|
14
|
+
* callers treat a throwing condition as "false" (hidden) defensively but
|
|
15
|
+
* conditions should fail open themselves when unsure.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type SkillCondition = () => boolean;
|
|
19
|
+
|
|
20
|
+
const conditions = new Map<string, SkillCondition>();
|
|
21
|
+
|
|
22
|
+
/** Registers (or replaces) the visibility condition for a skill name. */
|
|
23
|
+
export function registerSkillCondition(skillName: string, condition: SkillCondition): void {
|
|
24
|
+
conditions.set(skillName, condition);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function unregisterSkillCondition(skillName: string): void {
|
|
28
|
+
conditions.delete(skillName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the skill has no registered condition, or its condition returns true. A throwing
|
|
32
|
+
* condition counts as false (hidden) — safer than crashing the turn to show a broken skill. */
|
|
33
|
+
export function skillConditionPasses(skillName: string): boolean {
|
|
34
|
+
const condition = conditions.get(skillName);
|
|
35
|
+
if (!condition) return true;
|
|
36
|
+
try {
|
|
37
|
+
return condition();
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reason a skill is currently hidden (for a clear `/skill:name` refusal), or undefined when
|
|
44
|
+
* it isn't gated by a condition, or its condition is registered but no reason was given. */
|
|
45
|
+
export type SkillConditionReason = () => string | undefined;
|
|
46
|
+
|
|
47
|
+
const reasons = new Map<string, SkillConditionReason>();
|
|
48
|
+
|
|
49
|
+
/** Optional: a human reason shown when `/skill:name` is blocked because the condition is false. */
|
|
50
|
+
export function registerSkillConditionReason(skillName: string, reason: SkillConditionReason): void {
|
|
51
|
+
reasons.set(skillName, reason);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function skillConditionReason(skillName: string): string | undefined {
|
|
55
|
+
const fn = reasons.get(skillName);
|
|
56
|
+
if (!fn) return undefined;
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Test hook. */
|
|
65
|
+
export function resetSkillConditionsForTests(): void {
|
|
66
|
+
conditions.clear();
|
|
67
|
+
reasons.clear();
|
|
68
|
+
}
|
|
@@ -272,6 +272,27 @@ export async function workLogPage(
|
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/** UNI-282: a subagent's mini-transcript (published by @pi-unipi/subagents). */
|
|
276
|
+
export type WorkTranscriptItem =
|
|
277
|
+
| { kind: "task"; text: string }
|
|
278
|
+
| { kind: "text"; text: string }
|
|
279
|
+
| { kind: "tool"; name: string; arg: string; output: string; isError: boolean; running: boolean; durationMs?: number };
|
|
280
|
+
|
|
281
|
+
const SUBAGENT_TRANSCRIPT_KEY = Symbol.for("unipi.subagents.transcript");
|
|
282
|
+
|
|
283
|
+
/** Transcript items + final report of a subagent (work id `agent-<id>`, or a bare subagent id). */
|
|
284
|
+
export function workTranscript(id: string): { items: WorkTranscriptItem[]; report?: string; status?: string } | { error: string } {
|
|
285
|
+
const [kind, rest] = splitId(id);
|
|
286
|
+
const agentId = kind === "agent" ? rest : kind === "bg" || kind === "sidekick" ? undefined : id;
|
|
287
|
+
if (agentId === undefined) return { error: "Only subagents have a transcript." };
|
|
288
|
+
const read = (globalThis as unknown as Record<symbol, unknown>)[SUBAGENT_TRANSCRIPT_KEY] as
|
|
289
|
+
| ((id: string) => { items: WorkTranscriptItem[]; report?: string; status?: string } | undefined)
|
|
290
|
+
| undefined;
|
|
291
|
+
if (!read) return { error: "Subagents are not available." };
|
|
292
|
+
const out = read(agentId);
|
|
293
|
+
return out ?? { error: "That subagent is gone." };
|
|
294
|
+
}
|
|
295
|
+
|
|
275
296
|
// ── change signal ────────────────────────────────────────────────────────
|
|
276
297
|
//
|
|
277
298
|
// A cheap "something in the work list might have changed" notifier so a TUI
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* ── ▌Background tasks (2 · 1 running)▐ Subagents (3) ───── ←→ tabs · esc close
|
|
7
7
|
* <the active tab's pane: the bg task list/logs, or the subagent list/transcript>
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* While the tray is closed and has items, ONE dim hint line sits under the
|
|
10
|
+
* editor (UNI-291) — `Background tasks 1 running · Subagents 2 · ↓ open` —
|
|
11
|
+
* so ↓ is discoverable; nothing at all when the tray is empty or open (no
|
|
12
|
+
* strip chrome, no per-item spinners).
|
|
10
13
|
*
|
|
11
14
|
* Owners register a tab (`registerWorkTrayTab`) and supply the pane; this
|
|
12
15
|
* module owns the tab strip, ←/→ switching, the ↓ key and the open/close
|
|
@@ -63,11 +66,9 @@ export interface WorkTrayTab {
|
|
|
63
66
|
|
|
64
67
|
export type TrayTheme = Pick<Theme, "fg" | "bold">;
|
|
65
68
|
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* lines messy); the footer's single "Working… · ↓ open" line is the only
|
|
70
|
-
* hint that wake-capable work is pending. */
|
|
69
|
+
/** The below-editor widget: learns the TUI (for the ↓ focus check) and draws
|
|
70
|
+
* the one-line hint while the tray is closed and has items (UNI-291). The
|
|
71
|
+
* old "◆ …" strip + per-agent spinner lines stay gone (5b7f2e1: messy). */
|
|
71
72
|
const STRIP_KEY = "work-tray-strip";
|
|
72
73
|
export const TRAY_HINT = "←→ tabs · esc close";
|
|
73
74
|
|
|
@@ -261,6 +262,47 @@ export class WorkTray implements Component {
|
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
// ── hint line (below the editor while the tray is closed) ─────────────────────
|
|
266
|
+
|
|
267
|
+
export const TRAY_OPEN_HINT = "↓ open";
|
|
268
|
+
|
|
269
|
+
/** One tab's part of the hint: `Label N`, `Label N running` (all running) or
|
|
270
|
+
* `Label N (k running)`; level 1 uses the short label, level 2 also the
|
|
271
|
+
* compact `Short N/k` (k running of N). */
|
|
272
|
+
function hintPart(tab: WorkTrayTab, c: WorkTrayCounts, level: number): string {
|
|
273
|
+
const label = level > 0 ? (tab.shortLabel ?? tab.label) : tab.label;
|
|
274
|
+
const n = String(c.total);
|
|
275
|
+
if (c.running <= 0) return `${label} ${n}`;
|
|
276
|
+
if (level >= 2) return `${label} ${n}/${String(c.running)}`;
|
|
277
|
+
return c.running >= c.total ? `${label} ${n} running` : `${label} ${n} (${String(c.running)} running)`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The plain hint text (no colour) for `width` columns, or undefined when no
|
|
281
|
+
* visible tab has items. Full labels, then short labels, then compact
|
|
282
|
+
* counts; past that the counts are cut so "↓ open" always stays visible. */
|
|
283
|
+
export function trayHintText(list: readonly WorkTrayTab[], width: number): string | undefined {
|
|
284
|
+
const items = list.map((tab) => ({ tab, c: safeCounts(tab) })).filter(({ c }) => c.total > 0);
|
|
285
|
+
if (items.length === 0 || width <= 0) return undefined;
|
|
286
|
+
const suffix = ` · ${TRAY_OPEN_HINT}`;
|
|
287
|
+
let line = "";
|
|
288
|
+
for (const level of [0, 1, 2]) {
|
|
289
|
+
line = items.map(({ tab, c }) => hintPart(tab, c, level)).join(" · ");
|
|
290
|
+
if (visibleWidth(line) + visibleWidth(suffix) <= width) return `${line}${suffix}`;
|
|
291
|
+
}
|
|
292
|
+
const room = width - visibleWidth(suffix);
|
|
293
|
+
if (room < 4) return truncateToWidth(TRAY_OPEN_HINT, width, "");
|
|
294
|
+
return `${truncateToWidth(line, room, "…")}${suffix}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The below-editor hint: [] when the tray is open or empty, else one dim
|
|
298
|
+
* line one column short of the terminal (exactly-full-width lines desync
|
|
299
|
+
* wrapping terminals — footer issue #31), indented like the editor text. */
|
|
300
|
+
export function renderTrayHint(theme: TrayTheme, list: readonly WorkTrayTab[], width: number, open = false): string[] {
|
|
301
|
+
if (open || width <= 2) return [];
|
|
302
|
+
const text = trayHintText(list, width - 2);
|
|
303
|
+
return text === undefined ? [] : [` ${theme.fg("dim", text)}`];
|
|
304
|
+
}
|
|
305
|
+
|
|
264
306
|
// ── registration + lifecycle ────────────────────────────────────────────────
|
|
265
307
|
|
|
266
308
|
/** True while the tray is open. */
|
|
@@ -301,10 +343,29 @@ function ensureWorkTray(pi: ExtensionAPI): void {
|
|
|
301
343
|
unsubInput?.();
|
|
302
344
|
unsubInput = undefined;
|
|
303
345
|
if (!ctx.hasUI) return;
|
|
304
|
-
//
|
|
305
|
-
|
|
346
|
+
// The hint line under the editor (UNI-291), also the TUI ref for the
|
|
347
|
+
// ↓ focus check. Renders nothing with no items, so idle sessions show
|
|
348
|
+
// no line; work changes re-render it (the TUI diffs, so an unchanged
|
|
349
|
+
// line costs nothing and never moves focus).
|
|
350
|
+
ctx.ui.setWidget(STRIP_KEY, (tui, theme) => {
|
|
306
351
|
tuiRef = tui;
|
|
307
|
-
|
|
352
|
+
let unsub: () => void = () => {};
|
|
353
|
+
try {
|
|
354
|
+
unsub = subscribeWorkChanges(() => tui.requestRender());
|
|
355
|
+
} catch {
|
|
356
|
+
/* the line is still right on the next render */
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
render: (width: number) => {
|
|
360
|
+
try {
|
|
361
|
+
return renderTrayHint(theme, sortedTabs(), width, openTray !== undefined);
|
|
362
|
+
} catch {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
invalidate() {},
|
|
367
|
+
dispose: () => unsub(),
|
|
368
|
+
};
|
|
308
369
|
}, { placement: "belowEditor" });
|
|
309
370
|
unsubInput = ctx.ui.onTerminalInput(onTerminalInput);
|
|
310
371
|
} catch {
|