@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
- package/dist/cli.js +9554 -12379
- package/dist/template-8vdv6xb4.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +8 -11
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- 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/cleanse.d.ts +23 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -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/export/html/index.d.ts +2 -0
- 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-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- 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 +8 -5
- 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 +1 -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/async-job-delivery.d.ts +8 -0
- 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/model-controls.d.ts +3 -3
- 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/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- 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/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- 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 +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/tui/output-block.d.ts +5 -5
- 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 +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +138 -24
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/async/job-manager.ts +30 -7
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- 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/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/discovery/claude-plugins.ts +144 -34
- 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/export/html/index.ts +17 -10
- 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-ai-shim.ts +46 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/broker.ts +14 -4
- 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 +4 -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 +27 -18
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- 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/settings-selector.ts +9 -1
- 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 +67 -64
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- 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/goals/guided-goal-interview.md +41 -6
- 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 +3 -0
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +330 -179
- package/src/session/async-job-delivery.ts +8 -0
- 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 +15 -7
- package/src/session/session-advisors.ts +142 -23
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-history-format.ts +31 -6
- 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 +136 -5
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/render.ts +20 -4
- package/src/task/worktree.ts +8 -0
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +7 -2
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +38 -5
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +44 -3
- package/src/tools/xdev.ts +39 -14
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/render.ts +2 -2
- package/src/web/search/types.ts +1 -1
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -41,6 +41,20 @@ interface ResolvedPluginDir {
|
|
|
41
41
|
warnings: string[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface ResolvedMCPConfig {
|
|
45
|
+
/** On-disk config file to read, or null when servers are inline or nothing applies. */
|
|
46
|
+
path: string | null;
|
|
47
|
+
/** Server map declared inline in the plugin manifest, or null when the source is a file. */
|
|
48
|
+
inlineServers: Record<string, unknown> | null;
|
|
49
|
+
/** Path recorded as each discovered server's capability source. */
|
|
50
|
+
sourcePath: string;
|
|
51
|
+
/** Directory that relative stdio `command`/`cwd` values resolve against. */
|
|
52
|
+
baseDir: string;
|
|
53
|
+
/** True when a plugin manifest named this source, false for the conventional fallback. */
|
|
54
|
+
declared: boolean;
|
|
55
|
+
warnings: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
async function readPluginManifest(root: ClaudePluginRoot): Promise<ClaudePluginManifest | null> {
|
|
45
59
|
const manifestPath = path.join(root.path, ".claude-plugin", "plugin.json");
|
|
46
60
|
const raw = await readFile(manifestPath);
|
|
@@ -375,50 +389,144 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
|
375
389
|
// MCP Servers
|
|
376
390
|
// =============================================================================
|
|
377
391
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Unwrap a parsed MCP config file to its server map. Supports the nested
|
|
394
|
+
* `{ mcpServers: { … } }` project shape and the flat `{ name: cfg, … }`
|
|
395
|
+
* marketplace-plugin shape. Returns null when a `mcpServers` field is present
|
|
396
|
+
* but not an object map (malformed) so the caller skips the file.
|
|
397
|
+
*/
|
|
398
|
+
function extractServerMap(obj: Record<string, unknown>): Record<string, unknown> | null {
|
|
399
|
+
if (isRecord(obj.mcpServers)) return obj.mcpServers;
|
|
400
|
+
if (!("mcpServers" in obj)) return obj;
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
384
403
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Resolve where a plugin's MCP servers come from, honoring the manifest's
|
|
406
|
+
* `mcpServers` field before the conventional root `.mcp.json`.
|
|
407
|
+
*
|
|
408
|
+
* `.omp-plugin/plugin.json` takes precedence over `.claude-plugin/plugin.json`.
|
|
409
|
+
* The field may be an inline object (the server map itself) or a string path to
|
|
410
|
+
* a config file within the plugin root; a path escaping the root is rejected
|
|
411
|
+
* with a warning. When no manifest declares the field, `<root>/.mcp.json` is the
|
|
412
|
+
* fallback source.
|
|
413
|
+
*/
|
|
414
|
+
async function resolvePluginMCPConfig(root: ClaudePluginRoot): Promise<ResolvedMCPConfig> {
|
|
415
|
+
const fallback = path.join(root.path, ".mcp.json");
|
|
416
|
+
for (const manifestDir of [".omp-plugin", ".claude-plugin"]) {
|
|
417
|
+
const manifestPath = path.join(root.path, manifestDir, "plugin.json");
|
|
418
|
+
const raw = await readFile(manifestPath);
|
|
419
|
+
if (raw === null) continue;
|
|
389
420
|
|
|
390
421
|
let parsed: unknown;
|
|
391
422
|
try {
|
|
392
423
|
parsed = JSON.parse(raw);
|
|
393
424
|
} catch {
|
|
394
|
-
warnings.push(`[claude-plugins] Invalid JSON in ${mcpPath}`);
|
|
395
|
-
logger.warn(`[claude-plugins] Invalid JSON in ${mcpPath}`);
|
|
396
425
|
continue;
|
|
397
426
|
}
|
|
427
|
+
if (!isRecord(parsed)) continue;
|
|
428
|
+
const pointer = parsed.mcpServers;
|
|
429
|
+
|
|
430
|
+
// Inline object form: the manifest value is the server map itself, rooted
|
|
431
|
+
// at the plugin directory (Claude's ${CLAUDE_PLUGIN_ROOT} base).
|
|
432
|
+
if (isRecord(pointer)) {
|
|
433
|
+
return {
|
|
434
|
+
path: null,
|
|
435
|
+
inlineServers: pointer,
|
|
436
|
+
sourcePath: manifestPath,
|
|
437
|
+
baseDir: root.path,
|
|
438
|
+
declared: true,
|
|
439
|
+
warnings: [],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// File-pointer form: resolve the named config file within the plugin root.
|
|
444
|
+
if (typeof pointer === "string") {
|
|
445
|
+
const configured = pointer.trim();
|
|
446
|
+
if (configured.length === 0) continue;
|
|
447
|
+
const resolved = path.resolve(root.path, configured);
|
|
448
|
+
if (!isWithinPluginRoot(root.path, resolved)) {
|
|
449
|
+
return {
|
|
450
|
+
path: null,
|
|
451
|
+
inlineServers: null,
|
|
452
|
+
sourcePath: manifestPath,
|
|
453
|
+
baseDir: root.path,
|
|
454
|
+
declared: true,
|
|
455
|
+
warnings: [
|
|
456
|
+
`[claude-plugins] Ignoring mcpServers path outside plugin root for ${root.id}: ${configured}`,
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
path: resolved,
|
|
462
|
+
inlineServers: null,
|
|
463
|
+
sourcePath: resolved,
|
|
464
|
+
baseDir: path.dirname(resolved),
|
|
465
|
+
declared: true,
|
|
466
|
+
warnings: [],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
path: fallback,
|
|
473
|
+
inlineServers: null,
|
|
474
|
+
sourcePath: fallback,
|
|
475
|
+
baseDir: path.dirname(fallback),
|
|
476
|
+
declared: false,
|
|
477
|
+
warnings: [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
482
|
+
const items: MCPServer[] = [];
|
|
483
|
+
const warnings: string[] = [];
|
|
398
484
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
485
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
486
|
+
warnings.push(...rootWarnings);
|
|
487
|
+
|
|
488
|
+
for (const root of roots) {
|
|
489
|
+
const resolved = await resolvePluginMCPConfig(root);
|
|
490
|
+
warnings.push(...resolved.warnings);
|
|
491
|
+
|
|
492
|
+
let servers: Record<string, unknown> | null;
|
|
493
|
+
if (resolved.inlineServers) {
|
|
494
|
+
servers = resolved.inlineServers;
|
|
495
|
+
} else if (resolved.path !== null) {
|
|
496
|
+
const raw = await readFile(resolved.path);
|
|
497
|
+
if (raw === null) {
|
|
498
|
+
// The conventional fallback is optional, but a manifest that names a
|
|
499
|
+
// missing file is an authoring error that would otherwise register
|
|
500
|
+
// zero servers with no explanation.
|
|
501
|
+
if (resolved.declared) {
|
|
502
|
+
const warning = `[claude-plugins] Missing mcpServers file declared by ${root.id}: ${resolved.path}`;
|
|
503
|
+
warnings.push(warning);
|
|
504
|
+
logger.warn(warning);
|
|
505
|
+
}
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
let parsed: unknown;
|
|
510
|
+
try {
|
|
511
|
+
parsed = JSON.parse(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
warnings.push(`[claude-plugins] Invalid JSON in ${resolved.path}`);
|
|
514
|
+
logger.warn(`[claude-plugins] Invalid JSON in ${resolved.path}`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
// Two file shapes are supported:
|
|
518
|
+
// nested: { "mcpServers": { name: cfg, ... } } (OMP/Claude Code project shape)
|
|
519
|
+
// flat: { name: cfg, ... } (Claude marketplace plugin shape)
|
|
520
|
+
if (!isRecord(parsed)) continue;
|
|
521
|
+
servers = extractServerMap(parsed);
|
|
417
522
|
} else {
|
|
418
523
|
continue;
|
|
419
524
|
}
|
|
525
|
+
if (servers === null) continue;
|
|
420
526
|
|
|
421
|
-
|
|
527
|
+
const { sourcePath, baseDir } = resolved;
|
|
528
|
+
for (const serverName in servers) {
|
|
529
|
+
const serverCfg = servers[serverName];
|
|
422
530
|
if (!serverCfg || typeof serverCfg !== "object" || Array.isArray(serverCfg)) continue;
|
|
423
531
|
const raw = serverCfg as {
|
|
424
532
|
enabled?: boolean;
|
|
@@ -437,7 +545,9 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
437
545
|
// occasionally ship .mcp.json entries with neither, which would register a useless
|
|
438
546
|
// server and surface as a connection error at runtime.
|
|
439
547
|
if (typeof raw.command !== "string" && typeof raw.url !== "string") {
|
|
440
|
-
warnings.push(
|
|
548
|
+
warnings.push(
|
|
549
|
+
`[claude-plugins] Skipping MCP server "${serverName}" in ${sourcePath}: missing command or url`,
|
|
550
|
+
);
|
|
441
551
|
continue;
|
|
442
552
|
}
|
|
443
553
|
const namespacedName = root.plugin ? `${root.plugin}:${serverName}` : serverName;
|
|
@@ -446,7 +556,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
446
556
|
const substitutedCwd = raw.cwd !== undefined ? substitutePluginRoot(raw.cwd, root.path) : undefined;
|
|
447
557
|
// Root relative command/cwd at the plugin's config directory, not the
|
|
448
558
|
// session cwd (MCP stdio spawning resolves relative values there).
|
|
449
|
-
const rooted = resolvePluginStdioPaths({ command: substitutedCommand, cwd: substitutedCwd },
|
|
559
|
+
const rooted = resolvePluginStdioPaths({ command: substitutedCommand, cwd: substitutedCwd }, baseDir);
|
|
450
560
|
const server: MCPServer = {
|
|
451
561
|
name: namespacedName,
|
|
452
562
|
...(raw.enabled !== undefined && { enabled: raw.enabled }),
|
|
@@ -460,7 +570,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
460
570
|
...(raw.auth !== undefined && { auth: raw.auth }),
|
|
461
571
|
...(raw.oauth !== undefined && { oauth: raw.oauth }),
|
|
462
572
|
...(raw.type !== undefined && { transport: raw.type as MCPServer["transport"] }),
|
|
463
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
573
|
+
_source: createSourceMeta(PROVIDER_ID, sourcePath, root.scope),
|
|
464
574
|
};
|
|
465
575
|
items.push(server);
|
|
466
576
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-bound edit clipboard register.
|
|
3
|
+
*
|
|
4
|
+
* `CUT`/`PASTE` hashline ops share one {@link Clipboard} per session so
|
|
5
|
+
* content captured by one edit call can be pasted by a later one (and across
|
|
6
|
+
* files within one call). The executor works on a fork per batch and commits
|
|
7
|
+
* it back only after every write lands — see `executeHashlineSingle`.
|
|
8
|
+
*/
|
|
9
|
+
import type { Clipboard } from "@oh-my-pi/hashline";
|
|
10
|
+
|
|
11
|
+
interface EditClipboardOwner {
|
|
12
|
+
editClipboard?: Clipboard;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Look up (or lazily create) the clipboard register attached to a session.
|
|
17
|
+
* Storage lives on `session.editClipboard` so it ages out exactly with the
|
|
18
|
+
* session itself.
|
|
19
|
+
*/
|
|
20
|
+
export function getEditClipboard(session: EditClipboardOwner): Clipboard {
|
|
21
|
+
session.editClipboard ??= {};
|
|
22
|
+
return session.editClipboard;
|
|
23
|
+
}
|
|
@@ -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)
|