@deepseek-ai/dsh-client-ui-trajectory 0.2.0-rc.1 → 0.2.1-alpha.1

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.i18n.yaml CHANGED
@@ -18,14 +18,14 @@
18
18
  en: 4bd17d05b8385675
19
19
  zh: 5174e27f754f2a03
20
20
  /deepseek-ai-dsh-client-ui-trajectory/use-this-package/inspecting-records:
21
- en: 89758270607a5fd4
22
- zh: 0efba26195ef1f80
21
+ en: 5d4c2ccb73bcc71d
22
+ zh: f15664c3f1b28d9d
23
23
  /deepseek-ai-dsh-client-ui-trajectory/use-this-package/the-timing-overview:
24
24
  en: 0331f90cbd904862
25
25
  zh: bdc2d4ed1c3c1f5e
26
26
  /deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation:
27
- en: 925abc8e4b092cb3
28
- zh: ebe85c5acdbea3b1
27
+ en: 02ef008ff74a38df
28
+ zh: fa54d85ab89ed103
29
29
  /deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation/virtual-rows:
30
30
  en: c85e342bc7eee4e6
31
31
  zh: 9feab66ef1755b94
@@ -45,5 +45,5 @@
45
45
  en: d7da620f56eb3c65
46
46
  zh: fb030b68322b94cd
47
47
  /deepseek-ai-dsh-client-ui-trajectory/known-limitations-and-deferred-work/dev-note:
48
- en: f1893e3a6c69cc58
49
- zh: b6e0dfe20567c35f
48
+ en: e67f633dc6fe7773
49
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -29,7 +29,7 @@ Open the Trajectory tab in the conversation's view ring to inspect agent activit
29
29
 
30
30
  ### Inspecting records
31
31
 
32
- Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting uses only an unambiguous TypeScript or Python hint in the recorded tool schema; missing or conflicting hints leave plain source. See the [PTC inspection decision](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md) for replay constraints.
32
+ Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting uses only an unambiguous TypeScript or Python hint in the recorded tool schema; missing or conflicting hints leave plain source.
33
33
 
34
34
  Selection, timeline navigation, folding, and search cover the React-visible window. Request numbers and cumulative usage cover the complete resident snapshot. Selecting a record opens a local inspector for token usage, duration, Input, Output, Timing, and durable images. Image URLs use the Conversation-owned per-session cache, so Chat and Trajectory share one authorized read per attachment. A user record shows both nonzero image and ordinary-file counts beside its text, including attachment-only records. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn.
35
35
 
@@ -55,7 +55,7 @@ The view is a pure projection: Trajectory-owned Definitions assemble business re
55
55
 
56
56
  Native and nested PTC Tool results retain their raw structured error details. Failed records show the error code in the ledger and the error name and code in the inspector.
57
57
 
58
- Tool records begin at durable tool/call events and use complete arguments. Chat's transient preparing stage does not create Trajectory tool rows or alter historical tool timing.
58
+ Tool records begin at durable tool/call events and retain complete argument text with a lazy `args` view, reused by paired results. Chat's transient preparing stage does not create Trajectory tool rows or alter historical tool timing.
59
59
 
60
60
  A complete appended prompt without a loaded request header appears as a standalone system row; only its known text is available, with no inferred request options or tool catalog. Prepending its request history replaces that standalone presentation without duplicating the prompt. In-history system prompt changes compare against the most recent request state, including earlier prompt updates without a new request header. Each request retains the prompt and change that applied at its own position. Surface replacements, including compaction, restore the last nonempty surviving system prompt even without a new system event; an unloaded prompt remains unavailable until its page arrives.
61
61
 
@@ -112,5 +112,3 @@ These limits define what the view can show while work is in flight; they are cur
112
112
  None.
113
113
 
114
114
  </details>
115
-
116
- **Runtime invariant:** No companion is published. It is a pure-consumer plugin: it emits no Cordis events and owns no mutable cross-plugin state; its view-slot registration is a plain effect whose disposal the slot ledger's own specs and this package's behavior specs observe directly.
package/README.zh.md CHANGED
@@ -29,7 +29,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
29
29
 
