pi-ui-extend 0.1.11 → 0.1.13
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/dist/app/app.js +6 -4
- package/dist/app/cli/install.d.ts +14 -0
- package/dist/app/cli/install.js +19 -7
- package/dist/app/cli/startup-info.js +5 -2
- package/dist/app/cli/update.d.ts +7 -0
- package/dist/app/cli/update.js +11 -3
- package/dist/app/commands/shell-command.d.ts +7 -0
- package/dist/app/commands/shell-command.js +12 -4
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
- package/dist/app/input/prompt-enhancer-controller.js +12 -3
- package/dist/app/input/voice-controller.d.ts +49 -1
- package/dist/app/input/voice-controller.js +16 -5
- package/dist/app/rendering/conversation-entry-renderer.js +2 -11
- package/dist/app/rendering/status-line-renderer.js +3 -11
- package/dist/app/rendering/toast-renderer.js +10 -13
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
- package/dist/app/rendering/tool-block-renderer.js +3 -2
- package/dist/app/screen/clipboard.d.ts +9 -0
- package/dist/app/screen/clipboard.js +19 -6
- package/dist/app/screen/file-link-opener.d.ts +8 -0
- package/dist/app/screen/file-link-opener.js +11 -3
- package/dist/app/screen/file-links.js +3 -3
- package/dist/app/screen/image-opener.d.ts +12 -0
- package/dist/app/screen/image-opener.js +13 -5
- package/dist/app/session/queued-message-controller.js +5 -1
- package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
- package/dist/app/terminal/nerd-font-controller.js +20 -12
- package/dist/default-pix-config.js +7 -6
- package/dist/schemas/index.d.ts +5 -0
- package/dist/schemas/index.js +5 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +177 -0
- package/dist/schemas/pi-tools-suite-schema.js +218 -0
- package/dist/schemas/pix-schema.d.ts +65 -0
- package/dist/schemas/pix-schema.js +91 -0
- package/dist/terminal-width.js +73 -56
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +1 -0
- package/external/pi-tools-suite/src/todo/index.ts +4 -2
- package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
- package/external/pi-tools-suite/src/todo/todo.ts +2 -6
- package/package.json +12 -3
- package/schemas/pi-tools-suite.json +881 -0
- package/schemas/pix.json +298 -0
package/dist/app/app.js
CHANGED
|
@@ -874,12 +874,14 @@ export class PiUiExtendApp {
|
|
|
874
874
|
}
|
|
875
875
|
toggleSuperCompactTools() {
|
|
876
876
|
this.superCompactTools = !this.superCompactTools;
|
|
877
|
-
if (!this.superCompactTools)
|
|
878
|
-
return;
|
|
879
877
|
for (const entry of this.entries) {
|
|
880
|
-
if (entry.kind !== "tool"
|
|
878
|
+
if (entry.kind !== "tool")
|
|
879
|
+
continue;
|
|
880
|
+
const defaultExpanded = resolveToolRule(entry.toolName, this.pixConfig.toolRenderer).defaultExpanded === true;
|
|
881
|
+
const nextExpanded = this.superCompactTools ? false : defaultExpanded;
|
|
882
|
+
if (entry.expanded === nextExpanded)
|
|
881
883
|
continue;
|
|
882
|
-
entry.expanded =
|
|
884
|
+
entry.expanded = nextExpanded;
|
|
883
885
|
this.touchEntry(entry);
|
|
884
886
|
}
|
|
885
887
|
}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { installJetBrainsNerdFont, isJetBrainsNerdFontInstalled } from "../terminal/nerd-font-controller.js";
|
|
4
|
+
import { clipboardInstallHint, clipboardSupportAvailable } from "../screen/clipboard.js";
|
|
5
|
+
type PixInstallTestDeps = {
|
|
6
|
+
existsSync: typeof existsSync;
|
|
7
|
+
spawn: typeof spawn;
|
|
8
|
+
isJetBrainsNerdFontInstalled: typeof isJetBrainsNerdFontInstalled;
|
|
9
|
+
installJetBrainsNerdFont: typeof installJetBrainsNerdFont;
|
|
10
|
+
clipboardSupportAvailable: typeof clipboardSupportAvailable;
|
|
11
|
+
clipboardInstallHint: typeof clipboardInstallHint;
|
|
12
|
+
};
|
|
13
|
+
export declare function setPixInstallTestDeps(overrides?: Partial<PixInstallTestDeps>): void;
|
|
1
14
|
export type PixInstallCliOptions = {
|
|
2
15
|
checkOnly: boolean;
|
|
3
16
|
help: boolean;
|
|
@@ -10,3 +23,4 @@ export declare function formatPixInstallNextSteps(homeDir?: string): string;
|
|
|
10
23
|
export declare function pixInstallUsage(): string;
|
|
11
24
|
export declare function parsePixInstallArgs(argv: readonly string[]): PixInstallCliOptions;
|
|
12
25
|
export declare function runPixInstallCli(argv?: readonly string[], context?: PixInstallCliContext): Promise<number>;
|
|
26
|
+
export {};
|
package/dist/app/cli/install.js
CHANGED
|
@@ -5,6 +5,18 @@ import { join } from "node:path";
|
|
|
5
5
|
import { FONT_FAMILY_NAME, installJetBrainsNerdFont, isJetBrainsNerdFontInstalled, } from "../terminal/nerd-font-controller.js";
|
|
6
6
|
import { clipboardInstallHint, clipboardSupportAvailable } from "../screen/clipboard.js";
|
|
7
7
|
import { getPixConfigPath } from "../../config.js";
|
|
8
|
+
const defaultPixInstallDeps = {
|
|
9
|
+
existsSync,
|
|
10
|
+
spawn,
|
|
11
|
+
isJetBrainsNerdFontInstalled,
|
|
12
|
+
installJetBrainsNerdFont,
|
|
13
|
+
clipboardSupportAvailable,
|
|
14
|
+
clipboardInstallHint,
|
|
15
|
+
};
|
|
16
|
+
let pixInstallDeps = defaultPixInstallDeps;
|
|
17
|
+
export function setPixInstallTestDeps(overrides) {
|
|
18
|
+
pixInstallDeps = overrides ? { ...defaultPixInstallDeps, ...overrides } : defaultPixInstallDeps;
|
|
19
|
+
}
|
|
8
20
|
export function formatPixInstallNextSteps(homeDir = homedir()) {
|
|
9
21
|
const pixConfigPath = getPixConfigPath(homeDir);
|
|
10
22
|
const toolsConfigPath = join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
@@ -64,7 +76,7 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
64
76
|
const env = context.env ?? process.env;
|
|
65
77
|
let failures = 0;
|
|
66
78
|
console.log("Pix install checks");
|
|
67
|
-
if (await isJetBrainsNerdFontInstalled()) {
|
|
79
|
+
if (await pixInstallDeps.isJetBrainsNerdFontInstalled()) {
|
|
68
80
|
console.log(`✓ ${FONT_FAMILY_NAME} is installed`);
|
|
69
81
|
}
|
|
70
82
|
else if (options.checkOnly) {
|
|
@@ -73,7 +85,7 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
73
85
|
}
|
|
74
86
|
else {
|
|
75
87
|
try {
|
|
76
|
-
await installJetBrainsNerdFont();
|
|
88
|
+
await pixInstallDeps.installJetBrainsNerdFont();
|
|
77
89
|
console.log(`✓ Installed ${FONT_FAMILY_NAME}`);
|
|
78
90
|
}
|
|
79
91
|
catch (error) {
|
|
@@ -100,11 +112,11 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
100
112
|
failures += 1;
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
|
-
if (await clipboardSupportAvailable(env)) {
|
|
115
|
+
if (await pixInstallDeps.clipboardSupportAvailable(env)) {
|
|
104
116
|
console.log("✓ Clipboard support is available");
|
|
105
117
|
}
|
|
106
118
|
else {
|
|
107
|
-
console.log(`! Clipboard support is missing. ${clipboardInstallHint()}`);
|
|
119
|
+
console.log(`! Clipboard support is missing. ${pixInstallDeps.clipboardInstallHint()}`);
|
|
108
120
|
if (process.platform === "linux")
|
|
109
121
|
failures += 1;
|
|
110
122
|
}
|
|
@@ -113,7 +125,7 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
113
125
|
}
|
|
114
126
|
async function resolvePiCliStatus(env) {
|
|
115
127
|
const bundledBin = env.PIX_BUNDLED_PI_BIN;
|
|
116
|
-
if (bundledBin && (existsSync(join(bundledBin, process.platform === "win32" ? "pi.cmd" : "pi")) || existsSync(join(bundledBin, "pi")))) {
|
|
128
|
+
if (bundledBin && (pixInstallDeps.existsSync(join(bundledBin, process.platform === "win32" ? "pi.cmd" : "pi")) || pixInstallDeps.existsSync(join(bundledBin, "pi")))) {
|
|
117
129
|
return { available: true, detail: "bundled with Pix" };
|
|
118
130
|
}
|
|
119
131
|
if (commandExists("pi", env))
|
|
@@ -127,11 +139,11 @@ function commandExists(command, env = process.env) {
|
|
|
127
139
|
const pathValue = env.PATH ?? "";
|
|
128
140
|
const dirs = pathValue.split(process.platform === "win32" ? ";" : ":").filter(Boolean);
|
|
129
141
|
const names = process.platform === "win32" ? [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`] : [command];
|
|
130
|
-
return dirs.some((dir) => names.some((name) => existsSync(join(dir, name))));
|
|
142
|
+
return dirs.some((dir) => names.some((name) => pixInstallDeps.existsSync(join(dir, name))));
|
|
131
143
|
}
|
|
132
144
|
async function runRequired(command, args) {
|
|
133
145
|
await new Promise((resolve, reject) => {
|
|
134
|
-
const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
146
|
+
const child = pixInstallDeps.spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
135
147
|
let stderr = "";
|
|
136
148
|
child.stderr.on("data", (chunk) => {
|
|
137
149
|
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-800);
|
|
@@ -158,9 +158,12 @@ function displayPath(pathValue, cwd) {
|
|
|
158
158
|
}
|
|
159
159
|
function formatPath(pathValue, cwd) {
|
|
160
160
|
if (!isAbsolute(pathValue))
|
|
161
|
-
return pathValue;
|
|
161
|
+
return displayPathSeparators(pathValue);
|
|
162
162
|
const rel = relative(cwd, pathValue);
|
|
163
|
-
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : basename(pathValue);
|
|
163
|
+
return displayPathSeparators(rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : basename(pathValue));
|
|
164
|
+
}
|
|
165
|
+
function displayPathSeparators(pathValue) {
|
|
166
|
+
return pathValue.replace(/\\/g, "/");
|
|
164
167
|
}
|
|
165
168
|
function unique(...groups) {
|
|
166
169
|
const seen = new Set();
|
package/dist/app/cli/update.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
type PixUpdateTestDeps = {
|
|
2
|
+
checkPixUpdate: typeof checkPixUpdate;
|
|
3
|
+
runCommand: typeof runCommand;
|
|
4
|
+
};
|
|
5
|
+
export declare function setPixUpdateTestDeps(overrides?: Partial<PixUpdateTestDeps>): void;
|
|
1
6
|
export type PixUpdateCliOptions = {
|
|
2
7
|
checkOnly: boolean;
|
|
3
8
|
force: boolean;
|
|
@@ -36,3 +41,5 @@ export declare function formatPixUpdateCheck(result: PixUpdateCheckResult): stri
|
|
|
36
41
|
export declare function formatPixStartupUpdateDialog(result: PixUpdateCheckResult): string;
|
|
37
42
|
export declare function getPixSelfUpdateCommand(packageName: string, latestVersion?: string, packageRoot?: string): PixSelfUpdateCommand | undefined;
|
|
38
43
|
export declare function runPixUpdateCli(argv?: readonly string[]): Promise<number>;
|
|
44
|
+
declare function runCommand(command: PixSelfUpdateCommand): Promise<void>;
|
|
45
|
+
export {};
|
package/dist/app/cli/update.js
CHANGED
|
@@ -5,6 +5,14 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
const DEFAULT_UPDATE_TIMEOUT_MS = 10_000;
|
|
7
7
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
8
|
+
const defaultPixUpdateDeps = {
|
|
9
|
+
checkPixUpdate,
|
|
10
|
+
runCommand,
|
|
11
|
+
};
|
|
12
|
+
let pixUpdateDeps = defaultPixUpdateDeps;
|
|
13
|
+
export function setPixUpdateTestDeps(overrides) {
|
|
14
|
+
pixUpdateDeps = overrides ? { ...defaultPixUpdateDeps, ...overrides } : defaultPixUpdateDeps;
|
|
15
|
+
}
|
|
8
16
|
export function pixUpdateUsage() {
|
|
9
17
|
return `Usage: pix update [--check] [--force]
|
|
10
18
|
|
|
@@ -157,7 +165,7 @@ export async function runPixUpdateCli(argv = process.argv.slice(2)) {
|
|
|
157
165
|
console.log(pixUpdateUsage());
|
|
158
166
|
return 0;
|
|
159
167
|
}
|
|
160
|
-
const check = await checkPixUpdate();
|
|
168
|
+
const check = await pixUpdateDeps.checkPixUpdate();
|
|
161
169
|
console.log(formatPixUpdateCheck(check));
|
|
162
170
|
if (options.checkOnly)
|
|
163
171
|
return check.status === "unavailable" ? 1 : 0;
|
|
@@ -165,14 +173,14 @@ export async function runPixUpdateCli(argv = process.argv.slice(2)) {
|
|
|
165
173
|
return 0;
|
|
166
174
|
if ((check.status === "skipped" || check.status === "unavailable") && !options.force)
|
|
167
175
|
return 1;
|
|
168
|
-
const command = getPixSelfUpdateCommand(check.packageName, check.latestVersion);
|
|
176
|
+
const command = getPixSelfUpdateCommand(check.packageName, check.latestVersion, check.packageRoot);
|
|
169
177
|
if (!command) {
|
|
170
178
|
console.error(`pix cannot self-update this installation. ${sourceCheckoutUpdateHint()}`);
|
|
171
179
|
return 1;
|
|
172
180
|
}
|
|
173
181
|
console.log(`Updating Pix with ${command.display}...`);
|
|
174
182
|
try {
|
|
175
|
-
await runCommand(command);
|
|
183
|
+
await pixUpdateDeps.runCommand(command);
|
|
176
184
|
console.log("Updated Pix. Restart any running pix sessions.");
|
|
177
185
|
return 0;
|
|
178
186
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
type ShellCommandDeps = {
|
|
3
|
+
spawn: typeof spawn;
|
|
4
|
+
waitForReturnToPix: () => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export declare function setShellCommandTestDeps(overrides: Partial<ShellCommandDeps>): () => void;
|
|
1
7
|
export type InteractiveShellCommandResult = {
|
|
2
8
|
exitCode: number | null;
|
|
3
9
|
signal: NodeJS.Signals | null;
|
|
@@ -25,3 +31,4 @@ export declare function shellCommandFromBangInput(text: string): string | undefi
|
|
|
25
31
|
export declare function runChatShellCommand(command: string, cwd: string, handlers?: ChatShellCommandHandlers): RunningChatShellCommand;
|
|
26
32
|
export declare function runInteractiveShellCommand(command: string, cwd: string): Promise<InteractiveShellCommandResult>;
|
|
27
33
|
export declare function formatShellCommandEntry(command: string, result: InteractiveShellCommandResult, prefix?: string): string;
|
|
34
|
+
export {};
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
let deps = { spawn, waitForReturnToPix: waitForReturnToPixImpl };
|
|
3
|
+
export function setShellCommandTestDeps(overrides) {
|
|
4
|
+
const previous = deps;
|
|
5
|
+
deps = { ...deps, ...overrides };
|
|
6
|
+
return () => {
|
|
7
|
+
deps = previous;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
2
10
|
export function bangShellCommandFromInput(text) {
|
|
3
11
|
const trimmed = text.trimStart();
|
|
4
12
|
if (!trimmed.startsWith("!"))
|
|
@@ -12,7 +20,7 @@ export function shellCommandFromBangInput(text) {
|
|
|
12
20
|
export function runChatShellCommand(command, cwd, handlers = {}) {
|
|
13
21
|
let child;
|
|
14
22
|
try {
|
|
15
|
-
child = spawn(command, {
|
|
23
|
+
child = deps.spawn(command, {
|
|
16
24
|
cwd,
|
|
17
25
|
env: process.env,
|
|
18
26
|
shell: shellOption(),
|
|
@@ -79,7 +87,7 @@ export async function runInteractiveShellCommand(command, cwd) {
|
|
|
79
87
|
try {
|
|
80
88
|
const result = await spawnShellCommand(command, cwd);
|
|
81
89
|
process.stdout.write(`\n[pix] ${formatInteractiveShellResult(result)}\n`);
|
|
82
|
-
await waitForReturnToPix();
|
|
90
|
+
await deps.waitForReturnToPix();
|
|
83
91
|
return result;
|
|
84
92
|
}
|
|
85
93
|
finally {
|
|
@@ -93,7 +101,7 @@ export function formatShellCommandEntry(command, result, prefix = "!") {
|
|
|
93
101
|
}
|
|
94
102
|
async function spawnShellCommand(command, cwd) {
|
|
95
103
|
try {
|
|
96
|
-
const child = spawn(command, {
|
|
104
|
+
const child = deps.spawn(command, {
|
|
97
105
|
cwd,
|
|
98
106
|
env: process.env,
|
|
99
107
|
shell: shellOption(),
|
|
@@ -160,7 +168,7 @@ function formatInteractiveShellResult(result) {
|
|
|
160
168
|
return `terminated by ${result.signal}`;
|
|
161
169
|
return `exit ${result.exitCode ?? 0}`;
|
|
162
170
|
}
|
|
163
|
-
async function
|
|
171
|
+
async function waitForReturnToPixImpl() {
|
|
164
172
|
if (!process.stdin.isTTY || !process.stdin.readable)
|
|
165
173
|
return;
|
|
166
174
|
process.stdout.write("[pix] Press Enter to return to pix…");
|
package/dist/app/icons.d.ts
CHANGED
package/dist/app/icons.js
CHANGED
|
@@ -20,6 +20,7 @@ const NERD_FONT_ICONS = {
|
|
|
20
20
|
info: "\u{f02fc}",
|
|
21
21
|
microphone: "\u{f036c}",
|
|
22
22
|
plus: "\u{f0415}",
|
|
23
|
+
pause: "\u{f03e4}",
|
|
23
24
|
record: "\u{f044a}",
|
|
24
25
|
refresh: "\u{f0450}",
|
|
25
26
|
volumeHigh: "\u{f057e}",
|
|
@@ -45,6 +46,7 @@ const FALLBACK_ICONS = {
|
|
|
45
46
|
info: "i",
|
|
46
47
|
microphone: "m",
|
|
47
48
|
plus: "+",
|
|
49
|
+
pause: "⏸",
|
|
48
50
|
record: "●",
|
|
49
51
|
refresh: "↻",
|
|
50
52
|
volumeHigh: "♪",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { createAgentSessionFromServices, createAgentSessionServices, SessionManager, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { PromptEnhancerConfig } from "../../config.js";
|
|
3
3
|
import type { InputEditor } from "../../input-editor.js";
|
|
4
4
|
import type { ToastNotifier } from "../../ui.js";
|
|
@@ -19,6 +19,12 @@ export type AppPromptEnhancerControllerHost = {
|
|
|
19
19
|
render(): void;
|
|
20
20
|
};
|
|
21
21
|
type PromptEnhanceRunner = typeof enhancePromptWithPi;
|
|
22
|
+
type PromptEnhancerPiDeps = {
|
|
23
|
+
createAgentSessionServices: typeof createAgentSessionServices;
|
|
24
|
+
createAgentSessionFromServices: typeof createAgentSessionFromServices;
|
|
25
|
+
sessionManagerInMemory: typeof SessionManager.inMemory;
|
|
26
|
+
};
|
|
27
|
+
export declare function setPromptEnhancerPiTestDeps(overrides?: Partial<PromptEnhancerPiDeps>): void;
|
|
22
28
|
type AppPromptEnhancerControllerOptions = {
|
|
23
29
|
enhancePromptWithPi?: PromptEnhanceRunner;
|
|
24
30
|
};
|
|
@@ -11,6 +11,15 @@ Do not add unsupported assumptions.
|
|
|
11
11
|
Add useful constraints, acceptance criteria, and context requests when helpful.
|
|
12
12
|
Output only the improved prompt. No commentary, no markdown fences.`;
|
|
13
13
|
const PROMPT_ENHANCER_MIN_TEXT_LENGTH = 3;
|
|
14
|
+
const defaultPromptEnhancerPiDeps = {
|
|
15
|
+
createAgentSessionServices,
|
|
16
|
+
createAgentSessionFromServices,
|
|
17
|
+
sessionManagerInMemory: SessionManager.inMemory,
|
|
18
|
+
};
|
|
19
|
+
let promptEnhancerPiDeps = defaultPromptEnhancerPiDeps;
|
|
20
|
+
export function setPromptEnhancerPiTestDeps(overrides) {
|
|
21
|
+
promptEnhancerPiDeps = overrides ? { ...defaultPromptEnhancerPiDeps, ...overrides } : defaultPromptEnhancerPiDeps;
|
|
22
|
+
}
|
|
14
23
|
export class AppPromptEnhancerController {
|
|
15
24
|
host;
|
|
16
25
|
enhancing = false;
|
|
@@ -115,7 +124,7 @@ export function promptEnhancerTextIsSufficient(text) {
|
|
|
115
124
|
}
|
|
116
125
|
async function enhancePromptWithPi(runtime, draft, config) {
|
|
117
126
|
const parsedModel = parseModelRef(config.modelRef);
|
|
118
|
-
const services = await createAgentSessionServices({
|
|
127
|
+
const services = await promptEnhancerPiDeps.createAgentSessionServices({
|
|
119
128
|
cwd: runtime.cwd,
|
|
120
129
|
agentDir: runtime.services.agentDir,
|
|
121
130
|
authStorage: runtime.services.authStorage,
|
|
@@ -135,9 +144,9 @@ async function enhancePromptWithPi(runtime, draft, config) {
|
|
|
135
144
|
if (!model) {
|
|
136
145
|
throw new Error(modelNotFoundMessage(parsedModel.provider, parsedModel.modelId, services.modelRegistry.getAll()));
|
|
137
146
|
}
|
|
138
|
-
const { session } = await createAgentSessionFromServices({
|
|
147
|
+
const { session } = await promptEnhancerPiDeps.createAgentSessionFromServices({
|
|
139
148
|
services,
|
|
140
|
-
sessionManager:
|
|
149
|
+
sessionManager: promptEnhancerPiDeps.sessionManagerInMemory(runtime.cwd),
|
|
141
150
|
model,
|
|
142
151
|
thinkingLevel: parsedModel.thinkingLevel ?? "minimal",
|
|
143
152
|
noTools: "all",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { savePixDictationLanguage, type DictationConfig, type DictationLanguageModelConfig } from "../../config.js";
|
|
2
3
|
export type VoiceLanguage = string;
|
|
3
4
|
export type VoiceInputState = "idle" | "installing" | "downloading" | "loading" | "listening";
|
|
4
5
|
export type AppVoiceControllerHost = {
|
|
@@ -8,6 +9,49 @@ export type AppVoiceControllerHost = {
|
|
|
8
9
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
9
10
|
render(): void;
|
|
10
11
|
};
|
|
12
|
+
type VoskRecognitionResult = string | {
|
|
13
|
+
text?: unknown;
|
|
14
|
+
partial?: unknown;
|
|
15
|
+
};
|
|
16
|
+
type VoskModel = {
|
|
17
|
+
free?: () => void;
|
|
18
|
+
};
|
|
19
|
+
type VoskRecognizer = {
|
|
20
|
+
acceptWaveform(buffer: Buffer): boolean;
|
|
21
|
+
partialResult?: () => VoskRecognitionResult;
|
|
22
|
+
result(): VoskRecognitionResult;
|
|
23
|
+
finalResult(): VoskRecognitionResult;
|
|
24
|
+
free?: () => void;
|
|
25
|
+
};
|
|
26
|
+
type VoskModule = {
|
|
27
|
+
Model: new (modelPath: string) => VoskModel;
|
|
28
|
+
Recognizer: new (options: {
|
|
29
|
+
model: VoskModel;
|
|
30
|
+
sampleRate: number;
|
|
31
|
+
}) => VoskRecognizer;
|
|
32
|
+
setLogLevel?: (level: number) => void;
|
|
33
|
+
};
|
|
34
|
+
type VoskLoadAttempt = {
|
|
35
|
+
ok: true;
|
|
36
|
+
module: VoskModule;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: unknown;
|
|
40
|
+
};
|
|
41
|
+
type VoiceModelDefinition = DictationLanguageModelConfig;
|
|
42
|
+
type RecorderCommand = {
|
|
43
|
+
command: string;
|
|
44
|
+
args: string[];
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
type VoiceControllerTestDeps = {
|
|
48
|
+
tryLoadVosk: typeof tryLoadVosk;
|
|
49
|
+
ensureModel: typeof ensureModel;
|
|
50
|
+
selectRecorderCommand: typeof selectRecorderCommand;
|
|
51
|
+
spawn: typeof spawn;
|
|
52
|
+
savePixDictationLanguage: typeof savePixDictationLanguage;
|
|
53
|
+
};
|
|
54
|
+
export declare function setVoiceControllerTestDeps(overrides?: Partial<VoiceControllerTestDeps>): void;
|
|
11
55
|
export declare class AppVoiceController {
|
|
12
56
|
private readonly host;
|
|
13
57
|
private readonly modelDefinitions;
|
|
@@ -52,3 +96,7 @@ export declare class AppVoiceController {
|
|
|
52
96
|
private isCurrentStart;
|
|
53
97
|
private isCurrentAudioProcess;
|
|
54
98
|
}
|
|
99
|
+
declare function ensureModel(language: VoiceLanguage, definition: VoiceModelDefinition): Promise<string>;
|
|
100
|
+
declare function tryLoadVosk(): VoskLoadAttempt;
|
|
101
|
+
declare function selectRecorderCommand(): Promise<RecorderCommand>;
|
|
102
|
+
export {};
|
|
@@ -17,6 +17,17 @@ const VOSK_PACKAGE_SPEC = "vosk@0.3.39";
|
|
|
17
17
|
const VOICE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
18
18
|
const VOICE_PARTIAL_TRANSCRIPT_THROTTLE_MS = 100;
|
|
19
19
|
let voskInstallPromise;
|
|
20
|
+
const defaultVoiceControllerDeps = {
|
|
21
|
+
tryLoadVosk,
|
|
22
|
+
ensureModel,
|
|
23
|
+
selectRecorderCommand,
|
|
24
|
+
spawn,
|
|
25
|
+
savePixDictationLanguage,
|
|
26
|
+
};
|
|
27
|
+
let voiceControllerDeps = defaultVoiceControllerDeps;
|
|
28
|
+
export function setVoiceControllerTestDeps(overrides) {
|
|
29
|
+
voiceControllerDeps = overrides ? { ...defaultVoiceControllerDeps, ...overrides } : defaultVoiceControllerDeps;
|
|
30
|
+
}
|
|
20
31
|
export class AppVoiceController {
|
|
21
32
|
host;
|
|
22
33
|
modelDefinitions;
|
|
@@ -116,7 +127,7 @@ export class AppVoiceController {
|
|
|
116
127
|
const generation = this.startGeneration + 1;
|
|
117
128
|
this.startGeneration = generation;
|
|
118
129
|
try {
|
|
119
|
-
const initialVosk = tryLoadVosk();
|
|
130
|
+
const initialVosk = voiceControllerDeps.tryLoadVosk();
|
|
120
131
|
const vosk = initialVosk.ok
|
|
121
132
|
? initialVosk.module
|
|
122
133
|
: await this.installAndLoadVosk(initialVosk.error, generation);
|
|
@@ -125,15 +136,15 @@ export class AppVoiceController {
|
|
|
125
136
|
vosk.setLogLevel?.(-1);
|
|
126
137
|
this.state = "downloading";
|
|
127
138
|
this.host.render();
|
|
128
|
-
const modelPath = await ensureModel(language, this.modelDefinition(language));
|
|
139
|
+
const modelPath = await voiceControllerDeps.ensureModel(language, this.modelDefinition(language));
|
|
129
140
|
if (!this.isCurrentStart(generation))
|
|
130
141
|
return;
|
|
131
142
|
this.state = "loading";
|
|
132
143
|
this.host.render();
|
|
133
144
|
const model = this.cachedModel(language, modelPath, vosk);
|
|
134
|
-
const recorder = await selectRecorderCommand();
|
|
145
|
+
const recorder = await voiceControllerDeps.selectRecorderCommand();
|
|
135
146
|
const recognizer = new vosk.Recognizer({ model, sampleRate: SAMPLE_RATE });
|
|
136
|
-
const audioProcess = spawn(recorder.command, recorder.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
147
|
+
const audioProcess = voiceControllerDeps.spawn(recorder.command, recorder.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
137
148
|
this.recognizer = recognizer;
|
|
138
149
|
this.audioProcess = audioProcess;
|
|
139
150
|
this.state = "listening";
|
|
@@ -172,7 +183,7 @@ export class AppVoiceController {
|
|
|
172
183
|
}
|
|
173
184
|
saveLanguageSelection(language) {
|
|
174
185
|
try {
|
|
175
|
-
savePixDictationLanguage(language);
|
|
186
|
+
voiceControllerDeps.savePixDictationLanguage(language);
|
|
176
187
|
}
|
|
177
188
|
catch (error) {
|
|
178
189
|
this.host.showToast(`Could not save voice language: ${errorMessage(error)}`, "warning");
|
|
@@ -31,18 +31,9 @@ export function renderConversationEntry(entry, width, options) {
|
|
|
31
31
|
lines.push(userLine("", userEntry.id));
|
|
32
32
|
return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
|
|
33
33
|
};
|
|
34
|
-
const queuedMessagePrefix = (queuedEntry) => {
|
|
35
|
-
const label = queuedEntry.queueSource === "sdk-steering"
|
|
36
|
-
? "steer"
|
|
37
|
-
: queuedEntry.queueSource === "sdk-follow-up"
|
|
38
|
-
? "follow"
|
|
39
|
-
: "queued";
|
|
40
|
-
return `${APP_ICONS.timerSand} ${label}:`;
|
|
41
|
-
};
|
|
42
34
|
const queuedMessageLines = (queuedEntry) => {
|
|
43
|
-
const icon = APP_ICONS.timerSand;
|
|
44
|
-
const
|
|
45
|
-
const contentLines = wrapText(`${prefix} ${queuedEntry.text}`, userContentWidth);
|
|
35
|
+
const icon = queuedEntry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
|
|
36
|
+
const contentLines = wrapText(`${icon} ${queuedEntry.text}`, userContentWidth);
|
|
46
37
|
return contentLines.map((text, index) => queuedLine(text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined));
|
|
47
38
|
};
|
|
48
39
|
switch (entry.kind) {
|
|
@@ -20,9 +20,7 @@ export class StatusLineRenderer {
|
|
|
20
20
|
const terminalBellSoundWidgetText = this.host.terminalBellSoundStatusWidgetText();
|
|
21
21
|
const promptEnhancerWidgetText = this.host.promptEnhancerStatusWidgetText();
|
|
22
22
|
const voiceWidgetText = this.host.voiceStatusWidgetText();
|
|
23
|
-
const rightWidgetParts = draftQueueButton
|
|
24
|
-
? [draftQueueButton, promptEnhancerWidgetText, userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, voiceWidgetText]
|
|
25
|
-
: [userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, promptEnhancerWidgetText, voiceWidgetText];
|
|
23
|
+
const rightWidgetParts = [draftQueueButton, promptEnhancerWidgetText, userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, voiceWidgetText];
|
|
26
24
|
const rightWidgetText = rightWidgetParts.filter((text) => text.length > 0).join(" ");
|
|
27
25
|
const rightWidgetWidth = stringDisplayWidth(rightWidgetText);
|
|
28
26
|
const leftWidth = rightWidgetWidth > 0 && contentWidth > rightWidgetWidth + 1 ? contentWidth - rightWidgetWidth - 1 : contentWidth;
|
|
@@ -77,14 +75,8 @@ export class StatusLineRenderer {
|
|
|
77
75
|
if (compactToolsWidget)
|
|
78
76
|
nextWidgetStartColumn = compactToolsWidget.endColumn + 1;
|
|
79
77
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
appendCoreStatusWidgets();
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
appendCoreStatusWidgets();
|
|
86
|
-
appendPromptEnhancerWidget();
|
|
87
|
-
}
|
|
78
|
+
appendPromptEnhancerWidget();
|
|
79
|
+
appendCoreStatusWidgets();
|
|
88
80
|
const voiceWidget = leftWidth < contentWidth && voiceWidgetText.length > 0 ? this.voiceWidgetLayout(nextWidgetStartColumn, voiceWidgetText) : undefined;
|
|
89
81
|
return {
|
|
90
82
|
details,
|
|
@@ -64,11 +64,9 @@ function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
|
|
|
64
64
|
if (maxRows <= 0 || width <= 0)
|
|
65
65
|
return [];
|
|
66
66
|
const maxDialogWidth = Math.max(1, Math.min(width - 4, 72));
|
|
67
|
-
const icon = toastKindIcon(state.kind);
|
|
68
67
|
const closeLabel = `[${APP_ICONS.close}]`;
|
|
69
68
|
const wrappedLines = dialogMessageLines(state.message, Math.max(1, maxDialogWidth - 4));
|
|
70
|
-
const
|
|
71
|
-
const requiredWidth = Math.max(16, stringDisplayWidth(` ${title} ${closeLabel} `) + 2, ...wrappedLines.map((line) => stringDisplayWidth(line) + 4));
|
|
69
|
+
const requiredWidth = Math.max(16, stringDisplayWidth(closeLabel) + 4, ...wrappedLines.map((line) => stringDisplayWidth(line) + 4));
|
|
72
70
|
const dialogWidth = Math.min(maxDialogWidth, Math.max(16, requiredWidth));
|
|
73
71
|
const bodyWidth = Math.max(1, dialogWidth - 4);
|
|
74
72
|
const bodyLines = dialogMessageLines(state.message, bodyWidth);
|
|
@@ -76,14 +74,14 @@ function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
|
|
|
76
74
|
const visibleBodyLines = bodyLines.slice(0, bodyRows);
|
|
77
75
|
const includeBottom = maxRows > 1;
|
|
78
76
|
const dialogRows = [
|
|
79
|
-
dialogTopLine(
|
|
77
|
+
dialogTopLine(closeLabel, dialogWidth),
|
|
80
78
|
...visibleBodyLines.map((line) => `│ ${padOrTrimPlain(line, bodyWidth)} │`),
|
|
81
79
|
...(includeBottom ? [`╰${"─".repeat(Math.max(0, dialogWidth - 2))}╯`] : []),
|
|
82
80
|
].slice(0, maxRows);
|
|
83
81
|
const leftWidth = Math.max(0, width - dialogWidth - 2);
|
|
84
82
|
const column = leftWidth + 1;
|
|
85
83
|
const style = toastKindStyle(state.kind, theme);
|
|
86
|
-
const closeStartColumn = column + 1 + dialogTopCloseOffset(
|
|
84
|
+
const closeStartColumn = column + 1 + dialogTopCloseOffset(closeLabel, dialogWidth);
|
|
87
85
|
const closeEndColumn = closeStartColumn + stringDisplayWidth(closeLabel);
|
|
88
86
|
return dialogRows.map((text, index) => ({
|
|
89
87
|
id: state.id,
|
|
@@ -101,18 +99,17 @@ function dialogMessageLines(message, maxWidth) {
|
|
|
101
99
|
const lines = sanitizeText(message).split("\n").flatMap((line) => wrapDisplayLine(line, safeMaxWidth));
|
|
102
100
|
return lines.length > 0 ? lines : [""];
|
|
103
101
|
}
|
|
104
|
-
function dialogTopLine(
|
|
102
|
+
function dialogTopLine(closeLabel, width) {
|
|
105
103
|
const innerWidth = Math.max(0, width - 2);
|
|
106
|
-
const closeOffset = dialogTopCloseOffset(
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
return `╭${
|
|
104
|
+
const closeOffset = dialogTopCloseOffset(closeLabel, width);
|
|
105
|
+
const closeWidth = stringDisplayWidth(closeLabel);
|
|
106
|
+
const rightWidth = Math.max(0, innerWidth - closeOffset - closeWidth);
|
|
107
|
+
return `╭${"─".repeat(closeOffset)}${padOrTrimPlain(closeLabel, Math.min(closeWidth, innerWidth - closeOffset))}${"─".repeat(rightWidth)}╮`;
|
|
110
108
|
}
|
|
111
|
-
function dialogTopCloseOffset(
|
|
109
|
+
function dialogTopCloseOffset(closeLabel, width) {
|
|
112
110
|
const innerWidth = Math.max(0, width - 2);
|
|
113
|
-
const leftLabel = ` ${title} `;
|
|
114
111
|
const closeWidth = stringDisplayWidth(closeLabel);
|
|
115
|
-
return Math.max(
|
|
112
|
+
return Math.max(0, innerWidth - closeWidth - 1);
|
|
116
113
|
}
|
|
117
114
|
function toastKindIcon(kind) {
|
|
118
115
|
switch (kind) {
|
|
@@ -6,6 +6,7 @@ import type { ToolBodyLineStyle, ToolHeaderSegment } from "../../tool-renderers/
|
|
|
6
6
|
export type ToolBlockEntry = {
|
|
7
7
|
id: string;
|
|
8
8
|
toolName: string;
|
|
9
|
+
headerLabel?: string | undefined;
|
|
9
10
|
headerArgs?: string | undefined;
|
|
10
11
|
headerArgsSegments?: readonly ToolHeaderSegment[] | undefined;
|
|
11
12
|
bodyLineStyles?: readonly ToolBodyLineStyle[] | undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveColor } from "../../config.js";
|
|
2
2
|
import { expandTabs, sliceByDisplayWidth, stringDisplayWidth, wrapDisplayLineByWords } from "../../terminal-width.js";
|
|
3
3
|
import { alertIconPrefixLength, hasToolLspDiagnosticsAfterMutation, lspDiagnosticSeverityForLine, sanitizeText, toolStatusIcon, toolStatusIconColor, wrapLine } from "./render-text.js";
|
|
4
|
-
const TRUNCATED_PREVIEW_MARKER = "
|
|
4
|
+
const TRUNCATED_PREVIEW_MARKER = "▶ ";
|
|
5
5
|
export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
6
6
|
if (rule.hidden)
|
|
7
7
|
return [];
|
|
@@ -10,7 +10,8 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
10
10
|
const stateIcon = toolStatusIcon(entry);
|
|
11
11
|
const toolColor = resolveColor(rule.color, colors);
|
|
12
12
|
const toolOutputColor = colors.statusForeground;
|
|
13
|
-
const
|
|
13
|
+
const headerLabel = entry.headerLabel ?? entry.toolName;
|
|
14
|
+
const headerPrefix = headerLabel ? `${stateIcon} ${headerLabel}` : stateIcon;
|
|
14
15
|
const headerArgs = formatToolHeaderArgs(entry.headerArgs);
|
|
15
16
|
const headerArgsWidth = width - stringDisplayWidth(headerPrefix) - 1;
|
|
16
17
|
const clippedHeaderArgs = headerArgsWidth > 0 ? sliceByDisplayWidth(headerArgs, headerArgsWidth) : "";
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { commandExists, runProcess } from "../process.js";
|
|
2
|
+
type ClipboardDeps = {
|
|
3
|
+
commandExists: typeof commandExists;
|
|
4
|
+
requireResolve(specifier: string): string;
|
|
5
|
+
runProcess: typeof runProcess;
|
|
6
|
+
stdout: Pick<NodeJS.WriteStream, "destroyed" | "isTTY" | "write">;
|
|
7
|
+
};
|
|
8
|
+
export declare function setClipboardTestDeps(overrides: Partial<ClipboardDeps>): () => void;
|
|
1
9
|
export declare function copyTextToClipboard(text: string): Promise<void>;
|
|
2
10
|
export declare function clipboardSupportAvailable(env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
3
11
|
export declare function clipboardInstallHint(): string;
|
|
4
12
|
export declare function osc52ClipboardSequence(text: string, env?: NodeJS.ProcessEnv): string;
|
|
13
|
+
export {};
|