@nextclaw/ui 0.9.10 → 0.9.12

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/assets/{ChannelsList-BgJbR6E9.js → ChannelsList-CJy2GG1a.js} +1 -1
  3. package/dist/assets/ChatPage-C7WxI8VY.js +41 -0
  4. package/dist/assets/{DocBrowser-Dw9BGO1m.js → DocBrowser-Nu-ae-eS.js} +1 -1
  5. package/dist/assets/{LogoBadge-CLc2B6st.js → LogoBadge-DbbMxPlr.js} +1 -1
  6. package/dist/assets/{MarketplacePage-ChqCNL7k.js → MarketplacePage-BQYQPeg2.js} +2 -2
  7. package/dist/assets/{McpMarketplacePage-B3PF-7ED.js → McpMarketplacePage-kiMJbS8r.js} +1 -1
  8. package/dist/assets/{ModelConfig-Dqz_NOow.js → ModelConfig-DRQ07Snj.js} +1 -1
  9. package/dist/assets/{ProvidersList-D2WaZShJ.js → ProvidersList-C0NjzKX1.js} +1 -1
  10. package/dist/assets/RemoteAccessPage-DVJ5hBNJ.js +1 -0
  11. package/dist/assets/{RuntimeConfig-TDxQLuGy.js → RuntimeConfig-BkYWyRW7.js} +1 -1
  12. package/dist/assets/{SearchConfig-gba64nGn.js → SearchConfig-DZTW8Wnq.js} +1 -1
  13. package/dist/assets/{SecretsConfig-DpL8wgly.js → SecretsConfig-WMcwg5KV.js} +2 -2
  14. package/dist/assets/{SessionsConfig-CAODVTNW.js → SessionsConfig-CWtCXQRn.js} +1 -1
  15. package/dist/assets/{chat-message-CSG50nNb.js → chat-message-BcjCODYN.js} +1 -1
  16. package/dist/assets/index-BOhlxC12.js +8 -0
  17. package/dist/assets/{index-DfEAJJsA.css → index-SGSkQCPi.css} +1 -1
  18. package/dist/assets/{label-3T28q3PJ.js → label-DOWMfYPL.js} +1 -1
  19. package/dist/assets/{page-layout-BrXOQeua.js → page-layout-DQtmTgqR.js} +1 -1
  20. package/dist/assets/popover-k11l1-ko.js +1 -0
  21. package/dist/assets/{security-config-oGAhN4Zf.js → security-config-FFy-bOJb.js} +1 -1
  22. package/dist/assets/skeleton-DQ4QRdSe.js +1 -0
  23. package/dist/assets/{status-dot-QL3hmT1d.js → status-dot-CsZRxe8p.js} +1 -1
  24. package/dist/assets/{switch-Dbt2kUg2.js → switch-DfMy8G96.js} +1 -1
  25. package/dist/assets/{tabs-custom-y5hdkzXk.js → tabs-custom-CITPDGXY.js} +1 -1
  26. package/dist/assets/{useConfirmDialog-B4zwBVbl.js → useConfirmDialog-Dr39o-0I.js} +1 -1
  27. package/dist/assets/{vendor-CmQZsDAE.js → vendor-TJ2hy_Lv.js} +87 -82
  28. package/dist/index.html +3 -3
  29. package/package.json +3 -3
  30. package/src/account/managers/account.manager.ts +8 -1
  31. package/src/account/stores/account.store.ts +3 -0
  32. package/src/api/api-base.ts +16 -0
  33. package/src/api/client.test.ts +69 -0
  34. package/src/api/client.ts +29 -87
  35. package/src/api/config.stream.test.ts +115 -0
  36. package/src/api/config.ts +50 -125
  37. package/src/api/raw-client.ts +87 -0
  38. package/src/components/chat/ChatSidebar.test.tsx +134 -1
  39. package/src/components/chat/ChatSidebar.tsx +87 -37
  40. package/src/components/chat/chat-session-label.service.ts +34 -0
  41. package/src/components/chat/chat-sidebar-session-item.tsx +147 -0
  42. package/src/components/chat/ncp/NcpChatPage.tsx +3 -10
  43. package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +69 -0
  44. package/src/components/chat/ncp/ncp-app-client-fetch.ts +127 -0
  45. package/src/components/remote/RemoteAccessPage.test.tsx +103 -0
  46. package/src/components/remote/RemoteAccessPage.tsx +28 -93
  47. package/src/hooks/use-realtime-query-bridge.ts +77 -71
  48. package/src/lib/i18n.remote.ts +20 -8
  49. package/src/remote/managers/remote-access.manager.ts +13 -0
  50. package/src/remote/remote-access-feedback.service.test.ts +75 -0
  51. package/src/remote/remote-access-feedback.service.ts +195 -0
  52. package/src/transport/app-client.ts +1 -1
  53. package/src/transport/local.transport.ts +8 -125
  54. package/src/transport/remote.transport.ts +44 -74
  55. package/src/transport/sse-stream.ts +114 -0
  56. package/src/transport/transport-websocket-url.ts +24 -0
  57. package/dist/assets/ChatPage-Bv9UJPse.js +0 -38
  58. package/dist/assets/RemoteAccessPage-D_l9irp4.js +0 -1
  59. package/dist/assets/index-DaEflNCE.js +0 -8
  60. package/dist/assets/popover-BrBJjElY.js +0 -1
  61. package/dist/assets/skeleton-CIPQUKo2.js +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-DaEflNCE.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-CmQZsDAE.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-DfEAJJsA.css">
