@1agents/session-reader 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -473,6 +473,28 @@ $ 1session graph ca8325e1
473
473
 
474
474
  运行时捕获:若环境注入了 `SESSION_READER_CALLER_SESSION`,调用当下就直接落边(`observed / runtime:caller-env`),无需事后从历史里恢复。
475
475
 
476
+ ## DSH 插件入口
477
+
478
+ 从 npm 安装到 DSH Web profile(包内声明了 `dsh.bundle.patch`,安装后自动启用):
479
+
480
+ ```sh
481
+ pnpm dsh plugin --profile web add @1agents/session-reader @1agents/dsh-acp
482
+ ```
483
+
484
+ 只查看历史时可只安装 `@1agents/session-reader`;插件按请求查询 ACP 服务,不把 ACP 注册为加载依赖。已安装独立 `dsh` 命令时省略开头的 `pnpm`;桌面版将 profile 改为 `desktop`。更新包后重启对应的 DSH 服务。原会话续聊还需单独启动 `@1agents/acp-service@^0.2.0`,具体配置见 [ACP 插件说明](https://github.com/scottzx/1acp/tree/main/packages/dsh-plugin)。
485
+
486
+ DSH 侧栏中的「历史会话」打开跨 Agent 会话浏览器。入口沿用「插件」「自动化任务」的主文字色、字号、行高、圆角、悬停与键盘焦点样式;展开时使用 16px 图标,折叠时使用 18px 图标和 36px 按钮,颜色跟随 DSH 的亮暗主题。
487
+
488
+ ### 在 DSH 中继续原会话
489
+
490
+ 安装并启用 `@1agents/dsh-acp`,启动其配置的 acp-service 后,Claude、Codex、Grok 的历史详情提供「在 DSH 中继续原会话」。session-reader 读取历史并调用 ACP 插件的 `oneagentsAcpSessions` 服务;1acp 恢复原 Agent 的原生会话,后续输入继续发给它。原工作目录必须存在;不会改用当前目录或新建空会话。DSH 来源直接打开原有 DSH 会话,不复制或重写日志。
491
+
492
+ ACP 未安装、Agent 未就绪或来源不支持时,历史仍可只读查看。导入、鉴权和恢复失败会显示原因并保留预览。重复导入打开同一个 DSH 会话;导入历史是一次性快照,不持续同步其他客户端新增的消息。旧版导入产生的历史副本不会自动改绑。外部工具调用与结果保留为只读历史记录,不触发 DSH 工具执行。
493
+
494
+ `session_open_in_dsh` 和 `POST /api/session-reader/open-in-dsh` 共用同一入口,接受 `sessionId`(ID、前缀或原始文件路径)和可选的 `provider`;provider 用于区分不同来源的同名 ID。外部成功结果包含 `dshSessionId`、`workspace`、`workspaceId`、`agent` 与 `continuation: "native"`,DSH 来源返回 `continuation: "dsh"`。`GET /api/session-reader/continuation?provider=codex` 返回续聊可用性及不可用原因。独立 CLI、解析库与历史搜索不需要安装 ACP 插件。
495
+
496
+ 验证当前 DSH 事件兼容性:先运行 `npm run build`,再运行 `node scripts/check-dsh-import.mjs /absolute/path/DSH`(DSH checkout 须已构建)。`test/fixtures/dsh-import/` 保存用户消息、思考、工具调用与结果,以及未完成尾部的固定历史快照。
497
+
476
498
  ## `1session web`:独立浏览器
477
499
 
478
500
  没有 DSH 也能看同一套历史会话列表、对话预览和文件账本:
@@ -10,14 +10,16 @@ export function convertSessionToDshEvents(session, options = {}) {
10
10
  let currentStep = 1;
11
11
  let turnOpen = false;
12
12
  let stepOpen = false;
13
+ let hasSystemHead = false;
13
14
  const defaultTime = session.ref.createdAt
14
15
  ? Date.parse(session.ref.createdAt)
15
16
  : Date.now();
17
+ const fallbackTime = Number.isFinite(defaultTime) ? defaultTime : Date.now();
16
18
  function parseTime(timestamp) {
17
19
  if (!timestamp)
18
- return defaultTime;
20
+ return fallbackTime;
19
21
  const parsed = Date.parse(timestamp);
20
- return isNaN(parsed) ? defaultTime : parsed;
22
+ return isNaN(parsed) ? fallbackTime : parsed;
21
23
  }
22
24
  function emit(type, data, time, surfaceOp) {
23
25
  events.push({
@@ -39,6 +41,13 @@ export function convertSessionToDshEvents(session, options = {}) {
39
41
  if (!stepOpen) {
40
42
  emit('step/start', { turn: turnNum, step: stepNum }, time);
41
43
  stepOpen = true;
44
+ if (!hasSystemHead) {
45
+ emit('system/message', {
46
+ turn: turnNum, step: stepNum,
47
+ message: { id: `system-${session.ref.id}`, role: 'system', source: { kind: 'system-prompt' }, content: [] },
48
+ }, time, 'append');
49
+ hasSystemHead = true;
50
+ }
42
51
  }
43
52
  }
44
53
  function closeStep(turnNum, stepNum, time) {
@@ -61,7 +70,7 @@ export function convertSessionToDshEvents(session, options = {}) {
61
70
  title: session.ref.title.trim(),
62
71
  messageSeqs: [],
63
72
  source: { kind: 'user' },
64
- }, defaultTime);
73
+ }, fallbackTime);
65
74
  }
66
75
  for (let i = 0; i < session.turns.length; i++) {
67
76
  const turnEvent = session.turns[i];
@@ -97,6 +106,7 @@ export function convertSessionToDshEvents(session, options = {}) {
97
106
  source: { kind: 'model', provider: providerName, model: modelName },
98
107
  },
99
108
  stream: [
109
+ { type: 'chunk', time: eventTime, chunk: { type: 'block-start', index: 0, blockType: 'reasoning' } },
100
110
  {
101
111
  type: 'chunk',
102
112
  time: eventTime,
@@ -106,53 +116,31 @@ export function convertSessionToDshEvents(session, options = {}) {
106
116
  text: thinkingText,
107
117
  },
108
118
  },
119
+ { type: 'chunk', time: eventTime, chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text: thinkingText } } },
109
120
  ],
110
121
  }, eventTime, 'append');
111
122
  }
112
123
  break;
113
124
  }
114
- case 'tool_call': {
115
- ensureStepStart(currentTurn, currentStep, eventTime);
116
- const callId = turnEvent.id;
117
- const toolName = turnEvent.toolName ?? 'tool';
118
- const toolArgsStr = typeof turnEvent.toolArgs === 'string'
119
- ? turnEvent.toolArgs
120
- : JSON.stringify(turnEvent.toolArgs ?? {});
121
- emit('tool/call', {
122
- turn: currentTurn,
123
- step: currentStep,
124
- callId,
125
- name: toolName,
126
- arguments: toolArgsStr,
127
- }, eventTime);
128
- break;
129
- }
125
+ case 'tool_call':
130
126
  case 'tool_result': {
131
127
  ensureStepStart(currentTurn, currentStep, eventTime);
132
- const callId = turnEvent.id;
133
- const toolName = turnEvent.toolName ?? 'tool';
134
- const resultContent = turnEvent.toolResult ?? '';
135
- emit('tool/result', {
136
- turn: currentTurn,
137
- step: currentStep,
138
- message: {
139
- id: `tool-res-${callId}`,
140
- role: 'user',
141
- source: { kind: 'tool', callId, tool: toolName },
142
- content: [
143
- {
144
- type: 'tool-result',
145
- toolCallId: callId,
146
- content: [{ type: 'text', text: resultContent }],
147
- ...(turnEvent.isError ? { isError: true } : {}),
148
- },
149
- ],
150
- },
151
- ...(turnEvent.exitCode !== undefined ? { meta: { exitCode: turnEvent.exitCode } } : {}),
128
+ const label = turnEvent.kind === 'tool_call' ? 'External tool call' : 'External tool result';
129
+ const name = turnEvent.toolName ?? 'tool';
130
+ const detail = turnEvent.kind === 'tool_call' ? JSON.stringify(turnEvent.toolArgs ?? {}) : turnEvent.toolResult ?? '';
131
+ const status = `${turnEvent.isError ? ' · error' : ''}${turnEvent.exitCode !== undefined ? ` · exit ${turnEvent.exitCode}` : ''}`;
132
+ const text = `[${label} · ${name}${status}]\n${detail}`;
133
+ // Normalized providers do not preserve reliable call/result identities.
134
+ // External activity remains read-only transcript, as in live ACP output.
135
+ emit('assistant/message', {
136
+ turn: currentTurn, step: currentStep,
137
+ message: { id: `external-${i}-${turnEvent.id}`, role: 'assistant', source: { kind: 'model', provider: providerName, model: modelName }, content: [{ type: 'reasoning', text }] },
138
+ stream: [
139
+ { type: 'chunk', time: eventTime, chunk: { type: 'block-start', index: 0, blockType: 'reasoning' } },
140
+ { type: 'reasoning-chunks', time0: eventTime, index: 0, dt: [], texts: [text] },
141
+ { type: 'chunk', time: eventTime, chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text } } },
142
+ ],
152
143
  }, eventTime, 'append');
153
- // Tool execution concludes a step; next action starts a new step
154
- closeStep(currentTurn, currentStep, eventTime);
155
- currentStep++;
156
144
  break;
157
145
  }
