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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +3 -3
  4. package/src/config/prompt-templates.ts +0 -5
  5. package/src/config/settings-schema.ts +38 -0
  6. package/src/eval/eval.lark +10 -31
  7. package/src/eval/index.ts +1 -0
  8. package/src/eval/parse.ts +156 -255
  9. package/src/eval/sniff.ts +28 -0
  10. package/src/export/html/template.css +38 -0
  11. package/src/export/html/template.generated.ts +1 -1
  12. package/src/export/html/template.js +209 -15
  13. package/src/extensibility/extensions/runner.ts +173 -177
  14. package/src/hashline/apply.ts +8 -24
  15. package/src/hashline/constants.ts +20 -0
  16. package/src/hashline/execute.ts +0 -1
  17. package/src/hashline/grammar.lark +16 -27
  18. package/src/hashline/hash.ts +4 -34
  19. package/src/hashline/input.ts +16 -2
  20. package/src/hashline/parser.ts +12 -40
  21. package/src/hashline/types.ts +1 -2
  22. package/src/internal-urls/agent-protocol.ts +1 -0
  23. package/src/internal-urls/artifact-protocol.ts +1 -0
  24. package/src/internal-urls/docs-index.generated.ts +2 -1
  25. package/src/internal-urls/jobs-protocol.ts +1 -0
  26. package/src/internal-urls/local-protocol.ts +1 -0
  27. package/src/internal-urls/mcp-protocol.ts +1 -0
  28. package/src/internal-urls/memory-protocol.ts +1 -0
  29. package/src/internal-urls/pi-protocol.ts +1 -0
  30. package/src/internal-urls/router.ts +2 -1
  31. package/src/internal-urls/rule-protocol.ts +1 -0
  32. package/src/internal-urls/skill-protocol.ts +1 -0
  33. package/src/internal-urls/types.ts +18 -2
  34. package/src/mcp/transports/http.ts +49 -47
  35. package/src/prompts/system/custom-system-prompt.md +0 -2
  36. package/src/prompts/system/now-prompt.md +7 -0
  37. package/src/prompts/system/project-prompt.md +2 -0
  38. package/src/prompts/system/subagent-system-prompt.md +18 -9
  39. package/src/prompts/system/subagent-user-prompt.md +1 -10
  40. package/src/prompts/system/system-prompt.md +154 -233
  41. package/src/prompts/tools/bash.md +0 -24
  42. package/src/prompts/tools/eval.md +26 -13
  43. package/src/prompts/tools/hashline.md +1 -4
  44. package/src/sdk.ts +12 -22
  45. package/src/session/agent-session.ts +49 -17
  46. package/src/system-prompt.ts +38 -104
  47. package/src/task/executor.ts +15 -9
  48. package/src/task/index.ts +38 -33
  49. package/src/task/render.ts +4 -2
  50. package/src/tools/bash.ts +15 -41
  51. package/src/tools/eval.ts +13 -36
  52. package/src/tools/index.ts +0 -3
  53. package/src/tools/path-utils.ts +21 -1
  54. package/src/tools/read.ts +71 -49
  55. package/src/tools/search.ts +13 -1
  56. package/src/utils/file-display-mode.ts +11 -5
  57. package/src/workspace-tree.ts +210 -410
  58. package/src/task/template.ts +0 -47
  59. package/src/tools/bash-normalize.ts +0 -107
package/src/task/index.ts CHANGED
@@ -23,6 +23,7 @@ import type { ToolSession } from "..";
23
23
  import { resolveAgentModelPatterns } from "../config/model-resolver";
24
24
  import type { Theme } from "../modes/theme/theme";
25
25
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
26
+ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
26
27
  import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
27
28
  import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
28
29
  import { formatBytes, formatDuration } from "../tools/render-utils";
@@ -38,7 +39,6 @@ import { AgentOutputManager } from "./output-manager";
38
39
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
39
40
  import { renderResult, renderCall as renderTaskCall } from "./render";
