@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.3
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 +61 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-c5hpqt9r.md} +61 -0
- package/dist/cli.js +3856 -3850
- 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/ask-dialog.d.ts +0 -1
- 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 +50 -1
- package/dist/types/tools/browser/registry.d.ts +7 -0
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -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/launch/broker.ts +59 -41
- 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/ask-dialog.ts +65 -23
- 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 +394 -113
- 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 +50 -3
- 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 +12 -6
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +110 -16
- package/src/tools/browser/registry.ts +140 -31
- package/src/tools/browser/shared-daemon.ts +190 -0
- 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/browser.ts +1 -1
- 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/codex.ts +31 -6
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
package/src/launch/broker.ts
CHANGED
|
@@ -342,6 +342,15 @@ class DaemonBroker {
|
|
|
342
342
|
readonly #token: string;
|
|
343
343
|
readonly #idleGraceMs: number;
|
|
344
344
|
readonly #records = new Map<string, ManagedDaemon>();
|
|
345
|
+
/**
|
|
346
|
+
* Names reserved by an in-flight `start` before its record lands in
|
|
347
|
+
* `#records`. Requests dispatch concurrently, and `#start` awaits (cwd stat,
|
|
348
|
+
* log open) between the duplicate check and the record insert; without a
|
|
349
|
+
* synchronous reservation two clients can both pass the check and spawn
|
|
350
|
+
* duplicate processes — one exits on a held resource (e.g. a Chromium
|
|
351
|
+
* profile lock) or keeps running untracked.
|
|
352
|
+
*/
|
|
353
|
+
readonly #startingNames = new Set<string>();
|
|
345
354
|
readonly #clients = new Set<net.Socket>();
|
|
346
355
|
readonly #finished = Promise.withResolvers<void>();
|
|
347
356
|
readonly #sockets = new Set<net.Socket>();
|
|
@@ -500,50 +509,59 @@ class DaemonBroker {
|
|
|
500
509
|
) {
|
|
501
510
|
throw new Error('Windows batch files require application "cmd.exe" with the batch path after "/c"');
|
|
502
511
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (existing && !terminalState(existing.snapshot.state)) {
|
|
506
|
-
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
512
|
+
if (this.#startingNames.has(spec.name)) {
|
|
513
|
+
throw new Error(`Daemon ${spec.name} is already starting`);
|
|
507
514
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
515
|
+
this.#startingNames.add(spec.name);
|
|
516
|
+
let record: ManagedDaemon;
|
|
517
|
+
try {
|
|
518
|
+
const existing = this.#records.get(spec.name);
|
|
519
|
+
if (existing) await this.#refreshDetached(existing);
|
|
520
|
+
if (existing && !terminalState(existing.snapshot.state)) {
|
|
521
|
+
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
522
|
+
}
|
|
523
|
+
if (spec.ready?.log) {
|
|
524
|
+
try {
|
|
525
|
+
new RegExp(spec.ready.log, "u");
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw new Error(`Invalid readiness regex: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
}
|
|
513
529
|
}
|
|
530
|
+
const stat = await fs.stat(spec.cwd);
|
|
531
|
+
if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
|
|
532
|
+
const dir = path.join(this.#runtimeDir, "daemons", spec.name);
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
record = {
|
|
535
|
+
spec,
|
|
536
|
+
snapshot: {
|
|
537
|
+
name: spec.name,
|
|
538
|
+
id: crypto.randomUUID(),
|
|
539
|
+
state: "starting",
|
|
540
|
+
createdAt: now,
|
|
541
|
+
startedAt: now,
|
|
542
|
+
restartCount: 0,
|
|
543
|
+
outputBytes: 0,
|
|
544
|
+
owner,
|
|
545
|
+
persist: spec.persist,
|
|
546
|
+
detached: spec.detached,
|
|
547
|
+
},
|
|
548
|
+
dir,
|
|
549
|
+
log: await DaemonLog.open(dir),
|
|
550
|
+
generation: 0,
|
|
551
|
+
stopRequested: false,
|
|
552
|
+
logReady: !spec.ready?.log,
|
|
553
|
+
portReady: spec.ready?.port === undefined,
|
|
554
|
+
readinessBuffer: "",
|
|
555
|
+
outputOffset: 0,
|
|
556
|
+
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
557
|
+
consecutiveFailures: 0,
|
|
558
|
+
persistQueue: Promise.resolve(),
|
|
559
|
+
};
|
|
560
|
+
syncReadyPending(record);
|
|
561
|
+
this.#records.set(spec.name, record);
|
|
562
|
+
} finally {
|
|
563
|
+
this.#startingNames.delete(spec.name);
|
|
514
564
|
}
|
|
515
|
-
const stat = await fs.stat(spec.cwd);
|
|
516
|
-
if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
|
|
517
|
-
const dir = path.join(this.#runtimeDir, "daemons", spec.name);
|
|
518
|
-
const now = Date.now();
|
|
519
|
-
const record: ManagedDaemon = {
|
|
520
|
-
spec,
|
|
521
|
-
snapshot: {
|
|
522
|
-
name: spec.name,
|
|
523
|
-
id: crypto.randomUUID(),
|
|
524
|
-
state: "starting",
|
|
525
|
-
createdAt: now,
|
|
526
|
-
startedAt: now,
|
|
527
|
-
restartCount: 0,
|
|
528
|
-
outputBytes: 0,
|
|
529
|
-
owner,
|
|
530
|
-
persist: spec.persist,
|
|
531
|
-
detached: spec.detached,
|
|
532
|
-
},
|
|
533
|
-
dir,
|
|
534
|
-
log: await DaemonLog.open(dir),
|
|
535
|
-
generation: 0,
|
|
536
|
-
stopRequested: false,
|
|
537
|
-
logReady: !spec.ready?.log,
|
|
538
|
-
portReady: spec.ready?.port === undefined,
|
|
539
|
-
readinessBuffer: "",
|
|
540
|
-
outputOffset: 0,
|
|
541
|
-
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
542
|
-
consecutiveFailures: 0,
|
|
543
|
-
persistQueue: Promise.resolve(),
|
|
544
|
-
};
|
|
545
|
-
syncReadyPending(record);
|
|
546
|
-
this.#records.set(spec.name, record);
|
|
547
565
|
await this.#launch(record);
|
|
548
566
|
let readyTimedOut = false;
|
|
549
567
|
if (spec.ready && !terminalState(record.snapshot.state)) {
|
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 {
|
|
|
15
15
|
wrapTextWithAnsi,
|
|
16
16
|
} from "@oh-my-pi/pi-tui";
|
|
17
17
|
import type {
|
|
18
|
+
ExtensionAskDialogOption,
|
|
18
19
|
ExtensionAskDialogQuestion,
|
|
19
20
|
ExtensionAskDialogResultItem,
|
|
20
21
|
ExtensionAskDialogSubmitResult,
|
|
@@ -337,6 +338,45 @@ function renderRowLabel(
|
|
|
337
338
|
return lines;
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Coerce untrusted dialog questions into a render-safe shape. The live ask
|
|
343
|
+
* dialog is reached from the public `askDialog` extension surface and from
|
|
344
|
+
* streamed tool args, where a question entry can arrive with a missing or
|
|
345
|
+
* non-string `question` field. The render helpers (`replaceTabs`,
|
|
346
|
+
* `renderQuestionTitle`, `questionTabLabel`) assume strings, so a malformed
|
|
347
|
+
* entry throws and takes down the whole TUI render loop. Mirrors
|
|
348
|
+
* `normalizeRenderQuestions` on the transcript path.
|
|
349
|
+
*/
|
|
350
|
+
function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]): ExtensionAskDialogQuestion[] {
|
|
351
|
+
if (!Array.isArray(questions)) return [];
|
|
352
|
+
const out: ExtensionAskDialogQuestion[] = [];
|
|
353
|
+
for (const entry of questions) {
|
|
354
|
+
if (!entry || typeof entry !== "object") continue;
|
|
355
|
+
const q = entry as Partial<ExtensionAskDialogQuestion>;
|
|
356
|
+
const options: ExtensionAskDialogOption[] = [];
|
|
357
|
+
if (Array.isArray(q.options)) {
|
|
358
|
+
for (const opt of q.options) {
|
|
359
|
+
if (!opt || typeof opt !== "object") continue;
|
|
360
|
+
const o = opt as Partial<ExtensionAskDialogOption>;
|
|
361
|
+
options.push({
|
|
362
|
+
label: typeof o.label === "string" ? o.label : "",
|
|
363
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
364
|
+
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
out.push({
|
|
369
|
+
id: typeof q.id === "string" ? q.id : "?",
|
|
370
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
371
|
+
...(typeof q.header === "string" ? { header: q.header } : {}),
|
|
372
|
+
options,
|
|
373
|
+
...(typeof q.multi === "boolean" ? { multi: q.multi } : {}),
|
|
374
|
+
...(Number.isInteger(q.recommended) ? { recommended: q.recommended } : {}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
340
380
|
export class AskDialogComponent implements Component {
|
|
341
381
|
#states: QuestionState[];
|
|
342
382
|
#activeTabIndex = 0;
|
|
@@ -352,13 +392,15 @@ export class AskDialogComponent implements Component {
|
|
|
352
392
|
#stableHeight: { key: string; total: number } | undefined;
|
|
353
393
|
#previewCache: PreviewRenderCache = new Map();
|
|
354
394
|
#overflowLayouts = new WeakMap<ExtensionAskDialogQuestion, Set<string>>();
|
|
395
|
+
readonly #questions: ExtensionAskDialogQuestion[];
|
|
355
396
|
|
|
356
397
|
constructor(
|
|
357
|
-
|
|
398
|
+
questions: ExtensionAskDialogQuestion[],
|
|
358
399
|
private readonly callbacks: AskDialogCallbacks,
|
|
359
400
|
private readonly options: AskDialogOptions = {},
|
|
360
401
|
) {
|
|
361
|
-
this.#
|
|
402
|
+
this.#questions = normalizeDialogQuestions(questions);
|
|
403
|
+
this.#states = this.#questions.map(question => {
|
|
362
404
|
const recommended = Number.isInteger(question.recommended) ? question.recommended : 0;
|
|
363
405
|
const maxIndex = Math.max(0, question.options.length - 1);
|
|
364
406
|
return {
|
|
@@ -474,8 +516,8 @@ export class AskDialogComponent implements Component {
|
|
|
474
516
|
const tabBarRows = this.#hasSubmitTab() ? 1 : 0;
|
|
475
517
|
const mdTheme = getMarkdownTheme();
|
|
476
518
|
let needed = MIN_DIALOG_ROWS;
|
|
477
|
-
for (let index = 0; index < this
|
|
478
|
-
const question = this
|
|
519
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
520
|
+
const question = this.#questions[index];
|
|
479
521
|
const state = this.#states[index];
|
|
480
522
|
if (!question || !state) continue;
|
|
481
523
|
const headerRows = tabBarRows + renderQuestionTitle(question, width).length;
|
|
@@ -493,7 +535,7 @@ export class AskDialogComponent implements Component {
|
|
|
493
535
|
if (this.#hasSubmitTab()) {
|
|
494
536
|
// Warning line + blank, one summary line per question, blank, and
|
|
495
537
|
// the Submit row; note lines added later scroll within the body.
|
|
496
|
-
const body = 2 + this
|
|
538
|
+
const body = 2 + this.#questions.length + 2;
|
|
497
539
|
needed = Math.max(needed, chrome + tabBarRows + 1 + Math.max(MIN_BODY_ROWS, body));
|
|
498
540
|
}
|
|
499
541
|
return Math.min(needed, maxHeight);
|
|
@@ -507,11 +549,11 @@ export class AskDialogComponent implements Component {
|
|
|
507
549
|
// Multi questions confirm on the Submit tab (Enter toggles, never
|
|
508
550
|
// submits), so any multi question forces the tab even when there is
|
|
509
551
|
// only one question.
|
|
510
|
-
return this
|
|
552
|
+
return this.#questions.length > 1 || this.#questions.some(question => question.multi);
|
|
511
553
|
}
|
|
512
554
|
|
|
513
555
|
#submitTabIndex(): number {
|
|
514
|
-
return this
|
|
556
|
+
return this.#questions.length;
|
|
515
557
|
}
|
|
516
558
|
|
|
517
559
|
#isSubmitTab(): boolean {
|
|
@@ -519,7 +561,7 @@ export class AskDialogComponent implements Component {
|
|
|
519
561
|
}
|
|
520
562
|
|
|
521
563
|
#currentQuestionIndex(): number {
|
|
522
|
-
return clamp(this.#activeTabIndex, 0, Math.max(0, this
|
|
564
|
+
return clamp(this.#activeTabIndex, 0, Math.max(0, this.#questions.length - 1));
|
|
523
565
|
}
|
|
524
566
|
|
|
525
567
|
#requestRender(): void {
|
|
@@ -530,7 +572,7 @@ export class AskDialogComponent implements Component {
|
|
|
530
572
|
const lines: string[] = [];
|
|
531
573
|
if (this.#hasSubmitTab()) {
|
|
532
574
|
const tabs: Tab[] = [
|
|
533
|
-
...this
|
|
575
|
+
...this.#questions.map((question, index) => ({
|
|
534
576
|
id: String(index),
|
|
535
577
|
label: questionTabLabel(question, index),
|
|
536
578
|
})),
|
|
@@ -545,7 +587,7 @@ export class AskDialogComponent implements Component {
|
|
|
545
587
|
return lines;
|
|
546
588
|
}
|
|
547
589
|
const questionIndex = this.#currentQuestionIndex();
|
|
548
|
-
const question = this
|
|
590
|
+
const question = this.#questions[questionIndex];
|
|
549
591
|
if (!question) return lines;
|
|
550
592
|
lines.push(...renderQuestionTitle(question, width));
|
|
551
593
|
return lines;
|
|
@@ -559,7 +601,7 @@ export class AskDialogComponent implements Component {
|
|
|
559
601
|
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
560
602
|
return `Enter submit · ↑/↓ scroll ·${scroll} ${cancel}`;
|
|
561
603
|
}
|
|
562
|
-
const question = this
|
|
604
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
563
605
|
const action = question?.multi ? "Space/Enter toggle · n note" : "Enter select · n note";
|
|
564
606
|
const tabs = this.#hasSubmitTab() ? " · Tab/←/→" : "";
|
|
565
607
|
if (this.#questionCanPage && indicator) {
|
|
@@ -585,7 +627,7 @@ export class AskDialogComponent implements Component {
|
|
|
585
627
|
}
|
|
586
628
|
|
|
587
629
|
#activeQuestionState(): { question: ExtensionAskDialogQuestion; state: QuestionState } | undefined {
|
|
588
|
-
const question = this
|
|
630
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
589
631
|
const state = this.#states[this.#currentQuestionIndex()];
|
|
590
632
|
if (!question || !state) return undefined;
|
|
591
633
|
return { question, state };
|
|
@@ -672,18 +714,18 @@ export class AskDialogComponent implements Component {
|
|
|
672
714
|
}
|
|
673
715
|
|
|
674
716
|
#switchTab(direction: 1 | -1): void {
|
|
675
|
-
const tabCount = this
|
|
717
|
+
const tabCount = this.#questions.length + 1;
|
|
676
718
|
this.#activeTabIndex = (this.#activeTabIndex + direction + tabCount) % tabCount;
|
|
677
719
|
this.#submitScrollOffset = 0;
|
|
678
720
|
}
|
|
679
721
|
|
|
680
722
|
#advanceAfterQuestion(): void {
|
|
681
723
|
const current = this.#currentQuestionIndex();
|
|
682
|
-
if (this
|
|
724
|
+
if (this.#questions.length === 1) {
|
|
683
725
|
this.#finishSubmit();
|
|
684
726
|
return;
|
|
685
727
|
}
|
|
686
|
-
this.#activeTabIndex = current + 1 < this
|
|
728
|
+
this.#activeTabIndex = current + 1 < this.#questions.length ? current + 1 : this.#submitTabIndex();
|
|
687
729
|
this.#submitScrollOffset = 0;
|
|
688
730
|
this.#requestRender();
|
|
689
731
|
}
|
|
@@ -829,8 +871,8 @@ export class AskDialogComponent implements Component {
|
|
|
829
871
|
);
|
|
830
872
|
allLines.push("");
|
|
831
873
|
}
|
|
832
|
-
for (let index = 0; index < this
|
|
833
|
-
const question = this
|
|
874
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
875
|
+
const question = this.#questions[index];
|
|
834
876
|
const state = this.#states[index];
|
|
835
877
|
if (!question || !state) continue;
|
|
836
878
|
const label = questionTabLabel(question, index);
|
|
@@ -895,8 +937,8 @@ export class AskDialogComponent implements Component {
|
|
|
895
937
|
|
|
896
938
|
#unansweredCount(): number {
|
|
897
939
|
let count = 0;
|
|
898
|
-
for (let index = 0; index < this
|
|
899
|
-
const question = this
|
|
940
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
941
|
+
const question = this.#questions[index];
|
|
900
942
|
const state = this.#states[index];
|
|
901
943
|
if (!question || !state) continue;
|
|
902
944
|
if (state.selectedOptions.size === 0 && state.customInput === undefined) count += 1;
|
|
@@ -911,8 +953,8 @@ export class AskDialogComponent implements Component {
|
|
|
911
953
|
return;
|
|
912
954
|
}
|
|
913
955
|
this.options.onTimeout?.();
|
|
914
|
-
for (let index = 0; index < this
|
|
915
|
-
const question = this
|
|
956
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
957
|
+
const question = this.#questions[index];
|
|
916
958
|
const state = this.#states[index];
|
|
917
959
|
if (!question || !state) continue;
|
|
918
960
|
if (state.selectedOptions.size === 0 && state.customInput === undefined) {
|
|
@@ -952,8 +994,8 @@ export class AskDialogComponent implements Component {
|
|
|
952
994
|
|
|
953
995
|
#buildResults(): ExtensionAskDialogResultItem[] {
|
|
954
996
|
const results: ExtensionAskDialogResultItem[] = [];
|
|
955
|
-
for (let index = 0; index < this
|
|
956
|
-
const question = this
|
|
997
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
998
|
+
const question = this.#questions[index];
|
|
957
999
|
const state = this.#states[index];
|
|
958
1000
|
if (!question || !state) continue;
|
|
959
1001
|
const selectedOptions = question.options
|
|
@@ -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"],
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utilities for formatting keybinding hints in the UI.
|
|
3
3
|
*/
|
|
4
|
-
import { getKeybindings, type Keybinding
|
|
5
|
-
import type
|
|
4
|
+
import { getKeybindings, type Keybinding } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { type AppKeybinding, formatKeyHints, type KeybindingsManager } from "../../config/keybindings";
|
|
6
6
|
import { theme } from "../../modes/theme/theme";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Format keys array as display string (e.g., ["ctrl+c", "escape"] -> "ctrl+c/escape").
|
|
10
|
-
*/
|
|
11
|
-
function formatKeys(keys: KeyId[]): string {
|
|
12
|
-
if (keys.length === 0) return "";
|
|
13
|
-
if (keys.length === 1) return keys[0]!;
|
|
14
|
-
return keys.join("/");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
8
|
/**
|
|
18
9
|
* Get display string for an editor action.
|
|
19
10
|
*/
|
|
20
11
|
export function editorKey(action: Keybinding): string {
|
|
21
|
-
return
|
|
12
|
+
return formatKeyHints(getKeybindings().getKeys(action));
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
/**
|
|
25
16
|
* Get display string for an app action.
|
|
26
17
|
*/
|
|
27
18
|
export function appKey(keybindings: KeybindingsManager, action: AppKeybinding): string {
|
|
28
|
-
return
|
|
19
|
+
return formatKeyHints(keybindings.getKeys(action));
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
/**
|
|
@@ -5,6 +5,7 @@ import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
|
5
5
|
import { formatDuration, formatNumber, getProjectDir, pathIsWithin, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { type ThemeColor, theme } from "../../../modes/theme/theme";
|
|
7
7
|
import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../../tools/render-utils";
|
|
8
|
+
import { fileHyperlink } from "../../../tui/hyperlink";
|
|
8
9
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
|
|
9
10
|
import { sanitizeStatusText } from "../../shared";
|
|
10
11
|
import { formatContextUsage, getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
|
|
@@ -280,7 +281,8 @@ const pathSegment: StatusLineSegment = {
|
|
|
280
281
|
if (stripPrefix && ctx.worktree) {
|
|
281
282
|
const { projectName, worktreeName } = ctx.worktree;
|
|
282
283
|
const label = ctx.git.branch === worktreeName ? projectName : `${projectName}/${worktreeName}`;
|
|
283
|
-
const
|
|
284
|
+
const text = fileHyperlink(getProjectDir(), clampPathLength(label, opts.maxLength ?? 40));
|
|
285
|
+
const content = withIcon(theme.icon.worktree, text);
|
|
284
286
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
285
287
|
}
|
|
286
288
|
|
|
@@ -301,13 +303,10 @@ const pathSegment: StatusLineSegment = {
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
pwd = clampPathLength(pwd, opts.maxLength ?? 40);
|
|
304
|
-
if (repoSuffix) {
|
|
305
|
-
pwd = `${pwd}${repoSuffix}`;
|
|
306
|
-
}
|
|
307
306
|
|
|
308
307
|
const showScratchIcon = scratch && stripPrefix;
|
|
309
308
|
const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
|
|
310
|
-
const content = withIcon(icon, pwd);
|
|
309
|
+
const content = withIcon(icon, `${fileHyperlink(projectDir, pwd)}${repoSuffix}`);
|
|
311
310
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
312
311
|
},
|
|
313
312
|
};
|