dsh-context 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/lib/client.js +300 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,6 +86,14 @@ Six collapsible category sections (system prompt, tool schemas, user messages, i
86
86
  - **Linked with the history chart** — hover any bar in the History card and the browser previews that step instantly; leave the chart and it returns to your own pick. Keep a category open while scrubbing to compare one category across steps.
87
87
  - **Honest about coverage** — steps before a compaction are reconstructed from the removed-message archive, and the card says so when a step's makeup is only approximate. Elements older than the loaded chat window page older history in automatically when you expand them, and live injections (AGENTS.md, session-start context, …) are always listed — never a token sum without its items.
88
88
 
89
+ ### 🖼 Multimodal — image attachments in full view (DeepSeek Harness 0.1.1+)
90
+
91
+ Fully adapted to DeepSeek Harness 0.1.1's multimodal pipeline and the vision capability of **DeepSeek-V4-Flash-Vision-Exp**. A user message carrying images expands into a card layout — prose in the text card (with the usual raw/Markdown toggle), each image attachment as a thumbnail card with its name, normalized dimensions (plus the pre-normalization size when 0.1.1's image pipeline downscaled it) and stored size, and anything unrecognized as raw content:
92
+
93
+ ![Context browser rendering image attachments](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-browser-images.png)
94
+
95
+ Images load through the harness's own session-authorized loader — the same one the chat history uses — and degrade to metadata-only cards when it is unavailable. Image blocks in assistant messages and tool results (e.g. `read_image` output) now render too, instead of being silently dropped.
96
+
89
97
  ## Like it?
90
98
 