40
41
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
41
- import { renderTemplate } from "./template";
42
42
  import {
43
43
  type AgentDefinition,
44
44
  type AgentProgress,
@@ -65,6 +65,12 @@ import {
65
65
  type WorktreeBaseline,
66
66
  } from "./worktree";
67
67
 
68
+ function renderSubagentUserPrompt(assignment: string, simpleMode: TaskSimpleMode): string {
69
+ return prompt.render(subagentUserPromptTemplate, {
70
+ assignment: assignment.trim(),
71
+ independentMode: simpleMode === "independent",
72
+ });
73
+ }
68
74
  function createUsageTotals(): Usage {
69
75
  return {
70
76
  input: 0,
@@ -282,21 +288,19 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
282
288
  const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
283
289
  const fallbackAgentSource =
284
290
  this.#discoveredAgents.find(agent => agent.name === params.agent)?.source ?? "bundled";
285
- const { contextEnabled } = getTaskSimpleModeCapabilities(simpleMode);
286
- const sharedContext = contextEnabled ? params.context : undefined;
287
- const renderedTasks = taskItems.map(taskItem => renderTemplate(sharedContext, taskItem, simpleMode));
288
291
  const progressByTaskId = new Map<string, AgentProgress>();
289
- for (let index = 0; index < renderedTasks.length; index++) {
290
- const renderedTask = renderedTasks[index];
291
- progressByTaskId.set(renderedTask.id, {
292
+ for (let index = 0; index < taskItems.length; index++) {
293
+ const taskItem = taskItems[index];
294
+ const assignment = taskItem.assignment.trim();
295
+ progressByTaskId.set(taskItem.id, {
292
296
  index,
293
- id: renderedTask.id,
297
+ id: taskItem.id,
294
298
  agent: params.agent,
295
299
  agentSource: fallbackAgentSource,
296
300
  status: "pending",
297
- task: renderedTask.task,
298
- assignment: renderedTask.assignment,
299
- description: renderedTask.description,
301
+ task: renderSubagentUserPrompt(assignment, simpleMode),
302
+ assignment,
303
+ description: taskItem.description,
300
304
  recentTools: [],
301
305
  recentOutput: [],
302
306
  toolCount: 0,
@@ -506,7 +510,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
506
510
  const { agent: agentName, context, schema: outputSchema } = params;
507
511
  const simpleMode = this.#getTaskSimpleMode();
508
512
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
509
- const sharedContext = contextEnabled ? context : undefined;
513
+ const sharedContext = contextEnabled ? context?.trim() : undefined;
510
514
  const isolationMode = this.session.settings.get("task.isolation.mode");
511
515
  const isolationRequested = "isolated" in params ? params.isolated === true : false;
512
516
  const isIsolated = isolationMode !== "none" && isolationRequested;
@@ -802,8 +806,6 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
802
806
  }
803
807
  const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
804
808
 
805
- // Build full prompts using shared context only when the current task mode allows it.
806
- const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(sharedContext, t, simpleMode));
807
809
  const availableSkills = [...(this.session.skills ?? [])];
808
810
  const contextFiles = this.session.contextFiles?.filter(
809
811
  file => path.basename(file.path).toLowerCase() !== "agents.md",
@@ -811,34 +813,36 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
811
813
  const promptTemplates = this.session.promptTemplates;
812
814
 
813
815
  // Initialize progress for all tasks
814
- for (let i = 0; i < tasksWithContext.length; i++) {
815
- const t = tasksWithContext[i];
816
+ for (let i = 0; i < tasksWithUniqueIds.length; i++) {
817
+ const taskItem = tasksWithUniqueIds[i];
818
+ const assignment = taskItem.assignment.trim();
816
819
  progressMap.set(i, {
817
820
  index: i,
818
- id: t.id,
821
+ id: taskItem.id,
819
822
  agent: agentName,
820
823
  agentSource: agent.source,
821
824
  status: "pending",
822
- task: t.task,
823
- assignment: t.assignment,
825
+ task: renderSubagentUserPrompt(assignment, simpleMode),
826
+ assignment,
824
827
  recentTools: [],
825
828
  recentOutput: [],
826
829
  toolCount: 0,
827
830
  tokens: 0,
828
831
  durationMs: 0,
829
832
  modelOverride,
830
- description: t.description,
833
+ description: taskItem.description,
831
834
  });
832
835
  }
833
836
  emitProgress();
834
837
 
835
- const runTask = async (task: (typeof tasksWithContext)[number], index: number) => {
838
+ const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
836
839
  if (!isIsolated) {
837
840
  return runSubprocess({
838
841
  cwd: this.session.cwd,
839
842
  agent,
840
- task: task.task,
841
- assignment: task.assignment,
843
+ task: renderSubagentUserPrompt(task.assignment, simpleMode),
844
+ assignment: task.assignment.trim(),
845
+ context: sharedContext,
842
846
  description: task.description,
843
847
  index,
844
848
  id: task.id,
@@ -866,7 +870,6 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
866
870
  mcpManager: this.session.mcpManager,
867
871
  contextFiles,
868
872
  skills: availableSkills,
869
- agentsMdSearch: this.session.agentsMdSearch,
870
873
  workspaceTree: this.session.workspaceTree,
871
874
  promptTemplates,
872
875
  localProtocolOptions,
@@ -895,8 +898,9 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
895
898
  cwd: this.session.cwd,
896
899
  worktree: isolationDir,
897
900
  agent,
898
- task: task.task,
899
- assignment: task.assignment,
901
+ task: renderSubagentUserPrompt(task.assignment, simpleMode),
902
+ assignment: task.assignment.trim(),
903
+ context: sharedContext,
900
904
  description: task.description,
901
905
  index,
902
906
  id: task.id,
@@ -924,7 +928,6 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
924
928
  mcpManager: this.session.mcpManager,
925
929
  contextFiles,
926
930
  skills: availableSkills,
927
- agentsMdSearch: this.session.agentsMdSearch,
928
931
  workspaceTree: this.session.workspaceTree,
929
932
  promptTemplates,
930
933
  localProtocolOptions,
@@ -981,13 +984,14 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
981
984
  return result;
982
985
  } catch (err) {
983
986
  const message = err instanceof Error ? err.message : String(err);
987
+ const assignment = task.assignment.trim();
984
988
  return {
985
989
  index,
986
990
  id: task.id,
987
991
  agent: agent.name,
988
992
  agentSource: agent.source,
989
- task: task.task,
990
- assignment: task.assignment,
993
+ task: renderSubagentUserPrompt(assignment, simpleMode),
994
+ assignment,
991
995
  description: task.description,
992
996
  exitCode: 1,
993
997
  output: "",
@@ -1013,7 +1017,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1013
1017
 
1014
1018
  // Execute in parallel with concurrency limit
1015
1019
  const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
1016
- tasksWithContext,
1020
+ tasksWithUniqueIds,
1017
1021
  maxConcurrency,
1018
1022
  runTask,
1019
1023
  signal,
@@ -1024,14 +1028,15 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1024
1028
  if (result !== undefined) {
1025
1029
  return result;
1026
1030
  }
1027
- const task = tasksWithContext[index];
1031
+ const task = tasksWithUniqueIds[index];
1032
+ const assignment = task.assignment.trim();
1028
1033
  return {
1029
1034
  index,
1030
1035
  id: task.id,
1031
1036
  agent: agentName,
1032
1037
  agentSource: agent.source,
1033
- task: task.task,
1034
- assignment: task.assignment,
1038
+ task: renderSubagentUserPrompt(assignment, simpleMode),
1039
+ assignment,
1035
1040
  description: task.description,
1036
1041
  exitCode: 1,
1037
1042
  output: "",
@@ -470,14 +470,16 @@ export function renderCall(args: TaskParams, _options: RenderResultOptions, them
470
470
  lines.push(` ${vertical} ${content}`);
471
471
  }
472
472
  const taskPrefix = showIsolated ? branch : last;
473
- lines.push(` ${taskPrefix} ${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks.length} agents`)}`);
473
+ lines.push(
474
+ ` ${taskPrefix} ${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks?.length ?? 0} agents`)}`,
475
+ );
474
476
  if (showIsolated) {
475
477
  lines.push(` ${last} ${theme.fg("dim", "Isolated")}: ${theme.fg("muted", "true")}`);
476
478
  }
477
479
  return new Text(lines.join("\n"), 0, 0);
478
480
  }
479
481
 
480
- lines.push(`${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks.length} agents`)}`);
482
+ lines.push(`${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks?.length ?? 0} agents`)}`);
481
483
  if (showIsolated) {
482
484
  lines.push(`${theme.fg("dim", "Isolated")}: ${theme.fg("muted", "true")}`);
483
485
  }
package/src/tools/bash.ts CHANGED
@@ -16,7 +16,6 @@ import { getSixelLineMask } from "../utils/sixel";
16
16
  import type { ToolSession } from ".";
17
17
  import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
18
18
  import { checkBashInterception } from "./bash-interceptor";
19
- import { applyHeadTail } from "./bash-normalize";
20
19
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
21
20
  import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
22
21
  import { resolveToCwd } from "./path-utils";
@@ -50,8 +49,7 @@ const bashSchemaBase = Type.Object({
50
49
  ),
51
50
  timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 300 })),
52
51
  cwd: Type.Optional(Type.String({ description: "working directory", examples: ["src/", "/tmp"] })),
53
- head: Type.Optional(Type.Number({ description: "first n lines of output" })),
54
- tail: Type.Optional(Type.Number({ description: "last n lines of output" })),
52
+
55
53
  pty: Type.Optional(
56
54
  Type.Boolean({
57
55
  description: "run in pty mode",
@@ -75,8 +73,7 @@ export interface BashToolInput {
75
73
  env?: Record<string, string>;
76
74
  timeout?: number;
77
75
  cwd?: string;
78
- head?: number;
79
- tail?: number;
76
+
80
77
  async?: boolean;
81
78
  pty?: boolean;
82
79
  }
@@ -266,16 +263,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
266
263
  });
267
264
  }
268
265
 
269
- #formatResultOutput(result: BashResult | BashInteractiveResult, headLines?: number, tailLines?: number): string {
270
- let outputText = normalizeResultOutput(result);
271
- const headTailResult = applyHeadTail(outputText, headLines, tailLines);
272
- if (headTailResult.applied) {
273
- outputText = headTailResult.text;
274
- }
275
- if (!outputText) {
276
- outputText = "(no output)";
277
- }
278
- return outputText;
266
+ #formatResultOutput(result: BashResult | BashInteractiveResult): string {
267
+ const outputText = normalizeResultOutput(result);
268
+ return outputText || "(no output)";
279
269
  }
280
270
 
281
271
  #buildResultText(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): string {
@@ -297,11 +287,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
297
287
  #buildCompletedResult(
298
288
  result: BashResult | BashInteractiveResult,
299
289
  timeoutSec: number,
300
- headLines?: number,
301
- tailLines?: number,
302
290
  options: { requestedTimeoutSec?: number; notices?: string[] } = {},
303
291
  ): AgentToolResult<BashToolDetails> {
304
- const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
292
+ const outputLines = [this.#formatResultOutput(result)];
305
293
  const notices = options.notices?.filter(Boolean) ?? [];
306
294
  if (notices.length > 0) outputLines.push("", ...notices);
307
295
  const outputText = outputLines.join("\n");
@@ -356,8 +344,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
356
344
  timeoutSec: number;
357
345
  requestedTimeoutSec?: number;
358
346
  timeoutClampNotice?: string;
359
- headLines?: number;
360
- tailLines?: number;
347
+
361
348
  resolvedEnv?: Record<string, string>;
362
349
  onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
363
350
  startBackgrounded: boolean;
@@ -394,16 +381,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
394
381
  },
395
382
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
396
383
  });
397
- const finalResult = this.#buildCompletedResult(
398
- result,
399
- options.timeoutSec,
400
- options.headLines,
401
- options.tailLines,
402
- {
403
- requestedTimeoutSec: options.requestedTimeoutSec,
404
- notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
405
- },
406
- );
384
+ const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
385
+ requestedTimeoutSec: options.requestedTimeoutSec,
386
+ notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
387
+ });
407
388
  const finalText = this.#extractTextResult(finalResult);
408
389
  latestText = finalText;
409
390
  completion.resolve({ kind: "completed", result: finalResult });
@@ -481,8 +462,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
481
462
  env: rawEnv,
482
463
  timeout: rawTimeout = 300,
483
464
  cwd,
484
- head,
485
- tail,
465
+
486
466
  async: asyncRequested = false,
487
467
  pty = false,
488
468
  }: BashToolInput,
@@ -505,10 +485,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
505
485
  throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
506
486
  }
