dsh-milestone 0.7.0 → 0.7.2

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
@@ -114,7 +114,7 @@ shell.overlay (root scope)
114
114
 
115
115
  ## 版本与兼容
116
116
 
117
- - 当前官方支持线:**`0.1.5-rc.1`**(与 `@deepseek-ai/dsh` 最新 `latest` / `next` 标签一致)。
117
+ - 当前官方支持线:**`0.1.5` 线**(`0.1.5-rc.1` npm `latest`、`0.1.5-rc.2` `next`;peer 范围 `>=0.1.5-rc.1 <0.3.0-0` 同时覆盖两者)。
118
118
  - peer/dev 范围为收紧的 `>=0.1.5-rc.1 <0.3.0-0`(`dsh-client-locale` / `dsh-client-store` / `dsh-client-ui-slots`):依赖解析到旧线时会得到明确的 ERESOLVE,而不是静默错配。
119
119
  - 0.1.5 起流式时序内嵌在 `assistant/message` / `assistant/attempt` 的紧凑 `stream` 中(独立的 `assistant/chunk` 事件已移除),TTFT 由官方 `assistantStreamFirstTokenTime` 读取;`TokenUsage` 字段更名为 `inputTokens`/`outputTokens`/`totalTokens` 并拆分缓存计数,插件的 `input` 显示为三项之和(缓存计入),保持旧的「提示词 token」含义。
120
120
  - 会话数据自 0.1.2 起改经**会话投影**提供(插件注册 `milestone.messages`,client 端 `useProjection` 读取);`defineStore` 由 `@deepseek-ai/dsh-client-store` 提供。
@@ -140,7 +140,24 @@ shell.overlay (root scope)
140
140
  ## 更新日志
141
141
 
142
142
  <details>
