@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.
Files changed (167) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +6 -2
  3. package/package.json +7 -5
  4. package/plugins/code-review/index.mjs +5 -2
  5. package/plugins/compaction/threshold.mjs +12 -0
  6. package/plugins/credentials/index.mjs +13 -0
  7. package/plugins/grok/adapter.mjs +114 -0
  8. package/plugins/grok/auth.mjs +76 -0
  9. package/plugins/grok/billing.mjs +146 -0
  10. package/plugins/grok/index.mjs +118 -0
  11. package/plugins/grok/models.mjs +129 -0
  12. package/plugins/grok/status.mjs +28 -0
  13. package/plugins/grok/wire.mjs +268 -0
  14. package/plugins/i18n/messages.mjs +6 -6
  15. package/plugins/providers/catalog.mjs +2 -0
  16. package/plugins/session-metrics/view.mjs +34 -3
  17. package/vendor/compaction-basic/index.js +91 -1
  18. package/vendor/persistent/index.js +10 -9
  19. package/vendor/terminal/index.js +5 -0
  20. package/vendor/tui/lib/app.mjs +6711 -0
  21. package/vendor/tui/lib/approval.mjs +122 -0
  22. package/vendor/tui/lib/attachments.mjs +218 -0
  23. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  24. package/vendor/tui/lib/authorization.mjs +111 -0
  25. package/vendor/tui/lib/commands.mjs +123 -0
  26. package/vendor/tui/lib/dscode/chat.mjs +84 -0
  27. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  28. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  29. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  30. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  31. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  32. package/vendor/tui/lib/editor-keys.mjs +346 -0
  33. package/vendor/tui/lib/editor.mjs +50 -0
  34. package/vendor/tui/lib/fork.mjs +24 -0
  35. package/vendor/tui/lib/git-workflow.mjs +278 -0
  36. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  37. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  38. package/vendor/tui/lib/index.mjs +2200 -0
  39. package/vendor/tui/lib/input-split.mjs +170 -0
  40. package/vendor/tui/lib/internals.mjs +90 -0
  41. package/vendor/tui/lib/invariant.mjs +22 -0
  42. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  43. package/vendor/tui/lib/keyboard.mjs +283 -0
  44. package/vendor/tui/lib/language-panel.mjs +41 -0
  45. package/vendor/tui/lib/locales/en.mjs +495 -0
  46. package/vendor/tui/lib/locales/zh.mjs +495 -0
  47. package/vendor/tui/lib/mentions.mjs +150 -0
  48. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  49. package/vendor/tui/lib/models.mjs +215 -0
  50. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  51. package/vendor/tui/lib/permissions.mjs +45 -0
  52. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  53. package/vendor/tui/lib/presets.mjs +46 -0
  54. package/vendor/tui/lib/provider-settings.mjs +600 -0
  55. package/vendor/tui/lib/questions.mjs +116 -0
  56. package/vendor/tui/lib/rainbow.mjs +181 -0
  57. package/vendor/tui/lib/render/animations.mjs +665 -0
  58. package/vendor/tui/lib/render/editor.mjs +437 -0
  59. package/vendor/tui/lib/render/export.mjs +119 -0
  60. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  61. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  62. package/vendor/tui/lib/render/inspector.mjs +93 -0
  63. package/vendor/tui/lib/render/lines.mjs +538 -0
  64. package/vendor/tui/lib/render/markdown.mjs +592 -0
  65. package/vendor/tui/lib/render/projection.mjs +1864 -0
  66. package/vendor/tui/lib/render/status.mjs +591 -0
  67. package/vendor/tui/lib/render/text.mjs +157 -0
  68. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  69. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  70. package/vendor/tui/lib/render/usage.mjs +336 -0
  71. package/vendor/tui/lib/render/width.mjs +191 -0
  72. package/vendor/tui/lib/session-directory.mjs +329 -0
  73. package/vendor/tui/lib/session-query.mjs +184 -0
  74. package/vendor/tui/lib/session-switch.mjs +51 -0
  75. package/vendor/tui/lib/settings-file.mjs +75 -0
  76. package/vendor/tui/lib/skills.mjs +119 -0
  77. package/vendor/tui/lib/startup.mjs +111 -0
  78. package/vendor/tui/lib/store.mjs +129 -0
  79. package/vendor/tui/lib/subagents.mjs +200 -0
  80. package/vendor/tui/lib/terminal-title.mjs +186 -0
  81. package/vendor/tui/lib/theme-panel.mjs +60 -0
  82. package/vendor/tui/lib/theme.mjs +379 -0
  83. package/vendor/tui/lib/update-panel.mjs +239 -0
  84. package/vendor/tui/lib/update.mjs +101 -0
  85. package/vendor/tui/lib/version.mjs +105 -0
  86. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  87. package/vendor/tui/LICENSE +0 -21
  88. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  89. package/vendor/tui/dscode-email/cli.mjs +0 -32
  90. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  91. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  92. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  93. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  94. package/vendor/tui/dscode-email/imap.mjs +0 -136
  95. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  96. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  97. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  98. package/vendor/tui/dscode-email/store.mjs +0 -32
  99. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  100. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  101. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  102. package/vendor/tui/index.mjs +0 -44441
  103. package/vendor/tui/invariant.mjs +0 -21
  104. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  105. package/vendor/tui/session-query.mjs +0 -150
  106. package/vendor/tui/startup.mjs +0 -109
  107. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  108. package/vendor/tui/types/app.d.ts +0 -348
  109. package/vendor/tui/types/approval.d.ts +0 -59
  110. package/vendor/tui/types/attachments.d.ts +0 -52
  111. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  112. package/vendor/tui/types/authorization.d.ts +0 -36
  113. package/vendor/tui/types/commands.d.ts +0 -52
  114. package/vendor/tui/types/editor-keys.d.ts +0 -105
  115. package/vendor/tui/types/editor.d.ts +0 -6
  116. package/vendor/tui/types/fork.d.ts +0 -8
  117. package/vendor/tui/types/git-workflow.d.ts +0 -121
  118. package/vendor/tui/types/index.d.ts +0 -223
  119. package/vendor/tui/types/input-split.d.ts +0 -54
  120. package/vendor/tui/types/internals.d.ts +0 -26
  121. package/vendor/tui/types/invariant.d.ts +0 -15
  122. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  123. package/vendor/tui/types/keyboard.d.ts +0 -80
  124. package/vendor/tui/types/language-panel.d.ts +0 -12
  125. package/vendor/tui/types/locales/en.d.ts +0 -450
  126. package/vendor/tui/types/locales/zh.d.ts +0 -9
  127. package/vendor/tui/types/mentions.d.ts +0 -85
  128. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  129. package/vendor/tui/types/models.d.ts +0 -133
  130. package/vendor/tui/types/permissions.d.ts +0 -27
  131. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  132. package/vendor/tui/types/presets.d.ts +0 -22
  133. package/vendor/tui/types/provider-settings.d.ts +0 -239
  134. package/vendor/tui/types/questions.d.ts +0 -54
  135. package/vendor/tui/types/rainbow.d.ts +0 -69
  136. package/vendor/tui/types/render/animations.d.ts +0 -307
  137. package/vendor/tui/types/render/editor.d.ts +0 -163
  138. package/vendor/tui/types/render/export.d.ts +0 -9
  139. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  140. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  141. package/vendor/tui/types/render/inspector.d.ts +0 -62
  142. package/vendor/tui/types/render/lines.d.ts +0 -86
  143. package/vendor/tui/types/render/markdown.d.ts +0 -29
  144. package/vendor/tui/types/render/projection.d.ts +0 -587
  145. package/vendor/tui/types/render/status.d.ts +0 -196
  146. package/vendor/tui/types/render/text.d.ts +0 -64
  147. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  148. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  149. package/vendor/tui/types/render/usage.d.ts +0 -113
  150. package/vendor/tui/types/render/width.d.ts +0 -29
  151. package/vendor/tui/types/session-directory.d.ts +0 -190
  152. package/vendor/tui/types/session-query.d.ts +0 -92
  153. package/vendor/tui/types/session-switch.d.ts +0 -17
  154. package/vendor/tui/types/settings-file.d.ts +0 -41
  155. package/vendor/tui/types/skills.d.ts +0 -47
  156. package/vendor/tui/types/startup.d.ts +0 -65
  157. package/vendor/tui/types/store.d.ts +0 -58
  158. package/vendor/tui/types/subagents.d.ts +0 -70
  159. package/vendor/tui/types/terminal-title.d.ts +0 -66
  160. package/vendor/tui/types/theme-panel.d.ts +0 -24
  161. package/vendor/tui/types/theme.d.ts +0 -434
  162. package/vendor/tui/types/update-panel.d.ts +0 -49
  163. package/vendor/tui/types/update.d.ts +0 -75
  164. package/vendor/tui/types/version.d.ts +0 -19
  165. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  166. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  167. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Read-only Git inspection used by /diff and /review. Every diff
