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

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 (153) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli.js +5615 -2833
  3. package/dist/types/advisor/runtime.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +64 -14
  5. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  6. package/dist/types/eval/backend-helpers.d.ts +23 -0
  7. package/dist/types/eval/executor-base.d.ts +118 -0
  8. package/dist/types/eval/index.d.ts +2 -0
  9. package/dist/types/eval/jl/executor.d.ts +44 -0
  10. package/dist/types/eval/jl/index.d.ts +11 -0
  11. package/dist/types/eval/jl/kernel.d.ts +28 -0
  12. package/dist/types/eval/jl/prelude.d.ts +1 -0
  13. package/dist/types/eval/jl/runtime.d.ts +22 -0
  14. package/dist/types/eval/kernel-base.d.ts +105 -0
  15. package/dist/types/eval/py/kernel.d.ts +3 -61
  16. package/dist/types/eval/rb/executor.d.ts +77 -0
  17. package/dist/types/eval/rb/index.d.ts +11 -0
  18. package/dist/types/eval/rb/kernel.d.ts +31 -0
  19. package/dist/types/eval/rb/prelude.d.ts +1 -0
  20. package/dist/types/eval/rb/runtime.d.ts +23 -0
  21. package/dist/types/eval/runtime-env.d.ts +24 -0
  22. package/dist/types/eval/types.d.ts +3 -3
  23. package/dist/types/export/share.d.ts +8 -1
  24. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  25. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  26. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  27. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  28. package/dist/types/mcp/transports/stdio.d.ts +12 -1
  29. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  30. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  31. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  32. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  33. package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
  34. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +0 -2
  36. package/dist/types/modes/theme/theme.d.ts +8 -1
  37. package/dist/types/modes/types.d.ts +0 -2
  38. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  39. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  40. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  41. package/dist/types/sdk.d.ts +1 -1
  42. package/dist/types/secrets/obfuscator.d.ts +3 -3
  43. package/dist/types/session/agent-session.d.ts +2 -2
  44. package/dist/types/stt/asr-client.d.ts +3 -29
  45. package/dist/types/subprocess/worker-client.d.ts +149 -0
  46. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  47. package/dist/types/tiny/title-client.d.ts +14 -34
  48. package/dist/types/tools/eval-backends.d.ts +6 -3
  49. package/dist/types/tools/eval-render.d.ts +6 -5
  50. package/dist/types/tools/eval.d.ts +13 -15
  51. package/dist/types/tools/index.d.ts +3 -3
  52. package/dist/types/tts/tts-client.d.ts +3 -28
  53. package/dist/types/tui/code-cell.d.ts +7 -0
  54. package/dist/types/utils/file-display-mode.d.ts +1 -1
  55. package/dist/types/utils/shell-snapshot.d.ts +10 -0
  56. package/dist/types/web/parallel.d.ts +6 -0
  57. package/dist/types/web/search/providers/perplexity.d.ts +17 -3
  58. package/package.json +12 -12
  59. package/src/advisor/__tests__/advisor.test.ts +114 -0
  60. package/src/advisor/runtime.ts +129 -1
  61. package/src/config/model-registry.ts +12 -4
  62. package/src/config/settings-schema.ts +74 -18
  63. package/src/config/settings.ts +5 -0
  64. package/src/dap/client.ts +13 -107
  65. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  66. package/src/eval/backend-helpers.ts +48 -0
  67. package/src/eval/executor-base.ts +425 -0
  68. package/src/eval/index.ts +2 -0
  69. package/src/eval/jl/executor.ts +540 -0
  70. package/src/eval/jl/index.ts +54 -0
  71. package/src/eval/jl/kernel.ts +235 -0
  72. package/src/eval/jl/prelude.jl +930 -0
  73. package/src/eval/jl/prelude.ts +3 -0
  74. package/src/eval/jl/runner.jl +634 -0
  75. package/src/eval/jl/runtime.ts +118 -0
  76. package/src/eval/js/index.ts +3 -14
  77. package/src/eval/kernel-base.ts +569 -0
  78. package/src/eval/py/executor.ts +43 -252
  79. package/src/eval/py/index.ts +9 -20
  80. package/src/eval/py/kernel.ts +29 -544
  81. package/src/eval/rb/executor.ts +504 -0
  82. package/src/eval/rb/index.ts +54 -0
  83. package/src/eval/rb/kernel.ts +230 -0
  84. package/src/eval/rb/prelude.rb +721 -0
  85. package/src/eval/rb/prelude.ts +3 -0
  86. package/src/eval/rb/runner.rb +474 -0
  87. package/src/eval/rb/runtime.ts +132 -0
  88. package/src/eval/runtime-env.ts +104 -0
  89. package/src/eval/types.ts +3 -3
  90. package/src/exec/bash-executor.ts +44 -0
  91. package/src/export/share.ts +51 -28
  92. package/src/extensibility/extensions/runner.ts +4 -11
  93. package/src/extensibility/hooks/loader.ts +3 -21
  94. package/src/extensibility/session-handler-types.ts +21 -0
  95. package/src/internal-urls/docs-index.generated.txt +1 -1
  96. package/src/jsonrpc/message-framing.ts +142 -0
  97. package/src/lsp/client.ts +13 -109
  98. package/src/mcp/transports/stdio.ts +20 -4
  99. package/src/mnemopi/embed-client.ts +43 -198
  100. package/src/modes/components/agent-dashboard.ts +17 -40
  101. package/src/modes/components/chat-transcript-builder.ts +18 -102
  102. package/src/modes/components/custom-editor.test.ts +22 -0
  103. package/src/modes/components/custom-editor.ts +29 -1
  104. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  105. package/src/modes/components/extensions/extension-list.ts +16 -44
  106. package/src/modes/components/footer.ts +4 -3
  107. package/src/modes/components/history-search.ts +4 -16
  108. package/src/modes/components/selector-helpers.ts +129 -0
  109. package/src/modes/components/settings-selector.ts +7 -5
  110. package/src/modes/components/status-line/component.ts +5 -1
  111. package/src/modes/components/status-line/context-thresholds.ts +11 -3
  112. package/src/modes/components/status-line/segments.ts +1 -1
  113. package/src/modes/components/status-line/types.ts +1 -0
  114. package/src/modes/components/tree-selector.ts +13 -18
  115. package/src/modes/controllers/command-controller.ts +1 -0
  116. package/src/modes/controllers/event-controller.ts +3 -9
  117. package/src/modes/controllers/input-controller.ts +32 -54
  118. package/src/modes/interactive-mode.ts +5 -7
  119. package/src/modes/theme/theme.ts +35 -0
  120. package/src/modes/types.ts +0 -2
  121. package/src/modes/utils/copy-targets.ts +3 -2
  122. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  123. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  124. package/src/modes/utils/ui-helpers.ts +21 -126
  125. package/src/prompts/system/system-prompt.md +5 -5
  126. package/src/prompts/tools/bash.md +2 -3
  127. package/src/prompts/tools/eval.md +6 -4
  128. package/src/prompts/tools/find.md +0 -4
  129. package/src/prompts/tools/read.md +1 -2
  130. package/src/prompts/tools/replace.md +1 -1
  131. package/src/sdk.ts +13 -7
  132. package/src/secrets/obfuscator.ts +3 -9
  133. package/src/session/agent-session.ts +42 -9
  134. package/src/slash-commands/builtin-registry.ts +2 -1
  135. package/src/stt/asr-client.ts +35 -215
  136. package/src/stt/asr-worker.ts +29 -181
  137. package/src/subprocess/worker-client.ts +297 -0
  138. package/src/subprocess/worker-runtime.ts +277 -0
  139. package/src/task/executor.ts +4 -4
  140. package/src/tiny/title-client.ts +53 -219
  141. package/src/tiny/worker.ts +29 -180
  142. package/src/tools/eval-backends.ts +10 -3
  143. package/src/tools/eval-render.ts +17 -8
  144. package/src/tools/eval.ts +187 -22
  145. package/src/tools/index.ts +51 -28
  146. package/src/tts/tts-client.ts +38 -206
  147. package/src/tts/tts-worker.ts +23 -97
  148. package/src/tui/code-cell.ts +12 -1
  149. package/src/utils/file-display-mode.ts +2 -3
  150. package/src/utils/shell-snapshot.ts +63 -1
  151. package/src/web/parallel.ts +43 -42
  152. package/src/web/search/providers/parallel.ts +10 -99
  153. package/src/web/search/providers/perplexity.ts +18 -6
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Shared scaffolding for the TUI selector/list/dashboard components: viewport
3
+ * windowing, scrollbar-aware row widths, ScrollView rendering, selection
4
+ * clamping, search-character classification, tab-cycling keys, and full-screen
5
+ * padding. Behaviour is identical to the per-component copies these helpers
6
+ * replace.
7
+ */
8
+ import { extractPrintableText, matchesKey, ScrollView } from "@oh-my-pi/pi-tui";
9
+ import { theme } from "../theme/theme";
10
+
11
+ /**
12
+ * Render `rows` through a {@link ScrollView} with the shared list theme (muted
13
+ * track / accent thumb) and an "auto" scrollbar, positioned at `scrollOffset`.
14
+ * Returns the rendered lines for the caller to append to its output.
15
+ */
16
+ export function renderScrollableList(
17
+ rows: readonly string[],
18
+ options: { width: number; totalRows: number; scrollOffset: number },
19
+ ): readonly string[] {
20
+ const sv = new ScrollView(rows, {
21
+ height: rows.length,
22
+ scrollbar: "auto",
23
+ totalRows: options.totalRows,
24
+ theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
25
+ });
26
+ sv.setScrollOffset(options.scrollOffset);
27
+ return sv.render(options.width);
28
+ }
29
+
30
+ /**
31
+ * Center a viewport window of `maxVisible` rows on `selectedIndex` within a
32
+ * list of `total` rows, clamped to valid bounds. Used by the selection-centered
33
+ * list panes (history search, tree selector).
34
+ */
35
+ export function centeredWindow(
36
+ selectedIndex: number,
37
+ total: number,
38
+ maxVisible: number,
39
+ ): { startIndex: number; endIndex: number } {
40
+ const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
41
+ const endIndex = Math.min(startIndex + maxVisible, total);
42
+ return { startIndex, endIndex };
43
+ }
44
+
45
+ /**
46
+ * Width available for row content, reserving the rightmost column for the
47
+ * scrollbar when the list overflows its visible window.
48
+ */
49
+ export function contentRowWidth(width: number, total: number, maxVisible: number): number {
50
+ const overflow = total > maxVisible;
51
+ return Math.max(0, width - (overflow ? 1 : 0));
52
+ }
53
+
54
+ /**
55
+ * Clamp `selectedIndex` into `[0, total)` and nudge `scrollOffset` so the
56
+ * selection stays within the visible window of `maxVisible` rows. Returns the
57
+ * adjusted pair; on an empty list both reset to 0.
58
+ */
59
+ export function clampSelection(
60
+ selectedIndex: number,
61
+ scrollOffset: number,
62
+ total: number,
63
+ maxVisible: number,
64
+ ): { selectedIndex: number; scrollOffset: number } {
65
+ if (total === 0) {
66
+ return { selectedIndex: 0, scrollOffset: 0 };
67
+ }
68
+
69
+ const selected = Math.max(0, Math.min(selectedIndex, total - 1));
70
+
71
+ let scroll = scrollOffset;
72
+ if (selected < scroll) {
73
+ scroll = selected;
74
+ } else if (selected >= scroll + maxVisible) {
75
+ scroll = selected - maxVisible + 1;
76
+ }
77
+
78
+ return { selectedIndex: selected, scrollOffset: scroll };
79
+ }
80
+
81
+ /**
82
+ * Classify a key event for search-query text entry. Returns the single
83
+ * printable character to append to the query, or `null` when the key is not a
84
+ * searchable character: non-printable, multi-byte, or a reserved `j`/`k`
85
+ * navigation key.
86
+ */
87
+ export function searchableChar(data: string): string | null {
88
+ const printableText = extractPrintableText(data);
89
+ if (printableText && printableText.length === 1) {
90
+ const printableCharCode = printableText.charCodeAt(0);
91
+ if (printableCharCode > 32 && printableCharCode < 127) {
92
+ if (printableText === "j" || printableText === "k") {
93
+ return null;
94
+ }
95
+ return printableText;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Handle the shared tab-cycling keys: Tab/Right advance to the next tab,
103
+ * Shift+Tab/Left to the previous. Invokes `switchTab` with the direction and
104
+ * returns true when the key was consumed.
105
+ */
106
+ export function handleTabSwitchKey(data: string, switchTab: (direction: 1 | -1) => void): boolean {
107
+ if (matchesKey(data, "tab") || matchesKey(data, "right")) {
108
+ switchTab(1);
109
+ return true;
110
+ }
111
+ if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
112
+ switchTab(-1);
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+
118
+ /**
119
+ * Pad `lines` with blank rows up to `rows` so a full-screen overlay covers the
120
+ * viewport instead of letting the transcript peek through below it. Copies
121
+ * before padding — the source array may be component-owned and must not be
122
+ * mutated.
123
+ */
124
+ export function padLinesToHeight(lines: readonly string[], rows: number): readonly string[] {
125
+ if (lines.length >= rows) return lines;
126
+ const padded = lines.slice();
127
+ while (padded.length < rows) padded.push("");
128
+ return padded;
129
+ }
@@ -473,15 +473,17 @@ export class SettingsSelectorComponent implements Component {
473
473
  return true;
474
474
  }
475
475
 
476
- if (event.wheel !== null) {
477
- list?.handleWheel(event.wheel);
478
- return true;
479
- }
480
-
481
476
  const tabLine = event.row - this.#tabRowStart;
482
477
  const overTabs = tabLine >= 0 && tabLine < this.#tabRowCount;
483
478
  const overContent = contentLine >= 0 && contentLine < this.#contentRowCount;
484
479
 
480
+ if (event.wheel !== null) {
481
+ if (overContent) {
482
+ list?.handleWheelAt(event.wheel, contentLine, innerCol);
483
+ }
484
+ return true;
485
+ }
486
+
485
487
  if (event.motion) {
486
488
  const hovered = overTabs ? this.#tabBar.tabAt(tabLine, innerCol) : undefined;
487
489
  this.#tabBar.setHoverTab(hovered && !hovered.muted ? hovered.id : null);
@@ -727,10 +727,12 @@ export class StatusLineComponent implements Component {
727
727
 
728
728
  let contextWindow = state.model?.contextWindow ?? this.session.model?.contextWindow ?? 0;
729
729
  let contextPercent: number | null = 0;
730
+ let contextTokens = 0;
730
731
  if (includeContext) {
731
732
  const breakdown = this.getCachedContextBreakdown();
733
+ contextTokens = breakdown.usedTokens;
732
734
  contextWindow = breakdown.contextWindow || contextWindow;
733
- contextPercent = contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : 0;
735
+ contextPercent = contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : null;
734
736
  }
735
737
 
736
738
  // Collab guest: context comes from the host's state frames — the local
@@ -738,6 +740,7 @@ export class StatusLineComponent implements Component {
738
740
  const collabState = this.#collabStatus?.stateOverride;
739
741
  if (collabState?.contextUsage) {
740
742
  contextWindow = collabState.contextUsage.contextWindow || contextWindow;
743
+ contextTokens = collabState.contextUsage.tokens ?? contextTokens;
741
744
  contextPercent = collabState.contextUsage.percent ?? contextPercent;
742
745
  }
743
746
 
@@ -756,6 +759,7 @@ export class StatusLineComponent implements Component {
756
759
  collab: this.#collabStatus,
757
760
  usageStats,
758
761
  contextPercent,
762
+ contextTokens,
759
763
  contextWindow,
760
764
  autoCompactEnabled: this.#autoCompactEnabled,
761
765
  subagentCount: this.#subagentCount,
@@ -56,10 +56,18 @@ export function getContextUsageLevel(contextPercent: number, contextWindow: numb
56
56
  }
57
57
 
58
58
  /**
59
- * Format context usage as `<percent>%/<window>` (e.g. `5.1%/1M`), matching the
60
- * status line's context gauge so subagent and footer renderers stay in sync.
59
+ * Format context usage as `<percent>%/<window>` when the model window is known.
60
+ * Unknown windows render as `<tokens>/?`, because `0.0%/0` suggests a real
61
+ * empty context instead of missing provider metadata.
61
62
  */
62
- export function formatContextUsage(contextPercent: number | null | undefined, contextWindow: number): string {
63
+ export function formatContextUsage(
64
+ contextPercent: number | null | undefined,
65
+ contextWindow: number,
66
+ usedTokens?: number,
67
+ ): string {
68
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
69
+ return `${formatNumber(usedTokens ?? 0)}/?`;
70
+ }
63
71
  const pct = contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`;
64
72
  return `${pct}/${formatNumber(contextWindow)}`;
65
73
  }
@@ -375,7 +375,7 @@ const contextPctSegment: StatusLineSegment = {
375
375
  const window = ctx.contextWindow;
376
376
 
377
377
  const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
378
- const text = `${formatContextUsage(pct, window)}${autoIcon}`;
378
+ const text = `${formatContextUsage(pct, window, ctx.contextTokens)}${autoIcon}`;
379
379
 
380
380
  const color = getContextUsageThemeColor(getContextUsageLevel(pct ?? 0, window));
381
381
  const content = withIcon(theme.icon.context, theme.fg(color, text));
@@ -73,6 +73,7 @@ export interface SegmentContext {
73
73
  };
74
74
  /** Context usage percent, or null when unknown (e.g. right after compaction). */
75
75
  contextPercent: number | null;
76
+ contextTokens: number;
76
77
  contextWindow: number;
77
78
  autoCompactEnabled: boolean;
78
79
  subagentCount: number;
@@ -6,7 +6,6 @@ import {
6
6
  fuzzyMatch,
7
7
  Input,
8
8
  matchesKey,
9
- ScrollView,
10
9
  Spacer,
11
10
  Text,
12
11
  TruncatedText,
@@ -20,6 +19,7 @@ import { shortenPath } from "../../tools/render-utils";
20
19
  import { toPathList } from "../../tools/search";
21
20
  import { canonicalizeMessage } from "../../utils/thinking-display";
22
21
  import { DynamicBorder } from "./dynamic-border";
22
+ import { centeredWindow, contentRowWidth, renderScrollableList } from "./selector-helpers";
23
23
 
24
24
  /** Gutter info: position (displayIndent where connector was) and whether to show │ */
25
25
  interface GutterInfo {
@@ -475,14 +475,11 @@ class TreeList implements Component {
475
475
  return lines;
476
476
  }
477
477
 
478
- const startIndex = Math.max(
479
- 0,
480
- Math.min(
481
- this.#selectedIndex - Math.floor(this.maxVisibleLines / 2),
482
- this.#filteredNodes.length - this.maxVisibleLines,
483
- ),
478
+ const { startIndex, endIndex } = centeredWindow(
479
+ this.#selectedIndex,
480
+ this.#filteredNodes.length,
481
+ this.maxVisibleLines,
484
482
  );
485
- const endIndex = Math.min(startIndex + this.maxVisibleLines, this.#filteredNodes.length);
486
483
 
487
484
  // Cap the per-row gutter prefix so a content budget is always preserved.
488
485
  // Each indent level renders as 3 cells; deep branching would otherwise eat the
@@ -494,8 +491,7 @@ class TreeList implements Component {
494
491
  const contentReserve = Math.max(MIN_CONTENT_COLS, Math.floor(width / 2));
495
492
  const maxIndentLevels = Math.max(1, Math.floor((width - contentReserve - OVERHEAD_COLS) / 3));
496
493
 
497
- const overflow = this.#filteredNodes.length > this.maxVisibleLines;
498
- const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
494
+ const rowWidth = contentRowWidth(width, this.#filteredNodes.length, this.maxVisibleLines);
499
495
  const rows: string[] = [];
500
496
 
501
497
  for (let i = startIndex; i < endIndex; i++) {
@@ -584,14 +580,13 @@ class TreeList implements Component {
584
580
  rows.push(truncateToWidth(line, rowWidth));
585
581
  }
586
582
 
587
- const sv = new ScrollView(rows, {
588
- height: rows.length,
589
- scrollbar: "auto",
590
- totalRows: this.#filteredNodes.length,
591
- theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
592
- });
593
- sv.setScrollOffset(startIndex);
594
- lines.push(...sv.render(width));
583
+ lines.push(
584
+ ...renderScrollableList(rows, {
585
+ width,
586
+ totalRows: this.#filteredNodes.length,
587
+ scrollOffset: startIndex,
588
+ }),
589
+ );
595
590
 
596
591
  const filterLabel = this.#getFilterLabel();
597
592
  if (filterLabel) {
@@ -217,6 +217,7 @@ export class CommandController {
217
217
  try {
218
218
  const result = await shareSession(this.ctx.session.sessionManager, {
219
219
  serverUrl: this.ctx.settings.get("share.serverUrl"),
220
+ store: this.ctx.settings.get("share.store"),
220
221
  state: this.ctx.session.state,
221
222
  obfuscator: this.ctx.settings.get("share.redactSecrets") ? this.ctx.session.obfuscator : undefined,
222
223
  });
@@ -5,7 +5,7 @@ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
5
5
  import { extractTextContent } from "../../commit/utils";
6
6
  import { settings } from "../../config/settings";
7
7
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
8
- import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
+ import type { AssistantMessageComponent } from "../../modes/components/assistant-message";
9
9
  import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
10
10
  import {
11
11
  ReadToolGroupComponent,
@@ -25,6 +25,7 @@ import type { ResolveToolDetails } from "../../tools/resolve";
25
25
  import { vocalizer } from "../../tts/vocalizer";
26
26
  import { canonicalizeMessage } from "../../utils/thinking-display";
27
27
  import { interruptHint } from "../shared";
28
+ import { createAssistantMessageComponent } from "../utils/interactive-context-helpers";
28
29
  import { StreamingRevealController } from "./streaming-reveal";
29
30
  import { ToolArgsRevealController } from "./tool-args-reveal";
30
31
 
@@ -332,14 +333,7 @@ export class EventController {
332
333
  this.ctx.ui.requestRender();
333
334
  } else if (event.message.role === "assistant") {
334
335
  this.#lastVisibleBlockCount = 0;
335
- this.ctx.streamingComponent = new AssistantMessageComponent(
336
- undefined,
337
- this.ctx.hideThinkingBlock,
338
- () => this.ctx.ui.requestRender(),
339
- this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
340
- this.ctx.ui.imageBudget,
341
- this.ctx.proseOnlyThinking,
342
- );
336
+ this.ctx.streamingComponent = createAssistantMessageComponent(this.ctx);
343
337
  this.ctx.streamingMessage = event.message;
344
338
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
345
339
  this.#streamingReveal.begin(this.ctx.streamingComponent, this.ctx.streamingMessage);
@@ -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[];