pi-ui-extend 0.1.2 → 0.1.4
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 +24 -1
- package/dist/app/app.d.ts +0 -1
- package/dist/app/app.js +4 -7
- 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/conversation-entry-renderer.d.ts +0 -1
- package/dist/app/conversation-entry-renderer.js +2 -6
- package/dist/app/conversation-tool-renderer.js +2 -3
- package/dist/app/conversation-viewport.d.ts +0 -1
- package/dist/app/conversation-viewport.js +0 -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/config.d.ts +0 -3
- package/dist/config.js +0 -79
- package/dist/default-pix-config.js +2 -2
- package/dist/markdown-format.js +18 -1
- package/dist/ui.d.ts +5 -1
- package/dist/ui.js +2 -2
- package/external/pi-tools-suite/README.md +4 -4
- package/external/pi-tools-suite/licenses/opencode-dynamic-context-pruning-AGPL-3.0.txt +619 -0
- package/external/pi-tools-suite/package.json +1 -1
- package/external/pi-tools-suite/src/config.ts +5 -1
- package/external/pi-tools-suite/src/{compress → dcp}/config.ts +10 -70
- package/external/pi-tools-suite/src/{compress → dcp}/index.ts +16 -66
- package/external/pi-tools-suite/src/dcp/ui.ts +45 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -2
- package/external/pi-tools-suite/src/index.ts +1 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- package/package.json +1 -1
- package/external/pi-tools-suite/src/compress/dcp-tui-filter.ts +0 -498
- package/external/pi-tools-suite/src/compress/ui.ts +0 -308
- /package/external/pi-tools-suite/src/{compress → dcp}/commands.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/compress-tool.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/compression-blocks.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/prompts.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-candidates.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-compression-blocks.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-message-ids.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-metadata.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-nudge.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-tools.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-types.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/state.ts +0 -0
|
@@ -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[]>;
|
|
@@ -114,6 +114,10 @@ export class AppMouseController {
|
|
|
114
114
|
if (event.button !== 0)
|
|
115
115
|
return;
|
|
116
116
|
if (target?.kind === "toast") {
|
|
117
|
+
if (!toastTargetContainsEvent(target, event))
|
|
118
|
+
return;
|
|
119
|
+
if (target.action === "body")
|
|
120
|
+
return;
|
|
117
121
|
if (this.copyErrorToast(target.id)) {
|
|
118
122
|
this.showClickFlashForEvent(event);
|
|
119
123
|
return;
|
|
@@ -235,6 +239,14 @@ export class AppMouseController {
|
|
|
235
239
|
const statusTarget = this.statusTargetAt(event);
|
|
236
240
|
if (statusTarget)
|
|
237
241
|
return statusTarget;
|
|
242
|
+
const toastTarget = this.renderedTargets.get(event.y);
|
|
243
|
+
if (toastTarget?.kind === "toast" && toastTargetContainsEvent(toastTarget, event)) {
|
|
244
|
+
return {
|
|
245
|
+
y: event.y,
|
|
246
|
+
startColumn: toastTarget.startColumn ?? event.x,
|
|
247
|
+
endColumn: toastTarget.endColumn ?? event.x + 1,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
238
250
|
if (this.renderedTargets.has(event.y)) {
|
|
239
251
|
const bounds = nonBlankLineBounds(this.renderedRowTexts.get(event.y) ?? "", event.x);
|
|
240
252
|
return { y: event.y, startColumn: bounds.startColumn, endColumn: bounds.endColumn };
|
|
@@ -469,7 +481,8 @@ export class AppMouseController {
|
|
|
469
481
|
const session = this.host.runtimeSession();
|
|
470
482
|
if (!session)
|
|
471
483
|
return false;
|
|
472
|
-
|
|
484
|
+
const message = formatDcpStatsToast(session);
|
|
485
|
+
this.host.showToast(message, "info", { variant: "dialog" });
|
|
473
486
|
return true;
|
|
474
487
|
}
|
|
475
488
|
handleStatusModelUsageClick(event) {
|
|
@@ -910,6 +923,11 @@ export function screenSelectionLineText(row, text, startColumn, endColumn, input
|
|
|
910
923
|
function sameConversationPoint(left, right) {
|
|
911
924
|
return !!left && left.line === right.line && left.x === right.x;
|
|
912
925
|
}
|
|
926
|
+
function toastTargetContainsEvent(target, event) {
|
|
927
|
+
if (target.startColumn === undefined || target.endColumn === undefined)
|
|
928
|
+
return true;
|
|
929
|
+
return event.x >= target.startColumn && event.x < target.endColumn;
|
|
930
|
+
}
|
|
913
931
|
function displayCellsInColumnRange(text, startColumn, endColumn) {
|
|
914
932
|
let cells = "";
|
|
915
933
|
for (let column = startColumn; column < endColumn; column += 1) {
|
|
@@ -2,6 +2,9 @@ export type NerdFontInstallHost = {
|
|
|
2
2
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
3
3
|
render(): void;
|
|
4
4
|
};
|
|
5
|
+
export declare const FONT_FAMILY_NAME = "JetBrainsMono Nerd Font Mono";
|
|
6
|
+
export declare const FONT_FILE_NAME = "JetBrainsMonoNerdFontMono-Regular.ttf";
|
|
7
|
+
export declare const FONT_DOWNLOAD_URL = "https://raw.githubusercontent.com/ryanoasis/nerd-fonts/master/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFontMono-Regular.ttf";
|
|
5
8
|
export declare class NerdFontController {
|
|
6
9
|
private readonly host;
|
|
7
10
|
private ensureStarted;
|
|
@@ -9,3 +12,6 @@ export declare class NerdFontController {
|
|
|
9
12
|
ensureInstalledOnStartup(): void;
|
|
10
13
|
private ensureInstalled;
|
|
11
14
|
}
|
|
15
|
+
export declare function isJetBrainsNerdFontInstalled(): Promise<boolean>;
|
|
16
|
+
export declare function installJetBrainsNerdFont(): Promise<string>;
|
|
17
|
+
export declare function userFontInstallPath(): string;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
5
6
|
const CASK_NAME = "font-jetbrains-mono-nerd-font";
|
|
7
|
+
export const FONT_FAMILY_NAME = "JetBrainsMono Nerd Font Mono";
|
|
8
|
+
export const FONT_FILE_NAME = "JetBrainsMonoNerdFontMono-Regular.ttf";
|
|
9
|
+
export const FONT_DOWNLOAD_URL = "https://raw.githubusercontent.com/ryanoasis/nerd-fonts/master/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFontMono-Regular.ttf";
|
|
6
10
|
const FONT_FILE_PATTERN = /(?:JetBrainsMono|JetBrains).*Nerd.*\.(?:ttf|otf)$/iu;
|
|
7
11
|
export class NerdFontController {
|
|
8
12
|
host;
|
|
@@ -19,17 +23,9 @@ export class NerdFontController {
|
|
|
19
23
|
async ensureInstalled() {
|
|
20
24
|
if (await isJetBrainsNerdFontInstalled())
|
|
21
25
|
return;
|
|
22
|
-
if (process.platform !== "darwin") {
|
|
23
|
-
this.host.showToast("Nerd Font is missing; auto-install is only configured for macOS Homebrew", "warning");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (!commandExists("brew")) {
|
|
27
|
-
this.host.showToast("Nerd Font is missing; install Homebrew or JetBrainsMono Nerd Font manually", "warning");
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
26
|
this.host.showToast("Installing JetBrainsMono Nerd Font…", "info");
|
|
31
27
|
try {
|
|
32
|
-
await
|
|
28
|
+
await installJetBrainsNerdFont();
|
|
33
29
|
if (await isJetBrainsNerdFontInstalled()) {
|
|
34
30
|
this.host.showToast("JetBrainsMono Nerd Font installed", "success");
|
|
35
31
|
}
|
|
@@ -45,18 +41,89 @@ export class NerdFontController {
|
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
|
-
async function isJetBrainsNerdFontInstalled() {
|
|
44
|
+
export async function isJetBrainsNerdFontInstalled() {
|
|
49
45
|
if (commandExists("brew") && spawnSync("brew", ["list", "--cask", CASK_NAME], { stdio: "ignore" }).status === 0)
|
|
50
46
|
return true;
|
|
51
|
-
|
|
47
|
+
if (process.platform === "linux" && commandExists("fc-match")) {
|
|
48
|
+
const result = spawnSync("fc-match", ["-f", "%{family}", FONT_FAMILY_NAME], { encoding: "utf8" });
|
|
49
|
+
if (result.status === 0 && /JetBrains.*Nerd/iu.test(result.stdout))
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const fontDirs = platformFontDirs();
|
|
52
53
|
for (const dir of fontDirs) {
|
|
54
|
+
if (await directoryContainsFont(dir))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
export async function installJetBrainsNerdFont() {
|
|
60
|
+
if (process.platform === "darwin" && commandExists("brew")) {
|
|
61
|
+
await runBrewInstall();
|
|
62
|
+
return CASK_NAME;
|
|
63
|
+
}
|
|
64
|
+
const targetPath = userFontInstallPath();
|
|
65
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
66
|
+
const response = await fetch(FONT_DOWNLOAD_URL, {
|
|
67
|
+
headers: { "User-Agent": "pix-font-installer" },
|
|
68
|
+
signal: AbortSignal.timeout(30_000),
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok)
|
|
71
|
+
throw new Error(`download failed with HTTP ${response.status}`);
|
|
72
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
73
|
+
if (bytes.length < 100_000)
|
|
74
|
+
throw new Error("downloaded font is unexpectedly small");
|
|
75
|
+
await writeFile(targetPath, bytes);
|
|
76
|
+
if (process.platform === "linux")
|
|
77
|
+
runOptionalCommand("fc-cache", ["-f", dirname(targetPath)]);
|
|
78
|
+
if (process.platform === "win32")
|
|
79
|
+
registerWindowsUserFont(targetPath);
|
|
80
|
+
return targetPath;
|
|
81
|
+
}
|
|
82
|
+
export function userFontInstallPath() {
|
|
83
|
+
switch (process.platform) {
|
|
84
|
+
case "darwin":
|
|
85
|
+
return join(homedir(), "Library", "Fonts", FONT_FILE_NAME);
|
|
86
|
+
case "win32":
|
|
87
|
+
return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "Microsoft", "Windows", "Fonts", FONT_FILE_NAME);
|
|
88
|
+
default:
|
|
89
|
+
return join(homedir(), ".local", "share", "fonts", "pix", FONT_FILE_NAME);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function platformFontDirs() {
|
|
93
|
+
switch (process.platform) {
|
|
94
|
+
case "darwin":
|
|
95
|
+
return [join(homedir(), "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
|
|
96
|
+
case "win32":
|
|
97
|
+
return [
|
|
98
|
+
join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "Microsoft", "Windows", "Fonts"),
|
|
99
|
+
join(process.env.WINDIR ?? "C:\\Windows", "Fonts"),
|
|
100
|
+
];
|
|
101
|
+
default:
|
|
102
|
+
return [join(homedir(), ".local", "share", "fonts"), join(homedir(), ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function directoryContainsFont(root) {
|
|
106
|
+
if (!existsSync(root))
|
|
107
|
+
return false;
|
|
108
|
+
const pending = [{ dir: root, depth: 0 }];
|
|
109
|
+
let scanned = 0;
|
|
110
|
+
while (pending.length > 0 && scanned < 5_000) {
|
|
111
|
+
const current = pending.pop();
|
|
112
|
+
if (!current)
|
|
113
|
+
continue;
|
|
114
|
+
let entries;
|
|
53
115
|
try {
|
|
54
|
-
|
|
55
|
-
if (files.some((file) => FONT_FILE_PATTERN.test(file)))
|
|
56
|
-
return true;
|
|
116
|
+
entries = await readdir(current.dir, { withFileTypes: true });
|
|
57
117
|
}
|
|
58
118
|
catch {
|
|
59
|
-
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
scanned += 1;
|
|
123
|
+
if (entry.isFile() && FONT_FILE_PATTERN.test(entry.name))
|
|
124
|
+
return true;
|
|
125
|
+
if (entry.isDirectory() && current.depth < 4)
|
|
126
|
+
pending.push({ dir: join(current.dir, entry.name), depth: current.depth + 1 });
|
|
60
127
|
}
|
|
61
128
|
}
|
|
62
129
|
return false;
|
|
@@ -80,6 +147,20 @@ async function runBrewInstall() {
|
|
|
80
147
|
});
|
|
81
148
|
});
|
|
82
149
|
}
|
|
150
|
+
function registerWindowsUserFont(fontPath) {
|
|
151
|
+
const escapedPath = fontPath.replaceAll("'", "''");
|
|
152
|
+
const escapedName = `${FONT_FAMILY_NAME} (TrueType)`.replaceAll("'", "''");
|
|
153
|
+
runOptionalCommand("powershell.exe", [
|
|
154
|
+
"-NoProfile",
|
|
155
|
+
"-ExecutionPolicy",
|
|
156
|
+
"Bypass",
|
|
157
|
+
"-Command",
|
|
158
|
+
`New-Item -Path 'HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' -Force | Out-Null; New-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' -Name '${escapedName}' -Value '${escapedPath}' -PropertyType String -Force | Out-Null`,
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
function runOptionalCommand(command, args) {
|
|
162
|
+
spawnSync(command, args, { stdio: "ignore" });
|
|
163
|
+
}
|
|
83
164
|
function commandExists(command) {
|
|
84
165
|
if (process.platform === "win32")
|
|
85
166
|
return spawnSync("where", [command], { stdio: "ignore" }).status === 0;
|
|
@@ -243,10 +243,11 @@ export class AppRenderController {
|
|
|
243
243
|
}
|
|
244
244
|
for (const toastOverlay of renderToastOverlays(this.deps.toastController.toast.visibleStates, columns, Math.max(0, statusRow - topReservedRows - 1), this.deps.theme)) {
|
|
245
245
|
const row = topReservedRows + toastOverlay.row;
|
|
246
|
-
this.deps.mouseController.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
const rowText = this.deps.mouseController.renderedRowTexts.get(row) ?? "";
|
|
247
|
+
if (toastOverlay.target)
|
|
248
|
+
this.deps.mouseController.renderedTargets.set(row, toastOverlay.target);
|
|
249
|
+
this.deps.mouseController.renderedRowTexts.set(row, overlayText(rowText, toastOverlay.column, toastOverlay.text));
|
|
250
|
+
appendFrameOutput(regionForOverlayRow(row), row, `\x1b[${row};${toastOverlay.column}H${toastOverlay.output}`);
|
|
250
251
|
}
|
|
251
252
|
if (topReservedRows === 0) {
|
|
252
253
|
const newTabTarget = tabLayout.targets.find((target) => target.kind === "new-tab");
|
|
@@ -14,7 +14,7 @@ export async function checkPiCliAvailability(pathValue = process.env.PATH ?? "")
|
|
|
14
14
|
return [];
|
|
15
15
|
return [{
|
|
16
16
|
kind: "error",
|
|
17
|
-
message: "pi CLI is not available on PATH.
|
|
17
|
+
message: "pi CLI is not available on PATH. Run `pix install` or add pi to PATH before starting pix.",
|
|
18
18
|
}];
|
|
19
19
|
}
|
|
20
20
|
export function checkPiToolsSuiteExtensionAvailability(extensionsResult) {
|
|
@@ -34,13 +34,16 @@ export function checkPiToolsSuiteExtensionAvailability(extensionsResult) {
|
|
|
34
34
|
}
|
|
35
35
|
async function executableExistsOnPath(command, pathValue) {
|
|
36
36
|
const dirs = pathValue.split(delimiter).filter((part) => part.length > 0);
|
|
37
|
+
const names = process.platform === "win32" ? [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`] : [command];
|
|
37
38
|
for (const dir of dirs) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
try {
|
|
41
|
+
await access(join(dir, name), fsConstants.X_OK);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Keep scanning PATH entries.
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Toast, type ToastKind } from "../ui.js";
|
|
1
|
+
import { Toast, type ToastKind, type ToastVariant } from "../ui.js";
|
|
2
2
|
export type AppToastControllerHost = {
|
|
3
3
|
render(): void;
|
|
4
4
|
};
|
|
@@ -7,7 +7,10 @@ export declare class AppToastController {
|
|
|
7
7
|
readonly toast: Toast;
|
|
8
8
|
private readonly timers;
|
|
9
9
|
constructor(host: AppToastControllerHost);
|
|
10
|
-
showToast(message: string, kind?: ToastKind
|
|
10
|
+
showToast(message: string, kind?: ToastKind, options?: {
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
variant?: ToastVariant;
|
|
13
|
+
}): void;
|
|
11
14
|
dismissToast(toastId: number): void;
|
|
12
15
|
clearToastTimers(): void;
|
|
13
16
|
}
|
|
@@ -7,17 +7,20 @@ export class AppToastController {
|
|
|
7
7
|
constructor(host) {
|
|
8
8
|
this.host = host;
|
|
9
9
|
}
|
|
10
|
-
showToast(message, kind = "info") {
|
|
11
|
-
const toastId = this.toast.show(message, kind);
|
|
12
|
-
if (kind === "error") {
|
|
10
|
+
showToast(message, kind = "info", options = {}) {
|
|
11
|
+
const toastId = this.toast.show(message, kind, options.variant ? { variant: options.variant } : {});
|
|
12
|
+
if (kind === "error" || options.variant === "dialog") {
|
|
13
13
|
this.host.render();
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
+
const durationMs = typeof options.durationMs === "number" && Number.isFinite(options.durationMs) && options.durationMs > 0
|
|
17
|
+
? Math.floor(options.durationMs)
|
|
18
|
+
: TOAST_DURATION_MS;
|
|
16
19
|
const timer = setTimeout(() => {
|
|
17
20
|
this.toast.hide(toastId);
|
|
18
21
|
this.timers.delete(toastId);
|
|
19
22
|
this.host.render();
|
|
20
|
-
},
|
|
23
|
+
}, durationMs);
|
|
21
24
|
this.timers.set(toastId, timer);
|
|
22
25
|
timer.unref();
|
|
23
26
|
this.host.render();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { type Theme } from "../theme.js";
|
|
2
2
|
import type { ToastEntry } from "../ui.js";
|
|
3
|
+
import type { ToastLineTarget } from "./types.js";
|
|
3
4
|
export type ToastOverlay = {
|
|
4
5
|
id: number;
|
|
5
6
|
row: number;
|
|
7
|
+
column: number;
|
|
6
8
|
text: string;
|
|
7
9
|
output: string;
|
|
10
|
+
target?: ToastLineTarget;
|
|
8
11
|
};
|
|
9
12
|
export declare function renderToastOverlays(states: readonly ToastEntry[], width: number, maxRows: number, theme: Theme): ToastOverlay[];
|
|
@@ -9,6 +9,10 @@ export function renderToastOverlays(states, width, maxRows, theme) {
|
|
|
9
9
|
for (const state of [...states].reverse()) {
|
|
10
10
|
if (overlays.length >= maxRows)
|
|
11
11
|
break;
|
|
12
|
+
if (state.variant === "dialog") {
|
|
13
|
+
overlays.push(...renderDialogToastOverlay(state, width, Math.max(0, maxRows - overlays.length), theme, overlays.length));
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
12
16
|
const icon = toastKindIcon(state.kind);
|
|
13
17
|
const lines = toastMessageLines(state.message, icon, Math.max(1, width - 6));
|
|
14
18
|
const visibleLines = lines.slice(0, Math.max(0, maxRows - overlays.length));
|
|
@@ -17,19 +21,22 @@ export function renderToastOverlays(states, width, maxRows, theme) {
|
|
|
17
21
|
const contentWidth = Math.max(...visibleLines.map((line) => stringDisplayWidth(line)));
|
|
18
22
|
const toastWidth = Math.min(Math.max(12, contentWidth + 2), Math.max(1, width - 4));
|
|
19
23
|
const leftWidth = Math.max(0, width - toastWidth - 2);
|
|
20
|
-
const
|
|
24
|
+
const column = leftWidth + 1;
|
|
21
25
|
for (const line of visibleLines) {
|
|
22
26
|
const message = ` ${padOrTrimPlain(line, Math.max(0, toastWidth - 2))} `;
|
|
23
|
-
const text =
|
|
24
|
-
const output =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const text = padOrTrimPlain(message, toastWidth);
|
|
28
|
+
const output = colorLine(message, toastWidth, {
|
|
29
|
+
...toastKindStyle(state.kind, theme),
|
|
30
|
+
bold: true,
|
|
31
|
+
});
|
|
32
|
+
overlays.push({
|
|
33
|
+
id: state.id,
|
|
34
|
+
row: overlays.length + 1,
|
|
35
|
+
column,
|
|
36
|
+
text,
|
|
37
|
+
output,
|
|
38
|
+
target: { kind: "toast", id: state.id, action: "toast", startColumn: column, endColumn: column + toastWidth },
|
|
39
|
+
});
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
return overlays;
|
|
@@ -53,6 +60,60 @@ function toastMessageLines(message, icon, maxWidth) {
|
|
|
53
60
|
}
|
|
54
61
|
return lines.length > 0 ? lines : [firstPrefix.trimEnd()];
|
|
55
62
|
}
|
|
63
|
+
function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
|
|
64
|
+
if (maxRows <= 0 || width <= 0)
|
|
65
|
+
return [];
|
|
66
|
+
const maxDialogWidth = Math.max(1, Math.min(width - 4, 72));
|
|
67
|
+
const icon = toastKindIcon(state.kind);
|
|
68
|
+
const closeLabel = `[${APP_ICONS.close}]`;
|
|
69
|
+
const wrappedLines = dialogMessageLines(state.message, Math.max(1, maxDialogWidth - 4));
|
|
70
|
+
const title = `${icon} Dialog`;
|
|
71
|
+
const requiredWidth = Math.max(16, stringDisplayWidth(` ${title} ${closeLabel} `) + 2, ...wrappedLines.map((line) => stringDisplayWidth(line) + 4));
|
|
72
|
+
const dialogWidth = Math.min(maxDialogWidth, Math.max(16, requiredWidth));
|
|
73
|
+
const bodyWidth = Math.max(1, dialogWidth - 4);
|
|
74
|
+
const bodyLines = dialogMessageLines(state.message, bodyWidth);
|
|
75
|
+
const bodyRows = Math.max(0, maxRows - 2);
|
|
76
|
+
const visibleBodyLines = bodyLines.slice(0, bodyRows);
|
|
77
|
+
const includeBottom = maxRows > 1;
|
|
78
|
+
const dialogRows = [
|
|
79
|
+
dialogTopLine(title, closeLabel, dialogWidth),
|
|
80
|
+
...visibleBodyLines.map((line) => `│ ${padOrTrimPlain(line, bodyWidth)} │`),
|
|
81
|
+
...(includeBottom ? [`╰${"─".repeat(Math.max(0, dialogWidth - 2))}╯`] : []),
|
|
82
|
+
].slice(0, maxRows);
|
|
83
|
+
const leftWidth = Math.max(0, width - dialogWidth - 2);
|
|
84
|
+
const column = leftWidth + 1;
|
|
85
|
+
const style = toastKindStyle(state.kind, theme);
|
|
86
|
+
const closeStartColumn = column + 1 + dialogTopCloseOffset(title, closeLabel, dialogWidth);
|
|
87
|
+
const closeEndColumn = closeStartColumn + stringDisplayWidth(closeLabel);
|
|
88
|
+
return dialogRows.map((text, index) => ({
|
|
89
|
+
id: state.id,
|
|
90
|
+
row: rowOffset + index + 1,
|
|
91
|
+
column,
|
|
92
|
+
text,
|
|
93
|
+
output: colorLine(text, dialogWidth, { ...style, bold: true }),
|
|
94
|
+
target: index === 0
|
|
95
|
+
? { kind: "toast", id: state.id, action: "close", startColumn: closeStartColumn, endColumn: closeEndColumn }
|
|
96
|
+
: { kind: "toast", id: state.id, action: "body", startColumn: column, endColumn: column + dialogWidth },
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function dialogMessageLines(message, maxWidth) {
|
|
100
|
+
const safeMaxWidth = Math.max(1, maxWidth);
|
|
101
|
+
const lines = sanitizeText(message).split("\n").flatMap((line) => wrapDisplayLine(line, safeMaxWidth));
|
|
102
|
+
return lines.length > 0 ? lines : [""];
|
|
103
|
+
}
|
|
104
|
+
function dialogTopLine(title, closeLabel, width) {
|
|
105
|
+
const innerWidth = Math.max(0, width - 2);
|
|
106
|
+
const closeOffset = dialogTopCloseOffset(title, closeLabel, width);
|
|
107
|
+
const leftLabel = ` ${title} `;
|
|
108
|
+
const spacer = " ".repeat(Math.max(0, closeOffset - stringDisplayWidth(leftLabel)));
|
|
109
|
+
return `╭${padOrTrimPlain(`${leftLabel}${spacer}${closeLabel} `, innerWidth)}╮`;
|
|
110
|
+
}
|
|
111
|
+
function dialogTopCloseOffset(title, closeLabel, width) {
|
|
112
|
+
const innerWidth = Math.max(0, width - 2);
|
|
113
|
+
const leftLabel = ` ${title} `;
|
|
114
|
+
const closeWidth = stringDisplayWidth(closeLabel);
|
|
115
|
+
return Math.max(stringDisplayWidth(leftLabel), innerWidth - closeWidth - 1);
|
|
116
|
+
}
|
|
56
117
|
function toastKindIcon(kind) {
|
|
57
118
|
switch (kind) {
|
|
58
119
|
case "success":
|
package/dist/app/types.d.ts
CHANGED
|
@@ -227,10 +227,14 @@ export type RenderedLine = {
|
|
|
227
227
|
} | {
|
|
228
228
|
kind: "queue-message";
|
|
229
229
|
id: string;
|
|
230
|
-
} |
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
} | ToastLineTarget;
|
|
231
|
+
};
|
|
232
|
+
export type ToastLineTarget = {
|
|
233
|
+
kind: "toast";
|
|
234
|
+
id: number;
|
|
235
|
+
action?: "toast" | "body" | "close";
|
|
236
|
+
startColumn?: number;
|
|
237
|
+
endColumn?: number;
|
|
234
238
|
};
|
|
235
239
|
export type ImageClickTarget = {
|
|
236
240
|
start: number;
|
package/dist/config.d.ts
CHANGED
|
@@ -54,9 +54,6 @@ export declare function savePixDictationLanguage(language: string): void;
|
|
|
54
54
|
export declare function upsertPixDictationLanguageInJsonc(source: string, language: string): string;
|
|
55
55
|
export declare function resolveModelColor(modelRef: string, config: ModelColorsConfig): string | undefined;
|
|
56
56
|
export declare function compileOutputFilterPatterns(patterns: readonly string[]): RegExp[];
|
|
57
|
-
export declare function stripDcpDisplayMetadata(text: string): string;
|
|
58
57
|
export declare function applyOutputFilters(text: string, filters: readonly RegExp[]): string;
|
|
59
|
-
export declare function outputFiltersRemoveDcpIdMetadataLine(filters: readonly RegExp[]): boolean;
|
|
60
|
-
export declare function suppressPendingDcpIdMetadataLine(text: string): string;
|
|
61
58
|
export declare function resolveToolRule(toolName: string, config: ToolRendererConfig): ResolvedToolRule;
|
|
62
59
|
export declare function resolveColor(colorRef: string, themeColors: Record<string, string>): string;
|