3
+ * invocation carries --no-ext-diff and --no-textconv, so configured
4
+ * external diff drivers and text converters can never execute as a
5
+ * side effect of reading a diff.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import { t } from './i18n.mjs';
9
+ /** Split Git's stable `diff --git` framing without interpreting patch content. */
10
+ export function parseGitDiffFiles(text) {
11
+ if (text === '')
12
+ return [];
13
+ const chunks = text.split(/(?=^diff --git )/mu).filter(chunk => chunk !== '');
14
+ return chunks.map((chunk, index) => {
15
+ const lines = chunk.replace(/\n$/u, '').split('\n');
16
+ const plus = lines.find(line => line.startsWith('+++ b/'));
17
+ const minus = lines.find(line => line.startsWith('--- a/'));
18
+ const header = /^diff --git a\/(.+) b\/(.+)$/u.exec(lines[0] ?? '');
19
+ const path = plus?.slice(6) || minus?.slice(6) || header?.[2] || header?.[1] || `file ${index + 1}`;
20
+ return { path, lines };
21
+ });
22
+ }
23
+ /** Parse the intentionally small, option-safe /diff argument vocabulary. */
24
+ export function parseGitDiffSpec(argument) {
25
+ const value = argument.trim();
26
+ if (value === '')
27
+ return { label: 'working tree vs HEAD', args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', 'HEAD', '--'] };
28
+ if (value === '--staged' || value === '--cached') {
29
+ return { label: 'staged changes', args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', '--cached', '--'] };
30
+ }
31
+ if (value.startsWith('-') || /\s/u.test(value))
32
+ throw new Error('usage: /diff [--staged|git-ref]');
33
+ return { label: `changes since ${value}`, args: ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', value, '--'] };
34
+ }
35
+ function executeGit(cwd, args, signal) {
36
+ return new Promise((resolve, reject) => {
37
+ execFile('git', [...args], { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, windowsHide: true, signal }, (error, stdout, stderr) => {
38
+ if (error !== null) {
39
+ reject(new Error(stderr.trim() || error.message));
40
+ return;
41
+ }
42
+ resolve(stdout.replace(/\r\n/gu, '\n'));
43
+ });
44
+ });
45
+ }
46
+ /** Arguments for the unstaged-only fallback below. */
47
+ const UNSTAGED_DIFF_ARGS = ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', '--'];
48
+ /** Whether the repository has at least one commit (a HEAD revision). */
49
+ function hasHeadRevision(cwd, signal) {
50
+ return executeGit(cwd, ['rev-parse', '--verify', '--quiet', 'HEAD'], signal)
51
+ .then(() => true)
52
+ .catch(() => false);
53
+ }
54
+ /**
55
+ * Load one complete textual diff without invoking external programs.
56
+ * @param signal - aborted by the caller on session switches/quit, killing the
57
+ * git subprocess instead of letting a stale repository's diff land later.
58
+ */
59
+ export async function loadGitDiff(cwd, argument, signal) {
60
+ const spec = parseGitDiffSpec(argument);
61
+ try {
62
+ const text = await executeGit(cwd, spec.args, signal);
63
+ return { title: `git diff - ${spec.label}`, files: parseGitDiffFiles(text) };
64
+ }
65
+ catch (error) {
66
+ // Only a repository without commits (no HEAD to diff against) may
67
+ // narrow the default form to the unstaged fallback. Every other
68
+ // failure — output past the buffer limit, a corrupt index, a missing
69
+ // repository, or the caller aborting between the two calls — must
70
+ // surface, not silently shrink what /diff and /review end up seeing
71
+ // (an aborted probe would otherwise masquerade as an unborn repo).
72
+ if (argument.trim() !== '' || signal?.aborted === true || (await hasHeadRevision(cwd, signal)))
73
+ throw error;
74
+ const text = await executeGit(cwd, UNSTAGED_DIFF_ARGS, signal);
75
+ return { title: 'git diff - working tree (no commits yet)', files: parseGitDiffFiles(text) };
76
+ }
77
+ }
78
+ /**
79
+ * The /review argument is always a free-form note applied to the uncommitted
80
+ * working tree (`/review 使用中文` reviews the uncommitted diff in Chinese);
81
+ * branch and commit targets come from the candidate picker, never from
82
+ * argument guessing.
83
+ */
84
+ export function parseReviewArgument(argument) {
85
+ const value = argument.trim();
86
+ if (value === '')
87
+ return { kind: 'uncommitted' };
88
+ if (value.startsWith('-'))
89
+ throw new Error('usage: /review [note]');
90
+ if (value.length > 4000)
91
+ throw new Error('review note is too long');
92
+ return { kind: 'custom', instructions: value };
93
+ }
94
+ /** The merge base of HEAD and one branch, or undefined when git cannot compute one. */
95
+ export function mergeBaseWith(cwd, branch, signal) {
96
+ return executeGit(cwd, ['merge-base', 'HEAD', branch, '--'], signal)
97
+ .then(output => output.trim() === '' ? undefined : output.trim())
98
+ .catch(() => undefined);
99
+ }
100
+ /** One commit's own patch (parent..commit), falling back to `git show` for root commits. */
101
+ export async function loadCommitDiff(cwd, sha, signal) {
102
+ const args = ['diff', '--no-ext-diff', '--no-textconv', '--unified=3', `${sha}~1`, sha, '--'];
103
+ try {
104
+ const text = await executeGit(cwd, args, signal);
105
+ return { title: `git diff - commit ${sha.slice(0, 7)}`, files: parseGitDiffFiles(text) };
106
+ }
107
+ catch (error) {
108
+ // A root commit has no parent to diff against; show its full patch.
109
+ if (signal?.aborted === true)
110
+ throw error;
111
+ const text = await executeGit(cwd, ['show', '--format=', '--no-ext-diff', '--no-textconv', '--unified=3', sha, '--'], signal);
112
+ return { title: `git diff - commit ${sha.slice(0, 7)}`, files: parseGitDiffFiles(text) };
113
+ }
114
+ }
115
+ /**
116
+ * Build the in-session review prompt: the diff is pasted whole (truncated at
117
+ * the character cap with an explicit marker the model can see), the rubric's
118
+ * essentials ride along — P0-P3 priorities, file/line anchors, only defects
119
+ * this change introduced — and an optional user note (language, focus)
120
+ * prefixes everything as the user's explicit instruction.
121
+ */
122
+ export function buildReviewPrompt(diff, label, note, maxChars = 200_000) {
123
+ const truncated = diff.length > maxChars;
124
+ const body = truncated ? diff.slice(0, maxChars) : diff;
125
+ return [
126
+ 'Review the following Git changes. Do not modify files or run write operations.',
127
+ 'Lead with concrete bugs, regressions, security risks, and missing tests, ordered by severity.',
128
+ '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.',
129
+ `Scope: ${label}${truncated ? ' (diff truncated by CLI — later files are not visible)' : ''}`,
130
+ ...(note === undefined || note.trim() === '' ? [] : [`User note: ${note.trim()}`]),
131
+ '',
132
+ '```diff',
133
+ body,
134
+ '```',
135
+ ].join('\n');
136
+ }
137
+ /**
138
+ * Local branch names for the review picker, newest activity first and with
139
+ * the current branch excluded (reviewing against it is always empty).
140
+ */
141
+ export async function listReviewBranches(cwd, signal) {
142
+ const text = await executeGit(cwd, ['branch', '--sort=-committerdate', '--format=%(HEAD)%(refname:short)'], signal);
143
+ return text.split('\n')
144
+ .map(line => line.trim())
145
+ .filter(line => line !== '' && !line.startsWith('*'))
146
+ .map(name => ({ name }));
147
+ }
148
+ /** Recent commits on the current branch for the review picker. */
149
+ export async function listReviewCommits(cwd, signal, limit = 30) {
150
+ const text = await executeGit(cwd, ['log', `-n${limit}`, '--format=%H%x09%s%x09%ct'], signal);
151
+ const commits = [];
152
+ for (const line of text.split('\n')) {
153
+ if (line === '')
154
+ continue;
155
+ const [sha, title, seconds] = line.split('\t');
156
+ if (sha === undefined || title === undefined)
157
+ continue;
158
+ commits.push({ sha, title, at: Number(seconds ?? 0) * 1000 });
159
+ }
160
+ return commits;
161
+ }
162
+ /** Extract the first balanced {...} substring (the Codex fallback parse). */
163
+ function firstJsonObject(text) {
164
+ const start = text.indexOf('{');
165
+ if (start < 0)
166
+ return undefined;
167
+ let depth = 0;
168
+ let insideString = false;
169
+ let escaped = false;
170
+ for (let at = start; at < text.length; at += 1) {
171
+ const ch = text[at];
172
+ if (escaped) {
173
+ escaped = false;
174
+ continue;
175
+ }
176
+ if (ch === '\\') {
177
+ if (insideString)
178
+ escaped = true;
179
+ continue;
180
+ }
181
+ if (ch === '"')
182
+ insideString = !insideString;
183
+ if (insideString)
184
+ continue;
185
+ if (ch === '{')
186
+ depth += 1;
187
+ if (ch === '}') {
188
+ depth -= 1;
189
+ if (depth === 0)
190
+ return text.slice(start, at + 1);
191
+ }
192
+ }
193
+ return undefined;
194
+ }
195
+ function coercePriority(value) {
196
+ if (typeof value !== 'number' || !Number.isInteger(value))
197
+ return undefined;
198
+ return value >= 0 && value <= 3 ? value : undefined;
199
+ }
200
+ /**
201
+ * Parse the reviewer's reply into a conclusion: whole-text JSON first, then
202
+ * the first balanced JSON object anywhere in the text, then undefined (the
203
+ * reply renders as plain markdown with no summary line). Malformed fields
204
+ * are dropped, never thrown.
205
+ */
206
+ export function parseReviewConclusion(text) {
207
+ const candidates = [text.trim(), firstJsonObject(text)];
208
+ for (const candidate of candidates) {
209
+ if (candidate === undefined || !candidate.startsWith('{'))
210
+ continue;
211
+ let parsed;
212
+ try {
213
+ parsed = JSON.parse(candidate);
214
+ }
215
+ catch {
216
+ continue;
217
+ }
218
+ if (typeof parsed !== 'object' || parsed === null)
219
+ continue;
220
+ const record = parsed;
221
+ const findings = [];
222
+ if (Array.isArray(record['findings'])) {
223
+ for (const item of record['findings']) {
224
+ if (typeof item !== 'object' || item === null)
225
+ continue;
226
+ const finding = item;
227
+ const title = typeof finding['title'] === 'string' ? finding['title'].replace(/^\s*\[P[0-3]\]\s*/u, '').trim() : '';
228
+ if (title === '')
229
+ continue;
230
+ findings.push({
231
+ priority: coercePriority(finding['priority']),
232
+ title,
233
+ ...(typeof finding['path'] === 'string' && finding['path'] !== '' ? { path: finding['path'] } : {}),
234
+ ...(typeof finding['range'] === 'string' && finding['range'] !== '' ? { range: finding['range'] } : {}),
235
+ });
236
+ }
237
+ }
238
+ const overall = record['overall'] === 'incorrect' ? 'incorrect' : record['overall'] === 'correct' ? 'correct' : undefined;
239
+ return {
240
+ findings,
241
+ ...(overall === undefined ? {} : { overall }),
242
+ ...(typeof record['explanation'] === 'string' && record['explanation'].trim() !== '' ? { explanation: record['explanation'].trim() } : {}),
243
+ };
244
+ }
245
+ return undefined;
246
+ }
247
+ /**
248
+ * One compact summary line for a finished review: finding counts per
249
+ * priority plus the overall verdict, or a no-findings phrasing.
250
+ */
251
+ export function reviewSummaryLine(conclusion) {
252
+ const counts = [0, 0, 0, 0];
253
+ let untagged = 0;
254
+ for (const finding of conclusion.findings) {
255
+ if (finding.priority === undefined)
256
+ untagged += 1;
257
+ else
258
+ counts[finding.priority] += 1;
259
+ }
260
+ const total = conclusion.findings.length;
261
+ const parts = [];
262
+ for (let level = 0; level <= 3; level += 1) {
263
+ if (counts[level] > 0)
264
+ parts.push(`P${level}×${counts[level]}`);
265
+ }
266
+ if (untagged > 0)
267
+ parts.push(t('review.summary.untagged', { n: untagged }));
268
+ const verdict = conclusion.overall === 'incorrect'
269
+ ? t('review.summary.incorrect')
270
+ : conclusion.overall === 'correct'
271
+ ? (total === 0 ? t('review.summary.correctClean') : t('review.summary.correct'))
272
+ : undefined;
273
+ if (total === 0) {
274
+ return verdict === undefined ? t('review.summary.noConclusion') : t('review.summary.zero', { verdict });
275
+ }
276
+ const params = { count: total, parts: parts.join(' ') };
277
+ return verdict === undefined ? t('review.summary.count', params) : t('review.summary.countVerdict', { ...params, verdict });
278
+ }
@@ -8,9 +8,11 @@
8
8
  * @module @deepseek-ai/dsh-tui/history
9
9
  */
10
10
  /** Maximum entries retained in the persistent history file and the local recall pool. */
11
- export declare const HISTORY_MAX_ENTRIES = 100;
11
+ export const HISTORY_MAX_ENTRIES = 100;
12
12
  /** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
13
- export declare function serializeHistoryEntry(text: string): string;
13
+ export function serializeHistoryEntry(text) {
14
+ return JSON.stringify(text);
15
+ }
14
16
  /**
15
17
  * Parse a persisted history file (one JSON entry per line): invalid lines
16
18
  * drop out, empty entries are ignored, adjacent duplicates collapse, and the
@@ -19,7 +21,26 @@ export declare function serializeHistoryEntry(text: string): string;
19
21
  * @param max - entry cap.
20
22
  * @returns persistent entries, oldest first.
21
23
  */
22
- export declare function parseHistoryFile(raw: string, max?: number): readonly string[];
24
+ export function parseHistoryFile(raw, max = HISTORY_MAX_ENTRIES) {
25
+ const kept = [];
26
+ for (const line of raw.split('\n')) {
27
+ if (line === '')
28
+ continue;
29
+ let text;
30
+ try {
31
+ text = JSON.parse(line);
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ if (typeof text !== 'string' || text === '')
37
+ continue;
38
+ if (kept.length > 0 && kept[kept.length - 1] === text)
39
+ continue;
40
+ kept.push(text);
41
+ }
42
+ return kept.slice(-max);
43
+ }
23
44
  /**
24
45
  * The append unit for the persistent file: one JSON line, so a multi-line
25
46
  * draft still occupies exactly one physical line. Each submission appends
@@ -29,13 +50,17 @@ export declare function parseHistoryFile(raw: string, max?: number): readonly st
29
50
  * chunks, and the damaged line then drops out at the next parse —
30
51
  * recall tolerates the loss by design.
31
52
  */
32
- export declare function historyLine(text: string): string;
53
+ export function historyLine(text) {
54
+ return serializeHistoryEntry(text) + '\n';
55
+ }
33
56
  /**
34
57
  * Whether the file on disk differs from its canonical form (deduped and
35
58
  * capped). True means stale lines have accumulated and the next boot
36
59
  * should rewrite it once, atomically.
37
60
  */
38
- export declare function needsCompaction(raw: string, max?: number): boolean;
61
+ export function needsCompaction(raw, max = HISTORY_MAX_ENTRIES) {
62
+ return serializeHistoryList(parseHistoryFile(raw, max)) !== raw;
63
+ }
39
64
  /**
40
65
  * Record one in-session submission: empty text is ignored and an adjacent
41
66
  * duplicate collapses (Codex `record_local_submission` semantics). The local
@@ -45,7 +70,13 @@ export declare function needsCompaction(raw: string, max?: number): boolean;
45
70
  * @param max - the local pool cap.
46
71
  * @returns the updated local list.
47
72
  */
48
- export declare function recordLocalEntry(local: readonly string[], text: string, max?: number): readonly string[];
73
+ export function recordLocalEntry(local, text, max = HISTORY_MAX_ENTRIES) {
74
+ if (text === '')
75
+ return local;
76
+ if (local.length > 0 && local[local.length - 1] === text)
77
+ return local;
78
+ return [...local, text].slice(-max);
79
+ }
49
80
  /**
50
81
  * Serialize a capped entry list to the history file format (one JSON line
51
82
  * per entry, trailing newline). The boot-time compaction writes this
@@ -54,7 +85,11 @@ export declare function recordLocalEntry(local: readonly string[], text: string,
54
85
  * @param entries - the entries to persist, oldest first.
55
86
  * @returns the file content, '' for an empty list.
56
87
  */
57
- export declare function serializeHistoryList(entries: readonly string[]): string;
88
+ export function serializeHistoryList(entries) {
89
+ if (entries.length === 0)
90
+ return '';
91
+ return entries.map(serializeHistoryEntry).join('\n') + '\n';
92
+ }
58
93
  /**
59
94
  * Build the recall space, newest first: local entries, then persistent
60
95
  * entries whose text is not duplicated locally (the local copy wins and the
@@ -64,25 +99,13 @@ export declare function serializeHistoryList(entries: readonly string[]): string
64
99
  * @param local - this process's submissions, oldest first.
65
100
  * @returns recall entries, newest first.
66
101
  */
67
- export declare function recallEntries(persistent: readonly string[], local: readonly string[]): readonly string[];
68
- /** Shell-style recall navigation over a fixed recall space. */
69
- export interface RecallState {
70
- /** Recall entries, newest first (frozen at navigation start). */
71
- entries: readonly string[];
72
- /** Current recall index; null when not browsing. */
73
- index: number | null;
74
- /** Draft saved when browsing started; restored on Down past the newest. */
75
- savedDraft: string;
76
- /** The recalled text currently in the composer (the boundary gate's anchor). */
77
- lastRecalled: string | null;
102
+ export function recallEntries(persistent, local) {
103
+ const localSet = new Set(local);
104
+ return [...persistent.filter(entry => !localSet.has(entry)), ...local].reverse();
78
105
  }
79
106
  /** Fresh navigation state over one recall space. */
80
- export declare function beginRecall(entries: readonly string[], draft: string): RecallState;
81
- /** The outcome of one recall step. */
82
- export interface RecallStep {
83
- state: RecallState;
84
- /** The text to place in the composer; undefined means "no movement". */
85
- entry: string | undefined;
107
+ export function beginRecall(entries, draft) {
108
+ return { entries, index: null, savedDraft: draft, lastRecalled: null };
86
109
  }
87
110
  /**
88
111
  * Move one entry older (Up, toward index +1 in the newest-first space). The
@@ -92,6 +115,26 @@ export interface RecallStep {
92
115
  * @param state - current navigation state.
93
116
  * @param draft - the composer text to preserve when browsing starts.
94
117
  */
95
- export declare function recallOlder(state: RecallState, draft: string): RecallStep;
118
+ export function recallOlder(state, draft) {
119
+ if (state.index === null) {
120
+ const entry = state.entries[0];
121
+ if (entry === undefined)
122
+ return { state, entry: undefined };
123
+ return { state: { ...state, index: 0, savedDraft: draft, lastRecalled: entry }, entry };
124
+ }
125
+ if (state.index >= state.entries.length - 1)
126
+ return { state, entry: undefined };
127
+ const entry = state.entries[state.index + 1];
128
+ return { state: { ...state, index: state.index + 1, lastRecalled: entry }, entry };
129
+ }
96
130
  /** Move one entry newer (Down, toward index 0); past the newest, browsing ends and the saved draft returns. */
97
- export declare function recallNewer(state: RecallState): RecallStep;
131
+ export function recallNewer(state) {
132
+ if (state.index === null)
133
+ return { state, entry: undefined };
134
+ const next = state.index - 1;
135
+ if (next < 0) {
136
+ return { state: { ...state, index: null, lastRecalled: null }, entry: state.savedDraft };
137
+ }
138
+ const entry = state.entries[next];
139
+ return { state: { ...state, index: next, lastRecalled: entry }, entry };
140
+ }
@@ -8,32 +8,40 @@
8
8
  *
9
9
  * @module @deepseek-ai/dsh-tui/i18n
10
10
  */
11
- import { type MessageKey } from './locales/en.ts';
12
- export type { MessageKey } from './locales/en.ts';
13
- /** Selectable interface languages: English (default) and Chinese. */
14
- export type LanguageName = 'en' | 'zh';
11
+ import { en } from './locales/en.mjs';
12
+ import { zh } from './locales/zh.mjs';
15
13
  /** Valid language names for argument parsing. */
16
- export declare const LANGUAGE_NAMES: readonly LanguageName[];
17
- /** One language's picker row. */
18
- export interface LanguageDescriptor {
19
- readonly id: LanguageName;
20
- readonly label: string;
21
- }
14
+ export const LANGUAGE_NAMES = ['en', 'zh'];
22
15
  /** The /language picker rows in canonical order. */
23
- export declare const LANGUAGES: readonly LanguageDescriptor[];
16
+ export const LANGUAGES = [
17
+ { id: 'en', label: 'English' },
18
+ { id: 'zh', label: '中文' },
19
+ ];
20
+ const CATALOGS = { en, zh };
21
+ /** The language in force. */
22
+ let activeName = 'en';
24
23
  /**
25
24
  * Parse a persisted or typed language name: only 'en' and 'zh' survive;
26
25
  * anything else falls back to English.
27
26
  */
28
- export declare function parseLanguageName(value: unknown): LanguageName;
27
+ export function parseLanguageName(value) {
28
+ return value === 'zh' ? 'zh' : 'en';
29
+ }
29
30
  /** The language name in force. */
30
- export declare function getLanguage(): LanguageName;
31
+ export function getLanguage() {
32
+ return activeName;
33
+ }
31
34
  /** Switch the active language; the next render paints with the new catalog. */
32
- export declare function setLanguage(name: LanguageName): void;
35
+ export function setLanguage(name) {
36
+ activeName = name;
37
+ }
33
38
  /**
34
39
  * One message from the active catalog, with `{n}`-style placeholders filled
35
40
  * from the params record. Unknown placeholders stay literal; a missing key
36
41
  * falls back to the English entry so a catalog gap degrades visibly but
37
42
  * never crashes.
38
43
  */
39
- export declare function t(key: MessageKey, params?: Readonly<Record<string, string | number>>): string;
44
+ export function t(key, params = {}) {
45
+ const template = CATALOGS[activeName][key] ?? en[key];
46
+ return template.replace(/\{(\w+)\}/gu, (whole, name) => params[name] === undefined ? whole : String(params[name]));
47
+ }