dsh-sidenote 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -290,6 +290,8 @@ window.__ModuleLoader__.load({
290
290
  termExpand: "Show all ({n} hidden lines)",
291
291
  termExpandAria: "Show all ({n} hidden lines)",
292
292
  inheritedLabel: "Inherited from main session · up to the fork point · {n}",
293
+ snapshotLabel: "Mainline progress snapshot",
294
+ snapshotLabelAt: "Mainline progress snapshot · taken {at}",
293
295
  collapseAll: "Collapse all",
294
296
  expandAll: "Expand all",
295
297
  jumpToLatest: "↓ Jump to latest",
@@ -434,6 +436,8 @@ window.__ModuleLoader__.load({
434
436
  termExpand: "展开全部(隐藏 {n} 行)",
435
437
  termExpandAria: "展开全部(隐藏 {n} 行)",
436
438
  inheritedLabel: "继承自主会话 · 截至 fork 点 · {n} 条",
439
+ snapshotLabel: "主线进展快照",
440
+ snapshotLabelAt: "主线进展快照 · 截于 {at}",
437
441
  collapseAll: "全部折叠",
438
442
  expandAll: "全部展开",
439
443
  jumpToLatest: "↓ 跳到最新",
@@ -593,7 +597,11 @@ window.__ModuleLoader__.load({
593
597
  if (typeof raw.childId === "string" && raw.childId !== "") out.childId = raw.childId;
594
598
  if (typeof raw.parentSessionId === "string" && raw.parentSessionId !== "") out.parentSessionId = raw.parentSessionId;
595
599
  if (typeof raw.pendingDraft === "string" && raw.pendingDraft !== "") out.pendingDraft = raw.pendingDraft;
596
- if (typeof raw.boundarySeq === "number" && Number.isInteger(raw.boundarySeq)) out.boundarySeq = raw.boundarySeq;
600
+ if (typeof raw.boundarySeq === "number" && Number.isFinite(raw.boundarySeq)) out.boundarySeq = raw.boundarySeq;
601
+ if (raw.forkedMidTurn === true) out.forkedMidTurn = true;
602
+ if (raw.snapshotOnly === true) out.snapshotOnly = true;
603
+ if (typeof raw.leakedPromptPrefix === "string" && raw.leakedPromptPrefix !== "") out.leakedPromptPrefix = raw.leakedPromptPrefix;
604
+ if (raw.inheritedPurged === true) out.inheritedPurged = true;
597
605
  return out;
598
606
  }
599
607
  /** 清除 meta.pendingDraft(其余字段原样保留)。 */
@@ -751,8 +759,12 @@ window.__ModuleLoader__.load({
751
759
  * 与主输入框同语义),submit 走机器事务(序列化/裁决/默认 sink 全在机器内)。
752
760
  * 降级路径:草稿住本地 state,发送直连 session.prompt([...], 'queue'),
753
761
  * 失败时回填草稿并报错。
762
+ *
763
+ * firstSendPrefix(Delivery_04 busy fork):首条消息发送瞬间现取的前缀块
764
+ * (主线进展快照)。拼进即消费(由调用方清标记)——机器路径的失败回填与
765
+ * 降级路径的重试都保留已拼文本,天然不会二次拼接。
754
766
  */
755
- function useComposer(ctx, session, childId) {
767
+ function useComposer(ctx, session, childId, options) {
756
768
  const input = (0, react.useMemo)(() => childId === void 0 ? null : resolveSessionInput(ctx, childId), [ctx, childId]);
757
769
  const machineState = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => input === null ? NOOP_UNSUBSCRIBE$1 : input.state.subscribe(notify), [input]), () => input === null ? null : input.state.getSnapshot());
758
770
  const machineDraft = machineState?.draft ?? "";
@@ -778,9 +790,12 @@ window.__ModuleLoader__.load({
778
790
  else input.setDraft(appendDraftText(readInputDraft(input), text));
779
791
  }, [input]);
780
792
  const submit = (0, react.useCallback)((mode) => {
781
- const text = draft.trim();
782
- if (text === "") return;
793
+ const body = draft.trim();
794
+ if (body === "") return;
795
+ const prefix = options?.firstSendPrefix?.() ?? null;
796
+ const text = prefix === null ? body : `${prefix}\n\n${body}`;
783
797
  if (input !== null) {
798
+ if (prefix !== null) input.setDraft(text);
784
799
  input.submit(mode);
785
800
  return;
786
801
  }
@@ -802,7 +817,8 @@ window.__ModuleLoader__.load({
802
817
  }, [
803
818
  draft,
804
819
  input,
805
- session
820
+ session,
821
+ options
806
822
  ]);
807
823
  return {
808
824
  machine: input !== null,
@@ -862,7 +878,11 @@ window.__ModuleLoader__.load({
862
878
  ...typeof r.childId === "string" ? { childId: r.childId } : {},
863
879
  ...typeof r.parentSessionId === "string" ? { parentSessionId: r.parentSessionId } : {},
864
880
  ...typeof r.pendingDraft === "string" ? { pendingDraft: r.pendingDraft } : {},
865
- ...typeof r.boundarySeq === "number" && Number.isInteger(r.boundarySeq) ? { boundarySeq: r.boundarySeq } : {},
881
+ ...typeof r.boundarySeq === "number" && Number.isFinite(r.boundarySeq) ? { boundarySeq: r.boundarySeq } : {},
882
+ ...r.forkedMidTurn === true ? { forkedMidTurn: true } : {},
883
+ ...r.snapshotOnly === true ? { snapshotOnly: true } : {},
884
+ ...typeof r.leakedPromptPrefix === "string" ? { leakedPromptPrefix: r.leakedPromptPrefix } : {},
885
+ ...r.inheritedPurged === true ? { inheritedPurged: true } : {},
866
886
  number: r.number,
867
887
  createdAt: typeof r.createdAt === "number" ? r.createdAt : 0,
868
888
  ...typeof r.runId === "string" ? { runId: r.runId } : {}
@@ -1208,1223 +1228,1629 @@ window.__ModuleLoader__.load({
1208
1228
  }
1209
1229
  }
1210
1230
  //#endregion
1211
- //#region src/client/sidechat/lifecycle.ts
1212
- /**
1213
- * 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
1214
- * 归档隐藏出会话列表(best-effort)→ 模型跟随主会话(best-effort)。
1215
- * fork 失败抛错(面板组件转错误态);后两步失败只告警不阻断。
1216
- * @returns fork 出的子会话 id。
1217
- */
1218
- async function forkAndRegister(ctx, parentSessionId, tabId) {
1219
- let boundarySeq;
1220
- try {
1221
- const snap = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy();
1222
- let max = -1;
1223
- for (const node of snap?.nodes ?? []) {
1224
- const seq = node.seq;
1225
- if (typeof seq === "number" && seq > max) max = seq;
1226
- }
1227
- if (max >= 0) boundarySeq = max;
1228
- } catch {}
1229
- const forked = await ctx.sessions.fork({ sessionId: parentSessionId });
1230
- const owner = rootContext(ctx);
1231
- updateTabMeta(ctx, parentSessionId, tabId, (current) => ({
1232
- ...current,
1233
- childId: forked,
1234
- parentSessionId,
1235
- ...boundarySeq !== void 0 ? { boundarySeq } : {}
1236
- }));
1237
- try {
1238
- await owner.workspaces.archiveSession(forked);
1239
- } catch (error) {
1240
- console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
1241
- }
1242
- try {
1243
- const selection = await readModelSelection(owner, parentSessionId);
1244
- if (selection === void 0) console.warn("[dsh-sidenote] 同步主会话模型跳过:主机模型 API 不可用");
1245
- else if (!await writeModelSelection(owner, forked, selection)) console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型)");
1246
- } catch (error) {
1247
- console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
1248
- }
1249
- return forked;
1250
- }
1251
- /**
1252
- * Tab meta 读-改-写。双腿:直连原生腿的 meta 在自有 metaStore(原生无
1253
- * updateTab 面,布局 memory-only);legacy 腿在布局快照 + betterSidebar.updateTab。
1254
- * 分腿判据统一为 directNativeLeg(对抗性审查 m1:判据单点化——metaStore
1255
- * 有无记录不能再当判据,否则记录缺失时 childId 登记会静默落到 no-op 的
1256
- * legacy 面)。直连腿记录缺失 = 异常态(reconcile 应先已建):warn 并 no-op。
1257
- */
1258
- function updateTabMeta(ctx, sessionId, tabId, mutate) {
1259
- if (directNativeLeg(ctx)) {
1260
- const direct = readSideChatMeta(sessionId, tabId);
1261
- if (direct === void 0) {
1262
- console.warn("[dsh-sidenote] meta 记录缺失(reconcile 应先已建),本次 meta 写入跳过:", tabId);
1263
- return;
1264
- }
1265
- const next = mutate(parseSideChatMeta(direct));
1266
- writeSideChatMeta({
1267
- tabId,
1268
- sessionId: direct.sessionId,
1269
- ...next.childId !== void 0 ? { childId: next.childId } : {},
1270
- ...next.parentSessionId !== void 0 ? { parentSessionId: next.parentSessionId } : {},
1271
- ...next.pendingDraft !== void 0 ? { pendingDraft: next.pendingDraft } : {},
1272
- ...next.boundarySeq !== void 0 ? { boundarySeq: next.boundarySeq } : {},
1273
- number: direct.number,
1274
- createdAt: direct.createdAt,
1275
- ...direct.runId !== void 0 ? { runId: direct.runId } : {}
1276
- });
1277
- return;
1278
- }
1279
- const bs = betterSidebarOf(ctx);
1280
- if (bs === void 0) return;
1281
- const current = parseSideChatMeta(readTab(ctx, sessionId, tabId)?.meta);
1282
- bs.updateTab(tabId, { meta: mutate(current) });
1283
- }
1284
- /** 关闭侧聊 tab:直连腿走 ISidebarRight.close,legacy 走 betterSidebar.closeTab。 */
1285
- function closeSideTab(ctx, tabId, sessionId) {
1286
- if (directNativeLeg(ctx)) {
1287
- const face = sidebarRightOf(ctx);
1288
- if (face !== void 0) {
1289
- face.close(tabId);
1290
- return;
1291
- }
1231
+ //#region src/client/chat/cards.ts
1232
+ /** ContentBlock[] → 纯文本(transcript.ts 同款的极简版,避免互依赖)。 */
1233
+ function blocksText$1(content) {
1234
+ if (!Array.isArray(content)) return void 0;
1235
+ const parts = [];
1236
+ for (const block of content) {
1237
+ const b = block;
1238
+ if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
1292
1239
  }
1293
- betterSidebarOf(ctx)?.closeTab(tabId, { sessionId });
1294
- }
1295
- /**
1296
- * 程序化入口(/side、bridge 划选提问)打开 Tab 时面板可能处于折叠态——
1297
- * 类型型 openTab 不自动展开(better-sidebar 只对 path/url 内容型打开展开)。
1298
- * 幂等展开(off-face:store.update 是 SidebarStore 公开 mutator 面;
1299
- * feature-check + 吞错,兜底失败不影响功能)。
1300
- */
1301
- function ensurePanelOpen(store) {
1302
- const mutable = store;
1303
- try {
1304
- mutable?.update?.((state) => {
1305
- if (state.panelOpen === false) state.panelOpen = true;
1306
- });
1307
- } catch {}
1240
+ return parts.length === 0 ? void 0 : parts.join("\n");
1308
1241
  }
1309
- /**
1310
- * 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
1311
- * public 且幂等,但不在 SessionFace 契约上——契约面只有 staged 会话会被
1312
- * 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
1313
- */
1314
- function openSessionWindow(session) {
1315
- const openable = session;
1316
- if (typeof openable?.open !== "function") return;
1317
- openable.open().catch((error) => {
1318
- console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
1319
- });
1242
+ /** 未知 card 值的 warn-once 登记(T3 纪律:降级不静默)。 */
1243
+ const warnedCards = /* @__PURE__ */ new Set();
1244
+ function warnUnknownCard(value) {
1245
+ if (warnedCards.has(value)) return;
1246
+ warnedCards.add(value);
1247
+ console.warn(`[dsh-sidenote] 未知工具卡种类 "${value}"——降级为 generic 卡(宿主可能扩展了 card union)`);
1320
1248
  }
1321
- /**
1322
- * Current model selection of a session, read from its `modelSelection`
1323
- * projection (dsh >= 0.1.2): the durable selection is projected state, so no RPC
1324
- * is needed and the read is safe for any session id. Snapshot shape authority:
1325
- * `@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts`
1326
- * (`ModelSelectionProjection = { lastUsed, next }`,两者均可为 null——
1327
- * `next` 缺省时按投影文档回落 `lastUsed`)。
1328
- */
1329
- function projectedModelSelection(ctx, sessionId) {
1330
- try {
1331
- const snapshot = (ctx.sessions.binding(sessionId)?.session)?.projections?.faceOf?.("modelSelection")?.getSnapshot?.();
1332
- return snapshot?.next ?? snapshot?.lastUsed ?? void 0;
1333
- } catch {
1334
- return;
1335
- }
1249
+ /** 相对 cwd 按会话工作区折成绝对路径(UI bridge 责任——presentation 注释明文)。 */
1250
+ function resolveCwd(cwd, cwdBase) {
1251
+ if (cwd === void 0) return cwdBase;
1252
+ if (cwd.startsWith("/") || /^[A-Za-z]:[\\/]/.test(cwd)) return cwd;
1253
+ if (cwdBase === void 0) return cwd;
1254
+ return `${cwdBase.replace(/\/+$/, "")}/${cwd}`;
1336
1255
  }
1337
1256
  /**
1338
- * Remote session face carrying `selectModel`/`modelCatalog`(dsh 0.1.5 删除
1339
- * `connection.api.sessions.*` client 面后的模型 RPC 面;事实上 0.1.2-rc.1
1340
- * 起存在——authority: dsh-api-session-controller lib/typert.remote-client.d.ts)。
1341
- * 探测走 `ctx.get`(cordis 的 get 不受 inject 门禁限制,本插件为兼容旧宿主
1342
- * 不声明 inject 'remote.session';机制见 contracts.ts RemoteSessionModelFace)。
1343
- * `ctx.get('remote')` 返回命名空间容器时下钻其 `.session` 成员。
1257
+ * 展示态路径缩短:>3 段的绝对路径只保末 3 段(≈仓库相对形态,与主区
1258
+ * 「Read · dsh-sidenote/scripts/x.sh」同款)。窄栏标题位只够放语义尾部。
1344
1259
  */
1345
- function remoteSessionFace(ctx) {
1346
- for (const name of ["remote.session", "remote"]) {
1347
- let candidate;
1348
- try {
1349
- candidate = ctx.get(name);
1350
- } catch {
1351
- continue;
1352
- }
1353
- const face = candidate;
1354
- if (typeof face?.selectModel === "function") return face;
1355
- if (typeof face?.session?.selectModel === "function") return face.session;
1356
- }
1260
+ function shortPath(p) {
1261
+ if (!p.startsWith("/")) return p;
1262
+ const segs = p.split("/").filter((s) => s !== "");
1263
+ return segs.length > 3 ? segs.slice(-3).join("/") : p;
1357
1264
  }
1358
1265
  /**
1359
- * 读会话当前模型选择:投影面(0.1.2+,同步无 RPC)优先;旧
1360
- * `connection.api.sessions.models` RPC(<= 0.1.2 的 client 面,0.1.5 已删)
1361
- * 回退(双版本链,同 chatSourceOf 先例)。永不抛错。
1266
+ * 标题整形:「Tool /abs/path」→「Tool · 末3段」(主区同名同款);
1267
+ * 裸绝对路径标题 → 末 3 段;不含路径的标题原样。
1362
1268
  */
1363
- async function readModelSelection(ctx, sessionId) {
1364
- const projected = projectedModelSelection(ctx, sessionId);
1365
- if (projected !== void 0) return projected;
1366
- try {
1367
- const res = await ctx.connection.api.sessions.models({ sessionId });
1368
- if (res.result.ok) return res.result.value.current;
1369
- } catch {}
1269
+ function displayTitle(title) {
1270
+ const m = /^([A-Za-z][\w-]*) (\/.+)$/.exec(title);
1271
+ const name = m?.[1];
1272
+ const path = m?.[2];
1273
+ if (name !== void 0 && path !== void 0) return `${name} · ${shortPath(path)}`;
1274
+ return title.startsWith("/") ? shortPath(title) : title;
1370
1275
  }
1371
- /**
1372
- * 写会话模型选择:`remote.session.selectModel`(新面)优先,旧
1373
- * `connection.api.sessions.selectModel` 回退。面在而调用失败(模型不可路由等)
1374
- * 属真实失败,不再落旧面重试。
1375
- */
1376
- async function writeModelSelection(ctx, sessionId, selection) {
1377
- const request = {
1378
- sessionId,
1379
- provider: selection.provider,
1380
- model: selection.model,
1381
- ...selection.reasoningEffort !== void 0 ? { reasoningEffort: selection.reasoningEffort } : {}
1382
- };
1383
- const face = remoteSessionFace(ctx);
1384
- if (face !== void 0) try {
1385
- return (await face.selectModel(request)).ok;
1386
- } catch {
1387
- return false;
1388
- }
1389
- try {
1390
- return (await ctx.connection.api.sessions.selectModel(request)).result.ok;
1391
- } catch {
1392
- return false;
1276
+ /** todo_write 入参形状校验(字段不齐即放弃 todo 卡)。两种数据源形状:
1277
+ * 0.1.1 wire:callView.rawInput = todos 数组本身(dsh-tool-todo presentCall 实证);
1278
+ * 0.1.2 argsRaw:{ todos: [...] }。 */
1279
+ function todoItemsOf(raw) {
1280
+ const list = Array.isArray(raw) ? raw : typeof raw === "object" && raw !== null ? raw.todos : void 0;
1281
+ if (!Array.isArray(list) || list.length === 0) return void 0;
1282
+ const items = [];
1283
+ for (const it of list) {
1284
+ const o = it;
1285
+ if (typeof o?.content !== "string" || typeof o?.status !== "string") return void 0;
1286
+ items.push({
1287
+ content: o.content,
1288
+ status: o.status
1289
+ });
1393
1290
  }
1291
+ return items;
1394
1292
  }
1395
- /** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
1396
- async function readModelName(ctx, sessionId) {
1397
- return (await readModelSelection(ctx, sessionId))?.model ?? null;
1293
+ /** todo 卡标题:任务 · 非零状态组计数(主区「任务 · N 已完成 · …」同款)。 */
1294
+ function todoTitleOf(items) {
1295
+ let done = 0;
1296
+ let doing = 0;
1297
+ let pending = 0;
1298
+ for (const it of items) if (it.status === "completed") done += 1;
1299
+ else if (it.status === "in_progress") doing += 1;
1300
+ else pending += 1;
1301
+ const parts = [];
1302
+ if (done > 0) parts.push(t("todoDone", { n: done }));
1303
+ if (doing > 0) parts.push(t("todoDoing", { n: doing }));
1304
+ if (pending > 0) parts.push(t("todoPending", { n: pending }));
1305
+ return parts.length === 0 ? t("todoTitle") : `${t("todoTitle")} · ${parts.join(" · ")}`;
1398
1306
  }
1399
1307
  /**
1400
- * 0.1.2 优先:`uiConversation` 服务 → binding(会话 binding) → target('chat')
1401
- * → 快照 .legacy 切片(0.1.2 把内容面从 Session 快照顶层拆到此处——
1402
- * 根因与证据见 reports/ux-review/W00-fork-replay-012.md);0.1.1 回退
1403
- * Session 快照顶层(nodes 直接在)。均 miss 返回 undefined(面板维持
1404
- * 既有降级,不崩)。
1308
+ * callView + resultView → 一张卡的视图模型。三层判别全在这里;
1309
+ * 结果态优先(title 等字段 result 覆盖 call),缺省逐字段回退。
1405
1310
  */
1406
- function chatSourceOf(ctx, binding) {
1407
- const session = binding?.session;
1408
- if (session === void 0) return void 0;
1409
- try {
1410
- const target = ctx.get("uiConversation")?.binding?.(binding)?.target?.("chat");
1411
- if (target !== void 0 && typeof target.getSnapshot === "function" && typeof target.subscribe === "function") return {
1412
- subscribe: (fn) => target.subscribe(fn),
1413
- getLegacy: () => {
1414
- return target.getSnapshot()?.legacy ?? null;
1415
- }
1416
- };
1417
- } catch {}
1418
- const snap = session.getSnapshot();
1419
- if (snap !== null && Array.isArray(snap.nodes)) return {
1420
- subscribe: (fn) => session.subscribe(fn),
1421
- getLegacy: () => session.getSnapshot()
1422
- };
1423
- }
1424
- /**
1425
- * 拉会话模型目录:0.1.5 走 `remote.session.modelCatalog()`(目录是宿主级,
1426
- * 菜单选中项所需的 current 由投影/旧面补上);旧面回退
1427
- * `connection.api.sessions.models`。失败 null → 菜单保持只读标签态。
1428
- */
1429
- async function listModels(ctx, sessionId) {
1430
- const face = remoteSessionFace(ctx);
1431
- if (typeof face?.modelCatalog === "function") try {
1432
- const result = await face.modelCatalog();
1433
- if (result.ok) {
1434
- const catalog = result.value;
1311
+ function cardModelOf(input) {
1312
+ const { callView, resultView } = input;
1313
+ const call = callView ?? void 0;
1314
+ const result = resultView ?? void 0;
1315
+ const card = result?.card ?? call?.card ?? "generic";
1316
+ switch (card) {
1317
+ case "terminal": {
1318
+ const rc = result?.card === "terminal" ? result : void 0;
1319
+ const cc = call?.card === "terminal" ? call : void 0;
1320
+ const command = rc?.title ?? cc?.title ?? input.toolName;
1435
1321
  return {
1436
- current: await readModelSelection(ctx, sessionId) ?? catalog.default,
1437
- routable: catalog.routableProviders.length > 0,
1438
- groups: catalog.groups,
1439
- ...catalog.failures !== void 0 ? { failures: catalog.failures } : {}
1322
+ kind: "terminal",
1323
+ title: cc?.description !== void 0 ? `${displayToolName(input.toolName)} · ${cc.description}` : command,
1324
+ command,
1325
+ ...cc?.description !== void 0 ? { description: cc.description } : {},
1326
+ ...resolveCwd(cc?.cwd, input.cwdBase) !== void 0 ? { cwd: resolveCwd(cc?.cwd, input.cwdBase) } : {},
1327
+ ...rc?.output !== void 0 ? { output: rc.output } : {},
1328
+ ...rc?.exitCode !== void 0 ? { exitCode: rc.exitCode } : {},
1329
+ ...rc?.signal !== void 0 ? { signal: rc.signal } : {}
1440
1330
  };
1441
1331
  }
1442
- } catch {}
1443
- try {
1444
- const res = await ctx.connection.api.sessions.models({ sessionId });
1445
- return res.result.ok ? res.result.value : null;
1446
- } catch {
1447
- return null;
1332
+ case "diff": {
1333
+ const rc = result?.card === "diff" ? result : void 0;
1334
+ const cc = call?.card === "diff" ? call : void 0;
1335
+ return {
1336
+ kind: "diff",
1337
+ title: displayTitle(rc?.title ?? cc?.title ?? input.toolName),
1338
+ diffs: rc?.diffs ?? cc?.diffs ?? [],
1339
+ ...cc?.locations !== void 0 ? { locations: cc.locations } : {}
1340
+ };
1341
+ }
1342
+ case "search": {
1343
+ const rc = result?.card === "search" ? result : void 0;
1344
+ if (rc === void 0) return genericModel(input, call, result);
1345
+ if (rc.shape === "matches") return {
1346
+ kind: "search",
1347
+ title: rc.title ?? call?.title ?? input.toolName,
1348
+ shape: "matches",
1349
+ files: rc.files,
1350
+ truncated: rc.truncated,
1351
+ total: rc.total
1352
+ };
1353
+ if (rc.shape === "paths") return {
1354
+ kind: "search",
1355
+ title: rc.title ?? call?.title ?? input.toolName,
1356
+ shape: "paths",
1357
+ paths: rc.paths,
1358
+ truncated: rc.truncated,
1359
+ total: rc.total
1360
+ };
1361
+ warnUnknownCard(`search/${String(rc.shape)}`);
1362
+ return genericModel(input, call, result);
1363
+ }
1364
+ case "read": {
1365
+ const rc = result?.card === "read" ? result : void 0;
1366
+ if (rc === void 0) return genericModel(input, call, result);
1367
+ return {
1368
+ kind: "read",
1369
+ title: `Read · ${shortPath(rc.path)}`,
1370
+ path: rc.path,
1371
+ lines: rc.lines,
1372
+ totalLines: rc.totalLines,
1373
+ ...rc.lang !== void 0 ? { lang: rc.lang } : {}
1374
+ };
1375
+ }
1376
+ case "web": {
1377
+ const rc = result?.card === "web" ? result : void 0;
1378
+ if (rc === void 0) return genericModel(input, call, result);
1379
+ const base = {
1380
+ kind: "web",
1381
+ title: rc.title ?? call?.title ?? input.toolName
1382
+ };
1383
+ if (rc.kind === "search") {
1384
+ const rs = rc;
1385
+ return {
1386
+ ...base,
1387
+ webKind: "search",
1388
+ sources: rs.sources,
1389
+ truncated: rs.truncated,
1390
+ ...rs.answer !== void 0 ? { answer: rs.answer } : {}
1391
+ };
1392
+ }
1393
+ if (rc.kind === "fetch") {
1394
+ const rf = rc;
1395
+ return {
1396
+ ...base,
1397
+ webKind: "fetch",
1398
+ url: rf.url,
1399
+ statusCode: rf.statusCode,
1400
+ truncated: rf.truncated
1401
+ };
1402
+ }
1403
+ warnUnknownCard(`web/${String(rc.kind)}`);
1404
+ return genericModel(input, call, result);
1405
+ }
1406
+ case "generic": return genericModel(input, call, result);
1407
+ default:
1408
+ warnUnknownCard(String(card));
1409
+ return genericModel(input, call, result);
1448
1410
  }
1449
1411
  }
1450
- /** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
1451
- async function switchModel(ctx, sessionId, provider, model) {
1452
- return await writeModelSelection(ctx, sessionId, {
1453
- provider,
1454
- model
1455
- }) ? model : null;
1456
- }
1457
- //#endregion
1458
- //#region src/client/host/probes.ts
1459
- const PROBES = [
1460
- {
1461
- name: "Session.open()(非 staged 会话开窗)",
1462
- check: (ctx) => {
1463
- return typeof ctx.sessions?.binding === "function";
1464
- },
1465
- fallback: "侧边聊天消息流降级为只发不收"
1466
- },
1467
- {
1468
- name: "conversation.input 输入机器",
1469
- check: (ctx) => {
1470
- return typeof (ctx.get?.("conversation"))?.input?.for === "function";
1471
- },
1472
- fallback: "侧边 composer 降级为本地草稿 + session.prompt"
1473
- },
1474
- {
1475
- name: "commandUi(斜杠命令注册)",
1476
- check: (ctx) => {
1477
- return typeof (ctx.get?.("commandUi"))?.register === "function";
1478
- },
1479
- fallback: "/side 与「/侧边」命令不可用(菜单/顶栏入口仍在)"
1480
- },
1481
- {
1482
- name: "ctx.modules.import(动态组件复用)",
1483
- check: (ctx) => {
1484
- return typeof ctx.modules?.import === "function";
1485
- },
1486
- fallback: "ProducedFiles 等动态复用件降级为自绘"
1487
- },
1488
- {
1489
- name: "sidebarRight.focus(native tab 程序化聚焦)",
1490
- check: (ctx) => {
1491
- if (!nativeSidebarHost(ctx)) return true;
1492
- return typeof (ctx.get?.("sidebarRight"))?.focus === "function";
1493
- },
1494
- fallback: "聚焦既有侧边聊天 tab 不可用(activateTab 在 native 面是空操作)"
1495
- },
1496
- {
1497
- name: "会话模型面(remote.session / connection.api.sessions 双版本链)",
1498
- check: (ctx) => {
1499
- if (remoteSessionFace(ctx) !== void 0) return true;
1500
- return typeof ctx.connection?.api?.sessions?.models === "function";
1501
- },
1502
- fallback: "fork 模型同步、模型标签与模型切换不可用(子会话用宿主默认模型)"
1412
+ function genericModel(input, call, result) {
1413
+ const gc = call?.card === "generic" ? call : void 0;
1414
+ const gr = result?.card === "generic" ? result : void 0;
1415
+ if (input.toolName === "todo_write") {
1416
+ const items = todoItemsOf(gc?.rawInput);
1417
+ if (items !== void 0) return {
1418
+ kind: "todo",
1419
+ title: todoTitleOf(items),
1420
+ items
1421
+ };
1503
1422
  }
1504
- ];
1505
- /** 激活时跑一遍探测清单;缺失项逐项 warn-once。返回缺失项数(测试可断言)。 */
1506
- function probeHost(ctx) {
1507
- let missing = 0;
1508
- for (const probe of PROBES) {
1509
- let ok = false;
1510
- try {
1511
- ok = probe.check(ctx);
1512
- } catch {
1513
- ok = false;
1514
- }
1515
- if (!ok) {
1516
- missing += 1;
1517
- console.warn(`[dsh-sidenote] 宿主能力缺失:${probe.name} → ${probe.fallback}`);
1518
- }
1423
+ return {
1424
+ kind: "generic",
1425
+ title: displayTitle(gr?.title ?? gc?.title ?? input.toolName),
1426
+ icon: gc?.kind ?? "other",
1427
+ ...gc?.rawInput !== void 0 ? { rawInput: gc.rawInput } : {},
1428
+ bodyText: blocksText$1(gr?.content) ?? blocksText$1(gc?.content) ?? input.rawText,
1429
+ ...gc?.locations !== void 0 ? { locations: gc.locations } : {}
1430
+ };
1431
+ }
1432
+ /** 工具机器名 → 展示名(主区「Bash · …」的同款首字母大写)。 */
1433
+ function displayToolName(name) {
1434
+ return name.charAt(0).toUpperCase() + name.slice(1);
1435
+ }
1436
+ /** 结果正文尾标解析(宿主 parseExitStatus 的线格式)。 */
1437
+ function parseExitStatus(text) {
1438
+ const m = /\n\[(?:exit code: (\d+)|killed by signal: ([A-Z]+))\]\s*$/.exec(text);
1439
+ if (m === null) return { output: text };
1440
+ const output = text.slice(0, m.index).replace(/\n+$/, "");
1441
+ if (m[1] !== void 0) return {
1442
+ output,
1443
+ exitCode: Number(m[1])
1444
+ };
1445
+ return {
1446
+ output,
1447
+ signal: m[2]
1448
+ };
1449
+ }
1450
+ /** FsReadMeta 形状+语义校验(readMetaFromMeta 的忠实子集)。 */
1451
+ function readMetaOf(meta) {
1452
+ if (typeof meta !== "object" || meta === null) return void 0;
1453
+ const m = meta;
1454
+ if (typeof m.path !== "string" || !Array.isArray(m.lines) || typeof m.totalLines !== "number") return void 0;
1455
+ if (!Number.isInteger(m.totalLines) || m.totalLines < 0) return void 0;
1456
+ let prev = 0;
1457
+ const lines = [];
1458
+ for (const raw of m.lines) {
1459
+ const l = raw;
1460
+ if (typeof l?.number !== "number" || typeof l?.text !== "string") return void 0;
1461
+ if (!Number.isInteger(l.number) || l.number <= prev || l.number > m.totalLines) return void 0;
1462
+ prev = l.number;
1463
+ lines.push({
1464
+ number: l.number,
1465
+ text: l.text
1466
+ });
1519
1467
  }
1520
- return missing;
1468
+ return {
1469
+ path: m.path,
1470
+ lines,
1471
+ totalLines: m.totalLines,
1472
+ ...typeof m.lang === "string" ? { lang: m.lang } : {}
1473
+ };
1521
1474
  }
1522
- //#endregion
1523
- //#region src/client/reflow.ts
1524
1475
  /**
1525
- * 回流通道的受控对象层(Delivery_02 Workitem_04 v2):侧边聊天的结论以
1526
- * 「待回流对象」进入主会话——主 composer 上方的 chip(可预览/可移除),
1527
- * 发送瞬间由 annotate/send.ts 拦截器与注释协议块一起序列化进消息。
1528
- * 不经草稿文本流(用户裁定:直接进草稿「很脏」)。
1529
- *
1530
- * 气泡留痕:序列化形态是 XML 协议块(见 buildReflowBlock),随注释协议块
1531
- * 一起作为消息连续前缀注入,bubble.ts 反解析后折叠为「含侧边回流上下文」标签。
1532
- *
1533
- * 持久化:localStorage 按会话键(dsh-sidenote:reflow:v1:<sessionId>),
1534
- * 与注释 store 同款纪律(刷新不丢、容错 revive、空删键)。
1476
+ * FsDiffMeta 校验(0.1.2 diff 卡,dsh-tool-fs computeHunkDiffs 产物的忠实子集)。
1477
+ * 返回值三态:{diffs 非空} / 'empty'(空数组——write create/同内容覆写是合法值,
1478
+ * 客户端回退 args 整文件 diff)/ undefined(malformed——降级)。
1479
+ * 逐项校验,一项违规整卡降级(官方 narrowDiffs 同款)。
1535
1480
  */
1481
+ function diffMetaOf(meta) {
1482
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
1483
+ const m = meta;
1484
+ if (!Array.isArray(m.diffs)) return void 0;
1485
+ if (m.diffs.length === 0) return "empty";
1486
+ const diffs = [];
1487
+ for (const raw of m.diffs) {
1488
+ const d = raw;
1489
+ if (typeof d?.path !== "string") return void 0;
1490
+ if (d.oldText !== null && typeof d.oldText !== "string") return void 0;
1491
+ if (typeof d.newText !== "string") return void 0;
1492
+ diffs.push({
1493
+ path: d.path,
1494
+ oldText: d.oldText,
1495
+ newText: d.newText
1496
+ });
1497
+ }
1498
+ return { diffs };
1499
+ }
1500
+ /** SearchMeta 校验(0.1.2 search 卡;truncated/total 是硬字段;空结果合法)。 */
1501
+ function searchMetaOf(meta) {
1502
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
1503
+ const m = meta;
1504
+ if (typeof m.truncated !== "boolean") return void 0;
1505
+ if (typeof m.total !== "number" || !Number.isInteger(m.total) || m.total < 0) return void 0;
1506
+ if (m.shape === "matches") {
1507
+ if (!Array.isArray(m.files)) return void 0;
1508
+ const files = [];
1509
+ for (const rawF of m.files) {
1510
+ const f = rawF;
1511
+ if (typeof f?.path !== "string" || !Array.isArray(f.matches)) return void 0;
1512
+ const matches = [];
1513
+ for (const rawM of f.matches) {
1514
+ const mm = rawM;
1515
+ if (typeof mm?.lineNumber !== "number" || !Number.isInteger(mm.lineNumber) || mm.lineNumber < 1) return void 0;
1516
+ if (typeof mm.line !== "string") return void 0;
1517
+ matches.push({
1518
+ lineNumber: mm.lineNumber,
1519
+ line: mm.line
1520
+ });
1521
+ }
1522
+ files.push({
1523
+ path: f.path,
1524
+ matches
1525
+ });
1526
+ }
1527
+ return {
1528
+ shape: "matches",
1529
+ files,
1530
+ truncated: m.truncated,
1531
+ total: m.total
1532
+ };
1533
+ }
1534
+ if (m.shape === "paths") {
1535
+ if (!Array.isArray(m.paths) || !m.paths.every((p) => typeof p === "string")) return void 0;
1536
+ return {
1537
+ shape: "paths",
1538
+ paths: m.paths,
1539
+ truncated: m.truncated,
1540
+ total: m.total
1541
+ };
1542
+ }
1543
+ }
1544
+ /** WebSearchMeta 校验(sources 逐项 url 必填、title/snippet 可选;publishedAt 校验后丢弃)。 */
1545
+ function webSearchMetaOf(meta) {
1546
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
1547
+ const m = meta;
1548
+ if (typeof m.truncated !== "boolean") return void 0;
1549
+ if (!Array.isArray(m.sources)) return void 0;
1550
+ const sources = [];
1551
+ for (const raw of m.sources) {
1552
+ const s = raw;
1553
+ if (typeof s?.url !== "string") return void 0;
1554
+ if (s.title !== void 0 && typeof s.title !== "string") return void 0;
1555
+ if (s.snippet !== void 0 && typeof s.snippet !== "string") return void 0;
1556
+ if (s.publishedAt !== void 0 && typeof s.publishedAt !== "string") return void 0;
1557
+ sources.push({
1558
+ url: s.url,
1559
+ ...s.title !== void 0 ? { title: s.title } : {},
1560
+ ...s.snippet !== void 0 ? { snippet: s.snippet } : {}
1561
+ });
1562
+ }
1563
+ if (m.answer !== void 0 && typeof m.answer !== "string") return void 0;
1564
+ return {
1565
+ sources,
1566
+ truncated: m.truncated,
1567
+ ...m.answer !== void 0 ? { answer: m.answer } : {}
1568
+ };
1569
+ }
1570
+ /** WebFetchMeta 校验(url + 整数 statusCode + truncated 硬字段)。 */
1571
+ function webFetchMetaOf(meta) {
1572
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
1573
+ const m = meta;
1574
+ if (typeof m.url !== "string") return void 0;
1575
+ if (typeof m.statusCode !== "number" || !Number.isInteger(m.statusCode)) return void 0;
1576
+ if (typeof m.truncated !== "boolean") return void 0;
1577
+ return {
1578
+ url: m.url,
1579
+ statusCode: m.statusCode,
1580
+ truncated: m.truncated
1581
+ };
1582
+ }
1583
+ /** argsRaw(JSON 字符串)里尽力提取展示字段。 */
1584
+ function argsSummary(argsRaw) {
1585
+ if (argsRaw === void 0 || argsRaw === "") return {};
1586
+ try {
1587
+ const parsed = JSON.parse(argsRaw);
1588
+ if (typeof parsed !== "object" || parsed === null) return {};
1589
+ const a = parsed;
1590
+ return {
1591
+ ...typeof a.command === "string" ? { command: a.command } : {},
1592
+ ...typeof a.description === "string" ? { description: a.description } : {},
1593
+ ...typeof a.file_path === "string" ? { path: a.file_path } : typeof a.path === "string" ? { path: a.path } : {},
1594
+ ...Array.isArray(a.todos) ? { todos: a.todos } : {},
1595
+ ...typeof a.content === "string" ? { content: a.content } : {},
1596
+ ...typeof a.old_string === "string" ? { oldString: a.old_string } : {},
1597
+ ...typeof a.new_string === "string" ? { newString: a.new_string } : {},
1598
+ ...typeof a.pattern === "string" ? { pattern: a.pattern } : {},
1599
+ ...typeof a.include === "string" ? { include: a.include } : {},
1600
+ ...Array.isArray(a.queries) && a.queries.every((q) => typeof q === "string") ? { queries: a.queries } : {},
1601
+ ...typeof a.url === "string" ? { url: a.url } : {}
1602
+ };
1603
+ } catch {
1604
+ return {};
1605
+ }
1606
+ }
1536
1607
  /**
1537
- * reflow 注入通道(Workitem_06):优先经宿主路由 /sidenote/reflow 以
1538
- * agent.inject 落父会话日志(plugin-source 独立事件,context row 折叠);
1539
- * 路由缺席/父会话无 live agent/超时 → 调用方回落搭车形态(发送拦截器
1540
- * 序列化前缀)。逐个 item 投递;成功项由调用方从 store 移除(不等发送
1541
- * 确认——注入已入 inbox,chip 消失表达「已注入」)。
1542
- *
1543
- * 有界等待:注入尝试绝不能拖住发送手势——300ms 超时即回落搭车。
1608
+ * 0.1.2 推导路径:节点原始字段 → ToolCardModel。与 cardModelOf(0.1.1 wire
1609
+ * 面)平级;选择点在 transcript 层(节点带不带 callView 一眼可分)。
1544
1610
  */
1545
- const REFLOW_ROUTE_TIMEOUT_MS = 300;
1546
- /** 尝试经宿主路由注入一批回流项;任何失败都降级为「全未注入」。 */
1547
- async function tryInjectReflows(parentSessionId, items) {
1548
- if (items.length === 0) return { injectedIds: /* @__PURE__ */ new Set() };
1549
- const empty = { injectedIds: /* @__PURE__ */ new Set() };
1550
- try {
1551
- const controller = new AbortController();
1552
- const timer = setTimeout(() => {
1553
- controller.abort();
1554
- }, REFLOW_ROUTE_TIMEOUT_MS);
1555
- let res;
1556
- try {
1557
- res = await fetch("/sidenote/reflow", {
1558
- method: "POST",
1559
- headers: { "content-type": "application/json" },
1560
- body: JSON.stringify({
1561
- parentSessionId,
1562
- items: items.map((item) => ({
1563
- text: `以下是用户从侧边对话带回的结论(参考上下文):\n\n${buildReflowBlock(item)}`,
1564
- sideTitle: item.sideTitle
1565
- }))
1566
- }),
1567
- signal: controller.signal
1568
- });
1569
- } finally {
1570
- clearTimeout(timer);
1611
+ function cardModelFromNode(input) {
1612
+ const args = argsSummary(input.argsRaw);
1613
+ const name = input.name;
1614
+ if (name === "bash" || name === "bash-persistent" || name === "pwsh" || name === "pwsh-persistent") {
1615
+ const { output, exitCode, signal } = parseExitStatus(input.rawText);
1616
+ const command = args.command ?? name;
1617
+ return {
1618
+ kind: "terminal",
1619
+ title: args.description !== void 0 ? `${displayToolName(name)} · ${args.description}` : command,
1620
+ command,
1621
+ ...args.description !== void 0 ? { description: args.description } : {},
1622
+ output,
1623
+ ...exitCode !== void 0 ? { exitCode } : {},
1624
+ ...signal !== void 0 ? { signal } : {}
1625
+ };
1626
+ }
1627
+ if (name === "read") {
1628
+ const meta = readMetaOf(input.meta);
1629
+ if (meta !== void 0) return {
1630
+ kind: "read",
1631
+ title: `Read · ${shortPath(meta.path)}`,
1632
+ path: meta.path,
1633
+ lines: meta.lines,
1634
+ totalLines: meta.totalLines,
1635
+ ...meta.lang !== void 0 ? { lang: meta.lang } : {}
1636
+ };
1637
+ }
1638
+ if (name === "todo_write") {
1639
+ const items = todoItemsOf(args.todos);
1640
+ if (items !== void 0) return {
1641
+ kind: "todo",
1642
+ title: todoTitleOf(items),
1643
+ items
1644
+ };
1645
+ }
1646
+ if ((name === "write" || name === "edit") && input.isError !== true) {
1647
+ const path = args.path;
1648
+ if (path !== void 0 && path.trim() !== "") {
1649
+ const dm = diffMetaOf(input.meta);
1650
+ if (dm !== void 0 && dm !== "empty") return {
1651
+ kind: "diff",
1652
+ title: displayTitle(`${name === "write" ? "Write" : "Edit"} ${path}`),
1653
+ diffs: dm.diffs,
1654
+ locations: [{ path }]
1655
+ };
1656
+ if (name === "write" && args.content !== void 0) return {
1657
+ kind: "diff",
1658
+ title: displayTitle(`Write ${path}`),
1659
+ diffs: [{
1660
+ path,
1661
+ oldText: null,
1662
+ newText: args.content
1663
+ }],
1664
+ locations: [{ path }]
1665
+ };
1666
+ }
1667
+ }
1668
+ if ((name === "grep" || name === "glob") && input.isError !== true) {
1669
+ const sm = searchMetaOf(input.meta);
1670
+ const wantShape = name === "grep" ? "matches" : "paths";
1671
+ if (sm !== void 0 && sm.shape === wantShape && args.pattern !== void 0) {
1672
+ const where = args.path !== void 0 && args.path.trim() !== "" ? ` in ${shortPath(args.path)}` : "";
1673
+ const inc = name === "grep" && args.include !== void 0 && args.include.trim() !== "" ? ` (${args.include})` : "";
1674
+ const title = `${name === "grep" ? "Grep" : "Glob"} ${args.pattern}${where}${inc}`;
1675
+ return sm.shape === "matches" ? {
1676
+ kind: "search",
1677
+ title,
1678
+ shape: "matches",
1679
+ files: sm.files,
1680
+ truncated: sm.truncated,
1681
+ total: sm.total
1682
+ } : {
1683
+ kind: "search",
1684
+ title,
1685
+ shape: "paths",
1686
+ paths: sm.paths,
1687
+ truncated: sm.truncated,
1688
+ total: sm.total
1689
+ };
1571
1690
  }
1572
- if (!res.ok) return empty;
1573
- if ((await res.json()).ok !== true) return empty;
1574
- return { injectedIds: new Set(items.map((item) => item.id)) };
1575
- } catch {
1576
- return empty;
1577
1691
  }
1692
+ if (name === "web_search" && input.isError !== true) {
1693
+ const wm = webSearchMetaOf(input.meta);
1694
+ const queries = args.queries?.filter((q) => q.trim() !== "") ?? [];
1695
+ if (wm !== void 0 && queries.length > 0) return {
1696
+ kind: "web",
1697
+ title: queries.join(", "),
1698
+ webKind: "search",
1699
+ sources: wm.sources,
1700
+ truncated: wm.truncated,
1701
+ ...wm.answer !== void 0 ? { answer: wm.answer } : {}
1702
+ };
1703
+ }
1704
+ if (name === "web_fetch" && input.isError !== true) {
1705
+ const wm = webFetchMetaOf(input.meta);
1706
+ if (wm !== void 0 && args.url !== void 0 && args.url.trim() !== "") return {
1707
+ kind: "web",
1708
+ title: args.url,
1709
+ webKind: "fetch",
1710
+ url: wm.url,
1711
+ statusCode: wm.statusCode,
1712
+ truncated: wm.truncated
1713
+ };
1714
+ }
1715
+ return {
1716
+ kind: "generic",
1717
+ title: args.path !== void 0 ? `${name} · ${shortPath(args.path)}` : args.command ?? name,
1718
+ icon: KIND_BY_NAME[name] ?? "other",
1719
+ bodyText: input.rawText === "" ? void 0 : input.rawText
1720
+ };
1578
1721
  }
1579
- const STORAGE_PREFIX$1 = "dsh-sidenote:reflow:v1:";
1580
- /**
1581
- * 回流块序列化(XML 形态,与注释协议同族):
1582
- *
1583
- * <reflow source="侧边 2" reason="用户选择从侧边聊天带回主线">
1584
- * <问>用户在侧边聊天里的提问</问>
1585
- * <答>结论全文(不截断)</答>
1586
- * </reflow>
1587
- *
1588
- * 问答成对:只有答没有问,模型理解打折;question 缺省时只有 <答>。
1589
- * 来源与意图放在属性里,内容行保持纯净;模型对 XML 包裹的上下文理解最好
1590
- * (Claude 官方推荐形态 / Codex additionalContext 同款思路)。
1591
- */
1592
- function buildReflowBlock(item) {
1722
+ /** 工具名 → 图标类别的静态映射(0.1.2 没有 callView.kind 可用)。
1723
+ * wire 名实证:str_replace_editor 是下划线(uitool client.js L381);
1724
+ * web_search 的 0.1.1 wire kind 是 'search'(presentSearchCall),
1725
+ * 'fetch' 是 web_fetch 的(presentFetchCall)。 */
1726
+ const KIND_BY_NAME = {
1727
+ read: "read",
1728
+ write: "edit",
1729
+ edit: "edit",
1730
+ str_replace_editor: "edit",
1731
+ bash: "execute",
1732
+ "bash-persistent": "execute",
1733
+ pwsh: "execute",
1734
+ "pwsh-persistent": "execute",
1735
+ grep: "search",
1736
+ glob: "search",
1737
+ web_search: "search",
1738
+ web_fetch: "fetch"
1739
+ };
1740
+ //#endregion
1741
+ //#region src/client/chat/transcript.ts
1742
+ /** 工具结果正文截断上限(面板是窄栏,超长输出不撑爆 DOM)。 */
1743
+ const TOOL_TEXT_LIMIT = 4e3;
1744
+ function truncateText(text, max) {
1745
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
1746
+ }
1747
+ /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
1748
+ function contentTextOf(content) {
1749
+ if (!Array.isArray(content)) return "";
1593
1750
  const parts = [];
1594
- if (item.question !== void 0 && item.question.trim() !== "") parts.push(`<问>${item.question}</问>`);
1595
- parts.push(`<答>${item.text}</答>`);
1596
- return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${parts.join("\n")}\n</reflow>`;
1751
+ for (const block of content) {
1752
+ if (typeof block !== "object" || block === null) continue;
1753
+ const b = block;
1754
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
1755
+ else if (b.type === "image") parts.push(t("imagePlaceholder"));
1756
+ }
1757
+ return parts.join("\n");
1597
1758
  }
1598
- function revive(value) {
1599
- if (typeof value !== "object" || value === null) return null;
1600
- const r = value;
1601
- if (typeof r.id !== "number" || typeof r.sessionId !== "string" || r.sessionId === "") return null;
1602
- if (typeof r.sideTitle !== "string" || typeof r.text !== "string") return null;
1759
+ function seqKey(prefix, node) {
1760
+ return `${prefix}:${typeof node.seq === "number" ? node.seq : "?"}`;
1761
+ }
1762
+ /** 节点 seq 随行(D1 折叠边界用;缺省不带)。 */
1763
+ function seqOf(node) {
1764
+ return typeof node.seq === "number" ? { seq: node.seq } : {};
1765
+ }
1766
+ function assistantParts(blocks) {
1767
+ const texts = [];
1768
+ const reasonings = [];
1769
+ let hasToolCall = false;
1770
+ if (Array.isArray(blocks)) for (const block of blocks) {
1771
+ if (typeof block !== "object" || block === null) continue;
1772
+ const b = block;
1773
+ if (b.kind === "text" && typeof b.text === "string") texts.push(b.text);
1774
+ else if (b.kind === "reasoning" && typeof b.text === "string") reasonings.push(b.text);
1775
+ else if (b.kind === "tool-call") hasToolCall = true;
1776
+ }
1603
1777
  return {
1604
- id: r.id,
1605
- sessionId: r.sessionId,
1606
- sideTitle: r.sideTitle,
1607
- text: r.text,
1608
- ...typeof r.question === "string" && r.question.trim() !== "" ? { question: r.question } : {},
1609
- createdAt: typeof r.createdAt === "number" ? r.createdAt : 0
1778
+ text: texts.join("\n\n"),
1779
+ reasoning: reasonings.join("\n\n"),
1780
+ hasToolCall
1610
1781
  };
1611
1782
  }
1612
- function createReflowStore(now = () => Date.now(), storage) {
1613
- let items = [];
1614
- let nextId = 1;
1615
- let version = 0;
1616
- const listeners = /* @__PURE__ */ new Set();
1617
- const store = storage === void 0 ? typeof localStorage !== "undefined" ? localStorage : null : storage;
1618
- if (store != null) try {
1619
- const keys = [];
1620
- for (let i = 0; i < store.length; i += 1) {
1621
- const key = store.key(i);
1622
- if (typeof key === "string" && key.startsWith(STORAGE_PREFIX$1)) keys.push(key);
1623
- }
1624
- for (const key of keys) {
1625
- const parsed = JSON.parse(store.getItem(key) ?? "null");
1626
- if (!Array.isArray(parsed)) continue;
1627
- for (const raw of parsed) {
1628
- const item = revive(raw);
1629
- if (item === null) continue;
1630
- items.push(item);
1631
- nextId = Math.max(nextId, item.id + 1);
1632
- }
1783
+ /**
1784
+ * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
1785
+ * (context 注入、未知面事件、已取消的重试等对读者无信息的节点)。
1786
+ */
1787
+ function nodeToMessage(node) {
1788
+ if (typeof node !== "object" || node === null) return null;
1789
+ const n = node;
1790
+ switch (n.kind) {
1791
+ case "user": return {
1792
+ key: seqKey("u", n),
1793
+ ...seqOf(n),
1794
+ role: "user",
1795
+ text: contentTextOf(n.content)
1796
+ };
1797
+ case "steering": return {
1798
+ key: seqKey("s", n),
1799
+ ...seqOf(n),
1800
+ role: "user",
1801
+ text: contentTextOf(n.content)
1802
+ };
1803
+ case "assistant": {
1804
+ const { text, reasoning, hasToolCall } = assistantParts(n.blocks);
1805
+ if (text === "" && reasoning === "" && hasToolCall) return null;
1806
+ return {
1807
+ key: seqKey("a", n),
1808
+ ...seqOf(n),
1809
+ role: "assistant",
1810
+ text,
1811
+ ...reasoning !== "" ? { reasoning } : {},
1812
+ ...n.interrupted === true ? { interrupted: true } : {}
1813
+ };
1633
1814
  }
1634
- } catch (error) {
1635
- console.warn("[dsh-sidenote] 回流持久化读取失败(按空起步):", error);
1636
- }
1637
- const persist = (sessionId) => {
1638
- if (store == null) return;
1639
- try {
1640
- const mine = items.filter((i) => i.sessionId === sessionId);
1641
- const key = STORAGE_PREFIX$1 + sessionId;
1642
- if (mine.length === 0) store.removeItem(key);
1643
- else store.setItem(key, JSON.stringify(mine));
1644
- } catch (error) {
1645
- console.warn("[dsh-sidenote] 回流持久化写入失败:", error);
1815
+ case "tool-result": {
1816
+ const call = n.call;
1817
+ const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : t("toolFallback");
1818
+ const text = truncateText(contentTextOf(n.content), TOOL_TEXT_LIMIT);
1819
+ const card = (n.callView ?? n.resultView) != null ? cardModelOf({
1820
+ toolName,
1821
+ callView: n.callView,
1822
+ resultView: n.resultView,
1823
+ rawText: text
1824
+ }) : cardModelFromNode({
1825
+ name: toolName,
1826
+ argsRaw: typeof call?.argsRaw === "string" ? call.argsRaw : void 0,
1827
+ meta: n.meta,
1828
+ rawText: text,
1829
+ ...n.isError === true ? { isError: true } : {}
1830
+ });
1831
+ return {
1832
+ key: seqKey("t", n),
1833
+ ...seqOf(n),
1834
+ role: "tool",
1835
+ toolName,
1836
+ text,
1837
+ card,
1838
+ ...n.isError === true ? { isError: true } : {}
1839
+ };
1646
1840
  }
1647
- };
1648
- const notify = () => {
1649
- version += 1;
1650
- for (const fn of [...listeners]) fn();
1651
- };
1652
- return {
1653
- getSnapshot: () => version,
1654
- subscribe(fn) {
1655
- listeners.add(fn);
1656
- return () => {
1657
- listeners.delete(fn);
1841
+ case "turn-error": return {
1842
+ key: seqKey("e", n),
1843
+ ...seqOf(n),
1844
+ role: "error",
1845
+ text: typeof n.message === "string" ? n.message : t("unknownError")
1846
+ };
1847
+ case "model-retry":
1848
+ if (n.retryState === "cancelled") return null;
1849
+ return {
1850
+ key: seqKey("r", n),
1851
+ ...seqOf(n),
1852
+ role: "notice",
1853
+ text: t(n.retryState === "started" ? "modelRetryStarted" : "modelRetryWaiting")
1658
1854
  };
1659
- },
1660
- add(sessionId, sideTitle, text, question) {
1661
- const item = {
1662
- id: nextId,
1663
- sessionId,
1664
- sideTitle,
1665
- text,
1666
- ...typeof question === "string" && question.trim() !== "" ? { question } : {},
1667
- createdAt: now()
1855
+ case "turn-max-tokens": return {
1856
+ key: seqKey("m", n),
1857
+ ...seqOf(n),
1858
+ role: "notice",
1859
+ text: t("maxTokens")
1860
+ };
1861
+ case "command": {
1862
+ const name = typeof n.name === "string" && n.name !== "" ? n.name : t("commandNameFallback");
1863
+ const args = typeof n.args === "string" ? n.args.trimStart() : "";
1864
+ return {
1865
+ key: seqKey("c", n),
1866
+ ...seqOf(n),
1867
+ role: "notice",
1868
+ text: t("runCommand", { cmd: `/${name}${args === "" ? "" : ` ${args}`}` })
1668
1869
  };
1669
- nextId += 1;
1670
- items = [...items, item];
1671
- persist(sessionId);
1672
- notify();
1673
- return item;
1674
- },
1675
- remove(id) {
1676
- const target = items.find((i) => i.id === id);
1677
- if (target === void 0) return;
1678
- items = items.filter((i) => i.id !== id);
1679
- persist(target.sessionId);
1680
- notify();
1681
- },
1682
- clearSession(sessionId) {
1683
- if (!items.some((i) => i.sessionId === sessionId)) return;
1684
- items = items.filter((i) => i.sessionId !== sessionId);
1685
- persist(sessionId);
1686
- notify();
1687
- },
1688
- list: (sessionId) => items.filter((i) => i.sessionId === sessionId)
1689
- };
1690
- }
1691
- //#endregion
1692
- //#region src/client/bridge.ts
1693
- const sideChatBridge = { current: null };
1694
- //#endregion
1695
- //#region src/client/chat/cards.ts
1696
- /** ContentBlock[] → 纯文本(transcript.ts 同款的极简版,避免互依赖)。 */
1697
- function blocksText(content) {
1698
- if (!Array.isArray(content)) return void 0;
1699
- const parts = [];
1700
- for (const block of content) {
1701
- const b = block;
1702
- if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
1870
+ }
1871
+ case "compaction": return {
1872
+ key: seqKey("k", n),
1873
+ ...seqOf(n),
1874
+ role: "notice",
1875
+ text: t("compacted")
1876
+ };
1877
+ default: return null;
1703
1878
  }
1704
- return parts.length === 0 ? void 0 : parts.join("\n");
1705
- }
1706
- /** 未知 card 值的 warn-once 登记(T3 纪律:降级不静默)。 */
1707
- const warnedCards = /* @__PURE__ */ new Set();
1708
- function warnUnknownCard(value) {
1709
- if (warnedCards.has(value)) return;
1710
- warnedCards.add(value);
1711
- console.warn(`[dsh-sidenote] 未知工具卡种类 "${value}"——降级为 generic 卡(宿主可能扩展了 card union)`);
1712
- }
1713
- /** 相对 cwd 按会话工作区折成绝对路径(UI bridge 责任——presentation 注释明文)。 */
1714
- function resolveCwd(cwd, cwdBase) {
1715
- if (cwd === void 0) return cwdBase;
1716
- if (cwd.startsWith("/") || /^[A-Za-z]:[\\/]/.test(cwd)) return cwd;
1717
- if (cwdBase === void 0) return cwd;
1718
- return `${cwdBase.replace(/\/+$/, "")}/${cwd}`;
1719
1879
  }
1720
1880
  /**
1721
- * 展示态路径缩短:>3 段的绝对路径只保末 3 段(≈仓库相对形态,与主区
1722
- * 「Read · dsh-sidenote/scripts/x.sh」同款)。窄栏标题位只够放语义尾部。
1881
+ * ConversationSnapshot → 渲染消息列表:终态节点 + 在途项(按 turn/step 归并)。
1882
+ * 快照缺省(未绑定)时为空列表。
1883
+ *
1884
+ * 时序归并(WI-01):在途项不再恒追加尾部——真实交错是「partial 文本 → 它
1885
+ * 自己 step 发出的工具卡」:partial 与同 step 的 runningCall 并存时必须文本
1886
+ * 在前(原文早于调用),不同 step 按 (turn, step) 升序;并行多 call 保持
1887
+ * 宿主 dispatch 序(同 turn/step 内稳定)。终态节点自带 seq 全序,不参与
1888
+ * 归并(在途项永远属于当前 turn 的前沿,尾部插入点天然正确)。
1723
1889
  */
1724
- function shortPath(p) {
1725
- if (!p.startsWith("/")) return p;
1726
- const segs = p.split("/").filter((s) => s !== "");
1727
- return segs.length > 3 ? segs.slice(-3).join("/") : p;
1890
+ function transcriptOf(snapshot) {
1891
+ if (snapshot === void 0 || snapshot === null) return [];
1892
+ const out = [];
1893
+ for (const node of snapshot.nodes ?? []) {
1894
+ const message = nodeToMessage(node);
1895
+ if (message !== null) out.push(message);
1896
+ }
1897
+ const inflight = [];
1898
+ const partial = snapshot.partial;
1899
+ if (partial !== void 0 && partial !== null) {
1900
+ const { text, reasoning, hasToolCall } = assistantParts(partial.blocks);
1901
+ if (text !== "" || reasoning !== "" || !hasToolCall) inflight.push({
1902
+ turn: typeof partial.turn === "number" ? partial.turn : Number.MAX_SAFE_INTEGER,
1903
+ step: typeof partial.step === "number" ? partial.step : Number.MAX_SAFE_INTEGER,
1904
+ order: 0,
1905
+ message: {
1906
+ key: "partial",
1907
+ role: "assistant",
1908
+ text,
1909
+ ...reasoning !== "" ? { reasoning } : {},
1910
+ streaming: true
1911
+ }
1912
+ });
1913
+ }
1914
+ if (Array.isArray(snapshot.runningCalls)) for (const call of snapshot.runningCalls) {
1915
+ if (typeof call !== "object" || call === null) continue;
1916
+ const c = call;
1917
+ const toolName = typeof c.name === "string" ? c.name : t("toolFallback");
1918
+ inflight.push({
1919
+ turn: typeof c.turn === "number" ? c.turn : Number.MAX_SAFE_INTEGER,
1920
+ step: typeof c.step === "number" ? c.step : Number.MAX_SAFE_INTEGER,
1921
+ order: 1,
1922
+ message: {
1923
+ key: `rc:${typeof c.callId === "string" ? c.callId : "?"}`,
1924
+ role: "tool",
1925
+ toolName,
1926
+ text: "",
1927
+ streaming: true,
1928
+ card: c.callView != null ? cardModelOf({
1929
+ toolName,
1930
+ callView: c.callView,
1931
+ resultView: null
1932
+ }) : cardModelFromNode({
1933
+ name: toolName,
1934
+ argsRaw: typeof c.argsRaw === "string" ? c.argsRaw : void 0,
1935
+ rawText: ""
1936
+ })
1937
+ }
1938
+ });
1939
+ }
1940
+ inflight.sort((a, b) => a.turn - b.turn || a.step - b.step || a.order - b.order);
1941
+ for (const item of inflight) out.push(item.message);
1942
+ return out;
1728
1943
  }
1729
1944
  /**
1730
- * 标题整形:「Tool /abs/path」→「Tool · 末3段」(主区同名同款);
1731
- * 裸绝对路径标题 → 末 3 段;不含路径的标题原样。
1945
+ * D1 父历史折叠的划分:seq <= boundarySeq 的消息为继承区(fork 时刻快照,
1946
+ * 折叠进指示卡),其余(含在途项——无 seq)为新鲜区。boundarySeq 缺省 =
1947
+ * 全新鲜(升级前老 Tab 的行为不变)。
1732
1948
  */
1733
- function displayTitle(title) {
1734
- const m = /^([A-Za-z][\w-]*) (\/.+)$/.exec(title);
1735
- const name = m?.[1];
1736
- const path = m?.[2];
1737
- if (name !== void 0 && path !== void 0) return `${name} · ${shortPath(path)}`;
1738
- return title.startsWith("/") ? shortPath(title) : title;
1949
+ function partitionInherited(messages, boundarySeq) {
1950
+ if (boundarySeq === void 0) return {
1951
+ inherited: [],
1952
+ fresh: [...messages]
1953
+ };
1954
+ const inherited = [];
1955
+ const fresh = [];
1956
+ for (const message of messages) if (message.seq !== void 0 && message.seq <= boundarySeq) inherited.push(message);
1957
+ else fresh.push(message);
1958
+ return {
1959
+ inherited,
1960
+ fresh
1961
+ };
1739
1962
  }
1740
- /** todo_write 入参形状校验(字段不齐即放弃 todo 卡)。两种数据源形状:
1741
- * 0.1.1 wire:callView.rawInput = todos 数组本身(dsh-tool-todo presentCall 实证);
1742
- * 0.1.2 argsRaw:{ todos: [...] }。 */
1743
- function todoItemsOf(raw) {
1744
- const list = Array.isArray(raw) ? raw : typeof raw === "object" && raw !== null ? raw.todos : void 0;
1745
- if (!Array.isArray(list) || list.length === 0) return void 0;
1746
- const items = [];
1747
- for (const it of list) {
1748
- const o = it;
1749
- if (typeof o?.content !== "string" || typeof o?.status !== "string") return void 0;
1750
- items.push({
1751
- content: o.content,
1752
- status: o.status
1753
- });
1963
+ //#endregion
1964
+ //#region src/client/sidechat/snapshot.ts
1965
+ function blocksText(blocks, kinds) {
1966
+ if (!Array.isArray(blocks)) return "";
1967
+ const parts = [];
1968
+ for (const block of blocks) {
1969
+ if (typeof block !== "object" || block === null) continue;
1970
+ const b = block;
1971
+ const kind = b.kind ?? b.type;
1972
+ if (kind !== void 0 && kinds.includes(kind) && typeof b.text === "string") parts.push(b.text);
1754
1973
  }
1755
- return items;
1974
+ return parts.join("\n");
1756
1975
  }
1757
- /** todo 卡标题:任务 · 非零状态组计数(主区「任务 · N 已完成 · …」同款)。 */
1758
- function todoTitleOf(items) {
1759
- let done = 0;
1760
- let doing = 0;
1761
- let pending = 0;
1762
- for (const it of items) if (it.status === "completed") done += 1;
1763
- else if (it.status === "in_progress") doing += 1;
1764
- else pending += 1;
1765
- const parts = [];
1766
- if (done > 0) parts.push(t("todoDone", { n: done }));
1767
- if (doing > 0) parts.push(t("todoDoing", { n: doing }));
1768
- if (pending > 0) parts.push(t("todoPending", { n: pending }));
1769
- return parts.length === 0 ? t("todoTitle") : `${t("todoTitle")} · ${parts.join(" · ")}`;
1976
+ function contentText(content) {
1977
+ return blocksText(content, ["text"]);
1770
1978
  }
1771
1979
  /**
1772
- * callView + resultView → 一张卡的视图模型。三层判别全在这里;
1773
- * 结果态优先(title 等字段 result 覆盖 call),缺省逐字段回退。
1980
+ * 投影 nodes → 当前 turn 的快照拍序列。纯函数(node 可测)。
1981
+ *
1982
+ * 当前 turn 起点 = 最后一个 user 节点(在飞 turn 的任务消息)。注意:压缩
1983
+ * 摘要在投影里也是 user 形态节点(<compacted-summary> 框定的替换消息)——
1984
+ * 若最后一个 user 节点是摘要,从它取起同样正确(摘要即主线模型此刻的上下文
1985
+ * 头部)。起点之后:assistant 拆思考/正文两拍,tool-result 成拍,其余节点
1986
+ * (context 注入、系统面)不入快照。
1774
1987
  */
1775
- function cardModelOf(input) {
1776
- const { callView, resultView } = input;
1777
- const call = callView ?? void 0;
1778
- const result = resultView ?? void 0;
1779
- const card = result?.card ?? call?.card ?? "generic";
1780
- switch (card) {
1781
- case "terminal": {
1782
- const rc = result?.card === "terminal" ? result : void 0;
1783
- const cc = call?.card === "terminal" ? call : void 0;
1784
- const command = rc?.title ?? cc?.title ?? input.toolName;
1785
- return {
1786
- kind: "terminal",
1787
- title: cc?.description !== void 0 ? `${displayToolName(input.toolName)} · ${cc.description}` : command,
1788
- command,
1789
- ...cc?.description !== void 0 ? { description: cc.description } : {},
1790
- ...resolveCwd(cc?.cwd, input.cwdBase) !== void 0 ? { cwd: resolveCwd(cc?.cwd, input.cwdBase) } : {},
1791
- ...rc?.output !== void 0 ? { output: rc.output } : {},
1792
- ...rc?.exitCode !== void 0 ? { exitCode: rc.exitCode } : {},
1793
- ...rc?.signal !== void 0 ? { signal: rc.signal } : {}
1794
- };
1795
- }
1796
- case "diff": {
1797
- const rc = result?.card === "diff" ? result : void 0;
1798
- const cc = call?.card === "diff" ? call : void 0;
1799
- return {
1800
- kind: "diff",
1801
- title: displayTitle(rc?.title ?? cc?.title ?? input.toolName),
1802
- diffs: rc?.diffs ?? cc?.diffs ?? [],
1803
- ...cc?.locations !== void 0 ? { locations: cc.locations } : {}
1804
- };
1805
- }
1806
- case "search": {
1807
- const rc = result?.card === "search" ? result : void 0;
1808
- if (rc === void 0) return genericModel(input, call, result);
1809
- if (rc.shape === "matches") return {
1810
- kind: "search",
1811
- title: rc.title ?? call?.title ?? input.toolName,
1812
- shape: "matches",
1813
- files: rc.files,
1814
- truncated: rc.truncated,
1815
- total: rc.total
1816
- };
1817
- if (rc.shape === "paths") return {
1818
- kind: "search",
1819
- title: rc.title ?? call?.title ?? input.toolName,
1820
- shape: "paths",
1821
- paths: rc.paths,
1822
- truncated: rc.truncated,
1823
- total: rc.total
1824
- };
1825
- warnUnknownCard(`search/${String(rc.shape)}`);
1826
- return genericModel(input, call, result);
1827
- }
1828
- case "read": {
1829
- const rc = result?.card === "read" ? result : void 0;
1830
- if (rc === void 0) return genericModel(input, call, result);
1831
- return {
1832
- kind: "read",
1833
- title: `Read · ${shortPath(rc.path)}`,
1834
- path: rc.path,
1835
- lines: rc.lines,
1836
- totalLines: rc.totalLines,
1837
- ...rc.lang !== void 0 ? { lang: rc.lang } : {}
1838
- };
1988
+ function snapshotEntriesOf(nodes, partial) {
1989
+ let start = -1;
1990
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
1991
+ const n = nodes[i];
1992
+ if (n?.kind === "user" || n?.kind === "steering") {
1993
+ start = i;
1994
+ break;
1839
1995
  }
1840
- case "web": {
1841
- const rc = result?.card === "web" ? result : void 0;
1842
- if (rc === void 0) return genericModel(input, call, result);
1843
- const base = {
1844
- kind: "web",
1845
- title: rc.title ?? call?.title ?? input.toolName
1846
- };
1847
- if (rc.kind === "search") {
1848
- const rs = rc;
1849
- return {
1850
- ...base,
1851
- webKind: "search",
1852
- sources: rs.sources,
1853
- truncated: rs.truncated,
1854
- ...rs.answer !== void 0 ? { answer: rs.answer } : {}
1855
- };
1996
+ }
1997
+ if (start < 0) return [];
1998
+ const entries = [];
1999
+ for (const node of nodes.slice(start)) {
2000
+ const n = node;
2001
+ switch (n.kind) {
2002
+ case "user":
2003
+ case "steering": {
2004
+ const text = contentText(n.content);
2005
+ if (text !== "") entries.push({
2006
+ kind: "request",
2007
+ text
2008
+ });
2009
+ break;
1856
2010
  }
1857
- if (rc.kind === "fetch") {
1858
- const rf = rc;
1859
- return {
1860
- ...base,
1861
- webKind: "fetch",
1862
- url: rf.url,
1863
- statusCode: rf.statusCode,
1864
- truncated: rf.truncated
1865
- };
2011
+ case "assistant": {
2012
+ const thinking = blocksText(n.blocks, ["reasoning"]);
2013
+ const reply = blocksText(n.blocks, ["text"]);
2014
+ if (thinking !== "") entries.push({
2015
+ kind: "thinking",
2016
+ text: thinking
2017
+ });
2018
+ if (reply !== "") entries.push({
2019
+ kind: "reply",
2020
+ text: reply
2021
+ });
2022
+ break;
2023
+ }
2024
+ case "tool-result": {
2025
+ const call = n.call;
2026
+ const name = typeof call?.name === "string" ? call.name : "tool";
2027
+ const args = typeof call?.argsRaw === "string" ? call.argsRaw : "";
2028
+ const result = contentText(n.content);
2029
+ entries.push({
2030
+ kind: "tool",
2031
+ toolName: name,
2032
+ text: args === "" ? result : `调用:${args}\n${result}`
2033
+ });
2034
+ break;
1866
2035
  }
1867
- warnUnknownCard(`web/${String(rc.kind)}`);
1868
- return genericModel(input, call, result);
1869
2036
  }
1870
- case "generic": return genericModel(input, call, result);
1871
- default:
1872
- warnUnknownCard(String(card));
1873
- return genericModel(input, call, result);
1874
2037
  }
2038
+ const streaming = typeof partial === "string" ? partial : contentText(partial?.blocks);
2039
+ if (streaming !== "") entries.push({
2040
+ kind: "streaming",
2041
+ text: streaming
2042
+ });
2043
+ return entries;
1875
2044
  }
1876
- function genericModel(input, call, result) {
1877
- const gc = call?.card === "generic" ? call : void 0;
1878
- const gr = result?.card === "generic" ? result : void 0;
1879
- if (input.toolName === "todo_write") {
1880
- const items = todoItemsOf(gc?.rawInput);
1881
- if (items !== void 0) return {
1882
- kind: "todo",
1883
- title: todoTitleOf(items),
1884
- items
1885
- };
2045
+ /** 拍序列 → 协议块文本(拼进首条侧边消息前缀的形态)。 */
2046
+ function serializeSnapshot(entries, takenAt) {
2047
+ const pad = (v) => String(v).padStart(2, "0");
2048
+ return `<mainline-progress-snapshot taken-at="${`${takenAt.getFullYear()}-${pad(takenAt.getMonth() + 1)}-${pad(takenAt.getDate())} ${pad(takenAt.getHours())}:${pad(takenAt.getMinutes())}`}">
2049
+ 你是从主线会话 fork 出的侧边会话。主线有一个任务正在执行中——它在你 fork 时正在进行,所以以「已中断」形态出现在上面的历史末尾(那只是快照切点,主线本体仍在正常运行)。以下是主线该任务截至此刻的实时进展(与主线模型当前可见的上下文一致):
2050
+
2051
+ ${entries.map((entry) => {
2052
+ switch (entry.kind) {
2053
+ case "request": return `<user-request>\n${entry.text}\n</user-request>`;
2054
+ case "reply": return `<reply>\n${entry.text}\n</reply>`;
2055
+ case "thinking": return `<thinking>\n${entry.text}\n</thinking>`;
2056
+ case "tool": return `<tool-call name="${entry.toolName ?? "tool"}">\n${entry.text}\n</tool-call>`;
2057
+ case "streaming": return `<streaming>\n${entry.text}\n(主线正在输出中)</streaming>`;
2058
+ }
2059
+ }).join("\n\n")}
2060
+ </mainline-progress-snapshot>`;
2061
+ }
2062
+ /** 协议块标记(WI-03 呈现层据此把快照从气泡提升到继承卡)。 */
2063
+ const SNAPSHOT_BLOCK_MARK = "<mainline-progress-snapshot";
2064
+ /**
2065
+ * 现取主线当前 turn 的进展快照块。无任何在飞内容(主线恰在两轮之间等)
2066
+ * 返回 null(调用方跳过拼接)。
2067
+ */
2068
+ function buildMainlineSnapshot(ctx, parentSessionId, now = /* @__PURE__ */ new Date()) {
2069
+ try {
2070
+ const snap = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy();
2071
+ if (snap === void 0 || snap === null) return null;
2072
+ const entries = snapshotEntriesOf(snap.nodes ?? [], snap.partial);
2073
+ if (entries.length === 0) return null;
2074
+ return serializeSnapshot(entries, now);
2075
+ } catch {
2076
+ return null;
1886
2077
  }
1887
- return {
1888
- kind: "generic",
1889
- title: displayTitle(gr?.title ?? gc?.title ?? input.toolName),
1890
- icon: gc?.kind ?? "other",
1891
- ...gc?.rawInput !== void 0 ? { rawInput: gc.rawInput } : {},
1892
- bodyText: blocksText(gr?.content) ?? blocksText(gc?.content) ?? input.rawText,
1893
- ...gc?.locations !== void 0 ? { locations: gc.locations } : {}
1894
- };
1895
2078
  }
1896
- /** 工具机器名 → 展示名(主区「Bash · …」的同款首字母大写)。 */
1897
- function displayToolName(name) {
1898
- return name.charAt(0).toUpperCase() + name.slice(1);
2079
+ //#endregion
2080
+ //#region src/client/sidechat/lifecycle.ts
2081
+ /**
2082
+ * 主线忙碌判定(busy-fork 路由)。2026-09-22 实证的宿主 fork 语义缺口:fork
2083
+ * 一个 turn 在飞的会话,会把在飞消息以「未认领的 inbox splice」形态遗传
2084
+ * 给子会话——子会话 boot 后把它当成自己的首个任务执行,用户真正发的侧边
2085
+ * 消息则排进不可见队列(用户视角 = 「发不了」+「主线最后一条消息被抄到
2086
+ * 侧边」)。busy 时改走 busy-fork 路径(fork 后监护中和遗传 turn,见
2087
+ * armInheritedTurnPurge)。
2088
+ *
2089
+ * busy = running(Session 快照顶层,0.1.1/0.1.2/0.1.5 同在)或 pending
2090
+ * 非空(审批/提问挂起:0.1.1 在 Session 快照顶层,0.1.2 起在
2091
+ * uiConversation.chat 的 legacy 切片——双面都读,同面板 R8 链)。
2092
+ * 探测失败一律按「空闲」放行(fork 错误面兜底),绝不因读不到状态而卡死。
2093
+ */
2094
+ function sessionBusyOf(ctx, sessionId) {
2095
+ try {
2096
+ const binding = ctx.sessions.binding(sessionId);
2097
+ const snap = binding?.session?.getSnapshot?.();
2098
+ if (snap?.running === true) return true;
2099
+ if ((snap?.pending?.length ?? 0) > 0) return true;
2100
+ return ((chatSourceOf(ctx, binding)?.getLegacy())?.pending?.length ?? 0) > 0;
2101
+ } catch {
2102
+ return false;
2103
+ }
1899
2104
  }
1900
- /** 结果正文尾标解析(宿主 parseExitStatus 的线格式)。 */
1901
- function parseExitStatus(text) {
1902
- const m = /\n\[(?:exit code: (\d+)|killed by signal: ([A-Z]+))\]\s*$/.exec(text);
1903
- if (m === null) return { output: text };
1904
- const output = text.slice(0, m.index).replace(/\n+$/, "");
1905
- if (m[1] !== void 0) return {
1906
- output,
1907
- exitCode: Number(m[1])
1908
- };
1909
- return {
1910
- output,
1911
- signal: m[2]
1912
- };
2105
+ /**
2106
+ * 监护中的子会话登记处:完成通知模块(notify.ts)据此抑制误报——遗传 turn
2107
+ * 被 cancel 的 running 翻转不是「侧边回复完成」。
2108
+ */
2109
+ const purgingChildren = /* @__PURE__ */ new Set();
2110
+ /** 该子会话当前是否处于遗传 turn 监护/清除窗口。 */
2111
+ function isPurgingInheritedTurn(childId) {
2112
+ return purgingChildren.has(childId);
1913
2113
  }
1914
- /** FsReadMeta 形状+语义校验(readMetaFromMeta 的忠实子集)。 */
1915
- function readMetaOf(meta) {
1916
- if (typeof meta !== "object" || meta === null) return void 0;
1917
- const m = meta;
1918
- if (typeof m.path !== "string" || !Array.isArray(m.lines) || typeof m.totalLines !== "number") return void 0;
1919
- if (!Number.isInteger(m.totalLines) || m.totalLines < 0) return void 0;
1920
- let prev = 0;
1921
- const lines = [];
1922
- for (const raw of m.lines) {
1923
- const l = raw;
1924
- if (typeof l?.number !== "number" || typeof l?.text !== "string") return void 0;
1925
- if (!Number.isInteger(l.number) || l.number <= prev || l.number > m.totalLines) return void 0;
1926
- prev = l.number;
1927
- lines.push({
1928
- number: l.number,
1929
- text: l.text
1930
- });
2114
+ /** 读父会话当前最大节点 seq(折叠边界;读不到不阻断)。 */
2115
+ function readMaxNodeSeq(ctx, sessionId) {
2116
+ try {
2117
+ const snap = chatSourceOf(ctx, ctx.sessions.binding(sessionId))?.getLegacy();
2118
+ let max = -1;
2119
+ for (const node of snap?.nodes ?? []) {
2120
+ const seq = node.seq;
2121
+ if (typeof seq === "number" && seq > max) max = seq;
2122
+ }
2123
+ return max >= 0 ? max : void 0;
2124
+ } catch {
2125
+ return;
1931
2126
  }
1932
- return {
1933
- path: m.path,
1934
- lines,
1935
- totalLines: m.totalLines,
1936
- ...typeof m.lang === "string" ? { lang: m.lang } : {}
1937
- };
1938
2127
  }
1939
2128
  /**
1940
- * FsDiffMeta 校验(0.1.2 diff 卡,dsh-tool-fs computeHunkDiffs 产物的忠实子集)。
1941
- * 返回值三态:{diffs 非空} / 'empty'(空数组——write create/同内容覆写是合法值,
1942
- * 客户端回退 args 整文件 diff)/ undefined(malformed——降级)。
1943
- * 逐项校验,一项违规整卡降级(官方 narrowDiffs 同款)。
2129
+ * 读 fork 折叠边界 = 父会话「在飞 turn 之前」的最大节点 seq。
2130
+ * busy 时在飞 turn 的节点(最后一条 user/steering 起)不随 fork 继承,必须
2131
+ * 排除——否则边界会越过遗传消息在子会话里的 seq,监护把泄漏误判成「继承
2132
+ * 内容」而漏 cancel(2026-09-23 真机实证:sleep 240 在子会话里跑起来了)。
1944
2133
  */
1945
- function diffMetaOf(meta) {
1946
- if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
1947
- const m = meta;
1948
- if (!Array.isArray(m.diffs)) return void 0;
1949
- if (m.diffs.length === 0) return "empty";
1950
- const diffs = [];
1951
- for (const raw of m.diffs) {
1952
- const d = raw;
1953
- if (typeof d?.path !== "string") return void 0;
1954
- if (d.oldText !== null && typeof d.oldText !== "string") return void 0;
1955
- if (typeof d.newText !== "string") return void 0;
1956
- diffs.push({
1957
- path: d.path,
1958
- oldText: d.oldText,
1959
- newText: d.newText
1960
- });
2134
+ function readInheritedBoundary(ctx, sessionId, busy) {
2135
+ if (!busy) return readMaxNodeSeq(ctx, sessionId);
2136
+ try {
2137
+ const nodes = chatSourceOf(ctx, ctx.sessions.binding(sessionId))?.getLegacy()?.nodes ?? [];
2138
+ let endExclusive = nodes.length;
2139
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
2140
+ const kind = nodes[i]?.kind;
2141
+ if (kind === "user" || kind === "steering") {
2142
+ endExclusive = i;
2143
+ break;
2144
+ }
2145
+ }
2146
+ let max = -1;
2147
+ for (const node of nodes.slice(0, endExclusive)) {
2148
+ const seq = node.seq;
2149
+ if (typeof seq === "number" && seq > max) max = seq;
2150
+ }
2151
+ return max >= 0 ? max : void 0;
2152
+ } catch {
2153
+ return;
1961
2154
  }
1962
- return { diffs };
1963
2155
  }
1964
- /** SearchMeta 校验(0.1.2 search 卡;truncated/total 是硬字段;空结果合法)。 */
1965
- function searchMetaOf(meta) {
1966
- if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
1967
- const m = meta;
1968
- if (typeof m.truncated !== "boolean") return void 0;
1969
- if (typeof m.total !== "number" || !Number.isInteger(m.total) || m.total < 0) return void 0;
1970
- if (m.shape === "matches") {
1971
- if (!Array.isArray(m.files)) return void 0;
1972
- const files = [];
1973
- for (const rawF of m.files) {
1974
- const f = rawF;
1975
- if (typeof f?.path !== "string" || !Array.isArray(f.matches)) return void 0;
1976
- const matches = [];
1977
- for (const rawM of f.matches) {
1978
- const mm = rawM;
1979
- if (typeof mm?.lineNumber !== "number" || !Number.isInteger(mm.lineNumber) || mm.lineNumber < 1) return void 0;
1980
- if (typeof mm.line !== "string") return void 0;
1981
- matches.push({
1982
- lineNumber: mm.lineNumber,
1983
- line: mm.line
1984
- });
2156
+ /** fork-unavailable 判定(主线没有任何已完成 turn)。 */
2157
+ function isForkUnavailable(error) {
2158
+ const text = error instanceof Error ? `${error.name} ${error.message}` : String(error);
2159
+ return /fork-unavailable|no completed turn/.test(text);
2160
+ }
2161
+ /**
2162
+ * 读主线在飞 turn 的任务消息前缀(监护的判别依据):父投影最后一个
2163
+ * user/steering 节点即它。截 200 字符入 meta(够判别即可,meta 走
2164
+ * localStorage,不扛全文)。读不到 = undefined(监护退化为纯时序判别)。
2165
+ */
2166
+ function readInflightUserPrefix(ctx, parentSessionId) {
2167
+ try {
2168
+ const nodes = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy()?.nodes ?? [];
2169
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
2170
+ const n = nodes[i];
2171
+ if (n?.kind === "user" || n?.kind === "steering") {
2172
+ const text = contentTextOf(n.content);
2173
+ if (text !== "") return text.slice(0, 200);
1985
2174
  }
1986
- files.push({
1987
- path: f.path,
1988
- matches
1989
- });
1990
2175
  }
1991
- return {
1992
- shape: "matches",
1993
- files,
1994
- truncated: m.truncated,
1995
- total: m.total
1996
- };
1997
- }
1998
- if (m.shape === "paths") {
1999
- if (!Array.isArray(m.paths) || !m.paths.every((p) => typeof p === "string")) return void 0;
2000
- return {
2001
- shape: "paths",
2002
- paths: m.paths,
2003
- truncated: m.truncated,
2004
- total: m.total
2005
- };
2006
- }
2176
+ } catch {}
2007
2177
  }
2008
- /** WebSearchMeta 校验(sources 逐项 url 必填、title/snippet 可选;publishedAt 校验后丢弃)。 */
2009
- function webSearchMetaOf(meta) {
2010
- if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
2011
- const m = meta;
2012
- if (typeof m.truncated !== "boolean") return void 0;
2013
- if (!Array.isArray(m.sources)) return void 0;
2014
- const sources = [];
2015
- for (const raw of m.sources) {
2016
- const s = raw;
2017
- if (typeof s?.url !== "string") return void 0;
2018
- if (s.title !== void 0 && typeof s.title !== "string") return void 0;
2019
- if (s.snippet !== void 0 && typeof s.snippet !== "string") return void 0;
2020
- if (s.publishedAt !== void 0 && typeof s.publishedAt !== "string") return void 0;
2021
- sources.push({
2022
- url: s.url,
2023
- ...s.title !== void 0 ? { title: s.title } : {},
2024
- ...s.snippet !== void 0 ? { snippet: s.snippet } : {}
2025
- });
2026
- }
2027
- if (m.answer !== void 0 && typeof m.answer !== "string") return void 0;
2028
- return {
2029
- sources,
2030
- truncated: m.truncated,
2031
- ...m.answer !== void 0 ? { answer: m.answer } : {}
2032
- };
2178
+ /**
2179
+ * 零完成轮兜底:主线首轮在飞时 fork 不可用(fork-unavailable)——退化为
2180
+ * create 一个纯快照会话(无继承历史,全部上下文由 WI-02 的进展快照供给)。
2181
+ * sessions.create 是 off-face(client 契约面只有 fork/binding/scope/open),
2182
+ * 探测不到就抛错(面板转 fork-error 态)。
2183
+ */
2184
+ async function createSideSession(ctx, parentSessionId) {
2185
+ const svc = ctx.sessions;
2186
+ if (typeof svc.create !== "function") throw new Error("宿主 sessions.create 面缺席,无法创建快照会话");
2187
+ let cwd;
2188
+ try {
2189
+ const summary = ctx.sessions.list.getSnapshot().byId?.[parentSessionId];
2190
+ cwd = typeof summary?.cwd === "string" ? summary.cwd : void 0;
2191
+ } catch {}
2192
+ return svc.create(cwd === void 0 ? {} : { cwd });
2033
2193
  }
2034
- /** WebFetchMeta 校验(url + 整数 statusCode + truncated 硬字段)。 */
2035
- function webFetchMetaOf(meta) {
2036
- if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
2037
- const m = meta;
2038
- if (typeof m.url !== "string") return void 0;
2039
- if (typeof m.statusCode !== "number" || !Number.isInteger(m.statusCode)) return void 0;
2040
- if (typeof m.truncated !== "boolean") return void 0;
2041
- return {
2042
- url: m.url,
2043
- statusCode: m.statusCode,
2044
- truncated: m.truncated
2194
+ /**
2195
+ * 遗传 turn 监护(busy fork 的核心中和手段):busy fork 的子会话日志带着
2196
+ * 主线在飞消息的未认领 inbox insert(宿主 fork cut 缺陷),boot 后会被认领
2197
+ * 执行。子会话 fork 后是休眠的(实测不 open 不 prompt 不跑)。
2198
+ *
2199
+ * 信号面(实证教训,勿回退):Session 控制快照的 running 位由管理器的
2200
+ * 列表/活动通道驱动,**非在屏会话不实时翻牌**——watch 它永远等不到 true。
2201
+ * 可靠信号是 chat 投影(openSessionWindow 打开的事件窗驱动):泄漏消息只在
2202
+ * 被认领(= 遗传 turn 开跑)后才落成 user 节点——**该节点在子会话投影中
2203
+ * 出现即 turn 已开跑**,此刻 cancel 恰落在首个 LLM 响应窗口内(≥1s),
2204
+ * 工具来不及执行。
2205
+ *
2206
+ * 判别(双门,防误伤):先过 seq 门——继承节点的 seq 与父会话原值一致
2207
+ * (fork 前缀逐字拷贝),只有 seq > forkBoundarySeq 的用户节点才是 fork 后
2208
+ * 新增的(继承历史里的用户消息不参与判别);再过文本门——expectedPrefix
2209
+ * 在场时节点的文本须命中 fork 时刻记下的在飞消息前缀,未命中(宿主已修好
2210
+ * cut 缺陷/用户自己的首个侧边消息)即解除,绝不误 cancel。前缀缺席
2211
+ * (读取失败)退化为纯 seq 门判别。
2212
+ *
2213
+ * 监护一次性:settle(cancel 完成/判别未命中/用户自己的首条消息带快照前缀)
2214
+ * 后解除并把 meta 标 inheritedPurged——面板重挂载(关 tab 重开)靠该标记
2215
+ * 跳过重复监护。cancel 后推进折叠边界覆盖 aborted turn(界面无感)。
2216
+ * **不设静置超时**:子会话休眠到用户首条消息才认领 inbox(真机实证),
2217
+ * 泄漏 turn 可能在任意晚的时刻开跑——超时撤防等于把洞留给慢用户。
2218
+ * 幂等:同一 childId 重复调用只监护一次(purgingChildren 守卫)。
2219
+ */
2220
+ function armInheritedTurnPurge(ctx, childId, parentSessionId, tabId, expectedPrefix, forkBoundarySeq) {
2221
+ if (purgingChildren.has(childId)) return;
2222
+ purgingChildren.add(childId);
2223
+ let settled = false;
2224
+ const offs = [];
2225
+ const markPurged = () => {
2226
+ updateTabMeta(ctx, parentSessionId, tabId, (cur) => ({
2227
+ ...cur,
2228
+ inheritedPurged: true
2229
+ }));
2230
+ };
2231
+ const disarm = () => {
2232
+ if (settled) return;
2233
+ settled = true;
2234
+ for (const off of offs) off();
2235
+ purgingChildren.delete(childId);
2236
+ markPurged();
2237
+ };
2238
+ /**
2239
+ * 子会话投影里「fork 后新增」的用户消息文本(无 = ''):seq ≤ fork 边界的
2240
+ * 节点全是继承历史(seq 随 fork 前缀逐字拷贝),直接排除。
2241
+ */
2242
+ const freshUserText = () => {
2243
+ try {
2244
+ const nodes = chatSourceOf(ctx, ctx.sessions.binding(childId))?.getLegacy()?.nodes ?? [];
2245
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
2246
+ const n = nodes[i];
2247
+ if (n?.kind !== "user" && n?.kind !== "steering") continue;
2248
+ if (forkBoundarySeq !== void 0 && typeof n.seq === "number" && n.seq <= forkBoundarySeq) return "";
2249
+ return contentTextOf(n.content);
2250
+ }
2251
+ } catch {}
2252
+ return "";
2045
2253
  };
2046
- }
2047
- /** argsRaw(JSON 字符串)里尽力提取展示字段。 */
2048
- function argsSummary(argsRaw) {
2049
- if (argsRaw === void 0 || argsRaw === "") return {};
2050
2254
  try {
2051
- const parsed = JSON.parse(argsRaw);
2052
- if (typeof parsed !== "object" || parsed === null) return {};
2053
- const a = parsed;
2054
- return {
2055
- ...typeof a.command === "string" ? { command: a.command } : {},
2056
- ...typeof a.description === "string" ? { description: a.description } : {},
2057
- ...typeof a.file_path === "string" ? { path: a.file_path } : typeof a.path === "string" ? { path: a.path } : {},
2058
- ...Array.isArray(a.todos) ? { todos: a.todos } : {},
2059
- ...typeof a.content === "string" ? { content: a.content } : {},
2060
- ...typeof a.old_string === "string" ? { oldString: a.old_string } : {},
2061
- ...typeof a.new_string === "string" ? { newString: a.new_string } : {},
2062
- ...typeof a.pattern === "string" ? { pattern: a.pattern } : {},
2063
- ...typeof a.include === "string" ? { include: a.include } : {},
2064
- ...Array.isArray(a.queries) && a.queries.every((q) => typeof q === "string") ? { queries: a.queries } : {},
2065
- ...typeof a.url === "string" ? { url: a.url } : {}
2066
- };
2255
+ const binding = ctx.sessions.binding(childId);
2256
+ const session = binding?.session;
2257
+ const cancelFace = session;
2258
+ const chat = binding === void 0 ? void 0 : chatSourceOf(ctx, binding);
2259
+ if (session === void 0 || typeof cancelFace?.cancel !== "function" || chat === void 0) {
2260
+ console.warn("[dsh-sidenote] 子会话 cancel/chat 面缺席,遗传 turn 监护未启用:", childId, "session:", session !== void 0, "chat:", chat !== void 0);
2261
+ purgingChildren.delete(childId);
2262
+ return;
2263
+ }
2264
+ const cancelChild = cancelFace.cancel;
2265
+ offs.push(chat.subscribe(() => {
2266
+ if (settled) return;
2267
+ const text = freshUserText();
2268
+ if (text === "") return;
2269
+ if (text.startsWith("<mainline-progress-snapshot")) {
2270
+ disarm();
2271
+ return;
2272
+ }
2273
+ if (expectedPrefix !== void 0 && !text.startsWith(expectedPrefix)) {
2274
+ disarm();
2275
+ return;
2276
+ }
2277
+ settled = true;
2278
+ for (const off of offs) off();
2279
+ cancelChild.call(session).catch((error) => console.warn("[dsh-sidenote] 遗传 turn 取消失败:", error)).finally(() => {
2280
+ purgingChildren.delete(childId);
2281
+ markPurged();
2282
+ advanceBoundaryPastTail(ctx, parentSessionId, tabId, childId);
2283
+ reconcileChildQueueAfterCancel(ctx, childId, expectedPrefix);
2284
+ });
2285
+ }));
2067
2286
  } catch {
2068
- return {};
2287
+ purgingChildren.delete(childId);
2069
2288
  }
2070
2289
  }
2071
2290
  /**
2072
- * 0.1.2 推导路径:节点原始字段 → ToolCardModel。与 cardModelOf(0.1.1 wire
2073
- * 面)平级;选择点在 transcript 层(节点带不带 callView 一眼可分)。
2291
+ * cancel 后队列和解(真机实证:宿主 cancel 保留 inbox 但不自动续跑,且
2292
+ * 休眠子会话的队列要等 agent 暖机才物化——认领前剔除对休眠子会话不可行)。
2293
+ * 等 turn 落定(running 翻 false)后读队列:
2294
+ * - 命中在飞前缀的项 = 没跑赢 claim 的泄漏/主线积压 → remove;
2295
+ * - 带快照前缀的项 = 用户的真实侧边消息 → remove + 原样重投(内容含快照
2296
+ * 前缀,remove 先行保证不重影),让它真正跑起来。
2297
+ * 两者皆无(主线积压、用户的快手第二条)→ 留在队列里不动(保守不删用户文字)。
2074
2298
  */
2075
- function cardModelFromNode(input) {
2076
- const args = argsSummary(input.argsRaw);
2077
- const name = input.name;
2078
- if (name === "bash" || name === "bash-persistent" || name === "pwsh" || name === "pwsh-persistent") {
2079
- const { output, exitCode, signal } = parseExitStatus(input.rawText);
2080
- const command = args.command ?? name;
2081
- return {
2082
- kind: "terminal",
2083
- title: args.description !== void 0 ? `${displayToolName(name)} · ${args.description}` : command,
2084
- command,
2085
- ...args.description !== void 0 ? { description: args.description } : {},
2086
- output,
2087
- ...exitCode !== void 0 ? { exitCode } : {},
2088
- ...signal !== void 0 ? { signal } : {}
2089
- };
2299
+ function reconcileChildQueueAfterCancel(ctx, childId, expectedPrefix) {
2300
+ let attempts = 0;
2301
+ const tick = () => {
2302
+ attempts += 1;
2303
+ try {
2304
+ const session = ctx.sessions.binding(childId)?.session;
2305
+ const snap = session?.getSnapshot?.();
2306
+ if (session === void 0 || snap === void 0 || snap === null || typeof session.updateQueue !== "function" || typeof session.prompt !== "function") return;
2307
+ if (snap.running === true) {
2308
+ if (attempts < 8) setTimeout(tick, 400);
2309
+ return;
2310
+ }
2311
+ const updateQueue = session.updateQueue.bind(session);
2312
+ const prompt = session.prompt.bind(session);
2313
+ for (const item of snap.queue ?? []) {
2314
+ if (typeof item?.id !== "string") continue;
2315
+ const text = typeof item.text === "string" ? item.text : contentTextOf(item.content);
2316
+ const isLeak = expectedPrefix !== void 0 && text.startsWith(expectedPrefix);
2317
+ const isUserReal = text.startsWith(SNAPSHOT_BLOCK_MARK);
2318
+ if (!isLeak && !isUserReal) continue;
2319
+ const itemId = item.id;
2320
+ const content = item.content;
2321
+ updateQueue(itemId, { kind: "remove" }).then(() => {
2322
+ if (!isUserReal) return void 0;
2323
+ return prompt(content, "queue");
2324
+ }).catch((error) => console.warn("[dsh-sidenote] 取消后队列和解失败:", error));
2325
+ }
2326
+ } catch {}
2327
+ };
2328
+ setTimeout(tick, 300);
2329
+ }
2330
+ /**
2331
+ * cancel 后把折叠边界推进到子会话内容尾(覆盖 aborted turn)。中断 turn 的
2332
+ * 冻结节点带小数 seq(宿主 fork 文档实证),故 boundarySeq 允许非整数。
2333
+ * 等 running 翻 false 且节点落定再读;最多重试 ~3s,失败不阻断(aborted
2334
+ * turn 会短暂可见,属可接受降级)。
2335
+ */
2336
+ function advanceBoundaryPastTail(ctx, parentSessionId, tabId, childId) {
2337
+ let attempts = 0;
2338
+ const tick = () => {
2339
+ attempts += 1;
2340
+ try {
2341
+ const max = (ctx.sessions.binding(childId)?.session.getSnapshot())?.running === true ? void 0 : readMaxNodeSeq(ctx, childId);
2342
+ if (max !== void 0) {
2343
+ updateTabMeta(ctx, parentSessionId, tabId, (cur) => ({
2344
+ ...cur,
2345
+ boundarySeq: Math.max(cur.boundarySeq ?? -1, max)
2346
+ }));
2347
+ return;
2348
+ }
2349
+ } catch {}
2350
+ if (attempts < 8) setTimeout(tick, 400);
2351
+ };
2352
+ setTimeout(tick, 300);
2353
+ }
2354
+ /**
2355
+ * 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
2356
+ * 归档隐藏出会话列表(best-effort)→ 模型跟随主会话(best-effort)。
2357
+ * fork 失败抛错(面板组件转错误态);后两步失败只告警不阻断。
2358
+ *
2359
+ * 主线 turn 在飞(busy)时走 busy-fork(Delivery_04):立即 fork + 遗传
2360
+ * turn 监护 + meta 打 forkedMidTurn 标记(首条侧边消息拼进展快照);
2361
+ * 主线零完成轮(fork-unavailable)退化为 create 纯快照会话(snapshotOnly)。
2362
+ * @returns fork/创建出的子会话 id。
2363
+ */
2364
+ async function forkAndRegister(ctx, parentSessionId, tabId, signal) {
2365
+ if (signal?.aborted === true) throw new DOMException("side chat fork aborted", "AbortError");
2366
+ const busy = sessionBusyOf(ctx, parentSessionId);
2367
+ const boundarySeq = readInheritedBoundary(ctx, parentSessionId, busy);
2368
+ const leakedPromptPrefix = busy ? readInflightUserPrefix(ctx, parentSessionId) : void 0;
2369
+ const owner = rootContext(ctx);
2370
+ let forked;
2371
+ let snapshotOnly = false;
2372
+ if (!busy) forked = await ctx.sessions.fork({ sessionId: parentSessionId });
2373
+ else {
2374
+ try {
2375
+ forked = await ctx.sessions.fork({ sessionId: parentSessionId });
2376
+ } catch (error) {
2377
+ if (!isForkUnavailable(error)) throw error;
2378
+ forked = await createSideSession(owner, parentSessionId);
2379
+ snapshotOnly = true;
2380
+ }
2381
+ if (!snapshotOnly) armInheritedTurnPurge(ctx, forked, parentSessionId, tabId, leakedPromptPrefix, boundarySeq);
2090
2382
  }
2091
- if (name === "read") {
2092
- const meta = readMetaOf(input.meta);
2093
- if (meta !== void 0) return {
2094
- kind: "read",
2095
- title: `Read · ${shortPath(meta.path)}`,
2096
- path: meta.path,
2097
- lines: meta.lines,
2098
- totalLines: meta.totalLines,
2099
- ...meta.lang !== void 0 ? { lang: meta.lang } : {}
2100
- };
2383
+ updateTabMeta(ctx, parentSessionId, tabId, (current) => ({
2384
+ ...current,
2385
+ childId: forked,
2386
+ parentSessionId,
2387
+ ...boundarySeq !== void 0 ? { boundarySeq } : {},
2388
+ ...busy ? { forkedMidTurn: true } : {},
2389
+ ...snapshotOnly ? { snapshotOnly: true } : {},
2390
+ ...leakedPromptPrefix !== void 0 ? { leakedPromptPrefix } : {}
2391
+ }));
2392
+ try {
2393
+ await owner.workspaces.archiveSession(forked);
2394
+ } catch (error) {
2395
+ console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
2101
2396
  }
2102
- if (name === "todo_write") {
2103
- const items = todoItemsOf(args.todos);
2104
- if (items !== void 0) return {
2105
- kind: "todo",
2106
- title: todoTitleOf(items),
2107
- items
2108
- };
2397
+ try {
2398
+ const selection = await readModelSelection(owner, parentSessionId);
2399
+ if (selection === void 0) console.warn("[dsh-sidenote] 同步主会话模型跳过:主机模型 API 不可用");
2400
+ else if (!await writeModelSelection(owner, forked, selection)) console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型)");
2401
+ } catch (error) {
2402
+ console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
2109
2403
  }
2110
- if ((name === "write" || name === "edit") && input.isError !== true) {
2111
- const path = args.path;
2112
- if (path !== void 0 && path.trim() !== "") {
2113
- const dm = diffMetaOf(input.meta);
2114
- if (dm !== void 0 && dm !== "empty") return {
2115
- kind: "diff",
2116
- title: displayTitle(`${name === "write" ? "Write" : "Edit"} ${path}`),
2117
- diffs: dm.diffs,
2118
- locations: [{ path }]
2119
- };
2120
- if (name === "write" && args.content !== void 0) return {
2121
- kind: "diff",
2122
- title: displayTitle(`Write ${path}`),
2123
- diffs: [{
2124
- path,
2125
- oldText: null,
2126
- newText: args.content
2127
- }],
2128
- locations: [{ path }]
2129
- };
2404
+ return forked;
2405
+ }
2406
+ /**
2407
+ * Tab meta 读-改-写。双腿:直连原生腿的 meta 在自有 metaStore(原生无
2408
+ * updateTab 面,布局 memory-only);legacy 腿在布局快照 + betterSidebar.updateTab。
2409
+ * 分腿判据统一为 directNativeLeg(对抗性审查 m1:判据单点化——metaStore
2410
+ * 有无记录不能再当判据,否则记录缺失时 childId 登记会静默落到 no-op 的
2411
+ * legacy 面)。直连腿记录缺失 = 异常态(reconcile 应先已建):warn 并 no-op。
2412
+ */
2413
+ function updateTabMeta(ctx, sessionId, tabId, mutate) {
2414
+ if (directNativeLeg(ctx)) {
2415
+ const direct = readSideChatMeta(sessionId, tabId);
2416
+ if (direct === void 0) {
2417
+ console.warn("[dsh-sidenote] meta 记录缺失(reconcile 应先已建),本次 meta 写入跳过:", tabId);
2418
+ return;
2130
2419
  }
2420
+ const next = mutate(parseSideChatMeta(direct));
2421
+ writeSideChatMeta({
2422
+ tabId,
2423
+ sessionId: direct.sessionId,
2424
+ ...next.childId !== void 0 ? { childId: next.childId } : {},
2425
+ ...next.parentSessionId !== void 0 ? { parentSessionId: next.parentSessionId } : {},
2426
+ ...next.pendingDraft !== void 0 ? { pendingDraft: next.pendingDraft } : {},
2427
+ ...next.boundarySeq !== void 0 ? { boundarySeq: next.boundarySeq } : {},
2428
+ ...next.forkedMidTurn === true ? { forkedMidTurn: true } : {},
2429
+ ...next.snapshotOnly === true ? { snapshotOnly: true } : {},
2430
+ ...next.leakedPromptPrefix !== void 0 ? { leakedPromptPrefix: next.leakedPromptPrefix } : {},
2431
+ ...next.inheritedPurged === true ? { inheritedPurged: true } : {},
2432
+ number: direct.number,
2433
+ createdAt: direct.createdAt,
2434
+ ...direct.runId !== void 0 ? { runId: direct.runId } : {}
2435
+ });
2436
+ return;
2131
2437
  }
2132
- if ((name === "grep" || name === "glob") && input.isError !== true) {
2133
- const sm = searchMetaOf(input.meta);
2134
- const wantShape = name === "grep" ? "matches" : "paths";
2135
- if (sm !== void 0 && sm.shape === wantShape && args.pattern !== void 0) {
2136
- const where = args.path !== void 0 && args.path.trim() !== "" ? ` in ${shortPath(args.path)}` : "";
2137
- const inc = name === "grep" && args.include !== void 0 && args.include.trim() !== "" ? ` (${args.include})` : "";
2138
- const title = `${name === "grep" ? "Grep" : "Glob"} ${args.pattern}${where}${inc}`;
2139
- return sm.shape === "matches" ? {
2140
- kind: "search",
2141
- title,
2142
- shape: "matches",
2143
- files: sm.files,
2144
- truncated: sm.truncated,
2145
- total: sm.total
2146
- } : {
2147
- kind: "search",
2148
- title,
2149
- shape: "paths",
2150
- paths: sm.paths,
2151
- truncated: sm.truncated,
2152
- total: sm.total
2153
- };
2438
+ const bs = betterSidebarOf(ctx);
2439
+ if (bs === void 0) return;
2440
+ const current = parseSideChatMeta(readTab(ctx, sessionId, tabId)?.meta);
2441
+ bs.updateTab(tabId, { meta: mutate(current) });
2442
+ }
2443
+ /** 关闭侧聊 tab:直连腿走 ISidebarRight.close,legacy 走 betterSidebar.closeTab。 */
2444
+ function closeSideTab(ctx, tabId, sessionId) {
2445
+ if (directNativeLeg(ctx)) {
2446
+ const face = sidebarRightOf(ctx);
2447
+ if (face !== void 0) {
2448
+ face.close(tabId);
2449
+ return;
2154
2450
  }
2155
2451
  }
2156
- if (name === "web_search" && input.isError !== true) {
2157
- const wm = webSearchMetaOf(input.meta);
2158
- const queries = args.queries?.filter((q) => q.trim() !== "") ?? [];
2159
- if (wm !== void 0 && queries.length > 0) return {
2160
- kind: "web",
2161
- title: queries.join(", "),
2162
- webKind: "search",
2163
- sources: wm.sources,
2164
- truncated: wm.truncated,
2165
- ...wm.answer !== void 0 ? { answer: wm.answer } : {}
2166
- };
2167
- }
2168
- if (name === "web_fetch" && input.isError !== true) {
2169
- const wm = webFetchMetaOf(input.meta);
2170
- if (wm !== void 0 && args.url !== void 0 && args.url.trim() !== "") return {
2171
- kind: "web",
2172
- title: args.url,
2173
- webKind: "fetch",
2174
- url: wm.url,
2175
- statusCode: wm.statusCode,
2176
- truncated: wm.truncated
2177
- };
2178
- }
2179
- return {
2180
- kind: "generic",
2181
- title: args.path !== void 0 ? `${name} · ${shortPath(args.path)}` : args.command ?? name,
2182
- icon: KIND_BY_NAME[name] ?? "other",
2183
- bodyText: input.rawText === "" ? void 0 : input.rawText
2184
- };
2452
+ betterSidebarOf(ctx)?.closeTab(tabId, { sessionId });
2185
2453
  }
2186
- /** 工具名 → 图标类别的静态映射(0.1.2 没有 callView.kind 可用)。
2187
- * wire 名实证:str_replace_editor 是下划线(uitool client.js L381);
2188
- * web_search 的 0.1.1 wire kind 是 'search'(presentSearchCall),
2189
- * 'fetch' 是 web_fetch 的(presentFetchCall)。 */
2190
- const KIND_BY_NAME = {
2191
- read: "read",
2192
- write: "edit",
2193
- edit: "edit",
2194
- str_replace_editor: "edit",
2195
- bash: "execute",
2196
- "bash-persistent": "execute",
2197
- pwsh: "execute",
2198
- "pwsh-persistent": "execute",
2199
- grep: "search",
2200
- glob: "search",
2201
- web_search: "search",
2202
- web_fetch: "fetch"
2203
- };
2204
- //#endregion
2205
- //#region src/client/chat/transcript.ts
2206
- /** 工具结果正文截断上限(面板是窄栏,超长输出不撑爆 DOM)。 */
2207
- const TOOL_TEXT_LIMIT = 4e3;
2208
- function truncateText(text, max) {
2209
- return text.length <= max ? text : `${text.slice(0, max)}…`;
2454
+ /**
2455
+ * 程序化入口(/side、bridge 划选提问)打开 Tab 时面板可能处于折叠态——
2456
+ * 类型型 openTab 不自动展开(better-sidebar 只对 path/url 内容型打开展开)。
2457
+ * 幂等展开(off-face:store.update 是 SidebarStore 公开 mutator 面;
2458
+ * feature-check + 吞错,兜底失败不影响功能)。
2459
+ */
2460
+ function ensurePanelOpen(store) {
2461
+ const mutable = store;
2462
+ try {
2463
+ mutable?.update?.((state) => {
2464
+ if (state.panelOpen === false) state.panelOpen = true;
2465
+ });
2466
+ } catch {}
2467
+ }
2468
+ /**
2469
+ * 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
2470
+ * public 且幂等,但不在 SessionFace 契约上——契约面只有 staged 会话会被
2471
+ * 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
2472
+ */
2473
+ function openSessionWindow(session) {
2474
+ const openable = session;
2475
+ if (typeof openable?.open !== "function") return;
2476
+ openable.open().catch((error) => {
2477
+ console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
2478
+ });
2210
2479
  }
2211
- /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
2212
- function contentTextOf(content) {
2213
- if (!Array.isArray(content)) return "";
2214
- const parts = [];
2215
- for (const block of content) {
2216
- if (typeof block !== "object" || block === null) continue;
2217
- const b = block;
2218
- if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
2219
- else if (b.type === "image") parts.push(t("imagePlaceholder"));
2480
+ /**
2481
+ * Current model selection of a session, read from its `modelSelection`
2482
+ * projection (dsh >= 0.1.2): the durable selection is projected state, so no RPC
2483
+ * is needed and the read is safe for any session id. Snapshot shape authority:
2484
+ * `@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts`
2485
+ * (`ModelSelectionProjection = { lastUsed, next }`,两者均可为 null——
2486
+ * `next` 缺省时按投影文档回落 `lastUsed`)。
2487
+ */
2488
+ function projectedModelSelection(ctx, sessionId) {
2489
+ try {
2490
+ const snapshot = (ctx.sessions.binding(sessionId)?.session)?.projections?.faceOf?.("modelSelection")?.getSnapshot?.();
2491
+ return snapshot?.next ?? snapshot?.lastUsed ?? void 0;
2492
+ } catch {
2493
+ return;
2220
2494
  }
2221
- return parts.join("\n");
2222
2495
  }
2223
- function seqKey(prefix, node) {
2224
- return `${prefix}:${typeof node.seq === "number" ? node.seq : "?"}`;
2496
+ /**
2497
+ * Remote session face carrying `selectModel`/`modelCatalog`(dsh 0.1.5 删除
2498
+ * `connection.api.sessions.*` client 面后的模型 RPC 面;事实上 0.1.2-rc.1
2499
+ * 起存在——authority: dsh-api-session-controller lib/typert.remote-client.d.ts)。
2500
+ * 探测走 `ctx.get`(cordis 的 get 不受 inject 门禁限制,本插件为兼容旧宿主
2501
+ * 不声明 inject 'remote.session';机制见 contracts.ts RemoteSessionModelFace)。
2502
+ * `ctx.get('remote')` 返回命名空间容器时下钻其 `.session` 成员。
2503
+ */
2504
+ function remoteSessionFace(ctx) {
2505
+ for (const name of ["remote.session", "remote"]) {
2506
+ let candidate;
2507
+ try {
2508
+ candidate = ctx.get(name);
2509
+ } catch {
2510
+ continue;
2511
+ }
2512
+ const face = candidate;
2513
+ if (typeof face?.selectModel === "function") return face;
2514
+ if (typeof face?.session?.selectModel === "function") return face.session;
2515
+ }
2225
2516
  }
2226
- /** 节点 seq 随行(D1 折叠边界用;缺省不带)。 */
2227
- function seqOf(node) {
2228
- return typeof node.seq === "number" ? { seq: node.seq } : {};
2517
+ /**
2518
+ * 读会话当前模型选择:投影面(0.1.2+,同步无 RPC)优先;旧
2519
+ * `connection.api.sessions.models` RPC(<= 0.1.2 的 client 面,0.1.5 已删)
2520
+ * 回退(双版本链,同 chatSourceOf 先例)。永不抛错。
2521
+ */
2522
+ async function readModelSelection(ctx, sessionId) {
2523
+ const projected = projectedModelSelection(ctx, sessionId);
2524
+ if (projected !== void 0) return projected;
2525
+ try {
2526
+ const res = await ctx.connection.api.sessions.models({ sessionId });
2527
+ if (res.result.ok) return res.result.value.current;
2528
+ } catch {}
2229
2529
  }
2230
- function assistantParts(blocks) {
2231
- const texts = [];
2232
- const reasonings = [];
2233
- let hasToolCall = false;
2234
- if (Array.isArray(blocks)) for (const block of blocks) {
2235
- if (typeof block !== "object" || block === null) continue;
2236
- const b = block;
2237
- if (b.kind === "text" && typeof b.text === "string") texts.push(b.text);
2238
- else if (b.kind === "reasoning" && typeof b.text === "string") reasonings.push(b.text);
2239
- else if (b.kind === "tool-call") hasToolCall = true;
2240
- }
2241
- return {
2242
- text: texts.join("\n\n"),
2243
- reasoning: reasonings.join("\n\n"),
2244
- hasToolCall
2530
+ /**
2531
+ * 写会话模型选择:`remote.session.selectModel`(新面)优先,旧
2532
+ * `connection.api.sessions.selectModel` 回退。面在而调用失败(模型不可路由等)
2533
+ * 属真实失败,不再落旧面重试。
2534
+ */
2535
+ async function writeModelSelection(ctx, sessionId, selection) {
2536
+ const request = {
2537
+ sessionId,
2538
+ provider: selection.provider,
2539
+ model: selection.model,
2540
+ ...selection.reasoningEffort !== void 0 ? { reasoningEffort: selection.reasoningEffort } : {}
2245
2541
  };
2542
+ const face = remoteSessionFace(ctx);
2543
+ if (face !== void 0) try {
2544
+ return (await face.selectModel(request)).ok;
2545
+ } catch {
2546
+ return false;
2547
+ }
2548
+ try {
2549
+ return (await ctx.connection.api.sessions.selectModel(request)).result.ok;
2550
+ } catch {
2551
+ return false;
2552
+ }
2553
+ }
2554
+ /** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
2555
+ async function readModelName(ctx, sessionId) {
2556
+ return (await readModelSelection(ctx, sessionId))?.model ?? null;
2246
2557
  }
2247
2558
  /**
2248
- * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
2249
- * (context 注入、未知面事件、已取消的重试等对读者无信息的节点)。
2559
+ * 0.1.2 优先:`uiConversation` 服务 → binding(会话 binding) → target('chat')
2560
+ * → 快照 .legacy 切片(0.1.2 把内容面从 Session 快照顶层拆到此处——
2561
+ * 根因与证据见 reports/ux-review/W00-fork-replay-012.md);0.1.1 回退
2562
+ * Session 快照顶层(nodes 直接在)。均 miss 返回 undefined(面板维持
2563
+ * 既有降级,不崩)。
2250
2564
  */
2251
- function nodeToMessage(node) {
2252
- if (typeof node !== "object" || node === null) return null;
2253
- const n = node;
2254
- switch (n.kind) {
2255
- case "user": return {
2256
- key: seqKey("u", n),
2257
- ...seqOf(n),
2258
- role: "user",
2259
- text: contentTextOf(n.content)
2260
- };
2261
- case "steering": return {
2262
- key: seqKey("s", n),
2263
- ...seqOf(n),
2264
- role: "user",
2265
- text: contentTextOf(n.content)
2565
+ function chatSourceOf(ctx, binding) {
2566
+ const session = binding?.session;
2567
+ if (session === void 0) return void 0;
2568
+ try {
2569
+ const target = ctx.get("uiConversation")?.binding?.(binding)?.target?.("chat");
2570
+ if (target !== void 0 && typeof target.getSnapshot === "function" && typeof target.subscribe === "function") return {
2571
+ subscribe: (fn) => target.subscribe(fn),
2572
+ getLegacy: () => {
2573
+ return target.getSnapshot()?.legacy ?? null;
2574
+ }
2266
2575
  };
2267
- case "assistant": {
2268
- const { text, reasoning, hasToolCall } = assistantParts(n.blocks);
2269
- if (text === "" && reasoning === "" && hasToolCall) return null;
2576
+ } catch {}
2577
+ const snap = session.getSnapshot();
2578
+ if (snap !== null && Array.isArray(snap.nodes)) return {
2579
+ subscribe: (fn) => session.subscribe(fn),
2580
+ getLegacy: () => session.getSnapshot()
2581
+ };
2582
+ }
2583
+ /**
2584
+ * 拉会话模型目录:0.1.5 走 `remote.session.modelCatalog()`(目录是宿主级,
2585
+ * 菜单选中项所需的 current 由投影/旧面补上);旧面回退
2586
+ * `connection.api.sessions.models`。失败 null → 菜单保持只读标签态。
2587
+ */
2588
+ async function listModels(ctx, sessionId) {
2589
+ const face = remoteSessionFace(ctx);
2590
+ if (typeof face?.modelCatalog === "function") try {
2591
+ const result = await face.modelCatalog();
2592
+ if (result.ok) {
2593
+ const catalog = result.value;
2270
2594
  return {
2271
- key: seqKey("a", n),
2272
- ...seqOf(n),
2273
- role: "assistant",
2274
- text,
2275
- ...reasoning !== "" ? { reasoning } : {},
2276
- ...n.interrupted === true ? { interrupted: true } : {}
2595
+ current: await readModelSelection(ctx, sessionId) ?? catalog.default,
2596
+ routable: catalog.routableProviders.length > 0,
2597
+ groups: catalog.groups,
2598
+ ...catalog.failures !== void 0 ? { failures: catalog.failures } : {}
2277
2599
  };
2278
2600
  }
2279
- case "tool-result": {
2280
- const call = n.call;
2281
- const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : t("toolFallback");
2282
- const text = truncateText(contentTextOf(n.content), TOOL_TEXT_LIMIT);
2283
- const card = (n.callView ?? n.resultView) != null ? cardModelOf({
2284
- toolName,
2285
- callView: n.callView,
2286
- resultView: n.resultView,
2287
- rawText: text
2288
- }) : cardModelFromNode({
2289
- name: toolName,
2290
- argsRaw: typeof call?.argsRaw === "string" ? call.argsRaw : void 0,
2291
- meta: n.meta,
2292
- rawText: text,
2293
- ...n.isError === true ? { isError: true } : {}
2294
- });
2295
- return {
2296
- key: seqKey("t", n),
2297
- ...seqOf(n),
2298
- role: "tool",
2299
- toolName,
2300
- text,
2301
- card,
2302
- ...n.isError === true ? { isError: true } : {}
2303
- };
2601
+ } catch {}
2602
+ try {
2603
+ const res = await ctx.connection.api.sessions.models({ sessionId });
2604
+ return res.result.ok ? res.result.value : null;
2605
+ } catch {
2606
+ return null;
2607
+ }
2608
+ }
2609
+ /** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
2610
+ async function switchModel(ctx, sessionId, provider, model) {
2611
+ return await writeModelSelection(ctx, sessionId, {
2612
+ provider,
2613
+ model
2614
+ }) ? model : null;
2615
+ }
2616
+ //#endregion
2617
+ //#region src/client/host/probes.ts
2618
+ const PROBES = [
2619
+ {
2620
+ name: "Session.open()(非 staged 会话开窗)",
2621
+ check: (ctx) => {
2622
+ return typeof ctx.sessions?.binding === "function";
2623
+ },
2624
+ fallback: "侧边聊天消息流降级为只发不收"
2625
+ },
2626
+ {
2627
+ name: "conversation.input 输入机器",
2628
+ check: (ctx) => {
2629
+ return typeof (ctx.get?.("conversation"))?.input?.for === "function";
2630
+ },
2631
+ fallback: "侧边 composer 降级为本地草稿 + session.prompt"
2632
+ },
2633
+ {
2634
+ name: "commandUi(斜杠命令注册)",
2635
+ check: (ctx) => {
2636
+ return typeof (ctx.get?.("commandUi"))?.register === "function";
2637
+ },
2638
+ fallback: "/side 与「/侧边」命令不可用(菜单/顶栏入口仍在)"
2639
+ },
2640
+ {
2641
+ name: "ctx.modules.import(动态组件复用)",
2642
+ check: (ctx) => {
2643
+ return typeof ctx.modules?.import === "function";
2644
+ },
2645
+ fallback: "ProducedFiles 等动态复用件降级为自绘"
2646
+ },
2647
+ {
2648
+ name: "sidebarRight.focus(native tab 程序化聚焦)",
2649
+ check: (ctx) => {
2650
+ if (!nativeSidebarHost(ctx)) return true;
2651
+ return typeof (ctx.get?.("sidebarRight"))?.focus === "function";
2652
+ },
2653
+ fallback: "聚焦既有侧边聊天 tab 不可用(activateTab 在 native 面是空操作)"
2654
+ },
2655
+ {
2656
+ name: "会话模型面(remote.session / connection.api.sessions 双版本链)",
2657
+ check: (ctx) => {
2658
+ if (remoteSessionFace(ctx) !== void 0) return true;
2659
+ return typeof ctx.connection?.api?.sessions?.models === "function";
2660
+ },
2661
+ fallback: "fork 模型同步、模型标签与模型切换不可用(子会话用宿主默认模型)"
2662
+ }
2663
+ ];
2664
+ /** 激活时跑一遍探测清单;缺失项逐项 warn-once。返回缺失项数(测试可断言)。 */
2665
+ function probeHost(ctx) {
2666
+ let missing = 0;
2667
+ for (const probe of PROBES) {
2668
+ let ok = false;
2669
+ try {
2670
+ ok = probe.check(ctx);
2671
+ } catch {
2672
+ ok = false;
2304
2673
  }
2305
- case "turn-error": return {
2306
- key: seqKey("e", n),
2307
- ...seqOf(n),
2308
- role: "error",
2309
- text: typeof n.message === "string" ? n.message : t("unknownError")
2310
- };
2311
- case "model-retry":
2312
- if (n.retryState === "cancelled") return null;
2313
- return {
2314
- key: seqKey("r", n),
2315
- ...seqOf(n),
2316
- role: "notice",
2317
- text: t(n.retryState === "started" ? "modelRetryStarted" : "modelRetryWaiting")
2318
- };
2319
- case "turn-max-tokens": return {
2320
- key: seqKey("m", n),
2321
- ...seqOf(n),
2322
- role: "notice",
2323
- text: t("maxTokens")
2324
- };
2325
- case "command": {
2326
- const name = typeof n.name === "string" && n.name !== "" ? n.name : t("commandNameFallback");
2327
- const args = typeof n.args === "string" ? n.args.trimStart() : "";
2328
- return {
2329
- key: seqKey("c", n),
2330
- ...seqOf(n),
2331
- role: "notice",
2332
- text: t("runCommand", { cmd: `/${name}${args === "" ? "" : ` ${args}`}` })
2333
- };
2674
+ if (!ok) {
2675
+ missing += 1;
2676
+ console.warn(`[dsh-sidenote] 宿主能力缺失:${probe.name} → ${probe.fallback}`);
2334
2677
  }
2335
- case "compaction": return {
2336
- key: seqKey("k", n),
2337
- ...seqOf(n),
2338
- role: "notice",
2339
- text: t("compacted")
2340
- };
2341
- default: return null;
2342
2678
  }
2679
+ return missing;
2343
2680
  }
2681
+ //#endregion
2682
+ //#region src/client/reflow.ts
2344
2683
  /**
2345
- * ConversationSnapshot → 渲染消息列表:终态节点 + 在途项(按 turn/step 归并)。
2346
- * 快照缺省(未绑定)时为空列表。
2684
+ * 回流通道的受控对象层(Delivery_02 Workitem_04 v2):侧边聊天的结论以
2685
+ * 「待回流对象」进入主会话——主 composer 上方的 chip(可预览/可移除),
2686
+ * 发送瞬间由 annotate/send.ts 拦截器与注释协议块一起序列化进消息。
2687
+ * 不经草稿文本流(用户裁定:直接进草稿「很脏」)。
2347
2688
  *
2348
- * 时序归并(WI-01):在途项不再恒追加尾部——真实交错是「partial 文本 → 它
2349
- * 自己 step 发出的工具卡」:partial 与同 step 的 runningCall 并存时必须文本
2350
- * 在前(原文早于调用),不同 step 按 (turn, step) 升序;并行多 call 保持
2351
- * 宿主 dispatch 序(同 turn/step 内稳定)。终态节点自带 seq 全序,不参与
2352
- * 归并(在途项永远属于当前 turn 的前沿,尾部插入点天然正确)。
2689
+ * 气泡留痕:序列化形态是 XML 协议块(见 buildReflowBlock),随注释协议块
2690
+ * 一起作为消息连续前缀注入,bubble.ts 反解析后折叠为「含侧边回流上下文」标签。
2691
+ *
2692
+ * 持久化:localStorage 按会话键(dsh-sidenote:reflow:v1:<sessionId>),
2693
+ * 与注释 store 同款纪律(刷新不丢、容错 revive、空删键)。
2353
2694
  */
2354
- function transcriptOf(snapshot) {
2355
- if (snapshot === void 0 || snapshot === null) return [];
2356
- const out = [];
2357
- for (const node of snapshot.nodes ?? []) {
2358
- const message = nodeToMessage(node);
2359
- if (message !== null) out.push(message);
2360
- }
2361
- const inflight = [];
2362
- const partial = snapshot.partial;
2363
- if (partial !== void 0 && partial !== null) {
2364
- const { text, reasoning, hasToolCall } = assistantParts(partial.blocks);
2365
- if (text !== "" || reasoning !== "" || !hasToolCall) inflight.push({
2366
- turn: typeof partial.turn === "number" ? partial.turn : Number.MAX_SAFE_INTEGER,
2367
- step: typeof partial.step === "number" ? partial.step : Number.MAX_SAFE_INTEGER,
2368
- order: 0,
2369
- message: {
2370
- key: "partial",
2371
- role: "assistant",
2372
- text,
2373
- ...reasoning !== "" ? { reasoning } : {},
2374
- streaming: true
2375
- }
2376
- });
2377
- }
2378
- if (Array.isArray(snapshot.runningCalls)) for (const call of snapshot.runningCalls) {
2379
- if (typeof call !== "object" || call === null) continue;
2380
- const c = call;
2381
- const toolName = typeof c.name === "string" ? c.name : t("toolFallback");
2382
- inflight.push({
2383
- turn: typeof c.turn === "number" ? c.turn : Number.MAX_SAFE_INTEGER,
2384
- step: typeof c.step === "number" ? c.step : Number.MAX_SAFE_INTEGER,
2385
- order: 1,
2386
- message: {
2387
- key: `rc:${typeof c.callId === "string" ? c.callId : "?"}`,
2388
- role: "tool",
2389
- toolName,
2390
- text: "",
2391
- streaming: true,
2392
- card: c.callView != null ? cardModelOf({
2393
- toolName,
2394
- callView: c.callView,
2395
- resultView: null
2396
- }) : cardModelFromNode({
2397
- name: toolName,
2398
- argsRaw: typeof c.argsRaw === "string" ? c.argsRaw : void 0,
2399
- rawText: ""
2400
- })
2401
- }
2402
- });
2695
+ /**
2696
+ * reflow 注入通道(Workitem_06):优先经宿主路由 /sidenote/reflow 以
2697
+ * agent.inject 落父会话日志(plugin-source 独立事件,context row 折叠);
2698
+ * 路由缺席/父会话无 live agent/超时 → 调用方回落搭车形态(发送拦截器
2699
+ * 序列化前缀)。逐个 item 投递;成功项由调用方从 store 移除(不等发送
2700
+ * 确认——注入已入 inbox,chip 消失表达「已注入」)。
2701
+ *
2702
+ * 有界等待:注入尝试绝不能拖住发送手势——300ms 超时即回落搭车。
2703
+ */
2704
+ const REFLOW_ROUTE_TIMEOUT_MS = 300;
2705
+ /** 尝试经宿主路由注入一批回流项;任何失败都降级为「全未注入」。 */
2706
+ async function tryInjectReflows(parentSessionId, items) {
2707
+ if (items.length === 0) return { injectedIds: /* @__PURE__ */ new Set() };
2708
+ const empty = { injectedIds: /* @__PURE__ */ new Set() };
2709
+ try {
2710
+ const controller = new AbortController();
2711
+ const timer = setTimeout(() => {
2712
+ controller.abort();
2713
+ }, REFLOW_ROUTE_TIMEOUT_MS);
2714
+ let res;
2715
+ try {
2716
+ res = await fetch("/sidenote/reflow", {
2717
+ method: "POST",
2718
+ headers: { "content-type": "application/json" },
2719
+ body: JSON.stringify({
2720
+ parentSessionId,
2721
+ items: items.map((item) => ({
2722
+ text: `以下是用户从侧边对话带回的结论(参考上下文):\n\n${buildReflowBlock(item)}`,
2723
+ sideTitle: item.sideTitle
2724
+ }))
2725
+ }),
2726
+ signal: controller.signal
2727
+ });
2728
+ } finally {
2729
+ clearTimeout(timer);
2730
+ }
2731
+ if (!res.ok) return empty;
2732
+ if ((await res.json()).ok !== true) return empty;
2733
+ return { injectedIds: new Set(items.map((item) => item.id)) };
2734
+ } catch {
2735
+ return empty;
2403
2736
  }
2404
- inflight.sort((a, b) => a.turn - b.turn || a.step - b.step || a.order - b.order);
2405
- for (const item of inflight) out.push(item.message);
2406
- return out;
2407
2737
  }
2738
+ const STORAGE_PREFIX$1 = "dsh-sidenote:reflow:v1:";
2408
2739
  /**
2409
- * D1 父历史折叠的划分:seq <= boundarySeq 的消息为继承区(fork 时刻快照,
2410
- * 折叠进指示卡),其余(含在途项——无 seq)为新鲜区。boundarySeq 缺省 =
2411
- * 全新鲜(升级前老 Tab 的行为不变)。
2740
+ * 回流块序列化(XML 形态,与注释协议同族):
2741
+ *
2742
+ * <reflow source="侧边 2" reason="用户选择从侧边聊天带回主线">
2743
+ * <问>用户在侧边聊天里的提问</问>
2744
+ * <答>结论全文(不截断)</答>
2745
+ * </reflow>
2746
+ *
2747
+ * 问答成对:只有答没有问,模型理解打折;question 缺省时只有 <答>。
2748
+ * 来源与意图放在属性里,内容行保持纯净;模型对 XML 包裹的上下文理解最好
2749
+ * (Claude 官方推荐形态 / Codex additionalContext 同款思路)。
2412
2750
  */
2413
- function partitionInherited(messages, boundarySeq) {
2414
- if (boundarySeq === void 0) return {
2415
- inherited: [],
2416
- fresh: [...messages]
2751
+ function buildReflowBlock(item) {
2752
+ const parts = [];
2753
+ if (item.question !== void 0 && item.question.trim() !== "") parts.push(`<问>${item.question}</问>`);
2754
+ parts.push(`<答>${item.text}</答>`);
2755
+ return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${parts.join("\n")}\n</reflow>`;
2756
+ }
2757
+ function revive(value) {
2758
+ if (typeof value !== "object" || value === null) return null;
2759
+ const r = value;
2760
+ if (typeof r.id !== "number" || typeof r.sessionId !== "string" || r.sessionId === "") return null;
2761
+ if (typeof r.sideTitle !== "string" || typeof r.text !== "string") return null;
2762
+ return {
2763
+ id: r.id,
2764
+ sessionId: r.sessionId,
2765
+ sideTitle: r.sideTitle,
2766
+ text: r.text,
2767
+ ...typeof r.question === "string" && r.question.trim() !== "" ? { question: r.question } : {},
2768
+ createdAt: typeof r.createdAt === "number" ? r.createdAt : 0
2769
+ };
2770
+ }
2771
+ function createReflowStore(now = () => Date.now(), storage) {
2772
+ let items = [];
2773
+ let nextId = 1;
2774
+ let version = 0;
2775
+ const listeners = /* @__PURE__ */ new Set();
2776
+ const store = storage === void 0 ? typeof localStorage !== "undefined" ? localStorage : null : storage;
2777
+ if (store != null) try {
2778
+ const keys = [];
2779
+ for (let i = 0; i < store.length; i += 1) {
2780
+ const key = store.key(i);
2781
+ if (typeof key === "string" && key.startsWith(STORAGE_PREFIX$1)) keys.push(key);
2782
+ }
2783
+ for (const key of keys) {
2784
+ const parsed = JSON.parse(store.getItem(key) ?? "null");
2785
+ if (!Array.isArray(parsed)) continue;
2786
+ for (const raw of parsed) {
2787
+ const item = revive(raw);
2788
+ if (item === null) continue;
2789
+ items.push(item);
2790
+ nextId = Math.max(nextId, item.id + 1);
2791
+ }
2792
+ }
2793
+ } catch (error) {
2794
+ console.warn("[dsh-sidenote] 回流持久化读取失败(按空起步):", error);
2795
+ }
2796
+ const persist = (sessionId) => {
2797
+ if (store == null) return;
2798
+ try {
2799
+ const mine = items.filter((i) => i.sessionId === sessionId);
2800
+ const key = STORAGE_PREFIX$1 + sessionId;
2801
+ if (mine.length === 0) store.removeItem(key);
2802
+ else store.setItem(key, JSON.stringify(mine));
2803
+ } catch (error) {
2804
+ console.warn("[dsh-sidenote] 回流持久化写入失败:", error);
2805
+ }
2806
+ };
2807
+ const notify = () => {
2808
+ version += 1;
2809
+ for (const fn of [...listeners]) fn();
2417
2810
  };
2418
- const inherited = [];
2419
- const fresh = [];
2420
- for (const message of messages) if (message.seq !== void 0 && message.seq <= boundarySeq) inherited.push(message);
2421
- else fresh.push(message);
2422
2811
  return {
2423
- inherited,
2424
- fresh
2812
+ getSnapshot: () => version,
2813
+ subscribe(fn) {
2814
+ listeners.add(fn);
2815
+ return () => {
2816
+ listeners.delete(fn);
2817
+ };
2818
+ },
2819
+ add(sessionId, sideTitle, text, question) {
2820
+ const item = {
2821
+ id: nextId,
2822
+ sessionId,
2823
+ sideTitle,
2824
+ text,
2825
+ ...typeof question === "string" && question.trim() !== "" ? { question } : {},
2826
+ createdAt: now()
2827
+ };
2828
+ nextId += 1;
2829
+ items = [...items, item];
2830
+ persist(sessionId);
2831
+ notify();
2832
+ return item;
2833
+ },
2834
+ remove(id) {
2835
+ const target = items.find((i) => i.id === id);
2836
+ if (target === void 0) return;
2837
+ items = items.filter((i) => i.id !== id);
2838
+ persist(target.sessionId);
2839
+ notify();
2840
+ },
2841
+ clearSession(sessionId) {
2842
+ if (!items.some((i) => i.sessionId === sessionId)) return;
2843
+ items = items.filter((i) => i.sessionId !== sessionId);
2844
+ persist(sessionId);
2845
+ notify();
2846
+ },
2847
+ list: (sessionId) => items.filter((i) => i.sessionId === sessionId)
2425
2848
  };
2426
2849
  }
2427
2850
  //#endregion
2851
+ //#region src/client/bridge.ts
2852
+ const sideChatBridge = { current: null };
2853
+ //#endregion
2428
2854
  //#region src/client/host/markdown.ts
2429
2855
  let cache;
2430
2856
  /** 当前语言的 chrome 对象(同语言复用同一引用)。 */
@@ -2560,7 +2986,7 @@ window.__ModuleLoader__.load({
2560
2986
  }
2561
2987
  //#endregion
2562
2988
  //#region \0dsh-css:/home/runner/work/dsh-sidenote/dsh-sidenote/src/client/sidechat/sidechat.module.css.mjs
2563
- const css$1 = "._5KEEsG_nativeHost{flex-direction:column;height:100%;min-height:0;display:flex}._5KEEsG_root{height:100%;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:13px;display:flex;position:relative}._5KEEsG_body{flex-direction:column;flex:1;padding:12px 10px;display:flex;overflow-y:auto}._5KEEsG_transcript{flex-direction:column;gap:10px;display:flex}._5KEEsG_empty,._5KEEsG_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}._5KEEsG_emptyIcon{color:var(--dsw-alias-label-tertiary)}._5KEEsG_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}._5KEEsG_emptyText{color:var(--dsw-alias-label-tertiary);max-width:240px;font-size:12px;line-height:1.6}._5KEEsG_stateDetail{color:var(--dsw-alias-state-error-primary);word-break:break-word;max-width:260px;font-size:12px;line-height:1.6}._5KEEsG_userRow{justify-content:flex-end;display:flex}._5KEEsG_userBubble{background:var(--dsw-specific-bubble,#edf3fe);max-width:88%;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:16px;padding:8px 12px;font-size:13px;line-height:1.55}._5KEEsG_assistantRow{justify-content:flex-start;display:flex;position:relative}._5KEEsG_rowActions{opacity:0;z-index:2;transition:opacity .12s ease-out;position:absolute;top:-8px;right:0}._5KEEsG_assistantRow:hover ._5KEEsG_rowActions{opacity:1}._5KEEsG_reflowButton{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);width:22px;height:22px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;border-radius:999px;place-items:center;padding:0;display:grid;box-shadow:0 2px 8px #00000014}._5KEEsG_reflowButton:hover{color:var(--dsw-alias-state-business-primary,#2f88ff);border-color:var(--dsw-alias-state-business-primary,#2f88ff)}._5KEEsG_assistantBody{word-break:break-word;max-width:100%;font-size:13px;line-height:1.6}._5KEEsG_reasoning{border-left:3px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin-bottom:6px;padding-left:8px;font-size:12px}._5KEEsG_reasoning>summary{cursor:pointer;user-select:none}._5KEEsG_reasoningBody{white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-tertiary);margin-top:2px;padding-bottom:2px;padding-left:22px}._5KEEsG_streamingHint{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolCard{font-size:12px}._5KEEsG_toolHead{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;display:flex}._5KEEsG_toolError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_toolRunning{color:var(--dsw-alias-label-tertiary)}._5KEEsG_toolBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;max-height:160px;padding:2px 0 2px 22px;font-size:12px;overflow-y:auto}._5KEEsG_errorRow{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;padding:4px 2px;font-size:12px}._5KEEsG_noticeRow{color:var(--dsw-alias-label-dimmed);text-align:center;padding:2px 0;font-size:12px}._5KEEsG_composer{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding:8px 10px;display:flex;position:relative}._5KEEsG_composerCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:14px;flex-direction:column;gap:2px;padding:4px 6px 6px;transition:border-color .12s;display:flex}._5KEEsG_composerCard:focus-within{border-color:var(--dsw-alias-state-business-primary)}._5KEEsG_input{resize:none;width:100%;min-height:36px;max-height:160px;color:var(--dsw-alias-label-primary);font:inherit;box-sizing:border-box;background:0 0;border:none;padding:6px 6px 2px}._5KEEsG_input:focus{outline:none}._5KEEsG_composerFoot{justify-content:space-between;align-items:center;gap:8px;padding:0 2px;display:flex}._5KEEsG_modelLabel{color:var(--dsw-alias-label-dimmed);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_sendButton,._5KEEsG_stopButton{appearance:none;background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;width:28px;height:28px;padding:0;display:inline-flex}._5KEEsG_sendButton:disabled{opacity:.45;cursor:default}._5KEEsG_headerSide{border:1px solid var(--dsw-alias-border-l1,#0000001a);color:var(--dsw-alias-label-secondary,#3f454d);cursor:pointer;user-select:none;background:0 0;border-radius:999px;align-items:center;gap:5px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex}._5KEEsG_headerSide:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._5KEEsG_sentChipRow{align-items:center;gap:6px;margin-right:8px;display:inline-flex}._5KEEsG_sentChip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-secondary,#3f454d);user-select:none;border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;line-height:16px;display:inline-flex}._5KEEsG_flowRow{font-size:12px}._5KEEsG_rowPreview{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-dimmed);flex:1;margin-left:8px;font-size:12px;overflow:hidden}._5KEEsG_flowRowError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_todoBody{flex-direction:column;gap:3px;padding:2px 0 4px 22px;display:flex}._5KEEsG_todoItem{align-items:baseline;gap:7px;min-width:0;font-size:12px;line-height:1.5;display:flex}._5KEEsG_todoDot{background:var(--dsw-alias-label-dimmed);border-radius:50%;flex:none;width:6px;height:6px;transform:translateY(-1px)}._5KEEsG_todoDot[data-status=completed]{background:var(--dsw-alias-state-success-primary,#34a853)}._5KEEsG_todoDot[data-status=in_progress]{background:var(--dsw-alias-state-business-primary)}._5KEEsG_todoText[data-status=completed]{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolGenericBody{flex-direction:column;gap:6px;padding:4px 0 4px 22px;display:flex}._5KEEsG_toolBodyText{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.55;font-family:var(--dsw-alias-font-mono,ui-monospace, monospace);margin:0}._5KEEsG_toolLocations{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-all;font-size:11px}._5KEEsG_inheritedBody{border-top:1px dashed var(--dsw-alias-border-l2);opacity:.92;padding:6px 0 2px}._5KEEsG_densityRow{justify-content:flex-end;align-items:center;padding:2px 2px 4px;display:flex}._5KEEsG_densityToggle{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:2px 6px;font-size:11px}._5KEEsG_densityToggle:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_jumpBottom{z-index:5;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:999px;padding:3px 10px;font-size:11px;position:absolute;bottom:96px;right:12px;box-shadow:0 2px 8px #0000001f}._5KEEsG_jumpBottom:hover{background:var(--dsw-alias-bg-layer-2)}._5KEEsG_modelSelect{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:6px;align-items:center;gap:3px;max-width:180px;padding:3px 6px;font-size:12px;display:inline-flex;overflow:hidden}._5KEEsG_modelSelect>span:first-child{text-overflow:ellipsis;overflow:hidden}._5KEEsG_modelSelect>svg{color:var(--dsw-alias-label-dimmed);flex:none}._5KEEsG_modelSelect:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_composerChips{align-items:center;gap:4px;min-width:0;display:inline-flex;overflow:hidden}._5KEEsG_slashMenu{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);z-index:20;border-radius:10px;max-height:260px;margin-bottom:6px;padding:4px;position:absolute;bottom:100%;left:8px;right:8px;overflow-y:auto;box-shadow:0 8px 24px #00000024}._5KEEsG_slashGroupTitle{color:var(--dsw-alias-label-tertiary);text-transform:none;padding:4px 8px 2px;font-size:11px}._5KEEsG_slashItem,._5KEEsG_slashItemActive{text-align:left;cursor:pointer;width:100%;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:6px;align-items:baseline;gap:8px;padding:6px 8px;font-size:13px;display:flex}._5KEEsG_slashItemActive,._5KEEsG_slashItem:hover{background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_slashItemName{flex:none;font-weight:500}._5KEEsG_slashItemDetail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_attachRail{flex-wrap:wrap;gap:6px;padding:2px 0;display:flex}._5KEEsG_attachThumb{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;width:44px;height:44px;position:relative;overflow:hidden}._5KEEsG_attachImg{object-fit:cover;width:100%;height:100%;display:block}._5KEEsG_attachRemove{color:#fff;cursor:pointer;background:#0000008c;border:none;border-radius:999px;place-items:center;width:16px;height:16px;padding:0;font-size:11px;line-height:1;display:grid;position:absolute;top:1px;right:1px}._5KEEsG_attachButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:4px;display:inline-flex}._5KEEsG_attachButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_pendingCard{border:1px solid var(--dsw-alias-border-l2);border-left:3px solid var(--dsw-alias-state-warning-primary,#e8a13a);background:var(--dsw-alias-bg-layer-2);border-radius:10px;flex-direction:column;gap:6px;margin:0 10px 6px;padding:10px 12px;font-size:12px;display:flex}._5KEEsG_pendingHead{color:var(--dsw-alias-state-warning-primary,#e8a13a);align-items:center;gap:6px;display:flex}._5KEEsG_pendingTitle{color:var(--dsw-alias-label-primary);font-weight:600}._5KEEsG_pendingDesc{color:var(--dsw-alias-label-secondary)}._5KEEsG_pendingReason{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word}._5KEEsG_pendingQuestion{flex-direction:column;gap:4px;padding:4px 0;display:flex}._5KEEsG_pendingQHeader{color:var(--dsw-alias-label-primary);font-weight:600}._5KEEsG_pendingQText{color:var(--dsw-alias-label-secondary)}._5KEEsG_pendingOption{text-align:left;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:6px;flex-direction:column;align-items:flex-start;gap:2px;padding:5px 8px;font-size:12px;display:flex}._5KEEsG_pendingOption:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._5KEEsG_pendingOptionActive{text-align:left;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;flex-direction:column;align-items:flex-start;gap:2px;padding:5px 8px;font-size:12px;display:flex}._5KEEsG_pendingOptionDesc{color:var(--dsw-alias-label-dimmed);font-size:11px}._5KEEsG_pendingCustom{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-size:12px;font:inherit;background:0 0;border-radius:6px;padding:5px 8px}._5KEEsG_pendingCustom:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}._5KEEsG_pendingActions{justify-content:flex-end;gap:8px;margin-top:2px;display:flex}._5KEEsG_pendingBtnGhost{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:6px;padding:4px 12px;font-size:12px}._5KEEsG_pendingBtnPrimary{background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:6px;padding:4px 12px;font-size:12px}._5KEEsG_pendingBtnGhost:disabled,._5KEEsG_pendingBtnPrimary:disabled{opacity:.5;cursor:default}._5KEEsG_pendingBar{border:1px solid var(--dsw-alias-state-warning-primary,#d4a017);background:var(--dsw-alias-bg-layer-2);width:calc(100% - 16px);color:var(--dsw-alias-state-warning-primary,#d4a017);text-align:left;cursor:pointer;border-radius:8px;margin:0 8px 6px;padding:6px 10px;font-size:12px;display:block}._5KEEsG_pendingBar:hover{background:var(--dsw-alias-bg-layer-3)}._5KEEsG_writeNotice{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);border-radius:8px;align-items:center;gap:8px;margin:0 8px 6px;padding:6px 10px;font-size:12px;display:flex}._5KEEsG_writeNoticeClose{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;margin-left:auto;padding:0 4px;font-size:13px}._5KEEsG_parentStrip{border:none;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left;align-items:center;gap:6px;padding:5px 12px;font-size:11px;display:flex}._5KEEsG_parentStrip:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}._5KEEsG_dotRunning,._5KEEsG_dotPending,._5KEEsG_dotIdle{border-radius:999px;flex:none;width:7px;height:7px}._5KEEsG_dotRunning{background:var(--dsw-alias-state-business-primary,#2f88ff);animation:1.2s ease-in-out infinite _5KEEsG_sidechat-pulse}._5KEEsG_dotPending{background:var(--dsw-alias-state-warning-primary,#d4a017)}._5KEEsG_dotIdle{background:var(--dsw-alias-label-dimmed)}@keyframes _5KEEsG_sidechat-pulse{50%{opacity:.45}}._5KEEsG_actionGroup{gap:2px;margin-right:auto;display:inline-flex}";
2989
+ const css$1 = "._5KEEsG_nativeHost{flex-direction:column;height:100%;min-height:0;display:flex}._5KEEsG_root{height:100%;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:13px;display:flex;position:relative}._5KEEsG_body{flex-direction:column;flex:1;padding:12px 10px;display:flex;overflow-y:auto}._5KEEsG_transcript{flex-direction:column;gap:10px;display:flex}._5KEEsG_empty,._5KEEsG_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}._5KEEsG_emptyIcon{color:var(--dsw-alias-label-tertiary)}._5KEEsG_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}._5KEEsG_emptyText{color:var(--dsw-alias-label-tertiary);max-width:240px;font-size:12px;line-height:1.6}._5KEEsG_stateDetail{color:var(--dsw-alias-state-error-primary);word-break:break-word;max-width:260px;font-size:12px;line-height:1.6}._5KEEsG_userRow{justify-content:flex-end;display:flex}._5KEEsG_userBubble{background:var(--dsw-specific-bubble,#edf3fe);max-width:88%;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:16px;padding:8px 12px;font-size:13px;line-height:1.55}._5KEEsG_assistantRow{justify-content:flex-start;display:flex;position:relative}._5KEEsG_rowActions{opacity:0;z-index:2;transition:opacity .12s ease-out;position:absolute;top:-8px;right:0}._5KEEsG_assistantRow:hover ._5KEEsG_rowActions{opacity:1}._5KEEsG_reflowButton{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);width:22px;height:22px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;border-radius:999px;place-items:center;padding:0;display:grid;box-shadow:0 2px 8px #00000014}._5KEEsG_reflowButton:hover{color:var(--dsw-alias-state-business-primary,#2f88ff);border-color:var(--dsw-alias-state-business-primary,#2f88ff)}._5KEEsG_assistantBody{word-break:break-word;max-width:100%;font-size:13px;line-height:1.6}._5KEEsG_reasoning{border-left:3px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin-bottom:6px;padding-left:8px;font-size:12px}._5KEEsG_reasoning>summary{cursor:pointer;user-select:none}._5KEEsG_reasoningBody{white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-tertiary);margin-top:2px;padding-bottom:2px;padding-left:22px}._5KEEsG_streamingHint{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolCard{font-size:12px}._5KEEsG_toolHead{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;display:flex}._5KEEsG_toolError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_toolRunning{color:var(--dsw-alias-label-tertiary)}._5KEEsG_toolBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;max-height:160px;padding:2px 0 2px 22px;font-size:12px;overflow-y:auto}._5KEEsG_errorRow{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;padding:4px 2px;font-size:12px}._5KEEsG_noticeRow{color:var(--dsw-alias-label-dimmed);text-align:center;padding:2px 0;font-size:12px}._5KEEsG_composer{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding:8px 10px;display:flex;position:relative}._5KEEsG_composerCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:14px;flex-direction:column;gap:2px;padding:4px 6px 6px;transition:border-color .12s;display:flex}._5KEEsG_composerCard:focus-within{border-color:var(--dsw-alias-state-business-primary)}._5KEEsG_input{resize:none;width:100%;min-height:36px;max-height:160px;color:var(--dsw-alias-label-primary);font:inherit;box-sizing:border-box;background:0 0;border:none;padding:6px 6px 2px}._5KEEsG_input:focus{outline:none}._5KEEsG_composerFoot{justify-content:space-between;align-items:center;gap:8px;padding:0 2px;display:flex}._5KEEsG_modelLabel{color:var(--dsw-alias-label-dimmed);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_sendButton,._5KEEsG_stopButton{appearance:none;background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;width:28px;height:28px;padding:0;display:inline-flex}._5KEEsG_sendButton:disabled{opacity:.45;cursor:default}._5KEEsG_headerSide{border:1px solid var(--dsw-alias-border-l1,#0000001a);color:var(--dsw-alias-label-secondary,#3f454d);cursor:pointer;user-select:none;background:0 0;border-radius:999px;align-items:center;gap:5px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex}._5KEEsG_headerSide:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._5KEEsG_sentChipRow{align-items:center;gap:6px;margin-right:8px;display:inline-flex}._5KEEsG_sentChip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-secondary,#3f454d);user-select:none;border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;line-height:16px;display:inline-flex}._5KEEsG_flowRow{font-size:12px}._5KEEsG_rowPreview{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-dimmed);flex:1;margin-left:8px;font-size:12px;overflow:hidden}._5KEEsG_flowRowError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_todoBody{flex-direction:column;gap:3px;padding:2px 0 4px 22px;display:flex}._5KEEsG_todoItem{align-items:baseline;gap:7px;min-width:0;font-size:12px;line-height:1.5;display:flex}._5KEEsG_todoDot{background:var(--dsw-alias-label-dimmed);border-radius:50%;flex:none;width:6px;height:6px;transform:translateY(-1px)}._5KEEsG_todoDot[data-status=completed]{background:var(--dsw-alias-state-success-primary,#34a853)}._5KEEsG_todoDot[data-status=in_progress]{background:var(--dsw-alias-state-business-primary)}._5KEEsG_todoText[data-status=completed]{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolGenericBody{flex-direction:column;gap:6px;padding:4px 0 4px 22px;display:flex}._5KEEsG_toolBodyText{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.55;font-family:var(--dsw-alias-font-mono,ui-monospace, monospace);margin:0}._5KEEsG_toolLocations{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-all;font-size:11px}._5KEEsG_inheritedBody{border-top:1px dashed var(--dsw-alias-border-l2);opacity:.92;padding:6px 0 2px}._5KEEsG_snapshotBody{border-top:1px dashed var(--dsw-alias-border-l2);padding:6px 0 2px}._5KEEsG_snapshotText{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.55;font-family:var(--dsw-alias-font-mono,ui-monospace, monospace);max-height:320px;margin:0;overflow-y:auto}._5KEEsG_densityRow{justify-content:flex-end;align-items:center;padding:2px 2px 4px;display:flex}._5KEEsG_densityToggle{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:2px 6px;font-size:11px}._5KEEsG_densityToggle:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_jumpBottom{z-index:5;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:999px;padding:3px 10px;font-size:11px;position:absolute;bottom:96px;right:12px;box-shadow:0 2px 8px #0000001f}._5KEEsG_jumpBottom:hover{background:var(--dsw-alias-bg-layer-2)}._5KEEsG_modelSelect{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:6px;align-items:center;gap:3px;max-width:180px;padding:3px 6px;font-size:12px;display:inline-flex;overflow:hidden}._5KEEsG_modelSelect>span:first-child{text-overflow:ellipsis;overflow:hidden}._5KEEsG_modelSelect>svg{color:var(--dsw-alias-label-dimmed);flex:none}._5KEEsG_modelSelect:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_composerChips{align-items:center;gap:4px;min-width:0;display:inline-flex;overflow:hidden}._5KEEsG_slashMenu{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);z-index:20;border-radius:10px;max-height:260px;margin-bottom:6px;padding:4px;position:absolute;bottom:100%;left:8px;right:8px;overflow-y:auto;box-shadow:0 8px 24px #00000024}._5KEEsG_slashGroupTitle{color:var(--dsw-alias-label-tertiary);text-transform:none;padding:4px 8px 2px;font-size:11px}._5KEEsG_slashItem,._5KEEsG_slashItemActive{text-align:left;cursor:pointer;width:100%;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:6px;align-items:baseline;gap:8px;padding:6px 8px;font-size:13px;display:flex}._5KEEsG_slashItemActive,._5KEEsG_slashItem:hover{background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_slashItemName{flex:none;font-weight:500}._5KEEsG_slashItemDetail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_attachRail{flex-wrap:wrap;gap:6px;padding:2px 0;display:flex}._5KEEsG_attachThumb{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;width:44px;height:44px;position:relative;overflow:hidden}._5KEEsG_attachImg{object-fit:cover;width:100%;height:100%;display:block}._5KEEsG_attachRemove{color:#fff;cursor:pointer;background:#0000008c;border:none;border-radius:999px;place-items:center;width:16px;height:16px;padding:0;font-size:11px;line-height:1;display:grid;position:absolute;top:1px;right:1px}._5KEEsG_attachButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:4px;display:inline-flex}._5KEEsG_attachButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_pendingCard{border:1px solid var(--dsw-alias-border-l2);border-left:3px solid var(--dsw-alias-state-warning-primary,#e8a13a);background:var(--dsw-alias-bg-layer-2);border-radius:10px;flex-direction:column;gap:6px;margin:0 10px 6px;padding:10px 12px;font-size:12px;display:flex}._5KEEsG_pendingHead{color:var(--dsw-alias-state-warning-primary,#e8a13a);align-items:center;gap:6px;display:flex}._5KEEsG_pendingTitle{color:var(--dsw-alias-label-primary);font-weight:600}._5KEEsG_pendingDesc{color:var(--dsw-alias-label-secondary)}._5KEEsG_pendingReason{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word}._5KEEsG_pendingQuestion{flex-direction:column;gap:4px;padding:4px 0;display:flex}._5KEEsG_pendingQHeader{color:var(--dsw-alias-label-primary);font-weight:600}._5KEEsG_pendingQText{color:var(--dsw-alias-label-secondary)}._5KEEsG_pendingOption{text-align:left;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:6px;flex-direction:column;align-items:flex-start;gap:2px;padding:5px 8px;font-size:12px;display:flex}._5KEEsG_pendingOption:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._5KEEsG_pendingOptionActive{text-align:left;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;flex-direction:column;align-items:flex-start;gap:2px;padding:5px 8px;font-size:12px;display:flex}._5KEEsG_pendingOptionDesc{color:var(--dsw-alias-label-dimmed);font-size:11px}._5KEEsG_pendingCustom{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-size:12px;font:inherit;background:0 0;border-radius:6px;padding:5px 8px}._5KEEsG_pendingCustom:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}._5KEEsG_pendingActions{justify-content:flex-end;gap:8px;margin-top:2px;display:flex}._5KEEsG_pendingBtnGhost{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:6px;padding:4px 12px;font-size:12px}._5KEEsG_pendingBtnPrimary{background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:6px;padding:4px 12px;font-size:12px}._5KEEsG_pendingBtnGhost:disabled,._5KEEsG_pendingBtnPrimary:disabled{opacity:.5;cursor:default}._5KEEsG_pendingBar{border:1px solid var(--dsw-alias-state-warning-primary,#d4a017);background:var(--dsw-alias-bg-layer-2);width:calc(100% - 16px);color:var(--dsw-alias-state-warning-primary,#d4a017);text-align:left;cursor:pointer;border-radius:8px;margin:0 8px 6px;padding:6px 10px;font-size:12px;display:block}._5KEEsG_pendingBar:hover{background:var(--dsw-alias-bg-layer-3)}._5KEEsG_writeNotice{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);border-radius:8px;align-items:center;gap:8px;margin:0 8px 6px;padding:6px 10px;font-size:12px;display:flex}._5KEEsG_writeNoticeClose{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;margin-left:auto;padding:0 4px;font-size:13px}._5KEEsG_parentStrip{border:none;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left;align-items:center;gap:6px;padding:5px 12px;font-size:11px;display:flex}._5KEEsG_parentStrip:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}._5KEEsG_dotRunning,._5KEEsG_dotPending,._5KEEsG_dotIdle{border-radius:999px;flex:none;width:7px;height:7px}._5KEEsG_dotRunning{background:var(--dsw-alias-state-business-primary,#2f88ff);animation:1.2s ease-in-out infinite _5KEEsG_sidechat-pulse}._5KEEsG_dotPending{background:var(--dsw-alias-state-warning-primary,#d4a017)}._5KEEsG_dotIdle{background:var(--dsw-alias-label-dimmed)}@keyframes _5KEEsG_sidechat-pulse{50%{opacity:.45}}._5KEEsG_actionGroup{gap:2px;margin-right:auto;display:inline-flex}";
2564
2990
  const tagId$1 = "dsh-sidenote/sidechat.module.css";
2565
2991
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
2566
2992
  const tag = document.createElement("style");
@@ -2570,93 +2996,95 @@ window.__ModuleLoader__.load({
2570
2996
  document.head.appendChild(tag);
2571
2997
  }
2572
2998
  var sidechat_module_css_default = {
2573
- "toolGenericBody": "_5KEEsG_toolGenericBody",
2574
- "jumpBottom": "_5KEEsG_jumpBottom",
2999
+ "pendingOptionActive": "_5KEEsG_pendingOptionActive",
2575
3000
  "rowActions": "_5KEEsG_rowActions",
2576
- "composer": "_5KEEsG_composer",
2577
- "stateScreen": "_5KEEsG_stateScreen",
3001
+ "toolHead": "_5KEEsG_toolHead",
3002
+ "pendingQText": "_5KEEsG_pendingQText",
3003
+ "pendingBar": "_5KEEsG_pendingBar",
3004
+ "assistantRow": "_5KEEsG_assistantRow",
3005
+ "writeNotice": "_5KEEsG_writeNotice",
3006
+ "pendingQuestion": "_5KEEsG_pendingQuestion",
2578
3007
  "composerFoot": "_5KEEsG_composerFoot",
2579
- "flowRowError": "_5KEEsG_flowRowError",
3008
+ "slashItemName": "_5KEEsG_slashItemName",
3009
+ "body": "_5KEEsG_body",
3010
+ "root": "_5KEEsG_root",
3011
+ "emptyTitle": "_5KEEsG_emptyTitle",
3012
+ "userRow": "_5KEEsG_userRow",
3013
+ "errorRow": "_5KEEsG_errorRow",
3014
+ "toolBodyText": "_5KEEsG_toolBodyText",
3015
+ "stopButton": "_5KEEsG_stopButton",
3016
+ "todoDot": "_5KEEsG_todoDot",
3017
+ "snapshotBody": "_5KEEsG_snapshotBody",
3018
+ "snapshotText": "_5KEEsG_snapshotText",
3019
+ "pendingReason": "_5KEEsG_pendingReason",
2580
3020
  "pendingOption": "_5KEEsG_pendingOption",
2581
- "pendingCustom": "_5KEEsG_pendingCustom",
2582
- "toolCard": "_5KEEsG_toolCard",
2583
- "densityRow": "_5KEEsG_densityRow",
2584
- "pendingActions": "_5KEEsG_pendingActions",
2585
- "reasoning": "_5KEEsG_reasoning",
2586
- "slashMenu": "_5KEEsG_slashMenu",
2587
- "pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
2588
3021
  "parentStrip": "_5KEEsG_parentStrip",
2589
- "dotRunning": "_5KEEsG_dotRunning",
2590
- "transcript": "_5KEEsG_transcript",
2591
- "toolError": "_5KEEsG_toolError",
2592
- "reflowButton": "_5KEEsG_reflowButton",
2593
- "input": "_5KEEsG_input",
3022
+ "sentChipRow": "_5KEEsG_sentChipRow",
2594
3023
  "pendingTitle": "_5KEEsG_pendingTitle",
2595
- "slashItem": "_5KEEsG_slashItem",
2596
- "pendingOptionActive": "_5KEEsG_pendingOptionActive",
2597
- "pendingCard": "_5KEEsG_pendingCard",
2598
- "pendingQuestion": "_5KEEsG_pendingQuestion",
2599
- "root": "_5KEEsG_root",
3024
+ "pendingActions": "_5KEEsG_pendingActions",
2600
3025
  "attachThumb": "_5KEEsG_attachThumb",
2601
- "pendingBar": "_5KEEsG_pendingBar",
2602
- "streamingHint": "_5KEEsG_streamingHint",
2603
- "dotIdle": "_5KEEsG_dotIdle",
2604
- "sidechat-pulse": "_5KEEsG_sidechat-pulse",
3026
+ "pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
2605
3027
  "todoBody": "_5KEEsG_todoBody",
2606
- "todoItem": "_5KEEsG_todoItem",
2607
- "sentChipRow": "_5KEEsG_sentChipRow",
2608
- "sentChip": "_5KEEsG_sentChip",
2609
- "toolRunning": "_5KEEsG_toolRunning",
2610
- "toolBody": "_5KEEsG_toolBody",
2611
- "pendingQText": "_5KEEsG_pendingQText",
2612
- "empty": "_5KEEsG_empty",
2613
- "assistantBody": "_5KEEsG_assistantBody",
2614
- "attachImg": "_5KEEsG_attachImg",
2615
- "sendButton": "_5KEEsG_sendButton",
2616
- "attachButton": "_5KEEsG_attachButton",
2617
- "userRow": "_5KEEsG_userRow",
2618
- "toolHead": "_5KEEsG_toolHead",
2619
- "emptyTitle": "_5KEEsG_emptyTitle",
2620
- "slashGroupTitle": "_5KEEsG_slashGroupTitle",
2621
- "writeNoticeClose": "_5KEEsG_writeNoticeClose",
2622
- "slashItemActive": "_5KEEsG_slashItemActive",
2623
- "pendingReason": "_5KEEsG_pendingReason",
2624
- "writeNotice": "_5KEEsG_writeNotice",
3028
+ "jumpBottom": "_5KEEsG_jumpBottom",
2625
3029
  "nativeHost": "_5KEEsG_nativeHost",
2626
- "userBubble": "_5KEEsG_userBubble",
2627
- "stateDetail": "_5KEEsG_stateDetail",
2628
- "slashItemName": "_5KEEsG_slashItemName",
2629
- "todoDot": "_5KEEsG_todoDot",
2630
- "toolLocations": "_5KEEsG_toolLocations",
2631
- "pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
2632
- "emptyIcon": "_5KEEsG_emptyIcon",
2633
- "composerCard": "_5KEEsG_composerCard",
2634
- "noticeRow": "_5KEEsG_noticeRow",
2635
3030
  "inheritedBody": "_5KEEsG_inheritedBody",
3031
+ "attachRail": "_5KEEsG_attachRail",
3032
+ "slashItem": "_5KEEsG_slashItem",
2636
3033
  "pendingHead": "_5KEEsG_pendingHead",
2637
- "composerChips": "_5KEEsG_composerChips",
2638
- "toolBodyText": "_5KEEsG_toolBodyText",
2639
- "modelSelect": "_5KEEsG_modelSelect",
2640
- "actionGroup": "_5KEEsG_actionGroup",
2641
- "pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
2642
- "stopButton": "_5KEEsG_stopButton",
3034
+ "userBubble": "_5KEEsG_userBubble",
2643
3035
  "modelLabel": "_5KEEsG_modelLabel",
2644
- "assistantRow": "_5KEEsG_assistantRow",
2645
- "emptyText": "_5KEEsG_emptyText",
2646
- "reasoningBody": "_5KEEsG_reasoningBody",
2647
3036
  "todoText": "_5KEEsG_todoText",
2648
- "densityToggle": "_5KEEsG_densityToggle",
2649
- "dotPending": "_5KEEsG_dotPending",
2650
- "pendingDesc": "_5KEEsG_pendingDesc",
2651
- "errorRow": "_5KEEsG_errorRow",
2652
- "pendingQHeader": "_5KEEsG_pendingQHeader",
3037
+ "emptyText": "_5KEEsG_emptyText",
3038
+ "pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
2653
3039
  "flowRow": "_5KEEsG_flowRow",
3040
+ "reasoningBody": "_5KEEsG_reasoningBody",
3041
+ "todoItem": "_5KEEsG_todoItem",
3042
+ "reasoning": "_5KEEsG_reasoning",
2654
3043
  "rowPreview": "_5KEEsG_rowPreview",
2655
- "attachRail": "_5KEEsG_attachRail",
3044
+ "flowRowError": "_5KEEsG_flowRowError",
3045
+ "toolRunning": "_5KEEsG_toolRunning",
3046
+ "actionGroup": "_5KEEsG_actionGroup",
2656
3047
  "headerSide": "_5KEEsG_headerSide",
2657
- "attachRemove": "_5KEEsG_attachRemove",
3048
+ "slashItemActive": "_5KEEsG_slashItemActive",
3049
+ "sidechat-pulse": "_5KEEsG_sidechat-pulse",
3050
+ "sentChip": "_5KEEsG_sentChip",
3051
+ "attachImg": "_5KEEsG_attachImg",
3052
+ "stateScreen": "_5KEEsG_stateScreen",
3053
+ "slashMenu": "_5KEEsG_slashMenu",
3054
+ "toolGenericBody": "_5KEEsG_toolGenericBody",
3055
+ "dotPending": "_5KEEsG_dotPending",
3056
+ "densityRow": "_5KEEsG_densityRow",
2658
3057
  "slashItemDetail": "_5KEEsG_slashItemDetail",
2659
- "body": "_5KEEsG_body"
3058
+ "input": "_5KEEsG_input",
3059
+ "toolLocations": "_5KEEsG_toolLocations",
3060
+ "toolError": "_5KEEsG_toolError",
3061
+ "attachButton": "_5KEEsG_attachButton",
3062
+ "pendingCard": "_5KEEsG_pendingCard",
3063
+ "attachRemove": "_5KEEsG_attachRemove",
3064
+ "pendingQHeader": "_5KEEsG_pendingQHeader",
3065
+ "assistantBody": "_5KEEsG_assistantBody",
3066
+ "empty": "_5KEEsG_empty",
3067
+ "dotIdle": "_5KEEsG_dotIdle",
3068
+ "pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
3069
+ "transcript": "_5KEEsG_transcript",
3070
+ "composer": "_5KEEsG_composer",
3071
+ "writeNoticeClose": "_5KEEsG_writeNoticeClose",
3072
+ "emptyIcon": "_5KEEsG_emptyIcon",
3073
+ "composerChips": "_5KEEsG_composerChips",
3074
+ "densityToggle": "_5KEEsG_densityToggle",
3075
+ "stateDetail": "_5KEEsG_stateDetail",
3076
+ "noticeRow": "_5KEEsG_noticeRow",
3077
+ "streamingHint": "_5KEEsG_streamingHint",
3078
+ "toolCard": "_5KEEsG_toolCard",
3079
+ "composerCard": "_5KEEsG_composerCard",
3080
+ "sendButton": "_5KEEsG_sendButton",
3081
+ "pendingCustom": "_5KEEsG_pendingCustom",
3082
+ "dotRunning": "_5KEEsG_dotRunning",
3083
+ "modelSelect": "_5KEEsG_modelSelect",
3084
+ "slashGroupTitle": "_5KEEsG_slashGroupTitle",
3085
+ "reflowButton": "_5KEEsG_reflowButton",
3086
+ "pendingDesc": "_5KEEsG_pendingDesc",
3087
+ "toolBody": "_5KEEsG_toolBody"
2660
3088
  };
2661
3089
  //#endregion
2662
3090
  //#region src/client/chat/ToolCard.tsx
@@ -2951,18 +3379,76 @@ window.__ModuleLoader__.load({
2951
3379
  ]
2952
3380
  });
2953
3381
  }
3382
+ /**
3383
+ * 剥出首条侧边用户消息里的主线进展快照块(Delivery_04):快照提升到继承卡
3384
+ * 区域渲染(SnapshotCard),气泡只留用户正文(零污染)。块不完整(缺闭合
3385
+ * 标签)视为无快照——消息原样渲染,绝不吃文本。
3386
+ */
3387
+ function splitSnapshotPrefix(text) {
3388
+ if (!text.startsWith("<mainline-progress-snapshot")) return null;
3389
+ const close = text.indexOf("</mainline-progress-snapshot>");
3390
+ if (close < 0) return null;
3391
+ const block = text.slice(0, close + 29);
3392
+ return {
3393
+ snapshot: block,
3394
+ takenAt: /taken-at="([^"]*)"/.exec(block)?.[1] ?? null,
3395
+ rest: text.slice(close + 29).trimStart()
3396
+ };
3397
+ }
3398
+ /** 主线进展快照卡:继承卡同族的可展开行(协议块原文,等宽换行)。 */
3399
+ function SnapshotCard({ takenAt, text, fold }) {
3400
+ useLocaleTick();
3401
+ const rowKey = "mainline-snapshot";
3402
+ const open = (0, react.useSyncExternalStore)((fn) => fold.subscribe(fn), () => fold.isOpen(rowKey));
3403
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3404
+ className: sidechat_module_css_default.flowRow,
3405
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
3406
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutline16, { size: 14 }),
3407
+ title: takenAt !== null ? t("snapshotLabelAt", { at: takenAt }) : t("snapshotLabel"),
3408
+ open,
3409
+ expandable: true,
3410
+ expandOnRowClick: true,
3411
+ previewChevron: true,
3412
+ onToggle: () => {
3413
+ fold.toggle(rowKey);
3414
+ },
3415
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3416
+ className: sidechat_module_css_default.snapshotBody,
3417
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
3418
+ className: sidechat_module_css_default.snapshotText,
3419
+ children: text
3420
+ })
3421
+ })
3422
+ })
3423
+ });
3424
+ }
2954
3425
  function MessageList({ messages, fold, boundarySeq, reflow, parentSessionId, sideTitle, onPromote }) {
2955
3426
  const [reflowAllDone, setReflowAllDone] = (0, react.useState)(false);
2956
3427
  const questions = (0, react.useMemo)(() => pairQuestions(messages), [messages]);
2957
3428
  const { inherited, fresh } = (0, react.useMemo)(() => partitionInherited(messages, boundarySeq), [messages, boundarySeq]);
2958
- const renderRow = (message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageRow, {
2959
- message,
2960
- question: questions.get(message.key),
2961
- fold,
2962
- reflow,
2963
- parentSessionId,
2964
- sideTitle
2965
- }, message.key);
3429
+ const snapshotInfo = (0, react.useMemo)(() => {
3430
+ const first = fresh.find((m) => m.role === "user");
3431
+ if (first === void 0) return null;
3432
+ const split = splitSnapshotPrefix(first.text);
3433
+ return split === null ? null : {
3434
+ key: first.key,
3435
+ ...split
3436
+ };
3437
+ }, [fresh]);
3438
+ const renderRow = (message) => {
3439
+ const m = snapshotInfo !== null && message.key === snapshotInfo.key ? {
3440
+ ...message,
3441
+ text: snapshotInfo.rest
3442
+ } : message;
3443
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageRow, {
3444
+ message: m,
3445
+ question: questions.get(m.key),
3446
+ fold,
3447
+ reflow,
3448
+ parentSessionId,
3449
+ sideTitle
3450
+ }, m.key);
3451
+ };
2966
3452
  const foldableKeys = (0, react.useMemo)(() => {
2967
3453
  const keys = [];
2968
3454
  if (inherited.length > 0) keys.push("inherited");
@@ -3017,6 +3503,11 @@ window.__ModuleLoader__.load({
3017
3503
  fold,
3018
3504
  children: inherited.map(renderRow)
3019
3505
  }),
3506
+ snapshotInfo !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapshotCard, {
3507
+ takenAt: snapshotInfo.takenAt,
3508
+ text: snapshotInfo.snapshot,
3509
+ fold
3510
+ }),
3020
3511
  fresh.map(renderRow)
3021
3512
  ]
3022
3513
  });
@@ -4054,11 +4545,13 @@ window.__ModuleLoader__.load({
4054
4545
  if (childId !== void 0 || forkStarted.current) return;
4055
4546
  forkStarted.current = true;
4056
4547
  let cancelled = false;
4057
- forkAndRegister(ctx, scope.sessionId, tab.id).catch((error) => {
4058
- if (!cancelled) setForkError(error instanceof Error ? error.message : String(error));
4548
+ const controller = new AbortController();
4549
+ forkAndRegister(ctx, scope.sessionId, tab.id, controller.signal).catch((error) => {
4550
+ if (!cancelled && !controller.signal.aborted) setForkError(error instanceof Error ? error.message : String(error));
4059
4551
  });
4060
4552
  return () => {
4061
4553
  cancelled = true;
4554
+ controller.abort();
4062
4555
  };
4063
4556
  }, [
4064
4557
  ctx,
@@ -4072,6 +4565,21 @@ window.__ModuleLoader__.load({
4072
4565
  (0, react.useEffect)(() => {
4073
4566
  openSessionWindow(session);
4074
4567
  }, [session]);
4568
+ (0, react.useEffect)(() => {
4569
+ if (meta.forkedMidTurn !== true || meta.inheritedPurged === true) return;
4570
+ if (childId === void 0 || session === void 0) return;
4571
+ armInheritedTurnPurge(ctx, childId, scope.sessionId, tab.id, meta.leakedPromptPrefix, meta.boundarySeq);
4572
+ }, [
4573
+ ctx,
4574
+ childId,
4575
+ session,
4576
+ meta.forkedMidTurn,
4577
+ meta.inheritedPurged,
4578
+ meta.leakedPromptPrefix,
4579
+ meta.boundarySeq,
4580
+ scope.sessionId,
4581
+ tab.id
4582
+ ]);
4075
4583
  const phase = phaseOf({
4076
4584
  childId,
4077
4585
  forkError,
@@ -4097,7 +4605,23 @@ window.__ModuleLoader__.load({
4097
4605
  ]);
4098
4606
  const pendingItems = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => visible && pendingStore !== void 0 ? pendingStore.subscribe(notify) : NOOP_UNSUBSCRIBE, [visible, pendingStore]), () => pendingStore?.getSnapshot() ?? NO_PENDING);
4099
4607
  const [writeNoticeDismissed, setWriteNoticeDismissed] = (0, react.useState)(() => typeof localStorage !== "undefined" && localStorage.getItem(WRITE_NOTICE_KEY) === "1");
4100
- const composer = useComposer(ctx, session, childId);
4608
+ const tabMetaRef = (0, react.useRef)(meta);
4609
+ tabMetaRef.current = meta;
4610
+ const composer = useComposer(ctx, session, childId, (0, react.useMemo)(() => ({ firstSendPrefix: () => {
4611
+ const m = tabMetaRef.current;
4612
+ if (m.forkedMidTurn !== true || m.parentSessionId === void 0) return null;
4613
+ const block = buildMainlineSnapshot(ctx, m.parentSessionId);
4614
+ if (block === null) return null;
4615
+ updateTabMeta(ctx, scope.sessionId, tab.id, (cur) => ({
4616
+ ...cur,
4617
+ forkedMidTurn: void 0
4618
+ }));
4619
+ return block;
4620
+ } }), [
4621
+ ctx,
4622
+ scope.sessionId,
4623
+ tab.id
4624
+ ]));
4101
4625
  const [modelName, setModelName] = (0, react.useState)(null);
4102
4626
  (0, react.useEffect)(() => {
4103
4627
  if (childId === void 0 || session === void 0) return;
@@ -4824,6 +5348,10 @@ window.__ModuleLoader__.load({
4824
5348
  };
4825
5349
  watcher.unsub = session.subscribe(() => {
4826
5350
  const running = session.getSnapshot()?.running === true;
5351
+ if (isPurgingInheritedTurn(childId)) {
5352
+ watcher.wasRunning = running;
5353
+ return;
5354
+ }
4827
5355
  if (shouldNotify(watcher.wasRunning, running, watcher.since, Date.now())) showToast(t("sideChatDone", { title }));
4828
5356
  watcher.wasRunning = running;
4829
5357
  });
@@ -5373,43 +5901,43 @@ window.__ModuleLoader__.load({
5373
5901
  document.head.appendChild(tag);
5374
5902
  }
5375
5903
  var annotate_module_css_default = {
5904
+ "editorEdit": "_SVViG_editorEdit",
5905
+ "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
5906
+ "editorNew": "_SVViG_editorNew",
5376
5907
  "toolbar": "_SVViG_toolbar",
5377
- "chipRow": "_SVViG_chipRow",
5378
5908
  "chipWrap": "_SVViG_chipWrap",
5379
- "editorAsk": "_SVViG_editorAsk",
5380
- "chipNumber": "_SVViG_chipNumber",
5909
+ "chipPanel": "_SVViG_chipPanel",
5910
+ "editorTarget": "_SVViG_editorTarget",
5381
5911
  "chipNote": "_SVViG_chipNote",
5382
- "editorAskRow": "_SVViG_editorAskRow",
5912
+ "editorAsk": "_SVViG_editorAsk",
5383
5913
  "badgeSent": "_SVViG_badgeSent",
5384
- "hint": "_SVViG_hint",
5385
- "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
5386
- "cancelButton": "_SVViG_cancelButton",
5387
- "saveButton": "_SVViG_saveButton",
5388
- "editorError": "_SVViG_editorError",
5914
+ "sentChip": "_SVViG_sentChip",
5915
+ "sentCardNote": "_SVViG_sentCardNote",
5916
+ "confirmButton": "_SVViG_confirmButton",
5917
+ "editorFooter": "_SVViG_editorFooter",
5918
+ "chipRow": "_SVViG_chipRow",
5919
+ "toolbarButton": "_SVViG_toolbarButton",
5920
+ "editorTextarea": "_SVViG_editorTextarea",
5921
+ "deleteButton": "_SVViG_deleteButton",
5922
+ "badge": "_SVViG_badge",
5923
+ "chipRemove": "_SVViG_chipRemove",
5389
5924
  "chipClearAll": "_SVViG_chipClearAll",
5925
+ "sentCardTitle": "_SVViG_sentCardTitle",
5926
+ "editorAskRow": "_SVViG_editorAskRow",
5927
+ "editorError": "_SVViG_editorError",
5928
+ "chip": "_SVViG_chip",
5390
5929
  "sentCardQuote": "_SVViG_sentCardQuote",
5391
5930
  "editorSpacer": "_SVViG_editorSpacer",
5392
- "confirmButton": "_SVViG_confirmButton",
5931
+ "highlight": "_SVViG_highlight",
5932
+ "sentChipRow": "_SVViG_sentChipRow",
5933
+ "cancelButton": "_SVViG_cancelButton",
5934
+ "saveButton": "_SVViG_saveButton",
5393
5935
  "editorInput": "_SVViG_editorInput",
5394
- "editorFooter": "_SVViG_editorFooter",
5395
- "badge": "_SVViG_badge",
5396
- "sentChip": "_SVViG_sentChip",
5397
5936
  "chipText": "_SVViG_chipText",
5398
5937
  "hintClose": "_SVViG_hintClose",
5399
- "chipPanel": "_SVViG_chipPanel",
5400
- "deleteButton": "_SVViG_deleteButton",
5401
- "editorNew": "_SVViG_editorNew",
5402
- "highlight": "_SVViG_highlight",
5403
- "editorTextarea": "_SVViG_editorTextarea",
5404
- "sentCardTitle": "_SVViG_sentCardTitle",
5405
- "sentCardNote": "_SVViG_sentCardNote",
5406
- "toolbarButton": "_SVViG_toolbarButton",
5407
- "editorEdit": "_SVViG_editorEdit",
5408
- "chip": "_SVViG_chip",
5409
- "chipRemove": "_SVViG_chipRemove",
5410
- "editorTarget": "_SVViG_editorTarget",
5411
5938
  "toolbarBelow": "_SVViG_toolbarBelow",
5412
- "sentChipRow": "_SVViG_sentChipRow"
5939
+ "chipNumber": "_SVViG_chipNumber",
5940
+ "hint": "_SVViG_hint"
5413
5941
  };
5414
5942
  //#endregion
5415
5943
  //#region src/client/annotate/hint.tsx
@@ -5740,7 +6268,8 @@ window.__ModuleLoader__.load({
5740
6268
  /**
5741
6269
  * 浮层水平落点(W03):优先锚点右侧;右侧剩余空间不足(贴着展开的
5742
6270
  * better-sidebar 面板时必然不足——角标 gutter 就在消息列右缘)翻到左侧。
5743
- * 右边界取面板左缘([data-dsh-better-sidebar],宿主 DOM 属性,查不到按视口)。
6271
+ * 右边界取面板左缘([data-dsh-better-sidebar],better-sidebar 的根锚点——
6272
+ * 仅在装了它的宿主上存在,查不到按视口)。
5744
6273
  */
5745
6274
  function popoverLeft(x, width) {
5746
6275
  const panel = typeof document === "undefined" ? null : document.querySelector("[data-dsh-better-sidebar]")?.getBoundingClientRect() ?? null;