pi-ui-extend 0.1.1 → 0.1.3
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/README.md +14 -3
- package/bin/pix.mjs +37 -49
- package/dist/app/app.js +3 -3
- package/dist/app/cli.js +3 -1
- package/dist/app/clipboard.d.ts +2 -0
- package/dist/app/clipboard.js +54 -1
- package/dist/app/dcp-stats.js +143 -14
- package/dist/app/install.d.ts +10 -0
- package/dist/app/install.js +135 -0
- package/dist/app/mouse-controller.d.ts +6 -6
- package/dist/app/mouse-controller.js +19 -1
- package/dist/app/nerd-font-controller.d.ts +6 -0
- package/dist/app/nerd-font-controller.js +98 -17
- package/dist/app/render-controller.js +5 -4
- package/dist/app/startup-checks.js +10 -7
- package/dist/app/toast-controller.d.ts +5 -2
- package/dist/app/toast-controller.js +7 -4
- package/dist/app/toast-renderer.d.ts +3 -0
- package/dist/app/toast-renderer.js +72 -11
- package/dist/app/types.d.ts +8 -4
- package/dist/ui.d.ts +5 -1
- package/dist/ui.js +2 -2
- package/external/pi-tools-suite/src/compress/commands.ts +157 -3
- package/external/pi-tools-suite/src/compress/index.ts +19 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,8 +16,9 @@ The npm package is currently named `pi-ui-extend` and installs the `pix` CLI.
|
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
|
-
- Node.js
|
|
20
|
-
- A terminal with good Unicode support. JetBrainsMono Nerd Font is recommended for the default icon theme.
|
|
19
|
+
- Node.js `>=22.19.0 <25` (`24.16.0` is pinned for development).
|
|
20
|
+
- A terminal with good Unicode support. JetBrainsMono Nerd Font is recommended for the default icon theme; Pix can install it for the current user on macOS, Linux, and Windows.
|
|
21
|
+
- Linux clipboard support: Pix first tries `wl-copy`, `xclip`, `xsel`, or `termux-clipboard-set`, then falls back to its bundled native clipboard package.
|
|
21
22
|
- Optional for voice input: SoX (`rec`/`sox`), `ffmpeg`, or Linux `arecord`.
|
|
22
23
|
|
|
23
24
|
Development uses `mise` when available. `.node-version` and `.nvmrc` are also provided for other Node version managers.
|
|
@@ -32,6 +33,16 @@ npm install -g pi-ui-extend --ignore-scripts
|
|
|
32
33
|
|
|
33
34
|
The published package contains built JavaScript, the `pix` launcher, renderer extensions, documentation, and the bundled `pi-tools-suite` extension payload. Users do not need to clone the repository or build TypeScript locally.
|
|
34
35
|
|
|
36
|
+
After installation, run the setup check when installing on a new machine:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pix install
|
|
40
|
+
# or report only:
|
|
41
|
+
pix install --check
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`pix install` checks the icon font, `pi` CLI availability, and clipboard helpers. The `pix` launcher also prepends Pix's bundled dependency bin directory to `PATH`, so a package-manager install can use the bundled Pi CLI even when a separate global `pi` command is not present.
|
|
45
|
+
|
|
35
46
|
On startup, Pix ensures the bundled suite is available at:
|
|
36
47
|
|
|
37
48
|
```text
|
|
@@ -185,7 +196,7 @@ Runtime requirements:
|
|
|
185
196
|
|
|
186
197
|
- Optional npm package `vosk`. Pix installs or rebuilds it automatically with scripts enabled on first voice start if the native binding is missing.
|
|
187
198
|
- A local recorder: SoX (`rec`/`sox`) preferred, or `ffmpeg`; Linux also supports `arecord`.
|
|
188
|
-
- JetBrainsMono Nerd Font for default app icons.
|
|
199
|
+
- JetBrainsMono Nerd Font for default app icons. Pix checks this at startup and can install the font for the current user on macOS, Linux, and Windows when it is missing.
|
|
189
200
|
|
|
190
201
|
If your terminal renders missing glyphs, start Pix with `PIX_USE_FALLBACK_ICONS=1` or set `iconTheme` to `fallback` in `~/.config/pi/pix.jsonc`.
|
|
191
202
|
|
package/bin/pix.mjs
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { delimiter, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const minimumNodeVersion = [22, 19, 0];
|
|
8
|
+
const minimumNodeVersionLabel = "22.19.0";
|
|
8
9
|
const launcherPath = fileURLToPath(import.meta.url);
|
|
10
|
+
const packageRoot = dirname(dirname(launcherPath));
|
|
9
11
|
const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
|
|
10
12
|
const updatePath = fileURLToPath(new URL("../dist/app/update.js", import.meta.url));
|
|
13
|
+
const installPath = fileURLToPath(new URL("../dist/app/install.js", import.meta.url));
|
|
11
14
|
const distPath = dirname(mainPath);
|
|
12
15
|
const rawArgs = process.argv.slice(2);
|
|
13
16
|
const childArgs = [];
|
|
14
17
|
let reloadOnBuild = truthyEnv(process.env.PIX_RELOAD_ON_BUILD);
|
|
15
18
|
|
|
16
|
-
if (
|
|
17
|
-
|
|
19
|
+
if (!isCurrentNodeSupported()) {
|
|
20
|
+
console.error(`[pix] Node ${minimumNodeVersionLabel}+ is required; current Node is ${process.versions.node}.`);
|
|
21
|
+
console.error("[pix] Install/use a newer Node, for example `mise install node@22.19.0` or `nvm install 22`.");
|
|
22
|
+
process.exit(1);
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
for (const arg of rawArgs) {
|
|
@@ -38,6 +43,15 @@ if (childArgs[0] === "update") {
|
|
|
38
43
|
process.exit(await runPixUpdateCli(childArgs.slice(1)));
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
if (childArgs[0] === "install" || childArgs[0] === "setup") {
|
|
47
|
+
if (!existsSync(installPath)) {
|
|
48
|
+
console.error("pix install is not built yet. Run `npm run build:pix` or update from a published package.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const { runPixInstallCli } = await import(new URL("../dist/app/install.js", import.meta.url));
|
|
52
|
+
process.exit(await runPixInstallCli(childArgs.slice(1), { env: pixChildEnv() }));
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
if (!existsSync(mainPath)) {
|
|
42
56
|
console.error("pix is not built yet. Run `npm run build:pix` or `npm run watch:pix`.");
|
|
43
57
|
process.exit(1);
|
|
@@ -66,57 +80,21 @@ function truthyEnv(value) {
|
|
|
66
80
|
return !["0", "false", "no", "off"].includes(value.toLowerCase());
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (const candidate of candidates) {
|
|
78
|
-
const result = await runNode24Candidate(candidate.command, candidate.args);
|
|
79
|
-
if (!result.launched) continue;
|
|
80
|
-
if (result.signal) process.exitCode = result.signal === "SIGINT" ? 130 : result.signal === "SIGTERM" ? 143 : 1;
|
|
81
|
-
else process.exitCode = result.code ?? 1;
|
|
82
|
-
process.exit();
|
|
83
|
+
function isCurrentNodeSupported() {
|
|
84
|
+
const parts = process.versions.node.split(".").map((part) => Number.parseInt(part, 10));
|
|
85
|
+
for (let index = 0; index < minimumNodeVersion.length; index += 1) {
|
|
86
|
+
const current = parts[index] ?? 0;
|
|
87
|
+
const minimum = minimumNodeVersion[index];
|
|
88
|
+
if (current > minimum) return true;
|
|
89
|
+
if (current < minimum) return false;
|
|
83
90
|
}
|
|
84
|
-
|
|
85
|
-
console.error("[pix] Node 24 is required. Install/use it with `mise install node@24.16.0` or set PIX_NODE24=/path/to/node24.");
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function node24Candidates(args) {
|
|
90
|
-
const envNode = process.env.PIX_NODE24;
|
|
91
|
-
return [
|
|
92
|
-
...(envNode ? [{ command: envNode, args: [launcherPath, ...args] }] : []),
|
|
93
|
-
{ command: "mise", args: ["exec", "node@24.16.0", "--", "node", launcherPath, ...args] },
|
|
94
|
-
{ command: "node24", args: [launcherPath, ...args] },
|
|
95
|
-
{ command: "node-24", args: [launcherPath, ...args] },
|
|
96
|
-
];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function runNode24Candidate(command, args) {
|
|
100
|
-
return await new Promise((resolve) => {
|
|
101
|
-
const child = spawn(command, args, {
|
|
102
|
-
stdio: "inherit",
|
|
103
|
-
env: { ...process.env, PIX_NODE24_REEXEC: "1" },
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
child.once("error", (error) => {
|
|
107
|
-
if (error && error.code === "ENOENT") resolve({ launched: false });
|
|
108
|
-
else {
|
|
109
|
-
console.error(error?.message ?? String(error));
|
|
110
|
-
resolve({ launched: true, code: 1 });
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
child.once("exit", (code, signal) => resolve({ launched: true, code, signal }));
|
|
114
|
-
});
|
|
91
|
+
return true;
|
|
115
92
|
}
|
|
116
93
|
|
|
117
94
|
function startChild() {
|
|
118
95
|
child = spawn(process.execPath, [mainPath, ...childArgs], {
|
|
119
96
|
stdio: "inherit",
|
|
97
|
+
env: pixChildEnv(),
|
|
120
98
|
});
|
|
121
99
|
|
|
122
100
|
child.on("error", (error) => {
|
|
@@ -138,6 +116,16 @@ function startChild() {
|
|
|
138
116
|
});
|
|
139
117
|
}
|
|
140
118
|
|
|
119
|
+
function pixChildEnv() {
|
|
120
|
+
const env = { ...process.env };
|
|
121
|
+
const bundledBinPath = join(packageRoot, "node_modules", ".bin");
|
|
122
|
+
if (existsSync(bundledBinPath)) {
|
|
123
|
+
env.PATH = [bundledBinPath, env.PATH ?? ""].filter(Boolean).join(delimiter);
|
|
124
|
+
env.PIX_BUNDLED_PI_BIN = bundledBinPath;
|
|
125
|
+
}
|
|
126
|
+
return env;
|
|
127
|
+
}
|
|
128
|
+
|
|
141
129
|
function startDistPolling() {
|
|
142
130
|
const pollInterval = Number(process.env.PIX_RELOAD_POLL_MS ?? 1000);
|
|
143
131
|
distPollTimer = setInterval(() => {
|
package/dist/app/app.js
CHANGED
|
@@ -493,7 +493,7 @@ export class PiUiExtendApp {
|
|
|
493
493
|
void this.tabsController.closeTab(tabId);
|
|
494
494
|
},
|
|
495
495
|
toastEntry: (toastId) => this.toastController.toast.entry(toastId),
|
|
496
|
-
showToast: (message, kind) => this.showToast(message, kind),
|
|
496
|
+
showToast: (message, kind, options) => this.showToast(message, kind, options),
|
|
497
497
|
dismissToast: (toastId) => this.toastController.dismissToast(toastId),
|
|
498
498
|
refreshModelUsageStatus: () => this.refreshModelUsageStatusFromClick(),
|
|
499
499
|
toggleAllThinkingExpanded: () => {
|
|
@@ -842,8 +842,8 @@ export class PiUiExtendApp {
|
|
|
842
842
|
this.showToast("Failed to refresh model usage limits", "error");
|
|
843
843
|
});
|
|
844
844
|
}
|
|
845
|
-
showToast(message, kind = "info") {
|
|
846
|
-
this.toastController.showToast(message, kind);
|
|
845
|
+
showToast(message, kind = "info", options) {
|
|
846
|
+
this.toastController.showToast(message, kind, options);
|
|
847
847
|
}
|
|
848
848
|
clearToastTimers() {
|
|
849
849
|
this.toastController.clearToastTimers();
|
package/dist/app/cli.js
CHANGED
|
@@ -3,12 +3,14 @@ import { parseThemeName } from "../theme.js";
|
|
|
3
3
|
export function usage() {
|
|
4
4
|
return `Usage: pix [--cwd <path>] [--no-session] [--session <path>] [--theme dark|light] [--model <provider/model[:thinking]>]
|
|
5
5
|
pix update [--check] [--force]
|
|
6
|
+
pix install [--check]
|
|
6
7
|
npm run dev -- [--cwd <path>] [--no-session] [--session <path>] [--theme dark|light] [--model <provider/model[:thinking]>]
|
|
7
8
|
|
|
8
9
|
Examples:
|
|
9
10
|
pix --cwd ../pi-mono
|
|
10
11
|
pix --cwd ../pi-mono --theme light --model anthropic/claude-sonnet-4-20250514:medium
|
|
11
|
-
pix update --check
|
|
12
|
+
pix update --check
|
|
13
|
+
pix install --check`;
|
|
12
14
|
}
|
|
13
15
|
export function parseArgs(argv) {
|
|
14
16
|
let cwd = process.cwd();
|
package/dist/app/clipboard.d.ts
CHANGED
package/dist/app/clipboard.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
2
4
|
export function copyTextToClipboard(text) {
|
|
3
5
|
const commands = clipboardCommands();
|
|
4
6
|
for (const [command, args] of commands) {
|
|
@@ -6,7 +8,24 @@ export function copyTextToClipboard(text) {
|
|
|
6
8
|
if (!result.error && result.status === 0)
|
|
7
9
|
return;
|
|
8
10
|
}
|
|
9
|
-
|
|
11
|
+
if (copyWithNativeClipboard(text))
|
|
12
|
+
return;
|
|
13
|
+
throw new Error(`No clipboard command found. ${clipboardInstallHint()}`);
|
|
14
|
+
}
|
|
15
|
+
export function clipboardSupportAvailable(env = process.env) {
|
|
16
|
+
if (clipboardCommands().some(([command]) => commandExists(command, env)))
|
|
17
|
+
return true;
|
|
18
|
+
return resolveNativeClipboardEntrypoint() !== undefined;
|
|
19
|
+
}
|
|
20
|
+
export function clipboardInstallHint() {
|
|
21
|
+
if (process.platform === "linux") {
|
|
22
|
+
return "Install wl-clipboard for Wayland or xclip/xsel for X11 (for example: sudo apt install wl-clipboard xclip xsel).";
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === "darwin")
|
|
25
|
+
return "Install pbcopy or check macOS clipboard permissions.";
|
|
26
|
+
if (process.platform === "win32")
|
|
27
|
+
return "Install clip.exe or check Windows clipboard access.";
|
|
28
|
+
return "Install a platform clipboard command.";
|
|
10
29
|
}
|
|
11
30
|
function clipboardCommands() {
|
|
12
31
|
switch (process.platform) {
|
|
@@ -19,6 +38,40 @@ function clipboardCommands() {
|
|
|
19
38
|
["wl-copy", []],
|
|
20
39
|
["xclip", ["-selection", "clipboard"]],
|
|
21
40
|
["xsel", ["--clipboard", "--input"]],
|
|
41
|
+
["termux-clipboard-set", []],
|
|
22
42
|
];
|
|
23
43
|
}
|
|
24
44
|
}
|
|
45
|
+
function copyWithNativeClipboard(text) {
|
|
46
|
+
const entrypoint = resolveNativeClipboardEntrypoint();
|
|
47
|
+
if (!entrypoint)
|
|
48
|
+
return false;
|
|
49
|
+
const script = `
|
|
50
|
+
import { createRequire } from "node:module";
|
|
51
|
+
import { readFileSync } from "node:fs";
|
|
52
|
+
const require = createRequire(${JSON.stringify(import.meta.url)});
|
|
53
|
+
const clipboard = require(${JSON.stringify(entrypoint)});
|
|
54
|
+
await clipboard.setText(readFileSync(0, "utf8"));
|
|
55
|
+
`;
|
|
56
|
+
const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
|
|
57
|
+
input: text,
|
|
58
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
59
|
+
timeout: 3_000,
|
|
60
|
+
});
|
|
61
|
+
return !result.error && result.status === 0;
|
|
62
|
+
}
|
|
63
|
+
function resolveNativeClipboardEntrypoint() {
|
|
64
|
+
try {
|
|
65
|
+
return require.resolve("@mariozechner/clipboard");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function commandExists(command, env) {
|
|
72
|
+
const names = process.platform === "win32" ? [command, command.replace(/\.exe$/iu, ".cmd"), command.replace(/\.exe$/iu, ".bat")] : [command];
|
|
73
|
+
return names.some((name) => spawnSync(process.platform === "win32" ? "where" : "sh", process.platform === "win32" ? [name] : ["-lc", `command -v ${shellQuote(name)}`], { env, stdio: "ignore" }).status === 0);
|
|
74
|
+
}
|
|
75
|
+
function shellQuote(value) {
|
|
76
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
77
|
+
}
|
package/dist/app/dcp-stats.js
CHANGED
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import { normalizeToolName, parseArgsText } from "../tool-renderers/utils.js";
|
|
2
|
+
const NUDGE_TYPES = ["turn", "iteration", "context-soft", "context-strong"];
|
|
2
3
|
export function formatDcpStatsToast(session) {
|
|
3
4
|
const stats = collectDcpSessionStats(session);
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
const nudgeStats = collectDcpNudgeStats(session);
|
|
6
|
+
const activeBlocks = stats.activeBlocks ?? 0;
|
|
7
|
+
const totalBlocks = stats.totalBlocks ?? stats.activeBlocks ?? 0;
|
|
8
|
+
const totalNudgeEvents = nudgeStats.emitted + nudgeStats.upgraded;
|
|
9
|
+
const activeAnchors = NUDGE_TYPES.reduce((sum, type) => sum + nudgeStats.activeByType[type], 0);
|
|
10
|
+
const lines = [
|
|
11
|
+
"DCP Session Statistics:",
|
|
12
|
+
` Tokens saved (estimated): ${fmt(stats.tokensSaved)}`,
|
|
13
|
+
` Total pruning operations: ${fmt(stats.totalPruneCount)}`,
|
|
14
|
+
` Compression blocks active: ${activeBlocks} / ${totalBlocks} total`,
|
|
15
|
+
" Manual mode: off",
|
|
16
|
+
"",
|
|
17
|
+
"Nudge telemetry:",
|
|
18
|
+
` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.upgraded)} upgraded`,
|
|
19
|
+
` By type: ${NUDGE_TYPES.map((type) => `${type}=${fmt(nudgeStats.byType[type])}`).join(", ")}`,
|
|
20
|
+
` Active anchors: ${fmt(activeAnchors)}${activeAnchors > 0 ? ` (${NUDGE_TYPES.map((type) => `${type}=${fmt(nudgeStats.activeByType[type])}`).join(", ")})` : ""}`,
|
|
21
|
+
` Cleared after compress: ${fmt(nudgeStats.clearedEvents)} time${nudgeStats.clearedEvents === 1 ? "" : "s"} (${fmt(nudgeStats.clearedAnchors)} anchor${nudgeStats.clearedAnchors === 1 ? "" : "s"})`,
|
|
22
|
+
` Compliance proxy: ${fmt(nudgeStats.clearedEvents)} compress-after-nudge / ${fmt(totalNudgeEvents)} nudge event${totalNudgeEvents === 1 ? "" : "s"} (${pct(nudgeStats.clearedEvents, totalNudgeEvents)})`,
|
|
23
|
+
nudgeStats.last
|
|
24
|
+
? ` Last nudge: ${nudgeStats.last.type} ${nudgeStats.last.event} at ${formatDate(nudgeStats.last.createdAt)} (${formatContextPercent(nudgeStats.last.contextPercent)})`
|
|
25
|
+
: " Last nudge: none recorded",
|
|
26
|
+
"",
|
|
27
|
+
`Context: ${formatContextUsage(stats)}`,
|
|
28
|
+
];
|
|
29
|
+
return lines.join("\n");
|
|
14
30
|
}
|
|
15
31
|
function collectDcpSessionStats(session) {
|
|
16
32
|
const usage = session.getContextUsage();
|
|
17
33
|
const stats = {
|
|
18
34
|
runs: 0,
|
|
19
35
|
tokensSaved: 0,
|
|
36
|
+
totalPruneCount: 0,
|
|
20
37
|
items: 0,
|
|
21
38
|
summaryTokens: 0,
|
|
22
39
|
prunedTools: 0,
|
|
@@ -24,9 +41,15 @@ function collectDcpSessionStats(session) {
|
|
|
24
41
|
...(usage?.contextWindow != null ? { contextWindow: usage.contextWindow } : {}),
|
|
25
42
|
...(usage?.percent != null ? { contextPercent: usage.percent } : {}),
|
|
26
43
|
};
|
|
27
|
-
|
|
44
|
+
const branch = session.sessionManager.getBranch();
|
|
45
|
+
const latestState = latestCustomEntryData(branch, "dcp-state");
|
|
46
|
+
if (latestState)
|
|
47
|
+
applyDcpStateStats(stats, latestState);
|
|
48
|
+
for (const entry of branch) {
|
|
28
49
|
if (entry.type !== "message")
|
|
29
50
|
continue;
|
|
51
|
+
if (latestState)
|
|
52
|
+
continue;
|
|
30
53
|
const message = entry.message;
|
|
31
54
|
if (message.role !== "toolResult")
|
|
32
55
|
continue;
|
|
@@ -39,6 +62,7 @@ function collectDcpSessionStats(session) {
|
|
|
39
62
|
continue;
|
|
40
63
|
stats.runs += 1;
|
|
41
64
|
stats.tokensSaved += numberValue(result.tokensSaved) ?? 0;
|
|
65
|
+
stats.totalPruneCount = Math.max(stats.totalPruneCount, numberValue(result.totalPruneCount) ?? 0);
|
|
42
66
|
stats.items += numberValue(result.itemCount) ?? sumDefined(numberValue(result.ranges), numberValue(result.messages)) ?? 0;
|
|
43
67
|
stats.summaryTokens += numberValue(result.totalSummaryTokens) ?? 0;
|
|
44
68
|
stats.prunedTools += numberValue(result.prunedTools) ?? 0;
|
|
@@ -60,6 +84,91 @@ function collectDcpSessionStats(session) {
|
|
|
60
84
|
}
|
|
61
85
|
return stats;
|
|
62
86
|
}
|
|
87
|
+
function applyDcpStateStats(stats, data) {
|
|
88
|
+
stats.tokensSaved = numberValue(data.tokensSaved) ?? stats.tokensSaved;
|
|
89
|
+
stats.totalPruneCount = numberValue(data.totalPruneCount) ?? stats.totalPruneCount;
|
|
90
|
+
const blocks = Array.isArray(data.compressionBlocks) ? data.compressionBlocks : undefined;
|
|
91
|
+
if (blocks) {
|
|
92
|
+
stats.totalBlocks = blocks.length;
|
|
93
|
+
stats.activeBlocks = blocks.filter((block) => isRecord(block) && block.active !== false).length;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(data.prunedToolIds))
|
|
96
|
+
stats.prunedTools = data.prunedToolIds.length;
|
|
97
|
+
}
|
|
98
|
+
function collectDcpNudgeStats(session) {
|
|
99
|
+
const stats = {
|
|
100
|
+
emitted: 0,
|
|
101
|
+
upgraded: 0,
|
|
102
|
+
clearedEvents: 0,
|
|
103
|
+
clearedAnchors: 0,
|
|
104
|
+
byType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
|
|
105
|
+
activeByType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
|
|
106
|
+
};
|
|
107
|
+
const branch = session.sessionManager.getBranch();
|
|
108
|
+
const latestState = latestCustomEntryData(branch, "dcp-state");
|
|
109
|
+
if (latestState)
|
|
110
|
+
applyActiveAnchorStats(stats, latestState);
|
|
111
|
+
for (const entry of branch) {
|
|
112
|
+
const data = customEntryData(entry, "dcp-nudge");
|
|
113
|
+
if (!data)
|
|
114
|
+
continue;
|
|
115
|
+
const event = data.event;
|
|
116
|
+
if ((event === "emitted" || event === "upgraded") && isNudgeType(data.type)) {
|
|
117
|
+
if (event === "emitted")
|
|
118
|
+
stats.emitted += 1;
|
|
119
|
+
else
|
|
120
|
+
stats.upgraded += 1;
|
|
121
|
+
stats.byType[data.type] += 1;
|
|
122
|
+
const createdAt = numberValue(data.createdAt);
|
|
123
|
+
const contextPercent = typeof data.contextPercent === "number" || data.contextPercent === null ? data.contextPercent : undefined;
|
|
124
|
+
if (!stats.last || (createdAt ?? 0) >= (stats.last.createdAt ?? 0)) {
|
|
125
|
+
stats.last = {
|
|
126
|
+
type: data.type,
|
|
127
|
+
event,
|
|
128
|
+
...(createdAt !== undefined ? { createdAt } : {}),
|
|
129
|
+
...(contextPercent !== undefined ? { contextPercent } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (event === "cleared") {
|
|
134
|
+
stats.clearedEvents += 1;
|
|
135
|
+
stats.clearedAnchors += Math.max(0, numberValue(data.clearedAnchors) ?? 0);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return stats;
|
|
139
|
+
}
|
|
140
|
+
function applyActiveAnchorStats(stats, data) {
|
|
141
|
+
stats.activeByType = { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 };
|
|
142
|
+
const anchors = Array.isArray(data.nudgeAnchors) ? data.nudgeAnchors : [];
|
|
143
|
+
for (const anchor of anchors) {
|
|
144
|
+
if (isRecord(anchor) && isNudgeType(anchor.type))
|
|
145
|
+
stats.activeByType[anchor.type] += 1;
|
|
146
|
+
}
|
|
147
|
+
const last = isRecord(data.lastNudge) ? data.lastNudge : undefined;
|
|
148
|
+
if (last && isNudgeType(last.type) && !stats.last) {
|
|
149
|
+
const contextPercent = numberValue(last.contextPercent);
|
|
150
|
+
const createdAt = numberValue(last.createdAt);
|
|
151
|
+
stats.last = {
|
|
152
|
+
type: last.type,
|
|
153
|
+
event: "emitted",
|
|
154
|
+
...(createdAt !== undefined ? { createdAt } : {}),
|
|
155
|
+
...(contextPercent !== undefined ? { contextPercent: contextPercent * 100 } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function customEntryData(entry, customType) {
|
|
160
|
+
if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== customType)
|
|
161
|
+
return undefined;
|
|
162
|
+
return isRecord(entry.data) ? entry.data : undefined;
|
|
163
|
+
}
|
|
164
|
+
function latestCustomEntryData(entries, customType) {
|
|
165
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
166
|
+
const data = customEntryData(entries[i], customType);
|
|
167
|
+
if (data)
|
|
168
|
+
return data;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
63
172
|
function parseToolResultText(content) {
|
|
64
173
|
const parsed = parseArgsText(textContent(content));
|
|
65
174
|
return isRecord(parsed) ? parsed : undefined;
|
|
@@ -87,6 +196,29 @@ function formatContextUsage(stats) {
|
|
|
87
196
|
return `${percentText} of ${formatCompactNumber(window)}`;
|
|
88
197
|
return percentText;
|
|
89
198
|
}
|
|
199
|
+
function isNudgeType(value) {
|
|
200
|
+
return typeof value === "string" && NUDGE_TYPES.includes(value);
|
|
201
|
+
}
|
|
202
|
+
function fmt(n) {
|
|
203
|
+
return Math.round(n).toLocaleString();
|
|
204
|
+
}
|
|
205
|
+
function pct(numerator, denominator) {
|
|
206
|
+
if (denominator <= 0)
|
|
207
|
+
return "n/a";
|
|
208
|
+
return `${((numerator / denominator) * 100).toFixed(1)}%`;
|
|
209
|
+
}
|
|
210
|
+
function formatDate(ts) {
|
|
211
|
+
if (typeof ts !== "number" || !Number.isFinite(ts) || ts <= 0)
|
|
212
|
+
return "unknown time";
|
|
213
|
+
return new Date(ts).toLocaleString();
|
|
214
|
+
}
|
|
215
|
+
function formatContextPercent(value) {
|
|
216
|
+
if (value === null)
|
|
217
|
+
return "unknown context";
|
|
218
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
219
|
+
return "unknown context";
|
|
220
|
+
return `${value.toFixed(1)}% context`;
|
|
221
|
+
}
|
|
90
222
|
function numberValue(value) {
|
|
91
223
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
92
224
|
}
|
|
@@ -108,9 +240,6 @@ function formatPercent(value) {
|
|
|
108
240
|
function trimDecimal(value) {
|
|
109
241
|
return value.toFixed(1).replace(/\.0$/, "");
|
|
110
242
|
}
|
|
111
|
-
function plural(count, word) {
|
|
112
|
-
return count === 1 ? word : `${word}s`;
|
|
113
|
-
}
|
|
114
243
|
function isRecord(value) {
|
|
115
244
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
116
245
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type PixInstallCliOptions = {
|
|
2
|
+
checkOnly: boolean;
|
|
3
|
+
help: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type PixInstallCliContext = {
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
};
|
|
8
|
+
export declare function pixInstallUsage(): string;
|
|
9
|
+
export declare function parsePixInstallArgs(argv: readonly string[]): PixInstallCliOptions;
|
|
10
|
+
export declare function runPixInstallCli(argv?: readonly string[], context?: PixInstallCliContext): Promise<number>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { FONT_FAMILY_NAME, installJetBrainsNerdFont, isJetBrainsNerdFontInstalled, } from "./nerd-font-controller.js";
|
|
5
|
+
import { clipboardInstallHint, clipboardSupportAvailable } from "./clipboard.js";
|
|
6
|
+
export function pixInstallUsage() {
|
|
7
|
+
return `Usage: pix install [--check]
|
|
8
|
+
pix setup [--check]
|
|
9
|
+
|
|
10
|
+
Check and install Pix runtime helpers for this user.
|
|
11
|
+
|
|
12
|
+
What it checks:
|
|
13
|
+
- ${FONT_FAMILY_NAME} icon font for Pix glyphs
|
|
14
|
+
- pi CLI availability, including Pix's bundled Pi dependency
|
|
15
|
+
- Linux clipboard helpers / native clipboard fallback
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--check Only report missing helpers, do not install
|
|
19
|
+
-h, --help Show this help`;
|
|
20
|
+
}
|
|
21
|
+
export function parsePixInstallArgs(argv) {
|
|
22
|
+
let checkOnly = false;
|
|
23
|
+
let help = false;
|
|
24
|
+
for (const arg of argv) {
|
|
25
|
+
if (arg === "--check") {
|
|
26
|
+
checkOnly = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === "--help" || arg === "-h") {
|
|
30
|
+
help = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unknown pix install argument: ${arg}\n\n${pixInstallUsage()}`);
|
|
34
|
+
}
|
|
35
|
+
return { checkOnly, help };
|
|
36
|
+
}
|
|
37
|
+
export async function runPixInstallCli(argv = process.argv.slice(2), context = {}) {
|
|
38
|
+
let options;
|
|
39
|
+
try {
|
|
40
|
+
options = parsePixInstallArgs(argv);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (options.help) {
|
|
47
|
+
console.log(pixInstallUsage());
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const env = context.env ?? process.env;
|
|
51
|
+
let failures = 0;
|
|
52
|
+
console.log("Pix install checks");
|
|
53
|
+
if (await isJetBrainsNerdFontInstalled()) {
|
|
54
|
+
console.log(`✓ ${FONT_FAMILY_NAME} is installed`);
|
|
55
|
+
}
|
|
56
|
+
else if (options.checkOnly) {
|
|
57
|
+
console.log(`! ${FONT_FAMILY_NAME} is missing`);
|
|
58
|
+
failures += 1;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
try {
|
|
62
|
+
await installJetBrainsNerdFont();
|
|
63
|
+
console.log(`✓ Installed ${FONT_FAMILY_NAME}`);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`✗ Failed to install ${FONT_FAMILY_NAME}: ${errorMessage(error)}`);
|
|
67
|
+
failures += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const piCli = await resolvePiCliStatus(env);
|
|
71
|
+
if (piCli.available) {
|
|
72
|
+
console.log(`✓ pi CLI is available${piCli.detail ? ` (${piCli.detail})` : ""}`);
|
|
73
|
+
}
|
|
74
|
+
else if (options.checkOnly) {
|
|
75
|
+
console.log("! pi CLI is missing");
|
|
76
|
+
failures += 1;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
try {
|
|
80
|
+
await installPiCli();
|
|
81
|
+
console.log("✓ Installed pi CLI globally");
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`✗ Failed to install pi CLI: ${errorMessage(error)}`);
|
|
85
|
+
console.error(" Pix can still use its bundled SDK, but sub-agent helpers may need `pi` on PATH.");
|
|
86
|
+
failures += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (clipboardSupportAvailable(env)) {
|
|
90
|
+
console.log("✓ Clipboard support is available");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(`! Clipboard support is missing. ${clipboardInstallHint()}`);
|
|
94
|
+
if (process.platform === "linux")
|
|
95
|
+
failures += 1;
|
|
96
|
+
}
|
|
97
|
+
return failures === 0 ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
async function resolvePiCliStatus(env) {
|
|
100
|
+
const bundledBin = env.PIX_BUNDLED_PI_BIN;
|
|
101
|
+
if (bundledBin && (existsSync(join(bundledBin, process.platform === "win32" ? "pi.cmd" : "pi")) || existsSync(join(bundledBin, "pi")))) {
|
|
102
|
+
return { available: true, detail: "bundled with Pix" };
|
|
103
|
+
}
|
|
104
|
+
if (commandExists("pi", env))
|
|
105
|
+
return { available: true, detail: "PATH" };
|
|
106
|
+
return { available: false };
|
|
107
|
+
}
|
|
108
|
+
async function installPiCli() {
|
|
109
|
+
await runRequired("npm", ["install", "-g", "--ignore-scripts", "--min-release-age=0", "@earendil-works/pi-coding-agent"]);
|
|
110
|
+
}
|
|
111
|
+
function commandExists(command, env = process.env) {
|
|
112
|
+
const pathValue = env.PATH ?? "";
|
|
113
|
+
const dirs = pathValue.split(process.platform === "win32" ? ";" : ":").filter(Boolean);
|
|
114
|
+
const names = process.platform === "win32" ? [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`] : [command];
|
|
115
|
+
return dirs.some((dir) => names.some((name) => existsSync(join(dir, name))));
|
|
116
|
+
}
|
|
117
|
+
async function runRequired(command, args) {
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
120
|
+
let stderr = "";
|
|
121
|
+
child.stderr.on("data", (chunk) => {
|
|
122
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-800);
|
|
123
|
+
});
|
|
124
|
+
child.once("error", reject);
|
|
125
|
+
child.once("close", (code) => {
|
|
126
|
+
if (code === 0)
|
|
127
|
+
resolve();
|
|
128
|
+
else
|
|
129
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? "unknown"}`));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function errorMessage(error) {
|
|
134
|
+
return error instanceof Error ? error.message : String(error);
|
|
135
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AppCommandController } from "./command-controller.js";
|
|
|
2
2
|
import type { ConversationViewport } from "./conversation-viewport.js";
|
|
3
3
|
import type { EditorLayoutRenderer } from "./editor-layout-renderer.js";
|
|
4
4
|
import type { ImageContent, InputEditor } from "../input-editor.js";
|
|
5
|
-
import type { ToastEntry } from "../ui.js";
|
|
5
|
+
import type { ToastEntry, ToastVariant } from "../ui.js";
|
|
6
6
|
import type { AppPopupActionController } from "./popup-action-controller.js";
|
|
7
7
|
import type { AppPopupMenuController } from "./popup-menu-controller.js";
|
|
8
8
|
import type { AppScrollController } from "./scroll-controller.js";
|
|
@@ -50,7 +50,10 @@ export type AppMouseControllerHost = {
|
|
|
50
50
|
switchToTab(tabId: string): void;
|
|
51
51
|
closeTab(tabId: string): void;
|
|
52
52
|
toastEntry(toastId: number): ToastEntry | undefined;
|
|
53
|
-
showToast(message: string, kind: "success" | "error" | "warning" | "info"
|
|
53
|
+
showToast(message: string, kind: "success" | "error" | "warning" | "info", options?: {
|
|
54
|
+
durationMs?: number;
|
|
55
|
+
variant?: ToastVariant;
|
|
56
|
+
}): void;
|
|
54
57
|
dismissToast(toastId: number): void;
|
|
55
58
|
refreshModelUsageStatus(): void | Promise<void>;
|
|
56
59
|
toggleAllThinkingExpanded?(): void;
|
|
@@ -87,10 +90,7 @@ export declare class AppMouseController {
|
|
|
87
90
|
} | {
|
|
88
91
|
kind: "queue-message";
|
|
89
92
|
id: string;
|
|
90
|
-
} |
|
|
91
|
-
kind: "toast";
|
|
92
|
-
id: number;
|
|
93
|
-
} | undefined>;
|
|
93
|
+
} | import("./types.js").ToastLineTarget | undefined>;
|
|
94
94
|
readonly renderedRowTexts: Map<number, string>;
|
|
95
95
|
readonly renderedRowBackgrounds: Map<number, string>;
|
|
96
96
|
readonly renderedImageTargets: Map<number, readonly ImageClickTarget[]>;
|