30
30
  ### 检查记录
31
31
 
32
- 调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮只采用已记录工具 Schema 中明确且无冲突的 TypeScript 或 Python 提示;提示缺失或冲突时显示纯文本源码。回放约束见 [PTC 检查决策](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md)。
32
+ 调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮只采用已记录工具 Schema 中明确且无冲突的 TypeScript 或 Python 提示;提示缺失或冲突时显示纯文本源码。
33
33
 
34
34
  选择、时间线导航、折叠与搜索只覆盖 React 可见窗口。请求编号与累计用量覆盖完整的驻留快照。选择记录会打开局部检查器,查看 token 用量、耗时、输入、输出、计时与持久保留的图片。图片 URL 使用 Conversation 拥有的逐会话缓存,因此 Chat 与 Trajectory 对每个附件共享一次已授权读取。用户记录会在文本旁同时显示非零的图片与普通文件数量,仅含附件的记录也显示这些数量。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。
35
35
 
@@ -55,7 +55,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
55
55
 
56
56
  原生与嵌套 PTC 工具结果保留原始结构化错误详情。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。
57
57
 
58
- 工具记录从持久 tool/call 事件开始,使用完整参数。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。
58
+ 工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。
59
59
 
60
60
  完整的追加提示词在请求头未加载时显示为独立系统行;仅提供已知文本,不推断请求选项或工具目录。补入其请求历史后,该独立展示被替代而不重复提示词。历史中的系统提示词变更与最近的请求状态比较,包括没有新请求头的先前提示词更新。每个请求保留其所在位置生效的提示词与变更。包括压缩在内的 surface 替换会恢复最后一个非空的存活系统提示词,即使没有新的系统事件;未加载的提示词在对应分页到达前仍不可用。
61
61
 
@@ -112,5 +112,3 @@ Trajectory 要求会话壳把 composer 作为浮层置于全高记录表上方
112
112
  无。
113
113
 
114
114
  </details>
