@oh-my-pi/pi-coding-agent 14.9.3 → 14.9.5

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 (79) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/package.json +7 -7
  3. package/src/async/job-manager.ts +66 -9
  4. package/src/capability/rule.ts +20 -0
  5. package/src/config/model-registry.ts +13 -0
  6. package/src/config/model-resolver.ts +8 -2
  7. package/src/config/settings-schema.ts +1 -1
  8. package/src/edit/index.ts +8 -0
  9. package/src/edit/renderer.ts +6 -1
  10. package/src/edit/streaming.ts +53 -2
  11. package/src/eval/js/context-manager.ts +1 -38
  12. package/src/eval/js/prelude.txt +0 -2
  13. package/src/eval/py/executor.ts +24 -8
  14. package/src/eval/py/index.ts +1 -0
  15. package/src/eval/py/prelude.py +11 -80
  16. package/src/export/html/template.css +12 -0
  17. package/src/export/html/template.generated.ts +1 -1
  18. package/src/export/html/template.js +20 -2
  19. package/src/extensibility/plugins/loader.ts +31 -6
  20. package/src/extensibility/skills.ts +20 -0
  21. package/src/internal-urls/agent-protocol.ts +63 -52
  22. package/src/internal-urls/artifact-protocol.ts +51 -51
  23. package/src/internal-urls/docs-index.generated.ts +33 -1
  24. package/src/internal-urls/index.ts +6 -19
  25. package/src/internal-urls/local-protocol.ts +49 -7
  26. package/src/internal-urls/mcp-protocol.ts +2 -8
  27. package/src/internal-urls/memory-protocol.ts +89 -59
  28. package/src/internal-urls/router.ts +38 -22
  29. package/src/internal-urls/rule-protocol.ts +2 -20
  30. package/src/internal-urls/skill-protocol.ts +4 -27
  31. package/src/main.ts +1 -1
  32. package/src/mcp/manager.ts +17 -0
  33. package/src/modes/components/session-observer-overlay.ts +2 -2
  34. package/src/modes/components/tool-execution.ts +6 -0
  35. package/src/modes/components/tree-selector.ts +4 -0
  36. package/src/modes/controllers/event-controller.ts +23 -2
  37. package/src/modes/controllers/mcp-command-controller.ts +7 -10
  38. package/src/modes/interactive-mode.ts +2 -2
  39. package/src/modes/theme/theme.ts +27 -27
  40. package/src/modes/types.ts +1 -1
  41. package/src/modes/utils/ui-helpers.ts +14 -9
  42. package/src/prompts/commands/orchestrate.md +1 -0
  43. package/src/prompts/system/project-prompt.md +10 -2
  44. package/src/prompts/system/subagent-system-prompt.md +8 -8
  45. package/src/prompts/system/system-prompt.md +13 -7
  46. package/src/prompts/tools/ask.md +0 -1
  47. package/src/prompts/tools/bash.md +0 -10
  48. package/src/prompts/tools/eval.md +1 -3
  49. package/src/prompts/tools/github.md +6 -5
  50. package/src/prompts/tools/hashline.md +1 -0
  51. package/src/prompts/tools/job.md +14 -6
  52. package/src/prompts/tools/task.md +20 -3
  53. package/src/registry/agent-registry.ts +2 -1
  54. package/src/sdk.ts +87 -89
  55. package/src/session/agent-session.ts +58 -20
  56. package/src/session/artifacts.ts +7 -4
  57. package/src/session/session-manager.ts +30 -1
  58. package/src/ssh/connection-manager.ts +32 -16
  59. package/src/ssh/sshfs-mount.ts +10 -7
  60. package/src/system-prompt.ts +0 -5
  61. package/src/task/executor.ts +14 -2
  62. package/src/task/index.ts +19 -5
  63. package/src/tool-discovery/tool-index.ts +21 -8
  64. package/src/tools/ast-edit.ts +3 -2
  65. package/src/tools/ast-grep.ts +3 -2
  66. package/src/tools/bash.ts +15 -9
  67. package/src/tools/browser/tab-supervisor.ts +12 -2
  68. package/src/tools/eval.ts +48 -10
  69. package/src/tools/fetch.ts +1 -1
  70. package/src/tools/gh.ts +140 -4
  71. package/src/tools/index.ts +12 -11
  72. package/src/tools/job.ts +48 -12
  73. package/src/tools/read.ts +5 -4
  74. package/src/tools/search.ts +3 -2
  75. package/src/tools/todo-write.ts +1 -1
  76. package/src/web/scrapers/mastodon.ts +1 -1
  77. package/src/web/scrapers/repology.ts +7 -7
  78. package/src/internal-urls/jobs-protocol.ts +0 -120
  79. package/src/prompts/system/now-prompt.md +0 -7
