@oh-my-pi/pi-coding-agent 14.9.8 → 15.0.0

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 (138) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/package.json +7 -7
  3. package/scripts/build-binary.ts +11 -0
  4. package/scripts/format-prompts.ts +1 -1
  5. package/src/cli/args.ts +2 -2
  6. package/src/cli/stats-cli.ts +2 -0
  7. package/src/cli.ts +24 -1
  8. package/src/commands/acp.ts +24 -0
  9. package/src/commands/launch.ts +6 -4
  10. package/src/commit/agentic/prompts/system.md +1 -1
  11. package/src/config/model-resolver.ts +30 -0
  12. package/src/config/settings-schema.ts +61 -9
  13. package/src/config/settings.ts +18 -1
  14. package/src/edit/index.ts +22 -1
  15. package/src/edit/modes/patch.ts +10 -0
  16. package/src/edit/modes/replace.ts +3 -0
  17. package/src/edit/renderer.ts +10 -0
  18. package/src/edit/streaming.ts +1 -1
  19. package/src/eval/js/context-manager.ts +10 -9
  20. package/src/eval/js/shared/rewrite-imports.ts +120 -48
  21. package/src/eval/js/shared/runtime.ts +31 -4
  22. package/src/eval/js/tool-bridge.ts +43 -21
  23. package/src/extensibility/extensions/runner.ts +54 -1
  24. package/src/extensibility/extensions/types.ts +11 -0
  25. package/src/extensibility/skills.ts +33 -1
  26. package/src/hashline/grammar.lark +1 -1
  27. package/src/hashline/input.ts +11 -5
  28. package/src/internal-urls/docs-index.generated.ts +7 -7
  29. package/src/internal-urls/index.ts +1 -0
  30. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  31. package/src/internal-urls/router.ts +6 -3
  32. package/src/internal-urls/types.ts +22 -1
  33. package/src/main.ts +13 -9
  34. package/src/modes/acp/acp-agent.ts +361 -54
  35. package/src/modes/acp/acp-client-bridge.ts +152 -0
  36. package/src/modes/acp/acp-event-mapper.ts +180 -15
  37. package/src/modes/acp/terminal-auth.ts +37 -0
  38. package/src/modes/components/read-tool-group.ts +29 -1
  39. package/src/modes/controllers/command-controller.ts +14 -6
  40. package/src/modes/controllers/event-controller.ts +24 -11
  41. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  42. package/src/modes/controllers/input-controller.ts +72 -39
  43. package/src/modes/interactive-mode.ts +71 -7
  44. package/src/modes/rpc/rpc-mode.ts +17 -2
  45. package/src/modes/types.ts +6 -2
  46. package/src/modes/utils/ui-helpers.ts +15 -3
  47. package/src/prompts/agents/designer.md +5 -5
  48. package/src/prompts/agents/explore.md +7 -7
  49. package/src/prompts/agents/init.md +9 -9
  50. package/src/prompts/agents/librarian.md +14 -14
  51. package/src/prompts/agents/plan.md +4 -4
  52. package/src/prompts/agents/reviewer.md +5 -5
  53. package/src/prompts/agents/task.md +10 -10
  54. package/src/prompts/commands/orchestrate.md +2 -2
  55. package/src/prompts/compaction/branch-summary.md +3 -3
  56. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  57. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  58. package/src/prompts/compaction/compaction-summary.md +5 -5
  59. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  60. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  61. package/src/prompts/memories/consolidation.md +2 -2
  62. package/src/prompts/memories/read-path.md +1 -1
  63. package/src/prompts/memories/stage_one_input.md +1 -1
  64. package/src/prompts/memories/stage_one_system.md +5 -5
  65. package/src/prompts/review-request.md +4 -4
  66. package/src/prompts/system/agent-creation-architect.md +17 -17
  67. package/src/prompts/system/agent-creation-user.md +2 -2
  68. package/src/prompts/system/commit-message-system.md +2 -2
  69. package/src/prompts/system/custom-system-prompt.md +2 -2
  70. package/src/prompts/system/eager-todo.md +6 -6
  71. package/src/prompts/system/handoff-document.md +1 -1
  72. package/src/prompts/system/plan-mode-active.md +22 -21
  73. package/src/prompts/system/plan-mode-approved.md +4 -4
  74. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  75. package/src/prompts/system/plan-mode-reference.md +2 -2
  76. package/src/prompts/system/plan-mode-subagent.md +8 -8
  77. package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
  78. package/src/prompts/system/project-prompt.md +4 -4
  79. package/src/prompts/system/subagent-system-prompt.md +7 -7
  80. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  81. package/src/prompts/system/system-prompt.md +72 -71
  82. package/src/prompts/system/ttsr-interrupt.md +1 -1
  83. package/src/prompts/tools/apply-patch.md +1 -1
  84. package/src/prompts/tools/ast-edit.md +3 -3
  85. package/src/prompts/tools/ast-grep.md +3 -3
  86. package/src/prompts/tools/browser.md +3 -3
  87. package/src/prompts/tools/checkpoint.md +3 -3
  88. package/src/prompts/tools/exit-plan-mode.md +2 -2
  89. package/src/prompts/tools/find.md +3 -3
  90. package/src/prompts/tools/github.md +2 -5
  91. package/src/prompts/tools/hashline.md +20 -20
  92. package/src/prompts/tools/image-gen.md +3 -3
  93. package/src/prompts/tools/irc.md +1 -1
  94. package/src/prompts/tools/lsp.md +2 -2
  95. package/src/prompts/tools/patch.md +6 -6
  96. package/src/prompts/tools/read.md +7 -7
  97. package/src/prompts/tools/replace.md +5 -5
  98. package/src/prompts/tools/retain.md +1 -1
  99. package/src/prompts/tools/rewind.md +2 -2
  100. package/src/prompts/tools/search.md +2 -2
  101. package/src/prompts/tools/ssh.md +2 -2
  102. package/src/prompts/tools/task.md +12 -6
  103. package/src/prompts/tools/web-search.md +2 -2
  104. package/src/prompts/tools/write.md +3 -3
  105. package/src/sdk.ts +69 -12
  106. package/src/session/agent-session.ts +231 -22
  107. package/src/session/client-bridge.ts +81 -0
  108. package/src/session/compaction/errors.ts +31 -0
  109. package/src/session/compaction/index.ts +1 -0
  110. package/src/slash-commands/acp-builtins.ts +46 -0
  111. package/src/slash-commands/builtin-registry.ts +699 -116
  112. package/src/slash-commands/helpers/context-report.ts +39 -0
  113. package/src/slash-commands/helpers/format.ts +23 -0
  114. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  115. package/src/slash-commands/helpers/mcp.ts +532 -0
  116. package/src/slash-commands/helpers/parse.ts +85 -0
  117. package/src/slash-commands/helpers/ssh.ts +193 -0
  118. package/src/slash-commands/helpers/todo.ts +279 -0
  119. package/src/slash-commands/helpers/usage-report.ts +91 -0
  120. package/src/slash-commands/types.ts +126 -0
  121. package/src/task/executor.ts +10 -3
  122. package/src/task/index.ts +29 -51
  123. package/src/task/render.ts +6 -3
  124. package/src/task/worktree.ts +170 -239
  125. package/src/tools/bash.ts +176 -2
  126. package/src/tools/browser/tab-supervisor.ts +13 -13
  127. package/src/tools/conflict-detect.ts +6 -6
  128. package/src/tools/fetch.ts +15 -4
  129. package/src/tools/find.ts +19 -1
  130. package/src/tools/gh-renderer.ts +0 -12
  131. package/src/tools/gh.ts +682 -176
  132. package/src/tools/github-cache.ts +548 -0
  133. package/src/tools/index.ts +3 -0
  134. package/src/tools/read.ts +110 -27
  135. package/src/tools/write.ts +23 -1
  136. package/src/tui/code-cell.ts +70 -2
  137. package/src/utils/git.ts +5 -0
  138. package/src/task/isolation-backend.ts +0 -94
