@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 +12 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +43 -1
- package/src/capability/tool.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +17 -1
- package/src/discovery/builtin.ts +16 -5
- package/src/discovery/claude-plugins.ts +1 -0
- package/src/discovery/claude.ts +2 -2
- package/src/modes/components/diff.ts +49 -19
- package/src/prompts/system/custom-system-prompt.md +10 -0
- package/src/tools/await-tool.ts +12 -26
- package/src/tools/render-utils.ts +5 -2
- package/src/tools/todo-write.ts +11 -9
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.
|
|
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.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.
|
|
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",
|
package/src/async/job-manager.ts
CHANGED
|
@@ -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.#
|
|
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,
|
package/src/capability/tool.ts
CHANGED
|
@@ -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
|
|
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,
|
package/src/config/settings.ts
CHANGED
|
@@ -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
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -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:
|
|
665
|
+
name: toolName,
|
|
661
666
|
path,
|
|
662
|
-
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:
|
|
680
|
+
name: toolName,
|
|
671
681
|
path,
|
|
672
|
-
description
|
|
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:
|
|
729
|
+
description: `${entryName} custom tool`,
|
|
719
730
|
level,
|
|
720
731
|
_source: createSourceMeta(PROVIDER_ID, indexPath, level),
|
|
721
732
|
});
|
package/src/discovery/claude.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
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 +
|
|
27
|
-
visible +=
|
|
28
|
-
pos +=
|
|
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
|
|
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,
|
|
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(
|
|
162
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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}}
|
package/src/tools/await-tool.ts
CHANGED
|
@@ -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
|
-
|
|
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: []
|
|
58
|
+
details: { jobs: [] },
|
|
65
59
|
};
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
const
|
|
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: []
|
|
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(
|
|
82
|
+
return this.#buildResult(manager, jobsToWatch);
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
// Block until at least one running job finishes or
|
|
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(
|
|
103
|
+
return this.#buildResult(manager, jobsToWatch);
|
|
112
104
|
}
|
|
113
105
|
|
|
114
|
-
|
|
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
|
|
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,
|
|
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
|
// =============================================================================
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -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"),
|