@prettier-ai/dsh-client-ui-chat 0.1.2-alpha.2 → 0.1.2-alpha.3

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
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md
5
- README.md: 5c7549da07e8101c6b08b4cad57afd5158980b7f
6
- README.zh.md: a558bf94f5436eebdf035ca7507ecd25ec3c8d8a
5
+ README.md: 756b09ee1bc1f849d507bc06faad6d5365f0b851
6
+ README.zh.md: 6e6278c5fe3f7a31c4e7f4485aa6ebe0dcf95f60
package/README.md CHANGED
@@ -8,13 +8,14 @@ English | [中文](README.zh.md)
8
8
 
9
9
  ## Summary
10
10
 
11
- The browser Chat target for Conversation assembly. It registers Chat event definitions and snapshot construction, supplies `useChat`, renders transcript nodes and details, and owns Chat-specific stores, actions, localization, and scroll restoration; historical image URLs resolve through the Conversation-owned per-session cache (`ctx.uiConversation.imageUrl`). Its Assistant and Turn Tail definitions fold packed historical Assistant runs without expanding their members. The flow tail renders the session's local submission echoes (`SessionSnapshot.pendingSubmissions`) with the same bubble as their eventual durable user nodes, hidden per render once a user/steering node or queue occurrence carries the echo's prompt `rpcId`, so the echo-to-durable swap is atomic.
11
+ The browser Chat target for Conversation assembly. It registers Chat event definitions and snapshot construction, supplies `useChat`, renders transcript nodes and details, and owns Chat-specific stores, actions, localization, and scroll restoration; historical image URLs resolve through the Conversation-owned per-session cache (`ctx.uiConversation.imageUrl`). Its Assistant and Turn Tail definitions fold packed historical Assistant runs without expanding their members. Steering classification retains only next-step Inbox IDs through persistent splice state; next-turn splices create no Chat Context. Local submission echoes (`SessionSnapshot.pendingSubmissions`) retain the surface selected when the submit begins: transcript echoes render at the flow tail, steering echoes render with the pending-steering marker, and queued echoes stay out of Chat. Each echo is hidden per render once a user/steering node or queue occurrence carries its prompt `rpcId`, so the handoff is atomic.
12
12
 
13
13
  ## Table of Contents
14
14
 