507
487
 
508
- // Only apply explicit head/tail params from tool input.
509
- const headLines = head;
510
- const tailLines = tail;
511
-
512
488
  // Check both the original command and the cwd-normalized command so
513
489
  // leading `cd ... &&` wrappers do not hide either shell-navigation rules
514
490
  // or the dedicated-tool command that follows the directory change.
@@ -583,8 +559,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
583
559
  timeoutSec,
584
560
  requestedTimeoutSec,
585
561
  timeoutClampNotice,
586
- headLines,
587
- tailLines,
562
+
588
563
  resolvedEnv,
589
564
  onUpdate,
590
565
  startBackgrounded: true,
@@ -605,8 +580,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
605
580
  timeoutSec,
606
581
  requestedTimeoutSec,
607
582
  timeoutClampNotice,
608
- headLines,
609
- tailLines,
583
+
610
584
  resolvedEnv,
611
585
  onUpdate,
612
586
  startBackgrounded,
@@ -675,7 +649,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
675
649
  if (isInteractiveResult(result) && result.timedOut) {
676
650
  throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
677
651
  }
678
- return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
652
+ return this.#buildCompletedResult(result, timeoutSec, {
679
653
  requestedTimeoutSec,
680
654
  notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
681
655
  });
package/src/tools/eval.ts CHANGED
@@ -4,10 +4,10 @@ import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Markdown, Text } from "@oh-my-pi/pi-tui";
5
5
  import { prompt } from "@oh-my-pi/pi-utils";
