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,5 @@
1
1
  import { getIdleRuntime, getRuntime } from "./command-runtime.js";
2
+ import { savePixAutocompleteModel, savePixDefaultModel, savePixDefaultThinking } from "../../config.js";
2
3
  import { createId } from "../id.js";
3
4
  import { isThinkingLevel, parseScopedModelRef } from "../model/model-ref.js";
4
5
  export class ModelCommandActions {
@@ -22,6 +23,7 @@ export class ModelCommandActions {
22
23
  `session: ${runtime.session.sessionFile ?? "in-memory"}`,
23
24
  `model: ${currentModel}`,
24
25
  `prompt enhancer model: ${this.host.promptEnhancerModelRef()}`,
26
+ `autocomplete model: ${this.host.autocompleteModelRef() || "disabled"}`,
25
27
  `thinking: ${runtime.session.thinkingLevel}`,
26
28
  `theme: ${settings.getTheme() ?? this.host.options.themeName}`,
27
29
  `skill commands: ${settings.getEnableSkillCommands() ? "enabled" : "disabled"}`,
@@ -70,6 +72,59 @@ export class ModelCommandActions {
70
72
  this.host.setSessionStatus(runtime.session);
71
73
  }
72
74
  }
75
+ async runDefaultModelSlashCommand(argumentsText) {
76
+ const modelRef = argumentsText.trim();
77
+ if (!modelRef) {
78
+ const selected = await this.host.showMenu(this.host.getModelMenuItems(""), {
79
+ title: "Select default model",
80
+ placeholder: "Search models",
81
+ emptyText: "No matching models",
82
+ });
83
+ if (!selected) {
84
+ this.host.setSessionStatus(this.host.runtime()?.session);
85
+ this.host.render();
86
+ return;
87
+ }
88
+ this.saveDefaultModel(this.host.modelRef(selected.model));
89
+ this.host.render();
90
+ return;
91
+ }
92
+ const runtime = getRuntime(this.host, "default-model");
93
+ if (!runtime)
94
+ return;
95
+ const parsed = parseScopedModelRef(modelRef);
96
+ if (!parsed)
97
+ throw new Error("Model must use provider/model[:thinking] format");
98
+ runtime.services.modelRegistry.refresh();
99
+ const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
100
+ if (!model)
101
+ throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
102
+ this.saveDefaultModel(modelRef);
103
+ this.host.setSessionStatus(runtime.session);
104
+ }
105
+ async runAutocompleteSlashCommand(argumentsText) {
106
+ const modelRef = argumentsText.trim();
107
+ if (!modelRef) {
108
+ const saved = savePixAutocompleteModel("");
109
+ this.host.setAutocompleteModelRef(saved.modelRef);
110
+ this.host.addEntry({ id: createId("system"), kind: "system", text: saved.modelRef ? `Autocomplete model set to ${saved.modelRef}` : "Inline autocomplete disabled." });
111
+ return;
112
+ }
113
+ const runtime = getRuntime(this.host, "autocomplete");
114
+ if (!runtime)
115
+ return;
116
+ const parsed = parseScopedModelRef(modelRef);
117
+ if (!parsed)
118
+ throw new Error("Model must use provider/model[:thinking] format, or run /autocomplete with no arguments to disable");
119
+ runtime.services.modelRegistry.refresh();
120
+ const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
121
+ if (!model)
122
+ throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
123
+ const saved = savePixAutocompleteModel(modelRef);
124
+ this.host.setAutocompleteModelRef(saved.modelRef);
125
+ this.host.addEntry({ id: createId("system"), kind: "system", text: `Autocomplete model set to ${saved.modelRef}.` });
126
+ this.host.setSessionStatus(runtime.session);
127
+ }
73
128
  async runScopedModelsCommand(argumentsText) {
74
129
  const runtime = getIdleRuntime(this.host, "scoped-models");
75
130
  if (!runtime)
@@ -152,6 +207,27 @@ export class ModelCommandActions {
152
207
  throw new Error(`Unknown thinking level: ${level}`);
153
208
  await this.runThinkingCommand(level);
154
209
  }
210
+ async runDefaultThinkingSlashCommand(argumentsText) {
211
+ const level = argumentsText.trim();
212
+ if (!level) {
213
+ const selected = await this.host.showMenu(this.host.getThinkingMenuItems(""), {
214
+ title: "Select default thinking level",
215
+ placeholder: "Search thinking levels",
216
+ emptyText: "No matching thinking levels",
217
+ });
218
+ if (!selected) {
219
+ this.host.setSessionStatus(this.host.runtime()?.session);
220
+ this.host.render();
221
+ return;
222
+ }
223
+ this.saveDefaultThinking(selected.level);
224
+ this.host.render();
225
+ return;
226
+ }
227
+ if (!isThinkingLevel(level))
228
+ throw new Error(`Unknown thinking level: ${level}`);
229
+ this.saveDefaultThinking(level);
230
+ }
155
231
  async runModelCommand(model) {
156
232
  const runtime = getRuntime(this.host, "model");
157
233
  if (!runtime)
@@ -173,4 +249,32 @@ export class ModelCommandActions {
173
249
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected thinking level ${runtime.session.thinkingLevel}` });
174
250
  this.host.setSessionStatus(runtime.session);
175
251
  }
252
+ saveDefaultModel(modelRef) {
253
+ const saved = savePixDefaultModel(modelRef);
254
+ if (!saved)
255
+ throw new Error("Model must use provider/model[:thinking] format");
256
+ this.host.addEntry({
257
+ id: createId("system"),
258
+ kind: "system",
259
+ text: `Default model set to ${formatDefaultModelRef(saved)}. New sessions will use it unless --model is provided.`,
260
+ });
261
+ }
262
+ saveDefaultThinking(level) {
263
+ const runtime = getRuntime(this.host, "default-thinking");
264
+ if (!runtime)
265
+ return;
266
+ const fallbackModelRef = runtime.session.model ? this.host.modelRef(runtime.session.model) : undefined;
267
+ const saved = savePixDefaultThinking(level, fallbackModelRef);
268
+ if (!saved)
269
+ throw new Error("Set /default-model first or select a session model before setting default thinking");
270
+ this.host.addEntry({
271
+ id: createId("system"),
272
+ kind: "system",
273
+ text: `Default thinking level set to ${saved.thinking ?? level} for ${saved.modelRef}. New sessions will use it unless --model is provided.`,
274
+ });
275
+ this.host.setSessionStatus(runtime.session);
276
+ }
277
+ }
278
+ function formatDefaultModelRef(defaultModel) {
279
+ return defaultModel.thinking ? `${defaultModel.modelRef}:${defaultModel.thinking}` : defaultModel.modelRef;
176
280
  }
@@ -1,8 +1,12 @@
1
+ import type { SessionInfo } from "@earendil-works/pi-coding-agent";
1
2
  import type { CommandControllerHost } from "./command-host.js";
3
+ import { type ResumeSessionLoaderOptions } from "../session/resume-session-loader.js";
2
4
  import type { PopupMenuPlacement } from "../types.js";
3
5
  export declare class NavigationCommandActions {
4
6
  private readonly host;
5
- constructor(host: CommandControllerHost);
7
+ private readonly resumeSessionLoader;
8
+ private resumeLoadId;
9
+ constructor(host: CommandControllerHost, resumeSessionLoader?: (options: ResumeSessionLoaderOptions) => Promise<readonly SessionInfo[]>);
6
10
  runForkCommand(argumentsText: string): Promise<void>;
7
11
  runCloneCommand(): Promise<void>;
8
12
  runTreeCommand(argumentsText: string): Promise<void>;
@@ -14,6 +18,7 @@ export declare class NavigationCommandActions {
14
18
  preserveStatus?: boolean;
15
19
  placement?: PopupMenuPlacement;
16
20
  }): Promise<void>;
21
+ private loadResumeSessionsInBackground;
17
22
  private formatSessionTree;
18
23
  private describeSessionTreeEntry;
19
24
  }
@@ -1,15 +1,23 @@
1
1
  import { resolve } from "node:path";
2
- import { SessionManager } from "@earendil-works/pi-coding-agent";
3
2
  import { getIdleRuntime, getRuntime } from "./command-runtime.js";
4
3
  import { createId } from "../id.js";
5
4
  import { isRecord } from "../guards.js";
6
5
  import { renderContent } from "../rendering/message-content.js";
7
6
  import { sanitizeText } from "../rendering/render-text.js";
8
7
  import { createSessionSearchMenuItems, searchSessions } from "../session/session-search.js";
8
+ import { loadResumeSessionsInChunks } from "../session/resume-session-loader.js";
9
+ function nextTick() {
10
+ return new Promise((resolve) => {
11
+ setImmediate(resolve);
12
+ });
13
+ }
9
14
  export class NavigationCommandActions {
10
15
  host;
11
- constructor(host) {
16
+ resumeSessionLoader;
17
+ resumeLoadId = 0;
18
+ constructor(host, resumeSessionLoader = loadResumeSessionsInChunks) {
12
19
  this.host = host;
20
+ this.resumeSessionLoader = resumeSessionLoader;
13
21
  }
14
22
  async runForkCommand(argumentsText) {
15
23
  const runtime = getIdleRuntime(this.host, "fork");
@@ -197,29 +205,44 @@ export class NavigationCommandActions {
197
205
  this.host.setStatus("loading sessions…");
198
206
  this.host.openDirectPopupMenu("resume", { preserveStatus, ...(placement === undefined ? {} : { placement }) });
199
207
  this.host.setDirectPopupMenuQuery(initialQuery);
208
+ if (this.host.getResumeSessions().length > 0) {
209
+ this.host.openResumeMenuWithQuery(initialQuery);
210
+ }
200
211
  this.host.render();
212
+ const loadId = ++this.resumeLoadId;
213
+ void this.loadResumeSessionsInBackground({ loadId, preserveStatus, session: runtime.session });
214
+ }
215
+ async loadResumeSessionsInBackground(options) {
201
216
  try {
202
- const sessions = await SessionManager.list(this.host.options.cwd);
203
- this.host.setResumeSessions(sessions);
204
- this.host.setResumeLoading(false);
205
- if (this.host.getDirectPopupMenu() !== "resume")
206
- return;
207
- this.host.openResumeMenuWithQuery(this.host.getDirectPopupMenuQuery());
208
- if (!preserveStatus)
209
- this.host.setSessionStatus(runtime.session);
210
- this.host.render();
217
+ await nextTick();
218
+ await this.resumeSessionLoader({
219
+ cwd: this.host.options.cwd,
220
+ onChunk: (sessions, progress) => {
221
+ if (options.loadId !== this.resumeLoadId)
222
+ return;
223
+ this.host.setResumeSessions([...sessions]);
224
+ if (progress.done) {
225
+ this.host.setResumeLoading(false);
226
+ if (!options.preserveStatus)
227
+ this.host.setSessionStatus(options.session);
228
+ }
229
+ this.host.render();
230
+ },
231
+ });
211
232
  }
212
233
  catch (error) {
234
+ if (options.loadId !== this.resumeLoadId)
235
+ return;
213
236
  this.host.setResumeLoading(false);
214
237
  this.host.setDirectPopupMenu(undefined);
215
238
  this.host.setDirectPopupMenuPreserveStatus(false);
216
239
  this.host.setDirectPopupMenuQuery("");
217
240
  this.host.closeResumeMenu();
218
241
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Session list failed: ${error instanceof Error ? error.message : String(error)}` });
219
- if (!preserveStatus)
242
+ if (!options.preserveStatus)
220
243
  this.host.toast.error("Failed to load sessions");
221
- if (!preserveStatus)
222
- this.host.setSessionStatus(runtime.session);
244
+ if (!options.preserveStatus)
245
+ this.host.setSessionStatus(options.session);
223
246
  this.host.render();
224
247
  }
225
248
  }
