dsh-code 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +21 -13
- package/README.md +21 -13
- package/lib/index.mjs +1156 -720
- package/lib/types/app.d.ts +2 -0
- package/lib/types/editor-keys.d.ts +105 -0
- package/lib/types/git-workflow.d.ts +6 -2
- package/lib/types/model-capabilities.d.ts +82 -0
- package/lib/types/provider-settings.d.ts +7 -0
- package/lib/types/render/lines.d.ts +25 -0
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +15 -1
- package/lib/types/render/text.d.ts +15 -9
- package/lib/types/render/width.d.ts +29 -0
- package/lib/types/session-directory.d.ts +27 -0
- package/lib/types/settings-file.d.ts +33 -0
- package/lib/types/store.d.ts +10 -0
- package/lib/types/subagents.d.ts +13 -3
- package/package.json +159 -159
- package/src/app.ts +1104 -1041
- package/src/editor-keys.ts +371 -0
- package/src/git-workflow.ts +10 -6
- package/src/index.ts +1637 -1523
- package/src/model-capabilities.ts +318 -0
- package/src/provider-settings.ts +16 -0
- package/src/render/lines.ts +403 -356
- package/src/render/markdown.ts +4 -7
- package/src/render/projection.ts +63 -40
- package/src/render/text.ts +152 -150
- package/src/render/width.ts +189 -0
- package/src/session-directory.ts +56 -0
- package/src/settings-file.ts +56 -0
- package/src/store.ts +26 -7
- package/src/subagents.ts +39 -6
|
@@ -0,0 +1,371 @@
|
|
|
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
|
+
|
|
13
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
14
|
+
import { basename, dirname, join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
/** Integrated-terminal editor variants this module can repair. */
|
|
17
|
+
export type EditorTerminalFamily = 'vscode' | 'cursor' | 'vscodium' | 'windsurf'
|
|
18
|
+
|
|
19
|
+
/** Install directory names per family ('vscode' covers stable and Insiders). */
|
|
20
|
+
const FAMILY_DIRS: Record<EditorTerminalFamily, readonly string[]> = {
|
|
21
|
+
vscode: ['Code', 'Code - Insiders'],
|
|
22
|
+
cursor: ['Cursor'],
|
|
23
|
+
vscodium: ['VSCodium'],
|
|
24
|
+
windsurf: ['Windsurf'],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect the editor hosting this integrated terminal.
|
|
29
|
+
* @param env - process environment (TERM_PROGRAM decides; case/whitespace tolerant).
|
|
30
|
+
* @returns the family, or undefined outside VS Code-family terminals.
|
|
31
|
+
*/
|
|
32
|
+
export function detectEditorTerminalFamily(env: NodeJS.ProcessEnv = process.env): EditorTerminalFamily | undefined {
|
|
33
|
+
const program = env.TERM_PROGRAM?.trim().toLowerCase()
|
|
34
|
+
if (program === 'vscode' || program === 'cursor' || program === 'vscodium' || program === 'windsurf') return program
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether the pty is hosted away from the editor UI (ssh/container/tunnel).
|
|
40
|
+
* Keybindings live on the client machine, so a remote session must never
|
|
41
|
+
* write them server-side.
|
|
42
|
+
*/
|
|
43
|
+
export function isRemoteTerminalEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
44
|
+
return (env.VSCODE_IPC_HOOK_CLI ?? '').trim() !== ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Filesystem anchors used to resolve editor config paths (injectable for tests). */
|
|
48
|
+
export interface EditorPathContext {
|
|
49
|
+
/** User home directory. */
|
|
50
|
+
homedir: string
|
|
51
|
+
/** %APPDATA% on Windows; only read for win32 resolution. */
|
|
52
|
+
appdata?: string
|
|
53
|
+
/** Node platform qualifier. */
|
|
54
|
+
platform: NodeJS.Platform
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the user keybindings.json candidates for one family, most likely
|
|
59
|
+
* install first. Only paths that exist on disk are repaired.
|
|
60
|
+
*/
|
|
61
|
+
export function editorKeybindingCandidates(family: EditorTerminalFamily, context: EditorPathContext): readonly string[] {
|
|
62
|
+
return FAMILY_DIRS[family].map(dir => {
|
|
63
|
+
if (context.platform === 'win32') {
|
|
64
|
+
return join(context.appdata ?? join(context.homedir, 'AppData', 'Roaming'), dir, 'User', 'keybindings.json')
|
|
65
|
+
}
|
|
66
|
+
if (context.platform === 'darwin') {
|
|
67
|
+
return join(context.homedir, 'Library', 'Application Support', dir, 'User', 'keybindings.json')
|
|
68
|
+
}
|
|
69
|
+
return join(context.homedir, '.config', dir, 'User', 'keybindings.json')
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The one workbench rule that hands Ctrl+R to the focused terminal. */
|
|
74
|
+
export const CTRL_R_PASSTHROUGH_RULE = {
|
|
75
|
+
key: 'ctrl+r',
|
|
76
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
77
|
+
args: { text: '\u0012' },
|
|
78
|
+
when: 'terminalFocus',
|
|
79
|
+
} as const
|
|
80
|
+
|
|
81
|
+
/** Serialized rule block (4-space indent, the editors' default style). */
|
|
82
|
+
const RULE_BLOCK = [
|
|
83
|
+
' {',
|
|
84
|
+
' "key": "ctrl+r",',
|
|
85
|
+
' "command": "workbench.action.terminal.sendSequence",',
|
|
86
|
+
' "args": { "text": "\\u0012" },',
|
|
87
|
+
' "when": "terminalFocus"',
|
|
88
|
+
' }',
|
|
89
|
+
].join('\n')
|
|
90
|
+
|
|
91
|
+
/** Fresh-file template carrying the editors' standard header comment. */
|
|
92
|
+
const KEYBINDINGS_TEMPLATE = '// Place your key bindings in this file to override the defaults\n[\n' + RULE_BLOCK + '\n]\n'
|
|
93
|
+
|
|
94
|
+
/** Whether one parsed keybindings entry already forwards Ctrl+R to the terminal. */
|
|
95
|
+
function isCtrlRPassthroughEntry(entry: unknown): boolean {
|
|
96
|
+
if (typeof entry !== 'object' || entry === null) return false
|
|
97
|
+
const record = entry as Record<string, unknown>
|
|
98
|
+
if (record.command !== CTRL_R_PASSTHROUGH_RULE.command) return false
|
|
99
|
+
if (typeof record.key !== 'string' || record.key.trim().toLowerCase() !== CTRL_R_PASSTHROUGH_RULE.key) return false
|
|
100
|
+
const args = record.args
|
|
101
|
+
if (typeof args !== 'object' || args === null) return false
|
|
102
|
+
const text = (args as Record<string, unknown>).text
|
|
103
|
+
if (typeof text !== 'string' || !text.includes('\u0012')) return false
|
|
104
|
+
return typeof record.when === 'string' && /\bterminalFocus\b/u.test(record.when)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove // and block comments from one JSONC document. Double-quoted strings
|
|
109
|
+
* survive untouched, so comment markers inside string values are preserved.
|
|
110
|
+
*/
|
|
111
|
+
export function stripJsoncComments(text: string): string {
|
|
112
|
+
let out = ''
|
|
113
|
+
let index = 0
|
|
114
|
+
let inString = false
|
|
115
|
+
while (index < text.length) {
|
|
116
|
+
const char = text[index]!
|
|
117
|
+
if (inString) {
|
|
118
|
+
out += char
|
|
119
|
+
if (char === '\\' && index + 1 < text.length) {
|
|
120
|
+
out += text[index + 1]!
|
|
121
|
+
index += 2
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (char === '"') inString = false
|
|
125
|
+
index += 1
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (char === '"') {
|
|
129
|
+
inString = true
|
|
130
|
+
out += char
|
|
131
|
+
index += 1
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (char === '/' && text[index + 1] === '/') {
|
|
135
|
+
while (index < text.length && text[index] !== '\n') index += 1
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (char === '/' && text[index + 1] === '*') {
|
|
139
|
+
index += 2
|
|
140
|
+
while (index < text.length && !(text[index] === '*' && text[index + 1] === '/')) {
|
|
141
|
+
if (text[index] === '\n') out += '\n'
|
|
142
|
+
index += 1
|
|
143
|
+
}
|
|
144
|
+
index += 2
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
out += char
|
|
148
|
+
index += 1
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Drop commas directly before a closing bracket (string-aware). */
|
|
154
|
+
function removeTrailingCommas(text: string): string {
|
|
155
|
+
let out = ''
|
|
156
|
+
let index = 0
|
|
157
|
+
let inString = false
|
|
158
|
+
while (index < text.length) {
|
|
159
|
+
const char = text[index]!
|
|
160
|
+
if (inString) {
|
|
161
|
+
out += char
|
|
162
|
+
if (char === '\\' && index + 1 < text.length) {
|
|
163
|
+
out += text[index + 1]!
|
|
164
|
+
index += 2
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (char === '"') inString = false
|
|
168
|
+
index += 1
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (char === '"') {
|
|
172
|
+
inString = true
|
|
173
|
+
out += char
|
|
174
|
+
index += 1
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (char === ',') {
|
|
178
|
+
let peek = index + 1
|
|
179
|
+
while (peek < text.length && (text[peek] === ' ' || text[peek] === '\t' || text[peek] === '\n' || text[peek] === '\r')) peek += 1
|
|
180
|
+
const next = text[peek]
|
|
181
|
+
if (next === '}' || next === ']') {
|
|
182
|
+
index += 1
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
out += char
|
|
187
|
+
index += 1
|
|
188
|
+
}
|
|
189
|
+
return out
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Parse one JSONC document; trailing commas are tolerated. */
|
|
193
|
+
export function parseJsonc(text: string): unknown {
|
|
194
|
+
return JSON.parse(removeTrailingCommas(stripJsoncComments(text)))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Raw index of the top-level rule array's opening bracket, or -1 when absent. */
|
|
198
|
+
function rawOpenBracketIndex(text: string): number {
|
|
199
|
+
let index = 0
|
|
200
|
+
let inString = false
|
|
201
|
+
while (index < text.length) {
|
|
202
|
+
const char = text[index]!
|
|
203
|
+
if (inString) {
|
|
204
|
+
if (char === '\\') {
|
|
205
|
+
index += 2
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (char === '"') inString = false
|
|
209
|
+
index += 1
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if (char === '"') {
|
|
213
|
+
inString = true
|
|
214
|
+
index += 1
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (char === '/' && text[index + 1] === '/') {
|
|
218
|
+
while (index < text.length && text[index] !== '\n') index += 1
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (char === '/' && text[index + 1] === '*') {
|
|
222
|
+
index += 2
|
|
223
|
+
while (index < text.length && !(text[index] === '*' && text[index + 1] === '/')) index += 1
|
|
224
|
+
index += 2
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (char === '[') return index
|
|
228
|
+
index += 1
|
|
229
|
+
}
|
|
230
|
+
return -1
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Outcome of merging the passthrough rule into one keybindings document. */
|
|
234
|
+
export type KeybindingsMerge =
|
|
235
|
+
| { readonly status: 'present' }
|
|
236
|
+
| { readonly status: 'updated'; readonly text: string }
|
|
237
|
+
| { readonly status: 'created'; readonly text: string }
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Merge the Ctrl+R passthrough into one keybindings.json document. The raw
|
|
241
|
+
* text is preserved verbatim (comments included); the rule is inserted right
|
|
242
|
+
* after the array opener so it cannot be shadowed by later conflicting user
|
|
243
|
+
* rules. Missing files resolve to a fresh template.
|
|
244
|
+
* @throws when the document does not carry a rule array.
|
|
245
|
+
*/
|
|
246
|
+
export function mergeCtrlRPassthrough(raw: string | undefined): KeybindingsMerge {
|
|
247
|
+
if (raw === undefined) return { status: 'created', text: KEYBINDINGS_TEMPLATE }
|
|
248
|
+
const parsed = parseJsonc(raw)
|
|
249
|
+
if (!Array.isArray(parsed)) throw new Error('keybindings.json does not contain a rule array')
|
|
250
|
+
if (parsed.some(isCtrlRPassthroughEntry)) return { status: 'present' }
|
|
251
|
+
const open = rawOpenBracketIndex(raw)
|
|
252
|
+
if (open === -1) throw new Error('keybindings.json does not contain a rule array')
|
|
253
|
+
const insert = parsed.length > 0 ? '\n' + RULE_BLOCK + ',' : '\n' + RULE_BLOCK
|
|
254
|
+
return { status: 'updated', text: raw.slice(0, open + 1) + insert + raw.slice(open + 1) }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** User-level marker file content: the startup hint fires at most once per install. */
|
|
258
|
+
export interface EditorKeysFlag {
|
|
259
|
+
hintShownAt?: string
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Parse one flag file snapshot; missing or corrupt content degrades to unshown. */
|
|
263
|
+
export function parseEditorKeysFlag(raw: string | undefined): EditorKeysFlag {
|
|
264
|
+
if (raw === undefined) return {}
|
|
265
|
+
try {
|
|
266
|
+
const parsed: unknown = JSON.parse(raw)
|
|
267
|
+
if (typeof parsed !== 'object' || parsed === null) return {}
|
|
268
|
+
const shown = (parsed as Record<string, unknown>).hintShownAt
|
|
269
|
+
return typeof shown === 'string' ? { hintShownAt: shown } : {}
|
|
270
|
+
} catch {
|
|
271
|
+
return {}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Persist the shown marker; best-effort, the hint is cosmetic and never a gate. */
|
|
276
|
+
export async function markEditorKeysHintShown(path: string): Promise<void> {
|
|
277
|
+
await mkdir(dirname(path), { recursive: true })
|
|
278
|
+
await writeFile(path, JSON.stringify({ hintShownAt: new Date().toISOString() }, null, 2) + '\n', 'utf8')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Inputs shared by the apply and startup-hint flows. */
|
|
282
|
+
export interface EditorKeysEnv {
|
|
283
|
+
/** Process environment (TERM_PROGRAM / VSCODE_IPC_HOOK_CLI). */
|
|
284
|
+
env: NodeJS.ProcessEnv
|
|
285
|
+
/** Filesystem anchors for editor config resolution. */
|
|
286
|
+
paths: EditorPathContext
|
|
287
|
+
/** Absolute path of the one-shot hint marker under the DSH home. */
|
|
288
|
+
flagPath: string
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Read one file if it exists; undefined otherwise (ENOENT and unreadable both). */
|
|
292
|
+
async function readIfPresent(path: string): Promise<string | undefined> {
|
|
293
|
+
try {
|
|
294
|
+
return await readFile(path, 'utf8')
|
|
295
|
+
} catch {
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Apply the Ctrl+R passthrough to every local keybindings.json of the hosting
|
|
302
|
+
* editor and mark the startup hint shown. Existing files get a .dsh-bak
|
|
303
|
+
* backup before the first write.
|
|
304
|
+
* @returns a one-line user-facing summary.
|
|
305
|
+
* @throws with an actionable message when the environment cannot be repaired.
|
|
306
|
+
*/
|
|
307
|
+
export async function applyCtrlRPassthrough({ env, paths, flagPath }: EditorKeysEnv): Promise<string> {
|
|
308
|
+
const family = detectEditorTerminalFamily(env)
|
|
309
|
+
if (family === undefined) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
'not a VS Code-family terminal (TERM_PROGRAM=' + (env.TERM_PROGRAM?.trim() || 'unset') + '); add the ctrl+r rule to keybindings.json manually',
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
if (isRemoteTerminalEnv(env)) {
|
|
315
|
+
throw new Error('remote terminal detected; apply the keybindings rule on the local machine instead')
|
|
316
|
+
}
|
|
317
|
+
const candidates = editorKeybindingCandidates(family, paths)
|
|
318
|
+
const targets: string[] = []
|
|
319
|
+
for (const candidate of candidates) {
|
|
320
|
+
if (await readIfPresent(candidate) !== undefined) targets.push(candidate)
|
|
321
|
+
}
|
|
322
|
+
if (targets.length === 0) targets.push(candidates[0]!)
|
|
323
|
+
const updated: string[] = []
|
|
324
|
+
const present: string[] = []
|
|
325
|
+
for (const target of targets) {
|
|
326
|
+
const raw = await readIfPresent(target)
|
|
327
|
+
const merge = mergeCtrlRPassthrough(raw)
|
|
328
|
+
if (merge.status === 'present') {
|
|
329
|
+
present.push(target)
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
await mkdir(dirname(target), { recursive: true })
|
|
333
|
+
if (raw !== undefined) await writeFile(target + '.dsh-bak', raw, 'utf8')
|
|
334
|
+
await writeFile(target, merge.text, 'utf8')
|
|
335
|
+
updated.push(target)
|
|
336
|
+
}
|
|
337
|
+
await markEditorKeysHintShown(flagPath).catch(() => {})
|
|
338
|
+
const label = (target: string): string => basename(dirname(dirname(target)))
|
|
339
|
+
if (updated.length === 0) {
|
|
340
|
+
return 'ctrl+r passthrough already configured in ' + present.map(label).join(', ')
|
|
341
|
+
}
|
|
342
|
+
return 'ctrl+r passthrough written to ' + updated.map(label).join(', ') + ' — effective immediately'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the one-shot startup hint for VS Code-family terminals. Fires at
|
|
347
|
+
* most once per install (flag file), never when the passthrough rule is
|
|
348
|
+
* already present, and never in remote ptys where the repair cannot run.
|
|
349
|
+
* @returns the hint line, or undefined to stay silent.
|
|
350
|
+
*/
|
|
351
|
+
export async function resolveEditorKeysStartupHint({ env, paths, flagPath }: EditorKeysEnv): Promise<string | undefined> {
|
|
352
|
+
const flag = parseEditorKeysFlag(await readIfPresent(flagPath))
|
|
353
|
+
if (flag.hintShownAt !== undefined) return undefined
|
|
354
|
+
const family = detectEditorTerminalFamily(env)
|
|
355
|
+
if (family === undefined || isRemoteTerminalEnv(env)) return undefined
|
|
356
|
+
for (const candidate of editorKeybindingCandidates(family, paths)) {
|
|
357
|
+
const raw = await readIfPresent(candidate)
|
|
358
|
+
if (raw === undefined) continue
|
|
359
|
+
try {
|
|
360
|
+
const parsed: unknown = parseJsonc(raw)
|
|
361
|
+
if (Array.isArray(parsed) && parsed.some(isCtrlRPassthroughEntry)) {
|
|
362
|
+
await markEditorKeysHintShown(flagPath).catch(() => {})
|
|
363
|
+
return undefined
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
// An unparseable config still deserves the hint; apply reports the error.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
await markEditorKeysHintShown(flagPath).catch(() => {})
|
|
370
|
+
return 'run /vscode-keys to pass ctrl+r through this editor (alt+r works meanwhile)'
|
|
371
|
+
}
|
package/src/git-workflow.ts
CHANGED
|
@@ -44,9 +44,9 @@ export function parseGitDiffSpec(argument: string): GitDiffSpec {
|
|
|
44
44
|
return { label: `changes since ${value}`, args: ['diff', '--no-ext-diff', '--unified=3', value, '--'] }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function executeGit(cwd: string, args: readonly string[]): Promise<string> {
|
|
47
|
+
function executeGit(cwd: string, args: readonly string[], signal?: AbortSignal): Promise<string> {
|
|
48
48
|
return new Promise((resolve, reject) => {
|
|
49
|
-
execFile('git', [...args], { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
49
|
+
execFile('git', [...args], { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true, signal }, (error, stdout, stderr) => {
|
|
50
50
|
if (error !== null) {
|
|
51
51
|
reject(new Error(stderr.trim() || error.message))
|
|
52
52
|
return
|
|
@@ -56,17 +56,21 @@ function executeGit(cwd: string, args: readonly string[]): Promise<string> {
|
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Load one complete textual diff without invoking external diff drivers.
|
|
61
|
+
* @param signal - aborted by the caller on session switches/quit, killing the
|
|
62
|
+
* git subprocess instead of letting a stale repository's diff land later.
|
|
63
|
+
*/
|
|
64
|
+
export async function loadGitDiff(cwd: string, argument: string, signal?: AbortSignal): Promise<GitDiffView> {
|
|
61
65
|
const spec = parseGitDiffSpec(argument)
|
|
62
66
|
try {
|
|
63
|
-
const text = await executeGit(cwd, spec.args)
|
|
67
|
+
const text = await executeGit(cwd, spec.args, signal)
|
|
64
68
|
return { title: `git diff - ${spec.label}`, files: parseGitDiffFiles(text) }
|
|
65
69
|
} catch (error: unknown) {
|
|
66
70
|
// An unborn repository has no HEAD. Preserve useful unstaged output for
|
|
67
71
|
// the default form while still surfacing all other Git failures.
|
|
68
72
|
if (argument.trim() !== '') throw error
|
|
69
|
-
const text = await executeGit(cwd, ['diff', '--no-ext-diff', '--unified=3', '--'])
|
|
73
|
+
const text = await executeGit(cwd, ['diff', '--no-ext-diff', '--unified=3', '--'], signal)
|
|
70
74
|
return { title: 'git diff - working tree', files: parseGitDiffFiles(text) }
|
|
71
75
|
}
|
|
72
76
|
}
|