@leanandmean/coding-agent 0.74.1-scramjet.72 → 0.74.1-scramjet.74

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 (35) hide show
  1. package/README.md +8 -1
  2. package/dist/core/agent-session.d.ts +19 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +293 -68
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/runner.d.ts +5 -3
  7. package/dist/core/extensions/runner.d.ts.map +1 -1
  8. package/dist/core/extensions/runner.js +18 -5
  9. package/dist/core/extensions/runner.js.map +1 -1
  10. package/dist/core/extensions/types.d.ts +9 -0
  11. package/dist/core/extensions/types.d.ts.map +1 -1
  12. package/dist/core/extensions/types.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  17. package/dist/modes/interactive/components/extension-editor.js +7 -3
  18. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  19. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  20. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.js +155 -34
  22. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  23. package/dist/modes/print-mode.d.ts.map +1 -1
  24. package/dist/modes/print-mode.js +35 -16
  25. package/dist/modes/print-mode.js.map +1 -1
  26. package/docs/compaction.md +6 -0
  27. package/docs/custom-provider.md +3 -1
  28. package/docs/extensions.md +13 -1
  29. package/docs/keybindings.md +1 -1
  30. package/docs/sdk.md +27 -0
  31. package/docs/settings.md +2 -0
  32. package/docs/terminal-setup.md +8 -4
  33. package/docs/terminal-verification.md +48 -0
  34. package/docs/tui.md +8 -0
  35. package/package.json +4 -4
@@ -11,7 +11,7 @@ import { getProviders, } from "@leanandmean/ai";
11
11
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, isKeyRelease, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, TUI_KEYBINDINGS, visibleWidth, } from "@leanandmean/tui";
12
12
  import { spawn, spawnSync } from "child_process";
13
13
  import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, VERSION } from "../../config.js";