@@ -2,7 +2,12 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
4
4
  import { $ } from "bun";
5
- import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
5
+ import {
6
+ getControlDir,
7
+ getControlPathTemplate,
8
+ type SSHConnectionTarget,
9
+ supportsSshControlMaster,
10
+ } from "./connection-manager";
6
11
  import { buildSshTarget, sanitizeHostName } from "./utils";
7
12
 
8
13
  const REMOTE_DIR = getRemoteDir();
@@ -40,14 +45,12 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
40
45
  "BatchMode=yes",
41
46
  "-o",
42
47
  "StrictHostKeyChecking=accept-new",
43
- "-o",
44
- "ControlMaster=auto",
45
- "-o",
46
- `ControlPath=${CONTROL_PATH}`,
47
- "-o",
48
- "ControlPersist=3600",
49
48
  ];
50
49
 
50
+ if (supportsSshControlMaster()) {
51
+ args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
52
+ }
53
+
51
54
  if (host.port) {
52
55
  args.push("-p", String(host.port));
53
56
  }
@@ -12,7 +12,6 @@ import type { SkillsSettings } from "./config/settings";
12
12
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
13
13
  import { loadSkills, type Skill } from "./extensibility/skills";
14
14
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
15
- import nowPromptTemplate from "./prompts/system/now-prompt.md" with { type: "text" };
16
15
  import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
17
16
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
18
17
  import { shortenPath } from "./tools/render-utils";
@@ -575,10 +574,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
575
574
  if (projectPrompt) {
576
575
  systemPrompt.push(projectPrompt);
577
576
  }
578
- const nowPrompt = prompt.render(nowPromptTemplate, data).trim();
579
- if (nowPrompt) {
580
- systemPrompt.push(nowPrompt);
581
- }
582
577
 
583
578
  return { systemPrompt };
584
579
  }
@@ -26,9 +26,11 @@ import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md
26
26
  import { AgentRegistry } from "../registry/agent-registry";
27
27
  import { createAgentSession, discoverAuthStorage } from "../sdk";
28
28
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
29
+ import type { ArtifactManager } from "../session/artifacts";
29
30
  import type { AuthStorage } from "../session/auth-storage";
30
31
  import { SessionManager } from "../session/session-manager";
31
- import { type ContextFileEntry, truncateTail } from "../tools";
32
+ import { truncateTail } from "../session/streaming-output";
33
+ import type { ContextFileEntry } from "../tools";
32
34
  import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
33
35
  import { ToolAbortError } from "../tools/tool-errors";
34
36
  import type { EventBus } from "../utils/event-bus";
@@ -172,6 +174,12 @@ export interface ExecutorOptions {
172
174
  settings?: Settings;
173
175
  /** Override local:// protocol options so subagent shares parent's local:// root */
174
176
  localProtocolOptions?: LocalProtocolOptions;
177
+ /**
178
+ * Parent session's ArtifactManager. Subagent adopts it so artifact IDs are
179
+ * unique across the whole agent tree and all artifacts land in the parent's
180
+ * artifacts directory (no per-subagent subdir).
181
+ */
182
+ parentArtifactManager?: ArtifactManager;
175
183
  parentHindsightSessionState?: HindsightSessionState;
176
184
  }
177
185
 
@@ -563,6 +571,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
563
571
 
564
572
  const lspEnabled = enableLsp ?? true;
565
573
  const ircEnabled = subagentSettings.get("irc.enabled") === true;
574
+ const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
566
575
  const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
567
576
 
568
577
  const outputChunks: string[] = [];