6
6
  import { type Static, Type } from "@sinclair/typebox";
7
- import { jsBackend, parseEvalInput, pythonBackend } from "../eval";
7
+ import { jsBackend, parseEvalInput, pythonBackend, sniffEvalLanguage } from "../eval";
8
8
  import type { ExecutorBackend } from "../eval/backend";
9
9
  import evalGrammar from "../eval/eval.lark" with { type: "text" };
10
- import type { ParsedEvalCell } from "../eval/parse";
10
+ import { ABORT_WARNING, type ParsedEvalCell } from "../eval/parse";
11
11
  import type { EvalCellResult, 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";
@@ -26,7 +26,7 @@ export const EVAL_DEFAULT_PREVIEW_LINES = 10;
26
26
 
27
27
  export const evalSchema = Type.Object({
28
28
  input: Type.String({
29
- description: "eval input as a sequence of `===== <info> =====` cell headers followed by code",
29
+ description: "eval input as a sequence of `*** Begin <LANG>` cell headers followed by code",
30
30
  }),
31
31
  });
32
32
  export type EvalToolParams = Static<typeof evalSchema>;
@@ -131,33 +131,6 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
131
131
  return clampTimeout("eval", timeoutMs / 1000);
132
132
  }
133
133
 
134
- /**
135
- * Best-effort language sniff for cells with no explicit `language`.
136
- *
137
- * Order:
138
- * 1. Shebang on first line (`#!/usr/bin/env python`, `#!/usr/bin/env node`, etc.)
139
- * 2. Strong syntactic markers unique to one language. We bias false negatives over
140
- * false positives — anything ambiguous returns `undefined` and the caller falls
141
- * back to the default-backend rules.
142
- */
143
- function sniffLanguage(code: string): EvalLanguage | undefined {
144
- const stripped = code.replace(/^\s+/, "");
145
- if (stripped.startsWith("#!")) {
146
- const firstLine = stripped.split("\n", 1)[0]!.toLowerCase();
147
- if (/(\bpython\d?\b|\bipython\b)/.test(firstLine)) return "python";
148
- if (/(\bnode\b|\bbun\b|\bdeno\b|\bjavascript\b|\bjs\b)/.test(firstLine)) return "js";
149
- }
150
- const jsMarkers =
151
- /(^|\n)\s*(const|let|var|async\s+function|function\s*\*?\s*[\w$]*\s*\(|import\s+[^\n]+\sfrom\s|export\s+(default|const|let|function|class|async)|require\s*\(|console\.\w+\s*\(|=>|;\s*$)/m;
152
- const pyMarkers =
153
- /(^|\n)\s*(def\s+\w+\s*\(|from\s+[\w.]+\s+import|import\s+\w+(\s+as\s+\w+)?\s*$|class\s+\w+\s*[(:]|print\s*\(|elif\s+[^\n]*:|with\s+[^\n]+:\s*$|@[\w.]+\s*$)/m;
154
- const hasJs = jsMarkers.test(code);
155
- const hasPy = pyMarkers.test(code);
156
- if (hasJs && !hasPy) return "js";
157
- if (hasPy && !hasJs) return "python";
158
- return undefined;
159
- }
160
-
161
134
  async function resolveBackend(
162
135
  session: ToolSession,
163
136
  requested: EvalLanguage | undefined,
@@ -180,7 +153,7 @@ async function resolveBackend(
180
153
  return { backend: jsBackend, fallback: false };
181
154
  }
182
155
  // Auto-detect.
183
- const sniffed = sniffLanguage(code);
156
+ const sniffed = sniffEvalLanguage(code);
184
157
  if (sniffed === "python" && allowPy && (await pythonBackend.isAvailable(session))) {
185
158
  return { backend: pythonBackend, fallback: false };
186
159
  }
@@ -446,10 +419,11 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
446
419
  pushUpdate();
447
420
  const errorMsg = result.output || "Command aborted";
448
421
  const combinedOutput = cellOutputs.join("\n\n");
422
+ const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
449
423
  const outputText =
450
- cells.length > 1
424
+ (cells.length > 1
451
425
  ? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
452
- : combinedOutput || errorMsg;
426
+ : combinedOutput || errorMsg) + abortSuffix;
453
427
 
454
428
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
455
429
  const details: EvalToolDetails = {
@@ -473,12 +447,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
473
447
  cellResult.status = "error";
474
448
  pushUpdate();
475
449
  const combinedOutput = cellOutputs.join("\n\n");
450
+ const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
476
451
  const outputText =
477
- cells.length > 1
452
+ (cells.length > 1
478
453
  ? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
479
454
  : combinedOutput
480
455
  ? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
481
- : `Command exited with code ${result.exitCode}`;
456
+ : `Command exited with code ${result.exitCode}`) + abortSuffix;
482
457
 
483
458
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
484
459
  const details: EvalToolDetails = {
@@ -503,8 +478,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
503
478
  }
504
479
 
505
480
  const combinedOutput = cellOutputs.join("\n\n");
481
+ const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
506
482
  const outputText =
507
- combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
483
+ (combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)")) +
484
+ abortSuffix;
508
485
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
509
486
 
510
487
  const details: EvalToolDetails = {
@@ -14,7 +14,6 @@ import type { PlanModeState } from "../plan-mode/state";
14
14
  import type { AgentRegistry } from "../registry/agent-registry";
15
15
  import type { CustomMessage } from "../session/messages";
16
16
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
17
- import type { AgentsMdSearch } from "../system-prompt";
18
17
  import { TaskTool } from "../task";
19
18
  import type { AgentOutputManager } from "../task/output-manager";
20
19
  import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
@@ -122,8 +121,6 @@ export interface ToolSession {
122
121
  skipPythonPreflight?: boolean;
123
122
  /** Pre-loaded context files (AGENTS.md, etc) */
124
123
  contextFiles?: ContextFileEntry[];
125
- /** Pre-loaded AGENTS.md search (forwarded to subagents to skip re-scanning) */
126
- agentsMdSearch?: AgentsMdSearch;
127
124
  /** Pre-loaded workspace tree (forwarded to subagents to skip re-scanning) */
128
125
  workspaceTree?: WorkspaceTree;
129
126
  /** Pre-loaded skills */
@@ -6,6 +6,8 @@ import { isEnoent } from "@oh-my-pi/pi-utils";
6
6
 
7
7
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
8
8
  const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+)?|raw)$/i;
9
+ const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+)?$/i;
10
+ const FILE_RAW_ONLY_RE = /^raw$/i;
9
11
  const NARROW_NO_BREAK_SPACE = "\u202F";
10
12
  const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
11
13
  "agent://",
@@ -110,7 +112,25 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
110
112
  const candidate = rawPath.slice(colon + 1);
111
113
  if (!FILE_LINE_RANGE_RE.test(candidate)) return { path: rawPath };
112
114
 
113
- return { path: rawPath.slice(0, colon), sel: candidate };
115
+ let basePath = rawPath.slice(0, colon);
116
+ let sel = candidate;
117
+
118
+ // Allow a compound trailing selector: `path:1-50:raw` or `path:raw:1-50`.
119
+ // The two chunks must be one line-range plus one `raw`, in either order.
120
+ const innerColon = basePath.lastIndexOf(":");
121
+ if (innerColon > 0) {
122
+ const innerCandidate = basePath.slice(innerColon + 1);
123
+ const innerIsRaw = FILE_RAW_ONLY_RE.test(innerCandidate);
124
+ const outerIsRaw = FILE_RAW_ONLY_RE.test(candidate);
125
+ const innerIsRange = FILE_LINE_RANGE_ONLY_RE.test(innerCandidate);
126
+ const outerIsRange = FILE_LINE_RANGE_ONLY_RE.test(candidate);
127
+ if ((innerIsRaw && outerIsRange) || (innerIsRange && outerIsRaw)) {
128
+ sel = `${innerCandidate}:${candidate}`;
129
+ basePath = basePath.slice(0, innerColon);
130
+ }
131
+ }
132
+
133
+ return { path: basePath, sel };
114
134
  }
115
135
 
116
136
  function assertNotInternalUrl(expanded: string, original: string): void {