15
15
  - [System prompt row](#system-prompt-row)
16
16
  - [Turn token usage](#turn-token-usage)
17
17
  - [Turn Process Folding](#turn-process-folding)
18
+ - [Scroll ownership](#scroll-ownership)
18
19
  - [Model Experience](#model-experience)
19
20
  - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
20
21
  - [Dev Note](#dev-note)
@@ -42,6 +43,13 @@ Settings → General exposes a persisted `Normal` / `Compact` conversation-displ
42
43
 
43
44
  -----
44
45
 
46
+ <a id="scroll-ownership"></a>
47
+ ## Scroll ownership
48
+
49
+ Chat restores semantic anchors across history prepend and renderer remounts. While the reader is pinned to the floor, `ResizeObserver` follows the new floor and selects the latest loaded Turn without reading row geometry. Once the reader moves away, flow-height changes preserve the top position and the reading-line geometry selects the active Turn ([loaded-Turn navigation](../../../.agents/notes/implemented/feature/2026-08-25-loaded-turn-chat-navigation.md)).
50
+
51
+ -----
52
+
45
53
  <a id="model-experience"></a>
46
54
  ## Model Experience
47
55
 
@@ -55,7 +63,8 @@ None; Chat presentation does not assemble or mutate provider requests.
55
63
 
56
64
  <a id="known-limitations-and-deferred-work"></a>
57
65
 
58
- - **The view reflects the loaded Session window** — older transcript nodes become available only after Session Controller loads the preceding event page. Turn navigation likewise represents only loaded Turns; loading an earlier page preserves existing Turn marks and redistributes the complete loaded set in a compact rail without an unloaded-history placeholder. Marks stay 10px apart until the loaded set exceeds the available height, then compress to fit.
66
+ - **The transcript reflects the loaded Session window** — older transcript nodes become available only after Session Controller loads the preceding event page. Turn navigation is wider than the window: the rail merges the loaded Turns with the host `turnOutline` projection, so every started Turn gets a fixed-pitch mark (10px apart; a ladder taller than the frame scrolls inside it with gradient fades), and activating an unloaded mark pages history through the Turn's `turn/start` seq before landing on its row. Without the projection (assemblies not mounting `dsh-session-turn-outline`) the rail falls back to loaded Turns only.
67
+ - **Rail previews are card-sized** — one prompt line (50 characters) and up to three response lines (120), on loaded and unloaded Turns alike; an unloaded Turn's response arrives from the outline only once the Turn settled, so an open Turn previews its prompt (or just the Turn number) until then.
59
68
 
60
69
 
61
70
  <a id="dev-note"></a>
package/README.zh.md CHANGED
@@ -8,13 +8,14 @@ kind: "package-reference"
8
8
 
9
9
  ## 概述
10
10
 
11
- Conversation 组装的浏览器 Chat target。本包注册 Chat event definition 与 snapshot 构造、提供 `useChat`、渲染 transcript node 和详情,并拥有 Chat 专属 store、action、本地化与滚动位置恢复;历史图片 URL 通过 Conversation 持有的按会话缓存(`ctx.uiConversation.imageUrl`)解析。其中 Assistant 与 Turn Tail definition 会直接 fold packed Assistant 历史 run,不展开其成员。消息流尾部渲染 session 的本地提交回显(`SessionSnapshot.pendingSubmissions`),气泡与其最终的 durable user 节点一致;一旦某个 user/steering 节点或 queue occurrence 携带回显的 prompt `rpcId`,该回显即在同一渲染中隐藏,因此回显到 durable 的替换是原子的。
11
+ Conversation 组装的浏览器 Chat target。本包注册 Chat event definition 与 snapshot 构造、提供 `useChat`、渲染 transcript node 和详情,并拥有 Chat 专属 store、action、本地化与滚动位置恢复;历史图片 URL 通过 Conversation 持有的按会话缓存(`ctx.uiConversation.imageUrl`)解析。其中 Assistant 与 Turn Tail definition 会直接 fold packed Assistant 历史 run,不展开其成员。steering 分类通过持久 splice state 只保留 next-step Inbox ID;next-turn splice 不创建 Chat Context。本地提交回显(`SessionSnapshot.pendingSubmissions`)保留提交开始时选定的区域:transcript 回显位于消息流末尾,steering 回显带 pending-steering 标记,queued 回显不进入 Chat。一旦 user/steering 节点或 queue occurrence 携带回显的 prompt `rpcId`,该回显即在同一渲染中隐藏,因此交接是原子的。
12
12
 
13
13
  ## 目录
14
14
 
15
15
  - [系统提示词行](#system-prompt-row)
16
16
  - [轮次 token 用量](#turn-token-usage)
17
17
  - [轮次过程折叠](#turn-process-folding)
18
+ - [滚动归属](#scroll-ownership)
18
19
  - [模型体验](#model-experience)
19
20
  - [已知限制与暂缓事项](#known-limitations-and-deferred-work)
20
21
  - [开发备注](#dev-note)
@@ -42,6 +43,13 @@ Chat 会为每个非空的初始或恢复请求、显式消息序列起点或真
42
43
 
43
44
  -----
44
45
 
46
+ <a id="scroll-ownership"></a>
47
+ ## 滚动归属
48
+
49
+ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点。读者跟随底部时,`ResizeObserver` 追随新的底部,并且无需读取行几何就选中最后一个已加载 Turn;读者离开底部后,高度变化会保持顶部位置,再由阅读线几何选择活跃 Turn([已加载 Turn 导航](../../../.agents/notes/implemented/feature/2026-08-25-loaded-turn-chat-navigation.zh.md))。
50
+
51
+ -----
52
+
45
53
  <a id="model-experience"></a>
46
54
  ## 模型体验
47
55
 
@@ -55,7 +63,8 @@ Chat 会为每个非空的初始或恢复请求、显式消息序列起点或真
55
63
 
56
64
  <a id="known-limitations-and-deferred-work"></a>
57
65
 
58
- - **视图只反映已加载的 Session 窗口**——只有 Session Controller 加载前一页 event 后,更早的 transcript node 才会出现。轮次导航同样只表示已加载的 Turn;加载更早一页时,已有 Turn 刻度保持身份不变,完整的已加载集合在紧凑轨道中重新排布,不显示未加载历史占位。刻度默认相隔 10px,仅在已加载集合超过可用高度时压缩间距。
66
+ - **transcript 只反映已加载的 Session 窗口**——只有 Session Controller 加载前一页 event 后,更早的 transcript node 才会出现。轮次导航比窗口更宽:轨道把已加载的 Turn 与宿主 `turnOutline` 投影合并,每个已开始的 Turn 都有固定间距刻度(相隔 10px;阶梯高于外框时在框内滚动并以渐变淡出标示可滚方向),激活未加载刻度会先把历史分页拉到该 Turn 的 `turn/start` seq 再落到它的行上。没有该投影时(未挂载 `dsh-session-turn-outline` 的装配),轨道回退到仅显示已加载 Turn。
67
+ - **导航预览按卡片尺寸截断**——提示词一行(50 字符)、回复至多三行(120 字符),已加载与未加载 Turn 一致;未加载 Turn 的回复要等该轮落定后才随大纲到达,进行中的轮次在此之前只预览提示词(或仅轮次号)。
59
68
 
60
69
 
61
70
  <a id="dev-note"></a>
package/lib/client.js CHANGED
@@ -1223,10 +1223,10 @@ window.__ModuleLoader__.load({
1223
1223
  });
1224
1224
  }
1225
1225
  /**
1226
- * Render one local submission echo with the exact visual language of the
1227
- * durable user node that replaces it: draft text plus object-URL previews,
1228
- * visible from the submit click until the durable `user/message` (or its
1229
- * queue occurrence) renders.
1226
+ * Render one local transcript or steering submission echo with the same
1227
+ * visual language and surface marker as the Host occurrence that replaces
1228
+ * it: draft text plus object-URL previews, visible from the submit click
1229
+ * until the durable `user/message` or steering occurrence renders.
1230
1230
  * @param props - the session snapshot's pending submission and render seats.
1231
1231
  * @returns the echoed user bubble.
1232
1232
  */
@@ -1243,6 +1243,7 @@ window.__ModuleLoader__.load({
1243
1243
  ...image.height === void 0 ? {} : { height: image.height }
1244
1244
  } })), [submission.images]),
1245
1245
  renderMessageImages,
1246
+ pending: submission.placement === "steering",
1246
1247
  echo: true,
1247
1248
  t,
1248
1249
  actions: (text) => (0, react_jsx_runtime.jsx)(MessageIconActions, {
@@ -1627,7 +1628,7 @@ window.__ModuleLoader__.load({
1627
1628
  });
1628
1629
  //#endregion
1629
1630
  //#region \0dsh-css:/home/runner/work/dsh-publisher/dsh-publisher/upstream/packages/client/ui-chat/src/client/chat/TurnNavigator.module.css.mjs
1630
- const css$10 = ".alqd3G_slot{z-index:6;pointer-events:none;height:0;position:sticky;top:0}.alqd3G_rail{--turn-rail-band:calc(var(--dsh-conversation-viewport-height,100dvh) - var(--dsh-composer-height,152px));--turn-preview-height:100px;top:calc(var(--turn-rail-band) / 2);right:calc(12px - (var(--dsh-composer-side-clearance) + 16px));width:28px;height:min(var(--turn-natural-height), max(0px, calc(var(--turn-rail-band) - 64px)), 420px);cursor:pointer;pointer-events:auto;transition:height .22s cubic-bezier(.2,.8,.2,1);position:absolute;transform:translateY(-50%)}.alqd3G_marks{inset:var(--turn-rail-inset) 0;position:absolute}.alqd3G_markPosition{top:min(var(--turn-natural-position), var(--turn-position));height:10px;transition:top .22s cubic-bezier(.2,.8,.2,1);animation:.15s ease-out alqd3G_dsh-turn-mark-enter;position:absolute;left:0;right:0;transform:translateY(-50%)}.alqd3G_mark{cursor:pointer;pointer-events:none;background:0 0;border:0;border-radius:8px;width:20px;padding:0;position:absolute;inset:0 0 0 auto}.alqd3G_mark:before{background:var(--dsw-alias-border-l4);content:\"\";border-radius:2px;width:12px;height:2px;transition:width .14s,background-color .14s;position:absolute;top:50%;right:0;transform:translateY(-50%)}.alqd3G_markPreview:before{background:var(--dsw-alias-label-tertiary);width:18px}.alqd3G_markActive:before{background:var(--dsw-alias-label-primary);width:20px}.alqd3G_mark:focus-visible:before{background:var(--dsw-alias-state-business-primary);width:20px}.alqd3G_mark:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.alqd3G_preview{top:clamp(0px, calc(min(var(--turn-natural-position), var(--turn-position)) + var(--turn-rail-inset) - var(--turn-preview-height) / 2), calc(100% - var(--turn-preview-height)));box-sizing:border-box;width:min(300px,100cqw - 120px);max-height:var(--turn-preview-height);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv2);pointer-events:none;border-radius:10px;padding:10px 12px;transition:top .14s cubic-bezier(.2,.8,.2,1);animation:.12s ease-out alqd3G_dsh-turn-preview-enter;position:absolute;right:calc(100% + 10px);overflow:hidden}.alqd3G_previewPrompt,.alqd3G_previewResponse{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.alqd3G_previewPrompt{font:var(--dsw-font-xs-strong-13);-webkit-line-clamp:2}.alqd3G_previewResponse{color:var(--dsw-alias-label-caption);font:var(--dsw-font-xxs-12);-webkit-line-clamp:2;margin-top:4px}@keyframes alqd3G_dsh-turn-mark-enter{0%{opacity:0}to{opacity:1}}@keyframes alqd3G_dsh-turn-preview-enter{0%{opacity:0;transform:translate(4px)}to{opacity:1;transform:translate(0)}}@container (width<=900px){.alqd3G_slot{display:none}}@media (prefers-reduced-motion:reduce){.alqd3G_rail,.alqd3G_markPosition,.alqd3G_mark:before,.alqd3G_preview{transition:none;animation:none}}";
1631
+ const css$10 = ".alqd3G_slot{z-index:6;pointer-events:none;height:0;position:sticky;top:0}.alqd3G_frame{--turn-rail-band:calc(var(--dsh-conversation-viewport-height,100dvh) - var(--dsh-composer-height,152px));--turn-preview-height:100px;top:calc(var(--turn-rail-band) / 2);right:calc(12px - (var(--dsh-composer-side-clearance) + 16px));width:28px;height:min(var(--turn-natural-height), max(0px, calc(var(--turn-rail-band) - 64px)), 420px);cursor:pointer;pointer-events:auto;transition:height .22s cubic-bezier(.2,.8,.2,1);position:absolute;transform:translateY(-50%)}.alqd3G_scroller{overscroll-behavior:contain;scrollbar-width:none;position:absolute;inset:0;overflow-y:auto}.alqd3G_scroller::-webkit-scrollbar{display:none}.alqd3G_fadeTop{mask-image:linear-gradient(#0000 0,#000 24px 100%)}.alqd3G_fadeBottom{mask-image:linear-gradient(#000 0 calc(100% - 24px),#0000 100%)}.alqd3G_fadeTop.alqd3G_fadeBottom{mask-image:linear-gradient(#0000 0,#000 24px calc(100% - 24px),#0000 100%)}.alqd3G_marks{height:var(--turn-natural-height);position:relative}.alqd3G_markPosition{top:calc(var(--turn-natural-position) + var(--turn-rail-inset));height:10px;transition:top .22s cubic-bezier(.2,.8,.2,1);animation:.15s ease-out alqd3G_dsh-turn-mark-enter;position:absolute;left:0;right:0;transform:translateY(-50%)}.alqd3G_mark{cursor:pointer;pointer-events:none;background:0 0;border:0;border-radius:8px;width:20px;padding:0;position:absolute;inset:0 0 0 auto}.alqd3G_mark:before{background:var(--dsw-alias-border-l4);content:\"\";border-radius:2px;width:12px;height:2px;transition:width .14s,background-color .14s;position:absolute;top:50%;right:0;transform:translateY(-50%)}.alqd3G_markUnloaded:before{opacity:.6;width:8px}.alqd3G_markPreview:before{background:var(--dsw-alias-label-tertiary);width:18px}.alqd3G_markBusy:before{animation:1s ease-in-out infinite alqd3G_dsh-turn-mark-busy}.alqd3G_markActive:before{background:var(--dsw-alias-label-primary);width:20px}.alqd3G_mark:focus-visible:before{background:var(--dsw-alias-state-business-primary);width:20px}.alqd3G_mark:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.alqd3G_preview{top:clamp(0px, calc(var(--turn-natural-position) + var(--turn-rail-inset) - var(--turn-scroll-top,0px) - var(--turn-preview-height) / 2), calc(100% - var(--turn-preview-height)));box-sizing:border-box;width:min(300px,100cqw - 120px);max-height:var(--turn-preview-height);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv2);pointer-events:none;border-radius:10px;padding:10px 12px;transition:top .14s cubic-bezier(.2,.8,.2,1);animation:.12s ease-out alqd3G_dsh-turn-preview-enter;position:absolute;right:calc(100% + 10px);overflow:hidden}.alqd3G_previewPrompt,.alqd3G_previewResponse{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.alqd3G_previewPrompt{font:var(--dsw-font-xs-strong-13);-webkit-line-clamp:1}.alqd3G_previewResponse{color:var(--dsw-alias-label-caption);font:var(--dsw-font-xxs-12);-webkit-line-clamp:3;margin-top:4px}@keyframes alqd3G_dsh-turn-mark-enter{0%{opacity:0}to{opacity:1}}@keyframes alqd3G_dsh-turn-preview-enter{0%{opacity:0;transform:translate(4px)}to{opacity:1;transform:translate(0)}}@keyframes alqd3G_dsh-turn-mark-busy{0%,to{opacity:1}50%{opacity:.35}}@container (width<=900px){.alqd3G_slot{display:none}}@media (prefers-reduced-motion:reduce){.alqd3G_frame,.alqd3G_scroller,.alqd3G_markPosition,.alqd3G_mark:before,.alqd3G_markBusy:before,.alqd3G_preview{scroll-behavior:auto;transition:none;animation:none}}";
1631
1632
  const tagId$10 = "@prettier-ai/dsh-client-ui-chat/TurnNavigator.module.css";
1632
1633
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$10) + "]") === null) {
1633
1634
  const tag = document.createElement("style");
@@ -1637,96 +1638,173 @@ window.__ModuleLoader__.load({
1637
1638
  document.head.appendChild(tag);
1638
1639
  }
1639
1640
  var TurnNavigator_module_css_default = {
1641
+ "dsh-turn-mark-busy": "alqd3G_dsh-turn-mark-busy",
1640
1642
  "dsh-turn-mark-enter": "alqd3G_dsh-turn-mark-enter",
1641
1643
  "dsh-turn-preview-enter": "alqd3G_dsh-turn-preview-enter",
1644
+ "fadeBottom": "alqd3G_fadeBottom",
1645
+ "fadeTop": "alqd3G_fadeTop",
1646
+ "frame": "alqd3G_frame",
1642
1647
  "mark": "alqd3G_mark",
1643
1648
  "markActive": "alqd3G_markActive",
1649
+ "markBusy": "alqd3G_markBusy",
1644
1650
  "markPosition": "alqd3G_markPosition",
1645
1651
  "markPreview": "alqd3G_markPreview",
1652
+ "markUnloaded": "alqd3G_markUnloaded",
1646
1653
  "marks": "alqd3G_marks",
1647
1654
  "preview": "alqd3G_preview",
1648
1655
  "previewPrompt": "alqd3G_previewPrompt",
1649
1656
  "previewResponse": "alqd3G_previewResponse",
1650
- "rail": "alqd3G_rail",
1657
+ "scroller": "alqd3G_scroller",
1651
1658
  "slot": "alqd3G_slot"
1652
1659
  };
1653
1660
  //#endregion
1654
1661
  //#region lib/types/client/chat/TurnNavigator.js
1655
- /** Resting gap between neighbouring marks before the rail compresses to fit. */
1662
+ /** Fixed pitch between neighbouring marks; overflow scrolls inside the frame. */
1656
1663
  const TURN_SPACING_PX = 10;
1657
1664
  /** Rail padding above the first mark and below the last one, per end. */
1658
1665
  const RAIL_INSET_PX = 6;
1659
- function itemPosition(index, count) {
1660
- const ratio = count <= 1 ? 0 : index / (count - 1);
1666
+ /** Fade band the mask reserves at a scrollable end. */
1667
+ const FADE_PX = 24;
1668
+ function itemPosition(index) {
1669
+ return { "--turn-natural-position": `${String(index * TURN_SPACING_PX)}px` };
1670
+ }
1671
+ function frameStyle(count, scrollTop) {
1661
1672
  return {
1662
- "--turn-natural-position": `${String(index * TURN_SPACING_PX)}px`,
1663
- "--turn-position": `${String(ratio * 100)}%`
1673
+ "--turn-natural-height": `${String((count - 1) * TURN_SPACING_PX + 2 * RAIL_INSET_PX)}px`,
1674
+ "--turn-rail-inset": `${String(RAIL_INSET_PX)}px`,
1675
+ "--turn-scroll-top": `${String(scrollTop)}px`
1664
1676
  };
1665
1677
  }
1666
- function railSize(count) {
1678
+ function itemAtPointer(items, frame, scrollTop, clientY) {
1679
+ const offset = clientY - frame.getBoundingClientRect().top + scrollTop - RAIL_INSET_PX;
1680
+ return items[Math.max(0, Math.min(items.length - 1, Math.round(offset / TURN_SPACING_PX)))];
1681
+ }
1682
+ const RAIL_AT_REST = {
1683
+ top: 0,
1684
+ canScrollUp: false,
1685
+ canScrollDown: false
1686
+ };
1687
+ function railScrollState(scroller) {
1688
+ const top = scroller.scrollTop;
1667
1689
  return {
1668
- "--turn-natural-height": `${String((count - 1) * TURN_SPACING_PX + 2 * RAIL_INSET_PX)}px`,
1669
- "--turn-rail-inset": `${String(RAIL_INSET_PX)}px`
1690
+ top,
1691
+ canScrollUp: top > 1,
1692
+ canScrollDown: top < scroller.scrollHeight - scroller.clientHeight - 1
1670
1693
  };
1671
1694
  }
1672
- function itemAtPointer(items, rail, clientY) {
1673
- const rect = rail.getBoundingClientRect();
1674
- const usableHeight = Math.max(1, rect.height - 2 * RAIL_INSET_PX);
1675
- const ratio = Math.max(0, Math.min(1, (clientY - rect.top - RAIL_INSET_PX) / usableHeight));
1676
- return items[Math.round(ratio * (items.length - 1))];
1695
+ function sameRailScrollState(left, right) {
1696
+ return left.top === right.top && left.canScrollUp === right.canScrollUp && left.canScrollDown === right.canScrollDown;
1677
1697
  }
1678
- function TurnNavigatorRail({ items, activeTurn, onNavigate, t }) {
1698
+ function TurnNavigatorRail({ items, activeTurn, busyTurn, onNavigate, t }) {
1679
1699
  const [previewTurn, setPreviewTurn] = (0, react.useState)(null);
1700
+ const [scrollState, setScrollState] = (0, react.useState)(RAIL_AT_REST);
1701
+ const scrollerRef = (0, react.useRef)(null);
1702
+ /** While the pointer works the rail, follow must not move it under the hand. */
1703
+ const pointerInsideRef = (0, react.useRef)(false);
1680
1704
  const previewId = (0, react.useId)();
1705
+ const syncScrollState = () => {
1706
+ const scroller = scrollerRef.current;
1707
+ if (scroller === null) return;
1708
+ const next = railScrollState(scroller);
1709
+ setScrollState((current) => sameRailScrollState(current, next) ? current : next);
1710
+ };
1711
+ (0, react.useEffect)(() => {
1712
+ const scroller = scrollerRef.current;
1713
+ if (scroller === null || typeof ResizeObserver === "undefined") return;
1714
+ const observer = new ResizeObserver(syncScrollState);
1715
+ observer.observe(scroller);
1716
+ return () => {
1717
+ observer.disconnect();
1718
+ };
1719
+ }, []);
1720
+ (0, react.useEffect)(syncScrollState, [items.length]);
1721
+ (0, react.useEffect)(() => {
1722
+ const scroller = scrollerRef.current;
1723
+ const index = items.findIndex((item) => item.turn === activeTurn);
1724
+ if (scroller === null || index < 0 || pointerInsideRef.current) return;
1725
+ const markTop = index * TURN_SPACING_PX + RAIL_INSET_PX;
1726
+ const viewTop = scroller.scrollTop;
1727
+ const viewHeight = scroller.clientHeight;
1728
+ if (viewHeight <= 0 || markTop >= viewTop + FADE_PX && markTop <= viewTop + viewHeight - FADE_PX) return;
1729
+ const target = Math.max(0, markTop - viewHeight / 2);
1730
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1731
+ if (typeof scroller.scrollTo === "function") scroller.scrollTo({
1732
+ top: target,
1733
+ behavior: reduced ? "auto" : "smooth"
1734
+ });
1735
+ else scroller.scrollTop = target;
1736
+ syncScrollState();
1737
+ }, [activeTurn, items]);
1681
1738
  if (items.length < 2) return null;
1682
1739
  const previewIndex = items.findIndex((item) => item.turn === previewTurn);
1683
1740
  const preview = previewIndex < 0 ? void 0 : items[previewIndex];
1684
- const previewPosition = previewIndex < 0 ? void 0 : itemPosition(previewIndex, items.length);
1741
+ const previewPosition = previewIndex < 0 ? void 0 : itemPosition(previewIndex);
1685
1742
  const previewAtPointer = (event) => {
1686
- setPreviewTurn(itemAtPointer(items, event.currentTarget, event.clientY)?.turn ?? null);
1743
+ const scrollTop = scrollerRef.current?.scrollTop ?? 0;
1744
+ setPreviewTurn(itemAtPointer(items, event.currentTarget, scrollTop, event.clientY)?.turn ?? null);
1687
1745
  };
1688
1746
  const navigateAtPointer = (event) => {
1689
- const item = itemAtPointer(items, event.currentTarget, event.clientY);
1747
+ const scrollTop = scrollerRef.current?.scrollTop ?? 0;
1748
+ const item = itemAtPointer(items, event.currentTarget, scrollTop, event.clientY);
1690
1749
  if (item !== void 0) onNavigate(item);
1691
1750
  };
1751
+ const fadeClasses = [TurnNavigator_module_css_default.scroller];
1752
+ if (scrollState.canScrollUp) fadeClasses.push(TurnNavigator_module_css_default.fadeTop);
1753
+ if (scrollState.canScrollDown) fadeClasses.push(TurnNavigator_module_css_default.fadeBottom);
1692
1754
  return (0, react_jsx_runtime.jsx)("div", {
1693
1755
  className: TurnNavigator_module_css_default.slot,
1694
1756
  children: (0, react_jsx_runtime.jsxs)("nav", {
1695
- className: TurnNavigator_module_css_default.rail,
1696
- style: railSize(items.length),
1757
+ className: TurnNavigator_module_css_default.frame,
1758
+ style: frameStyle(items.length, scrollState.top),
1697
1759
  "aria-label": t("chat.turnNavigation.label"),
1698
1760
  onClick: navigateAtPointer,
1699
1761
  onPointerMove: previewAtPointer,
1762
+ onPointerEnter: () => {
1763
+ pointerInsideRef.current = true;
1764
+ },
1700
1765
  onPointerLeave: () => {
1766
+ pointerInsideRef.current = false;
1701
1767
  setPreviewTurn(null);
1702
1768
  },
1703
1769
  children: [(0, react_jsx_runtime.jsx)("div", {
1704
- className: TurnNavigator_module_css_default.marks,
1705
- children: items.map((item, index) => {
1706
- const active = item.turn === activeTurn;
1707
- const showingPreview = item.turn === previewTurn;
1708
- const markClass = active ? `${TurnNavigator_module_css_default.mark} ${TurnNavigator_module_css_default.markActive}` : showingPreview ? `${TurnNavigator_module_css_default.mark} ${TurnNavigator_module_css_default.markPreview}` : TurnNavigator_module_css_default.mark;
1709
- return (0, react_jsx_runtime.jsx)("div", {
1710
- className: TurnNavigator_module_css_default.markPosition,
1711
- style: itemPosition(index, items.length),
1712
- children: (0, react_jsx_runtime.jsx)("button", {
1713
- type: "button",
1714
- className: markClass,
1715
- "aria-label": t("chat.turnNavigation.jump", { turn: item.turn }),
1716
- "aria-current": active ? "true" : void 0,
1717
- "aria-describedby": showingPreview ? previewId : void 0,
1718
- onClick: (event) => {
1719
- event.stopPropagation();
1720
- onNavigate(item);
1721
- },
1722
- onFocus: () => {
1723
- setPreviewTurn(item.turn);
1724
- },
1725
- onBlur: () => {
1726
- setPreviewTurn(null);
1727
- }
1728
- })
1729
- }, item.turn);
1770
+ ref: scrollerRef,
1771
+ className: fadeClasses.join(" "),
1772
+ onScroll: () => {
1773
+ syncScrollState();
1774
+ },
1775
+ children: (0, react_jsx_runtime.jsx)("div", {
1776
+ className: TurnNavigator_module_css_default.marks,
1777
+ children: items.map((item, index) => {
1778
+ const active = item.turn === activeTurn;
1779
+ const showingPreview = item.turn === previewTurn;
1780
+ const classes = [TurnNavigator_module_css_default.mark];
1781
+ if (item.anchor.kind === "unloaded") classes.push(TurnNavigator_module_css_default.markUnloaded);
1782
+ if (active) classes.push(TurnNavigator_module_css_default.markActive);
1783
+ else if (showingPreview) classes.push(TurnNavigator_module_css_default.markPreview);
1784
+ if (item.turn === busyTurn) classes.push(TurnNavigator_module_css_default.markBusy);
1785
+ return (0, react_jsx_runtime.jsx)("div", {
1786
+ className: TurnNavigator_module_css_default.markPosition,
1787
+ style: itemPosition(index),
1788
+ children: (0, react_jsx_runtime.jsx)("button", {
1789
+ type: "button",
1790
+ className: classes.join(" "),
1791
+ "aria-label": t(item.anchor.kind === "loaded" ? "chat.turnNavigation.jump" : "chat.turnNavigation.jumpLoad", { turn: item.turn }),
1792
+ "aria-current": active ? "true" : void 0,
1793
+ "aria-busy": item.turn === busyTurn ? "true" : void 0,
1794
+ "aria-describedby": showingPreview ? previewId : void 0,
1795
+ onClick: (event) => {
1796
+ event.stopPropagation();
1797
+ onNavigate(item);
1798
+ },
1799
+ onFocus: () => {
1800
+ setPreviewTurn(item.turn);
1801
+ },
1802
+ onBlur: () => {
1803
+ setPreviewTurn(null);
1804
+ }
1805
+ })
1806
+ }, item.turn);
1807
+ })
1730
1808
  })
1731
1809
  }), preview !== void 0 && previewPosition !== void 0 && (0, react_jsx_runtime.jsxs)("div", {
1732
1810
  id: previewId,
@@ -1745,9 +1823,12 @@ window.__ModuleLoader__.load({
1745
1823
  });
1746
1824
  }
1747
1825
  /**
1748
- * Compact rail of the currently loaded Turns with hover and focus previews.
1826
+ * Fixed-pitch rail of every known Turn loaded marks scroll, unloaded marks
1827
+ * page history in first — with hover and focus previews. Overflow scrolls
1828
+ * inside the frame, gradient fades marking each scrollable end, and the
1829
+ * active mark keeps itself in view while the pointer is elsewhere.
1749
1830
  *
1750
- * Memoized because it renders two host elements per loaded Turn while the
1831
+ * Memoized because it renders two host elements per Turn while the
1751
1832
  * enclosing view re-renders on every streaming delta: without the guard a long
1752
1833
  * session rebuilds hundreds of marks per commit for a rail that only changes
1753
1834
  * when a Turn is added, removed, or becomes active. Its props must therefore
@@ -1755,6 +1836,79 @@ window.__ModuleLoader__.load({
1755
1836
  */
1756
1837
  const TurnNavigator = (0, react.memo)(TurnNavigatorRail);
1757
1838
  //#endregion
1839
+ //#region lib/types/client/chat/turn-rail-items.js
1840
+ /**
1841
+ * View-layer union of the host turn outline and the loaded rail items. The
1842
+ * conversation snapshot never carries projection values, so this merge is the
1843
+ * one place the rail's two sources meet: the `turnOutline` projection names
1844
+ * every turn of the session, and the loaded window supplies anchors and
1845
+ * richer previews for the turns it holds.
1846
+ */
1847
+ const EMPTY_ITEMS$1 = [];
1848
+ /**
1849
+ * Structurally narrow one wire outline entry (projection values cross the
1850
+ * wire). `turn` and `seq` are the load-bearing fields — a mark cannot exist
1851
+ * or jump without them — so their damage drops the entry; the previews are
1852
+ * decorative, so a malformed one degrades to `''` and the turn stays
1853
+ * navigable by number.
1854
+ */
1855
+ function outlineEntry(value) {
1856
+ if (typeof value !== "object" || value === null) return void 0;
1857
+ const entry = value;
1858
+ if (typeof entry.turn !== "number" || !Number.isSafeInteger(entry.turn) || entry.turn < 0) return void 0;
1859
+ if (typeof entry.seq !== "number" || !Number.isSafeInteger(entry.seq) || entry.seq < 0) return void 0;
1860
+ return {
1861
+ turn: entry.turn,
1862
+ seq: entry.seq,
1863
+ prompt: typeof entry.prompt === "string" ? entry.prompt : "",
1864
+ response: typeof entry.response === "string" ? entry.response : ""
1865
+ };
1866
+ }
1867
+ /** Wire outline entries, or none when the projection is absent or malformed. */
1868
+ function outlineEntries(outline) {
1869
+ return Array.isArray(outline) ? outline : EMPTY_ITEMS$1;
1870
+ }
1871
+ /**
1872
+ * Merge the host outline with the loaded rail items into the full ladder.
1873
+ * A turn present in both sides keeps the loaded anchor, taking an outline
1874
+ * preview only where the window's own is empty (a mid-Turn window head, or a
1875
+ * turn whose loaded nodes carry no text); turns on one side only pass
1876
+ * through. Result ascends by turn.
1877
+ * @param loaded - loaded-window rail items (timeline order).
1878
+ * @param outline - `turnOutline` projection value, treated as wire data.
1879
+ * @returns every known turn, ascending; a stable empty array when none.
1880
+ */
1881
+ function mergeTurnRailItems(loaded, outline) {
1882
+ const byTurn = /* @__PURE__ */ new Map();
1883
+ for (const raw of outlineEntries(outline)) {
1884
+ const entry = outlineEntry(raw);
1885
+ if (entry === void 0) continue;
1886
+ byTurn.set(entry.turn, {
1887
+ turn: entry.turn,
1888
+ prompt: entry.prompt,
1889
+ response: entry.response,
1890
+ anchor: {
1891
+ kind: "unloaded",
1892
+ seq: entry.seq
1893
+ }
1894
+ });
1895
+ }
1896
+ for (const item of loaded) {
1897
+ const preview = byTurn.get(item.turn);
1898
+ byTurn.set(item.turn, {
1899
+ turn: item.turn,
1900
+ prompt: item.prompt !== "" ? item.prompt : preview?.prompt ?? "",
1901
+ response: item.response !== "" ? item.response : preview?.response ?? "",
1902
+ anchor: {
1903
+ kind: "loaded",
1904
+ key: item.anchorKey
1905
+ }
1906
+ });
1907
+ }
1908
+ if (byTurn.size === 0) return EMPTY_ITEMS$1;
1909
+ return [...byTurn.values()].sort((left, right) => left.turn - right.turn);
1910
+ }
1911
+ //#endregion
1758
1912
  //#region lib/types/client/chat/ChatView.js
1759
1913
  /** Active column host when present; otherwise the view-local scroller. */
1760
1914
  function scrollerOf(from) {
@@ -1892,10 +2046,12 @@ window.__ModuleLoader__.load({
1892
2046
  * The chat view slot entry: pure component over the composed props; each
1893
2047
  * ordered business Node crosses the keyed renderer seat.
1894
2048
  */
1895
- function ChatView({ useSession, useChat, useSessions, useStore, actions, renderSlot, sessionId, openFile, loadOlder, loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, t }) {
2049
+ function ChatView({ useSession, useChat, useSessions, useStore, actions, renderSlot, sessionId, openFile, loadOlder, loadThrough, loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, useProjection, t }) {
1896
2050
  const order = useChat((s) => s.order);
1897
2051
  const nodeStore = useChat((s) => s.nodes);
1898
2052
  const turnNavigationItems = useChat((s) => s.navigation.items());
2053
+ const turnOutline = useProjection("turnOutline");
2054
+ const railItems = (0, react.useMemo)(() => mergeTurnRailItems(turnNavigationItems, turnOutline), [turnNavigationItems, turnOutline]);
1899
2055
  const timeline = useChat((s) => s.timeline);
1900
2056
  const inbox = useSession((s) => s.queue);
1901
2057
  const cwd = useSessions((s) => s.byId[sessionId]?.cwd);
@@ -1938,7 +2094,7 @@ window.__ModuleLoader__.load({
1938
2094
  const visibleSubmissions = (0, react.useMemo)(() => {
1939
2095
  if (pendingSubmissions.length === 0) return pendingSubmissions;
1940
2096
  const observed = observedRpcIds(order, nodeStore, inbox);
1941
- return pendingSubmissions.filter((submission) => !observed.has(submission.requestId));
2097
+ return pendingSubmissions.filter((submission) => submission.placement !== "queued" && !observed.has(submission.requestId));
1942
2098
  }, [
1943
2099
  pendingSubmissions,
1944
2100
  order,
@@ -1960,6 +2116,15 @@ window.__ModuleLoader__.load({
1960
2116
  /** Paging anchor: semantic row/position at click, updated by reader scrolls
1961
2117
  * while the request is pending and restored after the prepend lands. */
1962
2118
  const anchorRef = (0, react.useRef)(null);
2119
+ /** Unloaded-turn jump in flight: target turn plus its load-through seq. */
2120
+ const pendingJumpRef = (0, react.useRef)(null);
2121
+ /** Whether the in-flight jump already landed mid-paging (settle then only corrects an untouched landing). */
2122
+ const jumpLandedRef = (0, react.useRef)(false);
2123
+ const [busyJumpTurn, setBusyJumpTurn] = (0, react.useState)(null);
2124
+ /** Bumped when a loadThrough completion settles, after its last page's commit. */
2125
+ const [jumpSettleTick, setJumpSettleTick] = (0, react.useState)(0);
2126
+ /** Window head at the last settle-time repage; an unmoved head falls back instead of repaging forever. */
2127
+ const jumpRepageHeadRef = (0, react.useRef)(null);
1963
2128
  const firstSeqRef = (0, react.useRef)(null);
1964
2129
  const openedRef = (0, react.useRef)(false);
1965
2130
  const lastKeyRef = (0, react.useRef)(null);
@@ -1984,13 +2149,17 @@ window.__ModuleLoader__.load({
1984
2149
  return;
1985
2150
  }
1986
2151
  const el = scrollerOf(local);
2152
+ if (el.scrollHeight - el.scrollTop - el.clientHeight <= 25) {
2153
+ const latest = turnNavigationItems.at(-1)?.turn ?? first.turn;
2154
+ setActiveTurn((current) => current === latest ? current : latest);
2155
+ return;
2156
+ }
1987
2157
  const reading = turnAtLine(local, el.getBoundingClientRect().top + Math.min(96, el.clientHeight * .2));
1988
2158
  let next = first.turn;
1989
2159
  if (reading !== null) for (const item of turnNavigationItems) {
1990
2160
  if (item.turn > reading) break;
1991
2161
  next = item.turn;
1992
2162
  }
1993
- if (el.scrollHeight - el.scrollTop - el.clientHeight <= 25) next = turnNavigationItems.at(-1)?.turn ?? next;
1994
2163
  setActiveTurn((current) => current === next ? current : next);
1995
2164
  }, [turnNavigationItems]);
1996
2165
  const activeTurnRef = (0, react.useRef)(null);
@@ -2015,6 +2184,8 @@ window.__ModuleLoader__.load({
2015
2184
  }, [scheduleActiveTurn]);
2016
2185
  const toBottom = (el) => {
2017
2186
  anchorRef.current = null;
2187
+ pendingJumpRef.current = null;
2188
+ setBusyJumpTurn((current) => current === null ? current : null);
2018
2189
  el.scrollTop = el.scrollHeight;
2019
2190
  observedTopRef.current = el.scrollTop;
2020
2191
  atBottomRef.current = true;
@@ -2022,6 +2193,50 @@ window.__ModuleLoader__.load({
2022
2193
  chatScroll.save(null);
2023
2194
  setActiveTurn(turnNavigationItems.at(-1)?.turn ?? null);
2024
2195
  };
2196
+ const landOnRowRef = (0, react.useRef)(() => {});
2197
+ landOnRowRef.current = (local, el, row, turn) => {
2198
+ el.scrollTop += flowTop(row, el) - 24;
2199
+ observedTopRef.current = el.scrollTop;
2200
+ const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 25;
2201
+ atBottomRef.current = isAtBottom;
2202
+ setAtBottom(isAtBottom);
2203
+ setActiveTurn(turn);
2204
+ const position = isAtBottom ? null : scrollPosition(local, el);
2205
+ if (isAtBottom) chatScroll.save(null);
2206
+ else if (position !== null) chatScroll.save(position);
2207
+ };
2208
+ /**
2209
+ * Land the pending jump once its Turn has a rendered anchor row; false
2210
+ * while it must keep waiting. Mid-jump landings (`settle` false) keep the
2211
+ * jump armed with the target row as the paging anchor, so later chunks and
2212
+ * the load-earlier button's unmount re-land on the same row; the settling
2213
+ * call clears the jump.
2214
+ */
2215
+ const realizePendingJump = (local, el, settle) => {
2216
+ const pending = pendingJumpRef.current;
2217
+ if (pending === null) return true;
2218
+ const item = railItems.find((candidate) => candidate.turn === pending.turn);
2219
+ if (item === void 0 || item.anchor.kind !== "loaded") return false;
2220
+ const row = anchorElement(local, item.anchor.key);
2221
+ if (row === null) return false;
2222
+ if (settle) {
2223
+ pendingJumpRef.current = null;
2224
+ setBusyJumpTurn(null);
2225
+ const held = anchorRef.current;
2226
+ const landedEarlier = jumpLandedRef.current;
2227
+ jumpLandedRef.current = false;
2228
+ anchorRef.current = null;
2229
+ if (!landedEarlier || held?.key === item.anchor.key) landOnRowRef.current(local, el, row, pending.turn);
2230
+ return true;
2231
+ }
2232
+ landOnRowRef.current(local, el, row, pending.turn);
2233
+ jumpLandedRef.current = true;
2234
+ anchorRef.current = {
2235
+ key: item.anchor.key,
2236
+ top: flowTop(row, el)
2237
+ };
2238
+ return true;
2239
+ };
2025
2240
  (0, react.useLayoutEffect)(() => {
2026
2241
  const local = listRef.current;
2027
2242
  /* v8 ignore next -- ref-null guard: React attaches the ref before layout effects run. */
@@ -2056,6 +2271,10 @@ window.__ModuleLoader__.load({
2056
2271
  const row = anchorElement(local, anchor.key);
2057
2272
  if (row !== null) el.scrollTop += flowTop(row, el) - anchor.top;
2058
2273
  observedTopRef.current = el.scrollTop;
2274
+ if (!realizePendingJump(local, el, false) && row !== null) anchorRef.current = {
2275
+ key: anchor.key,
2276
+ top: flowTop(row, el)
2277
+ };
2059
2278
  firstSeqRef.current = firstSeq;
2060
2279
  /* v8 ignore next -- ?? arm: a prepend adds nodes, so the flow list here is never empty. */
2061
2280
  lastKeyRef.current = lastKey;
@@ -2073,7 +2292,11 @@ window.__ModuleLoader__.load({
2073
2292
  lastSteeringIdRef.current = lastSteeringId;
2074
2293
  lastSubmissionIdRef.current = lastSubmissionId;
2075
2294
  followSigRef.current = followSig;
2076
- if (appendedUser || appendedSteering || appendedSubmission || tipMoved && atBottomRef.current) toBottom(el);
2295
+ if (appendedUser || appendedSteering || appendedSubmission || tipMoved && atBottomRef.current) {
2296
+ toBottom(el);
2297
+ return;
2298
+ }
2299
+ if (pendingJumpRef.current !== null) realizePendingJump(local, el, false);
2077
2300
  });
2078
2301
  const onScrollRef = (0, react.useRef)(() => {});
2079
2302
  onScrollRef.current = () => {
@@ -2142,6 +2365,39 @@ window.__ModuleLoader__.load({
2142
2365
  (0, react.useEffect)(() => {
2143
2366
  if (!loadingOlder) anchorRef.current = null;
2144
2367
  }, [loadingOlder]);
2368
+ (0, react.useEffect)(() => {
2369
+ const pending = pendingJumpRef.current;
2370
+ const local = listRef.current;
2371
+ if (pending === null || local === null) return;
2372
+ const el = scrollerOf(local);
2373
+ if (realizePendingJump(local, el, true)) return;
2374
+ if ((firstSeq === null || firstSeq > pending.seq) && hasMore) {
2375
+ if (loadingOlder) return;
2376
+ if (jumpRepageHeadRef.current !== firstSeq) {
2377
+ jumpRepageHeadRef.current = firstSeq;
2378
+ const held = pagingAnchor(local, el);
2379
+ if (held !== null && held.dataset.chatAnchorKey !== void 0) anchorRef.current = {
2380
+ key: held.dataset.chatAnchorKey,
2381
+ top: flowTop(held, el)
2382
+ };
2383
+ loadThrough(pending.seq).finally(() => {
2384
+ setJumpSettleTick((tick) => tick + 1);
2385
+ });
2386
+ return;
2387
+ }
2388
+ }
2389
+ for (const row of local.querySelectorAll("[data-chat-turn]:not([hidden])")) {
2390
+ const turn = Number(row.dataset.chatTurn);
2391
+ if (!Number.isSafeInteger(turn) || turn < pending.turn) continue;
2392
+ landOnRowRef.current(local, el, row, turn);
2393
+ break;
2394
+ }
2395
+ pendingJumpRef.current = null;
2396
+ setBusyJumpTurn(null);
2397
+ }, [jumpSettleTick]);
2398
+ (0, react.useEffect)(() => {
2399
+ if (!loadingOlder && pendingJumpRef.current !== null) setJumpSettleTick((tick) => tick + 1);
2400
+ }, [loadingOlder]);
2145
2401
  const loadOlderAnchored = () => {
2146
2402
  const local = listRef.current;
2147
2403
  /* v8 ignore next -- ref-null guard: the paging button renders inside the list tree. */
@@ -2158,24 +2414,38 @@ window.__ModuleLoader__.load({
2158
2414
  const navigateToTurn = (0, react.useCallback)((item) => {
2159
2415
  const local = listRef.current;
2160
2416
  if (local === null) return;
2161
- const row = anchorElement(local, item.anchorKey);
2162
- if (row === null) return;
2163
2417
  const el = scrollerOf(local);
2164
- el.scrollTop += flowTop(row, el) - 24;
2165
- observedTopRef.current = el.scrollTop;
2418
+ if (item.anchor.kind === "unloaded") {
2419
+ atBottomRef.current = false;
2420
+ setAtBottom(false);
2421
+ const held = pagingAnchor(local, el);
2422
+ if (held !== null && held.dataset.chatAnchorKey !== void 0) anchorRef.current = {
2423
+ key: held.dataset.chatAnchorKey,
2424
+ top: flowTop(held, el)
2425
+ };
2426
+ pendingJumpRef.current = {
2427
+ turn: item.turn,
2428
+ seq: item.anchor.seq
2429
+ };
2430
+ jumpRepageHeadRef.current = null;
2431
+ jumpLandedRef.current = false;
2432
+ setBusyJumpTurn(item.turn);
2433
+ loadThrough(item.anchor.seq).finally(() => {
2434
+ setJumpSettleTick((tick) => tick + 1);
2435
+ });
2436
+ return;
2437
+ }
2438
+ const row = anchorElement(local, item.anchor.key);
2439
+ if (row === null) return;
2440
+ pendingJumpRef.current = null;
2441
+ setBusyJumpTurn((current) => current === null ? current : null);
2442
+ landOnRowRef.current(local, el, row, item.turn);
2166
2443
  const landed = loadingOlder ? pagingAnchor(local, el) : null;
2167
2444
  anchorRef.current = landed === null || landed.dataset.chatAnchorKey === void 0 ? null : {
2168
2445
  key: landed.dataset.chatAnchorKey,
2169
2446
  top: flowTop(landed, el)
2170
2447
  };
2171
- const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 25;
2172
- atBottomRef.current = isAtBottom;
2173
- setAtBottom(isAtBottom);
2174
- setActiveTurn(item.turn);
2175
- const position = isAtBottom ? null : scrollPosition(local, el);
2176
- if (isAtBottom) chatScroll.save(null);
2177
- else if (position !== null) chatScroll.save(position);
2178
- }, [loadingOlder, chatScroll]);
2448
+ }, [loadingOlder, loadThrough]);
2179
2449
  return (0, react_jsx_runtime.jsxs)("div", {
2180
2450
  className: ChatView_module_css_default.root,
2181
2451
  children: [(0, react_jsx_runtime.jsxs)("div", {
@@ -2183,8 +2453,9 @@ window.__ModuleLoader__.load({
2183
2453
  className: ChatView_module_css_default.scroll,
2184
2454
  children: [
2185
2455
  (0, react_jsx_runtime.jsx)(TurnNavigator, {
2186
- items: turnNavigationItems,
2456
+ items: railItems,
2187
2457
  activeTurn,
2458
+ busyTurn: busyJumpTurn,
2188
2459
  onNavigate: navigateToTurn,
2189
2460
  t
2190
2461
  }),
@@ -2328,6 +2599,7 @@ window.__ModuleLoader__.load({
2328
2599
  "chat.deepDiving": "深度求索中...",
2329
2600
  "chat.turnNavigation.label": "轮次导航",
2330
2601
  "chat.turnNavigation.jump": "跳转到第 {turn} 轮",
2602
+ "chat.turnNavigation.jumpLoad": "加载并跳转到第 {turn} 轮",
2331
2603
  "chat.turnNavigation.turn": "第 {turn} 轮",
2332
2604
  "settings.transcript.title": "对话显示",
2333
2605
  "settings.transcript.description": "控制已完成轮次的过程内容",
@@ -2440,6 +2712,7 @@ window.__ModuleLoader__.load({
2440
2712
  "chat.deepDiving": "Deep diving...",
2441
2713
  "chat.turnNavigation.label": "Turn navigation",
2442
2714
  "chat.turnNavigation.jump": "Jump to turn {turn}",
2715
+ "chat.turnNavigation.jumpLoad": "Load and jump to turn {turn}",
2443
2716
  "chat.turnNavigation.turn": "Turn {turn}",
2444
2717
  "settings.transcript.title": "Conversation display",
2445
2718
  "settings.transcript.description": "Controls process content in completed turns",
@@ -4348,27 +4621,42 @@ window.__ModuleLoader__.load({
4348
4621
  //#endregion
4349
4622
  //#region lib/types/client/conversation-nodes/turn-navigation.js
4350
4623
  /**
4351
- * Preview budget per field. The rail clamps two short lines, so anything past
4352
- * this is invisible; copying whole transcripts into navigation state would
4624
+ * Preview budgets, sized to the rail card's clamps (one prompt line, up to
4625
+ * three response lines) and mirrored by the turnOutline projection so a turn
4626
+ * shows the same words before and after its events load. Anything past a
4627
+ * budget is invisible; copying whole transcripts into navigation state would
4353
4628
  * otherwise grow with the loaded window on every structural update.
4354
4629
  */
4355
- const PREVIEW_LIMIT = 160;
4356
- /** Join rendered text until the preview budget is met, then stop reading. */
4357
- function preview(parts) {
4630
+ const PROMPT_PREVIEW_LIMIT = 50;
4631
+ const RESPONSE_PREVIEW_LIMIT = 120;
4632
+ /** Join rendered text, collapse whitespace, and cap at `limit` with a trailing ellipsis when clipped. */
4633
+ function preview(parts, limit) {
4358
4634
  let text = "";
4635
+ let unread = false;
4359
4636
  for (const part of parts) {
4360
- text += text === "" ? part : ` ${part}`;
4361
- if (text.length >= PREVIEW_LIMIT) break;
4637
+ if (text.length >= limit * 2) {
4638
+ unread = true;
4639
+ break;
4640
+ }
4641
+ const clipped = part.length > limit * 2;
4642
+ const chunk = clipped ? part.slice(0, limit * 2) : part;
4643
+ text += text === "" ? chunk : ` ${chunk}`;
4644
+ if (clipped) {
4645
+ unread = true;
4646
+ break;
4647
+ }
4362
4648
  }
4363
- return text.replace(/\s+/g, " ").trim().slice(0, PREVIEW_LIMIT);
4649
+ const normalized = text.replace(/\s+/g, " ").trim();
4650
+ if (normalized.length > limit - 1) return `${normalized.slice(0, limit - 1).trimEnd()}…`;
4651
+ return unread ? `${normalized}…` : normalized;
4364
4652
  }
4365
4653
  function promptText(node) {
4366
4654
  if (node.kind !== "user") return "";
4367
- return preview(node.data.content.flatMap((block) => block.type === "text" ? [block.text] : []));
4655
+ return preview(node.data.content.flatMap((block) => block.type === "text" ? [block.text] : []), PROMPT_PREVIEW_LIMIT);
4368
4656
  }
4369
4657
  function responseText(node) {
4370
4658
  if (node.kind !== "assistant-step") return "";
4371
- return preview(node.data.blocks.flatMap((block) => block.kind === "text" ? [block.text] : []));
4659
+ return preview(node.data.blocks.flatMap((block) => block.kind === "text" ? [block.text] : []), RESPONSE_PREVIEW_LIMIT);
4372
4660
  }
4373
4661
  /**
4374
4662
  * Whether two items carry the same rail state, so the reader can keep its array.
@@ -5224,43 +5512,87 @@ window.__ModuleLoader__.load({
5224
5512
  }
5225
5513
  //#endregion
5226
5514
  //#region lib/types/client/conversation-nodes/inbox.js
5515
+ const EMPTY_PENDING = {
5516
+ kind: "snapshot",
5517
+ ids: []
5518
+ };
5519
+ const EMPTY_CURRENT_CLAIMED = /* @__PURE__ */ new Set();
5520
+ function materializePending(state) {
5521
+ const splices = [];
5522
+ let current = state;
5523
+ while (current.kind === "splice") {
5524
+ splices.push(current);
5525
+ current = current.previous;
5526
+ }
5527
+ const pending = [...current.ids];
5528
+ for (const splice of splices.reverse()) pending.splice(splice.start, splice.removedCount, ...splice.inserted);
5529
+ return pending;
5530
+ }
5531
+ function withoutInserted(claimed, inserted) {
5532
+ let next;
5533
+ for (const id of inserted) {
5534
+ if (!claimed.has(id)) continue;
5535
+ next ??= new Set(claimed);
5536
+ next.delete(id);
5537
+ }
5538
+ return next ?? claimed;
5539
+ }
5540
+ /**
5541
+ * Apply one next-step splice under the AgentLoop's durable event ordering.
5542
+ * An entered claim logs its complete message batch before another claim; a
5543
+ * rejected claim logs no messages, so only the current claim can classify a
5544
+ * later `user/message`.
5545
+ */
5227
5546
  function applySplice(previous, splice) {
5228
- const pending = [...previous?.state.pending ?? []];
5229
- const claimed = new Set(previous?.state.claimed ?? []);
5230
- const removed = pending.splice(splice.start, splice.removedCount ?? 0, ...splice.inserted);
5231
- for (const identity of splice.inserted) claimed.delete(identity.id);
5232
- if (splice.target === "next-step" && splice.outcome !== "canceled") for (const identity of removed) claimed.add(identity.id);
5547
+ const priorPending = previous?.state.pending ?? EMPTY_PENDING;
5548
+ const inserted = splice.inserted.map((identity) => identity.id);
5549
+ const removedCount = splice.removedCount ?? 0;
5550
+ if (removedCount > 0 && splice.outcome !== "canceled") {
5551
+ const pending = materializePending(priorPending);
5552
+ const removed = pending.splice(splice.start, removedCount, ...inserted);
5553
+ return {
5554
+ pending: {
5555
+ kind: "snapshot",
5556
+ ids: pending
5557
+ },
5558
+ currentClaimed: new Set(removed)
5559
+ };
5560
+ }
5561
+ const currentClaimed = withoutInserted(previous?.state.currentClaimed ?? EMPTY_CURRENT_CLAIMED, inserted);
5233
5562
  return {
5234
- pending,
5235
- claimed
5563
+ pending: {
5564
+ kind: "splice",
5565
+ previous: priorPending,
5566
+ start: splice.start,
5567
+ removedCount,
5568
+ inserted
5569
+ },
5570
+ currentClaimed
5236
5571
  };
5237
5572
  }
5238
- function inboxDefinition(target) {
5239
- const kind = `inbox-${target}`;
5240
- return {
5241
- kind,
5242
- match: (event) => event.type === "agent/inbox/spliced" && event.data.target === target ? {
5573
+ const NEXT_STEP_INBOX_KIND = "inbox-next-step";
5574
+ /** Persistent next-step Inbox state used to classify the current claimed batch as steering. */
5575
+ const nextStepInboxDefinition = {
5576
+ kind: NEXT_STEP_INBOX_KIND,
5577
+ match: (event) => {
5578
+ if (event.type === "agent/inbox/spliced" && event.data.target === "next-step") return {
5243
5579
  id: String(event.seq),
5244
5580
  role: "start"
5245
- } : null,
5246
- start: (_context, match, reader) => {
5247
- if (match.event.type !== "agent/inbox/spliced") throw new Error(`${kind} start requires agent/inbox/spliced`);
5248
- return applySplice(reader.previous(kind), match.event.data);
5249
- },
5250
- update: (context) => context.state,
5251
- publication: () => "none"
5252
- };
5253
- }
5254
- /** Cumulative next-turn inbox splice Definition. */
5255
- const nextTurnInboxDefinition = inboxDefinition("next-turn");
5256
- /** Cumulative next-step inbox splice Definition used to classify steering. */
5257
- const nextStepInboxDefinition = inboxDefinition("next-step");
5581
+ };
5582
+ return null;
5583
+ },
5584
+ start: (_context, match, reader) => {
5585
+ if (match.event.type !== "agent/inbox/spliced") throw new Error("inbox-next-step start requires agent/inbox/spliced");
5586
+ return applySplice(reader.previous(NEXT_STEP_INBOX_KIND), match.event.data);
5587
+ },
5588
+ update: (context) => context.state,
5589
+ publication: () => "none"
5590
+ };
5258
5591
  /**
5259
- * Register the two durable Inbox-state contributions.
5592
+ * Register the next-step Inbox state used by Chat message classification.
5260
5593
  * @param ctx - owning UI Conversation context.
5261
5594
  */
5262
5595
  function registerInboxConversationNodes(ctx) {
5263
- ctx.uiConversation.events.register(nextTurnInboxDefinition);
5264
5596
  ctx.uiConversation.events.register(nextStepInboxDefinition);
5265
5597
  }
5266
5598
  //#endregion
@@ -5290,7 +5622,7 @@ window.__ModuleLoader__.load({
5290
5622
  provenance: contextProvenance(event.data.source),
5291
5623
  form: contextForm(event.data.source)
5292
5624
  };
5293
- return reader.previous("inbox-next-step")?.state.claimed.has(String(event.data.id)) === true ? {
5625
+ return reader.previous("inbox-next-step")?.state.currentClaimed.has(String(event.data.id)) === true ? {
5294
5626
  kind: "steering",
5295
5627
  messageId: event.data.id,
5296
5628
  seq: event.seq,
@@ -7630,6 +7962,7 @@ window.__ModuleLoader__.load({
7630
7962
  loadOlder: () => {
7631
7963
  session.loadOlder();
7632
7964
  },
7965
+ loadThrough: (seq) => session.loadThrough(seq),
7633
7966
  loadImage: Object.assign((attachment) => ctx.uiConversation.imageUrl(sessionId, attachment), { peek: (attachment) => ctx.uiConversation.peekImageUrl(sessionId, attachment) }),
7634
7967
  chatScroll: {
7635
7968
  save: (position) => {
@@ -3,5 +3,5 @@ import type { ChatViewSlotProps } from '../contract/slots.ts';
3
3
  * The chat view slot entry: pure component over the composed props; each
4
4
  * ordered business Node crosses the keyed renderer seat.
5
5
  */
6
- export declare function ChatView({ useSession, useChat, useSessions, useStore, actions, renderSlot, sessionId, openFile, loadOlder, loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, t, }: ChatViewSlotProps): import("react").JSX.Element;
6
+ export declare function ChatView({ useSession, useChat, useSessions, useStore, actions, renderSlot, sessionId, openFile, loadOlder, loadThrough, loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, useProjection, t, }: ChatViewSlotProps): import("react").JSX.Element;
7
7
  //# sourceMappingURL=ChatView.d.ts.map
@@ -13,10 +13,10 @@ export declare function PendingSteeringBubble({ content, renderMessageImages, t
13
13
  t: ChatViewSlotProps['t'];
14
14
  }): ReactNode;
15
15
  /**
16
- * Render one local submission echo with the exact visual language of the
17
- * durable user node that replaces it: draft text plus object-URL previews,
18
- * visible from the submit click until the durable `user/message` (or its
19
- * queue occurrence) renders.
16
+ * Render one local transcript or steering submission echo with the same
17
+ * visual language and surface marker as the Host occurrence that replaces
18
+ * it: draft text plus object-URL previews, visible from the submit click
19
+ * until the durable `user/message` or steering occurrence renders.
20
20
  * @param props - the session snapshot's pending submission and render seats.
21
21
  * @returns the echoed user bubble.
22
22
  */
@@ -1,16 +1,21 @@
1
1
  import type { ChatViewSlotProps } from '../contract/slots.ts';
2
- import type { TurnNavigationItem } from '../contract/snapshot.ts';
2
+ import type { TurnRailItem } from './turn-rail-items.ts';
3
3
  interface TurnNavigatorProps {
4
- readonly items: readonly TurnNavigationItem[];
4
+ readonly items: readonly TurnRailItem[];
5
5
  readonly activeTurn: number | null;
6
- readonly onNavigate: (item: TurnNavigationItem) => void;
6
+ /** Turn whose jump is still paging history in; its mark pulses. */
7
+ readonly busyTurn: number | null;
8
+ readonly onNavigate: (item: TurnRailItem) => void;
7
9
  readonly t: ChatViewSlotProps['t'];
8
10
  }
9
- declare function TurnNavigatorRail({ items, activeTurn, onNavigate, t }: TurnNavigatorProps): import("react").JSX.Element | null;
11
+ declare function TurnNavigatorRail({ items, activeTurn, busyTurn, onNavigate, t }: TurnNavigatorProps): import("react").JSX.Element | null;
10
12
  /**
11
- * Compact rail of the currently loaded Turns with hover and focus previews.
13
+ * Fixed-pitch rail of every known Turn loaded marks scroll, unloaded marks
14
+ * page history in first — with hover and focus previews. Overflow scrolls
15
+ * inside the frame, gradient fades marking each scrollable end, and the
16
+ * active mark keeps itself in view while the pointer is elsewhere.
12
17
  *
13
- * Memoized because it renders two host elements per loaded Turn while the
18
+ * Memoized because it renders two host elements per Turn while the
14
19
  * enclosing view re-renders on every streaming delta: without the guard a long
15
20
  * session rebuilds hundreds of marks per commit for a rail that only changes
16
21
  * when a Turn is added, removed, or becomes active. Its props must therefore
@@ -0,0 +1,36 @@
1
+ /**
2
+ * View-layer union of the host turn outline and the loaded rail items. The
3
+ * conversation snapshot never carries projection values, so this merge is the
4
+ * one place the rail's two sources meet: the `turnOutline` projection names
5
+ * every turn of the session, and the loaded window supplies anchors and
6
+ * richer previews for the turns it holds.
7
+ */
8
+ import type { TurnNavigationItem } from '../contract/snapshot.ts';
9
+ /** One rail mark: a loaded Turn scrolls to its row; an unloaded one pages history through its seq first. */
10
+ export interface TurnRailItem {
11
+ readonly turn: number;
12
+ /** Bounded prompt preview (loaded window first, outline fallback). */
13
+ readonly prompt: string;
14
+ /** Bounded response preview (loaded window first, outline fallback). */
15
+ readonly response: string;
16
+ /** How the rail reaches the Turn. */
17
+ readonly anchor: {
18
+ readonly kind: 'loaded';
19
+ readonly key: string;
20
+ } | {
21
+ readonly kind: 'unloaded';
22
+ readonly seq: number;
23
+ };
24
+ }
25
+ /**
26
+ * Merge the host outline with the loaded rail items into the full ladder.
27
+ * A turn present in both sides keeps the loaded anchor, taking an outline
28
+ * preview only where the window's own is empty (a mid-Turn window head, or a
29
+ * turn whose loaded nodes carry no text); turns on one side only pass
30
+ * through. Result ascends by turn.
31
+ * @param loaded - loaded-window rail items (timeline order).
32
+ * @param outline - `turnOutline` projection value, treated as wire data.
33
+ * @returns every known turn, ascending; a stable empty array when none.
34
+ */
35
+ export declare function mergeTurnRailItems(loaded: readonly TurnNavigationItem[], outline: unknown): readonly TurnRailItem[];
36
+ //# sourceMappingURL=turn-rail-items.d.ts.map
@@ -95,6 +95,8 @@ export interface ChatViewInjected {
95
95
  openDetails: (target: SelectionTarget) => void;
96
96
  openFile: (path: string) => Promise<void>;
97
97
  loadOlder: () => void;
98
+ /** Jump loader: page history back through seq; resolves when the window covers it. */
99
+ loadThrough: (seq: number) => Promise<void>;
98
100
  loadImage: MessageImageLoader;
99
101
  chatScroll: {
100
102
  save: (position: ChatScrollPosition | null) => void;
@@ -1,19 +1,28 @@
1
1
  import type { Context } from '@prettier-ai/cordis';
2
2
  import type { ConversationNodeDefinition } from '@prettier-ai/dsh-client-ui-conversation/client';
3
- interface InboxIdentity {
4
- readonly id: string;
3
+ interface PendingSnapshot {
4
+ readonly kind: 'snapshot';
5
+ readonly ids: readonly string[];
5
6
  }
6
- /** Cumulative state after one durable inbox splice. */
7
+ interface PendingSplice {
8
+ readonly kind: 'splice';
9
+ readonly previous: PendingState;
10
+ readonly start: number;
11
+ readonly removedCount: number;
12
+ readonly inserted: readonly string[];
13
+ }
14
+ type PendingState = PendingSnapshot | PendingSplice;
15
+ /** Persistent next-step state after one durable Inbox splice. */
7
16
  export interface InboxState {
8
- readonly pending: readonly InboxIdentity[];
9
- readonly claimed: ReadonlySet<string>;
17
+ /** Persistent splice chain materialized only when a next-step batch is claimed. */
18
+ readonly pending: PendingState;
19
+ /** Message ids in the current claim, shared until the next claim. */
20
+ readonly currentClaimed: ReadonlySet<string>;
10
21
  }
11
- /** Cumulative next-turn inbox splice Definition. */
12
- export declare const nextTurnInboxDefinition: ConversationNodeDefinition<InboxState>;
13
- /** Cumulative next-step inbox splice Definition used to classify steering. */
22
+ /** Persistent next-step Inbox state used to classify the current claimed batch as steering. */
14
23
  export declare const nextStepInboxDefinition: ConversationNodeDefinition<InboxState>;
15
24
  /**
16
- * Register the two durable Inbox-state contributions.
25
+ * Register the next-step Inbox state used by Chat message classification.
17
26
  * @param ctx - owning UI Conversation context.
18
27
  */
19
28
  export declare function registerInboxConversationNodes(ctx: Context): void;
@@ -29,6 +29,7 @@ export declare const zh: {
29
29
  'chat.deepDiving': string;
30
30
  'chat.turnNavigation.label': string;
31
31
  'chat.turnNavigation.jump': string;
32
+ 'chat.turnNavigation.jumpLoad': string;
32
33
  'chat.turnNavigation.turn': string;
33
34
  'settings.transcript.title': string;
34
35
  'settings.transcript.description': string;
@@ -143,6 +144,7 @@ export declare const en: {
143
144
  'chat.deepDiving': string;
144
145
  'chat.turnNavigation.label': string;
145
146
  'chat.turnNavigation.jump': string;
147
+ 'chat.turnNavigation.jumpLoad': string;
146
148
  'chat.turnNavigation.turn': string;
147
149
  'settings.transcript.title': string;
148
150
  'settings.transcript.description': string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prettier-ai/dsh-client-ui-chat",
3
3
  "description": "Chat Conversation target, node definitions, renderers, and details surface",
4
- "version": "0.1.2-alpha.2",
4
+ "version": "0.1.2-alpha.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -54,34 +54,35 @@
54
54
  "@types/react-dom": "~18.3.0",
55
55
  "react": "^18.2.0",
56
56
  "react-dom": "^18.2.0",
57
+ "@prettier-ai/dsh-api-remotes": "^0.1.2-alpha.3",
57
58
  "@prettier-ai/cordis": "^4.0.2",
58
- "@prettier-ai/dsh-agent": "^0.1.2-alpha.2",
59
- "@prettier-ai/dsh-api-remotes": "^0.1.2-alpha.2",
60
- "@prettier-ai/dsh-api-workspace-controller": "^0.1.2-alpha.2",
61
- "@prettier-ai/dsh-api-session-controller": "^0.1.2-alpha.2",
62
- "@prettier-ai/dsh-attachment": "^0.1.2-alpha.2",
63
- "@prettier-ai/dsh-client-locale": "^0.1.2-alpha.2",
64
- "@prettier-ai/dsh-client-store": "^0.1.2-alpha.2",
65
- "@prettier-ai/dsh-client-ui-approval": "^0.1.2-alpha.2",
66
- "@prettier-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
67
- "@prettier-ai/dsh-client-ui-layout": "^0.1.2-alpha.2",
68
- "@prettier-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
69
- "@prettier-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
70
- "@prettier-ai/dsh-client-ui-session": "^0.1.2-alpha.2",
71
- "@prettier-ai/dsh-client-ui-workspace": "^0.1.2-alpha.2",
72
- "@prettier-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
73
- "@prettier-ai/dsh-commands": "^0.1.2-alpha.2",
74
- "@prettier-ai/dsh-compaction": "^0.1.2-alpha.2",
75
- "@prettier-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
76
- "@prettier-ai/dsh-llm": "^0.1.2-alpha.2",
77
- "@prettier-ai/dsh-session": "^0.1.2-alpha.2",
78
- "@prettier-ai/dsh-token-meter": "^0.1.2-alpha.2",
79
- "@prettier-ai/dsh-llm-retry": "^0.1.2-alpha.2",
80
- "@prettier-ai/dsh-invariants": "^0.1.2-alpha.2",
81
- "@prettier-ai/dsh-session-stats": "^0.1.2-alpha.2",
82
- "@prettier-ai/dsh-util-workspace-path": "^0.1.2-alpha.2",
83
- "@prettier-ai/dsh-settings": "^0.1.2-alpha.2",
84
- "@prettier-ai/dsh-tools": "^0.1.2-alpha.2"
59
+ "@prettier-ai/dsh-api-session-controller": "^0.1.2-alpha.3",
60
+ "@prettier-ai/dsh-agent": "^0.1.2-alpha.3",
61
+ "@prettier-ai/dsh-api-workspace-controller": "^0.1.2-alpha.3",
62
+ "@prettier-ai/dsh-attachment": "^0.1.2-alpha.3",
63
+ "@prettier-ai/dsh-client-ui-approval": "^0.1.2-alpha.3",
64
+ "@prettier-ai/dsh-client-store": "^0.1.2-alpha.3",
65
+ "@prettier-ai/dsh-client-ui-conversation": "^0.1.2-alpha.3",
66
+ "@prettier-ai/dsh-client-ui-layout": "^0.1.2-alpha.3",
67
+ "@prettier-ai/dsh-client-ui-primitives": "^0.1.2-alpha.3",
68
+ "@prettier-ai/dsh-client-locale": "^0.1.2-alpha.3",
69
+ "@prettier-ai/dsh-client-ui-renderer": "^0.1.2-alpha.3",
70
+ "@prettier-ai/dsh-client-ui-slots": "^0.1.2-alpha.3",
71
+ "@prettier-ai/dsh-client-ui-session": "^0.1.2-alpha.3",
72
+ "@prettier-ai/dsh-client-ui-workspace": "^0.1.2-alpha.3",
73
+ "@prettier-ai/dsh-client-ui-settings": "^0.1.2-alpha.3",
74
+ "@prettier-ai/dsh-commands": "^0.1.2-alpha.3",
75
+ "@prettier-ai/dsh-invariants": "^0.1.2-alpha.3",
76
+ "@prettier-ai/dsh-compaction": "^0.1.2-alpha.3",
77
+ "@prettier-ai/dsh-llm-retry": "^0.1.2-alpha.3",
78
+ "@prettier-ai/dsh-llm": "^0.1.2-alpha.3",
79
+ "@prettier-ai/dsh-session": "^0.1.2-alpha.3",
80
+ "@prettier-ai/dsh-session-stats": "^0.1.2-alpha.3",
81
+ "@prettier-ai/dsh-session-turn-outline": "^0.1.2-alpha.3",
82
+ "@prettier-ai/dsh-token-meter": "^0.1.2-alpha.3",
83
+ "@prettier-ai/dsh-util-workspace-path": "^0.1.2-alpha.3",
84
+ "@prettier-ai/dsh-settings": "^0.1.2-alpha.3",
85
+ "@prettier-ai/dsh-tools": "^0.1.2-alpha.3"
85
86
  },
86
87
  "dependencies": {
87
88
  "@prettier-ai/schemastery": "^3.18.2"