@@ -0,0 +1,126 @@
1
+ import type { Settings } from "../config/settings";
2
+ import type { InteractiveModeContext } from "../modes/types";
3
+ import type { AgentSession } from "../session/agent-session";
4
+ import type { SessionManager } from "../session/session-manager";
5
+
6
+ /** Declarative subcommand definition for commands like /mcp. */
7
+ export interface SubcommandDef {
8
+ name: string;
9
+ description: string;
10
+ /** Usage hint shown as dim ghost text, e.g. "<name> [--scope project|user]". */
11
+ usage?: string;
12
+ }
13
+
14
+ /** Declarative builtin slash command metadata used by autocomplete and help UI. */
15
+ export interface BuiltinSlashCommand {
16
+ name: string;
17
+ description: string;
18
+ /** Subcommands for dropdown completion (e.g. /mcp add, /mcp list). */
19
+ subcommands?: SubcommandDef[];
20
+ /** Static inline hint when command takes a simple argument (no subcommands). */
21
+ inlineHint?: string;
22
+ }
23
+
24
+ /** Parsed slash-command text after stripping the leading "/". */
25
+ export interface ParsedSlashCommand {
26
+ name: string;
27
+ args: string;
28
+ text: string;
29
+ }
30
+
31
+ /**
32
+ * Result returned by a slash-command handler.
33
+ *
34
+ * - `void` / `undefined` — command was handled and consumed; no further input.
35
+ * - `{ consumed: true }` — explicit equivalent of the above (ACP shape).
36
+ * - `{ prompt: string }` — command handled, pass `prompt` through as the new
37
+ * user input (e.g. `/force <tool> <prompt>` keeps `<prompt>` as the message).
38
+ */
39
+ export type SlashCommandResult = undefined | { consumed: true } | { prompt: string };
40
+
41
+ /**
42
+ * Runtime visible to slash-command handlers that run in text/ACP mode.
43
+ *
44
+ * Both the TUI dispatcher (when invoking a `handle` via its adapter) and the
45
+ * ACP dispatcher pass this shape. Implementations MUST NOT depend on TUI-only
46
+ * state (editor, selectors, status line).
47
+ */
48
+ export interface SlashCommandRuntime {
49
+ session: AgentSession;
50
+ sessionManager: SessionManager;
51
+ settings: Settings;
52
+ cwd: string;
53
+ /** Emit text to the operator. TUI maps to `ctx.showStatus`, ACP to `sessionUpdate`. */
54
+ output: (text: string) => Promise<void> | void;
55
+ /** Re-advertise the available command list (no-op outside ACP). */
56
+ refreshCommands: () => Promise<void> | void;
57
+ /**
58
+ * Reload plugin state (caches, slash command registry, project registries)
59
+ * and re-emit available commands. Used by `/reload-plugins`, `/move`, and
60
+ * `/marketplace`/`/plugins` mutations so the session sees a consistent view
61
+ * after plugin or project-scope changes.
62
+ */
63
+ reloadPlugins: () => Promise<void>;
64
+ notifyTitleChanged?: () => Promise<void> | void;
65
+ notifyConfigChanged?: () => Promise<void> | void;
66
+ }
67
+
68
+ /**
69
+ * Runtime visible to TUI-only handlers (`handleTui`). Carries the interactive
70
+ * mode context plus the background-detach hook. Intentionally narrower than
71
+ * `SlashCommandRuntime` so existing callers can keep building it from just
72
+ * `{ ctx, handleBackgroundCommand }`; when the TUI dispatcher needs to invoke
73
+ * a `handle` (no `handleTui` override), it synthesizes a `SlashCommandRuntime`
74
+ * from `ctx`.
75
+ */
76
+ export interface TuiSlashCommandRuntime {
77
+ ctx: InteractiveModeContext;
78
+ handleBackgroundCommand: () => void;
79
+ }
80
+
81
+ /** Unified slash-command spec consumed by both TUI and ACP dispatchers. */
82
+ export interface SlashCommandSpec extends BuiltinSlashCommand {
83
+ aliases?: string[];
84
+ /** When false, the dispatcher refuses to handle invocations that include arguments. */
85
+ allowArgs?: boolean;
86
+ /**
87
+ * ACP-specific override for `description`. Used by `ACP_BUILTIN_SLASH_COMMANDS`
88
+ * when building `available_commands_update` payloads so the client receives
89
+ * mode-appropriate copy (e.g. `/dump` advertises "Return full transcript as
90
+ * plain text" in ACP rather than the TUI's clipboard-centric copy).
91
+ */
92
+ acpDescription?: string;
93
+ /**
94
+ * ACP-specific override for the advertised input hint. `subcommands`-only
95
+ * specs that historically advertised `<subcommand>` / `[on|off|status]` /
96
+ * `info|delete` to ACP clients carry the hint here so the unification does
97
+ * not silently drop it from `available_commands_update`.
98
+ */
99
+ acpInputHint?: string;
100
+ /**
101
+ * Text/ACP-mode handler. The same body is invoked from the ACP dispatcher
102
+ * and, via the TUI adapter, when no `handleTui` override is provided.
103
+ */
104
+ handle?: (
105
+ command: ParsedSlashCommand,
106
+ runtime: SlashCommandRuntime,
107
+ ) => Promise<SlashCommandResult> | SlashCommandResult;
108
+ /**
109
+ * TUI-only handler that supersedes `handle` when both are present. Use for
110
+ * selectors, wizards, dashboards, and anything else that requires
111
+ * `InteractiveModeContext`.
112
+ */
113
+ handleTui?: (
114
+ command: ParsedSlashCommand,
115
+ runtime: TuiSlashCommandRuntime,
116
+ ) => Promise<SlashCommandResult> | SlashCommandResult;
117
+ }
118
+
119
+ /**
120
+ * @deprecated Use `SlashCommandRuntime` directly. Retained as an alias so
121
+ * downstream code that imported the ACP-specific name keeps compiling.
122
+ */
123
+ export type AcpBuiltinCommandRuntime = SlashCommandRuntime;
124
+
125
+ /** Result returned by `executeAcpBuiltinSlashCommand`. */
126
+ export type AcpBuiltinSlashCommandResult = false | { consumed: true } | { prompt: string };
@@ -947,10 +947,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
947
947
 
