@steerable/agent-shell-web 0.6.18 → 0.6.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.18",
3
+ "version": "0.6.19",
4
4
  "type": "module",
5
5
  "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/ + vite.base.ts), not a build artifact.",
6
6
  "license": "Apache-2.0",
@@ -39,8 +39,8 @@
39
39
  "react-router-dom": "^6.30.0",
40
40
  "rehype-raw": "^7.0.0",
41
41
  "remark-gfm": "^4.0.1",
42
- "@steerable/agent-protocol": "0.6.18",
43
- "@steerable/agent-ui": "0.6.18"
42
+ "@steerable/agent-protocol": "0.6.19",
43
+ "@steerable/agent-ui": "0.6.19"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@tailwindcss/vite": "^4.3.0",
@@ -53,7 +53,7 @@
53
53
  "typescript": "^5.8.3",
54
54
  "vite": "^5.4.21",
55
55
  "vitest": "^3.2.4",
56
- "@steerable/pack-sdk": "0.6.18"
56
+ "@steerable/pack-sdk": "0.6.19"
57
57
  },
58
58
  "scripts": {
59
59
  "gen": "echo agent-shell-web",
@@ -46,12 +46,12 @@ function installBridge(tasks: LocalTask[]) {
46
46
  return Promise.reject(new Error(`unexpected request: ${input.method} ${input.path}`));
47
47
  });
48
48
  const taskUpdatedListeners: Array<
49
- (payload: { chatId: string; taskId: string; status: string }) => void
49
+ (payload: { chatId: string; taskId: string }) => void
50
50
  > = [];
