pi-ui-extend 0.1.9 → 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 (121) hide show
  1. package/README.md +23 -2
  2. package/dist/app/app.d.ts +4 -0
  3. package/dist/app/app.js +76 -7
  4. package/dist/app/cli/install.d.ts +16 -0
  5. package/dist/app/cli/install.js +34 -7
  6. package/dist/app/cli/startup-info.js +5 -2
  7. package/dist/app/cli/update.d.ts +7 -0
  8. package/dist/app/cli/update.js +11 -3
  9. package/dist/app/commands/command-controller.js +4 -0
  10. package/dist/app/commands/command-host.d.ts +4 -0
  11. package/dist/app/commands/command-model-actions.d.ts +5 -0
  12. package/dist/app/commands/command-model-actions.js +104 -0
  13. package/dist/app/commands/command-navigation-actions.d.ts +6 -1
  14. package/dist/app/commands/command-navigation-actions.js +37 -14
  15. package/dist/app/commands/command-registry.d.ts +4 -0
  16. package/dist/app/commands/command-registry.js +32 -0
  17. package/dist/app/commands/command-session-actions.d.ts +1 -0
  18. package/dist/app/commands/command-session-actions.js +15 -5
  19. package/dist/app/commands/shell-command.d.ts +7 -0
  20. package/dist/app/commands/shell-command.js +12 -4
  21. package/dist/app/commands/shell-controller.d.ts +1 -0
  22. package/dist/app/commands/shell-controller.js +1 -1
  23. package/dist/app/constants.d.ts +1 -1
  24. package/dist/app/constants.js +1 -1
  25. package/dist/app/icons.d.ts +1 -0
  26. package/dist/app/icons.js +3 -1
  27. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  28. package/dist/app/input/autocomplete-controller.js +352 -0
  29. package/dist/app/input/input-action-controller.d.ts +1 -0
  30. package/dist/app/input/input-action-controller.js +21 -0
  31. package/dist/app/input/input-controller.d.ts +1 -0
  32. package/dist/app/input/input-controller.js +2 -0
  33. package/dist/app/input/input-paste-handler.d.ts +1 -0
  34. package/dist/app/input/input-paste-handler.js +22 -18
  35. package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
  36. package/dist/app/input/prompt-enhancer-controller.js +12 -3
  37. package/dist/app/input/voice-controller.d.ts +51 -1
  38. package/dist/app/input/voice-controller.js +42 -19
  39. package/dist/app/model/model-usage-status.d.ts +9 -0
  40. package/dist/app/model/model-usage-status.js +124 -34
  41. package/dist/app/popup/popup-action-controller.js +1 -1
  42. package/dist/app/process.d.ts +17 -0
  43. package/dist/app/process.js +68 -0
  44. package/dist/app/rendering/conversation-entry-renderer.js +8 -6
  45. package/dist/app/rendering/conversation-tool-renderer.js +3 -2
  46. package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
  47. package/dist/app/rendering/editor-layout-renderer.js +11 -1
  48. package/dist/app/rendering/message-content.js +65 -7
  49. package/dist/app/rendering/render-controller.js +6 -1
  50. package/dist/app/rendering/render-text.d.ts +3 -0
  51. package/dist/app/rendering/render-text.js +51 -3
  52. package/dist/app/rendering/status-line-renderer.d.ts +5 -1
  53. package/dist/app/rendering/status-line-renderer.js +61 -25
  54. package/dist/app/rendering/toast-renderer.js +10 -13
  55. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  56. package/dist/app/rendering/tool-block-renderer.js +16 -33
  57. package/dist/app/runtime.d.ts +6 -1
  58. package/dist/app/runtime.js +35 -2
  59. package/dist/app/screen/clipboard.d.ts +11 -2
  60. package/dist/app/screen/clipboard.js +29 -21
  61. package/dist/app/screen/file-link-opener.d.ts +8 -0
  62. package/dist/app/screen/file-link-opener.js +11 -3
  63. package/dist/app/screen/file-links.js +3 -3
  64. package/dist/app/screen/image-opener.d.ts +12 -0
  65. package/dist/app/screen/image-opener.js +13 -5
  66. package/dist/app/screen/mouse-controller.d.ts +5 -2
  67. package/dist/app/screen/mouse-controller.js +16 -1
  68. package/dist/app/screen/screen-styler.d.ts +4 -1
  69. package/dist/app/screen/screen-styler.js +3 -2
  70. package/dist/app/screen/status-controller.d.ts +3 -0
  71. package/dist/app/screen/status-controller.js +23 -8
  72. package/dist/app/session/queued-message-controller.d.ts +7 -1
  73. package/dist/app/session/queued-message-controller.js +36 -21
  74. package/dist/app/session/resume-session-loader.d.ts +15 -0
  75. package/dist/app/session/resume-session-loader.js +204 -0
  76. package/dist/app/session/session-event-controller.d.ts +5 -1
  77. package/dist/app/session/session-event-controller.js +72 -5
  78. package/dist/app/session/session-history.js +4 -3
  79. package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
  80. package/dist/app/session/session-lifecycle-controller.js +9 -1
  81. package/dist/app/session/tabs-controller.d.ts +10 -1
  82. package/dist/app/session/tabs-controller.js +101 -5
  83. package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
  84. package/dist/app/terminal/nerd-font-controller.js +30 -23
  85. package/dist/app/terminal/terminal-controller.d.ts +1 -0
  86. package/dist/app/terminal/terminal-controller.js +1 -0
  87. package/dist/app/types.d.ts +14 -0
  88. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
  89. package/dist/app/workspace/workspace-actions-controller.js +3 -3
  90. package/dist/app/workspace/workspace-undo.d.ts +1 -1
  91. package/dist/app/workspace/workspace-undo.js +22 -20
  92. package/dist/config.d.ts +27 -0
  93. package/dist/config.js +174 -1
  94. package/dist/default-pix-config.js +39 -353
  95. package/dist/input-editor.d.ts +7 -1
  96. package/dist/input-editor.js +47 -6
  97. package/dist/markdown-format.d.ts +1 -0
  98. package/dist/markdown-format.js +26 -1
  99. package/dist/schemas/index.d.ts +5 -0
  100. package/dist/schemas/index.js +5 -0
  101. package/dist/schemas/pi-tools-suite-schema.d.ts +177 -0
  102. package/dist/schemas/pi-tools-suite-schema.js +218 -0
  103. package/dist/schemas/pix-schema.d.ts +65 -0
  104. package/dist/schemas/pix-schema.js +91 -0
  105. package/dist/terminal-width.js +73 -56
  106. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
  107. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  108. package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
  109. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +46 -195
  110. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  111. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  112. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  113. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  114. package/external/pi-tools-suite/src/todo/index.ts +4 -2
  115. package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
  116. package/external/pi-tools-suite/src/todo/todo.ts +2 -6
  117. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  118. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  119. package/package.json +12 -3
  120. package/schemas/pi-tools-suite.json +881 -0
  121. package/schemas/pix.json +298 -0