143
- <summary>v0.7.0 / v0.6.6 / v0.6.5 / v0.6.4(点击展开)</summary>
143
+ <summary>v0.7.2 / v0.7.1 / v0.7.0 / v0.6.6 / v0.6.5 / v0.6.4(点击展开)</summary>
144
+
145
+ **v0.7.2** · 点击旧圆点自动翻页定位(深跳鲁棒性)· 459 项测试
146
+
147
+ - **点旧圆点不再"没反应"**:里程碑条覆盖整个会话,但 DOM 只渲染已加载窗口——此前点一个很早的圆点会静默无反应。现在会自动按页载入更早历史直到定位(有界 20 页),定位期间该圆点脉冲提示,失败则安静结束。
148
+ - 站内搜索的"下一个匹配"跳转同样受益。
149
+
150
+ > [GitHub Release v0.7.2](https://github.com/SnowCrescenter-tech/dsh-milestone/releases/tag/v0.7.2)
151
+
152
+ **v0.7.1** · 升级不再被卡(可选 peer + 兼容边界)· 恢复模型与错误徽标 · 跟随官方 0.1.5-rc.2 · 457 项测试
153
+
154
+ - **升级不再被 ERESOLVE 卡死**:三个 DSH 客户端包 peer 改为 **optional**——官方换 rc 线时只告警、不再拒绝安装;并新增**渲染期兼容边界**:契约一旦变化,给出明确提示("与当前 DSH 版本不兼容,请升级插件"),不再静默消失或拖垮宿主 UI。
155
+ - **恢复悬停卡的模型**:改从 0.1.5 的 `request/context` 事件读取 provider / model(此前恒为 `null`)。
156
+ - **恢复错误 / 重试徽标**:由 `turn/end` 的结束原因(error / max-tokens)与 `assistant/attempt` 结算次数重建(此前徽标恒空)。
157
+ - **跟随官方 `0.1.5-rc.2`**(npm `latest` 仍为 rc.1;peer 范围 `>=0.1.5-rc.1 <0.3.0-0` 同时覆盖两者);上游 `dsh-client-store` 漏声明 `zustand`/`immer` 的兜底扩到整条 0.1.5 线。
158
+ - **CI 新增 `advisory / dsh next`**:持续对官方 `next` 线跑类型检查与测试,提前预警破坏性变更(首跑即发现 rc.2 与上述打包缺陷)。
159
+
160
+ > [GitHub Release v0.7.1](https://github.com/SnowCrescenter-tech/dsh-milestone/releases/tag/v0.7.1)
144
161
 
145
162
  **v0.7.0** · 跟随官方 0.1.5(会话投影数据层)· 折叠为可拖动悬浮球(issue #4)· 447 项测试
146
163
 
package/lib/client.js CHANGED
@@ -4,9 +4,31 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react_jsx_runtime = require("react/jsx-runtime");
8
7
  let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
9
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
10
+ //#region src/client/modal-tokens.ts
11
+ /**
12
+ * modal-tokens: the ONE source for the settings modal's dark-panel palette and
13
+ * radius scale, shared with the 0.6.5 coach-tour bubble so the two surfaces
14
+ * cannot drift (same dark panel on a dark host, same three text tiers, same
15
+ * 12px panel / 8px control radius, same border tone).
16
+ *
17
+ * The static MODAL_CSS block (hover/focus-visible/reduced-motion rules) stays
18
+ * in MilestoneRail.tsx because it also styles the settings modal's own chrome
19
+ * (`[data-support-card]`, `[data-toolbar-*]`, ...); the tour bubble carries
20
+ * its own small TOUR_CSS block for its controls, built from these same tokens.
21
+ * MilestoneRail.tsx imports these constants verbatim so a future palette
22
+ * change edits exactly one file.
23
+ */
24
+ const MODAL_BG = "rgba(20, 24, 32, 0.98)";
25
+ const MODAL_FG = "#e6e8ee";
26
+ const MODAL_TITLE = "#c7cede";
27
+ const MODAL_TEXT = "#b9c2d4";
28
+ const MODAL_HINT = "#8b96ab";
29
+ const MODAL_BORDER = "rgba(255, 255, 255, 0.14)";
30
+ const MODAL_TIP_BG = "#222834";
31
+ //#endregion
10
32
  //#region src/client/MilestoneOverlay.tsx
11
33
  /**
12
34
  * @param props - runtime share (root kit) + the narrowed renderSlot and the
@@ -129,6 +151,23 @@ window.__ModuleLoader__.load({
129
151
  * @param input - the mark's snapshot signals.
130
152
  * @returns the winning badge kind, or null when no signal applies.
131
153
  */
154
+ /**
155
+ * 0.1.5: durable badge kinds for one projected turn. The fold no longer sees
156
+ * the chat-layer `turn-error` / `turn-max-tokens` node kinds, but `turn/end`'s
157
+ * reason carries the same signal, and every `assistant/attempt` settlement
158
+ * (failed / retried / cancelled / stream error) is a retry.
159
+ * @param turn - the projection's turn meta (only the two fields are read).
160
+ * @returns the node-kind vocabulary {@link deriveBadge} consumes; empty when normal.
161
+ */
162
+ function kindsForTurn(turn) {
163
+ const kinds = [];
164
+ if (turn.endReason === "error" || turn.endReason === "interrupted") kinds.push("turn-error");
165
+ if (turn.endReason === "max-tokens") kinds.push("turn-max-tokens");
166
+ if ((turn.attempts ?? 0) > 0) kinds.push("model-retry");
167
+ return kinds.length === 0 ? EMPTY_KINDS : kinds;
168
+ }
169
+ /** Stable empty result so normal turns never allocate a fresh array. */
170
+ const EMPTY_KINDS = Object.freeze([]);
132
171
  function deriveBadge(input) {
133
172
  if (input.nodeKinds.includes("turn-error")) return "error";
134
173
  if (input.nodeKinds.includes("turn-max-tokens")) return "max-tokens";
@@ -469,12 +508,12 @@ window.__ModuleLoader__.load({
469
508
  * @returns the turn's metadata, null where unknown.
470
509
  */
471
510
  function deriveTurnMetaFromProjection(turn) {
472
- if (turn?.usage === void 0) return EMPTY_META;
511
+ if (turn === void 0) return EMPTY_META;
473
512
  return {
474
- model: null,
513
+ model: turn.model ?? null,
475
514
  purpose: null,
476
- inputTokens: turn.usage.input,
477
- outputTokens: turn.usage.output
515
+ inputTokens: turn.usage?.input ?? null,
516
+ outputTokens: turn.usage?.output ?? null
478
517
  };
479
518
  }
480
519
  //#endregion
@@ -1463,12 +1502,6 @@ window.__ModuleLoader__.load({
1463
1502
  ]
1464
1503
  });
1465
1504
  }
1466
- const MODAL_FG = "#e6e8ee";
1467
- const MODAL_TITLE = "#c7cede";
1468
- const MODAL_TEXT = "#b9c2d4";
1469
- const MODAL_HINT = "#8b96ab";
1470
- const MODAL_BORDER = "rgba(255, 255, 255, 0.14)";
1471
- const MODAL_TIP_BG = "#222834";
1472
1505
  //#endregion
1473
1506
  //#region src/client/onboarding-store.ts
1474
1507
  /**
@@ -2832,7 +2865,7 @@ window.__ModuleLoader__.load({
2832
2865
  * Installed plugin version. Injected at build time as
2833
2866
  * `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
2834
2867
  */
2835
- const PLUGIN_VERSION = "0.7.0";
2868
+ const PLUGIN_VERSION = "0.7.2";
2836
2869
  //#endregion
2837
2870
  //#region src/client/MilestoneRail.tsx
2838
2871
  /**
@@ -3104,6 +3137,18 @@ window.__ModuleLoader__.load({
3104
3137
  const DEEP_LINK_MAX_POLLS = 5;
3105
3138
  /** P3: bounded polls after `loadOlder`, then the deep link gives up silently. */
3106
3139
  const DEEP_LINK_MAX_RETRY_POLLS = 5;
3140
+ /**
3141
+ * Jump paging: the rail's dots cover the WHOLE log while the DOM renders only
3142
+ * the loaded window, so activating an older dot must page history in first.
3143
+ * Bounded so an unreachable target gives up instead of paging forever.
3144
+ */
3145
+ const LOCATE_MAX_PAGES = 20;
3146
+ /** Delay between a page fetch and the DOM-row recheck (lets React commit). */
3147
+ const LOCATE_POLL_DELAY = 150;
3148
+ /** Pulse applied to the dot whose jump is still paging history in. */
3149
+ const LOCATING_CSS = `@keyframes ms-locating { 0%, 100% { opacity: 1 } 50% { opacity: 0.35 } }
3150
+ [data-locating="true"] { animation: ms-locating 800ms ease-in-out infinite; }
3151
+ @media (prefers-reduced-motion: reduce) { [data-locating="true"] { animation: none } }`;
3107
3152
  /** B4 update-check: mount-time silent check delay (ms) — give the harness
3108
3153
  * time to settle before hitting the registry. */
3109
3154
  const UPDATE_CHECK_MOUNT_DELAY = 1500;
@@ -3173,7 +3218,16 @@ window.__ModuleLoader__.load({
3173
3218
  preview: m.preview
3174
3219
  }));
3175
3220
  }, [projection]);
3176
- const kindsByTurn = EMPTY_KINDS_BY_TURN;
3221
+ const kindsByTurn = (0, react.useMemo)(() => {
3222
+ const turns = projection?.turns;
3223
+ if (turns === void 0 || turns.length === 0) return EMPTY_KINDS_BY_TURN;
3224
+ const map = /* @__PURE__ */ new Map();
3225
+ for (const t of turns) {
3226
+ const kinds = kindsForTurn(t);
3227
+ if (kinds.length > 0) map.set(t.turn, kinds);
3228
+ }
3229
+ return map;
3230
+ }, [projection]);
3177
3231
  const running = useSession((s) => s.running);
3178
3232
  const awaitingInput = useSession((s) => s.queue).length > 0;
3179
3233
  const [railBox, setRailBox] = (0, react.useState)(null);
@@ -3196,6 +3250,13 @@ window.__ModuleLoader__.load({
3196
3250
  const listRef = (0, react.useRef)(null);
3197
3251
  const currentKey = useCurrentAnchor(marks.map((m) => m.key));
3198
3252
  /**
3253
+ * Mark key whose jump is still paging older history in; its dot pulses so a
3254
+ * slow locate never looks like a dead click.
3255
+ */
3256
+ const [locatingKey, setLocatingKey] = (0, react.useState)(null);
3257
+ /** Guards against overlapping locate loops (a second activation is ignored). */
3258
+ const locatingRef = (0, react.useRef)(false);
3259
+ /**
3199
3260
  * P3: jump to the chat row with the given node key — smooth-scroll it into
3200
3261
  * view and write the position back into the URL hash (`#msg=<key>`) so
3201
3262
  * refresh and share preserve it. `history.replaceState` (not a
@@ -3213,6 +3274,45 @@ window.__ModuleLoader__.load({
3213
3274
  });
3214
3275
  history.replaceState(null, "", buildMessageHash(key));
3215
3276
  };
3277
+ /**
3278
+ * Jump to a mark, paging older history in first when its row is not rendered
3279
+ * yet. The dots cover the whole log but the DOM holds only the loaded window,
3280
+ * so without this an older dot is a silent no-op (the official rail pages
3281
+ * unloaded turns in the same way). Bounded to LOCATE_MAX_PAGES fetches; the
3282
+ * dot pulses meanwhile.
3283
+ * @param key - the mark key (deep-link payload + exact-match anchor).
3284
+ * @param messageId - the user/message id (real-harness anchor suffix).
3285
+ */
3286
+ const locateAndJump = (key, messageId) => {
3287
+ if (findRow(key, messageId) !== null) {
3288
+ jump(key, messageId);
3289
+ return;
3290
+ }
3291
+ if (!hasMore || locatingRef.current) return;
3292
+ locatingRef.current = true;
3293
+ setLocatingKey(key);
3294
+ (async () => {
3295
+ try {
3296
+ for (let page = 0; page < LOCATE_MAX_PAGES; page += 1) {
3297
+ try {
3298
+ await loadOlder();
3299
+ } catch {
3300
+ return;
3301
+ }
3302
+ await new Promise((resolve) => {
3303
+ window.setTimeout(resolve, LOCATE_POLL_DELAY);
3304
+ });
3305
+ if (findRow(key, messageId) !== null) {
3306
+ jump(key, messageId);
3307
+ return;
3308
+ }
3309
+ }
3310
+ } finally {
3311
+ locatingRef.current = false;
3312
+ setLocatingKey(null);
3313
+ }
3314
+ })();
3315
+ };
3216
3316
  const displayMarks = (0, react.useMemo)(() => {
3217
3317
  if (!bookmarksOnly) return marks;
3218
3318
  return filterByBookmarks(marks, bookmarkedKeys).visible.map((i) => marks[i]);
@@ -3582,7 +3682,7 @@ window.__ModuleLoader__.load({
3582
3682
  activePos: next
3583
3683
  }));
3584
3684
  const mark = displayMarks[matches[next]];
3585
- jump(mark.key, mark.messageId);
3685
+ locateAndJump(mark.key, mark.messageId);
3586
3686
  };
3587
3687
  const onSearchKeyDown = (e) => {
3588
3688
  if (e.key === "Enter") advanceMatch();
@@ -4169,6 +4269,7 @@ window.__ModuleLoader__.load({
4169
4269
  "data-inset": String(inset),
4170
4270
  children: [
4171
4271
  pulseCss !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: pulseCss }),
4272
+ locatingKey !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: LOCATING_CSS }),
4172
4273
  focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: buildFocusCss(prefs.focus) }),
4173
4274
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: MODAL_CSS }),
4174
4275
  showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -5273,7 +5374,7 @@ window.__ModuleLoader__.load({
5273
5374
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5274
5375
  style: { color: "#8b96ab" },
5275
5376
  children: [t("update.current"), ": "]
5276
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.7.0" })] }),
5377
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.7.2" })] }),
5277
5378
  updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5278
5379
  "data-update-latest": true,
5279
5380
  children: [
@@ -5451,8 +5552,9 @@ window.__ModuleLoader__.load({
5451
5552
  top: rect.top + rect.height / 2
5452
5553
  });
5453
5554
  },
5454
- onClick: () => jump(mark.key, mark.messageId),
5555
+ onClick: () => locateAndJump(mark.key, mark.messageId),
5455
5556
  "data-rail-dot": true,
5557
+ "data-locating": locatingKey === mark.key ? "true" : void 0,
5456
5558
  "data-turn-gap": showGroupGap ? "true" : void 0,
5457
5559
  "data-turn": showGroupGap && mark.turn !== void 0 ? mark.turn : void 0,
5458
5560
  "data-collapsed-summary": summaryCount !== void 0 ? "true" : void 0,
@@ -5556,6 +5658,70 @@ window.__ModuleLoader__.load({
5556
5658
  });
5557
5659
  }
5558
5660
  //#endregion
5661
+ //#region src/client/CompatBoundary.tsx
5662
+ /**
5663
+ * Compatibility boundary for both plugin surfaces.
5664
+ *
5665
+ * The plugin consumes the framework's session standard kit (`useSession`,
5666
+ * `useProjection`, the store seat) and the slot registry, all of which a DSH
5667
+ * upgrade may rename or reshape. Without a boundary such a failure throws out
5668
+ * of the plugin's render — and when the registry silently ignores a stale
5669
+ * entry the rail simply vanishes with no explanation.
5670
+ *
5671
+ * The boundary converts any throw into ONE compact, honest notice instead: the
5672
+ * surrounding harness keeps working, and the user is told what to do (0.1.2→
5673
+ * 0.1.5 has already shown that these upgrades do break third-party plugins).
5674
+ *
5675
+ * A class boundary rather than a per-render capability probe is deliberate:
5676
+ * React exposes error boundaries only through class components, and swapping
5677
+ * hook implementations based on a runtime probe would break hook order.
5678
+ */
5679
+ /** Catches a render failure anywhere below and shows the incompatibility notice. */
5680
+ var CompatBoundary = class extends react.Component {
5681
+ state = { failed: false };
5682
+ static getDerivedStateFromError() {
5683
+ return { failed: true };
5684
+ }
5685
+ componentDidCatch(error, info) {
5686
+ console.error("[dsh-milestone] incompatible with this DSH version — the rail is disabled. Please update dsh-milestone (or pin an older DSH).", error, info.componentStack);
5687
+ }
5688
+ render() {
5689
+ if (!this.state.failed) return this.props.children;
5690
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5691
+ "data-milestone-incompatible": true,
5692
+ role: "status",
5693
+ style: {
5694
+ position: "fixed",
5695
+ right: 14,
5696
+ bottom: 14,
5697
+ zIndex: 100,
5698
+ maxWidth: 300,
5699
+ padding: "10px 12px",
5700
+ background: MODAL_BG,
5701
+ color: MODAL_HINT,
5702
+ border: `1px solid ${MODAL_BORDER}`,
5703
+ borderRadius: 12,
5704
+ fontSize: 12,
5705
+ lineHeight: 1.5,
5706
+ fontFamily: "inherit"
5707
+ },
5708
+ children: [
5709
+ "dsh-milestone 与当前 DSH 版本不兼容,已停用。请升级插件。",
5710
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
5711
+ "Incompatible with this DSH version — please update dsh-milestone."
5712
+ ]
5713
+ });
5714
+ }
5715
+ };
5716
+ /** The `shell.overlay` seat wrapped in the compatibility boundary. */
5717
+ function GuardedMilestoneOverlay(props) {
5718
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneOverlay, { ...props }) });
5719
+ }
5720
+ /** The `milestone.rail` seat wrapped in the compatibility boundary. */
5721
+ function GuardedMilestoneRail(props) {
5722
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneRail, { ...props }) });
5723
+ }
5724
+ //#endregion
5559
5725
  //#region src/client/bookmarkStore.ts