115
-
116
- **运行时不变式:** 不发布伴生入口。这是纯消费插件,不发出 Cordis 事件,也不持有跨插件可变状态;其 view-slot 注册是普通 effect,slot ledger 自身的规格测试与本包的行为规格测试会直接观察其释放。
package/lib/client.js CHANGED
@@ -445,6 +445,648 @@ window.__ModuleLoader__.load({
445
445
  "layout.compactionInterrupted": "Compaction was interrupted before completion."
446
446
  };
447
447
  //#endregion
448
+ //#region ../../util/values/src/partial-json.ts
449
+ /**
450
+ * Lazily scanned view of one JSON object's top-level fields, built from text
451
+ * that may still be streaming or from an already parsed object. Nothing is
452
+ * scanned until a reader asks; the view remembers every question it answered
453
+ * and reports changed answers when the owner refreshes for publication.
454
+ * Used for model tool-call arguments: a row reads the fields it
455
+ * cares about at whatever granularity it displays, at every stage of the call.
456
+ * @module @deepseek-ai/dsh-util-values/src/partial-json
457
+ */
458
+ const SIMPLE_ESCAPES = {
459
+ "\"": "\"",
460
+ "\\": "\\",
461
+ "/": "/",
462
+ b: "\b",
463
+ f: "\f",
464
+ n: "\n",
465
+ r: "\r",
466
+ t: " "
467
+ };
468
+ const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
469
+ function isWhitespace(c) {
470
+ return c === " " || c === "\n" || c === "\r" || c === " ";
471
+ }
472
+ function isHex(c) {
473
+ return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
474
+ }
475
+ /**
476
+ * The view. A streaming instance grows through {@link PartialArguments.append};
477
+ * {@link PartialArguments.fromText} and {@link PartialArguments.fromObject} build
478
+ * sealed instances over a finished call. Every reader is total: an absent or
479
+ * differently typed field answers `undefined` (or `false`), never throws.
480
+ */
481
+ var PartialArguments = class PartialArguments {
482
+ /** The view of a call with no arguments available. */
483
+ static EMPTY = PartialArguments.fromObject({});
484
+ /**
485
+ * View finished argument text without scanning it until a reader asks.
486
+ * @param text - the complete argument JSON text.
487
+ * @returns a sealed view.
488
+ */
489
+ static fromText(text) {
490
+ const view = new PartialArguments();
491
+ view.append(text);
492
+ view.sealed = true;
493
+ return view;
494
+ }
495
+ /**
496
+ * View an already parsed argument payload, such as a PTC dispatch object.
497
+ * @param value - the parsed argument value.
498
+ * @returns a sealed view; a non-object payload has no fields.
499
+ */
500
+ static fromObject(value) {
501
+ const view = new PartialArguments();
502
+ view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
503
+ view.sealed = true;
504
+ return view;
505
+ }
506
+ /**
507
+ * The source: text so far or a parsed object, plus whether it can still grow.
508
+ * These are the only enumerable fields, so two views over the same source
509
+ * compare equal structurally however far each has been read.
510
+ */
511
+ chunks = [];
512
+ object;
513
+ sealed = false;
514
+ #ends = [];
515
+ #size = 0;
516
+ #consumed = 0;
517
+ #mode = "root";
518
+ #escape = false;
519
+ #keyStart = 0;
520
+ #keyEscaped = false;
521
+ #key = "";
522
+ #current = null;
523
+ #nestedEnds = [];
524
+ #nestedInString = false;
525
+ #invalidAt;
526
+ #invalidValue = false;
527
+ #entries = /* @__PURE__ */ new Map();
528
+ #order = [];
529
+ #reads = /* @__PURE__ */ new Map();
530
+ /** Whether this view rejects further appends; does not scan text or register reads. */
531
+ get isSealed() {
532
+ return this.sealed;
533
+ }
534
+ /** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
535
+ get invalid() {
536
+ this.scan();
537
+ return this.#mode === "invalid" || this.#invalidValue;
538
+ }
539
+ /**
540
+ * Retain streamed argument text without scanning or comparing observed answers.
541
+ * @param fragment - the text following every fragment appended before.
542
+ */
543
+ append(fragment) {
544
+ if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
545
+ if (fragment.length === 0) return;
546
+ this.chunks.push(fragment);
547
+ this.#size += fragment.length;
548
+ this.#ends.push(this.#size);
549
+ }
550
+ /**
551
+ * Reconcile a streamed prefix with authoritative complete text without joining the fragments.
552
+ * @param text - the final argument text, which replaces missing or conflicting deltas.
553
+ * @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
554
+ */
555
+ settle(text) {
556
+ if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
557
+ let offset = 0;
558
+ for (const chunk of this.chunks) {
559
+ if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
560
+ offset += chunk.length;
561
+ }
562
+ this.chunks = text.length === 0 ? [] : [text];
563
+ this.#ends = text.length === 0 ? [] : [text.length];
564
+ this.sealed = true;
565
+ return this;
566
+ }
567
+ /**
568
+ * Compare observed answers and advance their publication baseline. Unread views remain unscanned.
569
+ * @returns whether any observed answer changed since its first read or the preceding refresh.
570
+ */
571
+ refresh() {
572
+ if (this.#reads.size === 0) return false;
573
+ this.scan();
574
+ let changed = false;
575
+ let completions = false;
576
+ for (const read of this.#reads.values()) {
577
+ if (read.completion) {
578
+ completions = true;
579
+ continue;
580
+ }
581
+ changed = this.refreshRead(read) || changed;
582
+ }
583
+ if (completions) {
584
+ for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
585
+ }
586
+ if (this.sealed) this.#reads.clear();
587
+ return changed;
588
+ }
589
+ refreshRead(read) {
590
+ const now = read.answer();
591
+ if (Object.is(now, read.last)) return false;
592
+ read.last = now;
593
+ return true;
594
+ }
595
+ /**
596
+ * Check whether no further fields can arrive.
597
+ * @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
598
+ */
599
+ closed() {
600
+ return this.remember("closed", "", () => this.closedNow());
601
+ }
602
+ /**
603
+ * List discovered fields in first-appearance order.
604
+ * @returns top-level keys seen so far, in first-appearance order.
605
+ */
606
+ keys() {
607
+ return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
608
+ }
609
+ /**
610
+ * Check whether a top-level field has appeared.
611
+ * @param key - argument name.
612
+ * @returns whether the field has appeared (a string opened or another value began).
613
+ */
614
+ has(key) {
615
+ return this.remember("has", key, () => this.hasNow(key));
616
+ }
617
+ /**
618
+ * Check whether a field's closing delimiter has arrived, without validating its contents.
619
+ * @param key - argument name.
620
+ * @returns whether its delimiter arrived and no content reader has reported an error for this value.
621
+ */
622
+ complete(key) {
623
+ return this.remember("complete", key, () => this.completeNow(key));
624
+ }
625
+ /**
626
+ * Read string length without materializing its text.
627
+ * @param key - argument name.
628
+ * @param options - change granularity for a streaming string.
629
+ * @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
630
+ */
631
+ stringLength(key, options) {
632
+ const step = Math.max(1, Math.floor(options?.step ?? 1));
633
+ const offset = options?.offset ?? 0;
634
+ return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
635
+ }
636
+ /**
637
+ * Check a string against a decoded UTF-16 length limit without materializing it.
638
+ * @param key - argument name.
639
+ * @param maxLength - decoded UTF-16 limit, floored to at least zero.
640
+ * @returns whether the string is longer than the limit; false when absent or not a string.
641
+ */
642
+ stringExceeds(key, maxLength) {
643
+ const limit = Math.max(0, Math.floor(maxLength));
644
+ return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
645
+ }
646
+ /**
647
+ * Read a decoded string, including a streaming prefix.
648
+ * @param key - argument name.
649
+ * @returns the string field's decoded text so far; undefined when absent or not a string.
650
+ */
651
+ text(key) {
652
+ return this.remember("text", key, () => this.textNow(key));
653
+ }
654
+ /**
655
+ * Read at most the first decoded UTF-16 units of a string.
656
+ * @param key - argument name.
657
+ * @param maxLength - maximum decoded UTF-16 length, floored to at least one.
658
+ * @returns the bounded string prefix; undefined when absent or not a string.
659
+ */
660
+ textPrefix(key, maxLength) {
661
+ const limit = Math.max(1, Math.floor(maxLength));
662
+ return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
663
+ }
664
+ /**
665
+ * Read a completed non-string argument.
666
+ * @param key - argument name.
667
+ * @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
668
+ */
669
+ value(key) {
670
+ return this.remember("value", key, () => this.valueNow(key));
671
+ }
672
+ /** Answer a question and, on a streaming view, remember it for change detection. */
673
+ remember(kind, key, read, comparison) {
674
+ this.scan();
675
+ const result = read();
676
+ if (!this.sealed) {
677
+ const id = `${kind}/${key}`;
678
+ if (!this.#reads.has(id)) this.#reads.set(id, {
679
+ completion: kind === "complete",
680
+ answer: comparison === void 0 ? read : () => comparison(read()),
681
+ last: comparison === void 0 ? result : comparison(result)
682
+ });
683
+ }
684
+ return result;
685
+ }
686
+ closedNow() {
687
+ return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
688
+ }
689
+ keysNow() {
690
+ return this.object === void 0 ? this.#order : Object.keys(this.object);
691
+ }
692
+ hasNow(key) {
693
+ return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
694
+ }
695
+ completeNow(key) {
696
+ if (this.object !== void 0) return Object.hasOwn(this.object, key);
697
+ const entry = this.#entries.get(key);
698
+ return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
699
+ }
700
+ lengthNow(key, limit = Number.POSITIVE_INFINITY) {
701
+ if (this.object !== void 0) {
702
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
703
+ return typeof field === "string" ? field.length : void 0;
704
+ }
705
+ const entry = this.#entries.get(key);
706
+ if (entry?.kind !== "string") return void 0;
707
+ if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
708
+ const read = entry.length ??= {
709
+ at: entry.start,
710
+ length: 0,
711
+ text: ""
712
+ };
713
+ this.readString(entry, read, limit, false);
714
+ return read.length;
715
+ }
716
+ textNow(key) {
717
+ if (this.object !== void 0) {
718
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
719
+ return typeof field === "string" ? field : void 0;
720
+ }
721
+ const entry = this.#entries.get(key);
722
+ if (entry?.kind !== "string") return void 0;
723
+ if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
724
+ let text;
725
+ try {
726
+ text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
727
+ } catch (_error) {}
728
+ if (text !== void 0) entry.text = {
729
+ at: entry.end,
730
+ length: text.length,
731
+ text
732
+ };
733
+ }
734
+ const read = entry.text ??= {
735
+ at: entry.start,
736
+ length: 0,
737
+ text: ""
738
+ };
739
+ this.readString(entry, read, Number.POSITIVE_INFINITY, true);
740
+ return read.text;
741
+ }
742
+ textPrefixNow(key, maxLength) {
743
+ if (this.object !== void 0) {
744
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
745
+ return typeof field === "string" ? field.slice(0, maxLength) : void 0;
746
+ }
747
+ const entry = this.#entries.get(key);
748
+ if (entry?.kind !== "string") return void 0;
749
+ const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
750
+ let read = prefixes.get(maxLength);
751
+ if (read === void 0) {
752
+ read = {
753
+ at: entry.start,
754
+ length: 0,
755
+ text: ""
756
+ };
757
+ prefixes.set(maxLength, read);
758
+ }
759
+ this.readString(entry, read, maxLength, true);
760
+ return read.text;
761
+ }
762
+ valueNow(key) {
763
+ if (this.object !== void 0) {
764
+ if (!Object.hasOwn(this.object, key)) return void 0;
765
+ const field = this.object[key];
766
+ return typeof field === "string" ? void 0 : field;
767
+ }
768
+ const entry = this.#entries.get(key);
769
+ if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
770
+ if (entry.parsed === void 0) try {
771
+ entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
772
+ } catch (_error) {
773
+ entry.invalid = true;
774
+ this.#invalidValue = true;
775
+ }
776
+ return entry.parsed;
777
+ }
778
+ chunkAt(at) {
779
+ let low = 0;
780
+ let high = this.#ends.length;
781
+ while (low < high) {
782
+ const mid = low + high >>> 1;
783
+ if (this.#ends[mid] <= at) low = mid + 1;
784
+ else high = mid;
785
+ }
786
+ return low;
787
+ }
788
+ /** Materialize only a requested range, never the cumulative source. */
789
+ slice(start, end) {
790
+ if (start >= end) return "";
791
+ const first = this.chunkAt(start);
792
+ const last = this.chunkAt(end - 1);
793
+ const base = first === 0 ? 0 : this.#ends[first - 1];
794
+ if (first === last) return this.chunks[first].slice(start - base, end - base);
795
+ const parts = [this.chunks[first].slice(start - base)];
796
+ for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
797
+ parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
798
+ return parts.join("");
799
+ }
800
+ readString(entry, read, limit, materialize) {
801
+ const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
802
+ if (!entry.needsDecoding) {
803
+ const length = Math.min(end - read.at, limit - read.length);
804
+ if (length <= 0) return;
805
+ if (materialize) read.text += this.slice(read.at, read.at + length);
806
+ read.at += length;
807
+ read.length += length;
808
+ return;
809
+ }
810
+ let chunkIndex = this.chunkAt(read.at);
811
+ while (read.at < end && read.length < limit) {
812
+ const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
813
+ const chunk = this.chunks[chunkIndex];
814
+ const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
815
+ const boundary = remaining.search(CONTENT_ESCAPE);
816
+ const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
817
+ if (length > 0) {
818
+ if (materialize) read.text += remaining.slice(0, length);
819
+ read.at += length;
820
+ read.length += length;
821
+ if (read.at === base + chunk.length) chunkIndex++;
822
+ continue;
823
+ }
824
+ const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
825
+ let decoded;
826
+ let width = 2;
827
+ if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
828
+ if (remaining[0] === "\\" && type === "u") {
829
+ const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
830
+ let valid = true;
831
+ for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
832
+ if (valid) {
833
+ if (hex.length < 4 && entry.end < 0) return;
834
+ if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
835
+ }
836
+ width = 6;
837
+ } else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
838
+ if (decoded === void 0) {
839
+ entry.invalidAt = read.at;
840
+ this.#invalidValue = true;
841
+ return;
842
+ }
843
+ if (materialize) read.text += decoded;
844
+ read.length++;
845
+ read.at += width;
846
+ while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
847
+ }
848
+ }
849
+ /** Locate new field ranges without decoding or parsing their contents. */
850
+ scan() {
851
+ if (this.object !== void 0 || this.#consumed === this.#size) return;
852
+ for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
853
+ const pending = this.chunks[i];
854
+ const base = i === 0 ? 0 : this.#ends[i - 1];
855
+ for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
856
+ if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
857
+ const end = this.stringBoundary(pending, index);
858
+ this.#consumed += end - index;
859
+ index = end;
860
+ if (index === pending.length) break;
861
+ }
862
+ this.step(pending[index], this.#consumed);
863
+ this.#consumed++;
864
+ }
865
+ }
866
+ }
867
+ /** Only raw quotes and their preceding backslash runs can terminate a string. */
868
+ stringBoundary(fragment, start) {
869
+ let at = start;
870
+ while (true) {
871
+ const quote = fragment.indexOf("\"", at);
872
+ const end = quote < 0 ? fragment.length : quote;
873
+ if (this.#mode === "string") {
874
+ const entry = this.#current;
875
+ if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
876
+ }
877
+ let slashStart = end;
878
+ while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
879
+ const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
880
+ this.#escape = quote < 0 && escaped;
881
+ if (quote < 0 || !escaped) return end;
882
+ at = quote + 1;
883
+ }
884
+ }
885
+ step(c, at) {
886
+ switch (this.#mode) {
887
+ case "root":
888
+ if (isWhitespace(c)) return;
889
+ if (c === "{") {
890
+ this.#mode = "key-or-end";
891
+ return;
892
+ }
893
+ this.fail();
894
+ return;
895
+ case "key-or-end":
896
+ if (isWhitespace(c)) return;
897
+ if (c === "}") {
898
+ this.#mode = "closed";
899
+ return;
900
+ }
901
+ if (c === "\"") {
902
+ this.beginKey(at);
903
+ return;
904
+ }
905
+ this.fail();
906
+ return;
907
+ case "key-only":
908
+ if (isWhitespace(c)) return;
909
+ if (c === "\"") {
910
+ this.beginKey(at);
911
+ return;
912
+ }
913
+ this.fail();
914
+ return;
915
+ case "key":
916
+ this.stepKey(c, at);
917
+ return;
918
+ case "colon":
919
+ if (isWhitespace(c)) return;
920
+ if (c === ":") {
921
+ this.#mode = "value";
922
+ return;
923
+ }
924
+ this.fail();
925
+ return;
926
+ case "value":
927
+ this.beginValue(c, at);
928
+ return;
929
+ case "string": {
930
+ const entry = this.#current;
931
+ entry.end = at;
932
+ this.#current = null;
933
+ this.#mode = "comma-or-end";
934
+ return;
935
+ }
936
+ case "scalar":
937
+ this.stepScalar(c, at);
938
+ return;
939
+ case "nested":
940
+ this.stepNested(c, at);
941
+ return;
942
+ case "comma-or-end":
943
+ if (isWhitespace(c)) return;
944
+ if (c === ",") {
945
+ this.#mode = "key-only";
946
+ return;
947
+ }
948
+ if (c === "}") {
949
+ this.#mode = "closed";
950
+ return;
951
+ }
952
+ this.fail();
953
+ return;
954
+ case "closed":
955
+ if (isWhitespace(c)) return;
956
+ this.fail();
957
+ return;
958
+ /* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
959
+ case "invalid": return;
960
+ /* v8 ignore next 2 -- Every scanner mode has a handler above. */
961
+ default: assertNever(this.#mode);
962
+ }
963
+ }
964
+ fail() {
965
+ this.#invalidAt = this.#consumed;
966
+ this.#mode = "invalid";
967
+ this.#current = null;
968
+ }
969
+ beginKey(at) {
970
+ this.#mode = "key";
971
+ this.#keyStart = at + 1;
972
+ this.#keyEscaped = false;
973
+ this.#escape = false;
974
+ }
975
+ stepKey(c, at) {
976
+ if (c < " ") {
977
+ this.fail();
978
+ return;
979
+ }
980
+ if (this.#escape) {
981
+ this.#escape = false;
982
+ return;
983
+ }
984
+ if (c === "\\") {
985
+ this.#escape = true;
986
+ this.#keyEscaped = true;
987
+ return;
988
+ }
989
+ if (c !== "\"") return;
990
+ const raw = this.slice(this.#keyStart, at);
991
+ if (this.#keyEscaped) try {
992
+ this.#key = JSON.parse(`"${raw}"`);
993
+ } catch (_error) {
994
+ this.fail();
995
+ return;
996
+ }
997
+ else this.#key = raw;
998
+ this.#mode = "colon";
999
+ }
1000
+ open(entry) {
1001
+ if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
1002
+ this.#entries.set(this.#key, entry);
1003
+ this.#current = entry;
1004
+ }
1005
+ beginValue(c, at) {
1006
+ if (isWhitespace(c)) return;
1007
+ if (c === "\"") {
1008
+ this.open({
1009
+ kind: "string",
1010
+ start: at + 1,
1011
+ end: -1,
1012
+ needsDecoding: false,
1013
+ invalidAt: void 0,
1014
+ length: void 0,
1015
+ text: void 0,
1016
+ prefixes: void 0
1017
+ });
1018
+ this.#escape = false;
1019
+ this.#mode = "string";
1020
+ return;
1021
+ }
1022
+ if (c === "}" || c === "," || c === ":" || c === "]") {
1023
+ this.fail();
1024
+ return;
1025
+ }
1026
+ this.open({
1027
+ kind: "value",
1028
+ start: at,
1029
+ end: -1,
1030
+ parsed: void 0,
1031
+ invalid: false
1032
+ });
1033
+ if (c === "{" || c === "[") {
1034
+ this.#mode = "nested";
1035
+ this.#nestedEnds = [c === "{" ? "}" : "]"];
1036
+ this.#nestedInString = false;
1037
+ this.#escape = false;
1038
+ return;
1039
+ }
1040
+ this.#mode = "scalar";
1041
+ }
1042
+ stepScalar(c, at) {
1043
+ if (c !== "," && c !== "}" && !isWhitespace(c)) return;
1044
+ this.closeValue(at);
1045
+ this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
1046
+ }
1047
+ stepNested(c, at) {
1048
+ if (this.#nestedInString) {
1049
+ this.#nestedInString = false;
1050
+ return;
1051
+ }
1052
+ if (c === "\"") {
1053
+ this.#nestedInString = true;
1054
+ return;
1055
+ }
1056
+ if (c === "{" || c === "[") {
1057
+ this.#nestedEnds.push(c === "{" ? "}" : "]");
1058
+ return;
1059
+ }
1060
+ if (c === "}" || c === "]") {
1061
+ if (this.#nestedEnds.pop() !== c) {
1062
+ this.fail();
1063
+ return;
1064
+ }
1065
+ if (this.#nestedEnds.length === 0) {
1066
+ this.closeValue(at + 1);
1067
+ this.#mode = "comma-or-end";
1068
+ }
1069
+ }
1070
+ }
1071
+ closeValue(end) {
1072
+ const entry = this.#current;
1073
+ entry.end = end;
1074
+ this.#current = null;
1075
+ }
1076
+ };
1077
+ //#endregion
1078
+ //#region ../../util/values/src/index.ts
1079
+ /**
1080
+ * Mark an unreachable closed-union branch.
1081
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
1082
+ * @param context - optional switch-site label included in the failure message.
1083
+ * @returns never; a runtime value that escaped its type always throws.
1084
+ */
1085
+ function assertNever(value, context) {
1086
+ const rendered = JSON.stringify(value) ?? String(value);
1087
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
1088
+ }
1089
+ //#endregion
448
1090
  //#region ../../llm/llm/src/assistant-stream.ts
449
1091
  /**
450
1092
  * Whether one chunk carries the model's first output token for latency measurement.
@@ -1654,6 +2296,7 @@ window.__ModuleLoader__.load({
1654
2296
  callId: String(match.event.data.callId),
1655
2297
  name: match.event.data.name,
1656
2298
  argsRaw: match.event.data.arguments,
2299
+ args: PartialArguments.fromText(match.event.data.arguments),
1657
2300
  turn: match.event.data.turn,
1658
2301
  step: match.event.data.step,
1659
2302
  time: match.event.time,
@@ -1668,6 +2311,8 @@ window.__ModuleLoader__.load({
1668
2311
  seq: match.event.seq,
1669
2312
  time: match.event.time,
1670
2313
  callId: String(message.source.callId),
2314
+ name: previous?.name ?? "",
2315
+ args: previous?.args ?? PartialArguments.EMPTY,
1671
2316
  call: previous === void 0 ? null : {
1672
2317
  name: previous.name,
1673
2318
  argsRaw: previous.argsRaw
@@ -1693,6 +2338,7 @@ window.__ModuleLoader__.load({
1693
2338
  parentCallId: data.parentCallId,
1694
2339
  name: data.name,
1695
2340
  argsRaw: JSON.stringify(data.arguments),
2341
+ args: PartialArguments.fromObject(data.arguments),
1696
2342
  turn: locationTurn(match),
1697
2343
  step: locationStep(match),
1698
2344
  time: match.event.time,
@@ -1706,6 +2352,8 @@ window.__ModuleLoader__.load({
1706
2352
  time: match.event.time,
1707
2353
  callId: data.subCallId,
1708
2354
  parentCallId: data.parentCallId,
2355
+ name: data.name,
2356
+ args: previous?.args ?? PartialArguments.fromObject(data.arguments),
1709
2357
  call: {
1710
2358
  name: data.name,
1711
2359
  argsRaw: JSON.stringify(data.arguments)
@@ -1800,6 +2448,8 @@ window.__ModuleLoader__.load({
1800
2448
  time: interruptedAt.time,
1801
2449
  callId: block.callId,
1802
2450
  ...block.parentCallId === void 0 ? {} : { parentCallId: block.parentCallId },
2451
+ name: block.name,
2452
+ args: block.args,
1803
2453
  call: {
1804
2454
  name: block.name,
1805
2455
  argsRaw: block.argsRaw
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-trajectory",
3
3
  "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)",
4
- "version": "0.2.0-rc.1",
4
+ "version": "0.2.1-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "license": "MIT",
41
41
  "peerDependencies": {
42
- "@deepseek-ai/cordis": "~4.0.4"
42
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/react": "~18.3.1",
@@ -48,23 +48,24 @@
48
48
  "react-dom": "^18.2.0",
49
49
  "@tanstack/react-virtual": "^3.14.9",
50
50
  "diff": "^9.0.0",
51
- "@deepseek-ai/dsh-agent": "0.2.0-rc.1",
52
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
53
- "@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.1",
54
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
55
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
56
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
57
- "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
58
- "@deepseek-ai/cordis": "~4.0.4",
59
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
60
- "@deepseek-ai/dsh-client-ui-chat": "0.2.0-rc.1",
61
- "@deepseek-ai/dsh-session": "0.2.0-rc.1",
62
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
63
- "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.1",
64
- "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.1",
65
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.1",
66
- "@deepseek-ai/dsh-compaction": "0.2.0-rc.1",
67
- "@deepseek-ai/dsh-llm": "0.2.0-rc.1"
51
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.1",
52
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.1",
53
+ "@deepseek-ai/dsh-client-test-runtime": "0.2.1-alpha.1",
54
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.1",
55
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.1",
56
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.1",
57
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.1",
58
+ "@deepseek-ai/dsh-compaction": "0.2.1-alpha.1",
59
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
60
+ "@deepseek-ai/dsh-client-store": "0.2.1-alpha.1",
61
+ "@deepseek-ai/dsh-client-ui-chat": "0.2.1-alpha.1",
62
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.1",
63
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.1",
64
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.1",
65
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
66
+ "@deepseek-ai/dsh-attachment": "0.2.1-alpha.1",
67
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.1",
68
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.1"
68
69
  },
69
70
  "files": [
70
71
  "lib/index.js",