dsh-code 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +123 -26
- package/README.md +124 -27
- package/bin/deepseek.mjs +283 -35
- package/cordis.patch.yml +97 -0
- package/lib/index.mjs +5008 -881
- package/lib/session-query.mjs +150 -0
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +106 -62
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +100 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +107 -29
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +130 -4
- package/lib/types/render/status.d.ts +9 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +66 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +75 -0
- package/lib/types/version.d.ts +4 -3
- package/package.json +246 -90
- package/src/app.ts +1369 -509
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +534 -80
- package/src/input-split.ts +3 -3
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +554 -86
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +208 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +25 -24
- package/src/render/export.ts +116 -95
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +572 -21
- package/src/render/status.ts +59 -39
- package/src/render/text.ts +14 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +8 -6
- package/src/session-query.ts +239 -0
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +190 -0
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +256 -0
- package/src/update.ts +126 -0
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/git-workflow.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { execFile } from 'node:child_process'
|
|
9
|
+
import { t } from './i18n.ts'
|
|
9
10
|
|
|
10
11
|
export interface GitDiffSpec {
|
|
11
12
|
readonly label: string
|
|
@@ -94,17 +95,242 @@ export async function loadGitDiff(cwd: string, argument: string, signal?: AbortS
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
/**
|
|
98
|
-
export
|
|
98
|
+
/** One /review run's selection: what to review plus an optional user note. */
|
|
99
|
+
export type ReviewSelection =
|
|
100
|
+
| { readonly kind: 'uncommitted' }
|
|
101
|
+
| { readonly kind: 'base-branch'; readonly branch: string; readonly mergeBase?: string }
|
|
102
|
+
| { readonly kind: 'commit'; readonly sha: string }
|
|
103
|
+
| { readonly kind: 'custom'; readonly instructions: string }
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The /review argument is always a free-form note applied to the uncommitted
|
|
107
|
+
* working tree (`/review 使用中文` reviews the uncommitted diff in Chinese);
|
|
108
|
+
* branch and commit targets come from the candidate picker, never from
|
|
109
|
+
* argument guessing.
|
|
110
|
+
*/
|
|
111
|
+
export function parseReviewArgument(argument: string): ReviewSelection {
|
|
112
|
+
const value = argument.trim()
|
|
113
|
+
if (value === '') return { kind: 'uncommitted' }
|
|
114
|
+
if (value.startsWith('-')) throw new Error('usage: /review [note]')
|
|
115
|
+
if (value.length > 4000) throw new Error('review note is too long')
|
|
116
|
+
return { kind: 'custom', instructions: value }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The merge base of HEAD and one branch, or undefined when git cannot compute one. */
|
|
120
|
+
export function mergeBaseWith(cwd: string, branch: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
121
|
+
return executeGit(cwd, ['merge-base', 'HEAD', branch, '--'], signal)
|
|
122
|
+
.then(output => output.trim() === '' ? undefined : output.trim())
|
|
123
|
+
.catch(() => undefined)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One commit's own patch (parent..commit), falling back to `git show` for root commits. */
|
|
127
|
+
export async function loadCommitDiff(cwd: string, sha: string, signal?: AbortSignal): Promise<GitDiffView> {
|
|
128
|
+
const args = ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', `${sha}~1`, sha, '--']
|
|
129
|
+
try {
|
|
130
|
+
const text = await executeGit(cwd, args, signal)
|
|
131
|
+
return { title: `git diff - commit ${sha.slice(0, 7)}`, files: parseGitDiffFiles(text) }
|
|
132
|
+
} catch (error: unknown) {
|
|
133
|
+
// A root commit has no parent to diff against; show its full patch.
|
|
134
|
+
if (signal?.aborted === true) throw error
|
|
135
|
+
const text = await executeGit(cwd, ['show', '--format=', '--no-ext-diff', '--no-textconv', '--unified=3', sha, '--'], signal)
|
|
136
|
+
return { title: `git diff - commit ${sha.slice(0, 7)}`, files: parseGitDiffFiles(text) }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the in-session review prompt: the diff is pasted whole (truncated at
|
|
142
|
+
* the character cap with an explicit marker the model can see), the rubric's
|
|
143
|
+
* essentials ride along — P0-P3 priorities, file/line anchors, only defects
|
|
144
|
+
* this change introduced — and an optional user note (language, focus)
|
|
145
|
+
* prefixes everything as the user's explicit instruction.
|
|
146
|
+
*/
|
|
147
|
+
export function buildReviewPrompt(diff: string, label: string, note?: string, maxChars = 200_000): string {
|
|
99
148
|
const truncated = diff.length > maxChars
|
|
100
149
|
const body = truncated ? diff.slice(0, maxChars) : diff
|
|
101
150
|
return [
|
|
102
151
|
'Review the following Git changes. Do not modify files or run write operations.',
|
|
103
152
|
'Lead with concrete bugs, regressions, security risks, and missing tests, ordered by severity.',
|
|
104
|
-
|
|
153
|
+
'Tag every finding [P0]-[P3] (P0 drop everything · P1 urgent · P2 normal · P3 nice to have) and anchor it to file paths with line ranges from the diff; only report defects this change introduced, and prefer reporting nothing over speculation.',
|
|
154
|
+
`Scope: ${label}${truncated ? ' (diff truncated by CLI — later files are not visible)' : ''}`,
|
|
155
|
+
...(note === undefined || note.trim() === '' ? [] : [`User note: ${note.trim()}`]),
|
|
105
156
|
'',
|
|
106
157
|
'```diff',
|
|
107
158
|
body,
|
|
108
159
|
'```',
|
|
109
160
|
].join('\n')
|
|
110
161
|
}
|
|
162
|
+
|
|
163
|
+
/** One branch candidate for the review picker. */
|
|
164
|
+
export interface ReviewBranch {
|
|
165
|
+
readonly name: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** One commit candidate for the review picker. */
|
|
169
|
+
export interface ReviewCommit {
|
|
170
|
+
readonly sha: string
|
|
171
|
+
readonly title: string
|
|
172
|
+
/** Committer timestamp in Unix epoch milliseconds. */
|
|
173
|
+
readonly at: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Local branch names for the review picker, newest activity first and with
|
|
178
|
+
* the current branch excluded (reviewing against it is always empty).
|
|
179
|
+
*/
|
|
180
|
+
export async function listReviewBranches(cwd: string, signal?: AbortSignal): Promise<readonly ReviewBranch[]> {
|
|
181
|
+
const text = await executeGit(cwd, ['branch', '--sort=-committerdate', '--format=%(HEAD)%(refname:short)'], signal)
|
|
182
|
+
return text.split('\n')
|
|
183
|
+
.map(line => line.trim())
|
|
184
|
+
.filter(line => line !== '' && !line.startsWith('*'))
|
|
185
|
+
.map(name => ({ name }))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Recent commits on the current branch for the review picker. */
|
|
189
|
+
export async function listReviewCommits(cwd: string, signal?: AbortSignal, limit = 30): Promise<readonly ReviewCommit[]> {
|
|
190
|
+
const text = await executeGit(cwd, ['log', `-n${limit}`, '--format=%H%x09%s%x09%ct'], signal)
|
|
191
|
+
const commits: ReviewCommit[] = []
|
|
192
|
+
for (const line of text.split('\n')) {
|
|
193
|
+
if (line === '') continue
|
|
194
|
+
const [sha, title, seconds] = line.split('\t')
|
|
195
|
+
if (sha === undefined || title === undefined) continue
|
|
196
|
+
commits.push({ sha, title, at: Number(seconds ?? 0) * 1000 })
|
|
197
|
+
}
|
|
198
|
+
return commits
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
/** One finished review, ready for the terminal's result panel. */
|
|
203
|
+
export interface ReviewResultView {
|
|
204
|
+
/** Panel header naming what was reviewed. */
|
|
205
|
+
readonly title: string
|
|
206
|
+
/** The reviewer's reply markdown (the findings list). */
|
|
207
|
+
readonly body: string
|
|
208
|
+
/** Compact summary: finding counts per priority plus the verdict. */
|
|
209
|
+
readonly summary: string
|
|
210
|
+
/** The review session id, so follow-ups can @-reference its context. */
|
|
211
|
+
readonly sessionId: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** One parsed review finding from the reviewer's closing JSON block. */
|
|
215
|
+
export interface ReviewFinding {
|
|
216
|
+
/** Priority 0-3 for P0-P3; undefined when the reviewer omitted it. */
|
|
217
|
+
readonly priority?: number
|
|
218
|
+
/** Finding title, already free of the [P#] prefix when present. */
|
|
219
|
+
readonly title: string
|
|
220
|
+
/** File path the finding anchors to, when stated. */
|
|
221
|
+
readonly path?: string
|
|
222
|
+
/** Line range as written, when stated. */
|
|
223
|
+
readonly range?: string
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The parsed conclusion of one review reply. */
|
|
227
|
+
export interface ReviewConclusion {
|
|
228
|
+
readonly findings: readonly ReviewFinding[]
|
|
229
|
+
readonly overall?: 'correct' | 'incorrect'
|
|
230
|
+
readonly explanation?: string
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Extract the first balanced {...} substring (the Codex fallback parse). */
|
|
234
|
+
function firstJsonObject(text: string): string | undefined {
|
|
235
|
+
const start = text.indexOf('{')
|
|
236
|
+
if (start < 0) return undefined
|
|
237
|
+
let depth = 0
|
|
238
|
+
let insideString = false
|
|
239
|
+
let escaped = false
|
|
240
|
+
for (let at = start; at < text.length; at += 1) {
|
|
241
|
+
const ch = text[at]
|
|
242
|
+
if (escaped) {
|
|
243
|
+
escaped = false
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
if (ch === '\\') {
|
|
247
|
+
if (insideString) escaped = true
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
if (ch === '"') insideString = !insideString
|
|
251
|
+
if (insideString) continue
|
|
252
|
+
if (ch === '{') depth += 1
|
|
253
|
+
if (ch === '}') {
|
|
254
|
+
depth -= 1
|
|
255
|
+
if (depth === 0) return text.slice(start, at + 1)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function coercePriority(value: unknown): number | undefined {
|
|
262
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) return undefined
|
|
263
|
+
return value >= 0 && value <= 3 ? value : undefined
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse the reviewer's reply into a conclusion: whole-text JSON first, then
|
|
268
|
+
* the first balanced JSON object anywhere in the text, then undefined (the
|
|
269
|
+
* reply renders as plain markdown with no summary line). Malformed fields
|
|
270
|
+
* are dropped, never thrown.
|
|
271
|
+
*/
|
|
272
|
+
export function parseReviewConclusion(text: string): ReviewConclusion | undefined {
|
|
273
|
+
const candidates = [text.trim(), firstJsonObject(text)]
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
if (candidate === undefined || !candidate.startsWith('{')) continue
|
|
276
|
+
let parsed: unknown
|
|
277
|
+
try {
|
|
278
|
+
parsed = JSON.parse(candidate)
|
|
279
|
+
} catch {
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
if (typeof parsed !== 'object' || parsed === null) continue
|
|
283
|
+
const record = parsed as Record<string, unknown>
|
|
284
|
+
const findings: ReviewFinding[] = []
|
|
285
|
+
if (Array.isArray(record['findings'])) {
|
|
286
|
+
for (const item of record['findings']) {
|
|
287
|
+
if (typeof item !== 'object' || item === null) continue
|
|
288
|
+
const finding = item as Record<string, unknown>
|
|
289
|
+
const title = typeof finding['title'] === 'string' ? finding['title'].replace(/^\s*\[P[0-3]\]\s*/u, '').trim() : ''
|
|
290
|
+
if (title === '') continue
|
|
291
|
+
findings.push({
|
|
292
|
+
priority: coercePriority(finding['priority']),
|
|
293
|
+
title,
|
|
294
|
+
...(typeof finding['path'] === 'string' && finding['path'] !== '' ? { path: finding['path'] } : {}),
|
|
295
|
+
...(typeof finding['range'] === 'string' && finding['range'] !== '' ? { range: finding['range'] } : {}),
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const overall = record['overall'] === 'incorrect' ? 'incorrect' as const : record['overall'] === 'correct' ? 'correct' as const : undefined
|
|
300
|
+
return {
|
|
301
|
+
findings,
|
|
302
|
+
...(overall === undefined ? {} : { overall }),
|
|
303
|
+
...(typeof record['explanation'] === 'string' && record['explanation'].trim() !== '' ? { explanation: record['explanation'].trim() } : {}),
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return undefined
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* One compact summary line for a finished review: finding counts per
|
|
311
|
+
* priority plus the overall verdict, or a no-findings phrasing.
|
|
312
|
+
*/
|
|
313
|
+
export function reviewSummaryLine(conclusion: ReviewConclusion): string {
|
|
314
|
+
const counts = [0, 0, 0, 0]
|
|
315
|
+
let untagged = 0
|
|
316
|
+
for (const finding of conclusion.findings) {
|
|
317
|
+
if (finding.priority === undefined) untagged += 1
|
|
318
|
+
else counts[finding.priority] += 1
|
|
319
|
+
}
|
|
320
|
+
const total = conclusion.findings.length
|
|
321
|
+
const parts: string[] = []
|
|
322
|
+
for (let level = 0; level <= 3; level += 1) {
|
|
323
|
+
if (counts[level] > 0) parts.push(`P${level}×${counts[level]}`)
|
|
324
|
+
}
|
|
325
|
+
if (untagged > 0) parts.push(t('review.summary.untagged', { n: untagged }))
|
|
326
|
+
const verdict = conclusion.overall === 'incorrect'
|
|
327
|
+
? t('review.summary.incorrect')
|
|
328
|
+
: conclusion.overall === 'correct'
|
|
329
|
+
? (total === 0 ? t('review.summary.correctClean') : t('review.summary.correct'))
|
|
330
|
+
: undefined
|
|
331
|
+
if (total === 0) {
|
|
332
|
+
return verdict === undefined ? t('review.summary.noConclusion') : t('review.summary.zero', { verdict })
|
|
333
|
+
}
|
|
334
|
+
const params = { count: total, parts: parts.join(' ') }
|
|
335
|
+
return verdict === undefined ? t('review.summary.count', params) : t('review.summary.countVerdict', { ...params, verdict })
|
|
336
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface language for the TUI: a module-level current language with a
|
|
3
|
+
* message accessor, mirroring the theme module's setTheme/getPalette shape
|
|
4
|
+
* so a language switch re-renders every translated surface on the next
|
|
5
|
+
* render without touching call sites. English is the default; the persisted
|
|
6
|
+
* choice lives in language.json next to theme.json (see the runner's
|
|
7
|
+
* persistence block).
|
|
8
|
+
*
|
|
9
|
+
* @module @deepseek-ai/dsh-tui/i18n
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { en, type MessageCatalog, type MessageKey } from './locales/en.ts'
|
|
13
|
+
|
|
14
|
+
export type { MessageKey } from './locales/en.ts'
|
|
15
|
+
import { zh } from './locales/zh.ts'
|
|
16
|
+
|
|
17
|
+
/** Selectable interface languages: English (default) and Chinese. */
|
|
18
|
+
export type LanguageName = 'en' | 'zh'
|
|
19
|
+
|
|
20
|
+
/** Valid language names for argument parsing. */
|
|
21
|
+
export const LANGUAGE_NAMES: readonly LanguageName[] = ['en', 'zh']
|
|
22
|
+
|
|
23
|
+
/** One language's picker row. */
|
|
24
|
+
export interface LanguageDescriptor {
|
|
25
|
+
readonly id: LanguageName
|
|
26
|
+
readonly label: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The /language picker rows in canonical order. */
|
|
30
|
+
export const LANGUAGES: readonly LanguageDescriptor[] = [
|
|
31
|
+
{ id: 'en', label: 'English' },
|
|
32
|
+
{ id: 'zh', label: '中文' },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const CATALOGS: Record<LanguageName, MessageCatalog> = { en, zh }
|
|
36
|
+
|
|
37
|
+
/** The language in force. */
|
|
38
|
+
let activeName: LanguageName = 'en'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a persisted or typed language name: only 'en' and 'zh' survive;
|
|
42
|
+
* anything else falls back to English.
|
|
43
|
+
*/
|
|
44
|
+
export function parseLanguageName(value: unknown): LanguageName {
|
|
45
|
+
return value === 'zh' ? 'zh' : 'en'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The language name in force. */
|
|
49
|
+
export function getLanguage(): LanguageName {
|
|
50
|
+
return activeName
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Switch the active language; the next render paints with the new catalog. */
|
|
54
|
+
export function setLanguage(name: LanguageName): void {
|
|
55
|
+
activeName = name
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One message from the active catalog, with `{n}`-style placeholders filled
|
|
60
|
+
* from the params record. Unknown placeholders stay literal; a missing key
|
|
61
|
+
* falls back to the English entry so a catalog gap degrades visibly but
|
|
62
|
+
* never crashes.
|
|
63
|
+
*/
|
|
64
|
+
export function t(key: MessageKey, params: Readonly<Record<string, string | number>> = {}): string {
|
|
65
|
+
const template = CATALOGS[activeName][key] ?? en[key]
|
|
66
|
+
return template.replace(/\{(\w+)\}/gu, (whole, name: string) =>
|
|
67
|
+
params[name] === undefined ? whole : String(params[name]))
|
|
68
|
+
}
|