@yfwu2020/dsh-selection-explain 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +11 -4
  2. package/lib/chat-context.js +84 -0
  3. package/lib/chat-context.js.map +1 -0
  4. package/lib/client.js +463 -115
  5. package/lib/client.js.map +4 -4
  6. package/lib/config-values.js +2 -0
  7. package/lib/config-values.js.map +1 -1
  8. package/lib/host/analyze.js +127 -26
  9. package/lib/host/analyze.js.map +1 -1
  10. package/lib/host/chat-context.js +155 -0
  11. package/lib/host/chat-context.js.map +1 -0
  12. package/lib/host/chat-materials.js +64 -0
  13. package/lib/host/chat-materials.js.map +1 -0
  14. package/lib/host/config.js +2 -1
  15. package/lib/host/config.js.map +1 -1
  16. package/lib/host/history-attachments.js +17 -53
  17. package/lib/host/history-attachments.js.map +1 -1
  18. package/lib/host/history-store.js +77 -45
  19. package/lib/host/history-store.js.map +1 -1
  20. package/lib/host/http.js +2 -1
  21. package/lib/host/http.js.map +1 -1
  22. package/lib/host/mic.js +51 -6
  23. package/lib/host/mic.js.map +1 -1
  24. package/lib/host/promote.js +0 -1
  25. package/lib/host/promote.js.map +1 -1
  26. package/lib/host/settings-store.js +17 -18
  27. package/lib/host/settings-store.js.map +1 -1
  28. package/lib/settings.js +3 -1
  29. package/lib/settings.js.map +1 -1
  30. package/lib/types/chat-context.d.ts +53 -0
  31. package/lib/types/config-values.d.ts +6 -0
  32. package/lib/types/host/chat-context.d.ts +31 -0
  33. package/lib/types/host/chat-materials.d.ts +16 -0
  34. package/lib/types/host/config.d.ts +4 -2
  35. package/lib/types/host/history-attachments.d.ts +7 -40
  36. package/lib/types/host/history-store.d.ts +1 -0
  37. package/lib/types/host/types.d.ts +6 -0
  38. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -151,7 +151,6 @@ var VOICE_SILENCE_RMS = 0.012;
151
151
  var CONTEXT_WINDOW = 1500;
152
152
  var KEY_CONTEXT_CHARS = 300;
153
153
  var MAX_QUOTES = 4;
154
- var MAX_QUOTE_CHARS = 3e3;
155
154
  var MAX_QUOTE_SELECTION = 2e4;
156
155
  var QUOTE_ONLY_QUESTION = "\u5C31\u4E0A\u9762\u5F15\u7528\u7684\u6587\u5B57\uFF0C\u8BF4\u8BF4\u5B83\u5728\u8FD9\u91CC\u662F\u4EC0\u4E48\u610F\u601D\u3002";
157
156
  var QUOTE_CONTEXT_API = "/selection-explain/api/quote-context";
@@ -1836,6 +1835,13 @@ function adaptPreviewHtml(code, previewId) {
1836
1835
  }
1837
1836
  var previewSeq = 0;
1838
1837
  var previewFrames = {};
