@herbertgao/pi-extensions 2026.9.4 → 2026.9.5
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/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
- package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
- package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
- package/node_modules/@tifan/pi-copy-response/README.md +2 -2
- package/node_modules/@tifan/pi-copy-response/package.json +3 -2
- package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
- package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
- package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
- package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
- package/node_modules/@tifan/pi-recap/package.json +3 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +125 -19
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
- package/node_modules/pi-mcp-adapter/commands.ts +13 -15
- package/node_modules/pi-mcp-adapter/config.ts +198 -30
- package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
- package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
- package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
- package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
- package/node_modules/pi-mcp-adapter/index.ts +827 -164
- package/node_modules/pi-mcp-adapter/init.ts +48 -10
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
- package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
- package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
- package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
- package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
- package/node_modules/pi-mcp-adapter/package.json +13 -3
- package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
- package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
- package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
- package/node_modules/pi-mcp-adapter/state.ts +10 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
- package/node_modules/pi-mcp-adapter/types.ts +57 -4
- package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
- package/node_modules/pi-mcp-adapter/utils.ts +64 -6
- package/package.json +12 -15
|
@@ -4,14 +4,37 @@ const os = require('os');
|
|
|
4
4
|
const { getClaudeDir, getConfigDir } = require('./ponytail-config');
|
|
5
5
|
|
|
6
6
|
const STATE_FILE = '.ponytail-active';
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
// ponytail: VS Code Copilot never sets COPILOT_PLUGIN_DATA — it only injects
|
|
9
|
+
// CLAUDE_PLUGIN_ROOT, pointed at an install path under .vscode/agent-plugins/
|
|
10
|
+
// (#528). Without this fallback isCopilot was false, so ponytail assumed
|
|
11
|
+
// native Claude Code and emitted the statusline nudge, which VS Code Copilot
|
|
12
|
+
// doesn't read.
|
|
13
|
+
function isVsCodeCopilotRoot(pluginRoot) {
|
|
14
|
+
if (!pluginRoot) return false;
|
|
15
|
+
return pluginRoot.split(/[\\/]+/).includes('agent-plugins') &&
|
|
16
|
+
pluginRoot.toLowerCase().includes('.vscode');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) ||
|
|
20
|
+
isVsCodeCopilotRoot(process.env.CLAUDE_PLUGIN_ROOT);
|
|
8
21
|
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
|
|
9
22
|
const isQoder = !isCopilot && !isCodex && Boolean(process.env.QODER_SESSION_ID);
|
|
23
|
+
// Cursor (#817): CURSOR_VERSION is set only in the environment Cursor builds
|
|
24
|
+
// for hook processes (Cursor 3.20.17 assigns it in exactly one place, the hook
|
|
25
|
+
// env builder), so it never leaks into a Claude Code session running inside
|
|
26
|
+
// Cursor's terminal. Cursor also sets it when it runs a Claude-format plugin's
|
|
27
|
+
// hooks next to CLAUDE_PLUGIN_ROOT, and it needs Cursor-shaped JSON either
|
|
28
|
+
// way, so this check comes after the hosts with their own data dirs.
|
|
29
|
+
const isCursor = !isCopilot && !isCodex && !isQoder && Boolean(process.env.CURSOR_VERSION);
|
|
10
30
|
|
|
11
31
|
let stateDir = getClaudeDir();
|
|
12
32
|
if (isCodex) stateDir = process.env.PLUGIN_DATA;
|
|
13
|
-
|
|
33
|
+
// COPILOT_PLUGIN_DATA is unset under VS Code Copilot, so fall back to
|
|
34
|
+
// getClaudeDir() rather than building a path from undefined.
|
|
35
|
+
if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA || getClaudeDir();
|
|
14
36
|
if (isQoder) stateDir = path.join(os.homedir(), '.qoder');
|
|
37
|
+
if (isCursor) stateDir = path.join(os.homedir(), '.cursor');
|
|
15
38
|
|
|
16
39
|
const statePath = path.join(stateDir, STATE_FILE);
|
|
17
40
|
|
|
@@ -33,6 +56,27 @@ function readMode() {
|
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
|
|
59
|
+
// Cursor's always-on project rule (.cursor/rules/ponytail.mdc) already puts the
|
|
60
|
+
// ruleset in front of every prompt and no hook can switch a rule off, so while
|
|
61
|
+
// it is in the workspace the hooks step back instead of injecting a second,
|
|
62
|
+
// possibly contradicting, copy (#817). Cursor hands every hook the workspace
|
|
63
|
+
// root as CURSOR_PROJECT_DIR; project hooks also run from that directory.
|
|
64
|
+
// ponytail: first workspace root only, a rule in a secondary folder of a
|
|
65
|
+
// multi-root workspace goes undetected.
|
|
66
|
+
function cursorRulePath() {
|
|
67
|
+
const root = process.env.CURSOR_PROJECT_DIR || process.cwd();
|
|
68
|
+
const rule = path.join(root, '.cursor', 'rules', 'ponytail.mdc');
|
|
69
|
+
return fs.existsSync(rule) ? rule : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cursorRuleNotice(rule) {
|
|
73
|
+
return 'PONYTAIL: the always-on Cursor rule ' + rule + ' is active in this workspace and ' +
|
|
74
|
+
'already carries the ponytail ruleset, so the ponytail hooks injected nothing further. ' +
|
|
75
|
+
'Mode switching (/ponytail lite|full|ultra|off, "stop ponytail") is unavailable while ' +
|
|
76
|
+
'that rule exists. When the user tries to switch or turn off ponytail, tell them to ' +
|
|
77
|
+
'delete that rule so hooks.json can manage the level.';
|
|
78
|
+
}
|
|
79
|
+
|
|
36
80
|
function writeHookOutput(event, mode, context = '') {
|
|
37
81
|
if (isCopilot) {
|
|
38
82
|
// Copilot reads additionalContext on SessionStart; ignores output elsewhere.
|
|
@@ -64,6 +108,18 @@ function writeHookOutput(event, mode, context = '') {
|
|
|
64
108
|
process.stdout.write(JSON.stringify(output));
|
|
65
109
|
return;
|
|
66
110
|
}
|
|
111
|
+
if (isCursor) {
|
|
112
|
+
// Cursor parses stdout as JSON and treats empty stdout as "nothing to
|
|
113
|
+
// say"; raw text would be logged as a parse error. sessionStart takes
|
|
114
|
+
// additional_context into the conversation's system context;
|
|
115
|
+
// beforeSubmitPrompt needs continue:true and, in Cursor 3.20.17, injects
|
|
116
|
+
// additional_context into that turn (docs/cursor-hooks.md).
|
|
117
|
+
if (!context) return;
|
|
118
|
+
const output = { additional_context: context };
|
|
119
|
+
if (event === 'UserPromptSubmit') output.continue = true;
|
|
120
|
+
process.stdout.write(JSON.stringify(output));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
67
123
|
// Native Claude: SessionStart accepts raw stdout, but SubagentStart needs the
|
|
68
124
|
// hookSpecificOutput JSON form or the context is dropped.
|
|
69
125
|
if (event === 'SubagentStart') {
|
|
@@ -76,8 +132,11 @@ function writeHookOutput(event, mode, context = '') {
|
|
|
76
132
|
|
|
77
133
|
module.exports = {
|
|
78
134
|
clearMode,
|
|
135
|
+
cursorRuleNotice,
|
|
136
|
+
cursorRulePath,
|
|
79
137
|
isCodex,
|
|
80
138
|
isCopilot,
|
|
139
|
+
isCursor,
|
|
81
140
|
isQoder,
|
|
82
141
|
readMode,
|
|
83
142
|
setMode,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dietrichgebert/ponytail",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
|
|
5
5
|
"keywords": ["opencode-plugin", "opencode", "ponytail", "pi-package", "pi", "skills", "qoder"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
".qoder-plugin/",
|
|
31
31
|
"pi-extension/",
|
|
32
32
|
"scripts/uninstall.js",
|
|
33
|
+
"scripts/cursor-hooks.js",
|
|
33
34
|
"assets/",
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ponytail — install or remove the Cursor hooks (hooks/cursor-hooks.json) in
|
|
3
|
+
// ~/.cursor/hooks.json (default) or <cwd>/.cursor/hooks.json (--project),
|
|
4
|
+
// merging with whatever hooks are already there. Only entries that run one of
|
|
5
|
+
// ponytail's own hooks/ponytail-*.js scripts are added or removed; every other
|
|
6
|
+
// hook stays as it was.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/cursor-hooks.js install [--project]
|
|
9
|
+
// node scripts/cursor-hooks.js uninstall [--project]
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { isShellSafe } = require('../hooks/ponytail-config');
|
|
15
|
+
|
|
16
|
+
const ROOT = path.join(__dirname, '..');
|
|
17
|
+
const TEMPLATE = path.join(ROOT, 'hooks', 'cursor-hooks.json');
|
|
18
|
+
const PONYTAIL_HOOK = /ponytail-[\w-]+\.js/;
|
|
19
|
+
|
|
20
|
+
function isPonytailHook(entry) {
|
|
21
|
+
return Boolean(entry && typeof entry.command === 'string' && PONYTAIL_HOOK.test(entry.command));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hooksPath(scope) {
|
|
25
|
+
return scope === 'project'
|
|
26
|
+
? path.join(process.cwd(), '.cursor', 'hooks.json')
|
|
27
|
+
: path.join(os.homedir(), '.cursor', 'hooks.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Missing file → empty config. Malformed JSON throws a SyntaxError so the
|
|
31
|
+
// caller can refuse to touch the file rather than overwrite it.
|
|
32
|
+
function readConfig(file) {
|
|
33
|
+
let config = {};
|
|
34
|
+
try {
|
|
35
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.code !== 'ENOENT') throw e;
|
|
38
|
+
}
|
|
39
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
40
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) config.hooks = {};
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeConfig(file, config) {
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The template's PONYTAIL_DIR placeholder becomes this checkout's absolute
|
|
50
|
+
// path. Forward slashes run unchanged under cmd, PowerShell and bash, and the
|
|
51
|
+
// same allowlist that guards the statusline snippet keeps shell metacharacters
|
|
52
|
+
// out of the command string.
|
|
53
|
+
function ponytailEntries() {
|
|
54
|
+
const root = ROOT.replace(/\\/g, '/');
|
|
55
|
+
if (!isShellSafe(root)) {
|
|
56
|
+
throw new Error('ponytail is checked out at a path with shell metacharacters (' + ROOT +
|
|
57
|
+
'); move it, or copy hooks/cursor-hooks.json by hand and quote the path for your shell');
|
|
58
|
+
}
|
|
59
|
+
const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8'));
|
|
60
|
+
const hooks = {};
|
|
61
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
62
|
+
hooks[event] = entries.map((entry) => ({ ...entry, command: entry.command.replace(/PONYTAIL_DIR/g, root) }));
|
|
63
|
+
}
|
|
64
|
+
return hooks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stripPonytail(config) {
|
|
68
|
+
for (const [event, entries] of Object.entries(config.hooks)) {
|
|
69
|
+
if (!Array.isArray(entries)) continue;
|
|
70
|
+
const kept = entries.filter((entry) => !isPonytailHook(entry));
|
|
71
|
+
if (kept.length) config.hooks[event] = kept;
|
|
72
|
+
else delete config.hooks[event];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function install(scope) {
|
|
77
|
+
const file = hooksPath(scope);
|
|
78
|
+
const config = readConfig(file);
|
|
79
|
+
if (config.version === undefined) config.version = 1;
|
|
80
|
+
// Re-running replaces stale ponytail entries instead of duplicating them.
|
|
81
|
+
stripPonytail(config);
|
|
82
|
+
for (const [event, entries] of Object.entries(ponytailEntries())) {
|
|
83
|
+
config.hooks[event] = [...(config.hooks[event] || []), ...entries];
|
|
84
|
+
}
|
|
85
|
+
writeConfig(file, config);
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Returns the file it changed, or null when there was nothing of ponytail's in it.
|
|
90
|
+
function uninstall(scope) {
|
|
91
|
+
const file = hooksPath(scope);
|
|
92
|
+
if (!fs.existsSync(file)) return null;
|
|
93
|
+
const config = readConfig(file);
|
|
94
|
+
const before = JSON.stringify(config);
|
|
95
|
+
stripPonytail(config);
|
|
96
|
+
if (JSON.stringify(config) === before) return null;
|
|
97
|
+
const otherKeys = Object.keys(config).filter((k) => k !== 'version' && k !== 'hooks');
|
|
98
|
+
if (Object.keys(config.hooks).length === 0 && otherKeys.length === 0) {
|
|
99
|
+
// Only ponytail lived here: drop the file rather than leave an empty husk.
|
|
100
|
+
fs.unlinkSync(file);
|
|
101
|
+
} else {
|
|
102
|
+
writeConfig(file, config);
|
|
103
|
+
}
|
|
104
|
+
return file;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (require.main === module) {
|
|
108
|
+
const args = process.argv.slice(2);
|
|
109
|
+
const action = args[0];
|
|
110
|
+
const scope = args.includes('--project') ? 'project' : 'user';
|
|
111
|
+
try {
|
|
112
|
+
if (action === 'install') {
|
|
113
|
+
const file = install(scope);
|
|
114
|
+
console.log('Installed ponytail hooks in ' + file);
|
|
115
|
+
console.log('Cursor reloads hooks.json on save; start a new chat to activate.');
|
|
116
|
+
} else if (action === 'uninstall') {
|
|
117
|
+
const file = uninstall(scope);
|
|
118
|
+
console.log(file
|
|
119
|
+
? 'Removed ponytail hooks from ' + file
|
|
120
|
+
: 'No ponytail hooks in ' + hooksPath(scope));
|
|
121
|
+
} else {
|
|
122
|
+
console.error('usage: node scripts/cursor-hooks.js install|uninstall [--project]');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (e instanceof SyntaxError) {
|
|
127
|
+
console.error(hooksPath(scope) + ' is not valid JSON; nothing was changed, fix it by hand (' + e.message + ')');
|
|
128
|
+
} else {
|
|
129
|
+
console.error(e.message);
|
|
130
|
+
}
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { hooksPath, install, isPonytailHook, uninstall };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// ponytail — removes state ponytail wrote outside the plugin's own files:
|
|
3
|
-
// the mode flag, the config file,
|
|
4
|
-
// settings.json
|
|
5
|
-
// uninstall command (see README);
|
|
6
|
-
// can't see.
|
|
3
|
+
// the mode flag, the config file, the statusLine entry it added to
|
|
4
|
+
// settings.json, and its entries in ~/.cursor/hooks.json. Plugin files
|
|
5
|
+
// themselves are removed by each host's own uninstall command (see README);
|
|
6
|
+
// this only cleans up what those commands can't see.
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const { getConfigPath, getClaudeDir } = require('../hooks/ponytail-config');
|
|
12
|
+
const cursorHooks = require('./cursor-hooks');
|
|
11
13
|
|
|
12
14
|
const STATUSLINE_SCRIPT = 'ponytail-statusline';
|
|
13
15
|
|
|
@@ -21,8 +23,23 @@ function removeIfExists(filePath, label) {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
removeIfExists(path.join(getClaudeDir(), '.ponytail-active'), 'mode flag');
|
|
26
|
+
removeIfExists(path.join(os.homedir(), '.cursor', '.ponytail-active'), 'Cursor mode flag');
|
|
24
27
|
removeIfExists(getConfigPath(), 'config file');
|
|
25
28
|
|
|
29
|
+
// Cursor hooks (#817): drop only ponytail's entries from ~/.cursor/hooks.json,
|
|
30
|
+
// keep every other hook the user configured there.
|
|
31
|
+
try {
|
|
32
|
+
const hooksFile = cursorHooks.uninstall('user');
|
|
33
|
+
if (hooksFile) console.log(`Removed ponytail hooks from ${hooksFile}`);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e instanceof SyntaxError) {
|
|
36
|
+
// ponytail: malformed hooks.json — can't safely edit it; leave intact, warn
|
|
37
|
+
console.warn(`~/.cursor/hooks.json is malformed — could not remove the ponytail hook entries. Remove them manually from: ${cursorHooks.hooksPath('user')} (${e.message})`);
|
|
38
|
+
} else {
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
const settingsPath = path.join(getClaudeDir(), 'settings.json');
|
|
27
44
|
try {
|
|
28
45
|
const raw = fs.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, '');
|
|
@@ -334,6 +334,7 @@ export function createAutocompleteProvider(
|
|
|
334
334
|
current: AutocompleteProvider,
|
|
335
335
|
getReferences: () => Promise<SessionReference[]>,
|
|
336
336
|
currentCwd: string,
|
|
337
|
+
isCurrent: () => boolean = () => true,
|
|
337
338
|
): AutocompleteProvider {
|
|
338
339
|
return {
|
|
339
340
|
triggerCharacters: ["@"],
|
|
@@ -346,14 +347,15 @@ export function createAutocompleteProvider(
|
|
|
346
347
|
const currentLine = lines[cursorLine] ?? "";
|
|
347
348
|
const query = extractMentionQuery(currentLine.slice(0, cursorCol));
|
|
348
349
|
if (query === undefined) {
|
|
349
|
-
|
|
350
|
+
const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
351
|
+
return options.signal.aborted || !isCurrent() ? null : suggestions;
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
const [baseSuggestions, references] = await Promise.all([
|
|
353
355
|
current.getSuggestions(lines, cursorLine, cursorCol, options),
|
|
354
356
|
getReferences(),
|
|
355
357
|
]);
|
|
356
|
-
if (options.signal.aborted) return null;
|
|
358
|
+
if (options.signal.aborted || !isCurrent()) return null;
|
|
357
359
|
|
|
358
360
|
const sessionItems = filterSessions(references, query, currentCwd);
|
|
359
361
|
const fileItems = baseSuggestions?.prefix === `@${query}` ? baseSuggestions.items : [];
|
|
@@ -416,6 +418,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
416
418
|
subagentIds.clear();
|
|
417
419
|
clearLiveSubagentRecords();
|
|
418
420
|
let loadErrorShown = false;
|
|
421
|
+
const currentCwd = ctx.cwd;
|
|
419
422
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
420
423
|
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
421
424
|
let sessionsPromise: Promise<SessionInfo[]> | undefined;
|
|
@@ -429,7 +432,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
429
432
|
),
|
|
430
433
|
)
|
|
431
434
|
.catch((error: unknown) => {
|
|
432
|
-
if (!loadErrorShown) {
|
|
435
|
+
if (!loadErrorShown && generation === sessionGeneration) {
|
|
433
436
|
loadErrorShown = true;
|
|
434
437
|
const reason = error instanceof Error ? error.message : String(error);
|
|
435
438
|
ctx.ui.notify(`session-reference: failed to load sessions: ${reason}`, "error");
|
|
@@ -445,6 +448,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
445
448
|
| undefined;
|
|
446
449
|
const getReferences = async (): Promise<SessionReference[]> => {
|
|
447
450
|
const sessions = await getSessions();
|
|
451
|
+
// Session replacement can invalidate this closure while listAll() is pending.
|
|
452
|
+
if (generation !== sessionGeneration) return [];
|
|
448
453
|
const subagentKey = [...subagentIds].join("\0");
|
|
449
454
|
if (
|
|
450
455
|
referencesCache &&
|
|
@@ -455,21 +460,27 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
455
460
|
}
|
|
456
461
|
const ordered = orderSessionReferences(
|
|
457
462
|
mergeReferences(sessions, liveSubagentReferences(subagentIds, currentSessionId)),
|
|
458
|
-
|
|
463
|
+
currentCwd,
|
|
459
464
|
);
|
|
460
465
|
referencesCache = { sessions, subagentKey, ordered };
|
|
461
466
|
return ordered;
|
|
462
467
|
};
|
|
463
468
|
|
|
464
469
|
getAvailableReferences = getReferences;
|
|
465
|
-
|
|
470
|
+
const isTui = ctx.mode === "tui";
|
|
471
|
+
if (isTui) {
|
|
466
472
|
void getReferences();
|
|
467
473
|
// Register after other session_start handlers. pi-fff claims every @
|
|
468
474
|
// prefix, so a provider installed before it would never see session mentions.
|
|
469
475
|
setTimeout(() => {
|
|
470
476
|
if (generation !== sessionGeneration) return;
|
|
471
477
|
ctx.ui.addAutocompleteProvider((current) =>
|
|
472
|
-
createAutocompleteProvider(
|
|
478
|
+
createAutocompleteProvider(
|
|
479
|
+
current,
|
|
480
|
+
getReferences,
|
|
481
|
+
currentCwd,
|
|
482
|
+
() => generation === sessionGeneration,
|
|
483
|
+
),
|
|
473
484
|
);
|
|
474
485
|
}, 0);
|
|
475
486
|
}
|
|
@@ -479,14 +490,23 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
479
490
|
const referenceIds = extractSessionReferenceIds(event.prompt);
|
|
480
491
|
if (referenceIds.length === 0) return;
|
|
481
492
|
|
|
493
|
+
const generation = sessionGeneration;
|
|
482
494
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
483
495
|
const references = await (getAvailableReferences?.() ??
|
|
484
|
-
SessionManager.listAll()
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
496
|
+
SessionManager.listAll()
|
|
497
|
+
.then((sessions) =>
|
|
498
|
+
mergeReferences(
|
|
499
|
+
sessions.filter((session) => session.id !== currentSessionId),
|
|
500
|
+
liveSubagentReferences(subagentIds, currentSessionId),
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
.catch((error: unknown) => {
|
|
504
|
+
if (generation !== sessionGeneration) return [];
|
|
505
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
506
|
+
ctx.ui.notify(`session-reference: failed to load sessions: ${reason}`, "error");
|
|
507
|
+
return [];
|
|
508
|
+
}));
|
|
509
|
+
if (generation !== sessionGeneration) return;
|
|
490
510
|
const referencesById = new Map<string, SessionReference>();
|
|
491
511
|
const referencesByName = new Map<string, SessionReference>();
|
|
492
512
|
const ambiguousNames = new Set<string>();
|
|
@@ -565,9 +585,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
565
585
|
});
|
|
566
586
|
|
|
567
587
|
pi.on("session_before_switch", () => {
|
|
568
|
-
|
|
569
|
-
clearLiveSubagentRecords();
|
|
570
|
-
getAvailableReferences = undefined;
|
|
588
|
+
// This event can cancel; preserve current-session state until shutdown.
|
|
571
589
|
});
|
|
572
590
|
|
|
573
591
|
pi.on("session_shutdown", () => {
|
|
@@ -42,11 +42,13 @@ import {
|
|
|
42
42
|
isScrollBottomInput,
|
|
43
43
|
renderScrollButton,
|
|
44
44
|
resetScrollButtonState,
|
|
45
|
+
restoreOfficialScrollToEnd,
|
|
45
46
|
scheduleScrollButtonSync,
|
|
46
47
|
setScrollButtonHovered,
|
|
47
48
|
setScrollButtonVisible,
|
|
48
49
|
setScrollButtonWidget,
|
|
49
50
|
setToolMouseTui,
|
|
51
|
+
syncOfficialScrollToEnd,
|
|
50
52
|
getScrollButtonVisible,
|
|
51
53
|
getScrollButtonWidget,
|
|
52
54
|
getToolMouseTui,
|
|
@@ -879,6 +881,7 @@ export function teardownToolMouseInteraction(
|
|
|
879
881
|
}
|
|
880
882
|
restoreToolMouseRenderPatch();
|
|
881
883
|
restoreFullscreenViewportInput(getToolMouseTui());
|
|
884
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
882
885
|
resetScrollButtonState();
|
|
883
886
|
setToolMouseTui(null);
|
|
884
887
|
toolMouseUi = null;
|
|
@@ -894,6 +897,7 @@ export function resetToolHoverState(): void {
|
|
|
894
897
|
setHoveredCompactAssistant(null);
|
|
895
898
|
setScrollButtonVisible(false);
|
|
896
899
|
setScrollButtonHovered(false);
|
|
900
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
897
901
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
898
902
|
}
|
|
899
903
|
|
|
@@ -916,12 +920,12 @@ export function installToolMouseInteraction(
|
|
|
916
920
|
setToolMouseTui(tui);
|
|
917
921
|
setToolTuiFullscreen(fullscreenLazyTui(tui));
|
|
918
922
|
if (isLazyProxyTui(tui)) {
|
|
919
|
-
|
|
923
|
+
syncOfficialScrollToEnd(tui);
|
|
920
924
|
patchFullscreenViewportInput(tui);
|
|
921
925
|
ensureFullscreenToolMouseMotion(tui);
|
|
922
926
|
setScrollButtonWidget({
|
|
923
927
|
render: (width: number) => {
|
|
924
|
-
|
|
928
|
+
syncOfficialScrollToEnd(tui);
|
|
925
929
|
patchFullscreenViewportInput(tui);
|
|
926
930
|
ensureFullscreenToolMouseMotion(tui);
|
|
927
931
|
return renderScrollButton(width, theme);
|
|
@@ -83,9 +83,31 @@ export function fullscreenLazyTui(tui: any): boolean {
|
|
|
83
83
|
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const OFFICIAL_SCROLL_TO_END_KEY = Symbol.for("pi.ccstyle.official-scroll-to-end");
|
|
87
|
+
|
|
86
88
|
/** 关掉 pi 0.85 Jump to latest overlay,避免和本仓库 dock 按钮叠两层。 */
|
|
87
89
|
export function disableOfficialScrollToEnd(tui: any): void {
|
|
88
|
-
if (
|
|
90
|
+
if (!tui) return;
|
|
91
|
+
const current = tui.scrollToEndIndicator;
|
|
92
|
+
if (typeof current !== "function") return;
|
|
93
|
+
if (!tui[OFFICIAL_SCROLL_TO_END_KEY]) {
|
|
94
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = { original: current };
|
|
95
|
+
}
|
|
96
|
+
tui.scrollToEndIndicator = undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** /ccstyle off 或 teardown 时还回官方 overlay。 */
|
|
100
|
+
export function restoreOfficialScrollToEnd(tui: any): void {
|
|
101
|
+
if (!tui) return;
|
|
102
|
+
const original = tui[OFFICIAL_SCROLL_TO_END_KEY]?.original;
|
|
103
|
+
if (typeof original === "function") tui.scrollToEndIndicator = original;
|
|
104
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** on/compact 用 dock 按钮;off 还回 pi 原生 Jump to latest。 */
|
|
108
|
+
export function syncOfficialScrollToEnd(tui: any): void {
|
|
109
|
+
if (toolMouseInteractionActive()) disableOfficialScrollToEnd(tui);
|
|
110
|
+
else restoreOfficialScrollToEnd(tui);
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts
CHANGED
|
@@ -50,7 +50,7 @@ interface DiffTheme {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
type DiffLineKind = "add" | "remove" | "context";
|
|
53
|
-
type DiffEntryKind = "line" | "meta" | "hunk" | "file";
|
|
53
|
+
type DiffEntryKind = "line" | "omission" | "meta" | "hunk" | "file";
|
|
54
54
|
|
|
55
55
|
export interface DiffLineEntry {
|
|
56
56
|
kind: "line";
|
|
@@ -64,13 +64,19 @@ export interface DiffLineEntry {
|
|
|
64
64
|
hunkIndex: number;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface DiffOmissionEntry {
|
|
68
|
+
kind: "omission";
|
|
69
|
+
raw: string;
|
|
70
|
+
hunkIndex: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
interface DiffMetaEntry {
|
|
68
|
-
kind: Exclude<DiffEntryKind, "line">;
|
|
74
|
+
kind: Exclude<DiffEntryKind, "line" | "omission">;
|
|
69
75
|
raw: string;
|
|
70
76
|
hunkIndex: number;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
type ParsedDiffEntry = DiffLineEntry | DiffMetaEntry;
|
|
79
|
+
type ParsedDiffEntry = DiffLineEntry | DiffOmissionEntry | DiffMetaEntry;
|
|
74
80
|
|
|
75
81
|
export interface ParsedDiff {
|
|
76
82
|
entries: ParsedDiffEntry[];
|
|
@@ -94,6 +100,7 @@ interface RenderedRow {
|
|
|
94
100
|
interface SplitDiffRow {
|
|
95
101
|
left?: DiffLineEntry;
|
|
96
102
|
right?: DiffLineEntry;
|
|
103
|
+
omission?: DiffOmissionEntry;
|
|
97
104
|
meta?: DiffMetaEntry;
|
|
98
105
|
hunkIndex: number | null;
|
|
99
106
|
}
|
|
@@ -544,7 +551,7 @@ export function parseDiff(diffText: string): ParsedDiff {
|
|
|
544
551
|
|
|
545
552
|
// Pi 用空白行号填充省略上下文;它不是源码行,也不推进行号游标。
|
|
546
553
|
if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
|
|
547
|
-
entries.push(
|
|
554
|
+
entries.push({ kind: "omission", raw: rawLine, hunkIndex });
|
|
548
555
|
continue;
|
|
549
556
|
}
|
|
550
557
|
|
|
@@ -816,6 +823,12 @@ function buildSplitRows(entries: ParsedDiffEntry[]): SplitDiffRow[] {
|
|
|
816
823
|
break;
|
|
817
824
|
}
|
|
818
825
|
|
|
826
|
+
if (entry.kind === "omission") {
|
|
827
|
+
rows.push({ omission: entry, hunkIndex: entry.hunkIndex || null });
|
|
828
|
+
index++;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
819
832
|
if (entry.kind !== "line") {
|
|
820
833
|
rows.push({ meta: entry, hunkIndex: entry.hunkIndex || null });
|
|
821
834
|
index++;
|
|
@@ -1799,8 +1812,44 @@ interface DiffRenderContext {
|
|
|
1799
1812
|
showHashlineAnchors: boolean;
|
|
1800
1813
|
}
|
|
1801
1814
|
|
|
1802
|
-
|
|
1815
|
+
const OMISSION_GLYPH = "⋮";
|
|
1816
|
+
|
|
1817
|
+
function createOmissionRenderLine(entry: DiffOmissionEntry, compact = false): DiffLineEntry {
|
|
1818
|
+
return {
|
|
1819
|
+
kind: "line",
|
|
1820
|
+
lineKind: "context",
|
|
1821
|
+
oldLineNumber: null,
|
|
1822
|
+
newLineNumber: null,
|
|
1823
|
+
fallbackLineNumber: compact ? "" : OMISSION_GLYPH,
|
|
1824
|
+
content: compact ? OMISSION_GLYPH : "",
|
|
1825
|
+
raw: entry.raw,
|
|
1826
|
+
hunkIndex: entry.hunkIndex,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function materializeOmissionEntries(
|
|
1803
1831
|
entries: ParsedDiffEntry[],
|
|
1832
|
+
compact = false,
|
|
1833
|
+
): (DiffLineEntry | DiffMetaEntry)[] {
|
|
1834
|
+
return entries.map((entry) =>
|
|
1835
|
+
entry.kind === "omission" ? createOmissionRenderLine(entry, compact) : entry,
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function omitTerminalOmissions(entries: ParsedDiffEntry[]): ParsedDiffEntry[] {
|
|
1840
|
+
let lastLineIndex = -1;
|
|
1841
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
1842
|
+
if (entries[index]?.kind === "line") {
|
|
1843
|
+
lastLineIndex = index;
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return entries.filter((entry, index) => entry.kind !== "omission" || index < lastLineIndex);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function processDiffEntries(
|
|
1852
|
+
entries: (DiffLineEntry | DiffMetaEntry)[],
|
|
1804
1853
|
ctx: DiffRenderContext,
|
|
1805
1854
|
processLine: (entry: DiffLineEntry) => string[],
|
|
1806
1855
|
): RenderedRow[] {
|
|
@@ -1821,7 +1870,7 @@ function renderUnified(
|
|
|
1821
1870
|
ctx: DiffRenderContext,
|
|
1822
1871
|
lineNumberWidth: number,
|
|
1823
1872
|
): RenderedRow[] {
|
|
1824
|
-
return processDiffEntries(entries, ctx, (entry) => {
|
|
1873
|
+
return processDiffEntries(materializeOmissionEntries(entries), ctx, (entry) => {
|
|
1825
1874
|
const lineNumber =
|
|
1826
1875
|
entry.lineKind === "add"
|
|
1827
1876
|
? formatLineNumberLabel(
|
|
@@ -1871,6 +1920,10 @@ function toUnifiedFallbackRows(
|
|
|
1871
1920
|
): RenderedRow[] {
|
|
1872
1921
|
const flattened: ParsedDiffEntry[] = [];
|
|
1873
1922
|
for (const row of rows) {
|
|
1923
|
+
if (row.omission) {
|
|
1924
|
+
flattened.push(row.omission);
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1874
1927
|
if (row.meta) {
|
|
1875
1928
|
flattened.push(row.meta);
|
|
1876
1929
|
continue;
|
|
@@ -1886,7 +1939,7 @@ function toUnifiedFallbackRows(
|
|
|
1886
1939
|
}
|
|
1887
1940
|
|
|
1888
1941
|
function renderCompact(entries: ParsedDiffEntry[], ctx: DiffRenderContext): RenderedRow[] {
|
|
1889
|
-
return processDiffEntries(entries, ctx, (entry) => {
|
|
1942
|
+
return processDiffEntries(materializeOmissionEntries(entries, true), ctx, (entry) => {
|
|
1890
1943
|
const codeText = normalizeCodeWhitespace(
|
|
1891
1944
|
getCompactLineRenderContent(entry, ctx.showHashlineAnchors),
|
|
1892
1945
|
);
|
|
@@ -2105,8 +2158,9 @@ function renderSplit(
|
|
|
2105
2158
|
continue;
|
|
2106
2159
|
}
|
|
2107
2160
|
|
|
2161
|
+
const omissionLine = row.omission ? createOmissionRenderLine(row.omission) : undefined;
|
|
2108
2162
|
const leftCells = renderSplitCell(
|
|
2109
|
-
row.left,
|
|
2163
|
+
omissionLine ?? row.left,
|
|
2110
2164
|
"left",
|
|
2111
2165
|
leftWidth,
|
|
2112
2166
|
splitLineNumberWidth,
|
|
@@ -2120,7 +2174,7 @@ function renderSplit(
|
|
|
2120
2174
|
showHashlineAnchors,
|
|
2121
2175
|
);
|
|
2122
2176
|
const rightCells = renderSplitCell(
|
|
2123
|
-
row.right,
|
|
2177
|
+
omissionLine ?? row.right,
|
|
2124
2178
|
"right",
|
|
2125
2179
|
rightWidth,
|
|
2126
2180
|
splitLineNumberWidth,
|
|
@@ -2575,18 +2629,19 @@ export function renderEditDiffResult(
|
|
|
2575
2629
|
return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
|
|
2576
2630
|
}
|
|
2577
2631
|
|
|
2578
|
-
const
|
|
2632
|
+
const renderEntries = omitTerminalOmissions(parsed.entries);
|
|
2633
|
+
const splitRows = buildSplitRows(renderEntries);
|
|
2579
2634
|
const showHashlineAnchors =
|
|
2580
2635
|
options.expanded === true &&
|
|
2581
|
-
|
|
2582
|
-
const lineNumberWidth = getLineNumberWidth(
|
|
2636
|
+
renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
|
|
2637
|
+
const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
|
|
2583
2638
|
const palette = resolveDiffPalette(theme);
|
|
2584
2639
|
// Rich diffs use ccstyle's self shell. Keep the panel transparent so the
|
|
2585
2640
|
// separator cannot leak toolSuccessBg across the entire new column.
|
|
2586
2641
|
const containerBgAnsi = undefined;
|
|
2587
2642
|
const language = resolveLanguageFromPath(options.filePath);
|
|
2588
2643
|
const cache = createDiffRenderCache();
|
|
2589
|
-
const highlightLine = createCodeLineHighlighter(language, theme,
|
|
2644
|
+
const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
|
|
2590
2645
|
cache.invalidate();
|
|
2591
2646
|
options.invalidate?.();
|
|
2592
2647
|
});
|
|
@@ -2635,7 +2690,7 @@ export function renderEditDiffResult(
|
|
|
2635
2690
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
2636
2691
|
// Only highlight/render a prefix that can fill the display limit; full-diff
|
|
2637
2692
|
// LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
|
|
2638
|
-
const entryBudget = takeEntriesForLineBudget(
|
|
2693
|
+
const entryBudget = takeEntriesForLineBudget(renderEntries, processBudget);
|
|
2639
2694
|
const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
|
|
2640
2695
|
const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
|
|
2641
2696
|
const renderCtx: DiffRenderContext = {
|