91
99
  If `dsh-context` helped you understand what your agent is carrying around, a ⭐ on [GitHub](https://github.com/bowenliang123/dsh-context) is much appreciated — and issues/PRs are welcome!
package/lib/client.js CHANGED
@@ -67,6 +67,7 @@ var DICT_ZH = {
67
67
  "nodes.empty": "\u5F53\u524D\u6CA1\u6709\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F",
68
68
  "loading": "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u2026",
69
69
  "error": "\u4E0A\u4E0B\u6587\u6570\u636E\u8BFB\u53D6\u5931\u8D25\uFF1A",
70
+ "error.retry": "\u91CD\u8BD5",
70
71
  "footer": "\u4F30\u7B97\u53E3\u5F84\uFF1A\u4E0E dsh \u5185\u7F6E tokenMeter \u76F8\u540C\u7684\u56FA\u5B9A\u5BC6\u5EA6\u542F\u53D1\u5F0F\uFF08\u7EA6 4 \u5B57\u7B26 \u2248 1 token\uFF09\uFF1B\u300C\u5B9E\u9645\u300D\u4E3A\u4F9B\u5E94\u5546\u4E0A\u62A5\u7528\u91CF\u3002",
71
72
  "tip.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C{s}\u6B65",
72
73
  "tip.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
@@ -115,7 +116,22 @@ var DICT_ZH = {
115
116
  "rich.raw": "\u539F\u6587",
116
117
  "rich.md": "Markdown",
117
118
  "rich.toMd": "\u6309 Markdown \u6E32\u67D3\u67E5\u770B",
118
- "rich.toRaw": "\u67E5\u770B\u539F\u59CB\u6587\u672C"
119
+ "rich.toRaw": "\u67E5\u770B\u539F\u59CB\u6587\u672C",
120
+ // Assistant-message block titles inside the detail card: the reasoning
121
+ // trace and the response each carry a small label (Reasoning / Response,
122
+ // mirroring the harness's own block + locale vocabulary).
123
+ "block.thinking": "\u601D\u8003",
124
+ "block.answer": "\u56DE\u7B54",
125
+ // Attachment cards in a message detail: image refs render as thumbnail
126
+ // cards (name + normalized dims + stored size), anything unrecognized
127
+ // falls into the generic raw-JSON card.
128
+ "attach.images": "\u56FE\u7247\u9644\u4EF6",
129
+ "attach.other": "\u5176\u4ED6\u5185\u5BB9",
130
+ "attach.image": "\u56FE\u7247",
131
+ "attach.open": "\u67E5\u770B\u539F\u56FE",
132
+ "attach.loading": "\u2026",
133
+ "attach.loadFailed": "\u52A0\u8F7D\u5931\u8D25 \xB7 \u70B9\u51FB\u91CD\u8BD5",
134
+ "attach.orig": "\u539F\u56FE {d}"
119
135
  };
120
136
  var DICT_EN = {
121
137
  "tab": "Context",
@@ -177,6 +193,7 @@ var DICT_EN = {
177
193
  "nodes.empty": "No model-visible messages right now",
178
194
  "loading": "Reading the session log\u2026",
179
195
  "error": "Failed to read context data: ",
196
+ "error.retry": "Retry",
180
197
  "footer": "Estimate: same fixed-density heuristic as dsh\u2019s built-in tokenMeter (~4 chars \u2248 1 token); \u201Cactual\u201D is provider-reported usage.",
181
198
  "tip.step": "Turn {t} \xB7 Step {s}",
182
199
  "tip.turn": "Turn {t} \xB7 {n} steps",
@@ -225,7 +242,22 @@ var DICT_EN = {
225
242
  "rich.raw": "Raw",
226
243
  "rich.md": "Markdown",
227
244
  "rich.toMd": "View as Markdown",
228
- "rich.toRaw": "View Raw Text"
245
+ "rich.toRaw": "View Raw Text",
246
+ // Assistant-message block titles inside the detail card: the reasoning
247
+ // trace and the response each carry a small label (Reasoning / Response,
248
+ // mirroring the harness's own block + locale vocabulary).
249
+ "block.thinking": "Reasoning",
250
+ "block.answer": "Response",
251
+ // Attachment cards in a message detail: image refs render as thumbnail
252
+ // cards (name + normalized dims + stored size), anything unrecognized
253
+ // falls into the generic raw-JSON card.
254
+ "attach.images": "Images",
255
+ "attach.other": "Other content",
256
+ "attach.image": "Image",
257
+ "attach.open": "Open full image",
258
+ "attach.loading": "\u2026",
259
+ "attach.loadFailed": "Load failed \xB7 click to retry",
260
+ "attach.orig": "original {d}"
229
261
  };
230
262
 
231
263
  // src/client/modalStore.ts
@@ -338,12 +370,60 @@ function asRecord(value) {
338
370
  if (value === null || value === void 0 || typeof value !== "object") return null;
339
371
  return value;
340
372
  }
341
- var timelineOf = (value) => asRecord(value);
373
+ function numOf(value) {
374
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
375
+ }
376
+ function objectsOf(value) {
377
+ if (!Array.isArray(value)) return [];
378
+ return value.filter((v) => v !== null && typeof v === "object");
379
+ }
380
+ function timelineOf(value) {
381
+ const data = asRecord(value);
382
+ if (data === null) return null;
383
+ const current = data.current;
384
+ const numericBreakdown = current !== null && typeof current === "object" && ["system", "tools", "user", "inject", "assistant", "tool", "total"].every((k) => typeof current[k] === "number");
385
+ if (numericBreakdown && Array.isArray(data.requests) && Array.isArray(data.events) && Array.isArray(data.nodes) && Array.isArray(data.archive) && Array.isArray(data.toolList)) {
386
+ return data;
387
+ }
388
+ const safeCurrent = current !== null && typeof current === "object" ? current : {};
389
+ const cost = typeof data.cost === "object" && data.cost !== null && !Array.isArray(data.cost) ? data.cost : void 0;
390
+ return {
391
+ ok: true,
392
+ ...typeof data.model === "string" ? { model: data.model } : {},
393
+ ...typeof data.provider === "string" ? { provider: data.provider } : {},
394
+ ...typeof data.contextWindow === "number" ? { contextWindow: data.contextWindow } : {},
395
+ current: {
396
+ system: numOf(safeCurrent.system),
397
+ tools: numOf(safeCurrent.tools),
398
+ user: numOf(safeCurrent.user),
399
+ inject: numOf(safeCurrent.inject),
400
+ assistant: numOf(safeCurrent.assistant),
401
+ tool: numOf(safeCurrent.tool),
402
+ total: numOf(safeCurrent.total)
403
+ },
404
+ toolList: objectsOf(data.toolList),
405
+ requests: objectsOf(data.requests),
406
+ events: objectsOf(data.events),
407
+ nodes: objectsOf(data.nodes),
408
+ droppedNodes: numOf(data.droppedNodes),
409
+ archive: objectsOf(data.archive),
410
+ ...cost !== void 0 ? { cost } : {},
411
+ ...typeof data.surfaceFloor === "number" ? { surfaceFloor: data.surfaceFloor } : {},
412
+ ...typeof data.archiveFloor === "number" ? { archiveFloor: data.archiveFloor } : {}
413
+ };
414
+ }
342
415
  var contextPressureOf = (value) => asRecord(value);
343
416
  var tokenUsageOf = (value) => asRecord(value);
344
417
  function headersOf(value) {
345
418
  const headers = asRecord(value);
346
- return headers !== null && Array.isArray(headers.headers) ? headers : null;
419
+ if (headers === null || !Array.isArray(headers.headers)) return null;
420
+ for (const h2 of headers.headers) {
421
+ if (h2 === null || typeof h2 !== "object") return null;
422
+ const entry = h2;
423
+ if (!Array.isArray(entry.tools)) return null;
424
+ if (entry.system !== void 0 && typeof entry.system !== "string") return null;
425
+ }
426
+ return headers;
347
427
  }
348
428
 
349
429
  // src/client/assemble.ts
@@ -417,6 +497,96 @@ function makeNodeList(kit) {
417
497
  };
418
498
  }
419
499
 
500
+ // src/client/format.ts
501
+ function fmt(n) {
502
+ if (n === void 0 || n === null || isNaN(n)) return "\u2014";
503
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
504
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
505
+ return String(Math.round(n));
506
+ }
507
+ function fmtBytes(n) {
508
+ if (n === void 0 || n === null || isNaN(n) || n < 0) return "\u2014";
509
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + " MB";
510
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + " kB";
511
+ return String(Math.round(n)) + " B";
512
+ }
513
+ function fmtTime(t) {
514
+ const d = new Date(t);
515
+ if (isNaN(d.getTime())) return "\u2014";
516
+ return d.toLocaleTimeString("en-GB", { hour12: false });
517
+ }
518
+
519
+ // src/client/components/images.tsx
520
+ function imageRefOf(block) {
521
+ if (block === null || typeof block !== "object") return null;
522
+ const b = block;
523
+ if (b.type !== "image" && b.kind !== "image") return null;
524
+ const a = b.attachment;
525
+ if (a === null || typeof a !== "object") return null;
526
+ const r = a;
527
+ if (typeof r.attachmentId !== "string" || r.attachmentId === "") return null;
528
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
529
+ const orig = r.originalDimensions !== null && typeof r.originalDimensions === "object" ? r.originalDimensions : void 0;
530
+ const origDims = orig !== void 0 && num(orig.width) !== void 0 && num(orig.height) !== void 0 ? { width: num(orig.width), height: num(orig.height) } : void 0;
531
+ return {
532
+ attachmentId: r.attachmentId,
533
+ ...typeof r.name === "string" && r.name !== "" ? { name: r.name } : {},
534
+ ...num(r.bytes) !== void 0 ? { bytes: num(r.bytes) } : {},
535
+ ...num(r.width) !== void 0 ? { width: num(r.width) } : {},
536
+ ...num(r.height) !== void 0 ? { height: num(r.height) } : {},
537
+ ...origDims !== void 0 ? { originalDimensions: origDims } : {}
538
+ };
539
+ }
540
+ function makeImageCard(kit) {
541
+ const { t } = kit;
542
+ return function ImageCard(props) {
543
+ const { attachment, load } = props;
544
+ const [src, setSrc] = React.useState(null);
545
+ const [error, setError] = React.useState(false);
546
+ const [attempt, setAttempt] = React.useState(0);
547
+ React.useEffect(() => {
548
+ if (load === void 0) return;
549
+ let live = true;
550
+ setError(false);
551
+ setSrc(null);
552
+ void load(attachment).then((url) => {
553
+ if (live) setSrc(url);
554
+ }).catch(() => {
555
+ if (live) setError(true);
556
+ });
557
+ return () => {
558
+ live = false;
559
+ };
560
+ }, [attachment, load, attempt]);
561
+ const name = attachment.name ?? t("attach.image");
562
+ const dims = attachment.width !== void 0 && attachment.height !== void 0 ? attachment.width + "\xD7" + attachment.height : null;
563
+ const orig = attachment.originalDimensions;
564
+ const facts = [];
565
+ if (dims !== null) facts.push(dims);
566
+ if (orig !== void 0) facts.push(t("attach.orig", { d: orig.width + "\xD7" + orig.height }));
567
+ if (attachment.bytes !== void 0) facts.push(fmtBytes(attachment.bytes));
568
+ const open = () => {
569
+ if (src === null) return;
570
+ try {
571
+ window.open(src, "_blank", "noopener");
572
+ } catch {
573
+ }
574
+ };
575
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-att-item" }, /* @__PURE__ */ React.createElement(
576
+ "button",
577
+ {
578
+ type: "button",
579
+ className: "lc-att-thumb",
580
+ title: error ? t("attach.loadFailed") : t("attach.open"),
581
+ onClick: error ? () => {
582
+ setAttempt((a) => a + 1);
583
+ } : open
584
+ },
585
+ src !== null ? /* @__PURE__ */ React.createElement("img", { src, alt: name }) : /* @__PURE__ */ React.createElement("span", { className: error ? "lc-att-err" : "lc-att-ph" }, error ? "\u26A0" : load === void 0 ? "\u{1F5BC}" : t("attach.loading"))
586
+ ), /* @__PURE__ */ React.createElement("div", { className: "lc-att-meta" }, /* @__PURE__ */ React.createElement("span", { className: "lc-att-name", title: name }, name), facts.length > 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-att-dims" }, facts.join(" \xB7 ")) : null));
587
+ };
588
+ }
589
+
420
590
  // src/client/components/richText.tsx
421
591
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
422
592
  function makeRichText(kit) {
@@ -551,7 +721,7 @@ function ToolSchema(props) {
551
721
  ), jsonOpen ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, schemaJson) : null) : null);
552
722
  }
