@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3

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 (154) hide show
  1. package/CHANGELOG.md +113 -1
  2. package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +2 -2
  6. package/dist/types/config/model-provider-priority.d.ts +1 -0
  7. package/dist/types/config/model-resolver.d.ts +4 -1
  8. package/dist/types/config/settings.d.ts +7 -2
  9. package/dist/types/debug/report-bundle.d.ts +3 -0
  10. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  11. package/dist/types/edit/index.d.ts +0 -1
  12. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  14. package/dist/types/lsp/client.d.ts +10 -0
  15. package/dist/types/lsp/index.d.ts +0 -5
  16. package/dist/types/main.d.ts +14 -9
  17. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  18. package/dist/types/modes/components/assistant-message.d.ts +0 -9
  19. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  20. package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
  21. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  22. package/dist/types/modes/components/session-selector.d.ts +16 -7
  23. package/dist/types/modes/components/status-line.d.ts +2 -0
  24. package/dist/types/modes/components/tool-execution.d.ts +0 -18
  25. package/dist/types/modes/controllers/event-controller.d.ts +17 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -0
  27. package/dist/types/modes/magic-keywords.d.ts +1 -1
  28. package/dist/types/modes/markdown-prose.d.ts +1 -1
  29. package/dist/types/modes/types.d.ts +7 -0
  30. package/dist/types/modes/workflow.d.ts +3 -3
  31. package/dist/types/session/auth-storage.d.ts +1 -1
  32. package/dist/types/session/messages.d.ts +11 -8
  33. package/dist/types/session/session-manager.d.ts +5 -2
  34. package/dist/types/session/yield-queue.d.ts +10 -1
  35. package/dist/types/task/executor.d.ts +10 -0
  36. package/dist/types/tools/eval-render.d.ts +0 -1
  37. package/dist/types/tools/eval.d.ts +8 -0
  38. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  39. package/dist/types/tools/github-cache.d.ts +12 -0
  40. package/dist/types/tools/index.d.ts +31 -0
  41. package/dist/types/tools/path-utils.d.ts +13 -1
  42. package/dist/types/tools/read.d.ts +2 -1
  43. package/dist/types/tools/render-utils.d.ts +3 -1
  44. package/dist/types/tools/renderers.d.ts +0 -15
  45. package/dist/types/tools/search.d.ts +2 -2
  46. package/dist/types/tools/write.d.ts +0 -2
  47. package/dist/types/tools/yield.d.ts +8 -0
  48. package/dist/types/tui/code-cell.d.ts +0 -2
  49. package/dist/types/tui/hyperlink.d.ts +5 -7
  50. package/dist/types/tui/output-block.d.ts +0 -18
  51. package/package.json +9 -9
  52. package/src/cli/args.ts +3 -1
  53. package/src/cli/dry-balance-cli.ts +2 -4
  54. package/src/cli/gallery-cli.ts +4 -0
  55. package/src/cli/gallery-fixtures/codeintel.ts +0 -1
  56. package/src/cli/gallery-fixtures/fs.ts +68 -1
  57. package/src/cli/gallery-fixtures/types.ts +8 -1
  58. package/src/cli/startup-cwd.ts +68 -0
  59. package/src/commands/launch.ts +3 -0
  60. package/src/commit/agentic/agent.ts +1 -0
  61. package/src/commit/model-selection.ts +3 -2
  62. package/src/config/model-provider-priority.ts +55 -0
  63. package/src/config/model-registry.ts +4 -22
  64. package/src/config/model-resolver.ts +39 -7
  65. package/src/config/settings.ts +86 -41
  66. package/src/debug/index.ts +8 -0
  67. package/src/debug/raw-sse-buffer.ts +7 -4
  68. package/src/debug/report-bundle.ts +9 -0
  69. package/src/edit/file-snapshot-store.ts +33 -1
  70. package/src/edit/hashline/diff.ts +86 -0
  71. package/src/edit/hashline/execute.ts +14 -1
  72. package/src/edit/hashline/filesystem.ts +2 -1
  73. package/src/edit/index.ts +31 -17
  74. package/src/edit/renderer.ts +116 -31
  75. package/src/eval/__tests__/llm-bridge.test.ts +20 -0
  76. package/src/eval/js/context-manager.ts +32 -15
  77. package/src/eval/js/shared/prelude.txt +26 -10
  78. package/src/eval/llm-bridge.ts +14 -3
  79. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  80. package/src/eval/py/executor.ts +23 -11
  81. package/src/eval/py/prelude.py +1 -1
  82. package/src/extensibility/extensions/types.ts +10 -1
  83. package/src/internal-urls/docs-index.generated.ts +7 -7
  84. package/src/lsp/client.ts +23 -11
  85. package/src/lsp/config.ts +11 -1
  86. package/src/lsp/index.ts +189 -61
  87. package/src/main.ts +144 -78
  88. package/src/mcp/tool-bridge.ts +2 -0
  89. package/src/memories/index.ts +2 -2
  90. package/src/modes/components/assistant-message.ts +3 -15
  91. package/src/modes/components/custom-editor.ts +143 -111
  92. package/src/modes/components/late-diagnostics-message.ts +60 -0
  93. package/src/modes/components/model-selector.ts +59 -13
  94. package/src/modes/components/oauth-selector.ts +33 -7
  95. package/src/modes/components/plan-review-overlay.ts +26 -5
  96. package/src/modes/components/read-tool-group.ts +415 -35
  97. package/src/modes/components/session-selector.ts +89 -35
  98. package/src/modes/components/status-line.ts +19 -4
  99. package/src/modes/components/tips.txt +1 -1
  100. package/src/modes/components/tool-execution.ts +7 -49
  101. package/src/modes/components/transcript-container.ts +108 -32
  102. package/src/modes/components/user-message.ts +1 -1
  103. package/src/modes/controllers/event-controller.ts +32 -1
  104. package/src/modes/controllers/input-controller.ts +56 -9
  105. package/src/modes/interactive-mode.ts +107 -20
  106. package/src/modes/magic-keywords.ts +1 -1
  107. package/src/modes/markdown-prose.ts +1 -1
  108. package/src/modes/theme/shimmer.ts +20 -9
  109. package/src/modes/types.ts +7 -0
  110. package/src/modes/utils/ui-helpers.ts +26 -5
  111. package/src/modes/workflow.ts +10 -10
  112. package/src/prompts/system/manual-continue.md +7 -0
  113. package/src/prompts/system/plan-mode-active.md +56 -72
  114. package/src/prompts/system/workflow-notice.md +1 -1
  115. package/src/prompts/tools/bash.md +9 -0
  116. package/src/prompts/tools/browser.md +1 -1
  117. package/src/prompts/tools/eval.md +5 -2
  118. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  119. package/src/prompts/tools/read.md +2 -2
  120. package/src/sdk.ts +85 -10
  121. package/src/session/agent-session.ts +42 -15
  122. package/src/session/auth-storage.ts +2 -0
  123. package/src/session/messages.ts +21 -14
  124. package/src/session/session-manager.ts +98 -25
  125. package/src/session/yield-queue.ts +20 -2
  126. package/src/task/executor.ts +72 -36
  127. package/src/task/render.ts +3 -4
  128. package/src/tiny/title-client.ts +6 -1
  129. package/src/tools/bash.ts +7 -7
  130. package/src/tools/browser/tab-supervisor.ts +13 -1
  131. package/src/tools/browser/tab-worker.ts +33 -4
  132. package/src/tools/eval-render.ts +4 -23
  133. package/src/tools/eval.ts +13 -2
  134. package/src/tools/find.ts +148 -99
  135. package/src/tools/gh-cache-invalidation.ts +200 -0
  136. package/src/tools/github-cache.ts +25 -0
  137. package/src/tools/index.ts +32 -0
  138. package/src/tools/inspect-image.ts +2 -2
  139. package/src/tools/path-utils.ts +47 -24
  140. package/src/tools/plan-mode-guard.ts +52 -7
  141. package/src/tools/read.ts +41 -20
  142. package/src/tools/render-utils.ts +3 -1
  143. package/src/tools/renderers.ts +0 -15
  144. package/src/tools/search.ts +38 -3
  145. package/src/tools/ssh.ts +0 -1
  146. package/src/tools/todo.ts +1 -0
  147. package/src/tools/write.ts +5 -14
  148. package/src/tools/yield.ts +10 -1
  149. package/src/tui/code-cell.ts +1 -6
  150. package/src/tui/hyperlink.ts +13 -23
  151. package/src/tui/output-block.ts +2 -97
  152. package/src/utils/commit-message-generator.ts +2 -2
  153. package/src/utils/enhanced-paste.ts +30 -2
  154. package/src/web/search/providers/codex.ts +37 -8
