@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2
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 +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +168 -48
- package/src/modes/controllers/event-controller.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- package/src/web/search/types.ts +6 -1
package/src/tools/bash.ts
CHANGED
|
@@ -177,6 +177,8 @@ export interface BashToolDetails {
|
|
|
177
177
|
wallTimeMs?: number;
|
|
178
178
|
/** Exit code of a command that ran to completion but failed (non-zero). */
|
|
179
179
|
exitCode?: number;
|
|
180
|
+
/** True when the command was killed by its timeout deadline (not a failure). */
|
|
181
|
+
timedOut?: boolean;
|
|
180
182
|
terminalId?: string;
|
|
181
183
|
async?: {
|
|
182
184
|
state: "running" | "completed" | "failed";
|
|
@@ -208,10 +210,6 @@ function normalizeResultOutput(result: BashResult | BashInteractiveResult): stri
|
|
|
208
210
|
return result.output || "";
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
function isInteractiveResult(result: BashResult | BashInteractiveResult): result is BashInteractiveResult {
|
|
212
|
-
return "timedOut" in result;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
213
|
function normalizeBashEnv(env: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
216
214
|
if (!env || Object.keys(env).length === 0) return undefined;
|
|
217
215
|
const normalized: Record<string, string> = {};
|
|
@@ -439,12 +437,15 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
439
437
|
}
|
|
440
438
|
|
|
441
439
|
/**
|
|
442
|
-
* Throw for outcomes that are *not* a completed command: user
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
440
|
+
* Throw for outcomes that are *not* a completed command: user aborts and a
|
|
441
|
+
* missing exit status. Timeouts are handled separately by
|
|
442
|
+
* #buildCompletedResult, which returns a non-throwing error result with
|
|
443
|
+
* details.timedOut=true so the renderer can show a warning border. The
|
|
444
|
+
* foreground and bridge callers plus the async job manager rely on these
|
|
445
|
+
* throwing so cancellations surface as aborts and jobs are recorded as
|
|
446
|
+
* failed. A definite non-zero exit is a completed command that failed;
|
|
447
|
+
* #buildCompletedResult surfaces it as an error *result* (carrying
|
|
448
|
+
* execution details) rather than a throw.
|
|
448
449
|
*/
|
|
449
450
|
#throwIfUnfinished(
|
|
450
451
|
result: BashResult | BashInteractiveResult,
|
|
@@ -452,13 +453,14 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
452
453
|
outputText: string,
|
|
453
454
|
): void {
|
|
454
455
|
if (result.cancelled) {
|
|
455
|
-
//
|
|
456
|
-
// the sink; PTY/bridge
|
|
456
|
+
// Local executor output already carries a leading `[Command cancelled]`
|
|
457
|
+
// notice from the sink; PTY/bridge output does not, so annotate only
|
|
458
|
+
// the latter.
|
|
457
459
|
const out = normalizeResultOutput(result);
|
|
458
|
-
const annotated =
|
|
460
|
+
const annotated = out.startsWith("[Command cancelled]") ? out : out ? `${out}\n\n[Command aborted]` : out;
|
|
459
461
|
throw new ToolError(annotated || "Command aborted");
|
|
460
462
|
}
|
|
461
|
-
if (
|
|
463
|
+
if (result.timedOut === true) {
|
|
462
464
|
const out = normalizeResultOutput(result);
|
|
463
465
|
const message =
|
|
464
466
|
timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
|
|
@@ -496,8 +498,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
496
498
|
if (failedExit) outputLines.push("", formatExitCodeNotice(exitCode));
|
|
497
499
|
const outputText = outputLines.join("\n");
|
|
498
500
|
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
+
// Timeouts are not failures — the command ran its course. Return an error
|
|
502
|
+
// result (isError=true for the model) but flag timedOut so the renderer
|
|
503
|
+
// uses a warning border instead of error red. Both interactive and
|
|
504
|
+
// non-interactive results carry an explicit `timedOut` field from the
|
|
505
|
+
// executor/PTY layer.
|
|
506
|
+
const isTimeout = result.timedOut === true;
|
|
501
507
|
|
|
502
508
|
const details: BashToolDetails = {};
|
|
503
509
|
if (timeoutSec === undefined) {
|
|
@@ -517,6 +523,29 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
517
523
|
if (failedExit) {
|
|
518
524
|
details.exitCode = exitCode;
|
|
519
525
|
}
|
|
526
|
+
|
|
527
|
+
if (isTimeout) {
|
|
528
|
+
details.timedOut = true;
|
|
529
|
+
const message =
|
|
530
|
+
timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
|
|
531
|
+
// executeBash has already emitted this leading sink notice. PTY output
|
|
532
|
+
// has not, so provide the LLM-facing annotation exactly once.
|
|
533
|
+
if (!normalizeResultOutput(result).startsWith(`[${message}]\n`)) {
|
|
534
|
+
outputLines.push("", `[${message}]`);
|
|
535
|
+
}
|
|
536
|
+
const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"), {
|
|
537
|
+
saveArtifact: full => saveBashOriginalArtifact(this.session, full),
|
|
538
|
+
});
|
|
539
|
+
return toolResult(details)
|
|
540
|
+
.text(timeoutOutputText)
|
|
541
|
+
.truncationFromSummary(result, { direction: "tail" })
|
|
542
|
+
.error()
|
|
543
|
+
.done();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Non-timeout cancellations and missing exit status still propagate as thrown errors.
|
|
547
|
+
this.#throwIfUnfinished(result, timeoutSec, outputText);
|
|
548
|
+
|
|
520
549
|
// Final defense at the tool-result boundary: no bash path (client bridge,
|
|
521
550
|
// head-retention spill, minimizer miss) may emit more than
|
|
522
551
|
// ~DEFAULT_MAX_BYTES inline. No-op for already-bounded output.
|
|
@@ -992,12 +1021,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
992
1021
|
outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
993
1022
|
outputBytes: current.output.length,
|
|
994
1023
|
};
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
notices: pendingNotices,
|
|
998
|
-
terminalId: handle.terminalId,
|
|
999
|
-
wallTimeMs: performance.now() - bridgeWallTimeStart,
|
|
1000
|
-
});
|
|
1024
|
+
this.#throwIfUnfinished(timedOutResult, timeoutSec, this.#formatResultOutput(timedOutResult));
|
|
1025
|
+
throw new ToolError("Command timed out");
|
|
1001
1026
|
}
|
|
1002
1027
|
|
|
1003
1028
|
if (raced.kind === "exit") {
|
|
@@ -1102,20 +1127,27 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
1102
1127
|
});
|
|
1103
1128
|
const wallTimeMs = performance.now() - wallTimeStart;
|
|
1104
1129
|
if (result.cancelled) {
|
|
1105
|
-
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1130
|
+
// A cancelled result is either a timeout (the command's deadline fired)
|
|
1131
|
+
// or a user/system abort. Timeouts are handled by #buildCompletedResult
|
|
1132
|
+
// which returns a non-throwing error result with details.timedOut=true
|
|
1133
|
+
// so the renderer can show a warning border instead of error red.
|
|
1134
|
+
// Both interactive and non-interactive results carry an explicit
|
|
1135
|
+
// `timedOut` field from the executor/PTY layer.
|
|
1136
|
+
const isTimeout = result.timedOut === true;
|
|
1137
|
+
if (!isTimeout) {
|
|
1138
|
+
const out = normalizeResultOutput(result);
|
|
1139
|
+
// The local executor already prepends `[Command cancelled]`; PTY
|
|
1140
|
+
// output does not, so preserve one cancellation notice in either case.
|
|
1141
|
+
const message = out.startsWith("[Command cancelled]")
|
|
1142
|
+
? out
|
|
1143
|
+
: out
|
|
1144
|
+
? `${out}\n\n[Command aborted]`
|
|
1145
|
+
: "Command aborted";
|
|
1146
|
+
if (signal?.aborted) {
|
|
1147
|
+
throw new ToolAbortError(message);
|
|
1148
|
+
}
|
|
1149
|
+
throw new ToolError(message);
|
|
1111
1150
|
}
|
|
1112
|
-
throw new ToolError(message);
|
|
1113
|
-
}
|
|
1114
|
-
if (isInteractiveResult(result) && result.timedOut) {
|
|
1115
|
-
const out = normalizeResultOutput(result);
|
|
1116
|
-
const message =
|
|
1117
|
-
timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
|
|
1118
|
-
throw new ToolError(out ? `${out}\n\n[${message}]` : message);
|
|
1119
1151
|
}
|
|
1120
1152
|
return this.#buildCompletedResult(result, timeoutSec, {
|
|
1121
1153
|
requestedTimeoutSec,
|
|
@@ -1253,6 +1285,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1253
1285
|
const isError = result.isError === true;
|
|
1254
1286
|
const isPartial = options.isPartial === true;
|
|
1255
1287
|
const success = !isPartial && !isError;
|
|
1288
|
+
const details = result.details;
|
|
1289
|
+
const isTimeout = details?.timedOut === true;
|
|
1256
1290
|
const header =
|
|
1257
1291
|
config.showHeader === false
|
|
1258
1292
|
? undefined
|
|
@@ -1263,12 +1297,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1263
1297
|
title: config.resolveTitle(args, options),
|
|
1264
1298
|
}
|
|
1265
1299
|
: {
|
|
1266
|
-
icon: isPartial ? "pending" : "error",
|
|
1300
|
+
icon: isPartial ? "pending" : isTimeout ? "warning" : "error",
|
|
1267
1301
|
title: config.resolveTitle(args, options),
|
|
1268
1302
|
},
|
|
1269
1303
|
uiTheme,
|
|
1270
1304
|
);
|
|
1271
|
-
const details = result.details;
|
|
1272
1305
|
const outputBlock = new CachedOutputBlock();
|
|
1273
1306
|
|
|
1274
1307
|
// Per-instance cache for the expensive inner lines computation. Mirrors
|
|
@@ -1408,7 +1441,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1408
1441
|
const framed = outputBlock.render(
|
|
1409
1442
|
{
|
|
1410
1443
|
header,
|
|
1411
|
-
state: isPartial ? "pending" : isError ? "error" : "success",
|
|
1444
|
+
state: isPartial ? "pending" : isError ? (isTimeout ? "warning" : "error") : "success",
|
|
1412
1445
|
sections: [
|
|
1413
1446
|
{
|
|
1414
1447
|
// Viewport-sized tail window in every state — streaming and final
|
|
@@ -47,6 +47,13 @@ export interface CmuxBrowserHandle extends BrowserHandleCommon {
|
|
|
47
47
|
|
|
48
48
|
export type BrowserHandle = PuppeteerBrowserHandle | CmuxBrowserHandle;
|
|
49
49
|
|
|
50
|
+
/** Controls bounded browser-handle teardown and identifies the owning resource in timeout diagnostics. */
|
|
51
|
+
export interface ReleaseBrowserOptions {
|
|
52
|
+
kill: boolean;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
resource?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
const browsers = new Map<string, BrowserHandle>();
|
|
51
58
|
|
|
52
59
|
function browserKey(kind: BrowserKind): string {
|
|
@@ -221,7 +228,7 @@ export function holdBrowser(handle: BrowserHandle): void {
|
|
|
221
228
|
handle.refCount++;
|
|
222
229
|
}
|
|
223
230
|
|
|
224
|
-
export async function releaseBrowser(handle: BrowserHandle, opts:
|
|
231
|
+
export async function releaseBrowser(handle: BrowserHandle, opts: ReleaseBrowserOptions): Promise<void> {
|
|
225
232
|
handle.refCount = Math.max(0, handle.refCount - 1);
|
|
226
233
|
if (handle.refCount === 0) {
|
|
227
234
|
// Only evict if the registry still points at THIS handle. After a disconnect,
|
|
@@ -232,7 +239,7 @@ export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolea
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
async function disposeBrowserHandle(handle: BrowserHandle, opts:
|
|
242
|
+
async function disposeBrowserHandle(handle: BrowserHandle, opts: ReleaseBrowserOptions): Promise<void> {
|
|
236
243
|
if ("client" in handle) {
|
|
237
244
|
handle.client.close();
|
|
238
245
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPuppeteerDir, logger, postmortem, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { getPuppeteerDir, logger, postmortem, Snowflake, withTimeout, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Page, Target } from "puppeteer-core";
|
|
3
3
|
import { callSessionTool } from "../../eval/js/tool-bridge";
|
|
4
4
|
import { webpExclusionForModel } from "../../utils/image-loading";
|
|
@@ -123,6 +123,8 @@ export interface RunInTabOptions {
|
|
|
123
123
|
|
|
124
124
|
export interface ReleaseTabOptions {
|
|
125
125
|
kill?: boolean;
|
|
126
|
+
/** Maximum time for each asynchronous cleanup resource before close fails with diagnostics. */
|
|
127
|
+
timeoutMs?: number;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
const tabs = new Map<string, TabSession>();
|
|
@@ -135,6 +137,22 @@ const GRACE_MS = 750;
|
|
|
135
137
|
// mapped to the kill reason. Lets the next `run` on that name explain WHY the tab
|
|
136
138
|
// vanished instead of a bare "not alive". Cleared when the name is opened again.
|
|
137
139
|
const killedTabs = new Map<string, string>();
|
|
140
|
+
const DEFAULT_TAB_CLOSE_TIMEOUT_MS = 5_000;
|
|
141
|
+
|
|
142
|
+
async function waitForTabCleanup<T>(
|
|
143
|
+
tab: TabSession,
|
|
144
|
+
timeoutMs: number,
|
|
145
|
+
pendingResource: string,
|
|
146
|
+
promise: Promise<T>,
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
const message = `Timed out after ${timeoutMs}ms closing ${tab.kindTag} browser tab ${JSON.stringify(tab.name)}; pending resource: ${pendingResource}`;
|
|
149
|
+
try {
|
|
150
|
+
return await withTimeout(promise, timeoutMs, message);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof Error && error.message === message) throw new ToolError(message);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
138
156
|
|
|
139
157
|
export function getTab(name: string): TabSession | undefined {
|
|
140
158
|
return tabs.get(name);
|
|
@@ -519,26 +537,42 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
519
537
|
pending.reject(closeError);
|
|
520
538
|
}
|
|
521
539
|
tab.pending.clear();
|
|
540
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TAB_CLOSE_TIMEOUT_MS;
|
|
522
541
|
if (tab.backend === "cmux") {
|
|
523
|
-
let
|
|
542
|
+
let closeError: unknown;
|
|
524
543
|
if (wasAlive && tab.cmuxOwnsSurface) {
|
|
525
544
|
try {
|
|
526
|
-
await
|
|
545
|
+
await waitForTabCleanup(
|
|
546
|
+
tab,
|
|
547
|
+
timeoutMs,
|
|
548
|
+
`cmux surface ${JSON.stringify(tab.targetId)} (surface.close)`,
|
|
549
|
+
tab.browser.client.request("surface.close", { surface_id: tab.targetId }, { timeoutMs }),
|
|
550
|
+
);
|
|
527
551
|
} catch (err) {
|
|
528
552
|
if (isLastSurfaceCloseError(err)) {
|
|
529
553
|
logger.debug("Leaving cmux browser surface open because it is the last surface in the workspace", {
|
|
530
554
|
error: err instanceof Error ? err.message : String(err),
|
|
531
555
|
});
|
|
532
556
|
} else {
|
|
533
|
-
|
|
557
|
+
closeError = err;
|
|
534
558
|
}
|
|
535
559
|
}
|
|
536
560
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
561
|
+
try {
|
|
562
|
+
await releaseBrowser(tab.browser, {
|
|
563
|
+
kill: opts.kill ?? false,
|
|
564
|
+
timeoutMs,
|
|
565
|
+
resource: `tab ${JSON.stringify(name)}`,
|
|
566
|
+
});
|
|
567
|
+
} catch (error) {
|
|
568
|
+
closeError ??= error;
|
|
569
|
+
} finally {
|
|
570
|
+
tabs.delete(name);
|
|
571
|
+
}
|
|
572
|
+
if (closeError) throw closeError;
|
|
540
573
|
return true;
|
|
541
574
|
}
|
|
575
|
+
let cleanupError: unknown;
|
|
542
576
|
let forced = false;
|
|
543
577
|
if (wasAlive) {
|
|
544
578
|
try {
|
|
@@ -549,9 +583,30 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
549
583
|
}
|
|
550
584
|
}
|
|
551
585
|
await tab.worker.terminate().catch(() => undefined);
|
|
552
|
-
if (forced && tab.kindTag === "headless")
|
|
553
|
-
|
|
554
|
-
|
|
586
|
+
if (forced && tab.kindTag === "headless") {
|
|
587
|
+
try {
|
|
588
|
+
await waitForTabCleanup(
|
|
589
|
+
tab,
|
|
590
|
+
timeoutMs,
|
|
591
|
+
`orphan CDP target ${JSON.stringify(tab.targetId)} (Page.close)`,
|
|
592
|
+
closeOrphanTarget(tab),
|
|
593
|
+
);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
cleanupError = error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
await releaseBrowser(tab.browser, {
|
|
600
|
+
kill: opts.kill ?? false,
|
|
601
|
+
timeoutMs,
|
|
602
|
+
resource: `tab ${JSON.stringify(name)}`,
|
|
603
|
+
});
|
|
604
|
+
} catch (error) {
|
|
605
|
+
cleanupError ??= error;
|
|
606
|
+
} finally {
|
|
607
|
+
tabs.delete(name);
|
|
608
|
+
}
|
|
609
|
+
if (cleanupError) throw cleanupError;
|
|
555
610
|
return true;
|
|
556
611
|
}
|
|
557
612
|
|
package/src/tools/browser.ts
CHANGED
|
@@ -199,7 +199,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
199
199
|
case "open":
|
|
200
200
|
return await this.#open(name, params, details, timeoutMs, signal);
|
|
201
201
|
case "close":
|
|
202
|
-
return await this.#close(name, params, details, signal);
|
|
202
|
+
return await this.#close(name, params, details, timeoutMs, signal);
|
|
203
203
|
case "run":
|
|
204
204
|
return await this.#run(name, params, details, timeoutMs, signal);
|
|
205
205
|
default:
|
|
@@ -284,15 +284,16 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
284
284
|
name: string,
|
|
285
285
|
params: BrowserParams,
|
|
286
286
|
details: BrowserToolDetails,
|
|
287
|
+
timeoutMs: number,
|
|
287
288
|
signal?: AbortSignal,
|
|
288
289
|
): Promise<AgentToolResult<BrowserToolDetails>> {
|
|
289
290
|
const kill = !!params.kill;
|
|
290
291
|
if (params.all) {
|
|
291
|
-
const count = await untilAborted(signal, () => releaseAllTabs({ kill }));
|
|
292
|
+
const count = await untilAborted(signal, () => releaseAllTabs({ kill, timeoutMs }));
|
|
292
293
|
details.result = `Closed ${count} tab(s)`;
|
|
293
294
|
return toolResult(details).text(details.result).done();
|
|
294
295
|
}
|
|
295
|
-
const closed = await untilAborted(signal, () => releaseTab(name, { kill }));
|
|
296
|
+
const closed = await untilAborted(signal, () => releaseTab(name, { kill, timeoutMs }));
|
|
296
297
|
details.result = closed ? `Closed tab ${JSON.stringify(name)}` : `No tab named ${JSON.stringify(name)}`;
|
|
297
298
|
return toolResult(details).text(details.result).done();
|
|
298
299
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical set of built-in tools that must stay top-level.
|
|
3
|
+
*
|
|
4
|
+
* These are the coding essentials the model always needs directly in its
|
|
5
|
+
* callable schema. `read`/`write` are additionally the `xd://` transport
|
|
6
|
+
* (`read xd://` lists devices, `write xd://<tool>` executes them), so demoting
|
|
7
|
+
* them under xdev makes every mounted device unreachable.
|
|
8
|
+
*
|
|
9
|
+
* Adapter boundaries (extension `registerTool`, SDK custom tools, RPC host
|
|
10
|
+
* tools) default an omitted `loadMode` to `"discoverable"`. A UI-only
|
|
11
|
+
* re-register of a built-in — e.g. wrapping `read`/`write`/`bash`/`edit`/`glob`
|
|
12
|
+
* to customize rendering — would then silently demote it to `discoverable` and,
|
|
13
|
+
* with `tools.xdev` on, unmount it from the top-level schema (issue #5764).
|
|
14
|
+
* {@link defaultLoadModeForToolName} pins these names to `"essential"` when the
|
|
15
|
+
* definition omits `loadMode`, so re-registering a built-in never demotes it.
|
|
16
|
+
*/
|
|
17
|
+
import type { ToolLoadMode } from "@oh-my-pi/pi-agent-core";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Built-in tool names whose classes declare `loadMode = "essential"`. Kept in
|
|
21
|
+
* sync with the tool classes by `essential-tools.test.ts` (drift guard).
|
|
22
|
+
*/
|
|
23
|
+
export const ESSENTIAL_BUILTIN_TOOL_NAMES: Record<string, true> = {
|
|
24
|
+
read: true,
|
|
25
|
+
write: true,
|
|
26
|
+
bash: true,
|
|
27
|
+
edit: true,
|
|
28
|
+
glob: true,
|
|
29
|
+
eval: true,
|
|
30
|
+
task: true,
|
|
31
|
+
hub: true,
|
|
32
|
+
learn: true,
|
|
33
|
+
manage_skill: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a tool's presentation mode at an adapter boundary. An explicit
|
|
38
|
+
* `declared` mode always wins. When omitted, known essential built-in names
|
|
39
|
+
* default to `"essential"` (so a re-register never demotes them); everything
|
|
40
|
+
* else defaults to `"discoverable"`.
|
|
41
|
+
*/
|
|
42
|
+
export function defaultLoadModeForToolName(name: string, declared?: ToolLoadMode): ToolLoadMode {
|
|
43
|
+
if (declared) return declared;
|
|
44
|
+
return name in ESSENTIAL_BUILTIN_TOOL_NAMES ? "essential" : "discoverable";
|
|
45
|
+
}
|
package/src/tools/eval.ts
CHANGED
|
@@ -768,5 +768,8 @@ async function summarizeFinal(
|
|
|
768
768
|
outputLines,
|
|
769
769
|
outputBytes,
|
|
770
770
|
artifactId: rawSummary.artifactId,
|
|
771
|
+
columnDroppedBytes: rawSummary.columnDroppedBytes,
|
|
772
|
+
columnTruncatedLines: rawSummary.columnTruncatedLines,
|
|
773
|
+
columnMax: rawSummary.columnMax,
|
|
771
774
|
};
|
|
772
775
|
}
|
package/src/tools/gh.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
ToolApprovalDecision,
|
|
11
11
|
} from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
|
|
13
|
-
import { getWorktreeDir, hashPath, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { getWorktreeDir, hashPath, isEnoent, logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import { type } from "arktype";
|
|
15
15
|
import type { Settings } from "../config/settings";
|
|
16
16
|
import githubDescription from "../prompts/tools/github.md" with { type: "text" };
|
|
@@ -239,6 +239,8 @@ const RUN_WATCH_TAIL_DEFAULT = 15;
|
|
|
239
239
|
const RUN_WATCH_TAIL_MAX = 200;
|
|
240
240
|
const REVIEW_COMMENTS_PAGE_SIZE = 100;
|
|
241
241
|
const RUN_JOBS_PAGE_SIZE = 100;
|
|
242
|
+
const PR_DIFF_FILES_PAGE_SIZE = 100;
|
|
243
|
+
const PR_DIFF_FILES_MAX = 3000;
|
|
242
244
|
const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/.*)?$/;
|
|
243
245
|
const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)(?:\/.*)?$/;
|
|
244
246
|
const RUN_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/actions\/runs\/(\d+)(?:\/.*)?$/;
|
|
@@ -2931,6 +2933,160 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
|
|
|
2931
2933
|
return file;
|
|
2932
2934
|
}
|
|
2933
2935
|
|
|
2936
|
+
/**
|
|
2937
|
+
* A single entry from `GET /repos/{owner}/{repo}/pulls/{n}/files`. `patch` is
|
|
2938
|
+
* absent for binary files and for individual file diffs GitHub deems too large
|
|
2939
|
+
* to render.
|
|
2940
|
+
*/
|
|
2941
|
+
interface GhPrFileApi {
|
|
2942
|
+
filename?: string;
|
|
2943
|
+
previous_filename?: string;
|
|
2944
|
+
status?: string;
|
|
2945
|
+
additions?: number;
|
|
2946
|
+
deletions?: number;
|
|
2947
|
+
patch?: string;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
interface GhPrApi {
|
|
2951
|
+
changed_files?: number;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* GitHub rejects the aggregate PR diff endpoint with HTTP 406 once the diff
|
|
2956
|
+
* exceeds 20,000 lines. Detect that specific failure so the caller can fall
|
|
2957
|
+
* back to the per-file endpoint instead of aborting the whole review.
|
|
2958
|
+
*/
|
|
2959
|
+
function isPrDiffTooLargeError(err: unknown): boolean {
|
|
2960
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2961
|
+
return (
|
|
2962
|
+
/\bHTTP 406\b/.test(message) ||
|
|
2963
|
+
/exceeded the maximum number of lines/i.test(message) ||
|
|
2964
|
+
/\btoo_large\b/.test(message)
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function formatSyntheticDiffPath(prefix: "a/" | "b/", path: string): string {
|
|
2969
|
+
const prefixedPath = `${prefix}${path}`;
|
|
2970
|
+
if (!/[\u0000-\u001F\s"\\]/.test(prefixedPath)) return prefixedPath;
|
|
2971
|
+
|
|
2972
|
+
let escaped = "";
|
|
2973
|
+
for (const char of prefixedPath) {
|
|
2974
|
+
switch (char) {
|
|
2975
|
+
case "\\":
|
|
2976
|
+
escaped += "\\\\";
|
|
2977
|
+
break;
|
|
2978
|
+
case '"':
|
|
2979
|
+
escaped += '\\"';
|
|
2980
|
+
break;
|
|
2981
|
+
case "\n":
|
|
2982
|
+
escaped += "\\n";
|
|
2983
|
+
break;
|
|
2984
|
+
case "\r":
|
|
2985
|
+
escaped += "\\r";
|
|
2986
|
+
break;
|
|
2987
|
+
case "\t":
|
|
2988
|
+
escaped += "\\t";
|
|
2989
|
+
break;
|
|
2990
|
+
default: {
|
|
2991
|
+
const code = char.charCodeAt(0);
|
|
2992
|
+
escaped += code < 32 ? `\\${code.toString(8).padStart(3, "0")}` : char;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
return `"${escaped}"`;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
/**
|
|
3000
|
+
* Reconstruct a `diff --git` section from a single files-API entry. The API's
|
|
3001
|
+
* `patch` field carries only the hunk body, so the `diff --git`/`---`/`+++`
|
|
3002
|
+
* headers are synthesized to match `gh pr diff` output — this keeps
|
|
3003
|
+
* {@link parsePrUnifiedDiff} and the review parser producing identical section
|
|
3004
|
+
* boundaries and byte offsets. Files whose `patch` is omitted (binary or
|
|
3005
|
+
* too-large) stay visible with an explicit marker rather than being dropped.
|
|
3006
|
+
*/
|
|
3007
|
+
function buildSyntheticDiffSection(file: GhPrFileApi): string | undefined {
|
|
3008
|
+
const newPath = file.filename;
|
|
3009
|
+
if (!newPath) return undefined;
|
|
3010
|
+
const status = file.status ?? "modified";
|
|
3011
|
+
const oldPath = file.previous_filename ?? newPath;
|
|
3012
|
+
const oldDiffPath = formatSyntheticDiffPath("a/", oldPath);
|
|
3013
|
+
const newDiffPath = formatSyntheticDiffPath("b/", newPath);
|
|
3014
|
+
const lines: string[] = [`diff --git ${oldDiffPath} ${newDiffPath}`];
|
|
3015
|
+
if (status === "added") {
|
|
3016
|
+
lines.push("new file mode 100644");
|
|
3017
|
+
} else if (status === "removed") {
|
|
3018
|
+
lines.push("deleted file mode 100644");
|
|
3019
|
+
} else if (status === "renamed" || file.previous_filename) {
|
|
3020
|
+
lines.push(`rename from ${oldPath}`, `rename to ${newPath}`);
|
|
3021
|
+
}
|
|
3022
|
+
if (typeof file.patch === "string" && file.patch.length > 0) {
|
|
3023
|
+
lines.push(status === "added" ? "--- /dev/null" : `--- ${oldDiffPath}`);
|
|
3024
|
+
lines.push(status === "removed" ? "+++ /dev/null" : `+++ ${newDiffPath}`);
|
|
3025
|
+
lines.push(file.patch);
|
|
3026
|
+
} else {
|
|
3027
|
+
lines.push(
|
|
3028
|
+
`* patch unavailable (binary or too large); additions ${file.additions ?? 0}, deletions ${file.deletions ?? 0}`,
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
return lines.join("\n");
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/**
|
|
3035
|
+
* Fallback PR diff retrieval via the paginated per-file endpoint, used when the
|
|
3036
|
+
* aggregate `gh pr diff` is rejected for exceeding GitHub's 20,000-line limit.
|
|
3037
|
+
* The per-file patches are not subject to that aggregate cap, so even very
|
|
3038
|
+
* large PRs can be reassembled into a synthetic unified diff.
|
|
3039
|
+
*/
|
|
3040
|
+
async function fetchPrDiffViaFilesApi(
|
|
3041
|
+
cwd: string,
|
|
3042
|
+
repo: string,
|
|
3043
|
+
number: number,
|
|
3044
|
+
signal: AbortSignal | undefined,
|
|
3045
|
+
): Promise<string> {
|
|
3046
|
+
const pull = await git.github.json<GhPrApi>(
|
|
3047
|
+
cwd,
|
|
3048
|
+
["api", "--method", "GET", `/repos/${repo}/pulls/${number}`],
|
|
3049
|
+
signal,
|
|
3050
|
+
{ repoProvided: true },
|
|
3051
|
+
);
|
|
3052
|
+
if ((pull.changed_files ?? 0) > PR_DIFF_FILES_MAX) {
|
|
3053
|
+
throw new ToolError(
|
|
3054
|
+
`Pull request changes ${pull.changed_files} files, exceeding GitHub's ${PR_DIFF_FILES_MAX}-file limit for the per-file diff API.`,
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const sections: string[] = [];
|
|
3059
|
+
let page = 1;
|
|
3060
|
+
while (true) {
|
|
3061
|
+
const response = await git.github.json<GhPrFileApi[]>(
|
|
3062
|
+
cwd,
|
|
3063
|
+
[
|
|
3064
|
+
"api",
|
|
3065
|
+
"--method",
|
|
3066
|
+
"GET",
|
|
3067
|
+
`/repos/${repo}/pulls/${number}/files`,
|
|
3068
|
+
"-F",
|
|
3069
|
+
`per_page=${PR_DIFF_FILES_PAGE_SIZE}`,
|
|
3070
|
+
"-F",
|
|
3071
|
+
`page=${page}`,
|
|
3072
|
+
],
|
|
3073
|
+
signal,
|
|
3074
|
+
{ repoProvided: true },
|
|
3075
|
+
);
|
|
3076
|
+
for (const file of response) {
|
|
3077
|
+
const section = buildSyntheticDiffSection(file);
|
|
3078
|
+
if (section) sections.push(section);
|
|
3079
|
+
}
|
|
3080
|
+
if (response.length < PR_DIFF_FILES_PAGE_SIZE) {
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
3083
|
+
page += 1;
|
|
3084
|
+
}
|
|
3085
|
+
// Trailing newline mirrors `gh pr diff` so downstream parsers splitting on
|
|
3086
|
+
// `^diff --git ` see identical boundaries.
|
|
3087
|
+
return sections.length > 0 ? `${sections.join("\n")}\n` : "";
|
|
3088
|
+
}
|
|
3089
|
+
|
|
2934
3090
|
async function fetchPrDiffFresh(
|
|
2935
3091
|
cwd: string,
|
|
2936
3092
|
repo: string,
|
|
@@ -2939,7 +3095,18 @@ async function fetchPrDiffFresh(
|
|
|
2939
3095
|
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: PrDiffPayload }> {
|
|
2940
3096
|
const args = ["pr", "diff", String(number), "--color", "never"];
|
|
2941
3097
|
appendRepoFlag(args, repo, String(number));
|
|
2942
|
-
|
|
3098
|
+
let text: string;
|
|
3099
|
+
try {
|
|
3100
|
+
text = await git.github.text(cwd, args, signal, { repoProvided: true, trimOutput: false });
|
|
3101
|
+
} catch (err) {
|
|
3102
|
+
if (!isPrDiffTooLargeError(err)) throw err;
|
|
3103
|
+
logger.debug("gh pr diff exceeded GitHub's aggregate line limit; falling back to per-file API", {
|
|
3104
|
+
repo,
|
|
3105
|
+
number,
|
|
3106
|
+
err: String(err),
|
|
3107
|
+
});
|
|
3108
|
+
text = await fetchPrDiffViaFilesApi(cwd, repo, number, signal);
|
|
3109
|
+
}
|
|
2943
3110
|
const payload = parsePrUnifiedDiff(text);
|
|
2944
3111
|
// `rendered` already carries the verbatim diff; blank the payload copy so
|
|
2945
3112
|
// the cache row stores a potentially huge diff once instead of twice.
|