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.
Files changed (45) hide show
  1. package/dist/app/app.js +6 -4
  2. package/dist/app/cli/install.d.ts +14 -0
  3. package/dist/app/cli/install.js +19 -7
  4. package/dist/app/cli/startup-info.js +5 -2
  5. package/dist/app/cli/update.d.ts +7 -0
  6. package/dist/app/cli/update.js +11 -3
  7. package/dist/app/commands/shell-command.d.ts +7 -0
  8. package/dist/app/commands/shell-command.js +12 -4
  9. package/dist/app/icons.d.ts +1 -0
  10. package/dist/app/icons.js +2 -0
  11. package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
  12. package/dist/app/input/prompt-enhancer-controller.js +12 -3
  13. package/dist/app/input/voice-controller.d.ts +49 -1
  14. package/dist/app/input/voice-controller.js +16 -5
  15. package/dist/app/rendering/conversation-entry-renderer.js +2 -11
  16. package/dist/app/rendering/status-line-renderer.js +3 -11
  17. package/dist/app/rendering/toast-renderer.js +10 -13
  18. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  19. package/dist/app/rendering/tool-block-renderer.js +3 -2
  20. package/dist/app/screen/clipboard.d.ts +9 -0
  21. package/dist/app/screen/clipboard.js +19 -6
  22. package/dist/app/screen/file-link-opener.d.ts +8 -0
  23. package/dist/app/screen/file-link-opener.js +11 -3
  24. package/dist/app/screen/file-links.js +3 -3
  25. package/dist/app/screen/image-opener.d.ts +12 -0
  26. package/dist/app/screen/image-opener.js +13 -5
  27. package/dist/app/session/queued-message-controller.js +5 -1
  28. package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
  29. package/dist/app/terminal/nerd-font-controller.js +20 -12
  30. package/dist/default-pix-config.js +7 -6
  31. package/dist/schemas/index.d.ts +5 -0
  32. package/dist/schemas/index.js +5 -0
  33. package/dist/schemas/pi-tools-suite-schema.d.ts +177 -0
  34. package/dist/schemas/pi-tools-suite-schema.js +218 -0
  35. package/dist/schemas/pix-schema.d.ts +65 -0
  36. package/dist/schemas/pix-schema.js +91 -0
  37. package/dist/terminal-width.js +73 -56
  38. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
  39. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +1 -0
  40. package/external/pi-tools-suite/src/todo/index.ts +4 -2
  41. package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
  42. package/external/pi-tools-suite/src/todo/todo.ts +2 -6
  43. package/package.json +12 -3
  44. package/schemas/pi-tools-suite.json +881 -0
  45. 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" || !entry.expanded)
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 = false;
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 {};
@@ -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();
@@ -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 {};
@@ -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 waitForReturnToPix() {
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…");
@@ -14,6 +14,7 @@ declare const NERD_FONT_ICONS: {
14
14
  readonly info: "󰋼";
15
15
  readonly microphone: "󰍬";
16
16
  readonly plus: "󰐕";
17
+ readonly pause: "󰏤";
17
18
  readonly record: "󰑊";
18
19
  readonly refresh: "󰑐";
19
20
  readonly volumeHigh: "󰕾";
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: SessionManager.inMemory(runtime.cwd),
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 { type DictationConfig } from "../../config.js";
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 prefix = queuedMessagePrefix(queuedEntry);
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.length > 0
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
- if (draftQueueWidget) {
81
- appendPromptEnhancerWidget();
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 title = `${icon} Dialog`;
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(title, closeLabel, dialogWidth),
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(title, closeLabel, dialogWidth);
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(title, closeLabel, width) {
102
+ function dialogTopLine(closeLabel, width) {
105
103
  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)}╮`;
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(title, closeLabel, width) {
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(stringDisplayWidth(leftLabel), innerWidth - closeWidth - 1);
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 headerPrefix = `${stateIcon} ${entry.toolName}`;
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 {};