553
723
  function RawBlocks(props) {
554
- const { rich } = props;
724
+ const { rich, img } = props;
555
725
  return /* @__PURE__ */ React.createElement(React.Fragment, null, props.blocks.map((b, i) => {
556
726
  const blk = b;
557
727
  if (blk !== null && typeof blk === "object" && (blk.type === "text" || blk.type === "reasoning") && typeof blk.text === "string") {
@@ -560,14 +730,16 @@ function RawBlocks(props) {
560
730
  }
561
731
  return /* @__PURE__ */ React.createElement(rich.RichText, { key: i, text: blk.text, mode: props.mode });
562
732
  }
733
+ const image = imageRefOf(b);
734
+ if (image !== null) return /* @__PURE__ */ React.createElement(img.Card, { key: i, attachment: image, load: img.load });
563
735
  if (blk !== null && typeof blk === "object" && blk.type === "tool-result" && Array.isArray(blk.content)) {
564
- return /* @__PURE__ */ React.createElement(RawBlocks, { key: i, blocks: blk.content, mode: props.mode, rich });
736
+ return /* @__PURE__ */ React.createElement(RawBlocks, { key: i, blocks: blk.content, mode: props.mode, rich, img });
565
737
  }
566
738
  return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, JSON.stringify(b, null, 2));
567
739
  }));
