@sanqianx/claude-ai-workbench 0.12.0 → 0.13.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
@@ -4,6 +4,16 @@
4
4
 
5
5
  0.3.0 起,仓库同时提供 **Agent Terminal**:一个通用多 Agent 对话终端(Claude Code / Codex / OpenCode / ZCode),带多项目工作区、自定义模型供应商管理、Agent 发现与能力矩阵、推理力度与上下文用量显示。
6
6
 
7
+ ## 作为智脑产品的嵌入模块
8
+
9
+ 外壳使用真实 Agent Terminal 页面和 REST/SSE,不重新实现消息流。`embed=1` 与可配置 `apiBase` 隐藏重复品牌/项目管理,并接受同源父窗口的项目、主题与知识范围消息;独立产品行为保持不变。
10
+
11
+ 受信任本地外壳通过 `KB_WORKBENCH_RUNTIME_TOKEN`、`KB_EVENT_BRIDGE_URL` 和引擎地址连接后台服务。原生工作台捕获原始用户输入及成功最终回答,通过持久化 outbox 投递到 bridge 的鉴权入口;不捕获系统提示词、注入的知识、工具输出或供应商凭据。`/api/health` 中 `conversationCapture` 展示启用状态、待投递数和错误;已有用户显式关闭的捕获设置不会被覆盖。桥接不可用时保留待投递记录,恢复或重启后继续,不伪造会话。
12
+
13
+ 工作台是产品供应商与凭据的活动配置源。后台维护默认模型持久化,不随会话临时模型切换;解析后的凭据只交给鉴权本地服务,不进入浏览器消息。SDK 会话从当前项目登记的工作区恢复,也兼容旧登记文件。
14
+
15
+ Windows 上 Claude Code 探测与实际启动使用同一解析结果:失效的 `CLAUDE_CODE_EXECPATH` 不遮住有效安装,npm/CMD/PowerShell 启动脚本解析成实际文件,JavaScript 入口用当前 Node 探测版本。`/system/doctor` 返回实际启动路径、被跳过的失效路径及版本探测错误。发现和会话失败使用 `log/v2`;受外壳守护时写入外壳统一诊断,独立运行保存在工作台数据目录的 `diagnostics.jsonl`,不记录凭据或原始提示词。
16
+
7
17
  ## 仓库结构
8
18
 
9
19
  - `packages/`:可发布的 SDK 源码。
