dsh-input-assistant 0.1.3 → 0.1.4

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 (42) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/LICENSE +21 -21
  3. package/README.en.md +100 -0
  4. package/README.md +215 -184
  5. package/cordis.patch.yml +3 -3
  6. package/docs/CHANGELOG-0.1.2.md +102 -102
  7. package/docs/evaluation.md +111 -111
  8. package/docs/images/dark.png +0 -0
  9. package/docs/images/light.png +0 -0
  10. package/docs/images/optimize.png +0 -0
  11. package/docs/references.md +39 -39
  12. package/docs/smoke-comparison.md +37 -37
  13. package/docs/validation.md +180 -100
  14. package/lib/browser/components.d.ts +1 -1
  15. package/lib/browser/controller.d.ts +47 -4
  16. package/lib/browser/dom.d.ts +1 -1
  17. package/lib/browser/index.d.ts +1 -1
  18. package/lib/browser/public.d.ts +2 -2
  19. package/lib/browser/runtime.d.ts +4 -2
  20. package/lib/browser/settings.d.ts +1 -1
  21. package/lib/browser/styles.d.ts +1 -1
  22. package/lib/client.js +1164 -222
  23. package/lib/client.meta.json +40 -40
  24. package/lib/core/config.d.ts +1 -1
  25. package/lib/core/context.d.ts +1 -1
  26. package/lib/core/generation.d.ts +2 -2
  27. package/lib/core/index.d.ts +9 -8
  28. package/lib/core/models.d.ts +15 -0
  29. package/lib/core/protection.d.ts +3 -4
  30. package/lib/core/scheduler.d.ts +1 -1
  31. package/lib/core/snapshot.d.ts +1 -1
  32. package/lib/core/types.d.ts +11 -0
  33. package/lib/core.js +280 -53
  34. package/lib/core.js.map +2 -2
  35. package/lib/host/rpc.d.ts +2 -0
  36. package/lib/host/runtime.d.ts +5 -3
  37. package/lib/host/storage.d.ts +11 -3
  38. package/lib/host/transcript.d.ts +1 -1
  39. package/lib/index.d.ts +5 -3
  40. package/lib/index.js +451 -156
  41. package/lib/index.js.map +4 -4
  42. package/package.json +10 -3
