dsh-retrace 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.bundle.js +123 -202
- package/lib/client.js +151 -233
- package/lib/dynamic-client.js +123 -202
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -85,6 +85,7 @@ const zh = {
|
|
|
85
85
|
'timeline.open': '时间线',
|
|
86
86
|
'timeline.openAria': '打开会话时间线',
|
|
87
87
|
'timeline.title': '版本时间线',
|
|
88
|
+
'view.retrace': '版本',
|
|
88
89
|
'timeline.refresh': '刷新',
|
|
89
90
|
'timeline.close': '关闭',
|
|
90
91
|
'timeline.empty': '还没有版本。撤回 / 编辑 / 重新生成会在此记录版本。',
|
|
@@ -117,6 +118,7 @@ const zh = {
|
|
|
117
118
|
'timeline.cancel': '取消',
|
|
118
119
|
'timeline.busy': '回退中…',
|
|
119
120
|
'timeline.detail': '详情',
|
|
121
|
+
'timeline.trajectory': '轨迹台账',
|
|
120
122
|
'timeline.jump': '跳转',
|
|
121
123
|
'timeline.jumpFailed': '该版本在较远的过去(超出自动加载预算),无法直接定位。请向上滚动加载更早消息后重试;或用「详情」查看该版本当时的事件原文。',
|
|
122
124
|
'timeline.gitRepo': 'git 仓库',
|
|
@@ -159,6 +161,7 @@ const en = {
|
|
|
159
161
|
'timeline.open': 'Timeline',
|
|
160
162
|
'timeline.openAria': 'Open the session timeline',
|
|
161
163
|
'timeline.title': 'Version timeline',
|
|
164
|
+
'view.retrace': 'Versions',
|
|
162
165
|
'timeline.refresh': 'Refresh',
|
|
163
166
|
'timeline.close': 'Close',
|
|
164
167
|
'timeline.empty': 'No versions yet. Recall / edit / regenerate record a version here.',
|
|
@@ -191,6 +194,7 @@ const en = {
|
|
|
191
194
|
'timeline.cancel': 'Cancel',
|
|
192
195
|
'timeline.busy': 'Rolling back…',
|
|
193
196
|
'timeline.detail': 'Details',
|
|
197
|
+
'timeline.trajectory': 'Trajectory',
|
|
194
198
|
'timeline.jump': 'Jump',
|
|
195
199
|
'timeline.jumpFailed': 'This version lies too far back (beyond the auto-load budget) to locate directly. Scroll up to load earlier messages, or use Details to read the original event text of this version.',
|
|
196
200
|
'timeline.gitRepo': 'git repository',
|
|
@@ -884,17 +888,13 @@ const CSS = `
|
|
|
884
888
|
.dsh-rt-retention-input{width:64px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:2px 6px;font:inherit;font-size:13px;outline:none;margin-top:1px}
|
|
885
889
|
.dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
|
|
886
890
|
.dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
|
|
887
|
-
/* ---- P1 timeline ---- */
|
|
888
|
-
.dsh-rt-
|
|
889
|
-
.dsh-rt-timeline-trigger{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:transparent;border:none;border-radius:6px;display:inline-flex;align-items:center;gap:4px;padding:3px 6px;font-size:12px;line-height:18px}
|
|
890
|
-
.dsh-rt-timeline-trigger:hover,.dsh-rt-timeline-trigger:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
891
|
-
.dsh-rt-timeline-icon{font-size:13px}
|
|
892
|
-
.dsh-rt-timeline-panel{position:fixed;top:calc(100% + 6px);right:8px;z-index:2147483000;width:min(520px,calc(100vw - 32px));max-height:min(70vh,640px);box-sizing:border-box;display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:8px;overflow:hidden}
|
|
891
|
+
/* ---- P1 timeline (conversation view tab) ---- */
|
|
892
|
+
.dsh-rt-view{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;height:100%;min-height:0;padding:12px 16px 0}
|
|
893
893
|
.dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
|
|
894
894
|
.dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
|
|
895
895
|
.dsh-rt-timeline-git{display:flex;align-items:center;gap:6px;flex:none;border:1px dashed var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px}
|
|
896
896
|
.dsh-rt-timeline-git-text{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px}
|
|
897
|
-
.dsh-rt-timeline-list{overflow-y:auto;flex:
|
|
897
|
+
.dsh-rt-timeline-list{overflow-y:auto;flex:1;min-height:0;position:relative;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
|
|
898
898
|
.dsh-rt-timeline-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:12px 4px;text-align:center}
|
|
899
899
|
.dsh-rt-version{position:absolute;left:0;right:0;height:60px;box-sizing:border-box;display:flex;align-items:flex-start;gap:8px;border:1px solid transparent;border-radius:10px;padding:6px 8px}
|
|
900
900
|
.dsh-rt-version:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
|
|
@@ -983,12 +983,16 @@ function fileCountLabel(counts, t) {
|
|
|
983
983
|
}
|
|
984
984
|
|
|
985
985
|
/**
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
986
|
+
* P1 — Version timeline as an official conversation view tab (PLAN §5.1,
|
|
987
|
+
* 0.4.2 trajectory-leverage): rendered in the `conversation.view` slot (a tab
|
|
988
|
+
* next to 对话/轨迹). The versions data channel stays plugin-owned — the live
|
|
989
|
+
* `session/projection` push frames arrive through
|
|
990
|
+
* `useProjection('retrace/versions')` when present (zero polling), with the
|
|
991
|
+
* HTTP `/versions` route as fallback — because versions are DERIVED data the
|
|
992
|
+
* official event stream does not carry. The view shell, view switching,
|
|
993
|
+
* paging and the trajectory handoff reuse the official mechanisms.
|
|
989
994
|
*/
|
|
990
|
-
function
|
|
991
|
-
const [open, setOpen] = useState(false)
|
|
995
|
+
function RetraceView({ sessionId, useProjection, t, actions, store }) {
|
|
992
996
|
const [versions, setVersions] = useState(null)
|
|
993
997
|
const [loading, setLoading] = useState(false)
|
|
994
998
|
const [error, setError] = useState(null)
|
|
@@ -997,49 +1001,15 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
|
|
|
997
1001
|
const [preview, setPreview] = useState(null)
|
|
998
1002
|
const [previewScope, setPreviewScope] = useState('both')
|
|
999
1003
|
const [rollbackBusy, setRollbackBusy] = useState(false)
|
|
1000
|
-
const [detail, setDetail] = useState(null)
|
|
1001
|
-
const [jumpTarget, setJumpTarget] = useState(null)
|
|
1002
|
-
const [jumpNotice, setJumpNotice] = useState(null)
|
|
1003
1004
|
const [scrollTop, setScrollTop] = useState(0)
|
|
1004
|
-
const [panelPos, setPanelPos] = useState(null)
|
|
1005
|
-
const rootRef = { current: null }
|
|
1006
|
-
const triggerRef = { current: null }
|
|
1007
|
-
const snapRef = { current: null }
|
|
1008
1005
|
|
|
1009
1006
|
// Live push-frame path (projection standard kit) — falls back to HTTP.
|
|
1010
1007
|
const projected = typeof useProjection === 'function' ? useProjection('retrace/versions') : undefined
|
|
1011
|
-
useSession((snapshot) => {
|
|
1012
|
-
snapRef.current = snapshot
|
|
1013
|
-
return snapshot.chat.nodes.size
|
|
1014
|
-
})
|
|
1015
1008
|
|
|
1016
1009
|
useEffect(() => {
|
|
1017
1010
|
if (projected && Array.isArray(projected.versions)) setVersions(projected.versions)
|
|
1018
1011
|
}, [projected])
|
|
1019
1012
|
|
|
1020
|
-
/** Anchor the panel to the trigger's live viewport rect (fixed positioning). */
|
|
1021
|
-
const anchorPanel = () => {
|
|
1022
|
-
const rect = triggerRef.current?.getBoundingClientRect?.()
|
|
1023
|
-
if (!rect) return
|
|
1024
|
-
setPanelPos({
|
|
1025
|
-
top: rect.bottom + 6,
|
|
1026
|
-
right: Math.max(8, window.innerWidth - rect.right),
|
|
1027
|
-
})
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Re-anchor on open and while open (scroll/resize keep it under the trigger).
|
|
1031
|
-
useEffect(() => {
|
|
1032
|
-
if (!open) return
|
|
1033
|
-
anchorPanel()
|
|
1034
|
-
const onViewportChange = () => anchorPanel()
|
|
1035
|
-
window.addEventListener('resize', onViewportChange)
|
|
1036
|
-
window.addEventListener('scroll', onViewportChange, true)
|
|
1037
|
-
return () => {
|
|
1038
|
-
window.removeEventListener('resize', onViewportChange)
|
|
1039
|
-
window.removeEventListener('scroll', onViewportChange, true)
|
|
1040
|
-
}
|
|
1041
|
-
}, [open])
|
|
1042
|
-
|
|
1043
1013
|
const refresh = () => {
|
|
1044
1014
|
setLoading(true)
|
|
1045
1015
|
setError(null)
|
|
@@ -1059,91 +1029,82 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
|
|
|
1059
1029
|
.catch(() => setGit(null))
|
|
1060
1030
|
}
|
|
1061
1031
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
setOpen(next)
|
|
1065
|
-
if (next) {
|
|
1066
|
-
if (projected === undefined) refresh()
|
|
1067
|
-
refreshGit()
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// Dismiss on outside pointer (matching the jobs menu pattern).
|
|
1032
|
+
// Mount-time load: projection push frames arrive live when the host mounts
|
|
1033
|
+
// the registry; HTTP is the on-demand fallback for minimal compositions.
|
|
1072
1034
|
useEffect(() => {
|
|
1073
|
-
if (
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
// seek-to-seq API — only `loadOlder` (50/page) — so far-away targets are
|
|
1084
|
-
// unreachable without thousands of pages; cap the budget and fall back to a
|
|
1085
|
-
// clear notice (details drawer shows the version's event text instead).
|
|
1035
|
+
if (projected === undefined) refresh()
|
|
1036
|
+
refreshGit()
|
|
1037
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1038
|
+
}, [])
|
|
1039
|
+
|
|
1040
|
+
// Jump: switch to the chat view, then page up via the session store until
|
|
1041
|
+
// the anchor seq is loaded, then scroll the chat row into view once it
|
|
1042
|
+
// renders (the view switch unmounts this view, so the whole flow runs here).
|
|
1043
|
+
// The store has no seek-to-seq API — only `loadOlder` (50/page) — so
|
|
1044
|
+
// far-away targets are unreachable without thousands of pages; cap the budget.
|
|
1086
1045
|
const JUMP_PAGE_BUDGET = 24
|
|
1087
1046
|
const jump = async (boundarySeq) => {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1047
|
+
actions?.setView?.('chat')
|
|
1048
|
+
const target = store
|
|
1049
|
+
if (!target || typeof target.loadOlder !== 'function' || typeof target.getSnapshot !== 'function') return
|
|
1050
|
+
const keyOfSeq = (seq) => {
|
|
1051
|
+
const nodes = target.getSnapshot()?.chat?.nodes
|
|
1052
|
+
if (!nodes) return null
|
|
1053
|
+
for (const node of nodes.values()) {
|
|
1054
|
+
if (typeof node.anchorSeq === 'number' && node.anchorSeq === seq) return node.key
|
|
1055
|
+
}
|
|
1056
|
+
return null
|
|
1057
|
+
}
|
|
1058
|
+
let key = keyOfSeq(boundarySeq)
|
|
1093
1059
|
let pages = 0
|
|
1094
|
-
while (pages < JUMP_PAGE_BUDGET &&
|
|
1095
|
-
const before =
|
|
1096
|
-
await
|
|
1097
|
-
const after =
|
|
1060
|
+
while (key === null && pages < JUMP_PAGE_BUDGET && target.hasMore) {
|
|
1061
|
+
const before = target.getSnapshot()?.chat?.nodes?.size ?? 0
|
|
1062
|
+
await target.loadOlder()
|
|
1063
|
+
const after = target.getSnapshot()?.chat?.nodes?.size ?? 0
|
|
1098
1064
|
pages += 1
|
|
1099
1065
|
if (after === before) break // page returned nothing
|
|
1066
|
+
key = keyOfSeq(boundarySeq)
|
|
1100
1067
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
setJumpNotice(t('timeline.jumpFailed'))
|
|
1106
|
-
setJumpTarget(null)
|
|
1068
|
+
if (key === null) {
|
|
1069
|
+
// Give the last prepend a tick to propagate into the snapshot first.
|
|
1070
|
+
await new Promise((resolve) => setTimeout(resolve, 60))
|
|
1071
|
+
key = keyOfSeq(boundarySeq)
|
|
1107
1072
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
function nodePresent(seq) {
|
|
1112
|
-
const nodes = snapRef.current?.chat?.nodes
|
|
1113
|
-
if (!nodes) return false
|
|
1114
|
-
for (const node of nodes.values()) {
|
|
1115
|
-
if (typeof node.anchorSeq === 'number' && node.anchorSeq === seq) return true
|
|
1073
|
+
if (key === null) {
|
|
1074
|
+
console.warn(`[dsh-retrace] jump target seq ${boundarySeq} not reached within ${JUMP_PAGE_BUDGET} pages`)
|
|
1075
|
+
return
|
|
1116
1076
|
}
|
|
1117
|
-
|
|
1077
|
+
// The chat view is now mounted (setView above); poll for its rendered row.
|
|
1078
|
+
const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90)
|
|
1079
|
+
if (el === null) return
|
|
1080
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
1081
|
+
flashKey(key)
|
|
1118
1082
|
}
|
|
1119
1083
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1084
|
+
/** rAF poll for an element that appears after a view switch + render pass. */
|
|
1085
|
+
function waitForElement(selector, frames) {
|
|
1086
|
+
return new Promise((resolve) => {
|
|
1087
|
+
let remaining = frames
|
|
1088
|
+
const probe = () => {
|
|
1089
|
+
const el = document.querySelector(selector)
|
|
1090
|
+
if (el !== null) return resolve(el)
|
|
1091
|
+
if (remaining-- <= 0) return resolve(null)
|
|
1092
|
+
requestAnimationFrame(probe)
|
|
1093
|
+
}
|
|
1094
|
+
requestAnimationFrame(probe)
|
|
1095
|
+
})
|
|
1096
|
+
}
|
|
1127
1097
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
if (!document.querySelector(`style[data-plugin-css="${id}"]`)) {
|
|
1139
|
-
const tag = document.createElement('style')
|
|
1140
|
-
tag.dataset.pluginCss = id
|
|
1141
|
-
tag.textContent = `[data-chat-anchor-key=${JSON.stringify(foundKey)}]{animation:dsh-rt-flash 1.6s ease-out 2}@keyframes dsh-rt-flash{0%,100%{background:transparent}30%,70%{background:var(--dsw-alias-state-business-primary)}55%{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}}`
|
|
1142
|
-
document.head.appendChild(tag)
|
|
1143
|
-
setTimeout(() => tag.remove(), 3400)
|
|
1144
|
-
}
|
|
1145
|
-
setJumpTarget(null)
|
|
1146
|
-
}, [foundKey, jumpTarget])
|
|
1098
|
+
/** One-shot highlight for the jumped-to row (removes itself). */
|
|
1099
|
+
function flashKey(key) {
|
|
1100
|
+
const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, '-')}`
|
|
1101
|
+
if (document.querySelector(`style[data-plugin-css="${id}"]`)) return
|
|
1102
|
+
const tag = document.createElement('style')
|
|
1103
|
+
tag.dataset.pluginCss = id
|
|
1104
|
+
tag.textContent = `[data-chat-anchor-key=${JSON.stringify(key)}]{animation:dsh-rt-flash 1.6s ease-out 2}@keyframes dsh-rt-flash{0%,100%{background:transparent}30%,70%{background:var(--dsw-alias-state-business-primary)}55%{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}}`
|
|
1105
|
+
document.head.appendChild(tag)
|
|
1106
|
+
setTimeout(() => tag.remove(), 3400)
|
|
1107
|
+
}
|
|
1147
1108
|
|
|
1148
1109
|
const requestPreview = (record) => {
|
|
1149
1110
|
setPreviewScope('both')
|
|
@@ -1170,16 +1131,6 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
|
|
|
1170
1131
|
})
|
|
1171
1132
|
}
|
|
1172
1133
|
|
|
1173
|
-
const openDetail = (record) => {
|
|
1174
|
-
setDetail({ seq: record.boundarySeq, data: null, error: null })
|
|
1175
|
-
timelineGet(`/event?sessionId=${encodeURIComponent(sessionId)}&seq=${record.boundarySeq}&before=2&after=2`)
|
|
1176
|
-
.then((result) => {
|
|
1177
|
-
setDetail((prev) => prev && prev.seq === record.boundarySeq
|
|
1178
|
-
? { ...prev, data: result?.ok === true ? result.value : null, error: result?.ok === true ? null : result?.error?.message ?? null }
|
|
1179
|
-
: prev)
|
|
1180
|
-
})
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
1134
|
const initGit = () => {
|
|
1184
1135
|
if (!window.confirm(t('timeline.gitInitConfirm'))) return
|
|
1185
1136
|
setGitBusy(true)
|
|
@@ -1192,100 +1143,71 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
|
|
|
1192
1143
|
// ---- windowed list (uniform rows, zero-dep) ----
|
|
1193
1144
|
const ROW_H = 64
|
|
1194
1145
|
const list = versions ?? []
|
|
1195
|
-
const visible = list.slice(Math.max(0, Math.floor(scrollTop / ROW_H) - 2), Math.min(list.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(
|
|
1146
|
+
const visible = list.slice(Math.max(0, Math.floor(scrollTop / ROW_H) - 2), Math.min(list.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(640 / ROW_H) + 2))
|
|
1147
|
+
|
|
1148
|
+
return createElement('div', { className: 'dsh-rt-view' }, [
|
|
1149
|
+
createElement('div', { key: 'head', className: 'dsh-rt-timeline-head' }, [
|
|
1150
|
+
createElement('span', { key: 'title', className: 'dsh-rt-timeline-title' }, t('timeline.title')),
|
|
1151
|
+
createElement('button', {
|
|
1152
|
+
key: 'refresh',
|
|
1153
|
+
type: 'button',
|
|
1154
|
+
className: 'dsh-rt-chip',
|
|
1155
|
+
onClick: refresh,
|
|
1156
|
+
}, t('timeline.refresh')),
|
|
1157
|
+
]),
|
|
1196
1158
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1159
|
+
git !== null && git !== undefined && createElement('div', { key: 'git', className: 'dsh-rt-timeline-git' }, [
|
|
1160
|
+
git.headHash
|
|
1161
|
+
? [
|
|
1162
|
+
createElement('span', { key: 'r', className: 'dsh-rt-timeline-git-text' }, `${t('timeline.gitRepo')} · ${t('timeline.gitHead', { hash: git.headHash.slice(0, 8) })}${git.dirty ? ` · ${t('timeline.gitDirty')}` : ''}`),
|
|
1163
|
+
]
|
|
1164
|
+
: createElement('button', {
|
|
1165
|
+
key: 'init',
|
|
1166
|
+
type: 'button',
|
|
1167
|
+
className: 'dsh-rt-chip',
|
|
1168
|
+
disabled: gitBusy,
|
|
1169
|
+
onClick: initGit,
|
|
1170
|
+
title: t('timeline.gitInitDesc'),
|
|
1171
|
+
}, t('timeline.gitInit')),
|
|
1209
1172
|
]),
|
|
1210
1173
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
style: panelPos ? { top: `${panelPos.top}px`, right: `${panelPos.right}px` } : undefined,
|
|
1215
|
-
}, [
|
|
1216
|
-
createElement('div', { key: 'head', className: 'dsh-rt-timeline-head' }, [
|
|
1217
|
-
createElement('span', { key: 'title', className: 'dsh-rt-timeline-title' }, t('timeline.title')),
|
|
1218
|
-
createElement('button', {
|
|
1219
|
-
key: 'refresh',
|
|
1220
|
-
type: 'button',
|
|
1221
|
-
className: 'dsh-rt-chip',
|
|
1222
|
-
onClick: refresh,
|
|
1223
|
-
}, t('timeline.refresh')),
|
|
1224
|
-
createElement('button', {
|
|
1225
|
-
key: 'close',
|
|
1226
|
-
type: 'button',
|
|
1227
|
-
className: 'dsh-rt-chip',
|
|
1228
|
-
onClick: () => setOpen(false),
|
|
1229
|
-
}, t('timeline.close')),
|
|
1230
|
-
]),
|
|
1174
|
+
error !== null && createElement('div', { key: 'error', className: 'dsh-rt-error' }, error),
|
|
1175
|
+
loading && createElement('div', { key: 'loading', className: 'dsh-rt-timeline-empty' }, t('timeline.loading')),
|
|
1176
|
+
!loading && list.length === 0 && createElement('div', { key: 'empty', className: 'dsh-rt-timeline-empty' }, t('timeline.empty')),
|
|
1231
1177
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
error !== null && createElement('div', { key: 'error', className: 'dsh-rt-error' }, error),
|
|
1248
|
-
loading && createElement('div', { key: 'loading', className: 'dsh-rt-timeline-empty' }, t('timeline.loading')),
|
|
1249
|
-
!loading && list.length === 0 && createElement('div', { key: 'empty', className: 'dsh-rt-timeline-empty' }, t('timeline.empty')),
|
|
1250
|
-
|
|
1251
|
-
list.length > 0 && createElement('div', {
|
|
1252
|
-
key: 'list',
|
|
1253
|
-
className: 'dsh-rt-timeline-list',
|
|
1254
|
-
style: { height: `${Math.min(420, list.length * ROW_H)}px` },
|
|
1255
|
-
onScroll: (event) => setScrollTop(event.target.scrollTop),
|
|
1256
|
-
}, [
|
|
1257
|
-
createElement('div', { key: 'spacer', style: { height: `${list.length * ROW_H}px`, position: 'relative' } }, [
|
|
1258
|
-
visible.map((record) => createElement(VersionRow, {
|
|
1259
|
-
key: record.versionId,
|
|
1260
|
-
record,
|
|
1261
|
-
t,
|
|
1262
|
-
top: list.indexOf(record) * ROW_H,
|
|
1263
|
-
onPreview: () => requestPreview(record),
|
|
1264
|
-
onDetail: () => openDetail(record),
|
|
1265
|
-
onJump: () => jump(record.boundarySeq),
|
|
1266
|
-
})),
|
|
1267
|
-
]),
|
|
1178
|
+
list.length > 0 && createElement('div', {
|
|
1179
|
+
key: 'list',
|
|
1180
|
+
className: 'dsh-rt-timeline-list',
|
|
1181
|
+
onScroll: (event) => setScrollTop(event.target.scrollTop),
|
|
1182
|
+
}, [
|
|
1183
|
+
createElement('div', { key: 'spacer', style: { height: `${list.length * ROW_H}px`, position: 'relative' } }, [
|
|
1184
|
+
visible.map((record) => createElement(VersionRow, {
|
|
1185
|
+
key: record.versionId,
|
|
1186
|
+
record,
|
|
1187
|
+
t,
|
|
1188
|
+
top: list.indexOf(record) * ROW_H,
|
|
1189
|
+
onPreview: () => requestPreview(record),
|
|
1190
|
+
onTrajectory: () => actions?.setView?.('trajectory'),
|
|
1191
|
+
onJump: () => jump(record.boundarySeq),
|
|
1192
|
+
})),
|
|
1268
1193
|
]),
|
|
1269
|
-
|
|
1270
|
-
preview && createElement(PreviewBox, {
|
|
1271
|
-
key: 'preview',
|
|
1272
|
-
preview,
|
|
1273
|
-
scope: previewScope,
|
|
1274
|
-
setScope: setPreviewScope,
|
|
1275
|
-
busy: rollbackBusy,
|
|
1276
|
-
t,
|
|
1277
|
-
onConfirm: confirmRollback,
|
|
1278
|
-
onCancel: () => setPreview(null),
|
|
1279
|
-
}),
|
|
1280
|
-
|
|
1281
|
-
detail && createElement(DetailBox, { key: 'detail', detail, t, onClose: () => setDetail(null) }),
|
|
1282
|
-
jumpNotice !== null && createElement('div', { key: 'jump', className: 'dsh-rt-error' }, jumpNotice),
|
|
1283
1194
|
]),
|
|
1195
|
+
|
|
1196
|
+
preview && createElement(PreviewBox, {
|
|
1197
|
+
key: 'preview',
|
|
1198
|
+
preview,
|
|
1199
|
+
scope: previewScope,
|
|
1200
|
+
setScope: setPreviewScope,
|
|
1201
|
+
busy: rollbackBusy,
|
|
1202
|
+
t,
|
|
1203
|
+
onConfirm: confirmRollback,
|
|
1204
|
+
onCancel: () => setPreview(null),
|
|
1205
|
+
}),
|
|
1284
1206
|
])
|
|
1285
1207
|
}
|
|
1286
1208
|
|
|
1287
1209
|
/** One version row (uniform height for the windowed list). */
|
|
1288
|
-
function VersionRow({ record, top, t, onPreview,
|
|
1210
|
+
function VersionRow({ record, top, t, onPreview, onTrajectory, onJump }) {
|
|
1289
1211
|
return createElement('div', { className: 'dsh-rt-version', style: { top: `${top}px` } }, [
|
|
1290
1212
|
createElement('span', { key: 'kind', className: `dsh-rt-version-kind dsh-rt-version-kind-${record.kind}`, title: kindLabel(record.kind, t) }, KIND_ICONS[record.kind] ?? '•'),
|
|
1291
1213
|
createElement('div', { key: 'body', className: 'dsh-rt-version-body' }, [
|
|
@@ -1300,8 +1222,8 @@ function VersionRow({ record, top, t, onPreview, onDetail, onJump }) {
|
|
|
1300
1222
|
: null,
|
|
1301
1223
|
]),
|
|
1302
1224
|
createElement('span', { key: 'actions', className: 'dsh-rt-version-actions' }, [
|
|
1303
|
-
createElement('button', { key: 'detail', type: 'button', className: 'dsh-rt-chip', onClick: onDetail }, t('timeline.detail')),
|
|
1304
1225
|
createElement('button', { key: 'jump', type: 'button', className: 'dsh-rt-chip', onClick: onJump }, t('timeline.jump')),
|
|
1226
|
+
createElement('button', { key: 'trajectory', type: 'button', className: 'dsh-rt-chip', onClick: onTrajectory, title: t('timeline.trajectory') }, t('timeline.trajectory')),
|
|
1305
1227
|
createElement('button', { key: 'preview', type: 'button', className: 'dsh-rt-chip dsh-rt-chip-danger', onClick: onPreview }, t('timeline.preview')),
|
|
1306
1228
|
]),
|
|
1307
1229
|
])
|
|
@@ -1351,19 +1273,6 @@ function PreviewBox({ preview, scope, setScope, busy, t, onConfirm, onCancel })
|
|
|
1351
1273
|
])
|
|
1352
1274
|
}
|
|
1353
1275
|
|
|
1354
|
-
/** Lazy event viewer (GET /event). */
|
|
1355
|
-
function DetailBox({ detail, t, onClose }) {
|
|
1356
|
-
return createElement('div', { className: 'dsh-rt-modal' }, [
|
|
1357
|
-
createElement('div', { key: 'title', className: 'dsh-rt-modal-title' }, [
|
|
1358
|
-
`${t('timeline.detail')} · seq ${detail.seq}`,
|
|
1359
|
-
createElement('button', { key: 'close', type: 'button', className: 'dsh-rt-chip', onClick: onClose }, t('timeline.close')),
|
|
1360
|
-
]),
|
|
1361
|
-
detail.error && createElement('div', { key: 'err', className: 'dsh-rt-error' }, detail.error),
|
|
1362
|
-
detail.data === null && !detail.error && createElement('div', { key: 'wait', className: 'dsh-rt-timeline-empty' }, t('timeline.loading')),
|
|
1363
|
-
detail.data && createElement('pre', { key: 'json', className: 'dsh-rt-modal-json' }, JSON.stringify(detail.data, null, 2)),
|
|
1364
|
-
])
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
1276
|
// ---------------------------------------------------------------------------
|
|
1368
1277
|
// Plugin
|
|
1369
1278
|
// ---------------------------------------------------------------------------
|
|
@@ -1371,6 +1280,7 @@ export function apply(ctx) {
|
|
|
1371
1280
|
const disposeStyle = ensureStyle()
|
|
1372
1281
|
ctx.effect(() => () => disposeStyle(), 'dsh-retrace: styles')
|
|
1373
1282
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-retrace: dictionaries')
|
|
1283
|
+
const t = ctx.locale.bind(NS)
|
|
1374
1284
|
|
|
1375
1285
|
const conversationEvents = ctx.get('conversationEvents')
|
|
1376
1286
|
if (conversationEvents) {
|
|
@@ -1404,13 +1314,21 @@ export function apply(ctx) {
|
|
|
1404
1314
|
locale: NS,
|
|
1405
1315
|
}, RecallMarkerRow))
|
|
1406
1316
|
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1317
|
+
// P0 (0.4.2, trajectory-leverage) — the version timeline as an official
|
|
1318
|
+
// conversation view tab (next to 对话/轨迹). The view shell + switching are
|
|
1319
|
+
// the official mechanisms; the versions data channel stays plugin-owned
|
|
1320
|
+
// (projection push frames → HTTP /versions fallback).
|
|
1321
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
1322
|
+
name: 'conversation.view',
|
|
1323
|
+
id: 'retrace',
|
|
1324
|
+
order: 20,
|
|
1412
1325
|
locale: NS,
|
|
1413
|
-
|
|
1326
|
+
label: () => t('view.retrace'),
|
|
1327
|
+
inject: (sessionId, actions) => ({
|
|
1328
|
+
actions,
|
|
1329
|
+
store: ctx.get?.('sessions')?.binding?.(sessionId)?.session,
|
|
1330
|
+
}),
|
|
1331
|
+
}, RetraceView))
|
|
1414
1332
|
|
|
1415
1333
|
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
|
|
1416
1334
|
name: 'settings.general.item',
|