158
146
  case 'assistant': {
@@ -168,6 +156,7 @@ export function convertSessionToDshEvents(session, options = {}) {
168
156
  source: { kind: 'model', provider: providerName, model: modelName },
169
157
  },
170
158
  stream: [
159
+ { type: 'chunk', time: eventTime, chunk: { type: 'block-start', index: 0, blockType: 'text' } },
171
160
  {
172
161
  type: 'chunk',
173
162
  time: eventTime,
@@ -177,6 +166,7 @@ export function convertSessionToDshEvents(session, options = {}) {
177
166
  text,
178
167
  },
179
168
  },
169
+ { type: 'chunk', time: eventTime, chunk: { type: 'block-end', index: 0, block: { type: 'text', text } } },
180
170
  ],
181
171
  }, eventTime, 'append');
182
172
  const nextEvent = session.turns[i + 1];
@@ -191,8 +181,8 @@ export function convertSessionToDshEvents(session, options = {}) {
191
181
  }
192
182
  // Final cleanup if the last turn was still open
193
183
  if (turnOpen) {
194
- const lastTime = events.length > 0 ? events[events.length - 1].time : defaultTime;
195
- closeTurn(currentTurn, lastTime);
184
+ const lastTime = events.length > 0 ? events[events.length - 1].time : fallbackTime;
185
+ closeTurn(currentTurn, lastTime, 'interrupted');
196
186
  }
197
187
  return events;
198
188
  }
@@ -18080,54 +18080,89 @@ function mountConversation(container, options = {}) {
18080
18080
  };
18081
18081
  }
18082
18082
 
18083
+ // src/dsh/copy.ts
18084
+ var continuationDictionaries = {
18085
+ zh: {
18086
+ continue: "\u5728 DSH \u4E2D\u7EE7\u7EED\u539F\u4F1A\u8BDD",
18087
+ openDsh: "\u5728 DSH \u4E2D\u6253\u5F00",
18088
+ readOnly: "\u53EA\u8BFB\u67E5\u770B",
18089
+ loading: "\u6B63\u5728\u6062\u590D\u539F\u4F1A\u8BDD\u2026",
18090
+ failed: "\u7EED\u804A\u5931\u8D25\uFF1A",
18091
+ unavailable: "\u6682\u65F6\u65E0\u6CD5\u68C0\u67E5\u7EED\u804A\u72B6\u6001",
18092
+ navigationUnavailable: "DSH \u4F1A\u8BDD\u5BFC\u822A\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5386\u53F2\u9762\u677F"
18093
+ },
18094
+ en: {
18095
+ continue: "Continue original session in DSH",
18096
+ openDsh: "Open in DSH",
18097
+ readOnly: "Read only",
18098
+ loading: "Restoring session\u2026",
18099
+ failed: "Could not continue: ",
18100
+ unavailable: "Continuation status is unavailable",
18101
+ navigationUnavailable: "DSH navigation is unavailable; reopen the history panel"
18102
+ }
18103
+ };
18104
+ function continuationCopy(translate) {
18105
+ if (translate) return Object.fromEntries(Object.keys(continuationDictionaries.zh).map((key) => [key, translate(key)]));
18106
+ const locale = typeof document === "undefined" ? "zh" : document.documentElement.lang || navigator.language;
18107
+ return locale.startsWith("zh") ? continuationDictionaries.zh : continuationDictionaries.en;
18108
+ }
18109
+
18083
18110
  // src/dsh/client.ts
18084
18111
  var CSS_TEXT = `
18112
+ /* Match DSH SidebarRoot panel rows in expanded and collapsed layouts. */
18085
18113
  [data-dsh-session-reader-entry] {
18086
18114
  box-sizing: border-box;
18087
- width: 100%;
18088
- height: 36px;
18089
- color: var(--dsw-alias-label-secondary, #666);
18115
+ flex: none;
18116
+ min-height: 36px;
18117
+ margin: 0 2px 4px;
18118
+ color: var(--dsw-alias-label-primary);
18090
18119
  cursor: pointer;
18091
18120
  white-space: nowrap;
18092
18121
  background: transparent;
18093
18122
  border: none;
18094
- border-radius: 8px;
18123
+ border-radius: var(--dsw-radius-md);
18095
18124
  align-items: center;
18096
18125
  gap: 8px;
18097
- padding: 0 10px;
18098
- font-size: 13px;
18126
+ padding: 7px 8px;
18127
+ font: inherit;
18128
+ line-height: 22px;
18129
+ text-align: left;
18099
18130
  display: flex;
18100
- transition: background 0.15s ease, color 0.15s ease;
18101
18131
  }
18102
18132
  [data-dsh-session-reader-entry]:hover {
18103
- background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.05));
18104
- color: var(--dsw-alias-label-primary, #111);
18133
+ background: var(--dsw-alias-interactive-bg-hover);
18105
18134
  }
18106
- body[data-ds-dark-theme] [data-dsh-session-reader-entry]:hover {
18107
- background: rgba(255,255,255,0.08);
18108
- color: #fff;
18135
+ [data-dsh-session-reader-entry]:focus-visible {
18136
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
18137
+ outline-offset: -2px;
18109
18138
  }
18110
18139
  [data-dsh-session-reader-entry] .sr-icon {
18111
18140
  flex: none;
18112
18141
  display: inline-flex;
18113
18142
  align-items: center;
18114
18143
  justify-content: center;
18115
- width: 24px;
18116
- height: 24px;
18144
+ }
18145
+ [data-dsh-session-reader-entry] .sr-icon svg {
18146
+ width: 16px;
18147
+ height: 16px;
18117
18148
  }
18118
18149
  [data-dsh-session-reader-entry] .sr-label {
18150
+ min-width: 0;
18119
18151
  text-overflow: ellipsis;
18120
18152
  overflow: hidden;
18121
18153
  }
18122
18154
  [data-dsh-frame][data-sidebar-collapsed] [data-dsh-session-reader-entry],
18123
18155
  [data-sidebar-collapsed] [data-dsh-session-reader-entry] {
18124
- border-radius: 50%;
18125
18156
  justify-content: center;
18126
18157
  width: 36px;
18127
18158
  height: 36px;
18128
- margin: 0 auto 12px;
18159
+ margin: 0 0 12px;
18129
18160
  padding: 0;
18130
18161
  }
18162
+ [data-sidebar-collapsed] [data-dsh-session-reader-entry] .sr-icon svg {
18163
+ width: 18px;
18164
+ height: 18px;
18165
+ }
18131
18166
  [data-dsh-frame][data-sidebar-collapsed] [data-dsh-session-reader-entry] .sr-label,
18132
18167
  [data-sidebar-collapsed] [data-dsh-session-reader-entry] .sr-label {
18133
18168
  display: none;
@@ -18497,7 +18532,7 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18497
18532
  }
18498
18533
  `;
