dsh-markdown-bubble 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/client.js +215 -41
- package/src/index.js +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,9 @@ const a = 1
|
|
|
95
95
|
|
|
96
96
|
### 修复记录
|
|
97
97
|
|
|
98
|
+
- **0.1.4** —— 按评审意见把降级路径**合成一个函数**:此前「合成失败」「宿主投影抛错」「最后兜底」各自写了一份降级逻辑(一份调宿主投影、一份内联拼裸文本),三处要人工保持同步。现在只有 `degradedBody()` 一处:先试宿主自己的投影(保住引用 chip 等宿主渲染),投影也抛才退到裸文本;三条降级路径全部走它。内容优先这一点写进函数契约:裸文本是最后一步,只有宿主自己拒绝投影时才到。新增回归:断言 `projectUserText` 全仓只有一次调用、裸文本 span 只有一处构造、降级调用方都走同一函数(再造一份降级该测试转红)。
|
|
99
|
+
- **0.1.3** —— 把「座位 render 绝不抛」这条不变式补完:0.1.2 守住了合成与文案查询,但仍有形状会在**座位自己的渲染里**抛错(`node` 缺失 → `node.data`、`props` 缺失 → 解构本身、`new Date(time)` 遇到 Symbol / BigInt / 抛错的 valueOf、宿主图片渲染器抛错)。这些同样冒泡到槽位机制、退掉整条条目,症状与 0.1.2 修的完全一样。现在三层兜底:座位本体一层 try 并把 `node` / `data` 的取用改成防御式;时钟与附件卡在各自调用点兜底;最后一层 `renderPlainRow` 保证最坏情况只是这一行退回纯文本,而操作条与兄弟插件锚点仍在。新增回归:21 种恶意宿主形状逐一断言不抛、且行与操作条都在。
|
|
100
|
+
- **0.1.2** — 座位组件不再可能因渲染期异常被槽位机制摘除:`composeUserMarkdown` / `buildMarkdownLabels` 在座位渲染过程中运行,抛错不会被错误边界接住(错误边界只能接住其后代,接不住创建它的那次渲染),会冒泡到槽位机制并把该条目整条退掉——屏幕上就是气泡交还给宿主、Markdown 变回源码。现在合成与所有文案查询都在座位内部兜底:任何宿主形状不符(节点结构异常、投影返回不可识别、locale 席位不可调用)都退回宿主原始投影,气泡外壳、操作条与兄弟插件锚点全部保留。`data.content` 非数组、`t` 不可调用等情形一并加进回归测试。
|
|
98
101
|
- **0.1.1** — 修复两个引用 chip 之间只有空白时该空白被当作空 Markdown 块丢弃的问题:相邻 chip 不再粘成 `@a@b`,chip 之间的换行也按宿主 `pre-wrap` 气泡的样子保留为换行(空白段渲染为 `data-dshmb-gap` 的预换行节点,仍不产生空 Markdown 块);自建的操作条 / 复制按钮补上 `data-dshmb-actions` / `data-dshmb-action` 命名空间标记;座位优先级 `-1` 的宿主依据(宿主默认 0、最低者渲染、同优先级抛错)与三个兄弟插件依赖的锚点结构一并加进回归测试。
|
|
99
102
|
|
|
100
103
|
### License
|
|
@@ -177,6 +180,9 @@ Queued-message previews, the composer itself and the goal panel stay plain text
|
|
|
177
180
|
|
|
178
181
|
### Fixes
|
|
179
182
|
|
|
183
|
+
- **0.1.4** — synthesizes the degradation paths into ONE function, as the reviewer asked: composition failure, a throwing host projection and the last-resort net each used to carry their own copy (one calling the host projection, one building the raw-text span inline), all kept in step by hand. There is now a single `degradedBody()`: it tries the host's own projection first — so reference chips and the host's own inline pieces survive — and only a projection that itself throws degrades to the raw text. Content first is now the function's contract: raw text is the last step, reached only when the host refuses to project. New regression: exactly one `projectUserText` call in the whole file, one raw-text span construction, and every degradation caller sharing the function (re-introducing a second copy turns it red).
|
|
184
|
+
- **0.1.3** — completes the "the seat render never throws" invariant: 0.1.2 guarded composition and the copy lookups, but shapes still threw in the seat's own render (an absent `node` -> `node.data`, absent props -> the destructuring itself, a `time` the Date constructor refuses — Symbol, BigInt, a `valueOf` that throws — and a host image renderer that throws). Each escaped to the slot machinery and retired the entry, the same symptom 0.1.2 fixed. Three layers now: one try around the seat body with defensive `node` / `data` access, per-call guards for the clock and the attachment card, and a last-resort `renderPlainRow` so the worst case is one row falling back to plain text with its action strip and the sibling anchors intact. Regression: 21 hostile host shapes, each asserted not to throw and to keep the row and its strip.
|
|
185
|
+
- **0.1.2** — the seat component can no longer be retired by a render-time exception: `composeUserMarkdown` / `buildMarkdownLabels` run while the seat renders, and a throw there is not caught by the error boundary (a boundary catches its descendants, not the render that creates it) — it escapes to the slot machinery, which drops the whole entry and hands the row back to the host, so the bubble reverts to raw source on screen. Composition and every copy lookup now degrade inside the seat: any host-shape mismatch (an unexpected node, an unrecognised projection result, a locale seat that is not callable) falls back to the host's plain projection while the bubble shell, the action strip and the sibling-plugin anchors stay intact. `data.content` that is not an array and an uncallable `t` are covered by regression tests.
|
|
180
186
|
- **0.1.1** — fixed whitespace-only plain runs between two reference chips being dropped as an empty Markdown block: adjacent chips no longer glue into `@a@b`, and a newline between two chips stays a line break the way the host's `pre-wrap` bubble shows it (the run renders as a `data-dshmb-gap` pre-wrap node, still never an empty Markdown block); the self-created action strip and copy button now carry the `data-dshmb-actions` / `data-dshmb-action` namespace markers; and the host evidence behind `priority: -1` (host default 0, lowest renders, same priority throws) plus the anchor structure the three sibling plugins rely on are covered by regression tests.
|
|
181
187
|
|
|
182
188
|
### License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-markdown-bubble",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Markdown rendering for DeepSeek Harness sent user messages: every message bubble renders lists, code, tables, quotes and inline formatting through the host's own Markdown pipeline, while queues, previews and the composer keep their plain projection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/client.js
CHANGED
|
@@ -49,7 +49,7 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
const NS = 'dsh-markdown-bubble'
|
|
50
50
|
|
|
51
51
|
/** Keep in sync with package.json and src/index.js. */
|
|
52
|
-
const PLUGIN_VERSION = '0.1.
|
|
52
|
+
const PLUGIN_VERSION = '0.1.4'
|
|
53
53
|
|
|
54
54
|
/** Presence marker the live verifier reads. */
|
|
55
55
|
const DEBUG_KEY = '__DSH_MARKDOWN_BUBBLE__'
|
|
@@ -230,16 +230,85 @@ window.__ModuleLoader__.load({
|
|
|
230
230
|
return react.isValidElement(piece) && typeof piece.props.children === 'string' ? piece.props.children : ''
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
233
|
+
/**
|
|
234
|
+
* A copy lookup that cannot take the seat down. The framework injects a
|
|
235
|
+
* `t` for the declared `chat` namespace, but a host-shape mismatch (a
|
|
236
|
+
* locale service that was never bound, a renamed key) must fall back to
|
|
237
|
+
* the key rather than throw during render.
|
|
238
|
+
* @param t - the injected chat translator, possibly not callable.
|
|
239
|
+
* @param key - the dictionary key.
|
|
240
|
+
* @param params - template parameters.
|
|
241
|
+
* @returns the translated string, or the key.
|
|
242
|
+
*/
|
|
243
|
+
function safeTranslate(t, key, params) {
|
|
244
|
+
if (typeof t !== 'function') return key
|
|
245
|
+
try {
|
|
246
|
+
return t(key, params)
|
|
247
|
+
} catch {
|
|
248
|
+
return key
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The ONE degraded projection: what this plugin shows whenever the markdown
|
|
254
|
+
* pipeline cannot be used.
|
|
255
|
+
*
|
|
256
|
+
* The host's own projection is tried first — it is the rendering the host
|
|
257
|
+
* would have used, so reference chips and inline pieces survive it — and a
|
|
258
|
+
* projection that throws degrades once more, to the raw text. Composition
|
|
259
|
+
* failure (see {@link safeMarkdownBody}), a boundary catch and the
|
|
260
|
+
* last-resort net (see {@link renderPlainRow}) ALL answer here, so the ways
|
|
261
|
+
* this plugin degrades cannot drift apart: there is no second copy of the
|
|
262
|
+
* fallback for someone to keep in step.
|
|
263
|
+
*
|
|
264
|
+
* Content comes first: the raw-text step is the last step, reached only when
|
|
265
|
+
* the host itself refuses to project.
|
|
266
|
+
*
|
|
267
|
+
* @param text - the message text.
|
|
268
|
+
* @param referenceLabels - session recall labels.
|
|
269
|
+
* @param skillNames - skill names.
|
|
270
|
+
* @param references - file/skill open callbacks.
|
|
271
|
+
* @returns the degraded body, never null and never a throw.
|
|
272
|
+
*/
|
|
273
|
+
function degradedBody(text, referenceLabels, skillNames, references) {
|
|
235
274
|
try {
|
|
236
275
|
return primitives.projectUserText(text, referenceLabels, skillNames, 'skill', references)
|
|
237
276
|
} catch (error) {
|
|
238
|
-
|
|
277
|
+
warnOnce('the host user-text projection failed; showing raw text', error)
|
|
239
278
|
return h('span', { className: 'dshmb-plain' }, text)
|
|
240
279
|
}
|
|
241
280
|
}
|
|
242
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Compose the markdown body without ever throwing.
|
|
284
|
+
*
|
|
285
|
+
* `composeUserMarkdown` runs while the seat component renders, so a throw
|
|
286
|
+
* here does NOT reach {@link MarkdownBoundary} — a boundary catches its
|
|
287
|
+
* descendants, not the parent render that creates it. It would escape to
|
|
288
|
+
* the slot machinery, which answers by retiring the whole entry and handing
|
|
289
|
+
* the row back to the host's raw renderer; on screen that is
|
|
290
|
+
* indistinguishable from "the plugin stopped working". A host-shape
|
|
291
|
+
* mismatch (an unexpected node shape, an unrecognised projection result)
|
|
292
|
+
* therefore falls back to {@link degradedBody} right here, inside our own
|
|
293
|
+
* render, so the bubble keeps its shell, its actions and the sibling
|
|
294
|
+
* plugins' injection anchors.
|
|
295
|
+
* @param text - the message text.
|
|
296
|
+
* @param t - the injected chat translator.
|
|
297
|
+
* @param referenceLabels - session recall labels.
|
|
298
|
+
* @param skillNames - skill names.
|
|
299
|
+
* @param references - file/skill open callbacks.
|
|
300
|
+
* @returns the composed body, or null when markdown cannot be produced.
|
|
301
|
+
*/
|
|
302
|
+
function safeMarkdownBody(text, t, referenceLabels, skillNames, references) {
|
|
303
|
+
try {
|
|
304
|
+
const labels = buildMarkdownLabels(t)
|
|
305
|
+
return composeUserMarkdown(text, labels, referenceLabels, skillNames, references)
|
|
306
|
+
} catch (error) {
|
|
307
|
+
warnOnce('markdown composition failed; showing the degraded projection', error)
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
243
312
|
/**
|
|
244
313
|
* A whitespace-only plain run: the gap between two chips, or the leading /
|
|
245
314
|
* trailing padding the host's own pre-wrap bubble shows. It carries no
|
|
@@ -277,7 +346,9 @@ window.__ModuleLoader__.load({
|
|
|
277
346
|
*/
|
|
278
347
|
function composeUserMarkdown(text, labels, referenceLabels, skillNames, references) {
|
|
279
348
|
if (typeof text !== 'string' || text === '') return null
|
|
280
|
-
|
|
349
|
+
// The same single degraded projection, so a projection that throws inside
|
|
350
|
+
// composition degrades exactly as one that throws on the healthy path.
|
|
351
|
+
const projected = degradedBody(text, referenceLabels, skillNames, references)
|
|
281
352
|
const pieces =
|
|
282
353
|
react.isValidElement(projected) && projected.type === Fragment && Array.isArray(projected.props.children)
|
|
283
354
|
? projected.props.children
|
|
@@ -330,15 +401,15 @@ window.__ModuleLoader__.load({
|
|
|
330
401
|
function buildMarkdownLabels(t) {
|
|
331
402
|
return {
|
|
332
403
|
code: {
|
|
333
|
-
copyLabel: t
|
|
334
|
-
copiedLabel: t
|
|
404
|
+
copyLabel: safeTranslate(t, 'copy'),
|
|
405
|
+
copiedLabel: safeTranslate(t, 'copied'),
|
|
335
406
|
toolbarLabels: {
|
|
336
|
-
codeLabel: t
|
|
337
|
-
wrapLabel: t
|
|
338
|
-
unwrapLabel: t
|
|
407
|
+
codeLabel: safeTranslate(t, 'codeBlock.title'),
|
|
408
|
+
wrapLabel: safeTranslate(t, 'codeBlock.wrap'),
|
|
409
|
+
unwrapLabel: safeTranslate(t, 'codeBlock.unwrap'),
|
|
339
410
|
},
|
|
340
411
|
},
|
|
341
|
-
footnotes: t
|
|
412
|
+
footnotes: safeTranslate(t, 'markdown.footnotes'),
|
|
342
413
|
}
|
|
343
414
|
}
|
|
344
415
|
|
|
@@ -385,6 +456,23 @@ window.__ModuleLoader__.load({
|
|
|
385
456
|
return day
|
|
386
457
|
}
|
|
387
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Log one degradation per distinct reason.
|
|
461
|
+
*
|
|
462
|
+
* The seat renders on every transcript update, so a hostile host shape would
|
|
463
|
+
* otherwise print the same warning on every pass and bury the one line that
|
|
464
|
+
* matters. Keyed by the reason rather than by the row: what a reader needs to
|
|
465
|
+
* know is which guard fired, once.
|
|
466
|
+
* @param reason - one line naming what degraded.
|
|
467
|
+
* @param error - the caught value.
|
|
468
|
+
*/
|
|
469
|
+
const warnedReasons = new Set()
|
|
470
|
+
function warnOnce(reason, error) {
|
|
471
|
+
if (warnedReasons.has(reason)) return
|
|
472
|
+
warnedReasons.add(reason)
|
|
473
|
+
console.warn('[' + NS + '] ' + reason, error)
|
|
474
|
+
}
|
|
475
|
+
|
|
388
476
|
/**
|
|
389
477
|
* The host's message clock: `HH:mm` for today, the `clock.md` date plus
|
|
390
478
|
* the clock earlier this year, the `clock.ymd` form for other years.
|
|
@@ -394,12 +482,25 @@ window.__ModuleLoader__.load({
|
|
|
394
482
|
* @returns the display string.
|
|
395
483
|
*/
|
|
396
484
|
function formatMessageClock(time, t, now) {
|
|
397
|
-
|
|
398
|
-
|
|
485
|
+
// `new Date(x)` throws on a Symbol, a BigInt, or an object whose `valueOf`
|
|
486
|
+
// throws — and the host hands `data.time` through unchanged. A throw here
|
|
487
|
+
// happens INSIDE the seat's subtree, outside MarkdownBoundary (the strip is
|
|
488
|
+
// not the markdown body), so it would escape to the slot machinery and
|
|
489
|
+
// retire the whole entry. A missing clock is a cosmetic loss; a retired
|
|
490
|
+
// renderer is "the plugin stopped working".
|
|
491
|
+
let date
|
|
492
|
+
let reference
|
|
493
|
+
try {
|
|
494
|
+
date = new Date(time)
|
|
495
|
+
reference = new Date(now)
|
|
496
|
+
} catch (error) {
|
|
497
|
+
warnOnce('the message time cannot be read; the clock is hidden', error)
|
|
498
|
+
return ''
|
|
499
|
+
}
|
|
399
500
|
const clock = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
|
|
400
501
|
if (date.getFullYear() === reference.getFullYear() && date.getMonth() === reference.getMonth() && date.getDate() === reference.getDate()) return clock
|
|
401
502
|
const params = { y: date.getFullYear(), m: date.getMonth() + 1, d: date.getDate() }
|
|
402
|
-
return `${date.getFullYear() === reference.getFullYear() ? t
|
|
503
|
+
return `${date.getFullYear() === reference.getFullYear() ? safeTranslate(t, 'clock.md', params) : safeTranslate(t, 'clock.ymd', params)} ${clock}`
|
|
403
504
|
}
|
|
404
505
|
|
|
405
506
|
// --- pieces -----------------------------------------------------------------
|
|
@@ -477,7 +578,7 @@ window.__ModuleLoader__.load({
|
|
|
477
578
|
|
|
478
579
|
const clock =
|
|
479
580
|
time === undefined ? null : h('span', { className: 'dshmb-timeStart' }, formatMessageClock(time, t, day))
|
|
480
|
-
const label = copied ? t
|
|
581
|
+
const label = copied ? safeTranslate(t, 'copied') : safeTranslate(t, 'copy')
|
|
481
582
|
|
|
482
583
|
return h(
|
|
483
584
|
'div',
|
|
@@ -501,22 +602,36 @@ window.__ModuleLoader__.load({
|
|
|
501
602
|
* @param props - the attachment entry and the owner's image renderer.
|
|
502
603
|
* @returns the row entry.
|
|
503
604
|
*/
|
|
504
|
-
function AttachmentEntry(
|
|
605
|
+
function AttachmentEntry(props) {
|
|
606
|
+
// The image branch calls the HOST's renderer during our render, and the
|
|
607
|
+
// file branch calls host primitives (`fileExtension`, `fileSizeText`). Both
|
|
608
|
+
// sit outside MarkdownBoundary, so either can retire the entry. One
|
|
609
|
+
// attachment is worth losing; the row is not.
|
|
610
|
+
try {
|
|
611
|
+
return renderAttachmentEntry(props)
|
|
612
|
+
} catch (error) {
|
|
613
|
+
warnOnce('an attachment could not be rendered; it is skipped', error)
|
|
614
|
+
return null
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function renderAttachmentEntry({ attachment, renderMessageImages, compactImages }) {
|
|
505
619
|
if (attachment.type === 'image') {
|
|
506
620
|
return h(Fragment, null, renderMessageImages({ images: [attachment.image], align: 'end', compact: compactImages }))
|
|
507
621
|
}
|
|
508
622
|
const file = attachment.file ?? {}
|
|
509
|
-
const
|
|
623
|
+
const name = typeof file.name === 'string' ? file.name : ''
|
|
624
|
+
const meta = [primitives.fileExtension(name).toUpperCase().slice(0, 8), primitives.fileSizeText(file.bytes)]
|
|
510
625
|
.filter(Boolean)
|
|
511
626
|
.join(' ')
|
|
512
627
|
return h(
|
|
513
628
|
'span',
|
|
514
|
-
{ className: 'dshmb-fileCard', title:
|
|
515
|
-
h(primitives.FileTypeIcon, { path:
|
|
629
|
+
{ className: 'dshmb-fileCard', title: name },
|
|
630
|
+
h(primitives.FileTypeIcon, { path: name, className: 'dshmb-fileIcon' }),
|
|
516
631
|
h(
|
|
517
632
|
'span',
|
|
518
633
|
{ className: 'dshmb-fileContent' },
|
|
519
|
-
h('span', { className: 'dshmb-fileName' },
|
|
634
|
+
h('span', { className: 'dshmb-fileName' }, name),
|
|
520
635
|
h('span', { className: 'dshmb-fileMeta' }, meta),
|
|
521
636
|
),
|
|
522
637
|
)
|
|
@@ -529,22 +644,48 @@ window.__ModuleLoader__.load({
|
|
|
529
644
|
* @param props - keyed chat renderer seat for the `user`/`steering` kinds.
|
|
530
645
|
* @returns the right-aligned bubble with attachments, markdown, chips and actions.
|
|
531
646
|
*/
|
|
532
|
-
const MarkdownBubbleSeat = react.memo(function MarkdownBubbleSeat({
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
647
|
+
const MarkdownBubbleSeat = react.memo(function MarkdownBubbleSeat(props) {
|
|
648
|
+
try {
|
|
649
|
+
return renderMarkdownBubble(props)
|
|
650
|
+
} catch (error) {
|
|
651
|
+
// Last-resort net. Everything that can throw is guarded where it is
|
|
652
|
+
// called (see safeMarkdownBody, degradedBody, formatMessageClock,
|
|
653
|
+
// AttachmentEntry); this exists so an UNKNOWN host shape degrades one
|
|
654
|
+
// row to plain text instead of retiring the renderer. The fallback still
|
|
655
|
+
// renders the action strip, because the three sibling plugins anchor
|
|
656
|
+
// their own buttons on `row.querySelector('[class*="_actions"]')`.
|
|
657
|
+
warnOnce('the markdown seat failed to render; showing the plain text', error)
|
|
658
|
+
return renderPlainRow(props)
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* The real seat body. Separated from the memo wrapper so the wrapper can hold
|
|
664
|
+
* one try/catch for the whole synchronous render.
|
|
665
|
+
* @param props - keyed chat renderer seat for the `user`/`steering` kinds.
|
|
666
|
+
* @returns the right-aligned bubble with attachments, markdown, chips and actions.
|
|
667
|
+
*/
|
|
668
|
+
function renderMarkdownBubble(props) {
|
|
669
|
+
const safeProps = props !== null && typeof props === 'object' ? props : {}
|
|
670
|
+
const node = safeProps.node !== null && typeof safeProps.node === 'object' ? safeProps.node : {}
|
|
671
|
+
const renderMessageImages = safeProps.renderMessageImages
|
|
672
|
+
const openFile = safeProps.openFile
|
|
673
|
+
const openSkill = safeProps.openSkill
|
|
674
|
+
const t = safeProps.t
|
|
675
|
+
const data = node.data !== null && typeof node.data === 'object' ? node.data : {}
|
|
676
|
+
const { text, attachments, rest } = contentParts(Array.isArray(data.content) ? data.content : [])
|
|
677
|
+
const referenceLabels = Array.isArray(data.referenceLabels) ? data.referenceLabels : []
|
|
678
|
+
const skillNames = Array.isArray(data.skillNames) ? data.skillNames : []
|
|
543
679
|
const references = react.useMemo(() => ({ openFile, openSkill }), [openFile, openSkill])
|
|
544
|
-
const labels = react.useMemo(() => buildMarkdownLabels(t), [t])
|
|
545
680
|
const compactImages = attachments.length > 1
|
|
546
681
|
const showBubble = text !== '' || rest.length > 0
|
|
547
682
|
|
|
683
|
+
// Never let composition throw out of this render — see safeMarkdownBody.
|
|
684
|
+
// One degraded body per render, shared by the composition guard and the
|
|
685
|
+
// boundary's own fallback; the last-resort net calls it too.
|
|
686
|
+
const body = safeMarkdownBody(text, t, referenceLabels, skillNames, references)
|
|
687
|
+
const degraded = degradedBody(text, referenceLabels, skillNames, references)
|
|
688
|
+
|
|
548
689
|
// The action strip leads the row in DOM order and is pushed below the
|
|
549
690
|
// bubble by flex `order`. Sibling plugins pick their bar with
|
|
550
691
|
// `row.querySelector('[class*="_actions"]')` — first match in document
|
|
@@ -576,17 +717,13 @@ window.__ModuleLoader__.load({
|
|
|
576
717
|
h(
|
|
577
718
|
'div',
|
|
578
719
|
{ className: 'dshmb-bubble' },
|
|
579
|
-
h(
|
|
580
|
-
MarkdownBoundary,
|
|
581
|
-
{ key: text, fallback: plainProjection(text, referenceLabels, skillNames, references) },
|
|
582
|
-
composeUserMarkdown(text, labels, referenceLabels, skillNames, references),
|
|
583
|
-
),
|
|
720
|
+
h(MarkdownBoundary, { key: text, fallback: degraded }, body === null ? degraded : body),
|
|
584
721
|
rest.map((block, index) =>
|
|
585
722
|
h(primitives.JsonBlock, {
|
|
586
723
|
key: 'rest' + index,
|
|
587
|
-
label: t
|
|
724
|
+
label: safeTranslate(t, 'message.extraBlock'),
|
|
588
725
|
payload: block,
|
|
589
|
-
truncatedLabel: (total) => t
|
|
726
|
+
truncatedLabel: (total) => safeTranslate(t, 'json.truncated', { total }),
|
|
590
727
|
}),
|
|
591
728
|
),
|
|
592
729
|
),
|
|
@@ -594,11 +731,48 @@ window.__ModuleLoader__.load({
|
|
|
594
731
|
h(
|
|
595
732
|
'div',
|
|
596
733
|
{ className: 'dshmb-referenceSummary' },
|
|
597
|
-
t
|
|
734
|
+
safeTranslate(t, 'message.referenceSummary', {
|
|
735
|
+
labels: referenceLabels.join(safeTranslate(t, 'message.referenceSeparator')),
|
|
736
|
+
}),
|
|
598
737
|
),
|
|
599
738
|
),
|
|
600
739
|
)
|
|
601
|
-
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* The degraded row: the action strip plus the raw text, built so that nothing
|
|
744
|
+
* in it can throw.
|
|
745
|
+
* @param props - whatever the seat was handed.
|
|
746
|
+
* @returns the row with its sibling anchors intact.
|
|
747
|
+
*/
|
|
748
|
+
function renderPlainRow(props) {
|
|
749
|
+
const safeProps = props !== null && typeof props === 'object' ? props : {}
|
|
750
|
+
const node = safeProps.node !== null && typeof safeProps.node === 'object' ? safeProps.node : {}
|
|
751
|
+
const data = node.data !== null && typeof node.data === 'object' ? node.data : {}
|
|
752
|
+
let parts
|
|
753
|
+
try {
|
|
754
|
+
parts = contentParts(Array.isArray(data.content) ? data.content : [])
|
|
755
|
+
} catch {
|
|
756
|
+
parts = { text: '', attachments: [], rest: [] }
|
|
757
|
+
}
|
|
758
|
+
const text = parts.text
|
|
759
|
+
// The same degraded projection the healthy path uses, so a row that
|
|
760
|
+
// degraded in one place looks exactly like a row that degraded in another.
|
|
761
|
+
const degraded = degradedBody(
|
|
762
|
+
text,
|
|
763
|
+
Array.isArray(data.referenceLabels) ? data.referenceLabels : [],
|
|
764
|
+
Array.isArray(data.skillNames) ? data.skillNames : [],
|
|
765
|
+
{ openFile: safeProps.openFile, openSkill: safeProps.openSkill },
|
|
766
|
+
)
|
|
767
|
+
return h(
|
|
768
|
+
'div',
|
|
769
|
+
{ className: 'dshmb-row', 'data-dshmb-row': '1', 'data-dshmb-degraded': '1' },
|
|
770
|
+
h(MessageActions, { text, time: data.time, t: safeProps.t }),
|
|
771
|
+
text === ''
|
|
772
|
+
? null
|
|
773
|
+
: h('div', { className: 'dshmb-stack' }, h('div', { className: 'dshmb-bubble' }, degraded)),
|
|
774
|
+
)
|
|
775
|
+
}
|
|
602
776
|
|
|
603
777
|
// --- style ------------------------------------------------------------------
|
|
604
778
|
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
export const PLUGIN_ID = 'dsh-markdown-bubble'
|
|
16
16
|
|
|
17
17
|
/** Keep in sync with package.json and src/client.js. */
|
|
18
|
-
export const PLUGIN_VERSION = '0.1.
|
|
18
|
+
export const PLUGIN_VERSION = '0.1.4'
|
|
19
19
|
|
|
20
20
|
/** Cordis plugin name; the bundle patch resolves the package by this row. */
|
|
21
21
|
export const name = PLUGIN_ID
|