@sanqianx/claude-ai-workbench 0.13.2-rc.13 → 0.13.2-rc.15
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 +7 -1
- package/apps/agent-terminal/app.css +4 -0
- package/apps/agent-terminal/app.js +223 -25
- package/apps/agent-terminal/index.html +5 -1
- package/docs/sdk/openapi.yaml +39 -1
- package/package.json +1 -1
- package/packages/client/index.d.ts +5 -0
- package/packages/client/index.js +12 -1
- package/packages/client/package.json +2 -2
- package/packages/contracts/index.d.ts +10 -0
- package/packages/contracts/package.json +1 -1
- package/packages/core/package.json +2 -2
- package/packages/server/index.d.ts +6 -2
- package/packages/server/index.js +12 -12
- package/packages/server/lib/agent-discovery.js +90 -73
- package/packages/server/lib/claude-history-recovery.js +87 -0
- package/packages/server/lib/http-handler.js +7 -2
- package/packages/server/lib/session-history.js +184 -0
- package/packages/server/lib/system-doctor.js +14 -10
- package/packages/server/lib/workbench-service.js +85 -5
- package/packages/server/package.json +2 -2
- package/packages/ui/package.json +2 -2
- package/src/backend/lib/claude-cli-runner.js +4 -4
- package/src/backend/lib/sdk-turn-stream.js +16 -0
package/README.md
CHANGED
|
@@ -10,11 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
重复收到当前项目的嵌入通知不会取消正在创建的会话;真正切换项目时仍隔离迟到的响应和草稿。项目查找尚未完成时,以最新的选择为准。
|
|
12
12
|
|
|
13
|
+
Agent 发现为异步扫描,最多两个并发、每项 5 秒、整轮 12 秒;`GET /agents` 立即返回缓存及 `scan` 状态、代次和更新时间,`POST /agents/refresh` 强制发起或合并后台扫描并立即返回 202。不确定的超时保留上次探测结果,不误报卸载。界面先绑定控件再独立读取数据,手动重扫、页面重新可见和联网恢复可合并刷新;`kb:visibility` / `kb:refresh` 回执为 `kb:refreshed`,不重载 iframe、不清空草稿或运行会话。嵌入时可用 `transport=live` 选择同源外壳注入的 EventSource 兼容传输;独立运行保留原 SSE。
|
|
14
|
+
|
|
13
15
|
受信任本地外壳通过 `KB_WORKBENCH_RUNTIME_TOKEN`、`KB_EVENT_BRIDGE_URL` 和引擎地址连接后台服务。原生工作台捕获原始用户输入及成功最终回答,通过持久化 outbox 投递到 bridge 的鉴权入口;不捕获系统提示词、注入的知识、工具输出或供应商凭据。`/api/health` 中 `conversationCapture` 展示启用状态、待投递数和错误;已有用户显式关闭的捕获设置不会被覆盖。桥接不可用时保留待投递记录,恢复或重启后继续,不伪造会话。
|
|
14
16
|
|
|
15
17
|
工作台是产品供应商与凭据的活动配置源。后台维护默认模型持久化,不随会话临时模型切换;解析后的凭据只交给鉴权本地服务,不进入浏览器消息。SDK 会话从当前项目登记的工作区恢复,也兼容旧登记文件。
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
四客户端的运行快照使用同目录临时文件原子替换;5000 条实时事件只用于短期回放,不再作为完整会话历史。Workbench 在自己的数据目录 `session-history/` 中独立追加保存历史,并合并连续文字增量;页面先读取近期 20 轮,向上翻阅或点击“加载更早记录”时按轮分页,补入记录与断线恢复均保持阅读位置。浏览器关闭不停止历史采集。磁盘写入失败或记录损坏时保留已有数据,在页面明确提示不完整并写入本地诊断,实时对话不因此中断。
|
|
20
|
+
|
|
21
|
+
旧版已截断的 Claude 会话在原始 CLI 记录仍完整且会话空闲时,可只读恢复用户问答与工具过程,包括沿上下文压缩的逻辑父节点恢复早期记录;不修改原始文件、不调用模型。原始记录不存在、损坏或链路不完整时,只展示实际保存的部分并明确提示,不能凭空补造历史。新历史接口为 `GET /sessions/{sessionId}/history?limit=20&before=<cursor>`,Client SDK 对应 `loadHistory()`;现有快照与 SSE 游标保持兼容。
|
|
22
|
+
|
|
23
|
+
持续输出时,向上的滚轮或翻页键立即停止自动跟随;发送请求的迟到回执不会重新拉回底部。向下滚回底部或点击“回到底部”后恢复跟随,无需切换会话。
|
|
18
24
|
|
|
19
25
|
MiniMax 向量模型使用独立的 `/v1/embeddings` 接口,不复用聊天的 `/anthropic` 路由。官方域名的已知接口后缀在运行期规范化为服务根地址,现有配置文件不被迁移或改写;自定义网关路径保持原样。
|
|
20
26
|
|
|
@@ -147,6 +147,10 @@ body.light .top-strip .st { color:#2f5fd0; }
|
|
|
147
147
|
|
|
148
148
|
/* ===== 消息流(ZCode 风格:过程折叠,正文直出)===== */
|
|
149
149
|
.stream { flex:1; overflow-y:auto; padding:8px 24px 24px; }
|
|
150
|
+
.stream:focus { outline:none; }
|
|
151
|
+
.history-bar { max-width:860px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center; gap:8px; color:var(--muted); font-size:12px; }
|
|
152
|
+
.history-bar[hidden], .history-bar [hidden] { display:none; }
|
|
153
|
+
.history-bar button:disabled { opacity:.6; cursor:wait; }
|
|
150
154
|
.col { max-width:860px; margin:0 auto; }
|
|
151
155
|
.empty-hint { text-align:center; color:var(--muted); margin-top:18vh; font-size:13px; line-height:1.7; }
|
|
152
156
|
.empty-hint .big { display:flex; align-items:center; justify-content:center; margin-bottom:12px; }
|
|
@@ -14,6 +14,7 @@ let selectionEpoch = 0;
|
|
|
14
14
|
let sessionEpoch = 0;
|
|
15
15
|
let sessionCreationEpoch = 0;
|
|
16
16
|
let pendingEmbedSelection = null;
|
|
17
|
+
let historyView = null;
|
|
17
18
|
const projectDrafts = new Map();
|
|
18
19
|
function savedProjectDraft(projectId, text) {
|
|
19
20
|
try {
|
|
@@ -51,8 +52,12 @@ function forgetSessionSelection(sessionId) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function embeddedTransport() {
|
|
56
|
+
try { if (embedded && embedParams.get('transport') === 'live' && window.parent.location.origin === location.origin && window.parent.KnowledgeLive) return { EventSource: window.parent.KnowledgeLive.EventSource }; } catch { /* Cross-origin hosts retain standalone SSE. */ }
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
54
59
|
const client = window.createClaudeWorkbenchClient
|
|
55
|
-
? window.createClaudeWorkbenchClient({ apiBase })
|
|
60
|
+
? window.createClaudeWorkbenchClient({ apiBase, ...embeddedTransport() })
|
|
56
61
|
: null;
|
|
57
62
|
|
|
58
63
|
const PERMISSION_LABELS = { default: '默认防护', acceptEdits: '接受编辑', plan: '计划模式', bypassPermissions: '完全访问' };
|
|
@@ -61,6 +66,8 @@ const AGENT_LABELS = { 'claude-code': 'Claude Code', codex: 'Codex', opencode: '
|
|
|
61
66
|
|
|
62
67
|
const state = {
|
|
63
68
|
agents: [],
|
|
69
|
+
agentsScan: { state: 'idle' },
|
|
70
|
+
agentsError: false,
|
|
64
71
|
projects: [],
|
|
65
72
|
profiles: [],
|
|
66
73
|
profilesStatus: 'idle', // idle | loading | ready | error (error keeps the last good list)
|
|
@@ -78,6 +85,7 @@ const state = {
|
|
|
78
85
|
attachments: [],
|
|
79
86
|
busy: false,
|
|
80
87
|
follow: true, // auto-scroll sticks to bottom until the user scrolls away
|
|
88
|
+
scrollIntent: null,
|
|
81
89
|
replaying: false, // true while replaying a session snapshot: no live timers/durations
|
|
82
90
|
expanded: new Set(),
|
|
83
91
|
caps: null,
|
|
@@ -128,7 +136,7 @@ function applyPreferences(values) {
|
|
|
128
136
|
|
|
129
137
|
async function loadPreferences() {
|
|
130
138
|
try {
|
|
131
|
-
const response = await fetch(`${apiBase}/ui-preferences
|
|
139
|
+
const response = await fetch(`${apiBase}/ui-preferences`, { signal: AbortSignal.timeout(3000) });
|
|
132
140
|
if (!response.ok) return;
|
|
133
141
|
const body = await response.json();
|
|
134
142
|
savedPreferences = { ...(body && body.values || {}) };
|
|
@@ -154,14 +162,6 @@ function savePreferences(partial) {
|
|
|
154
162
|
/* ===================== init ===================== */
|
|
155
163
|
|
|
156
164
|
async function init() {
|
|
157
|
-
try {
|
|
158
|
-
const health = await client.health();
|
|
159
|
-
$('app-version').textContent = `v${health.version}`;
|
|
160
|
-
$('about-text').textContent = `Agent Terminal v${health.version} · API ${health.apiVersion} · ${health.sessions} 个会话`;
|
|
161
|
-
} catch { /* server unreachable is surfaced by the panels themselves */ }
|
|
162
|
-
await Promise.all([refreshAgents(), refreshProfiles()]);
|
|
163
|
-
await loadPreferences();
|
|
164
|
-
await refreshProjects();
|
|
165
165
|
wireChrome();
|
|
166
166
|
wireComposer();
|
|
167
167
|
wireSettings();
|
|
@@ -169,6 +169,13 @@ async function init() {
|
|
|
169
169
|
$('view-terminal').inert = false;
|
|
170
170
|
$('view-terminal').setAttribute('aria-busy', 'false');
|
|
171
171
|
$('view-settings').inert = false;
|
|
172
|
+
const healthJob = client.health().then(health => {
|
|
173
|
+
$('app-version').textContent = `v${health.version}`;
|
|
174
|
+
$('about-text').textContent = `Agent Terminal v${health.version} · API ${health.apiVersion} · ${health.sessions} 个会话`;
|
|
175
|
+
}).catch(() => {});
|
|
176
|
+
const preferences = loadPreferences();
|
|
177
|
+
const projects = preferences.then(() => refreshProjects()).catch(error => toast(`项目读取失败:${error.message}`, true));
|
|
178
|
+
await Promise.allSettled([healthJob, refreshAgents(), refreshProfiles(), projects]);
|
|
172
179
|
if (!state.activeSessionId) $('session-title').textContent = '选择或创建一个会话';
|
|
173
180
|
// Ready is the usable-page boundary: the host may immediately select a
|
|
174
181
|
// project or open settings, so data and handlers must both be available.
|
|
@@ -177,11 +184,21 @@ async function init() {
|
|
|
177
184
|
|
|
178
185
|
/* ===================== agents & capabilities ===================== */
|
|
179
186
|
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
let agentsRefreshJob = null, agentsPollTimer = null;
|
|
188
|
+
function refreshAgents(force = false) {
|
|
189
|
+
if (agentsRefreshJob) return force ? agentsRefreshJob.then(() => refreshAgents(true)) : agentsRefreshJob;
|
|
190
|
+
agentsRefreshJob = (async () => {
|
|
191
|
+
try {
|
|
192
|
+
const result = force && client.refreshAgents ? await client.refreshAgents() : client.agentStatus ? await client.agentStatus() : { agents: await client.listAgents() };
|
|
193
|
+
state.agents = result.agents || []; state.agentsScan = result.scan || { state: 'ready' }; state.agentsError = false;
|
|
194
|
+
} catch { state.agentsError = true; }
|
|
182
195
|
renderAgentSummary();
|
|
183
196
|
renderAgentCards();
|
|
184
197
|
applyCapabilityGates();
|
|
198
|
+
clearTimeout(agentsPollTimer);
|
|
199
|
+
if (state.agentsScan.state === 'scanning') agentsPollTimer = setTimeout(() => refreshAgents(), 500);
|
|
200
|
+
})().finally(() => { agentsRefreshJob = null; });
|
|
201
|
+
return agentsRefreshJob;
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
function renderAgentSummary() {
|
|
@@ -191,7 +208,8 @@ function renderAgentSummary() {
|
|
|
191
208
|
dot.style.background = available.length ? 'var(--good)' : 'var(--bad)';
|
|
192
209
|
$('agent-summary').textContent = available.length
|
|
193
210
|
? `已发现 ${available.length} 个 Agent:${available.map(a => a.label).join(' · ')}`
|
|
194
|
-
: '未发现可用 Agent';
|
|
211
|
+
: state.agentsScan.state === 'scanning' ? '正在检查 Agent…' : state.agentsError ? 'Agent 状态读取失败,请重试' : '未发现可用 Agent';
|
|
212
|
+
if (available.length && (state.agentsError || state.agentsScan.state === 'scanning')) $('agent-summary').textContent += state.agentsError ? ' · 刷新失败,显示上次结果' : ' · 正在检查';
|
|
195
213
|
}
|
|
196
214
|
|
|
197
215
|
function renderAgentCards() {
|
|
@@ -203,7 +221,7 @@ function renderAgentCards() {
|
|
|
203
221
|
const body = el('div', 'a-body');
|
|
204
222
|
const name = el('div', 'a-name');
|
|
205
223
|
name.append(el('span', null, agent.label));
|
|
206
|
-
const chip = el('span', 'chip ' + (agent.available ? 'good' : ''), agent.available ? (agent.version ? `v${agent.version}` : '可用') : '不可用');
|
|
224
|
+
const chip = el('span', 'chip ' + (agent.available ? 'good' : ''), agent.scanState === 'unknown' ? '待检查' : agent.scanState === 'error' ? '检查未完成' : agent.available ? (agent.version ? `v${agent.version}` : '可用') : '不可用');
|
|
207
225
|
name.append(chip);
|
|
208
226
|
body.append(name);
|
|
209
227
|
body.append(el('div', 'a-meta', `${agent.transport} · 认证:${agent.authModes.join(' / ')}${agent.reason ? ` · ${agent.reason}` : ''}`));
|
|
@@ -487,7 +505,11 @@ function renderModelMenu() {
|
|
|
487
505
|
/* ===================== projects & sidebar ===================== */
|
|
488
506
|
|
|
489
507
|
async function refreshProjects(keepActive) {
|
|
490
|
-
|
|
508
|
+
const epoch = selectionEpoch;
|
|
509
|
+
let projects;
|
|
510
|
+
try { projects = await client.listProjects(); } catch { toast('项目读取失败,保留上次内容', true); return; }
|
|
511
|
+
if (epoch !== selectionEpoch) return;
|
|
512
|
+
state.projects = projects;
|
|
491
513
|
if (!keepActive || !state.projects.some(p => p.id === state.activeProjectId)) {
|
|
492
514
|
state.activeProjectId = state.projects.length ? state.projects[0].id : null;
|
|
493
515
|
if (state.activeProjectId && !state.expanded.has(state.activeProjectId)) state.expanded.add(state.activeProjectId);
|
|
@@ -703,6 +725,15 @@ async function openSession(sessionId, options = {}) {
|
|
|
703
725
|
let snapshot;
|
|
704
726
|
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; }
|
|
705
727
|
if (epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
|
|
728
|
+
let page = null, historyError = false;
|
|
729
|
+
if (typeof client.loadHistory === 'function') {
|
|
730
|
+
try { page = await client.loadHistory(sessionId); } catch { historyError = true; }
|
|
731
|
+
if (epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
|
|
732
|
+
}
|
|
733
|
+
historyView = { sessionId, epoch, events: [...(page?.events || snapshot.events || [])],
|
|
734
|
+
nextCursor: page?.nextCursor || null, incomplete: page?.incomplete === true, error: historyError,
|
|
735
|
+
supported: typeof client.loadHistory === 'function', loading: false, revision: 0 };
|
|
736
|
+
renderHistoryBar();
|
|
706
737
|
const summary = snapshot.session;
|
|
707
738
|
state.sessionSummary = summary;
|
|
708
739
|
state.activeProjectId = summary.contextId || state.activeProjectId;
|
|
@@ -716,15 +747,16 @@ async function openSession(sessionId, options = {}) {
|
|
|
716
747
|
renderModeLabel();
|
|
717
748
|
$('session-title').textContent = sessionTitle(summary);
|
|
718
749
|
setBusyState(summary.state);
|
|
719
|
-
let last = 0;
|
|
750
|
+
let last = Number(page?.highWater) || 0;
|
|
751
|
+
let streamOpened = false;
|
|
720
752
|
let replayThrough = 0;
|
|
721
753
|
// Timers and per-step durations are wall-clock side effects; replayed
|
|
722
754
|
// snapshots did not witness them, so the stream must not fake them.
|
|
723
755
|
state.replaying = true;
|
|
724
756
|
try {
|
|
725
|
-
for (const event of
|
|
757
|
+
for (const event of historyView.events) { renderEvent(event); if (!page) last = Math.max(last, event.sequence || 0); }
|
|
726
758
|
} finally { state.replaying = false; }
|
|
727
|
-
rememberTitleFromEvents(
|
|
759
|
+
rememberTitleFromEvents(historyView.events);
|
|
728
760
|
// The SSE replay window can overlap the snapshot (a slow turn raced the
|
|
729
761
|
// open, or the server answered a stale cursor with a full replay). Late or
|
|
730
762
|
// duplicate events are dropped by session sequence so each renders once.
|
|
@@ -735,6 +767,13 @@ async function openSession(sessionId, options = {}) {
|
|
|
735
767
|
state.unsub = client.subscribe(sessionId, event => {
|
|
736
768
|
if (epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
|
|
737
769
|
if (event.type === 'workbench/resync') {
|
|
770
|
+
if (historyView?.supported && !historyView.error) {
|
|
771
|
+
last = Number(event.highWater) || 0;
|
|
772
|
+
// The replay cache is not conversation truth. Re-read durable history
|
|
773
|
+
// on a gap without replacing the full transcript with its 5000-event tail.
|
|
774
|
+
void reloadHistory(historyView);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
738
777
|
last = 0;
|
|
739
778
|
replayThrough = Number(event.highWater);
|
|
740
779
|
if (!Number.isFinite(replayThrough) || replayThrough < 0) replayThrough = 0;
|
|
@@ -748,8 +787,14 @@ async function openSession(sessionId, options = {}) {
|
|
|
748
787
|
last = sequence;
|
|
749
788
|
}
|
|
750
789
|
if (state.replaying && !(Number.isFinite(sequence) && sequence > 0 && sequence <= replayThrough)) state.replaying = false;
|
|
790
|
+
if (historyView?.sessionId === sessionId) historyView.events.push(event);
|
|
791
|
+
state.liveRevision = (state.liveRevision || 0) + 1;
|
|
751
792
|
renderEvent(event);
|
|
752
|
-
}, () => {}, { afterSequence: last
|
|
793
|
+
}, () => {}, { afterSequence: last, onOpen: () => {
|
|
794
|
+
if (epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
|
|
795
|
+
if (streamOpened) { lastVisibleRefresh = 0; void refreshVisibleState(); }
|
|
796
|
+
streamOpened = true;
|
|
797
|
+
} });
|
|
753
798
|
await renderProjectList();
|
|
754
799
|
refreshContextUsage();
|
|
755
800
|
if (embedded) {
|
|
@@ -761,6 +806,95 @@ async function openSession(sessionId, options = {}) {
|
|
|
761
806
|
}
|
|
762
807
|
}
|
|
763
808
|
|
|
809
|
+
function renderHistoryBar() {
|
|
810
|
+
const bar = $('history-bar'), more = $('history-more'), status = $('history-status');
|
|
811
|
+
if (!bar || !more || !status) return;
|
|
812
|
+
const view = historyView;
|
|
813
|
+
bar.hidden = !view || (!view.nextCursor && !view.incomplete && !view.error && !view.loading);
|
|
814
|
+
more.hidden = !view?.nextCursor && !view?.error;
|
|
815
|
+
more.disabled = view?.loading === true;
|
|
816
|
+
more.textContent = view?.loading ? '正在加载…' : view?.error ? '重试读取历史' : '加载更早记录';
|
|
817
|
+
status.textContent = view?.error ? '完整历史读取失败,当前记录可能不完整。'
|
|
818
|
+
: view?.incomplete ? '旧版记录已截断;当前仅展示实际保存的部分。' : '';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function historyEventKey(event) { return JSON.stringify([event.type, event.id || '', event.sequence || 0, event.at || '', event.text || '']); }
|
|
822
|
+
|
|
823
|
+
function rebuildHistory(view, events, prepend = true) {
|
|
824
|
+
const stream = $('stream');
|
|
825
|
+
const top = stream.scrollTop, height = stream.scrollHeight, following = state.follow, intent = state.scrollIntent;
|
|
826
|
+
const prompts = [...($('stream-col').querySelectorAll?.('.msg.user') || [])];
|
|
827
|
+
const streamY = stream.getBoundingClientRect?.().top;
|
|
828
|
+
const anchor = streamY == null ? null : prompts.filter(node => node.getBoundingClientRect().top <= streamY + 8).pop() || prompts[0];
|
|
829
|
+
const anchorKey = anchor?.dataset.historyKey;
|
|
830
|
+
const anchorY = anchor ? anchor.getBoundingClientRect().top - streamY : 0;
|
|
831
|
+
const folds = [...($('stream-col').querySelectorAll?.('details') || [])].map(node => node.open);
|
|
832
|
+
const busyState = state.sessionState;
|
|
833
|
+
resetStream();
|
|
834
|
+
state.follow = false;
|
|
835
|
+
state.replaying = true;
|
|
836
|
+
try { for (const event of events) renderEvent(event); }
|
|
837
|
+
finally { state.replaying = false; state.follow = following; state.scrollIntent = intent; }
|
|
838
|
+
const rebuiltFolds = [...($('stream-col').querySelectorAll?.('details') || [])];
|
|
839
|
+
const offset = rebuiltFolds.length - folds.length;
|
|
840
|
+
if (offset >= 0) for (let i = 0; i < folds.length; i++) rebuiltFolds[offset + i].open = folds[i];
|
|
841
|
+
if (busyState) setBusyState(busyState);
|
|
842
|
+
const restoredAnchor = anchorKey && [...($('stream-col').querySelectorAll?.('.msg.user') || [])].find(node => node.dataset.historyKey === anchorKey);
|
|
843
|
+
stream.scrollTop = following ? stream.scrollHeight : restoredAnchor
|
|
844
|
+
? stream.scrollTop + restoredAnchor.getBoundingClientRect().top - streamY - anchorY
|
|
845
|
+
: top + (prepend ? stream.scrollHeight - height : 0);
|
|
846
|
+
view.events = events;
|
|
847
|
+
renderHistoryBar();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function loadEarlierHistory() {
|
|
851
|
+
const view = historyView;
|
|
852
|
+
if (!view || view.loading) return;
|
|
853
|
+
if (view.error) return reloadHistory(view);
|
|
854
|
+
if (!view.nextCursor) return;
|
|
855
|
+
view.loading = true; renderHistoryBar();
|
|
856
|
+
const revision = ++view.revision;
|
|
857
|
+
try {
|
|
858
|
+
const page = await client.loadHistory(view.sessionId, { before: view.nextCursor });
|
|
859
|
+
if (historyView !== view || view.epoch !== sessionEpoch || revision !== view.revision) return;
|
|
860
|
+
view.nextCursor = page.nextCursor;
|
|
861
|
+
view.incomplete = page.incomplete;
|
|
862
|
+
view.error = false;
|
|
863
|
+
rebuildHistory(view, [...page.events, ...view.events]);
|
|
864
|
+
} catch { if (historyView === view) view.error = true; }
|
|
865
|
+
finally { if (historyView === view && revision === view.revision) { view.loading = false; renderHistoryBar(); } }
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function reloadHistory(view) {
|
|
869
|
+
if (!view || historyView !== view || !view.supported) return;
|
|
870
|
+
const revision = ++view.revision;
|
|
871
|
+
view.loading = true; renderHistoryBar();
|
|
872
|
+
const oldCursor = view.nextCursor;
|
|
873
|
+
const oldest = view.events.find(event => event.type === 'claude/user-prompt') || view.events[0];
|
|
874
|
+
const oldestKey = oldest && historyEventKey(oldest);
|
|
875
|
+
const buffered = view.events;
|
|
876
|
+
const liveStart = buffered.length;
|
|
877
|
+
try {
|
|
878
|
+
let page = await client.loadHistory(view.sessionId);
|
|
879
|
+
let events = page.events, nextCursor = page.nextCursor;
|
|
880
|
+
// Preserve the amount of history the reader already loaded, including a
|
|
881
|
+
// fully-read thread; a reconnect must not silently drop its older pages.
|
|
882
|
+
const seen = new Set();
|
|
883
|
+
while (nextCursor && (!oldCursor || (oldestKey && !events.some(event => historyEventKey(event) === oldestKey)))) {
|
|
884
|
+
if (seen.has(nextCursor)) throw new Error('history cursor did not advance');
|
|
885
|
+
seen.add(nextCursor);
|
|
886
|
+
const older = await client.loadHistory(view.sessionId, { before: nextCursor });
|
|
887
|
+
events = [...older.events, ...events]; nextCursor = older.nextCursor;
|
|
888
|
+
if (historyView !== view || revision !== view.revision) return;
|
|
889
|
+
}
|
|
890
|
+
if (historyView !== view || view.epoch !== sessionEpoch || revision !== view.revision) return;
|
|
891
|
+
const queued = buffered.slice(liveStart).filter(event => Number(event.sequence) > page.highWater);
|
|
892
|
+
view.nextCursor = nextCursor; view.incomplete = page.incomplete; view.error = false;
|
|
893
|
+
rebuildHistory(view, [...events, ...queued], false);
|
|
894
|
+
} catch { if (historyView === view) view.error = true; }
|
|
895
|
+
finally { if (historyView === view && revision === view.revision) { view.loading = false; renderHistoryBar(); } }
|
|
896
|
+
}
|
|
897
|
+
|
|
764
898
|
function publishKnowledge(projectId, sessionId, knowledge) {
|
|
765
899
|
if (embedded) window.parent.postMessage({ type: 'kb:knowledge', app: 'agent-terminal', projectId, sessionId, knowledge }, document.referrer ? new URL(document.referrer).origin : location.origin);
|
|
766
900
|
}
|
|
@@ -770,6 +904,8 @@ function detachSession(options = {}) {
|
|
|
770
904
|
if (state.unsub) { try { state.unsub(); } catch {} state.unsub = null; }
|
|
771
905
|
state.activeSessionId = null;
|
|
772
906
|
state.sessionSummary = null;
|
|
907
|
+
historyView = null;
|
|
908
|
+
renderHistoryBar();
|
|
773
909
|
if (!options.preserveAttachments) state.attachments = [];
|
|
774
910
|
$('btn-ctx').hidden = true;
|
|
775
911
|
renderAttachmentTray();
|
|
@@ -790,6 +926,7 @@ function resetStream() {
|
|
|
790
926
|
col.textContent = '';
|
|
791
927
|
turn = null;
|
|
792
928
|
state.follow = true;
|
|
929
|
+
state.scrollIntent = null;
|
|
793
930
|
const jump = $('jump-latest');
|
|
794
931
|
if (jump) jump.hidden = true;
|
|
795
932
|
}
|
|
@@ -956,7 +1093,7 @@ function endTurnVisuals(durationMs) {
|
|
|
956
1093
|
const meta = step.querySelector('.s-meta');
|
|
957
1094
|
if (meta) { meta.classList.remove('running'); meta.textContent = t.interrupted ? '已中断' : '完成'; }
|
|
958
1095
|
}
|
|
959
|
-
t.proc.open = false;
|
|
1096
|
+
if (state.replaying || state.follow) t.proc.open = false;
|
|
960
1097
|
}
|
|
961
1098
|
turn = null;
|
|
962
1099
|
}
|
|
@@ -1038,6 +1175,7 @@ function renderEvent(event) {
|
|
|
1038
1175
|
rememberTitleFromEvents([event]);
|
|
1039
1176
|
if (state.activeSessionId && event.text) state.sessionTitles.set(state.activeSessionId, event.text.replace(/\s+/g, ' ').slice(0, 24));
|
|
1040
1177
|
const wrap = el('div', 'msg user');
|
|
1178
|
+
wrap.dataset.historyKey = historyEventKey(event);
|
|
1041
1179
|
const bubble = el('div', 'bubble', event.text || '');
|
|
1042
1180
|
wrap.append(bubble);
|
|
1043
1181
|
const copy = el('button', 'bubble-copy');
|
|
@@ -1084,7 +1222,8 @@ function renderEvent(event) {
|
|
|
1084
1222
|
if (event.type === 'claude/text-delta') {
|
|
1085
1223
|
ctx.liveRaw += event.text || '';
|
|
1086
1224
|
ctx.liveText._rawFull = ctx.liveRaw;
|
|
1087
|
-
ctx.liveText._pacer.
|
|
1225
|
+
if (state.replaying) ctx.liveText._pacer.finish(ctx.liveRaw);
|
|
1226
|
+
else ctx.liveText._pacer.push(ctx.liveRaw);
|
|
1088
1227
|
}
|
|
1089
1228
|
break;
|
|
1090
1229
|
}
|
|
@@ -1678,6 +1817,7 @@ function fmtTokens(value) {
|
|
|
1678
1817
|
/* ===================== busy / activity / context ===================== */
|
|
1679
1818
|
|
|
1680
1819
|
function setBusyState(sessionState) {
|
|
1820
|
+
state.sessionState = sessionState;
|
|
1681
1821
|
state.busy = ['running', 'thinking', 'tool-running', 'spawning', 'pending-permission'].includes(sessionState);
|
|
1682
1822
|
const status = $('session-status');
|
|
1683
1823
|
const send = $('btn-send');
|
|
@@ -1841,6 +1981,10 @@ async function sendCurrent() {
|
|
|
1841
1981
|
renderAttachmentTray();
|
|
1842
1982
|
const sentSessionId = state.activeSessionId, sentProjectId = state.activeProjectId;
|
|
1843
1983
|
rememberProjectDraft(sentProjectId, { text: '', attachments: [], sessionId: sentSessionId });
|
|
1984
|
+
// Engage before awaiting the backend: output can arrive while send is still
|
|
1985
|
+
// pending, and its eventual acknowledgement must not undo an upward scroll.
|
|
1986
|
+
state.follow = true;
|
|
1987
|
+
state.scrollIntent = null;
|
|
1844
1988
|
try {
|
|
1845
1989
|
const accepted = await client.send({
|
|
1846
1990
|
sessionId: state.activeSessionId,
|
|
@@ -1852,7 +1996,6 @@ async function sendCurrent() {
|
|
|
1852
1996
|
if (accepted?.knowledge && state.activeSessionId === sentSessionId) publishKnowledge(sentProjectId, sentSessionId, accepted.knowledge);
|
|
1853
1997
|
if (state.activeSessionId !== sentSessionId) return;
|
|
1854
1998
|
setBusyState(accepted?.state || 'running');
|
|
1855
|
-
state.follow = true; // sending a message re-engages follow mode
|
|
1856
1999
|
} catch (error) {
|
|
1857
2000
|
if (state.activeProjectId !== sentProjectId || state.activeSessionId !== sentSessionId) {
|
|
1858
2001
|
const draft = projectDrafts.get(sentProjectId) || {};
|
|
@@ -1933,6 +2076,7 @@ function renderModeLabel() {
|
|
|
1933
2076
|
/* ===================== settings ===================== */
|
|
1934
2077
|
|
|
1935
2078
|
function openSettings(tab) {
|
|
2079
|
+
if (tab === 'agents') void refreshAgents();
|
|
1936
2080
|
closePops();
|
|
1937
2081
|
document.querySelectorAll('.stab').forEach(node => node.classList.toggle('active', node.dataset.s === tab));
|
|
1938
2082
|
document.querySelectorAll('.spanel').forEach(node => node.classList.remove('active'));
|
|
@@ -1955,7 +2099,7 @@ function wireSettings() {
|
|
|
1955
2099
|
$(`s-${tab.dataset.s}`).classList.add('active');
|
|
1956
2100
|
});
|
|
1957
2101
|
});
|
|
1958
|
-
$('btn-rescan').addEventListener('click', async () => { await refreshAgents(); toast('
|
|
2102
|
+
$('btn-rescan').addEventListener('click', async () => { await refreshAgents(true); toast(state.agentsError ? '检查未完成,请重试' : '正在重新检查 Agent', state.agentsError); });
|
|
1959
2103
|
$('mgmt-add').addEventListener('click', () => renderProviderDetail(null));
|
|
1960
2104
|
wireCaptureSetting();
|
|
1961
2105
|
}
|
|
@@ -2433,16 +2577,38 @@ function wireChrome() {
|
|
|
2433
2577
|
const stream = $('stream');
|
|
2434
2578
|
const jump = $('jump-latest');
|
|
2435
2579
|
if (stream && stream.addEventListener) {
|
|
2580
|
+
const pauseFollow = () => {
|
|
2581
|
+
state.follow = false;
|
|
2582
|
+
state.scrollIntent = 'up';
|
|
2583
|
+
if (jump) jump.hidden = false;
|
|
2584
|
+
};
|
|
2585
|
+
// Wheel intent precedes the browser's scroll action. Waiting for a 120px
|
|
2586
|
+
// distance lets a fast delta repeatedly win against small wheel steps.
|
|
2587
|
+
stream.addEventListener('wheel', event => {
|
|
2588
|
+
if (event.deltaY < 0) pauseFollow();
|
|
2589
|
+
else if (event.deltaY > 0) state.scrollIntent = 'down';
|
|
2590
|
+
}, { passive: true });
|
|
2591
|
+
stream.addEventListener('keydown', event => {
|
|
2592
|
+
if (['ArrowUp', 'PageUp', 'Home'].includes(event.key) || (event.key === ' ' && event.shiftKey)) pauseFollow();
|
|
2593
|
+
else if (['ArrowDown', 'PageDown', 'End', ' '].includes(event.key)) state.scrollIntent = 'down';
|
|
2594
|
+
});
|
|
2595
|
+
let previousTop = stream.scrollTop;
|
|
2436
2596
|
stream.addEventListener('scroll', () => {
|
|
2437
2597
|
const distance = stream.scrollHeight - stream.scrollTop - stream.clientHeight;
|
|
2438
|
-
if (distance >
|
|
2439
|
-
|
|
2598
|
+
if (stream.scrollTop < previousTop - 1 && distance > 2) state.follow = false;
|
|
2599
|
+
if (distance > 2) state.follow = false;
|
|
2600
|
+
else if (state.scrollIntent !== 'up') state.follow = true;
|
|
2601
|
+
previousTop = stream.scrollTop;
|
|
2440
2602
|
if (jump) jump.hidden = state.follow;
|
|
2603
|
+
if (stream.scrollTop < 80 && historyView?.nextCursor && !historyView.loading) void loadEarlierHistory();
|
|
2441
2604
|
});
|
|
2442
2605
|
}
|
|
2606
|
+
const historyMore = $('history-more');
|
|
2607
|
+
if (historyMore) historyMore.addEventListener('click', loadEarlierHistory);
|
|
2443
2608
|
if (jump) {
|
|
2444
2609
|
jump.addEventListener('click', () => {
|
|
2445
2610
|
state.follow = true;
|
|
2611
|
+
state.scrollIntent = null;
|
|
2446
2612
|
jump.hidden = true;
|
|
2447
2613
|
if (stream) stream.scrollTop = stream.scrollHeight;
|
|
2448
2614
|
});
|
|
@@ -2474,6 +2640,28 @@ function embedAllowedOrigin(origin) {
|
|
|
2474
2640
|
return origin === (document.referrer ? new URL(document.referrer).origin : location.origin);
|
|
2475
2641
|
} catch { return false; }
|
|
2476
2642
|
}
|
|
2643
|
+
let visibleRefreshJob = null, lastVisibleRefresh = 0, embedVisible = false;
|
|
2644
|
+
function refreshVisibleState() {
|
|
2645
|
+
if (visibleRefreshJob) return visibleRefreshJob;
|
|
2646
|
+
if (Date.now() - lastVisibleRefresh < 1000) return Promise.resolve();
|
|
2647
|
+
const project = state.activeProjectId, session = state.activeSessionId, epoch = sessionEpoch;
|
|
2648
|
+
visibleRefreshJob = (async () => {
|
|
2649
|
+
await Promise.allSettled([refreshAgents(), refreshProfiles(), refreshProjects(true)]);
|
|
2650
|
+
if (epoch !== sessionEpoch || project !== state.activeProjectId || session !== state.activeSessionId || !session) return;
|
|
2651
|
+
try {
|
|
2652
|
+
if (historyView?.supported) await reloadHistory(historyView);
|
|
2653
|
+
const revision = state.liveRevision;
|
|
2654
|
+
const snapshot = await client.loadSession(session);
|
|
2655
|
+
if (epoch !== sessionEpoch || session !== state.activeSessionId || revision !== state.liveRevision) return;
|
|
2656
|
+
state.sessionSummary = snapshot.session;
|
|
2657
|
+
setBusyState(snapshot.session.state);
|
|
2658
|
+
} catch { /* Stream stays alive; never erase an active conversation on a failed read. */ }
|
|
2659
|
+
})().finally(() => { visibleRefreshJob = null; lastVisibleRefresh = Date.now(); });
|
|
2660
|
+
return visibleRefreshJob;
|
|
2661
|
+
}
|
|
2662
|
+
window.addEventListener('focus', () => { void refreshVisibleState(); });
|
|
2663
|
+
window.addEventListener('online', () => { lastVisibleRefresh = 0; void refreshVisibleState(); });
|
|
2664
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) void refreshVisibleState(); });
|
|
2477
2665
|
function setEmbedSidebarVisible(visible) {
|
|
2478
2666
|
document.body.classList.toggle('embed-collapse', !visible);
|
|
2479
2667
|
}
|
|
@@ -2518,6 +2706,16 @@ window.addEventListener('message', async ev => {
|
|
|
2518
2706
|
if (!data || typeof data !== 'object') return;
|
|
2519
2707
|
const reply = payload => { if (ev.source) ev.source.postMessage(Object.assign({ app: 'agent-terminal', requestId: data.requestId }, payload), ev.origin); };
|
|
2520
2708
|
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; }
|
|
2709
|
+
if (data.type === 'kb:visibility' || data.type === 'kb:refresh') {
|
|
2710
|
+
if (data.type === 'kb:visibility') {
|
|
2711
|
+
const wasVisible = embedVisible; embedVisible = data.visible === true;
|
|
2712
|
+
if (!embedVisible || wasVisible) return;
|
|
2713
|
+
}
|
|
2714
|
+
const project = state.activeProjectId, session = state.activeSessionId, epoch = selectionEpoch;
|
|
2715
|
+
await refreshVisibleState();
|
|
2716
|
+
if (epoch === selectionEpoch && project === state.activeProjectId && session === state.activeSessionId) reply({ type: 'kb:refreshed', project, sessionId: session, ok: !state.agentsError });
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2521
2719
|
if (data.type === 'kb:open-settings') { openSettings('providers'); return; }
|
|
2522
2720
|
if (data.type === 'kb:sidebar') setEmbedSidebarVisible(!!data.visible);
|
|
2523
2721
|
else if (data.type === 'kb:select-project') {
|
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
<button class="ghost" id="btn-top-settings" title="设置(模型供应商)"><svg class="ics" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
|
|
41
41
|
</div>
|
|
42
42
|
|
|
43
|
-
<div class="stream" id="stream">
|
|
43
|
+
<div class="stream" id="stream" tabindex="0" aria-label="会话记录">
|
|
44
|
+
<div class="history-bar" id="history-bar" hidden>
|
|
45
|
+
<button class="linklike" id="history-more" type="button">加载更早记录</button>
|
|
46
|
+
<span id="history-status" role="status"></span>
|
|
47
|
+
</div>
|
|
44
48
|
<div class="col" id="stream-col">
|
|
45
49
|
<div class="empty-hint" id="empty-hint">
|
|
46
50
|
<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>
|
package/docs/sdk/openapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
3
|
title: Claude AI Workbench API
|
|
4
|
-
version: 0.13.2-rc.
|
|
4
|
+
version: 0.13.2-rc.15
|
|
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
|
|
@@ -48,6 +48,13 @@ paths:
|
|
|
48
48
|
agents:
|
|
49
49
|
type: array
|
|
50
50
|
items: { $ref: "#/components/schemas/AgentInfo" }
|
|
51
|
+
/agents/refresh:
|
|
52
|
+
post:
|
|
53
|
+
tags: [Agents]
|
|
54
|
+
operationId: refreshAgents
|
|
55
|
+
responses:
|
|
56
|
+
"202":
|
|
57
|
+
description: Background scan started or reused; previous agents remain available with scan state and generation.
|
|
51
58
|
/projects:
|
|
52
59
|
get:
|
|
53
60
|
tags: [Projects]
|
|
@@ -311,6 +318,37 @@ paths:
|
|
|
311
318
|
responses:
|
|
312
319
|
"200": { description: Session snapshot }
|
|
313
320
|
"404": { $ref: "#/components/responses/Error" }
|
|
321
|
+
/sessions/{sessionId}/history:
|
|
322
|
+
parameters:
|
|
323
|
+
- $ref: "#/components/parameters/SessionId"
|
|
324
|
+
get:
|
|
325
|
+
tags: [Sessions]
|
|
326
|
+
operationId: getSessionHistory
|
|
327
|
+
description: Durable conversation history, separate from the bounded SSE replay cache. Pages contain whole turns in chronological order.
|
|
328
|
+
parameters:
|
|
329
|
+
- in: query
|
|
330
|
+
name: before
|
|
331
|
+
description: Cursor returned by the previous page. Omit for recent turns.
|
|
332
|
+
schema: { type: string }
|
|
333
|
+
- in: query
|
|
334
|
+
name: limit
|
|
335
|
+
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
|
336
|
+
responses:
|
|
337
|
+
"200":
|
|
338
|
+
description: History page; incomplete is true when an earlier prefix cannot be recovered.
|
|
339
|
+
content:
|
|
340
|
+
application/json:
|
|
341
|
+
schema:
|
|
342
|
+
type: object
|
|
343
|
+
required: [events, nextCursor, incomplete, highWater]
|
|
344
|
+
properties:
|
|
345
|
+
events: { type: array, items: { type: object } }
|
|
346
|
+
nextCursor: { type: string, nullable: true }
|
|
347
|
+
incomplete: { type: boolean }
|
|
348
|
+
highWater: { type: integer, minimum: 0 }
|
|
349
|
+
"400": { $ref: "#/components/responses/Error" }
|
|
350
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
351
|
+
"503": { $ref: "#/components/responses/Error" }
|
|
314
352
|
/sessions/{sessionId}/events:
|
|
315
353
|
parameters:
|
|
316
354
|
- $ref: "#/components/parameters/SessionId"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/claude-ai-workbench",
|
|
3
|
-
"version": "0.13.2-rc.
|
|
3
|
+
"version": "0.13.2-rc.15",
|
|
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",
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
AttachmentInput,
|
|
16
16
|
SlashCommand,
|
|
17
17
|
SessionSnapshot,
|
|
18
|
+
SessionHistoryPage,
|
|
18
19
|
SessionSummary,
|
|
19
20
|
WorkbenchContext,
|
|
20
21
|
WorkbenchEvent,
|
|
@@ -41,12 +42,15 @@ export interface ClaudeWorkbenchClientOptions {
|
|
|
41
42
|
apiBase?: string;
|
|
42
43
|
fetch?: typeof fetch;
|
|
43
44
|
EventSource?: typeof EventSource;
|
|
45
|
+
readTimeoutMs?: number;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export interface ClaudeWorkbenchClient {
|
|
47
49
|
health(): Promise<HealthResult>;
|
|
48
50
|
runtime(): Promise<RuntimeInfo>;
|
|
49
51
|
listAgents(): Promise<AgentStatus[]>;
|
|
52
|
+
agentStatus(): Promise<{ agents: AgentStatus[]; scan?: { state: string; generation: number; updatedAt: string | null } }>;
|
|
53
|
+
refreshAgents(): ReturnType<ClaudeWorkbenchClient['agentStatus']>;
|
|
50
54
|
listProjects(): Promise<Project[]>;
|
|
51
55
|
createProject(project: ProjectInput): Promise<Project>;
|
|
52
56
|
getProject(id: string): Promise<Project>;
|
|
@@ -62,6 +66,7 @@ export interface ClaudeWorkbenchClient {
|
|
|
62
66
|
listSessions(context: WorkbenchContext | string, options?: ListSessionsOptions): Promise<SessionSummary[]>;
|
|
63
67
|
startSession(context: WorkbenchContext, options?: StartSessionOptions): Promise<SessionSummary>;
|
|
64
68
|
loadSession(id: string, options?: RequestOptions): Promise<SessionSnapshot>;
|
|
69
|
+
loadHistory(id: string, options?: RequestOptions & { before?: string; limit?: number }): Promise<SessionHistoryPage>;
|
|
65
70
|
listCommands(id: string, options?: RequestOptions): Promise<SlashCommand[]>;
|
|
66
71
|
subscribe(id: string, handlers: StreamHandlers | ((event: WorkbenchEvent) => void), onError?: (error: Error) => void, options?: {afterSequence?: number}): () => void;
|
|
67
72
|
send(sessionId: string, text: string, options?: SendOptions): Promise<unknown>;
|