@oh-my-pi/pi-coding-agent 15.1.6 → 15.1.7
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 +17 -0
- package/dist/types/config/settings-schema.d.ts +15 -7
- package/dist/types/hashline/hash.d.ts +4 -4
- package/dist/types/hashline/recovery.d.ts +5 -0
- package/dist/types/lsp/edits.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/session/client-bridge.d.ts +1 -0
- package/dist/types/tools/find.d.ts +4 -0
- package/dist/types/tools/resolve.d.ts +5 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/package.json +7 -7
- package/src/config/settings-schema.ts +22 -7
- package/src/dap/session.ts +58 -5
- package/src/edit/modes/patch.ts +46 -0
- package/src/eval/js/context-manager.ts +11 -7
- package/src/eval/js/shared/rewrite-imports.ts +21 -9
- package/src/eval/js/shared/runtime.ts +2 -1
- package/src/hashline/hash.ts +11 -8
- package/src/hashline/parser.ts +23 -6
- package/src/hashline/recovery.ts +44 -3
- package/src/lsp/edits.ts +92 -38
- package/src/lsp/index.ts +110 -7
- package/src/lsp/utils.ts +13 -0
- package/src/modes/acp/acp-client-bridge.ts +1 -0
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/prompts/tools/bash.md +14 -0
- package/src/prompts/tools/debug.md +4 -1
- package/src/prompts/tools/find.md +10 -0
- package/src/prompts/tools/hashline.md +5 -3
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/search.md +2 -1
- package/src/prompts/tools/task.md +4 -0
- package/src/prompts/tools/todo-write.md +2 -0
- package/src/session/agent-session.ts +116 -8
- package/src/session/client-bridge.ts +1 -0
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/index.ts +33 -5
- package/src/task/render.ts +4 -1
- package/src/tools/browser/tab-supervisor.ts +23 -3
- package/src/tools/browser/tab-worker.ts +4 -2
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +19 -2
- package/src/tools/find.ts +80 -24
- package/src/tools/read.ts +3 -6
- package/src/tools/resolve.ts +54 -22
- package/src/tools/search.ts +31 -0
- package/src/tools/todo-write.ts +11 -4
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/tools-manager.ts +29 -22
- package/src/web/search/providers/codex.ts +3 -0
|
@@ -65,11 +65,13 @@ import type {
|
|
|
65
65
|
} from "@oh-my-pi/pi-ai";
|
|
66
66
|
import {
|
|
67
67
|
calculateRateLimitBackoffMs,
|
|
68
|
+
clearAnthropicFastModeFallback,
|
|
68
69
|
getSupportedEfforts,
|
|
69
70
|
isContextOverflow,
|
|
70
71
|
isUsageLimitError,
|
|
71
72
|
modelsAreEqual,
|
|
72
73
|
parseRateLimitReason,
|
|
74
|
+
resolveServiceTier,
|
|
73
75
|
streamSimple,
|
|
74
76
|
} from "@oh-my-pi/pi-ai";
|
|
75
77
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
@@ -443,6 +445,44 @@ function todoClearKey(phaseName: string, taskContent: string): string {
|
|
|
443
445
|
return `${phaseName}\u0000${taskContent}`;
|
|
444
446
|
}
|
|
445
447
|
|
|
448
|
+
const IRC_REPLY_MAX_BYTES = 4096;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Collapse degenerate IRC ephemeral replies before they hit the relay.
|
|
452
|
+
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
453
|
+
* replies); compress runs longer than 3 down to one instance + `[…N×]`, then
|
|
454
|
+
* cap at 4 KiB so a runaway reply can't flood the channel.
|
|
455
|
+
*/
|
|
456
|
+
function dedupeIrcReply(text: string): string {
|
|
457
|
+
if (!text) return text;
|
|
458
|
+
const lines = text.split("\n");
|
|
459
|
+
const out: string[] = [];
|
|
460
|
+
let i = 0;
|
|
461
|
+
while (i < lines.length) {
|
|
462
|
+
let j = i + 1;
|
|
463
|
+
while (j < lines.length && lines[j] === lines[i]) j++;
|
|
464
|
+
const runLen = j - i;
|
|
465
|
+
if (runLen > 3) {
|
|
466
|
+
out.push(lines[i], `[…${runLen}×]`);
|
|
467
|
+
} else {
|
|
468
|
+
for (let k = 0; k < runLen; k++) out.push(lines[i]);
|
|
469
|
+
}
|
|
470
|
+
i = j;
|
|
471
|
+
}
|
|
472
|
+
let result = out.join("\n");
|
|
473
|
+
if (Buffer.byteLength(result, "utf8") > IRC_REPLY_MAX_BYTES) {
|
|
474
|
+
// Trim by characters until we're under the byte budget — handles multi-byte
|
|
475
|
+
// glyphs at the boundary without splitting them.
|
|
476
|
+
const suffix = "\n[…truncated]";
|
|
477
|
+
const budget = IRC_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
|
|
478
|
+
while (Buffer.byteLength(result, "utf8") > budget) {
|
|
479
|
+
result = result.slice(0, -1);
|
|
480
|
+
}
|
|
481
|
+
result += suffix;
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
|
|
446
486
|
/**
|
|
447
487
|
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
448
488
|
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
@@ -1578,6 +1618,16 @@ export class AgentSession {
|
|
|
1578
1618
|
if (event.message.role === "assistant") {
|
|
1579
1619
|
this.#lastAssistantMessage = event.message;
|
|
1580
1620
|
const assistantMsg = event.message as AssistantMessage;
|
|
1621
|
+
const currentGrantsAnthropicPriority =
|
|
1622
|
+
this.serviceTier === "priority" || this.serviceTier === "claude-only";
|
|
1623
|
+
if (assistantMsg.disabledFeatures?.includes("priority") && currentGrantsAnthropicPriority) {
|
|
1624
|
+
this.setServiceTier(undefined);
|
|
1625
|
+
this.emitNotice(
|
|
1626
|
+
"warning",
|
|
1627
|
+
"Priority/fast mode rejected for this model; retried without it. Fast mode is now off.",
|
|
1628
|
+
"priority",
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1581
1631
|
// Resolve TTSR resume gate before checking for new deferred injections.
|
|
1582
1632
|
// Gate on #ttsrAbortPending, not stopReason: a non-TTSR abort (e.g. streaming
|
|
1583
1633
|
// edit) also produces stopReason === "aborted" but has no continuation coming.
|
|
@@ -1713,10 +1763,6 @@ export class AgentSession {
|
|
|
1713
1763
|
}
|
|
1714
1764
|
this.#resolveRetry();
|
|
1715
1765
|
|
|
1716
|
-
if (msg.stopReason === "aborted" && this.#checkpointState) {
|
|
1717
|
-
this.#checkpointState = undefined;
|
|
1718
|
-
this.#pendingRewindReport = undefined;
|
|
1719
|
-
}
|
|
1720
1766
|
const compactionTask = this.#checkCompaction(msg);
|
|
1721
1767
|
this.#trackPostPromptTask(compactionTask);
|
|
1722
1768
|
await compactionTask;
|
|
@@ -3101,6 +3147,13 @@ export class AgentSession {
|
|
|
3101
3147
|
if (!permissionIntent) {
|
|
3102
3148
|
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
3103
3149
|
}
|
|
3150
|
+
const command =
|
|
3151
|
+
target.name === "bash" && args && typeof args === "object" && !Array.isArray(args)
|
|
3152
|
+
? getStringProperty(args as Record<string, unknown>, "command")
|
|
3153
|
+
: undefined;
|
|
3154
|
+
const commandContent = command
|
|
3155
|
+
? [{ type: "content" as const, content: { type: "text" as const, text: `$ ${command}` } }]
|
|
3156
|
+
: undefined;
|
|
3104
3157
|
// Short-circuit on persisted decisions.
|
|
3105
3158
|
const persisted = this.#acpPermissionDecisions.get(permissionIntent.cacheKey);
|
|
3106
3159
|
if (persisted === "allow_always") {
|
|
@@ -3125,8 +3178,10 @@ export class AgentSession {
|
|
|
3125
3178
|
toolCallId,
|
|
3126
3179
|
toolName: target.name,
|
|
3127
3180
|
title: permissionIntent.title,
|
|
3181
|
+
...(target.name === "bash" ? { kind: "execute" } : {}),
|
|
3128
3182
|
status: "pending",
|
|
3129
3183
|
rawInput: args,
|
|
3184
|
+
...(commandContent ? { content: commandContent } : {}),
|
|
3130
3185
|
locations: extractPermissionLocations(
|
|
3131
3186
|
args,
|
|
3132
3187
|
this.sessionManager.getCwd(),
|
|
@@ -4599,7 +4654,12 @@ export class AgentSession {
|
|
|
4599
4654
|
|
|
4600
4655
|
/** Schedule auto-removal of completed/abandoned tasks after a delay. */
|
|
4601
4656
|
#scheduleTodoAutoClear(phases: TodoPhase[]): void {
|
|
4602
|
-
|
|
4657
|
+
// Default bumped from 60s to 30 min: the prior 60s splice mutated canonical
|
|
4658
|
+
// state mid-turn, so the model observed phase totals shrinking ("6 → 5")
|
|
4659
|
+
// between tool calls. Surviving the turn matches user expectations; a
|
|
4660
|
+
// render-time filter in the UI consumer would be cleaner but lives in a
|
|
4661
|
+
// different package and is out of scope for this fix.
|
|
4662
|
+
const delaySec = this.settings.get("tasks.todoClearDelay") ?? 1800;
|
|
4603
4663
|
if (delaySec < 0) return; // "Never" — no auto-clear
|
|
4604
4664
|
const delayMs = delaySec * 1000;
|
|
4605
4665
|
const doneKeys = new Set<string>();
|
|
@@ -5110,17 +5170,50 @@ export class AgentSession {
|
|
|
5110
5170
|
return nextLevel;
|
|
5111
5171
|
}
|
|
5112
5172
|
|
|
5173
|
+
/**
|
|
5174
|
+
* True when *any* fast-mode-granting service tier is configured, regardless
|
|
5175
|
+
* of whether the active model's provider actually realizes it. Used by the
|
|
5176
|
+
* toggle (`/fast on|off`) so re-toggling a scoped tier (`openai-only`,
|
|
5177
|
+
* `claude-only`) doesn't silently broaden it to unscoped `priority`.
|
|
5178
|
+
*
|
|
5179
|
+
* For "is fast mode actually applied to the next request?" use
|
|
5180
|
+
* {@link isFastModeActive} instead — that one respects the model's provider.
|
|
5181
|
+
*/
|
|
5113
5182
|
isFastModeEnabled(): boolean {
|
|
5114
|
-
return
|
|
5183
|
+
return (
|
|
5184
|
+
this.serviceTier === "priority" || this.serviceTier === "claude-only" || this.serviceTier === "openai-only"
|
|
5185
|
+
);
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
/**
|
|
5189
|
+
* True when the configured `serviceTier` resolves to `"priority"` for the
|
|
5190
|
+
* *currently selected model's provider*. Returns false for scoped tiers
|
|
5191
|
+
* that don't match (e.g. `"openai-only"` on an anthropic model) and when
|
|
5192
|
+
* no model is selected.
|
|
5193
|
+
*/
|
|
5194
|
+
isFastModeActive(): boolean {
|
|
5195
|
+
return resolveServiceTier(this.serviceTier, this.model?.provider) === "priority";
|
|
5115
5196
|
}
|
|
5116
5197
|
|
|
5117
5198
|
setServiceTier(serviceTier: ServiceTier | undefined): void {
|
|
5118
5199
|
if (this.serviceTier === serviceTier) return;
|
|
5200
|
+
// Re-arming priority on Anthropic? Clear the per-session auto-fallback
|
|
5201
|
+
// sticky disable so the next request actually carries `speed: "fast"`
|
|
5202
|
+
// again. Without this, `/fast on` (or user switching to a tier that
|
|
5203
|
+
// grants anthropic priority) after an auto-disable is a silent no-op
|
|
5204
|
+
// and the warning notice fires every turn.
|
|
5205
|
+
if (serviceTier === "priority" || serviceTier === "claude-only") {
|
|
5206
|
+
clearAnthropicFastModeFallback(this.#providerSessionState);
|
|
5207
|
+
}
|
|
5119
5208
|
this.agent.serviceTier = serviceTier;
|
|
5120
5209
|
this.sessionManager.appendServiceTierChange(serviceTier ?? null);
|
|
5121
5210
|
}
|
|
5122
5211
|
|
|
5123
5212
|
setFastMode(enabled: boolean): void {
|
|
5213
|
+
if (enabled && this.isFastModeEnabled()) {
|
|
5214
|
+
// Already on under any scope — keep the user's scoped value.
|
|
5215
|
+
return;
|
|
5216
|
+
}
|
|
5124
5217
|
this.setServiceTier(enabled ? "priority" : undefined);
|
|
5125
5218
|
}
|
|
5126
5219
|
|
|
@@ -7574,6 +7667,11 @@ export class AgentSession {
|
|
|
7574
7667
|
const context: Context = {
|
|
7575
7668
|
systemPrompt: this.systemPrompt,
|
|
7576
7669
|
messages: llmMessages,
|
|
7670
|
+
// Empty tools array: with toolChoice="none" some encoders still serialize the
|
|
7671
|
+
// recipient's tool catalog and the model leaks raw call markup
|
|
7672
|
+
// (<function_calls>, DSML envelopes) into IRC replies. Stripping tools here
|
|
7673
|
+
// removes the surface entirely.
|
|
7674
|
+
tools: [],
|
|
7577
7675
|
};
|
|
7578
7676
|
const options = this.prepareSimpleStreamOptions(
|
|
7579
7677
|
{
|
|
@@ -7609,7 +7707,7 @@ export class AgentSession {
|
|
|
7609
7707
|
if (!assistantMessage) {
|
|
7610
7708
|
throw new Error("Ephemeral turn ended without a final message");
|
|
7611
7709
|
}
|
|
7612
|
-
return { replyText: replyText.trim(), assistantMessage };
|
|
7710
|
+
return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
|
|
7613
7711
|
}
|
|
7614
7712
|
|
|
7615
7713
|
/**
|
|
@@ -7622,14 +7720,24 @@ export class AgentSession {
|
|
|
7622
7720
|
const messages = [...this.messages];
|
|
7623
7721
|
const streaming = this.agent.state.streamMessage;
|
|
7624
7722
|
if (streaming && streaming.role === "assistant") {
|
|
7723
|
+
const preservedBlocks: AssistantMessage["content"] = [];
|
|
7724
|
+
// Preserve thinking blocks: DeepSeek-class encoders replay them as
|
|
7725
|
+
// `reasoning_content` and reject the request (HTTP 400) when the field
|
|
7726
|
+
// goes missing on a turn that previously emitted thinking.
|
|
7727
|
+
for (const c of streaming.content) {
|
|
7728
|
+
if (c.type === "thinking") preservedBlocks.push(c);
|
|
7729
|
+
}
|
|
7625
7730
|
const streamingText = streaming.content
|
|
7626
7731
|
.filter((c): c is TextContent => c.type === "text")
|
|
7627
7732
|
.map(c => c.text)
|
|
7628
7733
|
.join("");
|
|
7629
7734
|
if (streamingText) {
|
|
7735
|
+
preservedBlocks.push({ type: "text", text: streamingText });
|
|
7736
|
+
}
|
|
7737
|
+
if (preservedBlocks.length > 0) {
|
|
7630
7738
|
const normalized: AssistantMessage = {
|
|
7631
7739
|
...streaming,
|
|
7632
|
-
content:
|
|
7740
|
+
content: preservedBlocks,
|
|
7633
7741
|
};
|
|
7634
7742
|
const lastMessage = messages.at(-1);
|
|
7635
7743
|
if (lastMessage?.role === "assistant") {
|
|
@@ -147,7 +147,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
name: "fast",
|
|
150
|
-
description: "Toggle
|
|
150
|
+
description: "Toggle priority service tier (OpenAI service_tier=priority, Anthropic speed=fast)",
|
|
151
151
|
acpDescription: "Toggle fast mode",
|
|
152
152
|
acpInputHint: "[on|off|status]",
|
|
153
153
|
subcommands: [
|
package/src/task/index.ts
CHANGED
|
@@ -140,11 +140,25 @@ function renderDescription(
|
|
|
140
140
|
disabledAgents: string[],
|
|
141
141
|
simpleMode: TaskSimpleMode,
|
|
142
142
|
ircEnabled: boolean,
|
|
143
|
+
parentSpawns: string,
|
|
143
144
|
): string {
|
|
144
|
-
const
|
|
145
|
+
const spawningDisabled = parentSpawns === "";
|
|
146
|
+
let filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
|
|
147
|
+
if (spawningDisabled) {
|
|
148
|
+
filteredAgents = [];
|
|
149
|
+
} else if (parentSpawns !== "*") {
|
|
150
|
+
const allowed = new Set(
|
|
151
|
+
parentSpawns
|
|
152
|
+
.split(",")
|
|
153
|
+
.map(s => s.trim())
|
|
154
|
+
.filter(Boolean),
|
|
155
|
+
);
|
|
156
|
+
filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
|
|
157
|
+
}
|
|
145
158
|
const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
146
159
|
return prompt.render(taskDescriptionTemplate, {
|
|
147
160
|
agents: filteredAgents,
|
|
161
|
+
spawningDisabled,
|
|
148
162
|
MAX_CONCURRENCY: maxConcurrency,
|
|
149
163
|
isolationEnabled,
|
|
150
164
|
asyncEnabled,
|
|
@@ -230,6 +244,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
230
244
|
disabledAgents,
|
|
231
245
|
this.#getTaskSimpleMode(),
|
|
232
246
|
this.session.settings.get("irc.enabled") === true,
|
|
247
|
+
this.session.getSessionSpawns() ?? "*",
|
|
233
248
|
);
|
|
234
249
|
}
|
|
235
250
|
private constructor(
|
|
@@ -493,10 +508,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
493
508
|
taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
|
|
494
509
|
}
|
|
495
510
|
const startedListing = startedJobs
|
|
496
|
-
.map(({ taskId }) => {
|
|
511
|
+
.map(({ taskId, jobId }) => {
|
|
497
512
|
const id = taskIdByItemId.get(taskId) ?? taskId;
|
|
498
513
|
const desc = progressByTaskId.get(taskId)?.description;
|
|
499
|
-
|
|
514
|
+
const prefix = `- \`${id}\` (job \`${jobId}\`)`;
|
|
515
|
+
return desc ? `${prefix} — ${desc}` : prefix;
|
|
500
516
|
})
|
|
501
517
|
.join("\n");
|
|
502
518
|
const coordinationHint = ircEnabled
|
|
@@ -1075,6 +1091,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1075
1091
|
|
|
1076
1092
|
let mergeSummary = "";
|
|
1077
1093
|
let changesApplied: boolean | null = null;
|
|
1094
|
+
let hadAnyChanges = false;
|
|
1078
1095
|
let mergedBranchesForNestedPatches: Set<string> | null = null;
|
|
1079
1096
|
if (isIsolated && repoRoot) {
|
|
1080
1097
|
try {
|
|
@@ -1086,13 +1103,18 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1086
1103
|
|
|
1087
1104
|
if (branchEntries.length === 0) {
|
|
1088
1105
|
changesApplied = true;
|
|
1106
|
+
hadAnyChanges = false;
|
|
1107
|
+
mergeSummary = "\n\nNo changes to apply.";
|
|
1089
1108
|
} else {
|
|
1090
1109
|
const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
|
|
1091
1110
|
mergedBranchesForNestedPatches = new Set(mergeResult.merged);
|
|
1092
1111
|
changesApplied = mergeResult.failed.length === 0;
|
|
1112
|
+
hadAnyChanges = changesApplied && mergeResult.merged.length > 0;
|
|
1093
1113
|
|
|
1094
1114
|
if (changesApplied) {
|
|
1095
|
-
mergeSummary =
|
|
1115
|
+
mergeSummary = hadAnyChanges
|
|
1116
|
+
? `\n\nMerged ${mergeResult.merged.length} branch${mergeResult.merged.length === 1 ? "" : "es"}: ${mergeResult.merged.join(", ")}`
|
|
1117
|
+
: "\n\nNo changes to apply.";
|
|
1096
1118
|
} else {
|
|
1097
1119
|
const mergedPart =
|
|
1098
1120
|
mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
|
|
@@ -1113,6 +1135,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1113
1135
|
const missingPatch = results.some(result => !result.patchPath);
|
|
1114
1136
|
if (missingPatch) {
|
|
1115
1137
|
changesApplied = false;
|
|
1138
|
+
hadAnyChanges = false;
|
|
1116
1139
|
} else {
|
|
1117
1140
|
const patchStats = await Promise.all(
|
|
1118
1141
|
patchesInOrder.map(async patchPath => ({
|
|
@@ -1123,6 +1146,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1123
1146
|
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1124
1147
|
if (nonEmptyPatches.length === 0) {
|
|
1125
1148
|
changesApplied = true;
|
|
1149
|
+
hadAnyChanges = false;
|
|
1126
1150
|
} else {
|
|
1127
1151
|
const patchTexts = await Promise.all(
|
|
1128
1152
|
nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
|
|
@@ -1132,13 +1156,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1132
1156
|
.join("");
|
|
1133
1157
|
if (!combinedPatch.trim()) {
|
|
1134
1158
|
changesApplied = true;
|
|
1159
|
+
hadAnyChanges = false;
|
|
1135
1160
|
} else {
|
|
1136
1161
|
changesApplied = await git.patch.canApplyText(repoRoot, combinedPatch);
|
|
1137
1162
|
if (changesApplied) {
|
|
1138
1163
|
try {
|
|
1139
1164
|
await git.patch.applyText(repoRoot, combinedPatch);
|
|
1165
|
+
hadAnyChanges = true;
|
|
1140
1166
|
} catch {
|
|
1141
1167
|
changesApplied = false;
|
|
1168
|
+
hadAnyChanges = false;
|
|
1142
1169
|
}
|
|
1143
1170
|
}
|
|
1144
1171
|
}
|
|
@@ -1146,7 +1173,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1146
1173
|
}
|
|
1147
1174
|
|
|
1148
1175
|
if (changesApplied) {
|
|
1149
|
-
mergeSummary = "\n\nApplied patches: yes";
|
|
1176
|
+
mergeSummary = hadAnyChanges ? "\n\nApplied patches: yes" : "\n\nNo changes to apply.";
|
|
1150
1177
|
} else {
|
|
1151
1178
|
const notification =
|
|
1152
1179
|
"<system-notification>Patches were not applied and must be handled manually.</system-notification>";
|
|
@@ -1160,6 +1187,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1160
1187
|
} catch (mergeErr) {
|
|
1161
1188
|
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1162
1189
|
changesApplied = false;
|
|
1190
|
+
hadAnyChanges = false;
|
|
1163
1191
|
mergeSummary = `\n\n<system-notification>Merge phase failed: ${msg}\nTask outputs are preserved but changes were not applied.</system-notification>`;
|
|
1164
1192
|
}
|
|
1165
1193
|
}
|
package/src/task/render.ts
CHANGED
|
@@ -996,7 +996,10 @@ export function renderResult(
|
|
|
996
996
|
if (fallbackText.trim()) {
|
|
997
997
|
const summaryLines = fallbackText.split("\n");
|
|
998
998
|
const markerIndex = summaryLines.findIndex(
|
|
999
|
-
line =>
|
|
999
|
+
line =>
|
|
1000
|
+
line.includes("<system-notification>") ||
|
|
1001
|
+
line.startsWith("Applied patches:") ||
|
|
1002
|
+
line.startsWith("No changes to apply."),
|
|
1000
1003
|
);
|
|
1001
1004
|
if (markerIndex >= 0) {
|
|
1002
1005
|
const extra = summaryLines.slice(markerIndex);
|
|
@@ -120,14 +120,34 @@ export async function acquireTab(
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const initPayload = await buildInitPayload(browser, opts);
|
|
123
|
-
|
|
123
|
+
let worker = await spawnTabWorker();
|
|
124
124
|
let info: ReadyInfo;
|
|
125
125
|
try {
|
|
126
126
|
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
127
127
|
} catch (error) {
|
|
128
|
+
// `BuildMessage`-class failures arrive asynchronously via the worker's `error` event,
|
|
129
|
+
// after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
|
|
130
|
+
// the inline worker here so module-resolution failures don't poison every tab open.
|
|
128
131
|
await worker.terminate().catch(() => undefined);
|
|
129
|
-
if (
|
|
130
|
-
|
|
132
|
+
if (worker.mode === "inline") {
|
|
133
|
+
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
logger.warn("Tab worker init failed; retrying with inline tab worker (no sync-loop guard)", {
|
|
137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
worker = await spawnInlineWorker();
|
|
140
|
+
try {
|
|
141
|
+
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
|
|
142
|
+
} catch (inlineError) {
|
|
143
|
+
await worker.terminate().catch(() => undefined);
|
|
144
|
+
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
145
|
+
const finalError = new ToolError(
|
|
146
|
+
`Failed to start browser tab worker (inline fallback also failed): ${inlineError instanceof Error ? inlineError.message : String(inlineError)}`,
|
|
147
|
+
);
|
|
148
|
+
(finalError as { cause?: unknown }).cause = error;
|
|
149
|
+
throw finalError;
|
|
150
|
+
}
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
holdBrowser(browser);
|
|
@@ -459,7 +459,8 @@ export class WorkerCore {
|
|
|
459
459
|
if (payload.dialogs) this.#applyDialogPolicy(payload.dialogs);
|
|
460
460
|
if (payload.url) {
|
|
461
461
|
await this.#page.goto(payload.url, {
|
|
462
|
-
|
|
462
|
+
// Default to "load" because dev servers with HMR/WS never reach networkidle.
|
|
463
|
+
waitUntil: payload.waitUntil ?? "load",
|
|
463
464
|
timeout: payload.timeoutMs,
|
|
464
465
|
});
|
|
465
466
|
}
|
|
@@ -667,7 +668,8 @@ export class WorkerCore {
|
|
|
667
668
|
goto: async (url, opts) => {
|
|
668
669
|
this.#clearElementCache();
|
|
669
670
|
await untilAborted(signal, () =>
|
|
670
|
-
|
|
671
|
+
// Default to "load" because dev servers with HMR/WS never reach networkidle.
|
|
672
|
+
page.goto(url, { waitUntil: opts?.waitUntil ?? "load", timeout: timeoutMs }),
|
|
671
673
|
);
|
|
672
674
|
},
|
|
673
675
|
observe: opts => this.#collectObservation({ ...opts, signal }),
|
package/src/tools/browser.ts
CHANGED
|
@@ -45,7 +45,7 @@ const browserSchema = z.object({
|
|
|
45
45
|
.describe("auto-handle dialogs")
|
|
46
46
|
.optional(),
|
|
47
47
|
code: z.string().describe("js body to run in tab").optional(),
|
|
48
|
-
timeout: z.number().default(30).describe("timeout in seconds").optional(),
|
|
48
|
+
timeout: z.number().default(30).describe("timeout in seconds (default 30, max 300)").optional(),
|
|
49
49
|
all: z.boolean().describe("close every tab").optional(),
|
|
50
50
|
kill: z.boolean().describe("also kill spawned-app browsers").optional(),
|
|
51
51
|
});
|
package/src/tools/debug.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import type {
|
|
2
3
|
AgentTool,
|
|
3
4
|
AgentToolContext,
|
|
@@ -6,7 +7,7 @@ import type {
|
|
|
6
7
|
RenderResultOptions,
|
|
7
8
|
} from "@oh-my-pi/pi-agent-core";
|
|
8
9
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
10
11
|
import * as z from "zod/v4";
|
|
11
12
|
import {
|
|
12
13
|
type DapBreakpointRecord,
|
|
@@ -37,7 +38,7 @@ import { renderStatusLine } from "../tui";
|
|
|
37
38
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
38
39
|
import type { ToolSession } from ".";
|
|
39
40
|
import type { OutputMeta } from "./output-meta";
|
|
40
|
-
import { resolveToCwd } from "./path-utils";
|
|
41
|
+
import { formatPathRelativeToCwd, resolveToCwd } from "./path-utils";
|
|
41
42
|
import {
|
|
42
43
|
formatExpandHint,
|
|
43
44
|
formatStatusIcon,
|
|
@@ -469,6 +470,21 @@ function getConfiguredAdapters(cwd: string): string {
|
|
|
469
470
|
const adapters = getAvailableAdapters(cwd).map(adapter => adapter.name);
|
|
470
471
|
return adapters.length > 0 ? adapters.join(", ") : "none";
|
|
471
472
|
}
|
|
473
|
+
async function validateLaunchProgram(program: string, cwd: string): Promise<void> {
|
|
474
|
+
let isDirectory: boolean;
|
|
475
|
+
try {
|
|
476
|
+
isDirectory = (await fs.stat(program)).isDirectory();
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (isEnoent(error)) return;
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
if (!isDirectory) return;
|
|
482
|
+
|
|
483
|
+
const displayPath = formatPathRelativeToCwd(program, cwd, { trailingSlash: true });
|
|
484
|
+
throw new ToolError(
|
|
485
|
+
`launch program resolves to a directory: ${displayPath}. Pass an executable file path, or for Python use adapter "debugpy" with program set to the .py file.`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
472
488
|
|
|
473
489
|
interface DebugRenderArgs extends Partial<DebugParams> {}
|
|
474
490
|
|
|
@@ -628,6 +644,7 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
628
644
|
}
|
|
629
645
|
const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
|
|
630
646
|
const program = resolveToCwd(params.program, commandCwd);
|
|
647
|
+
await validateLaunchProgram(program, commandCwd);
|
|
631
648
|
const adapter = selectLaunchAdapter(program, commandCwd, params.adapter);
|
|
632
649
|
if (!adapter) {
|
|
633
650
|
throw new ToolError(
|