@toddzheng024/dscode-bundle 0.7.16 → 0.7.18
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 +0 -3
- package/package.json +2 -2
- package/plugins/compaction/engine.mjs +304 -0
- package/presets/dscode/agent.cordis.yml +1 -1
- package/vendor/tui/lib/app.mjs +5 -4
- package/vendor/tui/lib/i18n.mjs +20 -14
- package/vendor/compaction-basic/LICENSE +0 -21
- package/vendor/compaction-basic/index.js +0 -1094
- package/vendor/compaction-basic/types/config.d.ts +0 -37
- package/vendor/compaction-basic/types/index.d.ts +0 -84
- package/vendor/compaction-basic/types/region.d.ts +0 -65
- package/vendor/compaction-basic/types/summarizer.d.ts +0 -64
- package/vendor/compaction-basic/types/types.d.ts +0 -73
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -25,8 +25,5 @@ Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
|
25
25
|
@deepseek-ai/dsh-terminal-bash@0.1.5-rc.2: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/terminal/terminal-bash"}
|
|
26
26
|
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
27
27
|
|
|
28
|
-
@deepseek-ai/dsh-compaction-basic@0.1.5-rc.2: MIT; {"type":"git","url":"git+https://github.com/deepseek-ai/deepseek-harness.git","directory":"packages/compaction/compaction-basic"}
|
|
29
|
-
Local changes: DSCODE effort, shell control, TUI commands and footer.
|
|
30
|
-
|
|
31
28
|
dsh-code (DSCODE vendored terminal, forked from dsh-code@1.2.0): MIT; https://github.com/unlinearity/dsh-code
|
|
32
29
|
Local changes: DSCODE UI (welcome header, activity line, footer telemetry, effort bar), commands, panels and paste handling.
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.18",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Todd Zheng",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"./bash": "./vendor/bash/index.js",
|
|
50
50
|
"./persistent": "./vendor/persistent/index.js",
|
|
51
51
|
"./terminal": "./vendor/terminal/index.js",
|
|
52
|
-
"./compaction
|
|
52
|
+
"./compaction": "./plugins/compaction/engine.mjs",
|
|
53
53
|
"./policy": "./plugins/dscode/index.mjs",
|
|
54
54
|
"./code-review": "./plugins/code-review/index.mjs",
|
|
55
55
|
"./auto-review": "./plugins/auto-review/index.mjs",
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
|
|
2
|
+
import { ManualCompactionError, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction';
|
|
3
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
4
|
+
import { effectiveContextWindow, fitsInWindow, prefetchThresholdTokens, pricedCompactionPolicy } from './threshold.mjs';
|
|
5
|
+
|
|
6
|
+
// DSCODE's compaction policy as a subclass of the upstream engine instead of a
|
|
7
|
+
// build-time patch of its source. It owns exactly the decisions this deployment
|
|
8
|
+
// adds - a threshold priced from the route's cache discount, the adapter's
|
|
9
|
+
// completion reserve, background prefetch, and overflow recovery that lets
|
|
10
|
+
// pruning decide first - and leaves the durable surface transaction to the
|
|
11
|
+
// upstream `compactRegion`, which stays the only writer.
|
|
12
|
+
//
|
|
13
|
+
// `BasicCompactionEngine` registers the automatic hooks itself and dispatches
|
|
14
|
+
// them through `this.compactIfNeeded`, so overriding it here replaces the policy
|
|
15
|
+
// without touching the installed package.
|
|
16
|
+
export class DscodeCompactionEngine extends BasicCompactionEngine {
|
|
17
|
+
constructor(ctx, config) {
|
|
18
|
+
super(ctx, config);
|
|
19
|
+
// The upstream default hides whether the deployment configured a threshold,
|
|
20
|
+
// so the priced policy is enabled only when the raw config left it unset.
|
|
21
|
+
this.dscodePricedThreshold = config?.thresholdRatio === undefined;
|
|
22
|
+
this.dscodePrefetch = new WeakMap();
|
|
23
|
+
this.dscodePendingPrefetch = new WeakMap();
|
|
24
|
+
this.dscodeWarnedTargets = new Set();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compact for step-boundary pressure or one provider-confirmed overflow, with
|
|
29
|
+
* the threshold priced from the route's cache discount and the completion
|
|
30
|
+
* budget the adapter reserves inside the window.
|
|
31
|
+
* @param agent - agent whose latest durable routed request is measured.
|
|
32
|
+
* @param trigger - normal step-boundary pressure or context-overflow recovery.
|
|
33
|
+
* @param signal - live turn cancellation signal forwarded to summarization.
|
|
34
|
+
* @returns the latest summary compaction result, or `null` when no summary ran.
|
|
35
|
+
*/
|
|
36
|
+
async compactIfNeeded(agent, trigger, signal) {
|
|
37
|
+
const target = this.dscodeRoutedTarget(agent.session);
|
|
38
|
+
if (target === undefined) return null;
|
|
39
|
+
const policy = this.dscodeTargetPolicy(target);
|
|
40
|
+
const meter = this.ctx.tokenMeter;
|
|
41
|
+
const prune = this.ctx.get('toolResultPruner');
|
|
42
|
+
let measurement = meter.measure(agent.session);
|
|
43
|
+
|
|
44
|
+
if (trigger === 'context-overflow') {
|
|
45
|
+
// The pruner runs first and may already return the failed request under the
|
|
46
|
+
// window. The caller retries whenever that prune replaced the surface, so a
|
|
47
|
+
// summary here would only add a model call and its stall to a request that
|
|
48
|
+
// no longer needs one.
|
|
49
|
+
const generation = agent.session.surface.replaceGeneration;
|
|
50
|
+
if (prune !== undefined) {
|
|
51
|
+
prune.pruneSession(agent.session);
|
|
52
|
+
measurement = meter.measure(agent.session);
|
|
53
|
+
}
|
|
54
|
+
if (agent.session.surface.replaceGeneration > generation) {
|
|
55
|
+
const info = await this.ctx.llm.resolveModelInfo(target.provider, target.model, signal);
|
|
56
|
+
if (fitsInWindow(measurement.totalTokens, info)) return null;
|
|
57
|
+
}
|
|
58
|
+
return this.dscodeCompactOldest(agent, measurement, 0, signal);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const modelInfo = await this.ctx.llm.resolveModelInfo(target.provider, target.model, signal);
|
|
62
|
+
const context = modelInfo.context;
|
|
63
|
+
if (context === undefined) {
|
|
64
|
+
const key = `${target.provider}/${target.model}`;
|
|
65
|
+
if (!this.dscodeWarnedTargets.has(key)) {
|
|
66
|
+
this.dscodeWarnedTargets.add(key);
|
|
67
|
+
this.ctx.logger.warn(`compaction: no context capacity for ${key}; configure contextWindow on that adapter model`);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
this.dscodeAssertInactive(agent.session, 'automatic pressure compaction');
|
|
72
|
+
const spec = this.dscodeCompactSpec(await pricedCompactionPolicy({ ...this.config, dscodePricedThreshold: this.dscodePricedThreshold }, policy), effectiveContextWindow(context, modelInfo));
|
|
73
|
+
|
|
74
|
+
this.dscodePlanPrefetch(agent, measurement, spec, signal);
|
|
75
|
+
if (measurement.totalTokens < spec.thresholdTokens) return null;
|
|
76
|
+
if (prune !== undefined) {
|
|
77
|
+
prune.pruneSession(agent.session);
|
|
78
|
+
measurement = meter.measure(agent.session);
|
|
79
|
+
}
|
|
80
|
+
if (measurement.totalTokens < spec.thresholdTokens) return null;
|
|
81
|
+
|
|
82
|
+
let result = null;
|
|
83
|
+
const prefetched = await this.dscodeCommitPrefetch(agent, signal);
|
|
84
|
+
if (prefetched !== null) {
|
|
85
|
+
result = prefetched;
|
|
86
|
+
measurement = meter.measure(agent.session);
|
|
87
|
+
if (measurement.totalTokens < spec.thresholdTokens) return result;
|
|
88
|
+
}
|
|
89
|
+
for (let attempt = 0; attempt <= spec.compactionRetries; attempt += 1) {
|
|
90
|
+
const range = this.dscodeSelectRange(agent.session, measurement, spec.retainTokens);
|
|
91
|
+
if (range === null) {
|
|
92
|
+
if (result === null) return null;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
result = await this.compactRegion(range.start, range.end, agent, signal);
|
|
96
|
+
measurement = meter.measure(agent.session);
|
|
97
|
+
if (measurement.totalTokens < spec.thresholdTokens) return result;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`compaction still above threshold after ${spec.compactionRetries + 1} compaction attempts (${measurement.totalTokens} estimated tokens >= threshold ${spec.thresholdTokens})`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Summarize the replayed region through the upstream cache-reusing call, or
|
|
104
|
+
* return the summary a prefetch already produced for this exact commit.
|
|
105
|
+
* @param input - replayed conversation prefix to condense.
|
|
106
|
+
* @param agent - supplies routed-model history, fallback model, and session id.
|
|
107
|
+
* @param signal - optional cancellation forwarded to the adapter.
|
|
108
|
+
* @returns the summary blocks and the call envelope behind them.
|
|
109
|
+
*/
|
|
110
|
+
async summarize(input, agent, signal) {
|
|
111
|
+
const pending = this.dscodePendingPrefetch.get(agent.session);
|
|
112
|
+
if (pending === undefined) return super.summarize(input, agent, signal);
|
|
113
|
+
this.dscodePendingPrefetch.delete(agent.session);
|
|
114
|
+
// The upstream transaction already opened its marker, so this wait is the visible
|
|
115
|
+
// compaction. A failed or invalidated prefetch falls back to a fresh call instead
|
|
116
|
+
// of failing a transaction that has already begun.
|
|
117
|
+
await pending.wait;
|
|
118
|
+
if (pending.failure !== null || agent.session.surface.replaceGeneration !== pending.generation) return super.summarize(input, agent, signal);
|
|
119
|
+
return pending.summarized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Compact the oldest compactable span for a retention budget, or return null when none is safe. */
|
|
123
|
+
async dscodeCompactOldest(agent, measurement, retainTokens, signal) {
|
|
124
|
+
const range = this.dscodeSelectRange(agent.session, measurement, retainTokens);
|
|
125
|
+
if (range === null) return null;
|
|
126
|
+
return this.compactRegion(range.start, range.end, agent, signal);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The routed provider/model of the session's latest durable request. */
|
|
130
|
+
dscodeRoutedTarget(session) {
|
|
131
|
+
const config = session.requestHeader()?.config;
|
|
132
|
+
if (config === undefined || config.provider.length === 0 || config.model.length === 0) return undefined;
|
|
133
|
+
return { provider: config.provider, model: config.model };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Merge the exact-target override over the validated defaults for one routed target. */
|
|
137
|
+
dscodeTargetPolicy(target) {
|
|
138
|
+
const config = this.config;
|
|
139
|
+
const override = config.modelPolicies.find(entry => entry.provider === target.provider && entry.model === target.model);
|
|
140
|
+
const inherited = config.retainTokens === undefined ? { retainRatio: config.retainRatio } : { retainTokens: config.retainTokens };
|
|
141
|
+
const retention = override?.retainTokens !== undefined ? { retainTokens: override.retainTokens }
|
|
142
|
+
: override?.retainRatio !== undefined ? { retainRatio: override.retainRatio }
|
|
143
|
+
: inherited;
|
|
144
|
+
return {
|
|
145
|
+
target: { ...target },
|
|
146
|
+
thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,
|
|
147
|
+
...retention,
|
|
148
|
+
summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,
|
|
149
|
+
summarizationModel: override?.summarizationModel ?? config.summarizationModel,
|
|
150
|
+
maxTokens: override?.maxTokens ?? config.maxTokens,
|
|
151
|
+
compactionRetries: override?.compactionRetries ?? config.compactionRetries,
|
|
152
|
+
maxOverflowRetries: override?.maxOverflowRetries ?? config.maxOverflowRetries,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Scale one routed policy into concrete token budgets for its effective window. */
|
|
157
|
+
dscodeCompactSpec(policy, contextWindow) {
|
|
158
|
+
const key = `${policy.target.provider}/${policy.target.model}`;
|
|
159
|
+
if (!Number.isInteger(contextWindow) || contextWindow <= 0) throw new Error(`compaction: contextWindow (${contextWindow}) must be a positive integer for ${key}`);
|
|
160
|
+
const thresholdTokens = Math.floor(contextWindow * policy.thresholdRatio);
|
|
161
|
+
const retainTokens = policy.retainTokens === undefined ? Math.floor(contextWindow * policy.retainRatio) : policy.retainTokens;
|
|
162
|
+
if (retainTokens >= thresholdTokens) throw new Error(`compaction: retainTokens (${retainTokens}) must be less than threshold tokens ${thresholdTokens} for ${key}`);
|
|
163
|
+
return { ...policy, contextWindow, thresholdTokens, retainTokens };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve the next range starting at the first non-system surface node while
|
|
168
|
+
* retaining a priced recent tail and never splitting a tool-call/result pair.
|
|
169
|
+
* @param session - session supplying authoritative current surface positions.
|
|
170
|
+
* @param measurement - unified pressure and surface measurement from the conversation meter.
|
|
171
|
+
* @param retainTokens - minimum recent tail budget retained verbatim.
|
|
172
|
+
* @returns the positional range to compact, or `null`.
|
|
173
|
+
*/
|
|
174
|
+
dscodeSelectRange(session, measurement, retainTokens) {
|
|
175
|
+
const pricedNodes = measurement.nodes;
|
|
176
|
+
if (pricedNodes.length === 0) return null;
|
|
177
|
+
const surfaceNodes = session.surface.nodes;
|
|
178
|
+
if (surfaceNodes.length !== pricedNodes.length || surfaceNodes.some((seq, index) => seq !== pricedNodes[index]?.seq)) throw new Error('compaction: token-meter surface does not match the current session surface');
|
|
179
|
+
const firstIdx = this.dscodeSystemHead(session, surfaceNodes[0]) === undefined ? 0 : 1;
|
|
180
|
+
let accumulated = 0;
|
|
181
|
+
let keepFromIdx = pricedNodes.length;
|
|
182
|
+
for (let index = pricedNodes.length - 1; index >= 0; index -= 1) {
|
|
183
|
+
accumulated += pricedNodes[index].tokens;
|
|
184
|
+
keepFromIdx = index;
|
|
185
|
+
if (accumulated >= retainTokens) break;
|
|
186
|
+
}
|
|
187
|
+
if (keepFromIdx <= firstIdx) return null;
|
|
188
|
+
while (keepFromIdx > firstIdx) {
|
|
189
|
+
if (toolPairingBalancedBefore(session, surfaceNodes[keepFromIdx])) break;
|
|
190
|
+
keepFromIdx -= 1;
|
|
191
|
+
}
|
|
192
|
+
if (keepFromIdx <= firstIdx) return null;
|
|
193
|
+
return { start: surfaceNodes[firstIdx], end: surfaceNodes[keepFromIdx - 1], startIdx: firstIdx, endIdx: keepFromIdx - 1 };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The `system/message` at a surface head, or undefined when that node is something else. */
|
|
197
|
+
dscodeSystemHead(session, headSeq) {
|
|
198
|
+
if (headSeq === undefined) return undefined;
|
|
199
|
+
const head = session.eventAt(headSeq);
|
|
200
|
+
return head.type === 'system/message' ? head : undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The replayed conversation prefix a summary call condenses: system head, tools, then the shadowed messages. */
|
|
204
|
+
dscodeSummarizationInput(session, shadowedSeqs) {
|
|
205
|
+
const header = session.requestHeader();
|
|
206
|
+
const nodes = session.surface.nodes;
|
|
207
|
+
const system = this.dscodeSystemHead(session, nodes[0]);
|
|
208
|
+
const head = system === undefined ? null : session.deriveEventMessage(system);
|
|
209
|
+
const region = shadowedSeqs.map(seq => session.deriveEventMessage(session.eventAt(seq))).filter(message => message !== null);
|
|
210
|
+
return { ...header?.tools === undefined ? {} : { tools: header.tools }, messages: head === null ? region : [head, ...region] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The newest unmatched compaction start and end-seed boundary of one session. */
|
|
214
|
+
dscodeEntryState(session) {
|
|
215
|
+
let unmatchedCompactionStart;
|
|
216
|
+
let entryStateKnown = false;
|
|
217
|
+
let latestEndSeedSeq;
|
|
218
|
+
for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
|
|
219
|
+
const event = session.eventAt(SessionSeq(seq));
|
|
220
|
+
if (latestEndSeedSeq === undefined && event.type === 'session/end-seed') latestEndSeedSeq = event.seq;
|
|
221
|
+
if (!entryStateKnown) {
|
|
222
|
+
if (event.type === 'compaction/start') {
|
|
223
|
+
unmatchedCompactionStart = event;
|
|
224
|
+
entryStateKnown = true;
|
|
225
|
+
} else if (event.type === 'compaction/end') entryStateKnown = true;
|
|
226
|
+
}
|
|
227
|
+
if (entryStateKnown && latestEndSeedSeq !== undefined) break;
|
|
228
|
+
}
|
|
229
|
+
return { unmatchedCompactionStart, latestEndSeedSeq };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Reject a second compaction while the durable compaction lock is active. */
|
|
233
|
+
dscodeAssertInactive(session, stage) {
|
|
234
|
+
const { unmatchedCompactionStart, latestEndSeedSeq } = this.dscodeEntryState(session);
|
|
235
|
+
if (unmatchedCompactionStart === undefined || (latestEndSeedSeq !== undefined && latestEndSeedSeq > unmatchedCompactionStart.seq)) return;
|
|
236
|
+
throw new ManualCompactionError('busy', `${stage}: compaction already in progress; the session compaction lock is already active`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Start one background summarization of the oldest compactable span once the
|
|
241
|
+
* measured pressure crosses the mark one lead below the priced threshold.
|
|
242
|
+
* Nothing is appended and no compaction lock is taken, so an invalidated or
|
|
243
|
+
* cancelled prefetch costs only the summarization call.
|
|
244
|
+
* @param agent - agent whose pressure the caller just priced.
|
|
245
|
+
* @param measurement - the measurement the caller priced.
|
|
246
|
+
* @param spec - the caller's resolved spec for this route.
|
|
247
|
+
* @param signal - live turn signal forwarded to the summarizer.
|
|
248
|
+
*/
|
|
249
|
+
dscodePlanPrefetch(agent, measurement, spec, signal) {
|
|
250
|
+
const session = agent.session;
|
|
251
|
+
const existing = this.dscodePrefetch.get(session);
|
|
252
|
+
if (existing !== undefined) {
|
|
253
|
+
if (existing.failure === null) return;
|
|
254
|
+
this.dscodePrefetch.delete(session);
|
|
255
|
+
}
|
|
256
|
+
if (measurement.totalTokens >= spec.thresholdTokens) return;
|
|
257
|
+
if (measurement.totalTokens < prefetchThresholdTokens(spec.thresholdTokens, spec.contextWindow)) return;
|
|
258
|
+
if (this.dscodeEntryState(session).unmatchedCompactionStart !== undefined) return;
|
|
259
|
+
let range;
|
|
260
|
+
try {
|
|
261
|
+
range = this.dscodeSelectRange(session, measurement, spec.retainTokens);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (range === null) return;
|
|
266
|
+
const shadowedSeqs = session.surface.nodes.slice(range.startIdx, range.endIdx + 1);
|
|
267
|
+
const prefetch = { range: { start: range.start, end: range.end }, generation: session.surface.replaceGeneration, summarized: null, failure: null, wait: null };
|
|
268
|
+
this.dscodePrefetch.set(session, prefetch);
|
|
269
|
+
prefetch.wait = (async () => {
|
|
270
|
+
try {
|
|
271
|
+
const input = this.dscodeSummarizationInput(session, shadowedSeqs);
|
|
272
|
+
prefetch.summarized = await this.summarize(input, agent, signal);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
prefetch.failure = error;
|
|
275
|
+
}
|
|
276
|
+
})();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Commit the pending prefetch when its span is still the same replacement
|
|
281
|
+
* target, or return null so the caller summarizes afresh. The upstream
|
|
282
|
+
* `compactRegion` owns the marker pair, the surface replacement and every
|
|
283
|
+
* stability check; `summarize` then returns the summary already produced.
|
|
284
|
+
* @param agent - agent whose pressure triggered the automatic compaction.
|
|
285
|
+
* @param signal - live turn signal forwarded to the transaction.
|
|
286
|
+
* @returns the committed compaction result, or null when no prefetch was usable.
|
|
287
|
+
*/
|
|
288
|
+
async dscodeCommitPrefetch(agent, signal) {
|
|
289
|
+
const session = agent.session;
|
|
290
|
+
const prefetch = this.dscodePrefetch.get(session);
|
|
291
|
+
if (prefetch === undefined) return null;
|
|
292
|
+
this.dscodePrefetch.delete(session);
|
|
293
|
+
if (prefetch.failure !== null) return null;
|
|
294
|
+
if (session.surface.replaceGeneration !== prefetch.generation) return null;
|
|
295
|
+
this.dscodePendingPrefetch.set(session, prefetch);
|
|
296
|
+
try {
|
|
297
|
+
return await this.compactRegion(prefetch.range.start, prefetch.range.end, agent, signal);
|
|
298
|
+
} finally {
|
|
299
|
+
this.dscodePendingPrefetch.delete(session);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default DscodeCompactionEngine;
|
package/vendor/tui/lib/app.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { dscodeChatLines } from './dscode/chat.mjs';
|
|
|
32
32
|
import { readFileSync } from 'node:fs';
|
|
33
33
|
import { footerFor as dscodeFooterFor } from '../../../plugins/session-metrics/view.mjs';
|
|
34
34
|
import { newerVersion as dscodeNewerVersion } from '../../../plugins/tui-tools/update.mjs';
|
|
35
|
-
import {
|
|
35
|
+
import { languageName as dscodeLanguageName, normalizeLanguage as dscodeNormalizeLanguage, t as dscodeMessage } from '../../../plugins/i18n/messages.mjs';
|
|
36
36
|
import { dscodeTelemetryNodes } from './dscode/telemetry.mjs';
|
|
37
37
|
import { dscodeFooterHeader } from './render/status.mjs';
|
|
38
38
|
import { dscodePadEnd, welcomeArtRows, welcomePath, WELCOME_ART, WELCOME_ART_SMALL } from './dscode/welcome.mjs';
|
|
@@ -4604,9 +4604,10 @@ function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, de
|
|
|
4604
4604
|
const argument = text.slice('/language'.length).trim();
|
|
4605
4605
|
if (argument === '')
|
|
4606
4606
|
openLanguage();
|
|
4607
|
-
else if (argument
|
|
4608
|
-
|
|
4609
|
-
|
|
4607
|
+
else if (dscodeNormalizeLanguage(argument) !== null) {
|
|
4608
|
+
const name = parseLanguageName(argument);
|
|
4609
|
+
saveLanguage(name);
|
|
4610
|
+
notify(t('notice.languageSaved', { name: dscodeLanguageName(name) }));
|
|
4610
4611
|
refresh();
|
|
4611
4612
|
}
|
|
4612
4613
|
else
|
package/vendor/tui/lib/i18n.mjs
CHANGED
|
@@ -6,26 +6,32 @@
|
|
|
6
6
|
* choice lives in language.json next to theme.json (see the runner's
|
|
7
7
|
* persistence block).
|
|
8
8
|
*
|
|
9
|
+
* The selectable list and the alias table come from the DSCODE message
|
|
10
|
+
* tables, so `/language` offers everything those tables translate. The
|
|
11
|
+
* terminal's own catalogues still only cover English and Simplified Chinese:
|
|
12
|
+
* any other choice paints the shell in English and DSCODE's labels in the
|
|
13
|
+
* chosen language, which is exactly what `/language` promises.
|
|
14
|
+
*
|
|
9
15
|
* @module @deepseek-ai/dsh-tui/i18n
|
|
10
16
|
*/
|
|
11
17
|
import { en } from './locales/en.mjs';
|
|
12
18
|
import { zh } from './locales/zh.mjs';
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
import { LANGUAGES as DSCODE_LANGUAGES, normalizeLanguage } from '../../../plugins/i18n/messages.mjs';
|
|
20
|
+
/** Valid language names for argument parsing, in picker order. */
|
|
21
|
+
export const LANGUAGE_NAMES = DSCODE_LANGUAGES.map(language => language.code);
|
|
15
22
|
/** The /language picker rows in canonical order. */
|
|
16
|
-
export const LANGUAGES =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
];
|
|
20
|
-
const CATALOGS = { en, zh };
|
|
23
|
+
export const LANGUAGES = DSCODE_LANGUAGES.map(language => ({ id: language.code, label: language.name }));
|
|
24
|
+
/** Catalogues this module can paint; anything else keeps English. */
|
|
25
|
+
const CATALOGS = { en, 'zh-CN': zh, 'zh-TW': zh };
|
|
21
26
|
/** The language in force. */
|
|
22
27
|
let activeName = 'en';
|
|
23
28
|
/**
|
|
24
|
-
* Parse a persisted or typed language name
|
|
25
|
-
*
|
|
29
|
+
* Parse a persisted or typed language name through the DSCODE alias table, so
|
|
30
|
+
* `zh`, `jp` or `简体中文` all resolve. An unknown value falls back to English.
|
|
26
31
|
*/
|
|
27
32
|
export function parseLanguageName(value) {
|
|
28
|
-
|
|
33
|
+
const code = normalizeLanguage(value);
|
|
34
|
+
return code !== null && LANGUAGE_NAMES.includes(code) ? code : 'en';
|
|
29
35
|
}
|
|
30
36
|
/** The language name in force. */
|
|
31
37
|
export function getLanguage() {
|
|
@@ -37,11 +43,11 @@ export function setLanguage(name) {
|
|
|
37
43
|
}
|
|
38
44
|
/**
|
|
39
45
|
* One message from the active catalog, with `{n}`-style placeholders filled
|
|
40
|
-
* from the params record. Unknown placeholders stay literal; a
|
|
41
|
-
* falls back to the English entry so
|
|
42
|
-
* never crashes.
|
|
46
|
+
* from the params record. Unknown placeholders stay literal; a language the
|
|
47
|
+
* terminal cannot paint, or a missing key, falls back to the English entry so
|
|
48
|
+
* a catalog gap degrades visibly but never crashes.
|
|
43
49
|
*/
|
|
44
50
|
export function t(key, params = {}) {
|
|
45
|
-
const template = CATALOGS[activeName][key] ?? en[key];
|
|
51
|
+
const template = CATALOGS[activeName]?.[key] ?? en[key];
|
|
46
52
|
return template.replace(/\{(\w+)\}/gu, (whole, name) => params[name] === undefined ? whole : String(params[name]));
|
|
47
53
|
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 DeepSeek
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|