dsh-code 1.0.7 → 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.
Files changed (81) hide show
  1. package/README.en.md +70 -24
  2. package/README.md +71 -25
  3. package/bin/deepseek.mjs +202 -39
  4. package/cordis.patch.yml +13 -4
  5. package/lib/index.mjs +4063 -844
  6. package/lib/session-query.mjs +3 -2
  7. package/lib/startup.mjs +4 -4
  8. package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
  9. package/lib/types/app.d.ts +100 -63
  10. package/lib/types/authorization-panel.d.ts +3 -3
  11. package/lib/types/git-workflow.d.ts +91 -2
  12. package/lib/types/i18n.d.ts +39 -0
  13. package/lib/types/index.d.ts +73 -1
  14. package/lib/types/input-split.d.ts +1 -1
  15. package/lib/types/kernel-panels.d.ts +86 -31
  16. package/lib/types/language-panel.d.ts +12 -0
  17. package/lib/types/locales/en.d.ts +450 -0
  18. package/lib/types/locales/zh.d.ts +9 -0
  19. package/lib/types/mentions.d.ts +7 -3
  20. package/lib/types/models.d.ts +14 -0
  21. package/lib/types/panel-accent.d.ts +28 -0
  22. package/lib/types/rainbow.d.ts +69 -0
  23. package/lib/types/render/animations.d.ts +42 -0
  24. package/lib/types/render/inspector.d.ts +26 -0
  25. package/lib/types/render/lines.d.ts +21 -1
  26. package/lib/types/render/markdown.d.ts +1 -1
  27. package/lib/types/render/projection.d.ts +95 -4
  28. package/lib/types/render/status.d.ts +8 -8
  29. package/lib/types/render/text.d.ts +6 -0
  30. package/lib/types/render/usage.d.ts +113 -0
  31. package/lib/types/session-directory.d.ts +17 -0
  32. package/lib/types/startup.d.ts +1 -1
  33. package/lib/types/terminal-title.d.ts +8 -0
  34. package/lib/types/theme-panel.d.ts +2 -2
  35. package/lib/types/theme.d.ts +271 -52
  36. package/lib/types/update-panel.d.ts +5 -5
  37. package/lib/types/update.d.ts +10 -1
  38. package/lib/types/version.d.ts +4 -3
  39. package/package.json +24 -7
  40. package/src/app.ts +1155 -478
  41. package/src/approval.ts +166 -166
  42. package/src/authorization-panel.ts +19 -16
  43. package/src/editor-keys.ts +371 -371
  44. package/src/git-workflow.ts +229 -3
  45. package/src/i18n.ts +68 -0
  46. package/src/index.ts +412 -76
  47. package/src/input-split.ts +3 -3
  48. package/src/kernel-panels.ts +471 -89
  49. package/src/keyboard.ts +5 -4
  50. package/src/language-panel.ts +53 -0
  51. package/src/locales/en.ts +489 -0
  52. package/src/locales/zh.ts +488 -0
  53. package/src/mentions.ts +8 -4
  54. package/src/models.ts +264 -212
  55. package/src/panel-accent.ts +41 -0
  56. package/src/presets.ts +1 -1
  57. package/src/provider-settings.ts +1 -1
  58. package/src/rainbow.ts +208 -0
  59. package/src/render/animations.ts +104 -6
  60. package/src/render/editor.ts +20 -20
  61. package/src/render/export.ts +116 -95
  62. package/src/render/inspector.ts +42 -0
  63. package/src/render/lines.ts +628 -415
  64. package/src/render/markdown.ts +15 -3
  65. package/src/render/projection.ts +429 -19
  66. package/src/render/status.ts +41 -35
  67. package/src/render/text.ts +14 -0
  68. package/src/render/tool-preview.ts +77 -77
  69. package/src/render/usage.ts +430 -0
  70. package/src/render/width.ts +2 -2
  71. package/src/session-directory.ts +8 -6
  72. package/src/session-query.ts +8 -4
  73. package/src/startup.ts +3 -3
  74. package/src/subagents.ts +229 -229
  75. package/src/terminal-title.ts +22 -5
  76. package/src/theme-panel.ts +17 -21
  77. package/src/theme.ts +281 -33
  78. package/src/update-panel.ts +37 -27
  79. package/src/update.ts +19 -3
  80. package/src/version.ts +58 -20
  81. package/src/whale-glyph.ts +23 -23
@@ -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
- /** Review prompt capped before it reaches a provider context window. */
98
- export function buildReviewPrompt(diff: string, label: string, maxChars = 200_000): string {
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
- `Scope: ${label}${truncated ? ' (diff truncated by CLI)' : ''}`,
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
+ }