@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
@@ -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
+ }
@@ -39,16 +39,20 @@ import type { CompactionQueuedMessage, InteractiveModeContext } from "../../mode
39
39
  import {
40
40
  BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE,
41
41
  type CustomMessage,
42
- isSilentAbort,
43
42
  LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
44
- resolveAbortLabel,
45
43
  SKILL_PROMPT_MESSAGE_TYPE,
46
44
  type SkillPromptDetails,
47
45
  } from "../../session/messages";
48
46
  import type { SessionContext } from "../../session/session-context";
49
- import { createIrcMessageCard } from "../../tools/irc";
50
- import { formatBytes, formatDuration } from "../../tools/render-utils";
51
- import { canonicalizeMessage } from "../../utils/thinking-display";
47
+ import { createAssistantMessageComponent } from "./interactive-context-helpers";
48
+ import {
49
+ assistantHasVisibleContent,
50
+ buildAsyncResultBlock,
51
+ buildFileMentionBlock,
52
+ buildIrcMessageCard,
53
+ normalizeToolArgs,
54
+ resolveAssistantErrorMessage,
55
+ } from "./transcript-render-helpers";
52
56
 
53
57
  type TextBlock = { type: "text"; text: string };
54
58
  interface RenderInitialMessagesOptions {
@@ -143,47 +147,7 @@ export class UiHelpers {
143
147
  case "custom": {
144
148
  if (message.display) {
145
149
  if (message.customType === "async-result") {
146
- const details = (
147
- message as CustomMessage<{
148
- jobId?: string;
149
- type?: "bash" | "task";
150
- label?: string;
151
- durationMs?: number;
152
- jobs?: Array<{
153
- jobId?: string;
154
- type?: "bash" | "task";
155
- label?: string;
156
- durationMs?: number;
157
- }>;
158
- }>
159
- ).details;
160
- const jobs =
161
- details?.jobs && details.jobs.length > 0
162
- ? details.jobs
163
- : [
164
- {
165
- jobId: details?.jobId,
166
- type: details?.type,
167
- label: details?.label,
168
- durationMs: details?.durationMs,
169
- },
170
- ];
171
- const block = new TranscriptBlock();
172
- for (const job of jobs) {
173
- const jobId = job.jobId ?? "unknown";
174
- const typeLabel = job.type ? `[${job.type}]` : "[job]";
175
- const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
176
- const line = [
177
- theme.fg("success", `${theme.status.done} Background job completed`),
178
- theme.fg("dim", typeLabel),
179
- theme.fg("accent", jobId),
180
- duration ? theme.fg("dim", `(${duration})`) : undefined,
181
- ]
182
- .filter(Boolean)
183
- .join(" ");
184
- block.addChild(new Text(line, 1, 0));
185
- }
186
- this.ctx.chatContainer.addChild(block);
150
+ this.ctx.chatContainer.addChild(buildAsyncResultBlock(message));
187
151
  break;
188
152
  }
189
153
  if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
@@ -213,33 +177,7 @@ export class UiHelpers {
213
177
  message.customType === "irc:autoreply" ||
214
178
  message.customType === "irc:relay"
215
179
  ) {
216
- const details = (
217
- message as CustomMessage<{
218
- from?: string;
219
- to?: string;
220
- message?: string;
221
- body?: string;
222
- replyTo?: string;
223
- }>
224
- ).details;
225
- const kind =
226
- message.customType === "irc:incoming"
227
- ? ("incoming" as const)
228
- : message.customType === "irc:autoreply"
229
- ? ("autoreply" as const)
230
- : ("relay" as const);
231
- const card = createIrcMessageCard(
232
- {
233
- kind,
234
- from: details?.from,
235
- to: details?.to,
236
- body: kind === "incoming" ? details?.message : details?.body,
237
- replyTo: details?.replyTo,
238
- timestamp: message.timestamp,
239
- },
240
- () => this.ctx.toolOutputExpanded,
241
- theme,
242
- );
180
+ const card = buildIrcMessageCard(message, () => this.ctx.toolOutputExpanded);
243
181
  this.ctx.chatContainer.addChild(card);
244
182
  return [card];
245
183
  }
@@ -284,25 +222,7 @@ export class UiHelpers {
284
222
  }
285
223
  case "fileMention": {
286
224
  // Render compact file mention display
287
- const block = new TranscriptBlock();
288
- for (const file of message.files) {
289
- let suffix: string;
290
- if (file.skippedReason === "tooLarge") {
291
- const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
292
- suffix = `(skipped: ${size})`;
293
- } else {
294
- suffix = file.image
295
- ? "(image)"
296
- : file.lineCount === undefined
297
- ? "(unknown lines)"
298
- : `(${file.lineCount} lines)`;
299
- }
300
- const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
301
- "accent",
302
- file.path,
303
- )} ${theme.fg("dim", suffix)}`;
304
- block.addChild(new Text(text, 0, 0));
305
- }
225
+ const block = buildFileMentionBlock(message.files, 0);
306
226
  if (block.children.length > 0) this.ctx.chatContainer.addChild(block);
307
227
  break;
308
228
  }
@@ -326,14 +246,7 @@ export class UiHelpers {
326
246
  break;
327
247
  }
328
248
  case "assistant": {
329
- const assistantComponent = new AssistantMessageComponent(
330
- message,
331
- this.ctx.hideThinkingBlock,
332
- () => this.ctx.ui.requestRender(),
333
- this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
334
- this.ctx.ui.imageBudget,
335
- this.ctx.proseOnlyThinking,
336
- );
249
+ const assistantComponent = createAssistantMessageComponent(this.ctx, message);
337
250
  this.ctx.chatContainer.addChild(assistantComponent);
338
251
  break;
339
252
  }
@@ -425,11 +338,7 @@ export class UiHelpers {
425
338
  this.ctx.lastAssistantUsage = usage;
426
339
  }
427
340
  }
428
- const hasVisibleAssistantContent = message.content.some(
429
- content =>
430
- (content.type === "text" && canonicalizeMessage(content.text)) ||
431
- (content.type === "thinking" && canonicalizeMessage(content.thinking)),
432
- );
341
+ const hasVisibleAssistantContent = assistantHasVisibleContent(message);
433
342
  if (hasVisibleAssistantContent) {
434
343
  // Rebuild reconstructs immutable history; seal (not finalize) so the
435
344
  // group freezes even if a read's result was never persisted —
@@ -438,14 +347,10 @@ export class UiHelpers {
438
347
  readGroup?.seal();
439
348
  readGroup = null;
440
349
  }
441
- const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
442
- const hasErrorStop =
443
- !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
444
- const errorMessage = hasErrorStop
445
- ? message.stopReason === "aborted"
446
- ? resolveAbortLabel(message.errorMessage, this.ctx.viewSession.retryAttempt)
447
- : message.errorMessage || "Error"
448
- : null;
350
+ const { hasErrorStop, errorMessage } = resolveAssistantErrorMessage(
351
+ message,
352
+ this.ctx.viewSession.retryAttempt,
353
+ );
449
354
 
450
355
  // Render tool call components
451
356
  for (const content of message.content) {
@@ -474,10 +379,7 @@ export class UiHelpers {
474
379
  content.id,
475
380
  );
476
381
  } else {
477
- const normalizedArgs =
478
- content.arguments && typeof content.arguments === "object" && !Array.isArray(content.arguments)
479
- ? (content.arguments as Record<string, unknown>)
480
- : {};
382
+ const normalizedArgs = normalizeToolArgs(content.arguments);
481
383
  readToolCallArgs.set(content.id, normalizedArgs);
482
384
  if (assistantComponent) {
483
385
  readToolCallAssistantComponents.set(content.id, assistantComponent);
@@ -647,10 +549,7 @@ export class UiHelpers {
647
549
  }
648
550
 
649
551
  clearEditor(): void {
650
- this.ctx.editor.setText("");
651
- this.ctx.pendingImages = [];
652
- this.ctx.pendingImageLinks = [];
653
- this.ctx.editor.imageLinks = undefined;
552
+ this.ctx.editor.clearDraft();
654
553
  this.ctx.ui.requestRender();
655
554
  }
656
555
 
@@ -719,11 +618,7 @@ export class UiHelpers {
719
618
  queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void {
720
619
  const queuedImages = images && images.length > 0 ? images : undefined;
721
620
  this.ctx.compactionQueuedMessages.push({ text, mode, images: queuedImages } as CompactionQueuedMessage);
722
- this.ctx.editor.addToHistory(text);
723
- this.ctx.editor.setText("");
724
- this.ctx.editor.imageLinks = undefined;
725
- this.ctx.pendingImages = [];
726
- this.ctx.pendingImageLinks = [];
621
+ this.ctx.editor.clearDraft(text);
727
622
  this.ctx.updatePendingMessagesDisplay();
728
623
  this.ctx.showStatus(
729
624
  queuedImages ? "Queued message with image for after compaction" : "Queued message for after compaction",
@@ -101,12 +101,12 @@ Use tools whenever they improve correctness, completeness, or grounding.
101
101
  {{#if secretsEnabled}}- Redacted `#XXXX#` tokens in output are opaque strings.{{/if}}
102
102
  {{#has tools "inspect_image"}}- Image tasks: prefer `{{toolRefs.inspect_image}}` over `{{toolRefs.read}}` to spare session context.{{/has}}
103
103
 
104
- # Specialized Tool Priority
104
+ # Specialized Tools
105
105
  You MUST use the specialized tool over its shell equivalent:
106
- {{#has tools "read"}}- File or directory reads → `{{toolRefs.read}}`, not `cat` or `ls` (a directory path lists entries).{{/has}}
107
- {{#has tools "edit"}}- Surgical edits → `{{toolRefs.edit}}`, not `sed`.{{/has}}
108
- {{#has tools "write"}}- Create or overwrite → `{{toolRefs.write}}`, not shell redirection.{{/has}}
109
- {{#has tools "lsp"}}- Code intelligence → `{{toolRefs.lsp}}`, not blind search.{{/has}}
106
+ {{#has tools "read"}}- File or directory reads → `{{toolRefs.read}}` (a directory path lists entries).{{/has}}
107
+ {{#has tools "edit"}}- Surgical edits → `{{toolRefs.edit}}`.{{/has}}
108
+ {{#has tools "write"}}- Create or overwrite → `{{toolRefs.write}}`.{{/has}}
109
+ {{#has tools "lsp"}}- Code intelligence → `{{toolRefs.lsp}}`.{{/has}}
110
110
  {{#has tools "search"}}- Regex search → `{{toolRefs.search}}`, not `grep`, `rg`, or `awk`.{{/has}}
111
111
  {{#has tools "find"}}- Globbing → `{{toolRefs.find}}`, not `ls **/*.ext` or `fd`.{{/has}}
112
112
  {{#has tools "eval"}}- Quick compute → `{{toolRefs.eval}}`; you SHOULD go step by step.{{/has}}
@@ -14,9 +14,8 @@ Runs bash in a shell session — terminal ops: git, bun, cargo, python.
14
14
  </instruction>
15
15
 
16
16
  <critical>
17
- - NEVER shell out to fetch, display, list, page, or search what a dedicated tool serves: `cat`/`head`/`tail`/`less`/`more`/`ls` → `read`; `grep`/`rg`/`ag`/`ack` → `search`; `find`/`fd` → `find`; `sed -i`/`perl -i`/`awk -i` → `edit`; `echo >`/heredoc → `write`. Tools keep gitignore semantics, line anchors, structured output shell loses.
18
- - NEVER trim or silence output: no `| head -n N`, `| tail -n N`, `| less`, `2>&1`, `2>/dev/null`. stderr already merged; long output auto-truncated, FULL capture kept at `artifact://<id>`.
19
- - Pipelines that COMPUTE a new fact are correct bash: `wc -l`, `sort | uniq -c`, `comm`, `cut`, `diff a b`, `shasum`. Litmus: produces a count, frequency table, set difference, or checksum no tool returns → bash. Merely moves or trims bytes a tool can fetch → use the tool.
17
+ - NEVER shell out to search content or files: `grep/rg` → `search`.
18
+ - Avoid head/tail/redirections: stderr already merged; long output auto-truncated, FULL capture kept at `artifact://<id>`.
20
19
  </critical>
21
20
 
22
21
  <output>
@@ -5,7 +5,7 @@ Cells run in array order. State persists per language across cells, tool calls,
5
5
 
6
6
  Cell fields:
7
7
 
8
- - `language` — {{#if py}}`"py"` IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` persistent JavaScript VM{{/if}}.
8
+ - `language` — {{#if py}}`"py"` IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` persistent JavaScript VM{{/if}}{{#if rb}}{{#ifAny py js}}, {{/ifAny}}`"rb"` persistent Ruby kernel{{/if}}{{#if jl}}{{#ifAny py js rb}}, {{/ifAny}}`"jl"` persistent Julia kernel{{/if}}.
9
9
  - `code` — cell body, verbatim. Newlines/quotes JSON-encoded; no fences, no headers.
10
10
  - `title` (optional) — short transcript label (e.g. `"imports"`).
11
11
  - `timeout` (optional) — per-cell seconds. Raise only for heavy compute or long non-agent tool calls.
@@ -13,11 +13,13 @@ Cell fields:
13
13
 
14
14
  Work incrementally — one logical step per cell (imports, define, test, use), many small cells per call; workflow notes in the assistant message or `title`, never in cell code.
15
15
  {{#if py}}Live event loop: use top-level `await` directly; `asyncio.run(…)` raises "cannot be called from a running event loop".{{/if}}
16
+ {{#if rb}}Ruby: synchronous; helper options are keyword args (e.g. `tree(".", max_depth: 2)`); the last expression auto-displays unless it is `nil`, an assignment, or a definition (like IRB).{{/if}}
17
+ {{#if jl}}Julia: synchronous; helper options are standard keyword args (e.g. `tree(max_depth=2)`); the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
16
18
  Errors name the failing cell ("Cell 3 failed") — resubmit the fixed cell + any remaining.
17
19
  </instruction>
18
20
 
19
21
  <prelude>
20
- {{#ifAll py js}}Same helpers + arg order, both runtimes. Python: sync, options = trailing kwargs. JS: async/`await`able, options = ONE trailing object literal, never positional (extras throw).{{else}}{{#if py}}Sync; options = trailing kwargs.{{/if}}{{#if js}}Async/`await`able; options = ONE trailing object literal, never positional (extras throw).{{/if}}{{/ifAll}}
22
+ {{#ifAll py js}}Same helpers + arg order, both runtimes. Python: sync, options = trailing kwargs. JS: async/`await`able, options = ONE trailing object literal, never positional (extras throw).{{else}}{{#if py}}Sync; options = trailing kwargs.{{/if}}{{#if js}}Async/`await`able; options = ONE trailing object literal, never positional (extras throw).{{/if}}{{/ifAll}}{{#if rb}} Ruby: sync, options = trailing keyword args.{{/if}}{{#if jl}} Julia: sync, options = trailing keyword args.{{/if}}
21
23
  ```
22
24
  display(value) → None
23
25
  Cell output; figures/images/dataframes shown natively.
@@ -55,13 +57,13 @@ log(message) → None
55
57
  phase(title) → None
56
58
  Phase grouping subsequent status lines.
57
59
  budget → per-turn token budget
58
- {{#if py}}`budget.total` (ceiling or None), `budget.spent()`, `budget.remaining()` (math.inf when no ceiling), `budget.hard`.{{/if}}{{#if js}}`await budget.total()` (ceiling or null), `await budget.spent()`, `await budget.remaining()` (Infinity when no ceiling), `await budget.hard()`.{{/if}} Ceiling: `+Nk` (advisory) or `+Nk!`/Goal Mode (hard — `agent()` won't spawn past it); spend still tracked.
60
+ {{#if py}}`budget.total` (ceiling or None), `budget.spent()`, `budget.remaining()` (math.inf when no ceiling), `budget.hard`.{{/if}}{{#if js}}`await budget.total()` (ceiling or null), `await budget.spent()`, `await budget.remaining()` (Infinity when no ceiling), `await budget.hard()`.{{/if}}{{#if rb}} Ruby: `budget.total` (ceiling or nil), `budget.spent`, `budget.remaining` (Float::INFINITY when no ceiling), `budget.hard`.{{/if}}{{#if jl}} Julia: `budget.total` (ceiling or nothing), `budget.spent()`, `budget.remaining()` (Inf when no ceiling), `budget.hard`.{{/if}} Ceiling: `+Nk` (advisory) or `+Nk!`/Goal Mode (hard — `agent()` won't spawn past it); spend still tracked.
59
61
  ```
60
62
  </prelude>
61
63
  {{#if spawns}}
62
64
  <dag>
63
65
  Pipe handles through stage helpers to build a dependency graph — acyclic waves:
64
- - **Name nodes.** Capture each `agent(…, {{#if py}}return_handle=True{{/if}}{{#if js}}{ returnHandle: true }{{/if}})` result; carries `handle` (`agent://<id>`) + `output`.
66
+ - **Name nodes.** Capture each `agent(…, {{#if py}}return_handle=True{{/if}}{{#if js}}{ returnHandle: true }{{/if}}{{#if jl}}return_handle=true{{/if}})` result; carries `handle` (`agent://<id>`) + `output`.
65
67
  - **Wire edges by reference.** Put an upstream node's `handle`/`output` in the dependent stage's prompt — large transcript never re-inlined. Bulk: `write("local://<name>.md", …)`, pass the URI.
66
68
  - **`pipeline(items, *stages)` = staged waves**, barrier between stages (every item clears stage N before any enters N+1). **`parallel(thunks)` = one wave** of independent nodes.
67
69
  - **Isolate failure.** A raising node re-raises the lowest-index error, aborts its wave; wrap risky nodes in try/except so a failure degrades only its dependent subtree, independent branches finish.
@@ -13,7 +13,3 @@ Matching paths sorted by mtime (newest first), grouped under `# <dir>/` headers
13
13
  <avoid>
14
14
  Open-ended searches needing multiple rounds of globbing/searching: you MUST use the Task tool instead.
15
15
  </avoid>
16
-
17
- <critical>
18
- - You MUST use the built-in Find tool for every file-name lookup. NEVER shell out to `find`, `fd`, `locate`, `ls`, or `git ls-files` via Bash — they ignore `.gitignore`, blow past result limits, and waste tokens.
19
- </critical>
@@ -70,7 +70,6 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
70
70
  All URI schemes take the same line selectors. `artifact://<id>` recovers full output a bash/eval/tool result spilled or truncated. `history://<agentId>` = agent transcript; bare `history://` lists agents.
71
71
 
72
72
  <critical>
73
- - MUST use `read` for every file/directory/archive/URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN bash calls, however convenient.
74
- - Line ranges go in the selector (`path="src/foo.ts:50-200"`) — NEVER `sed -n`, `awk NR`, or `head`/`tail` pipelines.
73
+ - Line ranges go in the selector: `path="src/foo.ts:50-200"`.
75
74
  - Summary footer names elided ranges? Re-issue ONLY those ranges. NEVER guess `..`/`…` content.
76
75
  </critical>
@@ -25,5 +25,5 @@ For pattern-addressed bulk changes, bash is more efficient:
25
25
  |Bulk replace across files|`sd 'pattern' 'replacement' **/*.ts`|
26
26
 
27
27
  Use Replace when _content itself_ identifies location; use `ast_edit` for structure-aware codemods.
28
- NEVER use `sed -i`/`perl -i`/heredoc redirection for editsthose calls are blocked; use this tool or `write`.
28
+ For in-place edits prefer this tool or `write`you get a diff preview and fuzzy matching.
29
29
  </bash-alternatives>
package/src/sdk.ts CHANGED
@@ -46,7 +46,9 @@ import { Settings, type SkillsSettings } from "./config/settings";
46
46
  import { CursorExecHandlers } from "./cursor";
47
47
  import "./discovery";
48
48
  import { initializeWithSettings } from "./discovery";
49
+ import { disposeAllJuliaKernelSessions, disposeJuliaKernelSessionsByOwner } from "./eval/jl/executor";
49
50
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
51
+ import { disposeAllRubyKernelSessions, disposeRubyKernelSessionsByOwner } from "./eval/rb/executor";
50
52
  import { defaultEvalSessionId } from "./eval/session-id";
51
53
  import {
52
54
  type CustomCommandsLoadResult,
@@ -472,7 +474,7 @@ export interface CreateAgentSessionOptions {
472
474
 
473
475
  /** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
474
476
  enableLsp?: boolean;
475
- /** Skip Python kernel availability check and prelude warmup */
477
+ /** Skip subprocess-kernel availability checks and prelude warmup */
476
478
  skipPythonPreflight?: boolean;
477
479
  /** Tool names explicitly requested (enables disabled-by-default tools) */
478
480
  toolNames?: string[];
@@ -866,12 +868,14 @@ function registerSshCleanup(): void {
866
868
  postmortem.register("ssh-cleanup", cleanupSshResources);
867
869
  }
868
870
 
869
- let pythonCleanupRegistered = false;
871
+ let evalCleanupRegistered = false;
870
872
 
871
- function registerPythonCleanup(): void {
872
- if (pythonCleanupRegistered) return;
873
- pythonCleanupRegistered = true;
873
+ function registerEvalCleanup(): void {
874
+ if (evalCleanupRegistered) return;
875
+ evalCleanupRegistered = true;
874
876
  postmortem.register("python-cleanup", disposeAllKernelSessions);
877
+ postmortem.register("ruby-cleanup", disposeAllRubyKernelSessions);
878
+ postmortem.register("julia-cleanup", disposeAllJuliaKernelSessions);
875
879
  }
876
880
 
877
881
  function customToolToDefinition(tool: CustomTool): ToolDefinition {
@@ -1080,7 +1084,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1080
1084
  const eventBus = options.eventBus ?? new EventBus();
1081
1085
 
1082
1086
  registerSshCleanup();
1083
- registerPythonCleanup();
1087
+ registerEvalCleanup();
1084
1088
 
1085
1089
  // Pin authStorage to modelRegistry.authStorage: ModelRegistry.getApiKey() routes refresh
1086
1090
  // failures through that instance, so any divergent storage handed to the bridge / mcpManager
@@ -2693,7 +2697,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2693
2697
  const originalDispose = session.dispose.bind(session);
2694
2698
  session.dispose = async () => {
2695
2699
  try {
2696
- // Reject new session work (Python/eval starts) the moment disposal
2700
+ // Reject new session work (eval starts) the moment disposal
2697
2701
  // begins — the lifecycle await below opens an async gap before
2698
2702
  // AgentSession.dispose() would otherwise set its guards.
2699
2703
  session.beginDispose();
@@ -2913,6 +2917,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2913
2917
  await asyncJobManager.dispose({ timeoutMs: 3_000 });
2914
2918
  }
2915
2919
  await disposeKernelSessionsByOwner(evalKernelOwnerId);
2920
+ await disposeRubyKernelSessionsByOwner(evalKernelOwnerId);
2921
+ await disposeJuliaKernelSessionsByOwner(evalKernelOwnerId);
2916
2922
  if (ownsAuthStorage) authStorage.close();
2917
2923
  }
2918
2924
  } catch (cleanupError) {
@@ -259,9 +259,9 @@ export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages:
259
259
  }
260
260
 
261
261
  /**
262
- * Restore placeholders in assistant content: visible text, thinking text, and
263
- * tool-call arguments/intent/rawBlock. Signatures and redacted-thinking bytes
264
- * are opaque provider-replay data and pass through byte-identical.
262
+ * Restore placeholders in assistant content: visible text and tool-call
263
+ * arguments/intent/rawBlock. Thinking and signatures are opaque
264
+ * provider-replay/hidden-reasoning data and pass through byte-identical.
265
265
  */
266
266
  export function deobfuscateAssistantContent(
267
267
  obfuscator: SecretObfuscator,
@@ -276,12 +276,6 @@ export function deobfuscateAssistantContent(
276
276
  changed = true;
277
277
  return { ...block, text };
278
278
  }
279
- if (block.type === "thinking") {
280
- const thinking = obfuscator.deobfuscate(block.thinking);
281
- if (thinking === block.thinking) return block;
282
- changed = true;
283
- return { ...block, thinking };
284
- }
285
279
  if (block.type === "toolCall") {
286
280
  const args = deobfuscateToolArguments(obfuscator, block.arguments);
287
281
  const intent = block.intent === undefined ? undefined : obfuscator.deobfuscate(block.intent);