@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1
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 +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -13,6 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import {
|
|
14
14
|
type ApplyResult,
|
|
15
15
|
applyEdits,
|
|
16
|
+
type Clipboard,
|
|
16
17
|
type Cursor,
|
|
17
18
|
computeFileHash,
|
|
18
19
|
type Edit,
|
|
@@ -26,8 +27,10 @@ import {
|
|
|
26
27
|
parsePatchStreaming,
|
|
27
28
|
Recovery,
|
|
28
29
|
resolveBlockEdits,
|
|
30
|
+
resolveClipboardEdits,
|
|
29
31
|
type SnapshotStore,
|
|
30
32
|
stripBom,
|
|
33
|
+
validateClipboardSequence,
|
|
31
34
|
} from "@oh-my-pi/hashline";
|
|
32
35
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
33
36
|
import { generateDiffString } from "../diff";
|
|
@@ -48,6 +51,14 @@ export interface HashlineDiffOptions {
|
|
|
48
51
|
* authoring input; the final apply path still validates through Patcher.
|
|
49
52
|
*/
|
|
50
53
|
skipHashValidation?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Clipboard register shared across the sections of one patch preview.
|
|
56
|
+
* `CUT` in an earlier section feeds `PASTE` in a later one, so the preview
|
|
57
|
+
* matches apply. Multi-section previews MUST thread one register through
|
|
58
|
+
* sections in patch order; omitted, each section gets a
|
|
59
|
+
* private register (same-file cut/paste still previews correctly).
|
|
60
|
+
*/
|
|
61
|
+
clipboard?: Clipboard;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
async function readSectionText(absolutePath: string, sectionPath: string): Promise<string> {
|
|
@@ -144,6 +155,7 @@ function hasAnchorScopedEdit(edits: readonly Edit[]): boolean {
|
|
|
144
155
|
return edits.some(edit => {
|
|
145
156
|
if (edit.kind === "delete") return true;
|
|
146
157
|
if (edit.kind === "block") return true;
|
|
158
|
+
if (edit.kind === "cut") return true;
|
|
147
159
|
return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
|
|
148
160
|
});
|
|
149
161
|
}
|
|
@@ -204,14 +216,24 @@ function applyPreviewEdits(args: {
|
|
|
204
216
|
const liveMatches = expected !== undefined && computeFileHash(normalized) === expected;
|
|
205
217
|
const edits = parsePreviewEdits(section, options.streaming);
|
|
206
218
|
const resolved = resolvePreviewEdits({ section, absolutePath, normalized, snapshots, expected, liveMatches, edits });
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
const clipboard = options.clipboard ?? {};
|
|
220
|
+
// Mirror the Patcher: surface clipboard sequencing mistakes with their
|
|
221
|
+
// targeted message before the recovery path below swallows them. Streaming
|
|
222
|
+
// previews stay lenient — a mid-typed op transiently violating sequencing
|
|
223
|
+
// must not flash an error frame.
|
|
224
|
+
if (!options.streaming) validateClipboardSequence(resolved, clipboard);
|
|
225
|
+
const applyOptions = { clipboard, ...(options.streaming ? { onEmptyPaste: "drop" as const } : {}) };
|
|
226
|
+
if (options.skipHashValidation || expected === undefined || liveMatches) {
|
|
227
|
+
return applyEdits(normalized, resolved, applyOptions);
|
|
228
|
+
}
|
|
229
|
+
if (!hasAnchorScopedEdit(resolved)) return applyEdits(normalized, resolved, applyOptions);
|
|
209
230
|
|
|
210
231
|
const recovered = new Recovery(snapshots).tryRecover({
|
|
211
232
|
path: absolutePath,
|
|
212
233
|
currentText: normalized,
|
|
213
234
|
fileHash: expected,
|
|
214
235
|
edits: resolved,
|
|
236
|
+
clipboard,
|
|
215
237
|
});
|
|
216
238
|
if (recovered) return recovered;
|
|
217
239
|
throw createMismatchError(section, absolutePath, normalized, snapshots, expected);
|
|
@@ -250,18 +272,36 @@ function insertCursorLine(cursor: Cursor, fileLineCount: number): number {
|
|
|
250
272
|
function buildStreamingSectionDiff(
|
|
251
273
|
section: PatchSection,
|
|
252
274
|
normalized: string,
|
|
275
|
+
clipboard: Clipboard,
|
|
253
276
|
): { diff: string; firstChangedLine: number | undefined } | { error: string } {
|
|
254
277
|
const { edits, fileOp } = parsePatchStreaming(section.diff);
|
|
255
|
-
const
|
|
278
|
+
const blockResolved = resolveBlockEdits(edits, normalized, section.path, nativeBlockResolver, {
|
|
279
|
+
onUnresolved: "drop",
|
|
280
|
+
});
|
|
281
|
+
const fileLines = normalized.split("\n");
|
|
282
|
+
// Expand clipboard ops so a `PASTE` previews the moved rows. Resolve into
|
|
283
|
+
// a scratch register and commit it back only on success: a mid-typed
|
|
284
|
+
// capture (out-of-range while the range digits are still streaming,
|
|
285
|
+
// transient sequencing violations) throws AFTER earlier captures may have
|
|
286
|
+
// landed, and leaking those into the shared register would let a later
|
|
287
|
+
// section preview a paste that never happened. On failure, fall back to
|
|
288
|
+
// filtering the clipboard edits out of this section's frame.
|
|
289
|
+
const scratch: Clipboard = { ...clipboard };
|
|
290
|
+
let resolved: readonly Edit[];
|
|
291
|
+
try {
|
|
292
|
+
resolved = resolveClipboardEdits(blockResolved, fileLines, scratch, { onEmptyPaste: "drop" });
|
|
293
|
+
if (scratch.lines === undefined) delete clipboard.lines;
|
|
294
|
+
else clipboard.lines = scratch.lines;
|
|
295
|
+
} catch {
|
|
296
|
+
resolved = blockResolved.filter(edit => edit.kind !== "cut" && edit.kind !== "paste");
|
|
297
|
+
}
|
|
256
298
|
if (resolved.length === 0) {
|
|
257
|
-
// A whole-file op (REM / MV) carries no line edits
|
|
258
|
-
//
|
|
259
|
-
// diff rather than a misleading "No changes" error.
|
|
299
|
+
// A whole-file op (REM / MV) carries no line edits; the result header
|
|
300
|
+
// conveys the change, so emit an empty diff.
|
|
260
301
|
if (fileOp) return { diff: "", firstChangedLine: undefined };
|
|
261
302
|
return { error: `No changes would be made to ${section.path}.` };
|
|
262
303
|
}
|
|
263
304
|
|
|
264
|
-
const fileLines = normalized.split("\n");
|
|
265
305
|
const rows: string[] = [];
|
|
266
306
|
let firstChangedLine: number | undefined;
|
|
267
307
|
|
|
@@ -323,11 +363,10 @@ export async function computeHashlineSectionDiff(
|
|
|
323
363
|
// diff: feed the in-flight ops through the natural-order builder so the
|
|
324
364
|
// streamed cursor stays pinned to the bottom. The args-complete pass
|
|
325
365
|
// (`streaming` unset) falls through to the real Myers diff below.
|
|
326
|
-
if (options.streaming) return buildStreamingSectionDiff(section, normalized);
|
|
366
|
+
if (options.streaming) return buildStreamingSectionDiff(section, normalized, options.clipboard ?? {});
|
|
327
367
|
const result = applyPreviewEdits({ section, absolutePath, normalized, snapshots, options });
|
|
328
368
|
if (normalized === result.text) {
|
|
329
|
-
// REM/MV-only sections change no text; the header conveys the
|
|
330
|
-
// delete/move, so don't surface a "No changes" error.
|
|
369
|
+
// REM/MV-only sections change no text; the header conveys the op.
|
|
331
370
|
if (section.fileOp) return { diff: "", firstChangedLine: undefined };
|
|
332
371
|
return { error: `No changes would be made to ${section.path}.` };
|
|
333
372
|
}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
import {
|
|
14
14
|
type BlockResolution,
|
|
15
15
|
buildCompactDiffPreview,
|
|
16
|
+
type Clipboard,
|
|
17
|
+
commitClipboard,
|
|
18
|
+
forkClipboard,
|
|
16
19
|
MismatchError as HashlineMismatchError,
|
|
17
20
|
Patch,
|
|
18
21
|
Patcher,
|
|
@@ -25,6 +28,7 @@ import type { ToolSession } from "../../tools";
|
|
|
25
28
|
import { outputMeta } from "../../tools/output-meta";
|
|
26
29
|
import { ToolError } from "../../tools/tool-errors";
|
|
27
30
|
import { generateDiffString } from "../diff";
|
|
31
|
+
import { getEditClipboard } from "../edit-clipboard";
|
|
28
32
|
import { getFileSnapshotStore } from "../file-snapshot-store";
|
|
29
33
|
import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
|
|
30
34
|
import { pruneOversizedEditSnapshots } from "../snapshot-details";
|
|
@@ -98,12 +102,24 @@ interface RenderedSection {
|
|
|
98
102
|
perFileResult: EditToolPerFileResult;
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
const BLOCK_OP_LABELS: Record<BlockResolution["op"], string> = {
|
|
106
|
+
replace: "SWAP.BLK",
|
|
107
|
+
insert_after: "INS.BLK.POST",
|
|
108
|
+
cut: "CUT.BLK",
|
|
109
|
+
paste_after: "PASTE.BLK.POST",
|
|
110
|
+
};
|
|
111
|
+
|
|
101
112
|
function formatBlockResolution(resolution: BlockResolution): string {
|
|
102
|
-
const op = resolution.op
|
|
113
|
+
const op = BLOCK_OP_LABELS[resolution.op];
|
|
103
114
|
const lines = resolution.end - resolution.start + 1;
|
|
104
115
|
const span =
|
|
105
116
|
resolution.start === resolution.end ? `line ${resolution.start}` : `lines ${resolution.start}-${resolution.end}`;
|
|
106
|
-
const suffix =
|
|
117
|
+
const suffix =
|
|
118
|
+
resolution.op === "insert_after"
|
|
119
|
+
? `; body lands after line ${resolution.end}`
|
|
120
|
+
: resolution.op === "paste_after"
|
|
121
|
+
? `; clipboard lands after line ${resolution.end}`
|
|
122
|
+
: "";
|
|
107
123
|
return `${op} ${resolution.anchorLine} → resolved ${span} (${lines} line${lines === 1 ? "" : "s"})${suffix}`;
|
|
108
124
|
}
|
|
109
125
|
|
|
@@ -213,12 +229,19 @@ export async function executeHashlineSingle(
|
|
|
213
229
|
const enforceSeenLines = options.session.settings.get("edit.enforceSeenLines");
|
|
214
230
|
const patcher = new Patcher({ fs, snapshots, blockResolver: nativeBlockResolver, enforceSeenLines });
|
|
215
231
|
|
|
232
|
+
// The clipboard register is session-persistent: `CUT` in one edit call can
|
|
233
|
+
// `PASTE` in a later one. Each batch works on a fork and publishes it back
|
|
234
|
+
// only after writes land.
|
|
235
|
+
const sessionClipboard = getEditClipboard(options.session);
|
|
236
|
+
const clipboard = forkClipboard(sessionClipboard);
|
|
237
|
+
|
|
216
238
|
// Single-section fast path: prepare, commit, render.
|
|
217
239
|
const inputHash = hashPatchInput(options.input);
|
|
218
240
|
if (patch.sections.length === 1) {
|
|
219
241
|
fs.setBatchRequest(narrowBatchRequest(options.batchRequest, true));
|
|
220
|
-
const prepared = await patcher.prepare(patch.sections[0]);
|
|
242
|
+
const prepared = await patcher.prepare(patch.sections[0], clipboard);
|
|
221
243
|
const sectionResult = await patcher.commit(prepared);
|
|
244
|
+
commitClipboard(clipboard, sessionClipboard);
|
|
222
245
|
if (sectionResult.op === "noop") {
|
|
223
246
|
const { count, escalate } = recordNoopEdit(options.session, sectionResult.canonicalPath, inputHash);
|
|
224
247
|
if (escalate) {
|
|
@@ -231,9 +254,18 @@ export async function executeHashlineSingle(
|
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
// Multi-section: prepare every section up front so we fail fast before
|
|
234
|
-
// any write hits the filesystem.
|
|
257
|
+
// any write hits the filesystem. One clipboard register spans the batch,
|
|
258
|
+
// so `CUT` in one section feeds `PASTE` in a later one.
|
|
235
259
|
const prepared: PreparedSection[] = [];
|
|
236
|
-
|
|
260
|
+
// Register state after each section's prepare. Commits are non-atomic: a
|
|
261
|
+
// mid-batch write failure leaves earlier sections on disk, so the session
|
|
262
|
+
// register must reflect exactly the landed prefix — content a landed CUT
|
|
263
|
+
// deleted would otherwise be lost.
|
|
264
|
+
const sectionStates: Clipboard[] = [];
|
|
265
|
+
for (const section of patch.sections) {
|
|
266
|
+
prepared.push(await patcher.prepare(section, clipboard));
|
|
267
|
+
sectionStates.push(forkClipboard(clipboard));
|
|
268
|
+
}
|
|
237
269
|
assertUniqueCanonicalPaths(prepared);
|
|
238
270
|
for (const entry of prepared) {
|
|
239
271
|
if (entry.isNoop) {
|
|
@@ -251,6 +283,7 @@ export async function executeHashlineSingle(
|
|
|
251
283
|
const isLast = i === prepared.length - 1;
|
|
252
284
|
fs.setBatchRequest(narrowBatchRequest(options.batchRequest, isLast));
|
|
253
285
|
const sectionResult = await patcher.commit(prepared[i]);
|
|
286
|
+
commitClipboard(sectionStates[i], sessionClipboard);
|
|
254
287
|
if (sectionResult.op === "noop") {
|
|
255
288
|
const { count, escalate } = recordNoopEdit(options.session, sectionResult.canonicalPath, inputHash);
|
|
256
289
|
throw escalate
|
|
@@ -260,7 +293,6 @@ export async function executeHashlineSingle(
|
|
|
260
293
|
resetNoopEdit(options.session, sectionResult.canonicalPath);
|
|
261
294
|
rendered.push(renderSection(sectionResult, fs.consumeDiagnostics(sectionResult.path), prepared[i].section.path));
|
|
262
295
|
}
|
|
263
|
-
|
|
264
296
|
return {
|
|
265
297
|
content: [
|
|
266
298
|
{
|
|
@@ -198,9 +198,33 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
198
198
|
const finalContent = await serializeEditFileText(absolutePath, relativePath, content);
|
|
199
199
|
|
|
200
200
|
// Route through ACP bridge when available; skips internal artifacts.
|
|
201
|
-
|
|
201
|
+
// `finalContent` is storage-space (e.g. a notebook's full JSON); the
|
|
202
|
+
// bridge may also report content that diverges from it (e.g. the
|
|
203
|
+
// client reformatted on save). `WriteResult.text` must stay in
|
|
204
|
+
// view-space — the same space `readText` returns — so a follow-up
|
|
205
|
+
// `readText` sees exactly what this write reports.
|
|
206
|
+
const bridgeResult = await routeWriteThroughBridge(
|
|
207
|
+
this.session,
|
|
208
|
+
relativePath,
|
|
209
|
+
absolutePath,
|
|
210
|
+
finalContent,
|
|
211
|
+
this.#signal,
|
|
212
|
+
);
|
|
213
|
+
if (bridgeResult) {
|
|
202
214
|
this.#diagnosticsByPath.set(relativePath, undefined);
|
|
203
|
-
|
|
215
|
+
if (!bridgeResult.driftedFromRequest) {
|
|
216
|
+
// No client-side transform: the view we sent is what's on disk.
|
|
217
|
+
return { text: content };
|
|
218
|
+
}
|
|
219
|
+
// Drifted (e.g. format-on-save): re-derive the view from what
|
|
220
|
+
// actually landed on disk instead of assuming `content` still
|
|
221
|
+
// matches. Falls back to `content` if the drifted file can't be
|
|
222
|
+
// re-read as a valid view (e.g. a formatter broke notebook JSON).
|
|
223
|
+
try {
|
|
224
|
+
return { text: await readEditFileText(absolutePath, relativePath) };
|
|
225
|
+
} catch {
|
|
226
|
+
return { text: content };
|
|
227
|
+
}
|
|
204
228
|
}
|
|
205
229
|
|
|
206
230
|
const diagnostics = await this.#writethrough(
|
|
@@ -213,7 +237,7 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
213
237
|
);
|
|
214
238
|
invalidateFsScanAfterWrite(absolutePath);
|
|
215
239
|
this.#diagnosticsByPath.set(relativePath, diagnostics);
|
|
216
|
-
return { text:
|
|
240
|
+
return { text: content };
|
|
217
241
|
}
|
|
218
242
|
|
|
219
243
|
async exists(relativePath: string): Promise<boolean> {
|
package/src/edit/index.ts
CHANGED
|
@@ -399,14 +399,24 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
399
399
|
readonly #editMode?: EditMode;
|
|
400
400
|
readonly #deferredDiagnostics: DeferredDiagnostics;
|
|
401
401
|
|
|
402
|
-
|
|
402
|
+
/**
|
|
403
|
+
* `mode` pins the edit variant for this instance, for callers whose protocol
|
|
404
|
+
* fixes the shape of an edit. The Cursor `pi_edit` frame carries
|
|
405
|
+
* `old_text`/`new_text` pairs, which only `replace` accepts — under the
|
|
406
|
+
* default `hashline` mode those args do not match the schema at all. Left
|
|
407
|
+
* unset, the env/settings resolution applies as before.
|
|
408
|
+
*/
|
|
409
|
+
constructor(
|
|
410
|
+
private readonly session: ToolSession,
|
|
411
|
+
mode?: EditMode,
|
|
412
|
+
) {
|
|
403
413
|
const {
|
|
404
414
|
PI_EDIT_FUZZY: editFuzzy = "auto",
|
|
405
415
|
PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
|
|
406
416
|
PI_EDIT_VARIANT: envEditVariant = "auto",
|
|
407
417
|
} = Bun.env;
|
|
408
418
|
|
|
409
|
-
this.#editMode = resolveConfiguredEditMode(envEditVariant);
|
|
419
|
+
this.#editMode = mode ?? resolveConfiguredEditMode(envEditVariant);
|
|
410
420
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
|
411
421
|
this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
|
|
412
422
|
const deduplicateDiagnostics =
|
package/src/edit/renderer.ts
CHANGED
|
@@ -140,7 +140,7 @@ interface HashlineInputEntry {
|
|
|
140
140
|
path: string;
|
|
141
141
|
op?: Operation;
|
|
142
142
|
rename?: string;
|
|
143
|
-
/** A SWAP/
|
|
143
|
+
/** A SWAP/CUT/INS line-editing op precedes the file op — keeps a move framed. */
|
|
144
144
|
hasLineEdits?: boolean;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -568,9 +568,9 @@ function parseHashlineInputPreviewHeader(line: string): string | null {
|
|
|
568
568
|
return previewPath.length > 0 ? previewPath : null;
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
-
// Line-editing op headers (SWAP/
|
|
572
|
-
// REM/MV ops. Body rows are
|
|
573
|
-
const HL_LINE_OP_HEADER = /^(?:SWAP|
|
|
571
|
+
// Line-editing op headers (SWAP/CUT/INS family), distinct from file-level
|
|
572
|
+
// REM/MV ops. Body rows are `+TEXT`, so this only matches real headers.
|
|
573
|
+
const HL_LINE_OP_HEADER = /^(?:SWAP|CUT|INS)\b/;
|
|
574
574
|
|
|
575
575
|
/**
|
|
576
576
|
* Walk a (possibly mid-stream) hashline payload into per-section descriptors:
|
package/src/edit/streaming.ts
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
import {
|
|
17
17
|
ABORT_MARKER,
|
|
18
18
|
BEGIN_PATCH_MARKER,
|
|
19
|
+
type Clipboard,
|
|
19
20
|
containsRecognizableHashlineOperations,
|
|
20
21
|
END_PATCH_MARKER,
|
|
22
|
+
forkClipboard,
|
|
21
23
|
type PatchSection as HashlineInputSection,
|
|
22
24
|
Patch as HashlinePatch,
|
|
23
25
|
type SnapshotStore,
|
|
@@ -50,6 +52,13 @@ export interface StreamingDiffContext {
|
|
|
50
52
|
* not flicker in the preview.
|
|
51
53
|
*/
|
|
52
54
|
isStreaming?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Session-persistent clipboard register (`CUT`/`PASTE`). Previews
|
|
57
|
+
* fork it per frame — never mutating it — so a `PASTE` of content cut in
|
|
58
|
+
* an earlier edit call (or an earlier section of this patch) renders the
|
|
59
|
+
* real rows.
|
|
60
|
+
*/
|
|
61
|
+
clipboard?: Clipboard;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
@@ -411,7 +420,7 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
411
420
|
if (typeof edit?.diff !== "string") continue;
|
|
412
421
|
// `create` ops carry full file content in `diff` with no +/- markers;
|
|
413
422
|
// pass that content through whole.
|
|
414
|
-
const added = extractAddedLines(edit.diff,
|
|
423
|
+
const added = extractAddedLines(edit.diff, edit.op === "create");
|
|
415
424
|
digest = digest === undefined ? added : `${digest}\n${added}`;
|
|
416
425
|
}
|
|
417
426
|
return digest;
|
|
@@ -542,7 +551,9 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
542
551
|
// to parse; suppress until the next chunk arrives. Once args are
|
|
543
552
|
// complete, surface the error so the model sees what went wrong.
|
|
544
553
|
if (ctx.isStreaming) return null;
|
|
545
|
-
const result = await computeHashlineDiff({ input }, ctx.cwd, ctx.snapshots
|
|
554
|
+
const result = await computeHashlineDiff({ input }, ctx.cwd, ctx.snapshots, {
|
|
555
|
+
clipboard: forkClipboard(ctx.clipboard),
|
|
556
|
+
});
|
|
546
557
|
ctx.signal.throwIfAborted();
|
|
547
558
|
return [toPerFilePreview("", result)];
|
|
548
559
|
}
|
|
@@ -558,12 +569,16 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
558
569
|
const trailingProcessedIndex = sectionsToProcess.length - 1;
|
|
559
570
|
|
|
560
571
|
const previews: PerFileDiffPreview[] = [];
|
|
572
|
+
// Fork the session register per preview frame: sections feed each other
|
|
573
|
+
// in patch order, but a preview must never mutate the live register.
|
|
574
|
+
const clipboard = forkClipboard(ctx.clipboard);
|
|
561
575
|
for (let i = 0; i < sectionsToProcess.length; i++) {
|
|
562
576
|
ctx.signal.throwIfAborted();
|
|
563
577
|
const section = sectionsToProcess[i];
|
|
564
578
|
const result = await computeHashlineSectionDiff(section, ctx.cwd, ctx.snapshots, {
|
|
565
579
|
streaming: ctx.isStreaming,
|
|
566
580
|
skipHashValidation: ctx.isStreaming === true,
|
|
581
|
+
clipboard,
|
|
567
582
|
});
|
|
568
583
|
ctx.signal.throwIfAborted();
|
|
569
584
|
// Ignore parse/apply errors from the trailing (actively-typed)
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -376,10 +376,14 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
376
376
|
raise RuntimeError("tool bridge is unavailable in this kernel")
|
|
377
377
|
return (base.rstrip("/"), token, session)
|
|
378
378
|
|
|
379
|
+
import urllib.error, urllib.request
|
|
380
|
+
|
|
381
|
+
# urllib discovers environment and macOS SystemConfiguration proxies. This
|
|
382
|
+
# host-owned loopback endpoint must always connect directly.
|
|
383
|
+
_BRIDGE_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
384
|
+
|
|
379
385
|
def _bridge_call(name: str, args: dict):
|
|
380
386
|
"""POST one request to the host tool bridge and return its `value`."""
|
|
381
|
-
import urllib.request, urllib.error
|
|
382
|
-
|
|
383
387
|
base, token, session = _tool_proxy_from_env()
|
|
384
388
|
_run_id_getter = globals().get("__omp_current_run_id__")
|
|
385
389
|
_run_id = (
|
|
@@ -400,7 +404,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
400
404
|
},
|
|
401
405
|
)
|
|
402
406
|
try:
|
|
403
|
-
with
|
|
407
|
+
with _BRIDGE_OPENER.open(req) as resp:
|
|
404
408
|
body = resp.read()
|
|
405
409
|
except urllib.error.HTTPError as exc:
|
|
406
410
|
body = exc.read()
|
|
@@ -395,7 +395,7 @@
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// Apply filter mode
|
|
398
|
-
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change', 'mode_change', 'ttsr_injection', 'session_init'].includes(entry.type);
|
|
398
|
+
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change', 'mode_change', 'ttsr_injection', 'session_init', 'credential_pin'].includes(entry.type);
|
|
399
399
|
let passesFilter = true;
|
|
400
400
|
|
|
401
401
|
switch (filterMode) {
|
|
@@ -10,6 +10,7 @@ import type { Settings } from "../../config/settings";
|
|
|
10
10
|
import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
11
11
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
12
12
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
13
|
+
import type { AsyncJobSnapshot } from "../../session/agent-session";
|
|
13
14
|
import type { SessionManager } from "../../session/session-manager";
|
|
14
15
|
import type { BranchHandler, NavigateTreeHandler, NewSessionHandler } from "../session-handler-types";
|
|
15
16
|
import { ManagedTimers } from "./managed-timers";
|
|
@@ -40,6 +41,7 @@ import type {
|
|
|
40
41
|
ExtensionUIDialogOptions,
|
|
41
42
|
InputEvent,
|
|
42
43
|
InputEventResult,
|
|
44
|
+
McpNotificationEvent,
|
|
43
45
|
MessageRenderer,
|
|
44
46
|
RegisteredCommand,
|
|
45
47
|
RegisteredTool,
|
|
@@ -218,6 +220,14 @@ async function raceHandlerWithTimeout<T>(
|
|
|
218
220
|
|
|
219
221
|
const MAX_PENDING_CREDENTIAL_DISABLED = 32;
|
|
220
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Buffer cap for `mcp_notification` events received before {@link ExtensionRunner.initialize}
|
|
225
|
+
* has run. Sized to match the manager-side buffer in `MCPManager.NOTIFICATION_BUFFER_CAP` so
|
|
226
|
+
* the two layers can't drop different amounts of the same burst — the pipe drains, or it
|
|
227
|
+
* spills, but it does so consistently at both ends. Drop-oldest under pressure.
|
|
228
|
+
*/
|
|
229
|
+
const MAX_PENDING_MCP_NOTIFICATIONS = 100;
|
|
230
|
+
|
|
221
231
|
/**
|
|
222
232
|
* Events handled by the generic emit() method.
|
|
223
233
|
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
|
@@ -327,6 +337,7 @@ export class ExtensionRunner {
|
|
|
327
337
|
#getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
328
338
|
#compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
|
|
329
339
|
#getSystemPromptFn: () => string[] = () => [];
|
|
340
|
+
#getAsyncJobSnapshotFn: () => AsyncJobSnapshot | null = () => null;
|
|
330
341
|
#newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
331
342
|
#branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
332
343
|
#navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
@@ -345,6 +356,19 @@ export class ExtensionRunner {
|
|
|
345
356
|
*/
|
|
346
357
|
#pendingCredentialDisabled: CredentialDisabledEvent[] = [];
|
|
347
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Buffer for `mcp_notification` events received via {@link emitMcpNotification} before
|
|
361
|
+
* {@link initialize} has run. Two-layer race: `MCPManager` also buffers frames until
|
|
362
|
+
* its first `addNotificationListener` subscriber attaches, but the sdk.ts bridge is
|
|
363
|
+
* registered inside `createAgentSession` — BEFORE the mode controller calls
|
|
364
|
+
* `ExtensionRunner.initialize()`. Without this second buffer, the manager's drain
|
|
365
|
+
* arrives at the bridge → the bridge calls `emitMcpNotification` → the runner drops
|
|
366
|
+
* the frame because `#initialized === false`, and the frame evaporates a second time.
|
|
367
|
+
* Bounded at {@link MAX_PENDING_MCP_NOTIFICATIONS}; oldest entries are dropped under
|
|
368
|
+
* pressure. Drained in {@link initialize} once the runtime/UI context is wired.
|
|
369
|
+
*/
|
|
370
|
+
#pendingMcpNotifications: Array<Omit<McpNotificationEvent, "type">> = [];
|
|
371
|
+
|
|
348
372
|
/**
|
|
349
373
|
* Timers scheduled by extensions through the sanctioned `ctx.setInterval` /
|
|
350
374
|
* `ctx.setTimeout` helpers. Callbacks run with the same isolation as handler
|
|
@@ -392,9 +416,11 @@ export class ExtensionRunner {
|
|
|
392
416
|
getMemory?: () => MemoryRuntimeContext | undefined,
|
|
393
417
|
private readonly settings?: Settings,
|
|
394
418
|
private readonly localProtocolOptions?: LocalProtocolOptions,
|
|
419
|
+
getAsyncJobSnapshot?: () => AsyncJobSnapshot | null,
|
|
395
420
|
) {
|
|
396
421
|
this.#uiContext = noOpUIContext;
|
|
397
422
|
this.#getMemoryFn = getMemory;
|
|
423
|
+
this.#getAsyncJobSnapshotFn = getAsyncJobSnapshot ?? (() => null);
|
|
398
424
|
}
|
|
399
425
|
|
|
400
426
|
initialize(
|
|
@@ -457,6 +483,23 @@ export class ExtensionRunner {
|
|
|
457
483
|
});
|
|
458
484
|
}
|
|
459
485
|
});
|
|
486
|
+
|
|
487
|
+
// Drain events buffered by emitMcpNotification() before initialize ran, using the
|
|
488
|
+
// same deferred-microtask ordering as the credential-disabled drain above so any
|
|
489
|
+
// onError listener registered synchronously after initialize() still catches
|
|
490
|
+
// handler errors during flush.
|
|
491
|
+
const pendingMcp = this.#pendingMcpNotifications.splice(0);
|
|
492
|
+
queueMicrotask(() => {
|
|
493
|
+
for (const event of pendingMcp) {
|
|
494
|
+
this.emit({ type: "mcp_notification", ...event }).catch((error: unknown) => {
|
|
495
|
+
logger.warn("mcp_notification handler threw during initialize flush", {
|
|
496
|
+
server: event.server,
|
|
497
|
+
method: event.method,
|
|
498
|
+
error: error instanceof Error ? error.message : String(error),
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
460
503
|
}
|
|
461
504
|
|
|
462
505
|
/**
|
|
@@ -484,6 +527,32 @@ export class ExtensionRunner {
|
|
|
484
527
|
await this.emit({ type: "credential_disabled", ...event });
|
|
485
528
|
}
|
|
486
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Forward an MCP server notification to extension handlers.
|
|
532
|
+
*
|
|
533
|
+
* If {@link initialize} has not yet run, the notification is buffered and replayed
|
|
534
|
+
* once initialize wires the runtime/UI context. Matches the credential-disabled
|
|
535
|
+
* deferral above: the sdk.ts bridge registers `MCPManager.addNotificationListener`
|
|
536
|
+
* inside `createAgentSession` — BEFORE the mode controller calls `initialize()` on
|
|
537
|
+
* this runner — so notification frames drained by the manager (either fresh
|
|
538
|
+
* arrivals or replay from its own startup buffer) can reach us pre-init. Without
|
|
539
|
+
* this buffer they would evaporate for a second time here.
|
|
540
|
+
*
|
|
541
|
+
* Bounded at {@link MAX_PENDING_MCP_NOTIFICATIONS}; oldest entries drop under
|
|
542
|
+
* pressure. Never throws; per-handler errors are routed through {@link onError}
|
|
543
|
+
* via {@link emit}'s normal isolation.
|
|
544
|
+
*/
|
|
545
|
+
async emitMcpNotification(event: Omit<McpNotificationEvent, "type">): Promise<void> {
|
|
546
|
+
if (!this.#initialized) {
|
|
547
|
+
if (this.#pendingMcpNotifications.length >= MAX_PENDING_MCP_NOTIFICATIONS) {
|
|
548
|
+
this.#pendingMcpNotifications.shift();
|
|
549
|
+
}
|
|
550
|
+
this.#pendingMcpNotifications.push(event);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
await this.emit({ type: "mcp_notification", ...event });
|
|
554
|
+
}
|
|
555
|
+
|
|
487
556
|
/** Emits a session stop pass that can be cancelled with the active settle signal. */
|
|
488
557
|
async emitSessionStop(event: Omit<SessionStopEvent, "type">): Promise<SessionStopEventResult | undefined> {
|
|
489
558
|
if (event.signal.aborted) return undefined;
|
|
@@ -666,6 +735,7 @@ export class ExtensionRunner {
|
|
|
666
735
|
ui: this.#uiContext,
|
|
667
736
|
getContextUsage: () => this.#getContextUsageFn(),
|
|
668
737
|
compact: instructionsOrOptions => this.#compactFn(instructionsOrOptions),
|
|
738
|
+
getAsyncJobSnapshot: () => this.#getAsyncJobSnapshotFn(),
|
|
669
739
|
hasUI: this.hasUI(),
|
|
670
740
|
cwd: this.cwd,
|
|
671
741
|
sessionManager: this.sessionManager,
|
|
@@ -49,6 +49,7 @@ import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
|
49
49
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
50
50
|
import type { CustomEditor } from "../../modes/components/custom-editor";
|
|
51
51
|
import type { Theme } from "../../modes/theme/theme";
|
|
52
|
+
import type { AsyncJobSnapshot } from "../../session/agent-session";
|
|
52
53
|
import type { CompactMode } from "../../session/compact-modes";
|
|
53
54
|
import type { CustomMessage, CustomMessagePayload } from "../../session/messages";
|
|
54
55
|
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
|
@@ -415,6 +416,8 @@ export interface ExtensionContext {
|
|
|
415
416
|
ui: ExtensionUIContext;
|
|
416
417
|
/** Get current context usage for the active model. */
|
|
417
418
|
getContextUsage(): ContextUsage | undefined;
|
|
419
|
+
/** Get a read-only snapshot of async jobs owned by this session. */
|
|
420
|
+
getAsyncJobSnapshot(): AsyncJobSnapshot | null;
|
|
418
421
|
/** Compact the session context (interactive mode shows UI). */
|
|
419
422
|
compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
|
|
420
423
|
/** Whether UI is available (false in print/RPC mode) */
|
|
@@ -713,6 +716,31 @@ export interface CredentialDisabledEvent {
|
|
|
713
716
|
disabledCause: string;
|
|
714
717
|
}
|
|
715
718
|
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// MCP Events
|
|
721
|
+
// ============================================================================
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Fired for every JSON-RPC notification received from a connected MCP server,
|
|
725
|
+
* AFTER the runtime's own handling of known list/update methods. Unknown or
|
|
726
|
+
* server-custom methods are delivered too — extensions can bridge them into
|
|
727
|
+
* session behavior by inspecting `method`/`params` and injecting a follow-up
|
|
728
|
+
* via `pi.sendMessage(..., { deliverAs })` or `pi.sendUserMessage(...)`.
|
|
729
|
+
*/
|
|
730
|
+
export interface McpNotificationEvent {
|
|
731
|
+
type: "mcp_notification";
|
|
732
|
+
/**
|
|
733
|
+
* Server name as declared in the MCP config (raw, unsanitized). Note this
|
|
734
|
+
* differs from the sanitized prefix used in `mcp__<sanitized_server>_<tool>`
|
|
735
|
+
* tool names — filter by this raw name, not by tool-name prefix matching.
|
|
736
|
+
*/
|
|
737
|
+
server: string;
|
|
738
|
+
/** JSON-RPC method (e.g. `notifications/tools/list_changed`, or server-custom). */
|
|
739
|
+
method: string;
|
|
740
|
+
/** JSON-RPC params, opaque to the runtime. */
|
|
741
|
+
params: unknown;
|
|
742
|
+
}
|
|
743
|
+
|
|
716
744
|
// ============================================================================
|
|
717
745
|
// User Bash Events
|
|
718
746
|
// ============================================================================
|
|
@@ -941,6 +969,7 @@ export type ExtensionEvent =
|
|
|
941
969
|
| TodoReminderEvent
|
|
942
970
|
| GoalUpdatedEvent
|
|
943
971
|
| CredentialDisabledEvent
|
|
972
|
+
| McpNotificationEvent
|
|
944
973
|
| UserBashEvent
|
|
945
974
|
| UserPythonEvent
|
|
946
975
|
| InputEvent
|
|
@@ -1133,6 +1162,7 @@ export interface ExtensionAPI {
|
|
|
1133
1162
|
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|
|
1134
1163
|
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
|
|
1135
1164
|
on(event: "user_python", handler: ExtensionHandler<UserPythonEvent, UserPythonEventResult>): void;
|
|
1165
|
+
on(event: "mcp_notification", handler: ExtensionHandler<McpNotificationEvent>): void;
|
|
1136
1166
|
|
|
1137
1167
|
// =========================================================================
|
|
1138
1168
|
// Tool Registration
|