@@ -3,13 +3,17 @@ import type { SlashCommand } from "../types.js";
3
3
  export type CommandRegistryActions = {
4
4
  runSettingsCommand(): Promise<void>;
5
5
  runModelSlashCommand(argumentsText: string): Promise<void>;
6
+ runDefaultModelSlashCommand(argumentsText: string): Promise<void>;
7
+ runAutocompleteSlashCommand(argumentsText: string): Promise<void>;
6
8
  runScopedModelsCommand(argumentsText: string): Promise<void>;
7
9
  runThinkingSlashCommand(argumentsText: string): Promise<void>;
10
+ runDefaultThinkingSlashCommand(argumentsText: string): Promise<void>;
8
11
  runEnhanceCommand(): Promise<void>;
9
12
  runExportCommand(argumentsText: string): Promise<void>;
10
13
  runImportCommand(argumentsText: string): Promise<void>;
11
14
  runShareCommand(): Promise<void>;
12
15
  runCopyCommand(): Promise<void>;
16
+ runQueueCommand(argumentsText: string): Promise<void>;
13
17
  runNameCommand(argumentsText: string): Promise<void>;
14
18
  runSessionInfoCommand(): Promise<void>;
15
19
  runUsageCommand(): Promise<void>;
@@ -16,6 +16,22 @@ export function createSlashCommands(actions, host) {
16
16
  allowArguments: true,
17
17
  run: (argumentsText) => actions.runModelSlashCommand(argumentsText),
18
18
  },
19
+ {
20
+ name: "default-model",
21
+ description: "Set the default model for new sessions",
22
+ kind: "builtin",
23
+ keywords: ["model", "provider", "startup", "config"],
24
+ allowArguments: true,
25
+ run: (argumentsText) => actions.runDefaultModelSlashCommand(argumentsText),
26
+ },
27
+ {
28
+ name: "autocomplete",
29
+ description: "Set inline autocomplete model, or empty to disable",
30
+ kind: "builtin",
31
+ keywords: ["complete", "ghost", "suggest", "llm", "model"],
32
+ allowArguments: true,
33
+ run: (argumentsText) => actions.runAutocompleteSlashCommand(argumentsText),
34
+ },
19
35
  {
20
36
  name: "scoped-models",
21
37
  description: "Show or set models used by the model selector/cycling",
@@ -32,6 +48,14 @@ export function createSlashCommands(actions, host) {
32
48
  allowArguments: true,
33
49
  run: (argumentsText) => actions.runThinkingSlashCommand(argumentsText),
34
50
  },
51
+ {
52
+ name: "default-thinking",
53
+ description: "Set the default thinking level for new sessions",
54
+ kind: "builtin",
55
+ keywords: ["thinking", "reasoning", "startup", "config"],
56
+ allowArguments: true,
57
+ run: (argumentsText) => actions.runDefaultThinkingSlashCommand(argumentsText),
58
+ },
35
59
  {
36
60
  name: "enhance",
37
61
  description: "Improve the current prompt draft",
@@ -69,6 +93,14 @@ export function createSlashCommands(actions, host) {
69
93
  keywords: ["clipboard", "assistant", "message"],
70
94
  run: () => actions.runCopyCommand(),
71
95
  },
96
+ {
97
+ name: "queue",
98
+ description: "Queue a message for manual sending later",
99
+ kind: "builtin",
100
+ keywords: ["defer", "delayed", "message", "send later"],
101
+ allowArguments: true,
102
+ run: (argumentsText) => actions.runQueueCommand(argumentsText),
103
+ },
72
104
  {
73
105
  name: "name",
74
106
  description: "Show or set the session display name",
@@ -6,6 +6,7 @@ export declare class SessionCommandActions {
6
6
  runImportCommand(argumentsText: string): Promise<void>;
7
7
  runShareCommand(): Promise<void>;
8
8
  runCopyCommand(): Promise<void>;
9
+ runQueueCommand(argumentsText: string): Promise<void>;
9
10
  runNameCommand(argumentsText: string): Promise<void>;
10
11
  runSessionInfoCommand(): Promise<void>;
11
12
  runUsageCommand(): Promise<void>;
@@ -1,11 +1,12 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { mkdir, readFile, rm } from "node:fs/promises";
4
3
  import { dirname, join, resolve } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
- import { copyToClipboard, getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
6
  import { getIdleRuntime, getRuntime, parsePathArgument } from "./command-runtime.js";
8
7
  import { createId } from "../id.js";
8
+ import { runProcess } from "../process.js";
9
+ import { copyTextToClipboard } from "../screen/clipboard.js";
9
10
  import { formatAccountUsageReport, queryAccountUsageReport } from "../model/model-usage-status.js";
10
11
  import { checkPixUpdate, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsage } from "../cli/update.js";
11
12
  export class SessionCommandActions {
@@ -50,7 +51,7 @@ export class SessionCommandActions {
50
51
  const runtime = getIdleRuntime(this.host, "share");
51
52
  if (!runtime)
52
53
  return;
53
- const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf8" });
54
+ const authResult = await runProcess("gh", ["auth", "status"], { maxBufferBytes: 32 * 1024 });
54
55
  if (authResult.status !== 0)
55
56
  throw new Error("GitHub CLI is not installed or is not logged in. Run `gh auth login` first.");
56
57
  const shareDir = join(getAgentDir(), "pix");
@@ -60,7 +61,7 @@ export class SessionCommandActions {
60
61
  this.host.setStatus("creating share gist");
61
62
  this.host.render();
62
63
  await runtime.session.exportToHtml(tmpFile);
63
- const gistResult = spawnSync("gh", ["gist", "create", "--public=false", tmpFile], { encoding: "utf8" });
64
+ const gistResult = await runProcess("gh", ["gist", "create", "--public=false", tmpFile], { maxBufferBytes: 64 * 1024 });
64
65
  if (gistResult.status !== 0)
65
66
  throw new Error(gistResult.stderr?.trim() || "Failed to create gist");
66
67
  const gistUrl = gistResult.stdout.trim();
@@ -79,11 +80,19 @@ export class SessionCommandActions {
79
80
  const text = runtime.session.getLastAssistantText();
80
81
  if (!text)
81
82
  throw new Error("No agent messages to copy yet");
82
- await copyToClipboard(text);
83
+ await copyTextToClipboard(text);
83
84
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Copied last agent message to clipboard." });
84
85
  this.host.setSessionStatus(runtime.session);
85
86
  this.host.toast.success("Copied last agent message");
86
87
  }
88
+ async runQueueCommand(argumentsText) {
89
+ const text = argumentsText.trim();
90
+ if (!text)
91
+ throw new Error("Usage: /queue <message to send later>");
92
+ this.host.queueUserMessage(text);
93
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "Queued message for manual sending." });
94
+ this.host.setSessionStatus(this.host.runtime()?.session);
95
+ }
87
96
  async runNameCommand(argumentsText) {
88
97
  const runtime = getRuntime(this.host, "name");
89
98
  if (!runtime)
@@ -201,6 +210,7 @@ export class SessionCommandActions {
201
210
  "Enter: send message / run selected command",
202
211
  "!command: run a local shell command in chat (not saved to the session)",
203
212
  "while shell is running: Enter sends editor text to stdin; Ctrl-C interrupts; !!command uses the raw terminal",
213
+ "/queue <message>: save a delayed message; send it later from the queued-message menu or status button",
204
214
  "Tab: autocomplete selected popup item",
205
215
  "Esc: close popup; abort running work when input is empty",
206
216
  "Up/Down: history or popup navigation",
@@ -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…");
@@ -9,6 +9,7 @@ export type AppShellControllerHost = {
9
9
  setSessionActivity(activity: SessionActivity): void;
10
10
  restoreSessionStatus(): void;
11
11
  render(): void;
12
+ scheduleRender(): void;
12
13
  };
13
14
  export declare class AppShellController {
14
15
  private readonly host;
@@ -89,7 +89,7 @@ export class AppShellController {
89
89
  this.renderTimer = setTimeout(() => {
90
90
  this.renderTimer = undefined;
91
91
  if (this.host.isRunning())
92
- this.host.render();
92
+ this.host.scheduleRender();
93
93
  }, SHELL_RENDER_THROTTLE_MS);
94
94
  this.renderTimer.unref?.();
95
95
  }
@@ -38,7 +38,7 @@ export declare const SUBAGENTS_POLL_INTERVAL_MS = 1500;
38
38
  export declare const SUBAGENTS_WIDGET_MAX_ROWS = 8;
39
39
  export declare const DEFAULT_THINKING_TOOL_RULE: ResolvedToolRule;
40
40
  export declare const TERMINAL_COMMAND_MODIFIER_FLAG = 8;
41
- export declare const GIT_BRANCH_CACHE_MS = 2000;
41
+ export declare const GIT_BRANCH_CACHE_MS = 30000;
42
42
  export declare const TODO_TOOL_NAME = "todo";
43
43
  export declare const TODO_ACTIONS: readonly ["create", "update", "batch_create", "batch_update", "list", "get", "delete", "clear", "export", "import"];
44
44
  export declare const TODO_STATUSES: readonly ["pending", "in_progress", "deferred", "completed", "deleted"];
@@ -75,7 +75,7 @@ export const DEFAULT_THINKING_TOOL_RULE = {
75
75
  color: "accent",
76
76
  };
77
77
  export const TERMINAL_COMMAND_MODIFIER_FLAG = 8;
78
- export const GIT_BRANCH_CACHE_MS = 2_000;
78
+ export const GIT_BRANCH_CACHE_MS = 30_000;
79
79
  export const TODO_TOOL_NAME = "todo";
80
80
  export const TODO_ACTIONS = [
81
81
  "create",
@@ -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: "♪",
@@ -53,7 +55,7 @@ const FALLBACK_ICONS = {
53
55
  compactTools: "≡",
54
56
  thinkingExpanded: ">",
55
57
  stopCircle: "■",
56
- timerSand: "",
58
+ timerSand: "",
57
59
  down: "v",
58
60
  };
59
61
  export const APP_ICON_THEMES = {
@@ -0,0 +1,52 @@
1
+ import type { AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
+ import type { AutocompleteConfig } from "../../config.js";
3
+ import type { InputEditor } from "../../input-editor.js";
4
+ export type AppAutocompleteControllerHost = {
5
+ runtime(): AgentSessionRuntime | undefined;
6
+ inputEditor(): InputEditor;
7
+ autocompleteConfig(): AutocompleteConfig;
8
+ isRunning(): boolean;
9
+ render(): void;
10
+ };
11
+ export type AutocompleteHistoryMessage = {
12
+ role: "user" | "assistant";
13
+ text: string;
14
+ };
15
+ type AutocompleteRunner = typeof completeInputWithPi;
16
+ export type AppAutocompleteControllerOptions = {
17
+ completeInputWithPi?: AutocompleteRunner;
18
+ debounceMs?: number;
19
+ };
20
+ export declare class AppAutocompleteController {
21
+ private readonly host;
22
+ private timer;
23
+ private lastObservedKey;
24
+ private requestSeq;
25
+ private suggestion;
26
+ private activeAbortController;
27
+ private readonly completeInputWithPi;
28
+ private readonly debounceOverrideMs;
29
+ constructor(host: AppAutocompleteControllerHost, options?: AppAutocompleteControllerOptions);
30
+ observeInput(): void;
31
+ suggestionText(): string | undefined;
32
+ acceptSuggestion(): boolean;
33
+ dispose(): void;
34
+ private runAutocomplete;
35
+ private currentTarget;
36
+ private targetKey;
37
+ private sameTarget;
38
+ private currentDebounceMs;
39
+ private clearTimer;
40
+ private cancelInFlight;
41
+ }
42
+ export declare function completeInputWithPi(runtime: AgentSessionRuntime, draft: string, config: AutocompleteConfig, signal?: AbortSignal): Promise<string>;
43
+ export declare function autocompleteHistoryFromMessages(messages: readonly unknown[], includeRecentMessages: number): AutocompleteHistoryMessage[];
44
+ export declare function buildAutocompletePrompt(input: {
45
+ cwd: string;
46
+ draft: string;
47
+ history: readonly AutocompleteHistoryMessage[];
48
+ maxPromptTokens?: number;
49
+ }): string;
50
+ export declare function autocompletePromptTokenEstimate(prompt: string, systemPrompt?: string): number;
51
+ export declare function cleanupCompletion(output: string, draft: string, config?: Pick<AutocompleteConfig, "maxTokens">): string;
52
+ export {};