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 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.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.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
- /** The host projection, called in a guard: it is the fallback of last resort. */
234
- function plainProjection(text, referenceLabels, skillNames, references) {
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
- console.warn('[' + NS + '] the host user-text projection failed; showing raw text', error)
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
- const projected = plainProjection(text, referenceLabels, skillNames, references)
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('copy'),
334
- copiedLabel: t('copied'),
404
+ copyLabel: safeTranslate(t, 'copy'),
405
+ copiedLabel: safeTranslate(t, 'copied'),
335
406
  toolbarLabels: {
336
- codeLabel: t('codeBlock.title'),
337
- wrapLabel: t('codeBlock.wrap'),
338
- unwrapLabel: t('codeBlock.unwrap'),
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('markdown.footnotes'),
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
- const date = new Date(time)
398
- const reference = new Date(now)
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('clock.md', params) : t('clock.ymd', params)} ${clock}`
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('copied') : t('copy')
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({ attachment, renderMessageImages, compactImages }) {
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 meta = [primitives.fileExtension(file.name).toUpperCase().slice(0, 8), primitives.fileSizeText(file.bytes)]
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: file.name },
515
- h(primitives.FileTypeIcon, { path: file.name, className: 'dshmb-fileIcon' }),
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' }, file.name),
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
- node,
534
- renderMessageImages,
535
- openFile,
536
- openSkill,
537
- t,
538
- }) {
539
- const data = node.data
540
- const { text, attachments, rest } = contentParts(data.content)
541
- const referenceLabels = data.referenceLabels ?? []
542
- const skillNames = data.skillNames ?? []
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('message.extraBlock'),
724
+ label: safeTranslate(t, 'message.extraBlock'),
588
725
  payload: block,
589
- truncatedLabel: (total) => t('json.truncated', { total }),
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('message.referenceSummary', { labels: referenceLabels.join(t('message.referenceSeparator')) }),
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.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