@mariozechner/pi-coding-agent 0.16.0 → 0.18.0

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 (100) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +58 -1
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +5 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +4 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +30 -2
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +181 -21
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/compaction.d.ts +30 -5
  16. package/dist/core/compaction.d.ts.map +1 -1
  17. package/dist/core/compaction.js +194 -61
  18. package/dist/core/compaction.js.map +1 -1
  19. package/dist/core/hooks/index.d.ts +5 -0
  20. package/dist/core/hooks/index.d.ts.map +1 -0
  21. package/dist/core/hooks/index.js +4 -0
  22. package/dist/core/hooks/index.js.map +1 -0
  23. package/dist/core/hooks/loader.d.ts +56 -0
  24. package/dist/core/hooks/loader.d.ts.map +1 -0
  25. package/dist/core/hooks/loader.js +158 -0
  26. package/dist/core/hooks/loader.js.map +1 -0
  27. package/dist/core/hooks/runner.d.ts +69 -0
  28. package/dist/core/hooks/runner.d.ts.map +1 -0
  29. package/dist/core/hooks/runner.js +203 -0
  30. package/dist/core/hooks/runner.js.map +1 -0
  31. package/dist/core/hooks/tool-wrapper.d.ts +16 -0
  32. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -0
  33. package/dist/core/hooks/tool-wrapper.js +71 -0
  34. package/dist/core/hooks/tool-wrapper.js.map +1 -0
  35. package/dist/core/hooks/types.d.ts +220 -0
  36. package/dist/core/hooks/types.d.ts.map +1 -0
  37. package/dist/core/hooks/types.js +8 -0
  38. package/dist/core/hooks/types.js.map +1 -0
  39. package/dist/core/index.d.ts +1 -0
  40. package/dist/core/index.d.ts.map +1 -1
  41. package/dist/core/index.js +1 -0
  42. package/dist/core/index.js.map +1 -1
  43. package/dist/core/session-manager.d.ts +10 -3
  44. package/dist/core/session-manager.d.ts.map +1 -1
  45. package/dist/core/session-manager.js +78 -28
  46. package/dist/core/session-manager.js.map +1 -1
  47. package/dist/core/settings-manager.d.ts +6 -0
  48. package/dist/core/settings-manager.d.ts.map +1 -1
  49. package/dist/core/settings-manager.js +14 -0
  50. package/dist/core/settings-manager.js.map +1 -1
  51. package/dist/core/system-prompt.d.ts.map +1 -1
  52. package/dist/core/system-prompt.js +5 -3
  53. package/dist/core/system-prompt.js.map +1 -1
  54. package/dist/core/tools/truncate.d.ts +6 -2
  55. package/dist/core/tools/truncate.d.ts.map +1 -1
  56. package/dist/core/tools/truncate.js +11 -1
  57. package/dist/core/tools/truncate.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/main.d.ts.map +1 -1
  62. package/dist/main.js +23 -12
  63. package/dist/main.js.map +1 -1
  64. package/dist/modes/interactive/components/bash-execution.d.ts +1 -0
  65. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/bash-execution.js +17 -6
  67. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  68. package/dist/modes/interactive/components/hook-input.d.ts +12 -0
  69. package/dist/modes/interactive/components/hook-input.d.ts.map +1 -0
  70. package/dist/modes/interactive/components/hook-input.js +46 -0
  71. package/dist/modes/interactive/components/hook-input.js.map +1 -0
  72. package/dist/modes/interactive/components/hook-selector.d.ts +16 -0
  73. package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -0
  74. package/dist/modes/interactive/components/hook-selector.js +76 -0
  75. package/dist/modes/interactive/components/hook-selector.js.map +1 -0
  76. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/tool-execution.js +12 -7
  78. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts +37 -0
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +190 -7
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/print-mode.d.ts.map +1 -1
  84. package/dist/modes/print-mode.js +15 -0
  85. package/dist/modes/print-mode.js.map +1 -1
  86. package/dist/modes/rpc/rpc-mode.d.ts +2 -1
  87. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  88. package/dist/modes/rpc/rpc-mode.js +118 -3
  89. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  90. package/dist/modes/rpc/rpc-types.d.ts +41 -0
  91. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-types.js.map +1 -1
  93. package/docs/compaction.md +519 -0
  94. package/docs/hooks.md +609 -0
  95. package/docs/rpc.md +870 -0
  96. package/docs/session.md +89 -0
  97. package/docs/theme.md +586 -0
  98. package/docs/truncation.md +235 -0
  99. package/docs/undercompaction.md +313 -0
  100. package/package.json +18 -6
