@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.14

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 (129) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +5547 -2759
  3. package/dist/types/config/settings-schema.d.ts +44 -14
  4. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  5. package/dist/types/eval/backend-helpers.d.ts +23 -0
  6. package/dist/types/eval/executor-base.d.ts +118 -0
  7. package/dist/types/eval/index.d.ts +2 -0
  8. package/dist/types/eval/jl/executor.d.ts +44 -0
  9. package/dist/types/eval/jl/index.d.ts +11 -0
  10. package/dist/types/eval/jl/kernel.d.ts +28 -0
  11. package/dist/types/eval/jl/prelude.d.ts +1 -0
  12. package/dist/types/eval/jl/runtime.d.ts +22 -0
  13. package/dist/types/eval/kernel-base.d.ts +105 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -61
  15. package/dist/types/eval/rb/executor.d.ts +77 -0
  16. package/dist/types/eval/rb/index.d.ts +11 -0
  17. package/dist/types/eval/rb/kernel.d.ts +31 -0
  18. package/dist/types/eval/rb/prelude.d.ts +1 -0
  19. package/dist/types/eval/rb/runtime.d.ts +23 -0
  20. package/dist/types/eval/runtime-env.d.ts +24 -0
  21. package/dist/types/eval/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  23. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  24. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  25. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  26. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  27. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  29. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  30. package/dist/types/modes/interactive-mode.d.ts +0 -2
  31. package/dist/types/modes/theme/theme.d.ts +8 -1
  32. package/dist/types/modes/types.d.ts +0 -2
  33. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  34. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  35. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +2 -2
  38. package/dist/types/stt/asr-client.d.ts +3 -29
  39. package/dist/types/subprocess/worker-client.d.ts +149 -0
  40. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  41. package/dist/types/tiny/title-client.d.ts +14 -34
  42. package/dist/types/tools/eval-backends.d.ts +6 -3
  43. package/dist/types/tools/eval-render.d.ts +6 -5
  44. package/dist/types/tools/eval.d.ts +13 -15
  45. package/dist/types/tools/index.d.ts +3 -3
  46. package/dist/types/tts/tts-client.d.ts +3 -28
  47. package/dist/types/tui/code-cell.d.ts +7 -0
  48. package/dist/types/utils/file-display-mode.d.ts +1 -1
  49. package/dist/types/web/parallel.d.ts +6 -0
  50. package/package.json +12 -12
  51. package/src/config/settings-schema.ts +50 -18
  52. package/src/config/settings.ts +5 -0
  53. package/src/dap/client.ts +13 -107
  54. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  55. package/src/eval/backend-helpers.ts +48 -0
  56. package/src/eval/executor-base.ts +425 -0
  57. package/src/eval/index.ts +2 -0
  58. package/src/eval/jl/executor.ts +540 -0
  59. package/src/eval/jl/index.ts +54 -0
  60. package/src/eval/jl/kernel.ts +235 -0
  61. package/src/eval/jl/prelude.jl +930 -0
  62. package/src/eval/jl/prelude.ts +3 -0
  63. package/src/eval/jl/runner.jl +634 -0
  64. package/src/eval/jl/runtime.ts +118 -0
  65. package/src/eval/js/index.ts +3 -14
  66. package/src/eval/kernel-base.ts +569 -0
  67. package/src/eval/py/executor.ts +43 -252
  68. package/src/eval/py/index.ts +9 -20
  69. package/src/eval/py/kernel.ts +29 -544
  70. package/src/eval/rb/executor.ts +504 -0
  71. package/src/eval/rb/index.ts +54 -0
  72. package/src/eval/rb/kernel.ts +230 -0
  73. package/src/eval/rb/prelude.rb +721 -0
  74. package/src/eval/rb/prelude.ts +3 -0
  75. package/src/eval/rb/runner.rb +474 -0
  76. package/src/eval/rb/runtime.ts +132 -0
  77. package/src/eval/runtime-env.ts +104 -0
  78. package/src/eval/types.ts +3 -3
  79. package/src/extensibility/extensions/runner.ts +4 -11
  80. package/src/extensibility/hooks/loader.ts +3 -21
  81. package/src/extensibility/session-handler-types.ts +21 -0
  82. package/src/internal-urls/docs-index.generated.txt +1 -1
  83. package/src/jsonrpc/message-framing.ts +142 -0
  84. package/src/lsp/client.ts +13 -109
  85. package/src/mnemopi/embed-client.ts +43 -198
  86. package/src/modes/components/agent-dashboard.ts +17 -40
  87. package/src/modes/components/chat-transcript-builder.ts +18 -102
  88. package/src/modes/components/custom-editor.ts +19 -0
  89. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  90. package/src/modes/components/extensions/extension-list.ts +16 -44
  91. package/src/modes/components/history-search.ts +4 -16
  92. package/src/modes/components/selector-helpers.ts +129 -0
  93. package/src/modes/components/settings-selector.ts +7 -5
  94. package/src/modes/components/tree-selector.ts +13 -18
  95. package/src/modes/controllers/event-controller.ts +3 -9
  96. package/src/modes/controllers/input-controller.ts +32 -54
  97. package/src/modes/interactive-mode.ts +5 -7
  98. package/src/modes/theme/theme.ts +35 -0
  99. package/src/modes/types.ts +0 -2
  100. package/src/modes/utils/copy-targets.ts +3 -2
  101. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  102. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  103. package/src/modes/utils/ui-helpers.ts +21 -126
  104. package/src/prompts/system/system-prompt.md +10 -10
  105. package/src/prompts/tools/bash.md +0 -1
  106. package/src/prompts/tools/eval.md +6 -4
  107. package/src/prompts/tools/find.md +0 -4
  108. package/src/prompts/tools/read.md +1 -2
  109. package/src/prompts/tools/replace.md +1 -1
  110. package/src/prompts/tools/search.md +0 -1
  111. package/src/sdk.ts +13 -7
  112. package/src/session/agent-session.ts +9 -7
  113. package/src/stt/asr-client.ts +35 -215
  114. package/src/stt/asr-worker.ts +29 -181
  115. package/src/subprocess/worker-client.ts +297 -0
  116. package/src/subprocess/worker-runtime.ts +277 -0
  117. package/src/task/executor.ts +4 -4
  118. package/src/tiny/title-client.ts +53 -219
  119. package/src/tiny/worker.ts +29 -180
  120. package/src/tools/eval-backends.ts +10 -3
  121. package/src/tools/eval-render.ts +17 -8
  122. package/src/tools/eval.ts +187 -22
  123. package/src/tools/index.ts +51 -28
  124. package/src/tts/tts-client.ts +38 -206
  125. package/src/tts/tts-worker.ts +23 -97
  126. package/src/tui/code-cell.ts +12 -1
  127. package/src/utils/file-display-mode.ts +2 -3
  128. package/src/web/parallel.ts +43 -42
  129. package/src/web/search/providers/parallel.ts +10 -99