@@ -975,6 +984,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
975
984
  const sessionManager = sessionFile
976
985
  ? await SessionManager.open(sessionFile)
977
986
  : SessionManager.inMemory(worktree ?? cwd);
987
+ if (options.parentArtifactManager) {
988
+ sessionManager.adoptArtifactManager(options.parentArtifactManager);
989
+ }
978
990
 
979
991
  const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
980
992
  const enableMCP = !options.mcpManager;
@@ -1001,7 +1013,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1001
1013
  context: options.context?.trim() ?? "",
1002
1014
  worktree: worktree ?? "",
1003
1015
  outputSchema: normalizedOutputSchema,
1004
- contextFile: options.contextFile,
1016
+ contextFile: contextFileForPrompt,
1005
1017
  ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
1006
1018
  ircSelfId: ircEnabled ? id : "",
1007
1019
  });
package/src/task/index.ts CHANGED
@@ -20,7 +20,9 @@ import type { Usage } from "@oh-my-pi/pi-ai";
20
20
  import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
21
21
  import type { TSchema } from "@sinclair/typebox";
22
22
  import type { ToolSession } from "..";
23
+ import { AsyncJobManager } from "../async";
23
24
  import { resolveAgentModelPatterns } from "../config/model-resolver";
25
+ import { MCPManager } from "../mcp/manager";
24
26
  import type { Theme } from "../modes/theme/theme";
25
27
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
26
28
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
@@ -141,6 +143,7 @@ function renderDescription(
141
143
  asyncEnabled: boolean,
142
144
  disabledAgents: string[],
143
145
  simpleMode: TaskSimpleMode,
146
+ ircEnabled: boolean,
144
147
  ): string {
145
148
  const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
146
149
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
@@ -151,6 +154,7 @@ function renderDescription(
151
154
  asyncEnabled,
152
155
  contextEnabled,
153
156
  customSchemaEnabled,
157
+ ircEnabled,
154
158
  defaultMode: simpleMode === "default",
155
159
  schemaFreeMode: simpleMode === "schema-free",
156
160
  independentMode: simpleMode === "independent",
@@ -229,6 +233,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
229
233
  this.session.settings.get("async.enabled"),
230
234
  disabledAgents,
231
235
  this.#getTaskSimpleMode(),
236
+ this.session.settings.get("irc.enabled") === true,
232
237
  );
233
238
  }
234
239
  private constructor(
@@ -270,7 +275,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
270
275
  return this.#executeSync(_toolCallId, params, signal, onUpdate);
271
276
  }
272
277
 
273
- const manager = this.session.asyncJobManager;
278
+ const manager = AsyncJobManager.instance();
274
279
  if (!manager) {
275
280
  return {
276
281
  content: [{ type: "text", text: "Async execution is enabled but no async job manager is available." }],
@@ -444,6 +449,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
444
449
  },
445
450
  {
446
451
  id: label,
452
+ ownerId: this.session.getAgentId?.() ?? undefined,
447
453
  onProgress: (text, details) => {
448
454
  const progressDetails =
449
455
  (details as TaskToolDetails | undefined) ??
@@ -729,6 +735,10 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
729
735
  getSessionId: this.session.getSessionId ?? (() => null),
730
736
  };
731
737
 
738
+ // Subagents adopt the parent's ArtifactManager so artifact IDs are unique
739
+ // across the whole tree and outputs land flat in the parent's dir.
740
+ const parentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
741
+
732
742
  // Initialize progress tracking
733
743
  const progressMap = new Map<number, AgentProgress>();
734
744
 
@@ -785,9 +795,11 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
785
795
  };
786
796
  }
787
797
 
788
- // Write parent conversation context for subagents
798
+ // Write parent conversation context for subagents. When IRC is available,
799
+ // subagents should ask live peers instead of reading a stale markdown dump.
789
800
  await fs.mkdir(effectiveArtifactsDir, { recursive: true });
790
- const compactContext = this.session.getCompactContext?.();
801
+ const shouldWriteConversationContext = this.session.settings.get("irc.enabled") !== true;
802
+ const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
791
803
  let contextFilePath: string | undefined;
