@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-xfpkakrn.md} +47 -0
- package/dist/cli.js +3891 -3885
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +16 -5
- package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
- package/dist/types/edit/hashline/diff.d.ts +4 -4
- package/dist/types/eval/executor-base.d.ts +27 -0
- package/dist/types/eval/js/context-manager.d.ts +7 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +64 -5
- package/dist/types/extensibility/extensions/types.d.ts +15 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/live/visualizer.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -3
- package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +22 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/codex-auto-reset.d.ts +182 -64
- package/dist/types/session/session-storage.d.ts +5 -2
- package/dist/types/session/session-tools.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +7 -1
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +27 -0
- package/dist/types/task/persisted-revive.d.ts +7 -0
- package/dist/types/tools/bash-interceptor.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +9 -1
- package/dist/types/tools/browser/launch.d.ts +20 -1
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/shell-tokenize.d.ts +11 -0
- package/dist/types/utils/image-loading.d.ts +2 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +6 -1
- package/src/config/settings-schema.ts +16 -3
- package/src/edit/hashline/block-resolver.ts +5 -5
- package/src/edit/hashline/diff.ts +22 -15
- package/src/edit/hashline/execute.ts +14 -13
- package/src/edit/renderer.ts +3 -3
- package/src/eval/executor-base.ts +39 -0
- package/src/eval/jl/executor.ts +13 -2
- package/src/eval/js/context-manager.ts +97 -14
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/py/executor.ts +55 -15
- package/src/eval/rb/executor.ts +8 -1
- package/src/extensibility/extensions/runner.ts +121 -4
- package/src/extensibility/extensions/types.ts +15 -0
- package/src/extensibility/extensions/wrapper.ts +19 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
- package/src/live/visualizer.ts +8 -1
- package/src/lsp/index.ts +51 -11
- package/src/main.ts +3 -0
- package/src/modes/acp/acp-agent.ts +16 -5
- package/src/modes/components/assistant-message.ts +5 -10
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/keybinding-hints.ts +4 -13
- package/src/modes/components/status-line/segments.ts +4 -5
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/input-controller.ts +18 -40
- package/src/modes/controllers/live-command-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +28 -11
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/theme/theme.ts +13 -6
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/sdk.ts +47 -16
- package/src/secrets/index.ts +83 -3
- package/src/secrets/obfuscator.ts +70 -15
- package/src/session/agent-session-types.ts +7 -1
- package/src/session/agent-session.ts +393 -112
- package/src/session/codex-auto-reset.ts +594 -123
- package/src/session/eval-runner.ts +2 -0
- package/src/session/messages.ts +1 -1
- package/src/session/session-maintenance.ts +48 -1
- package/src/session/session-manager.ts +4 -3
- package/src/session/session-stats.ts +14 -3
- package/src/session/session-storage.ts +47 -15
- package/src/session/session-tools.ts +30 -2
- package/src/session/turn-recovery.ts +15 -2
- package/src/slash-commands/builtin-registry.ts +21 -15
- package/src/slash-commands/helpers/reset-usage.ts +2 -0
- package/src/system-prompt.ts +13 -1
- package/src/task/executor.ts +168 -15
- package/src/task/persisted-revive.ts +26 -1
- package/src/telemetry-export.ts +8 -4
- package/src/tools/bash-interceptor.ts +85 -8
- package/src/tools/bash.ts +1 -1
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +60 -11
- package/src/tools/browser/registry.ts +18 -2
- package/src/tools/browser/tab-protocol.ts +8 -0
- package/src/tools/browser/tab-supervisor.ts +31 -4
- package/src/tools/browser/tab-worker.ts +32 -2
- package/src/tools/output-meta.ts +16 -1
- package/src/tools/shell-tokenize.ts +101 -0
- package/src/utils/image-loading.ts +8 -3
- package/src/utils/shell-snapshot.ts +31 -21
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
package/src/eval/py/executor.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
getRemainingTimeoutMs,
|
|
14
14
|
isCancellationError,
|
|
15
15
|
isTimedOutCancellation,
|
|
16
|
+
resolveOwnerScopedSessionKey,
|
|
17
|
+
type SessionOwners,
|
|
16
18
|
waitForPromiseWithCancellation,
|
|
17
19
|
} from "../executor-base";
|
|
18
20
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
@@ -154,8 +156,12 @@ interface PythonSession {
|
|
|
154
156
|
hasFallbackOwner: boolean;
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
interface StartingPythonSession extends SessionOwners {
|
|
160
|
+
promise: Promise<PythonSession>;
|
|
161
|
+
}
|
|
162
|
+
|
|
157
163
|
const sessions = new Map<string, PythonSession>();
|
|
158
|
-
const startingSessions = new Map<string,
|
|
164
|
+
const startingSessions = new Map<string, StartingPythonSession>();
|
|
159
165
|
const resettingSessions = new Map<string, Promise<void>>();
|
|
160
166
|
|
|
161
167
|
function normalizeSessionCwd(cwd: string): string {
|
|
@@ -252,10 +258,10 @@ async function acquireSession(
|
|
|
252
258
|
}
|
|
253
259
|
const starting = startingSessions.get(sessionKey);
|
|
254
260
|
if (starting) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return session;
|
|
261
|
+
attachSessionOwner(starting, sessionId, options.kernelOwnerId);
|
|
262
|
+
return await starting.promise;
|
|
258
263
|
}
|
|
264
|
+
let startingSession!: StartingPythonSession;
|
|
259
265
|
const startup = (async () => {
|
|
260
266
|
const kernel = await startKernel(cwd, options);
|
|
261
267
|
const session: PythonSession = {
|
|
@@ -264,19 +270,28 @@ async function acquireSession(
|
|
|
264
270
|
cwd,
|
|
265
271
|
kernel,
|
|
266
272
|
generation: 0,
|
|
267
|
-
ownerIds: new Set(),
|
|
268
|
-
hasFallbackOwner:
|
|
273
|
+
ownerIds: new Set(startingSession.ownerIds),
|
|
274
|
+
hasFallbackOwner: startingSession.hasFallbackOwner,
|
|
269
275
|
};
|
|
270
|
-
|
|
276
|
+
// Publish only while this startup still owns the key: owner disposal or
|
|
277
|
+
// a concurrent dispose-all may have already reaped the starting record,
|
|
278
|
+
// and publishing here would resurrect a kernel that was just torn down.
|
|
279
|
+
if (startingSessions.get(sessionKey) === startingSession) {
|
|
280
|
+
sessions.set(sessionKey, session);
|
|
281
|
+
}
|
|
271
282
|
return session;
|
|
272
283
|
})();
|
|
273
|
-
|
|
284
|
+
startingSession = {
|
|
285
|
+
ownerIds: new Set(),
|
|
286
|
+
hasFallbackOwner: false,
|
|
287
|
+
promise: startup,
|
|
288
|
+
};
|
|
289
|
+
attachSessionOwner(startingSession, sessionId, options.kernelOwnerId);
|
|
290
|
+
startingSessions.set(sessionKey, startingSession);
|
|
274
291
|
try {
|
|
275
|
-
|
|
276
|
-
attachSessionOwner(session, sessionId, options.kernelOwnerId);
|
|
277
|
-
return session;
|
|
292
|
+
return await startup;
|
|
278
293
|
} finally {
|
|
279
|
-
if (startingSessions.get(sessionKey) ===
|
|
294
|
+
if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
297
|
|
|
@@ -370,7 +385,8 @@ async function acquireLiveSessionKernel(
|
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
async function resetSession(sessionKey: string): Promise<void> {
|
|
373
|
-
const existing =
|
|
388
|
+
const existing =
|
|
389
|
+
sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
|
|
374
390
|
if (!existing) return;
|
|
375
391
|
existing.generation += 1;
|
|
376
392
|
sessions.delete(sessionKey);
|
|
@@ -382,7 +398,7 @@ async function resetSession(sessionKey: string): Promise<void> {
|
|
|
382
398
|
// ---------------------------------------------------------------------------
|
|
383
399
|
|
|
384
400
|
export async function disposeAllKernelSessions(): Promise<void> {
|
|
385
|
-
const pending = [...startingSessions.values()];
|
|
401
|
+
const pending = [...startingSessions.values()].map(starting => starting.promise);
|
|
386
402
|
startingSessions.clear();
|
|
387
403
|
const started = await Promise.allSettled(pending);
|
|
388
404
|
const all = [...sessions.entries()];
|
|
@@ -422,10 +438,28 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
|
|
|
422
438
|
}
|
|
423
439
|
session.ownerIds.delete(ownerId);
|
|
424
440
|
}
|
|
441
|
+
const startingToShutdown: StartingPythonSession[] = [];
|
|
442
|
+
for (const [sessionKey, starting] of [...startingSessions.entries()]) {
|
|
443
|
+
if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
|
|
444
|
+
if (starting.ownerIds.size === 1) {
|
|
445
|
+
startingSessions.delete(sessionKey);
|
|
446
|
+
startingToShutdown.push(starting);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
starting.ownerIds.delete(ownerId);
|
|
450
|
+
}
|
|
425
451
|
for (const session of toShutdown) {
|
|
426
452
|
session.generation += 1;
|
|
427
453
|
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
428
454
|
}
|
|
455
|
+
const started = await Promise.allSettled(startingToShutdown.map(starting => starting.promise));
|
|
456
|
+
for (const result of started) {
|
|
457
|
+
if (result.status !== "fulfilled") continue;
|
|
458
|
+
const session = result.value;
|
|
459
|
+
session.generation += 1;
|
|
460
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
461
|
+
toShutdown.push(session);
|
|
462
|
+
}
|
|
429
463
|
const results = await Promise.allSettled(toShutdown.map(session => shutdownInvalidatedSession(session)));
|
|
430
464
|
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
431
465
|
const session = toShutdown[i];
|
|
@@ -503,7 +537,13 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
|
|
|
503
537
|
|
|
504
538
|
async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
505
539
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
506
|
-
const sessionKey =
|
|
540
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
541
|
+
baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
|
|
542
|
+
ownerId: options.kernelOwnerId,
|
|
543
|
+
reset: options.reset === true,
|
|
544
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
545
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
546
|
+
});
|
|
507
547
|
if (options.bridge && !options.bridgeSessionId) {
|
|
508
548
|
options.bridgeSessionId = sessionId;
|
|
509
549
|
}
|
package/src/eval/rb/executor.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getRemainingTimeoutMs,
|
|
14
14
|
isCancellationError,
|
|
15
15
|
isTimedOutCancellation,
|
|
16
|
+
resolveOwnerScopedSessionKey,
|
|
16
17
|
waitForPromiseWithCancellation,
|
|
17
18
|
} from "../executor-base";
|
|
18
19
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
@@ -407,7 +408,13 @@ async function ensureToolBridge(options: RubyExecutorOptions): Promise<void> {
|
|
|
407
408
|
|
|
408
409
|
async function executeOnSession(code: string, cwd: string, options: RubyExecutorOptions): Promise<RubyResult> {
|
|
409
410
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
410
|
-
const sessionKey =
|
|
411
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
412
|
+
baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
|
|
413
|
+
ownerId: options.kernelOwnerId,
|
|
414
|
+
reset: options.reset === true,
|
|
415
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
416
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
417
|
+
});
|
|
411
418
|
if (options.bridge && !options.bridgeSessionId) {
|
|
412
419
|
options.bridgeSessionId = sessionId;
|
|
413
420
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentMessage,
|
|
6
|
+
AgentTool,
|
|
7
|
+
AgentToolContext,
|
|
8
|
+
AgentToolResult,
|
|
9
|
+
AgentToolUpdateCallback,
|
|
10
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
5
11
|
import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMetadata } from "@oh-my-pi/pi-ai";
|
|
6
12
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
7
13
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
@@ -407,10 +413,72 @@ export class ExtensionRunner {
|
|
|
407
413
|
return this.#emittedToolCalls.delete(`${toolCallId}:${toolName}`);
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Resolves a tool NAME to its native built-in implementation (the pre-extension-override,
|
|
418
|
+
* unwrapped tool) plus a factory for the `AgentToolContext` that native tool expects, or
|
|
419
|
+
* undefined when no native built-in of that name exists. Set by the SDK; backs same-tool
|
|
420
|
+
* `invokeTool`. The context factory is the same one the agent loop uses for tool execution, so a
|
|
421
|
+
* delegated native call sees the ordinary session tool context (ui, cwd, snapshot state, etc.).
|
|
422
|
+
*/
|
|
423
|
+
#nativeToolResolver?: (name: string) => { tool: AgentTool; makeContext: () => AgentToolContext } | undefined;
|
|
424
|
+
|
|
425
|
+
/** Wires the native-tool resolver used by {@link invokeNativeTool}. */
|
|
426
|
+
setNativeToolResolver(
|
|
427
|
+
resolve: (name: string) => { tool: AgentTool; makeContext: () => AgentToolContext } | undefined,
|
|
428
|
+
): void {
|
|
429
|
+
this.#nativeToolResolver = resolve;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Whether a native built-in of `name` is available to delegate to. */
|
|
433
|
+
hasNativeTool(name: string): boolean {
|
|
434
|
+
return this.#nativeToolResolver?.(name) !== undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Run the native built-in of `name` with `params` and return its result — the delegation target
|
|
439
|
+
* of a same-tool `ctx.invokeTool`. Calls the unwrapped native `execute` directly with the loop's
|
|
440
|
+
* ordinary tool context, so it inherits the caller's already-granted approval (the caller is the
|
|
441
|
+
* same tool) rather than re-running the gate. `depth` guards a wrapper that recurses into itself;
|
|
442
|
+
* it is per call chain (threaded from the caller), not session-global, so concurrent independent
|
|
443
|
+
* delegations do not interfere.
|
|
444
|
+
*/
|
|
445
|
+
async invokeNativeTool<TDetails = unknown>(
|
|
446
|
+
name: string,
|
|
447
|
+
params: Record<string, unknown>,
|
|
448
|
+
options?: {
|
|
449
|
+
signal?: AbortSignal;
|
|
450
|
+
onUpdate?: AgentToolUpdateCallback<TDetails>;
|
|
451
|
+
depth?: number;
|
|
452
|
+
/**
|
|
453
|
+
* The caller tool's own context. Reused for the native call so metadata the native tool
|
|
454
|
+
* reads — `toolCall` (write/edit LSP batch flushing) and provider metadata /
|
|
455
|
+
* `providerSafetyApproved` (computer) — is preserved. Falls back to a fresh session tool
|
|
456
|
+
* context only when the caller had none.
|
|
457
|
+
*/
|
|
458
|
+
callerContext?: AgentToolContext;
|
|
459
|
+
},
|
|
460
|
+
): Promise<AgentToolResult<TDetails>> {
|
|
461
|
+
const resolved = this.#nativeToolResolver?.(name);
|
|
462
|
+
if (!resolved) throw new Error(`invokeTool: no native built-in named "${name}" to delegate to`);
|
|
463
|
+
const depth = options?.depth ?? 0;
|
|
464
|
+
if (depth >= 8) {
|
|
465
|
+
throw new Error(`invokeTool: delegation depth exceeded 8 (recursive invokeTool for "${name}"?)`);
|
|
466
|
+
}
|
|
467
|
+
const toolCallId = `invoke-${name}-${Date.now().toString(36)}-${depth}`;
|
|
468
|
+
return (await resolved.tool.execute(
|
|
469
|
+
toolCallId,
|
|
470
|
+
params as never,
|
|
471
|
+
options?.signal,
|
|
472
|
+
options?.onUpdate as never,
|
|
473
|
+
options?.callerContext ?? resolved.makeContext(),
|
|
474
|
+
)) as AgentToolResult<TDetails>;
|
|
475
|
+
}
|
|
476
|
+
|
|
410
477
|
constructor(
|
|
411
478
|
private readonly extensions: Extension[],
|
|
412
479
|
private readonly runtime: ExtensionRuntime,
|
|
413
|
-
|
|
480
|
+
/** Ignored: `cwd` is always read live via the `cwd` getter below, not cached here. */
|
|
481
|
+
_initialCwd: string,
|
|
414
482
|
private readonly sessionManager: SessionManager,
|
|
415
483
|
private readonly modelRegistry: ModelRegistry,
|
|
416
484
|
getMemory?: () => MemoryRuntimeContext | undefined,
|
|
@@ -423,6 +491,24 @@ export class ExtensionRunner {
|
|
|
423
491
|
this.#getAsyncJobSnapshotFn = getAsyncJobSnapshot ?? (() => null);
|
|
424
492
|
}
|
|
425
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Live session directory, not a session-start snapshot: `/move`
|
|
496
|
+
* (`SessionManager.moveTo()`) relocates the owning session by updating
|
|
497
|
+
* `sessionManager`'s own `#cwd`, not a process-global. Reading it here
|
|
498
|
+
* via the getter — instead of caching the constructor-time value in a
|
|
499
|
+
* field — keeps every `ExtensionContext` built below in sync with this
|
|
500
|
+
* session's actual, current directory. Deliberately `sessionManager.getCwd()`
|
|
501
|
+
* rather than `getProjectDir()`: the latter is a single process-wide value
|
|
502
|
+
* that only the interactive TUI's `/move` handler happens to also update
|
|
503
|
+
* (`InteractiveModeContext#applyCwdChange`) — an SDK/ACP host running
|
|
504
|
+
* several concurrent sessions each with their own `cwd` (see
|
|
505
|
+
* `CreateAgentSessionOptions.cwd`) must never have one session's move
|
|
506
|
+
* leak into another's `ctx.cwd` by reading a shared global.
|
|
507
|
+
*/
|
|
508
|
+
get cwd(): string {
|
|
509
|
+
return this.sessionManager.getCwd();
|
|
510
|
+
}
|
|
511
|
+
|
|
426
512
|
initialize(
|
|
427
513
|
actions: ExtensionActions,
|
|
428
514
|
contextActions: ExtensionContextActions,
|
|
@@ -728,8 +814,27 @@ export class ExtensionRunner {
|
|
|
728
814
|
return undefined;
|
|
729
815
|
}
|
|
730
816
|
|
|
731
|
-
/**
|
|
732
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Creates an extension context, optionally scoped to a provider request model.
|
|
819
|
+
*
|
|
820
|
+
* `delegation` wires the same-tool `ctx.invokeTool` for a re-registered built-in: when `toolName`
|
|
821
|
+
* names an existing native built-in, the context carries an `invokeTool` that runs it (see
|
|
822
|
+
* {@link invokeNativeTool}). The rest inherits the wrapper's own call so a bare
|
|
823
|
+
* `ctx.invokeTool(params)` behaves like the outer call — `context` preserves `toolCall`/provider
|
|
824
|
+
* metadata, `signal`/`onUpdate` default to the wrapper's own channels so aborting the outer tool
|
|
825
|
+
* call stops the native one and native progress still streams, and `depth` bounds recursion per
|
|
826
|
+
* call chain. Explicit options passed to `invokeTool` override the inherited `signal`/`onUpdate`.
|
|
827
|
+
*/
|
|
828
|
+
createContext(
|
|
829
|
+
model?: Model,
|
|
830
|
+
delegation?: {
|
|
831
|
+
toolName: string;
|
|
832
|
+
depth?: number;
|
|
833
|
+
context?: AgentToolContext;
|
|
834
|
+
signal?: AbortSignal;
|
|
835
|
+
onUpdate?: AgentToolUpdateCallback;
|
|
836
|
+
},
|
|
837
|
+
): ExtensionContext {
|
|
733
838
|
const getModel = model ? () => model : this.#getModel;
|
|
734
839
|
return {
|
|
735
840
|
ui: this.#uiContext,
|
|
@@ -754,6 +859,18 @@ export class ExtensionRunner {
|
|
|
754
859
|
setInterval: (callback, ms, ...args) => this.#managedTimers.setInterval(callback, ms, ...args),
|
|
755
860
|
setTimeout: (callback, ms, ...args) => this.#managedTimers.setTimeout(callback, ms, ...args),
|
|
756
861
|
clearTimer: timer => this.#managedTimers.clear(timer),
|
|
862
|
+
invokeTool:
|
|
863
|
+
delegation !== undefined && this.hasNativeTool(delegation.toolName)
|
|
864
|
+
? (params, options) =>
|
|
865
|
+
this.invokeNativeTool(delegation.toolName, params, {
|
|
866
|
+
// Inherit the wrapper's own channels so a bare `ctx.invokeTool(params)` aborts
|
|
867
|
+
// and streams with the outer call. Explicit options win.
|
|
868
|
+
signal: options?.signal ?? delegation.signal,
|
|
869
|
+
onUpdate: options?.onUpdate ?? delegation.onUpdate,
|
|
870
|
+
depth: (delegation.depth ?? 0) + 1,
|
|
871
|
+
callerContext: delegation.context,
|
|
872
|
+
})
|
|
873
|
+
: undefined,
|
|
757
874
|
};
|
|
758
875
|
}
|
|
759
876
|
|
|
@@ -464,6 +464,21 @@ export interface ExtensionContext {
|
|
|
464
464
|
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer;
|
|
465
465
|
/** Clear a timer scheduled via {@link setInterval} or {@link setTimeout}. */
|
|
466
466
|
clearTimer(timer: Timer): void;
|
|
467
|
+
/**
|
|
468
|
+
* Run the NATIVE built-in implementation of the tool this handler re-registered, with `params`,
|
|
469
|
+
* and return its result. Lets a tool that re-registers a built-in (e.g. wrapping `write` to add
|
|
470
|
+
* logging or a policy check) delegate to the original instead of reimplementing it — the native
|
|
471
|
+
* tool performs its own side effects and internal bookkeeping.
|
|
472
|
+
*
|
|
473
|
+
* Delegation is same-tool only: it invokes the built-in of the SAME name as the registering tool,
|
|
474
|
+
* never an arbitrary target, so it cannot escalate past the approval already granted for this
|
|
475
|
+
* call. Present only when a native built-in of that name exists (undefined otherwise, e.g. for a
|
|
476
|
+
* net-new tool that shadows no built-in). Recursion is depth-guarded per call chain.
|
|
477
|
+
*/
|
|
478
|
+
invokeTool?<TDetails = unknown>(
|
|
479
|
+
params: Record<string, unknown>,
|
|
480
|
+
options?: { signal?: AbortSignal; onUpdate?: AgentToolUpdateCallback<TDetails> },
|
|
481
|
+
): Promise<AgentToolResult<TDetails>>;
|
|
467
482
|
}
|
|
468
483
|
|
|
469
484
|
/**
|
|
@@ -64,9 +64,26 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
|
64
64
|
params: any,
|
|
65
65
|
signal?: AbortSignal,
|
|
66
66
|
onUpdate?: AgentToolUpdateCallback<any>,
|
|
67
|
-
|
|
67
|
+
context?: AgentToolContext,
|
|
68
68
|
) {
|
|
69
|
-
|
|
69
|
+
// Bind the extension context to this tool's own name so `ctx.invokeTool` delegates to the
|
|
70
|
+
// native built-in of the same name (present only when this tool re-registers a built-in). The
|
|
71
|
+
// wrapper's own context, abort signal, and progress callback are inherited by the delegated
|
|
72
|
+
// call, so a bare `ctx.invokeTool(params)` keeps the caller's `toolCall`/provider metadata
|
|
73
|
+
// (write/edit LSP batching, computer safety acknowledgement), stops when the outer call is
|
|
74
|
+
// aborted, and still streams native progress.
|
|
75
|
+
return this.registeredTool.definition.execute(
|
|
76
|
+
toolCallId,
|
|
77
|
+
params,
|
|
78
|
+
signal,
|
|
79
|
+
onUpdate,
|
|
80
|
+
this.runner.createContext(undefined, {
|
|
81
|
+
toolName: this.registeredTool.definition.name,
|
|
82
|
+
context,
|
|
83
|
+
signal,
|
|
84
|
+
onUpdate,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
|
|
@@ -1416,11 +1416,11 @@ export function getPackageDir(): string {
|
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
1418
|
// Legacy pi's `@earendil-works/pi-coding-agent` re-exported `estimateTokens`
|
|
1419
|
-
// from its package root (via `./core/compaction/index.ts`). In
|
|
1420
|
-
// `@oh-my-pi/pi-agent-core/compaction`, and the coding-agent
|
|
1421
|
-
// not forward
|
|
1422
|
-
// check during validation (
|
|
1423
|
-
export { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
1419
|
+
// and `compact` from its package root (via `./core/compaction/index.ts`). In
|
|
1420
|
+
// omp both live in `@oh-my-pi/pi-agent-core/compaction`, and the coding-agent
|
|
1421
|
+
// barrel below does not forward them, so legacy extensions importing either
|
|
1422
|
+
// fail Bun's static export check during validation (issues #6583, #7174).
|
|
1423
|
+
export { compact, estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
1424
1424
|
|
|
1425
1425
|
// Same barrel gap for two more legacy package-root exports: pi re-exported the
|
|
1426
1426
|
// `CONFIG_DIR_NAME` constant and the CLI parser `parseArgs`. In omp
|
package/src/live/visualizer.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Component,
|
|
3
|
+
type KeyId,
|
|
3
4
|
matchesKey,
|
|
4
5
|
replaceTabs,
|
|
5
6
|
sliceWithWidth,
|
|
@@ -15,6 +16,8 @@ export type LivePhase = "connecting" | "listening" | "working" | "speaking" | "m
|
|
|
15
16
|
export interface LiveVisualizerOptions {
|
|
16
17
|
onStop(): void;
|
|
17
18
|
onToggleMute(): void;
|
|
19
|
+
/** Configured `app.live.toggle` chords that also end the call (Ctrl+L by default). */
|
|
20
|
+
stopKeys?: readonly KeyId[];
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
function normalizeTranscript(text: string): string {
|
|
@@ -100,7 +103,11 @@ export class LiveVisualizer implements Component {
|
|
|
100
103
|
|
|
101
104
|
/** Processes user keypresses. */
|
|
102
105
|
handleInput(data: string): void {
|
|
103
|
-
if (
|
|
106
|
+
if (
|
|
107
|
+
matchesKey(data, "escape") ||
|
|
108
|
+
matchesKey(data, "ctrl+c") ||
|
|
109
|
+
this.#options.stopKeys?.some(key => matchesKey(data, key))
|
|
110
|
+
) {
|
|
104
111
|
this.#options.onStop();
|
|
105
112
|
} else if (matchesKey(data, "space")) {
|
|
106
113
|
this.#options.onToggleMute();
|
package/src/lsp/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
AgentToolUpdateCallback,
|
|
8
8
|
ToolApprovalDecision,
|
|
9
9
|
} from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import { logger, once, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { isEnoent, logger, once, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import type { BunFile } from "bun";
|
|
12
12
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
13
13
|
import lspDescription from "../prompts/tools/lsp.md" with { type: "text" };
|
|
@@ -1114,11 +1114,14 @@ export async function writethroughNoop(
|
|
|
1114
1114
|
|
|
1115
1115
|
interface PendingWritethrough {
|
|
1116
1116
|
dst: string;
|
|
1117
|
-
content: string;
|
|
1118
1117
|
file?: BunFile;
|
|
1119
1118
|
changeType: FileChangeType;
|
|
1120
1119
|
}
|
|
1121
1120
|
|
|
1121
|
+
interface RunLspWritethroughOptions {
|
|
1122
|
+
contentAlreadyWritten?: boolean;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1122
1125
|
interface LspWritethroughBatchRequest {
|
|
1123
1126
|
id: string;
|
|
1124
1127
|
flush: boolean;
|
|
@@ -1317,12 +1320,16 @@ async function runLspWritethrough(
|
|
|
1317
1320
|
onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
|
|
1318
1321
|
signal: AbortSignal;
|
|
1319
1322
|
},
|
|
1323
|
+
runOptions?: RunLspWritethroughOptions,
|
|
1320
1324
|
): Promise<FileDiagnosticsResult | undefined> {
|
|
1321
1325
|
const { enableFormat, enableDiagnostics } = options;
|
|
1326
|
+
const contentAlreadyWritten = runOptions?.contentAlreadyWritten ?? false;
|
|
1322
1327
|
|
|
1323
1328
|
let finalContent = content;
|
|
1324
1329
|
const writeContent = async (value: string) => (file ? file.write(value) : Bun.write(dst, value));
|
|
1325
|
-
const getWritePromise = once(() =>
|
|
1330
|
+
const getWritePromise = once(() =>
|
|
1331
|
+
contentAlreadyWritten && finalContent === content ? Promise.resolve() : writeContent(finalContent),
|
|
1332
|
+
);
|
|
1326
1333
|
let writeNotified = false;
|
|
1327
1334
|
const notifyWriteCommitted = async (notifySignal: AbortSignal | undefined = signal) => {
|
|
1328
1335
|
if (writeNotified) return;
|
|
@@ -1383,7 +1390,7 @@ async function runLspWritethrough(
|
|
|
1383
1390
|
if (useCustomFormatter) {
|
|
1384
1391
|
// Custom linters operate on on-disk input; the shared pre-write also
|
|
1385
1392
|
// supports implementations that inspect the file before formatting.
|
|
1386
|
-
await writeContent(content);
|
|
1393
|
+
if (!contentAlreadyWritten) await writeContent(content);
|
|
1387
1394
|
const [formattedContent, capturedVersions] = await Promise.all([
|
|
1388
1395
|
formatContent(dst, content, cwd, customLinterServers, operationSignal),
|
|
1389
1396
|
minVersionsPromise,
|
|
@@ -1391,7 +1398,7 @@ async function runLspWritethrough(
|
|
|
1391
1398
|
finalContent = formattedContent;
|
|
1392
1399
|
minVersions = capturedVersions;
|
|
1393
1400
|
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
1394
|
-
await writeContent(finalContent);
|
|
1401
|
+
if (!contentAlreadyWritten || finalContent !== content) await writeContent(finalContent);
|
|
1395
1402
|
await notifyWriteCommitted(operationSignal);
|
|
1396
1403
|
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal, enableDiagnostics);
|
|
1397
1404
|
} else {
|
|
@@ -1485,6 +1492,14 @@ async function flushWritethroughBatch(
|
|
|
1485
1492
|
const results: Array<FileDiagnosticsResult | undefined> = [];
|
|
1486
1493
|
for (const entry of batch) {
|
|
1487
1494
|
const bundle = getDeferred?.(entry.dst);
|
|
1495
|
+
let content: string;
|
|
1496
|
+
try {
|
|
1497
|
+
content = await fs.promises.readFile(entry.dst, "utf8");
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (!isEnoent(error)) throw error;
|
|
1500
|
+
bundle?.finalize(undefined);
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1488
1503
|
const deferredInner =
|
|
1489
1504
|
bundle &&
|
|
1490
1505
|
({
|
|
@@ -1493,13 +1508,14 @@ async function flushWritethroughBatch(
|
|
|
1493
1508
|
} as const);
|
|
1494
1509
|
const diag = await runLspWritethrough(
|
|
1495
1510
|
entry.dst,
|
|
1496
|
-
|
|
1511
|
+
content,
|
|
1497
1512
|
cwd,
|
|
1498
1513
|
options,
|
|
1499
1514
|
entry.changeType,
|
|
1500
1515
|
signal,
|
|
1501
1516
|
entry.file,
|
|
1502
1517
|
deferredInner,
|
|
1518
|
+
{ contentAlreadyWritten: true },
|
|
1503
1519
|
);
|
|
1504
1520
|
bundle?.finalize(diag);
|
|
1505
1521
|
results.push(diag);
|
|
@@ -1545,14 +1561,38 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
1545
1561
|
return diagnostics;
|
|
1546
1562
|
}
|
|
1547
1563
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
if (!batch.flush) {
|
|
1564
|
+
// File commits are never deferred: the batch owns only LSP post-processing,
|
|
1565
|
+
// so a later flush cannot replay an obsolete whole-file snapshot.
|
|
1566
|
+
try {
|
|
1552
1567
|
await writethroughNoop(dst, content, signal, file);
|
|
1553
|
-
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
if (batch.flush) {
|
|
1570
|
+
const pending = writethroughBatches.get(batch.id);
|
|
1571
|
+
if (pending) {
|
|
1572
|
+
writethroughBatches.delete(batch.id);
|
|
1573
|
+
try {
|
|
1574
|
+
await flushWritethroughBatch(
|
|
1575
|
+
Array.from(pending.entries.values()),
|
|
1576
|
+
cwd,
|
|
1577
|
+
pending.options,
|
|
1578
|
+
signal,
|
|
1579
|
+
getDeferred,
|
|
1580
|
+
);
|
|
1581
|
+
} catch (flushError) {
|
|
1582
|
+
logger.warn("Failed to flush pending LSP batch after final write failure", {
|
|
1583
|
+
batchId: batch.id,
|
|
1584
|
+
error: flushError instanceof Error ? flushError.message : String(flushError),
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
throw error;
|
|
1554
1590
|
}
|
|
1555
1591
|
|
|
1592
|
+
const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
|
|
1593
|
+
state.entries.set(dst, { dst, file, changeType });
|
|
1594
|
+
if (!batch.flush) return undefined;
|
|
1595
|
+
|
|
1556
1596
|
writethroughBatches.delete(batch.id);
|
|
1557
1597
|
return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal, getDeferred);
|
|
1558
1598
|
};
|
package/src/main.ts
CHANGED
|
@@ -504,6 +504,7 @@ async function runInteractiveMode(
|
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
if (initialMessage !== undefined) {
|
|
507
|
+
session.maybeStartTitleGeneration(initialMessage);
|
|
507
508
|
try {
|
|
508
509
|
using _keepalive = new EventLoopKeepalive();
|
|
509
510
|
await session.prompt(initialMessage, { images: initialImages });
|
|
@@ -514,6 +515,7 @@ async function runInteractiveMode(
|
|
|
514
515
|
}
|
|
515
516
|
|
|
516
517
|
for (const message of initialMessages) {
|
|
518
|
+
session.maybeStartTitleGeneration(message);
|
|
517
519
|
try {
|
|
518
520
|
using _keepalive = new EventLoopKeepalive();
|
|
519
521
|
await session.prompt(message);
|
|
@@ -1608,6 +1610,7 @@ export async function runRootCommand(
|
|
|
1608
1610
|
modelRegistry,
|
|
1609
1611
|
settings: settingsInstance,
|
|
1610
1612
|
enableLsp: sessionOptions.enableLsp ?? true,
|
|
1613
|
+
eventBus,
|
|
1611
1614
|
}),
|
|
1612
1615
|
Math.trunc(Number(settingsInstance.get("task.agentIdleTtlMs") ?? 420_000) || 0),
|
|
1613
1616
|
);
|
|
@@ -296,7 +296,7 @@ function buildAcpSpeechModelsCatalog(): Record<string, unknown> {
|
|
|
296
296
|
async function elicitFromAcpClient(
|
|
297
297
|
connection: AgentSideConnection,
|
|
298
298
|
sessionId: string,
|
|
299
|
-
method: "select" | "confirm" | "input",
|
|
299
|
+
method: "select" | "confirm" | "input" | "editor",
|
|
300
300
|
message: string,
|
|
301
301
|
property: ElicitationPropertySchema,
|
|
302
302
|
dialogOptions: ExtensionUIDialogOptions | undefined,
|
|
@@ -377,9 +377,9 @@ function isAcceptedElicitation(
|
|
|
377
377
|
* symmetric with every other `sessionUpdate` call in this file
|
|
378
378
|
* (`record.session.sessionId` is always evaluated at emit time).
|
|
379
379
|
*
|
|
380
|
-
* The non-elicitation surface (custom components,
|
|
381
|
-
*
|
|
382
|
-
*
|
|
380
|
+
* The non-elicitation surface (custom components, theming, terminal
|
|
381
|
+
* input) remains stubbed — ACP clients render those themselves or not
|
|
382
|
+
* at all. Capability gating respects the client's `initialize`
|
|
383
383
|
* advertisement.
|
|
384
384
|
*/
|
|
385
385
|
export function createAcpExtensionUiContext(
|
|
@@ -443,7 +443,18 @@ export function createAcpExtensionUiContext(
|
|
|
443
443
|
pasteToEditor: () => {},
|
|
444
444
|
setEditorText: () => {},
|
|
445
445
|
getEditorText: () => "",
|
|
446
|
-
editor: async () =>
|
|
446
|
+
editor: async (title, prefill, dialogOptions) => {
|
|
447
|
+
if (!supportsForm) return undefined;
|
|
448
|
+
const value = await elicitFromAcpClient(
|
|
449
|
+
connection,
|
|
450
|
+
getSessionId(),
|
|
451
|
+
"editor",
|
|
452
|
+
title,
|
|
453
|
+
{ type: "string", ...(prefill ? { default: prefill } : {}) },
|
|
454
|
+
dialogOptions,
|
|
455
|
+
);
|
|
456
|
+
return typeof value === "string" ? value : undefined;
|
|
457
|
+
},
|
|
447
458
|
addAutocompleteProvider: () => {},
|
|
448
459
|
setEditorComponent: () => {},
|
|
449
460
|
get theme() {
|
|
@@ -15,6 +15,7 @@ import chalk from "chalk";
|
|
|
15
15
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
16
16
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
17
17
|
import { expandKeyHint, getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
18
|
+
import { convertImageToPng } from "../../utils/image-loading";
|
|
18
19
|
import { canonicalizeMessage, formatThinkingForDisplay, hasDisplayableThinking } from "../../utils/thinking-display";
|
|
19
20
|
import { resolveAssistantErrorPresentation } from "../utils/transcript-render-helpers";
|
|
20
21
|
import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cache-invalidation-marker";
|
|
@@ -607,16 +608,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
607
608
|
if (image.mimeType === "image/png") continue;
|
|
608
609
|
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
|
|
609
610
|
this.#kittyConversionsInFlight.add(key);
|
|
610
|
-
|
|
611
|
-
.
|
|
612
|
-
.toBase64()
|
|
613
|
-
.then(data => {
|
|
611
|
+
convertImageToPng(image)
|
|
612
|
+
.then(converted => {
|
|
614
613
|
this.#kittyConversionsInFlight.delete(key);
|
|
615
|
-
this.#convertedKittyImages.set(key,
|
|
616
|
-
type: "image",
|
|
617
|
-
data,
|
|
618
|
-
mimeType: "image/png",
|
|
619
|
-
});
|
|
614
|
+
this.#convertedKittyImages.set(key, converted);
|
|
620
615
|
if (this.#lastMessage) {
|
|
621
616
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
622
617
|
}
|
|
@@ -871,7 +866,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
871
866
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
872
867
|
const trimmed = content.text.trim();
|
|
873
868
|
const mdOptions = this.#textColorTransform ? { color: this.#textColorTransform } : undefined;
|
|
874
|
-
const md = new Markdown(trimmed, 1, 0, getMarkdownTheme(), mdOptions);
|
|
869
|
+
const md = new Markdown(trimmed, 1, 0, getMarkdownTheme(), mdOptions, 0);
|
|
875
870
|
this.#contentContainer.addChild(md);
|
|
876
871
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
877
872
|
hasRenderedContent = true;
|
|
@@ -46,7 +46,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
46
46
|
"app.clear": ["ctrl+c"],
|
|
47
47
|
"app.exit": ["ctrl+d"],
|
|
48
48
|
"app.suspend": ["ctrl+z"],
|
|
49
|
-
"app.display.reset": ["
|
|
49
|
+
"app.display.reset": ["alt+l"],
|
|
50
50
|
"app.thinking.cycle": ["shift+tab"],
|
|
51
51
|
"app.model.cycleForward": ["ctrl+p"],
|
|
52
52
|
"app.model.cycleBackward": ["shift+ctrl+p"],
|