package/lib/index.js CHANGED
@@ -9,6 +9,7 @@ var DEFAULT_CONFIG = Object.freeze({
9
9
  optimizationStrength: "standard",
10
10
  nextEnabled: true,
11
11
  completionEnabled: true,
12
+ predictionDetail: "standard",
12
13
  displayMode: "ghost",
13
14
  debounceMs: 600,
14
15
  minIntervalMs: 2e3,
@@ -35,8 +36,8 @@ var numeric = {
35
36
  minIntervalMs: [500, 3e4],
36
37
  maxHistoryChars: [1e3, 64e3],
37
38
  maxDraftChars: [100, 32e3],
38
- maxNextChars: [20, 300],
39
- maxCompletionChars: [10, 200],
39
+ maxNextChars: [20, 1e3],
40
+ maxCompletionChars: [10, 500],
40
41
  optimizeTimeoutMs: [1e3, 12e4],
41
42
  suggestionTimeoutMs: [1e3, 6e4]
42
43
  };
@@ -48,6 +49,7 @@ function validate(config) {
48
49
  }
49
50
  if (!["ghost", "panel"].includes(config.displayMode)) throw new AssistantError("CONFIG", "\u663E\u793A\u65B9\u5F0F\u5FC5\u987B\u662F ghost \u6216 panel");
50
51
  if (!["light", "standard", "deep"].includes(config.optimizationStrength)) throw new AssistantError("CONFIG", "\u4F18\u5316\u5F3A\u5EA6\u5FC5\u987B\u662F light\u3001standard \u6216 deep");
52
+ if (!["concise", "standard", "detailed"].includes(config.predictionDetail)) throw new AssistantError("CONFIG", "\u9884\u6D4B\u8BE6\u7565\u5EA6\u5FC5\u987B\u662F concise\u3001standard \u6216 detailed");
51
53
  for (const [key, [min, max]] of Object.entries(numeric)) {
52
54
  const value = config[key];
53
55
  if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
@@ -69,6 +71,7 @@ function resolveStoredConfig(input) {
69
71
  const record = input && typeof input === "object" && !Array.isArray(input) ? input : {};
70
72
  const config = { ...DEFAULT_CONFIG };
71
73
  for (const key of Object.keys(DEFAULT_CONFIG)) {
74
+ if (key === "provider" || key === "model") continue;
72
75
  if (!(key in record)) continue;
73
76
  const value = record[key];
74
77
  const candidate = { ...DEFAULT_CONFIG, [key]: value };
@@ -79,9 +82,11 @@ function resolveStoredConfig(input) {
79
82
  }
80
83
  config[key] = typeof value === "string" ? value.trim() : value;
81
84
  }
82
- if (Boolean(config.provider) !== Boolean(config.model)) {
83
- config.provider = "";
84
- config.model = "";
85
+ try {
86
+ const route = validate({ ...DEFAULT_CONFIG, provider: record.provider ?? "", model: record.model ?? "" });
87
+ config.provider = route.provider;
88
+ config.model = route.model;
89
+ } catch {
85
90
  }
86
91
  return config;
87
92
  }
@@ -119,7 +124,7 @@ function buildContext(history, budget = 16e3) {
119
124
  }
120
125
 
121
126
  // src/core/protection.ts
122
- var FILE_EXTENSIONS = [
127
+ var FILE_EXTENSIONS = /* @__PURE__ */ new Set([
123
128
  "json",
124
129
  "jsonc",
125
130
  "json5",
@@ -209,8 +214,8 @@ var FILE_EXTENSIONS = [
209
214
  "stylelintrc",
210
215
  "babelrc",
211
216
  "browserslistrc"
212
- ].join("|");
213
- var NAMED_FILES = [
217
+ ]);
218
+ var NAMED_FILES = /* @__PURE__ */ new Set([
214
219
  "LICENSE",
215
220
  "LICENCE",
216
221
  "README",
@@ -227,28 +232,193 @@ var NAMED_FILES = [
227
232
  "Vagrantfile",
228
233
  "Jenkinsfile",
229
234
  "Brewfile"
230
- ].join("|");
231
- var pathSegment = "[\\p{L}\\p{N}\\p{M}_.@+~-]";
232
- function protectText(text, locked = []) {
235
+ ]);
236
+ var DOTFILES = [
237
+ ".env",
238
+ ".npmrc",
239
+ ".nvmrc",
240
+ ".editorconfig",
241
+ ".gitignore",
242
+ ".gitattributes",
243
+ ".gitmodules",
244
+ ".prettierrc",
245
+ ".eslintrc",
246
+ ".stylelintrc",
247
+ ".babelrc",
248
+ ".browserslistrc"
249
+ ];
250
+ var candidatePattern = /[\p{L}\p{N}\p{M}_.@+~\\/-]+/uy;
251
+ var cjk = "[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]";
252
+ var cjkCharacter = new RegExp(cjk, "u");
253
+ var candidateEnding = new RegExp(`(?:\\.[A-Za-z][A-Za-z0-9]{0,11}|${[...NAMED_FILES].join("|")})(?=${cjk}|$)`, "gu");
254
+ function knownFile(value) {
255
+ const dot = value.lastIndexOf(".");
256
+ return NAMED_FILES.has(value) || DOTFILES.some((name2) => value === name2 || value.startsWith(name2 + ".")) || dot > 0 && FILE_EXTENSIONS.has(value.slice(dot + 1).toLowerCase());
257
+ }
258
+ function pathOrFile(value) {
259
+ const slash = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
260
+ if (slash < 0) return knownFile(value);
261
+ if (/^(?:\.{1,2}[\\/]|[\\/])/.test(value)) return value.length > slash + 1 || slash > 0;
262
+ const tail = value.slice(slash + 1), dot = tail.lastIndexOf(".");
263
+ if (dot >= 0 && /^[A-Za-z][A-Za-z0-9]{0,11}$/.test(tail.slice(dot + 1))) return true;
264
+ let separators = 0;
265
+ for (const char of value) if (char === "/" || char === "\\") separators++;
266
+ return separators >= 2;
267
+ }
268
+ function literalStart(value) {
269
+ const slash = value.search(/[\\/]/);
270
+ const first = slash < 0 ? value : value.slice(0, slash);
271
+ if (slash >= 0) {
272
+ const relative = first.match(/(?:\.{1,2}|~)$/);
273
+ if (relative) return relative.index;
274
+ }
275
+ const dot = first.indexOf(".");
276
+ const stem = dot < 0 ? first : first.slice(0, dot);
277
+ let start = 0, previous = "", offset = 0;
278
+ for (const char of stem) {
279
+ if (cjkCharacter.test(previous) && /[A-Za-z0-9_@+~-]/.test(char)) start = offset;
280
+ previous = char;
281
+ offset += char.length;
282
+ }
283
+ if (!start && dot > 0 && DOTFILES.some((name2) => first.slice(dot) === name2 || first.slice(dot).startsWith(name2 + ".")) && Array.from(stem).every((char) => cjkCharacter.test(char))) return dot;
284
+ return start;
285
+ }
286
+ function candidateRanges(value, base) {
233
287
  const ranges = [];
234
- const patterns = [
235
- /```[^]*?(?:```|$)/g,
236
- /~~~[^]*?(?:~~~|$)/g,
237
- /`[^`\n]+`/g,
238
- /https?:\/\/[^\s<>"'))\],。;;]+/g,
239
- /[A-Za-z]:[\\/][^\s<>"',。;;))\]]+/g,
240
- /(?:\.{1,2}[\\/]|\/(?:[\p{L}\p{N}\p{M}_.-]+\/)+)[\p{L}\p{N}\p{M}_./\\@+-]+/gu,
241
- // Relative path carrying an extension: `src/index.ts`, `docs/validation.md`.
242
- // Requires at least one separator so plain prose words stay untouched.
243
- new RegExp(`(?:${pathSegment}+\\/)+${pathSegment}*\\.[A-Za-z][A-Za-z0-9]{0,11}`, "gu"),
244
- // Deep relative path without an extension: `src/core/generation`.
245
- new RegExp(`(?:${pathSegment}+\\/){2,}${pathSegment}+`, "gu"),
246
- // Bare file name with a known extension: `package.json`, `README.md`.
247
- new RegExp(`${pathSegment}+\\.(?:${FILE_EXTENSIONS})(?![A-Za-z0-9_])`, "giu"),
248
- // Well-known extensionless file names.
249
- new RegExp(`(?<![A-Za-z0-9_])(?:${NAMED_FILES})(?![A-Za-z0-9_])`, "g")
250
- ];
251
- for (const re of patterns) for (const m of text.matchAll(re)) ranges.push({ start: m.index, end: m.index + m[0].length });
288
+ let cursor = 0;
289
+ candidateEnding.lastIndex = 0;
290
+ for (const ending of value.matchAll(candidateEnding)) {
291
+ const end = ending.index + ending[0].length;
292
+ let next = end;
293
+ while (next < value.length) {
294
+ const char = String.fromCodePoint(value.codePointAt(next));
295
+ if (!cjkCharacter.test(char)) break;
296
+ next += char.length;
297
+ }
298
+ if (next > end && /[\\/]/.test(value[next] ?? "")) continue;
299
+ const portion = value.slice(cursor, end);
300
+ const start = cursor + literalStart(portion);
301
+ if (pathOrFile(value.slice(start, end))) ranges.push({ start: base + start, end: base + end });
302
+ cursor = end;
303
+ }
304
+ if (cursor < value.length && /[.\\/]/.test(value.slice(cursor))) {
305
+ const portion = value.slice(cursor);
306
+ const start = cursor + literalStart(portion);
307
+ let end = value.length;
308
+ const tailStart = Math.max(start, Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")) + 1);
309
+ const tail = value.slice(tailStart), boundary = tail.search(cjkCharacter);
310
+ if (tailStart >= start && boundary > 0 && /^[A-Za-z0-9_@+~.-]/.test(tail)) end = tailStart + boundary;
311
+ if (pathOrFile(value.slice(start, end))) ranges.push({ start: base + start, end: base + end });
312
+ }
313
+ return ranges;
314
+ }
315
+ function urlEnd(text, start) {
316
+ let end = start, parentheses = 0, brackets = 0;
317
+ while (end < text.length) {
318
+ const char = text[end];
319
+ if (/[\s<>"'`,。;;)]/.test(char)) break;
320
+ if (char === "(") parentheses++;
321
+ else if (char === ")") {
322
+ if (!parentheses) break;
323
+ parentheses--;
324
+ } else if (char === "[") brackets++;
325
+ else if (char === "]") {
326
+ if (!brackets) break;
327
+ brackets--;
328
+ }
329
+ end++;
330
+ }
331
+ while (end > start && /[.,!]/.test(text[end - 1])) end--;
332
+ return end;
333
+ }
334
+ function windowsPathEnd(text, start) {
335
+ const quote = text[start - 1];
336
+ if (quote === '"' || quote === "'") {
337
+ const closing = text.indexOf(quote, start);
338
+ if (closing >= 0 && !/[\r\n]/.test(text.slice(start, closing))) return closing;
339
+ }
340
+ let end = start + 3;
341
+ while (end < text.length && !/[\r\n<>"'`,。;;))\]!?!?::]/.test(text[end])) end++;
342
+ while (end > start && /[ \t]/.test(text[end - 1])) end--;
343
+ const value = text.slice(start, end);
344
+ const tailStart = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")) + 1;
345
+ const tail = value.slice(tailStart);
346
+ const dotfile = DOTFILES.some((name2) => tail.startsWith(name2) && /[. \t]/.test(tail[name2.length] ?? ""));
347
+ let dot = -1;
348
+ for (let i = 0; i < tail.length; i++) {
349
+ if (tail[i] === ".") dot = i;
350
+ if (/[ \t]/.test(tail[i]) && (dotfile || dot >= 0 && i - dot <= 13 && FILE_EXTENSIONS.has(tail.slice(dot + 1, i).toLowerCase()) || i <= 20 && NAMED_FILES.has(tail.slice(0, i)))) return start + tailStart + i;
351
+ }
352
+ return end;
353
+ }
354
+ function automaticRanges(text) {
355
+ const ranges = [];
356
+ let cursor = 0;
357
+ while (cursor < text.length) {
358
+ const start = cursor;
359
+ const fence = text.startsWith("```", cursor) ? "```" : text.startsWith("~~~", cursor) ? "~~~" : null;
360
+ if (fence) {
361
+ const closing = text.indexOf(fence, cursor + 3);
362
+ cursor = closing < 0 ? text.length : closing + 3;
363
+ ranges.push({ start, end: cursor });
364
+ continue;
365
+ }
366
+ if (text[cursor] === "`") {
367
+ let end = cursor + 1;
368
+ while (end < text.length && text[end] !== "`" && text[end] !== "\n") end++;
369
+ if (text[end] === "`" && end > cursor + 1) {
370
+ cursor = end + 1;
371
+ ranges.push({ start, end: cursor });
372
+ continue;
373
+ }
374
+ }
375
+ if (text[cursor] === '"' || text[cursor] === "'") {
376
+ const closing = text.indexOf(text[cursor], cursor + 1);
377
+ if (closing >= 0) {
378
+ const literal = text.slice(cursor + 1, closing);
379
+ if (!/[\r\n]/.test(literal) && (pathOrFile(literal) || /^[A-Za-z]:[\\/]/.test(literal))) {
380
+ ranges.push({ start: cursor + 1, end: closing });
381
+ cursor = closing + 1;
382
+ continue;
383
+ }
384
+ }
385
+ }
386
+ const scheme = text.slice(cursor, cursor + 8).toLowerCase();
387
+ if (scheme.startsWith("https://") || scheme.startsWith("http://")) {
388
+ cursor = urlEnd(text, cursor);
389
+ ranges.push({ start, end: cursor });
390
+ continue;
391
+ }
392
+ if (/[A-Za-z]/.test(text[cursor]) && text[cursor + 1] === ":" && /[\\/]/.test(text[cursor + 2] ?? "")) {
393
+ cursor = windowsPathEnd(text, cursor);
394
+ ranges.push({ start, end: cursor });
395
+ continue;
396
+ }
397
+ candidatePattern.lastIndex = cursor;
398
+ const match = candidatePattern.exec(text);
399
+ if (match) {
400
+ cursor = candidatePattern.lastIndex;
401
+ const fenceOffset = match[0].indexOf("~~~");
402
+ if (fenceOffset >= 0) cursor = start + fenceOffset;
403
+ else if (text[cursor] === ":") {
404
+ if (text.startsWith("//", cursor + 1)) {
405
+ const ending = text.slice(Math.max(start, cursor - 5), cursor).toLowerCase();
406
+ if (ending.endsWith("https")) cursor -= 5;
407
+ else if (ending.endsWith("http")) cursor -= 4;
408
+ else if (/[A-Za-z]/.test(text[cursor - 1] ?? "")) cursor--;
409
+ } else if (/[\\/]/.test(text[cursor + 1] ?? "") && /[A-Za-z]/.test(text[cursor - 1] ?? "")) cursor--;
410
+ }
411
+ if (cursor === start) continue;
412
+ let end = cursor;
413
+ if (!/[\\/]\.{1,2}$/.test(text.slice(start, cursor))) while (end > start && text[end - 1] === ".") end--;
414
+ const value = text.slice(start, end);
415
+ if (value) ranges.push(...candidateRanges(value, start));
416
+ } else cursor++;
417
+ }
418
+ return ranges;
419
+ }
420
+ function protectText(text, locked = []) {
421
+ const ranges = automaticRanges(text);
252
422
  for (const literal of locked) {
253
423
  if (typeof literal !== "string" || !literal.length || !text.includes(literal)) throw new AssistantError("LOCK_SCOPE", "\u9501\u5B9A\u7247\u6BB5\u5DF2\u53D8\u5316\u6216\u4E0D\u5728\u672C\u6B21\u4F18\u5316\u8303\u56F4\u5185");
254
424
  let start = text.indexOf(literal);
@@ -264,8 +434,14 @@ function protectText(text, locked = []) {
264
434
  if (prev && r.start < prev.end) prev.end = Math.max(prev.end, r.end);
265
435
  else merged.push({ ...r });
266
436
  }
267
- let prefix = "DSH_KEEP_";
268
- while (text.includes(prefix)) prefix = "_" + prefix;
437
+ let longestPrefix = -1, occurrence = text.indexOf("DSH_KEEP_");
438
+ while (occurrence >= 0) {
439
+ let start = occurrence;
440
+ while (start > 0 && text[start - 1] === "_") start--;
441
+ longestPrefix = Math.max(longestPrefix, occurrence - start);
442
+ occurrence = text.indexOf("DSH_KEEP_", occurrence + 9);
443
+ }
444
+ const prefix = "_".repeat(longestPrefix + 1) + "DSH_KEEP_";
269
445
  const fragments = [];
270
446
  let cursor = 0, masked = "";
271
447
  for (const r of merged) {
@@ -278,19 +454,20 @@ function protectText(text, locked = []) {
278
454
  return { original: text, masked: masked + text.slice(cursor), fragments };
279
455
  }
280
456
  function restoreProtected(output, protectedText) {
281
- let remaining = output;
282
- for (const { token } of protectedText.fragments) {
283
- if (remaining.split(token).length !== 2) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u672A\u5B8C\u6574\u4FDD\u7559\u9501\u5B9A\u5185\u5BB9\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
284
- remaining = remaining.replace(token, "");
285
- }
286
- for (const marker of remaining.match(/⟪_*DSH_KEEP_\d+⟫/gu) ?? []) {
287
- if (!protectedText.original.includes(marker)) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u5305\u542B\u65E0\u6548\u7684\u4FDD\u62A4\u6807\u8BB0\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
288
- }
289
- let restored = output;
290
- for (const { token, original } of protectedText.fragments) {
291
- if (restored.split(token).length !== 2) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u672A\u5B8C\u6574\u4FDD\u7559\u9501\u5B9A\u5185\u5BB9\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
292
- restored = restored.replace(token, () => original);
293
- }
457
+ const fragments = new Map(protectedText.fragments.map((fragment) => [fragment.token, fragment.original]));
458
+ const existing = new Set(protectedText.original.match(/⟪_*DSH_KEEP_\d+⟫/gu) ?? []);
459
+ const seen = /* @__PURE__ */ new Set();
460
+ const restored = output.replace(/⟪_*DSH_KEEP_\d+⟫/gu, (token) => {
461
+ const original = fragments.get(token);
462
+ if (original === void 0) {
463
+ if (!existing.has(token)) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u5305\u542B\u65E0\u6548\u7684\u4FDD\u62A4\u6807\u8BB0\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
464
+ return token;
465
+ }
466
+ if (seen.has(token)) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u672A\u5B8C\u6574\u4FDD\u7559\u9501\u5B9A\u5185\u5BB9\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
467
+ seen.add(token);
468
+ return original;
469
+ });
470
+ if (seen.size !== fragments.size) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u672A\u5B8C\u6574\u4FDD\u7559\u9501\u5B9A\u5185\u5BB9\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
294
471
  return restored;
295
472
  }
296
473
 
@@ -311,6 +488,13 @@ function prepareGeneration(request, history, config) {
311
488
  if (charCount(target) > config.maxDraftChars) throw new AssistantError("TOO_LONG", `\u672C\u6B21\u6587\u672C\u8D85\u8FC7 ${config.maxDraftChars} \u5B57\u7B26\uFF0C\u8BF7\u9009\u4E2D\u8F83\u77ED\u7247\u6BB5\u4F18\u5316`);
312
489
  if (request.operation === "optimize" && !target.trim()) throw new AssistantError("EMPTY", "\u8BF7\u5148\u8F93\u5165\u6216\u9009\u4E2D\u9700\u8981\u4F18\u5316\u7684\u6587\u5B57");
313
490
  if (request.operation === "complete" && !request.draft.trim()) throw new AssistantError("EMPTY", "\u6CA1\u6709\u53EF\u7EED\u5199\u7684\u8349\u7A3F");
491
+ const detail = config.predictionDetail;
492
+ const suggestionCap = request.operation === "next" ? config.maxNextChars : config.maxCompletionChars;
493
+ const detailGuidance = {
494
+ concise: "Detail level: CONCISE. Prefer one brief sentence or phrase. Do not add scope merely to fill the character budget.",
495
+ standard: "Detail level: STANDARD. Use a natural, complete expression with only the detail needed for the current intent.",
496
+ detailed: "Detail level: DETAILED. Explain only already-supported intent more fully; short paragraphs or a structured list are allowed when useful. There is no minimum length. Never add tasks, constraints, permissions, deliverables, or commitments merely to make the message longer."
497
+ }[detail];
314
498
  let system = common, protection = null;
315
499
  if (request.operation === "optimize") {
316
500
  protection = protectText(target, request.lockedFragments);
@@ -335,18 +519,22 @@ function prepareGeneration(request, history, config) {
335
519
  "Consider answering the latest question, a natural follow-up question, correcting a misunderstanding, or proceeding with an explicitly requested unfinished step.",
336
520
  "A question or correction is allowed when supported. Do not automatically propose commits, pushing, testing, or new features just because code was produced.",
337
521
  `If no single continuation is well-supported, or the task is naturally finished, output ${NO_SUGGESTION}. Do not guess an unresolved choice.`,
338
- `Return one short message of at most ${config.maxNextChars} characters.`,
522
+ `Return one message of at most ${suggestionCap} Unicode characters. This is a hard maximum, not a target or minimum.`,
523
+ detailGuidance,
524
+ detail === "detailed" ? "Internal line breaks are allowed for supported structure; do not add blank padding lines." : "Return a single line with no line breaks.",
339
525
  "Examples: user requested hit rate AND invalidation, only hit rate was explained -> \u518D\u89E3\u91CA\u4E00\u4E0B\u7F13\u5B58\u5931\u6548\u3002",
340
526
  "User said explanation only, assistant proposes file edits -> \u5148\u4E0D\u8981\u6539\u6587\u4EF6\uFF0C\u8BF7\u53EA\u89E3\u91CA\u539F\u56E0\u3002",
341
527
  `Assistant asks Redis or local cache, no preference in history -> ${NO_SUGGESTION}.`
342
528
  ].join("\n");
343
529
  } else {
344
530
  system += "\n" + [
345
- "Complete the user's current draft by returning ONLY a short suffix to append exactly at its end.",
531
+ "Complete the user's current draft by returning ONLY a suffix to append exactly at its end.",
346
532
  "The draft is the strongest constraint. Do not repeat its prefix, rewrite existing text, introduce a new topic, or answer the question.",
347
533
  "Preserve necessary leading whitespace in the suffix. A complete message does not need an arbitrary extra requirement.",
348
534
  `If the intended continuation is uncertain or the draft is already complete, output ${NO_SUGGESTION}.`,
349
- `Return at most ${config.maxCompletionChars} characters, with no explanation.`,
535
+ `Return at most ${suggestionCap} Unicode characters, with no explanation. This is a hard maximum, not a target or minimum.`,
536
+ detailGuidance,
537
+ detail === "detailed" ? "Line breaks are allowed only when they naturally continue the existing draft structure. Preserve a necessary leading line break, but do not start an unrelated list or task." : "Return a single line with no line breaks.",
350
538
  "Examples: draft \u8BF7\u89E3\u91CA\u8FD9\u6BB5\u4EE3\u7801\u7684\u65F6\u95F4 -> \u590D\u6742\u5EA6\u3002",
351
539
  `Draft Please explain the cause without editing files. -> ${NO_SUGGESTION}.`
352
540
  ].join("\n");
@@ -365,11 +553,46 @@ function prepareGeneration(request, history, config) {
365
553
  config,
366
554
  protection,
367
555
  timeoutMs: request.operation === "optimize" ? config.optimizeTimeoutMs : config.suggestionTimeoutMs,
368
- maxOutputTokens: request.operation === "optimize" ? 8192 : request.operation === "next" ? 256 : 192
556
+ // A Unicode character may require several tokens, especially for CJK and
557
+ // emoji. Keep the transport budget large enough for the explicit text cap.
558
+ maxOutputTokens: request.operation === "optimize" ? 8192 : Math.max(request.operation === "next" ? 128 : 96, suggestionCap * 4 + 32)
369
559
  };
370
560
  }
561
+ function hasUnpairedSurrogate(text) {
562
+ for (let i = 0; i < text.length; i++) {
563
+ const code = text.charCodeAt(i);
564
+ if (code >= 55296 && code <= 56319) {
565
+ const next = text.charCodeAt(++i);
566
+ if (!(next >= 56320 && next <= 57343)) return true;
567
+ } else if (code >= 56320 && code <= 57343) return true;
568
+ }
569
+ return false;
570
+ }
571
+ function cleanOptimizationOutput(raw, prepared) {
572
+ let cleaned = raw.trim();
573
+ const original = prepared.protection.original.trim();
574
+ const masked = prepared.protection.masked.trim();
575
+ if (cleaned === masked) return cleaned;
576
+ const fence = cleaned.match(/^```(?:markdown|prompt|text)?\r?\n([\s\S]*?)\r?\n```$/i);
577
+ if (fence) cleaned = fence[1].trim();
578
+ const preamble = /^(?:(?:好的|好的,|已为您|根据您的要求|针对您的需求|我为您)?(?:优化|改写|整理|修改|调整)后的?(?:提示词|prompt|内容|表达|文本)?(?:如下|为)?[::\s\n]+|Here(?:'s| is) (?:the |an )?(?:optimized|improved|rewritten) (?:prompt|version)[::\s\n]+)/i;
579
+ const prefix = cleaned.match(preamble)?.[0];
580
+ if (prefix && !original.toLowerCase().startsWith(prefix.trim().toLowerCase())) cleaned = cleaned.slice(prefix.length).trim();
581
+ for (const [start, end] of [['"', '"'], ["\u201C", "\u201D"], ["`", "`"]]) {
582
+ if (!cleaned.startsWith(start) || !cleaned.endsWith(end) || cleaned.length < 2) continue;
583
+ const inner = cleaned.slice(start.length, -end.length).trim();
584
+ const originalWrapped = masked.startsWith(start) && masked.endsWith(end);
585
+ if (!originalWrapped || inner.startsWith(start) && inner.endsWith(end)) cleaned = inner;
586
+ break;
587
+ }
588
+ return cleaned;
589
+ }
371
590
  function finishGeneration(raw, prepared) {
372
591
  if (typeof raw !== "string") throw new AssistantError("OUTPUT", "\u6A21\u578B\u672A\u8FD4\u56DE\u6709\u6548\u6587\u5B57");
592
+ if (hasUnpairedSurrogate(raw)) {
593
+ if (prepared.operation === "optimize") throw new AssistantError("OUTPUT", "\u6A21\u578B\u8FD4\u56DE\u4E86\u4E0D\u5B8C\u6574\u5B57\u7B26\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
594
+ return null;
595
+ }
373
596
  const trimmed = raw.trim();
374
597
  if (!trimmed || trimmed === NO_SUGGESTION) {
375
598
  if (prepared.operation === "optimize") throw new AssistantError("EMPTY_OUTPUT", "\u6A21\u578B\u672A\u8FD4\u56DE\u4F18\u5316\u6587\u5B57\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
@@ -377,16 +600,20 @@ function finishGeneration(raw, prepared) {
377
600
  }
378
601
  if (prepared.operation === "optimize") {
379
602
  if (raw.includes(NO_SUGGESTION)) throw new AssistantError("OUTPUT", "\u6A21\u578B\u8F93\u51FA\u4E0D\u5B8C\u6574\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
380
- const output = restoreProtected(trimmed, prepared.protection);
603
+ const cleaned = cleanOptimizationOutput(trimmed, prepared);
604
+ if (!cleaned.trim()) throw new AssistantError("EMPTY_OUTPUT", "\u6A21\u578B\u672A\u8FD4\u56DE\u4F18\u5316\u6587\u5B57\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
605
+ const output = restoreProtected(cleaned, prepared.protection);
606
+ if (hasUnpairedSurrogate(output)) throw new AssistantError("OUTPUT", "\u4F18\u5316\u7ED3\u679C\u5305\u542B\u4E0D\u5B8C\u6574\u5B57\u7B26\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
381
607
  if (charCount(output) > prepared.config.maxDraftChars * 2) throw new AssistantError("OUTPUT", "\u4F18\u5316\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
382
608
  return output;
383
609
  }
384
610
  const cap = prepared.operation === "next" ? prepared.config.maxNextChars : prepared.config.maxCompletionChars;
385
- if (charCount(raw) > cap || /[\r\n]/.test(trimmed) || /^(?:```|\{\s*"|<think>|(?:建议|预测|续写|Suggestion|Prediction)\s*[::])/i.test(trimmed) || trimmed.includes(NO_SUGGESTION)) return null;
611
+ const allowNewlines = prepared.config.predictionDetail === "detailed";
612
+ if (charCount(raw) > cap || !allowNewlines && /[\r\n]/.test(raw) || /^(?:```|\{\s*"|<think>|(?:建议|预测|续写|Suggestion|Prediction)\s*[::])/i.test(trimmed) || trimmed.includes(NO_SUGGESTION)) return null;
386
613
  if (prepared.operation === "next") return trimmed;
387
- if (trimmed.startsWith(prepared.request.draft.trim()) || prepared.request.draft.endsWith(raw)) return null;
388
- if (/[\uD800-\uDBFF]$/.test(raw) || /^[\uDC00-\uDFFF]/.test(raw)) return null;
389
- return raw.replace(/[ \t]+$/, "");
614
+ const draft = prepared.request.draft.trim();
615
+ if (charCount(draft) >= 4 && trimmed.startsWith(draft) || prepared.request.draft.endsWith(raw)) return null;
616
+ return raw.replace(/\s+$/u, "");
390
617
  }
391
618
 
392
619
  // src/core/scheduler.ts
@@ -441,19 +668,101 @@ var SessionScheduler = class {
441
668
  };
442
669
 
443
670
  // src/host/runtime.ts
444
- import { randomUUID } from "node:crypto";
445
- import { z as z2 } from "zod";
671
+ import { randomUUID as randomUUID2 } from "node:crypto";
672
+ import { z as z3 } from "zod";
446
673
  import { Session } from "@deepseek-ai/dsh-session";
447
674
  import { BlockAssembler } from "@deepseek-ai/dsh-llm";
448
675
 
449
- // src/host/transcript.ts
676
+ // src/host/storage.ts
450
677
  import { z } from "zod";
451
- var transcriptSchema = z.object({
452
- entries: z.array(z.object({ role: z.enum(["user", "assistant"]), text: z.string(), turnId: z.string().optional() })),
453
- currentTurn: z.number().int().nonnegative(),
454
- sourceTurnId: z.string().nullable(),
455
- running: z.boolean(),
456
- pendingAssistant: z.string().nullable()
678
+ import { randomUUID } from "node:crypto";
679
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
680
+ var draftSchema = z.object({ id: z.string().min(1).max(300), text: z.string().min(1).max(64e3).refine((text) => charCount(text) <= 32e3), createdAt: z.number().int().nonnegative() });
681
+ var sessionDraftsSchema = z.object({ version: z.literal(1), drafts: z.array(draftSchema).max(3) });
682
+ function createDomainSpec(initial) {
683
+ return defineDomain({
684
+ name: "input_assistant",
685
+ version: 1,
686
+ layout: "per-record",
687
+ global: { schema: z.object({ version: z.literal(1), settings: z.record(z.string(), z.unknown()) }), initial: { version: 1, settings: { ...initial } } },
688
+ tables: { drafts: domainTable(sessionDraftsSchema) }
689
+ });
690
+ }
691
+ var AssistantStorage = class {
692
+ constructor(domain) {
693
+ this.domain = domain;
694
+ }
695
+ chain = Promise.resolve();
696
+ transaction(work) {
697
+ const task = this.chain.then(async () => work(await this.domain()));
698
+ this.chain = task.then(() => void 0, () => void 0);
699
+ return task;
700
+ }
701
+ async settings() {
702
+ const domain = await this.domain();
703
+ return resolveStoredConfig(domain.global.get().settings);
704
+ }
705
+ updateSettings(patch) {
706
+ return this.transaction(async (domain) => {
707
+ const settings = resolveConfig({ ...resolveStoredConfig(domain.global.get().settings), ...patch });
708
+ await domain.global.set({ version: 1, settings });
709
+ return settings;
710
+ });
711
+ }
712
+ async list(sessionId) {
713
+ return (await this.domain()).table("drafts").get(sessionId)?.drafts.map((draft) => ({ ...draft })) ?? [];
714
+ }
715
+ save(sessionId, text, replaceId) {
716
+ if (text.trim().length === 0 || text.length > 64e3 || charCount(text) > 32e3) throw new AssistantError("DRAFT", "\u6682\u5B58\u5185\u5BB9\u4E3A\u7A7A\u6216\u8D85\u8FC7 32,000 \u5B57\u7B26");
717
+ return this.transaction(async (domain) => {
718
+ const drafts = [...domain.table("drafts").get(sessionId)?.drafts ?? []];
719
+ const index = replaceId === void 0 ? -1 : drafts.findIndex((draft) => draft.id === replaceId);
720
+ if (replaceId !== void 0 && index < 0) throw new AssistantError("DRAFT_NOT_FOUND", "\u8981\u66FF\u6362\u7684\u6682\u5B58\u8349\u7A3F\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
721
+ if (drafts.length >= 3 && index < 0) throw new AssistantError("DRAFT_LIMIT", "\u5F53\u524D\u4F1A\u8BDD\u5DF2\u6682\u5B58\u4E09\u6761\uFF0C\u8BF7\u9009\u62E9\u66FF\u6362\u6216\u5220\u9664\u4E00\u6761");
722
+ const next = { id: replaceId ?? randomUUID(), text, createdAt: Date.now() };
723
+ if (index < 0) drafts.push(next);
724
+ else drafts[index] = next;
725
+ await domain.table("drafts").put(sessionId, { version: 1, drafts });
726
+ return drafts.map((draft) => ({ ...draft }));
727
+ });
728
+ }
729
+ delete(sessionId, id) {
730
+ return this.transaction(async (domain) => {
731
+ const current = domain.table("drafts").get(sessionId)?.drafts ?? [];
732
+ const drafts = id === void 0 ? [] : current.filter((draft) => draft.id !== id);
733
+ const removed = id === void 0 ? current : current.filter((draft) => draft.id === id);
734
+ if (drafts.length === 0) await domain.table("drafts").delete(sessionId);
735
+ else await domain.table("drafts").put(sessionId, { version: 1, drafts });
736
+ return { drafts: drafts.map((draft) => ({ ...draft })), removed: removed.map((draft) => ({ ...draft })) };
737
+ });
738
+ }
739
+ /** Undo deletion without overwriting drafts created or changed elsewhere. */
740
+ restore(sessionId, deleted) {
741
+ const snapshots = z.array(draftSchema).max(3).parse(deleted);
742
+ return this.transaction(async (domain) => {
743
+ const drafts = [...domain.table("drafts").get(sessionId)?.drafts ?? []];
744
+ const ids = new Set(drafts.map((draft) => draft.id));
745
+ for (const snapshot of snapshots) {
746
+ if (!ids.has(snapshot.id)) {
747
+ drafts.push(snapshot);
748
+ ids.add(snapshot.id);
749
+ }
750
+ }
751
+ if (drafts.length > 3) throw new AssistantError("DRAFT_LIMIT", "\u5F53\u524D\u4F1A\u8BDD\u6682\u5B58\u7A7A\u95F4\u4E0D\u8DB3\uFF0C\u8BF7\u5220\u9664\u4E00\u6761\u540E\u518D\u64A4\u9500");
752
+ if (drafts.length) await domain.table("drafts").put(sessionId, { version: 1, drafts });
753
+ return drafts.map((draft) => ({ ...draft }));
754
+ });
755
+ }
756
+ };
757
+
758
+ // src/host/transcript.ts
759
+ import { z as z2 } from "zod";
760
+ var transcriptSchema = z2.object({
761
+ entries: z2.array(z2.object({ role: z2.enum(["user", "assistant"]), text: z2.string(), turnId: z2.string().optional() })),
762
+ currentTurn: z2.number().int().nonnegative(),
763
+ sourceTurnId: z2.string().nullable(),
764
+ running: z2.boolean(),
765
+ pendingAssistant: z2.string().nullable()
457
766
  });
458
767
  function emptyTranscript() {
459
768
  return { entries: [], currentTurn: 0, sourceTurnId: null, running: false, pendingAssistant: null };
@@ -492,36 +801,38 @@ function foldTranscript(state, event) {
492
801
  }
493
802
 
494
803
  // src/host/runtime.ts
495
- var stringId = z2.string().min(1).max(300);
496
- var sessionRequest = z2.object({ sessionId: stringId });
497
- var generationRequest = z2.object({
804
+ var stringId = z3.string().min(1).max(300);
805
+ var sessionRequest = z3.object({ sessionId: stringId });
806
+ var inputText = z3.string().max(64e3).refine((value) => charCount(value) <= 32e3);
807
+ var generationRequest = z3.object({
498
808
  requestId: stringId,
499
809
  sessionId: stringId,
500
- operation: z2.enum(["optimize", "next", "complete"]),
501
- sourceTurnId: z2.string().nullable(),
502
- draft: z2.string().max(128e3),
503
- draftRev: z2.number().int().nonnegative(),
504
- span: z2.object({ start: z2.number().int().nonnegative(), end: z2.number().int().nonnegative(), draftRev: z2.number().int().nonnegative() }),
810
+ operation: z3.enum(["optimize", "next", "complete"]),
811
+ sourceTurnId: z3.string().nullable(),
812
+ draft: z3.string().max(128e3),
813
+ draftRev: z3.number().int().nonnegative(),
814
+ span: z3.object({ start: z3.number().int().nonnegative(), end: z3.number().int().nonnegative(), draftRev: z3.number().int().nonnegative() }),
505
815
  connectionGeneration: stringId,
506
- routeKey: z2.string().max(1e3),
507
- targetText: z2.string().max(32e3).optional(),
508
- lockedFragments: z2.array(z2.string().min(1).max(32e3)).max(100).optional(),
509
- provider: z2.string().max(200).optional(),
510
- model: z2.string().max(200).optional()
816
+ routeKey: z3.string().max(1e3),
817
+ targetText: inputText.optional(),
818
+ lockedFragments: z3.array(inputText.refine((value) => value.length > 0)).max(100).optional(),
819
+ provider: z3.string().max(200).optional(),
820
+ model: z3.string().max(200).optional(),
821
+ manual: z3.boolean().optional()
511
822
  });
512
823
  var AssistantHostRuntime = class {
513
824
  constructor(ctx, storage) {
514
825
  this.ctx = ctx;
515
826
  this.storage = storage;
516
827
  }
517
- mount = randomUUID();
828
+ mount = randomUUID2();
518
829
  instances = /* @__PURE__ */ new WeakMap();
519
830
  scheduler = new SessionScheduler();
520
831
  disposed = false;
521
832
  state(session) {
522
833
  let state = this.instances.get(session);
523
834
  if (!state) {
524
- state = { generation: `${this.mount}:${randomUUID()}`, lastNextTurn: null, lastCompletionAt: 0 };
835
+ state = { generation: `${this.mount}:${randomUUID2()}`, nextTurn: null, nextRoutes: /* @__PURE__ */ new Set(), lastCompletionAt: -Infinity };
525
836
  this.instances.set(session, state);
526
837
  }
527
838
  return state;
@@ -558,9 +869,29 @@ var AssistantHostRuntime = class {
558
869
  ...route ? { route } : {}
559
870
  };
560
871
  }
561
- result(request, text) {
872
+ async models(signal) {
873
+ signal.throwIfAborted();
874
+ if (this.disposed) throw new AssistantError("DISPOSED", "\u8F93\u5165\u52A9\u624B\u5DF2\u505C\u7528");
875
+ const directory = this.ctx.get("sessionController");
876
+ if (!directory) throw new AssistantError("MODEL_CATALOG", "\u5F53\u524D\u5BBF\u4E3B\u672A\u63D0\u4F9B\u6A21\u578B\u76EE\u5F55\uFF0C\u8BF7\u4F7F\u7528\u624B\u52A8\u586B\u5199");
877
+ const catalog = await directory.modelCatalog();
878
+ signal.throwIfAborted();
879
+ if (this.disposed) throw new AssistantError("DISPOSED", "\u8F93\u5165\u52A9\u624B\u5DF2\u505C\u7528");
880
+ return {
881
+ groups: catalog.groups.map((group) => ({
882
+ provider: group.id,
883
+ name: group.name,
884
+ models: group.models.filter((model) => model.id.length > 0 && model.id.length <= 200).map((model) => ({ id: model.id, name: model.name }))
885
+ })).filter((group) => group.provider.length > 0 && group.provider.length <= 200 && group.models.length > 0),
886
+ // Provider diagnostics can include private details; advertise only the failed provider identity.
887
+ failures: catalog.failures.map((failure) => ({ provider: failure.id, name: failure.name }))
888
+ };
889
+ }
890
+ result(request, text, retryAfterMs) {
562
891
  return {
563
892
  text,
893
+ outcome: retryAfterMs === void 0 ? text === null ? "abstained" : "suggestion" : "throttled",
894
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs },
564
895
  requestId: request.requestId,
565
896
  sessionId: request.sessionId,
566
897
  sourceTurnId: request.sourceTurnId,
@@ -594,20 +925,28 @@ var AssistantHostRuntime = class {
594
925
  const state = this.state(session);
595
926
  const transcript = this.transcript(session);
596
927
  if (request.operation === "next") {
597
- if (!settings.nextEnabled || transcript.running || request.sourceTurnId === null || request.draft.length > 0 || state.lastNextTurn === request.sourceTurnId) return this.result(request, null);
598
- state.lastNextTurn = request.sourceTurnId;
928
+ if (!request.manual && !settings.nextEnabled || transcript.running || request.sourceTurnId === null || request.draft.length > 0) return this.result(request, null);
599
929
  } else if (request.operation === "complete") {
600
- if (!settings.completionEnabled || transcript.running) return this.result(request, null);
601
- const now = Date.now();
602
- if (now - state.lastCompletionAt < settings.minIntervalMs) return this.result(request, null);
603
- state.lastCompletionAt = now;
930
+ if (!request.manual && !settings.completionEnabled || transcript.running) return this.result(request, null);
604
931
  }
605
932
  const fallback = this.sessionRoute(session);
606
933
  const provider = settings.provider || request.provider || fallback?.provider;
607
934
  const model = settings.model || request.model || fallback?.model;
608
935
  if (!provider || !model) throw new AssistantError("MODEL", "\u8BF7\u5148\u5728 DSH \u8F93\u5165\u6846\u9009\u62E9\u53EF\u7528\u6A21\u578B\uFF0C\u6216\u5728\u8F93\u5165\u52A9\u624B\u8BBE\u7F6E\u8F85\u52A9\u6A21\u578B");
936
+ const actualRoute = JSON.stringify([provider, model]);
937
+ if (request.operation === "next") {
938
+ if (state.nextTurn !== request.sourceTurnId) {
939
+ state.nextTurn = request.sourceTurnId;
940
+ state.nextRoutes.clear();
941
+ }
942
+ if (!request.manual && state.nextRoutes.has(actualRoute)) return this.result(request, null);
943
+ } else if (request.operation === "complete") {
944
+ const retryAfterMs = settings.minIntervalMs - (Date.now() - state.lastCompletionAt);
945
+ if (retryAfterMs > 0) return this.result(request, null, retryAfterMs);
946
+ }
609
947
  const prepared = prepareGeneration(request, transcript.entries, settings);
610
948
  const text = await this.scheduler.run(request.sessionId, request.requestId, request.operation, prepared.timeoutMs, async (active) => {
949
+ if (request.operation === "complete") state.lastCompletionAt = Date.now();
611
950
  const call = await this.ctx.llm.prepareCall({ provider, model, maxTokens: prepared.maxOutputTokens }, active, (controls, info) => {
612
951
  const effort = info.reasoning?.efforts[0]?.id;
613
952
  return effort === void 0 ? { ...controls } : { ...controls, reasoningEffort: effort };
@@ -636,6 +975,10 @@ var AssistantHostRuntime = class {
636
975
  return finishGeneration(raw, prepared);
637
976
  }, signal);
638
977
  this.current(request, session);
978
+ if (request.operation === "next") {
979
+ if (state.nextRoutes.size >= 512) state.nextRoutes.clear();
980
+ state.nextRoutes.add(actualRoute);
981
+ }
639
982
  return this.result(request, text);
640
983
  }
641
984
  cancel(id, requestId) {
@@ -653,10 +996,12 @@ var AssistantHostRuntime = class {
653
996
  switch (payload.method) {
654
997
  case "generate":
655
998
  return this.generate(payload.data, signal);
999
+ case "models/list":
1000
+ return this.models(signal);
656
1001
  case "settings/get":
657
1002
  return this.storage.settings();
658
1003
  case "settings/set": {
659
- const patch = z2.record(z2.string(), z2.unknown()).parse(payload.data);
1004
+ const patch = z3.record(z3.string(), z3.unknown()).parse(payload.data);
660
1005
  const result = await this.storage.updateSettings(patch);
661
1006
  this.scheduler.cancelAll();
662
1007
  return result;
@@ -664,7 +1009,7 @@ var AssistantHostRuntime = class {
664
1009
  case "status":
665
1010
  return this.status(sessionRequest.parse(payload.data).sessionId, signal);
666
1011
  case "cancel": {
667
- const request = z2.object({ sessionId: stringId, requestId: stringId.optional() }).parse(payload.data);
1012
+ const request = z3.object({ sessionId: stringId, requestId: stringId.optional() }).parse(payload.data);
668
1013
  await this.session(request.sessionId, signal);
669
1014
  return { cancelled: this.cancel(request.sessionId, request.requestId) };
670
1015
  }
@@ -674,7 +1019,7 @@ var AssistantHostRuntime = class {
674
1019
  return this.storage.list(request.sessionId);
675
1020
  }
676
1021
  case "drafts/save": {
677
- const request = z2.object({ sessionId: stringId, text: z2.string().max(32e3), replaceId: stringId.optional() }).parse(payload.data);
1022
+ const request = z3.object({ sessionId: stringId, text: inputText, replaceId: stringId.optional() }).parse(payload.data);
678
1023
  await this.session(request.sessionId, signal);
679
1024
  if (!(await this.storage.settings()).stashEnabled) {
680
1025
  throw new AssistantError("DISABLED", "\u8349\u7A3F\u6682\u5B58\u5DF2\u5173\u95ED\uFF0C\u8BF7\u5728\u8F93\u5165\u52A9\u624B\u8BBE\u7F6E\u4E2D\u542F\u7528\uFF1B\u5DF2\u6709\u8349\u7A3F\u4ECD\u53EF\u67E5\u770B\u6216\u5220\u9664");
@@ -682,7 +1027,7 @@ var AssistantHostRuntime = class {
682
1027
  return this.storage.save(request.sessionId, request.text, request.replaceId);
683
1028
  }
684
1029
  case "drafts/delete": {
685
- const request = z2.object({ sessionId: stringId, id: stringId }).parse(payload.data);
1030
+ const request = z3.object({ sessionId: stringId, id: stringId }).parse(payload.data);
686
1031
  await this.session(request.sessionId, signal);
687
1032
  return this.storage.delete(request.sessionId, request.id);
688
1033
  }
@@ -691,79 +1036,20 @@ var AssistantHostRuntime = class {
691
1036
  await this.session(request.sessionId, signal);
692
1037
  return this.storage.delete(request.sessionId);
693
1038
  }
1039
+ case "drafts/restore": {
1040
+ const request = z3.object({ sessionId: stringId, drafts: z3.array(draftSchema).max(3) }).parse(payload.data);
1041
+ await this.session(request.sessionId, signal);
1042
+ return this.storage.restore(request.sessionId, request.drafts);
1043
+ }
694
1044
  }
695
1045
  }
696
1046
  };
697
1047
 
698
- // src/host/storage.ts
699
- import { z as z3 } from "zod";
700
- import { randomUUID as randomUUID2 } from "node:crypto";
701
- import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
702
- var draftSchema = z3.object({ id: z3.string().min(1), text: z3.string().min(1).max(32e3), createdAt: z3.number().int().nonnegative() });
703
- var sessionDraftsSchema = z3.object({ version: z3.literal(1), drafts: z3.array(draftSchema).max(3) });
704
- function createDomainSpec(initial) {
705
- return defineDomain({
706
- name: "input_assistant",
707
- version: 1,
708
- layout: "per-record",
709
- global: { schema: z3.object({ version: z3.literal(1), settings: z3.record(z3.string(), z3.unknown()) }), initial: { version: 1, settings: { ...initial } } },
710
- tables: { drafts: domainTable(sessionDraftsSchema) }
711
- });
712
- }
713
- var AssistantStorage = class {
714
- constructor(domain) {
715
- this.domain = domain;
716
- }
717
- chain = Promise.resolve();
718
- transaction(work) {
719
- const task = this.chain.then(async () => work(await this.domain()));
720
- this.chain = task.then(() => void 0, () => void 0);
721
- return task;
722
- }
723
- async settings() {
724
- const domain = await this.domain();
725
- return resolveStoredConfig(domain.global.get().settings);
726
- }
727
- updateSettings(patch) {
728
- return this.transaction(async (domain) => {
729
- const settings = resolveConfig({ ...resolveStoredConfig(domain.global.get().settings), ...patch });
730
- await domain.global.set({ version: 1, settings });
731
- return settings;
732
- });
733
- }
734
- async list(sessionId) {
735
- return (await this.domain()).table("drafts").get(sessionId)?.drafts.map((draft) => ({ ...draft })) ?? [];
736
- }
737
- save(sessionId, text, replaceId) {
738
- if (text.trim().length === 0 || text.length > 32e3) throw new AssistantError("DRAFT", "\u6682\u5B58\u5185\u5BB9\u4E3A\u7A7A\u6216\u8D85\u8FC7 32,000 \u5B57\u7B26");
739
- return this.transaction(async (domain) => {
740
- const drafts = [...domain.table("drafts").get(sessionId)?.drafts ?? []];
741
- const index = replaceId === void 0 ? -1 : drafts.findIndex((draft) => draft.id === replaceId);
742
- if (replaceId !== void 0 && index < 0) throw new AssistantError("DRAFT_NOT_FOUND", "\u8981\u66FF\u6362\u7684\u6682\u5B58\u8349\u7A3F\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
743
- if (drafts.length >= 3 && index < 0) throw new AssistantError("DRAFT_LIMIT", "\u5F53\u524D\u4F1A\u8BDD\u5DF2\u6682\u5B58\u4E09\u6761\uFF0C\u8BF7\u9009\u62E9\u66FF\u6362\u6216\u5220\u9664\u4E00\u6761");
744
- const next = { id: replaceId ?? randomUUID2(), text, createdAt: Date.now() };
745
- if (index < 0) drafts.push(next);
746
- else drafts[index] = next;
747
- await domain.table("drafts").put(sessionId, { version: 1, drafts });
748
- return drafts.map((draft) => ({ ...draft }));
749
- });
750
- }
751
- delete(sessionId, id) {
752
- return this.transaction(async (domain) => {
753
- const current = domain.table("drafts").get(sessionId)?.drafts ?? [];
754
- const drafts = id === void 0 ? [] : current.filter((draft) => draft.id !== id);
755
- if (drafts.length === 0) await domain.table("drafts").delete(sessionId);
756
- else await domain.table("drafts").put(sessionId, { version: 1, drafts });
757
- return drafts.map((draft) => ({ ...draft }));
758
- });
759
- }
760
- };
761
-
762
1048
  // src/host/rpc.ts
763
1049
  import { z as z4 } from "zod";
764
1050
  import { createTypertRemoteInfo } from "@deepseek-ai/dsh-typert-protocol/remote-info";
765
1051
  var rpcPayloadSchema = z4.strictObject({
766
- method: z4.enum(["status", "generate", "cancel", "drafts/list", "drafts/save", "drafts/delete", "drafts/clear", "settings/get", "settings/set"]),
1052
+ method: z4.enum(["status", "generate", "cancel", "models/list", "drafts/list", "drafts/save", "drafts/delete", "drafts/clear", "drafts/restore", "settings/get", "settings/set"]),
767
1053
  data: z4.json()
768
1054
  });
769
1055
  var inputCodec = {
@@ -802,13 +1088,14 @@ var Config = schema.object({
802
1088
  optimizationStrength: schema.union(["light", "standard", "deep"]).default("standard"),
803
1089
  nextEnabled: schema.boolean().default(true),
804
1090
  completionEnabled: schema.boolean().default(true),
1091
+ predictionDetail: schema.union(["concise", "standard", "detailed"]).default("standard"),
805
1092
  displayMode: schema.union(["ghost", "panel"]).default("ghost"),
806
1093
  debounceMs: schema.natural().min(200).max(5e3).default(600),
807
1094
  minIntervalMs: schema.natural().min(500).max(3e4).default(2e3),
808
1095
  maxHistoryChars: schema.natural().min(1e3).max(64e3).default(16e3),
809
1096
  maxDraftChars: schema.natural().min(100).max(32e3).default(8e3),
810
- maxNextChars: schema.natural().min(20).max(300).default(120),
811
- maxCompletionChars: schema.natural().min(10).max(200).default(80),
1097
+ maxNextChars: schema.natural().min(20).max(1e3).default(120),
1098
+ maxCompletionChars: schema.natural().min(10).max(500).default(80),
812
1099
  optimizeTimeoutMs: schema.natural().min(1e3).max(12e4).default(6e4),
813
1100
  suggestionTimeoutMs: schema.natural().min(1e3).max(6e4).default(15e3),
814
1101
  provider: schema.string().default(""),
@@ -834,7 +1121,15 @@ var InputAssistantService = class extends TypertRemoteService {
834
1121
  function apply(ctx, config = {}) {
835
1122
  const initial = resolveConfig({ ...DEFAULT_CONFIG, ...config });
836
1123
  let opened;
837
- const domain = () => opened ??= ctx.storageDomain.open(createDomainSpec(initial));
1124
+ const domain = () => {
1125
+ if (opened) return opened;
1126
+ const attempt = Promise.resolve().then(() => ctx.storageDomain.open(createDomainSpec(initial)));
1127
+ opened = attempt;
1128
+ void attempt.catch(() => {
1129
+ if (opened === attempt) opened = void 0;
1130
+ });
1131
+ return attempt;
1132
+ };
838
1133
  const runtime = new AssistantHostRuntime(ctx, new AssistantStorage(domain));
839
1134
  ctx.sessionProjections.register({
840
1135
  key: "inputAssistantTranscript",