792
804
  if (compactContext) {
793
805
  contextFilePath = path.join(effectiveArtifactsDir, "context.md");
@@ -867,12 +879,13 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
867
879
  authStorage: this.session.authStorage,
868
880
  modelRegistry: this.session.modelRegistry,
869
881
  settings: this.session.settings,
870
- mcpManager: this.session.mcpManager,
882
+ mcpManager: MCPManager.instance(),
871
883
  contextFiles,
872
884
  skills: availableSkills,
873
885
  workspaceTree: this.session.workspaceTree,
874
886
  promptTemplates,
875
887
  localProtocolOptions,
888
+ parentArtifactManager,
876
889
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
877
890
  });
878
891
  }
@@ -925,12 +938,13 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
925
938
  authStorage: this.session.authStorage,
926
939
  modelRegistry: this.session.modelRegistry,
927
940
  settings: this.session.settings,
928
- mcpManager: this.session.mcpManager,
941
+ mcpManager: MCPManager.instance(),
929
942
  contextFiles,
930
943
  skills: availableSkills,
931
944
  workspaceTree: this.session.workspaceTree,
932
945
  promptTemplates,
933
946
  localProtocolOptions,
947
+ parentArtifactManager,
934
948
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
935
949
  });
936
950
  if (mergeMode === "branch" && result.exitCode === 0) {
@@ -89,6 +89,7 @@ export interface DiscoverableMCPSearchResult {
89
89
 
90
90
  const BM25_K1 = 1.2;
91
91
  const BM25_B = 0.75;
92
+ const BM25_DELTA = 1.0;
92
93
  const FIELD_WEIGHTS = {
93
94
  name: 6,
94
95
  label: 4,
@@ -112,13 +113,24 @@ function getSchemaPropertyKeys(parameters: unknown): string[] {
112
113
  }
113
114
 
114
115
  function tokenize(value: string): string[] {
115
- return value
116
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
117
- .replace(/[^a-zA-Z0-9]+/g, " ")
118
- .toLowerCase()
119
- .trim()
120
- .split(/\s+/)
121
- .filter(token => token.length > 0);
116
+ return (
117
+ value
118
+ .normalize("NFKD")
119
+ // Drop combining marks (accents) so "café" → "cafe".
120
+ .replace(/\p{M}+/gu, "")
121
+ // Split ACRONYMBoundary: "MCPTool" → "MCP Tool".
122
+ .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, "$1 $2")
123
+ // Split camelCase / digit→letter: "fooBar" → "foo Bar", "v2Beta" → "v2 Beta".
124
+ .replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, "$1 $2")
125
+ // Everything that isn't a letter or digit becomes a separator. This subsumes markdown
126
+ // punctuation (`|*_`#-~>[]()`), box-drawing glyphs (─│┌), em/en dashes, smart quotes,
127
+ // zero-width spaces, NBSPs, etc.
128
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
129
+ .toLowerCase()
130
+ .trim()
131
+ .split(/\s+/)
132
+ .filter(token => token.length > 0)
133
+ );
122
134
  }
123
135
 
124
136
  function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
@@ -274,7 +286,8 @@ export function searchDiscoverableTools(
274
286
  const documentFrequency = index.documentFrequencies.get(token) ?? 0;
275
287
  const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
276
288
  const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
277
- score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
289
+ score +=
290
+ queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization) + BM25_DELTA);
278
291
  }
279
292
  return { tool: document.tool, score };
280
293
  })
@@ -7,6 +7,7 @@ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
10
+ import { InternalUrlRouter } from "../internal-urls";
10
11
  import type { Theme } from "../modes/theme/theme";
11
12
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
12
13
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -213,10 +214,10 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
213
214
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
214
215
  throw new ToolError("`paths` must contain non-empty paths or globs");
215
216
  }
216
- const internalRouter = this.session.internalRouter;
217
+ const internalRouter = InternalUrlRouter.instance();
217
218
  const resolvedPathInputs: string[] = [];