9
+ <script type="module" crossorigin src="/assets/index-BOhlxC12.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-TJ2hy_Lv.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-SGSkQCPi.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,11 +27,11 @@
27
27
  "tailwind-merge": "^2.5.4",
28
28
  "zod": "^3.23.8",
29
29
  "zustand": "^5.0.2",
30
- "@nextclaw/agent-chat": "0.1.1",
30
+ "@nextclaw/ncp": "0.3.1",
31
31
  "@nextclaw/ncp-http-agent-client": "0.3.1",
32
32
  "@nextclaw/agent-chat-ui": "0.2.1",
33
33
  "@nextclaw/ncp-react": "0.3.2",
34
- "@nextclaw/ncp": "0.3.1"
34
+ "@nextclaw/agent-chat": "0.1.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@testing-library/react": "^16.3.0",
@@ -51,9 +51,16 @@ export class AccountManager {
51
51
  return false;
52
52
  };
53
53
 
54
- startBrowserSignIn = async (params?: { apiBase?: string; status?: RemoteAccessView }) => {
54
+ startBrowserSignIn = async (params?: {
55
+ apiBase?: string;
56
+ status?: RemoteAccessView;
57
+ pendingAction?: AccountPendingAction;
58
+ }) => {
55
59
  try {
56
60
  const status = params?.status ?? (await ensureRemoteStatus());
61
+ if (params?.pendingAction) {
62
+ useAccountStore.getState().setPendingAction(params.pendingAction);
63
+ }
57
64
  const result = await startRemoteBrowserAuth({
58
65
  apiBase: resolveRemotePlatformApiBase(status, params?.apiBase)
59
66
  });
@@ -4,6 +4,9 @@ export type AccountPendingAction =
4
4
  | {
5
5
  type: 'enable-remote';
6
6
  }
7
+ | {
8
+ type: 'repair-remote';
9
+ }
7
10
  | null;
8
11
 
9
12
  type AccountStoreState = {
@@ -0,0 +1,16 @@
1
+ const DEFAULT_API_BASE = 'http://127.0.0.1:55667';
2
+
3
+ export const API_BASE = (() => {
4
+ const envBase = import.meta.env.VITE_API_BASE?.trim();
5
+ if (envBase) {
6
+ return envBase.replace(/\/$/, '');
7
+ }
8
+ if (typeof window !== 'undefined' && window.location?.origin) {
9
+ return window.location.origin;
10
+ }
11
+ return DEFAULT_API_BASE;
12
+ })();
13
+
14
+ if (import.meta.env.DEV && !import.meta.env.VITE_API_BASE) {
15
+ console.warn('VITE_API_BASE is not set; falling back to window origin.');
16
+ }
@@ -0,0 +1,69 @@
1
+ import { api, requestApiResponse } from '@/api/client';
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ request: vi.fn()
5
+ }));
6
+
7
+ vi.mock('@/transport', () => ({
8
+ appClient: {
9
+ request: mocks.request
10
+ }
11
+ }));
12
+
13
+ describe('api/client', () => {
14
+ beforeEach(() => {
15
+ mocks.request.mockReset();
16
+ });
17
+
18
+ it('routes GET requests through appClient.request', async () => {
19
+ mocks.request.mockResolvedValue({ ok: true });
20
+
21
+ const response = await api.get<{ ok: boolean }>('/api/config');
22
+
23
+ expect(mocks.request).toHaveBeenCalledWith({
24
+ method: 'GET',
25
+ path: '/api/config'
26
+ });
27
+ expect(response).toEqual({
28
+ ok: true,
29
+ data: { ok: true }
30
+ });
31
+ });
32
+
33
+ it('parses JSON request bodies before sending to appClient.request', async () => {
34
+ mocks.request.mockResolvedValue({ success: true });
35
+
36
+ const response = await requestApiResponse<{ success: boolean }>('/api/auth/login', {
37
+ method: 'POST',
38
+ body: JSON.stringify({ password: 'secret' })
39
+ });
40
+
41
+ expect(mocks.request).toHaveBeenCalledWith({
42
+ method: 'POST',
43
+ path: '/api/auth/login',
44
+ body: { password: 'secret' }
45
+ });
46
+ expect(response).toEqual({
47
+ ok: true,
48
+ data: { success: true }
49
+ });
50
+ });
51
+
52
+ it('wraps transport failures as ApiResponse errors', async () => {
53
+ mocks.request.mockRejectedValue(new Error('Invalid token'));
54
+
55
+ const response = await api.get('/api/auth/status');
56
+
57
+ expect(response).toEqual({
58
+ ok: false,
59
+ error: {
60
+ code: 'REQUEST_FAILED',
61
+ message: 'Invalid token',
62
+ details: {
63
+ method: 'GET',
64
+ endpoint: '/api/auth/status'
65
+ }
66
+ }
67
+ });
68
+ });
69
+ });
package/src/api/client.ts CHANGED
@@ -1,106 +1,34 @@
1
+ import { appClient } from '@/transport';
1
2
  import type { ApiResponse } from './types';
2
3
 
3
- const DEFAULT_API_BASE = 'http://127.0.0.1:55667';
4
- const API_BASE = (() => {
5
- const envBase = import.meta.env.VITE_API_BASE?.trim();
6
- if (envBase) {
7
- return envBase.replace(/\/$/, '');
8
- }
9
- if (typeof window !== 'undefined' && window.location?.origin) {
10
- return window.location.origin;
11
- }
12
- return DEFAULT_API_BASE;
13
- })();
14
-
15
- if (import.meta.env.DEV && !import.meta.env.VITE_API_BASE) {
16
- console.warn('VITE_API_BASE is not set; falling back to window origin.');
17
- }
18
-
19
- export { API_BASE };
20
-
21
- function compactSnippet(text: string): string {
22
- return text.replace(/\s+/g, ' ').trim().slice(0, 200);
23
- }
24
-
25
- function inferNonJsonHint(endpoint: string, status: number): string | undefined {
26
- if (
27
- status === 404 &&
28
- endpoint.startsWith('/api/config/providers/') &&
29
- endpoint.endsWith('/test')
30
- ) {
31
- return 'Provider test endpoint is missing. This usually means nextclaw runtime version is outdated.';
32
- }
33
- if (status === 401 || status === 403) {
34
- return 'Authentication failed. Check apiKey and custom headers.';
35
- }
36
- if (status === 429) {
37
- return 'Rate limited by upstream provider. Retry later or switch model/provider.';
38
- }
39
- if (status >= 500) {
40
- return 'Upstream service error. Retry later and inspect server logs if it persists.';
41
- }
42
- return undefined;
43
- }
44
-
45
4
  export async function requestApiResponse<T>(
46
5
  endpoint: string,
47
6
  options: RequestInit = {}
48
7
  ): Promise<ApiResponse<T>> {
49
- const url = `${API_BASE}${endpoint}`;
50
8
  const method = (options.method || 'GET').toUpperCase();
51
-
52
- const response = await fetch(url, {
53
- credentials: 'include',
54
- headers: {
55
- 'Content-Type': 'application/json',
56
- ...options.headers
57
- },
58
- ...options
59
- });
60
-
61
- const text = await response.text();
62
- let data: ApiResponse<T> | null = null;
63
- if (text) {
64
- try {
65
- data = JSON.parse(text) as ApiResponse<T>;
66
- } catch {
67
- // fall through to build a synthetic error response
68
- }
69
- }
70
-
71
- if (!data) {
72
- const snippet = text ? compactSnippet(text) : '';
73
- const hint = inferNonJsonHint(endpoint, response.status);
74
- const parts = [`Non-JSON response (${response.status} ${response.statusText}) on ${method} ${endpoint}`];
75
- if (snippet) {
76
- parts.push(`body=${snippet}`);
77
- }
78
- if (hint) {
79
- parts.push(`hint=${hint}`);
80
- }
9
+ try {
10
+ const data = await appClient.request<T>({
11
+ method: method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
12
+ path: endpoint,
13
+ ...(options.body !== undefined ? { body: parseRequestBody(options.body) } : {})
14
+ });
15
+ return {
16
+ ok: true,
17
+ data
18
+ };
19
+ } catch (error) {
81
20
  return {
82
21
  ok: false,
83
22
  error: {
84
- code: 'INVALID_RESPONSE',
85
- message: parts.join(' | '),
23
+ code: 'REQUEST_FAILED',
24
+ message: error instanceof Error ? error.message : String(error),
86
25
  details: {
87
- status: response.status,
88
- statusText: response.statusText,
89
26
  method,
90
- endpoint,
91
- url,
92
- bodySnippet: snippet || undefined,
93
- hint
27
+ endpoint
94
28
  }
95
29
  }
96
30
  };
97
31
  }
98
-
99
- if (!response.ok) {
100
- return data as ApiResponse<T>;
101
- }
102
-
103
- return data as ApiResponse<T>;
104
32
  }
105
33
 
106
34
  export const api = {
@@ -120,3 +48,17 @@ export const api = {
120
48
  method: 'DELETE'
121
49
  })
122
50
  };
51
+
52
+ function parseRequestBody(body: BodyInit | null | undefined): unknown {
53
+ if (body === undefined || body === null) {
54
+ return undefined;
55
+ }
56
+ if (typeof body === 'string') {
57
+ try {
58
+ return JSON.parse(body);
59
+ } catch {
60
+ return body;
61
+ }
62
+ }
63
+ return body;
64
+ }
@@ -0,0 +1,115 @@
1
+ import { sendChatTurnStream, streamChatRun } from '@/api/config';
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ request: vi.fn(),
5
+ openStream: vi.fn()
6
+ }));
7
+
8
+ vi.mock('@/transport', () => ({
9
+ appClient: {
10
+ request: mocks.request,
11
+ openStream: mocks.openStream
12
+ }
13
+ }));
14
+
15
+ describe('api/config stream routing', () => {
16
+ beforeEach(() => {
17
+ mocks.request.mockReset();
18
+ mocks.openStream.mockReset();
19
+ });
20
+
21
+ it('routes sendChatTurnStream through appClient.openStream', async () => {
22
+ const onReady = vi.fn();
23
+ const onDelta = vi.fn();
24
+ const onSessionEvent = vi.fn();
25
+
26
+ mocks.openStream.mockImplementation(({ onEvent }) => {
27
+ onEvent({ name: 'ready', payload: { sessionKey: 's1' } });
28
+ onEvent({ name: 'delta', payload: { delta: 'hello' } });
29
+ onEvent({ name: 'session_event', payload: { type: 'session.updated' } });
30
+ onEvent({ name: 'final', payload: { sessionKey: 's1', reply: 'hello world' } });
31
+ return {
32
+ finished: Promise.resolve({ sessionKey: 's1', reply: 'hello world' }),
33
+ cancel: vi.fn()
34
+ };
35
+ });
36
+
37
+ const result = await sendChatTurnStream(
38
+ { message: 'hi' } as never,
39
+ { onReady, onDelta, onSessionEvent }
40
+ );
41
+
42
+ expect(mocks.openStream).toHaveBeenCalledWith({
43
+ method: 'POST',
44
+ path: '/api/chat/turn/stream',
45
+ body: { message: 'hi' },
46
+ signal: undefined,
47
+ onEvent: expect.any(Function)
48
+ });
49
+ expect(onReady).toHaveBeenCalledWith({ sessionKey: 's1' });
50
+ expect(onDelta).toHaveBeenCalledWith({ delta: 'hello' });
51
+ expect(onSessionEvent).toHaveBeenCalledWith({ data: { type: 'session.updated' } });
52
+ expect(result).toEqual({ sessionKey: 's1', reply: 'hello world' });
53
+ });
54
+
55
+ it('routes streamChatRun through appClient.openStream and preserves query params', async () => {
56
+ const onReady = vi.fn();
57
+ const onDelta = vi.fn();
58
+ const onSessionEvent = vi.fn();
59
+
60
+ mocks.openStream.mockImplementation(() => ({
61
+ finished: Promise.resolve({ sessionKey: 's1', reply: 'resumed' }),
62
+ cancel: vi.fn()
63
+ }));
64
+
65
+ const result = await streamChatRun(
66
+ { runId: 'run-1', fromEventIndex: 42 },
67
+ { onReady, onDelta, onSessionEvent }
68
+ );
69
+
70
+ expect(mocks.openStream).toHaveBeenCalledWith({
71
+ method: 'GET',
72
+ path: '/api/chat/runs/run-1/stream?fromEventIndex=42',
73
+ signal: undefined,
74
+ onEvent: expect.any(Function)
75
+ });
76
+ expect(onReady).not.toHaveBeenCalled();
77
+ expect(onDelta).not.toHaveBeenCalled();
78
+ expect(onSessionEvent).not.toHaveBeenCalled();
79
+ expect(result).toEqual({ sessionKey: 's1', reply: 'resumed' });
80
+ });
81
+
82
+ it('surfaces transport error events as rejected stream promises', async () => {
83
+ mocks.openStream.mockImplementation(({ onEvent }) => {
84
+ let resolveFinished!: () => void;
85
+ let rejectFinished!: (error: Error) => void;
86
+ const finished = new Promise<void>((resolve, reject) => {
87
+ resolveFinished = resolve;
88
+ rejectFinished = reject;
89
+ });
90
+ queueMicrotask(() => {
91
+ try {
92
+ onEvent({ name: 'error', payload: { message: 'chat stream failed' } });
93
+ resolveFinished();
94
+ } catch (error) {
95
+ rejectFinished(error instanceof Error ? error : new Error(String(error)));
96
+ }
97
+ });
98
+ return {
99
+ finished,
100
+ cancel: vi.fn()
101
+ };
102
+ });
103
+
104
+ await expect(
105
+ sendChatTurnStream(
106
+ { message: 'hi' } as never,
107
+ {
108
+ onReady: vi.fn(),
109
+ onDelta: vi.fn(),
110
+ onSessionEvent: vi.fn()
111
+ }
112
+ )
113
+ ).rejects.toThrow('chat stream failed');
114
+ });
115
+ });
package/src/api/config.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { api, API_BASE } from './client';
1
+ import { api } from './client';
2
+ import { appClient } from '@/transport';
2
3
  import type {
3
4
  AuthEnabledUpdateRequest,
4
5
  AuthLoginRequest,
@@ -142,10 +143,7 @@ export async function fetchConfigSchema(): Promise<ConfigSchemaResponse> {
142
143
  }
143
144
 
144
145
  // PUT /api/config/model
145
- export async function updateModel(data: {
146
- model: string;
147
- workspace?: string;
148
- }): Promise<{ model: string; workspace?: string }> {
146
+ export async function updateModel(data: { model: string; workspace?: string }): Promise<{ model: string; workspace?: string }> {
149
147
  const response = await api.put<{ model: string; workspace?: string }>('/api/config/model', data);
150
148
  if (!response.ok) {
151
149
  throw new Error(response.error.message);
@@ -379,32 +377,6 @@ export async function sendChatTurn(data: ChatTurnRequest): Promise<ChatTurnView>
379
377
  return response.data;
380
378
  }
381
379
 
382
- function parseSseFrame(frame: string): { event: string; data: string } | null {
383
- const lines = frame.split('\n');
384
- let event = '';
385
- const dataLines: string[] = [];
386
- for (const raw of lines) {
387
- const line = raw.trimEnd();
388
- if (!line || line.startsWith(':')) {
389
- continue;
390
- }
391
- if (line.startsWith('event:')) {
392
- event = line.slice(6).trim();
393
- continue;
394
- }
395
- if (line.startsWith('data:')) {
396
- dataLines.push(line.slice(5).trimStart());
397
- }
398
- }
399
- if (!event) {
400
- return null;
401
- }
402
- return {
403
- event,
404
- data: dataLines.join('\n')
405
- };
406
- }
407
-
408
380
  async function readSseStream(params: {
409
381
  path: string;
410
382
  method: 'GET' | 'POST';
@@ -414,115 +386,68 @@ async function readSseStream(params: {
414
386
  onDelta: (event: ChatTurnStreamDeltaEvent) => void;
415
387
  onSessionEvent: (event: ChatTurnStreamSessionEvent) => void;
416
388
  }): Promise<{ sessionKey: string; reply: string }> {
417
- const response = await fetch(`${API_BASE}${params.path}`, {
418
- method: params.method,
419
- credentials: 'include',
420
- headers: {
421
- 'Content-Type': 'application/json',
422
- Accept: 'text/event-stream'
423
- },
424
- ...(params.body !== undefined ? { body: JSON.stringify(params.body) } : {}),
425
- ...(params.signal ? { signal: params.signal } : {})
426
- });
427
-
428
- if (!response.ok) {
429
- const text = await response.text();
430
- const fallback = `HTTP ${response.status}`;
431
- const trimmed = text.trim();
432
- throw new Error(trimmed || fallback);
433
- }
434
-
435
- const reader = response.body?.getReader();
436
- if (!reader) {
437
- throw new Error('SSE response body unavailable');
438
- }
439
-
440
- const decoder = new TextDecoder();
441
- let buffer = '';
442
389
  let finalResult: { sessionKey: string; reply: string } | null = null;
443
- let readySessionKey: string | null = null;
444
-
445
- const consumeFrame = (frame: string) => {
446
- const parsed = parseSseFrame(frame);
447
- if (!parsed) {
448
- return;
449
- }
390
+ let readySessionKey = '';
450
391
 
451
- let payload: unknown = undefined;
452
- if (parsed.data) {
453
- try {
454
- payload = JSON.parse(parsed.data);
455
- } catch {
456
- payload = undefined;
392
+ const session = appClient.openStream<ChatTurnView>({
393
+ method: params.method,
394
+ path: params.path,
395
+ ...(params.body !== undefined ? { body: params.body } : {}),
396
+ signal: params.signal,
397
+ onEvent: (event) => {
398
+ if (event.name === 'ready') {
399
+ const ready = (event.payload ?? {}) as ChatTurnStreamReadyEvent;
400
+ if (typeof ready.sessionKey === 'string' && ready.sessionKey.trim()) {
401
+ readySessionKey = ready.sessionKey;
402
+ }
403
+ params.onReady(ready);
404
+ return;
457
405
  }
458
- }
459
406
 
460
- if (parsed.event === 'ready') {
461
- const ready = (payload ?? {}) as ChatTurnStreamReadyEvent;
462
- readySessionKey = typeof ready.sessionKey === 'string' && ready.sessionKey.trim() ? ready.sessionKey : readySessionKey;
463
- params.onReady(ready);
464
- return;
465
- }
466
-
467
- if (parsed.event === 'delta') {
468
- params.onDelta((payload ?? { delta: '' }) as ChatTurnStreamDeltaEvent);
469
- return;
470
- }
471
-
472
- if (parsed.event === 'session_event') {
473
- params.onSessionEvent({ data: payload as ChatTurnStreamSessionEvent['data'] });
474
- return;
475
- }
407
+ if (event.name === 'delta') {
408
+ params.onDelta((event.payload ?? { delta: '' }) as ChatTurnStreamDeltaEvent);
409
+ return;
410
+ }
476
411
 
477
- if (parsed.event === 'final') {
478
- const result = payload as ChatTurnView;
479
- finalResult = {
480
- sessionKey: typeof result?.sessionKey === 'string' && result.sessionKey.trim()
481
- ? result.sessionKey
482
- : (readySessionKey ?? ''),
483
- reply: typeof result?.reply === 'string' ? result.reply : ''
484
- };
485
- return;
486
- }
412
+ if (event.name === 'session_event') {
413
+ params.onSessionEvent({ data: event.payload as ChatTurnStreamSessionEvent['data'] });
414
+ return;
415
+ }
487
416
 
488
- if (parsed.event === 'error') {
489
- const errorPayload = (payload ?? {}) as ChatTurnStreamErrorEvent;
490
- throw new Error((errorPayload.message ?? '').trim() || 'chat stream failed');
491
- }
492
- };
493
-
494
- try {
495
- let isReading = true;
496
- while (isReading) {
497
- const { value, done } = await reader.read();
498
- if (done) {
499
- isReading = false;
500
- continue;
417
+ if (event.name === 'final') {
418
+ const result = event.payload as ChatTurnView;
419
+ finalResult = {
420
+ sessionKey: typeof result?.sessionKey === 'string' && result.sessionKey.trim()
421
+ ? result.sessionKey
422
+ : readySessionKey,
423
+ reply: typeof result?.reply === 'string' ? result.reply : ''
424
+ };
425
+ return;
501
426
  }
502
- buffer += decoder.decode(value, { stream: true });
503
- let boundary = buffer.indexOf('\n\n');
504
- while (boundary !== -1) {
505
- const frame = buffer.slice(0, boundary);
506
- buffer = buffer.slice(boundary + 2);
507
- consumeFrame(frame);
508
- boundary = buffer.indexOf('\n\n');
427
+
428
+ if (event.name === 'error') {
429
+ const errorPayload = (event.payload ?? {}) as ChatTurnStreamErrorEvent;
430
+ throw new Error((errorPayload.message ?? '').trim() || 'chat stream failed');
509
431
  }
510
432
  }
511
- if (buffer.trim()) {
512
- consumeFrame(buffer);
513
- }
514
- } finally {
515
- reader.releaseLock();
516
- }
433
+ });
517
434
 
435
+ const result = await session.finished;
518
436
  if (finalResult) {
519
437
  return finalResult;
520
438
  }
521
-
522
439
  if (readySessionKey) {
523
- return { sessionKey: readySessionKey, reply: '' };
440
+ return {
441
+ sessionKey: readySessionKey,
442
+ reply: typeof result?.reply === 'string' ? result.reply : ''
443
+ };
444
+ }
445
+ if (typeof result?.sessionKey === 'string' && result.sessionKey.trim()) {
446
+ return {
447
+ sessionKey: result.sessionKey,
448
+ reply: typeof result?.reply === 'string' ? result.reply : ''
449
+ };
524
450
  }
525
-
526
451
  throw new Error('chat stream ended without final event');
527
452
  }
528
453