18499
18534
  var ICON_SVG = `<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.2"/><polyline points="8 4.2 8 8 10.5 9.5"/></svg>`;
18500
- var inject = ["sessions", "workspaces"];
18535
+ var inject = ["sessions", "workspaces", "uiWorkspace", "locale"];
18501
18536
  function getBoot() {
18502
18537
  if (typeof window === "undefined") return {};
18503
18538
  return window.__SR_UI__ ?? {};
@@ -18507,6 +18542,9 @@ function isStandalone() {
18507
18542
  }
18508
18543
  function apply(_ctx) {
18509
18544
  if (typeof document === "undefined") return;
18545
+ if (_ctx.locale) _ctx.effect(() => _ctx.locale.register("sessionReaderContinuation", continuationDictionaries));
18546
+ const translate = _ctx.locale?.bind("sessionReaderContinuation");
18547
+ const getContinuationCopy = () => continuationCopy(translate);
18510
18548
  if (!document.getElementById("dsh-session-reader-css")) {
18511
18549
  const style = document.createElement("style");
18512
18550
  style.id = "dsh-session-reader-css";
@@ -18586,48 +18624,35 @@ function apply(_ctx) {
18586
18624
  cachedSessions = [];
18587
18625
  currentTab = "chat";
18588
18626
  }
18589
- async function openInDshChat(sessionId, btn) {
18627
+ async function openInDshChat(sessionId, btn, provider) {
18628
+ const copy = getContinuationCopy();
18629
+ const label = btn?.textContent;
18590
18630
  if (btn) {
18591
- btn.innerHTML = "\u23F3 \u6B63\u5728\u52A0\u8F7D\u5230 DSH...";
18631
+ btn.textContent = copy.loading;
18592
18632
  btn.disabled = true;
18593
18633
  }
18594
18634
  try {
18595
- const activeWs = getActiveWorkspaceInfo();
18596
18635
  const res = await fetch("/api/session-reader/open-in-dsh", {
18597
18636
  method: "POST",
18598
18637
  headers: { "Content-Type": "application/json" },
18599
- body: JSON.stringify({
18600
- sessionId,
18601
- currentCwd: activeWs.cwd,
18602
- workspaceId: activeWs.workspaceId
18603
- })
18638
+ body: JSON.stringify({ sessionId, provider })
18604
18639
  });
18605
18640
  const result = await res.json();
18606
- if (!res.ok || !result.success) {
18607
- alert("\u63A5\u5165 DSH \u5931\u8D25: " + (result.error || res.statusText));
18608
- if (btn) {
18609
- btn.innerHTML = "\u{1F680} \u5728 DSH \u5B98\u65B9 Chat \u4E2D\u6253\u5F00";
18610
- btn.disabled = false;
18611
- }
18612
- return;
18613
- }
18614
- const dshSessionId = result.dshSessionId;
18615
- closePanel();
18641
+ if (!res.ok || !result.success) throw new Error(result.error || res.statusText);
18616
18642
  const sessions = dshCtx?.get ? dshCtx.get("sessions") ?? dshCtx.sessions : dshCtx?.sessions;
18617
- if (sessions) {
18618
- if (typeof sessions.refresh === "function") {
18619
- await sessions.refresh();
18620
- }
18621
- if (typeof sessions.open === "function") {
18622
- sessions.open(dshSessionId);
18623
- }
18624
- } else {
18625
- console.warn("[session-reader] Could not obtain DSH sessions service from ctx");
18626
- }
18643
+ const navigation = dshCtx?.get ? dshCtx.get("uiWorkspace") : dshCtx?.uiWorkspace;
18644
+ if (!navigation) throw new Error(copy.navigationUnavailable);
18645
+ await sessions.refresh();
18646
+ navigation.openSession(result.dshSessionId);
18647
+ closePanel();
18627
18648
  } catch (err) {
18628
- alert("\u8BF7\u6C42\u51FA\u9519: " + (err?.message || err));
18649
+ if (activeSessionData) {
18650
+ activeSessionData.continuationError = copy.failed + (err?.message || err);
18651
+ renderModalBody();
18652
+ }
18653
+ } finally {
18629
18654
  if (btn) {
18630
- btn.innerHTML = "\u{1F680} \u5728 DSH \u5B98\u65B9 Chat \u4E2D\u6253\u5F00";
18655
+ btn.textContent = label ?? copy.continue;
18631
18656
  btn.disabled = false;
18632
18657
  }
18633
18658
  }
@@ -18658,6 +18683,19 @@ function apply(_ctx) {
18658
18683
  const data = await res.json();
18659
18684
  if (detailAbort !== ac) return;
18660
18685
  activeSessionData = data;
18686
+ renderModalBody();
18687
+ if (!isStandalone()) {
18688
+ try {
18689
+ const status = await fetch(`/api/session-reader/continuation?provider=${encodeURIComponent(data.ref.provider)}`, { signal: ac.signal, cache: "no-store" });
18690
+ if (!status.ok) throw new Error(`HTTP ${status.status}`);
18691
+ const continuation = await status.json();
18692
+ if (detailAbort !== ac) return;
18693
+ activeSessionData.continuation = continuation;
18694
+ } catch (error) {
18695
+ if (detailAbort !== ac) return;
18696
+ activeSessionData.continuation = { available: false, reason: String(error) };
18697
+ }
18698
+ }
18661
18699
  } catch (err) {
18662
18700
  if (err?.name === "AbortError") {
18663
18701
  if (detailAbort === ac) {
@@ -18749,9 +18787,6 @@ function apply(_ctx) {
18749
18787
  item.classList.add("active");
18750
18788
  loadSessionDetail(s3.id);
18751
18789
  });
18752
- item.addEventListener("dblclick", () => {
18753
- openInDshChat(s3.id);
18754
- });
18755
18790
  listEl.appendChild(item);
18756
18791
  });
18757
18792
  }
@@ -18770,7 +18805,8 @@ function apply(_ctx) {
18770
18805
  contentEl.innerHTML = `<div style="padding:60px;text-align:center;color:#e11d48;font-size:13px;">\u52A0\u8F7D\u5931\u8D25: ${activeSessionData.error}</div>`;
18771
18806
  return;
18772
18807
  }
18773
- const { ref, overview, turns, files } = activeSessionData;
18808
+ const { ref, overview, turns, files, continuation, continuationError } = activeSessionData;
18809
+ const copy = getContinuationCopy();
18774
18810
  const standalone = isStandalone();
18775
18811
  const body = currentTab === "chat" ? '<div id="sr-chat-root" style="width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;"></div>' : currentTab === "files" ? renderFilesView(files) : renderOverviewView(overview);
18776
18812
  contentEl.innerHTML = `
@@ -18780,8 +18816,8 @@ function apply(_ctx) {
18780
18816
  ${escapeHtml2(ref.title || "Untitled")}
18781
18817
  </div>
18782
18818
  <div class="sr-header-actions">
18783
- ${standalone ? "" : `<button class="sr-open-dsh-btn" id="sr-btn-open-dsh" title="\u5C06\u6B64\u5386\u53F2\u4F1A\u8BDD\u63A5\u5165\u5E76\u5728 DSH \u5B98\u65B9\u4E3B\u804A\u5929\u7A97\u53E3\u4E2D\u67E5\u770B">
18784
- \u{1F680} \u5728 DSH \u5B98\u65B9 Chat \u4E2D\u6253\u5F00
18819
+ ${standalone ? "" : `<button class="sr-open-dsh-btn" id="sr-btn-open-dsh" ${continuation?.available ? "" : "disabled"}>
18820
+ ${escapeHtml2(continuation?.available ? (ref.provider === "dsh" ? copy.openDsh : copy.continue) + " \xB7 " + (continuation.agent || ref.provider) : copy.readOnly)}
18785
18821
  </button>`}
18786
18822
  <div class="sr-tabs">
18787
18823
  <button class="sr-tab-btn ${currentTab === "chat" ? "active" : ""}" data-tab="chat">\u{1F4AC} \u5FEB\u901F\u9884\u89C8</button>
@@ -18790,6 +18826,7 @@ function apply(_ctx) {
18790
18826
  </div>
18791
18827
  </div>
18792
18828
  </div>
18829
+ ${!standalone && (continuationError || !continuation?.available) ? `<div class="sr-header-path" role="status">${escapeHtml2(continuationError || continuation?.reason || copy.unavailable)}</div>` : ""}
18793
18830
  <div class="sr-header-path" title="${escapeHtml2(ref.path || "")}">
18794
18831
  <span class="sr-header-provider">${ref.provider}</span> \xB7 \u8DEF\u5F84: ${escapeHtml2(ref.path || "")}
18795
18832
  </div>
@@ -18801,7 +18838,7 @@ function apply(_ctx) {
18801
18838
  const openDshBtn = contentEl.querySelector("#sr-btn-open-dsh");
18802
18839
  if (openDshBtn) {
18803
18840
  openDshBtn.addEventListener("click", () => {
18804
- openInDshChat(activeSessionId, openDshBtn);
18841
+ openInDshChat(ref.path || activeSessionId, openDshBtn, ref.provider);
18805
18842
  });
18806
18843
  }
18807
18844
  contentEl.querySelectorAll(".sr-tab-btn").forEach((btn) => {
@@ -0,0 +1,26 @@
1
+ /** Copy for native continuation, shared by the embedded browser entry. */
2
+ export declare const continuationDictionaries: {
3
+ zh: {
4
+ continue: string;
5
+ openDsh: string;
6
+ readOnly: string;
7
+ loading: string;
8
+ failed: string;
9
+ unavailable: string;
10
+ navigationUnavailable: string;
11
+ };
12
+ en: {
13
+ continue: string;
14
+ openDsh: string;
15
+ readOnly: string;
16
+ loading: string;
17
+ failed: string;
18
+ unavailable: string;
19
+ navigationUnavailable: string;
20
+ };
21
+ };
22
+ type Copy = {
23
+ [K in keyof typeof continuationDictionaries.zh]: string;
24
+ };
25
+ export declare function continuationCopy(translate?: (key: keyof Copy) => string): Copy;
26
+ export {};
@@ -0,0 +1,19 @@
1
+ /** Copy for native continuation, shared by the embedded browser entry. */
2
+ export const continuationDictionaries = {
3
+ zh: {
4
+ continue: '在 DSH 中继续原会话', openDsh: '在 DSH 中打开', readOnly: '只读查看',
5
+ loading: '正在恢复原会话…', failed: '续聊失败:', unavailable: '暂时无法检查续聊状态',
6
+ navigationUnavailable: 'DSH 会话导航不可用,请重新打开历史面板',
7
+ },
8
+ en: {
9
+ continue: 'Continue original session in DSH', openDsh: 'Open in DSH', readOnly: 'Read only',
10
+ loading: 'Restoring session…', failed: 'Could not continue: ', unavailable: 'Continuation status is unavailable',
11
+ navigationUnavailable: 'DSH navigation is unavailable; reopen the history panel',
12
+ },
13
+ };
14
+ export function continuationCopy(translate) {
15
+ if (translate)
16
+ return Object.fromEntries(Object.keys(continuationDictionaries.zh).map(key => [key, translate(key)]));
17
+ const locale = typeof document === 'undefined' ? 'zh' : document.documentElement.lang || navigator.language;
18
+ return locale.startsWith('zh') ? continuationDictionaries.zh : continuationDictionaries.en;
19
+ }
@@ -0,0 +1,56 @@
1
+ /** DSH admission delegates native execution to the installed ACP plugin. */
2
+ import type { AgentProvider, SessionRef } from '../types.js';
3
+ import { type DshSessionEvent } from './adapter.js';
4
+ export interface Availability {
5
+ available: boolean;
6
+ agent?: string;
7
+ reason?: string;
8
+ }
9
+ interface OpenResult {
10
+ success: true;
11
+ dshSessionId: string;
12
+ workspace?: string;
13
+ workspaceId?: string;
14
+ agent?: string;
15
+ continuation: 'native' | 'dsh';
16
+ }
17
+ interface AcpSessions {
18
+ availability(provider: string): Promise<Availability>;
19
+ importSession(input: {
20
+ provider: string;
21
+ nativeSessionId: string;
22
+ cwd: string;
23
+ events: DshSessionEvent[];
24
+ }): Promise<OpenResult>;
25
+ }
26
+ export interface OpenContext {
27
+ get(key: 'oneagentsAcpSessions'): AcpSessions | undefined;
28
+ sessionController?: {
29
+ resolveAgent(id: string): Promise<{
30
+ error: Error;
31
+ } | {
32
+ agent: {
33
+ session: {
34
+ header: {
35
+ cwd?: string;
36
+ };
37
+ };
38
+ };
39
+ }>;
40
+ };
41
+ workspaceRegistry?: {
42
+ create(cwd: string): Promise<{
43
+ id: string;
44
+ attachSession(id: string): Promise<void>;
45
+ }>;
46
+ };
47
+ }
48
+ export interface OpenRequest {
49
+ sessionId: string;
50
+ provider?: AgentProvider;
51
+ }
52
+ /** Resolve the source once; missing/unsupported ACP leaves history readable without creating a DSH session. */
53
+ export declare function continuationAvailability(ctx: OpenContext, ref: Pick<SessionRef, 'provider'>): Promise<Availability>;
54
+ /** Open DSH's original identity or import native history through oneagentsAcpSessions; errors are never returned as success. */
55
+ export declare function openSessionInDsh(ctx: OpenContext, request: OpenRequest): Promise<OpenResult>;
56
+ export {};
@@ -0,0 +1,40 @@
1
+ import { resolveSession } from '../resolver.js';
2
+ import { convertSessionToDshEvents } from './adapter.js';
3
+ /** Resolve the source once; missing/unsupported ACP leaves history readable without creating a DSH session. */
4
+ export async function continuationAvailability(ctx, ref) {
5
+ if (ref.provider === 'dsh')
6
+ return { available: true, agent: 'dsh' };
7
+ const service = ctx.get?.('oneagentsAcpSessions');
8
+ if (!service)
9
+ return { available: false, reason: '续聊需要安装并启用 @1agents/dsh-acp;历史仍可只读查看' };
10
+ return service.availability(ref.provider);
11
+ }
12
+ /** Open DSH's original identity or import native history through oneagentsAcpSessions; errors are never returned as success. */
13
+ export async function openSessionInDsh(ctx, request) {
14
+ const resolved = await resolveSession(request.sessionId, request.provider);
15
+ if (!resolved)
16
+ throw new Error(`Session not found: ${request.sessionId}`);
17
+ const ref = resolved.ref;
18
+ if (ref.provider === 'dsh') {
19
+ if (!ctx.sessionController || !ctx.workspaceRegistry)
20
+ throw new Error('DSH session services are unavailable');
21
+ const id = ref.id.startsWith('session-') ? ref.id : `session-${ref.id}`;
22
+ const result = await ctx.sessionController.resolveAgent(id);
23
+ if ('error' in result)
24
+ throw result.error;
25
+ const cwd = result.agent.session.header.cwd;
26
+ if (!cwd)
27
+ throw new Error('DSH session has no workspace');
28
+ const workspace = await ctx.workspaceRegistry.create(cwd);
29
+ await workspace.attachSession(id);
30
+ return { success: true, dshSessionId: id, workspace: cwd, workspaceId: workspace.id, continuation: 'dsh' };
31
+ }
32
+ const available = await continuationAvailability(ctx, ref);
33
+ if (!available.available)
34
+ throw new Error(available.reason);
35
+ const normalized = await resolved.adapter.parse(resolved.candidate);
36
+ return ctx.get('oneagentsAcpSessions').importSession({
37
+ provider: normalized.ref.provider, nativeSessionId: normalized.ref.id,
38
+ cwd: normalized.ref.workspace ?? '', events: convertSessionToDshEvents(normalized),
39
+ });
40
+ }
@@ -1,14 +1,14 @@
1
- import fs from 'node:fs';
2
1
  import { resolveSession, searchSessions, buildOverview, summarizeTurns, turnDetail, eventDetails, } from '../index.js';
3
2
  import { handleSessionApi } from '../web/api.js';
4
- import { convertSessionToDshEvents } from './adapter.js';
3
+ import { openSessionInDsh, continuationAvailability } from './open.js';
5
4
  export const name = 'session-reader';
6
- export const inject = ['tools', 'webServer'];
5
+ // ACP is queried through ctx.get so history browsing remains available without it.
6
+ export const inject = ['tools', 'webServer', 'sessionController', 'workspaceRegistry'];
7
7
  export function apply(ctx) {
8
8
  // 1. Register tools if ctx.tools is present
9
9
  if (ctx.tools && typeof ctx.tools.register === 'function') {
10
10
  // session_search
11
- ctx.tools.register({
11
+ ctx.effect(() => ctx.tools.register({
12
12
  name: 'session_search',
13
13
  description: 'Search past AI coding sessions across Claude Code, Codex, Antigravity, Grok and DSH. By default searches sessions in the current working directory / workspace.',
14
14
  parameters: {
@@ -59,9 +59,9 @@ export function apply(ctx) {
59
59
  })),
60
60
  }));
61
61
  },
62
- });
62
+ }));
63
63
  // session_overview
64
- ctx.tools.register({
64
+ ctx.effect(() => ctx.tools.register({
65
65
  name: 'session_overview',
66
66
  description: 'Get statistics, files modified, commands executed, and end-state facts of a past session.',
67
67
  parameters: {
@@ -86,9 +86,9 @@ export function apply(ctx) {
86
86
  stats: overview.stats,
87
87
  };
88
88
  },
89
- });
89
+ }));
90
90
  // session_turns
91
- ctx.tools.register({
91
+ ctx.effect(() => ctx.tools.register({
92
92
  name: 'session_turns',
93
93
  description: 'Get turn-by-turn summary of what was discussed and done in a session.',
94
94
  parameters: {
@@ -105,9 +105,9 @@ export function apply(ctx) {
105
105
  const normalized = await resolved.adapter.parse(resolved.candidate);
106
106
  return summarizeTurns(normalized);
107
107
  },
108
- });
108
+ }));
109
109
  // session_turn_detail
110
- ctx.tools.register({
110
+ ctx.effect(() => ctx.tools.register({
111
111
  name: 'session_turn_detail',
112
112
  description: 'Inspect full untruncated events, tool calls, and results for a specific turn in a session.',
113
113
  parameters: {
@@ -129,114 +129,27 @@ export function apply(ctx) {
129
129
  }
130
130
  return turnDetail(normalized, args.turn);
131
131
  },
132
- });
132
+ }));
133
133
  // session_open_in_dsh
134
- ctx.tools.register({
134
+ ctx.effect(() => ctx.tools.register({
135
135
  name: 'session_open_in_dsh',
136
- description: 'Load a cross-agent historical session into DSH native sessions and automatically assign it to the DSH workspace.',
136
+ description: 'Open a historical session in DSH and continue with its original Agent. External sessions require the ACP plugin; unsupported sources remain read-only.',
137
137
  parameters: {
138
- sessionId: { type: 'string', required: true, description: 'Session ID to import into DSH' },
139
- currentCwd: { type: 'string', description: 'Optional current session working directory fallback' },
138
+ sessionId: { type: 'string', required: true, description: 'Session ID, prefix, or native transcript path' },
139
+ provider: { type: 'string', description: 'Source provider to disambiguate an ID: claude, codex, grok, dsh, or antigravity' },
140
140
  },
141
141
  output: {
142
142
  schema: { type: 'object' },
143
143
  render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
144
144
  },
145
- async execute(args, exec) {
146
- const resolved = await resolveSession(args.sessionId);
147
- if (!resolved)
148
- throw new Error(`Session not found: ${args.sessionId}`);
149
- const normalized = await resolved.adapter.parse(resolved.candidate);
150
- const dshEvents = convertSessionToDshEvents(normalized);
151
- const rawId = normalized.ref.id;
152
- const sanitizedId = rawId.replace(/[^a-zA-Z0-9_-]/g, '-');
153
- const dshSessionId = sanitizedId.startsWith('session-') ? sanitizedId : `session-${sanitizedId}`;
154
- const currentSessionCwd = exec?.agent?.session?.header?.cwd ?? args.currentCwd;
155
- let targetCwd;
156
- if (normalized.ref.workspace && typeof normalized.ref.workspace === 'string' && normalized.ref.workspace.startsWith('/')) {
157
- try {
158
- const st = await fs.promises.stat(normalized.ref.workspace);
159
- if (st.isDirectory()) {
160
- targetCwd = await fs.promises.realpath(normalized.ref.workspace);
161
- }
162
- }
163
- catch { }
164
- }
165
- if (!targetCwd) {
166
- const fallback = currentSessionCwd || process.cwd();
167
- try {
168
- const st = await fs.promises.stat(fallback);
169
- if (st.isDirectory()) {
170
- targetCwd = await fs.promises.realpath(fallback);
171
- }
172
- }
173
- catch { }
174
- }
175
- if (!targetCwd)
176
- targetCwd = process.cwd();
177
- const createdAt = normalized.ref.createdAt ? Date.parse(normalized.ref.createdAt) : Date.now();
178
- const header = {
179
- version: 2,
180
- id: dshSessionId,
181
- cwd: targetCwd,
182
- createdAt: isNaN(createdAt) ? Date.now() : createdAt,
183
- isSeeded: false,
184
- delegationDepth: 0,
185
- };
186
- const persistence = ctx.get ? ctx.get('sessionPersistence') : ctx.sessionPersistence;
187
- if (persistence && typeof persistence.create === 'function') {
188
- try {
189
- const existingStat = await persistence.stat(dshSessionId);
190
- if (!existingStat) {
191
- const handle = await persistence.create(header);
192
- await handle.append(dshEvents);
193
- await handle.flush();
194
- await handle.close();
195
- }
196
- }
197
- catch (e) {
198
- console.warn('[session-reader] persistence notice:', e?.message || e);
199
- }
200
- }
201
- const sessions = ctx.get ? ctx.get('sessions') : ctx.sessions;
202
- if (sessions && typeof sessions.create === 'function') {
203
- try {
204
- if (!sessions.get(dshSessionId)) {
205
- sessions.create(dshSessionId, { seed: dshEvents, meta: header });
206
- }
207
- }
208
- catch (e) {
209
- console.warn('[session-reader] sessions.create notice:', e?.message || e);
210
- }
211
- }
212
- const workspaceRegistry = ctx.get ? (ctx.get('workspaceRegistry') ?? ctx.workspaceRegistry) : ctx.workspaceRegistry;
213
- let attachedWorkspaceId;
214
- if (workspaceRegistry && typeof workspaceRegistry.create === 'function') {
215
- try {
216
- const ws = await workspaceRegistry.create(targetCwd);
217
- if (ws && typeof ws.attachSession === 'function') {
218
- await ws.attachSession(dshSessionId);
219
- attachedWorkspaceId = ws.id;
220
- }
221
- }
222
- catch (wsErr) {
223
- console.warn('[session-reader] workspace attach notice:', wsErr?.message || wsErr);
224
- }
225
- }
226
- return {
227
- success: true,
228
- dshSessionId,
229
- eventCount: dshEvents.length,
230
- title: normalized.ref.title,
231
- workspace: targetCwd,
232
- workspaceId: attachedWorkspaceId,
233
- };
145
+ async execute(args) {
146
+ return openSessionInDsh(ctx, { sessionId: args.sessionId, provider: args.provider });
234
147
  },
235
- });
148
+ }));
236
149
  }
237
150
  // 2. Register HTTP routes if ctx.webServer is present
238
151
  if (ctx.webServer && typeof ctx.webServer.register === 'function') {
239
- ctx.webServer.register({
152
+ ctx.effect(() => ctx.webServer.register({
240
153
  kind: 'prefix',
241
154
  path: '/api/session-reader',
242
155
  handler: async (req, res) => {
@@ -252,128 +165,37 @@ export function apply(ctx) {
252
165
  return;
253
166
  }
254
167
  try {
168
+ if (pathname === '/continuation') {
169
+ const provider = url.searchParams.get('provider');
170
+ if (!provider)
171
+ throw new Error('Missing provider');
172
+ res.end(JSON.stringify(await continuationAvailability(ctx, { provider })));
173
+ return;
174
+ }
255
175
  if (pathname === '/open-in-dsh') {
256
- let sessionId = url.searchParams.get('id');
257
- let currentCwd = url.searchParams.get('currentCwd') ?? undefined;
258
- if (!sessionId && (req.method === 'POST' || req.method === 'PUT')) {
259
- const body = await new Promise((resolve) => {
260
- let data = '';
261
- req.on('data', chunk => data += chunk);
262
- req.on('end', () => {
263
- try {
264
- resolve(JSON.parse(data));
265
- }
266
- catch {
267
- resolve({});
268
- }
269
- });
270
- req.on('error', () => resolve({}));
271
- });
272
- sessionId = body?.sessionId || body?.id;
273
- currentCwd = body?.currentCwd || currentCwd;
274
- }
275
- if (!sessionId) {
276
- res.statusCode = 400;
277
- res.end(JSON.stringify({ error: 'Missing sessionId' }));
176
+ if (req.method !== 'POST') {
177
+ res.statusCode = 405;
178
+ res.end(JSON.stringify({ error: 'Use POST to open a session' }));
278
179
  return;
279
180
  }
280
- const resolved = await resolveSession(sessionId);
281
- if (!resolved) {
282
- res.statusCode = 404;
283
- res.end(JSON.stringify({ error: `Session not found: ${sessionId}` }));
181
+ if (req.headers.origin && new URL(req.headers.origin).host !== req.headers.host) {
182
+ res.statusCode = 403;
183
+ res.end(JSON.stringify({ error: 'Cross-origin session imports are not allowed' }));
284
184
  return;
285
185
  }
286
- const normalized = await resolved.adapter.parse(resolved.candidate);
287
- const dshEvents = convertSessionToDshEvents(normalized);
288
- const rawId = normalized.ref.id;
289
- const sanitizedId = rawId.replace(/[^a-zA-Z0-9_-]/g, '-');
290
- const dshSessionId = sanitizedId.startsWith('session-') ? sanitizedId : `session-${sanitizedId}`;
291
- let targetCwd;
292
- if (normalized.ref.workspace && typeof normalized.ref.workspace === 'string' && normalized.ref.workspace.startsWith('/')) {
293
- try {
294
- const st = await fs.promises.stat(normalized.ref.workspace);
295
- if (st.isDirectory()) {
296
- targetCwd = await fs.promises.realpath(normalized.ref.workspace);
297
- }
298
- }
299
- catch { }
300
- }
301
- if (!targetCwd) {
302
- const fallback = currentCwd || process.cwd();
303
- try {
304
- const st = await fs.promises.stat(fallback);
305
- if (st.isDirectory()) {
306
- targetCwd = await fs.promises.realpath(fallback);
307
- }
308
- }
309
- catch { }
186
+ let body = '';
187
+ for await (const chunk of req) {
188
+ body += String(chunk);
189
+ if (body.length > 16384)
190
+ throw new Error('Request body too large');
310
191
  }
311
- if (!targetCwd)
312
- targetCwd = process.cwd();
313
- const createdAt = normalized.ref.createdAt ? Date.parse(normalized.ref.createdAt) : Date.now();
314
- const header = {
315
- version: 2,
316
- id: dshSessionId,
317
- cwd: targetCwd,
318
- createdAt: isNaN(createdAt) ? Date.now() : createdAt,
319
- isSeeded: false,
320
- delegationDepth: 0,
321
- };
322
- // 1. Persistence if available
323
- const persistence = ctx.get ? ctx.get('sessionPersistence') : ctx.sessionPersistence;
324
- if (persistence && typeof persistence.create === 'function') {
325
- try {
326
- const existingStat = await persistence.stat(dshSessionId);
327
- if (!existingStat) {
328
- const handle = await persistence.create(header);
329
- await handle.append(dshEvents);
330
- await handle.flush();
331
- await handle.close();
332
- }
333
- }
334
- catch (persistErr) {
335
- console.warn('[session-reader] persistence notice:', persistErr?.message || persistErr);
336
- }
337
- }
338
- // 2. In-memory sessions if available
339
- const sessions = ctx.get ? ctx.get('sessions') : ctx.sessions;
340
- if (sessions && typeof sessions.create === 'function') {
341
- try {
342
- if (!sessions.get(dshSessionId)) {
343
- sessions.create(dshSessionId, {
344
- seed: dshEvents,
345
- meta: header,
346
- });
347
- }
348
- }
349
- catch (sessionErr) {
350
- console.warn('[session-reader] sessions.create notice:', sessionErr?.message || sessionErr);
351
- }
352
- }
353
- // 3. Workspace attachment if workspaceRegistry is available
354
- const workspaceRegistry = ctx.get ? (ctx.get('workspaceRegistry') ?? ctx.workspaceRegistry) : ctx.workspaceRegistry;
355
- let attachedWorkspaceId;
356
- if (workspaceRegistry && typeof workspaceRegistry.create === 'function') {
357
- try {
358
- const ws = await workspaceRegistry.create(targetCwd);
359
- if (ws && typeof ws.attachSession === 'function') {
360
- await ws.attachSession(dshSessionId);
361
- attachedWorkspaceId = ws.id;
362
- }
363
- }
364
- catch (wsErr) {
365
- console.warn('[session-reader] workspace attach notice:', wsErr?.message || wsErr);
366
- }
192
+ const value = JSON.parse(body);
193
+ if (typeof value.sessionId !== 'string' || !value.sessionId || (value.provider !== undefined && typeof value.provider !== 'string')) {
194
+ res.statusCode = 400;
195
+ res.end(JSON.stringify({ error: 'sessionId and optional provider must be strings' }));
196
+ return;
367
197
  }
368
- res.statusCode = 200;
369
- res.end(JSON.stringify({
370
- success: true,
371
- dshSessionId,
372
- eventCount: dshEvents.length,
373
- title: normalized.ref.title,
374
- workspace: targetCwd,
375
- workspaceId: attachedWorkspaceId,
376
- }));
198
+ res.end(JSON.stringify(await openSessionInDsh(ctx, { sessionId: value.sessionId, provider: value.provider })));
377
199
  return;
378
200
  }
379
201
  const handled = await handleSessionApi(pathname, url, res, { fallbackCwd: process.cwd() });
@@ -387,6 +209,6 @@ export function apply(ctx) {
387
209
  res.end(JSON.stringify({ error: err?.message ?? String(err) }));
388
210
  }
389
211
  },
390
- });
212
+ }));
391
213
  }
392
214
  }
@@ -41,7 +41,7 @@ export declare function listRecentSessions(options?: ListOptions): Promise<Sessi
41
41
  export declare function findSessionsByWorkspace(workspacePath: string, options?: Omit<ListOptions, 'workspace'>): Promise<SessionRef[]>;
42
42
  export declare function findResolvedByWorkspace(workspacePath: string, options?: Omit<ListOptions, 'workspace'>): Promise<ResolvedSession[]>;
43
43
  /** Locates a session by full id, id prefix, or native file path. */
44
- export declare function resolveSession(sessionId: string): Promise<ResolvedSession | undefined>;
44
+ export declare function resolveSession(sessionId: string, provider?: AgentProvider): Promise<ResolvedSession | undefined>;
45
45
  export declare function parseSession(sessionId: string): Promise<NormalizedSession>;
46
46
  export interface LoadOptions {
47
47
  /** Set false to bypass the index entirely (CI, read-only checkouts). */
@@ -140,11 +140,13 @@ export async function findResolvedByWorkspace(workspacePath, options = {}) {
140
140
  return listResolvedSessions({ ...options, limit: options.limit ?? 50, workspace: workspacePath });
141
141
  }
142
142
  /** Locates a session by full id, id prefix, or native file path. */
143
- export async function resolveSession(sessionId) {
143
+ export async function resolveSession(sessionId, provider) {
144
144
  const asPath = sessionId.includes('/') ? canonicalizePath(sessionId) : undefined;
145
145
  const needle = sessionId.toLowerCase();
146
146
  let prefixHit;
147
147
  for (const adapter of adapters) {
148
+ if (provider && adapter.provider !== provider)
149
+ continue;
148
150
  for (const candidate of await adapter.listCandidates()) {
149
151
  if (asPath ? candidate.path === asPath : candidate.id.toLowerCase() === needle) {
150
152
  return { ref: await adapter.scanRef(candidate), adapter, candidate };
@@ -18049,54 +18049,89 @@ Please report this to https://github.com/markedjs/marked.`, e3) {
18049
18049
  };
18050
18050
  }
18051
18051
 
18052
+ // src/dsh/copy.ts
18053
+ var continuationDictionaries = {
18054
+ zh: {
18055
+ continue: "\u5728 DSH \u4E2D\u7EE7\u7EED\u539F\u4F1A\u8BDD",
18056
+ openDsh: "\u5728 DSH \u4E2D\u6253\u5F00",
18057
+ readOnly: "\u53EA\u8BFB\u67E5\u770B",
18058
+ loading: "\u6B63\u5728\u6062\u590D\u539F\u4F1A\u8BDD\u2026",
18059
+ failed: "\u7EED\u804A\u5931\u8D25\uFF1A",
18060
+ unavailable: "\u6682\u65F6\u65E0\u6CD5\u68C0\u67E5\u7EED\u804A\u72B6\u6001",
18061
+ navigationUnavailable: "DSH \u4F1A\u8BDD\u5BFC\u822A\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5386\u53F2\u9762\u677F"
18062
+ },
18063
+ en: {
18064
+ continue: "Continue original session in DSH",
18065
+ openDsh: "Open in DSH",
18066
+ readOnly: "Read only",
18067
+ loading: "Restoring session\u2026",
18068
+ failed: "Could not continue: ",
18069
+ unavailable: "Continuation status is unavailable",
18070
+ navigationUnavailable: "DSH navigation is unavailable; reopen the history panel"
18071
+ }
18072
+ };
18073
+ function continuationCopy(translate) {
18074
+ if (translate) return Object.fromEntries(Object.keys(continuationDictionaries.zh).map((key) => [key, translate(key)]));
18075
+ const locale = typeof document === "undefined" ? "zh" : document.documentElement.lang || navigator.language;
18076
+ return locale.startsWith("zh") ? continuationDictionaries.zh : continuationDictionaries.en;
18077
+ }
18078
+
18052
18079
  // src/dsh/client.ts
18053
18080
  var CSS_TEXT = `
18081
+ /* Match DSH SidebarRoot panel rows in expanded and collapsed layouts. */
18054
18082
  [data-dsh-session-reader-entry] {
18055
18083
  box-sizing: border-box;
18056
- width: 100%;
18057
- height: 36px;
18058
- color: var(--dsw-alias-label-secondary, #666);
18084
+ flex: none;
18085
+ min-height: 36px;
18086
+ margin: 0 2px 4px;
18087
+ color: var(--dsw-alias-label-primary);
18059
18088
  cursor: pointer;
18060
18089
  white-space: nowrap;
18061
18090
  background: transparent;
18062
18091
  border: none;
18063
- border-radius: 8px;
18092
+ border-radius: var(--dsw-radius-md);
18064
18093
  align-items: center;
18065
18094
  gap: 8px;
18066
- padding: 0 10px;
18067
- font-size: 13px;
18095
+ padding: 7px 8px;
18096
+ font: inherit;
18097
+ line-height: 22px;
18098
+ text-align: left;
18068
18099
  display: flex;
18069
- transition: background 0.15s ease, color 0.15s ease;
18070
18100
  }
18071
18101
  [data-dsh-session-reader-entry]:hover {
18072
- background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.05));
18073
- color: var(--dsw-alias-label-primary, #111);
18102
+ background: var(--dsw-alias-interactive-bg-hover);
18074
18103
  }
18075
- body[data-ds-dark-theme] [data-dsh-session-reader-entry]:hover {
18076
- background: rgba(255,255,255,0.08);
18077
- color: #fff;
18104
+ [data-dsh-session-reader-entry]:focus-visible {
18105
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
18106
+ outline-offset: -2px;
18078
18107
  }
18079
18108
  [data-dsh-session-reader-entry] .sr-icon {
18080
18109
  flex: none;
18081
18110
  display: inline-flex;
18082
18111
  align-items: center;
18083
18112
  justify-content: center;
18084
- width: 24px;
18085
- height: 24px;
18113
+ }
18114
+ [data-dsh-session-reader-entry] .sr-icon svg {
18115
+ width: 16px;
18116
+ height: 16px;
18086
18117
  }
18087
18118
  [data-dsh-session-reader-entry] .sr-label {
18119
+ min-width: 0;
18088
18120
  text-overflow: ellipsis;
18089
18121
  overflow: hidden;
18090
18122
  }
18091
18123
  [data-dsh-frame][data-sidebar-collapsed] [data-dsh-session-reader-entry],
18092
18124
  [data-sidebar-collapsed] [data-dsh-session-reader-entry] {
18093
- border-radius: 50%;
18094
18125
  justify-content: center;
18095
18126
  width: 36px;
18096
18127
  height: 36px;
18097
- margin: 0 auto 12px;
18128
+ margin: 0 0 12px;
18098
18129
  padding: 0;
18099
18130
  }
18131
+ [data-sidebar-collapsed] [data-dsh-session-reader-entry] .sr-icon svg {
18132
+ width: 18px;
18133
+ height: 18px;
18134
+ }
18100
18135
  [data-dsh-frame][data-sidebar-collapsed] [data-dsh-session-reader-entry] .sr-label,
18101
18136
  [data-sidebar-collapsed] [data-dsh-session-reader-entry] .sr-label {
18102
18137
  display: none;
@@ -18475,6 +18510,9 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18475
18510
  }
18476
18511
  function apply(_ctx) {
18477
18512
  if (typeof document === "undefined") return;
18513
+ if (_ctx.locale) _ctx.effect(() => _ctx.locale.register("sessionReaderContinuation", continuationDictionaries));
18514
+ const translate = _ctx.locale?.bind("sessionReaderContinuation");
18515
+ const getContinuationCopy = () => continuationCopy(translate);
18478
18516
  if (!document.getElementById("dsh-session-reader-css")) {
18479
18517
  const style = document.createElement("style");
18480
18518
  style.id = "dsh-session-reader-css";
@@ -18554,48 +18592,35 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18554
18592
  cachedSessions = [];
18555
18593
  currentTab = "chat";
18556
18594
  }
18557
- async function openInDshChat(sessionId, btn) {
18595
+ async function openInDshChat(sessionId, btn, provider) {
18596
+ const copy = getContinuationCopy();
18597
+ const label = btn?.textContent;
18558
18598
  if (btn) {
18559
- btn.innerHTML = "\u23F3 \u6B63\u5728\u52A0\u8F7D\u5230 DSH...";
18599
+ btn.textContent = copy.loading;
18560
18600
  btn.disabled = true;
18561
18601
  }
18562
18602
  try {
18563
- const activeWs = getActiveWorkspaceInfo();
18564
18603
  const res = await fetch("/api/session-reader/open-in-dsh", {
18565
18604
  method: "POST",
18566
18605
  headers: { "Content-Type": "application/json" },
18567
- body: JSON.stringify({
18568
- sessionId,
18569
- currentCwd: activeWs.cwd,
18570
- workspaceId: activeWs.workspaceId
18571
- })
18606
+ body: JSON.stringify({ sessionId, provider })
18572
18607
  });
18573
18608
  const result = await res.json();
18574
- if (!res.ok || !result.success) {
18575
- alert("\u63A5\u5165 DSH \u5931\u8D25: " + (result.error || res.statusText));
18576
- if (btn) {
18577
- btn.innerHTML = "\u{1F680} \u5728 DSH \u5B98\u65B9 Chat \u4E2D\u6253\u5F00";
18578
- btn.disabled = false;
18579
- }
18580
- return;
18581
- }
18582
- const dshSessionId = result.dshSessionId;
18583
- closePanel();
18609
+ if (!res.ok || !result.success) throw new Error(result.error || res.statusText);
18584
18610
  const sessions = dshCtx?.get ? dshCtx.get("sessions") ?? dshCtx.sessions : dshCtx?.sessions;
18585
- if (sessions) {
18586
- if (typeof sessions.refresh === "function") {
18587
- await sessions.refresh();
18588
- }
18589
- if (typeof sessions.open === "function") {
18590
- sessions.open(dshSessionId);
18591
- }
18592
- } else {
18593
- console.warn("[session-reader] Could not obtain DSH sessions service from ctx");
18594
- }
18611
+ const navigation = dshCtx?.get ? dshCtx.get("uiWorkspace") : dshCtx?.uiWorkspace;
18612
+ if (!navigation) throw new Error(copy.navigationUnavailable);
18613
+ await sessions.refresh();
18614
+ navigation.openSession(result.dshSessionId);
18615
+ closePanel();
18595
18616
  } catch (err) {
18596
- alert("\u8BF7\u6C42\u51FA\u9519: " + (err?.message || err));
18617
+ if (activeSessionData) {
18618
+ activeSessionData.continuationError = copy.failed + (err?.message || err);
18619
+ renderModalBody();
18620
+ }
18621
+ } finally {
18597
18622
  if (btn) {
18598
- btn.innerHTML = "\u{1F680} \u5728 DSH \u5B98\u65B9 Chat \u4E2D\u6253\u5F00";
18623
+ btn.textContent = label ?? copy.continue;
18599
18624
  btn.disabled = false;
18600
18625
  }
18601
18626
  }
@@ -18626,6 +18651,19 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18626
18651
  const data = await res.json();
18627
18652
  if (detailAbort !== ac) return;
18628
18653
  activeSessionData = data;
18654
+ renderModalBody();
18655
+ if (!isStandalone()) {
18656
+ try {
18657
+ const status = await fetch(`/api/session-reader/continuation?provider=${encodeURIComponent(data.ref.provider)}`, { signal: ac.signal, cache: "no-store" });
18658
+ if (!status.ok) throw new Error(`HTTP ${status.status}`);
18659
+ const continuation = await status.json();
18660
+ if (detailAbort !== ac) return;
18661
+ activeSessionData.continuation = continuation;
18662
+ } catch (error) {
18663
+ if (detailAbort !== ac) return;
18664
+ activeSessionData.continuation = { available: false, reason: String(error) };
18665
+ }
18666
+ }
18629
18667
  } catch (err) {
18630
18668
  if (err?.name === "AbortError") {
18631
18669
  if (detailAbort === ac) {
@@ -18717,9 +18755,6 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18717
18755
  item.classList.add("active");
18718
18756
  loadSessionDetail(s3.id);
18719
18757
  });
18720
- item.addEventListener("dblclick", () => {
18721
- openInDshChat(s3.id);
18722
- });
18723
18758
  listEl.appendChild(item);
18724
18759
  });
18725
18760
  }
@@ -18738,7 +18773,8 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18738
18773
  contentEl.innerHTML = `<div style="padding:60px;text-align:center;color:#e11d48;font-size:13px;">\u52A0\u8F7D\u5931\u8D25: ${activeSessionData.error}</div>`;
18739
18774
  return;
18740
18775
  }
18741
- const { ref, overview, turns, files } = activeSessionData;
18776
+ const { ref, overview, turns, files, continuation, continuationError } = activeSessionData;
18777
+ const copy = getContinuationCopy();
18742
18778
  const standalone = isStandalone();
18743
18779
  const body = currentTab === "chat" ? '<div id="sr-chat-root" style="width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;"></div>' : currentTab === "files" ? renderFilesView(files) : renderOverviewView(overview);
18744
18780
  contentEl.innerHTML = `
@@ -18748,8 +18784,8 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18748
18784
  ${escapeHtml2(ref.title || "Untitled")}
18749
18785
  </div>
18750
18786
  <div class="sr-header-actions">
18751
- ${standalone ? "" : `<button class="sr-open-dsh-btn" id="sr-btn-open-dsh" title="\u5C06\u6B64\u5386\u53F2\u4F1A\u8BDD\u63A5\u5165\u5E76\u5728 DSH \u5B98\u65B9\u4E3B\u804A\u5929\u7A97\u53E3\u4E2D\u67E5\u770B">
18752
- \u{1F680} \u5728 DSH \u5B98\u65B9 Chat \u4E2D\u6253\u5F00
18787
+ ${standalone ? "" : `<button class="sr-open-dsh-btn" id="sr-btn-open-dsh" ${continuation?.available ? "" : "disabled"}>
18788
+ ${escapeHtml2(continuation?.available ? (ref.provider === "dsh" ? copy.openDsh : copy.continue) + " \xB7 " + (continuation.agent || ref.provider) : copy.readOnly)}
18753
18789
  </button>`}
18754
18790
  <div class="sr-tabs">
18755
18791
  <button class="sr-tab-btn ${currentTab === "chat" ? "active" : ""}" data-tab="chat">\u{1F4AC} \u5FEB\u901F\u9884\u89C8</button>
@@ -18758,6 +18794,7 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18758
18794
  </div>
18759
18795
  </div>
18760
18796
  </div>
18797
+ ${!standalone && (continuationError || !continuation?.available) ? `<div class="sr-header-path" role="status">${escapeHtml2(continuationError || continuation?.reason || copy.unavailable)}</div>` : ""}
18761
18798
  <div class="sr-header-path" title="${escapeHtml2(ref.path || "")}">
18762
18799
  <span class="sr-header-provider">${ref.provider}</span> \xB7 \u8DEF\u5F84: ${escapeHtml2(ref.path || "")}
18763
18800
  </div>
@@ -18769,7 +18806,7 @@ body[data-ds-dark-theme] .sr-tab-btn.active {
18769
18806
  const openDshBtn = contentEl.querySelector("#sr-btn-open-dsh");
18770
18807
  if (openDshBtn) {
18771
18808
  openDshBtn.addEventListener("click", () => {
18772
- openInDshChat(activeSessionId, openDshBtn);
18809
+ openInDshChat(ref.path || activeSessionId, openDshBtn, ref.provider);
18773
18810
  });
18774
18811
  }
18775
18812
  contentEl.querySelectorAll(".sr-tab-btn").forEach((btn) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agents/session-reader",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Read Plane: cross-agent session discovery, turn inspection, workspace aggregation and distillation from raw local session files.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -45,7 +45,11 @@
45
45
  "patch": "./cordis.patch.yml"
46
46
  },
47
47
  "client": {
48
- "platform": "web"
48
+ "platform": "web",
49
+ "inject": [
50
+ "@deepseek-ai/dsh-client-ui-workspace",
51
+ "@deepseek-ai/dsh-client-locale"
52
+ ]
49
53
  }
50
54
  },
51
55
  "bin": {
@@ -64,7 +68,7 @@
64
68
  "scripts": {
65
69
  "build": "tsc -p tsconfig.build.json && node scripts/bundle-client.js",
66
70
  "typecheck": "tsc -p tsconfig.json --noEmit",
67
- "test": "node --import tsx --test test/session-reader.test.ts test/dsh-adapter.test.ts test/web.test.ts",
71
+ "test": "node --import tsx --test test/session-reader.test.ts test/dsh-adapter.test.ts test/dsh-open.test.ts test/web.test.ts",
68
72
  "prepack": "npm run build",
69
73
  "1session": "tsx bin/1session.ts"
70
74
  },