@@ -1,4 +1,13 @@
1
- export declare function copyTextToClipboard(text: string): void;
2
- export declare function clipboardSupportAvailable(env?: NodeJS.ProcessEnv): boolean;
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;
9
+ export declare function copyTextToClipboard(text: string): Promise<void>;
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 {};
@@ -1,22 +1,37 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { createRequire } from "node:module";
2
+ import { commandExists, runProcess } from "../process.js";
3
3
  const require = createRequire(import.meta.url);
4
- export function copyTextToClipboard(text) {
4
+ let deps = {
5
+ commandExists,
6
+ requireResolve: (specifier) => require.resolve(specifier),
7
+ runProcess,
8
+ stdout: process.stdout,
9
+ };
10
+ export function setClipboardTestDeps(overrides) {
11
+ const previous = deps;
12
+ deps = { ...deps, ...overrides };
13
+ return () => {
14
+ deps = previous;
15
+ };
16
+ }
17
+ export async function copyTextToClipboard(text) {
5
18
  const commands = clipboardCommands();
6
19
  for (const [command, args] of commands) {
7
- const result = spawnSync(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
20
+ const result = await deps.runProcess(command, args, { input: text, maxBufferBytes: 1024 });
8
21
  if (!result.error && result.status === 0)
9
22
  return;
10
23
  }
11
- if (copyWithNativeClipboard(text))
24
+ if (await copyWithNativeClipboard(text))
12
25
  return;
13
26
  if (copyWithOsc52(text))
14
27
  return;
15
28
  throw new Error(`No clipboard command found. ${clipboardInstallHint()}`);
16
29
  }
17
- export function clipboardSupportAvailable(env = process.env) {
18
- if (clipboardCommands().some(([command]) => commandExists(command, env)))
19
- return true;
30
+ export async function clipboardSupportAvailable(env = process.env) {
31
+ for (const [command] of clipboardCommands()) {
32
+ if (await deps.commandExists(command, env))
33
+ return true;
34
+ }
20
35
  return resolveNativeClipboardEntrypoint() !== undefined;
21
36
  }
22
37
  export function clipboardInstallHint() {
@@ -44,7 +59,7 @@ function clipboardCommands() {
44
59
  ];
45
60
  }
46
61
  }
47
- function copyWithNativeClipboard(text) {
62
+ async function copyWithNativeClipboard(text) {
48
63
  const entrypoint = resolveNativeClipboardEntrypoint();
49
64
  if (!entrypoint)
50
65
  return false;
@@ -55,17 +70,17 @@ function copyWithNativeClipboard(text) {
55
70
  const clipboard = require(${JSON.stringify(entrypoint)});
56
71
  await clipboard.setText(readFileSync(0, "utf8"));
57
72
  `;
58
- const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
73
+ const result = await deps.runProcess(process.execPath, ["--input-type=module", "-e", script], {
59
74
  input: text,
60
- stdio: ["pipe", "ignore", "ignore"],
61
- timeout: 3_000,
75
+ timeoutMs: 3_000,
76
+ maxBufferBytes: 1024,
62
77
  });
63
78
  return !result.error && result.status === 0;
64
79
  }
65
80
  function copyWithOsc52(text) {
66
- if (process.stdout.destroyed || (!process.stdout.isTTY && !process.env.TMUX && !process.env.STY))
81
+ if (deps.stdout.destroyed || (!deps.stdout.isTTY && !process.env.TMUX && !process.env.STY))
67
82
  return false;
68
- process.stdout.write(osc52ClipboardSequence(text));
83
+ deps.stdout.write(osc52ClipboardSequence(text));
69
84
  return true;
70
85
  }
71
86
  export function osc52ClipboardSequence(text, env = process.env) {
@@ -78,16 +93,9 @@ export function osc52ClipboardSequence(text, env = process.env) {
78
93
  }
79
94
  function resolveNativeClipboardEntrypoint() {
80
95
  try {
81
- return require.resolve("@mariozechner/clipboard");
96
+ return deps.requireResolve("@mariozechner/clipboard");
82
97
  }
83
98
  catch {
84
99
  return undefined;
85
100
  }
86
101
  }
87
- function commandExists(command, env) {
88
- const names = process.platform === "win32" ? [command, command.replace(/\.exe$/iu, ".cmd"), command.replace(/\.exe$/iu, ".bat")] : [command];
89
- return names.some((name) => spawnSync(process.platform === "win32" ? "where" : "sh", process.platform === "win32" ? [name] : ["-lc", `command -v ${shellQuote(name)}`], { env, stdio: "ignore" }).status === 0);
90
- }
91
- function shellQuote(value) {
92
- return `'${value.replaceAll("'", `'\\''`)}'`;
93
- }
@@ -1,2 +1,10 @@
1
+ import { existsSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
1
3
  import type { RenderedLink } from "./file-links.js";
4
+ type FileLinkOpenerDeps = {
5
+ existsSync: typeof existsSync;
6
+ spawn: typeof spawn;
7
+ };
8
+ export declare function setFileLinkOpenerTestDeps(overrides: Partial<FileLinkOpenerDeps>): () => void;
2
9
  export declare function openFileLink(link: RenderedLink): boolean;
10
+ export {};
@@ -2,6 +2,14 @@ import { existsSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ let deps = { existsSync, spawn };
6
+ export function setFileLinkOpenerTestDeps(overrides) {
7
+ const previous = deps;
8
+ deps = { ...deps, ...overrides };
9
+ return () => {
10
+ deps = previous;
11
+ };
12
+ }
5
13
  export function openFileLink(link) {
6
14
  const filePath = link.filePath ?? filePathFromUrl(link.url);
7
15
  if (!filePath)
@@ -37,7 +45,7 @@ function zedCommandCandidates() {
37
45
  }
38
46
  function trySpawnCandidates(candidates, args) {
39
47
  for (const command of candidates) {
40
- if (command.includes("/") && !existsSync(command))
48
+ if (command.includes("/") && !deps.existsSync(command))
41
49
  continue;
42
50
  if (!command.includes("/") && !commandOnPath(command))
43
51
  continue;
@@ -51,11 +59,11 @@ function commandOnPath(command) {
51
59
  const extensions = process.platform === "win32"
52
60
  ? (process.env.PATHEXT?.split(";") ?? [".EXE", ".CMD", ".BAT", ".COM"])
53
61
  : [""];
54
- return pathEntries.some((entry) => extensions.some((extension) => existsSync(join(entry, `${command}${extension}`))));
62
+ return pathEntries.some((entry) => extensions.some((extension) => deps.existsSync(join(entry, `${command}${extension}`))));
55
63
  }
56
64
  function spawnDetached(command, args) {
57
65
  try {
58
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
66
+ const child = deps.spawn(command, args, { detached: true, stdio: "ignore" });
59
67
  child.on("error", () => { });
60
68
  child.unref();
61
69
  return true;
@@ -2,11 +2,11 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- const FILE_PATH_CANDIDATE = /(?<![\p{L}\p{N}_:])((?:file:\/\/\/|~\/|\.{1,2}\/|\/|[A-Za-z0-9_.@-]+\/)[^\s"'`<>]*)/gu;
5
+ const FILE_PATH_CANDIDATE = /(?<![\p{L}\p{N}_:])((?:file:\/\/\/|~[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|[\\/]|[A-Za-z0-9_.@-]+[\\/])[^\s"'`<>]*)/gu;
6
6
  const TRAILING_PUNCTUATION = new Set([".", ",", ";", ")", "]", "}"]);
7
7
  export function detectFileLinks(text, cwd) {
8
8
  const links = [];
9
- if (!text.includes("/"))
9
+ if (!text.includes("/") && !text.includes("\\"))
10
10
  return links;
11
11
  for (const match of text.matchAll(FILE_PATH_CANDIDATE)) {
12
12
  const raw = match[1];
@@ -89,7 +89,7 @@ function resolveLocalPath(pathText, cwd) {
89
89
  return undefined;
90
90
  }
91
91
  }
92
- if (pathText.startsWith("~/"))
92
+ if (pathText.startsWith("~/") || pathText.startsWith("~\\"))
93
93
  return resolve(homedir(), pathText.slice(2));
94
94
  if (isAbsolute(pathText))
95
95
  return pathText;
@@ -1,2 +1,14 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
1
4
  import type { ImageContent } from "../../input-editor.js";
5
+ type ImageOpenerDeps = {
6
+ existsSync: typeof existsSync;
7
+ mkdirSync: typeof mkdirSync;
8
+ spawn: typeof spawn;
9
+ tmpdir: typeof tmpdir;
10
+ writeFileSync: typeof writeFileSync;
11
+ };
12
+ export declare function setImageOpenerTestDeps(overrides: Partial<ImageOpenerDeps>): () => void;
2
13
  export declare function openImageContent(image: ImageContent): boolean;
14
+ export {};
@@ -3,6 +3,14 @@ import { createHash } from "node:crypto";
3
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
+ let deps = { existsSync, mkdirSync, spawn, tmpdir, writeFileSync };
7
+ export function setImageOpenerTestDeps(overrides) {
8
+ const previous = deps;
9
+ deps = { ...deps, ...overrides };
10
+ return () => {
11
+ deps = previous;
12
+ };
13
+ }
6
14
  export function openImageContent(image) {
7
15
  const filePath = writeImageTempFile(image);
8
16
  if (!filePath)
@@ -14,12 +22,12 @@ function writeImageTempFile(image) {
14
22
  const data = Buffer.from(image.data, "base64");
15
23
  if (data.length === 0)
16
24
  return undefined;
17
- const dir = join(tmpdir(), "pix-image-open");
18
- mkdirSync(dir, { recursive: true });
25
+ const dir = join(deps.tmpdir(), "pix-image-open");
26
+ deps.mkdirSync(dir, { recursive: true });
19
27
  const hash = createHash("sha256").update(image.mimeType).update("\0").update(data).digest("hex").slice(0, 24);
20
28
  const filePath = join(dir, `${hash}${imageExtension(image.mimeType)}`);
21
- if (!existsSync(filePath))
22
- writeFileSync(filePath, data, { flag: "wx" });
29
+ if (!deps.existsSync(filePath))
30
+ deps.writeFileSync(filePath, data, { flag: "wx" });
23
31
  return filePath;
24
32
  }
25
33
  catch {
@@ -53,7 +61,7 @@ function openPathWithSystemViewer(filePath) {
53
61
  }
54
62
  function spawnDetached(command, args) {
55
63
  try {
56
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
64
+ const child = deps.spawn(command, args, { detached: true, stdio: "ignore" });
57
65
  child.on("error", () => { });
58
66
  child.unref();
59
67
  return true;
@@ -6,7 +6,7 @@ import type { ToastEntry, ToastVariant } from "../../ui.js";
6
6
  import type { AppPopupActionController } from "../popup/popup-action-controller.js";
7
7
  import type { AppPopupMenuController } from "../popup/popup-menu-controller.js";
8
8
  import type { AppScrollController } from "./scroll-controller.js";
9
- import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
9
+ import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
10
10
  import { type RenderedLink } from "./file-links.js";
11
11
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
12
  type ClickFlash = {
@@ -56,10 +56,11 @@ export type AppMouseControllerHost = {
56
56
  }): void;
57
57
  dismissToast(toastId: number): void;
58
58
  refreshModelUsageStatus(): void | Promise<void>;
59
+ queueInputFromStatus?(): void | Promise<void>;
59
60
  toggleAllThinkingExpanded?(): void;
60
61
  toggleSuperCompactTools?(): void;
61
62
  toggleTerminalBellSound?(): void;
62
- copyTextToClipboard?(text: string): void;
63
+ copyTextToClipboard?(text: string): void | Promise<void>;
63
64
  handleExtensionInputMouse(event: MouseEvent & {
64
65
  localRow: number;
65
66
  localColumn: number;
@@ -100,6 +101,7 @@ export declare class AppMouseController {
100
101
  statusContextTarget: StatusContextTarget | undefined;
101
102
  statusModelUsageTarget: StatusModelUsageTarget | undefined;
102
103
  statusUserJumpTarget: StatusUserJumpTarget | undefined;
104
+ statusDraftQueueTarget: StatusDraftQueueTarget | undefined;
103
105
  statusThinkingExpandTarget: StatusThinkingExpandTarget | undefined;
104
106
  statusCompactToolsTarget: StatusCompactToolsTarget | undefined;
105
107
  statusTerminalBellSoundTarget: StatusTerminalBellSoundTarget | undefined;
@@ -153,6 +155,7 @@ export declare class AppMouseController {
153
155
  private handleStatusContextClick;
154
156
  private handleStatusModelUsageClick;
155
157
  private handleStatusUserJumpClick;
158
+ private handleStatusDraftQueueClick;
156
159
  private handleStatusThinkingExpandClick;
157
160
  private handleStatusCompactToolsClick;
158
161
  private handleStatusTerminalBellSoundClick;
@@ -24,6 +24,7 @@ export class AppMouseController {
24
24
  statusContextTarget;
25
25
  statusModelUsageTarget;
26
26
  statusUserJumpTarget;
27
+ statusDraftQueueTarget;
27
28
  statusThinkingExpandTarget;
28
29
  statusCompactToolsTarget;
29
30
  statusTerminalBellSoundTarget;
@@ -75,6 +76,8 @@ export class AppMouseController {
75
76
  return;
76
77
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusModelUsageClick(event)))
77
78
  return;
79
+ if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusDraftQueueClick(event)))
80
+ return;
78
81
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusUserJumpClick(event)))
79
82
  return;
80
83
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusThinkingExpandClick(event)))
@@ -286,6 +289,7 @@ export class AppMouseController {
286
289
  this.statusThinkingTarget,
287
290
  this.statusContextTarget,
288
291
  this.statusModelUsageTarget,
292
+ this.statusDraftQueueTarget,
289
293
  this.statusUserJumpTarget,
290
294
  this.statusThinkingExpandTarget,
291
295
  this.statusCompactToolsTarget,
@@ -509,6 +513,15 @@ export class AppMouseController {
509
513
  this.host.render();
510
514
  return true;
511
515
  }
516
+ handleStatusDraftQueueClick(event) {
517
+ const target = this.statusDraftQueueTarget;
518
+ if (!target)
519
+ return false;
520
+ if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
521
+ return false;
522
+ void this.host.queueInputFromStatus?.();
523
+ return true;
524
+ }
512
525
  handleStatusThinkingExpandClick(event) {
513
526
  const target = this.statusThinkingExpandTarget;
514
527
  if (!target)
@@ -769,7 +782,9 @@ export class AppMouseController {
769
782
  return this.getSelectedScreenText(selection.anchor, selection.current);
770
783
  }
771
784
  copyTextToClipboard(text) {
772
- (this.host.copyTextToClipboard ?? copyTextToClipboard)(text);
785
+ void Promise.resolve((this.host.copyTextToClipboard ?? copyTextToClipboard)(text)).catch((error) => {
786
+ this.host.showToast(error instanceof Error ? error.message : String(error), "error");
787
+ });
773
788
  }
774
789
  getSelectedScreenText(anchor, current) {
775
790
  const range = orderedSelection(anchor, current);
@@ -24,7 +24,10 @@ export declare class ScreenStyler {
24
24
  styleInputLine(row: number, text: string, tagSpans: readonly {
25
25
  start: number;
26
26
  end: number;
27
- }[] | undefined, width: number, tagColor: string, frameColor?: string): string;
27
+ }[] | undefined, suggestionSpans: readonly {
28
+ start: number;
29
+ end: number;
30
+ }[] | undefined, width: number, tagColor: string, suggestionColor: string, frameColor?: string): string;
28
31
  private styleAnsiLine;
29
32
  selectionRangeForRow(row: number, width: number): {
30
33
  startIndex: number;
@@ -72,14 +72,14 @@ export class ScreenStyler {
72
72
  colorize(after, options),
73
73
  ].join("");
74
74
  }
75
- styleInputLine(row, text, tagSpans, width, tagColor, frameColor) {
75
+ styleInputLine(row, text, tagSpans, suggestionSpans, width, tagColor, suggestionColor, frameColor) {
76
76
  const colors = this.host.theme.colors;
77
77
  const baseOptions = { foreground: colors.inputForeground };
78
78
  if (this.selectionRangeForRow(row, width))
79
79
  return this.styleLine(row, text, width, baseOptions);
80
80
  const plain = padOrTrimPlain(text, width);
81
81
  const frameSpans = inputFrameSpans(plain, width, frameColor);
82
- if ((!tagSpans || tagSpans.length === 0) && frameSpans.length === 0) {
82
+ if ((!tagSpans || tagSpans.length === 0) && (!suggestionSpans || suggestionSpans.length === 0) && frameSpans.length === 0) {
83
83
  return hasAnsi(plain) ? this.styleAnsiLine(plain, baseOptions) : colorize(plain, baseOptions);
84
84
  }
85
85
  const chunks = [];
@@ -88,6 +88,7 @@ export class ScreenStyler {
88
88
  const spans = [
89
89
  ...frameSpans,
90
90
  ...(tagSpans ?? []).map((span) => ({ ...span, foreground: tagColor, bold: true })),
91
+ ...(suggestionSpans ?? []).map((span) => ({ ...span, foreground: suggestionColor })),
91
92
  ].sort((a, b) => a.start - b.start || a.end - b.end);
92
93
  for (const span of spans) {
93
94
  const start = Math.max(offset, Math.min(endOffset, span.start));
@@ -7,12 +7,14 @@ export type AppStatusControllerHost = {
7
7
  readonly theme: Theme;
8
8
  readonly blinkController: AppBlinkController;
9
9
  runtimeSession(): AgentSession | undefined;
10
+ render(): void;
10
11
  };
11
12
  export declare class AppStatusController {
12
13
  private readonly host;
13
14
  private status;
14
15
  private statusFollowsSession;
15
16
  private gitBranchCache;
17
+ private gitBranchLookupInFlight;
16
18
  sessionActivity: SessionActivity;
17
19
  get statusDotBright(): boolean;
18
20
  constructor(host: AppStatusControllerHost);
@@ -31,5 +33,6 @@ export declare class AppStatusController {
31
33
  roundedContextUsagePercent(session: AgentSession): number | undefined;
32
34
  contextUsagePercentColor(percent: number): string;
33
35
  private currentGitBranchName;
36
+ private refreshGitBranchName;
34
37
  private startStatusBlink;
35
38
  }
@@ -1,12 +1,13 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { basename } from "node:path";
3
2
  import { GIT_BRANCH_CACHE_MS } from "../constants.js";
3
+ import { runProcess } from "../process.js";
4
4
  const STATUS_DOT_BLINK_KEY = "status-dot";
5
5
  export class AppStatusController {
6
6
  host;
7
7
  status = "starting";
8
8
  statusFollowsSession = false;
9
9
  gitBranchCache;
10
+ gitBranchLookupInFlight = false;
10
11
  sessionActivity = "idle";
11
12
  get statusDotBright() {
12
13
  return this.host.blinkController.visible(STATUS_DOT_BLINK_KEY, false);
@@ -87,13 +88,27 @@ export class AppStatusController {
87
88
  if (this.gitBranchCache && now - this.gitBranchCache.checkedAt < GIT_BRANCH_CACHE_MS) {
88
89
  return this.gitBranchCache.branch;
89
90
  }
90
- const result = spawnSync("git", ["-C", this.host.cwd, "branch", "--show-current"], {
91
- encoding: "utf8",
92
- timeout: 150,
93
- });
94
- const branch = result.status === 0 ? result.stdout.trim() || undefined : undefined;
95
- this.gitBranchCache = { checkedAt: now, branch };
96
- return branch;
91
+ if (!this.gitBranchLookupInFlight) {
92
+ this.gitBranchLookupInFlight = true;
93
+ void this.refreshGitBranchName();
94
+ }
95
+ return this.gitBranchCache?.branch;
96
+ }
97
+ async refreshGitBranchName() {
98
+ const previous = this.gitBranchCache?.branch;
99
+ try {
100
+ const result = await runProcess("git", ["-C", this.host.cwd, "branch", "--show-current"], {
101
+ timeoutMs: 150,
102
+ maxBufferBytes: 1024,
103
+ });
104
+ const branch = result.status === 0 ? result.stdout.trim() || undefined : undefined;
105
+ this.gitBranchCache = { checkedAt: Date.now(), branch };
106
+ if (branch !== previous)
107
+ this.host.render();
108
+ }
109
+ finally {
110
+ this.gitBranchLookupInFlight = false;
111
+ }
97
112
  }
98
113
  startStatusBlink() {
99
114
  this.host.blinkController.setActive(STATUS_DOT_BLINK_KEY, true, {
@@ -19,6 +19,7 @@ export type AppQueuedMessageControllerHost = {
19
19
  setInput(value: string): void;
20
20
  insertInput(value: string): void;
21
21
  attachImage(data: string, mimeType: string): void;
22
+ onDeferredUserMessagesChanged?(): void;
22
23
  };
23
24
  export declare class AppQueuedMessageController {
24
25
  private readonly host;
@@ -28,6 +29,8 @@ export declare class AppQueuedMessageController {
28
29
  private immediateSendInProgress;
29
30
  constructor(host: AppQueuedMessageControllerHost);
30
31
  reset(): void;
32
+ captureDeferredUserMessages(): SubmittedUserMessage[];
33
+ restoreDeferredUserMessages(messages: readonly SubmittedUserMessage[]): void;
31
34
  createSubmittedUserMessage(promptText: string, displayText: string, images: ImageContent[]): SubmittedUserMessage;
32
35
  submitUserMessage(message: SubmittedUserMessage): Promise<void>;
33
36
  sendUserMessageToSession(message: SubmittedUserMessage, options?: {
@@ -44,11 +47,12 @@ export declare class AppQueuedMessageController {
44
47
  cancelQueuedMessage(entryId: string): Promise<void>;
45
48
  editQueuedMessage(entryId: string): Promise<void>;
46
49
  sendQueuedMessageImmediately(entryId: string): Promise<void>;
50
+ private sendQueuedEntryImmediately;
47
51
  findQueuedEntry(entryId: string): Extract<Entry, {
48
52
  kind: "queued";
49
53
  }> | undefined;
50
54
  private shouldDeferUserMessage;
51
- private deferUserMessage;
55
+ deferUserMessage(message: SubmittedUserMessage): void;
52
56
  private rewriteSdkQueuedMessages;
53
57
  private takeQueuedEntryForInterruptedSend;
54
58
  private restoreSdkQueuedMessages;
@@ -59,4 +63,6 @@ export declare class AppQueuedMessageController {
59
63
  private requeueRemovedEntry;
60
64
  private restoreSubmittedMessageToEditor;
61
65
  private restorableSubmittedMessageText;
66
+ private cloneSubmittedUserMessage;
67
+ private notifyDeferredUserMessagesChanged;
62
68
  }
@@ -14,6 +14,14 @@ export class AppQueuedMessageController {
14
14
  this.promptSubmissionInFlight = false;
15
15
  this.flushingDeferredUserMessages = false;
16
16
  }
17
+ captureDeferredUserMessages() {
18
+ return this.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
19
+ }
20
+ restoreDeferredUserMessages(messages) {
21
+ this.deferredUserMessages.length = 0;
22
+ this.deferredUserMessages.push(...messages.map((message) => this.cloneSubmittedUserMessage(message)));
23
+ this.updateQueuedMessageStatus();
24
+ }
17
25
  createSubmittedUserMessage(promptText, displayText, images) {
18
26
  return {
19
27
  id: createId("queued-user"),
@@ -24,6 +32,10 @@ export class AppQueuedMessageController {
24
32
  }
25
33
  async submitUserMessage(message) {
26
34
  const session = this.host.requireRuntime().session;
35
+ if (session.isStreaming) {
36
+ await this.sendUserMessageToSession(message, { streamingBehavior: "steer" });
37
+ return;
38
+ }
27
39
  if (this.shouldDeferUserMessage(session)) {
28
40
  this.deferUserMessage(message);
29
41
  return;
@@ -55,8 +67,6 @@ export class AppQueuedMessageController {
55
67
  }
56
68
  if (this.totalQueuedMessageCount() > 0)
57
69
  this.updateQueuedMessageStatus();
58
- if (!this.flushingDeferredUserMessages)
59
- void this.flushDeferredUserMessages();
60
70
  }
61
71
  }
62
72
  async flushDeferredUserMessages() {
@@ -80,22 +90,14 @@ export class AppQueuedMessageController {
80
90
  const message = this.deferredUserMessages.shift();
81
91
  if (!message)
82
92
  break;
93
+ this.notifyDeferredUserMessagesChanged();
83
94
  this.updateQueuedMessageStatus();
84
- if (!activeSession.isStreaming && this.deferredUserMessages.length > 0) {
85
- void this.sendUserMessageToSession(message).catch((error) => {
86
- this.deferredUserMessages.unshift(message);
87
- this.updateQueuedMessageStatus();
88
- this.host.addEntry({ id: createId("error"), kind: "error", text: `Queued message failed: ${stringifyUnknown(error)}` });
89
- if (this.host.isRunning())
90
- this.host.render();
91
- });
92
- break;
93
- }
94
95
  try {
95
96
  await this.sendUserMessageToSession(message);
96
97
  }
97
98
  catch (error) {
98
99
  this.deferredUserMessages.unshift(message);
100
+ this.notifyDeferredUserMessagesChanged();
99
101
  this.updateQueuedMessageStatus();
100
102
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Queued message failed: ${stringifyUnknown(error)}` });
101
103
  break;
@@ -103,17 +105,11 @@ export class AppQueuedMessageController {
103
105
  }
104
106
  }
105
107
  finally {
106
- const shouldRetryFlush = this.deferredUserMessages.length > 0 && Boolean(this.host.runtime()?.session) && !this.host.runtime()?.session.isCompacting;
107
108
  this.flushingDeferredUserMessages = false;
108
109
  if (this.totalQueuedMessageCount() > 0)
109
110
  this.updateQueuedMessageStatus();
110
111
  if (this.host.isRunning())
111
112
  this.host.render();
112
- if (shouldRetryFlush) {
113
- queueMicrotask(() => {
114
- void this.flushDeferredUserMessages();
115
- });
116
- }
117
113
  }
118
114
  }
119
115
  queuedMessageCounts() {
@@ -135,6 +131,8 @@ export class AppQueuedMessageController {
135
131
  const session = this.host.runtime()?.session;
136
132
  const sdkQueued = session?.clearQueue() ?? { steering: [], followUp: [] };
137
133
  const deferred = this.deferredUserMessages.splice(0);
134
+ if (deferred.length > 0)
135
+ this.notifyDeferredUserMessagesChanged();
138
136
  const restoredTexts = [
139
137
  ...sdkQueued.steering,
140
138
  ...deferred.map((message) => this.restorableSubmittedMessageText(message)),
@@ -186,6 +184,9 @@ export class AppQueuedMessageController {
186
184
  const entry = this.findQueuedEntry(entryId);
187
185
  if (!entry)
188
186
  throw new Error("Queued message is no longer available");
187
+ await this.sendQueuedEntryImmediately(entry);
188
+ }
189
+ async sendQueuedEntryImmediately(entry) {
189
190
  const session = this.host.requireRuntime().session;
190
191
  const shouldInterrupt = session.isStreaming || session.isCompacting;
191
192
  const taken = shouldInterrupt
@@ -224,8 +225,6 @@ export class AppQueuedMessageController {
224
225
  this.immediateSendInProgress = false;
225
226
  if (this.totalQueuedMessageCount() > 0)
226
227
  this.updateQueuedMessageStatus();
227
- if (!this.flushingDeferredUserMessages)
228
- void this.flushDeferredUserMessages();
229
228
  }
230
229
  }
231
230
  findQueuedEntry(entryId) {
@@ -233,11 +232,13 @@ export class AppQueuedMessageController {
233
232
  return entry?.kind === "queued" ? entry : undefined;
234
233
  }
235
234
  shouldDeferUserMessage(session) {
236
- return session.isCompacting || (!session.isStreaming && this.promptSubmissionInFlight);
235
+ return session.isCompacting || this.promptSubmissionInFlight;
237
236
  }
238
237
  deferUserMessage(message) {
239
238
  this.deferredUserMessages.push(message);
239
+ this.notifyDeferredUserMessagesChanged();
240
240
  this.updateQueuedMessageStatus();
241
+ this.host.showToast("Message queued; send it from the queue menu or status button", "info");
241
242
  this.host.render();
242
243
  }
243
244
  async rewriteSdkQueuedMessages(update) {
@@ -282,6 +283,7 @@ export class AppQueuedMessageController {
282
283
  if (!message)
283
284
  throw new Error("Queued message is no longer available");
284
285
  session.clearQueue();
286
+ this.notifyDeferredUserMessagesChanged();
285
287
  return { removed: message, sdkMessagesToRestore: sdkMessages };
286
288
  }
287
289
  const messages = entry.queueSource === "sdk-steering" ? sdkMessages.steering : sdkMessages.followUp;
@@ -334,6 +336,7 @@ export class AppQueuedMessageController {
334
336
  const [message] = this.deferredUserMessages.splice(entry.queueIndex, 1);
335
337
  if (!message)
336
338
  throw new Error("Queued message is no longer available");
339
+ this.notifyDeferredUserMessagesChanged();
337
340
  return message;
338
341
  }
339
342
  const removed = await this.rewriteSdkQueuedMessages((steering, followUp) => {
@@ -350,6 +353,7 @@ export class AppQueuedMessageController {
350
353
  if (typeof removed === "string")
351
354
  return;
352
355
  this.deferredUserMessages.splice(Math.min(entry.queueIndex, this.deferredUserMessages.length), 0, removed);
356
+ this.notifyDeferredUserMessagesChanged();
353
357
  return;
354
358
  }
355
359
  if (typeof removed !== "string")
@@ -374,4 +378,15 @@ export class AppQueuedMessageController {
374
378
  ? message.promptText.replace(/\[Image \d+(?:: [^\]]+)?\]\s*/g, "").trimEnd()
375
379
  : message.promptText.trimEnd();
376
380
  }
381
+ cloneSubmittedUserMessage(message) {
382
+ return {
383
+ id: message.id,
384
+ promptText: message.promptText,
385
+ displayText: message.displayText,
386
+ images: message.images.map((image) => ({ ...image })),
387
+ };
388
+ }
389
+ notifyDeferredUserMessagesChanged() {
390
+ this.host.onDeferredUserMessagesChanged?.();
391
+ }
377
392
  }
@@ -0,0 +1,15 @@
1
+ import { type SessionInfo } from "@earendil-works/pi-coding-agent";
2
+ export type ResumeSessionLoadProgress = {
3
+ loaded: number;
4
+ total: number;
5
+ done: boolean;
6
+ };
7
+ export type ResumeSessionLoaderOptions = {
8
+ cwd: string;
9
+ sessionDir?: string;
10
+ initialChunkSize?: number;
11
+ chunkSize?: number;
12
+ signal?: AbortSignal;
13
+ onChunk(sessions: readonly SessionInfo[], progress: ResumeSessionLoadProgress): void;
14
+ };
15
+ export declare function loadResumeSessionsInChunks(options: ResumeSessionLoaderOptions): Promise<SessionInfo[]>;