51
51
  (window as { electron?: unknown }).electron = {
52
52
  localBackend: { request },
53
53
  onTaskUpdated: (
54
- cb: (payload: { chatId: string; taskId: string; status: string }) => void,
54
+ cb: (payload: { chatId: string; taskId: string }) => void,
55
55
  ) => {
56
56
  taskUpdatedListeners.push(cb);
57
57
  return () => {
@@ -62,7 +62,7 @@ function installBridge(tasks: LocalTask[]) {
62
62
  };
63
63
  return {
64
64
  request,
65
- emitTaskUpdated: (payload: { chatId: string; taskId: string; status: string }) => {
65
+ emitTaskUpdated: (payload: { chatId: string; taskId: string }) => {
66
66
  for (const cb of taskUpdatedListeners) cb(payload);
67
67
  },
68
68
  };
@@ -165,8 +165,8 @@ describe('TaskPanelModal', () => {
165
165
  ([input]) => (input as { method: string }).method === 'GET',
166
166
  ).length;
167
167
 
168
- emitTaskUpdated({ chatId: 'chat_2', taskId: 'x', status: 'completed' });
169
- emitTaskUpdated({ chatId: 'chat_1', taskId: 'task-1', status: 'completed' });
168
+ emitTaskUpdated({ chatId: 'chat_2', taskId: 'x' });
169
+ emitTaskUpdated({ chatId: 'chat_1', taskId: 'task-1' });
170
170
 
171
171
  await waitFor(() => {
172
172
  const gets = request.mock.calls.filter(
@@ -34,7 +34,7 @@ function makeTask(overrides: Partial<LocalTask> = {}): LocalTask {
34
34
 
35
35
  function installBridge(initial: LocalTask[]) {
36
36
  let current = initial;
37
- const listeners: Array<(p: { chatId: string; taskId: string; status: string }) => void> = [];
37
+ const listeners: Array<(p: { chatId: string; taskId: string }) => void> = [];
38
38
  (window as { electron?: unknown }).electron = {
39
39
  localBackend: {
40
40
  request: vi.fn((input: { method: string; path: string }) => {
@@ -44,7 +44,7 @@ function installBridge(initial: LocalTask[]) {
44
44
  return Promise.reject(new Error(`unexpected request: ${input.path}`));
45
45
  }),
46
46
  },
47
- onTaskUpdated: (cb: (p: { chatId: string; taskId: string; status: string }) => void) => {
47
+ onTaskUpdated: (cb: (p: { chatId: string; taskId: string }) => void) => {
48
48
  listeners.push(cb);
49
49
  return () => listeners.splice(listeners.indexOf(cb), 1);
50
50
  },
@@ -53,7 +53,7 @@ function installBridge(initial: LocalTask[]) {
53
53
  async push(next: LocalTask[]) {
54
54
  current = next;
55
55
  await act(async () => {
56
- for (const cb of listeners) cb({ chatId: 'chat_1', taskId: 'task-1', status: 'x' });
56
+ for (const cb of listeners) cb({ chatId: 'chat_1', taskId: 'task-1' });
57
57
  });
58
58
  },
59
59
  };
@@ -67,10 +67,13 @@ describe('LocalBackendSseAdapter 帧归一化', () => {
67
67
  expect(ofType(events, 'done')).toHaveLength(1);
68
68
  });
69
69
 
70
+ // fixture 与服务端 router.ts onNotice 的真实发射形状一致(budget.kind + message)。
70
71
  it('budget_exhausted 在没有任何内容流过时原样透出、不结束流', () => {
71
72
  const { events, onEvent } = collectEvents();
72
73
  const adapter = new LocalBackendSseAdapter(onEvent);
73
- adapter.feed('data: {"type":"budget_exhausted","message":"token limit"}\n\n');
74
+ adapter.feed(
75
+ 'data: {"type":"budget_exhausted","budget":{"kind":"tokens"},"message":"budget_exhausted: tokens"}\n\n',
76
+ );
74
77
  expect(ofType(events, 'budget_exhausted')).toHaveLength(1);
75
78
  expect(ofType(events, 'done')).toHaveLength(0);
76
79
  });
@@ -79,7 +82,9 @@ describe('LocalBackendSseAdapter 帧归一化', () => {
79
82
  const { events, onEvent } = collectEvents();
80
83
  const adapter = new LocalBackendSseAdapter(onEvent);
81
84
  adapter.feed('data: {"content":"半截回答"}\n\n');
82
- adapter.feed('data: {"type":"budget_exhausted","message":"token limit"}\n\n');
85
+ adapter.feed(
86
+ 'data: {"type":"budget_exhausted","budget":{"kind":"tokens"},"message":"budget_exhausted: tokens"}\n\n',
87
+ );
83
88
  // 原事件被吞,替换为 suppression 标记 + done。
84
89
  expect(ofType(events, 'budget_exhausted')).toHaveLength(0);
85
90
  const suppressed = events.filter(
@@ -20,8 +20,7 @@
20
20
  * - Standard content tokens: `data: {"content":"..."}\n\n`
21
21
  * - User message echo: `data: {"type":"user_message", "message":{...}}\n\n`
22
22
  * - Tool execution summary: `data: {"type":"executed_actions", "actions":[...]}\n\n`
23
- * - Per-round completion: `data: {"type":"completion", "status":"...", "reason":"..."}\n\n`
24
- * - Budget exhausted: `data: {"type":"budget_exhausted", "budget":{...}}\n\n`
23
+ * - Budget exhausted: `data: {"type":"budget_exhausted", "budget":{"kind":"..."}, "message":"..."}\n\n`
25
24
  * - Persisted message id: `data: {"type":"message_id", "messageId":"..."}\n\n`
26
25
  * - Named error event: `event: error\ndata: {"message":"..."}\n\n`
27
26
  * - Stream terminator: `data: [DONE]\n\n`
@@ -164,10 +164,10 @@ export interface ElectronBridge {
164
164
  ) => () => void;
165
165
  /**
166
166
  * 4.6a 后台任务状态推送。任务到达终态或 worktree 合并/丢弃完成时主进程
167
- * 广播;载荷只有 chatId/taskId/status,面板收到后重新拉列表。
167
+ * 广播;载荷只有 chatId/taskId,面板收到后重新拉列表。
168
168
  */
169
169
  onTaskUpdated?: (
170
- callback: (payload: { chatId: string; taskId: string; status: string }) => void,
170
+ callback: (payload: { chatId: string; taskId: string }) => void,
171
171
  ) => () => void;
172
172
  /**
173
173
  * 场景包广播事件订阅(3.1 通用化):按通道名订阅主进程/BS 后端的
@@ -57,7 +57,7 @@ describe('localBackend.request', () => {
57
57
  await bridge.localBackend.request({ method: 'GET', path: '/api/v2/chats' });
58
58
  expect(fetchMock).toHaveBeenCalledWith('/api/v2/chats', {
59
59
  method: 'GET',
60
- headers: undefined,
60
+ headers: {},
61
61
  body: undefined,
62
62
  });
63
63
  });
@@ -227,6 +227,43 @@ describe('startStream / cancelStream', () => {
227
227
  });
228
228
  });
229
229
 
230
+ describe('BS token(bootstrap 注入的 Bearer)', () => {
231
+ const BOOT = { platform: 'darwin', flavor: 'test', brandName: 'T', token: 'tok 1' };
232
+
233
+ it('request 与 startStream 带 Authorization 头', async () => {
234
+ vi.stubGlobal('__DEEPPATH_BS__', BOOT);
235
+ const fetchMock = stubFetch(() => jsonResponse({}));
236
+ const bridge = createHttpBridge();
237
+ await bridge.localBackend.request({ method: 'GET', path: '/api/v2/chats' });
238
+ expect(fetchMock).toHaveBeenCalledWith('/api/v2/chats', {
239
+ method: 'GET',
240
+ headers: { Authorization: 'Bearer tok 1' },
241
+ body: undefined,
242
+ });
243
+ await bridge.localBackend.startStream({ method: 'POST', path: '/x' }, () => {});
244
+ expect(fetchMock).toHaveBeenCalledWith('/x', expect.objectContaining({
245
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer tok 1' },
246
+ }));
247
+ });
248
+
249
+ it('EventSource 不能设头,token 走 query(URL 编码)', () => {
250
+ vi.stubGlobal('__DEEPPATH_BS__', BOOT);
251
+ const urls: string[] = [];
252
+ vi.stubGlobal(
253
+ 'EventSource',
254
+ class {
255
+ constructor(url: string) {
256
+ urls.push(url);
257
+ }
258
+ addEventListener(): void {}
259
+ },
260
+ );
261
+ const bridge = createHttpBridge();
262
+ bridge.onChatCreated?.(() => {});
263
+ expect(urls).toEqual(['/api/v2/events?token=tok%201']);
264
+ });
265
+ });
266
+
230
267
  describe('浏览器形态的 local.* 降级', () => {
231
268
  it('目录选择直接取消(浏览器给不了路径)', async () => {
232
269
  stubFetch(() => jsonResponse({}));
@@ -28,6 +28,8 @@ interface BsBootstrap {
28
28
  platform: NodeJS.Platform;
29
29
  flavor: string;
30
30
  brandName: string;
31
+ /** 每次启动生成的 Bearer token;/api/v2/* 与 /host/* 全部要求携带。 */
32
+ token?: string;
31
33
  }
32
34
 
33
35
  function bootstrap(): BsBootstrap {
@@ -37,10 +39,18 @@ function bootstrap(): BsBootstrap {
37
39
  : { platform: 'linux', flavor: 'generic', brandName: '' };
38
40
  }
39
41
 
42
+ function authHeaders(): Record<string, string> {
43
+ const token = bootstrap().token;
44
+ return token ? { Authorization: `Bearer ${token}` } : {};
45
+ }
46
+
40
47
  async function http<T>(method: string, path: string, body?: unknown): Promise<T> {
41
48
  const res = await fetch(path, {
42
49
  method,
43
- headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
50
+ headers: {
51
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
52
+ ...authHeaders(),
53
+ },
44
54
  body: body !== undefined ? JSON.stringify(body) : undefined,
45
55
  });
46
56
  const data = (await res.json().catch(() => null)) as T & { detail?: string };
@@ -63,7 +73,11 @@ let eventSource: EventSource | null = null;
63
73
 
64
74
  function ensureEventSource(): EventSource {
65
75
  if (eventSource) return eventSource;
66
- eventSource = new EventSource('/api/v2/events');
76
+ // EventSource 不能设请求头,token 走 query(server 端两种都收)。
77
+ const token = bootstrap().token;
78
+ eventSource = new EventSource(
79
+ `/api/v2/events${token ? `?token=${encodeURIComponent(token)}` : ''}`,
80
+ );
67
81
  return eventSource;
68
82
  }
69
83
 
@@ -112,7 +126,7 @@ async function startStream(
112
126
  try {
113
127
  res = await fetch(input.path, {
114
128
  method: input.method,
115
- headers: { 'Content-Type': 'application/json' },
129
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
116
130
  body: input.body !== undefined ? JSON.stringify(input.body) : undefined,
117
131
  signal: controller.signal,
118
132
  });
@@ -231,7 +245,7 @@ export function createHttpBridge(): ElectronBridge {
231
245
  subscribeChannel<{ chatId: string; agentId?: string | null }>('chat-created', callback),
232
246
 
233
247
  onTaskUpdated: (callback) =>
234
- subscribeChannel<{ chatId: string; taskId: string; status: string }>(
248
+ subscribeChannel<{ chatId: string; taskId: string }>(
235
249
  'task-updated',
236
250
  callback,
237
251
  ),