@@ -530,10 +530,7 @@ export class InputController {
530
530
  // model continues the prior intent rather than second-guessing the interrupt.
531
531
  if (text === "." || text === "c") {
532
532
  if (this.ctx.onInputCallback) {
533
- this.ctx.editor.setText("");
534
- this.ctx.pendingImages = [];
535
- this.ctx.pendingImageLinks = [];
536
- this.ctx.editor.imageLinks = undefined;
533
+ this.ctx.editor.clearDraft();
537
534
  this.ctx.onInputCallback({
538
535
  text: manualContinuePrompt,
539
536
  cancelled: false,
@@ -546,16 +543,14 @@ export class InputController {
546
543
  }
547
544
 
548
545
  const runner = this.ctx.session.extensionRunner;
549
- let inputImages = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
550
- let inputImageLinks = this.ctx.pendingImageLinks.length > 0 ? [...this.ctx.pendingImageLinks] : undefined;
546
+ let inputImages = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
547
+ let inputImageLinks =
548
+ this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
551
549
 
552
550
  if (runner?.hasHandlers("input")) {
553
551
  const result = await runner.emitInput(text, inputImages, "interactive");
554
552
  if (result?.handled) {
555
- this.ctx.editor.setText("");
556
- this.ctx.pendingImages = [];
557
- this.ctx.pendingImageLinks = [];
558
- this.ctx.editor.imageLinks = undefined;
553
+ this.ctx.editor.clearDraft();
559
554
  return;
560
555
  }
561
556
  if (result?.text !== undefined) {
@@ -603,12 +598,8 @@ export class InputController {
603
598
  this.ctx.showStatus("This collab link is read-only — prompting is disabled");
604
599
  return;
605
600
  }
606
- this.ctx.editor.addToHistory(text);
607
- this.ctx.editor.setText("");
608
- this.ctx.editor.imageLinks = undefined;
609
601
  const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
610
- this.ctx.pendingImages = [];
611
- this.ctx.pendingImageLinks = [];
602
+ this.ctx.editor.clearDraft(text);
612
603
  // No local render: the prompt comes back from the host as a
613
604
  // collab-prompt event/entry and renders with the author badge.
614
605
  this.ctx.collabGuest.sendPrompt(text, images);
@@ -680,8 +671,8 @@ export class InputController {
680
671
  this.ctx.editor.setText("");
681
672
  this.ctx.editor.imageLinks = undefined;
682
673
  const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
683
- this.ctx.pendingImages = [];
684
- this.ctx.pendingImageLinks = [];
674
+ this.ctx.editor.pendingImages = [];
675
+ this.ctx.editor.pendingImageLinks = [];
685
676
  // Record the signature so the queued message's eventual delivery
686
677
  // (a user-role `message_start` event) leaves any draft the user has
687
678
  // typed since queuing intact. Same protection as #783, applied to
@@ -744,8 +735,8 @@ export class InputController {
744
735
  // Include any pending images from clipboard paste
745
736
  this.ctx.editor.imageLinks = undefined;
746
737
  const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
747
- this.ctx.pendingImages = [];
748
- this.ctx.pendingImageLinks = [];
738
+ this.ctx.editor.pendingImages = [];
739
+ this.ctx.editor.pendingImageLinks = [];
749
740
 
750
741
  // Render user message immediately, then let session events catch up.
751
742
  // Tag the submission as "steer": this is a normal Enter the controller
@@ -771,8 +762,8 @@ export class InputController {
771
762
  // semantics instead of throwing AgentBusyError.
772
763
  this.ctx.editor.imageLinks = undefined;
773
764
  const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
774
- this.ctx.pendingImages = [];
775
- this.ctx.pendingImageLinks = [];
765
+ this.ctx.editor.pendingImages = [];
766
+ this.ctx.editor.pendingImageLinks = [];
776
767
  try {
777
768
  await this.ctx.withLocalSubmission(
778
769
  text,
@@ -787,9 +778,11 @@ export class InputController {
787
778
  // extension command).
788
779
  this.ctx.editor.setText(text);
789
780
  if (images && images.length > 0) {
790
- this.ctx.pendingImages = [...images];
791
- this.ctx.pendingImageLinks = inputImageLinks ? [...inputImageLinks] : images.map(() => undefined);
792
- this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
781
+ this.ctx.editor.pendingImages = [...images];
782
+ this.ctx.editor.pendingImageLinks = inputImageLinks
783
+ ? [...inputImageLinks]
784
+ : images.map(() => undefined);
785
+ this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
793
786
  }
794
787
  this.ctx.showError(error instanceof Error ? error.message : String(error));
795
788
  }
@@ -816,12 +809,8 @@ export class InputController {
816
809
  this.ctx.showStatus("Commands run in the main session — press ←← to return first");
817
810
  return; // editor text not cleared: Editor does not auto-clear on submit
818
811
  }
819
- const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
820
- this.ctx.editor.addToHistory(text);
821
- this.ctx.editor.setText("");
822
- this.ctx.editor.imageLinks = undefined;
823
- this.ctx.pendingImages = [];
824
- this.ctx.pendingImageLinks = [];
812
+ const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
813
+ this.ctx.editor.clearDraft(text);
825
814
  try {
826
815
  // prompt() handles idle (new turn) and streaming (queues per streamingBehavior).
827
816
  await this.ctx.withLocalSubmission(text, () => target.prompt(text, { streamingBehavior, images }), {
@@ -989,10 +978,7 @@ export class InputController {
989
978
  }
990
979
  const didRetry = await this.ctx.viewSession.retry();
991
980
  if (didRetry) {
992
- this.ctx.editor.setText("");
993
- this.ctx.pendingImages = [];
994
- this.ctx.pendingImageLinks = [];
995
- this.ctx.editor.imageLinks = undefined;
981
+ this.ctx.editor.clearDraft();
996
982
  } else {
997
983
  this.ctx.showStatus("Nothing to retry");
998
984
  }
@@ -1016,7 +1002,7 @@ export class InputController {
1016
1002
  // the queued entry is later re-parsed into a skill invocation is a
1017
1003
  // separate concern owned by the compaction-resume path.
1018
1004
  if (this.ctx.session.isCompacting) {
1019
- const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
1005
+ const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
1020
1006
  this.ctx.queueCompactionMessage(text, "followUp", images);
1021
1007
  return;
1022
1008
  }
@@ -1040,14 +1026,10 @@ export class InputController {
1040
1026
 
1041
1027
  // Forward any pending clipboard-pasted images alongside the queued text;
1042
1028
  // otherwise the follow-up would drop the image (mirrors the Enter/steer path).
1043
- const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
1029
+ const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
1044
1030
 
1045
1031
  if (this.ctx.session.isStreaming) {
1046
- this.ctx.editor.addToHistory(text);
1047
- this.ctx.editor.setText("");
1048
- this.ctx.editor.imageLinks = undefined;
1049
- this.ctx.pendingImages = [];
1050
- this.ctx.pendingImageLinks = [];
1032
+ this.ctx.editor.clearDraft(text);
1051
1033
  await this.ctx.withLocalSubmission(
1052
1034
  text,
1053
1035
  () => this.ctx.session.prompt(text, { streamingBehavior: "followUp", images }),
@@ -1059,11 +1041,7 @@ export class InputController {
1059
1041
  }
1060
1042
 
1061
1043
  // Not streaming — just submit normally
1062
- this.ctx.editor.addToHistory(text);
1063
- this.ctx.editor.setText("");
1064
- this.ctx.editor.imageLinks = undefined;
1065
- this.ctx.pendingImages = [];
1066
- this.ctx.pendingImageLinks = [];
1044
+ this.ctx.editor.clearDraft(text);
1067
1045
  await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, { images }), {
1068
1046
  imageCount: images?.length ?? 0,
1069
1047
  });
@@ -1108,7 +1086,7 @@ export class InputController {
1108
1086
  let queuedText: string;
1109
1087
  if (queuedImages.length > 0) {
1110
1088
  const parts: string[] = [];
1111
- let imageOffset = this.ctx.pendingImages.length;
1089
+ let imageOffset = this.ctx.editor.pendingImages.length;
1112
1090
  for (const entry of allQueued) {
1113
1091
  parts.push(shiftImageMarkers(entry.text, imageOffset));
1114
1092
  if (entry.images && entry.images.length > 0) imageOffset += entry.images.length;
@@ -1124,9 +1102,9 @@ export class InputController {
1124
1102
  // re-materialized lazily; the restored text already carries the
1125
1103
  // renumbered `[Image #N, WxH]` markers).
1126
1104
  if (queuedImages.length > 0) {
1127
- this.ctx.pendingImages.push(...queuedImages);
1128
- this.ctx.pendingImageLinks.push(...queuedImages.map(() => undefined));
1129
- this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
1105
+ this.ctx.editor.pendingImages.push(...queuedImages);
1106
+ this.ctx.editor.pendingImageLinks.push(...queuedImages.map(() => undefined));
1107
+ this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
1130
1108
  }
1131
1109
  this.ctx.updatePendingMessagesDisplay();
1132
1110
  if (options?.abort) {
@@ -1148,14 +1126,14 @@ export class InputController {
1148
1126
  this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
1149
1127
  )
1150
1128
  )?.[0];
1151
- this.ctx.pendingImages.push({
1129
+ this.ctx.editor.pendingImages.push({
1152
1130
  type: "image",
1153
1131
  data: imageData.data,
1154
1132
  mimeType: imageData.mimeType,
1155
1133
  });
1156
- this.ctx.pendingImageLinks.push(imageLink);
1157
- this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
1158
- const imageNum = this.ctx.pendingImages.length;
1134
+ this.ctx.editor.pendingImageLinks.push(imageLink);
1135
+ this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
1136
+ const imageNum = this.ctx.editor.pendingImages.length;
1159
1137
  const dims = await this.#imageDimensions(imageData);
1160
1138
  const label = dims ? `[Image #${imageNum}, ${dims.width}x${dims.height}]` : `[Image #${imageNum}]`;
1161
1139
  this.ctx.editor.insertText(`${label} `);
@@ -408,8 +408,6 @@ export class InteractiveMode implements InteractiveModeContext {
408
408
  todoPhases: TodoPhase[] = [];
409
409
  hideThinkingBlock = false;
410
410
  proseOnlyThinking = true;
411
- pendingImages: ImageContent[] = [];
412
- pendingImageLinks: (string | undefined)[] = [];
413
411
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
414
412
  pendingTools = new Map<string, ToolExecutionHandle>();
415
413
  pendingBashComponents: BashExecutionComponent[] = [];
@@ -1094,7 +1092,7 @@ export class InteractiveMode implements InteractiveModeContext {
1094
1092
  if (this.#goalSuppressNextContinuation) return;
1095
1093
  if (this.#pendingSubmittedInput) return;
1096
1094
  if (this.editor.getText().trim().length > 0) return;
1097
- if ((this.pendingImages?.length ?? 0) > 0) return;
1095
+ if ((this.editor.pendingImages?.length ?? 0) > 0) return;
1098
1096
  const state = this.session.getGoalModeState();
1099
1097
  if (!state?.enabled || state.goal.status !== "active") return;
1100
1098
  const prompt = this.session.goalRuntime.buildContinuationPrompt();
@@ -1113,7 +1111,7 @@ export class InteractiveMode implements InteractiveModeContext {
1113
1111
  if (this.#isAutoSubmitBlocked()) return;
1114
1112
  if (this.#pendingSubmittedInput) return;
1115
1113
  if (this.editor.getText().trim().length > 0) return;
1116
- if ((this.pendingImages?.length ?? 0) > 0) return;
1114
+ if ((this.editor.pendingImages?.length ?? 0) > 0) return;
1117
1115
  const latestState = this.session.getGoalModeState();
1118
1116
  if (!latestState?.enabled || latestState.goal.status !== "active") return;
1119
1117
  this.#goalContinuationTurnInFlight = true;
@@ -1338,9 +1336,9 @@ export class InteractiveMode implements InteractiveModeContext {
1338
1336
  this.#stopLoadingAnimation(true);
1339
1337
  }
1340
1338
  if (!submission.customType) {
1341
- this.pendingImages = submission.images ? [...submission.images] : [];
1342
- this.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
1343
- this.editor.imageLinks = this.pendingImageLinks;
1339
+ this.editor.pendingImages = submission.images ? [...submission.images] : [];
1340
+ this.editor.pendingImageLinks = submission.imageLinks ? [...submission.imageLinks] : [];
1341
+ this.editor.imageLinks = this.editor.pendingImageLinks;
1344
1342
  this.rebuildChatFromMessages();
1345
1343
  this.editor.setText(submission.text);
1346
1344
  }
@@ -169,6 +169,7 @@ export type SymbolKey =
169
169
  | "lang.cpp"
170
170
  | "lang.csharp"
171
171
  | "lang.ruby"
172
+ | "lang.julia"
172
173
  | "lang.php"
173
174
  | "lang.swift"
174
175
  | "lang.kotlin"
@@ -369,6 +370,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
369
370
  "lang.cpp": "➕",
370
371
  "lang.csharp": "♯",
371
372
  "lang.ruby": "💎",
373
+ "lang.julia": "Ⓙ",
372
374
  "lang.php": "🐘",
373
375
  "lang.swift": "🕊",
374
376
  "lang.kotlin": "🅺",
@@ -673,6 +675,7 @@ const NERD_SYMBOLS: SymbolMap = {
673
675
  "lang.cpp": "\u{E61D}",
674
676
  "lang.csharp": "\u{E7BC}",
675
677
  "lang.ruby": "\u{E791}",
678
+ "lang.julia": "\u{E624}",
676
679
  "lang.php": "\u{E608}",
677
680
  "lang.swift": "\u{E755}",
678
681
  "lang.kotlin": "\u{E634}",
@@ -870,6 +873,7 @@ const ASCII_SYMBOLS: SymbolMap = {
870
873
  "lang.cpp": "cpp",
871
874
  "lang.csharp": "cs",
872
875
  "lang.ruby": "rb",
876
+ "lang.julia": "jl",
873
877
  "lang.php": "php",
874
878
  "lang.swift": "swift",
875
879
  "lang.kotlin": "kt",
@@ -1335,6 +1339,8 @@ const langMap: Record<string, SymbolKey> = {
1335
1339
  cs: "lang.csharp",
1336
1340
  ruby: "lang.ruby",
1337
1341
  rb: "lang.ruby",
1342
+ julia: "lang.julia",
1343
+ jl: "lang.julia",
1338
1344
  php: "lang.php",
1339
1345
  swift: "lang.swift",
1340
1346
  kotlin: "lang.kotlin",
@@ -1406,6 +1412,20 @@ const langMap: Record<string, SymbolKey> = {
1406
1412
  bin: "lang.binary",
1407
1413
  };
1408
1414
 
1415
+ /**
1416
+ * Brand colors for language icons, keyed by the resolved `lang.*` SymbolKey.
1417
+ * Used by {@link Theme.getLangIconStyled} so eval-kernel cell headers tint each
1418
+ * language with its recognizable hue (JS yellow, Ruby red, Julia purple, Python
1419
+ * blue) instead of a flat muted gray. Applied as truecolor/256 per the active
1420
+ * color mode; languages without an entry fall back to the muted theme color.
1421
+ */
1422
+ const LANG_BRAND_COLORS: Partial<Record<SymbolKey, string>> = {
1423
+ "lang.javascript": "#f7df1e",
1424
+ "lang.python": "#3776ab",
1425
+ "lang.ruby": "#cc342d",
1426
+ "lang.julia": "#9558b2",
1427
+ };
1428
+
1409
1429
  /**
1410
1430
  * Resolve a theme color value (hex string or 256-color index) to a CSS hex string.
1411
1431
  * Empty string represents the default terminal color.
@@ -1874,6 +1894,21 @@ export class Theme {
1874
1894
  const key = langMap[normalized];
1875
1895
  return key ? this.#symbols[key] : this.#symbols["lang.default"];
1876
1896
  }
1897
+
1898
+ /**
1899
+ * Language icon tinted with the language's brand color (see
1900
+ * {@link LANG_BRAND_COLORS}). Falls back to the muted theme color for
1901
+ * languages without a brand entry, and returns the bare (possibly empty)
1902
+ * icon when the active symbol preset has none.
1903
+ */
1904
+ getLangIconStyled(lang: string | undefined): string {
1905
+ const icon = this.getLangIcon(lang);
1906
+ if (!icon) return icon;
1907
+ const key = lang ? langMap[lang.toLowerCase()] : undefined;
1908
+ const hex = key ? LANG_BRAND_COLORS[key] : undefined;
1909
+ if (!hex) return this.fg("muted", icon);
1910
+ return `${colorToAnsi(hex, this.mode)}${icon}\x1b[39m`;
1911
+ }
1877
1912
  }
1878
1913
 
1879
1914
  // ============================================================================
@@ -160,8 +160,6 @@ export interface InteractiveModeContext {
160
160
  planModePlanFilePath?: string;
161
161
  hideThinkingBlock: boolean;
162
162
  proseOnlyThinking: boolean;
163
- pendingImages: ImageContent[];
164
- pendingImageLinks: (string | undefined)[];
165
163
  compactionQueuedMessages: CompactionQueuedMessage[];
166
164
  pendingTools: Map<string, ToolExecutionHandle>;
167
165
  pendingBashComponents: BashExecutionComponent[];
@@ -22,7 +22,7 @@ export type MessageBlock = ({ kind: "code" } & CodeBlock) | ({ kind: "quote" } &
22
22
  export interface LastCommand {
23
23
  kind: "bash" | "eval";
24
24
  code: string;
25
- /** Highlight language: "bash" for bash, "python"/"javascript" for eval. */
25
+ /** Highlight language: "bash" for bash, or the resolved eval language ("python"/"javascript"/"ruby"/"julia"). */
26
26
  language: string;
27
27
  }
28
28
 
@@ -139,7 +139,8 @@ function extractEvalCode(args: unknown): { code: string; language: string } | un
139
139
  if (typeof code !== "string" || code.length === 0) continue;
140
140
  codeBlocks.push(code);
141
141
  if (!languageResolved) {
142
- language = (cell as { language?: unknown }).language === "js" ? "javascript" : "python";
142
+ const lang = (cell as { language?: unknown }).language;
143
+ language = lang === "js" ? "javascript" : lang === "rb" ? "ruby" : lang === "jl" ? "julia" : "python";
143
144
  languageResolved = true;
144
145
  }
145
146
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Small helpers over {@link InteractiveModeContext} shared between
3
+ * {@link UiHelpers} and the input/event controllers, so the live chat surfaces
4
+ * construct components and reset editor state identically.
5
+ */
6
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
7
+ import { AssistantMessageComponent } from "../components/assistant-message";
8
+ import type { InteractiveModeContext } from "../types";
9
+
10
+ /**
11
+ * Construct an {@link AssistantMessageComponent} wired to the live context's
12
+ * thinking/image settings. `message` is omitted for the streaming placeholder
13
+ * component and supplied when rendering a persisted turn.
14
+ */
15
+ export function createAssistantMessageComponent(
16
+ ctx: InteractiveModeContext,
17
+ message?: AssistantMessage,
18
+ ): AssistantMessageComponent {
19
+ return new AssistantMessageComponent(
20
+ message,
21
+ ctx.hideThinkingBlock,
22
+ () => ctx.ui.requestRender(),
23
+ ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
24
+ ctx.ui.imageBudget,
25
+ ctx.proseOnlyThinking,
26
+ );
27
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Render helpers shared between the live transcript ({@link UiHelpers}) and the
3
+ * file/remote-backed {@link ChatTranscriptBuilder}. Both surfaces build the same
4
+ * transcript rows from persisted message entries; holding the row construction
5
+ * here keeps the two byte-for-byte identical.
6
+ */
7
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
+ import { type Component, Text } from "@oh-my-pi/pi-tui";
9
+ import { formatBytes, formatDuration } from "@oh-my-pi/pi-utils";
10
+ import { type CustomMessage, type FileMentionMessage, isSilentAbort, resolveAbortLabel } from "../../session/messages";
11
+ import { createIrcMessageCard } from "../../tools/irc";
12
+ import { canonicalizeMessage } from "../../utils/thinking-display";
13
+ import { TranscriptBlock } from "../components/transcript-container";
14
+ import { theme } from "../theme/theme";
15
+
16
+ type CustomOrHookMessage = Extract<AgentMessage, { role: "custom" | "hookMessage" }>;
17
+ type AssistantAgentMessage = Extract<AgentMessage, { role: "assistant" }>;
18
+
19
+ /**
20
+ * Render an `async-result` custom message (a completed background bash/task job,
21
+ * or a batch of them) as a transcript block of one "Background job completed"
22
+ * row per job.
23
+ */
24
+ export function buildAsyncResultBlock(message: CustomOrHookMessage): TranscriptBlock {
25
+ const details = (
26
+ message as CustomMessage<{
27
+ jobId?: string;
28
+ type?: "bash" | "task";
29
+ label?: string;
30
+ durationMs?: number;
31
+ jobs?: Array<{ jobId?: string; type?: "bash" | "task"; label?: string; durationMs?: number }>;
32
+ }>
33
+ ).details;
34
+ const jobs =
35
+ details?.jobs && details.jobs.length > 0
36
+ ? details.jobs
37
+ : [
38
+ {
39
+ jobId: details?.jobId,
40
+ type: details?.type,
41
+ label: details?.label,
42
+ durationMs: details?.durationMs,
43
+ },
44
+ ];
45
+ const block = new TranscriptBlock();
46
+ for (const job of jobs) {
47
+ const jobId = job.jobId ?? "unknown";
48
+ const typeLabel = job.type ? `[${job.type}]` : "[job]";
49
+ const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
50
+ const line = [
51
+ theme.fg("success", `${theme.status.done} Background job completed`),
52
+ theme.fg("dim", typeLabel),
53
+ theme.fg("accent", jobId),
54
+ duration ? theme.fg("dim", `(${duration})`) : undefined,
55
+ ]
56
+ .filter(Boolean)
57
+ .join(" ");
58
+ block.addChild(new Text(line, 1, 0));
59
+ }
60
+ return block;
61
+ }
62
+
63
+ /**
64
+ * Render a live IRC traffic custom message (`irc:incoming` / `irc:autoreply` /
65
+ * `irc:relay`) as a transcript card. `getExpanded` supplies the live
66
+ * expanded-state getter for the cached card.
67
+ */
68
+ export function buildIrcMessageCard(message: CustomOrHookMessage, getExpanded: () => boolean): Component {
69
+ const details = (
70
+ message as CustomMessage<{ from?: string; to?: string; message?: string; body?: string; replyTo?: string }>
71
+ ).details;
72
+ const kind =
73
+ message.customType === "irc:incoming"
74
+ ? ("incoming" as const)
75
+ : message.customType === "irc:autoreply"
76
+ ? ("autoreply" as const)
77
+ : ("relay" as const);
78
+ return createIrcMessageCard(
79
+ {
80
+ kind,
81
+ from: details?.from,
82
+ to: details?.to,
83
+ body: kind === "incoming" ? details?.message : details?.body,
84
+ replyTo: details?.replyTo,
85
+ timestamp: message.timestamp,
86
+ },
87
+ getExpanded,
88
+ theme,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Render a `fileMention` message's files as a transcript block of "Read <path>"
94
+ * rows. `indent` sets the left pad: the live chat renders within an outer gutter
95
+ * (0), the transcript viewer renders body rows without one so rows own their pad
96
+ * (1).
97
+ */
98
+ export function buildFileMentionBlock(files: FileMentionMessage["files"], indent: number): TranscriptBlock {
99
+ const block = new TranscriptBlock();
100
+ for (const file of files) {
101
+ let suffix: string;
102
+ if (file.skippedReason === "tooLarge") {
103
+ const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
104
+ suffix = `(skipped: ${size})`;
105
+ } else {
106
+ suffix = file.image
107
+ ? "(image)"
108
+ : file.lineCount === undefined
109
+ ? "(unknown lines)"
110
+ : `(${file.lineCount} lines)`;
111
+ }
112
+ const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
113
+ "accent",
114
+ file.path,
115
+ )} ${theme.fg("dim", suffix)}`;
116
+ block.addChild(new Text(text, indent, 0));
117
+ }
118
+ return block;
119
+ }
120
+
121
+ /**
122
+ * Whether an assistant turn has visible text or thinking content (after
123
+ * canonicalization) — i.e. content that closes the current read-tool run.
124
+ */
125
+ export function assistantHasVisibleContent(message: AssistantAgentMessage): boolean {
126
+ return message.content.some(
127
+ content =>
128
+ (content.type === "text" && canonicalizeMessage(content.text)) ||
129
+ (content.type === "thinking" && canonicalizeMessage(content.thinking)),
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Normalize raw tool-call arguments to a plain record, collapsing non-object or
135
+ * array values to an empty object.
136
+ */
137
+ export function normalizeToolArgs(args: unknown): Record<string, unknown> {
138
+ return args && typeof args === "object" && !Array.isArray(args) ? (args as Record<string, unknown>) : {};
139
+ }
140
+
141
+ /**
142
+ * Resolve the inline error label, if any, for a turn-ending assistant message.
143
+ * Silent aborts yield no label. `retryAttempt` tunes the abort label wording.
144
+ */
145
+ export function resolveAssistantErrorMessage(
146
+ message: AssistantAgentMessage,
147
+ retryAttempt = 0,
148
+ ): { hasErrorStop: boolean; errorMessage: string | null } {
149
+ const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
150
+ const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
151
+ const errorMessage = hasErrorStop
152
+ ? message.stopReason === "aborted"
153
+ ? resolveAbortLabel(message.errorMessage, retryAttempt)
154
+ : message.errorMessage || "Error"
155
+ : null;
156
+ return { hasErrorStop, errorMessage };
157
+ }