dsh-edit-turn 0.5.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -1
- package/lib/client.js +837 -134
- package/lib/index.js +158 -19
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -45,7 +45,7 @@ window.__ModuleLoader__.load({
|
|
|
45
45
|
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
46
46
|
|
|
47
47
|
/** Keep in sync with package.json and lib/index.js. */
|
|
48
|
-
const PLUGIN_VERSION = '0.5.
|
|
48
|
+
const PLUGIN_VERSION = '0.5.3'
|
|
49
49
|
|
|
50
50
|
// --- copy -----------------------------------------------------------------
|
|
51
51
|
|
|
@@ -78,9 +78,9 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
'config.summary.height.max': '一轮最大',
|
|
79
79
|
'config.summary.height.content': '随内容',
|
|
80
80
|
'config.summary.height.fill': '全部铺满',
|
|
81
|
-
'config.hint.height.max': '
|
|
82
|
-
'config.hint.height.content': '
|
|
83
|
-
'config.hint.height.fill': '
|
|
81
|
+
'config.hint.height.max': '打开时取这一轮所有思维链/正文里的最大高度;编辑中随你打字长高、删字变矮,但不会比打开时更矮。',
|
|
82
|
+
'config.hint.height.content': '打开时框高跟着当前块走;编辑中随你打字长高、删字变矮,但不会比打开时更矮。',
|
|
83
|
+
'config.hint.height.fill': '普通块打开时按一轮最大,编辑中随打字长高、删字变矮(不会比打开时更矮);选「全部」时铺满对话区到输入框之间的整段高度,只有一个滚动条。',
|
|
84
84
|
'placement.overlay': '浮层',
|
|
85
85
|
'placement.inline': '页面内联',
|
|
86
86
|
'placement.legacy': '整行旧版',
|
|
@@ -103,7 +103,6 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
'editor.block.reasoning': '思维链',
|
|
104
104
|
'editor.placeholder-block': '只改这一块;工具调用、其余内容和后续对话保持不变。',
|
|
105
105
|
'confirm.body-block': '确认后只替换这一块,工具调用、其他内容和后续对话保持不变。原始会话日志不会改写。',
|
|
106
|
-
'editor.patch-note': '思维链已改写(模型可见)',
|
|
107
106
|
'editor.review': '确认执行',
|
|
108
107
|
'editor.cancel': '取消',
|
|
109
108
|
'editor.pending': '正在保存…',
|
|
@@ -115,6 +114,7 @@ window.__ModuleLoader__.load({
|
|
|
115
114
|
'error.invalid': '请求无效,请刷新后重试。',
|
|
116
115
|
'error.session-not-active': '这个会话当前未激活,请先打开该会话再编辑。',
|
|
117
116
|
'error.session-not-found': '找不到该会话的日志。',
|
|
117
|
+
'error.session-empty': '这个会话还没有内容,暂时没有可编辑的东西。',
|
|
118
118
|
'error.busy': '该会话正在回复中,请等回复结束后再编辑。',
|
|
119
119
|
'error.not-editable': '这条消息不支持编辑。',
|
|
120
120
|
'error.unsupported': '当前宿主不支持就地改写回复,需要走重放路径。',
|
|
@@ -132,6 +132,7 @@ window.__ModuleLoader__.load({
|
|
|
132
132
|
'error.already-retired': '这一轮的内容已经不在当前上下文里了。',
|
|
133
133
|
'error.rerunning': '已经有一次重跑在进行中,请等它结束。',
|
|
134
134
|
'error.internal': '服务器内部错误,请稍后重试。',
|
|
135
|
+
'error.projection-missing': '编辑投影未注册,请重启 DSH 后重试。',
|
|
135
136
|
'error.attachment-refused': '附件没能存下来,本次修改没有执行。',
|
|
136
137
|
'error.attachments-dropped': '已保存,但这条消息的图片或文件没能保留:当前部署没有可用的附件存储。',
|
|
137
138
|
'error.attachment-unreadable': '这个附件读不出来,请换一个文件。',
|
|
@@ -164,9 +165,9 @@ window.__ModuleLoader__.load({
|
|
|
164
165
|
'config.summary.height.max': 'Turn max',
|
|
165
166
|
'config.summary.height.content': 'Content',
|
|
166
167
|
'config.summary.height.fill': 'Fill when all',
|
|
167
|
-
'config.hint.height.max': '
|
|
168
|
-
'config.hint.height.content': '
|
|
169
|
-
'config.hint.height.fill': 'Single blocks
|
|
168
|
+
'config.hint.height.max': 'Opens at the tallest reasoning/text block of the turn; while editing it grows with your text and shrinks back, never below the height it opened at.',
|
|
169
|
+
'config.hint.height.content': 'Opens sized for the current block; while editing it grows with your text and shrinks back, never below the height it opened at.',
|
|
170
|
+
'config.hint.height.fill': 'Single blocks open at the turn max and follow your text while editing (never below the opening height); the all-blocks page fills the whole band between the conversation top and the composer, with one scrollbar.',
|
|
170
171
|
'placement.overlay': 'Overlay',
|
|
171
172
|
'placement.inline': 'In-page',
|
|
172
173
|
'placement.legacy': 'Legacy row',
|
|
@@ -189,7 +190,6 @@ window.__ModuleLoader__.load({
|
|
|
189
190
|
'editor.block.reasoning': 'Reasoning',
|
|
190
191
|
'editor.placeholder-block': 'Only this block changes; tool calls, the rest of the reply and later turns stay.',
|
|
191
192
|
'confirm.body-block': 'Confirming replaces only this block; tool calls, the rest of the reply and later turns stay. The original session log is never rewritten.',
|
|
192
|
-
'editor.patch-note': 'Reasoning patched (model-visible)',
|
|
193
193
|
'editor.review': 'Confirm',
|
|
194
194
|
'editor.cancel': 'Cancel',
|
|
195
195
|
'editor.pending': 'Working...',
|
|
@@ -201,6 +201,7 @@ window.__ModuleLoader__.load({
|
|
|
201
201
|
'error.invalid': 'Invalid request; refresh and try again.',
|
|
202
202
|
'error.session-not-active': 'This session is not open in DSH; open it first.',
|
|
203
203
|
'error.session-not-found': 'No session log was found for this id.',
|
|
204
|
+
'error.session-empty': 'This session has nothing in it yet; there is nothing to edit.',
|
|
204
205
|
'error.busy': 'This session is still replying; wait for it to finish.',
|
|
205
206
|
'error.not-editable': 'This message cannot be edited.',
|
|
206
207
|
'error.unsupported': 'This harness does not expose the in-place reply path; the replay path is needed.',
|
|
@@ -218,6 +219,7 @@ window.__ModuleLoader__.load({
|
|
|
218
219
|
'error.already-retired': 'This turn is no longer part of the current context.',
|
|
219
220
|
'error.rerunning': 'A re-run is already in flight; wait for it to finish.',
|
|
220
221
|
'error.internal': 'The host hit an internal error; try again shortly.',
|
|
222
|
+
'error.projection-missing': 'The edit projection is not registered; restart DSH and retry.',
|
|
221
223
|
'error.attachment-refused': 'The attachment could not be stored, so nothing was changed.',
|
|
222
224
|
'error.attachments-dropped': 'Saved, but the images or files on this message could not be kept: this deployment mounts no attachment store.',
|
|
223
225
|
'error.attachment-unreadable': 'That attachment could not be read; try another file.',
|
|
@@ -236,7 +238,157 @@ window.__ModuleLoader__.load({
|
|
|
236
238
|
return t(typeof code === 'string' && ERROR_KEYS.has(`error.${code}`) ? `error.${code}` : 'error.generic')
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
// --- a rewritten prompt, rendered the way the bubble it stands in for is ---
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Whether the markdown bubble plugin is on this page.
|
|
245
|
+
*
|
|
246
|
+
* Its own presence marker is the only honest signal: the host draws a user
|
|
247
|
+
* bubble as plain text (`white-space: pre-wrap`), so rendering markdown in
|
|
248
|
+
* the revision bubble is only correct when something else is already
|
|
249
|
+
* rendering the OTHER user bubbles that way. Without it, a rewritten prompt
|
|
250
|
+
* would look different from every untouched prompt around it.
|
|
251
|
+
*/
|
|
252
|
+
function markdownBubbleInstalled() {
|
|
253
|
+
try {
|
|
254
|
+
return typeof globalThis !== 'undefined' && globalThis.__DSH_MARKDOWN_BUBBLE__ !== undefined
|
|
255
|
+
} catch {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One mounted markdown occurrence per bubble, so a rebuild can unmount it. */
|
|
261
|
+
const revisionRoots = new WeakMap()
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Draw one rewritten prompt's text into `host`.
|
|
265
|
+
*
|
|
266
|
+
* The host's own markdown renderer (`MarkdownText`) is a React component and
|
|
267
|
+
* this half builds DOM by hand, so it is mounted through `react-dom/client` -
|
|
268
|
+
* the same module the host's own bundles require. Anything missing (no
|
|
269
|
+
* react-dom, no primitives, a hostile host shape) falls back to the plain
|
|
270
|
+
* text node this bubble has always used.
|
|
271
|
+
*
|
|
272
|
+
* @param host - the bubble element that shows the text.
|
|
273
|
+
* @param text - the rewritten text.
|
|
274
|
+
* @returns whether the markdown path was used.
|
|
275
|
+
*/
|
|
276
|
+
function renderRevisionText(host, text) {
|
|
277
|
+
if (!markdownBubbleInstalled()) return false
|
|
278
|
+
try {
|
|
279
|
+
const react = require('react')
|
|
280
|
+
const reactDom = require('react-dom/client')
|
|
281
|
+
const MarkdownText = primitives.MarkdownText
|
|
282
|
+
if (
|
|
283
|
+
typeof react?.createElement !== 'function' ||
|
|
284
|
+
typeof reactDom?.createRoot !== 'function' ||
|
|
285
|
+
MarkdownText === undefined ||
|
|
286
|
+
MarkdownText === null
|
|
287
|
+
) {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
const previous = revisionRoots.get(host)
|
|
291
|
+
if (previous !== undefined && typeof previous.unmount === 'function') previous.unmount()
|
|
292
|
+
const node = document.createElement('span')
|
|
293
|
+
node.className = 'dshet-revision-text'
|
|
294
|
+
host.appendChild(node)
|
|
295
|
+
const root = reactDom.createRoot(node)
|
|
296
|
+
root.render(react.createElement(MarkdownText, { text }))
|
|
297
|
+
revisionRoots.set(host, root)
|
|
298
|
+
return true
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.warn('[dsh-edit-turn] the rewritten prompt could not be rendered as markdown:', error)
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
239
305
|
// --- style ----------------------------------------------------------------
|
|
306
|
+
/**
|
|
307
|
+
* Take the active skin's own surfaces when they are actually opaque.
|
|
308
|
+
*
|
|
309
|
+
* A skin may paint its surfaces translucent so artwork shows through, and it
|
|
310
|
+
* compensates with its own text halo - which a plugin element does not have.
|
|
311
|
+
* So the theme's surfaces are adopted only after resolving them to a real
|
|
312
|
+
* colour and finding them opaque; a translucent skin leaves this editor on
|
|
313
|
+
* its own opaque palette. Colours, radii and shadows need no such guard:
|
|
314
|
+
* those are hue and geometry, and they survive any skin.
|
|
315
|
+
*
|
|
316
|
+
* The result is written as inline variables on `body`, which beats both the
|
|
317
|
+
* `:root` defaults and the dark-theme rule below them.
|
|
318
|
+
*
|
|
319
|
+
* @returns nothing; the stylesheet defaults stand when no probe is possible.
|
|
320
|
+
*/
|
|
321
|
+
function resolveSkinPalette() {
|
|
322
|
+
if (typeof document === 'undefined' || !document.body || typeof document.createElement !== 'function') return
|
|
323
|
+
if (typeof getComputedStyle !== 'function') return
|
|
324
|
+
const probe = document.createElement('span')
|
|
325
|
+
probe.setAttribute('aria-hidden', 'true')
|
|
326
|
+
probe.style.cssText = 'position:absolute;left:-9999px;top:0;width:0;height:0;pointer-events:none'
|
|
327
|
+
document.body.appendChild(probe)
|
|
328
|
+
const read = (name) => {
|
|
329
|
+
try {
|
|
330
|
+
probe.style.color = `var(${name})`
|
|
331
|
+
const match = /rgba?\(([^)]+)\)/.exec(getComputedStyle(probe).color)
|
|
332
|
+
if (match === null) return null
|
|
333
|
+
const parts = match[1].split(',').map((value) => Number.parseFloat(value))
|
|
334
|
+
if (parts.length < 3 || parts.some((value) => !Number.isFinite(value))) return null
|
|
335
|
+
return { r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1 }
|
|
336
|
+
} catch {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const css = (color) => `rgba(${Math.round(color.r)},${Math.round(color.g)},${Math.round(color.b)},${color.a})`
|
|
341
|
+
const style = document.body.style
|
|
342
|
+
const setVar = (name, value) => {
|
|
343
|
+
if (style === undefined || style === null) return
|
|
344
|
+
if (typeof style.setProperty === 'function') style.setProperty(name, value)
|
|
345
|
+
else style[name] = value
|
|
346
|
+
}
|
|
347
|
+
const clearVar = (name) => {
|
|
348
|
+
if (style === undefined || style === null) return
|
|
349
|
+
if (typeof style.removeProperty === 'function') style.removeProperty(name)
|
|
350
|
+
else delete style[name]
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const panel = read('--dsw-alias-bg-layer-1')
|
|
354
|
+
const layer2 = read('--dsw-alias-bg-layer-2')
|
|
355
|
+
const accent = read('--dsw-alias-button-primary-fill')
|
|
356
|
+
// The skin's surface HUE is taken; its transparency is not. A skin paints
|
|
357
|
+
// its own elements translucent over artwork and gives them a text halo -
|
|
358
|
+
// a plugin element has none, so the same colour is adopted at full
|
|
359
|
+
// opacity. That keeps a dark artwork skin dark (instead of falling back
|
|
360
|
+
// to this plugin's light palette, which is what a skin that never sets
|
|
361
|
+
// `data-ds-dark-theme` used to get) without ever painting text on art.
|
|
362
|
+
const solid = (color) => (color === null ? null : { r: color.r, g: color.g, b: color.b, a: 1 })
|
|
363
|
+
const panelSolid = solid(panel)
|
|
364
|
+
if (panelSolid === null) {
|
|
365
|
+
clearVar('--dshet-panel')
|
|
366
|
+
clearVar('--dshet-chip')
|
|
367
|
+
clearVar('--dshet-field')
|
|
368
|
+
} else {
|
|
369
|
+
const fieldSolid = solid(layer2) ?? panelSolid
|
|
370
|
+
setVar('--dshet-panel', css(panelSolid))
|
|
371
|
+
setVar('--dshet-chip', css(panelSolid))
|
|
372
|
+
setVar('--dshet-field', css(fieldSolid))
|
|
373
|
+
}
|
|
374
|
+
if (accent === null) {
|
|
375
|
+
clearVar('--dshet-on-accent')
|
|
376
|
+
} else {
|
|
377
|
+
// The label that sits ON the accent fill: the platform exposes no
|
|
378
|
+
// inverse token here, so it is picked from the fill's own luminance.
|
|
379
|
+
const channel = (value) => {
|
|
380
|
+
const v = value / 255
|
|
381
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
|
|
382
|
+
}
|
|
383
|
+
const luminance = 0.2126 * channel(accent.r) + 0.7152 * channel(accent.g) + 0.0722 * channel(accent.b)
|
|
384
|
+
setVar('--dshet-on-accent', luminance > 0.35 ? '#0f1115' : '#fff')
|
|
385
|
+
}
|
|
386
|
+
} finally {
|
|
387
|
+
if (typeof probe.remove === 'function') probe.remove()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
240
392
|
|
|
241
393
|
// A skin is free to make the theme's surface colours translucent - that is
|
|
242
394
|
// the point of one: the artwork shows through, and the skin compensates by
|
|
@@ -251,8 +403,21 @@ window.__ModuleLoader__.load({
|
|
|
251
403
|
// UI packages use. Theme variables are still used for *colours* (hue,
|
|
252
404
|
// semantic tones) - those survive a skin; it is alpha that does not.
|
|
253
405
|
const CSS = [
|
|
254
|
-
|
|
255
|
-
|
|
406
|
+
// Colours, radii and shadows come from the active theme's own tokens, so a
|
|
407
|
+
// skin restyles this editor the way it restyles the rest of the app; every
|
|
408
|
+
// one of them falls back to the value this plugin shipped with. The palette
|
|
409
|
+
// hangs off `body`, not `:root`: a custom property substitutes against the
|
|
410
|
+
// values in scope where it is DEFINED, and the theme declares its tokens on
|
|
411
|
+
// body - on `:root` every reference would silently fall back. Only the
|
|
412
|
+
// SURFACES stay namespaced: a skin is free to make its own surfaces
|
|
413
|
+
// translucent (that is the point of one), and a plugin's element has no
|
|
414
|
+
// artwork-aware halo to stay readable behind. The surfaces are taken from
|
|
415
|
+
// the theme too, but only when a runtime probe finds them opaque (see
|
|
416
|
+
// resolveSkinPalette).
|
|
417
|
+
'body{--dshet-panel:rgba(255,255,255,.96);--dshet-field:rgba(244,247,252,.98);--dshet-chip:rgba(255,255,255,.82);--dshet-line:var(--dsw-alias-border-l2,rgba(16,24,40,.16));--dshet-ink:var(--dsw-alias-label-primary,#0f1524);--dshet-ink-dim:var(--dsw-alias-label-secondary,#4b5872);--dshet-ink-soft:var(--dsw-alias-label-tertiary,#81858c);--dshet-hover:rgba(16,24,40,.07);--dshet-shadow:var(--dsw-shadow-lv3,0 12px 32px rgba(9,18,40,.22));--dshet-accent:var(--dsw-alias-button-primary-fill,#4d6bfe);--dshet-on-accent:#fff;--dshet-bubble:var(--dsw-specific-bubble,var(--dshet-field));--dshet-radius-panel:var(--dsw-radius-panel,16px);--dshet-radius-xl:var(--dsw-radius-xl,16px);--dshet-radius-lg:var(--dsw-radius-lg,16px);--dshet-radius-md:var(--dsw-radius-md,12px);--dshet-radius-sm:var(--dsw-radius-sm,8px)}',
|
|
418
|
+
'body[data-ds-dark-theme]{--dshet-panel:rgba(22,28,44,.96);--dshet-field:rgba(14,19,33,.98);--dshet-chip:rgba(22,28,44,.82);--dshet-hover:rgba(255,255,255,.11);--dshet-shadow:var(--dsw-shadow-lv3,0 12px 32px rgba(0,0,0,.5))}',
|
|
419
|
+
// A hover wash that follows the skin's ink instead of a fixed grey.
|
|
420
|
+
'@supports (color:color-mix(in srgb,red,blue)){body{--dshet-hover:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent)}body[data-ds-dark-theme]{--dshet-hover:color-mix(in srgb,var(--dsw-alias-label-primary) 14%,transparent)}}',
|
|
256
421
|
// The pencil sits among the platform's own action icons, so it takes the
|
|
257
422
|
// platform's icon colour: our own `--dshet-ink-dim` is a blue-grey and
|
|
258
423
|
// read as a different, "active" icon next to the neutral grey ones. The
|
|
@@ -278,7 +443,7 @@ window.__ModuleLoader__.load({
|
|
|
278
443
|
// inherit picks up the row's 16px, fixed ignores the reader's content
|
|
279
444
|
// font setting, and either way the rewritten text changed size against
|
|
280
445
|
// the bubble it replaces.
|
|
281
|
-
'.dshet-revision{box-sizing:border-box;align-self:flex-end;width:fit-content;max-width:min(72ch,86%);margin:6px 0 6px auto;padding:9px 14px;border-radius:
|
|
446
|
+
'.dshet-revision{box-sizing:border-box;align-self:flex-end;width:fit-content;max-width:min(72ch,86%);margin:6px 0 6px auto;padding:9px 14px;border-radius:var(--dshet-radius-xl);background:var(--dshet-bubble);color:var(--dshet-ink);font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}',
|
|
282
447
|
'.dshet-revision:empty{display:none}',
|
|
283
448
|
'.dshet-revision{position:relative}',
|
|
284
449
|
// Fallback only: a row with no action bar to keep is displayed away
|
|
@@ -289,7 +454,7 @@ window.__ModuleLoader__.load({
|
|
|
289
454
|
'.dshet-revision.dshet-revision-action{padding-inline-end:40px}',
|
|
290
455
|
'.dshet-revision-text{display:block}',
|
|
291
456
|
'.dshet-revision-files{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}',
|
|
292
|
-
'.dshet-revision-file{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:2px 6px 2px 2px;border:1px solid var(--dshet-line);border-radius:
|
|
457
|
+
'.dshet-revision-file{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:2px 6px 2px 2px;border:1px solid var(--dshet-line);border-radius:var(--dshet-radius-sm);background:var(--dshet-chip);font-size:12px;line-height:18px}',
|
|
293
458
|
'.dshet-revision .dshet-action-host{position:absolute;inset-inline-end:6px;top:6px;opacity:0;transition:opacity .12s ease}',
|
|
294
459
|
'.dshet-revision:hover .dshet-action-host,.dshet-revision:focus-within .dshet-action-host{opacity:1}',
|
|
295
460
|
// A sibling plugin can append its own button (dsh-delete-turn's bin) into
|
|
@@ -331,15 +496,16 @@ window.__ModuleLoader__.load({
|
|
|
331
496
|
'.dshet-layer[data-dshet-inline="1"]{position:absolute;left:0;top:0;right:auto;bottom:auto;width:100%;height:0}',
|
|
332
497
|
'.dshet-layer{position:fixed;left:0;top:0;right:0;bottom:0;pointer-events:none;z-index:40}',
|
|
333
498
|
'.dshet-layer .dshet-editor{position:absolute;pointer-events:auto}',
|
|
334
|
-
'.dshet-editor{display:flex;flex-direction:column;max-height:calc(100vh - 24px);overflow-y:auto;overscroll-behavior:contain;margin:0;border:1px solid var(--dshet-line);border-radius:
|
|
499
|
+
'.dshet-editor{display:flex;flex-direction:column;max-height:calc(100vh - 24px);overflow-y:auto;overscroll-behavior:contain;margin:0;border:1px solid var(--dshet-line);border-radius:var(--dshet-radius-panel);background:var(--dshet-panel);backdrop-filter:blur(var(--dsh-input-card-blur,18px)) saturate(1.2);box-shadow:var(--dshet-shadow);color:var(--dshet-ink);transition:border-color .12s}',
|
|
335
500
|
'.dshet-editor[data-dshet-drop="1"]{outline:2px dashed #4a7dff;outline-offset:-4px}',
|
|
336
501
|
'.dshet-editor:focus-within{border-color:var(--dsw-alias-button-primary-fill,var(--dshet-accent))}',
|
|
337
|
-
'.dshet-editor[data-dshet-inplace="1"]{overflow:hidden;border:1.5px solid var(--dsw-alias-button-primary-fill,#4a7dff);border-radius:
|
|
502
|
+
'.dshet-editor[data-dshet-inplace="1"]{overflow:hidden;border:1.5px solid var(--dsw-alias-button-primary-fill,#4a7dff);border-radius:var(--dshet-radius-md);background:var(--dshet-panel);backdrop-filter:none;box-shadow:none}',
|
|
338
503
|
'.dshet-editor[data-dshet-inplace="1"] textarea{position:absolute;min-height:0;max-height:none;padding:6px 10px;border:0;border-radius:8px;background:transparent;overflow-y:auto}',
|
|
339
|
-
'.dshet-editor[data-dshet-inplace="1"] .dshet-footer{position:absolute;top:auto;left:0;right:0;bottom:0;height:46px;align-items:center;padding:6px 10px;box-sizing:border-box;background:var(--dshet-panel);border-radius:0 0
|
|
504
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-footer{position:absolute;top:auto;left:0;right:0;bottom:0;height:46px;align-items:center;padding:6px 10px;box-sizing:border-box;background:var(--dshet-panel);border-radius:0 0 var(--dshet-radius-md) var(--dshet-radius-md);gap:6px}',
|
|
340
505
|
'.dshet-editor[data-dshet-inplace="1"] .dshet-btn{padding:2px 8px;font-size:12px;line-height:18px;border-radius:8px}',
|
|
341
506
|
'.dshet-editor[data-dshet-inplace="1"] .dshet-pick{position:static;top:auto;left:auto;padding:0;background:transparent;border-radius:0}',
|
|
342
|
-
'.dshet-editor[data-dshet-inplace="1"] .dshet-note,.dshet-editor[data-dshet-inplace="1"] .dshet-warn
|
|
507
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-note,.dshet-editor[data-dshet-inplace="1"] .dshet-warn{position:absolute;left:8px;bottom:6px;max-width:60%;margin:0;background:var(--dshet-panel);border-radius:8px;padding:2px 6px}',
|
|
508
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-error{position:absolute;left:8px;right:8px;bottom:52px;max-width:none;margin:0;padding:6px 10px;border:1px solid var(--dshet-line);border-radius:8px;background:var(--dshet-panel);box-shadow:var(--dshet-shadow);font-weight:500}',
|
|
343
509
|
|
|
344
510
|
'.dshet-editor textarea{display:block;width:100%;min-height:47px;max-height:min(46vh,420px);overflow-y:auto;resize:none;box-sizing:border-box;padding:11px 14px 0;border:0;background:transparent;color:inherit;font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));outline:none}',
|
|
345
511
|
'.dshet-editor textarea::placeholder{color:var(--dshet-ink-dim)}',
|
|
@@ -368,7 +534,7 @@ window.__ModuleLoader__.load({
|
|
|
368
534
|
'.dshet-note{color:var(--dshet-ink-dim)}',
|
|
369
535
|
'.dshet-warn{color:var(--dsw-alias-state-warning-primary,#a2650a)}',
|
|
370
536
|
'.dshet-error{color:var(--dsw-alias-state-error-primary,#c93a31)}',
|
|
371
|
-
'.dshet-footer{position:absolute;left:0;right:0;bottom:0;top:auto;z-index:3;display:flex;align-items:center;justify-content:space-between;gap:6px;height:46px;padding:6px 10px;box-sizing:border-box;background:var(--dshet-panel);border-radius:0 0
|
|
537
|
+
'.dshet-footer{position:absolute;left:0;right:0;bottom:0;top:auto;z-index:3;display:flex;align-items:center;justify-content:space-between;gap:6px;height:46px;padding:6px 10px;box-sizing:border-box;background:var(--dshet-panel);border-radius:0 0 var(--dshet-radius-panel) var(--dshet-radius-panel)}',
|
|
372
538
|
'.dshet-footer-actions{display:flex;align-items:center;gap:6px}',
|
|
373
539
|
'.dshet-all-nav{position:sticky;top:0;z-index:2;display:flex;flex-wrap:nowrap;align-items:center;gap:6px;min-height:28px;padding:4px 0 6px;background:var(--dshet-panel);overflow-x:auto;scrollbar-width:none}',
|
|
374
540
|
'.dshet-all-nav button{flex:none;border:0;border-radius:999px;background:var(--dshet-hover);color:var(--dshet-ink-dim);font:inherit;font-size:12px;line-height:18px;padding:3px 10px;cursor:pointer;white-space:nowrap}',
|
|
@@ -392,10 +558,11 @@ window.__ModuleLoader__.load({
|
|
|
392
558
|
'.dshet-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}',
|
|
393
559
|
'.dshet-btn-primary{border-color:var(--dsw-alias-button-primary-fill,var(--dshet-accent));background:var(--dsw-alias-button-primary-fill,var(--dshet-accent));color:var(--dsw-alias-label-primary-foreground,var(--dshet-on-accent))}',
|
|
394
560
|
'.dshet-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-fill,var(--dshet-accent));filter:brightness(1.08)}',
|
|
395
|
-
'.dshet-btn.dshet-btn-save{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color
|
|
561
|
+
'.dshet-btn.dshet-btn-save{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dshet-ink-soft);filter:none}',
|
|
396
562
|
'.dshet-btn.dshet-btn-save:hover:not(:disabled){background:var(--dshet-hover);filter:none}',
|
|
563
|
+
'.dshet-btn.dshet-btn-save.dshet-btn-confirm{background:var(--dshet-hover);box-shadow:inset 0 0 0 1.5px var(--dshet-accent)}',
|
|
397
564
|
'.dshet-editor .dshet-footer .dshet-btn.dshet-btn-icon{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#81858c;filter:none}',
|
|
398
|
-
'.dshet-editor .dshet-footer .dshet-btn.dshet-btn-icon:hover:not(:disabled){background:var(--dshet-hover);color
|
|
565
|
+
'.dshet-editor .dshet-footer .dshet-btn.dshet-btn-icon:hover:not(:disabled){background:var(--dshet-hover);color:var(--dshet-ink-soft);filter:none}',
|
|
399
566
|
'.dshet-btn-icon svg{width:15px;height:15px;display:block;fill:none;stroke:currentColor;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round}',
|
|
400
567
|
'.dshet-editor[data-dshet-inplace="1"] .dshet-btn.dshet-btn-save{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#81858c;filter:none}',
|
|
401
568
|
'.dshet-editor[data-dshet-inplace="1"] .dshet-btn.dshet-btn-save:hover:not(:disabled){background:var(--dshet-hover);filter:none}',
|
|
@@ -403,13 +570,10 @@ window.__ModuleLoader__.load({
|
|
|
403
570
|
'.dshet-config{display:flex;flex-direction:column;gap:10px}',
|
|
404
571
|
'.dshet-config-row{display:flex;align-items:center;justify-content:space-between;gap:12px}',
|
|
405
572
|
'.dshet-config-hint{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
|
|
406
|
-
'.dshet-select{padding:3px 8px;border:1px solid var(--dshet-line);border-radius:
|
|
573
|
+
'.dshet-select{padding:3px 8px;border:1px solid var(--dshet-line);border-radius:var(--dshet-radius-sm);background:var(--dshet-field);color:var(--dshet-ink);font:inherit;font-size:12px;line-height:18px}',
|
|
407
574
|
'.dshet-pick .dshet-select{height:28px;padding:0 6px;border:0;border-radius:8px;background:transparent;line-height:1;cursor:pointer;outline:none}',
|
|
408
575
|
'.dshet-pick .dshet-select:hover{background:var(--dshet-hover)}',
|
|
409
576
|
'[data-dshet-patch]{white-space:pre-wrap}',
|
|
410
|
-
'.dshet-patch-note{margin:6px 0;padding:6px 10px;border:1px dashed var(--dshet-line);border-radius:10px;background:var(--dshet-panel);color:var(--dshet-ink-dim);font-size:12px;line-height:18px}',
|
|
411
|
-
'.dshet-patch-note summary{cursor:pointer}',
|
|
412
|
-
'.dshet-patch-note-body{margin:6px 0 0;white-space:pre-wrap;color:var(--dshet-ink);font-size:12px;line-height:18px}',
|
|
413
577
|
'.dshet-notice{margin:4px 0;padding:6px 10px;border:1px solid var(--dshet-line);border-radius:8px;background:var(--dshet-panel);backdrop-filter:blur(18px) saturate(1.2);box-shadow:var(--dshet-shadow);color:var(--dsw-alias-state-error-primary,#c93a31);font-size:12px;line-height:19px}',
|
|
414
578
|
'@media (prefers-reduced-motion:reduce){.dshet-collapsing{transition:none}.dshet-floating{transition:none}}',
|
|
415
579
|
].join('')
|
|
@@ -476,6 +640,8 @@ window.__ModuleLoader__.load({
|
|
|
476
640
|
this.editorStyle = 'frame'
|
|
477
641
|
this.editorFocus = false
|
|
478
642
|
this.editorDraft = ''
|
|
643
|
+
this.editorBlockKey = ''
|
|
644
|
+
this.editorDrafts = new Map()
|
|
479
645
|
// The block list being edited: what the carrier will be rebuilt from.
|
|
480
646
|
// Every entry is either `{ keep: <index> }` - a block of this message the
|
|
481
647
|
// user left alone - or `{ add: { data, mediaType?, name } }` for
|
|
@@ -634,6 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
634
800
|
? data.config.editorStyle
|
|
635
801
|
: 'frame',
|
|
636
802
|
surfaceReady: true,
|
|
803
|
+
busy: data.busy === true,
|
|
637
804
|
loaded: true,
|
|
638
805
|
loadError: false,
|
|
639
806
|
confirmStep: !(data.config && data.config.confirm === false),
|
|
@@ -648,6 +815,14 @@ window.__ModuleLoader__.load({
|
|
|
648
815
|
})
|
|
649
816
|
.finally(() => {
|
|
650
817
|
this.inflight = null
|
|
818
|
+
// While the session is replying, the editor stays open but cannot
|
|
819
|
+
// save; re-check so the banner clears itself when the turn ends.
|
|
820
|
+
if (this.view.busy === true && this.editor !== null) {
|
|
821
|
+
const token = (this.busyToken = (this.busyToken ?? 0) + 1)
|
|
822
|
+
window.setTimeout(() => {
|
|
823
|
+
if (this.busyToken === token && this.editor !== null) this.load()
|
|
824
|
+
}, 3000)
|
|
825
|
+
}
|
|
651
826
|
})
|
|
652
827
|
this.inflight = pending
|
|
653
828
|
return pending
|
|
@@ -695,8 +870,16 @@ window.__ModuleLoader__.load({
|
|
|
695
870
|
this.editorBlockKind = chosen === undefined ? null : chosen.kind
|
|
696
871
|
this.editorTargetSeq = chosen !== undefined && typeof chosen.seq === 'number' ? chosen.seq : null
|
|
697
872
|
this.editorAll = false
|
|
698
|
-
|
|
873
|
+
// The height the box opens at: the floor the user cannot shrink below
|
|
874
|
+
// for as long as this edit lasts (save or cancel ends it). Null until
|
|
875
|
+
// the first layout measures it.
|
|
876
|
+
this.editorBaseHeight = null
|
|
877
|
+
// ONE draft store per open, keyed by block: the 正文/思维链 pages and the
|
|
878
|
+
// sub-pages inside 全部 all read and write it, so switching pages never
|
|
879
|
+
// drops what the user typed (only closing the editor does).
|
|
880
|
+
this.editorDrafts = new Map()
|
|
699
881
|
if (!same) {
|
|
882
|
+
this.editorBlockKey = chosen === undefined ? '' : blockDraftKey(chosen)
|
|
700
883
|
this.editorDraft = chosen === undefined ? target.text : chosen.text
|
|
701
884
|
this.editorParts = blocksToParts(normalized, this.sessionId)
|
|
702
885
|
}
|
|
@@ -763,25 +946,37 @@ window.__ModuleLoader__.load({
|
|
|
763
946
|
if (target === null || !Array.isArray(target.blocks)) return
|
|
764
947
|
const raw = String(value)
|
|
765
948
|
if (raw === 'all') {
|
|
949
|
+
// The all-blocks page reads the SAME drafts: a block edited on its own
|
|
950
|
+
// page shows that edit in its sub-page, and the other way round.
|
|
766
951
|
this.editorAll = true
|
|
767
|
-
this.editorAllDrafts = new Map(target.blocks.map((block) => [`${typeof block.seq === 'number' ? block.seq : ''}:${block.index}`, block.text]))
|
|
768
952
|
this.editorElement = null
|
|
769
953
|
this.publish({ allBlocks: true, blockIndex: null, blockKind: null, targetSeq: null, failure: null })
|
|
770
954
|
return
|
|
771
955
|
}
|
|
772
|
-
this.editorAll = false
|
|
773
956
|
const split = raw.includes(':') ? raw.split(':') : ['', raw]
|
|
774
957
|
const seq = split[0] === '' ? null : Number(split[0])
|
|
775
958
|
const index = Number(split[1])
|
|
776
959
|
const block = target.blocks.find((entry) =>
|
|
777
960
|
entry.index === index && (seq === null || entry.seq === undefined || entry.seq === seq))
|
|
778
|
-
|
|
961
|
+
// Leaving the all-blocks view has to say so: the picker change alone left
|
|
962
|
+
// `allBlocks` true, so the box was redrawn as the all view again with a
|
|
963
|
+
// picker that said "正文" - two fields over a single-block selection.
|
|
964
|
+
this.editorAll = false
|
|
965
|
+
if (block === undefined || block.kind === undefined) {
|
|
966
|
+
// Nothing matched: still leave the all view, keeping the block that was
|
|
967
|
+
// selected before it, so the picker describes the box again.
|
|
968
|
+
this.publish({ allBlocks: false, failure: null })
|
|
969
|
+
return
|
|
970
|
+
}
|
|
779
971
|
this.editorBlockIndex = block.index
|
|
780
972
|
this.editorBlockKind = block.kind
|
|
781
973
|
this.editorTargetSeq = typeof block.seq === 'number' ? block.seq : this.editorTargetSeq
|
|
782
|
-
this.
|
|
974
|
+
this.editorBlockKey = blockDraftKey(block)
|
|
975
|
+
// This block's own draft, which may already carry what the user typed on
|
|
976
|
+
// another page - never the recorded text, which used to wipe it.
|
|
977
|
+
this.editorDraft = this.draftOfBlock(block)
|
|
783
978
|
this.editorElement = null
|
|
784
|
-
this.publish({ draft:
|
|
979
|
+
this.publish({ draft: this.editorDraft, blockIndex: block.index, blockKind: block.kind, targetSeq: this.editorTargetSeq, allBlocks: false, failure: null })
|
|
785
980
|
}
|
|
786
981
|
|
|
787
982
|
close() {
|
|
@@ -803,12 +998,40 @@ window.__ModuleLoader__.load({
|
|
|
803
998
|
this.editorBlockKind = null
|
|
804
999
|
this.editorTargetSeq = null
|
|
805
1000
|
this.editorAll = false
|
|
806
|
-
this.
|
|
1001
|
+
this.editorDrafts = new Map()
|
|
1002
|
+
this.editorBlockKey = ''
|
|
1003
|
+
this.editorBaseHeight = null
|
|
807
1004
|
this.publish({ editing: null, draft: '', parts: [], blocks: [], blockIndex: null, blockKind: null, confirming: false, failure: null })
|
|
808
1005
|
}
|
|
809
1006
|
|
|
1007
|
+
/** The draft of one block: what the user typed, else the recorded text. */
|
|
1008
|
+
draftOfBlock(block) {
|
|
1009
|
+
const drafts = this.editorDrafts
|
|
1010
|
+
if (drafts instanceof Map) {
|
|
1011
|
+
const key = blockDraftKey(block)
|
|
1012
|
+
const typed = drafts.get(key)
|
|
1013
|
+
if (typeof typed === 'string') return typed
|
|
1014
|
+
}
|
|
1015
|
+
return block !== null && block !== undefined && typeof block.text === 'string' ? block.text : ''
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Remember one block's draft, wherever the user typed it. */
|
|
1019
|
+
setDraftFor(block, value) {
|
|
1020
|
+
if (!(this.editorDrafts instanceof Map)) this.editorDrafts = new Map()
|
|
1021
|
+
this.editorDrafts.set(blockDraftKey(block), String(value ?? ''))
|
|
1022
|
+
}
|
|
1023
|
+
|
|
810
1024
|
setDraft(value) {
|
|
811
1025
|
this.editorDraft = value
|
|
1026
|
+
// The 正文/思维链 page and the matching sub-page inside 全部 are the same
|
|
1027
|
+
// block: one store, so the edit survives a page switch.
|
|
1028
|
+
const blocks = Array.isArray(this.editorBlocks) ? this.editorBlocks : []
|
|
1029
|
+
const current =
|
|
1030
|
+
blocks.find((block) => blockDraftKey(block) === this.editorBlockKey) ??
|
|
1031
|
+
(typeof this.editorBlockIndex === 'number'
|
|
1032
|
+
? blocks.find((block) => block.index === this.editorBlockIndex && (this.editorBlockKind === null || block.kind === this.editorBlockKind))
|
|
1033
|
+
: undefined)
|
|
1034
|
+
if (current !== undefined) this.setDraftFor(current, value)
|
|
812
1035
|
}
|
|
813
1036
|
|
|
814
1037
|
setEditorStyle(style) {
|
|
@@ -886,11 +1109,9 @@ window.__ModuleLoader__.load({
|
|
|
886
1109
|
/** The all-blocks save: one request carrying every edited block. */
|
|
887
1110
|
async confirmAll(target) {
|
|
888
1111
|
const blocks = Array.isArray(target.blocks) ? target.blocks : []
|
|
889
|
-
const drafts = this.editorAllDrafts instanceof Map ? this.editorAllDrafts : new Map()
|
|
890
1112
|
const targets = []
|
|
891
1113
|
for (const block of blocks) {
|
|
892
|
-
const
|
|
893
|
-
const text = drafts.get(key)
|
|
1114
|
+
const text = this.draftOfBlock(block)
|
|
894
1115
|
if (typeof text !== 'string' || text.trim() === '') {
|
|
895
1116
|
this.publish({ failure: 'empty' })
|
|
896
1117
|
return
|
|
@@ -905,22 +1126,27 @@ window.__ModuleLoader__.load({
|
|
|
905
1126
|
this.publish({ editing: null })
|
|
906
1127
|
return
|
|
907
1128
|
}
|
|
908
|
-
this.publish({ pending: true, failure: null })
|
|
1129
|
+
this.publish({ pending: true, failure: null, failureDetail: null })
|
|
909
1130
|
try {
|
|
910
1131
|
const res = await fetch(`${ROUTE_PREFIX}/apply`, {
|
|
911
1132
|
method: 'POST',
|
|
912
1133
|
headers: { 'content-type': 'application/json' },
|
|
1134
|
+
...(typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
1135
|
+
? { signal: AbortSignal.timeout(20000) }
|
|
1136
|
+
: {}),
|
|
913
1137
|
body: JSON.stringify({ sessionId: this.sessionId, targets }),
|
|
914
1138
|
})
|
|
915
1139
|
const data = await res.json().catch(() => ({}))
|
|
916
1140
|
if (!res.ok || !data.ok) {
|
|
917
|
-
|
|
1141
|
+
const code = data && data.code ? String(data.code) : 'generic'
|
|
1142
|
+
const detail = data && typeof data.error === 'string' && data.error !== '' ? data.error : null
|
|
1143
|
+
this.publish({ pending: false, failure: code, failureDetail: detail })
|
|
918
1144
|
return
|
|
919
1145
|
}
|
|
920
1146
|
this.editor = null
|
|
921
1147
|
this.editorDraft = ''
|
|
922
1148
|
this.editorAll = false
|
|
923
|
-
this.
|
|
1149
|
+
this.editorDrafts = new Map()
|
|
924
1150
|
this.editorElement = null
|
|
925
1151
|
this.editorBox = null
|
|
926
1152
|
this.editorRow = null
|
|
@@ -983,11 +1209,16 @@ window.__ModuleLoader__.load({
|
|
|
983
1209
|
const parts = target.mode === 'prompt' && this.view.attachmentsOk === true
|
|
984
1210
|
? this.editorParts.map((part) => (part.keep !== undefined ? { keep: part.keep } : { add: part.add }))
|
|
985
1211
|
: undefined
|
|
986
|
-
this.publish({ pending: true, failure: null })
|
|
1212
|
+
this.publish({ pending: true, failure: null, failureDetail: null })
|
|
987
1213
|
try {
|
|
988
1214
|
const res = await fetch(`${ROUTE_PREFIX}/apply`, {
|
|
989
1215
|
method: 'POST',
|
|
990
1216
|
headers: { 'content-type': 'application/json' },
|
|
1217
|
+
// A hung host must not leave the editor pending forever with a
|
|
1218
|
+
// button that looks dead.
|
|
1219
|
+
...(typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
1220
|
+
? { signal: AbortSignal.timeout(20000) }
|
|
1221
|
+
: {}),
|
|
991
1222
|
body: JSON.stringify({
|
|
992
1223
|
sessionId: this.sessionId,
|
|
993
1224
|
seq: target.seq,
|
|
@@ -1007,7 +1238,9 @@ window.__ModuleLoader__.load({
|
|
|
1007
1238
|
})
|
|
1008
1239
|
const data = await res.json().catch(() => ({}))
|
|
1009
1240
|
if (!res.ok || !data.ok) {
|
|
1010
|
-
|
|
1241
|
+
const code = data && data.code ? String(data.code) : 'generic'
|
|
1242
|
+
const detail = data && typeof data.error === 'string' && data.error !== '' ? data.error : null
|
|
1243
|
+
this.publish({ pending: false, failure: code, failureDetail: detail })
|
|
1011
1244
|
return
|
|
1012
1245
|
}
|
|
1013
1246
|
const shadowed = Array.isArray(data.shadowed) ? data.shadowed : []
|
|
@@ -1050,8 +1283,17 @@ window.__ModuleLoader__.load({
|
|
|
1050
1283
|
let patches = this.view.patches
|
|
1051
1284
|
let patchBySeq = this.view.patchBySeq
|
|
1052
1285
|
if (data.mode === 'patch' && data.target && typeof data.target.blockIndex === 'number') {
|
|
1286
|
+
// The BLOCK's own seq, not the reply's. A turn's reasoning can live
|
|
1287
|
+
// in another step's message, and the host reports that seq in the
|
|
1288
|
+
// answer. Keying the optimistic patch by the reply's seq looked up
|
|
1289
|
+
// the wrong block, took the wrong `original`, and the patch then
|
|
1290
|
+
// matched nothing in the DOM - the edit appeared only after a reload,
|
|
1291
|
+
// when /state supplied it under its own seq (reported from the field
|
|
1292
|
+
// for the 思维链 page; the 正文 page looked fine because its block IS
|
|
1293
|
+
// the reply's own).
|
|
1294
|
+
const patchSeq = typeof data.target.seq === 'number' ? data.target.seq : target.seq
|
|
1053
1295
|
const targetPatch = {
|
|
1054
|
-
seq:
|
|
1296
|
+
seq: patchSeq,
|
|
1055
1297
|
blockIndex: data.target.blockIndex,
|
|
1056
1298
|
kind: data.target.kind === 'reasoning' ? 'reasoning' : 'text',
|
|
1057
1299
|
text: String(data.target.text ?? ''),
|
|
@@ -1063,10 +1305,21 @@ window.__ModuleLoader__.load({
|
|
|
1063
1305
|
const byBlock = new Map(patchBySeq.get(targetPatch.seq) ?? [])
|
|
1064
1306
|
byBlock.set(targetPatch.blockIndex, targetPatch)
|
|
1065
1307
|
patchBySeq.set(targetPatch.seq, byBlock)
|
|
1066
|
-
|
|
1308
|
+
// The entry that carries this block: the reply's own, or the one
|
|
1309
|
+
// whose block list holds another step's reasoning.
|
|
1310
|
+
let entry = replies.get(patchSeq)
|
|
1311
|
+
if (entry === undefined || !Array.isArray(entry.blocks)) {
|
|
1312
|
+
for (const candidate of replies.values()) {
|
|
1313
|
+
if (Array.isArray(candidate?.blocks) && candidate.blocks.some((block) => block.seq === patchSeq)) {
|
|
1314
|
+
entry = candidate
|
|
1315
|
+
break
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1067
1319
|
if (entry !== undefined && Array.isArray(entry.blocks)) {
|
|
1068
1320
|
const blocks = entry.blocks.map((block) =>
|
|
1069
|
-
block.index === targetPatch.blockIndex
|
|
1321
|
+
block.index === targetPatch.blockIndex &&
|
|
1322
|
+
(block.seq === undefined || block.seq === patchSeq)
|
|
1070
1323
|
? { ...block, text: targetPatch.text, patched: true }
|
|
1071
1324
|
: block)
|
|
1072
1325
|
const text = blocks.filter((block) => block.kind === 'text').map((block) => block.text).join('\n')
|
|
@@ -1496,15 +1749,23 @@ window.__ModuleLoader__.load({
|
|
|
1496
1749
|
// like the images were deleted.
|
|
1497
1750
|
const signature = text + '|' + extraBlocks.map((block) =>
|
|
1498
1751
|
`${String(block.index)}:${String(block.type)}:${String(block.name ?? '')}:${String(block.bytes ?? '')}`).join(',')
|
|
1499
|
-
|
|
1752
|
+
// The signature carries whether the markdown path was used: installing or
|
|
1753
|
+
// removing the markdown bubble plugin must redraw this bubble too.
|
|
1754
|
+
const markdownSignature = signature + '|md:' + (markdownBubbleInstalled() ? '1' : '0')
|
|
1755
|
+
if (bubble.dataset.dshetRevisionContent !== markdownSignature) {
|
|
1756
|
+
const previous = revisionRoots.get(bubble)
|
|
1757
|
+
if (previous !== undefined && typeof previous.unmount === 'function') previous.unmount()
|
|
1758
|
+
revisionRoots.delete(bubble)
|
|
1500
1759
|
bubble.textContent = ''
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1760
|
+
if (!renderRevisionText(bubble, text)) {
|
|
1761
|
+
const textNode = document.createElement('span')
|
|
1762
|
+
textNode.className = 'dshet-revision-text'
|
|
1763
|
+
textNode.textContent = text
|
|
1764
|
+
bubble.appendChild(textNode)
|
|
1765
|
+
}
|
|
1505
1766
|
const files = revisionAttachments(entry, controller)
|
|
1506
1767
|
if (files !== null) bubble.appendChild(files)
|
|
1507
|
-
bubble.dataset.dshetRevisionContent =
|
|
1768
|
+
bubble.dataset.dshetRevisionContent = markdownSignature
|
|
1508
1769
|
}
|
|
1509
1770
|
// The edit entry goes where every other action of this row lives: the
|
|
1510
1771
|
// platform's own bar, which the collapse keeps standing, at its right end
|
|
@@ -1907,9 +2168,39 @@ window.__ModuleLoader__.load({
|
|
|
1907
2168
|
// `_actions` suffix is stable) and hides that strip's ancestors' other
|
|
1908
2169
|
// children up to the row. Returns false when there is no strip to keep, and
|
|
1909
2170
|
// false is what tells the caller to hide the whole row instead.
|
|
2171
|
+
/**
|
|
2172
|
+
* The host's own message markup inside a row - the stack / bubble / markdown
|
|
2173
|
+
* the transcript draws the text in. That subtree is the ONLY thing the
|
|
2174
|
+
* collapse walk may hide: a sibling that does not contain it belongs to
|
|
2175
|
+
* somebody else (another plugin's injected button, a portal), and hiding it
|
|
2176
|
+
* is issue #1 - `dsh-rewind-pro`'s button stayed in the DOM, stayed
|
|
2177
|
+
* clickable, and was `display:none` on screen with no console error to
|
|
2178
|
+
* notice. Null when the host shape is not one we recognise; the walk then
|
|
2179
|
+
* keeps its old "everything but ours" behaviour instead of leaving a
|
|
2180
|
+
* rewritten prompt on screen.
|
|
2181
|
+
*/
|
|
2182
|
+
function messageContentRoot(row) {
|
|
2183
|
+
const selectors = ['[class*="_bubble"]', '[class*="userStack"]', '[class*="_markdown"]', '[class*="_stack"]', '[class*="messageBody"]']
|
|
2184
|
+
for (const selector of selectors) {
|
|
2185
|
+
let found = null
|
|
2186
|
+
try {
|
|
2187
|
+
found = row.querySelector(selector)
|
|
2188
|
+
} catch {
|
|
2189
|
+
found = null
|
|
2190
|
+
}
|
|
2191
|
+
if (found === null || found === undefined) continue
|
|
2192
|
+
if (typeof found.closest === 'function' && found.closest('.dshet-editor') !== null) continue
|
|
2193
|
+
return found
|
|
2194
|
+
}
|
|
2195
|
+
return null
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1910
2198
|
function collapseRowContent(row, hide) {
|
|
1911
2199
|
const strip = row.querySelector('[class*="_actions"]')
|
|
1912
2200
|
if (strip === null) return false
|
|
2201
|
+
// Only consulted while hiding: the restore pass goes by the marker this
|
|
2202
|
+
// plugin wrote, so a node an older build had hidden is still restored.
|
|
2203
|
+
const contentRoot = hide ? messageContentRoot(row) : null
|
|
1913
2204
|
let node = strip
|
|
1914
2205
|
while (node !== null && node !== row) {
|
|
1915
2206
|
const parent = node.parentElement
|
|
@@ -1929,6 +2220,16 @@ window.__ModuleLoader__.load({
|
|
|
1929
2220
|
) {
|
|
1930
2221
|
continue
|
|
1931
2222
|
}
|
|
2223
|
+
// I5 / issue #1: another plugin's node beside the message is not ours
|
|
2224
|
+
// to hide. Only a sibling that holds the message content is.
|
|
2225
|
+
if (
|
|
2226
|
+
hide &&
|
|
2227
|
+
contentRoot !== null &&
|
|
2228
|
+
typeof child.contains === 'function' &&
|
|
2229
|
+
child.contains(contentRoot) !== true
|
|
2230
|
+
) {
|
|
2231
|
+
continue
|
|
2232
|
+
}
|
|
1932
2233
|
if (hide) {
|
|
1933
2234
|
// Written only when the value changes: this runs on every DOM pass,
|
|
1934
2235
|
// and an attribute write the observer can see would never settle.
|
|
@@ -2229,9 +2530,28 @@ window.__ModuleLoader__.load({
|
|
|
2229
2530
|
element.style[property] = value
|
|
2230
2531
|
}
|
|
2231
2532
|
|
|
2533
|
+
/**
|
|
2534
|
+
* The surface an in-place editor paints itself with, so the recorded text
|
|
2535
|
+
* underneath cannot show through the draft.
|
|
2536
|
+
*
|
|
2537
|
+
* The skin's own panel colour comes first. A skin that paints the app on
|
|
2538
|
+
* artwork usually leaves `body` transparent (the artwork is a layer behind
|
|
2539
|
+
* it), and the white fallback then put a white box on a night sky. The
|
|
2540
|
+
* palette keeps the skin's hue and drops its transparency, so what comes back
|
|
2541
|
+
* here is opaque by construction.
|
|
2542
|
+
*
|
|
2543
|
+
* @returns a CSS colour for the editor's own background.
|
|
2544
|
+
*/
|
|
2232
2545
|
function pageBackground() {
|
|
2233
2546
|
try {
|
|
2234
|
-
if (typeof getComputedStyle === 'function' && typeof document !== 'undefined') {
|
|
2547
|
+
if (typeof getComputedStyle === 'function' && typeof document !== 'undefined' && document.body) {
|
|
2548
|
+
const panel = String(getComputedStyle(document.body).getPropertyValue('--dshet-panel') || '').trim()
|
|
2549
|
+
const match = /rgba?\(([^)]+)\)/.exec(panel)
|
|
2550
|
+
if (match !== null) {
|
|
2551
|
+
const parts = match[1].split(',').map((value) => Number.parseFloat(value))
|
|
2552
|
+
const opaque = parts.length >= 3 && parts.every((value) => Number.isFinite(value)) && (parts.length < 4 || parts[3] >= 0.9)
|
|
2553
|
+
if (opaque) return panel
|
|
2554
|
+
}
|
|
2235
2555
|
const value = getComputedStyle(document.body).backgroundColor
|
|
2236
2556
|
if (value && value !== 'transparent' && !/rgba?\(0, 0, 0, 0\)/.test(value)) return value
|
|
2237
2557
|
}
|
|
@@ -2398,7 +2718,7 @@ window.__ModuleLoader__.load({
|
|
|
2398
2718
|
return bounds
|
|
2399
2719
|
}
|
|
2400
2720
|
|
|
2401
|
-
function placeEditor(row, box, mode = 'reply', style = 'frame', blockKind = null, blocks = null) {
|
|
2721
|
+
function placeEditor(row, box, mode = 'reply', style = 'frame', blockKind = null, blocks = null, controller = null) {
|
|
2402
2722
|
const layer = ensureEditorLayer()
|
|
2403
2723
|
if (box.parentElement !== layer) layer.appendChild(box)
|
|
2404
2724
|
const placement = readStoredEditorPlacement() ?? 'overlay'
|
|
@@ -2572,11 +2892,23 @@ window.__ModuleLoader__.load({
|
|
|
2572
2892
|
try {
|
|
2573
2893
|
const previousValue = area.value
|
|
2574
2894
|
const previousHeight = area.style.height
|
|
2895
|
+
// Assigning `value` drops a textarea's scroll position to the top -
|
|
2896
|
+
// even when the assignment is only a measuring detour. Verified in
|
|
2897
|
+
// Chromium: the SAME string keeps the scroll, a DIFFERENT string
|
|
2898
|
+
// resets it to 0, and putting the original back does not bring it
|
|
2899
|
+
// back. The "turn max" pass below writes every other block's text into
|
|
2900
|
+
// this field, so every cache-miss layout (the first one, a page
|
|
2901
|
+
// switch, and every pass while the turn's blocks change - i.e. while
|
|
2902
|
+
// the model streams) threw the reader back to the top. That is what
|
|
2903
|
+
// "the 思维链/正文 page cannot be scrolled down" was: the wheel worked,
|
|
2904
|
+
// the position did not survive the next layout.
|
|
2905
|
+
const previousScroll = typeof area.scrollTop === 'number' ? area.scrollTop : 0
|
|
2575
2906
|
const measure = (text) => {
|
|
2576
2907
|
area.value = text
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2908
|
+
// The settled height: one pass reads a line short (see
|
|
2909
|
+
// settleAreaHeight), and this number decides the whole box. The
|
|
2910
|
+
// scroller is the box itself, so its position is kept too.
|
|
2911
|
+
return Math.round(withScrollKept(box, () => settleAreaHeight(area)))
|
|
2580
2912
|
}
|
|
2581
2913
|
const own = measure(previousValue)
|
|
2582
2914
|
if (own > 0) contentHeight = own
|
|
@@ -2602,11 +2934,26 @@ window.__ModuleLoader__.load({
|
|
|
2602
2934
|
}
|
|
2603
2935
|
area.value = previousValue
|
|
2604
2936
|
area.style.height = previousHeight
|
|
2937
|
+
if (typeof area.scrollTop === 'number' && area.scrollTop !== previousScroll) {
|
|
2938
|
+
area.scrollTop = previousScroll
|
|
2939
|
+
}
|
|
2605
2940
|
} catch {}
|
|
2606
2941
|
}
|
|
2607
2942
|
// Every mode hugs the measured content; `coveredHeight` is only a
|
|
2608
2943
|
// fallback for a host whose textarea cannot be measured.
|
|
2609
|
-
|
|
2944
|
+
// While the editor is open its height FOLLOWS the draft: more lines grow
|
|
2945
|
+
// the box, deleting lines shrinks it - but never below the height it had
|
|
2946
|
+
// when it opened, and never past the composer below or the conversation
|
|
2947
|
+
// top above. The floor belongs to the whole edit (a page switch keeps it)
|
|
2948
|
+
// and only a save or a cancel ends it.
|
|
2949
|
+
const baseHeight =
|
|
2950
|
+
controller !== null &&
|
|
2951
|
+
controller !== undefined &&
|
|
2952
|
+
typeof controller.editorBaseHeight === 'number' &&
|
|
2953
|
+
controller.editorBaseHeight > 0
|
|
2954
|
+
? controller.editorBaseHeight
|
|
2955
|
+
: null
|
|
2956
|
+
let height = Math.max(minimumHeight, contentHeight + headerOffset + FOOTER_BAND, baseHeight === null ? 0 : baseHeight)
|
|
2610
2957
|
let clampedTop = null
|
|
2611
2958
|
if (clamped) {
|
|
2612
2959
|
const bounds = conversationBounds(row)
|
|
@@ -2617,14 +2964,32 @@ window.__ModuleLoader__.load({
|
|
|
2617
2964
|
// fills the WHOLE band (not just the message's own position), so the
|
|
2618
2965
|
// top touches the conversation edge and the bottom the composer.
|
|
2619
2966
|
const fillAll = allBox !== null && allBox !== undefined && heightMode !== 'content'
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2967
|
+
const band = bottomLimit - topLimit
|
|
2968
|
+
if (fillAll) {
|
|
2969
|
+
clampedTop = topLimit
|
|
2970
|
+
height = Math.max(minimumHeight, band)
|
|
2971
|
+
} else if (height <= band) {
|
|
2972
|
+
// It fits: grow DOWN from the row while there is room for it, and
|
|
2973
|
+
// when there is not, keep the bottom at the composer edge and grow
|
|
2974
|
+
// UP instead - the direction follows the space, the box never covers
|
|
2975
|
+
// the composer and never leaves the conversation's top.
|
|
2976
|
+
const room = bottomLimit - Math.max(base.top, topLimit)
|
|
2977
|
+
clampedTop = height <= room ? Math.max(base.top, topLimit) : Math.max(topLimit, bottomLimit - height)
|
|
2978
|
+
height = Math.min(height, band)
|
|
2979
|
+
} else {
|
|
2980
|
+
// Taller than the whole band: fill it and let the field scroll.
|
|
2981
|
+
clampedTop = topLimit
|
|
2982
|
+
height = Math.max(minimumHeight, band)
|
|
2983
|
+
}
|
|
2624
2984
|
}
|
|
2625
2985
|
}
|
|
2626
2986
|
if (!inline && !clamped && viewport.height > 0) height = Math.min(height, Math.max(minimumHeight, viewport.height - margin * 2))
|
|
2627
2987
|
height = Math.max(minimumHeight, height)
|
|
2988
|
+
if (baseHeight === null && controller !== null && controller !== undefined) {
|
|
2989
|
+
// The first layout of this open fixes the floor: from here on the box
|
|
2990
|
+
// may grow and shrink with the draft, but not below this.
|
|
2991
|
+
controller.editorBaseHeight = height
|
|
2992
|
+
}
|
|
2628
2993
|
box.style.height = `${height}px`
|
|
2629
2994
|
box.style.maxHeight = ''
|
|
2630
2995
|
let top = clampedTop === null ? base.top : clampedTop
|
|
@@ -2648,18 +3013,24 @@ window.__ModuleLoader__.load({
|
|
|
2648
3013
|
// is exactly as tall as its text and the ONE scrollbar appears when it
|
|
2649
3014
|
// overflows the band.
|
|
2650
3015
|
if (typeof allBox.querySelectorAll === 'function') {
|
|
2651
|
-
|
|
2652
|
-
blockArea
|
|
2653
|
-
|
|
2654
|
-
if (typeof measured === 'number' && measured > 0) blockArea.style.height = `${measured}px`
|
|
2655
|
-
}
|
|
3016
|
+
withScrollKept(allBox, () => {
|
|
3017
|
+
for (const blockArea of allBox.querySelectorAll('.dshet-all-area')) settleAreaHeight(blockArea)
|
|
3018
|
+
})
|
|
2656
3019
|
}
|
|
2657
3020
|
} else if (area !== null && area !== undefined) {
|
|
2658
3021
|
const offsetLeft = Math.max(0, bubble.rect.left - base.left)
|
|
2659
3022
|
// Reserve a header band for the controls; when the prompt carries image
|
|
2660
3023
|
// chips they sit above the text, so the text starts below them too.
|
|
2661
3024
|
const offsetTop = headerOffset
|
|
2662
|
-
|
|
3025
|
+
// The control band is painted over the box's bottom edge, so the field
|
|
3026
|
+
// has to stop above it: the old `height - offsetTop - 8` let the field
|
|
3027
|
+
// run 40px under the footer, and the last lines of a long block were
|
|
3028
|
+
// covered for good (the field scrolled, the footer never moved).
|
|
3029
|
+
const footerHeight =
|
|
3030
|
+
footerEl !== null && footerEl !== undefined && typeof footerEl.offsetHeight === 'number' && footerEl.offsetHeight > 0
|
|
3031
|
+
? footerEl.offsetHeight
|
|
3032
|
+
: FOOTER_BAND
|
|
3033
|
+
const available = Math.max(40, height - offsetTop - footerHeight)
|
|
2663
3034
|
area.style.position = 'absolute'
|
|
2664
3035
|
area.style.boxSizing = 'border-box'
|
|
2665
3036
|
area.style.left = `${Math.round(offsetLeft)}px`
|
|
@@ -2682,6 +3053,73 @@ window.__ModuleLoader__.load({
|
|
|
2682
3053
|
}
|
|
2683
3054
|
}
|
|
2684
3055
|
|
|
3056
|
+
/**
|
|
3057
|
+
* Run `work` without losing the reader's place.
|
|
3058
|
+
*
|
|
3059
|
+
* Measuring a textarea means collapsing it (`height: auto`), which shrinks
|
|
3060
|
+
* the document; the browser then clamps the scroller's `scrollTop` to the
|
|
3061
|
+
* new maximum, and the position is gone even though the heights come back.
|
|
3062
|
+
* The host re-renders constantly (a streaming turn, a state refresh), so
|
|
3063
|
+
* this is what "scroll down and it jumps back up" was. The position is
|
|
3064
|
+
* captured before the work and put back after.
|
|
3065
|
+
*
|
|
3066
|
+
* @param scroller - the scrolling element that holds the textareas.
|
|
3067
|
+
* @param work - the measuring pass.
|
|
3068
|
+
* @returns whatever `work` returned.
|
|
3069
|
+
*/
|
|
3070
|
+
function withScrollKept(scroller, work) {
|
|
3071
|
+
const keep =
|
|
3072
|
+
scroller !== null && scroller !== undefined && typeof scroller.scrollTop === 'number'
|
|
3073
|
+
? scroller.scrollTop
|
|
3074
|
+
: null
|
|
3075
|
+
const result = work()
|
|
3076
|
+
if (keep !== null && scroller.scrollTop !== keep) scroller.scrollTop = keep
|
|
3077
|
+
return result
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
/**
|
|
3081
|
+
* The content height of one textarea, SETTLED and applied.
|
|
3082
|
+
*
|
|
3083
|
+
* One measurement is not enough. With the height reset to `auto`, the first
|
|
3084
|
+
* read comes back exactly one line short - the wrap is resolved after the
|
|
3085
|
+
* style write - and the element is then pinned to that height with
|
|
3086
|
+
* `overflow:hidden`. In the all-blocks page that made the last line of a
|
|
3087
|
+
* long body unreachable: the surrounding scroller's height is computed from
|
|
3088
|
+
* the pinned value, so it cannot scroll to a line the textarea is hiding
|
|
3089
|
+
* (reported from the field: a long 正文 could not be scrolled past).
|
|
3090
|
+
*
|
|
3091
|
+
* Measured live: first read 2392, second read 2415, line height 23px.
|
|
3092
|
+
*
|
|
3093
|
+
* @param area - the textarea to measure and size.
|
|
3094
|
+
* @returns the applied height in CSS pixels, or 0 when unmeasurable.
|
|
3095
|
+
*/
|
|
3096
|
+
function settleAreaHeight(area) {
|
|
3097
|
+
if (area === null || area === undefined || typeof area.scrollHeight !== 'number') return 0
|
|
3098
|
+
area.style.height = 'auto'
|
|
3099
|
+
const first = area.scrollHeight
|
|
3100
|
+
if (!(typeof first === 'number' && first > 0)) return 0
|
|
3101
|
+
area.style.height = `${first}px`
|
|
3102
|
+
const settled = area.scrollHeight
|
|
3103
|
+
const height = typeof settled === 'number' && settled > first ? settled : first
|
|
3104
|
+
area.style.height = `${height}px`
|
|
3105
|
+
return height
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
/**
|
|
3109
|
+
* One block's identity inside the editor's draft store.
|
|
3110
|
+
*
|
|
3111
|
+
* A turn's blocks are addressed by (seq, index): the same block reached from
|
|
3112
|
+
* the 正文 page and from its sub-page inside 全部 must resolve to the SAME
|
|
3113
|
+
* key, or an unsaved edit disappears when the user switches pages.
|
|
3114
|
+
*
|
|
3115
|
+
* @param block - an editor block (seq, index).
|
|
3116
|
+
* @returns the stable key.
|
|
3117
|
+
*/
|
|
3118
|
+
function blockDraftKey(block) {
|
|
3119
|
+
if (block === null || block === undefined) return ''
|
|
3120
|
+
return `${typeof block.seq === 'number' ? block.seq : ''}:${block.index}`
|
|
3121
|
+
}
|
|
3122
|
+
|
|
2685
3123
|
// Grow the textarea to its content, the way a normal input box behaves.
|
|
2686
3124
|
// Without this it sits at a fixed `rows` height and a two-line edit floats
|
|
2687
3125
|
// in half an empty box. The stylesheet owns the limits (`min-height`,
|
|
@@ -2691,9 +3129,12 @@ window.__ModuleLoader__.load({
|
|
|
2691
3129
|
|
|
2692
3130
|
function fitEditorHeight(area) {
|
|
2693
3131
|
if (typeof area.closest === 'function' && area.closest('[data-dshet-inplace="1"]') !== null) return
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
3132
|
+
const scroller =
|
|
3133
|
+
typeof area.closest === 'function'
|
|
3134
|
+
? area.closest('.dshet-all') ?? area.closest('.dshet-editor')
|
|
3135
|
+
: null
|
|
3136
|
+
const height = withScrollKept(scroller, () => settleAreaHeight(area))
|
|
3137
|
+
if (height > 0) area.style.height = `${height + 2}px`
|
|
2697
3138
|
}
|
|
2698
3139
|
|
|
2699
3140
|
// Wire an action so a host re-render cannot eat the click.
|
|
@@ -2840,6 +3281,9 @@ window.__ModuleLoader__.load({
|
|
|
2840
3281
|
// saving would write, so both have to redraw the strip - and the chip
|
|
2841
3282
|
// keys are enough to tell one list from another.
|
|
2842
3283
|
view.parts.map((part) => part.key).join('.'),
|
|
3284
|
+
// The mode itself: leaving the all-blocks view must rebuild even when the
|
|
3285
|
+
// single block that comes back is the one that was selected before it.
|
|
3286
|
+
view.allBlocks === true ? 'all' : 'one',
|
|
2843
3287
|
// The selected reply block: switching blocks has to rebuild the box.
|
|
2844
3288
|
view.blockIndex === null || view.blockIndex === undefined ? '' : String(view.blockIndex),
|
|
2845
3289
|
view.targetSeq === null || view.targetSeq === undefined ? '' : String(view.targetSeq),
|
|
@@ -2977,7 +3421,7 @@ window.__ModuleLoader__.load({
|
|
|
2977
3421
|
? controller.editorBox
|
|
2978
3422
|
: null
|
|
2979
3423
|
if (existing && existing.dataset.dshetFor === marker) {
|
|
2980
|
-
placeEditor(row, existing, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
3424
|
+
placeEditor(row, existing, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks, controller)
|
|
2981
3425
|
return
|
|
2982
3426
|
}
|
|
2983
3427
|
|
|
@@ -3078,13 +3522,13 @@ window.__ModuleLoader__.load({
|
|
|
3078
3522
|
fitEditorHeight(area)
|
|
3079
3523
|
const row = controller.editorRow
|
|
3080
3524
|
const box = controller.editorBox
|
|
3081
|
-
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
3525
|
+
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks, controller)
|
|
3082
3526
|
})
|
|
3083
3527
|
area.addEventListener('compositionend', () => {
|
|
3084
3528
|
fitEditorHeight(area)
|
|
3085
3529
|
const row = controller.editorRow
|
|
3086
3530
|
const box = controller.editorBox
|
|
3087
|
-
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
3531
|
+
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks, controller)
|
|
3088
3532
|
})
|
|
3089
3533
|
area.addEventListener('focus', () => {
|
|
3090
3534
|
controller.editorFocus = true
|
|
@@ -3099,13 +3543,15 @@ window.__ModuleLoader__.load({
|
|
|
3099
3543
|
if (event.isComposing === true || event.keyCode === 229) return
|
|
3100
3544
|
if (event.key === 'Enter' && event.shiftKey !== true) {
|
|
3101
3545
|
event.preventDefault()
|
|
3102
|
-
|
|
3546
|
+
const live = liveView()
|
|
3547
|
+
if (live !== null && live !== undefined && live.confirming === true) controller.confirm()
|
|
3103
3548
|
else controller.review()
|
|
3104
3549
|
return
|
|
3105
3550
|
}
|
|
3106
3551
|
if (event.key === 'Escape') {
|
|
3107
3552
|
event.preventDefault()
|
|
3108
|
-
|
|
3553
|
+
const live = liveView()
|
|
3554
|
+
if (live !== null && live !== undefined && live.confirming === true) controller.back()
|
|
3109
3555
|
else controller.close()
|
|
3110
3556
|
}
|
|
3111
3557
|
})
|
|
@@ -3155,15 +3601,15 @@ window.__ModuleLoader__.load({
|
|
|
3155
3601
|
blockArea.style.position = 'static'
|
|
3156
3602
|
blockArea.style.left = 'auto'
|
|
3157
3603
|
blockArea.style.top = 'auto'
|
|
3158
|
-
blockArea.value = controller.
|
|
3604
|
+
blockArea.value = controller.draftOfBlock(block)
|
|
3159
3605
|
blockArea.disabled = view.pending || view.confirming
|
|
3160
|
-
const grow = () =>
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3606
|
+
const grow = () =>
|
|
3607
|
+
withScrollKept(
|
|
3608
|
+
typeof blockArea.closest === 'function' ? blockArea.closest('.dshet-all') : null,
|
|
3609
|
+
() => settleAreaHeight(blockArea),
|
|
3610
|
+
)
|
|
3165
3611
|
blockArea.addEventListener('input', () => {
|
|
3166
|
-
|
|
3612
|
+
controller.setDraftFor(block, blockArea.value)
|
|
3167
3613
|
grow()
|
|
3168
3614
|
})
|
|
3169
3615
|
section.appendChild(head)
|
|
@@ -3234,14 +3680,34 @@ window.__ModuleLoader__.load({
|
|
|
3234
3680
|
warn.textContent = texts.warn
|
|
3235
3681
|
box.appendChild(warn)
|
|
3236
3682
|
}
|
|
3237
|
-
|
|
3683
|
+
// A running turn only blocks the paths that move the surface (a prompt
|
|
3684
|
+
// rewrite, or a reply edit on a host without the in-place record). The
|
|
3685
|
+
// in-place record appends one event and changes nothing that is already
|
|
3686
|
+
// on the surface, so waiting for the agent to finish is not required -
|
|
3687
|
+
// the model picks the edit up on its next fold.
|
|
3688
|
+
const inPlaceEdit = target.mode === 'reply' && target.patch === true
|
|
3689
|
+
const busyBlocked = view.busy === true && !inPlaceEdit
|
|
3690
|
+
const failureCode = typeof view.failure === 'string' && view.failure !== ''
|
|
3691
|
+
? view.failure
|
|
3692
|
+
: busyBlocked
|
|
3693
|
+
? 'busy'
|
|
3694
|
+
: null
|
|
3695
|
+
if (failureCode !== null) {
|
|
3238
3696
|
const error = document.createElement('p')
|
|
3239
3697
|
error.className = 'dshet-error'
|
|
3240
3698
|
error.setAttribute('role', 'status')
|
|
3241
|
-
|
|
3699
|
+
const detail = typeof view.failureDetail === 'string' && view.failureDetail !== '' ? view.failureDetail : null
|
|
3700
|
+
error.textContent = (failureCode === 'internal' || failureCode === 'generic') && detail !== null
|
|
3701
|
+
? `${errorText(t, failureCode)}(${detail.slice(0, 300)})`
|
|
3702
|
+
: errorText(t, failureCode)
|
|
3242
3703
|
box.appendChild(error)
|
|
3243
3704
|
}
|
|
3244
3705
|
|
|
3706
|
+
// Handlers must read the CURRENT controller state: the host may not have
|
|
3707
|
+
// rebuilt the button after the first press, and a stale `view.confirming`
|
|
3708
|
+
// made the confirmation step a dead end (press again -> review again).
|
|
3709
|
+
const liveView = () => (typeof controller.getSnapshot === 'function' ? controller.getSnapshot() : view)
|
|
3710
|
+
|
|
3245
3711
|
const footer = document.createElement('div')
|
|
3246
3712
|
footer.className = 'dshet-footer'
|
|
3247
3713
|
const footerActions = document.createElement('div')
|
|
@@ -3282,11 +3748,17 @@ window.__ModuleLoader__.load({
|
|
|
3282
3748
|
submit.appendChild(submitText)
|
|
3283
3749
|
submit.title = `${submitLabel} (Enter)`
|
|
3284
3750
|
submit.setAttribute('aria-label', submitLabel)
|
|
3285
|
-
submit.disabled = view.pending
|
|
3751
|
+
submit.disabled = view.pending || busyBlocked
|
|
3286
3752
|
pressable(submit, () => {
|
|
3287
|
-
|
|
3753
|
+
const live = liveView()
|
|
3754
|
+
if (live !== null && live !== undefined && live.confirming === true) controller.confirm()
|
|
3288
3755
|
else controller.review()
|
|
3289
3756
|
})
|
|
3757
|
+
if (view.confirming === true) {
|
|
3758
|
+
// The save label is visually hidden, so the confirmation step needs a
|
|
3759
|
+
// visible state of its own or the first press looks like nothing.
|
|
3760
|
+
submit.classList.add('dshet-btn-confirm')
|
|
3761
|
+
}
|
|
3290
3762
|
|
|
3291
3763
|
footerActions.appendChild(cancel)
|
|
3292
3764
|
// The re-run button belongs to the sibling (dsh-rerun-turn): it appears
|
|
@@ -3338,7 +3810,7 @@ window.__ModuleLoader__.load({
|
|
|
3338
3810
|
controller.editorBox = box
|
|
3339
3811
|
controller.editorRow = row
|
|
3340
3812
|
controller.editorElement = area
|
|
3341
|
-
placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
3813
|
+
placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks, controller)
|
|
3342
3814
|
fitEditorHeight(area)
|
|
3343
3815
|
if (!view.confirming && !view.pending && (fresh || wasFocused)) {
|
|
3344
3816
|
try {
|
|
@@ -3377,8 +3849,11 @@ window.__ModuleLoader__.load({
|
|
|
3377
3849
|
// collapsed panel that may not be in the DOM at all.
|
|
3378
3850
|
|
|
3379
3851
|
const PATCH_ATTR = 'data-dshet-patch'
|
|
3380
|
-
const NOTE_ATTR = 'data-dshet-patch-note'
|
|
3381
3852
|
const patchOriginals = new WeakMap()
|
|
3853
|
+
// The text this plugin last painted for a key, so a second edit of the same
|
|
3854
|
+
// block can still find the element after the first text replaced the
|
|
3855
|
+
// recorded original in the DOM.
|
|
3856
|
+
const patchedTexts = new Map()
|
|
3382
3857
|
// React may re-render the assistant container and reset its attributes and
|
|
3383
3858
|
// style while leaving the text node we replaced. Remember the element by
|
|
3384
3859
|
// seq:block so the next pass can restore the marker and, on a later edit,
|
|
@@ -3456,10 +3931,11 @@ window.__ModuleLoader__.load({
|
|
|
3456
3931
|
// element in the owning row whose whole text is exactly the recorded text;
|
|
3457
3932
|
// never a button, a timestamp, our own chrome, or an ancestor that merely
|
|
3458
3933
|
// contains the text.
|
|
3459
|
-
function findPatchTextFallback(owner, wanted) {
|
|
3934
|
+
function findPatchTextFallback(owner, wanted, previousText = '') {
|
|
3460
3935
|
if (owner === null || owner === undefined || typeof owner.querySelectorAll !== 'function') return null
|
|
3461
3936
|
const original = matchText(wanted.original)
|
|
3462
|
-
|
|
3937
|
+
const previous = matchText(previousText)
|
|
3938
|
+
if (original === '' && previous === '') return null
|
|
3463
3939
|
let scope = owner
|
|
3464
3940
|
try {
|
|
3465
3941
|
const node = owner.querySelector('[data-slot="conversation.chat.node"]')
|
|
@@ -3471,12 +3947,12 @@ window.__ModuleLoader__.load({
|
|
|
3471
3947
|
if (el === scope) continue
|
|
3472
3948
|
if (el.getAttribute(PATCH_ATTR) !== null) continue
|
|
3473
3949
|
try {
|
|
3474
|
-
if (el.closest('.dshet-editor, .dshet-action, .dshet-pick,
|
|
3950
|
+
if (el.closest('.dshet-editor, .dshet-action, .dshet-pick, button, time') !== null) continue
|
|
3475
3951
|
} catch {
|
|
3476
3952
|
continue
|
|
3477
3953
|
}
|
|
3478
3954
|
const text = matchText(el.textContent)
|
|
3479
|
-
if (text !== original) continue
|
|
3955
|
+
if (text !== original && !(previous !== '' && text === previous)) continue
|
|
3480
3956
|
let deeper = false
|
|
3481
3957
|
for (const child of el.children ?? []) {
|
|
3482
3958
|
if (matchText(child.textContent) === text) {
|
|
@@ -3489,6 +3965,140 @@ window.__ModuleLoader__.load({
|
|
|
3489
3965
|
return null
|
|
3490
3966
|
}
|
|
3491
3967
|
|
|
3968
|
+
/**
|
|
3969
|
+
* The collapsed reasoning header draws its own truncation of the RECORDED
|
|
3970
|
+
* text ("思考 · ..."), and when the panel is closed the body is not in the
|
|
3971
|
+
* DOM at all - so a patch of the reasoning body alone leaves the edit
|
|
3972
|
+
* invisible. Rewrite that snippet too, keeping whatever the host appended
|
|
3973
|
+
* (its ellipsis) and its own truncation length.
|
|
3974
|
+
*/
|
|
3975
|
+
function patchReasoningSummary(owner, wanted) {
|
|
3976
|
+
if (owner === null || owner === undefined || typeof owner.querySelectorAll !== 'function') return false
|
|
3977
|
+
const original = String(wanted.original ?? '')
|
|
3978
|
+
const patched = String(wanted.text ?? '')
|
|
3979
|
+
if (original === '' || patched === '') return false
|
|
3980
|
+
const head = original.slice(0, 12)
|
|
3981
|
+
let changed = false
|
|
3982
|
+
const apply = (get, set) => {
|
|
3983
|
+
const data = String(get() ?? '')
|
|
3984
|
+
if (data === '' || !data.includes(head)) return
|
|
3985
|
+
// A node holding the whole text gets the whole replacement; a truncated
|
|
3986
|
+
// snippet keeps the host's length and gains an ellipsis when the new
|
|
3987
|
+
// text is longer.
|
|
3988
|
+
const next = data.length >= original.length ? patched : `${patched.slice(0, data.length)}${patched.length > data.length ? '…' : ''}`
|
|
3989
|
+
if (next === data) return
|
|
3990
|
+
set(next)
|
|
3991
|
+
changed = true
|
|
3992
|
+
}
|
|
3993
|
+
const walk = (node) => {
|
|
3994
|
+
for (const child of node.childNodes ?? []) {
|
|
3995
|
+
const type = child.nodeType
|
|
3996
|
+
if (type === 3) {
|
|
3997
|
+
apply(() => child.data, (value) => { child.data = value })
|
|
3998
|
+
continue
|
|
3999
|
+
}
|
|
4000
|
+
if (type !== 1) continue
|
|
4001
|
+
try {
|
|
4002
|
+
if (typeof child.closest === 'function' && child.closest('.dshet-editor, .dshet-action, .dshet-pick, button, time') !== null) continue
|
|
4003
|
+
} catch {
|
|
4004
|
+
/* a node without closest is walked anyway */
|
|
4005
|
+
}
|
|
4006
|
+
const kids = child.childNodes ?? []
|
|
4007
|
+
if (kids.length === 0) {
|
|
4008
|
+
apply(() => child.textContent, (value) => { child.textContent = value })
|
|
4009
|
+
continue
|
|
4010
|
+
}
|
|
4011
|
+
walk(child)
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
walk(owner)
|
|
4015
|
+
return changed
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
/**
|
|
4019
|
+
* The reasoning body on this host is its own row (the flow key carries a
|
|
4020
|
+
* `,reasoning` group part), so neither the answer row nor the first chat node
|
|
4021
|
+
* the shared fallback picks contains it. Search the whole conversation for
|
|
4022
|
+
* the deepest element whose text is the recorded original (or what we last
|
|
4023
|
+
* painted there).
|
|
4024
|
+
*/
|
|
4025
|
+
/**
|
|
4026
|
+
* EVERY element that shows this reasoning text.
|
|
4027
|
+
*
|
|
4028
|
+
* Plural on purpose: the host keeps more than one copy of a turn's reasoning.
|
|
4029
|
+
* The collapsed process row stays in the DOM with `display: none`, and opening
|
|
4030
|
+
* the panel renders a second, visible body. Patching only the first one left
|
|
4031
|
+
* the copy the user was looking at showing the recorded text.
|
|
4032
|
+
*
|
|
4033
|
+
* @param root - the conversation root to search.
|
|
4034
|
+
* @param wanted - the patch (`original` text, new `text`).
|
|
4035
|
+
* @param previousText - what the patch showed before this pass, if anything.
|
|
4036
|
+
* @returns the deepest matching elements, best match first.
|
|
4037
|
+
*/
|
|
4038
|
+
function findReasoningElements(root, wanted, previousText) {
|
|
4039
|
+
if (root === null || root === undefined || typeof root.querySelectorAll !== 'function') return []
|
|
4040
|
+
const original = matchText(wanted.original)
|
|
4041
|
+
const previous = matchText(previousText)
|
|
4042
|
+
if (original === '' && previous === '') return []
|
|
4043
|
+
const found = []
|
|
4044
|
+
for (const el of root.querySelectorAll('*')) {
|
|
4045
|
+
if (el.getAttribute(PATCH_ATTR) !== null) continue
|
|
4046
|
+
try {
|
|
4047
|
+
if (el.closest('.dshet-editor, .dshet-action, .dshet-pick, button, time') !== null) continue
|
|
4048
|
+
} catch {
|
|
4049
|
+
/* a detached node is still a candidate */
|
|
4050
|
+
}
|
|
4051
|
+
const text = matchText(el.textContent)
|
|
4052
|
+
if (text === '') continue
|
|
4053
|
+
if (text !== original && !(previous !== '' && text === previous)) continue
|
|
4054
|
+
let deeper = false
|
|
4055
|
+
for (const child of el.children ?? []) {
|
|
4056
|
+
if (matchText(child.textContent) === text) {
|
|
4057
|
+
deeper = true
|
|
4058
|
+
break
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
if (deeper) continue
|
|
4062
|
+
found.push({ el, score: Math.abs(text.length - original.length) })
|
|
4063
|
+
}
|
|
4064
|
+
found.sort((a, b) => a.score - b.score)
|
|
4065
|
+
return found.map((entry) => entry.el)
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function findReasoningElement(root, wanted, previousText) {
|
|
4069
|
+
if (root === null || root === undefined || typeof root.querySelectorAll !== 'function') return null
|
|
4070
|
+
const original = matchText(wanted.original)
|
|
4071
|
+
const previous = matchText(previousText)
|
|
4072
|
+
if (original === '' && previous === '') return null
|
|
4073
|
+
let best = null
|
|
4074
|
+
let bestScore = Infinity
|
|
4075
|
+
for (const el of root.querySelectorAll('*')) {
|
|
4076
|
+
if (el.getAttribute(PATCH_ATTR) !== null) continue
|
|
4077
|
+
try {
|
|
4078
|
+
if (el.closest('.dshet-editor, .dshet-action, .dshet-pick, button, time') !== null) continue
|
|
4079
|
+
} catch {
|
|
4080
|
+
/* a detached node is still a candidate */
|
|
4081
|
+
}
|
|
4082
|
+
const text = matchText(el.textContent)
|
|
4083
|
+
if (text === '') continue
|
|
4084
|
+
if (text !== original && !(previous !== '' && text === previous)) continue
|
|
4085
|
+
let deeper = false
|
|
4086
|
+
for (const child of el.children ?? []) {
|
|
4087
|
+
if (matchText(child.textContent) === text) {
|
|
4088
|
+
deeper = true
|
|
4089
|
+
break
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
if (deeper) continue
|
|
4093
|
+
const score = Math.abs(text.length - original.length)
|
|
4094
|
+
if (score < bestScore) {
|
|
4095
|
+
best = el
|
|
4096
|
+
bestScore = score
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
return best
|
|
4100
|
+
}
|
|
4101
|
+
|
|
3492
4102
|
function applyPatchDisplay(snapshot, view, t) {
|
|
3493
4103
|
// Map a reply seq to the host row that draws it, so a short answer like
|
|
3494
4104
|
// "好的" cannot be matched in a different row. Fall back to a global
|
|
@@ -3522,11 +4132,40 @@ window.__ModuleLoader__.load({
|
|
|
3522
4132
|
if (root === null) return
|
|
3523
4133
|
const replies = view.replies instanceof Map ? view.replies : new Map()
|
|
3524
4134
|
const wantedText = new Map()
|
|
3525
|
-
const
|
|
4135
|
+
const wantedReasoning = new Map()
|
|
3526
4136
|
for (const patch of Array.isArray(view.patches) ? view.patches : []) {
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
4137
|
+
// Find the block by (seq, index) AND the entry that carries it.
|
|
4138
|
+
//
|
|
4139
|
+
// One reply entry carries its whole TURN's blocks, so several blocks
|
|
4140
|
+
// share an index - the first step's reasoning and the fourth step's
|
|
4141
|
+
// reasoning are both "index 0" - and a later step's block lives in the
|
|
4142
|
+
// entry keyed by the ANSWER's seq, not by its own. Keying the lookup by
|
|
4143
|
+
// `patch.seq` alone found no entry, and `find(index === ...)` alone found
|
|
4144
|
+
// the wrong block: either way the patch matched nothing in the DOM and
|
|
4145
|
+
// the reasoning body kept the recorded text until a reload (reported from
|
|
4146
|
+
// the field for the 思维链 page; a save on another step's reasoning wrote
|
|
4147
|
+
// the record and /state reported it, yet the transcript did not move).
|
|
4148
|
+
let reply = replies.get(patch.seq)
|
|
4149
|
+
let block = Array.isArray(reply?.blocks)
|
|
4150
|
+
? reply.blocks.find(
|
|
4151
|
+
(entry) =>
|
|
4152
|
+
entry.index === patch.blockIndex &&
|
|
4153
|
+
(entry.seq === undefined || patch.seq === undefined || entry.seq === patch.seq),
|
|
4154
|
+
)
|
|
4155
|
+
: undefined
|
|
4156
|
+
if (block === undefined) {
|
|
4157
|
+
for (const candidate of replies.values()) {
|
|
4158
|
+
if (!Array.isArray(candidate?.blocks)) continue
|
|
4159
|
+
const found = candidate.blocks.find(
|
|
4160
|
+
(entry) => entry.index === patch.blockIndex && entry.seq === patch.seq,
|
|
4161
|
+
)
|
|
4162
|
+
if (found !== undefined) {
|
|
4163
|
+
reply = candidate
|
|
4164
|
+
block = found
|
|
4165
|
+
break
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
3530
4169
|
if (block === undefined) continue
|
|
3531
4170
|
if (patch.kind === 'text') {
|
|
3532
4171
|
wantedText.set(`${patch.seq}:${patch.blockIndex}`, {
|
|
@@ -3536,8 +4175,10 @@ window.__ModuleLoader__.load({
|
|
|
3536
4175
|
text: String(patch.text ?? ''),
|
|
3537
4176
|
})
|
|
3538
4177
|
} else if (patch.kind === 'reasoning') {
|
|
3539
|
-
|
|
4178
|
+
wantedReasoning.set(`${patch.seq}:${patch.blockIndex}`, {
|
|
3540
4179
|
key: `${patch.seq}:${patch.blockIndex}`,
|
|
4180
|
+
seq: patch.seq,
|
|
4181
|
+
original: String(block.original ?? ''),
|
|
3541
4182
|
text: String(patch.text ?? ''),
|
|
3542
4183
|
reply,
|
|
3543
4184
|
block,
|
|
@@ -3548,7 +4189,7 @@ window.__ModuleLoader__.load({
|
|
|
3548
4189
|
// A patch that no longer exists must give the host's recorded text back,
|
|
3549
4190
|
// even when a re-render already stripped our marker from the element.
|
|
3550
4191
|
for (const [key, el] of [...patchedElements]) {
|
|
3551
|
-
if (wantedText.has(key)) continue
|
|
4192
|
+
if (wantedText.has(key) || wantedReasoning.has(key)) continue
|
|
3552
4193
|
patchedElements.delete(key)
|
|
3553
4194
|
if (el.isConnected === true) restorePatchedBlock(el)
|
|
3554
4195
|
}
|
|
@@ -3566,18 +4207,14 @@ window.__ModuleLoader__.load({
|
|
|
3566
4207
|
el.textContent = shown
|
|
3567
4208
|
}
|
|
3568
4209
|
}
|
|
3569
|
-
|
|
3570
|
-
const key = note.getAttribute(NOTE_ATTR)
|
|
3571
|
-
if (!wantedNotes.has(key)) note.remove()
|
|
3572
|
-
}
|
|
3573
|
-
if (wantedText.size === 0 && wantedNotes.size === 0) return
|
|
4210
|
+
if (wantedText.size === 0 && wantedReasoning.size === 0) return
|
|
3574
4211
|
|
|
3575
4212
|
const nodes = [...root.querySelectorAll('[data-slot="conversation.chat.node"]')]
|
|
3576
4213
|
const containers = []
|
|
3577
4214
|
for (const node of nodes) {
|
|
3578
4215
|
for (const el of node.querySelectorAll('*')) {
|
|
3579
4216
|
try {
|
|
3580
|
-
if (isMarkdownContainer(el) && el.closest('.dshet-editor, .dshet-pick
|
|
4217
|
+
if (isMarkdownContainer(el) && el.closest('.dshet-editor, .dshet-pick') === null) containers.push(el)
|
|
3581
4218
|
} catch {
|
|
3582
4219
|
/* a detached node is skipped */
|
|
3583
4220
|
}
|
|
@@ -3608,7 +4245,8 @@ window.__ModuleLoader__.load({
|
|
|
3608
4245
|
continue
|
|
3609
4246
|
}
|
|
3610
4247
|
const original = matchText(wanted.original)
|
|
3611
|
-
|
|
4248
|
+
const previous = matchText(patchedTexts.get(key) ?? '')
|
|
4249
|
+
if (original === '' && previous === '') continue
|
|
3612
4250
|
let best = null
|
|
3613
4251
|
let bestScore = Infinity
|
|
3614
4252
|
for (const el of containers) {
|
|
@@ -3622,7 +4260,7 @@ window.__ModuleLoader__.load({
|
|
|
3622
4260
|
}
|
|
3623
4261
|
const text = matchText(el.textContent)
|
|
3624
4262
|
if (text === '') continue
|
|
3625
|
-
const exact = text === original
|
|
4263
|
+
const exact = text === original || (previous !== '' && text === previous)
|
|
3626
4264
|
const prefix = text.startsWith(original.slice(0, 60)) || original.startsWith(text.slice(0, 60))
|
|
3627
4265
|
if (!exact && !prefix) continue
|
|
3628
4266
|
// Prefer the exact match, then the container closest in length: a
|
|
@@ -3634,47 +4272,87 @@ window.__ModuleLoader__.load({
|
|
|
3634
4272
|
bestScore = score
|
|
3635
4273
|
}
|
|
3636
4274
|
}
|
|
3637
|
-
if (best === null) best = findPatchTextFallback(owner, wanted)
|
|
4275
|
+
if (best === null) best = findPatchTextFallback(owner, wanted, patchedTexts.get(key) ?? '')
|
|
3638
4276
|
if (best === null) continue
|
|
3639
4277
|
patchOriginals.set(best, { html: best.innerHTML, text: best.textContent })
|
|
3640
4278
|
best.textContent = displayText(wanted.text)
|
|
3641
4279
|
best.setAttribute(PATCH_ATTR, key)
|
|
3642
4280
|
best.style.whiteSpace = 'pre-wrap'
|
|
3643
4281
|
patchedElements.set(key, best)
|
|
4282
|
+
patchedTexts.set(key, wanted.text)
|
|
3644
4283
|
}
|
|
3645
4284
|
|
|
3646
|
-
//
|
|
3647
|
-
//
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
const
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
4285
|
+
// A reasoning patch is drawn in place when the host renders that
|
|
4286
|
+
// reasoning body; the note is only the fallback for a panel that is not
|
|
4287
|
+
// in the DOM (or is drawn as a short summary without the full text).
|
|
4288
|
+
for (const [key, wanted] of wantedReasoning) {
|
|
4289
|
+
const owner = rowBySeq.get(wanted.seq)
|
|
4290
|
+
const shown = displayText(wanted.text)
|
|
4291
|
+
const previousText = patchedTexts.get(key) ?? ''
|
|
4292
|
+
const original = matchText(wanted.original)
|
|
4293
|
+
const previous = matchText(previousText)
|
|
4294
|
+
// EVERY copy of this reasoning, not just the one this pass remembers.
|
|
4295
|
+
//
|
|
4296
|
+
// The host keeps the collapsed process row in the DOM with `display:
|
|
4297
|
+
// none` and renders a SECOND body when the panel is opened. Trusting the
|
|
4298
|
+
// remembered element because it is still `isConnected` meant the copy the
|
|
4299
|
+
// user was looking at kept the recorded text until a reload - which is
|
|
4300
|
+
// exactly the reported "思维链页保存后不能马上渲染".
|
|
4301
|
+
const targets = new Set()
|
|
4302
|
+
const remembered = patchedElements.get(key)
|
|
4303
|
+
if (remembered !== undefined) {
|
|
4304
|
+
if (remembered.isConnected === true) targets.add(remembered)
|
|
4305
|
+
else patchedElements.delete(key)
|
|
4306
|
+
}
|
|
4307
|
+
for (const el of root.querySelectorAll(`[${PATCH_ATTR}]`)) {
|
|
4308
|
+
if (el.getAttribute(PATCH_ATTR) === key) targets.add(el)
|
|
4309
|
+
}
|
|
4310
|
+
if (original !== '' || previous !== '') {
|
|
4311
|
+
// The owning row first (a host that draws the reasoning inside the
|
|
4312
|
+
// answer row), then the whole conversation: on this host the reasoning
|
|
4313
|
+
// is its own row, so the answer row holds no reasoning text at all.
|
|
4314
|
+
// EVERY scope is merged, never just the first one that matched: the
|
|
4315
|
+
// point of this pass is to catch all copies at once.
|
|
4316
|
+
for (const scope of owner === undefined ? [null] : [owner, null]) {
|
|
4317
|
+
let foundInScope = false
|
|
4318
|
+
for (const el of containers) {
|
|
4319
|
+
if (el.getAttribute(PATCH_ATTR) !== null) continue
|
|
4320
|
+
if (scope !== null) {
|
|
4321
|
+
try {
|
|
4322
|
+
if (el.closest('[data-chat-flow-key]') !== scope) continue
|
|
4323
|
+
} catch {
|
|
4324
|
+
/* keep the global match */
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
const text = matchText(el.textContent)
|
|
4328
|
+
if (text === '') continue
|
|
4329
|
+
if (text !== original && !(previous !== '' && text === previous)) continue
|
|
4330
|
+
targets.add(el)
|
|
4331
|
+
foundInScope = true
|
|
4332
|
+
}
|
|
4333
|
+
if (foundInScope) break
|
|
3662
4334
|
}
|
|
4335
|
+
const fallback = findPatchTextFallback(owner ?? null, wanted, previousText)
|
|
4336
|
+
if (fallback !== null) targets.add(fallback)
|
|
4337
|
+
for (const el of findReasoningElements(root, wanted, previousText)) targets.add(el)
|
|
3663
4338
|
}
|
|
3664
|
-
if (
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
4339
|
+
if (targets.size === 0) {
|
|
4340
|
+
// No body in the DOM (the panel is closed): the header snippet is the
|
|
4341
|
+
// only place the edit can show.
|
|
4342
|
+
if (owner !== undefined) patchReasoningSummary(owner, wanted)
|
|
4343
|
+
patchReasoningSummary(root, wanted)
|
|
4344
|
+
continue
|
|
4345
|
+
}
|
|
4346
|
+
for (const el of targets) {
|
|
4347
|
+
if (!patchOriginals.has(el)) patchOriginals.set(el, { html: el.innerHTML, text: el.textContent })
|
|
4348
|
+
if (normalizedText(el.textContent) !== normalizedText(shown)) el.textContent = shown
|
|
4349
|
+
if (el.getAttribute(PATCH_ATTR) !== key) el.setAttribute(PATCH_ATTR, key)
|
|
4350
|
+
el.style.whiteSpace = 'pre-wrap'
|
|
4351
|
+
patchedElements.set(key, el)
|
|
3674
4352
|
}
|
|
3675
|
-
|
|
3676
|
-
if (
|
|
3677
|
-
|
|
4353
|
+
patchedTexts.set(key, wanted.text)
|
|
4354
|
+
if (owner !== undefined) patchReasoningSummary(owner, wanted)
|
|
4355
|
+
patchReasoningSummary(root, wanted)
|
|
3678
4356
|
}
|
|
3679
4357
|
}
|
|
3680
4358
|
|
|
@@ -3895,7 +4573,7 @@ window.__ModuleLoader__.load({
|
|
|
3895
4573
|
const key = `${Math.round(rect.top)}:${Math.round(rect.height)}:${Math.round(typeof window !== 'undefined' && typeof window.scrollY === 'number' ? window.scrollY : 0)}:${viewportWidth}x${viewportHeight}:${controller.editorBlockKind}:${controller.editorAll === true ? 'all' : ''}`
|
|
3896
4574
|
if (key === lastPlacementKey) return
|
|
3897
4575
|
lastPlacementKey = key
|
|
3898
|
-
placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
4576
|
+
placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks, controller)
|
|
3899
4577
|
}
|
|
3900
4578
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run)
|
|
3901
4579
|
else window.setTimeout(run, 16)
|
|
@@ -4292,6 +4970,31 @@ window.__ModuleLoader__.load({
|
|
|
4292
4970
|
// A reply's edit entry lives in the official assistant-actions strip, one
|
|
4293
4971
|
// entry per assistant message. order 5 puts it ahead of the feedback
|
|
4294
4972
|
// entries (order 10), so the pencil sits with the leading icons.
|
|
4973
|
+
// The active skin repaints its surfaces at any time (switching one
|
|
4974
|
+
// rewrites body's attributes and inline variables), so the palette is
|
|
4975
|
+
// resolved once and then again on every theme or skin change.
|
|
4976
|
+
ctx.effect(() => {
|
|
4977
|
+
const run = () => {
|
|
4978
|
+
try {
|
|
4979
|
+
resolveSkinPalette()
|
|
4980
|
+
} catch {
|
|
4981
|
+
/* the stylesheet defaults stand */
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
run()
|
|
4985
|
+
const observer =
|
|
4986
|
+
typeof MutationObserver === 'function' && document && document.body
|
|
4987
|
+
? new MutationObserver(run)
|
|
4988
|
+
: null
|
|
4989
|
+
if (observer !== null) {
|
|
4990
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-ds-dark-theme'] })
|
|
4991
|
+
if (document.documentElement) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'] })
|
|
4992
|
+
}
|
|
4993
|
+
return () => {
|
|
4994
|
+
if (observer !== null) observer.disconnect()
|
|
4995
|
+
}
|
|
4996
|
+
}, 'dsh-edit-turn:skin-palette')
|
|
4997
|
+
|
|
4295
4998
|
ctx.slots.inject('conversation.chat.assistant-actions', () =>
|
|
4296
4999
|
ctx.slots.register(
|
|
4297
5000
|
{
|