@oh-my-pi/pi-coding-agent 13.1.1 → 13.2.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.2.0] - 2026-02-23
6
+ ### Breaking Changes
7
+
8
+ - Made `description` field required in CustomTool interface
9
+
10
+ ## [13.1.2] - 2026-02-23
11
+ ### Breaking Changes
12
+
13
+ - Removed `timeout` parameter from await tool—tool now waits indefinitely until jobs complete or the call is aborted
14
+ - Renamed `job_ids` parameter to `jobs` in await tool schema
15
+ - Removed `timedOut` field from await tool result details
16
+
5
17
  ## [13.1.1] - 2026-02-23
6
18
 
7
19
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.1.1",
4
+ "version": "13.2.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.1.1",
45
- "@oh-my-pi/pi-agent-core": "13.1.1",
46
- "@oh-my-pi/pi-ai": "13.1.1",
47
- "@oh-my-pi/pi-natives": "13.1.1",
48
- "@oh-my-pi/pi-tui": "13.1.1",
49
- "@oh-my-pi/pi-utils": "13.1.1",
44
+ "@oh-my-pi/omp-stats": "13.2.0",
45
+ "@oh-my-pi/pi-agent-core": "13.2.0",
46
+ "@oh-my-pi/pi-ai": "13.2.0",
47
+ "@oh-my-pi/pi-natives": "13.2.0",
48
+ "@oh-my-pi/pi-tui": "13.2.0",
49
+ "@oh-my-pi/pi-utils": "13.2.0",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -47,6 +47,7 @@ export interface AsyncJobRegisterOptions {
47
47
  export class AsyncJobManager {
48
48
  readonly #jobs = new Map<string, AsyncJob>();
49
49
  readonly #deliveries: AsyncJobDelivery[] = [];
50
+ readonly #suppressedDeliveries = new Set<string>();
50
51
  readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
51
52
  readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
52
53
  readonly #maxRunningJobs: number;
@@ -81,6 +82,7 @@ export class AsyncJobManager {
81
82
  }
82
83
 
83
84
  const id = this.#resolveJobId(options?.id);
85
+ this.#suppressedDeliveries.delete(id);
84
86
  const abortController = new AbortController();
85
87
  const startTime = Date.now();
86
88
 
@@ -182,6 +184,23 @@ export class AsyncJobManager {
182
184
  return this.#deliveries.length > 0;
183
185
  }
184
186
 
187
+ acknowledgeDeliveries(jobIds: string[]): number {
188
+ const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
189
+ if (uniqueJobIds.length === 0) return 0;
190
+
191
+ for (const jobId of uniqueJobIds) {
192
+ this.#suppressedDeliveries.add(jobId);
193
+ }
194
+
195
+ const before = this.#deliveries.length;
196
+ this.#deliveries.splice(
197
+ 0,
198
+ this.#deliveries.length,
199
+ ...this.#deliveries.filter(delivery => !this.#suppressedDeliveries.has(delivery.jobId)),
200
+ );
201
+ return before - this.#deliveries.length;
202
+ }
203
+
185
204
  cancelAll(): void {
186
205
  for (const job of this.getRunningJobs()) {
187
206
  job.status = "cancelled";
@@ -234,6 +253,7 @@ export class AsyncJobManager {
234
253
  this.#clearEvictionTimers();
235
254
  this.#jobs.clear();
236
255
  this.#deliveries.length = 0;
256
+ this.#suppressedDeliveries.clear();
237
257
  return drained;
238
258
  }
239
259
 
@@ -257,6 +277,7 @@ export class AsyncJobManager {
257
277
  #scheduleEviction(jobId: string): void {
258
278
  if (this.#retentionMs <= 0) {
259
279
  this.#jobs.delete(jobId);
280
+ this.#suppressedDeliveries.delete(jobId);
260
281
  return;
261
282
  }
262
283
  const existing = this.#evictionTimers.get(jobId);
@@ -266,6 +287,7 @@ export class AsyncJobManager {
266
287
  const timer = setTimeout(() => {
267
288
  this.#evictionTimers.delete(jobId);
268
289
  this.#jobs.delete(jobId);
290
+ this.#suppressedDeliveries.delete(jobId);
269
291
  }, this.#retentionMs);
270
292
  timer.unref();
271
293
  this.#evictionTimers.set(jobId, timer);
@@ -278,7 +300,14 @@ export class AsyncJobManager {
278
300
  this.#evictionTimers.clear();
279
301
  }
280
302
 
303
+ #isDeliverySuppressed(jobId: string): boolean {
304
+ return this.#suppressedDeliveries.has(jobId);
305
+ }
306
+
281
307
  #enqueueDelivery(jobId: string, text: string): void {
308
+ if (this.#isDeliverySuppressed(jobId)) {
309
+ return;
310
+ }
282
311
  this.#deliveries.push({
283
312
  jobId,
284
313
  text,
@@ -308,10 +337,21 @@ export class AsyncJobManager {
308
337
  async #runDeliveryLoop(): Promise<void> {
309
338
  while (this.#deliveries.length > 0) {
310
339
  const delivery = this.#deliveries[0];
340
+ if (this.#isDeliverySuppressed(delivery.jobId)) {
341
+ this.#deliveries.shift();
342
+ continue;
343
+ }
311
344
  const waitMs = delivery.nextAttemptAt - Date.now();
312
345
  if (waitMs > 0) {
313
346
  await Bun.sleep(waitMs);
314
347
  }
348
+ if (this.#deliveries[0] !== delivery) {
349
+ continue;
350
+ }
351
+ if (this.#isDeliverySuppressed(delivery.jobId)) {
352
+ this.#deliveries.shift();
353
+ continue;
354
+ }
315
355
 
316
356
  try {
317
357
  await this.#onJobComplete(delivery.jobId, delivery.text, this.#jobs.get(delivery.jobId));
@@ -321,7 +361,9 @@ export class AsyncJobManager {
321
361
  delivery.lastError = error instanceof Error ? error.message : String(error);
322
362
  delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
323
363
  this.#deliveries.shift();
324
- this.#deliveries.push(delivery);
364
+ if (!this.#isDeliverySuppressed(delivery.jobId)) {
365
+ this.#deliveries.push(delivery);
366
+ }
325
367
  logger.warn("Async job completion delivery failed", {
326
368
  jobId: delivery.jobId,
327
369
  attempt: delivery.attempt,
@@ -15,7 +15,7 @@ export interface CustomTool {
15
15
  /** Absolute path to tool definition file */
16
16
  path: string;
17
17
  /** Tool description */
18
- description?: string;
18
+ description: string;
19
19
  /** Tool implementation (script path or inline) */
20
20
  implementation?: string;
21
21
  /** Source level */
@@ -176,6 +176,16 @@ export const SETTINGS_SCHEMA = {
176
176
  description: "Use blue instead of green for diff additions",
177
177
  },
178
178
  },
179
+ "display.tabWidth": {
180
+ type: "number",
181
+ default: 3,
182
+ ui: {
183
+ tab: "display",
184
+ label: "Tab width",
185
+ description: "Default number of spaces used when rendering tab characters",
186
+ submenu: true,
187
+ },
188
+ },
179
189
  defaultThinkingLevel: {
180
190
  type: "enum",
181
191
  values: ["off", "minimal", "low", "medium", "high", "xhigh"] as const,
@@ -13,7 +13,7 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
16
+ import { isEnoent, logger, procmgr, setDefaultTabWidth } from "@oh-my-pi/pi-utils";
17
17
  import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
18
18
  import { YAML } from "bun";
19
19
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
@@ -438,6 +438,7 @@ export class Settings {
438
438
 
439
439
  // Build merged view
440
440
  this.#rebuildMerged();
441
+ this.#fireAllHooks();
441
442
  return this;
442
443
  }
443
444
 
@@ -610,6 +611,16 @@ export class Settings {
610
611
  this.#merged = this.#deepMerge(this.#merged, this.#overrides);
611
612
  }
612
613
 
614
+ #fireAllHooks(): void {
615
+ for (const key of Object.keys(SETTING_HOOKS) as SettingPath[]) {
616
+ const hook = SETTING_HOOKS[key];
617
+ if (hook) {
618
+ const value = this.get(key);
619
+ hook(value, value);
620
+ }
621
+ }
622
+ }
623
+
613
624
  #deepMerge(base: RawSettings, overrides: RawSettings): RawSettings {
614
625
  const result = { ...base };
615
626
  for (const key of Object.keys(overrides)) {
@@ -666,6 +677,11 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
666
677
  });
667
678
  }
668
679
  },
680
+ "display.tabWidth": value => {
681
+ if (typeof value === "number") {
682
+ setDefaultTabWidth(value);
683
+ }
684
+ },
669
685
  };
670
686
 
671
687
  // ═══════════════════════════════════════════════════════════════════════════
@@ -656,20 +656,30 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
656
656
  transform: (name, content, path, source) => {
657
657
  if (name.endsWith(".json")) {
658
658
  const data = parseJSON<{ name?: string; description?: string }>(content);
659
+ const toolName = data?.name || name.replace(/\.json$/, "");
660
+ const description =
661
+ typeof data?.description === "string" && data.description.trim()
662
+ ? data.description
663
+ : `${toolName} custom tool`;
659
664
  return {
660
- name: data?.name || name.replace(/\.json$/, ""),
665
+ name: toolName,
661
666
  path,
662
- description: data?.description,
667
+ description,
663
668
  level,
664
669
  _source: source,
665
670
  };
666
671
  }
667
672
  if (name.endsWith(".md")) {
668
673
  const { frontmatter } = parseFrontmatter(content, { source: path });
674
+ const toolName = (frontmatter.name as string) || name.replace(/\.md$/, "");
675
+ const description =
676
+ typeof frontmatter.description === "string" && frontmatter.description.trim()
677
+ ? String(frontmatter.description)
678
+ : `${toolName} custom tool`;
669
679
  return {
670
- name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
680
+ name: toolName,
671
681
  path,
672
- description: frontmatter.description as string | undefined,
682
+ description,
673
683
  level,
674
684
  _source: source,
675
685
  };
@@ -679,6 +689,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
679
689
  return {
680
690
  name: toolName,
681
691
  path,
692
+ description: `${toolName} custom tool`,
682
693
  level,
683
694
  _source: source,
684
695
  };
@@ -715,7 +726,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
715
726
  items.push({
716
727
  name: entryName,
717
728
  path: indexPath,
718
- description: undefined,
729
+ description: `${entryName} custom tool`,
719
730
  level,
720
731
  _source: createSourceMeta(PROVIDER_ID, indexPath, level),
721
732
  });
@@ -152,6 +152,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
152
152
  return {
153
153
  name: toolName,
154
154
  path: filePath,
155
+ description: `${toolName} custom tool`,
155
156
  level: root.scope,
156
157
  _source: source,
157
158
  };
@@ -324,10 +324,10 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
324
324
  const userResult = await loadFilesFromDir<CustomTool>(ctx, userToolsDir, PROVIDER_ID, "user", {
325
325
  transform: (name, _content, path, source) => {
326
326
  const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
327
-
328
327
  return {
329
328
  name: toolName,
330
329
  path,
330
+ description: `${toolName} custom tool`,
331
331
  level: "user",
332
332
  _source: source,
333
333
  };
@@ -343,10 +343,10 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
343
343
  const projectResult = await loadFilesFromDir<CustomTool>(ctx, projectToolsDir, PROVIDER_ID, "project", {
344
344
  transform: (name, _content, path, source) => {
345
345
  const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
346
-
347
346
  return {
348
347
  name: toolName,
349
348
  path,
349
+ description: `${toolName} custom tool`,
350
350
  level: "project",
351
351
  _source: source,
352
352
  };
@@ -1,3 +1,4 @@
1
+ import { getIndentation } from "@oh-my-pi/pi-utils";
1
2
  import * as Diff from "diff";
2
3
  import { theme } from "../../modes/theme/theme";
3
4
  import { replaceTabs } from "../../tools/render-utils";
@@ -12,26 +13,30 @@ const DIM_OFF = "\x1b[22m";
12
13
  * before the first non-whitespace character; remaining tabs in code
13
14
  * content are replaced with spaces (like replaceTabs).
14
15
  */
15
- function visualizeIndent(text: string): string {
16
+ function visualizeIndent(text: string, filePath?: string): string {
16
17
  const match = text.match(/^([ \t]+)/);
17
- if (!match) return replaceTabs(text);
18
+ if (!match) return replaceTabs(text, filePath);
18
19
  const indent = match[1];
19
20
  const rest = text.slice(indent.length);
20
- // Normalize: collapse 3-space groups (tab-width) into tab arrows,
21
- // then handle remaining tabs and lone spaces.
22
- const normalized = indent.replaceAll("\t", " ");
21
+ const indentation = getIndentation(filePath);
22
+ const tabWidth = indentation.length;
23
+ const leftPadding = Math.floor(tabWidth / 2);
24
+ const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
25
+ const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
26
+ // Normalize: collapse configured tab-width groups into tab markers, then handle remaining spaces.
27
+ const normalized = indent.replaceAll("\t", indentation);
23
28
  let visible = "";
24
29
  let pos = 0;
25
30
  while (pos < normalized.length) {
26
- if (pos + 3 <= normalized.length && normalized.slice(pos, pos + 3) === " ") {
27
- visible += `${DIM} → ${DIM_OFF}`;
28
- pos += 3;
31
+ if (pos + tabWidth <= normalized.length && normalized.slice(pos, pos + tabWidth) === indentation) {
32
+ visible += tabMarker;
33
+ pos += tabWidth;
29
34
  } else {
30
35
  visible += `${DIM}·${DIM_OFF}`;
31
36
  pos++;
32
37
  }
33
38
  }
34
- return `${visible}${replaceTabs(rest)}`;
39
+ return `${visible}${replaceTabs(rest, filePath)}`;
35
40
  }
36
41
 
37
42
  /**
@@ -96,7 +101,7 @@ function renderIntraLineDiff(oldContent: string, newContent: string): { removedL
96
101
  }
97
102
 
98
103
  export interface RenderDiffOptions {
99
- /** File path (unused, kept for API compatibility) */
104
+ /** File path used to resolve indentation (.editorconfig + defaults) */
100
105
  filePath?: string;
101
106
  }
102
107
 
@@ -106,7 +111,7 @@ export interface RenderDiffOptions {
106
111
  * - Removed lines: red, with inverse on changed tokens
107
112
  * - Added lines: green, with inverse on changed tokens
108
113
  */
109
- export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string {
114
+ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
110
115
  const lines = diffText.split("\n");
111
116
  const result: string[] = [];
112
117
 
@@ -154,30 +159,55 @@ export function renderDiff(diffText: string, _options: RenderDiffOptions = {}):
154
159
  const added = addedLines[0];
155
160
 
156
161
  const { removedLine, addedLine } = renderIntraLineDiff(
157
- replaceTabs(removed.content),
158
- replaceTabs(added.content),
162
+ replaceTabs(removed.content, options.filePath),
163
+ replaceTabs(added.content, options.filePath),
159
164
  );
160
165
 
161
- result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removedLine))));
162
- result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine))));
166
+ result.push(
167
+ theme.fg(
168
+ "toolDiffRemoved",
169
+ formatLine("-", removed.lineNum, visualizeIndent(removedLine, options.filePath)),
170
+ ),
171
+ );
172
+ result.push(
173
+ theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine, options.filePath))),
174
+ );
163
175
  } else {
164
176
  // Show all removed lines first, then all added lines
165
177
  for (const removed of removedLines) {
166
178
  result.push(
167
- theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removed.content))),
179
+ theme.fg(
180
+ "toolDiffRemoved",
181
+ formatLine("-", removed.lineNum, visualizeIndent(removed.content, options.filePath)),
182
+ ),
168
183
  );
169
184
  }
170
185
  for (const added of addedLines) {
171
- result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(added.content))));
186
+ result.push(
187
+ theme.fg(
188
+ "toolDiffAdded",
189
+ formatLine("+", added.lineNum, visualizeIndent(added.content, options.filePath)),
190
+ ),
191
+ );
172
192
  }
173
193
  }
