dsh-sidenote 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/lib/client.js +223 -187
- package/lib/types/client/host/chat.d.ts +13 -2
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/sidechat/InheritedContext.d.ts +1 -0
- package/lib/types/client/sidechat/NativeConversation.d.ts +1 -1
- package/lib/types/client/sidechat/context.d.ts +4 -8
- package/lib/types/client/sidechat/metaStore.d.ts +6 -8
- package/package.json +1 -1
- package/src/client/host/chat.ts +10 -7
- package/src/client/locales.ts +4 -0
- package/src/client/sidechat/InheritedContext.tsx +3 -1
- package/src/client/sidechat/NativeConversation.tsx +3 -1
- package/src/client/sidechat/SavedConversation.tsx +10 -2
- package/src/client/sidechat/SideChatPanel.tsx +8 -4
- package/src/client/sidechat/context.ts +11 -7
- package/src/client/sidechat/metaStore.ts +47 -58
- package/src/client/sidechat/native-tab.tsx +2 -0
- package/src/client/sidechat/useMainContext.ts +2 -7
- package/src/client/sidechat/visibility.tsx +2 -2
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ dsh plugin --profile web add dsh-sidenote
|
|
|
47
47
|
|
|
48
48
|
侧聊有独立的多轮对话、工具卡、模型和权限切换,也支持图片、`@` 引用以及面板内审批。主线和支线各自推进。
|
|
49
49
|
|
|
50
|
+
刷新或重启 DSH 后,侧聊自身的问答仍留在侧聊区,继承的主线上下文保持折叠分区。保存为正式会话保留这个分区;从保存的会话再次开侧聊时,才建立新的继承范围。来源尚未确认时可以阅读消息,确认后再使用回流。
|
|
51
|
+
|
|
50
52
|
### 有想法,直接落在原文上
|
|
51
53
|
|
|
52
54
|
划选主对话或侧聊中的一段回复,写下问题或修改意见。**原文和注释一起保留**,可累积多条,发送前预览、移除,正文草稿照常编辑。
|
package/lib/client.js
CHANGED
|
@@ -199,16 +199,18 @@ window.__ModuleLoader__.load({
|
|
|
199
199
|
if (last?.type !== "compaction/start" || last.data.turn !== null) return void 0;
|
|
200
200
|
return ((last.data.sourceCommandId === void 0 ? void 0 : entries.findLast(({ event }) => event.type === "command/run" && event.data.commandId === last.data.sourceCommandId)?.event)?.seq ?? last.seq) - 1;
|
|
201
201
|
}
|
|
202
|
-
/**
|
|
203
|
-
*
|
|
202
|
+
/** Restore also appends end-seed, but only inherited=true records a fork cut.
|
|
203
|
+
* A saved copy resolves the cut before its original first-send snapshot. */
|
|
204
|
+
function inheritedBoundaryOf(entries, beforeSeq = Infinity) {
|
|
205
|
+
const seed = entries.findLast(({ event }) => event.type === "session/end-seed" && event.data?.inherited === true && event.seq < beforeSeq);
|
|
206
|
+
return seed === void 0 ? void 0 : seed.event.seq - 1;
|
|
207
|
+
}
|
|
208
|
+
/** The loaded window can lack the inherited marker; undefined means pending. */
|
|
204
209
|
function inheritedBoundarySource(session) {
|
|
205
210
|
const source = contextEventSource(session);
|
|
206
211
|
return source && {
|
|
207
212
|
subscribe: (notify) => source.subscribe(notify),
|
|
208
|
-
getSnapshot: () =>
|
|
209
|
-
const seed = source.getSnapshot().entries.findLast((entry) => entry.event.type === "session/end-seed");
|
|
210
|
-
return seed === void 0 ? void 0 : seed.event.seq - 1;
|
|
211
|
-
}
|
|
213
|
+
getSnapshot: () => inheritedBoundaryOf(source.getSnapshot().entries)
|
|
212
214
|
};
|
|
213
215
|
}
|
|
214
216
|
/** Wait for the host's single-page pull before requesting a jump. loadThrough
|
|
@@ -740,7 +742,7 @@ window.__ModuleLoader__.load({
|
|
|
740
742
|
let dockRegistered = false;
|
|
741
743
|
let report = {
|
|
742
744
|
versions: {
|
|
743
|
-
sidenote: "0.8.
|
|
745
|
+
sidenote: "0.8.1",
|
|
744
746
|
dsh: "unknown",
|
|
745
747
|
betterSidebar: null
|
|
746
748
|
},
|
|
@@ -1080,6 +1082,8 @@ window.__ModuleLoader__.load({
|
|
|
1080
1082
|
expandAll: "Expand all",
|
|
1081
1083
|
jumpToLatest: "↓ Jump to latest",
|
|
1082
1084
|
reflowAll: "Reflow all",
|
|
1085
|
+
reflowPending: "Confirming this side chat’s origin before bringing replies back",
|
|
1086
|
+
sideRestoreMissing: "The original side chat could not be restored. Reopen it from the recently closed list in /side.",
|
|
1083
1087
|
reflowAllTitle: "Bring all Q&A pairs back to the main session",
|
|
1084
1088
|
reflowAllDone: "Reflowed ✓",
|
|
1085
1089
|
promote: "Save as session",
|
|
@@ -1303,6 +1307,8 @@ window.__ModuleLoader__.load({
|
|
|
1303
1307
|
expandAll: "全部展开",
|
|
1304
1308
|
jumpToLatest: "↓ 跳到最新",
|
|
1305
1309
|
reflowAll: "整段回流",
|
|
1310
|
+
reflowPending: "正在确认侧聊来源,确认后可回流",
|
|
1311
|
+
sideRestoreMissing: "未能恢复原侧聊。请从 /side 的最近关闭列表重新打开。",
|
|
1306
1312
|
reflowAllTitle: "把该侧聊全部问答对带回主会话(chip 可预览/逐条撤)",
|
|
1307
1313
|
reflowAllDone: "已回流 ✓",
|
|
1308
1314
|
promote: "保存为正式会话",
|
|
@@ -1409,15 +1415,15 @@ window.__ModuleLoader__.load({
|
|
|
1409
1415
|
document.head.appendChild(tag);
|
|
1410
1416
|
}
|
|
1411
1417
|
var diagnostics_module_css_default = {
|
|
1412
|
-
"panel": "tGe0LW_panel",
|
|
1413
|
-
"header": "tGe0LW_header",
|
|
1414
1418
|
"list": "tGe0LW_list",
|
|
1415
|
-
"
|
|
1419
|
+
"header": "tGe0LW_header",
|
|
1420
|
+
"actions": "tGe0LW_actions",
|
|
1421
|
+
"versions": "tGe0LW_versions",
|
|
1416
1422
|
"trigger": "tGe0LW_trigger",
|
|
1417
1423
|
"setting": "tGe0LW_setting",
|
|
1418
|
-
"
|
|
1419
|
-
"
|
|
1420
|
-
"
|
|
1424
|
+
"panel": "tGe0LW_panel",
|
|
1425
|
+
"description": "tGe0LW_description",
|
|
1426
|
+
"settingTitle": "tGe0LW_settingTitle"
|
|
1421
1427
|
};
|
|
1422
1428
|
//#endregion
|
|
1423
1429
|
//#region src/client/diagnostics/index.tsx
|
|
@@ -2363,33 +2369,33 @@ window.__ModuleLoader__.load({
|
|
|
2363
2369
|
document.head.appendChild(tag);
|
|
2364
2370
|
}
|
|
2365
2371
|
var annotations_module_css_default = {
|
|
2366
|
-
"chip": "SVnebq_chip",
|
|
2367
|
-
"chipDismissShade": "SVnebq_chipDismissShade",
|
|
2368
|
-
"chipClearAll": "SVnebq_chipClearAll",
|
|
2369
|
-
"sentChipGroup": "SVnebq_sentChipGroup",
|
|
2370
|
-
"contextAction": "SVnebq_contextAction",
|
|
2371
|
-
"sentChip": "SVnebq_sentChip",
|
|
2372
|
-
"sentChevron": "SVnebq_sentChevron",
|
|
2373
|
-
"contextEditor": "SVnebq_contextEditor",
|
|
2374
|
-
"contextLabel": "SVnebq_contextLabel",
|
|
2375
2372
|
"contextFooter": "SVnebq_contextFooter",
|
|
2376
|
-
"chipWrap": "SVnebq_chipWrap",
|
|
2377
|
-
"userContextHost": "SVnebq_userContextHost",
|
|
2378
|
-
"sentPreview": "SVnebq_sentPreview",
|
|
2379
|
-
"contextContent": "SVnebq_contextContent",
|
|
2380
2373
|
"contextText": "SVnebq_contextText",
|
|
2381
|
-
"contextNumber": "SVnebq_contextNumber",
|
|
2382
|
-
"contextActions": "SVnebq_contextActions",
|
|
2383
|
-
"storageNotice": "SVnebq_storageNotice",
|
|
2384
|
-
"contextItem": "SVnebq_contextItem",
|
|
2385
|
-
"contextList": "SVnebq_contextList",
|
|
2386
2374
|
"sentChips": "SVnebq_sentChips",
|
|
2375
|
+
"contextNumber": "SVnebq_contextNumber",
|
|
2376
|
+
"contextContent": "SVnebq_contextContent",
|
|
2387
2377
|
"chipMain": "SVnebq_chipMain",
|
|
2388
|
-
"
|
|
2389
|
-
"
|
|
2378
|
+
"sentChip": "SVnebq_sentChip",
|
|
2379
|
+
"contextAction": "SVnebq_contextAction",
|
|
2380
|
+
"sentPreview": "SVnebq_sentPreview",
|
|
2381
|
+
"chipWrap": "SVnebq_chipWrap",
|
|
2382
|
+
"sentChipGroup": "SVnebq_sentChipGroup",
|
|
2390
2383
|
"contextSource": "SVnebq_contextSource",
|
|
2384
|
+
"userContextHost": "SVnebq_userContextHost",
|
|
2385
|
+
"contextEditor": "SVnebq_contextEditor",
|
|
2386
|
+
"sentChevronOpen": "SVnebq_sentChevronOpen",
|
|
2387
|
+
"chipDismissShade": "SVnebq_chipDismissShade",
|
|
2388
|
+
"contextItem": "SVnebq_contextItem",
|
|
2389
|
+
"contextActions": "SVnebq_contextActions",
|
|
2390
|
+
"contextLabel": "SVnebq_contextLabel",
|
|
2391
|
+
"chipClearAll": "SVnebq_chipClearAll",
|
|
2392
|
+
"sentChevron": "SVnebq_sentChevron",
|
|
2393
|
+
"chip": "SVnebq_chip",
|
|
2391
2394
|
"contextAppear": "SVnebq_contextAppear",
|
|
2392
|
-
"
|
|
2395
|
+
"contextPanel": "SVnebq_contextPanel",
|
|
2396
|
+
"storageNotice": "SVnebq_storageNotice",
|
|
2397
|
+
"chipDismiss": "SVnebq_chipDismiss",
|
|
2398
|
+
"contextList": "SVnebq_contextList"
|
|
2393
2399
|
};
|
|
2394
2400
|
//#endregion
|
|
2395
2401
|
//#region src/client/annotations/NativeUserContext.tsx
|
|
@@ -2863,39 +2869,39 @@ window.__ModuleLoader__.load({
|
|
|
2863
2869
|
document.head.appendChild(tag);
|
|
2864
2870
|
}
|
|
2865
2871
|
var sidechat_module_css_default = {
|
|
2866
|
-
"contextChevronOpen": "_5KEEsG_contextChevronOpen",
|
|
2867
2872
|
"emptyIcon": "_5KEEsG_emptyIcon",
|
|
2868
|
-
"
|
|
2869
|
-
"
|
|
2870
|
-
"
|
|
2871
|
-
"
|
|
2872
|
-
"historyToggle": "_5KEEsG_historyToggle",
|
|
2873
|
-
"progressContent": "_5KEEsG_progressContent",
|
|
2874
|
-
"nativeConversation": "_5KEEsG_nativeConversation",
|
|
2873
|
+
"progressDetail": "_5KEEsG_progressDetail",
|
|
2874
|
+
"mainContext": "_5KEEsG_mainContext",
|
|
2875
|
+
"contextImage": "_5KEEsG_contextImage",
|
|
2876
|
+
"headerSide": "_5KEEsG_headerSide",
|
|
2875
2877
|
"contextBubble": "_5KEEsG_contextBubble",
|
|
2876
|
-
"
|
|
2877
|
-
"
|
|
2878
|
+
"progressEntries": "_5KEEsG_progressEntries",
|
|
2879
|
+
"progressLatest": "_5KEEsG_progressLatest",
|
|
2880
|
+
"stateScreen": "_5KEEsG_stateScreen",
|
|
2881
|
+
"historyToggle": "_5KEEsG_historyToggle",
|
|
2882
|
+
"progress": "_5KEEsG_progress",
|
|
2883
|
+
"progressProcess": "_5KEEsG_progressProcess",
|
|
2884
|
+
"reflowAction": "_5KEEsG_reflowAction",
|
|
2878
2885
|
"nativeHost": "_5KEEsG_nativeHost",
|
|
2879
|
-
"
|
|
2880
|
-
"
|
|
2886
|
+
"contextColumn": "_5KEEsG_contextColumn",
|
|
2887
|
+
"emptyText": "_5KEEsG_emptyText",
|
|
2888
|
+
"nativeToolbar": "_5KEEsG_nativeToolbar",
|
|
2889
|
+
"visibilitySetting": "_5KEEsG_visibilitySetting",
|
|
2890
|
+
"root": "_5KEEsG_root",
|
|
2891
|
+
"chatView": "_5KEEsG_chatView",
|
|
2881
2892
|
"contextChevron": "_5KEEsG_contextChevron",
|
|
2882
2893
|
"contextHint": "_5KEEsG_contextHint",
|
|
2883
|
-
"
|
|
2884
|
-
"progress": "_5KEEsG_progress",
|
|
2894
|
+
"progressContent": "_5KEEsG_progressContent",
|
|
2885
2895
|
"errorRow": "_5KEEsG_errorRow",
|
|
2886
|
-
"
|
|
2887
|
-
"
|
|
2896
|
+
"contextUserRow": "_5KEEsG_contextUserRow",
|
|
2897
|
+
"progressEntry": "_5KEEsG_progressEntry",
|
|
2898
|
+
"progressPreview": "_5KEEsG_progressPreview",
|
|
2899
|
+
"emptyTitle": "_5KEEsG_emptyTitle",
|
|
2900
|
+
"nativeConversation": "_5KEEsG_nativeConversation",
|
|
2901
|
+
"contextChevronOpen": "_5KEEsG_contextChevronOpen",
|
|
2888
2902
|
"nativeAction": "_5KEEsG_nativeAction",
|
|
2889
|
-
"
|
|
2890
|
-
"
|
|
2891
|
-
"emptyText": "_5KEEsG_emptyText",
|
|
2892
|
-
"progressEntries": "_5KEEsG_progressEntries",
|
|
2893
|
-
"progressDetail": "_5KEEsG_progressDetail",
|
|
2894
|
-
"mainContext": "_5KEEsG_mainContext",
|
|
2895
|
-
"chatView": "_5KEEsG_chatView",
|
|
2896
|
-
"contextColumn": "_5KEEsG_contextColumn",
|
|
2897
|
-
"contextImage": "_5KEEsG_contextImage",
|
|
2898
|
-
"stateScreen": "_5KEEsG_stateScreen"
|
|
2903
|
+
"nativeToolbarActions": "_5KEEsG_nativeToolbarActions",
|
|
2904
|
+
"stateDetail": "_5KEEsG_stateDetail"
|
|
2899
2905
|
};
|
|
2900
2906
|
//#endregion
|
|
2901
2907
|
//#region src/client/sidechat/ContextProgress.tsx
|
|
@@ -3185,38 +3191,53 @@ window.__ModuleLoader__.load({
|
|
|
3185
3191
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3186
3192
|
className: sidechat_module_css_default.contextColumn,
|
|
3187
3193
|
"data-sidenote-main-context": true,
|
|
3188
|
-
children: [
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3194
|
+
children: [
|
|
3195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3196
|
+
type: "button",
|
|
3197
|
+
className: sidechat_module_css_default.historyToggle,
|
|
3198
|
+
"aria-expanded": expanded,
|
|
3199
|
+
"aria-controls": contextId,
|
|
3200
|
+
onClick: () => {
|
|
3201
|
+
fold.toggle("inherited");
|
|
3202
|
+
},
|
|
3203
|
+
children: [
|
|
3204
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutline14, {
|
|
3205
|
+
size: 12,
|
|
3206
|
+
className: expanded ? sidechat_module_css_default.contextChevronOpen : sidechat_module_css_default.contextChevron
|
|
3207
|
+
}),
|
|
3208
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("mainContext") }),
|
|
3209
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3210
|
+
className: sidechat_module_css_default.contextHint,
|
|
3211
|
+
children: context.contextPending ? t("contextAtSend") : context.progress?.takenAt ? t("contextAsOf", { time: context.progress.takenAt }) : t("contextInherited")
|
|
3212
|
+
})
|
|
3213
|
+
]
|
|
3214
|
+
}),
|
|
3215
|
+
!expanded && context.loadError && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3216
|
+
className: sidechat_module_css_default.errorRow,
|
|
3217
|
+
role: "alert",
|
|
3218
|
+
children: [context.loadError, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3219
|
+
type: "button",
|
|
3220
|
+
className: sidechat_module_css_default.nativeAction,
|
|
3221
|
+
onClick: () => {
|
|
3222
|
+
fold.toggle("inherited");
|
|
3223
|
+
},
|
|
3224
|
+
children: t("contextRetry")
|
|
3225
|
+
})]
|
|
3226
|
+
}),
|
|
3227
|
+
expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextProgress, {
|
|
3228
|
+
id: contextId,
|
|
3229
|
+
history: context.history,
|
|
3230
|
+
entries: context.progress?.entries ?? [],
|
|
3231
|
+
live: context.contextPending === true,
|
|
3232
|
+
loading,
|
|
3233
|
+
error: error ?? (loading ? void 0 : context.loadError),
|
|
3234
|
+
onRetry: () => {
|
|
3235
|
+
setAttempt((value) => value + 1);
|
|
3236
|
+
},
|
|
3237
|
+
loadImage: context.loadImage,
|
|
3238
|
+
loadProgressImage: context.loadProgressImage
|
|
3239
|
+
})
|
|
3240
|
+
]
|
|
3220
3241
|
})
|
|
3221
3242
|
});
|
|
3222
3243
|
}
|
|
@@ -3544,16 +3565,19 @@ ${body}
|
|
|
3544
3565
|
function contextHistory(history, progress) {
|
|
3545
3566
|
return progress.some((entry) => entry.kind === "checkpoint") ? [] : history;
|
|
3546
3567
|
}
|
|
3547
|
-
/** Saving
|
|
3548
|
-
*
|
|
3549
|
-
function savedContextOf(nodes, events, hasMore = false) {
|
|
3550
|
-
for (const value of [...nodes].reverse()) {
|
|
3568
|
+
/** Saving preserves the latest side chat's reading cut. An active side chat
|
|
3569
|
+
* freezes its first own snapshot after afterSeq. Restore markers never establish ownership. */
|
|
3570
|
+
function savedContextOf(nodes, events, hasMore = false, afterSeq) {
|
|
3571
|
+
for (const value of afterSeq === void 0 ? [...nodes].reverse() : nodes) {
|
|
3551
3572
|
const node = value;
|
|
3552
|
-
if (node.kind !== "user" && node.kind !== "steering") continue;
|
|
3573
|
+
if (node.seq <= (afterSeq ?? -1) || node.kind !== "user" && node.kind !== "steering") continue;
|
|
3553
3574
|
const saved = splitSnapshotPrefix(node.content?.find((block) => block.type === "text")?.text ?? "");
|
|
3554
3575
|
if (!saved) continue;
|
|
3555
|
-
|
|
3556
|
-
|
|
3576
|
+
let boundarySeq = saved.boundarySeq ?? inheritedBoundaryOf(events, node.seq);
|
|
3577
|
+
if (boundarySeq === void 0) {
|
|
3578
|
+
if (hasMore || events.some(({ event }) => event.type === "session/end-seed" && event.seq < node.seq)) continue;
|
|
3579
|
+
boundarySeq = -1;
|
|
3580
|
+
}
|
|
3557
3581
|
if (!Number.isSafeInteger(boundarySeq) || boundarySeq < -1 || boundarySeq >= node.seq) continue;
|
|
3558
3582
|
return {
|
|
3559
3583
|
...saved,
|
|
@@ -3640,7 +3664,12 @@ ${body}
|
|
|
3640
3664
|
(0, react.useEffect)(() => {
|
|
3641
3665
|
if (typeof markerSeq !== "number" || saved) return;
|
|
3642
3666
|
let active = true;
|
|
3643
|
-
loadInheritedContext(session, markerSeq).
|
|
3667
|
+
loadInheritedContext(session, markerSeq).then(async () => {
|
|
3668
|
+
const window = source?.getSnapshot();
|
|
3669
|
+
const nodes = chat?.getLegacy()?.nodes ?? [];
|
|
3670
|
+
const prefix = splitSnapshotPrefix(nodes.find((value) => value.seq === markerSeq)?.content?.find((block) => block.type === "text")?.text ?? "");
|
|
3671
|
+
if (prefix && prefix.boundarySeq === void 0 && !savedContextOf(nodes, window?.entries ?? [], window?.hasMore) && window?.hasMore) await loadInheritedContext(session, 0);
|
|
3672
|
+
}).catch((reason) => {
|
|
3644
3673
|
if (active) setError(String(reason instanceof Error ? reason.message : reason));
|
|
3645
3674
|
});
|
|
3646
3675
|
return () => {
|
|
@@ -3649,7 +3678,9 @@ ${body}
|
|
|
3649
3678
|
}, [
|
|
3650
3679
|
markerSeq,
|
|
3651
3680
|
!!saved,
|
|
3652
|
-
session
|
|
3681
|
+
session,
|
|
3682
|
+
source,
|
|
3683
|
+
chat
|
|
3653
3684
|
]);
|
|
3654
3685
|
const progress = (0, react.useMemo)(() => saved ? {
|
|
3655
3686
|
entries: snapshotEntriesFromText(saved.snapshot, saved.content),
|
|
@@ -3837,7 +3868,8 @@ ${body}
|
|
|
3837
3868
|
}, { slots: { views: ChatView } });
|
|
3838
3869
|
}
|
|
3839
3870
|
/** Only completed, fork-local final answers are eligible for reflow. */
|
|
3840
|
-
function finalReplies(messages, boundarySeq
|
|
3871
|
+
function finalReplies(messages, boundarySeq, completed) {
|
|
3872
|
+
if (boundarySeq === void 0) return [];
|
|
3841
3873
|
const replies = [];
|
|
3842
3874
|
let final;
|
|
3843
3875
|
const finish = () => {
|
|
@@ -3859,6 +3891,7 @@ ${body}
|
|
|
3859
3891
|
return side.reflow.list(side.parentSessionId).some((item) => item.sourceKey === key || item.sourceKey === void 0 && item.sideTitle === side.title && item.text === message.text && item.question === question);
|
|
3860
3892
|
}
|
|
3861
3893
|
function addReply(side, message, question) {
|
|
3894
|
+
if (side.boundarySeq === void 0 || message.seq === void 0 || message.seq <= side.boundarySeq) return;
|
|
3862
3895
|
if (!isReplyAdded(side, message, question)) side.reflow.add(side.parentSessionId, side.title, message.text, question, `${side.sessionId}:${message.seq ?? message.key}`);
|
|
3863
3896
|
}
|
|
3864
3897
|
function ReflowAction(props) {
|
|
@@ -4042,15 +4075,11 @@ ${body}
|
|
|
4042
4075
|
const accept = (0, react.useCallback)(() => {
|
|
4043
4076
|
if (sending.current) setAccepted(sending.current);
|
|
4044
4077
|
}, []);
|
|
4045
|
-
const saved = (0, react.useMemo)(() => boundarySeq === void 0 ? void 0 : (legacy?.nodes ?? [])
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
...prefix,
|
|
4051
|
-
content: node.content
|
|
4052
|
-
}] : [];
|
|
4053
|
-
})[0], [legacy, boundarySeq]) ?? accepted;
|
|
4078
|
+
const saved = (0, react.useMemo)(() => boundarySeq === void 0 ? void 0 : savedContextOf(legacy?.nodes ?? [], childWindow?.entries ?? [], childWindow?.hasMore, boundarySeq), [
|
|
4079
|
+
legacy,
|
|
4080
|
+
childWindow,
|
|
4081
|
+
boundarySeq
|
|
4082
|
+
]) ?? accepted;
|
|
4054
4083
|
const preview = forkedMidTurn && !saved;
|
|
4055
4084
|
const parent = preview ? ctx.sessions.binding(parentSessionId)?.session : void 0;
|
|
4056
4085
|
const parentChat = (0, react.useMemo)(() => preview ? chatSourceOf(ctx, ctx.sessions.binding(parentSessionId)) : void 0, [
|
|
@@ -4318,6 +4347,16 @@ ${body}
|
|
|
4318
4347
|
function storage() {
|
|
4319
4348
|
return typeof localStorage === "undefined" ? null : localStorage;
|
|
4320
4349
|
}
|
|
4350
|
+
function windowStorage() {
|
|
4351
|
+
return typeof sessionStorage === "undefined" ? null : sessionStorage;
|
|
4352
|
+
}
|
|
4353
|
+
function readRecord(store, key) {
|
|
4354
|
+
try {
|
|
4355
|
+
return revive(JSON.parse(store?.getItem(key) ?? "null")) ?? void 0;
|
|
4356
|
+
} catch {
|
|
4357
|
+
return;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4321
4360
|
let version = 0;
|
|
4322
4361
|
const listeners$1 = /* @__PURE__ */ new Set();
|
|
4323
4362
|
function notify() {
|
|
@@ -4339,32 +4378,31 @@ ${body}
|
|
|
4339
4378
|
notify();
|
|
4340
4379
|
}
|
|
4341
4380
|
function readSideChatMeta(sessionId, tabId) {
|
|
4342
|
-
const
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
const raw = store.getItem(keyOf(sessionId, tabId));
|
|
4346
|
-
if (raw === null) return void 0;
|
|
4347
|
-
return revive(JSON.parse(raw)) ?? void 0;
|
|
4348
|
-
} catch {
|
|
4349
|
-
return;
|
|
4350
|
-
}
|
|
4381
|
+
const key = keyOf(sessionId, tabId);
|
|
4382
|
+
const record = readRecord(windowStorage(), key) ?? readRecord(storage(), key);
|
|
4383
|
+
return record?.runId === SIDENOTE_RUN_ID ? record : void 0;
|
|
4351
4384
|
}
|
|
4352
4385
|
/** 写/合并一个 tab 的 meta(整体替换语义由调用方组合:先读再写)。 */
|
|
4353
4386
|
function writeSideChatMeta(record, options) {
|
|
4354
|
-
const
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4387
|
+
const value = JSON.stringify({
|
|
4388
|
+
...record,
|
|
4389
|
+
runId: record.runId ?? SIDENOTE_RUN_ID
|
|
4390
|
+
});
|
|
4391
|
+
for (const store of [windowStorage(), storage()]) try {
|
|
4392
|
+
store?.setItem(keyOf(record.sessionId, record.tabId), value);
|
|
4359
4393
|
} catch {}
|
|
4394
|
+
if (options?.silent !== true) notify();
|
|
4360
4395
|
}
|
|
4361
4396
|
function dropSideChatMeta(sessionId, tabId) {
|
|
4362
|
-
const
|
|
4363
|
-
if (store === null) return;
|
|
4397
|
+
const key = keyOf(sessionId, tabId);
|
|
4364
4398
|
try {
|
|
4365
|
-
|
|
4366
|
-
|
|
4399
|
+
windowStorage()?.removeItem(key);
|
|
4400
|
+
} catch {}
|
|
4401
|
+
try {
|
|
4402
|
+
const store = storage();
|
|
4403
|
+
if (readRecord(store, key)?.runId === SIDENOTE_RUN_ID) store?.removeItem(key);
|
|
4367
4404
|
} catch {}
|
|
4405
|
+
notify();
|
|
4368
4406
|
}
|
|
4369
4407
|
/** 枚举一个主会话的全部 meta 记录(按创建序升序)。 */
|
|
4370
4408
|
function sideChatMetasOf(sessionId) {
|
|
@@ -4372,20 +4410,20 @@ ${body}
|
|
|
4372
4410
|
}
|
|
4373
4411
|
/** 枚举全部存活记录(跨会话;完成通知等全局监听用)。 */
|
|
4374
4412
|
function sideChatMetasAll() {
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4413
|
+
return storedSideChatMetas().filter((record) => record.runId === SIDENOTE_RUN_ID);
|
|
4414
|
+
}
|
|
4415
|
+
/** Durable recovery records; a window's own copy wins over another window. */
|
|
4416
|
+
function storedSideChatMetas() {
|
|
4417
|
+
const records = /* @__PURE__ */ new Map();
|
|
4418
|
+
for (const store of [storage(), windowStorage()]) try {
|
|
4419
|
+
for (let i = 0; store && i < store.length; i++) {
|
|
4380
4420
|
const key = store.key(i);
|
|
4381
|
-
if (
|
|
4382
|
-
const record =
|
|
4383
|
-
if (record
|
|
4421
|
+
if (!key?.startsWith(KEY_PREFIX$1)) continue;
|
|
4422
|
+
const record = readRecord(store, key);
|
|
4423
|
+
if (record) records.set(key, record);
|
|
4384
4424
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
return [];
|
|
4388
|
-
}
|
|
4425
|
+
} catch {}
|
|
4426
|
+
return [...records.values()];
|
|
4389
4427
|
}
|
|
4390
4428
|
/**
|
|
4391
4429
|
* 下一个实例编号:当前会话存活记录的最大编号 + 1。
|
|
@@ -4397,30 +4435,12 @@ ${body}
|
|
|
4397
4435
|
return existing.length === 0 ? 1 : Math.max(...existing.map((r) => r.number)) + 1;
|
|
4398
4436
|
}
|
|
4399
4437
|
/**
|
|
4400
|
-
*
|
|
4401
|
-
* 残留)。插件 apply 时调用一次;撤掉活身份,等待实际恢复记录认领。
|
|
4402
|
-
* 返回清扫数。
|
|
4438
|
+
* 启动时收集恢复候选;持久记录不作为存活证明,也不跨窗口删除。
|
|
4403
4439
|
*/
|
|
4404
4440
|
function sweepOrphanedSideChatMeta() {
|
|
4405
|
-
|
|
4406
|
-
if (
|
|
4407
|
-
|
|
4408
|
-
const staleKeys = [];
|
|
4409
|
-
for (let i = 0; i < store.length; i += 1) {
|
|
4410
|
-
const key = store.key(i);
|
|
4411
|
-
if (typeof key !== "string" || !key.startsWith(KEY_PREFIX$1)) continue;
|
|
4412
|
-
const record = revive(JSON.parse(store.getItem(key) ?? "null"));
|
|
4413
|
-
if (record === null || record.runId !== SIDENOTE_RUN_ID) {
|
|
4414
|
-
if (record !== null) restorationCandidates.set(key, record);
|
|
4415
|
-
staleKeys.push(key);
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
for (const key of staleKeys) store.removeItem(key);
|
|
4419
|
-
if (staleKeys.length > 0) notify();
|
|
4420
|
-
return staleKeys.length;
|
|
4421
|
-
} catch {
|
|
4422
|
-
return 0;
|
|
4423
|
-
}
|
|
4441
|
+
restorationCandidates.clear();
|
|
4442
|
+
for (const record of storedSideChatMetas()) if (record.runId !== SIDENOTE_RUN_ID) restorationCandidates.set(keyOf(record.sessionId, record.tabId), record);
|
|
4443
|
+
return restorationCandidates.size;
|
|
4424
4444
|
}
|
|
4425
4445
|
//#endregion
|
|
4426
4446
|
//#region src/client/sidechat/identity.ts
|
|
@@ -5279,10 +5299,19 @@ ${body}
|
|
|
5279
5299
|
}, [session]);
|
|
5280
5300
|
const context = useMainContext(ctx, meta.parentSessionId ?? scope.sessionId, childId, session, meta.snapshotOnly === true, meta.forkedMidTurn === true);
|
|
5281
5301
|
const { boundarySeq, messages, history, progress, loadContext, loadImage, childChat: chat } = context;
|
|
5302
|
+
const [boundaryError, setBoundaryError] = (0, react.useState)();
|
|
5282
5303
|
(0, react.useEffect)(() => {
|
|
5283
|
-
if (session
|
|
5284
|
-
|
|
5304
|
+
if (!session || meta.snapshotOnly || boundarySeq !== void 0) {
|
|
5305
|
+
setBoundaryError(void 0);
|
|
5306
|
+
return;
|
|
5307
|
+
}
|
|
5308
|
+
let active = true;
|
|
5309
|
+
loadForkBoundary(session).catch((error) => {
|
|
5310
|
+
if (active) setBoundaryError(String(error instanceof Error ? error.message : error));
|
|
5285
5311
|
});
|
|
5312
|
+
return () => {
|
|
5313
|
+
active = false;
|
|
5314
|
+
};
|
|
5286
5315
|
}, [
|
|
5287
5316
|
session,
|
|
5288
5317
|
meta.snapshotOnly,
|
|
@@ -5327,7 +5356,7 @@ ${body}
|
|
|
5327
5356
|
const composer = useComposer(ctx, session, childId);
|
|
5328
5357
|
(0, react.useEffect)(() => childId === void 0 ? void 0 : registerSubmissionExtras(childId, extras), [childId, extras]);
|
|
5329
5358
|
(0, react.useEffect)(() => {
|
|
5330
|
-
if (meta.topic !== void 0) return;
|
|
5359
|
+
if (meta.topic !== void 0 || boundarySeq === void 0) return;
|
|
5331
5360
|
const first = messages.find((message) => message.role === "user" && (message.seq ?? -1) > (boundarySeq ?? -1));
|
|
5332
5361
|
if (first) extras.onSubmitText(first.text);
|
|
5333
5362
|
}, [
|
|
@@ -5480,6 +5509,7 @@ ${body}
|
|
|
5480
5509
|
loadImage,
|
|
5481
5510
|
progress,
|
|
5482
5511
|
loadContext,
|
|
5512
|
+
loadError: boundaryError,
|
|
5483
5513
|
loadProgressImage: context.loadProgressImage,
|
|
5484
5514
|
contextPending: context.contextPending
|
|
5485
5515
|
};
|
|
@@ -5510,6 +5540,7 @@ ${body}
|
|
|
5510
5540
|
type: "button",
|
|
5511
5541
|
className: sidechat_module_css_default.nativeAction,
|
|
5512
5542
|
disabled: running || replies.length === 0 || allAdded,
|
|
5543
|
+
title: t(boundarySeq === void 0 ? "reflowPending" : "reflowAllTitle"),
|
|
5513
5544
|
"aria-pressed": allAdded,
|
|
5514
5545
|
onClick: () => {
|
|
5515
5546
|
for (const reply of replies) addReply(side, reply, questions.get(reply.key));
|
|
@@ -5684,6 +5715,11 @@ ${body}
|
|
|
5684
5715
|
title: titleOf(sessionId, nativeTab.id),
|
|
5685
5716
|
...meta === void 0 ? {} : { meta }
|
|
5686
5717
|
};
|
|
5718
|
+
if (isRestoredNativeTab(nativeTab) && !meta.childId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5719
|
+
role: "alert",
|
|
5720
|
+
className: sidechat_module_css_default.errorRow,
|
|
5721
|
+
children: t("sideRestoreMissing")
|
|
5722
|
+
});
|
|
5687
5723
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5688
5724
|
className: sidechat_module_css_default.nativeHost,
|
|
5689
5725
|
"data-dsh-sidenote-native-host": "",
|
|
@@ -6612,34 +6648,34 @@ ${body}
|
|
|
6612
6648
|
document.head.appendChild(tag);
|
|
6613
6649
|
}
|
|
6614
6650
|
var annotate_module_css_default = {
|
|
6615
|
-
"
|
|
6616
|
-
"
|
|
6617
|
-
"
|
|
6618
|
-
"editorNew": "_SVViG_editorNew",
|
|
6619
|
-
"toolbar": "_SVViG_toolbar",
|
|
6620
|
-
"editorTarget": "_SVViG_editorTarget",
|
|
6621
|
-
"hint": "_SVViG_hint",
|
|
6622
|
-
"sentCardQuote": "_SVViG_sentCardQuote",
|
|
6623
|
-
"confirmButton": "_SVViG_confirmButton",
|
|
6624
|
-
"saveButton": "_SVViG_saveButton",
|
|
6651
|
+
"toolbarBelow": "_SVViG_toolbarBelow",
|
|
6652
|
+
"editorSpacer": "_SVViG_editorSpacer",
|
|
6653
|
+
"badgeSent": "_SVViG_badgeSent",
|
|
6625
6654
|
"sentChip": "_SVViG_sentChip",
|
|
6655
|
+
"sentCardTitle": "_SVViG_sentCardTitle",
|
|
6656
|
+
"sentCardQuote": "_SVViG_sentCardQuote",
|
|
6657
|
+
"hint": "_SVViG_hint",
|
|
6626
6658
|
"dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
|
|
6659
|
+
"editorAskRow": "_SVViG_editorAskRow",
|
|
6627
6660
|
"hintClose": "_SVViG_hintClose",
|
|
6661
|
+
"toolbar": "_SVViG_toolbar",
|
|
6662
|
+
"badge": "_SVViG_badge",
|
|
6663
|
+
"deleteButton": "_SVViG_deleteButton",
|
|
6664
|
+
"sentChipRow": "_SVViG_sentChipRow",
|
|
6665
|
+
"saveButton": "_SVViG_saveButton",
|
|
6666
|
+
"sentCardNote": "_SVViG_sentCardNote",
|
|
6667
|
+
"toolbarButton": "_SVViG_toolbarButton",
|
|
6668
|
+
"editorNew": "_SVViG_editorNew",
|
|
6628
6669
|
"editorEdit": "_SVViG_editorEdit",
|
|
6670
|
+
"editorTextarea": "_SVViG_editorTextarea",
|
|
6629
6671
|
"editorFooter": "_SVViG_editorFooter",
|
|
6630
|
-
"toolbarBelow": "_SVViG_toolbarBelow",
|
|
6631
6672
|
"highlight": "_SVViG_highlight",
|
|
6632
|
-
"editorTextarea": "_SVViG_editorTextarea",
|
|
6633
|
-
"sentChipRow": "_SVViG_sentChipRow",
|
|
6634
|
-
"cancelButton": "_SVViG_cancelButton",
|
|
6635
6673
|
"editorAsk": "_SVViG_editorAsk",
|
|
6636
|
-
"deleteButton": "_SVViG_deleteButton",
|
|
6637
6674
|
"editorInput": "_SVViG_editorInput",
|
|
6638
|
-
"
|
|
6639
|
-
"
|
|
6640
|
-
"
|
|
6641
|
-
"
|
|
6642
|
-
"sentCardNote": "_SVViG_sentCardNote"
|
|
6675
|
+
"confirmButton": "_SVViG_confirmButton",
|
|
6676
|
+
"editorTarget": "_SVViG_editorTarget",
|
|
6677
|
+
"cancelButton": "_SVViG_cancelButton",
|
|
6678
|
+
"editorError": "_SVViG_editorError"
|
|
6643
6679
|
};
|
|
6644
6680
|
//#endregion
|
|
6645
6681
|
//#region src/client/annotate/hint.tsx
|
|
@@ -8182,7 +8218,7 @@ ${body}
|
|
|
8182
8218
|
/** Run before orphan-meta cleanup so previous live/closed children can migrate. */
|
|
8183
8219
|
function registerSideSessionVisibility(ctx) {
|
|
8184
8220
|
const visibility = sessionVisibilityOf(ctx);
|
|
8185
|
-
const ids = [...
|
|
8221
|
+
const ids = [...storedSideChatMetas(), ...allClosedSideChats()].flatMap((record) => record.childId ? [record.childId] : []);
|
|
8186
8222
|
visibility.hide(...ids);
|
|
8187
8223
|
ctx.effect(() => visibility.watch(), "dsh-sidenote: side session visibility");
|
|
8188
8224
|
registerSessionListFilter(ctx, visibility);
|
|
@@ -3,6 +3,7 @@ export interface ContextEvent {
|
|
|
3
3
|
type: string;
|
|
4
4
|
seq: number;
|
|
5
5
|
data: {
|
|
6
|
+
inherited?: boolean;
|
|
6
7
|
turn?: number | null;
|
|
7
8
|
commandId?: string;
|
|
8
9
|
sourceCommandId?: string;
|
|
@@ -42,8 +43,18 @@ export declare function contextEventSource(session: SessionFace | undefined): Ob
|
|
|
42
43
|
* its opening lock while it is unfinished; the first-send delta can then carry
|
|
43
44
|
* a checkpoint that commits later. Automatic compaction stays in the busy turn. */
|
|
44
45
|
export declare function unfinishedCompactionCut(session: SessionFace | undefined): number | undefined;
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
46
|
+
/** Restore also appends end-seed, but only inherited=true records a fork cut.
|
|
47
|
+
* A saved copy resolves the cut before its original first-send snapshot. */
|
|
48
|
+
export declare function inheritedBoundaryOf(entries: readonly {
|
|
49
|
+
event: {
|
|
50
|
+
type: string;
|
|
51
|
+
seq: number;
|
|
52
|
+
data?: {
|
|
53
|
+
inherited?: boolean;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}[], beforeSeq?: number): number | undefined;
|
|
57
|
+
/** The loaded window can lack the inherited marker; undefined means pending. */
|
|
47
58
|
export declare function inheritedBoundarySource(session: SessionFace | undefined): ObservableSnapshot<number | undefined> | undefined;
|
|
48
59
|
/** Reuse the native contiguous window, but verify coverage: the host swallows
|
|
49
60
|
* page failures and a resolved loadThrough alone does not establish success. */
|
|
@@ -12,6 +12,7 @@ export interface InheritedContextData {
|
|
|
12
12
|
takenAt?: string | null;
|
|
13
13
|
};
|
|
14
14
|
loadContext?: () => Promise<void>;
|
|
15
|
+
loadError?: string;
|
|
15
16
|
}
|
|
16
17
|
/** Side and saved sessions share the same reader and its local scroll owner. */
|
|
17
18
|
export declare function InheritedContext({ context }: {
|
|
@@ -18,7 +18,7 @@ export interface ConversationRendererProps {
|
|
|
18
18
|
export type RenderConversation = (reference: SessionReference, side: SideConversation) => ReactNode;
|
|
19
19
|
export declare function renderSideConversation(props: ConversationRendererProps, reference: SessionReference, side: SideConversation): ReactNode;
|
|
20
20
|
/** Only completed, fork-local final answers are eligible for reflow. */
|
|
21
|
-
export declare function finalReplies(messages: readonly ChatMessage[], boundarySeq
|
|
21
|
+
export declare function finalReplies(messages: readonly ChatMessage[], boundarySeq: number | undefined, completed?: ReadonlySet<number>): ChatMessage[];
|
|
22
22
|
export declare function isReplyAdded(side: SideConversation, message: ChatMessage, question?: string): boolean;
|
|
23
23
|
export declare function addReply(side: SideConversation, message: ChatMessage, question?: string): void;
|
|
24
24
|
export declare function registerNativeConversation(ctx: Context): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SnapshotEntry } from './snapshot.ts';
|
|
2
|
+
import { inheritedBoundaryOf } from '../host/chat.ts';
|
|
2
3
|
/** Read-only rows have event identities, independent of the native side flow. */
|
|
3
4
|
export interface ContextEntry extends SnapshotEntry {
|
|
4
5
|
key: string;
|
|
@@ -8,14 +9,9 @@ export interface ContextEntry extends SnapshotEntry {
|
|
|
8
9
|
export declare function contextEntriesOf(nodes: readonly unknown[]): ContextEntry[];
|
|
9
10
|
/** A committed checkpoint replaces the inherited prefix, not its surviving tail. */
|
|
10
11
|
export declare function contextHistory(history: readonly ContextEntry[], progress: readonly SnapshotEntry[]): readonly ContextEntry[];
|
|
11
|
-
/** Saving
|
|
12
|
-
*
|
|
13
|
-
export declare function savedContextOf(nodes: readonly unknown[], events:
|
|
14
|
-
event: {
|
|
15
|
-
type: string;
|
|
16
|
-
seq: number;
|
|
17
|
-
};
|
|
18
|
-
}[], hasMore?: boolean): {
|
|
12
|
+
/** Saving preserves the latest side chat's reading cut. An active side chat
|
|
13
|
+
* freezes its first own snapshot after afterSeq. Restore markers never establish ownership. */
|
|
14
|
+
export declare function savedContextOf(nodes: readonly unknown[], events: Parameters<typeof inheritedBoundaryOf>[0], hasMore?: boolean, afterSeq?: number): {
|
|
19
15
|
boundarySeq: number;
|
|
20
16
|
content: readonly {
|
|
21
17
|
type?: string;
|
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* 孤键清扫:不能用旧页面键判断 tab 仍存活,而
|
|
15
15
|
* 「有记录 ⇒ tab 存活」是编排层的判定——残留记录会把入口折叠成聚焦一个
|
|
16
16
|
* 不存在的 tab(死锁,对抗性审查 B2)。每条记录带本页面生命周期的
|
|
17
|
-
* runId
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* 活记录(其面板 reconcile 幂等重建,childId 丢失退回首开 fork)——
|
|
21
|
-
* 多窗口为边角场景,记录在案。
|
|
17
|
+
* runId,只有本页面认领的记录才算存活。sessionStorage 保留本窗口身份,
|
|
18
|
+
* localStorage 提供跨窗口关闭后的恢复副本;启动时只收集恢复候选,
|
|
19
|
+
* 不删除其他窗口的记录。仅 restored tab(revision 0)可认领旧身份。
|
|
22
20
|
*/
|
|
23
21
|
export interface SideChatMetaRecord {
|
|
24
22
|
readonly tabId: string;
|
|
@@ -68,6 +66,8 @@ export declare function dropSideChatMeta(sessionId: string, tabId: string): void
|
|
|
68
66
|
export declare function sideChatMetasOf(sessionId: string): SideChatMetaRecord[];
|
|
69
67
|
/** 枚举全部存活记录(跨会话;完成通知等全局监听用)。 */
|
|
70
68
|
export declare function sideChatMetasAll(): SideChatMetaRecord[];
|
|
69
|
+
/** Durable recovery records; a window's own copy wins over another window. */
|
|
70
|
+
export declare function storedSideChatMetas(): SideChatMetaRecord[];
|
|
71
71
|
/**
|
|
72
72
|
* 下一个实例编号:当前会话存活记录的最大编号 + 1。
|
|
73
73
|
* 单实例期调用方保证同会话同时至多一条存活记录(held 规则),
|
|
@@ -75,8 +75,6 @@ export declare function sideChatMetasAll(): SideChatMetaRecord[];
|
|
|
75
75
|
*/
|
|
76
76
|
export declare function nextSideChatNumber(sessionId: string): number;
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
* 残留)。插件 apply 时调用一次;撤掉活身份,等待实际恢复记录认领。
|
|
80
|
-
* 返回清扫数。
|
|
78
|
+
* 启动时收集恢复候选;持久记录不作为存活证明,也不跨窗口删除。
|
|
81
79
|
*/
|
|
82
80
|
export declare function sweepOrphanedSideChatMeta(): number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidenote",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Follow the tangent. Keep the thread. Annotate, explore on the side, and bring answers back to DeepSeek Harness. / 思路可以分岔,主线始终向前。划选注释、侧边追问,再把结论带回正在推进的任务。",
|
|
5
5
|
"icon": "docs/assets/logo.svg",
|
|
6
6
|
"type": "module",
|
package/src/client/host/chat.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Context, ConversationSnapshot, ObservableSnapshot, SessionBinding,
|
|
|
3
3
|
export interface ContextEvent {
|
|
4
4
|
type: string
|
|
5
5
|
seq: number
|
|
6
|
-
data: { turn?: number | null; commandId?: string; sourceCommandId?: string; source?: { kind?: string }; content?: readonly { type: string; text?: string }[]; message?: { content: readonly { type: string; text?: string }[] }; summary?: readonly { type: string; text?: string }[]; shadowedSeqs?: readonly number[] }
|
|
6
|
+
data: { inherited?: boolean; turn?: number | null; commandId?: string; sourceCommandId?: string; source?: { kind?: string }; content?: readonly { type: string; text?: string }[]; message?: { content: readonly { type: string; text?: string }[] }; summary?: readonly { type: string; text?: string }[]; shadowedSeqs?: readonly number[] }
|
|
7
7
|
surfaceOp?: 'append' | { op: 'replace'; startSeq: number; endSeq: number }
|
|
8
8
|
}
|
|
9
9
|
export interface ContextEventWindow { entries: readonly { event: ContextEvent }[]; hasMore?: boolean }
|
|
@@ -24,16 +24,19 @@ export function unfinishedCompactionCut(session: SessionFace | undefined): numbe
|
|
|
24
24
|
return (command?.seq ?? last.seq) - 1
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
27
|
+
/** Restore also appends end-seed, but only inherited=true records a fork cut.
|
|
28
|
+
* A saved copy resolves the cut before its original first-send snapshot. */
|
|
29
|
+
export function inheritedBoundaryOf(entries: readonly { event: { type: string; seq: number; data?: { inherited?: boolean } } }[], beforeSeq = Infinity): number | undefined {
|
|
30
|
+
const seed = entries.findLast(({ event }) => event.type === 'session/end-seed' && event.data?.inherited === true && event.seq < beforeSeq)
|
|
31
|
+
return seed === undefined ? undefined : seed.event.seq - 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The loaded window can lack the inherited marker; undefined means pending. */
|
|
29
35
|
export function inheritedBoundarySource(session: SessionFace | undefined): ObservableSnapshot<number | undefined> | undefined {
|
|
30
36
|
const source = contextEventSource(session)
|
|
31
37
|
return source && {
|
|
32
38
|
subscribe: notify => source.subscribe(notify),
|
|
33
|
-
getSnapshot: () =>
|
|
34
|
-
const seed = source.getSnapshot().entries.findLast(entry => entry.event.type === 'session/end-seed')
|
|
35
|
-
return seed === undefined ? undefined : seed.event.seq - 1
|
|
36
|
-
},
|
|
39
|
+
getSnapshot: () => inheritedBoundaryOf(source.getSnapshot().entries),
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
package/src/client/locales.ts
CHANGED
|
@@ -229,6 +229,8 @@ export const en = {
|
|
|
229
229
|
expandAll: 'Expand all',
|
|
230
230
|
jumpToLatest: '↓ Jump to latest',
|
|
231
231
|
reflowAll: 'Reflow all',
|
|
232
|
+
reflowPending: 'Confirming this side chat’s origin before bringing replies back',
|
|
233
|
+
sideRestoreMissing: 'The original side chat could not be restored. Reopen it from the recently closed list in /side.',
|
|
232
234
|
reflowAllTitle: 'Bring all Q&A pairs back to the main session',
|
|
233
235
|
reflowAllDone: 'Reflowed ✓',
|
|
234
236
|
promote: 'Save as session',
|
|
@@ -464,6 +466,8 @@ export const zh: Record<CopyKey, string> = {
|
|
|
464
466
|
expandAll: '全部展开',
|
|
465
467
|
jumpToLatest: '↓ 跳到最新',
|
|
466
468
|
reflowAll: '整段回流',
|
|
469
|
+
reflowPending: '正在确认侧聊来源,确认后可回流',
|
|
470
|
+
sideRestoreMissing: '未能恢复原侧聊。请从 /side 的最近关闭列表重新打开。',
|
|
467
471
|
reflowAllTitle: '把该侧聊全部问答对带回主会话(chip 可预览/逐条撤)',
|
|
468
472
|
reflowAllDone: '已回流 ✓',
|
|
469
473
|
promote: '保存为正式会话',
|
|
@@ -16,6 +16,7 @@ export interface InheritedContextData {
|
|
|
16
16
|
loadProgressImage?: (attachment: unknown) => Promise<string>
|
|
17
17
|
progress?: { entries: SnapshotEntry[]; takenAt?: string | null }
|
|
18
18
|
loadContext?: () => Promise<void>
|
|
19
|
+
loadError?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Side and saved sessions share the same reader and its local scroll owner. */
|
|
@@ -42,7 +43,8 @@ export function InheritedContext({ context }: { context: InheritedContextData })
|
|
|
42
43
|
<IconChevronDownOutline14 size={12} className={expanded ? css.contextChevronOpen : css.contextChevron} />
|
|
43
44
|
<span>{t('mainContext')}</span><span className={css.contextHint}>{context.contextPending ? t('contextAtSend') : context.progress?.takenAt ? t('contextAsOf', { time: context.progress.takenAt }) : t('contextInherited')}</span>
|
|
44
45
|
</button>
|
|
45
|
-
{expanded &&
|
|
46
|
+
{!expanded && context.loadError && <div className={css.errorRow} role="alert">{context.loadError}<button type="button" className={css.nativeAction} onClick={() => { fold.toggle('inherited') }}>{t('contextRetry')}</button></div>}
|
|
47
|
+
{expanded && <ContextProgress id={contextId} history={context.history} entries={context.progress?.entries ?? []} live={context.contextPending === true} loading={loading} error={error ?? (loading ? undefined : context.loadError)} onRetry={() => { setAttempt(value => value + 1) }} loadImage={context.loadImage} loadProgressImage={context.loadProgressImage} />}
|
|
46
48
|
</div>
|
|
47
49
|
</div>
|
|
48
50
|
}
|
|
@@ -41,7 +41,8 @@ function Content(props: ContentProps) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Only completed, fork-local final answers are eligible for reflow. */
|
|
44
|
-
export function finalReplies(messages: readonly ChatMessage[], boundarySeq
|
|
44
|
+
export function finalReplies(messages: readonly ChatMessage[], boundarySeq: number | undefined, completed?: ReadonlySet<number>): ChatMessage[] {
|
|
45
|
+
if (boundarySeq === undefined) return []
|
|
45
46
|
const replies: ChatMessage[] = []
|
|
46
47
|
let final: ChatMessage | undefined
|
|
47
48
|
const finish = (): void => { if (final && !final.streaming && !final.interrupted && final.text.trim() !== '' && (completed === undefined || (final.seq !== undefined && completed.has(final.seq)))) replies.push(final) }
|
|
@@ -59,6 +60,7 @@ export function isReplyAdded(side: SideConversation, message: ChatMessage, quest
|
|
|
59
60
|
return side.reflow.list(side.parentSessionId).some(item => item.sourceKey === key || (item.sourceKey === undefined && item.sideTitle === side.title && item.text === message.text && item.question === question))
|
|
60
61
|
}
|
|
61
62
|
export function addReply(side: SideConversation, message: ChatMessage, question?: string): void {
|
|
63
|
+
if (side.boundarySeq === undefined || message.seq === undefined || message.seq <= side.boundarySeq) return
|
|
62
64
|
if (!isReplyAdded(side, message, question)) {
|
|
63
65
|
side.reflow.add(side.parentSessionId, side.title, message.text, question, `${side.sessionId}:${message.seq ?? message.key}`)
|
|
64
66
|
}
|
|
@@ -77,9 +77,17 @@ export function SavedConversation({ ctx, Original, props }: { ctx: Context; Orig
|
|
|
77
77
|
useEffect(() => {
|
|
78
78
|
if (typeof markerSeq !== 'number' || saved) return
|
|
79
79
|
let active = true
|
|
80
|
-
void loadInheritedContext(session, markerSeq).
|
|
80
|
+
void loadInheritedContext(session, markerSeq).then(async () => {
|
|
81
|
+
// Legacy snapshots omit the cut. Their inherited marker can be on an
|
|
82
|
+
// earlier page than the indexed input; resolve it before classifying rows.
|
|
83
|
+
const window = source?.getSnapshot()
|
|
84
|
+
const nodes = chat?.getLegacy()?.nodes ?? []
|
|
85
|
+
const marker = nodes.find(value => (value as { seq: number }).seq === markerSeq) as { content?: readonly { type: string; text?: string }[] } | undefined
|
|
86
|
+
const prefix = splitSnapshotPrefix(marker?.content?.find(block => block.type === 'text')?.text ?? '')
|
|
87
|
+
if (prefix && prefix.boundarySeq === undefined && !savedContextOf(nodes, window?.entries ?? [], window?.hasMore) && window?.hasMore) await loadInheritedContext(session, 0)
|
|
88
|
+
}).catch(reason => { if (active) setError(String(reason instanceof Error ? reason.message : reason)) })
|
|
81
89
|
return () => { active = false }
|
|
82
|
-
}, [markerSeq, !!saved, session])
|
|
90
|
+
}, [markerSeq, !!saved, session, source, chat])
|
|
83
91
|
const progress = useMemo(() => saved ? { entries: snapshotEntriesFromText(saved.snapshot, saved.content), takenAt: saved.takenAt } : undefined, [saved])
|
|
84
92
|
const history = useMemo(() => contextHistory(contextEntriesOf(effectiveContextNodes(legacy?.nodes ?? [], window, saved?.boundarySeq ?? -1).nodes), progress?.entries ?? []), [legacy, window, saved, progress])
|
|
85
93
|
const loadImage = useMemo(() => contextImageLoader(ctx, props.sessionId), [ctx, props.sessionId])
|
|
@@ -57,8 +57,12 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore;
|
|
|
57
57
|
useEffect(() => { openSessionWindow(session) }, [session])
|
|
58
58
|
const context = useMainContext(ctx, meta.parentSessionId ?? scope.sessionId, childId, session, meta.snapshotOnly === true, meta.forkedMidTurn === true)
|
|
59
59
|
const { boundarySeq, messages, history, progress, loadContext, loadImage, childChat: chat } = context
|
|
60
|
+
const [boundaryError, setBoundaryError] = useState<string>()
|
|
60
61
|
useEffect(() => {
|
|
61
|
-
if (session
|
|
62
|
+
if (!session || meta.snapshotOnly || boundarySeq !== undefined) { setBoundaryError(undefined); return }
|
|
63
|
+
let active = true
|
|
64
|
+
void loadForkBoundary(session).catch(error => { if (active) setBoundaryError(String(error instanceof Error ? error.message : error)) })
|
|
65
|
+
return () => { active = false }
|
|
62
66
|
}, [session, meta.snapshotOnly, boundarySeq])
|
|
63
67
|
// Repair pre-fix tab metadata from the durable fork marker, without touching history.
|
|
64
68
|
useEffect(() => {
|
|
@@ -84,7 +88,7 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore;
|
|
|
84
88
|
const composer = useComposer(ctx, session, childId)
|
|
85
89
|
useEffect(() => childId === undefined ? undefined : registerSubmissionExtras(childId, extras), [childId, extras])
|
|
86
90
|
useEffect(() => {
|
|
87
|
-
if (meta.topic !== undefined) return
|
|
91
|
+
if (meta.topic !== undefined || boundarySeq === undefined) return
|
|
88
92
|
const first = messages.find(message => message.role === 'user' && (message.seq ?? -1) > (boundarySeq ?? -1))
|
|
89
93
|
if (first) extras.onSubmitText(first.text)
|
|
90
94
|
}, [messages, meta.topic, boundarySeq, extras])
|
|
@@ -145,7 +149,7 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore;
|
|
|
145
149
|
if (phase === 'fork-error') return <StateScreen title={t('forkErrorTitle')} detail={forkError ?? undefined} hint={t('forkErrorHint')} />
|
|
146
150
|
if (phase === 'missing') return <StateScreen title={t('missingTitle')} detail={t('missingDetail')} hint={t('missingHint')} />
|
|
147
151
|
if (phase !== 'chat' || !reference || !childId) return <StateScreen title={t('preparing')} />
|
|
148
|
-
const side: SideConversation = { sessionId: childId, parentSessionId: meta.parentSessionId ?? scope.sessionId, title: tab.title, boundarySeq, reflow: props.reflow, history, loadImage, progress, loadContext, loadProgressImage: context.loadProgressImage, contextPending: context.contextPending }
|
|
152
|
+
const side: SideConversation = { sessionId: childId, parentSessionId: meta.parentSessionId ?? scope.sessionId, title: tab.title, boundarySeq, reflow: props.reflow, history, loadImage, progress, loadContext, loadError: boundaryError, loadProgressImage: context.loadProgressImage, contextPending: context.contextPending }
|
|
149
153
|
const replies = finalReplies(messages, boundarySeq, new Set(chat?.getCompletedReplySeqs?.() ?? []))
|
|
150
154
|
const questions = pairQuestions(messages)
|
|
151
155
|
const allAdded = replies.length > 0 && replies.every(reply => isReplyAdded(side, reply, questions.get(reply.key)))
|
|
@@ -155,7 +159,7 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore;
|
|
|
155
159
|
<div className={css.nativeToolbar}>
|
|
156
160
|
<button type="button" className={css.nativeAction} onClick={() => { openSession(ctx, side.parentSessionId) }}>{t(parentPending ? 'parentPending' : parentSnapshot?.running ? 'parentRunning' : 'parentIdle')}</button>
|
|
157
161
|
<span className={css.nativeToolbarActions}>
|
|
158
|
-
<button type="button" className={css.nativeAction} disabled={running || replies.length === 0 || allAdded} aria-pressed={allAdded} onClick={() => { for (const reply of replies) addReply(side, reply, questions.get(reply.key)) }}>{t(allAdded ? 'reflowAllDone' : 'reflowAll')}</button>
|
|
162
|
+
<button type="button" className={css.nativeAction} disabled={running || replies.length === 0 || allAdded} title={t(boundarySeq === undefined ? 'reflowPending' : 'reflowAllTitle')} aria-pressed={allAdded} onClick={() => { for (const reply of replies) addReply(side, reply, questions.get(reply.key)) }}>{t(allAdded ? 'reflowAllDone' : 'reflowAll')}</button>
|
|
159
163
|
<button type="button" className={css.nativeAction} disabled={running || composer.pending || hasDraft || saving} title={t(running || composer.pending ? 'promoteBusy' : hasDraft ? 'promoteDraftBusy' : 'promoteTitle')} onClick={() => { void promote() }}>{t('promote')}</button>
|
|
160
164
|
</span>
|
|
161
165
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { snapshotEntriesOf, splitSnapshotPrefix, type SnapshotEntry } from './snapshot.ts'
|
|
2
|
+
import { inheritedBoundaryOf } from '../host/chat.ts'
|
|
2
3
|
|
|
3
4
|
/** Read-only rows have event identities, independent of the native side flow. */
|
|
4
5
|
export interface ContextEntry extends SnapshotEntry {
|
|
@@ -38,16 +39,19 @@ export function contextHistory(history: readonly ContextEntry[], progress: reado
|
|
|
38
39
|
return progress.some(entry => entry.kind === 'checkpoint') ? [] : history
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/** Saving
|
|
42
|
-
*
|
|
43
|
-
export function savedContextOf(nodes: readonly unknown[], events:
|
|
44
|
-
for (const value of [...nodes].reverse()) {
|
|
42
|
+
/** Saving preserves the latest side chat's reading cut. An active side chat
|
|
43
|
+
* freezes its first own snapshot after afterSeq. Restore markers never establish ownership. */
|
|
44
|
+
export function savedContextOf(nodes: readonly unknown[], events: Parameters<typeof inheritedBoundaryOf>[0], hasMore = false, afterSeq?: number) {
|
|
45
|
+
for (const value of afterSeq === undefined ? [...nodes].reverse() : nodes) {
|
|
45
46
|
const node = value as { kind: string; seq: number; content?: readonly { type?: string; text?: string; attachment?: unknown }[] }
|
|
46
|
-
if (node.kind !== 'user' && node.kind !== 'steering') continue
|
|
47
|
+
if (node.seq <= (afterSeq ?? -1) || node.kind !== 'user' && node.kind !== 'steering') continue
|
|
47
48
|
const saved = splitSnapshotPrefix(node.content?.find(block => block.type === 'text')?.text ?? '')
|
|
48
49
|
if (!saved) continue
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
let boundarySeq = saved.boundarySeq ?? inheritedBoundaryOf(events, node.seq)
|
|
51
|
+
if (boundarySeq === undefined) {
|
|
52
|
+
if (hasMore || events.some(({ event }) => event.type === 'session/end-seed' && event.seq < node.seq)) continue
|
|
53
|
+
boundarySeq = -1
|
|
54
|
+
}
|
|
51
55
|
if (!Number.isSafeInteger(boundarySeq) || boundarySeq < -1 || boundarySeq >= node.seq) continue
|
|
52
56
|
return { ...saved, boundarySeq, content: node.content }
|
|
53
57
|
}
|
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* 孤键清扫:不能用旧页面键判断 tab 仍存活,而
|
|
15
15
|
* 「有记录 ⇒ tab 存活」是编排层的判定——残留记录会把入口折叠成聚焦一个
|
|
16
16
|
* 不存在的 tab(死锁,对抗性审查 B2)。每条记录带本页面生命周期的
|
|
17
|
-
* runId
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* 活记录(其面板 reconcile 幂等重建,childId 丢失退回首开 fork)——
|
|
21
|
-
* 多窗口为边角场景,记录在案。
|
|
17
|
+
* runId,只有本页面认领的记录才算存活。sessionStorage 保留本窗口身份,
|
|
18
|
+
* localStorage 提供跨窗口关闭后的恢复副本;启动时只收集恢复候选,
|
|
19
|
+
* 不删除其他窗口的记录。仅 restored tab(revision 0)可认领旧身份。
|
|
22
20
|
*/
|
|
23
21
|
export interface SideChatMetaRecord {
|
|
24
22
|
readonly tabId: string
|
|
@@ -105,6 +103,15 @@ function storage(): Storage | null {
|
|
|
105
103
|
return typeof localStorage === 'undefined' ? null : localStorage
|
|
106
104
|
}
|
|
107
105
|
|
|
106
|
+
function windowStorage(): Storage | null {
|
|
107
|
+
return typeof sessionStorage === 'undefined' ? null : sessionStorage
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readRecord(store: Storage | null, key: string): SideChatMetaRecord | undefined {
|
|
111
|
+
try { return revive(JSON.parse(store?.getItem(key) ?? 'null')) ?? undefined }
|
|
112
|
+
catch { return undefined }
|
|
113
|
+
}
|
|
114
|
+
|
|
108
115
|
// ── 变更订阅(标题槽活渲染用;版本号单调递增即可,不区分键) ──────────────────
|
|
109
116
|
|
|
110
117
|
let version = 0
|
|
@@ -130,34 +137,29 @@ export function notifySideChatMetaListeners(): void {
|
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
export function readSideChatMeta(sessionId: string, tabId: string): SideChatMetaRecord | undefined {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const raw = store.getItem(keyOf(sessionId, tabId))
|
|
137
|
-
if (raw === null) return undefined
|
|
138
|
-
return revive(JSON.parse(raw)) ?? undefined
|
|
139
|
-
} catch {
|
|
140
|
-
return undefined
|
|
141
|
-
}
|
|
140
|
+
const key = keyOf(sessionId, tabId)
|
|
141
|
+
const record = readRecord(windowStorage(), key) ?? readRecord(storage(), key)
|
|
142
|
+
return record?.runId === SIDENOTE_RUN_ID ? record : undefined
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/** 写/合并一个 tab 的 meta(整体替换语义由调用方组合:先读再写)。 */
|
|
145
146
|
export function writeSideChatMeta(record: SideChatMetaRecord, options?: { silent?: boolean }): void {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
store
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
const value = JSON.stringify({ ...record, runId: record.runId ?? SIDENOTE_RUN_ID })
|
|
148
|
+
for (const store of [windowStorage(), storage()]) {
|
|
149
|
+
try { store?.setItem(keyOf(record.sessionId, record.tabId), value) }
|
|
150
|
+
catch { /* The other store can still preserve this identity. */ }
|
|
151
|
+
}
|
|
152
|
+
if (options?.silent !== true) notify()
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
export function dropSideChatMeta(sessionId: string, tabId: string): void {
|
|
155
|
-
const
|
|
156
|
-
|
|
156
|
+
const key = keyOf(sessionId, tabId)
|
|
157
|
+
try { windowStorage()?.removeItem(key) } catch { /* ignore */ }
|
|
157
158
|
try {
|
|
158
|
-
store
|
|
159
|
-
|
|
159
|
+
const store = storage()
|
|
160
|
+
if (readRecord(store, key)?.runId === SIDENOTE_RUN_ID) store?.removeItem(key)
|
|
160
161
|
} catch { /* ignore */ }
|
|
162
|
+
notify()
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
/** 枚举一个主会话的全部 meta 记录(按创建序升序)。 */
|
|
@@ -167,20 +169,23 @@ export function sideChatMetasOf(sessionId: string): SideChatMetaRecord[] {
|
|
|
167
169
|
|
|
168
170
|
/** 枚举全部存活记录(跨会话;完成通知等全局监听用)。 */
|
|
169
171
|
export function sideChatMetasAll(): SideChatMetaRecord[] {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
return storedSideChatMetas().filter(record => record.runId === SIDENOTE_RUN_ID)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Durable recovery records; a window's own copy wins over another window. */
|
|
176
|
+
export function storedSideChatMetas(): SideChatMetaRecord[] {
|
|
177
|
+
const records = new Map<string, SideChatMetaRecord>()
|
|
178
|
+
for (const store of [storage(), windowStorage()]) {
|
|
179
|
+
try {
|
|
180
|
+
for (let i = 0; store && i < store.length; i++) {
|
|
181
|
+
const key = store.key(i)
|
|
182
|
+
if (!key?.startsWith(KEY_PREFIX)) continue
|
|
183
|
+
const record = readRecord(store, key)
|
|
184
|
+
if (record) records.set(key, record)
|
|
185
|
+
}
|
|
186
|
+
} catch { /* Read the other store when one is unavailable. */ }
|
|
183
187
|
}
|
|
188
|
+
return [...records.values()]
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
/**
|
|
@@ -194,28 +199,12 @@ export function nextSideChatNumber(sessionId: string): number {
|
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
/**
|
|
197
|
-
*
|
|
198
|
-
* 残留)。插件 apply 时调用一次;撤掉活身份,等待实际恢复记录认领。
|
|
199
|
-
* 返回清扫数。
|
|
202
|
+
* 启动时收集恢复候选;持久记录不作为存活证明,也不跨窗口删除。
|
|
200
203
|
*/
|
|
201
204
|
export function sweepOrphanedSideChatMeta(): number {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const staleKeys: string[] = []
|
|
206
|
-
for (let i = 0; i < store.length; i += 1) {
|
|
207
|
-
const key = store.key(i)
|
|
208
|
-
if (typeof key !== 'string' || !key.startsWith(KEY_PREFIX)) continue
|
|
209
|
-
const record = revive(JSON.parse(store.getItem(key) ?? 'null'))
|
|
210
|
-
if (record === null || record.runId !== SIDENOTE_RUN_ID) {
|
|
211
|
-
if (record !== null) restorationCandidates.set(key, record)
|
|
212
|
-
staleKeys.push(key)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
for (const key of staleKeys) store.removeItem(key)
|
|
216
|
-
if (staleKeys.length > 0) notify()
|
|
217
|
-
return staleKeys.length
|
|
218
|
-
} catch {
|
|
219
|
-
return 0
|
|
205
|
+
restorationCandidates.clear()
|
|
206
|
+
for (const record of storedSideChatMetas()) {
|
|
207
|
+
if (record.runId !== SIDENOTE_RUN_ID) restorationCandidates.set(keyOf(record.sessionId, record.tabId), record)
|
|
220
208
|
}
|
|
209
|
+
return restorationCandidates.size
|
|
221
210
|
}
|
|
@@ -199,6 +199,8 @@ function NativeSideChatBodyInner(props: NativeBodyInjected & NativeTabFrameworkP
|
|
|
199
199
|
...(meta === undefined ? {} : { meta }),
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
if (isRestoredNativeTab(nativeTab) && !meta.childId) return <div role="alert" className={css.errorRow}>{t('sideRestoreMissing')}</div>
|
|
203
|
+
|
|
202
204
|
return (
|
|
203
205
|
<div className={css.nativeHost} data-dsh-sidenote-native-host="">
|
|
204
206
|
<SideChatPanel ctx={ctx} store={undefined} scope={scope} tab={tab} visible={nativeTab.visible} reflow={reflow} annotations={annotations} renderConversation={(reference, side) => renderSideConversation(props, reference, side)} quoteRequestId={parseOpenParams(nativeTab.navigation.params)?.quoteRequestId} />
|
|
@@ -3,7 +3,7 @@ import type { Context, SessionFace } from '../host/contracts.ts'
|
|
|
3
3
|
import { chatSourceOf, contextEventSource, inheritedBoundarySource, loadForkBoundary, loadInheritedContext } from '../host/chat.ts'
|
|
4
4
|
import { contextImageLoader } from '../host/attachments.ts'
|
|
5
5
|
import { transcriptOf } from '../chat/transcript.ts'
|
|
6
|
-
import { contextEntriesOf, contextHistory } from './context.ts'
|
|
6
|
+
import { contextEntriesOf, contextHistory, savedContextOf } from './context.ts'
|
|
7
7
|
import { snapshotImages, type ImageBlock } from '../../snapshotProtocol.ts'
|
|
8
8
|
import { effectiveContextNodes, loadMainlineSnapshot, snapshotEntriesFromText, snapshotEntriesOf, splitSnapshotPrefix } from './snapshot.ts'
|
|
9
9
|
|
|
@@ -34,12 +34,7 @@ export function useMainContext(ctx: Context, parentSessionId: string, childId: s
|
|
|
34
34
|
return { text, ...(attachments.length ? { images: { sessionId: parentSessionId, attachments } } : {}) }
|
|
35
35
|
}, [ctx, parentSessionId, childId, session, snapshotOnly])
|
|
36
36
|
const accept = useCallback(() => { if (sending.current) setAccepted(sending.current) }, [])
|
|
37
|
-
const saved = useMemo(() => boundarySeq === undefined ? undefined : (legacy?.nodes ?? [])
|
|
38
|
-
const node = value as { kind: string; seq: number; content?: readonly { type: string; text?: string; attachment?: unknown }[] }
|
|
39
|
-
if (!['user', 'steering'].includes(node.kind) || node.seq <= boundarySeq) return []
|
|
40
|
-
const prefix = splitSnapshotPrefix(node.content?.find(block => block.type === 'text')?.text ?? '')
|
|
41
|
-
return prefix ? [{ ...prefix, content: node.content }] : []
|
|
42
|
-
})[0], [legacy, boundarySeq]) ?? accepted
|
|
37
|
+
const saved = useMemo(() => boundarySeq === undefined ? undefined : savedContextOf(legacy?.nodes ?? [], childWindow?.entries ?? [], childWindow?.hasMore, boundarySeq), [legacy, childWindow, boundarySeq]) ?? accepted
|
|
43
38
|
const preview = forkedMidTurn && !saved
|
|
44
39
|
const parent = preview ? ctx.sessions.binding(parentSessionId)?.session : undefined
|
|
45
40
|
const parentChat = useMemo(() => preview ? chatSourceOf(ctx, ctx.sessions.binding(parentSessionId)) : undefined, [ctx, parentSessionId, parent, preview])
|
|
@@ -2,7 +2,7 @@ import { useId, useSyncExternalStore } from 'react'
|
|
|
2
2
|
import type { Context } from '../host/contracts.ts'
|
|
3
3
|
import { registerSessionListFilter } from '../host/sessionList.tsx'
|
|
4
4
|
import { sessionVisibilityOf, type SessionVisibility } from '../host/sessionVisibility.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { storedSideChatMetas } from './metaStore.ts'
|
|
6
6
|
import { allClosedSideChats } from './recentClosed.ts'
|
|
7
7
|
import { t } from '../locales.ts'
|
|
8
8
|
import { useLocaleTick } from '../locale-tick.ts'
|
|
@@ -23,7 +23,7 @@ function VisibilitySetting({ visibility }: { visibility: SessionVisibility }) {
|
|
|
23
23
|
/** Run before orphan-meta cleanup so previous live/closed children can migrate. */
|
|
24
24
|
export function registerSideSessionVisibility(ctx: Context): void {
|
|
25
25
|
const visibility = sessionVisibilityOf(ctx)
|
|
26
|
-
const ids = [...
|
|
26
|
+
const ids = [...storedSideChatMetas(), ...allClosedSideChats()].flatMap(record => record.childId ? [record.childId] : [])
|
|
27
27
|
visibility.hide(...ids)
|
|
28
28
|
ctx.effect(() => visibility.watch(), 'dsh-sidenote: side session visibility')
|
|
29
29
|
registerSessionListFilter(ctx, visibility)
|