948
948
  try {
949
949
  checkAbort();
950
- const authStorage = options.authStorage ?? (await discoverAuthStorage());
951
- checkAbort();
950
+ // Pin authStorage to modelRegistry.authStorage mirrors the createAgentSession invariant.
952
951
  const registryFromParent = options.modelRegistry !== undefined;
953
- const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
952
+ const modelRegistry =
953
+ options.modelRegistry ?? new ModelRegistry(options.authStorage ?? (await discoverAuthStorage()));
954
+ const authStorage = modelRegistry.authStorage;
955
+ if (options.authStorage && options.authStorage !== authStorage) {
956
+ throw new Error(
957
+ "options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
958
+ );
959
+ }
960
+ checkAbort();
954
961
  if (!registryFromParent) {
955
962
  await modelRegistry.refresh();
956
963
  } else {
package/src/task/index.ts CHANGED
@@ -36,7 +36,6 @@ import { generateCommitMessage } from "../utils/commit-message-generator";
36
36
  import * as git from "../utils/git";
37
37
  import { discoverAgents, getAgent } from "./discovery";
38
38
  import { runSubprocess } from "./executor";
39
- import { resolveIsolationBackendForTaskExecution } from "./isolation-backend";
40
39
  import { AgentOutputManager } from "./output-manager";
41
40
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
42
41
  import { renderResult, renderCall as renderTaskCall } from "./render";
@@ -50,20 +49,17 @@ import {
50
49
  type TaskToolDetails,
51
50
  } from "./types";
52
51
  import {
53
- applyBaseline,
54
52
  applyNestedPatches,
55
53
  captureBaseline,
56
54
  captureDeltaPatch,
57
- cleanupFuseOverlay,
58
- cleanupProjfsOverlay,
55
+ cleanupIsolation,
59
56
  cleanupTaskBranches,
60
- cleanupWorktree,
61
57
  commitToBranch,
62
- ensureFuseOverlay,
63
- ensureProjfsOverlay,
64
- ensureWorktree,
58
+ ensureIsolation,
65
59
  getRepoRoot,
60
+ type IsolationHandle,
66
61
  mergeTaskBranches,
62
+ parseIsolationMode,
67
63
  type WorktreeBaseline,
68
64
  } from "./worktree";
69
65
 
@@ -487,11 +483,27 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
487
483
  ? ` Failed to schedule ${failedSchedules.length} task${failedSchedules.length === 1 ? "" : "s"}.`
488
484
  : "";
489
485
 
486
+ const ircEnabled = this.session.settings.get("irc.enabled") === true;
487
+ const taskIdByItemId = new Map<string, string>();
488
+ for (let i = 0; i < taskItems.length; i++) {
489
+ taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
490
+ }
491
+ const startedListing = startedJobs
492
+ .map(({ taskId }) => {
493
+ const id = taskIdByItemId.get(taskId) ?? taskId;
494
+ const desc = progressByTaskId.get(taskId)?.description;
495
+ return desc ? `- \`${id}\` — ${desc}` : `- \`${id}\``;
496
+ })
497
+ .join("\n");
498
+ const coordinationHint = ircEnabled
499
+ ? ` DM these ids via \`irc\` to coordinate while they run; reach for \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
500
+ : ` Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task by id.`;
501
+
490
502
  return {
491
503
  content: [
492
504
  {
493
505
  type: "text",
494
- text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.`,
506
+ text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.\n${startedListing}\n${coordinationHint}`,
495
507
  },
496
508
  ],
497
509
  details: {
@@ -530,7 +542,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
530
542
  content: [
531
543
  {
532
544
  type: "text",
533
- text: "Task isolation is disabled. Remove the isolated argument or set task.isolation.mode to 'worktree', 'fuse-overlay', or 'fuse-projfs'.",
545
+ text: "Task isolation is disabled.",
534
546
  },
535
547
  ],
536
548
  details: {
@@ -700,28 +712,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
700
712
  }
701
713
  }
702
714
 
703
- let effectiveIsolationMode = isolationMode;
704
- let isolationBackendWarning = "";
705
- try {
706
- const resolvedIsolation = await resolveIsolationBackendForTaskExecution(isolationMode, isIsolated, repoRoot);
707
- effectiveIsolationMode = resolvedIsolation.effectiveIsolationMode;
708
- isolationBackendWarning = resolvedIsolation.warning;
709
- } catch (err) {
710
- const message = err instanceof Error ? err.message : String(err);
711
- return {
712
- content: [
713
- {
714
- type: "text",
715
- text: message,
716
- },
717
- ],
718
- details: {
719
- projectAgentsDir,
720
- results: [],
721
- totalDurationMs: Date.now() - startTime,
722
- },
723
- };
724
- }
715
+ const preferredIsolationBackend = parseIsolationMode(isolationMode);
725
716
 
726
717
  // Derive artifacts directory
727
718
  const sessionFile = this.session.getSessionFile();
@@ -891,21 +882,15 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
891
882
  }
892
883
 
893
884
  const taskStart = Date.now();
894
- let isolationDir: string | undefined;
885
+ let isolationHandle: IsolationHandle | undefined;
895
886
  try {
896
887
  if (!repoRoot || !baseline) {
897
888
  throw new Error("Isolated task execution not initialized.");
898
889
  }
899
890
  const taskBaseline = structuredClone(baseline);
900
891
 
901
- if (effectiveIsolationMode === "fuse-overlay") {
902
- isolationDir = await ensureFuseOverlay(repoRoot, task.id);
903
- } else if (effectiveIsolationMode === "fuse-projfs") {
904
- isolationDir = await ensureProjfsOverlay(repoRoot, task.id);
905
- } else {
906
- isolationDir = await ensureWorktree(repoRoot, task.id);
907
- await applyBaseline(isolationDir, taskBaseline);
908
- }
892
+ isolationHandle = await ensureIsolation(repoRoot, task.id, preferredIsolationBackend);
893
+ const isolationDir = isolationHandle.mergedDir;
909
894
 
910
895
  const result = await runSubprocess({
911
896
  cwd: this.session.cwd,
@@ -1017,14 +1002,8 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1017
1002
  error: message,
1018
1003
  };
1019
1004
  } finally {
1020
- if (isolationDir) {
1021
- if (effectiveIsolationMode === "fuse-overlay") {
1022
- await cleanupFuseOverlay(isolationDir);
1023
- } else if (effectiveIsolationMode === "fuse-projfs") {
1024
- await cleanupProjfsOverlay(isolationDir);
1025
- } else {
1026
- await cleanupWorktree(isolationDir);
1027
- }
1005
+ if (isolationHandle) {
1006
+ await cleanupIsolation(isolationHandle);
1028
1007
  }
1029
1008
  }
1030
1009
  };
@@ -1256,7 +1235,6 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1256
1235
  });
1257
1236
 
1258
1237
  const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
1259
- const backendSummaryPrefix = isolationBackendWarning ? `\n\n${isolationBackendWarning}` : "";
1260
1238
  const summary = prompt.render(taskSummaryTemplate, {
1261
1239
  successCount,
1262
1240
  totalCount: results.length,
@@ -1266,7 +1244,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1266
1244
  summaries,
1267
1245
  outputIds,
1268
1246
  agentName,
1269
- mergeSummary: `${backendSummaryPrefix}${mergeSummary}`,
1247
+ mergeSummary,
1270
1248
  });
1271
1249
 
1272
1250
  // Cleanup temp directory if used
@@ -661,8 +661,10 @@ function renderReviewResult(
661
661
  lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
662
662
  }
663
663
  } else {
664
- // Preview: first sentence or ~100 chars
665
- const preview = truncateToWidth(`${summary.explanation.split(/[.!?]/)[0]}.`, 100);
664
+ // Preview: first sentence or ~100 chars (flatten tabs/newlines first)
665
+ const flat = replaceTabs(summary.explanation).replace(/[\r\n]+/g, " ");
666
+ const firstSentence = flat.split(/[.!?]/)[0].trim();
667
+ const preview = truncateToWidth(`${firstSentence}.`, 100);
666
668
  lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
667
669
  }
668
670
  }
@@ -701,7 +703,8 @@ function renderFindings(
701
703
  const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
702
704
 
703
705
  const { color } = getPriorityInfo(finding.priority);
704
- const titleText = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
706
+ const rawTitle = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
707
+ const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
705
708
  const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
706
709
 
707
710
  lines.push(