568
740
  }
569
741
  function NodeContent(props) {
570
- const { node, conv, rich } = props;
742
+ const { node, conv, rich, img } = props;
571
743
  const richable = node.cat === "user" || node.cat === "assistant" || node.cat === "inject";
572
744
  const wrap = (render) => richable ? /* @__PURE__ */ React.createElement(rich.RichCard, { render }) : /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, render("raw"));
573
745
  if (conv === void 0) {
@@ -581,25 +753,52 @@ function NodeContent(props) {
581
753
  return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, blocks.map((b, i) => {
582
754
  const blk = b;
583
755
  if (blk.kind === "text" && typeof blk.text === "string") {
584
- return /* @__PURE__ */ React.createElement(rich.RichText, { key: i, text: blk.text, mode });
756
+ return /* @__PURE__ */ React.createElement("div", { key: i, className: "lc-br-blk" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-blk-label" }, props.labels.answer), /* @__PURE__ */ React.createElement(rich.RichText, { text: blk.text, mode }));
585
757
  }
586
758
  if (blk.kind === "reasoning" && typeof blk.text === "string") {
587
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, blk.text);
759
+ return /* @__PURE__ */ React.createElement("div", { key: i, className: "lc-br-blk" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-blk-label" }, props.labels.thinking), /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, blk.text));
588
760
  }
589
761
  if (blk.kind === "tool-call") {
590
762
  return /* @__PURE__ */ React.createElement("div", { key: i, className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2192 " + String(blk.name ?? "?")), typeof blk.argsRaw === "string" && blk.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, blk.argsRaw) : null);
591
763
  }
764
+ const image = imageRefOf(b);
765
+ if (image !== null) return /* @__PURE__ */ React.createElement(img.Card, { key: i, attachment: image, load: img.load });
592
766
  return null;
593
767
  })));
594
768
  }
595
769
  if (conv.kind === "tool-result") {
596
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.call != null ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2190 " + conv.call.name), conv.call.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, conv.call.argsRaw) : null) : null, Array.isArray(conv.content) ? /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content, mode: "raw", rich }) : null);
770
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.call != null ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2190 " + conv.call.name), conv.call.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, conv.call.argsRaw) : null) : null, Array.isArray(conv.content) ? /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content, mode: "raw", rich, img }) : null);
597
771
  }
598
772
  if (conv.kind === "compaction") {
599
773
  return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, typeof conv.summary === "string" && conv.summary !== "" ? /* @__PURE__ */ React.createElement(rich.RichText, { text: conv.summary, mode }) : null));
600
774
  }
601
775
  if (Array.isArray(conv.content)) {
602
- return wrap((mode) => /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content, mode, rich }));
776
+ const blocks = conv.content;
777
+ if (node.cat !== "user") {
778
+ return wrap((mode) => /* @__PURE__ */ React.createElement(RawBlocks, { blocks, mode, rich, img }));
779
+ }
780
+ const texts = [];
781
+ const images = [];
782
+ const others = [];
783
+ for (const b of blocks) {
784
+ const blk = b;
785
+ if (blk !== null && typeof blk === "object" && blk.type === "text" && typeof blk.text === "string") {
786
+ texts.push(blk.text);
787
+ continue;
788
+ }
789
+ const image = imageRefOf(b);
790
+ if (image !== null) {
791
+ images.push(image);
792
+ continue;
793
+ }
794
+ others.push(b);
795
+ }
796
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, texts.length > 0 ? (
797
+ // Un-padded RichCard: the outer lc-br-content already owns the
798
+ // body indent, so the text card aligns with the attachment cards
799
+ // instead of sitting one indent deeper.
800
+ /* @__PURE__ */ React.createElement(rich.RichCard, { className: "lc-br-rich-inline", render: (mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, texts.map((text, i) => /* @__PURE__ */ React.createElement(rich.RichText, { key: i, text, mode }))) })
801
+ ) : null, images.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card-head" }, /* @__PURE__ */ React.createElement("b", null, props.labels.images), /* @__PURE__ */ React.createElement("span", { className: "lc-ts-card-count" }, images.length)), /* @__PURE__ */ React.createElement("div", { className: "lc-att-grid" }, images.map((a, i) => /* @__PURE__ */ React.createElement(img.Card, { key: a.attachmentId + ":" + i, attachment: a, load: img.load })))) : null, others.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card-head" }, /* @__PURE__ */ React.createElement("b", null, props.labels.other), /* @__PURE__ */ React.createElement("span", { className: "lc-ts-card-count" }, others.length)), others.map((b, i) => /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, JSON.stringify(b, null, 2)))) : null);
603
802
  }
