@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +159 -182
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +11 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- 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/plugins/legacy-pi-compat.d.ts +11 -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/main.d.ts +5 -0
- 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 +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +10 -1
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -3
- 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 +5 -3
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +12 -3
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- 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 +0 -9
- package/dist/types/tools/index.d.ts +5 -12
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tui/output-block.d.ts +4 -3
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +12 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +148 -4
- 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/concurrency-bridge.ts +34 -0
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/eval/py/prelude.py +23 -15
- package/src/exec/bash-executor.ts +31 -5
- 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/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +19 -7
- package/src/modes/components/hook-selector.ts +229 -44
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +36 -7
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +6 -4
- package/src/modes/controllers/input-controller.ts +19 -16
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +127 -16
- 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 +7 -3
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +127 -57
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/ask.ts +74 -32
- package/src/tools/browser/tab-worker.ts +8 -5
- 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 +5 -29
- package/src/tools/index.ts +8 -38
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -3,6 +3,7 @@ import type { ToolSession } from "../../tools";
|
|
|
3
3
|
import { ToolError } from "../../tools/tool-errors";
|
|
4
4
|
import { EVAL_AGENT_BRIDGE_NAME, runEvalAgent } from "../agent-bridge";
|
|
5
5
|
import { EVAL_BUDGET_BRIDGE_NAME, type EvalBudgetResult, runEvalBudget } from "../budget-bridge";
|
|
6
|
+
import { EVAL_CONCURRENCY_BRIDGE_NAME, type EvalConcurrencyResult, runEvalConcurrency } from "../concurrency-bridge";
|
|
6
7
|
import { EVAL_LLM_BRIDGE_NAME, runEvalLlm } from "../llm-bridge";
|
|
7
8
|
import type { JsStatusEvent } from "./shared/types";
|
|
8
9
|
|
|
@@ -17,6 +18,7 @@ interface ToolBridgeOptions {
|
|
|
17
18
|
type ToolValue =
|
|
18
19
|
| string
|
|
19
20
|
| EvalBudgetResult
|
|
21
|
+
| EvalConcurrencyResult
|
|
20
22
|
| {
|
|
21
23
|
text: string;
|
|
22
24
|
details?: unknown;
|
|
@@ -114,6 +116,9 @@ export async function callSessionTool(name: string, args: unknown, options: Tool
|
|
|
114
116
|
if (name === EVAL_BUDGET_BRIDGE_NAME) {
|
|
115
117
|
return await runEvalBudget(args, options);
|
|
116
118
|
}
|
|
119
|
+
if (name === EVAL_CONCURRENCY_BRIDGE_NAME) {
|
|
120
|
+
return runEvalConcurrency(args, options);
|
|
121
|
+
}
|
|
117
122
|
const tool = getTool(options.session, name);
|
|
118
123
|
const normalizedArgs = normalizeArgs(args);
|
|
119
124
|
const toolCallId = `js-${name}-${crypto.randomUUID()}`;
|
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
|
}
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -503,27 +503,35 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
503
503
|
text = res.get("text") if isinstance(res, dict) else res
|
|
504
504
|
return json.loads(text) if schema is not None else text
|
|
505
505
|
|
|
506
|
-
def
|
|
507
|
-
"""
|
|
506
|
+
def _concurrency_limit():
|
|
507
|
+
"""Worker-pool ceiling from the host ``task.maxConcurrency`` setting.
|
|
508
|
+
|
|
509
|
+
An eval fan-out runs as wide as a ``task`` batch would. Returns ``0`` for
|
|
510
|
+
unbounded (run every item at once); falls back to ``0`` if the host
|
|
511
|
+
bridge is unreachable.
|
|
512
|
+
"""
|
|
508
513
|
try:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
snap = _bridge_call("__concurrency__", {}) or {}
|
|
515
|
+
n = int(snap.get("limit") or 0)
|
|
516
|
+
except Exception:
|
|
517
|
+
return 0
|
|
518
|
+
return n if n > 0 else 0
|
|
513
519
|
|
|
514
|
-
def _pool_map(items, fn
|
|
520
|
+
def _pool_map(items, fn):
|
|
515
521
|
"""Run ``fn`` over ``items`` through a bounded thread pool.
|
|
516
522
|
|
|
517
523
|
Preserves input order, barriers until every task settles, and raises the
|
|
518
524
|
lowest-index exception if any task failed. Each task runs inside a copy
|
|
519
525
|
of the submitting thread's context so the ``_CURRENT_RID`` ContextVar
|
|
520
|
-
propagates and bridge calls (agent(), tool.*, etc.) keep working.
|
|
526
|
+
propagates and bridge calls (agent(), tool.*, etc.) keep working. The
|
|
527
|
+
pool width tracks ``task.maxConcurrency`` (0 = run every item at once).
|
|
521
528
|
"""
|
|
522
529
|
import concurrent.futures, contextvars
|
|
523
530
|
items = list(items)
|
|
524
531
|
if not items:
|
|
525
532
|
return []
|
|
526
|
-
|
|
533
|
+
limit = _concurrency_limit()
|
|
534
|
+
workers = min(limit, len(items)) if limit > 0 else len(items)
|
|
527
535
|
results = [None] * len(items)
|
|
528
536
|
errors = {}
|
|
529
537
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
|
|
@@ -541,30 +549,30 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
541
549
|
raise errors[min(errors)]
|
|
542
550
|
return results
|
|
543
551
|
|
|
544
|
-
def parallel(thunks
|
|
552
|
+
def parallel(thunks):
|
|
545
553
|
"""Run zero-arg callables through a bounded pool, preserving input order.
|
|
546
554
|
|
|
547
555
|
Barriers until all finish; re-raises the lowest-index exception if any
|
|
548
|
-
thunk raised.
|
|
556
|
+
thunk raised. Pool width tracks the task tool's ``task.maxConcurrency``.
|
|
549
557
|
"""
|
|
550
558
|
thunks = list(thunks)
|
|
551
559
|
for t in thunks:
|
|
552
560
|
if not callable(t):
|
|
553
561
|
raise TypeError("parallel() expects an iterable of zero-arg callables")
|
|
554
|
-
return _pool_map(thunks, lambda t: t()
|
|
562
|
+
return _pool_map(thunks, lambda t: t())
|
|
555
563
|
|
|
556
|
-
def pipeline(items, *stages
|
|
564
|
+
def pipeline(items, *stages):
|
|
557
565
|
"""Map items left-to-right through one-arg stage callables.
|
|
558
566
|
|
|
559
567
|
Every item clears stage N before any item enters stage N+1 (barrier per
|
|
560
568
|
stage). Stage 1 receives the original item; later stages receive the
|
|
561
|
-
previous stage's result.
|
|
569
|
+
previous stage's result. Pool width tracks ``task.maxConcurrency``.
|
|
562
570
|
"""
|
|
563
571
|
current = list(items)
|
|
564
572
|
for stage in stages:
|
|
565
573
|
if not callable(stage):
|
|
566
574
|
raise TypeError("pipeline() stages must be callables")
|
|
567
|
-
current = _pool_map(current, stage
|
|
575
|
+
current = _pool_map(current, stage)
|
|
568
576
|
return current
|
|
569
577
|
|
|
570
578
|
def log(message):
|
|
@@ -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,
|
|
@@ -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>;
|