@@ -10,6 +10,7 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
10
10
  import { materializeImageReferenceLinks } from "../../modes/image-references";
11
11
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
12
12
  import type { InteractiveModeContext } from "../../modes/types";
13
+ import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
13
14
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails, USER_INTERRUPT_LABEL } from "../../session/messages";
14
15
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
15
16
  import { isTinyTitleLocalModelKey } from "../../tiny/models";
@@ -94,6 +95,7 @@ export class InputController {
94
95
  if (this.ctx.loopModeEnabled) {
95
96
  this.ctx.pauseLoop();
96
97
  if (this.ctx.session.isStreaming) {
98
+ this.ctx.notifyInterrupting();
97
99
  void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
98
100
  } else {
99
101
  this.ctx.cancelPendingSubmission();
@@ -124,6 +126,7 @@ export class InputController {
124
126
  this.ctx.isPythonMode = false;
125
127
  this.ctx.updateEditorBorderColor();
126
128
  } else if (this.ctx.session.isStreaming) {
129
+ this.ctx.notifyInterrupting();
127
130
  void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
128
131
  } else if (!this.ctx.editor.getText().trim()) {
129
132
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
@@ -284,14 +287,21 @@ export class InputController {
284
287
 
285
288
  if (!text) return;
286
289
 
287
- // Continue shortcuts: "." or "c" sends empty message (agent continues, no visible message)
290
+ // Continue shortcuts: "." or "c" resume the agent with a hidden agent-authored
291
+ // developer directive (no visible user message) instead of an empty turn, so the
292
+ // model continues the prior intent rather than second-guessing the interrupt.
288
293
  if (text === "." || text === "c") {
289
294
  if (this.ctx.onInputCallback) {
290
295
  this.ctx.editor.setText("");
291
296
  this.ctx.pendingImages = [];
292
297
  this.ctx.pendingImageLinks = [];
293
298
  this.ctx.editor.imageLinks = undefined;
294
- this.ctx.onInputCallback({ text: "", cancelled: false, started: true });
299
+ this.ctx.onInputCallback({
300
+ text: manualContinuePrompt,
301
+ cancelled: false,
302
+ started: true,
303
+ synthetic: true,
304
+ });
295
305
  }
296
306
  return;
297
307
  }
@@ -499,17 +509,44 @@ export class InputController {
499
509
  }
500
510
 
501
511
  handleCtrlZ(): void {
502
- // Set up handler to restore TUI when resumed
503
- process.once("SIGCONT", () => {
512
+ // SIGTSTP is POSIX job-control: Windows has no equivalent and
513
+ // `process.kill(_, "SIGTSTP")` throws `TypeError: Unknown signal:
514
+ // SIGTSTP` there, taking the whole agent down via an uncaught
515
+ // exception (issue #2036). No-op on platforms that cannot suspend.
516
+ if (process.platform === "win32") {
517
+ this.ctx.showStatus("Suspend (Ctrl+Z) is not supported on this platform");
518
+ return;
519
+ }
520
+
521
+ // Capture the listener so we can detach it if the signal never
522
+ // fires; otherwise a failed suspend would leave a stale SIGCONT
523
+ // handler that fires on the next unrelated continue and tries to
524
+ // re-`start()` an already-running TUI.
525
+ const onResume = (): void => {
504
526
  this.ctx.ui.start();
505
527
  this.ctx.ui.requestRender(true);
506
- });
528
+ };
529
+ process.once("SIGCONT", onResume);
507
530
 
508
- // Stop the TUI (restore terminal to normal mode)
531
+ // Stop the TUI (restore terminal to normal mode) before sending the
532
+ // signal so the parent shell sees a sane terminal state.
509
533
  this.ctx.ui.stop();
510
534
 
511
- // Send SIGTSTP to process group (pid=0 means all processes in group)
512
- process.kill(0, "SIGTSTP");
535
+ try {
536
+ // pid=0 → entire foreground process group; the shell receives
537
+ // SIGTSTP and parks the job.
538
+ process.kill(0, "SIGTSTP");
539
+ } catch (err) {
540
+ // Either the runtime refused the signal or the kernel rejected
541
+ // it (some sandboxes block sending to pid=0). Tear the resume
542
+ // hook down and bring the TUI back so the user is not stranded
543
+ // on a frozen prompt.
544
+ process.removeListener("SIGCONT", onResume);
545
+ this.ctx.ui.start();
546
+ this.ctx.ui.requestRender(true);
547
+ const reason = err instanceof Error ? err.message : String(err);
548
+ this.ctx.showError(`Failed to suspend: ${reason}`);
549
+ }
513
550
  }
514
551
 
515
552
  handleDequeue(): void {
@@ -589,7 +626,7 @@ export class InputController {
589
626
 
590
627
  /** Send editor text as a follow-up message (queued behind current stream). */
591
628
  async handleFollowUp(): Promise<void> {
592
- const text = this.ctx.editor.getText().trim();
629
+ let text = this.ctx.editor.getText().trim();
593
630
  if (!text) return;
594
631
 
595
632
  // Compaction first: while compacting, free text gets queued via
@@ -603,6 +640,16 @@ export class InputController {
603
640
  return;
604
641
  }
605
642
 
643
+ const slashResult = await executeBuiltinSlashCommand(text, {
644
+ ctx: this.ctx,
645
+ });
646
+ if (slashResult === true) {
647
+ return;
648
+ }
649
+ if (typeof slashResult === "string") {
650
+ text = slashResult;
651
+ }
652
+
606
653
  // Skill commands invoke through the custom-message path regardless of
607
654
  // which keybinding submitted them. Enter routes them as `steer`;
608
655
  // Ctrl+Enter (this handler) routes them as `followUp`.
@@ -50,7 +50,7 @@ import chalk from "chalk";
50
50
  import { reset as resetCapabilities } from "../capability";
51
51
  import { KeybindingsManager } from "../config/keybindings";
52
52
  import { MODEL_ROLES, type ModelRole } from "../config/model-registry";
53
- import { isSettingsInitialized, Settings, settings } from "../config/settings";
53
+ import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
54
54
  import { clearClaudePluginRootsCache } from "../discovery/helpers";
55
55
  import type {
56
56
  ContextUsage,
@@ -125,7 +125,7 @@ import {
125
125
  import { OAuthManualInputManager } from "./oauth-manual-input";
126
126
  import { SessionObserverRegistry } from "./session-observer-registry";
127
127
  import { interruptHint } from "./shared";
128
- import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
128
+ import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
129
129
  import type { Theme } from "./theme/theme";
130
130
  import {
131
131
  getEditorTheme,
@@ -157,6 +157,12 @@ interface WorkingMessageAccent {
157
157
  dim: string;
158
158
  }
159
159
 
160
+ interface WorkingMessageAccentCacheKey {
161
+ sessionName: string | undefined;
162
+ accentSurfaceLuminance: number | undefined;
163
+ sessionAccentEnabled: boolean;
164
+ }
165
+
160
166
  function renderWorkingMessage(message: string, accent?: WorkingMessageAccent): string {
161
167
  const palette = accent
162
168
  ? ({
@@ -301,6 +307,9 @@ export class InteractiveMode implements InteractiveModeContext {
301
307
  autoCompactionLoader: Loader | undefined = undefined;
302
308
  retryLoader: Loader | undefined = undefined;
303
309
  #pendingWorkingMessage: string | undefined;
310
+ #workingMessageAccentCacheKey?: WorkingMessageAccentCacheKey;
311
+ #workingMessageAccentCacheValue?: WorkingMessageAccent;
312
+ #workingMessageAccentCacheHasValue = false;
304
313
  get #defaultWorkingMessage(): string {
305
314
  return `Working…${interruptHint()}`;
306
315
  }
@@ -638,9 +647,17 @@ export class InteractiveMode implements InteractiveModeContext {
638
647
  this.session.subscribe(event => {
639
648
  void this.#handleGoalSessionEvent(event);
640
649
  }),
650
+ this.sessionManager.onSessionNameChanged(() => {
651
+ this.#handleSessionAccentInputsChanged();
652
+ }),
653
+ onStatusLineSessionAccentChanged(() => {
654
+ this.#syncStatusLineSettings();
655
+ this.#handleSessionAccentInputsChanged();
656
+ }),
641
657
  );
642
658
  // Set up theme file watcher
643
659
  onThemeChange(() => {
660
+ this.#clearWorkingMessageAccentCache();
644
661
  clearRenderCache();
645
662
  this.ui.invalidate();
646
663
  this.updateEditorBorderColor();
@@ -965,9 +982,7 @@ export class InteractiveMode implements InteractiveModeContext {
965
982
  this.#goalContinuationTurnInFlight = false;
966
983
  }
967
984
  if (this.loadingAnimation) {
968
- this.loadingAnimation.stop();
969
- this.loadingAnimation = undefined;
970
- this.statusContainer.clear();
985
+ this.#stopLoadingAnimation(true);
971
986
  }
972
987
  if (!submission.customType) {
973
988
  this.pendingImages = submission.images ? [...submission.images] : [];
@@ -1005,9 +1020,7 @@ export class InteractiveMode implements InteractiveModeContext {
1005
1020
  pendingSubmissionDispose?.();
1006
1021
  this.#pendingWorkingMessage = undefined;
1007
1022
  if (this.loadingAnimation) {
1008
- this.loadingAnimation.stop();
1009
- this.loadingAnimation = undefined;
1010
- this.statusContainer.clear();
1023
+ this.#stopLoadingAnimation(true);
1011
1024
  }
1012
1025
  }
1013
1026
  }
@@ -1023,6 +1036,24 @@ export class InteractiveMode implements InteractiveModeContext {
1023
1036
  this.editor.setMaxHeight(this.#computeEditorMaxHeight());
1024
1037
  }
1025
1038
 
1039
+ #syncStatusLineSettings(): void {
1040
+ this.statusLine.updateSettings({
1041
+ preset: settings.get("statusLine.preset"),
1042
+ leftSegments: settings.get("statusLine.leftSegments"),
1043
+ rightSegments: settings.get("statusLine.rightSegments"),
1044
+ separator: settings.get("statusLine.separator"),
1045
+ showHookStatus: settings.get("statusLine.showHookStatus"),
1046
+ sessionAccent: settings.get("statusLine.sessionAccent"),
1047
+ segmentOptions: settings.get("statusLine.segmentOptions"),
1048
+ });
1049
+ }
1050
+
1051
+ #handleSessionAccentInputsChanged(): void {
1052
+ this.#clearWorkingMessageAccentCache();
1053
+ this.statusLine.invalidate();
1054
+ this.updateEditorBorderColor();
1055
+ }
1056
+
1026
1057
  updateEditorBorderColor(): void {
1027
1058
  if (this.isBashMode) {
1028
1059
  this.editor.borderColor = theme.getBashModeBorderColor();
@@ -2416,8 +2447,7 @@ export class InteractiveMode implements InteractiveModeContext {
2416
2447
 
2417
2448
  stop(): void {
2418
2449
  if (this.loadingAnimation) {
2419
- this.loadingAnimation.stop();
2420
- this.loadingAnimation = undefined;
2450
+ this.#stopLoadingAnimation(false);
2421
2451
  }
2422
2452
  this.#cleanupMicAnimation();
2423
2453
  this.#cancelTodoAutoClearTimer();
@@ -2581,9 +2611,7 @@ export class InteractiveMode implements InteractiveModeContext {
2581
2611
  this.#pendingSubmissionDispose = undefined;
2582
2612
  this.#pendingWorkingMessage = undefined;
2583
2613
  if (this.loadingAnimation) {
2584
- this.loadingAnimation.stop();
2585
- this.loadingAnimation = undefined;
2586
- this.statusContainer.clear();
2614
+ this.#stopLoadingAnimation(true);
2587
2615
  }
2588
2616
  this.#uiHelpers.showError(message);
2589
2617
  }
@@ -2646,24 +2674,69 @@ export class InteractiveMode implements InteractiveModeContext {
2646
2674
  this.ui.requestRender();
2647
2675
  }
2648
2676
 
2677
+ #clearWorkingMessageAccentCache(): void {
2678
+ this.#workingMessageAccentCacheKey = undefined;
2679
+ this.#workingMessageAccentCacheValue = undefined;
2680
+ this.#workingMessageAccentCacheHasValue = false;
2681
+ }
2682
+
2683
+ #buildWorkingMessageAccentCacheKey(): WorkingMessageAccentCacheKey {
2684
+ const sessionAccentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
2685
+ return {
2686
+ sessionAccentEnabled,
2687
+ sessionName: sessionAccentEnabled ? this.sessionManager.getSessionName() : undefined,
2688
+ accentSurfaceLuminance: theme.accentSurfaceLuminance,
2689
+ };
2690
+ }
2691
+
2692
+ #workingMessageAccentCacheKeyEquals(a: WorkingMessageAccentCacheKey, b: WorkingMessageAccentCacheKey): boolean {
2693
+ return (
2694
+ a.sessionName === b.sessionName &&
2695
+ a.accentSurfaceLuminance === b.accentSurfaceLuminance &&
2696
+ a.sessionAccentEnabled === b.sessionAccentEnabled
2697
+ );
2698
+ }
2699
+
2700
+ #cacheWorkingMessageAccent(
2701
+ key: WorkingMessageAccentCacheKey,
2702
+ value: WorkingMessageAccent | undefined,
2703
+ ): WorkingMessageAccent | undefined {
2704
+ this.#workingMessageAccentCacheKey = key;
2705
+ this.#workingMessageAccentCacheValue = value;
2706
+ this.#workingMessageAccentCacheHasValue = true;
2707
+ return value;
2708
+ }
2709
+
2649
2710
  #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
2650
- const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
2651
- const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
2652
- if (!sessionName) return undefined;
2653
- const hex = getSessionAccentHex(sessionName, theme.accentSurfaceLuminance);
2711
+ const key = this.#buildWorkingMessageAccentCacheKey();
2712
+ if (
2713
+ this.#workingMessageAccentCacheHasValue &&
2714
+ this.#workingMessageAccentCacheKey &&
2715
+ this.#workingMessageAccentCacheKeyEquals(key, this.#workingMessageAccentCacheKey)
2716
+ ) {
2717
+ return this.#workingMessageAccentCacheValue;
2718
+ }
2719
+ if (!key.sessionAccentEnabled || !key.sessionName) {
2720
+ return this.#cacheWorkingMessageAccent(key, undefined);
2721
+ }
2722
+ const hex = getSessionAccentHex(key.sessionName, key.accentSurfaceLuminance);
2654
2723
  const main = getSessionAccentAnsi(hex);
2655
2724
  const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
2656
- return main && dim ? { main, dim } : undefined;
2725
+ return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
2657
2726
  }
2658
2727
 
2659
2728
  ensureLoadingAnimation(): void {
2660
2729
  if (!this.loadingAnimation) {
2730
+ this.#clearWorkingMessageAccentCache();
2661
2731
  this.statusContainer.clear();
2662
2732
  const messageColorFn = ((message: string) =>
2663
2733
  renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
2664
- animated: true;
2734
+ animated?: true;
2665
2735
  };
2666
- messageColorFn.animated = true;
2736
+ // Shimmer drives the 30fps redraw; when it is disabled the working
2737
+ // message is static, so leave `animated` unset and let the loader use
2738
+ // the spinner-only ~12.5fps cadence instead of repainting a frozen line.
2739
+ if (shimmerEnabled()) messageColorFn.animated = true;
2667
2740
  this.loadingAnimation = new Loader(
2668
2741
  this.ui,
2669
2742
  spinner => {
@@ -2680,6 +2753,16 @@ export class InteractiveMode implements InteractiveModeContext {
2680
2753
  this.applyPendingWorkingMessage();
2681
2754
  }
2682
2755
 
2756
+ #stopLoadingAnimation(clearStatusContainer: boolean): void {
2757
+ if (!this.loadingAnimation) return;
2758
+ this.loadingAnimation.stop();
2759
+ this.loadingAnimation = undefined;
2760
+ this.#clearWorkingMessageAccentCache();
2761
+ if (clearStatusContainer) {
2762
+ this.statusContainer.clear();
2763
+ }
2764
+ }
2765
+
2683
2766
  setWorkingMessage(message?: string): void {
2684
2767
  if (message === undefined) {
2685
2768
  this.#pendingWorkingMessage = undefined;
@@ -2707,6 +2790,10 @@ export class InteractiveMode implements InteractiveModeContext {
2707
2790
  this.setWorkingMessage(message);
2708
2791
  }
2709
2792
 
2793
+ notifyInterrupting(): void {
2794
+ this.#eventController.notifyInterrupting();
2795
+ }
2796
+
2710
2797
  showNewVersionNotification(newVersion: string): void {
2711
2798
  this.#uiHelpers.showNewVersionNotification(newVersion);
2712
2799
  }
@@ -4,7 +4,7 @@ import { highlightWorkflow } from "./workflow";
4
4
 
5
5
  /**
6
6
  * Gradient-highlight every magic keyword ("ultrathink", "orchestrate",
7
- * "workflow") that appears as standalone prose, skipping any occurrence inside a
7
+ * "workflowz") that appears as standalone prose, skipping any occurrence inside a
8
8
  * code block, inline code span, or XML/HTML section. Each highlighter paints its
9
9
  * own keyword with its own gradient, so chaining is order-independent — the
10
10
  * earlier passes only inject zero-width SGR escapes (no backticks or angle
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Markdown structure awareness for the magic-keyword affordances
3
- * ("ultrathink"/"orchestrate"/"workflow").
3
+ * ("ultrathink"/"orchestrate"/"workflowz").
4
4
  *
5
5
  * Keyword detection and editor/transcript highlighting must fire only on prose
6
6
  * the user is actually addressing to the model — never on a word that happens to
@@ -1,14 +1,20 @@
1
1
  import { isSettingsInitialized, settings } from "../../config/settings";
2
2
  import type { Theme, ThemeColor } from "./theme";
3
3
 
4
+ // ─── Animation velocity ──────────────────────────────────────────────────────
5
+ // Band/head travel speed in border cells per second. Driving position by a fixed
6
+ // velocity — instead of dividing a fixed sweep duration by the (length-derived)
7
+ // period — makes smoothness independent of message length: at the loader's
8
+ // default 30fps redraw cadence the band advances ≤1 cell per frame for any
9
+ // string, so it never visibly steps. Sweep/round-trip durations now scale with
10
+ // length. Keep ≤ the animated redraw fps (loader RENDER_INTERVAL_MS = 1000/30).
11
+ const SHIMMER_SPEED_CELLS_PER_S = 30;
12
+
4
13
  // ─── Classic sweep tunables ──────────────────────────────────────────────────
5
14
  const CLASSIC_PADDING = 10;
6
- const CLASSIC_SWEEP_MS = 1400;
7
15
  const CLASSIC_BAND_HALF_WIDTH = 6;
8
16
 
9
17
  // ─── KITT scanner tunables ───────────────────────────────────────────────────
10
- // 1.5s round trip ≈ classic 1982 K.I.T.T. scanner cadence (~0.75s per direction).
11
- const KITT_CYCLE_MS = 1500;
12
18
  const KITT_HEAD_HALF = 0.6;
13
19
  const KITT_TRAIL_LEN = 7;
14
20
 
@@ -103,9 +109,10 @@ function compile(theme: ShimmerTheme, palette: ShimmerPalette): CompiledPalette
103
109
  /** Smooth cosine bump sweeping left → right with edge padding. */
104
110
  function classicIntensity(time: number, index: number, length: number): number {
105
111
  const period = length + CLASSIC_PADDING * 2;
106
- // Fractional position — kept un-floored so the band glides at the host's
107
- // frame rate instead of stepping discretely.
108
- const pos = ((time % CLASSIC_SWEEP_MS) / CLASSIC_SWEEP_MS) * period;
112
+ // Fixed-velocity, un-floored band position: advancing at a constant
113
+ // cells/second (not period / fixed-sweep) keeps the per-frame step ≤1 cell at
114
+ // the default cadence for any length, so long messages are no steppier.
115
+ const pos = ((time / 1000) * SHIMMER_SPEED_CELLS_PER_S) % period;
109
116
  const dist = Math.abs(index + CLASSIC_PADDING - pos);
110
117
  if (dist >= CLASSIC_BAND_HALF_WIDTH) return 0;
111
118
  return 0.5 * (1 + Math.cos((Math.PI * dist) / CLASSIC_BAND_HALF_WIDTH));
@@ -119,9 +126,13 @@ function classicIntensity(time: number, index: number, length: number): number {
119
126
  function kittIntensity(time: number, index: number, length: number): number {
120
127
  const range = length - 1;
121
128
  if (range <= 0) return 1;
122
- const phase = (time % KITT_CYCLE_MS) / KITT_CYCLE_MS;
123
- const goingRight = phase < 0.5;
124
- const head = goingRight ? phase * 2 * range : (1 - phase) * 2 * range;
129
+ // Fixed head velocity: a triangle ping-pong over a 2*range round trip at a
130
+ // constant cells/second, so the bright head advances ≤1 cell per frame at the
131
+ // default cadence regardless of bar length. Round-trip duration scales with length.
132
+ const cycleCells = 2 * range;
133
+ const sweep = ((time / 1000) * SHIMMER_SPEED_CELLS_PER_S) % cycleCells;
134
+ const goingRight = sweep < range;
135
+ const head = goingRight ? sweep : cycleCells - sweep;
125
136
  const delta = index - head;
126
137
  const abs = delta < 0 ? -delta : delta;
127
138
  if (abs <= KITT_HEAD_HALF) return 1;
@@ -42,6 +42,10 @@ export type SubmittedUserInput = {
42
42
  images?: ImageContent[];
43
43
  imageLinks?: (string | undefined)[];
44
44
  customType?: string;
45
+ /** Route through `session.prompt(text, { synthetic: true })` so the text lands
46
+ * as a hidden agent-authored `developer` message rather than a visible user
47
+ * turn. Used by the `c`/`.` continue shortcut. */
48
+ synthetic?: boolean;
45
49
  display?: boolean;
46
50
  cancelled: boolean;
47
51
  started: boolean;
@@ -182,6 +186,9 @@ export interface InteractiveModeContext {
182
186
  flushPendingModelSwitch(): Promise<void>;
183
187
  setWorkingMessage(message?: string): void;
184
188
  applyPendingWorkingMessage(): void;
189
+ /** Acknowledge a user interrupt (Esc) by switching the loader to an
190
+ * "Interrupting…" label until the agent turn unwinds. */
191
+ notifyInterrupting(): void;
185
192
  ensureLoadingAnimation(): void;
186
193
  startPendingSubmission(input: {
187
194
  text: string;
@@ -10,6 +10,10 @@ import { CompactionSummaryMessageComponent } from "../../modes/components/compac
10
10
  import { CustomMessageComponent } from "../../modes/components/custom-message";
11
11
  import { DynamicBorder } from "../../modes/components/dynamic-border";
12
12
  import { EvalExecutionComponent } from "../../modes/components/eval-execution";
13
+ import {
14
+ type LateDiagnosticsFile,
15
+ LateDiagnosticsMessageComponent,
16
+ } from "../../modes/components/late-diagnostics-message";
13
17
  import {
14
18
  ReadToolGroupComponent,
15
19
  readArgsHaveTarget,
@@ -25,6 +29,7 @@ import type { CompactionQueuedMessage, InteractiveModeContext } from "../../mode
25
29
  import {
26
30
  type CustomMessage,
27
31
  isSilentAbort,
32
+ LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
28
33
  resolveAbortLabel,
29
34
  SKILL_PROMPT_MESSAGE_TYPE,
30
35
  type SkillPromptDetails,
@@ -168,6 +173,17 @@ export class UiHelpers {
168
173
  this.ctx.chatContainer.addChild(block);
169
174
  break;
170
175
  }
176
+ if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
177
+ const details = (
178
+ message as CustomMessage<{
179
+ files?: LateDiagnosticsFile[];
180
+ }>
181
+ ).details;
182
+ const component = new LateDiagnosticsMessageComponent(details?.files ?? []);
183
+ component.setExpanded(this.ctx.toolOutputExpanded);
184
+ this.ctx.chatContainer.addChild(component);
185
+ break;
186
+ }
171
187
  if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
172
188
  const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
173
189
  component.setExpanded(this.ctx.toolOutputExpanded);
@@ -342,7 +358,11 @@ export class UiHelpers {
342
358
  (content.type === "thinking" && content.thinking.trim().length > 0),
343
359
  );
344
360
  if (hasVisibleAssistantContent) {
345
- readGroup?.finalize();
361
+ // Rebuild reconstructs immutable history; seal (not finalize) so the
362
+ // group freezes even if a read's result was never persisted —
363
+ // finalize alone keeps a pending entry live and would stop the whole
364
+ // transcript below it from committing to native scrollback.
365
+ readGroup?.seal();
346
366
  readGroup = null;
347
367
  }
348
368
  const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
@@ -392,7 +412,7 @@ export class UiHelpers {
392
412
  continue;
393
413
  }
394
414
 
395
- readGroup?.finalize();
415
+ readGroup?.seal();
396
416
  readGroup = null;
397
417
  const tool = this.ctx.session.getToolByName(content.name);
398
418
  const renderArgs =
@@ -480,9 +500,10 @@ export class UiHelpers {
480
500
  }
481
501
  }
482
502
 
483
- // The trailing read run has no following break to close it; finalize so the
484
- // rebuilt group commits to native scrollback like every other historical block.
485
- readGroup?.finalize();
503
+ // The trailing read run has no following break to close it; seal so the
504
+ // rebuilt group freezes (even with a never-persisted result) and commits to
505
+ // native scrollback like every other historical block.
506
+ readGroup?.seal();
486
507
 
487
508
  // Render deferred messages (compaction summaries) at the bottom so they're visible
488
509
  for (const message of deferredMessages) {
@@ -3,25 +3,25 @@ import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-h
3
3
  import { keywordInProse } from "./markdown-prose";
4
4
 
5
5
  /**
6
- * "workflow" keyword support.
6
+ * "workflowz" keyword support.
7
7
  *
8
8
  * Typing the standalone word in the input editor paints it with a warm
9
9
  * amber→green gradient ({@link highlightWorkflow}); submitting a message that
10
10
  * mentions it appends a hidden {@link WORKFLOW_NOTICE} that steers the model to
11
11
  * author a deterministic multi-subagent workflow in eval cells (agent/parallel/
12
12
  * pipeline). Matching is whitespace-delimited and case-sensitive (lowercase
13
- * only) — "workflow"/"workflows" trigger, but "workflowed", "Workflow", and
14
- * "workflow.ts" never do.
13
+ * only) — "workflowz" triggers, but "workflowzed", "Workflowz", and
14
+ * "workflowz.ts" never do.
15
15
  */
16
16
 
17
- // Detection: lowercase keyword (singular or plural) flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
18
- const WORKFLOW_WORD = /(?<!\S)workflows?(?!\S)/;
17
+ // Detection: lowercase keyword flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
18
+ const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
19
19
 
20
- /** Hidden system notice appended after a user message that mentions "workflow". */
20
+ /** Hidden system notice appended after a user message that mentions "workflowz". */
21
21
  export const WORKFLOW_NOTICE: string = workflowNotice.trim();
22
22
 
23
23
  /**
24
- * Whether `text` contains the standalone keyword "workflow"/"workflows"
24
+ * Whether `text` contains the standalone keyword "workflowz"
25
25
  * (lowercase, whitespace-delimited) in prose — never inside a code block, inline
26
26
  * code span, or XML/HTML section.
27
27
  */
@@ -30,13 +30,13 @@ export function containsWorkflow(text: string): boolean {
30
30
  }
31
31
 
32
32
  /**
33
- * Highlight every standalone "workflow"/"workflows" in `text` for editor display
33
+ * Highlight every standalone "workflowz" in `text` for editor display
34
34
  * with a warm amber→green gradient (hue 30..150), visually distinct from
35
35
  * ultrathink's rainbow and orchestrate's teal→violet.
36
36
  */
37
37
  export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
38
- probe: /workflow/,
39
- highlight: /(?<!\S)workflows?(?!\S)/g,
38
+ probe: /workflowz/,
39
+ highlight: /(?<!\S)workflowz(?!\S)/g,
40
40
  stops: 14,
41
41
  hue: t => 30 + t * 120,
42
42
  });
@@ -0,0 +1,7 @@
1
+ <system-notice>
2
+ Continue. Keep going from where you left off.
3
+
4
+ - You MUST resume the most recent intent and carry the unfinished work to completion.
5
+ - Interrupted mid-step? Pick it back up from where it stopped.
6
+ - You NEVER pause to summarize progress, re-confirm the plan, or ask whether to proceed — just continue.
7
+ </system-notice>