@oh-my-pi/pi-coding-agent 13.5.1 → 13.5.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.
package/src/tools/ask.ts CHANGED
@@ -14,9 +14,11 @@
14
14
  * - Use recommended: <index> to mark the default option; "(Recommended)" suffix is added automatically
15
15
  * - Questions may time out and auto-select the recommended option (configurable, disabled in plan mode)
16
16
  */
17
+
17
18
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
18
19
  import type { Component } from "@oh-my-pi/pi-tui";
19
20
  import { TERMINAL, Text } from "@oh-my-pi/pi-tui";
21
+ import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
20
22
  import { type Static, Type } from "@sinclair/typebox";
21
23
  import { renderPromptTemplate } from "../config/prompt-templates";
22
24
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -25,6 +27,7 @@ import askDescription from "../prompts/tools/ask.md" with { type: "text" };
25
27
  import { renderStatusLine } from "../tui";
26
28
  import type { ToolSession } from ".";
27
29
  import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
30
+ import { ToolAbortError } from "./tool-errors";
28
31
 
29
32
  // =============================================================================
30
33
  // Types
@@ -110,14 +113,9 @@ interface UIContext {
110
113
  select(
111
114
  prompt: string,
112
115
  options: string[],
113
- options_?: { initialIndex?: number; timeout?: number; outline?: boolean },
116
+ options_?: { initialIndex?: number; signal?: AbortSignal; outline?: boolean },
114
117
  ): Promise<string | undefined>;
115
- input(prompt: string): Promise<string | undefined>;
116
- }
117
-
118
- interface AskQuestionOptions {
119
- /** Timeout in milliseconds, null/undefined to disable */
120
- timeout?: number | null;
118
+ input(prompt: string, options_?: { signal?: AbortSignal }): Promise<string | undefined>;
121
119
  }
122
120
 
123
121
  async function askSingleQuestion(
@@ -126,9 +124,8 @@ async function askSingleQuestion(
126
124
  optionLabels: string[],
127
125
  multi: boolean,
128
126
  recommended?: number,
129
- options?: AskQuestionOptions,
127
+ signal?: AbortSignal,
130
128
  ): Promise<SelectionResult> {
131
- const timeout = options?.timeout ?? undefined;
132
129
  const doneLabel = getDoneOptionLabel();
133
130
  let selectedOptions: string[] = [];
134
131
  let customInput: string | undefined;
@@ -152,22 +149,27 @@ async function askSingleQuestion(
152
149
  opts.push(OTHER_OPTION);
153
150
 
154
151
  const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
155
- const selectionStart = Date.now();
156
- const choice = await ui.select(`${prefix}${question}`, opts, {
157
- initialIndex: cursorIndex,
158
- timeout: timeout ?? undefined,
159
- outline: true,
160
- });
161
- const elapsed = Date.now() - selectionStart;
162
- const timedOut = timeout != null && elapsed >= timeout;
152
+ const choice = signal
153
+ ? await untilAborted(signal, () =>
154
+ ui.select(`${prefix}${question}`, opts, {
155
+ initialIndex: cursorIndex,
156
+ signal,
157
+ outline: true,
158
+ }),
159
+ )
160
+ : await ui.select(`${prefix}${question}`, opts, {
161
+ initialIndex: cursorIndex,
162
+ signal,
163
+ outline: true,
164
+ });
163
165
 
164
166
  if (choice === undefined || choice === doneLabel) break;
165
167
 
166
168
  if (choice === OTHER_OPTION) {
167
- if (!timedOut) {
168
- const input = await ui.input("Enter your response:");
169
- if (input) customInput = input;
170
- }
169
+ const input = signal
170
+ ? await untilAborted(signal, () => ui.input("Enter your response:", { signal }))
171
+ : await ui.input("Enter your response:", { signal });
172
+ if (input) customInput = input;
171
173
  break;
172
174
  }
173
175
 
@@ -192,21 +194,28 @@ async function askSingleQuestion(
192
194
  selected.add(opt);
193
195
  }
194
196
  }
195
-
196
- if (timedOut) {
197
- break;
198
- }
199
197
  }
200
198
  selectedOptions = Array.from(selected);
201
199
  } else {
202
200
  const displayLabels = addRecommendedSuffix(optionLabels, recommended);
203
- const choice = await ui.select(question, [...displayLabels, OTHER_OPTION], {
204
- timeout: timeout ?? undefined,
205
- initialIndex: recommended,
206
- outline: true,
207
- });
201
+ const choice = signal
202
+ ? await untilAborted(signal, () =>
203
+ ui.select(question, [...displayLabels, OTHER_OPTION], {
204
+ initialIndex: recommended,
205
+ signal,
206
+ outline: true,
207
+ }),
208
+ )
209
+ : await ui.select(question, [...displayLabels, OTHER_OPTION], {
210
+ initialIndex: recommended,
211
+ signal,
212
+ outline: true,
213
+ });
214
+
208
215
  if (choice === OTHER_OPTION) {
209
- const input = await ui.input("Enter your response:");
216
+ const input = signal
217
+ ? await untilAborted(signal, () => ui.input("Enter your response:", { signal }))
218
+ : await ui.input("Enter your response:", { signal });
210
219
  if (input) customInput = input;
211
220
  } else if (choice) {
212
221
  selectedOptions = [stripRecommendedSuffix(choice)];
@@ -265,7 +274,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
265
274
  async execute(
266
275
  _toolCallId: string,
267
276
  params: AskParams,
268
- _signal?: AbortSignal,
277
+ signal?: AbortSignal,
269
278
  _onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
270
279
  context?: AgentToolContext,
271
280
  ): Promise<AgentToolResult<AskToolDetails>> {
@@ -277,7 +286,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
277
286
  };
278
287
  }
279
288
 
280
- const { ui } = context;
289
+ const extensionUi = context.ui;
290
+ const ui: UIContext = {
291
+ select: (prompt, options, dialogOptions) => extensionUi.select(prompt, options, dialogOptions),
292
+ input: (prompt, dialogOptions) => extensionUi.input(prompt, undefined, dialogOptions),
293
+ };
281
294
 
282
295
  // Determine timeout based on settings and plan mode
283
296
  const planModeEnabled = this.session.getPlanModeState?.()?.enabled ?? false;
@@ -296,18 +309,41 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
296
309
  };