604
803
  return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
605
804
  }
@@ -622,6 +821,7 @@ function makeContextBrowser(kit, StackedBar) {
622
821
  const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
623
822
  const nodeText = makeNodeText(kit);
624
823
  const rich = makeRichText(kit);
824
+ const ImageCard = makeImageCard(kit);
625
825
  const catColor = {};
626
826
  for (const c of CATS) catColor[c.key] = c.color;
627
827
  const MAX_AUTO_PAGES = 20;
@@ -766,6 +966,13 @@ function makeContextBrowser(kit, StackedBar) {
766
966
  node: n,
767
967
  conv: bySeq.get(n.seq),
768
968
  rich,
969
+ img: { Card: ImageCard, load: props.loadImage },
970
+ labels: {
971
+ thinking: t("block.thinking"),
972
+ answer: t("block.answer"),
973
+ images: t("attach.images"),
974
+ other: t("attach.other")
975
+ },
769
976
  hint: bySeq.get(n.seq) === void 0 && awaiting ? t("browser.loading") : t("browser.noContent")
770
977
  }
771
978
  )
@@ -982,6 +1189,34 @@ function makeCurrentComposition(kit, StackedBar, Legend) {
982
1189
  };
983
1190
  }
984
1191
 
1192
+ // src/client/components/errorBoundary.tsx
1193
+ function makeErrorBoundary(t) {
1194
+ return class ErrorBoundary extends React.Component {
1195
+ constructor(props) {
1196
+ super(props);
1197
+ this.state = { error: null };
1198
+ }
1199
+ static getDerivedStateFromError(error) {
1200
+ return { error: error instanceof Error ? error : new Error(String(error)) };
1201
+ }
1202
+ render() {
1203
+ const error = this.state.error;
1204
+ if (error === null) return this.props.children;
1205
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root" }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty lc-error" }, /* @__PURE__ */ React.createElement("span", null, t("error")), /* @__PURE__ */ React.createElement("code", { className: "lc-error-msg" }, error.message), /* @__PURE__ */ React.createElement(
1206
+ "button",
1207
+ {
1208
+ type: "button",
1209
+ className: "lc-error-retry",
1210
+ onClick: () => {
1211
+ this.setState({ error: null });
1212
+ }
1213
+ },
1214
+ t("error.retry")
1215
+ )));
1216
+ }
1217
+ };
1218
+ }
1219
+
985
1220
  // src/client/components/contextModal.tsx