@@ -11,6 +11,7 @@ import { isBashExecutionMessage } from "../../core/messages.js";
11
11
  import { invalidateOAuthCache } from "../../core/model-config.js";
12
12
  import { listOAuthProviders, login, logout } from "../../core/oauth/index.js";
13
13
  import { getLatestCompactionEntry, SUMMARY_PREFIX, SUMMARY_SUFFIX } from "../../core/session-manager.js";
14
+ import { loadProjectContextFiles } from "../../core/system-prompt.js";
14
15
  import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
15
16
  import { copyToClipboard } from "../../utils/clipboard.js";
16
17
  import { AssistantMessageComponent } from "./components/assistant-message.js";
@@ -19,6 +20,8 @@ import { CompactionComponent } from "./components/compaction.js";
19
20
  import { CustomEditor } from "./components/custom-editor.js";
20
21
  import { DynamicBorder } from "./components/dynamic-border.js";
21
22
  import { FooterComponent } from "./components/footer.js";
23
+ import { HookInputComponent } from "./components/hook-input.js";
24
+ import { HookSelectorComponent } from "./components/hook-selector.js";
22
25
  import { ModelSelectorComponent } from "./components/model-selector.js";
23
26
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
24
27
  import { QueueModeSelectorComponent } from "./components/queue-mode-selector.js";
