@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 +2 -2
- package/README.md +11 -2
- package/README.zh.md +11 -2
- package/lib/client.js +441 -108
- package/lib/types/client/chat/ChatView.d.ts +1 -1
- package/lib/types/client/chat/MessageItem.d.ts +4 -4
- package/lib/types/client/chat/TurnNavigator.d.ts +11 -6
- package/lib/types/client/chat/turn-rail-items.d.ts +36 -0
- package/lib/types/client/contract/slots.d.ts +2 -0
- package/lib/types/client/conversation-nodes/inbox.d.ts +18 -9
- package/lib/types/client/locale.d.ts +2 -0
- package/package.json +29 -28
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:
|
|
6
|
-
README.zh.md:
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
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}.
|
|
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
|
-
"
|
|
1657
|
+
"scroller": "alqd3G_scroller",
|
|
1651
1658
|
"slot": "alqd3G_slot"
|
|
1652
1659
|
};
|
|
1653
1660
|
//#endregion
|
|
1654
1661
|
//#region lib/types/client/chat/TurnNavigator.js
|
|
1655
|
-
/**
|
|
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
|
-
|
|
1660
|
-
|
|
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-
|
|
1663
|
-
"--turn-
|
|
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
|
|
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
|
-
|
|
1669
|
-
|
|
1690
|
+
top,
|
|
1691
|
+
canScrollUp: top > 1,
|
|
1692
|
+
canScrollDown: top < scroller.scrollHeight - scroller.clientHeight - 1
|
|
1670
1693
|
};
|
|
1671
1694
|
}
|
|
1672
|
-
function
|
|
1673
|
-
|
|
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
|
|
1741
|
+
const previewPosition = previewIndex < 0 ? void 0 : itemPosition(previewIndex);
|
|
1685
1742
|
const previewAtPointer = (event) => {
|
|
1686
|
-
|
|
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
|
|
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.
|
|
1696
|
-
style:
|
|
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
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
-
*
|
|
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
|
|
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)
|
|
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
|
-
|
|
2165
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
4352
|
-
*
|
|
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
|
|
4356
|
-
|
|
4357
|
-
|
|
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
|
-
|
|
4361
|
-
|
|
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
|
-
|
|
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
|
|
5229
|
-
const
|
|
5230
|
-
const
|
|
5231
|
-
|
|
5232
|
-
|
|
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
|
-
|
|
5563
|
+
pending: {
|
|
5564
|
+
kind: "splice",
|
|
5565
|
+
previous: priorPending,
|
|
5566
|
+
start: splice.start,
|
|
5567
|
+
removedCount,
|
|
5568
|
+
inserted
|
|
5569
|
+
},
|
|
5570
|
+
currentClaimed
|
|
5236
5571
|
};
|
|
5237
5572
|
}
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
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
|
-
}
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
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
|
|
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.
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 {
|
|
2
|
+
import type { TurnRailItem } from './turn-rail-items.ts';
|
|
3
3
|
interface TurnNavigatorProps {
|
|
4
|
-
readonly items: readonly
|
|
4
|
+
readonly items: readonly TurnRailItem[];
|
|
5
5
|
readonly activeTurn: number | null;
|
|
6
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
4
|
-
readonly
|
|
3
|
+
interface PendingSnapshot {
|
|
4
|
+
readonly kind: 'snapshot';
|
|
5
|
+
readonly ids: readonly string[];
|
|
5
6
|
}
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
readonly
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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-
|
|
59
|
-
"@prettier-ai/dsh-
|
|
60
|
-
"@prettier-ai/dsh-api-workspace-controller": "^0.1.2-alpha.
|
|
61
|
-
"@prettier-ai/dsh-
|
|
62
|
-
"@prettier-ai/dsh-
|
|
63
|
-
"@prettier-ai/dsh-client-
|
|
64
|
-
"@prettier-ai/dsh-client-
|
|
65
|
-
"@prettier-ai/dsh-client-ui-
|
|
66
|
-
"@prettier-ai/dsh-client-ui-
|
|
67
|
-
"@prettier-ai/dsh-client-
|
|
68
|
-
"@prettier-ai/dsh-client-ui-
|
|
69
|
-
"@prettier-ai/dsh-client-ui-
|
|
70
|
-
"@prettier-ai/dsh-client-ui-session": "^0.1.2-alpha.
|
|
71
|
-
"@prettier-ai/dsh-client-ui-workspace": "^0.1.2-alpha.
|
|
72
|
-
"@prettier-ai/dsh-client-ui-
|
|
73
|
-
"@prettier-ai/dsh-commands": "^0.1.2-alpha.
|
|
74
|
-
"@prettier-ai/dsh-
|
|
75
|
-
"@prettier-ai/dsh-
|
|
76
|
-
"@prettier-ai/dsh-llm": "^0.1.2-alpha.
|
|
77
|
-
"@prettier-ai/dsh-
|
|
78
|
-
"@prettier-ai/dsh-
|
|
79
|
-
"@prettier-ai/dsh-
|
|
80
|
-
"@prettier-ai/dsh-
|
|
81
|
-
"@prettier-ai/dsh-
|
|
82
|
-
"@prettier-ai/dsh-util-workspace-path": "^0.1.2-alpha.
|
|
83
|
-
"@prettier-ai/dsh-settings": "^0.1.2-alpha.
|
|
84
|
-
"@prettier-ai/dsh-tools": "^0.1.2-alpha.
|
|
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"
|