@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.5
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 +39 -1
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/modes/components/assistant-message.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/config/model-registry.ts +54 -4
- package/src/config/settings-schema.ts +14 -4
- package/src/eval/__tests__/agent-bridge.test.ts +72 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/main.ts +7 -1
- package/src/modes/components/assistant-message.ts +22 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/tool-execution.ts +44 -0
- package/src/modes/components/transcript-container.ts +93 -32
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/event-controller.ts +42 -3
- package/src/modes/controllers/input-controller.ts +33 -1
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +17 -9
- package/src/session/agent-session.ts +37 -12
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/system-prompt.ts +4 -0
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +6 -8
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +60 -11
- package/src/tui/hyperlink.ts +42 -7
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -354,6 +354,7 @@ async function handleInstall(
|
|
|
354
354
|
console.error(chalk.dim(` ${APP_NAME} plugin install name@marketplace`));
|
|
355
355
|
console.error(chalk.dim(` ${APP_NAME} plugin install github:user/repo`));
|
|
356
356
|
console.error(chalk.dim(` ${APP_NAME} plugin install https://github.com/user/repo#v1.0`));
|
|
357
|
+
console.error(chalk.dim(` ${APP_NAME} plugin install ./path/to/local/plugin`));
|
|
357
358
|
process.exit(1);
|
|
358
359
|
}
|
|
359
360
|
|
|
@@ -382,6 +383,49 @@ async function handleInstall(
|
|
|
382
383
|
continue;
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
if (target.type === "local") {
|
|
387
|
+
// Local paths route to link(): symlink the directory into the plugins
|
|
388
|
+
// node_modules tree so source edits show up without a reinstall. Matches
|
|
389
|
+
// `omp plugin link <path>` so users can use either verb interchangeably.
|
|
390
|
+
if (flags.scope) {
|
|
391
|
+
console.error(
|
|
392
|
+
chalk.yellow(
|
|
393
|
+
`Warning: --scope is only supported for marketplace installs (name@marketplace). Ignoring for ${spec}.`,
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (flags.force) {
|
|
398
|
+
console.error(
|
|
399
|
+
chalk.yellow(
|
|
400
|
+
`Warning: --force has no effect for local path installs (link is already idempotent). Ignoring for ${spec}.`,
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (flags.dryRun) {
|
|
405
|
+
if (flags.json) {
|
|
406
|
+
console.log(JSON.stringify({ dryRun: true, action: "link", path: target.path }, null, 2));
|
|
407
|
+
} else {
|
|
408
|
+
console.log(chalk.dim(`[dry-run] Would link ${spec}`));
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const result = await manager.link(target.path);
|
|
414
|
+
if (flags.json) {
|
|
415
|
+
console.log(JSON.stringify(result, null, 2));
|
|
416
|
+
} else {
|
|
417
|
+
console.log(chalk.green(`${theme.status.success} Linked ${result.name} from ${spec}`));
|
|
418
|
+
if (result.manifest.description) {
|
|
419
|
+
console.log(chalk.dim(` ${result.manifest.description}`));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
385
429
|
// --scope only applies to marketplace installs; warn when it would be silently no-op'd for npm.
|
|
386
430
|
if (flags.scope) {
|
|
387
431
|
console.error(
|
|
@@ -923,6 +967,7 @@ ${chalk.bold("Sources:")}
|
|
|
923
967
|
github:user/repo[#ref] GitHub shorthand (also gitlab:, bitbucket:, codeberg:, sourcehut:)
|
|
924
968
|
https://github.com/user/repo Full git URL (https, ssh, or git protocol)
|
|
925
969
|
name@marketplace Marketplace plugin (see marketplace command)
|
|
970
|
+
./path, ../path, /abs, ~/path Local plugin directory (symlinked, same as plugin link)
|
|
926
971
|
|
|
927
972
|
${chalk.bold("Config Subcommands:")}
|
|
928
973
|
config list <pkg> List all settings
|
|
@@ -97,7 +97,6 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
|
97
97
|
const result = await runSearchQuery(params);
|
|
98
98
|
const component = renderSearchResult(result, { expanded: cmd.expanded, isPartial: false }, theme, {
|
|
99
99
|
query: cmd.query,
|
|
100
|
-
allowLongAnswer: true,
|
|
101
100
|
maxAnswerLines: cmd.expanded ? undefined : 6,
|
|
102
101
|
});
|
|
103
102
|
|
|
@@ -38,6 +38,45 @@ const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
|
38
38
|
// "socket connection was closed unexpectedly").
|
|
39
39
|
const DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
40
40
|
|
|
41
|
+
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
42
|
+
const OLLAMA_HOST_DEFAULT_PORT = "11434";
|
|
43
|
+
|
|
44
|
+
function normalizeOllamaHostEnv(value: string | undefined): string | undefined {
|
|
45
|
+
const trimmed = value?.trim();
|
|
46
|
+
if (!trimmed) return undefined;
|
|
47
|
+
const candidate = trimmed.includes("://")
|
|
48
|
+
? trimmed
|
|
49
|
+
: trimmed.startsWith("//")
|
|
50
|
+
? `http:${trimmed}`
|
|
51
|
+
: trimmed.startsWith(":")
|
|
52
|
+
? `http://127.0.0.1${trimmed}`
|
|
53
|
+
: `http://${trimmed}`;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = new URL(candidate);
|
|
56
|
+
if (!parsed.hostname || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!parsed.port && parsed.protocol === "http:") {
|
|
60
|
+
parsed.port = OLLAMA_HOST_DEFAULT_PORT;
|
|
61
|
+
}
|
|
62
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getImplicitOllamaBaseUrl(): string {
|
|
69
|
+
const baseUrl = Bun.env.OLLAMA_BASE_URL?.trim();
|
|
70
|
+
return baseUrl || normalizeOllamaHostEnv(Bun.env.OLLAMA_HOST) || DEFAULT_OLLAMA_BASE_URL;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getOllamaContextLengthOverride(): number | undefined {
|
|
74
|
+
const value = Bun.env.OLLAMA_CONTEXT_LENGTH?.trim();
|
|
75
|
+
if (!value) return undefined;
|
|
76
|
+
const parsed = Number(value);
|
|
77
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
// Anthropic-safe variant of the discovery cap. The Anthropic stream converter
|
|
42
81
|
// in `packages/ai/src/providers/anthropic.ts` derives the request limit as
|
|
43
82
|
// `(model.maxTokens / 3) | 0`, so the 32K default would surface as 10,922
|
|
@@ -1220,7 +1259,18 @@ export class ModelRegistry {
|
|
|
1220
1259
|
return models;
|
|
1221
1260
|
}
|
|
1222
1261
|
|
|
1223
|
-
|
|
1262
|
+
const contextLengthOverride = getOllamaContextLengthOverride();
|
|
1263
|
+
return models.map(model => {
|
|
1264
|
+
const normalized = model.api === "openai-completions" ? { ...model, api: "openai-responses" as const } : model;
|
|
1265
|
+
if (contextLengthOverride === undefined) {
|
|
1266
|
+
return normalized;
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
...normalized,
|
|
1270
|
+
contextWindow: contextLengthOverride,
|
|
1271
|
+
maxTokens: Math.min(contextLengthOverride, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1224
1274
|
}
|
|
1225
1275
|
|
|
1226
1276
|
#addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
|
|
@@ -1229,7 +1279,7 @@ export class ModelRegistry {
|
|
|
1229
1279
|
this.#discoverableProviders.push({
|
|
1230
1280
|
provider: "ollama",
|
|
1231
1281
|
api: "openai-responses",
|
|
1232
|
-
baseUrl:
|
|
1282
|
+
baseUrl: getImplicitOllamaBaseUrl(),
|
|
1233
1283
|
discovery: { type: "ollama" },
|
|
1234
1284
|
optional: true,
|
|
1235
1285
|
});
|
|
@@ -1993,12 +2043,12 @@ export class ModelRegistry {
|
|
|
1993
2043
|
}
|
|
1994
2044
|
}
|
|
1995
2045
|
#normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
1996
|
-
const raw = baseUrl ||
|
|
2046
|
+
const raw = baseUrl || DEFAULT_OLLAMA_BASE_URL;
|
|
1997
2047
|
try {
|
|
1998
2048
|
const parsed = new URL(raw);
|
|
1999
2049
|
return `${parsed.protocol}//${parsed.host}`;
|
|
2000
2050
|
} catch {
|
|
2001
|
-
return
|
|
2051
|
+
return DEFAULT_OLLAMA_BASE_URL;
|
|
2002
2052
|
}
|
|
2003
2053
|
}
|
|
2004
2054
|
|
|
@@ -635,7 +635,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
635
635
|
tab: "appearance",
|
|
636
636
|
label: "Terminal Hyperlinks",
|
|
637
637
|
description:
|
|
638
|
-
"Wrap
|
|
638
|
+
"Wrap paths and URLs in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)",
|
|
639
639
|
},
|
|
640
640
|
},
|
|
641
641
|
// Display rendering
|
|
@@ -722,6 +722,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
722
722
|
},
|
|
723
723
|
},
|
|
724
724
|
|
|
725
|
+
includeModelInPrompt: {
|
|
726
|
+
type: "boolean",
|
|
727
|
+
default: true,
|
|
728
|
+
ui: {
|
|
729
|
+
tab: "model",
|
|
730
|
+
label: "Include Model In Prompt",
|
|
731
|
+
description: "Surface the active model identifier in the system prompt so the agent knows which model it is",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
|
|
725
735
|
// Sampling
|
|
726
736
|
temperature: {
|
|
727
737
|
type: "number",
|
|
@@ -2483,13 +2493,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
2483
2493
|
// Tool Discovery
|
|
2484
2494
|
"tools.discoveryMode": {
|
|
2485
2495
|
type: "enum",
|
|
2486
|
-
values: ["off", "mcp-only", "all"] as const,
|
|
2487
|
-
default: "
|
|
2496
|
+
values: ["auto", "off", "mcp-only", "all"] as const,
|
|
2497
|
+
default: "auto",
|
|
2488
2498
|
ui: {
|
|
2489
2499
|
tab: "tools",
|
|
2490
2500
|
label: "Tool Discovery",
|
|
2491
2501
|
description:
|
|
2492
|
-
"Hide tools behind a search tool to save tokens. 'mcp-only' hides MCP tools; 'all' hides all non-essential built-ins too.",
|
|
2502
|
+
"Hide tools behind a search tool to save tokens. 'auto' hides MCP tools once the tool set has more than 40 tools; 'mcp-only' always hides MCP tools; 'all' hides all non-essential built-ins too.",
|
|
2493
2503
|
},
|
|
2494
2504
|
},
|
|
2495
2505
|
|
|
@@ -397,6 +397,78 @@ describe("agent() through eval runtimes", () => {
|
|
|
397
397
|
expect(maxInFlight).toBeLessThanOrEqual(2);
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
+
it("interrupting a Python parallel() fan-out settles the kernel cleanly and preserves session state", async () => {
|
|
401
|
+
using tempDir = TempDir.createSync("@omp-eval-agent-py-interrupt-");
|
|
402
|
+
const settings = Settings.isolated({
|
|
403
|
+
"async.enabled": false,
|
|
404
|
+
"task.isolation.mode": "none",
|
|
405
|
+
"task.enableLsp": true,
|
|
406
|
+
"task.maxConcurrency": 6,
|
|
407
|
+
});
|
|
408
|
+
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent-interrupt", settings);
|
|
409
|
+
mockAgents();
|
|
410
|
+
// Subagents that ignore the abort for far longer than the kernel's SIGINT
|
|
411
|
+
// escalation window. Each kernel worker thread blocks in a synchronous
|
|
412
|
+
// `urllib` bridge call, joined by `parallel()`'s ThreadPoolExecutor exit.
|
|
413
|
+
// The host must respond the instant the cell aborts so the kernel can
|
|
414
|
+
// unwind via KeyboardInterrupt instead of being hard-killed (which used to
|
|
415
|
+
// surface "[kernel] Python kernel shutdown" and lose all session state).
|
|
416
|
+
vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
417
|
+
await Bun.sleep(9000); // deliberately ignores options.signal
|
|
418
|
+
return singleResult(options, { output: options.assignment ?? "" });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Seed persistent session state and confirm the kernel is reusable.
|
|
422
|
+
const seed = await executePython("PREP_MARKER = 4242", {
|
|
423
|
+
cwd: tempDir.path(),
|
|
424
|
+
sessionId,
|
|
425
|
+
sessionFile,
|
|
426
|
+
kernelMode: "session",
|
|
427
|
+
toolSession: session,
|
|
428
|
+
});
|
|
429
|
+
if (seed.exitCode === undefined && seed.cancelled) {
|
|
430
|
+
expect(seed.output).toBe("");
|
|
431
|
+
return; // kernel unavailable in this environment
|
|
432
|
+
}
|
|
433
|
+
expect(seed.exitCode).toBe(0);
|
|
434
|
+
|
|
435
|
+
const ac = new AbortController();
|
|
436
|
+
// Abort ~1s in, after the worker threads are blocked in their bridge calls.
|
|
437
|
+
setTimeout(() => ac.abort(new Error("external interrupt")), 1000);
|
|
438
|
+
|
|
439
|
+
const start = Date.now();
|
|
440
|
+
const result = await executePython(
|
|
441
|
+
"import json\nprint(json.dumps(parallel([lambda n=n: agent(str(n)) for n in range(12)])))",
|
|
442
|
+
{
|
|
443
|
+
cwd: tempDir.path(),
|
|
444
|
+
sessionId,
|
|
445
|
+
sessionFile,
|
|
446
|
+
kernelMode: "session",
|
|
447
|
+
toolSession: session,
|
|
448
|
+
idleTimeoutMs: 60_000,
|
|
449
|
+
signal: ac.signal,
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
const elapsed = Date.now() - start;
|
|
453
|
+
|
|
454
|
+
// Cancelled, but cleanly: no hard-kill, settled well within the kernel's 5s
|
|
455
|
+
// SIGINT escalation window rather than ~6s after it.
|
|
456
|
+
expect(result.cancelled).toBe(true);
|
|
457
|
+
expect(result.output).not.toContain("Python kernel shutdown");
|
|
458
|
+
expect(elapsed).toBeLessThan(4000);
|
|
459
|
+
|
|
460
|
+
// The persistent kernel survived the interrupt: prior state is intact.
|
|
461
|
+
const after = await executePython("print(PREP_MARKER)", {
|
|
462
|
+
cwd: tempDir.path(),
|
|
463
|
+
sessionId,
|
|
464
|
+
sessionFile,
|
|
465
|
+
kernelMode: "session",
|
|
466
|
+
toolSession: session,
|
|
467
|
+
});
|
|
468
|
+
expect(after.exitCode).toBe(0);
|
|
469
|
+
expect(after.output.trim()).toBe("4242");
|
|
470
|
+
}, 30_000);
|
|
471
|
+
|
|
400
472
|
it("streams enriched agent progress through onStatus before the cell finishes", async () => {
|
|
401
473
|
using tempDir = TempDir.createSync("@omp-eval-agent-progress-");
|
|
402
474
|
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "js-agent-progress");
|
|
@@ -30,6 +30,48 @@ interface BridgeServer {
|
|
|
30
30
|
const registrations = new Map<string, PyToolBridgeEntry>();
|
|
31
31
|
let serverPromise: Promise<BridgeServer> | null = null;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Forward a bridge call to {@link callSessionTool}, but resolve the HTTP request
|
|
35
|
+
* the instant the cell's signal aborts instead of waiting for the tool/subagent
|
|
36
|
+
* to fully tear down.
|
|
37
|
+
*
|
|
38
|
+
* The kernel invokes this bridge with a *blocking* `urllib` request from a
|
|
39
|
+
* worker thread (each `agent()` / `tool.*` call). When the cell is interrupted,
|
|
40
|
+
* `parallel()`'s `ThreadPoolExecutor.__exit__` joins those worker threads
|
|
41
|
+
* (`shutdown(wait=True)`), so they cannot unwind until their `urllib` call
|
|
42
|
+
* returns — i.e. until this handler responds. A host-side `agent()` teardown
|
|
43
|
+
* (aborting nested LLM streams + tools across a wide fan-out) routinely exceeds
|
|
44
|
+
* the kernel's SIGINT escalation window, so the kernel was hard-killed and its
|
|
45
|
+
* persistent state lost while the subagents were still winding down. Responding
|
|
46
|
+
* immediately on abort lets the kernel raise through the blocked call and settle
|
|
47
|
+
* cleanly (preserving state); the already-signaled call keeps tearing down in
|
|
48
|
+
* the background, its eventual result/rejection swallowed.
|
|
49
|
+
*/
|
|
50
|
+
async function callSessionToolPromptOnAbort(name: string, args: unknown, entry: PyToolBridgeEntry): Promise<unknown> {
|
|
51
|
+
const call = callSessionTool(name, args, {
|
|
52
|
+
session: entry.toolSession,
|
|
53
|
+
signal: entry.signal,
|
|
54
|
+
emitStatus: entry.emitStatus,
|
|
55
|
+
});
|
|
56
|
+
const signal = entry.signal;
|
|
57
|
+
if (!signal) return await call;
|
|
58
|
+
if (signal.aborted) {
|
|
59
|
+
void call.catch(() => {});
|
|
60
|
+
throw new Error(`bridge call ${JSON.stringify(name)} aborted: eval cell was interrupted`);
|
|
61
|
+
}
|
|
62
|
+
const { promise: aborted, reject } = Promise.withResolvers<never>();
|
|
63
|
+
const onAbort = () => reject(new Error(`bridge call ${JSON.stringify(name)} aborted: eval cell was interrupted`));
|
|
64
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
try {
|
|
66
|
+
return await Promise.race([call, aborted]);
|
|
67
|
+
} finally {
|
|
68
|
+
signal.removeEventListener("abort", onAbort);
|
|
69
|
+
// `call` may still be settling (subagent teardown after its own abort);
|
|
70
|
+
// swallow its outcome so an abort-won race can't surface as unhandled.
|
|
71
|
+
void call.catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
async function startServer(): Promise<BridgeServer> {
|
|
34
76
|
const token = crypto.randomUUID();
|
|
35
77
|
const server = Bun.serve({
|
|
@@ -66,11 +108,7 @@ async function startServer(): Promise<BridgeServer> {
|
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
try {
|
|
69
|
-
const value = await
|
|
70
|
-
session: entry.toolSession,
|
|
71
|
-
signal: entry.signal,
|
|
72
|
-
emitStatus: entry.emitStatus,
|
|
73
|
-
});
|
|
111
|
+
const value = await callSessionToolPromptOnAbort(name, body.args, entry);
|
|
74
112
|
return Response.json({ ok: true, value });
|
|
75
113
|
} catch (err) {
|
|
76
114
|
return Response.json({
|
|
@@ -12,6 +12,35 @@ async function getHeadTag(api: CustomCommandAPI): Promise<string | undefined> {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
|
|
16
|
+
try {
|
|
17
|
+
return (await git.branch.current(api.cwd)) ?? "HEAD";
|
|
18
|
+
} catch {
|
|
19
|
+
return "HEAD";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getPushRemote(api: CustomCommandAPI, branch: string): Promise<string | undefined> {
|
|
24
|
+
try {
|
|
25
|
+
return (
|
|
26
|
+
(await git.config.getBranch(api.cwd, branch, "pushRemote")) ??
|
|
27
|
+
(await git.config.getBranch(api.cwd, branch, "remote"))
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function getHeadTagContext(api: CustomCommandAPI): Promise<{ branch: string; headTag?: string; remote: string }> {
|
|
35
|
+
const branch = await getCurrentBranch(api);
|
|
36
|
+
const [headTag, pushRemote] = await Promise.all([getHeadTag(api), getPushRemote(api, branch)]);
|
|
37
|
+
return {
|
|
38
|
+
headTag,
|
|
39
|
+
branch,
|
|
40
|
+
remote: pushRemote ?? "origin",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
export class GreenCommand implements CustomCommand {
|
|
16
45
|
name = "green";
|
|
17
46
|
description = "Generate a prompt to iterate on CI failures until the branch is green";
|
|
@@ -19,7 +48,7 @@ export class GreenCommand implements CustomCommand {
|
|
|
19
48
|
constructor(private api: CustomCommandAPI) {}
|
|
20
49
|
|
|
21
50
|
async execute(_args: string[], _ctx: HookCommandContext): Promise<string> {
|
|
22
|
-
const headTag = await
|
|
23
|
-
return prompt.render(ciGreenRequestTemplate, { headTag });
|
|
51
|
+
const { headTag, branch, remote } = await getHeadTagContext(this.api);
|
|
52
|
+
return prompt.render(ciGreenRequestTemplate, { headTag, branch, remote });
|
|
24
53
|
}
|
|
25
54
|
}
|