@@ -565,3 +565,37 @@ body.light .msg.ai.error { color:#b42318; border-color:rgba(180,35,24,.25); back
565
565
 
566
566
  /* ===== embed 模式(被知识库壳嵌入):折叠左侧项目栏,由壳 postMessage 控制 ===== */
567
567
  body.embed-collapse .sidebar { display: none; }
568
+
569
+ /* Embedded product tokens are owned by the shell; standalone styling stays. */
570
+ body.kb-embedded.light {
571
+ --page:#ffffff; --side:#f8f9fb; --surface:#f3f5f7; --surface2:#edf1f4;
572
+ --line:#dce2e8; --line2:#dce2e8; --pop:#ffffff;
573
+ --text:#202733; --muted:#687587; --faint:#8e99a7;
574
+ --good:#397761; --bad:#a05252; --blue:#516f89; --blue-soft:#e6edf3;
575
+ --code-bg:#f3f5f7; --code-line:#dce2e8;
576
+ }
577
+ body.kb-embedded:not(.light) {
578
+ --page:#20252d; --side:#1b2028; --surface:#252b34; --surface2:#2c333e;
579
+ --line:#39424f; --line2:#39424f; --pop:#252b34;
580
+ --text:#e6ebf0; --muted:#a0aaba; --faint:#788393; --blue:#8aa8cf; --blue-soft:#293748;
581
+ --good:#81b29a; --bad:#d88b8b; --code-bg:#252b34; --code-line:#39424f;
582
+ }
583
+ body.kb-embedded { --accent:var(--blue); --accent-soft:var(--blue-soft); }
584
+ body.kb-embedded .agent-badge { background:var(--blue); }
585
+ body.kb-embedded .sidebar { display:flex; width:208px; min-width:208px; border-right:1px solid var(--line); }
586
+ body.kb-embedded .brand, body.kb-embedded .group-label, body.kb-embedded .add-proj,
587
+ body.kb-embedded .proj-item .row-acts, body.kb-embedded .sidebar-foot, body.kb-embedded #btn-top-settings { display:none; }
588
+ body.kb-embedded .new-session { margin:16px 16px 8px; }
589
+ body.kb-embedded .hint-spark, body.kb-embedded .hint-art { display:none; }
590
+ body.kb-embedded .empty-hint .big { display:none; }
591
+ .embedded-hint { display:none; }
592
+ body.kb-embedded .standalone-hint { display:none; }
593
+ body.kb-embedded .embedded-hint { display:block; }
594
+ body.kb-embedded .cmdrow { min-width:0; }
595
+ body.kb-embedded .cmdrow > .bar-btn, body.kb-embedded .send { flex-shrink:0; }
596
+ body.kb-embedded .cmdrow > .anchor { flex:0 0 auto; }
597
+ body.kb-embedded .cmdrow > .model-anchor { flex:0 1 auto; min-width:0; }
598
+ body.kb-embedded #btn-model { min-width:0; max-width:100%; }
599
+ body.kb-embedded #model-label { display:block; min-width:0; max-width:100%; }
600
+ body.kb-embedded #btn-ctx .ctx-mini { display:none; }
601
+ body.kb-embedded .ctx-mini i { background:var(--blue); }
@@ -1,8 +1,52 @@
1
1
  /* Agent Terminal — full-page UI over the versioned REST/SSE API. */
2
2
  'use strict';
3
3
 
4
+ const embedParams = new URLSearchParams(location.search);
5
+ const embedded = embedParams.get('embed') === '1';
6
+ const configuredApiBase = embedParams.get('apiBase');
7
+ const apiBase = configuredApiBase && /^\/api\/[a-z0-9/-]+$/i.test(configuredApiBase) ? configuredApiBase.replace(/\/$/, '') : '/api/claude-workbench/v1';
8
+ let selectionEpoch = 0;
9
+ let sessionEpoch = 0;
10
+ let sessionCreationEpoch = 0;
11
+ const projectDrafts = new Map();
12
+ function savedProjectDraft(projectId, text) {
13
+ try {
14
+ const key = `kb.workbench.drafts:${apiBase}`;
15
+ const drafts = JSON.parse(localStorage.getItem(key) || '{}');
16
+ if (text !== undefined) {
17
+ delete drafts[projectId];
18
+ if (text) drafts[projectId] = String(text).slice(0, 100000);
19
+ localStorage.setItem(key, JSON.stringify(Object.fromEntries(Object.entries(drafts).slice(-20))));
20
+ }
21
+ return typeof drafts[projectId] === 'string' ? drafts[projectId] : '';
22
+ } catch { return ''; }
23
+ }
24
+ function rememberProjectDraft(projectId, draft) {
25
+ if (!projectId) return;
26
+ projectDrafts.set(projectId, draft);
27
+ savedProjectDraft(projectId, draft.text || '');
28
+ }
29
+ function savedProjectSession(projectId, sessionId) {
30
+ try {
31
+ const key = `kb.workbench.sessions:${apiBase}`;
32
+ const saved = JSON.parse(localStorage.getItem(key) || '{}');
33
+ if (sessionId !== undefined) {
34
+ if (sessionId) saved[projectId] = sessionId; else delete saved[projectId];
35
+ localStorage.setItem(key, JSON.stringify(saved));
36
+ }
37
+ return typeof saved[projectId] === 'string' ? saved[projectId] : null;
38
+ } catch { return null; }
39
+ }
40
+ function forgetSessionSelection(sessionId) {
41
+ for (const project of state.projects) {
42
+ if (savedProjectSession(project.id) === sessionId) savedProjectSession(project.id, null);
43
+ const draft = projectDrafts.get(project.id);
44
+ if (draft?.sessionId === sessionId) projectDrafts.set(project.id, { ...draft, sessionId: null });
45
+ }
46
+ }
47
+
4
48
  const client = window.createClaudeWorkbenchClient
5
- ? window.createClaudeWorkbenchClient({ apiBase: '/api/claude-workbench/v1' })
49
+ ? window.createClaudeWorkbenchClient({ apiBase })
6
50
  : null;
7
51
 
8
52
  const PERMISSION_LABELS = { default: '默认防护', acceptEdits: '接受编辑', plan: '计划模式', bypassPermissions: '完全访问' };
@@ -76,7 +120,7 @@ function applyPreferences(values) {
76
120
 
77
121
  async function loadPreferences() {
78
122
  try {
79
- const response = await fetch('/api/claude-workbench/v1/ui-preferences');
123
+ const response = await fetch(`${apiBase}/ui-preferences`);
80
124
  if (!response.ok) return;
81
125
  const body = await response.json();
82
126
  savedPreferences = { ...(body && body.values || {}) };
@@ -91,7 +135,7 @@ function savePreferences(partial) {
91
135
  clearTimeout(prefsSaveTimer);
92
136
  prefsSaveTimer = setTimeout(() => {
93
137
  if (typeof fetch !== 'function') return; // non-browser harness (unit tests)
94
- fetch('/api/claude-workbench/v1/ui-preferences', {
138
+ fetch(`${apiBase}/ui-preferences`, {
95
139
  method: 'PUT',
96
140
  headers: { 'Content-Type': 'application/json' },
97
141
  body: JSON.stringify(partial),
@@ -110,14 +154,13 @@ async function init() {
110
154
  await Promise.all([refreshAgents(), refreshProfiles()]);
111
155
  await loadPreferences();
112
156
  await refreshProjects();
113
- // 首个 kb:ready 在项目清单就绪前就已发出(上方 health/agents/profiles 都要
114
- // 先跑完),壳届时下发的 kb:select-project 会扑空。清单就绪后再广播一次,
115
- // 让壳重新选中项目,避免嵌入态卡在"未登记/无项目可选"。
116
- if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'agent-terminal' }, '*');
117
157
  wireChrome();
118
158
  wireComposer();
119
159
  wireSettings();
120
160
  wireProjectModal();
161
+ // Ready is the usable-page boundary: the host may immediately select a
162
+ // project or open settings, so data and handlers must both be available.
163
+ if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'agent-terminal' }, '*');
121
164
  }
122
165
 
123
166
  /* ===================== agents & capabilities ===================== */
@@ -404,6 +447,7 @@ async function renderProjectListOnce() {
404
447
  return;
405
448
  }
406
449
  for (const project of state.projects) {
450
+ if (embedded && project.id !== state.activeProjectId) continue;
407
451
  const proj = el('div', 'proj' + (state.activeProjectId === project.id ? ' active' : ''));
408
452
  proj.dataset.project = project.id;
409
453
  const head = el('button', 'proj-item');
@@ -482,6 +526,7 @@ function sessionRow(summary, projectId) {
482
526
  ev.stopPropagation();
483
527
  try {
484
528
  await client.archiveSession(summary.sessionId);
529
+ forgetSessionSelection(summary.sessionId);
485
530
  if (state.activeSessionId === summary.sessionId) detachSession();
486
531
  await renderProjectList();
487
532
  } catch (error) { toast(error.message, true); }
@@ -499,6 +544,7 @@ async function renderArchived() {
499
544
  list.textContent = '';
500
545
  const rows = [];
501
546
  for (const project of state.projects) {
547
+ if (embedded && project.id !== state.activeProjectId) continue;
502
548
  let archived = [];
503
549
  try { archived = await client.listSessions(project.id, { archived: true }); } catch { archived = []; }
504
550
  for (const summary of archived) {
@@ -547,11 +593,13 @@ function shortWhen(iso) {
547
593
 
548
594
  async function newSession() {
549
595
  if (!state.activeProjectId) { openProjectModal(); return; }
596
+ const projectId = state.activeProjectId, epoch = selectionEpoch, creation = ++sessionCreationEpoch;
550
597
  try {
551
598
  const summary = await client.startSession(
552
599
  { contextId: state.activeProjectId, workspaceRef: state.activeProjectId, aiProfileId: state.currentProfileId, displayName: (state.projects.find(p => p.id === state.activeProjectId) || {}).name },
553
600
  { agentId: state.currentAgent, model: state.currentProfileId === undefined ? undefined : state.currentModel || undefined, permissionMode: state.permissionMode, effort: state.caps && state.caps.generationParams.includes('effort') ? state.effort : undefined },
554
601
  );
602
+ if (epoch !== selectionEpoch || state.activeProjectId !== projectId || creation !== sessionCreationEpoch) { const draft = projectDrafts.get(projectId) || {}; if (!draft.sessionId) projectDrafts.set(projectId, { ...draft, sessionId: summary.sessionId }); return; }
555
603
  if (state.activeSessionId === summary.sessionId) return;
556
604
  state.sessionTitles.set(summary.sessionId, '新会话');
557
605
  await openSession(summary.sessionId, { preserveAttachments: true });
@@ -561,14 +609,18 @@ async function newSession() {
561
609
  }
562
610
 
563
611
  async function openSession(sessionId, options = {}) {
612
+ sessionCreationEpoch++;
564
613
  detachSession(options);
614
+ const epoch = sessionEpoch;
565
615
  state.activeSessionId = sessionId;
566
616
  resetStream();
567
617
  let snapshot;
568
- try { snapshot = await client.loadSession(sessionId); } catch (error) { state.activeSessionId = null; toast(error.message, true); return; }
618
+ try { snapshot = await client.loadSession(sessionId); } catch (error) { if (epoch !== sessionEpoch) return; state.activeSessionId = null; if (error.status === 404) forgetSessionSelection(sessionId); toast(error.message, true); return; }
619
+ if (epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
569
620
  const summary = snapshot.session;
570
621
  state.sessionSummary = summary;
571
622
  state.activeProjectId = summary.contextId || state.activeProjectId;
623
+ if (state.activeProjectId) savedProjectSession(state.activeProjectId, sessionId);
572
624
  state.currentAgent = summary.agentId || 'claude-code';
573
625
  state.currentProfileId = summary.aiProfileId || null;
574
626
  state.currentModel = summary.selectedModel || summary.model || null;
@@ -586,12 +638,24 @@ async function openSession(sessionId, options = {}) {
586
638
  for (const event of snapshot.events || []) { renderEvent(event); last = Math.max(last, event.sequence || 0); }
587
639
  } finally { state.replaying = false; }
588
640
  rememberTitleFromEvents(snapshot.events || []);
589
- state.unsub = client.subscribe(sessionId, event => renderEvent(event), () => {}, { afterSequence: last });
641
+ state.unsub = client.subscribe(sessionId, event => { if (epoch === sessionEpoch && state.activeSessionId === sessionId) renderEvent(event); }, () => {}, { afterSequence: last });
590
642
  await renderProjectList();
591
643
  refreshContextUsage();
644
+ if (embedded) {
645
+ try {
646
+ const response = await fetch(`${apiBase}/sessions/${encodeURIComponent(sessionId)}/knowledge`);
647
+ const knowledge = response.ok ? await response.json() : null;
648
+ if (epoch === sessionEpoch && state.activeSessionId === sessionId && knowledge) publishKnowledge(summary.contextId, sessionId, knowledge);
649
+ } catch { /* The native conversation remains usable if knowledge is offline. */ }
650
+ }
651
+ }
652
+
653
+ function publishKnowledge(projectId, sessionId, knowledge) {
654
+ if (embedded) window.parent.postMessage({ type: 'kb:knowledge', app: 'agent-terminal', projectId, sessionId, knowledge }, document.referrer ? new URL(document.referrer).origin : location.origin);
592
655
  }
593
656
 
594
657
  function detachSession(options = {}) {
658
+ sessionEpoch++;
595
659
  if (state.unsub) { try { state.unsub(); } catch {} state.unsub = null; }
596
660
  state.activeSessionId = null;
597
661
  state.sessionSummary = null;
@@ -1014,7 +1078,7 @@ function renderEvent(event) {
1014
1078
  setMarkdown(body, String(event.result));
1015
1079
  }
1016
1080
  const lastMsg = [...col.children].reverse().find(node => node.classList && node.classList.contains('ai'));
1017
- if (lastMsg) lastMsg.append(msgFooter(String(event.result || lastMsg.textContent), lastPrompt, { model: turnModel }));
1081
+ if (lastMsg) lastMsg.append(msgFooter(String(event.result || lastMsg.textContent), lastPrompt, { model: turnModel, at: event.at }));
1018
1082
  if (turnChanges && turnChanges.size) col.append(renderTurnDiffs(turnChanges));
1019
1083
  }
1020
1084
  setBusyState('idle');
@@ -1218,7 +1282,8 @@ function msgFooter(text, lastPrompt, options = {}) {
1218
1282
  const bits = [];
1219
1283
  if (options.model) bits.push(options.model);
1220
1284
  if (options.interrupted) bits.push('已中断');
1221
- bits.push(new Date().toTimeString().slice(0, 5));
1285
+ const timestamp = options.at ? new Date(options.at) : state.replaying ? null : new Date();
1286
+ if (timestamp && !Number.isNaN(timestamp.getTime())) bits.push(timestamp.toTimeString().slice(0, 5));
1222
1287
  row.append(el('span', 'foot-time', bits.join(' · ')));
1223
1288
  return row;
1224
1289
  }
@@ -1522,9 +1587,10 @@ function setBusyState(sessionState) {
1522
1587
 
1523
1588
  async function refreshContextUsage() {
1524
1589
  if (!state.activeSessionId) return;
1590
+ const sessionId = state.activeSessionId, epoch = sessionEpoch;
1525
1591
  try {
1526
- const usage = await fetch(`/api/claude-workbench/v1/sessions/${encodeURIComponent(state.activeSessionId)}/context-usage`).then(r => (r.ok ? r.json() : null));
1527
- if (!usage) return;
1592
+ const usage = await fetch(`${apiBase}/sessions/${encodeURIComponent(sessionId)}/context-usage`).then(r => (r.ok ? r.json() : null));
1593
+ if (!usage || epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
1528
1594
  $('btn-ctx').hidden = false;
1529
1595
  $('btn-ctx').title = usage.percent != null ? `上下文用量 ${usage.percent}%` : '上下文用量';
1530
1596
  $('ctx-label').textContent = `${fmtTokens(usage.used)} / ${fmtTokens(usage.contextWindow)}`;
@@ -1565,6 +1631,7 @@ function wireComposer() {
1565
1631
  input.addEventListener('input', () => {
1566
1632
  input.style.height = 'auto';
1567
1633
  input.style.height = `${Math.min(150, input.scrollHeight)}px`;
1634
+ if (embedded && state.activeProjectId) rememberProjectDraft(state.activeProjectId, { text: input.value, attachments: [...state.attachments], sessionId: state.activeSessionId });
1568
1635
  });
1569
1636
  input.addEventListener('keydown', event => {
1570
1637
  if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendCurrent(); }
@@ -1655,26 +1722,37 @@ async function sendCurrent() {
1655
1722
  const text = input.value.trim();
1656
1723
  if (!text && !state.attachments.length) return;
1657
1724
  state.sending = true;
1658
- if (!state.activeSessionId) { await newSession(); if (!state.activeSessionId) { state.sending = false; return; } }
1725
+ const originProjectId = state.activeProjectId, originEpoch = selectionEpoch;
1726
+ if (!state.activeSessionId) { await newSession(); if (!state.activeSessionId || state.activeProjectId !== originProjectId || selectionEpoch !== originEpoch) { state.sending = false; return; } }
1659
1727
  input.value = '';
1660
1728
  input.style.height = 'auto';
1661
1729
  const attachments = state.attachments.splice(0);
1662
1730
  renderAttachmentTray();
1731
+ const sentSessionId = state.activeSessionId, sentProjectId = state.activeProjectId;
1732
+ rememberProjectDraft(sentProjectId, { text: '', attachments: [], sessionId: sentSessionId });
1663
1733
  try {
1664
- await client.send({
1734
+ const accepted = await client.send({
1665
1735
  sessionId: state.activeSessionId,
1666
1736
  text,
1667
1737
  attachments: attachments.map(item => ({ name: item.name, mediaType: item.mediaType, data: item.data })),
1668
1738
  // Effort rides on every message so mid-session switches take effect.
1669
1739
  options: { effort: state.caps && state.caps.generationParams.includes('effort') ? state.effort : undefined },
1670
1740
  });
1671
- setBusyState('running');
1741
+ if (accepted?.knowledge && state.activeSessionId === sentSessionId) publishKnowledge(sentProjectId, sentSessionId, accepted.knowledge);
1742
+ if (state.activeSessionId !== sentSessionId) return;
1743
+ setBusyState(accepted?.state || 'running');
1672
1744
  state.follow = true; // sending a message re-engages follow mode
1673
1745
  } catch (error) {
1746
+ if (state.activeProjectId !== sentProjectId || state.activeSessionId !== sentSessionId) {
1747
+ const draft = projectDrafts.get(sentProjectId) || {};
1748
+ rememberProjectDraft(sentProjectId, { ...draft, text: draft.text || text, attachments: [...attachments, ...(draft.attachments || [])], sessionId: sentSessionId });
1749
+ return;
1750
+ }
1674
1751
  toast(error.message, true);
1675
1752
  input.value = text;
1676
1753
  state.attachments.unshift(...attachments);
1677
1754
  renderAttachmentTray();
1755
+ rememberProjectDraft(sentProjectId, { text, attachments: [...state.attachments], sessionId: sentSessionId });
1678
1756
  } finally {
1679
1757
  state.sending = false;
1680
1758
  }
@@ -1770,7 +1848,7 @@ function wireSettings() {
1770
1848
 
1771
1849
  /* ===================== 会话捕获(终端对话是否留档) ===================== */
1772
1850
 
1773
- const CAPTURE_API = '/api/claude-workbench/v1/capture-settings';
1851
+ const CAPTURE_API = `${apiBase}/capture-settings`;
1774
1852
 
1775
1853
  function renderCaptureNote(text, bad) {
1776
1854
  const note = $('capture-note');
@@ -2145,7 +2223,7 @@ function wireProjectModal() {
2145
2223
  };
2146
2224
 
2147
2225
  let pickingProject = false;
2148
- const pickerUrl = () => '/api/claude-workbench/v1/system/pick-folder';
2226
+ const pickerUrl = () => `${apiBase}/system/pick-folder`;
2149
2227
  const addProjectViaPicker = async () => {
2150
2228
  if (pickingProject) return;
2151
2229
  pickingProject = true;
@@ -2278,10 +2356,8 @@ function wireChrome() {
2278
2356
  * kb:sessions / kb:session / kb:archived / kb:removed-project(各操作回执)。
2279
2357
  * URL 参数:?embed=1 进入嵌入态(默认折叠项目栏,sidebar=1 保持展开),project= 预选。 */
2280
2358
  function embedAllowedOrigin(origin) {
2281
- if (origin === 'null') return true; // file:// 壳
2282
2359
  try {
2283
- const u = new URL(origin);
2284
- return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]';
2360
+ return origin === (document.referrer ? new URL(document.referrer).origin : location.origin);
2285
2361
  } catch { return false; }
2286
2362
  }
2287
2363
  function setEmbedSidebarVisible(visible) {
@@ -2296,6 +2372,7 @@ function embedSelectProject(ref) {
2296
2372
  const project = embedFindProject(ref);
2297
2373
  if (!project) return false;
2298
2374
  if (state.activeProjectId !== project.id) {
2375
+ if (state.activeProjectId) rememberProjectDraft(state.activeProjectId, { text: $('chat-input').value, attachments: [...state.attachments], sessionId: state.activeSessionId });
2299
2376
  detachSession();
2300
2377
  resetStream();
2301
2378
  setBusyState('idle');
@@ -2306,18 +2383,33 @@ function embedSelectProject(ref) {
2306
2383
  $('session-title').textContent = '新会话';
2307
2384
  applyCapabilityGates();
2308
2385
  renderModelLabel();
2386
+ const draft = projectDrafts.get(project.id) || { text: savedProjectDraft(project.id), sessionId: savedProjectSession(project.id) };
2387
+ $('chat-input').value = draft?.text || '';
2388
+ state.attachments = draft?.attachments || [];
2389
+ renderAttachmentTray();
2390
+ if (draft?.sessionId) openSession(draft.sessionId, { preserveAttachments: true });
2391
+ else publishKnowledge(project.id, null, { status: 'not-retrieved', references: [] });
2392
+ }
2393
+ if (!state.activeSessionId) {
2394
+ const sessionId = projectDrafts.get(project.id)?.sessionId || savedProjectSession(project.id);
2395
+ if (sessionId) openSession(sessionId, { preserveAttachments: true });
2309
2396
  }
2310
2397
  state.expanded.add(project.id);
2311
2398
  renderProjectList();
2312
2399
  return true;
2313
2400
  }
2314
2401
  window.addEventListener('message', async ev => {
2315
- if (!embedAllowedOrigin(ev.origin)) return;
2402
+ if (ev.source !== window.parent || !embedAllowedOrigin(ev.origin)) return;
2316
2403
  const data = ev.data;
2317
2404
  if (!data || typeof data !== 'object') return;
2318
- const reply = payload => { if (ev.source) ev.source.postMessage(Object.assign({ app: 'agent-terminal' }, payload), '*'); };
2405
+ const reply = payload => { if (ev.source) ev.source.postMessage(Object.assign({ app: 'agent-terminal', requestId: data.requestId }, payload), ev.origin); };
2406
+ if (data.type === 'kb:theme') { document.body.classList.toggle('light', data.theme !== 'dark'); document.body.dataset.kbTheme = data.theme === 'dark' ? 'dark' : 'light'; reply({ type: 'kb:configured', theme: document.body.dataset.kbTheme }); return; }
2407
+ if (data.type === 'kb:open-settings') { openSettings('providers'); return; }
2319
2408
  if (data.type === 'kb:sidebar') setEmbedSidebarVisible(!!data.visible);
2320
2409
  else if (data.type === 'kb:select-project') {
2410
+ const epoch = ++selectionEpoch;
2411
+ if (!embedFindProject(data.project)) await refreshProjects();
2412
+ if (epoch !== selectionEpoch) return;
2321
2413
  reply({ type: 'kb:projects', ok: embedSelectProject(data.project), activeProjectId: state.activeProjectId,
2322
2414
  projects: state.projects.map(p => ({ id: p.id, name: p.name, rootPath: p.rootPath })) });
2323
2415
  } else if (data.type === 'kb:list-sessions') {
@@ -2345,6 +2437,7 @@ window.addEventListener('message', async ev => {
2345
2437
  const sessionId = String(data.sessionId || '');
2346
2438
  try {
2347
2439
  await client.archiveSession(sessionId);
2440
+ forgetSessionSelection(sessionId);
2348
2441
  if (state.activeSessionId === sessionId) detachSession();
2349
2442
  await renderProjectList();
2350
2443
  reply({ type: 'kb:archived', ok: true, sessionId });
@@ -2370,14 +2463,16 @@ window.addEventListener('message', async ev => {
2370
2463
  function embedInit() {
2371
2464
  const q = new URLSearchParams(location.search);
2372
2465
  if (q.get('embed') === '1') {
2373
- setEmbedSidebarVisible(q.get('sidebar') !== '1');
2466
+ document.body.classList.add('kb-embedded');
2467
+ document.body.classList.toggle('light', q.get('theme') !== 'dark');
2468
+ document.body.dataset.kbTheme = q.get('theme') === 'dark' ? 'dark' : 'light';
2469
+ setEmbedSidebarVisible(true);
2374
2470
  const preset = q.get('project');
2375
2471
  if (preset) {
2376
2472
  const trySelect = () => { if (!embedSelectProject(preset)) setTimeout(trySelect, 500); };
2377
2473
  setTimeout(trySelect, 0);
2378
2474
  }
2379
2475
  }
2380
- if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'agent-terminal' }, '*');
2381
2476
  }
2382
2477
  embedInit();
2383
2478
  // 主区顶栏常驻设置入口(嵌入折叠侧栏后仍可达;与模型弹层内「管理模型」同目标)
@@ -44,8 +44,9 @@
44
44
  <div class="col" id="stream-col">
45
45
  <div class="empty-hint" id="empty-hint">
46
46
  <div class="big"><span class="hint-wrap"><span class="hint-art"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 8 4 4-4 4"/><path d="M13 16h6"/></svg></span><span class="hint-spark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l1.9 5.8L20 12l-6.1 3.2L12 21l-1.9-5.8L4 12l6.1-3.2z"/></svg></span></span></div>
47
- <p>从左侧选择一个项目,或 <button class="linklike" id="hint-add-proj">添加项目</button> 开始。<br>
47
+ <p class="standalone-hint">从左侧选择一个项目,或 <button class="linklike" id="hint-add-proj">添加项目</button> 开始。<br>
48
48
  新会话将使用底部选中的 Agent 与模型。</p>
49
+ <p class="embedded-hint">输入当前项目的任务,开始新会话。<br>相关知识会在发送前检索,并在右侧显示来源。</p>
49
50
  </div>
50
51
  </div>
51
52
  </div>
@@ -67,7 +68,7 @@
67
68
  <button class="bar-btn" id="btn-ctx" title="上下文用量" hidden><span id="ctx-label"></span></button>
68
69
  <div class="pop from-right ctx-pop" id="pop-ctx" hidden></div>
69
70
  </span>
70
- <span class="anchor">
71
+ <span class="anchor model-anchor">
71
72
  <button class="bar-btn" id="btn-model" title="选择供应商与模型"><span id="model-label">Agent 自身登录 · 默认模型</span> <span class="chev"><svg class="ics" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></button>
72
73
  <div class="pop from-right" id="pop-model" hidden></div>
73
74
  </span>
@@ -152,7 +153,7 @@
152
153
 
153
154
  <div class="toast-host" id="toast" aria-live="polite"></div>
154
155
 
155
- <script src="/packages/client/index.js"></script>
156
+ <script src="../packages/client/index.js"></script>
156
157
  <script src="./app.js"></script>
157
158
  </body>
158
159
  </html>
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Claude AI Workbench API
4
- version: 0.12.0
4
+ version: 0.13.1
5
5
  description: Versioned local API for AI Profile, credential and Claude Code session management.
6
6
  servers:
7
7
  - url: http://127.0.0.1:5760/api/claude-workbench/v1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/claude-ai-workbench",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Shared Claude Code terminal and Anthropic-compatible AI profile modules for local developer tools.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/client",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "browser": "index.js",
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@claude-ai-workbench/contracts": "0.12.0"
23
+ "@claude-ai-workbench/contracts": "0.13.1"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/contracts",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "browser": "index.js",
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@claude-ai-workbench/contracts": "0.12.0"
23
+ "@claude-ai-workbench/contracts": "0.13.1"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -25,12 +25,17 @@ app.server.listen(port, host, () => {
25
25
  // Compile the folder-picker helper up front so the first "browse…" click
26
26
  // pops the dialog in milliseconds instead of paying the powershell +
27
27
  // Add-Type startup. Never blocks or crashes the server.
28
- warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
28
+ if (process.env.KB_WORKBENCH_DISABLE_PICKER !== '1') warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
29
29
  });
30
30
 
31
+ let stopping = false;
31
32
  function shutdown() {
33
+ if (stopping) return; stopping = true;
34
+ app.service.destroy();
35
+ app.server.closeAllConnections?.();
32
36
  app.server.close(() => process.exit(0));
33
37
  setTimeout(() => process.exit(0), 3000).unref();
34
38
  }
35
39
  process.on('SIGINT', shutdown);
36
40
  process.on('SIGTERM', shutdown);
41
+ process.on('message', message => { if (message?.type === 'kb:shutdown') shutdown(); });
@@ -15,7 +15,8 @@ const { AttachmentStore } = require('./lib/attachment-store');
15
15
  const { ProjectStore } = require('./lib/project-store');
16
16
  const { SessionArchiveStore } = require('./lib/session-archive-store');
17
17
  const { AgentRegistry } = require('./lib/agent-registry');
18
- const { createAgentDiscovery, probeExecutableVersion } = require('./lib/agent-discovery');
18
+ const { createAgentDiscovery, probeExecutable } = require('./lib/agent-discovery');
19
+ const { createDiagnostics } = require('./lib/diagnostics');
19
20
  const { createSystemDoctor } = require('./lib/system-doctor');
20
21
  const { createModelDetection } = require('./lib/model-detection');
21
22
  const { createCodexDriver } = require('./lib/drivers/codex-driver');
@@ -38,7 +39,10 @@ function createWorkbenchServer(options = {}) {
38
39
  ...projects.toWorkspaceMap(),
39
40
  };
40
41
  const profiles = options.profiles || new ProfileStore({ dataDir });
41
- const captureSettings = options.captureSettings || new CaptureSettingsStore({ dataDir });
42
+ const bridgeUrl = options.bridgeUrl || process.env.KB_EVENT_BRIDGE_URL;
43
+ const runtimeToken = options.runtimeToken || process.env.KB_WORKBENCH_RUNTIME_TOKEN;
44
+ const conversationCapture = options.conversationCapture || (bridgeUrl && runtimeToken ? new (require('./lib/conversation-capture').ConversationCapture)({ file: path.join(dataDir, 'capture-outbox.v1.json'), bridgeUrl, token: runtimeToken }) : null);
45
+ const captureSettings = options.captureSettings || new CaptureSettingsStore({ dataDir, defaultConversations: !!conversationCapture });
42
46
  const uiPreferences = options.uiPreferences || new UiPreferencesStore({ dataDir });
43
47
  const attachments = options.attachments || new AttachmentStore({ root: path.join(dataDir, 'attachments') });
44
48
  const contexts = options.contexts || new ContextResolver({ workspaces, createMissing: options.createMissingWorkspaces !== false });
@@ -48,7 +52,9 @@ function createWorkbenchServer(options = {}) {
48
52
  // the driver at boot and re-applied on every settings save.
49
53
  const applyBridgeCapture = enabled => {
50
54
  if (claudeDriver && typeof claudeDriver.setBridgeCaptureConversation === 'function') {
51
- claudeDriver.setBridgeCaptureConversation(enabled);
55
+ // Native capture records the original input, not the augmented SDK
56
+ // prompt. Suppress inherited CLI hooks to avoid duplicate capture.
57
+ claudeDriver.setBridgeCaptureConversation(conversationCapture ? false : enabled);
52
58
  }
53
59
  };
54
60
  applyBridgeCapture(captureSettings.read().terminalConversations);
@@ -63,6 +69,7 @@ function createWorkbenchServer(options = {}) {
63
69
  },
64
70
  });
65
71
  const runner = registry.asRunner();
72
+ const diagnostics = options.logger || createDiagnostics({ dataDir, secrets: () => [...Object.values(profiles._credentials().credentials), process.env.ANTHROPIC_API_KEY, process.env.ANTHROPIC_AUTH_TOKEN] });
66
73
  const agentDiscovery = options.agentDiscovery || createAgentDiscovery({
67
74
  claudeProbe: () => {
68
75
  try {
@@ -72,11 +79,17 @@ function createWorkbenchServer(options = {}) {
72
79
  const cmd = typeof executable === 'string'
73
80
  ? executable
74
81
  : executable && executable.cmd;
75
- if (!cmd) return { available: false, version: null };
82
+ if (!cmd) {
83
+ const reason = executable?.reason || '未发现可用的 Claude Code 启动文件。';
84
+ diagnostics.error('agent.discovery.failed', 'Claude Code 不可用。', { phase: 'discovery', error: { code: 'CLAUDE_EXECUTABLE_NOT_FOUND', message: reason }, context: { agent: 'claude-code', rejectedPaths: executable?.rejected || [] } });
85
+ return { available: false, version: null, reason };
86
+ }
76
87
  // Honest probe: spawn the resolved executable with --version. Same
77
88
  // contract as every other agent: no version, no availability claim.
78
- const version = probeExecutableVersion(cmd);
79
- return { available: version != null, version };
89
+ const probe = probeExecutable(cmd);
90
+ if (!probe.available) diagnostics.error('agent.discovery.failed', 'Claude Code 版本探测失败。', { phase: 'version-probe', error: { code: probe.code, message: probe.reason }, context: { agent: 'claude-code', executable: cmd, exitCode: probe.exitCode } });
91
+ else if (executable.rejected?.length) diagnostics.warn('agent.discovery.recovered', '已跳过失效的 Claude Code 启动路径,使用有效安装。', { phase: 'discovery', context: { executable: cmd, rejectedPaths: executable.rejected } });
92
+ return probe;
80
93
  } catch { return { available: false, version: null }; }
81
94
  },
82
95
  });
@@ -84,23 +97,31 @@ function createWorkbenchServer(options = {}) {
84
97
  const modelDetection = options.modelDetection || createModelDetection({
85
98
  contextLookup: modelContextWindows && modelContextWindows.lookupContextWindow,
86
99
  });
87
- const doctor = options.doctor || createSystemDoctor({
100
+ const collectDoctor = options.doctor || createSystemDoctor({
88
101
  resolveClaudeExecutable: () => {
89
102
  try { return claudeDriver.findClaudeExecutableForSdk(); } catch { return { cmd: null }; }
90
103
  },
91
104
  discoverZCodeCli,
92
105
  probeZCodeVersion,
93
106
  });
107
+ const doctor = () => {
108
+ const report = collectDoctor();
109
+ const claude = report.agents?.['claude-code'];
110
+ if (claude && !claude.versionProbe?.ok) diagnostics.error('agent.doctor.failed', 'Claude Code 环境诊断失败。', { phase: 'doctor', error: { code: 'CLAUDE_PROBE_FAILED', message: claude.versionProbe?.error || claude.resolutionReason }, context: { agent: 'claude-code', executable: claude.resolvedExecutable, rejectedPaths: claude.rejectedPaths, exitCode: claude.versionProbe?.exitCode } });
111
+ return report;
112
+ };
94
113
  const service = options.service || new WorkbenchService({
95
- runner, profiles, contexts, attachments, apiPrefix,
114
+ knowledge: options.knowledge || new (require('./lib/knowledge-context').KnowledgeContext)({ engineUrl: options.engineUrl || process.env.KB_KNOWLEDGE_ENGINE_URL, contextFile: path.join(dataDir, 'knowledge-contexts.v1.json') }),
115
+ runner, profiles, contexts, attachments, apiPrefix, diagnostics,
96
116
  projects, sessionArchive, agentDiscovery, modelContextWindows, modelDetection,
97
117
  archiveRetentionDays: options.archiveRetentionDays,
98
118
  captureSettings, uiPreferences, onCaptureSettingsSaved: applyBridgeCapture,
119
+ conversationCapture,
99
120
  testWorkspace: path.join(testRoot, 'profile-test'), auditFile: path.join(dataDir, 'audit.jsonl'),
100
121
  version: options.version || require('./package.json').version,
101
122
  maxSessions: options.maxSessions, maxConcurrentRuns: options.maxConcurrentRuns,
102
123
  });
103
- const handleApi = createHttpHandler(service, { apiPrefix, corsOrigin: options.corsOrigin, doctor });
124
+ const handleApi = createHttpHandler(service, { apiPrefix, corsOrigin: options.corsOrigin, doctor, runtimeToken: options.runtimeToken || process.env.KB_WORKBENCH_RUNTIME_TOKEN });
104
125
  const distributionRoot = resolveDistributionRoot();
105
126
  const staticRoot = path.resolve(options.staticRoot || path.join(distributionRoot, 'apps', 'minimal-test-host'));
106
127
  const profileSettingsRoot = path.resolve(options.profileSettingsRoot || path.join(distributionRoot, 'apps', 'ai-profile-settings-host'));