5560
5726
  /**
5561
5727
  * Persisted per-session bookmarks store for the milestone rail.
@@ -5698,7 +5864,7 @@ window.__ModuleLoader__.load({
5698
5864
  kind: "single",
5699
5865
  scope: "session"
5700
5866
  } }
5701
- }, MilestoneOverlay));
5867
+ }, GuardedMilestoneOverlay));
5702
5868
  ctx.slots.inject("milestone.rail", () => ctx.slots.register({
5703
5869
  name: "milestone.rail",
5704
5870
  store: createBookmarksStore,
@@ -5709,7 +5875,7 @@ window.__ModuleLoader__.load({
5709
5875
  searchSessions: createSessionSearch(sessions),
5710
5876
  openSession: createOpenSession(sessions)
5711
5877
  })
5712
- }, MilestoneRail));
5878
+ }, GuardedMilestoneRail));
5713
5879
  }
5714
5880
  //#endregion
5715
5881
  exports.apply = apply;
package/lib/index.js CHANGED
@@ -20,7 +20,10 @@ const turnMetaSchema = z.object({
20
20
  input: z.number(),
21
21
  output: z.number(),
22
22
  total: z.number()
23
- }).optional()
23
+ }).optional(),
24
+ provider: z.string().optional(),
25
+ model: z.string().optional(),
26
+ attempts: z.number().int().nonnegative().optional()
24
27
  });
25
28
  z.object({
26
29
  messages: z.array(messageEntrySchema),
@@ -73,7 +76,7 @@ function initialState() {
73
76
  }
74
77
  const milestoneMessagesProjectionDefinition = {
75
78
  key: "milestone.messages",
76
- stateVersion: 3,
79
+ stateVersion: 4,
77
80
  stateSchema: z.custom(),
78
81
  init: (_header, _inheritedEventCount) => initialState(),
79
82
  apply(state, event) {
@@ -81,12 +84,17 @@ const milestoneMessagesProjectionDefinition = {
81
84
  case "turn/start": {
82
85
  const turn = event.data.turn;
83
86
  if (state.turns.some((t) => t.turn === turn)) return state;
87
+ const route = state.route;
84
88
  return {
85
- messages: state.messages,
89
+ ...state,
86
90
  turns: [...state.turns, {
87
91
  turn,
88
92
  startSeq: SessionSeq(event.seq),
89
- startTime: event.time
93
+ startTime: event.time,
94
+ ...route === void 0 ? {} : {
95
+ provider: route.provider,
96
+ model: route.model
97
+ }
90
98
  }]
91
99
  };
92
100
  }
@@ -97,6 +105,7 @@ const milestoneMessagesProjectionDefinition = {
97
105
  const preview = previewText(content, 80);
98
106
  const openTurn = state.turns[state.turns.length - 1];
99
107
  return {
108
+ ...state,
100
109
  messages: [...state.messages, {
101
110
  seq: SessionSeq(event.seq),
102
111
  messageId: typeof data.id === "string" ? data.id : "",
@@ -104,8 +113,7 @@ const milestoneMessagesProjectionDefinition = {
104
113
  turn: openTurn?.turn ?? 0,
105
114
  preview,
106
115
  text
107
- }],
108
- turns: state.turns
116
+ }]
109
117
  };
110
118
  }
111
119
  case "assistant/message": {
@@ -143,24 +151,31 @@ const milestoneMessagesProjectionDefinition = {
143
151
  const turns = state.turns.slice();
144
152
  turns[index] = next;
145
153
  return {
146
- messages: state.messages,
154
+ ...state,
147
155
  turns
148
156
  };
149
157
  }
150
158
  case "assistant/attempt": {
151
159
  const turn = event.data.turn;
152
160
  const index = state.turns.findIndex((t) => t.turn === turn);
153
- if (index < 0 || state.turns[index].firstChunkTime !== void 0) return state;
154
- const stream = event.data.stream;
155
- const firstToken = Array.isArray(stream) ? assistantStreamFirstTokenTime(stream) : void 0;
156
- if (firstToken === void 0) return state;
157
- const turns = state.turns.slice();
158
- turns[index] = {
159
- ...turns[index],
160
- firstChunkTime: firstToken
161
+ if (index < 0) return state;
162
+ const prior = state.turns[index];
163
+ let next = {
164
+ ...prior,
165
+ attempts: (prior.attempts ?? 0) + 1
161
166
  };
167
+ if (prior.firstChunkTime === void 0) {
168
+ const stream = event.data.stream;
169
+ const firstToken = Array.isArray(stream) ? assistantStreamFirstTokenTime(stream) : void 0;
170
+ if (firstToken !== void 0) next = {
171
+ ...next,
172
+ firstChunkTime: firstToken
173
+ };
174
+ }
175
+ const turns = state.turns.slice();
176
+ turns[index] = next;
162
177
  return {
163
- messages: state.messages,
178
+ ...state,
164
179
  turns
165
180
  };
166
181
  }
@@ -175,7 +190,39 @@ const milestoneMessagesProjectionDefinition = {
175
190
  endReason: event.data.reason.kind
176
191
  };
177
192
  return {
178
- messages: state.messages,
193
+ ...state,
194
+ turns
195
+ };
196
+ }
197
+ case "request/context": {
198
+ const provider = typeof event.data.provider === "string" ? event.data.provider : "";
199
+ const model = typeof event.data.model === "string" ? event.data.model : "";
200
+ if (provider === "" && model === "") return state;
201
+ const index = state.turns.length - 1;
202
+ const open = index >= 0 ? state.turns[index] : void 0;
203
+ if (open === void 0 || open.endTime !== void 0) {
204
+ if (state.route?.provider === provider && state.route.model === model) return state;
205
+ return {
206
+ ...state,
207
+ route: {
208
+ provider,
209
+ model
210
+ }
211
+ };
212
+ }
213
+ if (open.provider === provider && open.model === model) return state;
214
+ const turns = state.turns.slice();
215
+ turns[index] = {
216
+ ...open,
217
+ provider,
218
+ model
219
+ };
220
+ return {
221
+ ...state,
222
+ route: {
223
+ provider,
224
+ model
225
+ },
179
226
  turns
180
227
  };
181
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-milestone",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Git-style milestone timeline for DeepSeek Harness: hover for metadata, click to jump to any message. 会话里程碑导航条:圆点时间线,定位并跳转到每条提问。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -53,15 +53,26 @@
53
53
  "@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-rc.1 <0.3.0-0",
54
54
  "react": "^18.2.0"
55
55
  },
56
+ "peerDependenciesMeta": {
57
+ "@deepseek-ai/dsh-client-locale": {
58
+ "optional": true
59
+ },
60
+ "@deepseek-ai/dsh-client-store": {
61
+ "optional": true
62
+ },
63
+ "@deepseek-ai/dsh-client-ui-slots": {
64
+ "optional": true
65
+ }
66
+ },
56
67
  "devDependencies": {
57
68
  "@deepseek-ai/cordis": "^4.0.1",
58
- "@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.1",
59
- "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
60
- "@deepseek-ai/dsh-client-store": "0.1.5-rc.1",
61
- "@deepseek-ai/dsh-client-ui-layout": "0.1.5-rc.1",
62
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.1",
63
- "@deepseek-ai/dsh-client-ui-session": "0.1.5-rc.1",
64
- "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
69
+ "@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.2",
70
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
71
+ "@deepseek-ai/dsh-client-store": "0.1.5-rc.2",
72
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.5-rc.2",
73
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
74
+ "@deepseek-ai/dsh-client-ui-session": "0.1.5-rc.2",
75
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
65
76
  "@playwright/test": "^1.62.1",
66
77
  "@testing-library/jest-dom": "^7.0.1",
67
78
  "@testing-library/react": "^16.3.2",
@@ -75,9 +86,9 @@
75
86
  "vitest": "^4.1.10"
76
87
  },
77
88
  "dependencies": {
78
- "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
79
- "@deepseek-ai/dsh-session": "0.1.5-rc.1",
80
- "@deepseek-ai/dsh-session-projection": "0.1.5-rc.1",
89
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
90
+ "@deepseek-ai/dsh-session": "0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-session-projection": "0.1.5-rc.2",
81
92
  "zod": "^4.5.4"
82
93
  }
83
94
  }