986
1221
  function makeContextModal(ctx, kit) {
987
1222
  const { t } = kit;
@@ -990,7 +1225,8 @@ function makeContextModal(ctx, kit) {
990
1225
  const Legend = makeLegend(kit);
991
1226
  const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
992
1227
  const ContextBrowser = makeContextBrowser(kit, StackedBar);
993
- return function ContextModal(props) {
1228
+ const ErrorBoundary = makeErrorBoundary(t);
1229
+ function ContextModalBody(props) {
994
1230
  const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
995
1231
  const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
996
1232
  const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
@@ -1051,6 +1287,9 @@ function makeContextModal(ctx, kit) {
1051
1287
  }
1052
1288
  }
1053
1289
  ))));
1290
+ }
1291
+ return function ContextModal(props) {
1292
+ return h(ErrorBoundary, null, h(ContextModalBody, props));
1054
1293
  };
1055
1294
  }
1056
1295
 
@@ -1273,6 +1512,13 @@ var STYLES = [
1273
1512
  ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
1274
1513
  ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
1275
1514
  ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
1515
+ // Error-boundary fallback card: the tab degrades to this instead of
1516
+ // white-screening; the message runs in mono on a padded chip, and Retry
1517
+ // re-mounts the subtree once a healthy value arrives.
1518
+ ".lc-error { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 40px 16px; }",
1519
+ ".lc-error-msg { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 4px 8px; max-width: 100%; overflow-wrap: anywhere; }",
1520
+ ".lc-error-retry { border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 4px 14px; border-radius: 6px; cursor: pointer; }",
1521
+ ".lc-error-retry:hover { border-color: var(--dsw-alias-label-primary); }",
1276
1522
  ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }",
1277
1523
  // ---- /context modal (centered dialog; escapes the composer anchor via fixed positioning) ----
1278
1524
  ".lc-modal-backdrop { position: fixed; inset: 0; z-index: 200; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; }",
