@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.6
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 +69 -1
- package/dist/types/config/settings-schema.d.ts +12 -22
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +0 -1
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/command-controller.d.ts +2 -3
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +10 -7
- package/dist/types/session/shake-types.d.ts +3 -3
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +0 -14
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -9
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +4 -11
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +11 -10
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/scripts/build-binary.ts +0 -1
- package/src/cli.ts +59 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +13 -24
- package/src/config/settings.ts +10 -0
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +90 -0
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/mcp/manager.ts +87 -4
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +9 -7
- package/src/modes/components/hook-selector.ts +159 -32
- package/src/modes/components/tool-execution.ts +20 -4
- package/src/modes/controllers/command-controller.ts +7 -39
- package/src/modes/controllers/event-controller.ts +38 -28
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/session/agent-session.ts +75 -103
- package/src/session/shake-types.ts +4 -5
- package/src/slash-commands/builtin-registry.ts +2 -4
- package/src/task/executor.ts +14 -4
- package/src/task/index.ts +3 -2
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +0 -28
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +11 -16
- package/src/tiny/worker.ts +6 -61
- package/src/tools/ask.ts +74 -32
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/index.ts +7 -37
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +12 -1
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +42 -79
- package/src/utils/git.ts +9 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keepalive for in-flight host-side eval bridge calls.
|
|
3
|
+
*
|
|
4
|
+
* The eval watchdog ({@link ../tools/eval IdleTimeout}) caps a cell's `timeout`
|
|
5
|
+
* as a wall-clock budget on the cell's *own* work, but pauses that budget while
|
|
6
|
+
* a host-side `agent()`/`parallel()` (via `runSubprocess`) or `llm()` (a single
|
|
7
|
+
* completion) call is in flight. Those calls are the only thing that re-arms the
|
|
8
|
+
* watchdog — and they can run for long stretches with **no** status of their own
|
|
9
|
+
* (a subagent's time-to-first-token on a reasoning model, a long quiet nested
|
|
10
|
+
* tool, or the entire body of a oneshot `llm()` call). Without a keepalive the
|
|
11
|
+
* watchdog would mistake that delegated work for the cell stalling and abort it
|
|
12
|
+
* mid-flight, killing the subagent.
|
|
13
|
+
*
|
|
14
|
+
* {@link withBridgeHeartbeat} bridges that gap by emitting a synthetic
|
|
15
|
+
* {@link EVAL_HEARTBEAT_OP} status event immediately when the call begins and
|
|
16
|
+
* then on a fixed cadence until it settles. The event rides the same
|
|
17
|
+
* `emitStatus → onStatus` channel both runtimes already forward, so it re-arms
|
|
18
|
+
* the watchdog without any new plumbing. The heartbeat is the *sole* signal that
|
|
19
|
+
* extends the budget: consumers MUST treat it as a pure keepalive — bump the
|
|
20
|
+
* watchdog and drop it (never persist or render it) — see the executor display
|
|
21
|
+
* sinks and the eval tool's `onStatus` handler. Every other status event
|
|
22
|
+
* (compute helpers, `log()`/`phase()`, tool results) counts against the budget.
|
|
23
|
+
*/
|
|
24
|
+
import type { JsStatusEvent } from "./js/shared/types";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Synthetic status op emitted purely to keep the eval idle watchdog alive while
|
|
28
|
+
* a host-side bridge call is in flight. Carries no payload.
|
|
29
|
+
*/
|
|
30
|
+
export const EVAL_HEARTBEAT_OP = "heartbeat";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Heartbeat cadence. Comfortably below the default 30s idle budget (and the
|
|
34
|
+
* larger budgets long fanouts run under), so a working bridge call always bumps
|
|
35
|
+
* the watchdog before it expires, while a genuine stall is still bounded once
|
|
36
|
+
* the call settles and the heartbeat stops.
|
|
37
|
+
*/
|
|
38
|
+
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
39
|
+
|
|
40
|
+
let heartbeatIntervalMs = HEARTBEAT_INTERVAL_MS;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Test seam: override the heartbeat cadence so integration tests can exercise
|
|
44
|
+
* the keepalive within a sub-second idle budget. Pass no value to restore the
|
|
45
|
+
* production default.
|
|
46
|
+
*/
|
|
47
|
+
export function setBridgeHeartbeatIntervalMs(ms?: number): void {
|
|
48
|
+
heartbeatIntervalMs = ms === undefined ? HEARTBEAT_INTERVAL_MS : Math.max(1, Math.floor(ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run {@link operation}, pumping {@link EVAL_HEARTBEAT_OP} status events through
|
|
53
|
+
* {@link emitStatus} — one immediately, then on a fixed cadence — until it
|
|
54
|
+
* settles. The immediate beat pauses the watchdog the instant the call begins,
|
|
55
|
+
* so a bridge call that starts close to the budget edge (after the cell already
|
|
56
|
+
* spent most of it computing) is not aborted before the first interval tick. A
|
|
57
|
+
* no-op wrapper when no `emitStatus` sink is wired (the heartbeat would reach
|
|
58
|
+
* nobody).
|
|
59
|
+
*/
|
|
60
|
+
export async function withBridgeHeartbeat<T>(
|
|
61
|
+
emitStatus: ((event: JsStatusEvent) => void) | undefined,
|
|
62
|
+
operation: () => Promise<T>,
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
if (!emitStatus) return operation();
|
|
65
|
+
emitStatus({ op: EVAL_HEARTBEAT_OP });
|
|
66
|
+
const timer = setInterval(() => emitStatus({ op: EVAL_HEARTBEAT_OP }), heartbeatIntervalMs);
|
|
67
|
+
// Never keep the event loop alive for the heartbeat alone.
|
|
68
|
+
timer.unref?.();
|
|
69
|
+
try {
|
|
70
|
+
return await operation();
|
|
71
|
+
} finally {
|
|
72
|
+
clearInterval(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/eval/js/executor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_MAX_BYTES, OutputSink } from "../../session/streaming-output";
|
|
2
2
|
import type { ToolSession } from "../../tools";
|
|
3
3
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
|
|
4
|
+
import { EVAL_HEARTBEAT_OP } from "../heartbeat";
|
|
4
5
|
import { executeInVmContext, type JsDisplayOutput } from "./context-manager";
|
|
5
6
|
import type { JsStatusEvent } from "./shared/types";
|
|
6
7
|
|
|
@@ -59,11 +60,10 @@ function isTimeoutReason(reason: unknown): boolean {
|
|
|
59
60
|
);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function formatJsTimeoutAnnotation(timeoutMs: number | undefined
|
|
63
|
-
const suffix = idle ? " of inactivity" : "";
|
|
63
|
+
function formatJsTimeoutAnnotation(timeoutMs: number | undefined): string {
|
|
64
64
|
if (timeoutMs === undefined) return "Command timed out";
|
|
65
65
|
const secs = Math.max(1, Math.round(timeoutMs / 1000));
|
|
66
|
-
return `Command timed out after ${secs} seconds
|
|
66
|
+
return `Command timed out after ${secs} seconds`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export async function executeJs(code: string, options: JsExecutorOptions): Promise<JsResult> {
|
|
@@ -85,10 +85,9 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
85
85
|
options.signal && timeoutSignal
|
|
86
86
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
87
87
|
: (options.signal ?? timeoutSignal);
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
const idleMode = legacyTimeoutMs === undefined && options.idleTimeoutMs !== undefined;
|
|
88
|
+
// The eval tool drives cancellation via an idle-aware `signal` and passes only
|
|
89
|
+
// an inactivity budget; use it solely as worker cold-start headroom and never
|
|
90
|
+
// derive a competing fixed timer from it.
|
|
92
91
|
const acquireBudgetMs = legacyTimeoutMs ?? options.idleTimeoutMs;
|
|
93
92
|
|
|
94
93
|
try {
|
|
@@ -105,8 +104,13 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
105
104
|
signal,
|
|
106
105
|
onText: chunk => outputSink.push(chunk),
|
|
107
106
|
onDisplay: output => {
|
|
107
|
+
if (output.type === "status") {
|
|
108
|
+
// Heartbeats are pure idle-watchdog keepalives: forward them so
|
|
109
|
+
// the eval tool re-arms its timer, but never store or render them.
|
|
110
|
+
options.onStatus?.(output.event);
|
|
111
|
+
if (output.event.op === EVAL_HEARTBEAT_OP) return;
|
|
112
|
+
}
|
|
108
113
|
displayOutputs.push(output);
|
|
109
|
-
if (output.type === "status") options.onStatus?.(output.event);
|
|
110
114
|
},
|
|
111
115
|
},
|
|
112
116
|
});
|
|
@@ -127,7 +131,7 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
127
131
|
if (signal?.aborted || isAbortError(error)) {
|
|
128
132
|
const timedOut = Boolean(timeoutSignal?.aborted) || isTimeoutReason(options.signal?.reason);
|
|
129
133
|
if (timedOut) {
|
|
130
|
-
outputSink.push(formatJsTimeoutAnnotation(legacyTimeoutMs ?? options.idleTimeoutMs
|
|
134
|
+
outputSink.push(formatJsTimeoutAnnotation(legacyTimeoutMs ?? options.idleTimeoutMs));
|
|
131
135
|
}
|
|
132
136
|
const summary = await outputSink.dump();
|
|
133
137
|
return {
|
package/src/eval/llm-bridge.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit
|
|
|
18
18
|
import { expandRoleAlias, formatModelString, resolveModelFromString } from "../config/model-resolver";
|
|
19
19
|
import type { ToolSession } from "../tools";
|
|
20
20
|
import { ToolError } from "../tools/tool-errors";
|
|
21
|
+
import { withBridgeHeartbeat } from "./heartbeat";
|
|
21
22
|
import type { JsStatusEvent } from "./js/shared/types";
|
|
22
23
|
|
|
23
24
|
/** Synthetic bridge name reserved for the `llm()` helper across both runtimes. */
|
|
@@ -131,20 +132,25 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
|
|
|
131
132
|
|
|
132
133
|
const telemetry = resolveTelemetry(options.session.getTelemetry?.(), options.session.getSessionId?.() ?? undefined);
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
135
|
+
// A oneshot completion emits no status until it returns, so pump a heartbeat
|
|
136
|
+
// while it runs to keep the eval idle watchdog armed across a slow (e.g.
|
|
137
|
+
// reasoning-tier) request that would otherwise look like a stalled cell.
|
|
138
|
+
const response = await withBridgeHeartbeat(options.emitStatus, () =>
|
|
139
|
+
instrumentedCompleteSimple(
|
|
140
|
+
model,
|
|
141
|
+
{
|
|
142
|
+
systemPrompt: system ? [system] : undefined,
|
|
143
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }],
|
|
144
|
+
tools,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
apiKey,
|
|
148
|
+
signal: options.signal,
|
|
149
|
+
reasoning: reasoningForTier(tier, model),
|
|
150
|
+
toolChoice: schema ? { type: "tool", name: STRUCTURED_TOOL_NAME } : undefined,
|
|
151
|
+
},
|
|
152
|
+
{ telemetry, oneshotKind: "eval_llm" },
|
|
153
|
+
),
|
|
148
154
|
);
|
|
149
155
|
|
|
150
156
|
if (response.stopReason === "error") {
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Settings } from "../../config/settings";
|
|
|
5
5
|
import { OutputSink } from "../../session/streaming-output";
|
|
6
6
|
import type { ToolSession } from "../../tools";
|
|
7
7
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
|
|
8
|
+
import { EVAL_HEARTBEAT_OP } from "../heartbeat";
|
|
8
9
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
9
10
|
import {
|
|
10
11
|
checkPythonKernelAvailability,
|
|
@@ -231,20 +232,18 @@ async function waitForPromiseWithCancellation<T>(
|
|
|
231
232
|
// Result formatting
|
|
232
233
|
// ---------------------------------------------------------------------------
|
|
233
234
|
|
|
234
|
-
function formatTimeoutAnnotation(timeoutMs?: number
|
|
235
|
-
const suffix = idle ? " of inactivity" : "";
|
|
235
|
+
function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
|
|
236
236
|
if (timeoutMs === undefined) return "Command timed out";
|
|
237
237
|
const secs = Math.max(1, Math.round(timeoutMs / 1000));
|
|
238
|
-
return `Command timed out after ${secs} seconds
|
|
238
|
+
return `Command timed out after ${secs} seconds`;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean
|
|
241
|
+
function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
|
|
242
242
|
const secs = timeoutMs === undefined ? undefined : Math.max(1, Math.round(timeoutMs / 1000));
|
|
243
|
-
const suffix = idle ? " of inactivity" : "";
|
|
244
243
|
if (kernelKilled) {
|
|
245
|
-
return
|
|
244
|
+
return "eval cell timed out and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.";
|
|
246
245
|
}
|
|
247
|
-
const duration = secs === undefined ? "the configured timeout" : `${secs}s
|
|
246
|
+
const duration = secs === undefined ? "the configured timeout" : `${secs}s`;
|
|
248
247
|
return `eval cell timed out after ${duration}; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`;
|
|
249
248
|
}
|
|
250
249
|
|
|
@@ -488,16 +487,17 @@ async function executeWithKernel(
|
|
|
488
487
|
const displayOutputs: KernelDisplayOutput[] = [];
|
|
489
488
|
const deadlineMs = getExecutionDeadlineMs(options);
|
|
490
489
|
let executionTimeoutMs: number | undefined;
|
|
491
|
-
// Idle mode: the caller (eval tool) drives cancellation via an idle-aware
|
|
492
|
-
// signal and passes no wall-clock deadline, so annotate timeouts with the
|
|
493
|
-
// configured inactivity budget rather than a remaining-deadline figure.
|
|
494
|
-
const idleMode = deadlineMs === undefined && options?.idleTimeoutMs !== undefined;
|
|
495
490
|
|
|
496
491
|
// Collect every display output and, for status events, stream them live so
|
|
497
492
|
// long-running bridge helpers (e.g. `agent()`) surface progress mid-cell.
|
|
498
493
|
const collectDisplay = (output: KernelDisplayOutput) => {
|
|
494
|
+
if (output.type === "status") {
|
|
495
|
+
// Heartbeats are pure idle-watchdog keepalives: forward them so the
|
|
496
|
+
// eval tool re-arms its timer, but never store or render them.
|
|
497
|
+
options?.onStatus?.(output.event);
|
|
498
|
+
if (output.event.op === EVAL_HEARTBEAT_OP) return;
|
|
499
|
+
}
|
|
499
500
|
displayOutputs.push(output);
|
|
500
|
-
if (output.type === "status") options?.onStatus?.(output.event);
|
|
501
501
|
};
|
|
502
502
|
const emitStatus = options?.emitStatus ?? ((event: JsStatusEvent) => collectDisplay({ type: "status", event }));
|
|
503
503
|
const runId = `py-${crypto.randomUUID()}`;
|
|
@@ -524,11 +524,7 @@ async function executeWithKernel(
|
|
|
524
524
|
|
|
525
525
|
if (result.cancelled) {
|
|
526
526
|
const annotation = result.timedOut
|
|
527
|
-
? formatKernelTimeoutAnnotation(
|
|
528
|
-
executionTimeoutMs ?? options?.idleTimeoutMs,
|
|
529
|
-
result.kernelKilled ?? false,
|
|
530
|
-
idleMode,
|
|
531
|
-
)
|
|
527
|
+
? formatKernelTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, result.kernelKilled ?? false)
|
|
532
528
|
: undefined;
|
|
533
529
|
return {
|
|
534
530
|
exitCode: undefined,
|
|
@@ -566,7 +562,7 @@ async function executeWithKernel(
|
|
|
566
562
|
displayOutputs,
|
|
567
563
|
stdinRequested: false,
|
|
568
564
|
...(await sink.dump(
|
|
569
|
-
timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs
|
|
565
|
+
timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs) : undefined,
|
|
570
566
|
)),
|
|
571
567
|
};
|
|
572
568
|
}
|
|
@@ -52,6 +52,27 @@ export interface BashResult {
|
|
|
52
52
|
|
|
53
53
|
const shellSessions = new Map<string, Shell>();
|
|
54
54
|
const brokenShellSessions = new Set<string>();
|
|
55
|
+
const shellSessionQuarantines = new Map<string, Promise<unknown>>();
|
|
56
|
+
|
|
57
|
+
function quarantineShellSession(
|
|
58
|
+
sessionKey: string,
|
|
59
|
+
runPromise: Promise<ShellRunResult>,
|
|
60
|
+
abortCleanupPromise: Promise<void> | undefined,
|
|
61
|
+
): void {
|
|
62
|
+
brokenShellSessions.add(sessionKey);
|
|
63
|
+
const cleanup = abortCleanupPromise
|
|
64
|
+
? Promise.allSettled([runPromise, abortCleanupPromise])
|
|
65
|
+
: Promise.allSettled([runPromise]);
|
|
66
|
+
shellSessionQuarantines.set(sessionKey, cleanup);
|
|
67
|
+
void cleanup
|
|
68
|
+
.finally(() => {
|
|
69
|
+
if (shellSessionQuarantines.get(sessionKey) === cleanup) {
|
|
70
|
+
shellSessionQuarantines.delete(sessionKey);
|
|
71
|
+
brokenShellSessions.delete(sessionKey);
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.catch(() => undefined);
|
|
75
|
+
}
|
|
55
76
|
|
|
56
77
|
async function resolveShellCwd(cwd: string | undefined): Promise<string | undefined> {
|
|
57
78
|
if (!cwd) return undefined;
|
|
@@ -134,13 +155,13 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
134
155
|
}
|
|
135
156
|
const userSignal = options?.signal;
|
|
136
157
|
const runAbortController = new AbortController();
|
|
158
|
+
let abortCleanupPromise: Promise<void> | undefined;
|
|
137
159
|
const abortCurrentExecution = () => {
|
|
138
160
|
if (!runAbortController.signal.aborted) {
|
|
139
161
|
runAbortController.abort();
|
|
140
162
|
}
|
|
141
|
-
if (shellSession) {
|
|
142
|
-
|
|
143
|
-
void shellSession.abort();
|
|
163
|
+
if (shellSession && !abortCleanupPromise) {
|
|
164
|
+
abortCleanupPromise = shellSession.abort().catch(() => undefined);
|
|
144
165
|
}
|
|
145
166
|
};
|
|
146
167
|
const abortDeferred = Promise.withResolvers<"abort">();
|
|
@@ -209,8 +230,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
209
230
|
acceptingChunks = false;
|
|
210
231
|
if (shellSession) {
|
|
211
232
|
resetSession = true;
|
|
212
|
-
|
|
213
|
-
void runPromise.finally(() => brokenShellSessions.delete(sessionKey)).catch(() => undefined);
|
|
233
|
+
quarantineShellSession(sessionKey, runPromise, abortCleanupPromise);
|
|
214
234
|
} else {
|
|
215
235
|
void runPromise.catch(() => undefined);
|
|
216
236
|
}
|
|
@@ -235,6 +255,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
235
255
|
? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
|
|
236
256
|
: "Command timed out";
|
|
237
257
|
resetSession = true;
|
|
258
|
+
if (shellSession) {
|
|
259
|
+
quarantineShellSession(sessionKey, runPromise, abortCleanupPromise);
|
|
260
|
+
}
|
|
238
261
|
return {
|
|
239
262
|
exitCode: undefined,
|
|
240
263
|
cancelled: true,
|
|
@@ -245,6 +268,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
245
268
|
// Handle cancellation
|
|
246
269
|
if (winner.result.cancelled) {
|
|
247
270
|
resetSession = true;
|
|
271
|
+
if (shellSession) {
|
|
272
|
+
quarantineShellSession(sessionKey, runPromise, abortCleanupPromise);
|
|
273
|
+
}
|
|
248
274
|
return {
|
|
249
275
|
exitCode: undefined,
|
|
250
276
|
cancelled: true,
|
|
@@ -101,11 +101,11 @@ export type CustomToolSessionEvent =
|
|
|
101
101
|
| {
|
|
102
102
|
reason: "auto_compaction_start";
|
|
103
103
|
trigger: "threshold" | "overflow" | "idle" | "incomplete";
|
|
104
|
-
action: "context-full" | "handoff" | "shake"
|
|
104
|
+
action: "context-full" | "handoff" | "shake";
|
|
105
105
|
}
|
|
106
106
|
| {
|
|
107
107
|
reason: "auto_compaction_end";
|
|
108
|
-
action: "context-full" | "handoff" | "shake"
|
|
108
|
+
action: "context-full" | "handoff" | "shake";
|
|
109
109
|
result: CompactionResult | undefined;
|
|
110
110
|
aborted: boolean;
|
|
111
111
|
willRetry: boolean;
|
|
@@ -5,7 +5,8 @@ import type * as fs1 from "node:fs";
|
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { ImageContent, Model, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import * as PiCodingAgent from "@oh-my-pi/pi-coding-agent";
|
|
9
10
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
10
11
|
import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import * as Zod from "zod/v4";
|
|
@@ -22,6 +23,7 @@ import * as TypeBox from "../typebox";
|
|
|
22
23
|
|
|
23
24
|
import { resolvePath } from "../utils";
|
|
24
25
|
import type {
|
|
26
|
+
AssistantThinkingRenderer,
|
|
25
27
|
Extension,
|
|
26
28
|
ExtensionAPI,
|
|
27
29
|
ExtensionContext,
|
|
@@ -29,6 +31,7 @@ import type {
|
|
|
29
31
|
ExtensionRuntime as IExtensionRuntime,
|
|
30
32
|
LoadExtensionsResult,
|
|
31
33
|
MessageRenderer,
|
|
34
|
+
ProviderConfig,
|
|
32
35
|
RegisteredCommand,
|
|
33
36
|
ToolDefinition,
|
|
34
37
|
} from "./types";
|
|
@@ -55,8 +58,7 @@ export class ExtensionRuntimeNotInitializedError extends Error {
|
|
|
55
58
|
*/
|
|
56
59
|
export class ExtensionRuntime implements IExtensionRuntime {
|
|
57
60
|
flagValues = new Map<string, boolean | string>();
|
|
58
|
-
pendingProviderRegistrations: Array<{ name: string; config:
|
|
59
|
-
[];
|
|
61
|
+
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; sourceId: string }> = [];
|
|
60
62
|
|
|
61
63
|
sendMessage(): void {
|
|
62
64
|
throw new ExtensionRuntimeNotInitializedError();
|
|
@@ -123,12 +125,12 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
123
125
|
readonly flagValues = new Map<string, boolean | string>();
|
|
124
126
|
readonly pendingProviderRegistrations: Array<{
|
|
125
127
|
name: string;
|
|
126
|
-
config:
|
|
128
|
+
config: ProviderConfig;
|
|
127
129
|
sourceId: string;
|
|
128
130
|
}> = [];
|
|
129
131
|
|
|
130
132
|
constructor(
|
|
131
|
-
public readonly pi: typeof
|
|
133
|
+
public readonly pi: typeof PiCodingAgent,
|
|
132
134
|
private readonly extension: Extension,
|
|
133
135
|
private readonly runtime: IExtensionRuntime,
|
|
134
136
|
private readonly cwd: string,
|
|
@@ -141,10 +143,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
141
143
|
this.extension.handlers.set(event, list);
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
registerTool<
|
|
145
|
-
TParams extends import("@oh-my-pi/pi-ai").TSchema = import("@oh-my-pi/pi-ai").TSchema,
|
|
146
|
-
TDetails = unknown,
|
|
147
|
-
>(tool: ToolDefinition<TParams, TDetails>): void {
|
|
146
|
+
registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void {
|
|
148
147
|
this.extension.tools.set(tool.name, {
|
|
149
148
|
definition: tool,
|
|
150
149
|
extensionPath: this.extension.path,
|
|
@@ -190,6 +189,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
190
189
|
this.extension.messageRenderers.set(customType, renderer as MessageRenderer);
|
|
191
190
|
}
|
|
192
191
|
|
|
192
|
+
registerAssistantThinkingRenderer(renderer: AssistantThinkingRenderer): void {
|
|
193
|
+
this.extension.assistantThinkingRenderers.push(renderer);
|
|
194
|
+
}
|
|
195
|
+
|
|
193
196
|
getFlag(name: string): boolean | string | undefined {
|
|
194
197
|
if (!this.extension.flags.has(name)) return undefined;
|
|
195
198
|
return this.runtime.flagValues.get(name);
|
|
@@ -253,7 +256,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
253
256
|
return this.runtime.setSessionName(name);
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
registerProvider(name: string, config:
|
|
259
|
+
registerProvider(name: string, config: ProviderConfig): void {
|
|
257
260
|
this.runtime.pendingProviderRegistrations.push({ name, config, sourceId: this.extension.path });
|
|
258
261
|
}
|
|
259
262
|
}
|
|
@@ -267,6 +270,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
|
|
|
267
270
|
resolvedPath,
|
|
268
271
|
handlers: new Map(),
|
|
269
272
|
tools: new Map(),
|
|
273
|
+
assistantThinkingRenderers: [],
|
|
270
274
|
messageRenderers: new Map(),
|
|
271
275
|
commands: new Map(),
|
|
272
276
|
flags: new Map(),
|
|
@@ -293,13 +297,7 @@ async function loadExtension(
|
|
|
293
297
|
}
|
|
294
298
|
|
|
295
299
|
const extension = createExtension(extensionPath, resolvedPath);
|
|
296
|
-
const api = new ConcreteExtensionAPI(
|
|
297
|
-
await import("@oh-my-pi/pi-coding-agent"),
|
|
298
|
-
extension,
|
|
299
|
-
runtime,
|
|
300
|
-
cwd,
|
|
301
|
-
eventBus,
|
|
302
|
-
);
|
|
300
|
+
const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
|
|
303
301
|
await factory(api);
|
|
304
302
|
|
|
305
303
|
return { extension, error: null };
|
|
@@ -320,7 +318,7 @@ export async function loadExtensionFromFactory(
|
|
|
320
318
|
name = "<inline>",
|
|
321
319
|
): Promise<Extension> {
|
|
322
320
|
const extension = createExtension(name, name);
|
|
323
|
-
const api = new ConcreteExtensionAPI(
|
|
321
|
+
const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
|
|
324
322
|
await factory(api);
|
|
325
323
|
return extension;
|
|
326
324
|
}
|
|
@@ -10,6 +10,7 @@ import { type Theme, theme } from "../../modes/theme/theme";
|
|
|
10
10
|
import type { SessionManager } from "../../session/session-manager";
|
|
11
11
|
import type {
|
|
12
12
|
AfterProviderResponseEvent,
|
|
13
|
+
AssistantThinkingRenderer,
|
|
13
14
|
BeforeAgentStartEvent,
|
|
14
15
|
BeforeAgentStartEventResult,
|
|
15
16
|
BeforeProviderRequestEvent,
|
|
@@ -343,22 +344,22 @@ export class ExtensionRunner {
|
|
|
343
344
|
this.runtime.flagValues.set(name, value);
|
|
344
345
|
}
|
|
345
346
|
|
|
346
|
-
static readonly #RESERVED_SHORTCUTS =
|
|
347
|
-
"ctrl+c",
|
|
348
|
-
"ctrl+d",
|
|
349
|
-
"ctrl+z",
|
|
350
|
-
"ctrl+k",
|
|
351
|
-
"ctrl+p",
|
|
352
|
-
"ctrl+l",
|
|
353
|
-
"ctrl+o",
|
|
354
|
-
"ctrl+t",
|
|
355
|
-
"ctrl+g",
|
|
356
|
-
"shift+tab",
|
|
357
|
-
"shift+ctrl+p",
|
|
358
|
-
"alt+enter",
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
347
|
+
static readonly #RESERVED_SHORTCUTS: Record<string, true> = {
|
|
348
|
+
"ctrl+c": true,
|
|
349
|
+
"ctrl+d": true,
|
|
350
|
+
"ctrl+z": true,
|
|
351
|
+
"ctrl+k": true,
|
|
352
|
+
"ctrl+p": true,
|
|
353
|
+
"ctrl+l": true,
|
|
354
|
+
"ctrl+o": true,
|
|
355
|
+
"ctrl+t": true,
|
|
356
|
+
"ctrl+g": true,
|
|
357
|
+
"shift+tab": true,
|
|
358
|
+
"shift+ctrl+p": true,
|
|
359
|
+
"alt+enter": true,
|
|
360
|
+
escape: true,
|
|
361
|
+
enter: true,
|
|
362
|
+
};
|
|
362
363
|
|
|
363
364
|
getShortcuts(): Map<KeyId, ExtensionShortcut> {
|
|
364
365
|
const allShortcuts = new Map<KeyId, ExtensionShortcut>();
|
|
@@ -366,7 +367,7 @@ export class ExtensionRunner {
|
|
|
366
367
|
for (const [key, shortcut] of ext.shortcuts) {
|
|
367
368
|
const normalizedKey = key.toLowerCase() as KeyId;
|
|
368
369
|
|
|
369
|
-
if (ExtensionRunner.#RESERVED_SHORTCUTS
|
|
370
|
+
if (ExtensionRunner.#RESERVED_SHORTCUTS[normalizedKey]) {
|
|
370
371
|
logger.warn("Extension shortcut conflicts with built-in shortcut", {
|
|
371
372
|
key,
|
|
372
373
|
extensionPath: shortcut.extensionPath,
|
|
@@ -419,6 +420,10 @@ export class ExtensionRunner {
|
|
|
419
420
|
return undefined;
|
|
420
421
|
}
|
|
421
422
|
|
|
423
|
+
getAssistantThinkingRenderers(): AssistantThinkingRenderer[] {
|
|
424
|
+
return this.extensions.flatMap(ext => ext.assistantThinkingRenderers);
|
|
425
|
+
}
|
|
426
|
+
|
|
422
427
|
getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
|
|
423
428
|
this.#commandDiagnostics = [];
|
|
424
429
|
|
|
@@ -25,6 +25,8 @@ import type {
|
|
|
25
25
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
26
26
|
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
|
27
27
|
import type { AutocompleteItem, Component, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
28
|
+
import type { logger as PiLogger } from "@oh-my-pi/pi-utils";
|
|
29
|
+
import type * as Zod from "zod/v4";
|
|
28
30
|
import type { KeybindingsManager } from "../../config/keybindings";
|
|
29
31
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
30
32
|
import type { EditToolDetails } from "../../edit";
|
|
@@ -81,6 +83,7 @@ import type {
|
|
|
81
83
|
TurnStartEvent,
|
|
82
84
|
} from "../shared-events";
|
|
83
85
|
import type { SlashCommandInfo } from "../slash-commands";
|
|
86
|
+
import type * as TypeBox from "../typebox";
|
|
84
87
|
|
|
85
88
|
export type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
|
|
86
89
|
export type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
@@ -90,6 +93,17 @@ export type { AgentToolResult, AgentToolUpdateCallback };
|
|
|
90
93
|
// UI Context
|
|
91
94
|
// ============================================================================
|
|
92
95
|
|
|
96
|
+
export interface ExtensionUISelectOption {
|
|
97
|
+
label: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ExtensionUISelectItem = string | ExtensionUISelectOption;
|
|
102
|
+
|
|
103
|
+
export function getExtensionUISelectOptionLabel(option: ExtensionUISelectItem): string {
|
|
104
|
+
return typeof option === "string" ? option : option.label;
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
/**
|
|
94
108
|
* UI dialog options for extensions.
|
|
95
109
|
*/
|
|
@@ -135,8 +149,12 @@ export type ExtensionWidgetContent = string[] | ExtensionUiComponentFactory | un
|
|
|
135
149
|
// and may be invoked from event handlers that have already taken the agent
|
|
136
150
|
// loop's lock — hooks intentionally cannot.
|
|
137
151
|
export interface ExtensionUIContext {
|
|
138
|
-
/** Show a selector and return the
|
|
139
|
-
select(
|
|
152
|
+
/** Show a selector and return the selected label, even when an option also includes a description. */
|
|
153
|
+
select(
|
|
154
|
+
title: string,
|
|
155
|
+
options: ExtensionUISelectItem[],
|
|
156
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
157
|
+
): Promise<string | undefined>;
|
|
140
158
|
|
|
141
159
|
/** Show a confirmation dialog. */
|
|
142
160
|
confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean>;
|
|
@@ -799,6 +817,18 @@ export type MessageRenderer<T = unknown> = (
|
|
|
799
817
|
theme: Theme,
|
|
800
818
|
) => Component | undefined;
|
|
801
819
|
|
|
820
|
+
export interface AssistantThinkingRenderContext {
|
|
821
|
+
contentIndex: number;
|
|
822
|
+
thinkingIndex: number;
|
|
823
|
+
text: string;
|
|
824
|
+
requestRender(): void;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export type AssistantThinkingRenderer = (
|
|
828
|
+
context: AssistantThinkingRenderContext,
|
|
829
|
+
theme: Theme,
|
|
830
|
+
) => Component | undefined;
|
|
831
|
+
|
|
802
832
|
// ============================================================================
|
|
803
833
|
// Command Registration
|
|
804
834
|
// ============================================================================
|
|
@@ -830,13 +860,13 @@ export interface ExtensionAPI {
|
|
|
830
860
|
// =========================================================================
|
|
831
861
|
|
|
832
862
|
/** File logger for error/warning/debug messages */
|
|
833
|
-
logger: typeof
|
|
863
|
+
logger: typeof PiLogger;
|
|
834
864
|
|
|
835
865
|
/** Injected zod-backed typebox shim for legacy `Type.Object(...)` parameter authoring. */
|
|
836
|
-
typebox: typeof
|
|
866
|
+
typebox: typeof TypeBox;
|
|
837
867
|
|
|
838
868
|
/** Injected zod module for Zod-authored extension tools (canonical going forward). */
|
|
839
|
-
zod: typeof
|
|
869
|
+
zod: typeof Zod;
|
|
840
870
|
|
|
841
871
|
/** Injected pi-coding-agent exports for accessing SDK utilities */
|
|
842
872
|
pi: typeof piCodingAgent;
|
|
@@ -950,6 +980,9 @@ export interface ExtensionAPI {
|
|
|
950
980
|
/** Register a custom renderer for CustomMessageEntry. */
|
|
951
981
|
registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
|
|
952
982
|
|
|
983
|
+
/** Register a renderer for assistant thinking blocks. Rendered after the original thinking text. */
|
|
984
|
+
registerAssistantThinkingRenderer(renderer: AssistantThinkingRenderer): void;
|
|
985
|
+
|
|
953
986
|
// =========================================================================
|
|
954
987
|
// Actions
|
|
955
988
|
// =========================================================================
|
|
@@ -1232,6 +1265,7 @@ export interface Extension {
|
|
|
1232
1265
|
label?: string;
|
|
1233
1266
|
handlers: Map<string, HandlerFn[]>;
|
|
1234
1267
|
tools: Map<string, RegisteredTool<any, any>>;
|
|
1268
|
+
assistantThinkingRenderers: AssistantThinkingRenderer[];
|
|
1235
1269
|
messageRenderers: Map<string, MessageRenderer>;
|
|
1236
1270
|
commands: Map<string, RegisteredCommand>;
|
|
1237
1271
|
flags: Map<string, ExtensionFlag>;
|
|
@@ -204,13 +204,13 @@ export interface TurnEndEvent {
|
|
|
204
204
|
export interface AutoCompactionStartEvent {
|
|
205
205
|
type: "auto_compaction_start";
|
|
206
206
|
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
207
|
-
action: "context-full" | "handoff" | "shake"
|
|
207
|
+
action: "context-full" | "handoff" | "shake";
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/** Fired when auto-compaction ends */
|
|
211
211
|
export interface AutoCompactionEndEvent {
|
|
212
212
|
type: "auto_compaction_end";
|
|
213
|
-
action: "context-full" | "handoff" | "shake"
|
|
213
|
+
action: "context-full" | "handoff" | "shake";
|
|
214
214
|
result: CompactionResult | undefined;
|
|
215
215
|
aborted: boolean;
|
|
216
216
|
willRetry: boolean;
|