174
194
  } else if (parsed.prefix === "+") {
175
195
  // Standalone added line
176
- result.push(theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, visualizeIndent(parsed.content))));
196
+ result.push(
197
+ theme.fg(
198
+ "toolDiffAdded",
199
+ formatLine("+", parsed.lineNum, visualizeIndent(parsed.content, options.filePath)),
200
+ ),
201
+ );
177
202
  i++;
178
203
  } else {
179
204
  // Context line
180
- result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, visualizeIndent(parsed.content))));
205
+ result.push(
206
+ theme.fg(
207
+ "toolDiffContext",
208
+ formatLine(" ", parsed.lineNum, visualizeIndent(parsed.content, options.filePath)),
209
+ ),
210
+ );
181
211
  i++;
182
212
  }
183
213
  }
@@ -16,6 +16,16 @@
16
16
  </file>
17
17
  {{/list}}
18
18
  </instructions>
19
+ {{/if}}
20
+ {{#if git.isRepo}}
21
+ ## Version Control
22
+ Snapshot; does not update during conversation.
23
+ Current branch: {{git.currentBranch}}
24
+ Main branch: {{git.mainBranch}}
25
+ {{git.status}}
26
+ ### History
27
+ {{git.commits}}
28
+ {{/if}}
19
29
  </project>
20
30
  {{/ifAny}}
21
31
  {{#if skills.length}}
@@ -5,16 +5,11 @@ import awaitDescription from "../prompts/tools/await.md" with { type: "text" };
5
5
  import type { ToolSession } from "./index";
6
6
 
7
7
  const awaitSchema = Type.Object({
8
- job_ids: Type.Optional(
8
+ jobs: Type.Optional(
9
9
  Type.Array(Type.String(), {
10
10
  description: "Specific job IDs to wait for. If omitted, waits for any running job.",
11
11
  }),
12
12
  ),
13
- timeout: Type.Optional(
14
- Type.Number({
15
- description: "Maximum seconds to wait before returning (default: 300)",
16
- }),
17
- ),
18
13
  });
19
14
 
20
15
  type AwaitParams = Static<typeof awaitSchema>;
@@ -31,7 +26,6 @@ interface AwaitResult {
31
26
 
32
27
  export interface AwaitToolDetails {
33
28
  jobs: AwaitResult[];
34
- timedOut: boolean;
35
29
  }
36
30
 
37
31
  export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails> {
@@ -61,12 +55,11 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
61
55
  if (!manager) {
62
56
  return {
63
57
  content: [{ type: "text", text: "Async execution is disabled; no background jobs to poll." }],
64
- details: { jobs: [], timedOut: false },
58
+ details: { jobs: [] },
65
59
  };
66
60
  }
67
61
 
68
- const timeoutMs = (params.timeout ?? 300) * 1000;
69
- const requestedIds = params.job_ids;
62
+ const requestedIds = params.jobs;
70
63
 
71
64
  // Resolve which jobs to watch
72
65
  const jobsToWatch = requestedIds?.length
@@ -79,19 +72,18 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
79
72
  : "No running background jobs to wait for.";
80
73
  return {
81
74
  content: [{ type: "text", text: message }],
82
- details: { jobs: [], timedOut: false },
75
+ details: { jobs: [] },
83
76
  };
84
77
  }
85
78
 
86
79
  // If all watched jobs are already done, return immediately
87
80
  const runningJobs = jobsToWatch.filter(j => j.status === "running");
88
81
  if (runningJobs.length === 0) {
89
- return this.#buildResult(jobsToWatch, false);
82
+ return this.#buildResult(manager, jobsToWatch);
90
83
  }
91
84
 
92
- // Block until at least one running job finishes or timeout
85
+ // Block until at least one running job finishes or the call is aborted
93
86
  const racePromises: Promise<unknown>[] = runningJobs.map(j => j.promise);
94
- racePromises.push(Bun.sleep(timeoutMs));
95
87
 
96
88
  if (signal) {
97
89
  const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<void>();
@@ -108,17 +100,14 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
108
100
  }
109
101
 
110
102
  if (signal?.aborted) {
111
- return this.#buildResult(jobsToWatch, false);
103
+ return this.#buildResult(manager, jobsToWatch);
112
104
  }
113
105
 
114
- // Check if we timed out (all watched jobs still running)
115
- const stillRunning = jobsToWatch.filter(j => j.status === "running");
116
- const timedOut = stillRunning.length === runningJobs.length;
117
-
118
- return this.#buildResult(jobsToWatch, timedOut);
106
+ return this.#buildResult(manager, jobsToWatch);
119
107
  }
120
108
 
121
109
  #buildResult(
110
+ manager: NonNullable<ToolSession["asyncJobManager"]>,
122
111
  jobs: {
123
112
  id: string;
124
113
  type: "bash" | "task";
@@ -128,7 +117,6 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
128
117
  resultText?: string;
129
118
  errorText?: string;
130
119
  }[],
131
- timedOut: boolean,
132
120
  ): AgentToolResult<AwaitToolDetails> {
133
121
  const now = Date.now();
134
122
  const jobResults: AwaitResult[] = jobs.map(j => ({
@@ -141,14 +129,12 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
141
129
  ...(j.errorText ? { errorText: j.errorText } : {}),
142
130
  }));
143
131
 
132
+ manager.acknowledgeDeliveries(jobResults.filter(j => j.status !== "running").map(j => j.id));
133
+
144
134
  const completed = jobResults.filter(j => j.status !== "running");
145
135
  const running = jobResults.filter(j => j.status === "running");
146
136
 
147
137
  const lines: string[] = [];
148
- if (timedOut) {
149
- lines.push("Timed out waiting for jobs to complete.\n");
150
- }
151
-
152
138
  if (completed.length > 0) {
153
139
  lines.push(`## Completed (${completed.length})\n`);
154
140
  for (const j of completed) {
@@ -173,7 +159,7 @@ export class AwaitTool implements AgentTool<typeof awaitSchema, AwaitToolDetails
173
159
 
174
160
  return {
175
161
  content: [{ type: "text", text: lines.join("\n") }],
176
- details: { jobs: jobResults, timedOut },
162
+ details: { jobs: jobResults },
177
163
  };
178
164
  }
179
165
  }
@@ -6,11 +6,14 @@
6
6
  */
7
7
  import * as os from "node:os";
8
8
  import { type Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
9
- import { pluralize } from "@oh-my-pi/pi-utils";
9
+ import { getIndentation, pluralize } from "@oh-my-pi/pi-utils";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
 
12
- export { Ellipsis, replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
12
+ export { Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
13
13
 
14
+ export function replaceTabs(text: string, file?: string): string {
15
+ return text.replaceAll("\t", getIndentation(file));
16
+ }
14
17
  // =============================================================================
15
18
  // Standardized Display Constants
16
19
  // =============================================================================
@@ -41,16 +41,18 @@ export interface TodoWriteToolDetails {
41
41
  // Schema
42
42
  // =============================================================================
43
43
 
44
- const StatusEnum = StringEnum(["pending", "in_progress", "completed", "abandoned"] as const);
44
+ const StatusEnum = StringEnum(["pending", "in_progress", "completed", "abandoned"] as const, {
45
+ description: "Task status",
46
+ });
45
47
 
46
48
  const InputTask = Type.Object({
47
- content: Type.String(),
49
+ content: Type.String({ description: "Task description" }),
48
50
  status: Type.Optional(StatusEnum),
49
- notes: Type.Optional(Type.String()),
51
+ notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
50
52
  });
51
53
 
52
54
  const InputPhase = Type.Object({
53
- name: Type.String(),
55
+ name: Type.String({ description: "Phase name" }),
54
56
  tasks: Type.Optional(Type.Array(InputTask)),
55
57
  });
56
58
 
@@ -63,21 +65,21 @@ const todoWriteSchema = Type.Object({
63
65
  }),
64
66
  Type.Object({
65
67
  op: Type.Literal("add_phase"),
66
- name: Type.String(),
68
+ name: Type.String({ description: "Phase name" }),
67
69
  tasks: Type.Optional(Type.Array(InputTask)),
68
70
  }),
69
71
  Type.Object({
70
72
  op: Type.Literal("add_task"),
71
73
  phase: Type.String({ description: "Phase ID, e.g. phase-1" }),
72
- content: Type.String(),
73
- notes: Type.Optional(Type.String()),
74
+ content: Type.String({ description: "Task description" }),
75
+ notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
74
76
  }),
75
77
  Type.Object({
76
78
  op: Type.Literal("update"),
77
79
  id: Type.String({ description: "Task ID, e.g. task-3" }),
78
80
  status: Type.Optional(StatusEnum),
79
- content: Type.Optional(Type.String()),
80
- notes: Type.Optional(Type.String()),
81
+ content: Type.Optional(Type.String({ description: "Updated task description" })),
82
+ notes: Type.Optional(Type.String({ description: "Additional context or notes" })),
81
83
  }),
82
84
  Type.Object({
83
85
  op: Type.Literal("remove_task"),