dsh-sidenote 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,9 +78,12 @@ dsh plugin --profile web add dsh-sidenote
78
78
  |---|---|---|
79
79
  | 0.1.1-rc.x | ≥ 0.12.3 | ✅ CI 双档矩阵 |
80
80
  | 0.1.2-rc.x | ≥ 0.18.0 | ✅ CI 双档矩阵 |
81
+ | 0.1.5-rc.x | ≥ 0.19.0 | ✅ 沙箱实测(见下) |
81
82
 
82
83
  每周 canary 跟进宿主新版本;宿主面缺席时插件按能力降级(不崩页面)。
83
84
 
85
+ **native 右栏(better-sidebar ≥ 0.19)语义差异**:侧边聊天 tab 托管给 DSH 原生右侧边栏,受宿主「page kind 每 pane 单实例」规则约束——同一会话同时只开一个侧边聊天(再次打开/新建 = 聚焦既有 tab;关闭后可经 /side 重开恢复原会话)。多开诉求可用原生侧栏的分栏(每 pane 一个)。
86
+
84
87
  ## 设计要点
85
88
 
86
89
  - **真 fork,不压缩**:侧边会话是真实 DSH 会话(全量历史快照),与主会话能力对等;不是「摘要 + 一次性问答」。
package/lib/client.js CHANGED
@@ -36,17 +36,25 @@ window.__ModuleLoader__.load({
36
36
  /** Native tab id → live panel. Native layouts are memory-only, so is this. */
37
37
  const liveSideChats = /* @__PURE__ */ new Map();
38
38
  /**
39
+ * tabId → 首次注册序号(打开序的近似)。tab.id 跨重挂载稳定,重注册不改写
40
+ * ——遍历 Map 的插入序会随重挂漂移,「最近打开」只能锚在首次序号上。
41
+ */
42
+ const birthOrder = /* @__PURE__ */ new Map();
43
+ let birthSeq = 0;
44
+ /**
39
45
  * Publish a mounted panel so programmatic entry points can focus it and seed
40
46
  * its draft. Returns the disposer for the panel's effect cleanup.
41
47
  */
42
- function registerLiveSideChat(sessionId, tabId, seedDraft, readMeta) {
48
+ function registerLiveSideChat(sessionId, tabId, seedDraft, readMeta, readTitle) {
43
49
  const entry = {
44
50
  tabId,
45
51
  sessionId,
46
52
  seedDraft,
47
- readMeta
53
+ readMeta,
54
+ readTitle
48
55
  };
49
56
  liveSideChats.set(tabId, entry);
57
+ if (!birthOrder.has(tabId)) birthOrder.set(tabId, ++birthSeq);
50
58
  const opening = openings.get(sessionId);
51
59
  if (opening !== void 0) {
52
60
  openings.delete(sessionId);
@@ -76,14 +84,25 @@ window.__ModuleLoader__.load({
76
84
  }
77
85
  return mark;
78
86
  }
87
+ /** 一个会话的全部存活面板(按打开序升序;native 下枚举 side tab 的唯一数据源)。 */
88
+ function liveSideChatsOf(sessionId) {
89
+ return [...liveSideChats.values()].filter((entry) => entry.sessionId === sessionId).sort((a, b) => (birthOrder.get(a.tabId) ?? 0) - (birthOrder.get(b.tabId) ?? 0));
90
+ }
79
91
  /**
80
- * Most recently registered live panel of a session — the open-or-focus target
92
+ * Most recently OPENED live panel of a session — the open-or-focus target
81
93
  * when the legacy snapshot cannot see the tab (mirrors "last opened wins").
94
+ * 排序锚在首次注册序号上:面板重挂载不翻序(见 birthOrder)。
82
95
  */
83
96
  function lastLiveSideChat(sessionId) {
84
- let found;
85
- for (const entry of liveSideChats.values()) if (entry.sessionId === sessionId) found = entry;
86
- return found;
97
+ const all = liveSideChatsOf(sessionId);
98
+ return all[all.length - 1];
99
+ }
100
+ /** 插件卸载/重载时清场(apply 的 ctx.effect disposer 调用)。 */
101
+ function clearNativeRuntime() {
102
+ root = void 0;
103
+ liveSideChats.clear();
104
+ openings.clear();
105
+ birthOrder.clear();
87
106
  }
88
107
  /** Native tab shell for reads that go through the layout snapshot (`readTab`). */
89
108
  function nativeTabShell(tabId, type) {
@@ -851,6 +870,8 @@ window.__ModuleLoader__.load({
851
870
  if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
852
871
  const existing = collectSideTabs(snapshot.state);
853
872
  if (existing.length > 0) return existing[existing.length - 1].title;
873
+ const live = lastLiveSideChat(sessionId);
874
+ if (live !== void 0) return live.readTitle();
854
875
  return sideTabTitle([]);
855
876
  } catch {
856
877
  return;
@@ -867,6 +888,7 @@ window.__ModuleLoader__.load({
867
888
  const snapshot = ctx.betterSidebar.getSnapshot();
868
889
  if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
869
890
  if (!ctx.betterSidebar.isTabEnabled("dsh-sidenote:side")) return false;
891
+ if (nativeSidebarHost(ctx) && liveSideChatsOf(sessionId).length > 0) return false;
870
892
  const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
871
893
  const meta = {
872
894
  childId,
@@ -2159,92 +2181,92 @@ window.__ModuleLoader__.load({
2159
2181
  document.head.appendChild(tag);
2160
2182
  }
2161
2183
  var sidechat_module_css_default = {
2184
+ "pendingBar": "_5KEEsG_pendingBar",
2185
+ "composerCard": "_5KEEsG_composerCard",
2186
+ "attachRail": "_5KEEsG_attachRail",
2162
2187
  "reasoningBody": "_5KEEsG_reasoningBody",
2163
- "pendingTitle": "_5KEEsG_pendingTitle",
2164
- "todoItem": "_5KEEsG_todoItem",
2165
- "todoDot": "_5KEEsG_todoDot",
2166
- "composer": "_5KEEsG_composer",
2167
- "sendButton": "_5KEEsG_sendButton",
2168
- "userBubble": "_5KEEsG_userBubble",
2169
- "toolLocations": "_5KEEsG_toolLocations",
2170
- "pendingCard": "_5KEEsG_pendingCard",
2171
- "pendingQuestion": "_5KEEsG_pendingQuestion",
2172
- "errorRow": "_5KEEsG_errorRow",
2173
- "composerFoot": "_5KEEsG_composerFoot",
2174
- "sentChip": "_5KEEsG_sentChip",
2175
- "noticeRow": "_5KEEsG_noticeRow",
2176
- "stopButton": "_5KEEsG_stopButton",
2177
- "stateDetail": "_5KEEsG_stateDetail",
2178
- "flowRowError": "_5KEEsG_flowRowError",
2179
- "emptyTitle": "_5KEEsG_emptyTitle",
2180
- "slashGroupTitle": "_5KEEsG_slashGroupTitle",
2181
- "pendingCustom": "_5KEEsG_pendingCustom",
2182
- "pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
2183
- "attachButton": "_5KEEsG_attachButton",
2184
- "pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
2185
- "writeNotice": "_5KEEsG_writeNotice",
2186
- "input": "_5KEEsG_input",
2187
- "userRow": "_5KEEsG_userRow",
2188
- "sidechat-pulse": "_5KEEsG_sidechat-pulse",
2189
2188
  "empty": "_5KEEsG_empty",
2190
- "emptyIcon": "_5KEEsG_emptyIcon",
2191
- "toolCard": "_5KEEsG_toolCard",
2192
- "composerCard": "_5KEEsG_composerCard",
2193
- "toolError": "_5KEEsG_toolError",
2189
+ "dotRunning": "_5KEEsG_dotRunning",
2190
+ "toolBody": "_5KEEsG_toolBody",
2194
2191
  "attachImg": "_5KEEsG_attachImg",
2195
- "pendingQHeader": "_5KEEsG_pendingQHeader",
2196
- "pendingActions": "_5KEEsG_pendingActions",
2197
- "toolHead": "_5KEEsG_toolHead",
2192
+ "slashItem": "_5KEEsG_slashItem",
2193
+ "stateDetail": "_5KEEsG_stateDetail",
2198
2194
  "sentChipRow": "_5KEEsG_sentChipRow",
2199
- "pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
2200
- "dotPending": "_5KEEsG_dotPending",
2201
- "rowActions": "_5KEEsG_rowActions",
2202
- "transcript": "_5KEEsG_transcript",
2203
- "modelLabel": "_5KEEsG_modelLabel",
2204
- "flowRow": "_5KEEsG_flowRow",
2205
- "toolBodyText": "_5KEEsG_toolBodyText",
2195
+ "userBubble": "_5KEEsG_userBubble",
2196
+ "rowPreview": "_5KEEsG_rowPreview",
2197
+ "reflowButton": "_5KEEsG_reflowButton",
2198
+ "toolLocations": "_5KEEsG_toolLocations",
2206
2199
  "toolGenericBody": "_5KEEsG_toolGenericBody",
2207
- "composerChips": "_5KEEsG_composerChips",
2208
- "attachRail": "_5KEEsG_attachRail",
2209
- "pendingQText": "_5KEEsG_pendingQText",
2200
+ "jumpBottom": "_5KEEsG_jumpBottom",
2201
+ "streamingHint": "_5KEEsG_streamingHint",
2202
+ "pendingTitle": "_5KEEsG_pendingTitle",
2203
+ "inheritedBody": "_5KEEsG_inheritedBody",
2204
+ "pendingQHeader": "_5KEEsG_pendingQHeader",
2210
2205
  "pendingOption": "_5KEEsG_pendingOption",
2211
- "dotIdle": "_5KEEsG_dotIdle",
2206
+ "pendingOptionActive": "_5KEEsG_pendingOptionActive",
2207
+ "rowActions": "_5KEEsG_rowActions",
2212
2208
  "stateScreen": "_5KEEsG_stateScreen",
2213
- "attachRemove": "_5KEEsG_attachRemove",
2214
- "dotRunning": "_5KEEsG_dotRunning",
2209
+ "pendingActions": "_5KEEsG_pendingActions",
2210
+ "pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
2215
2211
  "actionGroup": "_5KEEsG_actionGroup",
2212
+ "toolHead": "_5KEEsG_toolHead",
2213
+ "toolBodyText": "_5KEEsG_toolBodyText",
2214
+ "sidechat-pulse": "_5KEEsG_sidechat-pulse",
2216
2215
  "assistantBody": "_5KEEsG_assistantBody",
2217
- "todoText": "_5KEEsG_todoText",
2218
- "pendingReason": "_5KEEsG_pendingReason",
2219
- "root": "_5KEEsG_root",
2220
- "pendingOptionActive": "_5KEEsG_pendingOptionActive",
2221
- "parentStrip": "_5KEEsG_parentStrip",
2222
- "toolRunning": "_5KEEsG_toolRunning",
2223
- "modelSelect": "_5KEEsG_modelSelect",
2224
- "pendingDesc": "_5KEEsG_pendingDesc",
2225
- "body": "_5KEEsG_body",
2226
- "densityToggle": "_5KEEsG_densityToggle",
2227
- "headerSide": "_5KEEsG_headerSide",
2228
- "reflowButton": "_5KEEsG_reflowButton",
2229
- "emptyText": "_5KEEsG_emptyText",
2230
- "densityRow": "_5KEEsG_densityRow",
2231
- "jumpBottom": "_5KEEsG_jumpBottom",
2232
- "inheritedBody": "_5KEEsG_inheritedBody",
2216
+ "todoDot": "_5KEEsG_todoDot",
2217
+ "todoBody": "_5KEEsG_todoBody",
2218
+ "toolError": "_5KEEsG_toolError",
2233
2219
  "reasoning": "_5KEEsG_reasoning",
2234
- "pendingBar": "_5KEEsG_pendingBar",
2235
- "toolBody": "_5KEEsG_toolBody",
2236
- "writeNoticeClose": "_5KEEsG_writeNoticeClose",
2220
+ "attachThumb": "_5KEEsG_attachThumb",
2221
+ "attachButton": "_5KEEsG_attachButton",
2222
+ "densityToggle": "_5KEEsG_densityToggle",
2223
+ "composerFoot": "_5KEEsG_composerFoot",
2224
+ "stopButton": "_5KEEsG_stopButton",
2237
2225
  "slashItemActive": "_5KEEsG_slashItemActive",
2238
2226
  "slashMenu": "_5KEEsG_slashMenu",
2239
- "todoBody": "_5KEEsG_todoBody",
2240
- "streamingHint": "_5KEEsG_streamingHint",
2241
- "rowPreview": "_5KEEsG_rowPreview",
2242
- "slashItem": "_5KEEsG_slashItem",
2227
+ "writeNotice": "_5KEEsG_writeNotice",
2228
+ "input": "_5KEEsG_input",
2229
+ "densityRow": "_5KEEsG_densityRow",
2230
+ "composerChips": "_5KEEsG_composerChips",
2231
+ "sentChip": "_5KEEsG_sentChip",
2232
+ "pendingQuestion": "_5KEEsG_pendingQuestion",
2233
+ "todoText": "_5KEEsG_todoText",
2234
+ "pendingReason": "_5KEEsG_pendingReason",
2235
+ "pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
2236
+ "dotIdle": "_5KEEsG_dotIdle",
2237
+ "assistantRow": "_5KEEsG_assistantRow",
2238
+ "flowRow": "_5KEEsG_flowRow",
2239
+ "userRow": "_5KEEsG_userRow",
2240
+ "toolCard": "_5KEEsG_toolCard",
2241
+ "errorRow": "_5KEEsG_errorRow",
2242
+ "noticeRow": "_5KEEsG_noticeRow",
2243
2243
  "slashItemDetail": "_5KEEsG_slashItemDetail",
2244
- "attachThumb": "_5KEEsG_attachThumb",
2244
+ "transcript": "_5KEEsG_transcript",
2245
+ "toolRunning": "_5KEEsG_toolRunning",
2246
+ "emptyIcon": "_5KEEsG_emptyIcon",
2247
+ "pendingCustom": "_5KEEsG_pendingCustom",
2248
+ "headerSide": "_5KEEsG_headerSide",
2249
+ "parentStrip": "_5KEEsG_parentStrip",
2250
+ "modelSelect": "_5KEEsG_modelSelect",
2245
2251
  "pendingHead": "_5KEEsG_pendingHead",
2252
+ "root": "_5KEEsG_root",
2253
+ "flowRowError": "_5KEEsG_flowRowError",
2254
+ "writeNoticeClose": "_5KEEsG_writeNoticeClose",
2255
+ "slashGroupTitle": "_5KEEsG_slashGroupTitle",
2256
+ "sendButton": "_5KEEsG_sendButton",
2246
2257
  "slashItemName": "_5KEEsG_slashItemName",
2247
- "assistantRow": "_5KEEsG_assistantRow"
2258
+ "body": "_5KEEsG_body",
2259
+ "emptyText": "_5KEEsG_emptyText",
2260
+ "composer": "_5KEEsG_composer",
2261
+ "emptyTitle": "_5KEEsG_emptyTitle",
2262
+ "attachRemove": "_5KEEsG_attachRemove",
2263
+ "dotPending": "_5KEEsG_dotPending",
2264
+ "todoItem": "_5KEEsG_todoItem",
2265
+ "pendingQText": "_5KEEsG_pendingQText",
2266
+ "pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
2267
+ "pendingDesc": "_5KEEsG_pendingDesc",
2268
+ "modelLabel": "_5KEEsG_modelLabel",
2269
+ "pendingCard": "_5KEEsG_pendingCard"
2248
2270
  };
2249
2271
  //#endregion
2250
2272
  //#region src/client/chat/ToolCard.tsx
@@ -3730,7 +3752,7 @@ window.__ModuleLoader__.load({
3730
3752
  requestAnimationFrame(() => {
3731
3753
  rootRef.current?.querySelector("textarea")?.focus();
3732
3754
  });
3733
- }, () => metaRef.current);
3755
+ }, () => metaRef.current, () => titleRef.current);
3734
3756
  return () => {
3735
3757
  dispose();
3736
3758
  if (nativeSidebarHost(ctx)) {
@@ -3973,18 +3995,28 @@ window.__ModuleLoader__.load({
3973
3995
  ui: {
3974
3996
  kind: "popupSelect",
3975
3997
  options: (session) => {
3976
- const options = [{
3998
+ const options = [];
3999
+ const native = nativeSidebarHost(ctx);
4000
+ const lives = native ? liveSideChatsOf(session.sessionId) : [];
4001
+ if (!native || lives.length === 0) options.push({
3977
4002
  id: "new",
3978
4003
  label: t("cmdNew"),
3979
4004
  detail: t("cmdNewDetail")
3980
- }];
3981
- const snapshot = ctx.betterSidebar.getSnapshot();
3982
- if (snapshot.sessionId === session.sessionId && snapshot.state !== void 0) for (const tab of collectSideTabs(snapshot.state)) options.push({
3983
- id: `focus:${tab.id}`,
3984
- label: t("cmdFocus", { title: tab.title }),
4005
+ });
4006
+ if (native) for (const live of lives) options.push({
4007
+ id: `focus:${live.tabId}`,
4008
+ label: t("cmdFocus", { title: live.readTitle() }),
3985
4009
  detail: t("cmdFocusDetail")
3986
4010
  });
3987
- for (const entry of listClosedSideChats(session.sessionId)) options.push({
4011
+ else {
4012
+ const snapshot = ctx.betterSidebar.getSnapshot();
4013
+ if (snapshot.sessionId === session.sessionId && snapshot.state !== void 0) for (const tab of collectSideTabs(snapshot.state)) options.push({
4014
+ id: `focus:${tab.id}`,
4015
+ label: t("cmdFocus", { title: tab.title }),
4016
+ detail: t("cmdFocusDetail")
4017
+ });
4018
+ }
4019
+ if (!(native && lives.length > 0)) for (const entry of listClosedSideChats(session.sessionId)) options.push({
3988
4020
  id: `reopen:${entry.childId}`,
3989
4021
  label: t("cmdReopen", { title: entry.title }),
3990
4022
  detail: t("cmdReopenDetail")
@@ -3997,7 +4029,8 @@ window.__ModuleLoader__.load({
3997
4029
  return;
3998
4030
  }
3999
4031
  if (option.id.startsWith("focus:")) {
4000
- ctx.betterSidebar.activateTab(option.id.slice(6), { sessionId: session.sessionId });
4032
+ const tabId = option.id.slice(6);
4033
+ if (!focusNativeTab(ctx, tabId)) ctx.betterSidebar.activateTab(tabId, { sessionId: session.sessionId });
4001
4034
  return;
4002
4035
  }
4003
4036
  if (option.id.startsWith("reopen:")) {
@@ -4183,11 +4216,18 @@ window.__ModuleLoader__.load({
4183
4216
  icon: (size) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size }),
4184
4217
  order: 60,
4185
4218
  available: (availableCtx, scope) => canForkFrom(availableCtx, scope.sessionId),
4186
- createTab: (state) => ({ tab: {
4187
- id: mintSideTabId(),
4188
- type: SIDE_TAB_TYPE,
4189
- title: sideTabTitle(collectSideTabs(state).map((tab) => tab.title))
4190
- } }),
4219
+ createTab: (state) => {
4220
+ const titles = collectSideTabs(state).map((tab) => tab.title);
4221
+ if (nativeSidebarHost(ctx)) {
4222
+ const current = ctx.sessions.list.getSnapshot().current;
4223
+ if (current !== void 0) for (const live of liveSideChatsOf(current)) titles.push(live.readTitle());
4224
+ }
4225
+ return { tab: {
4226
+ id: mintSideTabId(),
4227
+ type: SIDE_TAB_TYPE,
4228
+ title: sideTabTitle(titles)
4229
+ } };
4230
+ },
4191
4231
  onClose: (closedTab) => {
4192
4232
  const meta = parseSideChatMeta(closedTab.meta);
4193
4233
  if (meta.childId !== void 0 && meta.parentSessionId !== void 0) recordClosedSideChat(meta.parentSessionId, {
@@ -4669,43 +4709,43 @@ window.__ModuleLoader__.load({
4669
4709
  document.head.appendChild(tag);
4670
4710
  }
4671
4711
  var annotate_module_css_default = {
4672
- "editorNew": "_SVViG_editorNew",
4673
- "chipText": "_SVViG_chipText",
4674
- "toolbarBelow": "_SVViG_toolbarBelow",
4675
- "sentCardTitle": "_SVViG_sentCardTitle",
4712
+ "badgeSent": "_SVViG_badgeSent",
4676
4713
  "chipClearAll": "_SVViG_chipClearAll",
4677
- "chip": "_SVViG_chip",
4678
4714
  "highlight": "_SVViG_highlight",
4679
- "chipNote": "_SVViG_chipNote",
4680
4715
  "chipRemove": "_SVViG_chipRemove",
4681
- "editorEdit": "_SVViG_editorEdit",
4682
- "sentCardNote": "_SVViG_sentCardNote",
4683
- "editorFooter": "_SVViG_editorFooter",
4684
- "toolbar": "_SVViG_toolbar",
4685
4716
  "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
4717
+ "editorNew": "_SVViG_editorNew",
4718
+ "chipRow": "_SVViG_chipRow",
4719
+ "editorTextarea": "_SVViG_editorTextarea",
4720
+ "editorAsk": "_SVViG_editorAsk",
4721
+ "chipNote": "_SVViG_chipNote",
4722
+ "chip": "_SVViG_chip",
4723
+ "chipText": "_SVViG_chipText",
4724
+ "chipPanel": "_SVViG_chipPanel",
4725
+ "editorError": "_SVViG_editorError",
4726
+ "toolbarBelow": "_SVViG_toolbarBelow",
4686
4727
  "sentCardQuote": "_SVViG_sentCardQuote",
4687
- "editorInput": "_SVViG_editorInput",
4728
+ "hint": "_SVViG_hint",
4729
+ "sentChipRow": "_SVViG_sentChipRow",
4688
4730
  "deleteButton": "_SVViG_deleteButton",
4689
- "badge": "_SVViG_badge",
4731
+ "sentCardNote": "_SVViG_sentCardNote",
4690
4732
  "confirmButton": "_SVViG_confirmButton",
4691
- "saveButton": "_SVViG_saveButton",
4692
- "badgeSent": "_SVViG_badgeSent",
4693
- "sentChip": "_SVViG_sentChip",
4694
- "chipNumber": "_SVViG_chipNumber",
4695
- "hint": "_SVViG_hint",
4696
- "hintClose": "_SVViG_hintClose",
4697
- "editorTarget": "_SVViG_editorTarget",
4698
- "chipWrap": "_SVViG_chipWrap",
4699
4733
  "cancelButton": "_SVViG_cancelButton",
4700
- "editorError": "_SVViG_editorError",
4734
+ "editorFooter": "_SVViG_editorFooter",
4735
+ "sentCardTitle": "_SVViG_sentCardTitle",
4736
+ "badge": "_SVViG_badge",
4737
+ "editorTarget": "_SVViG_editorTarget",
4738
+ "chipNumber": "_SVViG_chipNumber",
4701
4739
  "editorAskRow": "_SVViG_editorAskRow",
4702
- "sentChipRow": "_SVViG_sentChipRow",
4703
- "editorTextarea": "_SVViG_editorTextarea",
4704
- "editorAsk": "_SVViG_editorAsk",
4705
- "chipRow": "_SVViG_chipRow",
4706
- "chipPanel": "_SVViG_chipPanel",
4740
+ "editorSpacer": "_SVViG_editorSpacer",
4741
+ "hintClose": "_SVViG_hintClose",
4742
+ "editorInput": "_SVViG_editorInput",
4743
+ "saveButton": "_SVViG_saveButton",
4707
4744
  "toolbarButton": "_SVViG_toolbarButton",
4708
- "editorSpacer": "_SVViG_editorSpacer"
4745
+ "editorEdit": "_SVViG_editorEdit",
4746
+ "chipWrap": "_SVViG_chipWrap",
4747
+ "sentChip": "_SVViG_sentChip",
4748
+ "toolbar": "_SVViG_toolbar"
4709
4749
  };
4710
4750
  //#endregion
4711
4751
  //#region src/client/annotate/hint.tsx
@@ -5817,7 +5857,12 @@ window.__ModuleLoader__.load({
5817
5857
  "locale"
5818
5858
  ];
5819
5859
  function apply(ctx) {
5820
- setRootContext(ctx);
5860
+ ctx.effect(() => {
5861
+ setRootContext(ctx);
5862
+ return () => {
5863
+ clearNativeRuntime();
5864
+ };
5865
+ }, "dsh-sidenote: native bridge");
5821
5866
  attachLocale(ctx.locale);
5822
5867
  probeHost(ctx);
5823
5868
  const reflow = createReflowStore();
@@ -38,12 +38,14 @@ interface LiveSideChat {
38
38
  seedDraft(text: string): void;
39
39
  /** Current tab meta, read at call time (the panel re-renders on meta change). */
40
40
  readMeta(): unknown;
41
+ /** Current tab title(聚焦列表/编号数据源的读取面)。 */
42
+ readTitle(): string;
41
43
  }
42
44
  /**
43
45
  * Publish a mounted panel so programmatic entry points can focus it and seed
44
46
  * its draft. Returns the disposer for the panel's effect cleanup.
45
47
  */
46
- export declare function registerLiveSideChat(sessionId: string, tabId: string, seedDraft: (text: string) => void, readMeta: () => unknown): () => void;
48
+ export declare function registerLiveSideChat(sessionId: string, tabId: string, seedDraft: (text: string) => void, readMeta: () => unknown, readTitle: () => string): () => void;
47
49
  interface OpeningMark {
48
50
  readonly at: number;
49
51
  /** 窗口内到达的待投递草稿(面板注册时经 seedDraft 回放)。 */
@@ -55,11 +57,16 @@ export declare function markSideChatOpening(sessionId: string): void;
55
57
  export declare function sideChatOpening(sessionId: string): OpeningMark | undefined;
56
58
  /** Live panel for a native tab id (undefined for legacy layout tabs). */
57
59
  export declare function liveSideChat(tabId: string): LiveSideChat | undefined;
60
+ /** 一个会话的全部存活面板(按打开序升序;native 下枚举 side tab 的唯一数据源)。 */
61
+ export declare function liveSideChatsOf(sessionId: string): LiveSideChat[];
58
62
  /**
59
- * Most recently registered live panel of a session — the open-or-focus target
63
+ * Most recently OPENED live panel of a session — the open-or-focus target
60
64
  * when the legacy snapshot cannot see the tab (mirrors "last opened wins").
65
+ * 排序锚在首次注册序号上:面板重挂载不翻序(见 birthOrder)。
61
66
  */
62
67
  export declare function lastLiveSideChat(sessionId: string): LiveSideChat | undefined;
68
+ /** 插件卸载/重载时清场(apply 的 ctx.effect disposer 调用)。 */
69
+ export declare function clearNativeRuntime(): void;
63
70
  /** Native tab shell for reads that go through the layout snapshot (`readTab`). */
64
71
  export declare function nativeTabShell(tabId: string, type: string): {
65
72
  id: string;
@@ -2,7 +2,9 @@
2
2
  * /side 斜杠命令(spike 落点):经 client 侧 `ctx.commandUi.register`
3
3
  * (dsh-client-ui-commands 的 CommandContribution,ui 形态 popupSelect)
4
4
  * 注册。popupSelect 是该注册面唯一的 UI 形态 —— 菜单行被选中后弹一个
5
- * 选项壳,选项 = 新建 + 当前会话已并存的侧边聊天(聚焦)。
5
+ * 选项壳,选项 = 新建 + 当前会话已并存的侧边聊天(聚焦)+ 最近关闭(重开)。
6
+ * native 宿主(better-sidebar >= 0.19)受 dsh sidebar-right 的 held 规则约束
7
+ * (page kind 每 pane 单实例):有存活实例时不列「新建」与「重开」。
6
8
  *
7
9
  * 服务经 ctx.get 惰性解析(commandUi 不在 inject 清单里):服务缺失、
8
10
  * 注册抛错(如与 host 命令撞名)都降级为「只有 Tab 入口」,绝不影响面板。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidenote",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "DSH web plugin: Codex-style side chat (fork the current session into a persistent side panel) + selection annotations with numbered badges and a composer chip. A thin consumer of dsh-better-sidebar. / Codex 风格侧边聊天 + 划选注释的 DSH 插件。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,14 +13,18 @@ import { attachLocale, type LocaleServiceLike } from './locales.ts'
13
13
  import { createReflowStore } from './reflow.ts'
14
14
  import { registerSideChat } from './sidechat/index.tsx'
15
15
  import { registerAnnotations } from './annotate/index.tsx'
16
- import { setRootContext } from './sidechat/native.ts'
16
+ import { clearNativeRuntime, setRootContext } from './sidechat/native.ts'
17
17
 
18
18
  export const inject = ['betterSidebar', 'sessions', 'workspaces', 'slots', 'connection', 'locale']
19
19
 
20
20
  export function apply(ctx: Context): void {
21
21
  // 侧栏 Tab 的原生面(better-sidebar >= 0.19)与宿主调用都要用插件根 ctx:
22
22
  // 面板拿到的是 slot 注入的收缩 ctx(inject 里没有 workspaces),见 native.ts。
23
- setRootContext(ctx)
23
+ // fiber 撤销即清场(registry/openings/birthOrder 全模块级,重激活不得带病存活)。
24
+ ctx.effect(() => {
25
+ setRootContext(ctx)
26
+ return () => { clearNativeRuntime() }
27
+ }, 'dsh-sidenote: native bridge')
24
28
  // 跟随 DSH 通用设置里的语言(locale.preference,Host-backed,实时切换)。
25
29
  attachLocale(ctx.locale as LocaleServiceLike | undefined)
26
30
  // T2 off-face 全景探测:宿主升级后哪些能力降级了,开发者工具里一眼可见。
@@ -222,6 +222,7 @@ export function SideChatPanel(props: TabComponentProps & { reflow: ReflowStore }
222
222
  requestAnimationFrame(() => { rootRef.current?.querySelector('textarea')?.focus() })
223
223
  },
224
224
  () => metaRef.current,
225
+ () => titleRef.current,
225
226
  )
226
227
  return () => {
227
228
  dispose()
@@ -23,6 +23,7 @@ import { SideChatPanel } from './SideChatPanel.tsx'
23
23
  import { SIDE_TAB_TYPE, canForkFrom, collectSideTabs, mintSideTabId, sideTabTitle } from './model.ts'
24
24
  import { parseSideChatMeta } from './model.ts'
25
25
  import { recordClosedSideChat } from './recentClosed.ts'
26
+ import { liveSideChatsOf, nativeSidebarHost } from './native.ts'
26
27
  import { openOrFocusSideChat, sideChatTargetTitle } from './open.ts'
27
28
  import { t } from '../locales.ts'
28
29
  import { registerHeaderEntry } from './header.tsx'
@@ -39,9 +40,18 @@ export function registerSideChat(ctx: Context, reflow: ReflowStore): void {
39
40
  icon: (size: number) => <IconNewChatOutline16 size={size} />,
40
41
  order: 60,
41
42
  available: (availableCtx, scope) => canForkFrom(availableCtx, scope.sessionId),
42
- createTab: (state) => ({
43
- tab: { id: mintSideTabId(), type: SIDE_TAB_TYPE, title: sideTabTitle(collectSideTabs(state).map(tab => tab.title)) },
44
- }),
43
+ createTab: (state) => {
44
+ // 标题编号数据源:legacy 读布局快照;native 的 tab 不进快照,并入
45
+ // live registry(当前会话——openTab 的会话守卫保证 state 属在屏会话)。
46
+ const titles = collectSideTabs(state).map(tab => tab.title)
47
+ if (nativeSidebarHost(ctx)) {
48
+ const current = ctx.sessions.list.getSnapshot().current
49
+ if (current !== undefined) {
50
+ for (const live of liveSideChatsOf(current)) titles.push(live.readTitle())
51
+ }
52
+ }
53
+ return { tab: { id: mintSideTabId(), type: SIDE_TAB_TYPE, title: sideTabTitle(titles) } }
54
+ },
45
55
  // × 即焚观感 + 后悔药:关 Tab 时登记「最近关闭」(会话本体仍归档在盘)。
46
56
  onClose: (closedTab) => {
47
57
  const meta = parseSideChatMeta(closedTab.meta)
@@ -58,11 +58,20 @@ interface LiveSideChat {
58
58
  seedDraft(text: string): void
59
59
  /** Current tab meta, read at call time (the panel re-renders on meta change). */
60
60
  readMeta(): unknown
61
+ /** Current tab title(聚焦列表/编号数据源的读取面)。 */
62
+ readTitle(): string
61
63
  }
62
64
 
63
65
  /** Native tab id → live panel. Native layouts are memory-only, so is this. */
64
66
  const liveSideChats = new Map<string, LiveSideChat>()
65
67
 
68
+ /**
69
+ * tabId → 首次注册序号(打开序的近似)。tab.id 跨重挂载稳定,重注册不改写
70
+ * ——遍历 Map 的插入序会随重挂漂移,「最近打开」只能锚在首次序号上。
71
+ */
72
+ const birthOrder = new Map<string, number>()
73
+ let birthSeq = 0
74
+
66
75
  /**
67
76
  * Publish a mounted panel so programmatic entry points can focus it and seed
68
77
  * its draft. Returns the disposer for the panel's effect cleanup.
@@ -72,9 +81,11 @@ export function registerLiveSideChat(
72
81
  tabId: string,
73
82
  seedDraft: (text: string) => void,
74
83
  readMeta: () => unknown,
84
+ readTitle: () => string,
75
85
  ): () => void {
76
- const entry: LiveSideChat = { tabId, sessionId, seedDraft, readMeta }
86
+ const entry: LiveSideChat = { tabId, sessionId, seedDraft, readMeta, readTitle }
77
87
  liveSideChats.set(tabId, entry)
88
+ if (!birthOrder.has(tabId)) birthOrder.set(tabId, ++birthSeq)
78
89
  // 挂载即回放在 openTab→注册窗口内暂存的草稿(seedDraft 内有相位门)。
79
90
  const opening = openings.get(sessionId)
80
91
  if (opening !== undefined) {
@@ -123,16 +134,29 @@ export function liveSideChat(tabId: string): LiveSideChat | undefined {
123
134
  return liveSideChats.get(tabId)
124
135
  }
125
136
 
137
+ /** 一个会话的全部存活面板(按打开序升序;native 下枚举 side tab 的唯一数据源)。 */
138
+ export function liveSideChatsOf(sessionId: string): LiveSideChat[] {
139
+ return [...liveSideChats.values()]
140
+ .filter(entry => entry.sessionId === sessionId)
141
+ .sort((a, b) => (birthOrder.get(a.tabId) ?? 0) - (birthOrder.get(b.tabId) ?? 0))
142
+ }
143
+
126
144
  /**
127
- * Most recently registered live panel of a session — the open-or-focus target
145
+ * Most recently OPENED live panel of a session — the open-or-focus target
128
146
  * when the legacy snapshot cannot see the tab (mirrors "last opened wins").
147
+ * 排序锚在首次注册序号上:面板重挂载不翻序(见 birthOrder)。
129
148
  */
130
149
  export function lastLiveSideChat(sessionId: string): LiveSideChat | undefined {
131
- let found: LiveSideChat | undefined
132
- for (const entry of liveSideChats.values()) {
133
- if (entry.sessionId === sessionId) found = entry
134
- }
135
- return found
150
+ const all = liveSideChatsOf(sessionId)
151
+ return all[all.length - 1]
152
+ }
153
+
154
+ /** 插件卸载/重载时清场(apply 的 ctx.effect disposer 调用)。 */
155
+ export function clearNativeRuntime(): void {
156
+ root = undefined
157
+ liveSideChats.clear()
158
+ openings.clear()
159
+ birthOrder.clear()
136
160
  }
137
161
 
138
162
  /** Native tab shell for reads that go through the layout snapshot (`readTab`). */
@@ -16,7 +16,7 @@ import {
16
16
  sideTabTitle,
17
17
  } from './model.ts'
18
18
  import { readInputDraft, resolveSessionInput } from './composer.ts'
19
- import { focusNativeTab, lastLiveSideChat, markSideChatOpening, nativeSidebarHost, nativeTabShell, sideChatOpening } from './native.ts'
19
+ import { focusNativeTab, lastLiveSideChat, liveSideChatsOf, markSideChatOpening, nativeSidebarHost, nativeTabShell, sideChatOpening } from './native.ts'
20
20
 
21
21
  /**
22
22
  * 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。
@@ -133,6 +133,9 @@ export function sideChatTargetTitle(ctx: Context, sessionId: string): string | u
133
133
  if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return undefined
134
134
  const existing = collectSideTabs(snapshot.state)
135
135
  if (existing.length > 0) return existing[existing.length - 1]!.title
136
+ // native:布局快照看不到 tab,改读 live registry(最近打开者)。
137
+ const live = lastLiveSideChat(sessionId)
138
+ if (live !== undefined) return live.readTitle()
136
139
  return sideTabTitle([])
137
140
  } catch {
138
141
  return undefined
@@ -150,6 +153,10 @@ export function reopenSideChat(ctx: Context, sessionId: string, childId: string,
150
153
  const snapshot = ctx.betterSidebar.getSnapshot()
151
154
  if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return false
152
155
  if (!ctx.betterSidebar.isTabEnabled(SIDE_TAB_TYPE)) return false
156
+ // native 下同 kind 每 pane 单实例(dsh sidebar-right held 规则):有存活
157
+ // 实例时 openTab 必折叠为聚焦既有 tab 且 seed.meta 被丢弃——拒绝(弹层
158
+ // 侧此时本就不列重开项,这里是双保险)。
159
+ if (nativeSidebarHost(ctx) && liveSideChatsOf(sessionId).length > 0) return false
153
160
  const before = new Set(collectTabs(snapshot.state).map(tab => tab.id))
154
161
  // native(>= 0.19):seed.meta 携带恢复信息(native 面采纳 seed.meta;
155
162
  // 面板挂载即走绑定恢复路径)。legacy 的 createTab 铸造忽略 seed.meta,
@@ -2,7 +2,9 @@
2
2
  * /side 斜杠命令(spike 落点):经 client 侧 `ctx.commandUi.register`
3
3
  * (dsh-client-ui-commands 的 CommandContribution,ui 形态 popupSelect)
4
4
  * 注册。popupSelect 是该注册面唯一的 UI 形态 —— 菜单行被选中后弹一个
5
- * 选项壳,选项 = 新建 + 当前会话已并存的侧边聊天(聚焦)。
5
+ * 选项壳,选项 = 新建 + 当前会话已并存的侧边聊天(聚焦)+ 最近关闭(重开)。
6
+ * native 宿主(better-sidebar >= 0.19)受 dsh sidebar-right 的 held 规则约束
7
+ * (page kind 每 pane 单实例):有存活实例时不列「新建」与「重开」。
6
8
  *
7
9
  * 服务经 ctx.get 惰性解析(commandUi 不在 inject 清单里):服务缺失、
8
10
  * 注册抛错(如与 host 命令撞名)都降级为「只有 Tab 入口」,绝不影响面板。
@@ -12,6 +14,7 @@
12
14
  */
13
15
  import type { Context } from '../host/contracts.ts'
14
16
  import { canForkFrom, collectSideTabs } from './model.ts'
17
+ import { focusNativeTab, liveSideChatsOf, nativeSidebarHost } from './native.ts'
15
18
  import { createSideChat, reopenSideChat } from './open.ts'
16
19
  import { dropClosedSideChat, listClosedSideChats } from './recentClosed.ts'
17
20
  import { t } from '../locales.ts'
@@ -61,19 +64,37 @@ function makeContribution(ctx: Context, name: string) {
61
64
  ui: {
62
65
  kind: 'popupSelect' as const,
63
66
  options: (session: CommandSession) => {
64
- const options: SelectOption[] = [
65
- { id: 'new', label: t('cmdNew'), detail: t('cmdNewDetail') },
66
- ]
67
+ const options: SelectOption[] = []
68
+ const native = nativeSidebarHost(ctx)
69
+ const lives = native ? liveSideChatsOf(session.sessionId) : []
70
+ // native 下同 kind 每 pane 单实例(dsh-client-ui-sidebar-right 的 held
71
+ // 规则:page kind 的 contentId 恒为 sidebar://<kind>,openTab 必折叠为
72
+ // 聚焦既有 tab)——已有存活实例时「新建」名不副实,不列。
73
+ if (!native || lives.length === 0) {
74
+ options.push({ id: 'new', label: t('cmdNew'), detail: t('cmdNewDetail') })
75
+ }
67
76
  // 已并存的侧边聊天列为聚焦项(命令弹层即多实例管理入口)。
68
- const snapshot = ctx.betterSidebar.getSnapshot()
69
- if (snapshot.sessionId === session.sessionId && snapshot.state !== undefined) {
70
- for (const tab of collectSideTabs(snapshot.state)) {
71
- options.push({ id: `focus:${tab.id}`, label: t('cmdFocus', { title: tab.title }), detail: t('cmdFocusDetail') })
77
+ // native(>= 0.19):tab 不进布局快照,枚举改读 live registry;
78
+ // legacy:读布局快照(现状)。
79
+ if (native) {
80
+ for (const live of lives) {
81
+ options.push({ id: `focus:${live.tabId}`, label: t('cmdFocus', { title: live.readTitle() }), detail: t('cmdFocusDetail') })
82
+ }
83
+ } else {
84
+ const snapshot = ctx.betterSidebar.getSnapshot()
85
+ if (snapshot.sessionId === session.sessionId && snapshot.state !== undefined) {
86
+ for (const tab of collectSideTabs(snapshot.state)) {
87
+ options.push({ id: `focus:${tab.id}`, label: t('cmdFocus', { title: tab.title }), detail: t('cmdFocusDetail') })
88
+ }
72
89
  }
73
90
  }
74
91
  // D3 后悔药:最近关闭的可重开(Cmd+Shift+T 心智)。
75
- for (const entry of listClosedSideChats(session.sessionId)) {
76
- options.push({ id: `reopen:${entry.childId}`, label: t('cmdReopen', { title: entry.title }), detail: t('cmdReopenDetail') })
92
+ // native 单实例约束下,有存活实例时 reopen 必被 held 折叠(seed.meta
93
+ // 丢弃且聚焦错对象)——此时不列重开项。
94
+ if (!(native && lives.length > 0)) {
95
+ for (const entry of listClosedSideChats(session.sessionId)) {
96
+ options.push({ id: `reopen:${entry.childId}`, label: t('cmdReopen', { title: entry.title }), detail: t('cmdReopenDetail') })
97
+ }
77
98
  }
78
99
  return Promise.resolve(options)
79
100
  },
@@ -85,7 +106,9 @@ function makeContribution(ctx: Context, name: string) {
85
106
  return
86
107
  }
87
108
  if (option.id.startsWith('focus:')) {
88
- ctx.betterSidebar.activateTab(option.id.slice('focus:'.length), { sessionId: session.sessionId })
109
+ const tabId = option.id.slice('focus:'.length)
110
+ // native 宿主的 activateTab 是空操作——聚焦走 ISidebarRight.focus 探测。
111
+ if (!focusNativeTab(ctx, tabId)) ctx.betterSidebar.activateTab(tabId, { sessionId: session.sessionId })
89
112
  return
90
113
  }
91
114
  if (option.id.startsWith('reopen:')) {