@toddzheng024/dscode-bundle 0.7.12 → 0.7.14
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +6 -2
- package/package.json +7 -5
- package/plugins/code-review/index.mjs +5 -2
- package/plugins/compaction/threshold.mjs +12 -0
- package/plugins/credentials/index.mjs +13 -0
- package/plugins/grok/adapter.mjs +114 -0
- package/plugins/grok/auth.mjs +76 -0
- package/plugins/grok/billing.mjs +146 -0
- package/plugins/grok/index.mjs +118 -0
- package/plugins/grok/models.mjs +129 -0
- package/plugins/grok/status.mjs +28 -0
- package/plugins/grok/wire.mjs +268 -0
- package/plugins/i18n/messages.mjs +6 -6
- package/plugins/providers/catalog.mjs +2 -0
- package/plugins/session-metrics/view.mjs +34 -3
- package/vendor/compaction-basic/index.js +91 -1
- package/vendor/persistent/index.js +10 -9
- package/vendor/terminal/index.js +5 -0
- package/vendor/tui/lib/app.mjs +6711 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +84 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2200 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1864 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44441
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dscode: the welcome screen's pixel snowflake, its half-block row packing and
|
|
3
|
+
* the path folding that keeps a long directory inside the details column.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-code/dscode/welcome
|
|
6
|
+
*/
|
|
7
|
+
import { singleLineText, truncateColumns } from '../render/text.mjs';
|
|
8
|
+
import { visibleColumns } from '../render/markdown.mjs';
|
|
9
|
+
export function welcomePath(path, width) {
|
|
10
|
+
const full = singleLineText(path || '');
|
|
11
|
+
if (visibleColumns(full) <= width)
|
|
12
|
+
return full;
|
|
13
|
+
const segments = full.split(/[\\/]/).filter(Boolean);
|
|
14
|
+
let suffix = '';
|
|
15
|
+
for (let index = segments.length - 1; index >= 0; index--) {
|
|
16
|
+
const candidate = '/' + segments[index] + suffix;
|
|
17
|
+
if (visibleColumns('…' + candidate) > width)
|
|
18
|
+
break;
|
|
19
|
+
suffix = candidate;
|
|
20
|
+
}
|
|
21
|
+
return suffix ? '…' + suffix : '…' + truncateColumns(segments.at(-1) || full, Math.max(1, width - 1));
|
|
22
|
+
}
|
|
23
|
+
/** Pixel rows of the welcome snowflake for terminals of 24+ rows. */
|
|
24
|
+
export const WELCOME_ART = [
|
|
25
|
+
"...........1.1..........",
|
|
26
|
+
"............1...........",
|
|
27
|
+
"............1...........",
|
|
28
|
+
".........1..1..1........",
|
|
29
|
+
".......1..22222..1......",
|
|
30
|
+
"...1...1....2....1...1..",
|
|
31
|
+
"..11...2....2....2...11.",
|
|
32
|
+
"....11.2...222...2.11...",
|
|
33
|
+
"......22.22.3.22.22.....",
|
|
34
|
+
".....22.22.333.22.22....",
|
|
35
|
+
"....1...2.33332.2...1...",
|
|
36
|
+
"........2.33322.2.......",
|
|
37
|
+
"....1...2.33222.2...1...",
|
|
38
|
+
".....22.22.222.22.22....",
|
|
39
|
+
"......22.22.3.22.22.....",
|
|
40
|
+
"....11.2...222...2.11...",
|
|
41
|
+
"..11...2....2....2...11.",
|
|
42
|
+
"...1...1....2....1...1..",
|
|
43
|
+
".......1..22222..1......",
|
|
44
|
+
".........1..1..1........",
|
|
45
|
+
"............1...........",
|
|
46
|
+
"............1...........",
|
|
47
|
+
"...........1.1..........",
|
|
48
|
+
"........................"
|
|
49
|
+
];
|
|
50
|
+
/** The shorter snowflake used under 26 rows. */
|
|
51
|
+
export const WELCOME_ART_SMALL = [
|
|
52
|
+
"..........1.1.........",
|
|
53
|
+
"...........1..........",
|
|
54
|
+
"........1..1..1.......",
|
|
55
|
+
".........1.2.1........",
|
|
56
|
+
"...1..1...222...1..1..",
|
|
57
|
+
"...1..1....2....1..1..",
|
|
58
|
+
"..1.1.2...222...2.1.1.",
|
|
59
|
+
".....22.22.3.22.22....",
|
|
60
|
+
"....12.22.333.22.21...",
|
|
61
|
+
"...1...2.33332.2...1..",
|
|
62
|
+
".......2.33322.2......",
|
|
63
|
+
"...1...2.33222.2...1..",
|
|
64
|
+
"....12.22.222.22.21...",
|
|
65
|
+
".....22.22.3.22.22....",
|
|
66
|
+
"..1.1.2...222...2.1.1.",
|
|
67
|
+
"...1..1....2....1..1..",
|
|
68
|
+
"...1..1...222...1..1..",
|
|
69
|
+
".........1.2.1........",
|
|
70
|
+
"........1..1..1.......",
|
|
71
|
+
"...........1..........",
|
|
72
|
+
"..........1.1.........",
|
|
73
|
+
"......................"
|
|
74
|
+
];
|
|
75
|
+
export function welcomeArtRows(grid, tones) {
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (let y = 0; y < grid.length; y += 2) {
|
|
78
|
+
const top = grid[y] || '';
|
|
79
|
+
const bottom = grid[y + 1] || '';
|
|
80
|
+
const segments = [];
|
|
81
|
+
for (let x = 0; x < Math.max(top.length, bottom.length); x++) {
|
|
82
|
+
const upper = tones[top[x]] || '';
|
|
83
|
+
const lower = tones[bottom[x]] || '';
|
|
84
|
+
const glyph = upper && lower ? upper === lower ? '\u2588' : '\u2580' : upper ? '\u2580' : lower ? '\u2584' : ' ';
|
|
85
|
+
const color = upper || lower;
|
|
86
|
+
const background = upper && lower && upper !== lower ? lower : '';
|
|
87
|
+
const last = segments[segments.length - 1];
|
|
88
|
+
if (last && last.glyph === glyph && last.color === color && last.background === background)
|
|
89
|
+
last.text += glyph;
|
|
90
|
+
else
|
|
91
|
+
segments.push({ glyph, text: glyph, color, background });
|
|
92
|
+
}
|
|
93
|
+
rows.push(segments);
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
/** dscode: pad a label to a column width (CJK-safe). */
|
|
98
|
+
export function dscodePadEnd(text, width) {
|
|
99
|
+
return text + ' '.repeat(Math.max(1, width - visibleColumns(text)));
|
|
100
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code-family terminal keybinding repair. VS Code hands Ctrl+R to the
|
|
3
|
+
* workbench (Open Recent) even while an integrated terminal owns focus, so
|
|
4
|
+
* the reasoning-fold key never reaches the TUI. Workspace-scoped keybindings
|
|
5
|
+
* do not exist, so the fix is one user-level keybindings.json rule that
|
|
6
|
+
* forwards the raw Ctrl byte via sendSequence under terminalFocus. This
|
|
7
|
+
* module detects the hosting editor variant, resolves its user
|
|
8
|
+
* keybindings.json, and merges the rule idempotently; pure merge/detect
|
|
9
|
+
* helpers are separated from the fs orchestration so both stay testable.
|
|
10
|
+
* @module @deepseek-ai/dsh-code/editor-keys
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { basename, dirname, join } from 'node:path';
|
|
14
|
+
/** Install directory names per family ('vscode' covers stable and Insiders). */
|
|
15
|
+
const FAMILY_DIRS = {
|
|
16
|
+
vscode: ['Code', 'Code - Insiders'],
|
|
17
|
+
cursor: ['Cursor'],
|
|
18
|
+
vscodium: ['VSCodium'],
|
|
19
|
+
windsurf: ['Windsurf'],
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Detect the editor hosting this integrated terminal.
|
|
23
|
+
* @param env - process environment (TERM_PROGRAM decides; case/whitespace tolerant).
|
|
24
|
+
* @returns the family, or undefined outside VS Code-family terminals.
|
|
25
|
+
*/
|
|
26
|
+
export function detectEditorTerminalFamily(env = process.env) {
|
|
27
|
+
const program = env.TERM_PROGRAM?.trim().toLowerCase();
|
|
28
|
+
if (program === 'vscode' || program === 'cursor' || program === 'vscodium' || program === 'windsurf')
|
|
29
|
+
return program;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether the pty is hosted away from the editor UI (ssh/container/tunnel).
|
|
34
|
+
* Keybindings live on the client machine, so a remote session must never
|
|
35
|
+
* write them server-side.
|
|
36
|
+
*/
|
|
37
|
+
export function isRemoteTerminalEnv(env = process.env) {
|
|
38
|
+
return (env.VSCODE_IPC_HOOK_CLI ?? '').trim() !== '';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the user keybindings.json candidates for one family, most likely
|
|
42
|
+
* install first. Only paths that exist on disk are repaired.
|
|
43
|
+
*/
|
|
44
|
+
export function editorKeybindingCandidates(family, context) {
|
|
45
|
+
return FAMILY_DIRS[family].map(dir => {
|
|
46
|
+
if (context.platform === 'win32') {
|
|
47
|
+
return join(context.appdata ?? join(context.homedir, 'AppData', 'Roaming'), dir, 'User', 'keybindings.json');
|
|
48
|
+
}
|
|
49
|
+
if (context.platform === 'darwin') {
|
|
50
|
+
return join(context.homedir, 'Library', 'Application Support', dir, 'User', 'keybindings.json');
|
|
51
|
+
}
|
|
52
|
+
return join(context.homedir, '.config', dir, 'User', 'keybindings.json');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** The one workbench rule that hands Ctrl+R to the focused terminal. */
|
|
56
|
+
export const CTRL_R_PASSTHROUGH_RULE = {
|
|
57
|
+
key: 'ctrl+r',
|
|
58
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
59
|
+
args: { text: '\u0012' },
|
|
60
|
+
when: 'terminalFocus',
|
|
61
|
+
};
|
|
62
|
+
/** Serialized rule block (4-space indent, the editors' default style). */
|
|
63
|
+
const RULE_BLOCK = [
|
|
64
|
+
' {',
|
|
65
|
+
' "key": "ctrl+r",',
|
|
66
|
+
' "command": "workbench.action.terminal.sendSequence",',
|
|
67
|
+
' "args": { "text": "\\u0012" },',
|
|
68
|
+
' "when": "terminalFocus"',
|
|
69
|
+
' }',
|
|
70
|
+
].join('\n');
|
|
71
|
+
/** Fresh-file template carrying the editors' standard header comment. */
|
|
72
|
+
const KEYBINDINGS_TEMPLATE = '// Place your key bindings in this file to override the defaults\n[\n' + RULE_BLOCK + '\n]\n';
|
|
73
|
+
/** Whether one parsed keybindings entry already forwards Ctrl+R to the terminal. */
|
|
74
|
+
function isCtrlRPassthroughEntry(entry) {
|
|
75
|
+
if (typeof entry !== 'object' || entry === null)
|
|
76
|
+
return false;
|
|
77
|
+
const record = entry;
|
|
78
|
+
if (record.command !== CTRL_R_PASSTHROUGH_RULE.command)
|
|
79
|
+
return false;
|
|
80
|
+
if (typeof record.key !== 'string' || record.key.trim().toLowerCase() !== CTRL_R_PASSTHROUGH_RULE.key)
|
|
81
|
+
return false;
|
|
82
|
+
const args = record.args;
|
|
83
|
+
if (typeof args !== 'object' || args === null)
|
|
84
|
+
return false;
|
|
85
|
+
const text = args.text;
|
|
86
|
+
if (typeof text !== 'string' || !text.includes('\u0012'))
|
|
87
|
+
return false;
|
|
88
|
+
return typeof record.when === 'string' && /\bterminalFocus\b/u.test(record.when);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Remove // and block comments from one JSONC document. Double-quoted strings
|
|
92
|
+
* survive untouched, so comment markers inside string values are preserved.
|
|
93
|
+
*/
|
|
94
|
+
export function stripJsoncComments(text) {
|
|
95
|
+
let out = '';
|
|
96
|
+
let index = 0;
|
|
97
|
+
let inString = false;
|
|
98
|
+
while (index < text.length) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
if (inString) {
|
|
101
|
+
out += char;
|
|
102
|
+
if (char === '\\' && index + 1 < text.length) {
|
|
103
|
+
out += text[index + 1];
|
|
104
|
+
index += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '"')
|
|
108
|
+
inString = false;
|
|
109
|
+
index += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (char === '"') {
|
|
113
|
+
inString = true;
|
|
114
|
+
out += char;
|
|
115
|
+
index += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (char === '/' && text[index + 1] === '/') {
|
|
119
|
+
while (index < text.length && text[index] !== '\n')
|
|
120
|
+
index += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (char === '/' && text[index + 1] === '*') {
|
|
124
|
+
index += 2;
|
|
125
|
+
while (index < text.length && !(text[index] === '*' && text[index + 1] === '/')) {
|
|
126
|
+
if (text[index] === '\n')
|
|
127
|
+
out += '\n';
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
index += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
out += char;
|
|
134
|
+
index += 1;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/** Drop commas directly before a closing bracket (string-aware). */
|
|
139
|
+
function removeTrailingCommas(text) {
|
|
140
|
+
let out = '';
|
|
141
|
+
let index = 0;
|
|
142
|
+
let inString = false;
|
|
143
|
+
while (index < text.length) {
|
|
144
|
+
const char = text[index];
|
|
145
|
+
if (inString) {
|
|
146
|
+
out += char;
|
|
147
|
+
if (char === '\\' && index + 1 < text.length) {
|
|
148
|
+
out += text[index + 1];
|
|
149
|
+
index += 2;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (char === '"')
|
|
153
|
+
inString = false;
|
|
154
|
+
index += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (char === '"') {
|
|
158
|
+
inString = true;
|
|
159
|
+
out += char;
|
|
160
|
+
index += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === ',') {
|
|
164
|
+
let peek = index + 1;
|
|
165
|
+
while (peek < text.length && (text[peek] === ' ' || text[peek] === '\t' || text[peek] === '\n' || text[peek] === '\r'))
|
|
166
|
+
peek += 1;
|
|
167
|
+
const next = text[peek];
|
|
168
|
+
if (next === '}' || next === ']') {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
out += char;
|
|
174
|
+
index += 1;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** Parse one JSONC document; trailing commas are tolerated. */
|
|
179
|
+
export function parseJsonc(text) {
|
|
180
|
+
return JSON.parse(removeTrailingCommas(stripJsoncComments(text)));
|
|
181
|
+
}
|
|
182
|
+
/** Raw index of the top-level rule array's opening bracket, or -1 when absent. */
|
|
183
|
+
function rawOpenBracketIndex(text) {
|
|
184
|
+
let index = 0;
|
|
185
|
+
let inString = false;
|
|
186
|
+
while (index < text.length) {
|
|
187
|
+
const char = text[index];
|
|
188
|
+
if (inString) {
|
|
189
|
+
if (char === '\\') {
|
|
190
|
+
index += 2;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (char === '"')
|
|
194
|
+
inString = false;
|
|
195
|
+
index += 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (char === '"') {
|
|
199
|
+
inString = true;
|
|
200
|
+
index += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (char === '/' && text[index + 1] === '/') {
|
|
204
|
+
while (index < text.length && text[index] !== '\n')
|
|
205
|
+
index += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (char === '/' && text[index + 1] === '*') {
|
|
209
|
+
index += 2;
|
|
210
|
+
while (index < text.length && !(text[index] === '*' && text[index + 1] === '/'))
|
|
211
|
+
index += 1;
|
|
212
|
+
index += 2;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (char === '[')
|
|
216
|
+
return index;
|
|
217
|
+
index += 1;
|
|
218
|
+
}
|
|
219
|
+
return -1;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Merge the Ctrl+R passthrough into one keybindings.json document. The raw
|
|
223
|
+
* text is preserved verbatim (comments included); the rule is inserted right
|
|
224
|
+
* after the array opener so it cannot be shadowed by later conflicting user
|
|
225
|
+
* rules. Missing files resolve to a fresh template.
|
|
226
|
+
* @throws when the document does not carry a rule array.
|
|
227
|
+
*/
|
|
228
|
+
export function mergeCtrlRPassthrough(raw) {
|
|
229
|
+
if (raw === undefined)
|
|
230
|
+
return { status: 'created', text: KEYBINDINGS_TEMPLATE };
|
|
231
|
+
const parsed = parseJsonc(raw);
|
|
232
|
+
if (!Array.isArray(parsed))
|
|
233
|
+
throw new Error('keybindings.json does not contain a rule array');
|
|
234
|
+
if (parsed.some(isCtrlRPassthroughEntry))
|
|
235
|
+
return { status: 'present' };
|
|
236
|
+
const open = rawOpenBracketIndex(raw);
|
|
237
|
+
if (open === -1)
|
|
238
|
+
throw new Error('keybindings.json does not contain a rule array');
|
|
239
|
+
const insert = parsed.length > 0 ? '\n' + RULE_BLOCK + ',' : '\n' + RULE_BLOCK;
|
|
240
|
+
return { status: 'updated', text: raw.slice(0, open + 1) + insert + raw.slice(open + 1) };
|
|
241
|
+
}
|
|
242
|
+
/** Parse one flag file snapshot; missing or corrupt content degrades to unshown. */
|
|
243
|
+
export function parseEditorKeysFlag(raw) {
|
|
244
|
+
if (raw === undefined)
|
|
245
|
+
return {};
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(raw);
|
|
248
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
249
|
+
return {};
|
|
250
|
+
const shown = parsed.hintShownAt;
|
|
251
|
+
return typeof shown === 'string' ? { hintShownAt: shown } : {};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Persist the shown marker; best-effort, the hint is cosmetic and never a gate. */
|
|
258
|
+
export async function markEditorKeysHintShown(path) {
|
|
259
|
+
await mkdir(dirname(path), { recursive: true });
|
|
260
|
+
await writeFile(path, JSON.stringify({ hintShownAt: new Date().toISOString() }, null, 2) + '\n', 'utf8');
|
|
261
|
+
}
|
|
262
|
+
/** Read one file if it exists; undefined otherwise (ENOENT and unreadable both). */
|
|
263
|
+
async function readIfPresent(path) {
|
|
264
|
+
try {
|
|
265
|
+
return await readFile(path, 'utf8');
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Apply the Ctrl+R passthrough to every local keybindings.json of the hosting
|
|
273
|
+
* editor and mark the startup hint shown. Existing files get a .dsh-bak
|
|
274
|
+
* backup before the first write.
|
|
275
|
+
* @returns a one-line user-facing summary.
|
|
276
|
+
* @throws with an actionable message when the environment cannot be repaired.
|
|
277
|
+
*/
|
|
278
|
+
export async function applyCtrlRPassthrough({ env, paths, flagPath }) {
|
|
279
|
+
const family = detectEditorTerminalFamily(env);
|
|
280
|
+
if (family === undefined) {
|
|
281
|
+
throw new Error('not a VS Code-family terminal (TERM_PROGRAM=' + (env.TERM_PROGRAM?.trim() || 'unset') + '); add the ctrl+r rule to keybindings.json manually');
|
|
282
|
+
}
|
|
283
|
+
if (isRemoteTerminalEnv(env)) {
|
|
284
|
+
throw new Error('remote terminal detected; apply the keybindings rule on the local machine instead');
|
|
285
|
+
}
|
|
286
|
+
const candidates = editorKeybindingCandidates(family, paths);
|
|
287
|
+
const targets = [];
|
|
288
|
+
for (const candidate of candidates) {
|
|
289
|
+
if (await readIfPresent(candidate) !== undefined)
|
|
290
|
+
targets.push(candidate);
|
|
291
|
+
}
|
|
292
|
+
if (targets.length === 0)
|
|
293
|
+
targets.push(candidates[0]);
|
|
294
|
+
const updated = [];
|
|
295
|
+
const present = [];
|
|
296
|
+
for (const target of targets) {
|
|
297
|
+
const raw = await readIfPresent(target);
|
|
298
|
+
const merge = mergeCtrlRPassthrough(raw);
|
|
299
|
+
if (merge.status === 'present') {
|
|
300
|
+
present.push(target);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
await mkdir(dirname(target), { recursive: true });
|
|
304
|
+
if (raw !== undefined)
|
|
305
|
+
await writeFile(target + '.dsh-bak', raw, 'utf8');
|
|
306
|
+
await writeFile(target, merge.text, 'utf8');
|
|
307
|
+
updated.push(target);
|
|
308
|
+
}
|
|
309
|
+
await markEditorKeysHintShown(flagPath).catch(() => { });
|
|
310
|
+
const label = (target) => basename(dirname(dirname(target)));
|
|
311
|
+
if (updated.length === 0) {
|
|
312
|
+
return 'ctrl+r passthrough already configured in ' + present.map(label).join(', ');
|
|
313
|
+
}
|
|
314
|
+
return 'ctrl+r passthrough written to ' + updated.map(label).join(', ') + ' — effective immediately';
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Resolve the one-shot startup hint for VS Code-family terminals. Fires at
|
|
318
|
+
* most once per install (flag file), never when the passthrough rule is
|
|
319
|
+
* already present, and never in remote ptys where the repair cannot run.
|
|
320
|
+
* @returns the hint line, or undefined to stay silent.
|
|
321
|
+
*/
|
|
322
|
+
export async function resolveEditorKeysStartupHint({ env, paths, flagPath }) {
|
|
323
|
+
const flag = parseEditorKeysFlag(await readIfPresent(flagPath));
|
|
324
|
+
if (flag.hintShownAt !== undefined)
|
|
325
|
+
return undefined;
|
|
326
|
+
const family = detectEditorTerminalFamily(env);
|
|
327
|
+
if (family === undefined || isRemoteTerminalEnv(env))
|
|
328
|
+
return undefined;
|
|
329
|
+
for (const candidate of editorKeybindingCandidates(family, paths)) {
|
|
330
|
+
const raw = await readIfPresent(candidate);
|
|
331
|
+
if (raw === undefined)
|
|
332
|
+
continue;
|
|
333
|
+
try {
|
|
334
|
+
const parsed = parseJsonc(raw);
|
|
335
|
+
if (Array.isArray(parsed) && parsed.some(isCtrlRPassthroughEntry)) {
|
|
336
|
+
await markEditorKeysHintShown(flagPath).catch(() => { });
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// An unparseable config still deserves the hint; apply reports the error.
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await markEditorKeysHintShown(flagPath).catch(() => { });
|
|
345
|
+
return 'run /vscode-keys to pass ctrl+r through this editor (alt+r works meanwhile)';
|
|
346
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Host editor and clipboard adapters used by the terminal surface. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
function waitForProcess(command, args, input) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, [...args], {
|
|
6
|
+
stdio: input === undefined ? 'inherit' : ['pipe', 'ignore', 'pipe'],
|
|
7
|
+
windowsHide: true,
|
|
8
|
+
});
|
|
9
|
+
let stderr = '';
|
|
10
|
+
child.stderr?.on('data', chunk => { stderr += String(chunk); });
|
|
11
|
+
child.once('error', reject);
|
|
12
|
+
child.once('exit', code => {
|
|
13
|
+
if (code === 0)
|
|
14
|
+
resolve();
|
|
15
|
+
else
|
|
16
|
+
reject(new Error(`${command} exited with code ${String(code)}${stderr.trim() === '' ? '' : `: ${stderr.trim()}`}`));
|
|
17
|
+
});
|
|
18
|
+
if (input !== undefined)
|
|
19
|
+
child.stdin?.end(input);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Copy UTF-8 text through the platform clipboard command. */
|
|
23
|
+
export async function copyText(text) {
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
// Windows PowerShell 5 reads redirected stdin using the active console
|
|
26
|
+
// code page by default. Node writes UTF-8, so CJK copied through `$input`
|
|
27
|
+
// became mojibake. Set the pipe encoding before reading it.
|
|
28
|
+
await waitForProcess('powershell.exe', [
|
|
29
|
+
'-NoProfile',
|
|
30
|
+
'-NonInteractive',
|
|
31
|
+
'-Command',
|
|
32
|
+
'[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); Set-Clipboard -Value ([Console]::In.ReadToEnd())',
|
|
33
|
+
], text);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (process.platform === 'darwin') {
|
|
37
|
+
await waitForProcess('pbcopy', [], text);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await waitForProcess('xclip', ['-selection', 'clipboard'], text);
|
|
41
|
+
}
|
|
42
|
+
/** Latest complete assistant text, excluding streaming and reasoning. */
|
|
43
|
+
export function latestAssistantText(view) {
|
|
44
|
+
for (let index = view.entries.length - 1; index >= 0; index -= 1) {
|
|
45
|
+
const entry = view.entries[index];
|
|
46
|
+
if (entry?.kind === 'assistant' && entry.text !== '')
|
|
47
|
+
return entry.text;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Pure session-fork boundary policy shared by the TUI command and tests. */
|
|
2
|
+
/** Select a completed turn and trailing between-turn metadata. */
|
|
3
|
+
export function selectForkSeed(events, atSeq) {
|
|
4
|
+
if (atSeq !== undefined && (!Number.isSafeInteger(atSeq) || atSeq < 0)) {
|
|
5
|
+
throw new Error('fork event sequence must be a non-negative integer');
|
|
6
|
+
}
|
|
7
|
+
const lastSeq = events.at(-1)?.seq ?? -1;
|
|
8
|
+
const anchored = atSeq === undefined
|
|
9
|
+
? undefined
|
|
10
|
+
: events.find(event => event.type === 'turn/end' && event.seq >= atSeq);
|
|
11
|
+
const boundary = anchored
|
|
12
|
+
?? (atSeq === undefined || atSeq > lastSeq
|
|
13
|
+
? events.findLast(event => event.type === 'turn/end')
|
|
14
|
+
: undefined);
|
|
15
|
+
if (boundary === undefined) {
|
|
16
|
+
throw new Error(atSeq !== undefined && atSeq <= lastSeq
|
|
17
|
+
? `the turn containing event ${atSeq} has not completed`
|
|
18
|
+
: 'this session has no completed turn to fork from');
|
|
19
|
+
}
|
|
20
|
+
let cut = events.indexOf(boundary) + 1;
|
|
21
|
+
while (cut < events.length && events[cut]?.type !== 'turn/start')
|
|
22
|
+
cut += 1;
|
|
23
|
+
return { boundarySeq: boundary.seq, events: events.slice(0, cut) };
|
|
24
|
+
}
|