218
219
  for (const rawPath of rawPaths) {
219
- if (!internalRouter?.canHandle(rawPath)) {
220
+ if (!internalRouter.canHandle(rawPath)) {
220
221
  resolvedPathInputs.push(rawPath);
221
222
  continue;
222
223
  }
@@ -6,6 +6,7 @@ import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
+ import { InternalUrlRouter } from "../internal-urls";
9
10
  import type { Theme } from "../modes/theme/theme";
10
11
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
11
12
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -158,10 +159,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
158
159
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
159
160
  throw new ToolError("`paths` must contain non-empty paths or globs");
160
161
  }
161
- const internalRouter = this.session.internalRouter;
162
+ const internalRouter = InternalUrlRouter.instance();
162
163
  const resolvedPathInputs: string[] = [];
163
164
  for (const rawPath of rawPaths) {
164
- if (!internalRouter?.canHandle(rawPath)) {
165
+ if (!internalRouter.canHandle(rawPath)) {
165
166
  resolvedPathInputs.push(rawPath);
166
167
  continue;
167
168
  }
package/src/tools/bash.ts CHANGED
@@ -4,8 +4,10 @@ import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
5
5
  import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
6
6
  import { Type } from "@sinclair/typebox";
7
+ import { AsyncJobManager } from "../async";
7
8
  import { type BashResult, executeBash } from "../exec/bash-executor";
8
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
+ import { InternalUrlRouter } from "../internal-urls";
9
11
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
10
12
  import type { Theme } from "../modes/theme/theme";
11
13
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
@@ -326,7 +328,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
326
328
  }
327
329
  lines.push(`Background job ${jobId} started: ${label}`);
328
330
  lines.push("Result will be delivered automatically when complete.");
329
- lines.push(`Use \`job\` (with \`poll\` or \`cancel\`) or \`read jobs://${jobId}\` if needed.`);
331
+ lines.push(
332
+ `You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
333
+ );
330
334
  return {
331
335
  content: [{ type: "text", text: lines.join("\n") }],
332
336
  details,
@@ -349,7 +353,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
349
353
  onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
350
354
  startBackgrounded: boolean;
351
355
  }): ManagedBashJobHandle {
352
- const manager = this.session.asyncJobManager;
356
+ const manager = AsyncJobManager.instance();
353
357
  if (!manager) {
354
358
  throw new ToolError("Background job manager unavailable for this session.");
355
359
  }
@@ -399,6 +403,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
399
403
  }
400
404
  },
401
405
  {
406
+ ownerId: this.session.getAgentId?.() ?? undefined,
402
407
  onProgress: async (text, details) => {
403
408
  latestText = text;
404
409
  await options.onUpdate?.({
@@ -501,7 +506,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
501
506
 
502
507
  const internalUrlOptions: InternalUrlExpansionOptions = {
503
508
  skills: this.session.skills ?? [],
504
- internalRouter: this.session.internalRouter,
509
+ internalRouter: InternalUrlRouter.instance(),
505
510
  localOptions: {
506
511
  getArtifactsDir: this.session.getArtifactsDir,
507
512
  getSessionId: this.session.getSessionId,
@@ -549,7 +554,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
549
554
  const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
550
555
 
551
556
  if (asyncRequested) {
552
- if (!this.session.asyncJobManager) {
557
+ if (!AsyncJobManager.instance()) {
553
558
  throw new ToolError("Async job manager unavailable for this session.");
554
559
  }
555
560
  const job = this.#startManagedBashJob({
@@ -570,7 +575,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
570
575
  });
571
576
  }
572
577
 
573
- if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
578
+ const autoBgManager = AsyncJobManager.instance();
579
+ if (this.#autoBackgroundEnabled && !pty && autoBgManager) {
574
580
  const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
575
581
  const startBackgrounded = autoBackgroundWaitMs === 0;
576
582
  const job = this.#startManagedBashJob({
@@ -593,16 +599,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
593
599
  }
594
600
  const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
595
601
  if (waitResult.kind === "completed") {
596
- this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
602
+ autoBgManager.acknowledgeDeliveries([job.jobId]);
597
603
  return waitResult.result;
598
604
  }
599
605
  if (waitResult.kind === "failed") {
600
- this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
606
+ autoBgManager.acknowledgeDeliveries([job.jobId]);
601
607
  throw waitResult.error;
602
608
  }
603
609
  if (waitResult.kind === "aborted") {
604
- this.session.asyncJobManager.cancel(job.jobId);
605
- this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
610
+ autoBgManager.cancel(job.jobId);
611
+ autoBgManager.acknowledgeDeliveries([job.jobId]);
606
612
  throw new ToolAbortError(job.getLatestText() || "Command aborted");
607
613
  }
608
614
  job.setBackgrounded(true);
@@ -16,6 +16,17 @@ import type {
16
16
  WorkerInitPayload,
17
17
  WorkerOutbound,
18
18
  } from "./tab-protocol";
19
+ // Imported with `type: "file"` so Bun's bundler statically discovers the
20
+ // worker entry and embeds it inside `bun build --compile` single-file
21
+ // binaries. Without this attribute the bundler cannot reach the entry through
22
+ // a `new URL(..., import.meta.url)` literal stored in a local variable, and
23
+ // the prebuilt binary surfaces `Timed out initializing browser tab worker`
24
+ // (issue #1011) because `/$bunfs/root/tab-worker-entry.ts` is missing.
25
+ // tsgo doesn't recognize Bun's `with { type: "file" }` attribute and treats
26
+ // this as a normal TS source import, raising TS1192/TS5097. Bun's bundler
27
+ // (and runtime) honors the attribute and returns the embedded file URL.
28
+ // @ts-expect-error -- Bun file-URL import (see comment above).
29
+ import tabWorkerEntryUrl from "./tab-worker-entry.ts" with { type: "file" };
19
30
 
20
31
  interface WorkerHandle {
21
32
  send(msg: WorkerInbound, transferList?: Transferable[]): void;
@@ -364,8 +375,7 @@ async function raceWithTimeout<T>(
364
375
 
365
376
  async function spawnTabWorker(): Promise<WorkerHandle> {
366
377
  try {
367
- const url = new URL("./tab-worker-entry.ts", import.meta.url);
368
- const worker = new Worker(url.href, { type: "module" });
378
+ const worker = new Worker(tabWorkerEntryUrl, { type: "module" });
369
379
  return wrapBunWorker(worker);
370
380
  } catch (err) {
371
381
  logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", {
package/src/tools/eval.ts CHANGED
@@ -8,7 +8,7 @@ import { jsBackend, parseEvalInput, pythonBackend, sniffEvalLanguage } from "../
8
8
  import type { ExecutorBackend } from "../eval/backend";
9
9
  import evalGrammar from "../eval/eval.lark" with { type: "text" };
10
10
  import { ABORT_WARNING, type ParsedEvalCell } from "../eval/parse";
11
- import type { EvalCellResult, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
11
+ import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
13
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
14
14
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
@@ -47,6 +47,38 @@ function formatJsonScalar(value: unknown): string {
47
47
  return "[object]";
48
48
  }
49
49
 
50
+ /** Cap per `display()` value sent back to the model. */
51
+ const MAX_DISPLAY_TEXT_BYTES = 8000;
52
+
53
+ function formatDisplayJsonForText(value: unknown): string {
54
+ let text: string;
55
+ try {
56
+ text = JSON.stringify(value, null, 2) ?? String(value);
57
+ } catch {
58
+ text = String(value);
59
+ }
60
+ if (text.length > MAX_DISPLAY_TEXT_BYTES) {
61
+ text = `${text.slice(0, MAX_DISPLAY_TEXT_BYTES)}\n… (${text.length - MAX_DISPLAY_TEXT_BYTES} chars truncated)`;
62
+ }
63
+ return text;
64
+ }
65
+
66
+ /**
67
+ * Format display() JSON values into text the model can see. Images are surfaced
68
+ * separately as ImageContent so the model can actually inspect them; this helper
69
+ * intentionally does not touch images.
70
+ */
71
+ function formatDisplayOutputsForText(outputs: EvalDisplayOutput[]): string {
72
+ const chunks: string[] = [];
73
+ let displayIndex = 0;
74
+ for (const output of outputs) {
75
+ if (output.type !== "json") continue;
76
+ displayIndex++;
77
+ chunks.push(`display[${displayIndex}]:\n${formatDisplayJsonForText(output.data)}`);
78
+ }
79
+ return chunks.join("\n\n");
80
+ }
81
+
50
82
  function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDepth = expanded ? 6 : 2): string[] {
51
83
  const maxItems = expanded ? 20 : 5;
52
84
 
@@ -370,13 +402,16 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
370
402
  const durationMs = Date.now() - startTime;
371
403
 
372
404
  const cellStatusEvents: EvalStatusEvent[] = [];
405
+ const cellDisplayOutputs: EvalDisplayOutput[] = [];
373
406
  let cellHasMarkdown = false;
374
407
  for (const output of result.displayOutputs) {
375
408
  if (output.type === "json") {
376
409
  jsonOutputs.push(output.data);
410
+ cellDisplayOutputs.push(output);
377
411
  }
378
412
  if (output.type === "image") {
379
413
  images.push({ type: "image", data: output.data, mimeType: output.mimeType });
414
+ cellDisplayOutputs.push(output);
380
415
  }
381
416
  if (output.type === "status") {
382
417
  statusEvents.push(output.event);
@@ -387,7 +422,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
387
422
  }
388
423
  }
389
424
 
390
- const cellOutput = result.output.trim();
425
+ const stdoutTrimmed = result.output.trim();
426
+ const displayText = formatDisplayOutputsForText(cellDisplayOutputs);
427
+ const cellOutput =
428
+ stdoutTrimmed && displayText ? `${stdoutTrimmed}\n\n${displayText}` : stdoutTrimmed || displayText;
391
429
  cellResult.output = cellOutput;
392
430
  cellResult.exitCode = result.exitCode;
393
431
  cellResult.durationMs = durationMs;
@@ -431,14 +469,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
431
469
  languages,
432
470
  cells: cellResults,
433
471
  jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
434
- images: images.length > 0 ? images : undefined,
435
472
  statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
436
473
  isError: true,
437
474
  };
438
475
  if (notice) details.notice = notice;
439
476
 
440
477
  return toolResult(details)
441
- .text(outputText)
478
+ .content([{ type: "text", text: outputText }, ...images])
442
479
  .truncationFromSummary(summaryForMeta, { direction: "tail" })
443
480
  .done();
444
481
  }
@@ -461,14 +498,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
461
498
  languages,
462
499
  cells: cellResults,
463
500
  jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
464
- images: images.length > 0 ? images : undefined,
465
501
  statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
466
502
  isError: true,
467
503
  };
468
504
  if (notice) details.notice = notice;
469
505
 
470
506
  return toolResult(details)
471
- .text(outputText)
507
+ .content([{ type: "text", text: outputText }, ...images])
472
508
  .truncationFromSummary(summaryForMeta, { direction: "tail" })
473
509
  .done();
474
510
  }
@@ -479,9 +515,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
479
515
 
480
516
  const combinedOutput = cellOutputs.join("\n\n");
481
517
  const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
518
+ const hasImages = images.length > 0;
482
519
  const outputText =
483
- (combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)")) +
484
- abortSuffix;
520
+ (combinedOutput ||
521
+ (hasImages
522
+ ? `(displayed ${images.length} image${images.length === 1 ? "" : "s"}; no text output)`
523
+ : "(no output)")) + abortSuffix;
485
524
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
486
525
 
487
526
  const details: EvalToolDetails = {
@@ -489,13 +528,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
489
528
  languages,
490
529
  cells: cellResults,
491
530
  jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
492
- images: images.length > 0 ? images : undefined,
493
531
  statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
494
532
  };
495
533
  if (notice) details.notice = notice;
496
534
 
497
535
  return toolResult(details)
498
- .text(outputText)
536
+ .content([{ type: "text", text: outputText }, ...images])
499
537
  .truncationFromSummary(summaryForMeta, { direction: "tail" })
500
538
  .done();
501
539
  } finally {
@@ -1352,7 +1352,7 @@ export function renderReadUrlCall(
1352
1352
  ): Component {
1353
1353
  const url = args.path ?? args.url ?? "";
1354
1354
  const domain = getDomain(url);
1355
- const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "\u2026");
1355
+ const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "");
1356
1356
  const description = `${domain}${path ? ` ${path}` : ""}`.trim();
1357
1357
  const meta: string[] = [];
1358
1358
  if (args.raw) meta.push("raw");