14
- import { parseSkillBlock } from "../../core/agent-session.js";
14
+ import { parseSkillBlock, } from "../../core/agent-session.js";
15
15
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
16
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
17
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -130,6 +130,7 @@ export class InteractiveMode {
130
130
  version;
131
131
  isInitialized = false;
132
132
  onInputCallback;
133
+ promptSubmission;
133
134
  loadingAnimation = undefined;
134
135
  workingMessage = undefined;
135
136
  workingVisible = true;
@@ -877,7 +878,7 @@ export class InteractiveMode {
877
878
  // Process initial messages
878
879
  if (initialMessage) {
879
880
  try {
880
- await this.session.prompt(initialMessage, { images: initialImages });
881
+ await this.promptInteractive(initialMessage, initialImages);
881
882
  }
882
883
  catch (error) {
883
884
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -887,7 +888,7 @@ export class InteractiveMode {
887
888
  if (initialMessages) {
888
889
  for (const message of initialMessages) {
889
890
  try {
890
- await this.session.prompt(message);
891
+ await this.promptInteractive(message);
891
892
  }
892
893
  catch (error) {
893
894
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -899,7 +900,7 @@ export class InteractiveMode {
899
900
  while (true) {
900
901
  const userInput = await this.getUserInput();
901
902
  try {
902
- await this.session.prompt(userInput);
903
+ await this.promptInteractive(userInput);
903
904
  }
904
905
  catch (error) {
905
906
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -907,6 +908,71 @@ export class InteractiveMode {
907
908
  }
908
909
  }
909
910
  }
911
+ // SCRAMJET-DIVERGENCE: native consumers retain invocation identity through recovery.
912
+ async promptInteractive(text, images) {
913
+ const submission = { session: this.session, retried: false };
914
+ this.promptSubmission = submission;
915
+ const draftGeneration = this.clipboardPasteGeneration;
916
+ try {
917
+ await submission.session.prompt(text, {
918
+ images,
919
+ onRunSettlement: (settlement) => {
920
+ submission.settlement = settlement;
921
+ void settlement.catch(() => { });
922
+ },
923
+ });
924
+ const outcome = submission.settlement ? await submission.settlement : undefined;
925
+ if (this.promptSubmission !== submission || this.session !== submission.session)
926
+ return;
927
+ if (outcome?.status === "cancelled") {
928
+ this.showStatus("Execution cancelled. Review interrupted work before submitting a new prompt.");
929
+ }
930
+ else if (outcome?.status === "failed") {
931
+ const retryRecord = submission.session.sessionManager
932
+ .getBranch()
933
+ .reverse()
934
+ .find((entry) => entry.type === "custom" && entry.customType === "coding-agent:auto-retry");
935
+ const exhausted = submission.retryFailure &&
936
+ retryRecord?.type === "custom" &&
937
+ retryRecord.data?.outcome === "exhausted";
938
+ const prefix = exhausted ? "Recovery exhausted" : "Execution failed";
939
+ this.showError(`${prefix}: ${outcome.errorMessage}. Review the failure before trying again.`);
940
+ }
941
+ else if (outcome?.status === "completed" && submission.retried) {
942
+ this.showStatus("Execution recovered and continued.");
943
+ }
944
+ }
945
+ catch (error) {
946
+ if (this.promptSubmission !== submission || this.session !== submission.session)
947
+ return;
948
+ if (!this.editor.getText() && this.clipboardPasteGeneration === draftGeneration)
949
+ this.editor.setText(text);
950
+ this.showError(`${error instanceof Error ? error.message : String(error)}. Review the failure before trying again.`);
951
+ }
952
+ finally {
953
+ if (this.promptSubmission === submission && this.session === submission.session) {
954
+ this.promptSubmission = undefined;
955
+ this.clearRetryControls();
956
+ this.stopWorkingLoader();
957
+ if (this.settingsManager.getShowTerminalProgress())
958
+ this.ui.terminal.setProgress(false);
959
+ this.ui.requestRender();
960
+ }
961
+ }
962
+ }
963
+ clearRetryControls() {
964
+ if (this.retryEscapeHandler) {
965
+ this.defaultEditor.onEscape = this.retryEscapeHandler;
966
+ this.retryEscapeHandler = undefined;
967
+ }
968
+ this.retryCountdown?.dispose();
969
+ this.retryCountdown = undefined;
970
+ if (this.retryLoader) {
971
+ this.statusContainer.removeChild(this.retryLoader);
972
+ this.retryLoader.stop();
973
+ this.retryLoader = undefined;
974
+ }
975
+ }
910
976
  async checkForPackageUpdates() {
911
977
  // SCRAMJET-DIVERGENCE: prefer SCRAMJET_OFFLINE, fall back to PI_OFFLINE.
912
978
  if (process.env.SCRAMJET_OFFLINE || process.env.PI_OFFLINE) {
@@ -1513,6 +1579,13 @@ export class InteractiveMode {
1513
1579
  }
1514
1580
  }
1515
1581
  async rebindCurrentSession() {
1582
+ if (this.promptSubmission && this.promptSubmission.session !== this.session) {
1583
+ this.promptSubmission = undefined;
1584
+ this.clearRetryControls();
1585
+ this.stopWorkingLoader();
1586
+ if (this.settingsManager.getShowTerminalProgress())
1587
+ this.ui.terminal.setProgress(false);
1588
+ }
1516
1589
  this.unsubscribe?.();
1517
1590
  this.unsubscribe = undefined;
1518
1591
  this.applyRuntimeSettings();
@@ -2366,7 +2439,7 @@ export class InteractiveMode {
2366
2439
  this.pendingSelectorOpenGeneration = undefined;
2367
2440
  return;
2368
2441
  }
2369
- if (this.session.isStreaming) {
2442
+ if (this.promptSubmission?.session === this.session || this.session.isStreaming || this.session.isRetrying) {
2370
2443
  this.restoreQueuedMessagesToEditor({ abort: true });
2371
2444
  }
2372
2445
  else if (this.session.isBashRunning) {
@@ -2608,11 +2681,16 @@ export class InteractiveMode {
2608
2681
  if (this.session.isStreaming) {
2609
2682
  this.editor.addToHistory?.(text);
2610
2683
  this.editor.setText("");
2611
- await this.session.prompt(text, { streamingBehavior: "steer" });
2684
+ await this.submitQueuedPrompt(text, "steer");
2612
2685
  this.updatePendingMessagesDisplay();
2613
2686
  this.ui.requestRender();
2614
2687
  return;
2615
2688
  }
2689
+ if (!this.onInputCallback) {
2690
+ this.editor.setText(text);
2691
+ this.showWarning("Execution is still settling or recovering. Wait, or press Esc to cancel before submitting again.");
2692
+ return;
2693
+ }
2616
2694
  // Normal message submission
2617
2695
  // First, move any pending bash components to chat
2618
2696
  this.flushPendingBashComponents();
@@ -2823,10 +2901,10 @@ export class InteractiveMode {
2823
2901
  await Promise.all(pendingToolFinalizations);
2824
2902
  if (runGeneration !== this.agentRunGeneration || pendingToolFinalizations !== this.pendingToolFinalizations)
2825
2903
  break;
2826
- if (this.settingsManager.getShowTerminalProgress()) {
2904
+ if (this.promptSubmission?.session !== this.session && this.settingsManager.getShowTerminalProgress()) {
2827
2905
  this.ui.terminal.setProgress(false);
2828
2906
  }
2829
- if (this.loadingAnimation) {
2907
+ if (this.loadingAnimation && this.promptSubmission?.session !== this.session) {
2830
2908
  this.loadingAnimation.stop();
2831
2909
  this.loadingAnimation = undefined;
2832
2910
  this.statusContainer.clear();
@@ -2853,8 +2931,14 @@ export class InteractiveMode {
2853
2931
  }
2854
2932
  // Keep editor active; submissions are queued during compaction.
2855
2933
  this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
2934
+ const compactingSession = this.session;
2856
2935
  this.defaultEditor.onEscape = () => {
2857
- this.session.abortCompaction();
2936
+ if (this.session !== compactingSession)
2937
+ return;
2938
+ if (this.promptSubmission?.session === compactingSession)
2939
+ compactingSession.abortRetry();
2940
+ else
2941
+ compactingSession.abortCompaction();
2858
2942
  };
2859
2943
  this.statusContainer.clear();
2860
2944
  const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
@@ -2867,7 +2951,7 @@ export class InteractiveMode {
2867
2951
  break;
2868
2952
  }
2869
2953
  case "compaction_end": {
2870
- if (this.settingsManager.getShowTerminalProgress()) {
2954
+ if (this.promptSubmission?.session !== this.session && this.settingsManager.getShowTerminalProgress()) {
2871
2955
  this.ui.terminal.setProgress(false);
2872
2956
  }
2873
2957
  if (this.autoCompactionEscapeHandler) {
@@ -2908,9 +2992,14 @@ export class InteractiveMode {
2908
2992
  }
2909
2993
  case "auto_retry_start": {
2910
2994
  // Set up escape to abort retry
2995
+ if (this.promptSubmission?.session === this.session)
2996
+ this.promptSubmission.retried = true;
2997
+ this.clearRetryControls();
2911
2998
  this.retryEscapeHandler = this.defaultEditor.onEscape;
2999
+ const retrySession = this.session;
2912
3000
  this.defaultEditor.onEscape = () => {
2913
- this.session.abortRetry();
3001
+ if (this.session === retrySession)
3002
+ this.restoreQueuedMessagesToEditor({ abort: true });
2914
3003
  };
2915
3004
  // Show retry indicator
2916
3005
  this.statusContainer.clear();
@@ -2949,8 +3038,11 @@ export class InteractiveMode {
2949
3038
  this.statusContainer.clear();
2950
3039
  }
2951
3040
  // Show error only on final failure (success shows normal response)
2952
- if (!event.success) {
2953
- this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
3041
+ if (!event.success && this.promptSubmission?.session === this.session) {
3042
+ this.promptSubmission.retryFailure = event.finalError;
3043
+ }
3044
+ else if (!event.success) {
3045
+ this.showStatus("Recovery stopped. Review interrupted work before submitting a new prompt.");
2954
3046
  }
2955
3047
  this.ui.requestRender();
2956
3048
  break;
@@ -3265,21 +3357,25 @@ export class InteractiveMode {
3265
3357
  if (this.isShuttingDown)
3266
3358
  return;
3267
3359
  this.isShuttingDown = true;
3268
- this.unregisterSignalHandlers();
3269
- // Drain any in-flight Kitty key release events before stopping.
3270
- // This prevents escape sequences from leaking to the parent shell over slow SSH.
3271
- await this.ui.terminal.drainInput(1000);
3272
- await this.settingsManager.flush();
3273
3360
  let exitCode = 0;
3274
3361
  try {
3275
- this.stop({ retainContent: this.settingsManager.getRetainTranscriptOnExit() });
3362
+ await this.ui.terminal.drainInput(1000);
3363
+ await this.settingsManager.flush();
3364
+ try {
3365
+ this.stop({ retainContent: this.settingsManager.getRetainTranscriptOnExit() });
3366
+ }
3367
+ catch (error) {
3368
+ await this.ui.terminal.flush?.();
3369
+ throw error;
3370
+ }
3371
+ await this.ui.terminal.flush?.();
3276
3372
  }
3277
3373
  catch (error) {
3278
3374
  exitCode = 1;
3279
- console.error(`Could not retain terminal history: ${error instanceof Error ? error.message : String(error)}`);
3375
+ console.error(`Could not stop terminal safely: ${error instanceof Error ? error.message : String(error)}`);
3280
3376
  }
3281
- await this.ui.terminal.flush?.();
3282
3377
  await this.runtimeHost.dispose();
3378
+ this.unregisterSignalHandlers();
3283
3379
  process.exit(exitCode);
3284
3380
  }
3285
3381
  emergencyTerminalExit() {
@@ -3317,7 +3413,9 @@ export class InteractiveMode {
3317
3413
  try {
3318
3414
  this.ui.stop();
3319
3415
  }
3320
- catch { }
3416
+ catch (releaseError) {
3417
+ console.error("Terminal release failed; exiting without restart:", releaseError);
3418
+ }
3321
3419
  console.error("pi exiting due to uncaughtException:");
3322
3420
  console.error(error);
3323
3421
  process.exit(1);
@@ -3384,12 +3482,13 @@ export class InteractiveMode {
3384
3482
  const ignoreSigint = () => { };
3385
3483
  process.on("SIGINT", ignoreSigint);
3386
3484
  // Set up handler to restore TUI when resumed
3387
- process.once("SIGCONT", () => {
3485
+ const resume = () => {
3388
3486
  clearInterval(suspendKeepAlive);
3389
3487
  process.removeListener("SIGINT", ignoreSigint);
3390
3488
  this.ui.start();
3391
3489
  this.ui.rebuild();
3392
- });
3490
+ };
3491
+ process.once("SIGCONT", resume);
3393
3492
  try {
3394
3493
  // SCRAMJET-DIVERGENCE: drain releases before handing keyboard ownership back to the shell.
3395
3494
  await this.ui.terminal.drainInput();
@@ -3400,9 +3499,24 @@ export class InteractiveMode {
3400
3499
  catch (error) {
3401
3500
  clearInterval(suspendKeepAlive);
3402
3501
  process.removeListener("SIGINT", ignoreSigint);
3502
+ process.removeListener("SIGCONT", resume);
3403
3503
  throw error;
3404
3504
  }
3405
3505
  }
3506
+ async submitQueuedPrompt(text, streamingBehavior) {
3507
+ const session = this.session;
3508
+ const generation = this.clipboardPasteGeneration;
3509
+ try {
3510
+ await session.prompt(text, { streamingBehavior });
3511
+ }
3512
+ catch (error) {
3513
+ if (this.session !== session)
3514
+ return;
3515
+ if (!this.editor.getText() && generation === this.clipboardPasteGeneration)
3516
+ this.editor.setText(text);
3517
+ this.showError(`${error instanceof Error ? error.message : String(error)}. Wait or cancel recovery before submitting again.`);
3518
+ }
3519
+ }
3406
3520
  async handleFollowUp() {
3407
3521
  const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
3408
3522
  if (!text)
@@ -3425,14 +3539,14 @@ export class InteractiveMode {
3425
3539
  if (this.session.isStreaming) {
3426
3540
  this.editor.addToHistory?.(text);
3427
3541
  this.editor.setText("");
3428
- await this.session.prompt(text, { streamingBehavior: "followUp" });
3542
+ await this.submitQueuedPrompt(text, "followUp");
3429
3543
  this.updatePendingMessagesDisplay();
3430
3544
  this.ui.requestRender();
3431
3545
  }
3432
3546
  // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
3433
3547
  else if (this.editor.onSubmit) {
3434
3548
  this.editor.setText("");
3435
- this.editor.onSubmit(text);
3549
+ await this.editor.onSubmit(text);
3436
3550
  }
3437
3551
  }
3438
3552
  handleDequeue() {
@@ -3523,12 +3637,14 @@ export class InteractiveMode {
3523
3637
  }
3524
3638
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
3525
3639
  const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
3640
+ let released = false;
3526
3641
  try {
3527
3642
  // Write current content to temp file
3528
3643
  fs.writeFileSync(tmpFile, currentText, "utf-8");
3529
3644
  // SCRAMJET-DIVERGENCE: the editor must not inherit the key release that opened it.
3530
3645
  await this.ui.terminal.drainInput();
3531
3646
  this.ui.stop();
3647
+ released = true;
3532
3648
  // Split by space to support editor arguments (e.g., "code --wait")
3533
3649
  const [editor, ...editorArgs] = editorCmd.split(" ");
3534
3650
  // Spawn editor synchronously with inherited stdio for interactive editing
@@ -3551,9 +3667,11 @@ export class InteractiveMode {
3551
3667
  catch {
3552
3668
  // Ignore cleanup errors
3553
3669
  }
3554
- // Restart TUI
3555
- this.ui.start();
3556
- this.ui.rebuild();
3670
+ // SCRAMJET-DIVERGENCE: failed release is not permission to reacquire inherited stdin.
3671
+ if (released) {
3672
+ this.ui.start();
3673
+ this.ui.rebuild();
3674
+ }
3557
3675
  }
3558
3676
  }
3559
3677
  // =========================================================================
@@ -3647,7 +3765,7 @@ export class InteractiveMode {
3647
3765
  if (allQueued.length === 0) {
3648
3766
  this.updatePendingMessagesDisplay();
3649
3767
  if (options?.abort) {
3650
- this.agent.abort();
3768
+ this.session.abortRetry();
3651
3769
  }
3652
3770
  return 0;
3653
3771
  }
@@ -3657,7 +3775,7 @@ export class InteractiveMode {
3657
3775
  this.editor.setText(combinedText);
3658
3776
  this.updatePendingMessagesDisplay();
3659
3777
  if (options?.abort) {
3660
- this.agent.abort();
3778
+ this.session.abortRetry();
3661
3779
  }
3662
3780
  return allQueued.length;
3663
3781
  }
@@ -5223,17 +5341,20 @@ export class InteractiveMode {
5223
5341
  }
5224
5342
  }
5225
5343
  stop(options) {
5344
+ let released = false;
5226
5345
  try {
5227
5346
  if (this.isInitialized) {
5228
- this.isInitialized = false;
5229
5347
  this.ui.stop(options);
5348
+ this.isInitialized = false;
5230
5349
  }
5350
+ released = true;
5231
5351
  }
5232
5352
  finally {
5233
5353
  this.selectorOpenGeneration++;
5234
5354
  this.pendingSelectorOpenGeneration = undefined;
5235
- this.unregisterSignalHandlers();
5236
- if (this.settingsManager.getShowTerminalProgress()) {
5355
+ if (!this.isShuttingDown)
5356
+ this.unregisterSignalHandlers();
5357
+ if (released && this.settingsManager.getShowTerminalProgress()) {
5237
5358
  this.ui.terminal.setProgress(false);
5238
5359
  }
5239
5360
  if (this.loadingAnimation) {