1838
+ function releasePreviewFrames(root) {
1839
+ if (!root) return;
1840
+ for (var key in previewFrames) {
1841
+ var frame = previewFrames[key].frame;
1842
+ if (frame === root || root.contains && root.contains(frame)) delete previewFrames[key];
1843
+ }
1844
+ }
1839
1845
  function registerPreviewFrame(frame) {
1840
1846
  previewSeq += 1;
1841
1847
  var id = "pv" + previewSeq;
@@ -1919,6 +1925,7 @@ function webPreviewBlock(code) {
1919
1925
  return { root: wrap, pre, frame };
1920
1926
  }
1921
1927
  function renderRich(parent2, text, options) {
1928
+ releasePreviewFrames(parent2);
1922
1929
  parent2.textContent = "";
1923
1930
  var lines = String(text || "").replace(/\r/g, "").split("\n");
1924
1931
  var current = null;
@@ -3259,7 +3266,7 @@ function createVoiceCardController(env) {
3259
3266
  }
3260
3267
  function unpinVCardBox() {
3261
3268
  if (vcardState.unpinTimer) {
3262
- clearTimeout(vcardState.unpinTimer);
3269
+ env.timerScope.clear(vcardState.unpinTimer);
3263
3270
  vcardState.unpinTimer = null;
3264
3271
  }
3265
3272
  if (vcard && vcardState.unpinHandler) {
@@ -3273,7 +3280,7 @@ function createVoiceCardController(env) {
3273
3280
  function scheduleVCardUnpin() {
3274
3281
  if (!vcard) return;
3275
3282
  if (vcardState.unpinTimer) {
3276
- clearTimeout(vcardState.unpinTimer);
3283
+ env.timerScope.clear(vcardState.unpinTimer);
3277
3284
  vcardState.unpinTimer = null;
3278
3285
  }
3279
3286
  if (vcardState.unpinHandler) {
@@ -3582,7 +3589,7 @@ function createVoiceCardController(env) {
3582
3589
  line.setAttribute("data-off", "a");
3583
3590
  var done = function() {
3584
3591
  if (vcardState.crtTimer) {
3585
- clearTimeout(vcardState.crtTimer);
3592
+ env.timerScope.clear(vcardState.crtTimer);
3586
3593
  vcardState.crtTimer = null;
3587
3594
  }
3588
3595
  if (vcard && vcardState.crtHandler) {
@@ -3601,7 +3608,7 @@ function createVoiceCardController(env) {
3601
3608
  function finishCrtOff() {
3602
3609
  vcardState.closing = false;
3603
3610
  if (vcardState.crtTimer) {
3604
- clearTimeout(vcardState.crtTimer);
3611
+ env.timerScope.clear(vcardState.crtTimer);
3605
3612
  vcardState.crtTimer = null;
3606
3613
  }
3607
3614
  if (vcard && vcardState.crtHandler) {
@@ -3755,14 +3762,13 @@ function createVoiceCardController(env) {
3755
3762
  function startMicWatch() {
3756
3763
  if (vcardState.timer) return;
3757
3764
  if (!env.state.selection && !env.state.quoteSelection) return;
3758
- vcardState.timer = setInterval(micTick, MIC_POLL_MS);
3759
- env.timers.push(vcardState.timer);
3765
+ vcardState.timer = env.timerScope.interval(micTick, MIC_POLL_MS);
3760
3766
  micTick();
3761
3767
  }
3762
3768
  function stopMicWatch() {
3763
3769
  quoteVoiceHandled = false;
3764
3770
  if (!vcardState.timer) return;
3765
- clearInterval(vcardState.timer);
3771
+ env.timerScope.clear(vcardState.timer);
3766
3772
  vcardState.timer = null;
3767
3773
  }
3768
3774
  function micTick() {
@@ -3882,8 +3888,6 @@ function createQuotesController(env) {
3882
3888
  function addQuote(text, label, extra) {
3883
3889
  var clean = String(text || "").replace(/\u00a0/g, " ").trim();
3884
3890
  if (!clean) return false;
3885
- var truncated = clean.length > MAX_QUOTE_CHARS;
3886
- if (truncated) clean = clean.slice(0, MAX_QUOTE_CHARS) + "\u2026";
3887
3891
  for (var i = 0; i < env.state.quotes.length; i += 1) {
3888
3892
  if (env.state.quotes[i].text === clean) {
3889
3893
  env.setStatus("\u8FD9\u6BB5\u5DF2\u7ECF\u5728\u5F15\u7528\u91CC\u4E86");
@@ -3896,19 +3900,27 @@ function createQuotesController(env) {
3896
3900
  }
3897
3901
  quoteSeq += 1;
3898
3902
  var context = extra && typeof extra.context === "string" ? extra.context.trim() : "";
3899
- env.state.quotes.push({
3903
+ var quote = {
3900
3904
  id: quoteSeq,
3901
3905
  label: label || "\u5F15\u7528",
3902
3906
  text: clean,
3903
3907
  context,
3904
- session: !!(extra && extra.session === true)
3905
- });
3908
+ session: !!(extra && extra.session === true),
3909
+ sessionId: extra && extra.sessionId || env.currentSessionId(),
3910
+ contextJob: null
3911
+ };
3912
+ env.state.quotes.push(quote);
3913
+ if (quote.session && quote.sessionId) {
3914
+ quote.contextJob = fetchQuoteContext(quote.sessionId, quote.text).then(function(context2) {
3915
+ if (context2) quote.context = context2;
3916
+ });
3917
+ }
3906
3918
  renderQuotes();
3907
3919
  env.refreshAskState();
3908
3920
  env.askRow.style.display = "";
3909
3921
  if (env.composerVisible()) env.focusAskBox();
3910
3922
  env.setStatus(
3911
- "\u5DF2\u52A0\u5165\u5F15\u7528\uFF08" + quoteLabelOf(env.state.quotes[env.state.quotes.length - 1]) + " \xB7 " + clean.length + " \u5B57" + (truncated ? "\uFF08\u539F\u6587\u66F4\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09" : "") + (context ? " \xB7 \u5E26\u4E0A\u4E0B\u6587 " + context.length + " \u5B57" : "") + "\uFF09"
3923
+ "\u5DF2\u52A0\u5165\u5F15\u7528\uFF08" + quoteLabelOf(env.state.quotes[env.state.quotes.length - 1]) + " \xB7 " + clean.length + " \u5B57" + (context ? " \xB7 \u5E26\u4E0A\u4E0B\u6587 " + context.length + " \u5B57" : "") + "\uFF09"
3912
3924
  );
3913
3925
  return true;
3914
3926
  }
@@ -3973,6 +3985,22 @@ function createQuotesController(env) {
3973
3985
  function quoteContextFor(selection) {
3974
3986
  if (!selection) return { context: "", session: false };
3975
3987
  if (selection.source === "panel") {
3988
+ var index = selection.turnIndex;
3989
+ var through = env.state.contextState && env.state.contextState.through || 0;
3990
+ var visibleBefore = env.state.turns.slice(0, index).filter(function(t) {
3991
+ return !t.hidden;
3992
+ }).length;
3993
+ if (index >= 0 && visibleBefore < through) {
3994
+ var pieces = env.state.turns.slice(Math.max(0, index - 1), index + 2).filter(function(t) {
3995
+ return !t.hidden;
3996
+ }).map(function(t) {
3997
+ var text = t.role === "assistant" ? env.foldForHistory(t.text || "") : sentTextOf(t);
3998
+ var at = text.indexOf(selection.text);
3999
+ var start = at >= 0 ? Math.max(0, at - 500) : 0;
4000
+ return (t.role === "user" ? "\u7528\u6237\uFF1A" : "\u52A9\u624B\uFF1A") + text.slice(start, start + Math.max(1500, selection.text.length + 500));
4001
+ });
4002
+ return { context: pieces.join("\n"), session: false };
4003
+ }
3976
4004
  return { context: "", session: false };
3977
4005
  }
3978
4006
  if (selection.source === "iframe") {
@@ -3993,22 +4021,16 @@ function createQuotesController(env) {
3993
4021
  for (var i = 0; i < quotes.length; i += 1) {
3994
4022
  if (quotes[i].session === true) jobs.push(quotes[i]);
3995
4023
  }
3996
- var sessionId = env.currentSessionId();
3997
- if (jobs.length === 0 || !sessionId) return Promise.resolve(quotes);
3998
- return Promise.all(
3999
- jobs.map(function(quote) {
4000
- return fetchQuoteContext(sessionId, quote.text).then(function(context) {
4001
- if (context) quote.context = context;
4002
- return quote;
4003
- });
4004
- })
4005
- ).then(function() {
4024
+ if (jobs.length === 0) return Promise.resolve(quotes);
4025
+ return Promise.all(jobs.map(function(quote) {
4026
+ return quote.contextJob || Promise.resolve();
4027
+ })).then(function() {
4006
4028
  return quotes;
4007
4029
  });
4008
4030
  }
4009
4031
  function fetchQuoteContext(sessionId, text) {
4010
4032
  var controller = typeof AbortController === "function" ? new AbortController() : null;
4011
- var timer = setTimeout(function() {
4033
+ var timer = env.timerScope.later(function() {
4012
4034
  if (controller) {
4013
4035
  try {
4014
4036
  controller.abort();
@@ -4016,7 +4038,6 @@ function createQuotesController(env) {
4016
4038
  }
4017
4039
  }
4018
4040
  }, QUOTE_CONTEXT_TIMEOUT);
4019
- env.timers.push(timer);
4020
4041
  return fetch(QUOTE_CONTEXT_API, {
4021
4042
  method: "POST",
4022
4043
  headers: { "content-type": "application/json" },
@@ -4025,11 +4046,11 @@ function createQuotesController(env) {
4025
4046
  }).then(function(response) {
4026
4047
  return response.ok ? response.json() : null;
4027
4048
  }).then(function(data) {
4028
- clearTimeout(timer);
4049
+ env.timerScope.clear(timer);
4029
4050
  if (data && data.ok === true && data.matched === true && typeof data.context === "string") return data.context;
4030
4051
  return "";
4031
4052
  }).catch(function() {
4032
- clearTimeout(timer);
4053
+ env.timerScope.clear(timer);
4033
4054
  return "";
4034
4055
  });
4035
4056
  }
@@ -4071,6 +4092,7 @@ function createQuotesController(env) {
4071
4092
  out.push({
4072
4093
  role: turn.role,
4073
4094
  text: sentTextOf(turn),
4095
+ ...turn.contextExcluded === true ? { contextExcluded: true } : {},
4074
4096
  ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
4075
4097
  });
4076
4098
  }
@@ -4919,7 +4941,7 @@ function createPreviewController(env) {
4919
4941
  // src/client/panel.js
4920
4942
  function createPanelController(env) {
4921
4943
  function panelOrCurrentSessionId() {
4922
- return env.panelSessionId || currentSessionId();
4944
+ return env.state.contextState && typeof env.state.contextState.sessionId === "string" ? env.state.contextState.sessionId : env.panelSessionId || currentSessionId();
4923
4945
  }
4924
4946
  function currentSessionId() {
4925
4947
  try {
@@ -5015,8 +5037,9 @@ function createPanelController(env) {
5015
5037
  env.state.selection = null;
5016
5038
  }
5017
5039
  function openPanelWithInner(text, context, label, anchor, keyContext, options) {
5018
- env.state.voiceAsk = !!(options && options.askFirst);
5040
+ env.state.skipInterpretation = !!(options && options.askFirst);
5019
5041
  env.panelSessionId = currentSessionId();
5042
+ env.state.contextState = null;
5020
5043
  env.state.payload = buildPayload(text, context, label);
5021
5044
  env.state.kind = env.state.payload.kind;
5022
5045
  env.hideButton();
@@ -5050,6 +5073,7 @@ function createPanelController(env) {
5050
5073
  }
5051
5074
  env.renderTurns();
5052
5075
  if (cached) {
5076
+ env.state.skipInterpretation = false;
5053
5077
  env.state.turns = (cached.turns || []).slice();
5054
5078
  env.state.phase = "done";
5055
5079
  env.state.stage = "";
@@ -5281,12 +5305,12 @@ function createPanelController(env) {
5281
5305
  env.panel.style.transition = "transform " + PANEL_GROW_MS + "ms " + PANEL_GROW_EASE;
5282
5306
  env.panel.style.transform = "none";
5283
5307
  if (env.panelGrowTimer) {
5284
- clearTimeout(env.panelGrowTimer);
5308
+ env.timerScope.clear(env.panelGrowTimer);
5285
5309
  env.panelGrowTimer = null;
5286
5310
  }
5287
5311
  var settle = function() {
5288
5312
  if (env.panelGrowTimer) {
5289
- clearTimeout(env.panelGrowTimer);
5313
+ env.timerScope.clear(env.panelGrowTimer);
5290
5314
  env.panelGrowTimer = null;
5291
5315
  }
5292
5316
  if (env.panelGrowHandler) {
@@ -5309,7 +5333,7 @@ function createPanelController(env) {
5309
5333
  env.panelOpen = false;
5310
5334
  env.panel.style.display = "none";
5311
5335
  if (env.panelGrowTimer) {
5312
- clearTimeout(env.panelGrowTimer);
5336
+ env.timerScope.clear(env.panelGrowTimer);
5313
5337
  env.panelGrowTimer = null;
5314
5338
  }
5315
5339
  if (env.panelGrowHandler) {
@@ -5327,6 +5351,11 @@ function createPanelController(env) {
5327
5351
  if (env.state.request) env.state.aborted = true;
5328
5352
  if (env.state.request) env.state.request.abort();
5329
5353
  env.state.request = null;
5354
+ if (env.state.askRequest) {
5355
+ env.state.stopped = true;
5356
+ env.state.askRequest.abort();
5357
+ }
5358
+ releaseRenderedContent();
5330
5359
  env.paintPill();
5331
5360
  env.syncRouteWatch();
5332
5361
  }
@@ -5340,7 +5369,7 @@ function createPanelController(env) {
5340
5369
  for (var i = 0; i < env.state.turns.length; i += 1) {
5341
5370
  if (env.state.turns[i].role === "user") asked = true;
5342
5371
  }
5343
- env.expandButton.style.display = !hasDetail && translationReady && !asked && !env.state.voiceAsk ? "" : "none";
5372
+ env.expandButton.style.display = !hasDetail && translationReady && !asked && !env.state.skipInterpretation ? "" : "none";
5344
5373
  env.expandButton.disabled = false;
5345
5374
  return asked;
5346
5375
  }
@@ -5355,14 +5384,35 @@ function createPanelController(env) {
5355
5384
  var what = env.state.toolBusy ? "\u6B63\u5728\u68C0\u7D22\u8D44\u6599\u2026" : env.state.phase === "loading" ? "\u6B63\u5728\u5206\u6790\u2026" : "\u751F\u6210\u4E2D\u2026";
5356
5385
  return what + " " + seconds + "s";
5357
5386
  }
5387
+ function releaseRenderedContent() {
5388
+ env.clearWaiting();
5389
+ env.clearRenderedTurns();
5390
+ for (var content of [env.translationSection.content, env.detailSection.content]) {
5391
+ releasePreviewFrames(content);
5392
+ content.textContent = "";
5393
+ }
5394
+ }
5358
5395
  function paint() {
5396
+ if (!env.panelOpen) {
5397
+ releaseRenderedContent();
5398
+ env.paintPill();
5399
+ return;
5400
+ }
5359
5401
  var streaming = env.state.phase === "loading" || env.state.phase === "streaming";
5360
5402
  var stage = env.state.stage;
5361
5403
  var selected = env.state.payload && env.state.payload.text ? env.state.payload.text : env.state.selection ? env.state.selection.text : "";
5362
5404
  var sections = splitSections(env.visibleRaw());
5363
- env.translationSection.root.style.display = env.state.voiceAsk ? "none" : "";
5364
5405
  var live = stripEchoPrefix(stage === "detail" ? "" : sections.translation || sections.other, selected);
5365
5406
  var liveDetail = (sections.detail || sections.other || "").trim();
5407
+ var askedSoFar = false;
5408
+ for (var qi = 0; qi < env.state.turns.length; qi += 1) {
5409
+ if (env.state.turns[qi].role === "user") {
5410
+ askedSoFar = true;
5411
+ break;
5412
+ }
5413
+ }
5414
+ var emptyInterpretation = !env.state.parts.translation && !env.state.parts.detail && !live.trim();
5415
+ env.translationSection.root.style.display = env.state.skipInterpretation || emptyInterpretation && askedSoFar ? "none" : "";
5366
5416
  if (env.state.parts.translation) {
5367
5417
  renderRich(env.translationSection.content, stripEchoPrefix(env.state.parts.translation, selected), { callout: true, settled: true });
5368
5418
  } else if (stage !== "detail" && live.trim()) {
@@ -5423,7 +5473,7 @@ function createPanelController(env) {
5423
5473
  var surfaceTheme = env.currentSurfaceTheme();
5424
5474
  env.layer.setAttribute("data-theme", surfaceTheme);
5425
5475
  env.panel.setAttribute("data-theme", surfaceTheme);
5426
- env.askRow.style.display = wide || translationReady || env.state.voiceAsk || env.state.quotes.length > 0 ? "" : "none";
5476
+ env.askRow.style.display = wide || translationReady || env.state.skipInterpretation || env.state.quotes.length > 0 ? "" : "none";
5427
5477
  syncExpandCta();
5428
5478
  env.closeButton.title = "\u5173\u95ED\uFF08Esc\uFF09";
5429
5479
  if (env.state.phase !== "error" && env.state.phase !== "paused" && env.retryButton.parentNode) {
@@ -5546,6 +5596,7 @@ function createPanelController(env) {
5546
5596
  env.panel.style.top = Math.round(top) + "px";
5547
5597
  }
5548
5598
  return {
5599
+ releaseRenderedContent,
5549
5600
  openForSelection,
5550
5601
  currentSessionId,
5551
5602
  loadPanelSize,
@@ -6134,9 +6185,69 @@ function createPillController(env) {
6134
6185
  };
6135
6186
  }
6136
6187
 
6188
+ // src/chat-context.ts
6189
+ var CHAT_REQUEST_BYTES = 512 * 1024;
6190
+ var EMPTY_DIGEST = "811c9dc5";
6191
+ function readContextState(raw) {
6192
+ if (!raw || typeof raw !== "object") return null;
6193
+ const s = raw;
6194
+ if (s.version !== 1 || !Number.isSafeInteger(s.through) || s.through < 0 || !/^[a-f0-9]{8}$/.test(s.source) || !/^[a-f0-9]{8}$/.test(s.digest) || typeof s.summary !== "string" || s.summary.length > 12e3 || s.through > 0 && !s.summary.trim()) return null;
6195
+ if (s.background !== null && (!s.background || typeof s.background.transcript !== "string" || s.background.transcript.length > 2e5 || typeof s.background.marked !== "boolean")) return null;
6196
+ if (s.sessionId !== void 0 && (typeof s.sessionId !== "string" || s.sessionId.length > 500)) return null;
6197
+ return {
6198
+ version: 1,
6199
+ ...typeof s.sessionId === "string" ? { sessionId: s.sessionId } : {},
6200
+ source: s.source,
6201
+ through: s.through,
6202
+ digest: s.digest,
6203
+ summary: s.summary,
6204
+ background: s.background ? { transcript: s.background.transcript, marked: s.background.marked } : null
6205
+ };
6206
+ }
6207
+ function modelTurn(turn) {
6208
+ if (turn.contextExcluded) return {
6209
+ role: turn.role === "assistant" ? "assistant" : "user",
6210
+ text: turn.role === "assistant" ? "\uFF08\u8BE5\u8F6E\u672A\u751F\u6210\u56DE\u7B54\u3002\uFF09" : "\uFF08\u8BE5\u63D0\u95EE\u672A\u63D0\u4EA4\u7ED9\u6A21\u578B\uFF0C\u539F\u59CB\u5185\u5BB9\u4ECD\u4FDD\u5B58\u5728\u754C\u9762\u5386\u53F2\u4E2D\uFF1B\u9700\u8981\u65F6\u8BF7\u91CD\u65B0\u5F15\u7528\u5FC5\u8981\u90E8\u5206\u3002\uFF09"
6211
+ };
6212
+ return {
6213
+ role: turn.role === "assistant" ? "assistant" : "user",
6214
+ text: turn.sent || turn.text || "",
6215
+ ...turn.attachments?.length ? { attachments: turn.attachments } : {}
6216
+ };
6217
+ }
6218
+ function historyDigest(turns, initial = EMPTY_DIGEST) {
6219
+ let hash = Number.parseInt(initial, 16) >>> 0;
6220
+ for (const raw of turns) {
6221
+ const t = modelTurn(raw);
6222
+ const text = JSON.stringify([t.role, t.text, (t.attachments || []).map((a) => [a.kind, a.ref.attachmentId])]) + "\n";
6223
+ for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619) >>> 0;
6224
+ }
6225
+ return hash.toString(16).padStart(8, "0");
6226
+ }
6227
+ function contextSource(session, text, context, initial) {
6228
+ return historyDigest([{ role: "user", text: JSON.stringify([session, text, context, initial]) }]);
6229
+ }
6230
+ function prepareChatHistory(turns, rawState) {
6231
+ const visible = turns.filter((t) => !t.hidden);
6232
+ let state = readContextState(rawState);
6233
+ if (state && (state.through > visible.length || historyDigest(visible.slice(0, state.through)) !== state.digest)) state = null;
6234
+ const start = state?.through || 0;
6235
+ const materials = [];
6236
+ const seen = /* @__PURE__ */ new Set();
6237
+ visible.forEach((turn, index) => {
6238
+ for (const item of turn.attachments || []) {
6239
+ const key = item.kind + ":" + item.ref.attachmentId;
6240
+ if (!seen.has(key)) {
6241
+ seen.add(key);
6242
+ materials.push({ kind: item.kind, ref: item.ref, turn: index });
6243
+ }
6244
+ }
6245
+ });
6246
+ return { history: visible.slice(start).map(modelTurn), historyStart: start, contextState: state, materials };
6247
+ }
6248
+
6137
6249
  // src/client/history.js
6138
6250
  function createHistoryController(env) {
6139
- var FOLD_HISTORY_KEEP_LATEST = true;
6140
6251
  var foldCount = 0;
6141
6252
  var HTML_VOID_TAGS = {
6142
6253
  area: 1,
@@ -6417,16 +6528,6 @@ function createHistoryController(env) {
6417
6528
  src = src.replace(/```html[ \t]*\n([\s\S]*)$/, one);
6418
6529
  return src;
6419
6530
  }
6420
- function historyTextOf(turn, keepRaw) {
6421
- if (!turn) return "";
6422
- if (turn.role === "user") return env.sentTextOf(turn);
6423
- if (keepRaw) return turn.text;
6424
- if (typeof turn.foldedCache === "string") return turn.foldedCache;
6425
- if (turn.streaming === true) return turn.text;
6426
- var folded = foldForHistory(turn.text);
6427
- turn.foldedCache = folded;
6428
- return folded;
6429
- }
6430
6531
  function currentSurfaceTheme() {
6431
6532
  try {
6432
6533
  var bodyEl = document.body;
@@ -6455,6 +6556,7 @@ function createHistoryController(env) {
6455
6556
  function visibleRaw() {
6456
6557
  return env.state.raw;
6457
6558
  }
6559
+ var historySaveSequence = 0;
6458
6560
  function saveHistory(options) {
6459
6561
  var payload = env.state.payload;
6460
6562
  if (!payload || !env.state.cacheKey) return;
@@ -6465,6 +6567,22 @@ function createHistoryController(env) {
6465
6567
  if (turn.hidden === true) continue;
6466
6568
  turns.push(exported[i]);
6467
6569
  }
6570
+ var owner = env.state.turns;
6571
+ var ownerKey = env.state.cacheKey;
6572
+ var sequence = ++historySaveSequence;
6573
+ function noteSaved(error) {
6574
+ if (sequence !== historySaveSequence || env.state.turns !== owner || env.state.cacheKey !== ownerKey) return;
6575
+ var last = owner[owner.length - 1];
6576
+ if (!last || last.role !== "assistant") return;
6577
+ if (error) {
6578
+ last.notice = "\u5386\u53F2\u8BB0\u5F55\u4FDD\u5B58\u5931\u8D25\uFF0C\u5F53\u524D\u539F\u6587\u4ECD\u5728\u5C0F\u7A97\u4E2D\uFF1B\u5237\u65B0\u6216\u9000\u51FA\u524D\u8BF7\u5148\u590D\u5236\u91CD\u8981\u5185\u5BB9\u3002";
6579
+ last.noticeCode = "history-save-failed";
6580
+ } else if (last.noticeCode === "history-save-failed") {
6581
+ last.notice = "";
6582
+ last.noticeCode = "";
6583
+ } else return;
6584
+ renderTurns();
6585
+ }
6468
6586
  try {
6469
6587
  fetch(HISTORY, {
6470
6588
  method: "POST",
@@ -6477,16 +6595,40 @@ function createHistoryController(env) {
6477
6595
  parts: env.state.parts,
6478
6596
  turns,
6479
6597
  toolDigest: env.state.toolDigest,
6480
- pinned: !!(options && options.pinned)
6598
+ contextState: env.state.contextState || null,
6599
+ pinned: !!(options && options.pinned),
6600
+ // 这段对话有没有解读,必须跟着条目落盘:回放时靠它决定画不画「翻译」一节。
6601
+ // 不落盘的话,语音卡那条"跳过翻译与详解、直接追问"的对话下次开出来,
6602
+ // 会顶着一张空翻译卡片("这次没有返回内容")——用户实测。
6603
+ skipped: env.state.skipInterpretation === true
6481
6604
  })
6605
+ }).then(function(response) {
6606
+ if (!response.ok) throw new Error("HTTP " + response.status);
6607
+ return response.json();
6608
+ }).then(function(result) {
6609
+ noteSaved(!result || result.ok !== true);
6482
6610
  }).catch(function() {
6611
+ noteSaved(true);
6483
6612
  });
6484
6613
  } catch (error) {
6614
+ noteSaved(true);
6485
6615
  }
6486
6616
  }
6487
6617
  function applyHistoryEntry(entry, source) {
6488
6618
  var parts = entry.parts || {};
6489
6619
  env.state.parts = { translation: parts.translation || "", detail: parts.detail || "" };
6620
+ var replayTurns = Array.isArray(entry.turns) ? entry.turns : [];
6621
+ var hasAskTurn = false;
6622
+ for (var step = 0; step < replayTurns.length; step += 1) {
6623
+ if (replayTurns[step] && replayTurns[step].role === "user") {
6624
+ hasAskTurn = true;
6625
+ break;
6626
+ }
6627
+ }
6628
+ var noInterpretation = !String(parts.translation || "").trim() && !String(parts.detail || "").trim();
6629
+ env.state.skipInterpretation = entry.skipped === true || entry.skipped === void 0 && noInterpretation && hasAskTurn;
6630
+ env.state.contextState = readContextState(entry.contextState);
6631
+ if (env.state.contextState && typeof env.state.contextState.sessionId === "string") env.panelSessionId = env.state.contextState.sessionId;
6490
6632
  env.state.turns = Array.isArray(entry.turns) ? entry.turns.slice() : [];
6491
6633
  env.clearQuotes();
6492
6634
  env.state.toolDigest = typeof entry.toolDigest === "string" ? entry.toolDigest : "";
@@ -6518,8 +6660,7 @@ function createHistoryController(env) {
6518
6660
  settled = true;
6519
6661
  if (env.state.cacheKey === cacheKey && onMiss) onMiss();
6520
6662
  }
6521
- var timer = setTimeout(miss, 1500);
6522
- env.timers.push(timer);
6663
+ var timer = env.timerScope.later(miss, 1500);
6523
6664
  try {
6524
6665
  fetch(HISTORY + "?key=" + encodeURIComponent(cacheKey)).then(function(response) {
6525
6666
  return response.json();
@@ -6531,7 +6672,7 @@ function createHistoryController(env) {
6531
6672
  }
6532
6673
  if (data && data.ok === true && data.entry) {
6533
6674
  settled = true;
6534
- clearTimeout(timer);
6675
+ env.timerScope.clear(timer);
6535
6676
  applyHistoryEntry(data.entry, "history");
6536
6677
  return;
6537
6678
  }
@@ -6686,7 +6827,7 @@ function createHistoryController(env) {
6686
6827
  });
6687
6828
  bar.appendChild(undo);
6688
6829
  env.historyList.appendChild(bar);
6689
- setTimeout(function() {
6830
+ env.timerScope.later(function() {
6690
6831
  if (bar.parentNode) bar.parentNode.removeChild(bar);
6691
6832
  }, 5e3);
6692
6833
  }
@@ -6818,24 +6959,81 @@ function createHistoryController(env) {
6818
6959
  if (Date.now() - stickAt < 150) return;
6819
6960
  env.state.follow = nearBottom();
6820
6961
  }
6962
+ var turnWindowSize = 40;
6963
+ var turnWindowKey = null;
6964
+ var renderedTurns = [];
6965
+ function clearRenderedTurns() {
6966
+ env.clearWaiting(env.chatLog);
6967
+ releasePreviewFrames(env.chatLog);
6968
+ env.chatLog.textContent = "";
6969
+ renderedTurns = [];
6970
+ turnWindowSize = 40;
6971
+ turnWindowKey = null;
6972
+ }
6821
6973
  function renderTurns(options) {
6974
+ if (!env.panelOpen) {
6975
+ clearRenderedTurns();
6976
+ return;
6977
+ }
6822
6978
  var force = !!(options && options.stick === true);
6823
- var stick = force || nearBottom();
6979
+ var stick = !(options && options.preservePosition) && (force || nearBottom());
6824
6980
  var visible = [];
6825
6981
  for (var i = 0; i < env.state.turns.length; i++) {
6826
6982
  if (env.state.turns[i].hidden !== true) visible.push(env.state.turns[i]);
6827
6983
  }
6828
- if (env.chatLog.children.length !== visible.length) {
6984
+ if (turnWindowKey !== env.state.cacheKey) {
6985
+ turnWindowKey = env.state.cacheKey;
6986
+ turnWindowSize = 40;
6987
+ }
6988
+ var earlier = Math.max(0, visible.length - turnWindowSize);
6989
+ visible = visible.slice(earlier);
6990
+ var sameWindow = renderedTurns.length === visible.length && visible.every(function(turn2, index) {
6991
+ return renderedTurns[index] === turn2;
6992
+ });
6993
+ var prependCount = visible.length - renderedTurns.length;
6994
+ var canPrepend = options && options.prepend && renderedTurns.length > 0 && prependCount > 0 && renderedTurns.every(function(turn2, index) {
6995
+ return visible[prependCount + index] === turn2;
6996
+ });
6997
+ if (canPrepend) {
6998
+ var moreButton = env.chatLog.children[0];
6999
+ var firstBubble = env.chatLog.children[1];
7000
+ if (earlier > 0) moreButton.textContent = "\u663E\u793A\u66F4\u65E9\u7684\u6D88\u606F\uFF08\u8FD8\u6709 " + earlier + " \u6761\uFF09";
7001
+ else env.chatLog.removeChild(moreButton);
7002
+ for (var p = 0; p < prependCount; p++) {
7003
+ var prepended = bubbleFor(visible[p]);
7004
+ prepended.__turn = visible[p];
7005
+ env.chatLog.insertBefore(prepended, firstBubble);
7006
+ }
7007
+ renderedTurns = visible.slice();
7008
+ } else if (!sameWindow || env.chatLog.children.length !== visible.length + (earlier > 0 ? 1 : 0)) {
6829
7009
  env.clearWaiting(env.chatLog);
7010
+ releasePreviewFrames(env.chatLog);
6830
7011
  env.chatLog.textContent = "";
7012
+ if (earlier > 0) {
7013
+ var more = el("button", "dsh-sel-expand", "\u663E\u793A\u66F4\u65E9\u7684\u6D88\u606F\uFF08\u8FD8\u6709 " + earlier + " \u6761\uFF09");
7014
+ more.type = "button";
7015
+ more.setAttribute("data-history-more", "1");
7016
+ listen(more, "click", function(event) {
7017
+ event.preventDefault();
7018
+ event.stopPropagation();
7019
+ var top = env.body.scrollTop;
7020
+ var height = env.body.scrollHeight;
7021
+ turnWindowSize += 40;
7022
+ renderTurns({ preservePosition: true, prepend: true });
7023
+ env.body.scrollTop = top + Math.max(0, env.body.scrollHeight - height);
7024
+ env.state.follow = false;
7025
+ });
7026
+ env.chatLog.appendChild(more);
7027
+ }
6831
7028
  for (var j = 0; j < visible.length; j++) {
6832
7029
  var bubble = bubbleFor(visible[j]);
6833
7030
  bubble.__turn = visible[j];
6834
7031
  env.chatLog.appendChild(bubble);
6835
7032
  }
7033
+ renderedTurns = visible.slice();
6836
7034
  var fresh = visible[visible.length - 1];
6837
7035
  if (fresh && fresh.role === "assistant" && fresh.asking === true && !fresh.text) {
6838
- env.showWaiting(env.chatLog.children[visible.length - 1], "\u6B63\u5728\u56DE\u7B54\u2026", {
7036
+ env.showWaiting(env.chatLog.children[env.chatLog.children.length - 1], "\u6B63\u5728\u56DE\u7B54\u2026", {
6839
7037
  startedAt: fresh.startedAt,
6840
7038
  effort: fresh.effort,
6841
7039
  effortOf: function() {
@@ -6847,7 +7045,7 @@ function createHistoryController(env) {
6847
7045
  });
6848
7046
  }
6849
7047
  } else if (visible.length > 0) {
6850
- var node = env.chatLog.children[visible.length - 1];
7048
+ var node = env.chatLog.children[env.chatLog.children.length - 1];
6851
7049
  var turn = visible[visible.length - 1];
6852
7050
  var painted = false;
6853
7051
  node.className = "dsh-sel-bubble " + (turn.role === "user" ? "dsh-sel-bubble-user" : "dsh-sel-bubble-bot") + (turn.error ? " dsh-sel-bubble-err" : "");
@@ -7030,6 +7228,7 @@ function createHistoryController(env) {
7030
7228
  return seed.length > 1500 ? seed.slice(0, 1500) + "\u2026" : seed;
7031
7229
  }
7032
7230
  return {
7231
+ clearRenderedTurns,
7033
7232
  hideHistoryList,
7034
7233
  placeHistoryList,
7035
7234
  toggleHistoryList,
@@ -7062,13 +7261,6 @@ function createHistoryController(env) {
7062
7261
  loadHistoryEntry,
7063
7262
  seedHistoryFromExplanation,
7064
7263
  ensureChatFollow,
7065
- get FOLD_HISTORY_KEEP_LATEST() {
7066
- return FOLD_HISTORY_KEEP_LATEST;
7067
- },
7068
- set FOLD_HISTORY_KEEP_LATEST(value) {
7069
- FOLD_HISTORY_KEEP_LATEST = value;
7070
- },
7071
- historyTextOf,
7072
7264
  clearTransientNotice,
7073
7265
  saveHistory,
7074
7266
  applyToolEvent,
@@ -7189,7 +7381,8 @@ function createConversationController(env) {
7189
7381
  if (env.state.turns.length === 0 && (env.state.raw.trim() || env.state.parts.translation || env.state.parts.detail)) {
7190
7382
  env.state.turns.push({ role: "assistant", text: env.seedHistoryFromExplanation(), seed: true, hidden: true });
7191
7383
  }
7192
- env.state.turns.push({ role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent, attachments: withAttachments });
7384
+ var questionTurn = { role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent, attachments: withAttachments };
7385
+ env.state.turns.push(questionTurn);
7193
7386
  var answerTurn = {
7194
7387
  role: "assistant",
7195
7388
  text: "",
@@ -7211,23 +7404,22 @@ function createConversationController(env) {
7211
7404
  if (env.state.sessionOpen !== void 0) {
7212
7405
  }
7213
7406
  var startedAt = Date.now();
7214
- var timer = setInterval(function() {
7407
+ var timer = env.timerScope.interval(function() {
7215
7408
  env.paintPill();
7216
7409
  env.setStatus(
7217
7410
  (env.state.toolBusy ? "\u6B63\u5728\u68C0\u7D22\u8D44\u6599\u2026 " : "\u8FFD\u95EE\u4E2D\u2026 ") + ((Date.now() - startedAt) / 1e3).toFixed(1) + "s"
7218
7411
  );
7219
7412
  updateWaitClocks();
7220
7413
  }, 200);
7221
- env.timers.push(timer);
7222
7414
  var historyTurns = env.state.turns.slice(0, env.state.turns.length - 2);
7223
- var history = historyTurns.map(function(turn, index) {
7224
- var keepRaw = env.FOLD_HISTORY_KEEP_LATEST && index === historyTurns.length - 1;
7225
- return {
7226
- role: turn.role,
7227
- text: env.historyTextOf(turn, keepRaw),
7228
- ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
7229
- };
7230
- });
7415
+ var initialExplanation = env.state.skipInterpretation ? "" : env.seedHistoryFromExplanation();
7416
+ var source = contextSource(env.panelOrCurrentSessionId(), payload.text.trim(), payload.context, initialExplanation);
7417
+ if (env.state.contextState && env.state.contextState.source !== source) env.state.contextState = null;
7418
+ var prepared = prepareChatHistory(historyTurns, env.state.contextState);
7419
+ env.state.contextState = prepared.contextState;
7420
+ var history = prepared.history;
7421
+ var requestTurns = env.state.turns;
7422
+ var requestKey = env.state.cacheKey;
7231
7423
  env.state.stopped = false;
7232
7424
  var askController = new AbortController();
7233
7425
  env.state.askRequest = {
@@ -7239,7 +7431,7 @@ function createConversationController(env) {
7239
7431
  }
7240
7432
  };
7241
7433
  function finish(error) {
7242
- clearInterval(timer);
7434
+ env.timerScope.clear(timer);
7243
7435
  env.state.asking = false;
7244
7436
  env.state.toolBusy = false;
7245
7437
  env.clearTransientNotice(answerTurn);
@@ -7263,24 +7455,36 @@ function createConversationController(env) {
7263
7455
  );
7264
7456
  env.askFocusAfterTurn();
7265
7457
  }
7458
+ var requestBody = JSON.stringify({
7459
+ text: payload.text,
7460
+ context: payload.context,
7461
+ label: payload.label,
7462
+ sessionId: env.panelOrCurrentSessionId(),
7463
+ question: sent,
7464
+ history,
7465
+ key: requestKey,
7466
+ historyStart: prepared.historyStart,
7467
+ contextState: prepared.contextState,
7468
+ materials: prepared.materials,
7469
+ initialExplanation,
7470
+ // 附件只带**引用**(图片/文件都已经在 host 的附件服务里),body 不会因为一张截图胖几 MB
7471
+ ...withAttachments.length > 0 ? { attachments: withAttachments } : {},
7472
+ // 网页输出要跟着当前深浅走:深色 Harness 里给一张白底页面会很刺眼
7473
+ ...env.state.webAnswer ? { webAnswer: true, theme: env.currentSurfaceTheme() } : {},
7474
+ ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {},
7475
+ ...env.state.effort ? { effort: env.state.effort } : {},
7476
+ ...env.state.toolDigest ? { toolDigest: env.state.toolDigest } : {}
7477
+ });
7478
+ if (new TextEncoder().encode(requestBody).byteLength > CHAT_REQUEST_BYTES) {
7479
+ questionTurn.contextExcluded = true;
7480
+ answerTurn.contextExcluded = true;
7481
+ finish("\u672C\u8F6E\u6750\u6599\u8D85\u8FC7\u8BF7\u6C42\u5BB9\u91CF\uFF0C\u8BF7\u7F29\u5C0F\u63D0\u95EE\u6216\u5F15\u7528\u8303\u56F4\uFF1B\u539F\u6587\u4ECD\u4FDD\u7559\u5728\u5F53\u524D\u5386\u53F2\u4E2D");
7482
+ return;
7483
+ }
7266
7484
  fetch(API, {
7267
7485
  method: "POST",
7268
7486
  headers: { "content-type": "application/json" },
7269
- body: JSON.stringify({
7270
- text: payload.text,
7271
- context: payload.context,
7272
- label: payload.label,
7273
- sessionId: env.panelOrCurrentSessionId(),
7274
- question: sent,
7275
- history,
7276
- // 附件只带**引用**(图片/文件都已经在 host 的附件服务里),body 不会因为一张截图胖几 MB
7277
- ...withAttachments.length > 0 ? { attachments: withAttachments } : {},
7278
- // 网页输出要跟着当前深浅走:深色 Harness 里给一张白底页面会很刺眼
7279
- ...env.state.webAnswer ? { webAnswer: true, theme: env.currentSurfaceTheme() } : {},
7280
- ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {},
7281
- ...env.state.effort ? { effort: env.state.effort } : {},
7282
- ...env.state.toolDigest ? { toolDigest: env.state.toolDigest } : {}
7283
- }),
7487
+ body: requestBody,
7284
7488
  signal: askController.signal
7285
7489
  }).then(function(response) {
7286
7490
  if (!response.ok) {
@@ -7306,7 +7510,18 @@ function createConversationController(env) {
7306
7510
  } catch (error) {
7307
7511
  continue;
7308
7512
  }
7309
- if (message.type === "delta") {
7513
+ if (message.type === "context-state") {
7514
+ if (!askController.signal.aborted && env.state.turns === requestTurns && env.state.cacheKey === requestKey) {
7515
+ env.state.contextState = readContextState(message.state);
7516
+ }
7517
+ } else if (message.type === "material-restored") {
7518
+ var material = message.attachment;
7519
+ if (!askController.signal.aborted && material && (material.kind === "image" || material.kind === "file") && material.ref && material.ref.attachmentId) {
7520
+ if (!questionTurn.attachments.some(function(a) {
7521
+ return a.kind === material.kind && a.ref.attachmentId === material.ref.attachmentId;
7522
+ })) questionTurn.attachments.push(material);
7523
+ }
7524
+ } else if (message.type === "delta") {
7310
7525
  env.clearTransientNotice(answerTurn);
7311
7526
  if (!answerTurn.text.trim()) answerTurn.asking = false;
7312
7527
  answerTurn.text += message.text;
@@ -7345,6 +7560,10 @@ function createConversationController(env) {
7345
7560
  }
7346
7561
  scheduleTurnsRender();
7347
7562
  } else if (message.type === "error") {
7563
+ if (message.code === "context-rejected") {
7564
+ questionTurn.contextExcluded = true;
7565
+ answerTurn.contextExcluded = true;
7566
+ }
7348
7567
  finish(message.message || "\u6A21\u578B\u8FD4\u56DE\u9519\u8BEF");
7349
7568
  }
7350
7569
  }
@@ -7502,16 +7721,15 @@ function createConversationController(env) {
7502
7721
  env.state.startedAt = Date.now();
7503
7722
  env.state.elapsed = 0;
7504
7723
  env.paint();
7505
- var ticker = setInterval(function() {
7724
+ var ticker = env.timerScope.interval(function() {
7506
7725
  env.paintPill();
7507
7726
  if (env.state.phase === "loading" || env.state.phase === "streaming") {
7508
7727
  env.setStatus(env.progressText());
7509
7728
  updateWaitClocks();
7510
7729
  }
7511
7730
  }, 200);
7512
- env.timers.push(ticker);
7513
7731
  function stopTicker() {
7514
- clearInterval(ticker);
7732
+ env.timerScope.clear(ticker);
7515
7733
  }
7516
7734
  function fail(message) {
7517
7735
  if (env.state.phase === "done") return;
@@ -9100,6 +9318,101 @@ function createVoiceController(env) {
9100
9318
  };
9101
9319
  }
9102
9320
 
9321
+ // src/client/memory.js
9322
+ function createResultCache(maxEntries = 20, maxBytes = 8 * 1024 * 1024) {
9323
+ const entries = /* @__PURE__ */ new Map();
9324
+ let textBytes = 0;
9325
+ let disposed = false;
9326
+ function remove(key) {
9327
+ const entry = entries.get(key);
9328
+ if (!entry) return;
9329
+ textBytes -= entry.bytes;
9330
+ entries.delete(key);
9331
+ }
9332
+ const cache = {
9333
+ get size() {
9334
+ return entries.size;
9335
+ },
9336
+ // Conservative UTF-16 size of serialized text, not a measurement of JS heap size.
9337
+ get textBytes() {
9338
+ return textBytes;
9339
+ },
9340
+ keys() {
9341
+ return entries.keys();
9342
+ },
9343
+ get(key) {
9344
+ const entry = entries.get(key);
9345
+ if (!entry) return void 0;
9346
+ entries.delete(key);
9347
+ entries.set(key, entry);
9348
+ return JSON.parse(entry.json);
9349
+ },
9350
+ set(key, value) {
9351
+ if (disposed) return cache;
9352
+ remove(key);
9353
+ let json;
9354
+ try {
9355
+ json = JSON.stringify(value);
9356
+ } catch {
9357
+ return cache;
9358
+ }
9359
+ if (typeof json !== "string") return cache;
9360
+ const bytes = (String(key).length + json.length) * 2;
9361
+ if (bytes > maxBytes || maxEntries <= 0) return cache;
9362
+ entries.set(key, { json, bytes });
9363
+ textBytes += bytes;
9364
+ while (entries.size > maxEntries || textBytes > maxBytes) remove(entries.keys().next().value);
9365
+ return cache;
9366
+ },
9367
+ clear() {
9368
+ entries.clear();
9369
+ textBytes = 0;
9370
+ },
9371
+ dispose() {
9372
+ disposed = true;
9373
+ cache.clear();
9374
+ }
9375
+ };
9376
+ return cache;
9377
+ }
9378
+ function createTimerScope() {
9379
+ const active = /* @__PURE__ */ new Set();
9380
+ let disposed = false;
9381
+ return {
9382
+ get size() {
9383
+ return active.size;
9384
+ },
9385
+ later(fn, ms) {
9386
+ if (disposed) return null;
9387
+ const id = setTimeout(() => {
9388
+ active.delete(id);
9389
+ if (!disposed) fn();
9390
+ }, ms);
9391
+ active.add(id);
9392
+ return id;
9393
+ },
9394
+ interval(fn, ms) {
9395
+ if (disposed) return null;
9396
+ const id = setInterval(fn, ms);
9397
+ active.add(id);
9398
+ return id;
9399
+ },
9400
+ clear(id) {
9401
+ clearTimeout(id);
9402
+ clearInterval(id);
9403
+ active.delete(id);
9404
+ },
9405
+ dispose() {
9406
+ disposed = true;
9407
+ active.forEach((id) => {
9408
+ clearTimeout(id);
9409
+ clearInterval(id);
9410
+ });
9411
+ active.clear();
9412
+ }
9413
+ };
9414
+ }
9415
+
9103
9416
  // src/client/index.js
9104
9417
  function apply(ctx) {
9105
9418
  var state = {
@@ -9148,6 +9461,14 @@ function apply(ctx) {
9148
9461
  payload: null,
9149
9462
  /** 已生成的两节内容(分阶段产出)。 */
9150
9463
  parts: { translation: "", detail: "" },
9464
+ /**
9465
+ * 这段对话**没有解读**(语音卡那条"跳过翻译与详解、直接追问"的路):
9466
+ * 翻译节整节不画,追问输入框直接可见。
9467
+ *
9468
+ * 语义单位是"一段对话"而不是"一次开窗":落盘时写进历史条目的 skipped,
9469
+ * 回放/缓存命中时按它复原(否则同一个词再开一次,空翻译卡片又冒出来 —— 用户实测)。
9470
+ */
9471
+ skipInterpretation: false,
9151
9472
  /** 当前阶段:'translation' | 'detail' | 'chat'。 */
9152
9473
  stage: "",
9153
9474
  /** 追问轮次:[{ role: 'user' | 'assistant', text }],只活在面板/内存里(临时)。 */
@@ -9204,7 +9525,8 @@ function apply(ctx) {
9204
9525
  /** 升格请求进行中。 */
9205
9526
  promoting: false
9206
9527
  };
9207
- var cache = /* @__PURE__ */ new Map();
9528
+ var cache = createResultCache();
9529
+ var timerScope = createTimerScope();
9208
9530
  var styleEl = el("style");
9209
9531
  styleEl.setAttribute("data-plugin", "@yfwu2020/dsh-selection-explain");
9210
9532
  styleEl.textContent = CSS;
@@ -9639,8 +9961,8 @@ function apply(ctx) {
9639
9961
  get frameTouchedAt() {
9640
9962
  return previewController.frameTouchedAt;
9641
9963
  },
9642
- get timers() {
9643
- return timers;
9964
+ get timerScope() {
9965
+ return timerScope;
9644
9966
  },
9645
9967
  get voice() {
9646
9968
  return voiceController.voice;
@@ -9665,6 +9987,9 @@ function apply(ctx) {
9665
9987
  }
9666
9988
  });
9667
9989
  var quotesController = createQuotesController({
9990
+ get foldForHistory() {
9991
+ return historyController.foldForHistory;
9992
+ },
9668
9993
  get state() {
9669
9994
  return state;
9670
9995
  },
@@ -9698,8 +10023,8 @@ function apply(ctx) {
9698
10023
  get currentSessionId() {
9699
10024
  return panelController.currentSessionId;
9700
10025
  },
9701
- get timers() {
9702
- return timers;
10026
+ get timerScope() {
10027
+ return timerScope;
9703
10028
  }
9704
10029
  });
9705
10030
  var previewController = createPreviewController({
@@ -9720,6 +10045,9 @@ function apply(ctx) {
9720
10045
  }
9721
10046
  });
9722
10047
  var panelController = createPanelController({
10048
+ get timerScope() {
10049
+ return timerScope;
10050
+ },
9723
10051
  get panelSessionId() {
9724
10052
  return panelSessionId;
9725
10053
  },
@@ -9756,6 +10084,12 @@ function apply(ctx) {
9756
10084
  get renderTurns() {
9757
10085
  return historyController.renderTurns;
9758
10086
  },
10087
+ get clearRenderedTurns() {
10088
+ return historyController.clearRenderedTurns;
10089
+ },
10090
+ get clearWaiting() {
10091
+ return conversationController.clearWaiting;
10092
+ },
9759
10093
  get sendAsk() {
9760
10094
  return conversationController.sendAsk;
9761
10095
  },
@@ -10015,6 +10349,9 @@ function apply(ctx) {
10015
10349
  }
10016
10350
  });
10017
10351
  var historyController = createHistoryController({
10352
+ set panelSessionId(value) {
10353
+ panelSessionId = value;
10354
+ },
10018
10355
  get sentTextOf() {
10019
10356
  return quotesController.sentTextOf;
10020
10357
  },
@@ -10045,8 +10382,8 @@ function apply(ctx) {
10045
10382
  get paint() {
10046
10383
  return panelController.paint;
10047
10384
  },
10048
- get timers() {
10049
- return timers;
10385
+ get timerScope() {
10386
+ return timerScope;
10050
10387
  },
10051
10388
  get panelOpen() {
10052
10389
  return panelOpen;
@@ -10176,14 +10513,8 @@ function apply(ctx) {
10176
10513
  get paintPill() {
10177
10514
  return pillController.paintPill;
10178
10515
  },
10179
- get timers() {
10180
- return timers;
10181
- },
10182
- get FOLD_HISTORY_KEEP_LATEST() {
10183
- return historyController.FOLD_HISTORY_KEEP_LATEST;
10184
- },
10185
- get historyTextOf() {
10186
- return historyController.historyTextOf;
10516
+ get timerScope() {
10517
+ return timerScope;
10187
10518
  },
10188
10519
  get clearTransientNotice() {
10189
10520
  return historyController.clearTransientNotice;
@@ -10457,7 +10788,6 @@ function apply(ctx) {
10457
10788
  var STREAM_RENDER_MS = 90;
10458
10789
  var turnsRenderPending = false;
10459
10790
  var lastTurnsRenderAt = 0;
10460
- var timers = [];
10461
10791
  var PILL_POLL_MIN = 2e3;
10462
10792
  var PILL_POLL_MAX = 6e4;
10463
10793
  var pillPollDelay = PILL_POLL_MIN;
@@ -10477,9 +10807,7 @@ function apply(ctx) {
10477
10807
  var pillObserver = null;
10478
10808
  var waitList = [];
10479
10809
  function later(fn, ms) {
10480
- var id = setTimeout(fn, ms);
10481
- timers.push(id);
10482
- return id;
10810
+ return timerScope.later(fn, ms);
10483
10811
  }
10484
10812
  var offBridgeMessage = listen(window, "message", function(event) {
10485
10813
  var data = event && event.data;
@@ -11049,6 +11377,14 @@ function apply(ctx) {
11049
11377
  turns: function() {
11050
11378
  return state.turns.slice();
11051
11379
  },
11380
+ memory: function() {
11381
+ return {
11382
+ cacheEntries: cache.size,
11383
+ cacheTextBytes: cache.textBytes,
11384
+ timers: timerScope.size,
11385
+ previewFrames: Object.keys(previewFrames).length
11386
+ };
11387
+ },
11052
11388
  /** 自检用:当前待发送的引用(引用区里那几张卡片)。 */
11053
11389
  quotes: function() {
11054
11390
  return state.quotes.map(function(quote2) {
@@ -11393,6 +11729,7 @@ function apply(ctx) {
11393
11729
  state.selection = null;
11394
11730
  state.bridgeActive = false;
11395
11731
  state.parts = { translation: "", detail: "" };
11732
+ state.contextState = null;
11396
11733
  state.turns = [];
11397
11734
  quotesController.clearQuotes();
11398
11735
  state.raw = "";
@@ -11420,6 +11757,13 @@ function apply(ctx) {
11420
11757
  console.log("[dsh-selection-explain] \u5212\u8BCD\u89E3\u8BFB\u5DF2\u5C31\u7EEA\uFF08\u9009\u4E2D\u6587\u5B57\u8BD5\u8BD5\uFF09");
11421
11758
  }
11422
11759
  return function() {
11760
+ panelOpen = false;
11761
+ timerScope.dispose();
11762
+ cache.dispose();
11763
+ voiceCardController.closeVCard(false);
11764
+ attachController.clear();
11765
+ panelController.releaseRenderedContent();
11766
+ if (state.askRequest) state.askRequest.abort();
11423
11767
  offPopEnd();
11424
11768
  offQuotePopEnd();
11425
11769
  offMouseUp();
@@ -11507,13 +11851,17 @@ function apply(ctx) {
11507
11851
  }
11508
11852
  });
11509
11853
  previewController.bridged.clear();
11854
+ previewController.frameTouchedAt.clear();
11510
11855
  state.turns = [];
11511
11856
  state.quotes = [];
11857
+ state.selection = null;
11858
+ state.quoteSelection = null;
11859
+ state.payload = null;
11860
+ state.parts = { translation: "", detail: "" };
11861
+ state.raw = "";
11512
11862
  if (state.request) state.request.abort();
11513
- for (var i = 0; i < timers.length; i++) clearTimeout(timers[i]);
11514
11863
  if (pillIdleId) clearTimeout(pillIdleId);
11515
11864
  if (pillController.pillStarTimer) clearTimeout(pillController.pillStarTimer);
11516
- cache.clear();
11517
11865
  if (layer.parentNode) layer.parentNode.removeChild(layer);
11518
11866
  if (button.parentNode) button.parentNode.removeChild(button);
11519
11867
  if (quoteButton.parentNode) quoteButton.parentNode.removeChild(quoteButton);