@@ -1333,6 +1579,11 @@ var STYLES = [
1333
1579
  ".lc-br-pre { margin: 0; padding: 8px 10px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }",
1334
1580
  ".lc-br-dim { color: var(--dsw-alias-label-secondary); }",
1335
1581
  ".lc-br-call { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }",
1582
+ // Assistant-message block title: a slim label line above each block
1583
+ // (thinking / answer) so a mixed reply stays scannable — same weight and
1584
+ // tone as the tool-schema card heads, without an extra bordered card.
1585
+ ".lc-br-blk { display: flex; flex-direction: column; gap: 4px; }",
1586
+ ".lc-br-blk-label { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); }",
1336
1587
  // ---- Tool schema body (browser card, tools category). The description
1337
1588
  // and parameter table share one "card with a small title" chrome — a
1338
1589
  // labeled bordered block so a reader can scan three stacked sections
@@ -1392,24 +1643,27 @@ var STYLES = [
1392
1643
  ".lc-br-md div p, .lc-ts-desc-md div p { margin: 6px 0; }",
1393
1644
  ".lc-br-md div pre, .lc-ts-desc-md div pre { margin: 6px 0; }",
1394
1645
  ".lc-br-md div :not(pre) > code, .lc-ts-desc-md div :not(pre) > code { font-size: 1em !important; line-height: 1.3; padding: 1px 5px; box-decoration-break: clone; }",
1395
- ".lc-br-md div table code, .lc-ts-desc-md div table code { font-size: inherit; }"
1646
+ ".lc-br-md div table code, .lc-ts-desc-md div table code { font-size: inherit; }",
1647
+ // Un-padded RichCard variant: used when the text card is one section
1648
+ // among several (attachment cards) inside an already-indented body.
1649
+ ".lc-br-rich-inline { display: flex; flex-direction: column; gap: 6px; }",
1650
+ // ---- Attachment cards (image refs in a message detail). The card chrome
1651
+ // reuses the tool-schema sections' `lc-ts-card`/`lc-ts-card-head`; only the
1652
+ // image grid and tile are new. Each image is a 64px cover tile (the chat
1653
+ // history's tile size) beside a name + dims + size column. ----
1654
+ ".lc-ts-card .lc-br-pre { margin: 8px 10px; }",
1655
+ ".lc-att-grid { display: flex; flex-wrap: wrap; gap: 10px; padding: 8px 10px; }",
1656
+ ".lc-att-item { display: flex; gap: 8px; align-items: center; min-width: 0; }",
1657
+ ".lc-att-thumb { width: 64px; height: 64px; flex: none; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; background: var(--dsw-alias-bg-layer-1); cursor: pointer; font: inherit; }",
1658
+ ".lc-att-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }",
1659
+ ".lc-att-ph, .lc-att-err { font-size: 10px; color: var(--dsw-alias-label-secondary); text-align: center; padding: 2px; }",
1660
+ ".lc-att-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }",
1661
+ ".lc-att-name { font-size: 12px; color: var(--dsw-alias-label-primary); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1662
+ ".lc-att-dims { font-size: 11px; color: var(--dsw-alias-label-secondary); }"
1396
1663
  ].join("\n");
1397
1664
 
1398
1665
  // src/client/components/events.tsx
1399
1666
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1400
-
1401
- // src/client/format.ts
1402
- function fmt(n) {
1403
- if (n === void 0 || n === null || isNaN(n)) return "\u2014";
1404
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
1405
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
1406
- return String(Math.round(n));
1407
- }
1408
- function fmtTime(t) {
1409
- return new Date(t).toLocaleTimeString("en-GB", { hour12: false });
1410
- }
1411
-
1412
- // src/client/components/events.tsx
1413
1667
  var EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
1414
1668
  function makeEventText(t) {
1415
1669
  function eventLabel(ev) {
@@ -1472,7 +1726,7 @@ function makeEventList(kit) {
1472
1726
 
1473
1727
  // src/client/meta.ts
1474
1728
  var PLUGIN_NAME = "dsh-context";
1475
- var PLUGIN_VERSION = true ? "0.21.0" : "0.0.0-dev";
1729
+ var PLUGIN_VERSION = true ? "0.22.0" : "0.0.0-dev";
1476
1730
  var PLUGIN_REPO = true ? "https://github.com/bowenliang123/dsh-context" : "https://github.com/bowenliang123/dsh-context";
1477
1731
  var PLUGIN_REPO_SHORT = PLUGIN_REPO.replace(/^https?:\/\/github\.com\//, "");
1478
1732
 
@@ -1563,7 +1817,7 @@ function estimateSessionCost(usage, currency) {
1563
1817
  const b = fam[period];
1564
1818
  if (b === void 0) continue;
1565
1819
  const p = PRICES[currency][family][period];
1566
- total += (b.cacheRead * p.hit + (b.uncached + b.cacheWrite) * p.miss + b.output * p.out) / 1e6;
1820
+ total += (numOf(b.cacheRead) * p.hit + (numOf(b.uncached) + numOf(b.cacheWrite)) * p.miss + numOf(b.output) * p.out) / 1e6;
1567
1821
  any = true;
1568
1822
  }
1569
1823
  }
@@ -1586,9 +1840,12 @@ function formatPriceRate(amount, currency) {
1586
1840
 
1587
1841
  // src/client/components/statsBoard.tsx
1588
1842
  function cacheHitPercent(usage) {
1589
- const billed = usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
1843
+ const uncached = numOf(usage.uncachedInputTokens);
1844
+ const reads = numOf(usage.cacheReadTokens);
1845
+ const writes = numOf(usage.cacheWriteTokens);
1846
+ const billed = uncached + reads + writes;
1590
1847
  if (billed === 0) return null;
1591
- const hundredths = Math.trunc(usage.cacheReadTokens / billed * 1e4 + 1e-9);
1848
+ const hundredths = Math.trunc(reads / billed * 1e4 + 1e-9);
1592
1849
  return `${Math.floor(hundredths / 100)}.${String(hundredths % 100).padStart(2, "0")}`;
1593
1850
  }
1594
1851
  function makeStatsBoard(kit) {
@@ -1811,7 +2068,8 @@ function makeContextView(ctx, kit) {
1811
2068
  const StatsBoard = makeStatsBoard(kit);
1812
2069
  const PluginInfo = makePluginInfo(kit);
1813
2070
  const ContextBrowser = makeContextBrowser(kit, StackedBar);
1814
- return function ContextView(props) {
2071
+ const ErrorBoundary = makeErrorBoundary(t);
2072
+ function ContextViewBody(props) {
1815
2073
  const sessionId = props.sessionId;
1816
2074
  const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
1817
2075
  const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
@@ -1835,6 +2093,12 @@ function makeContextView(ctx, kit) {
1835
2093
  const clearToolFocus = React.useCallback(() => {
1836
2094
  setToolFocus(null);
1837
2095
  }, []);
2096
+ const loadImage = React.useMemo(() => {
2097
+ if (typeof sessionId !== "string" || sessionId === "") return void 0;
2098
+ const conversation = ctx.get("conversation");
2099
+ if (conversation === void 0 || typeof conversation.resolveImage !== "function") return void 0;
2100
+ return (attachment) => conversation.resolveImage(sessionId, attachment);
2101
+ }, [sessionId]);
1838
2102
  const rootRef = React.useRef(null);
1839
2103
  const scrollerRef = React.useRef(null);
1840
2104
  const restoredRef = React.useRef(null);
@@ -1946,7 +2210,8 @@ function makeContextView(ctx, kit) {
1946
2210
  hoverKey: hoverCat,
1947
2211
  onHoverKey: setHoverCat,
1948
2212
  toolFocus,
1949
- onToolFocusHandled: clearToolFocus
2213
+ onToolFocusHandled: clearToolFocus,
2214
+ loadImage
1950
2215
  }
1951
2216
  ))), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("events.title")), /* @__PURE__ */ React.createElement("div", { className: "lc-kinds" }, EVENT_KINDS.map((k) => /* @__PURE__ */ React.createElement(
1952
2217
  "button",
@@ -1959,6 +2224,9 @@ function makeContextView(ctx, kit) {
1959
2224
  },
1960
2225
  t("kind." + k)
1961
2226
  )))), /* @__PURE__ */ React.createElement(EventList, { events: shownEvents })), /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("nodes.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("nodes.hint"))), /* @__PURE__ */ React.createElement(NodeList, { nodes, dropped: data.droppedNodes || 0 }))), /* @__PURE__ */ React.createElement("div", { className: "lc-foot" }, t("footer")));
2227
+ }
2228
+ return function ContextView(props) {
2229
+ return h(ErrorBoundary, null, h(ContextViewBody, props));
1962
2230
  };
1963
2231
  }
1964
2232
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {