@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,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
|
|
11
|
+
export const HISTORY_MAX_ENTRIES = 100;
|
|
12
12
|
/** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
|
|
13
|
-
export
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
81
|
-
|
|
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
|
|
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
|
|
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 {
|
|
12
|
-
|
|
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
|
|
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
|
|
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
|
|
27
|
+
export function parseLanguageName(value) {
|
|
28
|
+
return value === 'zh' ? 'zh' : 'en';
|
|
29
|
+
}
|
|
29
30
|
/** The language name in force. */
|
|
30
|
-
export
|
|
31
|
+
export function getLanguage() {
|
|
32
|
+
return activeName;
|
|
33
|
+
}
|
|
31
34
|
/** Switch the active language; the next render paints with the new catalog. */
|
|
32
|
-
export
|
|
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
|
|
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
|
+
}
|