297
310
  }
298
311
 
312
+ const askQuestion = async (q: AskParams["questions"][number]) => {
313
+ const optionLabels = q.options.map(o => o.label);
314
+ const timeoutSignal = timeout == null ? undefined : AbortSignal.timeout(timeout);
315
+ const questionSignal = ptree.combineSignals(signal, timeoutSignal);
316
+ try {
317
+ const { selectedOptions, customInput } = await askSingleQuestion(
318
+ ui,
319
+ q.question,
320
+ optionLabels,
321
+ q.multi ?? false,
322
+ q.recommended,
323
+ questionSignal,
324
+ );
325
+ return { optionLabels, selectedOptions, customInput, timedOut: false };
326
+ } catch (error) {
327
+ if (error instanceof Error && error.name === "AbortError") {
328
+ if (signal?.aborted) {
329
+ throw new ToolAbortError("Ask input was cancelled");
330
+ }
331
+ if (timeoutSignal?.aborted) {
332
+ return { optionLabels, selectedOptions: [], customInput: undefined, timedOut: true };
333
+ }
334
+ }
335
+ throw error;
336
+ }
337
+ };
338
+
299
339
  if (params.questions.length === 1) {
300
340
  const [q] = params.questions;
301
- const optionLabels = q.options.map(o => o.label);
302
- const { selectedOptions, customInput } = await askSingleQuestion(
303
- ui,
304
- q.question,
305
- optionLabels,
306
- q.multi ?? false,
307
- q.recommended,
308
- { timeout },
309
- );
341
+ const { optionLabels, selectedOptions, customInput, timedOut } = await askQuestion(q);
310
342
 
343
+ if (!timedOut && selectedOptions.length === 0 && !customInput) {
344
+ context.abort();
345
+ throw new ToolAbortError("Ask tool was cancelled by the user");
346
+ }
311
347
  const details: AskToolDetails = {
312
348
  question: q.question,
313
349
  options: optionLabels,
@@ -333,16 +369,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
333
369
  const results: QuestionResult[] = [];
334
370
 
335
371
  for (const q of params.questions) {
336
- const optionLabels = q.options.map(o => o.label);
337
- const { selectedOptions, customInput } = await askSingleQuestion(
338
- ui,
339
- q.question,
340
- optionLabels,
341
- q.multi ?? false,
342
- q.recommended,
343
- { timeout },
344
- );
372
+ const { optionLabels, selectedOptions, customInput, timedOut } = await askQuestion(q);
345
373
 
374
+ if (!timedOut && selectedOptions.length === 0 && !customInput) {
375
+ context.abort();
376
+ throw new ToolAbortError("Ask tool was cancelled by the user");
377
+ }
346
378
  results.push({
347
379
  id: q.id,
348
380
  question: q.question,
package/src/tools/bash.ts CHANGED
@@ -98,7 +98,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
98
98
  constructor(private readonly session: ToolSession) {
99
99
  this.#asyncEnabled = this.session.settings.get("async.enabled");
100
100
  this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
101
- this.description = renderPromptTemplate(bashDescription, { asyncEnabled: this.#asyncEnabled });
101
+ this.description = renderPromptTemplate(bashDescription, {
102
+ asyncEnabled: this.#asyncEnabled,
103
+ hasAstGrep: this.session.settings.get("astGrep.enabled"),
104
+ hasAstEdit: this.session.settings.get("astEdit.enabled"),
105
+ });
102
106
  }
103
107
 
104
108
  #formatResultOutput(result: BashResult | BashInteractiveResult, headLines?: number, tailLines?: number): string {
@@ -0,0 +1,128 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import { type Static, Type } from "@sinclair/typebox";
3
+ import { renderPromptTemplate } from "../config/prompt-templates";
4
+ import checkpointDescription from "../prompts/tools/checkpoint.md" with { type: "text" };
5
+ import rewindDescription from "../prompts/tools/rewind.md" with { type: "text" };
6
+ import type { ToolSession } from ".";
7
+ import type { OutputMeta } from "./output-meta";
8
+ import { ToolError } from "./tool-errors";
9
+ import { toolResult } from "./tool-result";
10
+
11
+ export interface CheckpointState {
12
+ /** Number of in-memory messages at checkpoint (AFTER checkpoint tool result is appended) */
13
+ checkpointMessageCount: number;
14
+ /** Session entry ID at checkpoint (for session tree branching) */
15
+ checkpointEntryId: string | null;
16
+ /** Timestamp */
17
+ startedAt: string;
18
+ }
19
+
20
+ const checkpointSchema = Type.Object({
21
+ goal: Type.String({ description: "What you are investigating and why" }),
22
+ });
23
+
24
+ type CheckpointParams = Static<typeof checkpointSchema>;
25
+
26
+ const rewindSchema = Type.Object({
27
+ report: Type.String({ description: "Concise investigation findings to retain after rewind" }),
28
+ });
29
+
30
+ type RewindParams = Static<typeof rewindSchema>;
31
+
32
+ export interface CheckpointToolDetails {
33
+ goal: string;
34
+ startedAt: string;
35
+ meta?: OutputMeta;
36
+ }
37
+
38
+ export interface RewindToolDetails {
39
+ report: string;
40
+ rewound: boolean;
41
+ meta?: OutputMeta;
42
+ }
43
+
44
+ function isTopLevelSession(session: ToolSession): boolean {
45
+ const depth = session.taskDepth;
46
+ return depth === undefined || depth === 0;
47
+ }
48
+
49
+ export class CheckpointTool implements AgentTool<typeof checkpointSchema, CheckpointToolDetails> {
50
+ readonly name = "checkpoint";
51
+ readonly label = "Checkpoint";
52
+ readonly description: string;
53
+ readonly parameters = checkpointSchema;
54
+ readonly strict = true;
55
+
56
+ constructor(private readonly session: ToolSession) {
57
+ this.description = renderPromptTemplate(checkpointDescription);
58
+ }
59
+
60
+ static createIf(session: ToolSession): CheckpointTool | null {
61
+ if (!isTopLevelSession(session)) return null;
62
+ return new CheckpointTool(session);
63
+ }
64
+
65
+ async execute(
66
+ _toolCallId: string,
67
+ params: CheckpointParams,
68
+ _signal?: AbortSignal,
69
+ _onUpdate?: AgentToolUpdateCallback<CheckpointToolDetails>,
70
+ _context?: AgentToolContext,
71
+ ): Promise<AgentToolResult<CheckpointToolDetails>> {
72
+ if (!isTopLevelSession(this.session)) {
73
+ throw new ToolError("Checkpoint not available in subagents.");
74
+ }
75
+ if (this.session.getCheckpointState?.()) {
76
+ throw new ToolError("Checkpoint already active.");
77
+ }
78
+ const startedAt = new Date().toISOString();
79
+ return toolResult<CheckpointToolDetails>({ goal: params.goal, startedAt })
80
+ .text(
81
+ [
82
+ "Checkpoint created.",
83
+ `Goal: ${params.goal}`,
84
+ "Run your investigation, then call rewind with a concise report.",
85
+ ].join("\n"),
86
+ )
87
+ .done();
88
+ }
89
+ }
90
+
91
+ export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDetails> {
92
+ readonly name = "rewind";
93
+ readonly label = "Rewind";
94
+ readonly description: string;
95
+ readonly parameters = rewindSchema;
96
+ readonly strict = true;
97
+
98
+ constructor(private readonly session: ToolSession) {
99
+ this.description = renderPromptTemplate(rewindDescription);
100
+ }
101
+
102
+ static createIf(session: ToolSession): RewindTool | null {
103
+ if (!isTopLevelSession(session)) return null;
104
+ return new RewindTool(session);
105
+ }
106
+
107
+ async execute(
108
+ _toolCallId: string,
109
+ params: RewindParams,
110
+ _signal?: AbortSignal,
111
+ _onUpdate?: AgentToolUpdateCallback<RewindToolDetails>,
112
+ _context?: AgentToolContext,
113
+ ): Promise<AgentToolResult<RewindToolDetails>> {
114
+ if (!isTopLevelSession(this.session)) {
115
+ throw new ToolError("Checkpoint not available in subagents.");
116
+ }
117
+ if (!this.session.getCheckpointState?.()) {
118
+ throw new ToolError("No active checkpoint.");
119
+ }
120
+ const report = params.report.trim();
121
+ if (report.length === 0) {
122
+ throw new ToolError("Report cannot be empty.");
123
+ }
124
+ return toolResult<RewindToolDetails>({ report, rewound: true })
125
+ .text(["Rewind requested.", "Report captured for context replacement."].join("\n"))
126
+ .done();
127
+ }
128
+ }
@@ -22,6 +22,7 @@ import { BashTool } from "./bash";
22
22
  import { BrowserTool } from "./browser";
23
23
  import { CalculatorTool } from "./calculator";
24
24
  import { CancelJobTool } from "./cancel-job";
25
+ import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
25
26
  import { ExitPlanModeTool } from "./exit-plan-mode";
26
27
  import { FetchTool } from "./fetch";
27
28
  import { FindTool } from "./find";
@@ -30,6 +31,7 @@ import { NotebookTool } from "./notebook";
30
31
  import { wrapToolWithMetaNotice } from "./output-meta";
31
32
  import { PythonTool } from "./python";
32
33
  import { ReadTool } from "./read";
34
+ import { RenderMermaidTool } from "./render-mermaid";
33
35
  import { ResolveTool } from "./resolve";
34
36
  import { reportFindingTool } from "./review";
35
37
  import { loadSshTool } from "./ssh";
@@ -54,6 +56,7 @@ export * from "./bash";
54
56
  export * from "./browser";
55
57
  export * from "./calculator";
56
58
  export * from "./cancel-job";
59
+ export * from "./checkpoint";
57
60
  export * from "./exit-plan-mode";
58
61
  export * from "./fetch";
59
62
  export * from "./find";
@@ -63,6 +66,7 @@ export * from "./notebook";
63
66
  export * from "./pending-action";
64
67
  export * from "./python";
65
68
  export * from "./read";
69
+ export * from "./render-mermaid";
66
70
  export * from "./resolve";
67
71
  export * from "./review";
68
72
  export * from "./ssh";
@@ -143,6 +147,10 @@ export interface ToolSession {
143
147
  setTodoPhases?: (phases: TodoPhase[]) => void;
144
148
  /** Pending action store for preview/apply workflows */
145
149
  pendingActionStore?: import("./pending-action").PendingActionStore;
150
+ /** Get active checkpoint state if any. */
151
+ getCheckpointState?: () => CheckpointState | undefined;
152
+ /** Set or clear active checkpoint state. */
153
+ setCheckpointState?: (state: CheckpointState | null) => void;
146
154
  }
147
155
 
148
156
  type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
@@ -150,6 +158,7 @@ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
150
158
  export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
151
159
  ast_grep: s => new AstGrepTool(s),
152
160
  ast_edit: s => new AstEditTool(s),
161
+ render_mermaid: s => new RenderMermaidTool(s),
153
162
  ask: AskTool.createIf,
154
163
  bash: s => new BashTool(s),
155
164
  python: s => new PythonTool(s),
@@ -162,6 +171,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
162
171
  notebook: s => new NotebookTool(s),
163
172
  read: s => new ReadTool(s),
164
173
  browser: s => new BrowserTool(s),
174
+ checkpoint: CheckpointTool.createIf,
175
+ rewind: RewindTool.createIf,
165
176
  task: TaskTool.create,
166
177
  cancel_job: CancelJobTool.createIf,
167
178
  await: AwaitTool.createIf,
@@ -271,6 +282,24 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
271
282
  ) {
272
283
  requestedTools.push("bash");
273
284
  }
285
+
286
+ // Auto-include AST counterparts when their text-based sibling is present
287
+ if (requestedTools) {
288
+ if (
289
+ requestedTools.includes("grep") &&
290
+ !requestedTools.includes("ast_grep") &&
291
+ session.settings.get("astGrep.enabled")
292
+ ) {
293
+ requestedTools.push("ast_grep");
294
+ }
295
+ if (
296
+ requestedTools.includes("edit") &&
297
+ !requestedTools.includes("ast_edit") &&
298
+ session.settings.get("astEdit.enabled")
299
+ ) {
300
+ requestedTools.push("ast_edit");
301
+ }
302
+ }
274
303
  const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
275
304
  const isToolAllowed = (name: string) => {
276
305
  if (name === "lsp") return enableLsp;
@@ -281,12 +310,14 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
281
310
  if (name === "grep") return session.settings.get("grep.enabled");
282
311
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
283
312
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");
313
+ if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
284
314
  if (name === "notebook") return session.settings.get("notebook.enabled");
285
315
  if (name === "fetch") return session.settings.get("fetch.enabled");
286
316
  if (name === "web_search") return session.settings.get("web_search.enabled");
287
317
  if (name === "lsp") return session.settings.get("lsp.enabled");
288
318
  if (name === "calc") return session.settings.get("calc.enabled");
289
319
  if (name === "browser") return session.settings.get("browser.enabled");
320
+ if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
290
321
  if (name === "task") {
291
322
  const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
292
323
  const currentDepth = session.taskDepth ?? 0;
@@ -0,0 +1,67 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import { type MermaidAsciiRenderOptions, renderMermaidAscii } from "@oh-my-pi/pi-utils";
3
+ import { type Static, Type } from "@sinclair/typebox";
4
+ import { renderPromptTemplate } from "../config/prompt-templates";
5
+ import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
6
+ import type { ToolSession } from "./index";
7
+
8
+ const renderMermaidSchema = Type.Object({
9
+ mermaid: Type.String({ description: "Mermaid graph source text" }),
10
+ config: Type.Optional(
11
+ Type.Object({
12
+ useAscii: Type.Optional(Type.Boolean()),
13
+ paddingX: Type.Optional(Type.Number()),
14
+ paddingY: Type.Optional(Type.Number()),
15
+ boxBorderPadding: Type.Optional(Type.Number()),
16
+ }),
17
+ ),
18
+ });
19
+
20
+ type RenderMermaidParams = Static<typeof renderMermaidSchema>;
21
+
22
+ function sanitizeRenderConfig(config: MermaidAsciiRenderOptions | undefined): MermaidAsciiRenderOptions | undefined {
23
+ if (!config) return undefined;
24
+ return {
25
+ useAscii: config.useAscii,
26
+ boxBorderPadding:
27
+ config.boxBorderPadding === undefined ? undefined : Math.max(0, Math.floor(config.boxBorderPadding)),
28
+ paddingX: config.paddingX === undefined ? undefined : Math.max(0, Math.floor(config.paddingX)),
29
+ paddingY: config.paddingY === undefined ? undefined : Math.max(0, Math.floor(config.paddingY)),
30
+ };
31
+ }
32
+ export interface RenderMermaidToolDetails {
33
+ artifactId?: string;
34
+ }
35
+
36
+ export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
37
+ readonly name = "render_mermaid";
38
+ readonly label = "RenderMermaid";
39
+ readonly description: string;
40
+ readonly parameters = renderMermaidSchema;
41
+ readonly strict = true;
42
+
43
+ constructor(private readonly session: ToolSession) {
44
+ this.description = renderPromptTemplate(renderMermaidDescription);
45
+ }
46
+
47
+ async execute(
48
+ _toolCallId: string,
49
+ params: RenderMermaidParams,
50
+ _signal?: AbortSignal,
51
+ _onUpdate?: AgentToolUpdateCallback<RenderMermaidToolDetails>,
52
+ _context?: AgentToolContext,
53
+ ): Promise<AgentToolResult<RenderMermaidToolDetails>> {
54
+ const ascii = renderMermaidAscii(params.mermaid, sanitizeRenderConfig(params.config));
55
+ const { path: artifactPath, id: artifactId } =
56
+ (await this.session.allocateOutputArtifact?.("render_mermaid")) ?? {};
57
+ if (artifactPath) {
58
+ await Bun.write(artifactPath, ascii);
59
+ }
60
+
61
+ const artifactLine = artifactId ? `\n\nSaved artifact: artifact://${artifactId}` : "";
62
+ return {
63
+ content: [{ type: "text", text: `${ascii}${artifactLine}` }],
64
+ details: { artifactId },
65
+ };
66
+ }
67
+ }
@@ -86,9 +86,8 @@ export function formatPromptContent(content: string, options: PromptFormatOption
86
86
 
87
87
  for (let i = 0; i < lines.length; i++) {
88
88
  let line = lines[i].trimEnd();
89
- const trimmed = line.trimStart();
90
-
91
- if (CODE_FENCE.test(trimmed)) {
89
+ let trimmedStart = line.trimStart();
90
+ if (CODE_FENCE.test(trimmedStart)) {
92
91
  inCodeBlock = !inCodeBlock;
93
92
  result.push(line);
94
93
  continue;
@@ -102,30 +101,29 @@ export function formatPromptContent(content: string, options: PromptFormatOption
102
101
  if (replaceAsciiSymbols) {
103
102
  line = replaceCommonAsciiSymbols(line);
104
103
  }
104
+ trimmedStart = line.trimStart();
105
+ const trimmed = line.trim();
105
106
 
106
- const isOpeningXml = OPENING_XML.test(trimmed) && !trimmed.endsWith("/>");
107
- if (isOpeningXml && line.length === trimmed.length) {
108
- const match = OPENING_XML.exec(trimmed);
107
+ const isOpeningXml = OPENING_XML.test(trimmedStart) && !trimmedStart.endsWith("/>");
108
+ if (isOpeningXml && line.length === trimmedStart.length) {
109
+ const match = OPENING_XML.exec(trimmedStart);
109
110
  if (match) topLevelTags.push(match[1]);
110
111
  }
111
112
 
112
- const closingMatch = CLOSING_XML.exec(trimmed);
113
+ const closingMatch = CLOSING_XML.exec(trimmedStart);
113
114
  if (closingMatch) {
114
115
  const tagName = closingMatch[1];
115
116
  if (topLevelTags.length > 0 && topLevelTags[topLevelTags.length - 1] === tagName) {
116
- line = trimmed;
117
117
  topLevelTags.pop();
118
- } else {
119
- line = line.trimEnd();
120
118
  }
121
- } else if (isPreRender && trimmed.startsWith("{{")) {
122
- line = trimmed;
123
- } else if (TABLE_SEP.test(trimmed)) {
124
- line = compactTableSep(trimmed);
125
- } else if (TABLE_ROW.test(trimmed)) {
126
- line = compactTableRow(trimmed);
127
- } else {
128
- line = line.trimEnd();
119
+ } else if (isPreRender && trimmedStart.startsWith("{{")) {
120
+ /* keep indentation as-is in pre-render for Handlebars markers */
121
+ } else if (TABLE_SEP.test(trimmedStart)) {
122
+ const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
123
+ line = `${leadingWhitespace}${compactTableSep(trimmedStart)}`;
124
+ } else if (TABLE_ROW.test(trimmedStart)) {
125
+ const leadingWhitespace = line.slice(0, line.length - trimmedStart.length);
126
+ line = `${leadingWhitespace}${compactTableRow(trimmedStart)}`;
129
127
  }
130
128
 
131
129
  if (shouldBoldRfc2119) {