@pi-unipi/unipi 3.0.0-alpha.41 → 3.0.0-alpha.43
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 +20 -0
- package/package.json +26 -24
- package/packages/app-bridge/package.json +2 -2
- package/packages/app-bridge/src/bridge.ts +75 -6
- package/packages/app-bridge/src/snapshot.ts +26 -0
- package/packages/app-bridge/src/wire.ts +14 -2
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +2 -0
- 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/command-echo.ts +47 -1
- package/packages/core/constants.ts +1 -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/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/package.json +2 -2
- 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 +1 -1
- package/packages/unipi/bundled.js +1268 -588
- 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/skills/cron/SKILL.md +131 -0
- package/packages/utility/src/browser/state.ts +218 -0
- package/packages/utility/src/cron/index.ts +323 -0
- package/packages/utility/src/cron/settings.ts +185 -0
- package/packages/utility/src/index.ts +41 -0
- package/packages/watchdog/package.json +3 -3
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ 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.43] — 2026-10-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Cron: scheduled, unattended pi runs on your PC, run by the UniPi host (needs UniPi host 0.5.1). `skill:cron` sets jobs up from a plain request ("every weekday at 18:30 summarize my inbox"), with plain-language schedules, per-job notifications (ntfy, Gotify, Telegram) and an optional wake-up message to the session that set it. `/unipi:cron` lists jobs, their runs and results, and runs, stops, enables or pauses them. Cron settings are in `/unipi:settings`. (UNI-288, UNI-314–318)
|
|
13
|
+
- `/skill:craft-skill` and `/skill:web` are listed as normal skills. (UNI-307)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- The UniPi app now gets argument suggestions for every extension's commands (e.g. `/usage `), not only UniPi's own. (UNI-304)
|
|
17
|
+
- The app's model list stayed partial (only openrouter) when a provider such as OmniRoute registered after the app connected; the list is now pushed again when it changes.
|
|
18
|
+
- Removing a steered message from the app no longer stops the run. (UNI-305)
|
|
19
|
+
- `/reload` from the app reloads pi instead of being sent as a message. (UNI-308)
|
|
20
|
+
|
|
21
|
+
## [3.0.0-alpha.42] — 2026-10-16
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `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)
|
|
25
|
+
- 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)
|
|
26
|
+
- 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)
|
|
27
|
+
- 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)
|
|
28
|
+
|
|
9
29
|
## [3.0.0-alpha.41] — 2026-10-13
|
|
10
30
|
|
|
11
31
|
### Changed
|
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.43",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -70,7 +70,9 @@
|
|
|
70
70
|
"packages/notify/skills",
|
|
71
71
|
"packages/kanboard/skills",
|
|
72
72
|
"packages/updater/skills",
|
|
73
|
-
"packages/subagents/skills"
|
|
73
|
+
"packages/subagents/skills",
|
|
74
|
+
"packages/web-api/skills",
|
|
75
|
+
"packages/dream/skills"
|
|
74
76
|
]
|
|
75
77
|
},
|
|
76
78
|
"peerDependencies": {
|
|
@@ -80,28 +82,28 @@
|
|
|
80
82
|
"typebox": "^1.1.38"
|
|
81
83
|
},
|
|
82
84
|
"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.
|
|
85
|
+
"@pi-unipi/app-bridge": "3.0.0-alpha.43",
|
|
86
|
+
"@pi-unipi/ask-user": "3.0.0-alpha.43",
|
|
87
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.43",
|
|
88
|
+
"@pi-unipi/btw": "3.0.0-alpha.43",
|
|
89
|
+
"@pi-unipi/command-enchantment": "3.0.0-alpha.43",
|
|
90
|
+
"@pi-unipi/compactor": "3.0.0-alpha.43",
|
|
91
|
+
"@pi-unipi/core": "3.0.0-alpha.43",
|
|
92
|
+
"@pi-unipi/dream": "3.0.0-alpha.43",
|
|
93
|
+
"@pi-unipi/footer": "3.0.0-alpha.43",
|
|
94
|
+
"@pi-unipi/fusion": "3.0.0-alpha.43",
|
|
95
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.43",
|
|
96
|
+
"@pi-unipi/input-shortcuts": "3.0.0-alpha.43",
|
|
97
|
+
"@pi-unipi/kanboard": "3.0.0-alpha.43",
|
|
98
|
+
"@pi-unipi/mcp": "3.0.0-alpha.43",
|
|
99
|
+
"@pi-unipi/memory": "3.0.0-alpha.43",
|
|
100
|
+
"@pi-unipi/notify": "3.0.0-alpha.43",
|
|
101
|
+
"@pi-unipi/skill-registry": "3.0.0-alpha.43",
|
|
102
|
+
"@pi-unipi/subagents": "3.0.0-alpha.43",
|
|
103
|
+
"@pi-unipi/updater": "3.0.0-alpha.43",
|
|
104
|
+
"@pi-unipi/utility": "3.0.0-alpha.43",
|
|
105
|
+
"@pi-unipi/web-api": "3.0.0-alpha.43",
|
|
106
|
+
"@pi-unipi/workflow": "3.0.0-alpha.43",
|
|
105
107
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
106
108
|
"beautiful-mermaid": "^1.1.3"
|
|
107
109
|
},
|
|
@@ -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.43",
|
|
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.43"
|
|
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,
|
|
@@ -658,6 +658,23 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
658
658
|
}
|
|
659
659
|
};
|
|
660
660
|
|
|
661
|
+
// The model list changes after `hello`: providers that register late
|
|
662
|
+
// (pi-omniroute-bridge refreshes its provider in session_start, after this
|
|
663
|
+
// bridge's hello) or a /model in the TUI that loads more. A phone that
|
|
664
|
+
// connected early kept the first, partial list (only openrouter) until
|
|
665
|
+
// something forced a new hello. Push a `models` line whenever the list
|
|
666
|
+
// differs from the last one sent, and check a few times after start.
|
|
667
|
+
let lastModelsKey = "";
|
|
668
|
+
const modelsKey = (list: ModelInfo[]) => list.map((m) => `${m.provider}/${m.id}`).join("\n");
|
|
669
|
+
const pushModelsIfChanged = () => {
|
|
670
|
+
if (!ctx || clients.size === 0) return;
|
|
671
|
+
const list = models();
|
|
672
|
+
const key = modelsKey(list);
|
|
673
|
+
if (key === lastModelsKey) return;
|
|
674
|
+
lastModelsKey = key;
|
|
675
|
+
send({ t: "models", models: list } as OutMsg);
|
|
676
|
+
};
|
|
677
|
+
|
|
661
678
|
// UNI-222: long-horizon progress (sticky LH_PROGRESS on the core bus).
|
|
662
679
|
const lhProgress = (): LhProgressInfo | undefined => bus.get(UNIPI_EVENTS.LH_PROGRESS) as LhProgressInfo | undefined;
|
|
663
680
|
// UNI-269: the working mode (sticky LH_STATE — the footer's "Graph Mode").
|
|
@@ -675,7 +692,11 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
675
692
|
streaming: streaming ? { id: streaming.id, role: "assistant" as const, content: phoneSafe(streaming.content) as unknown[] } : undefined,
|
|
676
693
|
tools: [...tools.values()].map((t) => ({ ...t, text: t.text ? clipText(t.text, 8 * 1024) : t.text })),
|
|
677
694
|
commands: commands().map((x) => ({ ...x, description: x.description ? clipText(x.description, 160) : x.description })),
|
|
678
|
-
models:
|
|
695
|
+
models: (() => {
|
|
696
|
+
const list = models();
|
|
697
|
+
lastModelsKey = modelsKey(list);
|
|
698
|
+
return list;
|
|
699
|
+
})(),
|
|
679
700
|
dialogs: hub.list(),
|
|
680
701
|
queue: queueView(),
|
|
681
702
|
fusion: fusionPreset(),
|
|
@@ -1121,12 +1142,32 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1121
1142
|
if (!c) return fail("pi is gone.");
|
|
1122
1143
|
const i = tuiQueue.findIndex((q) => q.id === id);
|
|
1123
1144
|
if (i < 0) return fail("That queued message is gone.");
|
|
1124
|
-
|
|
1145
|
+
// UNI-305: pi has no API to drop ONE item from its own live steer/followUp
|
|
1146
|
+
// queue — the only way to edit it is to abort the whole run and re-send
|
|
1147
|
+
// what's left (see the class comment above `tuiQueue`). That's fine
|
|
1148
|
+
// between tool calls (there is nothing running to lose), but aborting
|
|
1149
|
+
// WHILE a tool is executing kills it mid-flight — real, unsaved work —
|
|
1150
|
+
// and if the removed item was the queue's only entry nothing is left to
|
|
1151
|
+
// re-send: the run just dies instead of continuing. So this waits a
|
|
1152
|
+
// short beat for the in-flight tool to reach its own end (the next safe
|
|
1153
|
+
// boundary) before touching the queue; a tool still running past that
|
|
1154
|
+
// is left alone and the edit is refused instead of destroying it.
|
|
1155
|
+
if (tools.size > 0) {
|
|
1156
|
+
const deadline = Date.now() + 4_000;
|
|
1157
|
+
while (tools.size > 0 && Date.now() < deadline) await sleep(100);
|
|
1158
|
+
if (tools.size > 0) return fail("That message was already handed to pi — it can't be taken back while a tool is running.");
|
|
1159
|
+
}
|
|
1160
|
+
// The wait above may have let pi drain this very item into a real turn
|
|
1161
|
+
// (the message_start handler below removes it from tuiQueue once pi
|
|
1162
|
+
// actually sends it) — re-check instead of operating on a stale index.
|
|
1163
|
+
const liveIndex = tuiQueue.findIndex((q) => q.id === id);
|
|
1164
|
+
if (liveIndex < 0) return fail("That message was already handed to pi — it can't be taken back now.");
|
|
1165
|
+
const [target] = tuiQueue.splice(liveIndex, 1);
|
|
1125
1166
|
const action = apply(target!) ?? undefined;
|
|
1126
1167
|
// Keep the edited/kept target in its original slot (promote: first).
|
|
1127
1168
|
const rest = tuiQueue.splice(0, tuiQueue.length);
|
|
1128
1169
|
if (action === "promote") rest.unshift(target!);
|
|
1129
|
-
else if (action !== "drop") rest.splice(
|
|
1170
|
+
else if (action !== "drop") rest.splice(liveIndex, 0, target!);
|
|
1130
1171
|
if (!c.isIdle()) {
|
|
1131
1172
|
await abortKeepingEditor(c);
|
|
1132
1173
|
const deadline = Date.now() + 10_000;
|
|
@@ -1507,14 +1548,36 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1507
1548
|
}
|
|
1508
1549
|
return;
|
|
1509
1550
|
}
|
|
1551
|
+
case "reload": {
|
|
1552
|
+
// UNI-308: `/reload` must work from the app, not fall through to a plain
|
|
1553
|
+
// prompt. `ctx.reload()` (ExtensionCommandContext) is the same call the
|
|
1554
|
+
// terminal's built-in `/reload` makes.
|
|
1555
|
+
if (!(await ensureIdle(msg.force))) return write(sock, { t: "error", message: "pi is busy. Stop the current run first.", code: "busy", ref: msg.ref });
|
|
1556
|
+
try {
|
|
1557
|
+
await runCommandOp(async (cctx) => {
|
|
1558
|
+
await cctx.reload();
|
|
1559
|
+
});
|
|
1560
|
+
ack();
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1563
|
+
}
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1510
1566
|
case "tree_go": {
|
|
1511
1567
|
if (!(await ensureIdle(msg.force))) return write(sock, { t: "error", message: "pi is busy. Stop the current run first.", code: "busy", ref: msg.ref });
|
|
1512
1568
|
try {
|
|
1569
|
+
let restore: TreeGoRestore | undefined;
|
|
1513
1570
|
await runCommandOp(async (cctx) => {
|
|
1571
|
+
// Read before navigating: the target is a user message → pi moves
|
|
1572
|
+
// the leaf to just before it, and (like /tree) its text goes back
|
|
1573
|
+
// to the composer (UNI-290).
|
|
1574
|
+
const target = cctx.sessionManager.getEntry?.(msg.id);
|
|
1514
1575
|
const result = await cctx.navigateTree(msg.id, { summarize: msg.summarize });
|
|
1515
1576
|
if (result.cancelled) throw new Error("Cancelled");
|
|
1577
|
+
const leaf = cctx.sessionManager.getLeafId?.() ?? null;
|
|
1578
|
+
restore = treeGoRestore(target, (result as { editorText?: unknown }).editorText, leaf);
|
|
1516
1579
|
});
|
|
1517
|
-
ack();
|
|
1580
|
+
write(sock, { t: "ack", ref: msg.ref, ...(restore ?? {}) });
|
|
1518
1581
|
} catch (error) {
|
|
1519
1582
|
fail(error instanceof Error ? error.message : String(error));
|
|
1520
1583
|
}
|
|
@@ -1815,6 +1878,8 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1815
1878
|
setRemoteDialogs(hub);
|
|
1816
1879
|
listen();
|
|
1817
1880
|
writeRecord();
|
|
1881
|
+
// Late providers (registered after this hello): re-check the model list.
|
|
1882
|
+
for (const ms of [1500, 5000, 15000]) setTimeout(() => safe(pushModelsIfChanged)(), ms).unref?.();
|
|
1818
1883
|
if (event?.reason && event.reason !== "startup") {
|
|
1819
1884
|
streaming = undefined;
|
|
1820
1885
|
tools.clear();
|
|
@@ -1981,6 +2046,7 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1981
2046
|
});
|
|
1982
2047
|
|
|
1983
2048
|
on("agent_start", safe(() => {
|
|
2049
|
+
pushModelsIfChanged();
|
|
1984
2050
|
// A fresh run after a phone Stop: the queue delivers after THIS one.
|
|
1985
2051
|
releaseHold();
|
|
1986
2052
|
running = true;
|
|
@@ -2148,7 +2214,10 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
2148
2214
|
|
|
2149
2215
|
// The preset's `active` changes with the model (Fusion leaves fusion mode
|
|
2150
2216
|
// on a /model switch), so it rides along (UNI-212).
|
|
2151
|
-
on("model_select", safe(() =>
|
|
2217
|
+
on("model_select", safe(() => {
|
|
2218
|
+
stateWithFusion();
|
|
2219
|
+
pushModelsIfChanged();
|
|
2220
|
+
}));
|
|
2152
2221
|
on("thinking_level_select", safe(() => send({ t: "state", ...runState() })));
|
|
2153
2222
|
on("session_compact", safe(() => {
|
|
2154
2223
|
if (!ctx) return;
|
|
@@ -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
|
+
}
|
|
@@ -251,6 +251,8 @@ export type OutMsg =
|
|
|
251
251
|
dream?: DreamStatusInfo;
|
|
252
252
|
}
|
|
253
253
|
| ({ t: "state" } & Partial<RunState>)
|
|
254
|
+
/** The model list changed after `hello` (a late provider, a /model in the TUI). */
|
|
255
|
+
| { t: "models"; models: ModelInfo[] }
|
|
254
256
|
| ({ t: "session"; reason: string } & SessionInfo)
|
|
255
257
|
| { t: "entry"; entry: unknown }
|
|
256
258
|
| { t: "msg_start"; id: string; role: string }
|
|
@@ -275,8 +277,11 @@ export type OutMsg =
|
|
|
275
277
|
| { t: "btw_list"; pages: BtwListPage[]; complete?: true; ref?: string }
|
|
276
278
|
/** `code: "busy"`: new/resume/fork/tree_go while pi is running — the phone may retry with `force:true`. */
|
|
277
279
|
| { t: "error"; message: string; ref?: string; code?: "busy" }
|
|
278
|
-
/** `as: "command"`: the prompt ran an extension command (no chat message follows).
|
|
279
|
-
|
|
280
|
+
/** `as: "command"`: the prompt ran an extension command (no chat message follows).
|
|
281
|
+
* `restore` (UNI-290, `tree_go` only): the target was a user message, so pi
|
|
282
|
+
* moved the leaf to just before it — its text (and images, phone-safe refs)
|
|
283
|
+
* go back into the phone's composer, like /tree puts it in the TUI editor. */
|
|
284
|
+
| { t: "ack"; ref?: string; as?: "command"; restore?: { text: string; images?: Array<{ mime: string; data?: string; mediaRef?: string; bytes?: number }> } }
|
|
280
285
|
/** The session is being replaced (new/resume/fork/reload): reconnect to get the new session's hello. */
|
|
281
286
|
| { t: "reconnect"; reason: string }
|
|
282
287
|
/** A page of older entries (oldest first) ending right before `before`. */
|
|
@@ -410,6 +415,10 @@ export type InMsg =
|
|
|
410
415
|
| { t: "tree"; ref?: string }
|
|
411
416
|
/** Navigate to a different point in the tree. `force`: abort the current run first. */
|
|
412
417
|
| { t: "tree_go"; id: string; summarize?: boolean; force?: boolean; ref?: string }
|
|
418
|
+
/** UNI-308: `/reload` sent from the app — re-reads extensions, skills, prompts, context files
|
|
419
|
+
* via `ctx.reload()` (the same thing the terminal's `/reload` built-in does). `force`: abort
|
|
420
|
+
* the current run first, like the other session ops. */
|
|
421
|
+
| { t: "reload"; force?: boolean; ref?: string }
|
|
413
422
|
/** Fetches the full bytes behind an image `mediaRef` the bridge sent in a
|
|
414
423
|
* `media`-omitted placeholder (see `phoneSafe`): replies with one or more
|
|
415
424
|
* `media_chunk` (≤ 700 KB of base64 per chunk) ending in `done:true`, or a
|
|
@@ -498,6 +507,7 @@ const IN_TYPES = new Set([
|
|
|
498
507
|
"session_fork",
|
|
499
508
|
"tree",
|
|
500
509
|
"tree_go",
|
|
510
|
+
"reload",
|
|
501
511
|
"session_rename",
|
|
502
512
|
"media",
|
|
503
513
|
"file_share",
|
|
@@ -591,6 +601,8 @@ export function parseIn(line: string): InMsg | { bad: string; ref?: string } | u
|
|
|
591
601
|
case "tree_go":
|
|
592
602
|
if (typeof m.id !== "string" || !m.id) return { bad: "tree_go.id must be a string", ref };
|
|
593
603
|
return { t: "tree_go", id: m.id, summarize: m.summarize === true, force: m.force === true, ref };
|
|
604
|
+
case "reload":
|
|
605
|
+
return { t: "reload", force: m.force === true, ref };
|
|
594
606
|
case "session_rename":
|
|
595
607
|
if (typeof m.name !== "string") return { bad: "session_rename.name must be a string", ref };
|
|
596
608
|
return { t: "session_rename", name: m.name.slice(0, 200), ref };
|
|
@@ -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.43",
|
|
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.43"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -77,6 +77,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
77
77
|
"unipi:dream": "utility",
|
|
78
78
|
"unipi:answer": "utility",
|
|
79
79
|
"unipi:summarize": "utility",
|
|
80
|
+
"unipi:cron": "utility",
|
|
80
81
|
"unipi:move": "utility",
|
|
81
82
|
|
|
82
83
|
// skill-registry
|
|
@@ -165,6 +166,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
165
166
|
"unipi:doctor": "Check UniPi's runtime: config, model cache, Decision Model, skills",
|
|
166
167
|
"unipi:answer": "Answer the last reply — fixed input under the scrollable reply, per question, or web form",
|
|
167
168
|
"unipi:summarize": "Summarize your last reply again (answer first, then findings, open items, questions). Add text to ask about it",
|
|
169
|
+
"unipi:cron": "Scheduled pi runs on this PC — jobs, next runs, results; run, stop, enable, pause",
|
|
168
170
|
"unipi:skills": "Manage skills — on/off per project, listed or not, the skill vault",
|
|
169
171
|
"unipi:move": "Re-link sessions, trust, worktrees, boards and memories after moving this project",
|
|
170
172
|
|
|
@@ -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.43",
|
|
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.43",
|
|
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.43",
|
|
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.43"
|
|
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.43",
|
|
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.43",
|
|
37
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.43",
|
|
38
38
|
"@earendil-works/pi-agent-core": "^0.87.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* failures never block the command itself.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type
|
|
15
|
+
import { ExtensionRunner, type ExtensionAPI, type ExtensionCommandContext, type RegisteredCommand } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
17
|
|
|
18
18
|
export const COMMAND_ECHO_TYPE = "unipi-command-echo";
|
|
@@ -96,6 +96,48 @@ export function commandArgCompleters(): Map<string, ArgCompleter> {
|
|
|
96
96
|
return (g[COMPLETERS_KEY] ??= new Map<string, ArgCompleter>());
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* UNI-304: `commandArgCompleters()` used to only ever hold the commands
|
|
101
|
+
* this module's own `withCommandEcho` wrapper saw (UniPi's commands). Other
|
|
102
|
+
* extensions' commands — pi-omniroute-bridge's `/usage`, a future `/skill:*`
|
|
103
|
+
* completer, anything not registered through `withCommandEcho` — never got
|
|
104
|
+
* an entry, so the app (which can only ask the bridge, never pi's live
|
|
105
|
+
* in-memory command registry) got nothing for them.
|
|
106
|
+
*
|
|
107
|
+
* `ExtensionRunner.resolveRegisteredCommands()` is where pi itself resolves
|
|
108
|
+
* every extension's registered command (with its real `getArgumentCompletions`,
|
|
109
|
+
* see `RegisteredCommand`) into the de-duplicated list the TUI's own
|
|
110
|
+
* autocomplete reads (`interactive-mode.js` calls `getRegisteredCommands()`,
|
|
111
|
+
* which calls this). It runs on every keystroke in a command's argument area,
|
|
112
|
+
* so it is already a safe, repeatedly-called hook. Patching it here — once,
|
|
113
|
+
* idempotently, on the shared class — is the least invasive way to see every
|
|
114
|
+
* extension's completer: no extension author needs to go through
|
|
115
|
+
* `withCommandEcho`, and pi needs no API change. UniPi's `unipi/index.ts`
|
|
116
|
+
* calls `withCommandEcho` (which calls this) before any other module loads,
|
|
117
|
+
* so the patch is in place before any extension — ours or a third party's —
|
|
118
|
+
* registers a single command.
|
|
119
|
+
*/
|
|
120
|
+
let runnerPatched = false;
|
|
121
|
+
export function patchExtensionRunnerCommandCompleters(): void {
|
|
122
|
+
if (runnerPatched) return;
|
|
123
|
+
runnerPatched = true;
|
|
124
|
+
const proto = ExtensionRunner.prototype as unknown as {
|
|
125
|
+
resolveRegisteredCommands: () => Array<{ invocationName: string; name: string; getArgumentCompletions?: ArgCompleter }>;
|
|
126
|
+
};
|
|
127
|
+
const original = proto.resolveRegisteredCommands;
|
|
128
|
+
if (typeof original !== "function") return; // Unknown/future pi build: no-op rather than throw.
|
|
129
|
+
proto.resolveRegisteredCommands = function (this: unknown, ...args: unknown[]) {
|
|
130
|
+
const resolved = original.apply(this, args as []);
|
|
131
|
+
const map = commandArgCompleters();
|
|
132
|
+
for (const command of resolved) {
|
|
133
|
+
if (typeof command.getArgumentCompletions === "function") {
|
|
134
|
+
map.set(command.invocationName, command.getArgumentCompletions.bind(command));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return resolved;
|
|
138
|
+
} as typeof original;
|
|
139
|
+
}
|
|
140
|
+
|
|
99
141
|
/** Never echo `/name` (internal, programmatic commands). Call before or after registering. */
|
|
100
142
|
export function silenceCommandEcho(name: string): void {
|
|
101
143
|
silentSet().add(name.replace(/^\//, ""));
|
|
@@ -114,6 +156,10 @@ export function isSilentEcho(text: unknown): boolean {
|
|
|
114
156
|
}
|
|
115
157
|
|
|
116
158
|
export function withCommandEcho(pi: ExtensionAPI): ExtensionAPI {
|
|
159
|
+
// UNI-304: patch once, before any command is registered, so every
|
|
160
|
+
// extension's own completer (not just ones that happen to go through this
|
|
161
|
+
// wrapper) ends up in commandArgCompleters().
|
|
162
|
+
patchExtensionRunnerCommandCompleters();
|
|
117
163
|
try {
|
|
118
164
|
pi.registerEntryRenderer<{ text?: string }>(
|
|
119
165
|
COMMAND_ECHO_TYPE,
|
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
|
+
}
|