@@ -66,6 +69,9 @@ export class InteractiveMode {
66
69
  // Auto-compaction state
67
70
  autoCompactionLoader = null;
68
71
  autoCompactionEscapeHandler;
72
+ // Hook UI state
73
+ hookSelector = null;
74
+ hookInput = null;
69
75
  // Convenience accessors
70
76
  get agent() {
71
77
  return this.session.agent;
@@ -189,6 +195,8 @@ export class InteractiveMode {
189
195
  // Start the UI
190
196
  this.ui.start();
191
197
  this.isInitialized = true;
198
+ // Initialize hooks with TUI-based UI context
199
+ await this.initHooks();
192
200
  // Subscribe to agent events
193
201
  this.subscribeToAgent();
194
202
  // Set up theme file watcher
@@ -202,6 +210,161 @@ export class InteractiveMode {
202
210
  this.ui.requestRender();
203
211
  });
204
212
  }
213
+ // =========================================================================
214
+ // Hook System
215
+ // =========================================================================
216
+ /**
217
+ * Initialize the hook system with TUI-based UI context.
218
+ */
219
+ async initHooks() {
220
+ // Show loaded project context files
221
+ const contextFiles = loadProjectContextFiles();
222
+ if (contextFiles.length > 0) {
223
+ const contextList = contextFiles.map((f) => theme.fg("dim", ` ${f.path}`)).join("\n");
224
+ this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded context:\n") + contextList, 0, 0));
225
+ this.chatContainer.addChild(new Spacer(1));
226
+ }
227
+ const hookRunner = this.session.hookRunner;
228
+ if (!hookRunner) {
229
+ return; // No hooks loaded
230
+ }
231
+ // Set TUI-based UI context on the hook runner
232
+ hookRunner.setUIContext(this.createHookUIContext(), true);
233
+ hookRunner.setSessionFile(this.session.sessionFile);
234
+ // Subscribe to hook errors
235
+ hookRunner.onError((error) => {
236
+ this.showHookError(error.hookPath, error.error);
237
+ });
238
+ // Set up send handler for pi.send()
239
+ hookRunner.setSendHandler((text, attachments) => {
240
+ this.handleHookSend(text, attachments);
241
+ });
242
+ // Show loaded hooks
243
+ const hookPaths = hookRunner.getHookPaths();
244
+ if (hookPaths.length > 0) {
245
+ const hookList = hookPaths.map((p) => theme.fg("dim", ` ${p}`)).join("\n");
246
+ this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded hooks:\n") + hookList, 0, 0));
247
+ this.chatContainer.addChild(new Spacer(1));
248
+ }
249
+ // Emit session_start event
250
+ await hookRunner.emit({ type: "session_start" });
251
+ }
252
+ /**
253
+ * Create the UI context for hooks.
254
+ */
255
+ createHookUIContext() {
256
+ return {
257
+ select: (title, options) => this.showHookSelector(title, options),
258
+ confirm: (title, message) => this.showHookConfirm(title, message),
259
+ input: (title, placeholder) => this.showHookInput(title, placeholder),
260
+ notify: (message, type) => this.showHookNotify(message, type),
261
+ };
262
+ }
263
+ /**
264
+ * Show a selector for hooks.
265
+ */
266
+ showHookSelector(title, options) {
267
+ return new Promise((resolve) => {
268
+ this.hookSelector = new HookSelectorComponent(title, options, (option) => {
269
+ this.hideHookSelector();
270
+ resolve(option);
271
+ }, () => {
272
+ this.hideHookSelector();
273
+ resolve(null);
274
+ });
275
+ this.editorContainer.clear();
276
+ this.editorContainer.addChild(this.hookSelector);
277
+ this.ui.setFocus(this.hookSelector);
278
+ this.ui.requestRender();
279
+ });
280
+ }
281
+ /**
282
+ * Hide the hook selector.
283
+ */
284
+ hideHookSelector() {
285
+ this.editorContainer.clear();
286
+ this.editorContainer.addChild(this.editor);
287
+ this.hookSelector = null;
288
+ this.ui.setFocus(this.editor);
289
+ this.ui.requestRender();
290
+ }
291
+ /**
292
+ * Show a confirmation dialog for hooks.
293
+ */
294
+ async showHookConfirm(title, message) {
295
+ const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"]);
296
+ return result === "Yes";
297
+ }
298
+ /**
299
+ * Show a text input for hooks.
300
+ */
301
+ showHookInput(title, placeholder) {
302
+ return new Promise((resolve) => {
303
+ this.hookInput = new HookInputComponent(title, placeholder, (value) => {
304
+ this.hideHookInput();
305
+ resolve(value);
306
+ }, () => {
307
+ this.hideHookInput();
308
+ resolve(null);
309
+ });
310
+ this.editorContainer.clear();
311
+ this.editorContainer.addChild(this.hookInput);
312
+ this.ui.setFocus(this.hookInput);
313
+ this.ui.requestRender();
314
+ });
315
+ }
316
+ /**
317
+ * Hide the hook input.
318
+ */
319
+ hideHookInput() {
320
+ this.editorContainer.clear();
321
+ this.editorContainer.addChild(this.editor);
322
+ this.hookInput = null;
323
+ this.ui.setFocus(this.editor);
324
+ this.ui.requestRender();
325
+ }
326
+ /**
327
+ * Show a notification for hooks.
328
+ */
329
+ showHookNotify(message, type) {
330
+ if (type === "error") {
331
+ this.showError(message);
332
+ }
333
+ else if (type === "warning") {
334
+ this.showWarning(message);
335
+ }
336
+ else {
337
+ this.showStatus(message);
338
+ }
339
+ }
340
+ /**
341
+ * Show a hook error in the UI.
342
+ */
343
+ showHookError(hookPath, error) {
344
+ const errorText = new Text(theme.fg("error", `Hook "${hookPath}" error: ${error}`), 1, 0);
345
+ this.chatContainer.addChild(errorText);
346
+ this.ui.requestRender();
347
+ }
348
+ /**
349
+ * Handle pi.send() from hooks.
350
+ * If streaming, queue the message. Otherwise, start a new agent loop.
351
+ */
352
+ handleHookSend(text, attachments) {
353
+ if (this.session.isStreaming) {
354
+ // Queue the message for later (note: attachments are lost when queuing)
355
+ this.session.queueMessage(text);
356
+ this.updatePendingMessagesDisplay();
357
+ }
358
+ else {
359
+ // Start a new agent loop immediately
360
+ this.session.prompt(text, { attachments }).catch((err) => {
361
+ this.showError(err instanceof Error ? err.message : String(err));
362
+ });
363
+ }
364
+ }
365
+ // =========================================================================
366
+ // Key Handlers
367
+ // =========================================================================
205
368
  setupKeyHandlers() {
206
369
  this.editor.onEscape = () => {
207
370
  if (this.loadingAnimation) {
@@ -350,6 +513,10 @@ export class InteractiveMode {
350
513
  return;
351
514
  }
352
515
  }
516
+ // Block input during compaction (will retry automatically)
517
+ if (this.session.isCompacting) {
518
+ return;
519
+ }
353
520
  // Queue message if agent is streaming
354
521
  if (this.session.isStreaming) {
355
522
  await this.session.queueMessage(text);
@@ -484,19 +651,21 @@ export class InteractiveMode {
484
651
  this.pendingTools.clear();
485
652
  this.ui.requestRender();
486
653
  break;
487
- case "auto_compaction_start":
654
+ case "auto_compaction_start": {
488
655
  // Set up escape to abort auto-compaction
489
656
  this.autoCompactionEscapeHandler = this.editor.onEscape;
490
657
  this.editor.onEscape = () => {
491
658
  this.session.abortCompaction();
492
659
  };
493
- // Show compacting indicator
660
+ // Show compacting indicator with reason
494
661
  this.statusContainer.clear();
495
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Auto-compacting... (esc to cancel)");
662
+ const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
663
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (esc to cancel)`);
496
664
  this.statusContainer.addChild(this.autoCompactionLoader);
497
665
  this.ui.requestRender();
498
666
  break;
499
- case "auto_compaction_end":
667
+ }
668
+ case "auto_compaction_end": {
500
669
  // Restore escape handler
501
670
  if (this.autoCompactionEscapeHandler) {
502
671
  this.editor.onEscape = this.autoCompactionEscapeHandler;
@@ -524,6 +693,7 @@ export class InteractiveMode {
524
693
  }
525
694
  this.ui.requestRender();
526
695
  break;
696
+ }
527
697
  }
528
698
  }
529
699
  /** Extract text content from a user message */
@@ -639,6 +809,13 @@ export class InteractiveMode {
639
809
  }
640
810
  renderInitialMessages(state) {
641
811
  this.renderMessages(state.messages, { updateFooter: true, populateHistory: true });
812
+ // Show compaction info if session was compacted
813
+ const entries = this.sessionManager.loadEntries();
814
+ const compactionCount = entries.filter((e) => e.type === "compaction").length;
815
+ if (compactionCount > 0) {
816
+ const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
817
+ this.showStatus(`Session compacted ${times}`);
818
+ }
642
819
  }
643
820
  async getUserInput() {
644
821
  return new Promise((resolve) => {
@@ -869,12 +1046,18 @@ export class InteractiveMode {
869
1046
  return;
870
1047
  }
871
1048
  this.showSelector((done) => {
872
- const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ index: m.entryIndex, text: m.text })), (entryIndex) => {
873
- const selectedText = this.session.branch(entryIndex);
1049
+ const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ index: m.entryIndex, text: m.text })), async (entryIndex) => {
1050
+ const result = await this.session.branch(entryIndex);
1051
+ if (result.skipped) {
1052
+ // Hook requested to skip conversation restore
1053
+ done();
1054
+ this.ui.requestRender();
1055
+ return;
1056
+ }
874
1057
  this.chatContainer.clear();
875
1058
  this.isFirstUserMessage = true;
876
1059
  this.renderInitialMessages(this.session.state);
877
- this.editor.setText(selectedText);
1060
+ this.editor.setText(result.selectedText);
878
1061
  done();
879
1062
  this.showStatus("Branched to new session");
880
1063
  }, () => {