@nextclaw/ui 0.9.14 → 0.9.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/dist/assets/ChannelsList-Cu_hLbps.js +1 -0
  4. package/dist/assets/ChatPage-Dmpau_7n.js +41 -0
  5. package/dist/assets/DocBrowser-C3ijFxFF.js +1 -0
  6. package/dist/assets/LogoBadge-BgjXmBcw.js +1 -0
  7. package/dist/assets/MarketplacePage-CAIdEiw8.js +49 -0
  8. package/dist/assets/McpMarketplacePage-DPtH1xcY.js +40 -0
  9. package/dist/assets/ModelConfig-D-pqArCg.js +1 -0
  10. package/dist/assets/ProvidersList-DnWsJqMQ.js +1 -0
  11. package/dist/assets/RemoteAccessPage-BrXq-x0-.js +1 -0
  12. package/dist/assets/RuntimeConfig-UE9VaFO7.js +1 -0
  13. package/dist/assets/SearchConfig-CP-RM3V3.js +1 -0
  14. package/dist/assets/SecretsConfig-CfN_bazs.js +3 -0
  15. package/dist/assets/SessionsConfig-CgkKzKGv.js +2 -0
  16. package/dist/assets/chat-message-CGL3sMsS.js +3 -0
  17. package/dist/assets/config-hints-CApS3K_7.js +1 -0
  18. package/dist/assets/config-layout-BHnOoweL.js +1 -0
  19. package/dist/assets/index-D4alkESd.js +8 -0
  20. package/dist/assets/index-SGSkQCPi.css +1 -0
  21. package/dist/assets/label-CbOSodIL.js +1 -0
  22. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  23. package/dist/assets/page-layout-BtDnyNLf.js +1 -0
  24. package/dist/assets/popover-DGlUjPQc.js +1 -0
  25. package/dist/assets/provider-models-BOeNnjk9.js +1 -0
  26. package/dist/assets/security-config-D6Bs1yoK.js +1 -0
  27. package/dist/assets/skeleton-BLV99JbX.js +1 -0
  28. package/dist/assets/status-dot-C8vM3IN1.js +1 -0
  29. package/dist/assets/switch-AuwUiga3.js +1 -0
  30. package/dist/assets/tabs-custom-CTS7SaFG.js +1 -0
  31. package/dist/assets/useConfirmDialog-DrMAdNfN.js +1 -0
  32. package/dist/assets/vendor-TJ2hy_Lv.js +441 -0
  33. package/dist/index.html +18 -0
  34. package/dist/logo.svg +5 -0
  35. package/dist/logos/aihubmix.png +0 -0
  36. package/dist/logos/anthropic.svg +1 -0
  37. package/dist/logos/dashscope.png +0 -0
  38. package/dist/logos/deepseek.png +0 -0
  39. package/dist/logos/dingtalk.svg +1 -0
  40. package/dist/logos/discord.svg +1 -0
  41. package/dist/logos/email.svg +1 -0
  42. package/dist/logos/feishu.svg +12 -0
  43. package/dist/logos/gemini.svg +1 -0
  44. package/dist/logos/groq.svg +1 -0
  45. package/dist/logos/minimax.svg +1 -0
  46. package/dist/logos/mochat.svg +6 -0
  47. package/dist/logos/moonshot.png +0 -0
  48. package/dist/logos/openai.svg +1 -0
  49. package/dist/logos/openrouter.svg +1 -0
  50. package/dist/logos/qq.svg +1 -0
  51. package/dist/logos/slack.svg +1 -0
  52. package/dist/logos/telegram.svg +1 -0
  53. package/dist/logos/vllm.svg +1 -0
  54. package/dist/logos/wecom.svg +11 -0
  55. package/dist/logos/weixin.svg +5 -0
  56. package/dist/logos/whatsapp.svg +1 -0
  57. package/dist/logos/zhipu.svg +15 -0
  58. package/package.json +16 -17
  59. package/src/api/config.ts +4 -2
  60. package/src/components/chat/chat-stream/transport.ts +42 -2
  61. package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +0 -9
  62. package/src/components/chat/ncp/ncp-app-client-fetch.ts +0 -10
  63. package/src/components/chat/ncp/ncp-session-adapter.ts +1 -0
  64. package/src/components/config/ModelConfig.tsx +1 -4
  65. package/src/transport/app-client.ts +6 -22
  66. package/src/transport/local.transport.ts +5 -7
  67. package/src/transport/remote.transport.ts +8 -7
  68. package/src/transport/sse-stream.test.ts +5 -19
  69. package/src/transport/sse-stream.ts +5 -60
  70. package/src/transport/transport.types.ts +0 -2
  71. package/src/components/config/ModelConfig.test.tsx +0 -78
@@ -22,24 +22,22 @@ function encodeFrame(event: string, payload: unknown): string {
22
22
  }
23
23
 
24
24
  describe('readSseStreamResult', () => {
25
- it('accepts terminal-event streams without final frame when configured', async () => {
25
+ it('preserves final frames for callers while still resolving with the final payload', async () => {
26
26
  const events: Array<{ name: string; payload?: unknown }> = [];
27
27
  const response = createSseResponse([
28
28
  encodeFrame('ncp-event', { type: 'message.text-delta', payload: { delta: 'hello' } }),
29
- encodeFrame('run.finished', { sessionId: 's1' })
29
+ encodeFrame('final', { sessionId: 's1', reply: 'hello' })
30
30
  ]);
31
31
 
32
32
  const result = await readSseStreamResult(response, (event) => {
33
33
  events.push(event);
34
- }, {
35
- terminalEventNames: ['run.finished']
36
34
  });
37
35
 
38
- expect(result).toBeUndefined();
39
- expect(events.map((event) => event.name)).toEqual(['ncp-event', 'run.finished']);
36
+ expect(result).toEqual({ sessionId: 's1', reply: 'hello' });
37
+ expect(events.map((event) => event.name)).toEqual(['ncp-event', 'final']);
40
38
  });
41
39
 
42
- it('accepts terminal payload-type streams without final frame when configured', async () => {
40
+ it('allows passthrough SSE streams to end without a final frame', async () => {
43
41
  const events: Array<{ name: string; payload?: unknown }> = [];
44
42
  const response = createSseResponse([
45
43
  encodeFrame('ncp-event', { type: 'message.text-delta', payload: { delta: 'hello' } }),
@@ -48,21 +46,9 @@ describe('readSseStreamResult', () => {
48
46
 
49
47
  const result = await readSseStreamResult(response, (event) => {
50
48
  events.push(event);
51
- }, {
52
- terminalEventPayloadTypes: {
53
- 'ncp-event': ['run.finished']
54
- }
55
49
  });
56
50
 
57
51
  expect(result).toBeUndefined();
58
52
  expect(events.map((event) => event.name)).toEqual(['ncp-event', 'ncp-event']);
59
53
  });
60
-
61
- it('still rejects streams that end without final or configured terminal event', async () => {
62
- const response = createSseResponse([
63
- encodeFrame('ncp-event', { type: 'message.text-delta', payload: { delta: 'hello' } })
64
- ]);
65
-
66
- await expect(readSseStreamResult(response, () => undefined)).rejects.toThrow('stream ended without final event');
67
- });
68
54
  });
@@ -1,12 +1,6 @@
1
1
  import type { StreamEvent } from './transport.types';
2
2
 
3
- type SseErrorPayload = { message?: string } | string | undefined;
4
3
  type FinalResultSink = (value: unknown) => void;
5
- type TerminalEventSink = (frame: StreamEvent) => void;
6
- type TerminalDetectionOptions = {
7
- terminalEventNames?: readonly string[];
8
- terminalEventPayloadTypes?: Partial<Record<string, readonly string[]>>;
9
- };
10
4
 
11
5
  function parseSseFrame(frame: string): StreamEvent | null {
12
6
  const lines = frame.split('\n');
@@ -42,35 +36,10 @@ function parseSseFrame(frame: string): StreamEvent | null {
42
36
  return { name, payload };
43
37
  }
44
38
 
45
- function readSseErrorMessage(payload: SseErrorPayload, fallback: string): string {
46
- return typeof payload === 'string'
47
- ? payload
48
- : payload?.message ?? fallback;
49
- }
50
-
51
- function matchesTerminalFrame(frame: StreamEvent, options: TerminalDetectionOptions): boolean {
52
- if ((options.terminalEventNames ?? []).includes(frame.name)) {
53
- return true;
54
- }
55
- const payloadTypes = options.terminalEventPayloadTypes?.[frame.name];
56
- if (!payloadTypes || payloadTypes.length === 0) {
57
- return false;
58
- }
59
- const payloadType =
60
- typeof frame.payload === 'object' &&
61
- frame.payload &&
62
- 'type' in frame.payload &&
63
- typeof (frame.payload as { type?: unknown }).type === 'string'
64
- ? (frame.payload as { type: string }).type
65
- : null;
66
- return payloadType !== null && payloadTypes.includes(payloadType);
67
- }
68
-
69
39
  function processSseFrame(
70
40
  rawFrame: string,
71
41
  onEvent: (event: StreamEvent) => void,
72
- setFinalResult: FinalResultSink,
73
- setTerminalEvent: TerminalEventSink
42
+ setFinalResult: FinalResultSink
74
43
  ): void {
75
44
  const frame = parseSseFrame(rawFrame);
76
45
  if (!frame) {
@@ -78,24 +47,18 @@ function processSseFrame(
78
47
  }
79
48
  if (frame.name === 'final') {
80
49
  setFinalResult(frame.payload);
81
- return;
82
- }
83
- if (frame.name === 'error') {
84
- throw new Error(readSseErrorMessage(frame.payload as SseErrorPayload, 'chat stream failed'));
85
50
  }
86
- setTerminalEvent(frame);
87
51
  onEvent(frame);
88
52
  }
89
53
 
90
54
  function flushBufferedFrames(
91
55
  bufferState: { value: string },
92
56
  onEvent: (event: StreamEvent) => void,
93
- setFinalResult: FinalResultSink,
94
- setTerminalEvent: TerminalEventSink
57
+ setFinalResult: FinalResultSink
95
58
  ): void {
96
59
  let boundary = bufferState.value.indexOf('\n\n');
97
60
  while (boundary !== -1) {
98
- processSseFrame(bufferState.value.slice(0, boundary), onEvent, setFinalResult, setTerminalEvent);
61
+ processSseFrame(bufferState.value.slice(0, boundary), onEvent, setFinalResult);
99
62
  bufferState.value = bufferState.value.slice(boundary + 2);
100
63
  boundary = bufferState.value.indexOf('\n\n');
101
64
  }
@@ -103,8 +66,7 @@ function flushBufferedFrames(
103
66
 
104
67
  export async function readSseStreamResult<TFinal>(
105
68
  response: Response,
106
- onEvent: (event: StreamEvent) => void,
107
- options: TerminalDetectionOptions = {}
69
+ onEvent: (event: StreamEvent) => void
108
70
  ): Promise<TFinal> {
109
71
  const reader = response.body?.getReader();
110
72
  if (!reader) {
@@ -114,7 +76,6 @@ export async function readSseStreamResult<TFinal>(
114
76
  const decoder = new TextDecoder();
115
77
  const bufferState = { value: '' };
116
78
  let finalResult: unknown = undefined;
117
- let sawTerminalEvent = false;
118
79
  try {
119
80
  while (true) {
120
81
  const { value, done } = await reader.read();
@@ -124,32 +85,16 @@ export async function readSseStreamResult<TFinal>(
124
85
  bufferState.value += decoder.decode(value, { stream: true });
125
86
  flushBufferedFrames(bufferState, onEvent, (nextValue) => {
126
87
  finalResult = nextValue;
127
- }, (event) => {
128
- if (matchesTerminalFrame(event, options)) {
129
- sawTerminalEvent = true;
130
- }
131
88
  });
132
89
  }
133
90
  if (bufferState.value.trim()) {
134
- processSseFrame(bufferState.value, (event) => {
135
- if (matchesTerminalFrame(event, options)) {
136
- sawTerminalEvent = true;
137
- }
138
- onEvent(event);
139
- }, (nextValue) => {
91
+ processSseFrame(bufferState.value, onEvent, (nextValue) => {
140
92
  finalResult = nextValue;
141
- }, (event) => {
142
- if (matchesTerminalFrame(event, options)) {
143
- sawTerminalEvent = true;
144
- }
145
93
  });
146
94
  }
147
95
  } finally {
148
96
  reader.releaseLock();
149
97
  }
150
98
 
151
- if (finalResult === undefined && !sawTerminalEvent) {
152
- throw new Error('stream ended without final event');
153
- }
154
99
  return finalResult as TFinal;
155
100
  }
@@ -18,8 +18,6 @@ export type StreamInput = {
18
18
  path: string;
19
19
  body?: unknown;
20
20
  signal?: AbortSignal;
21
- terminalEventNames?: readonly string[];
22
- terminalEventPayloadTypes?: Partial<Record<string, readonly string[]>>;
23
21
  onEvent: (event: StreamEvent) => void;
24
22
  };
25
23
 
@@ -1,78 +0,0 @@
1
- import { render, screen, waitFor } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import { ModelConfig } from '@/components/config/ModelConfig';
4
-
5
- const mocks = vi.hoisted(() => ({
6
- mutate: vi.fn(),
7
- configQuery: {
8
- data: {
9
- agents: {
10
- defaults: {
11
- model: 'openai/gpt-5.2',
12
- workspace: '~/old-workspace'
13
- }
14
- },
15
- providers: {
16
- openai: {
17
- enabled: true,
18
- apiKeySet: true,
19
- models: ['gpt-5.2']
20
- }
21
- }
22
- },
23
- isLoading: false
24
- },
25
- metaQuery: {
26
- data: {
27
- providers: [
28
- {
29
- name: 'openai',
30
- displayName: 'OpenAI',
31
- modelPrefix: 'openai',
32
- defaultModels: ['openai/gpt-5.2'],
33
- keywords: [],
34
- envKey: 'OPENAI_API_KEY'
35
- }
36
- ]
37
- }
38
- },
39
- schemaQuery: {
40
- data: {
41
- uiHints: {}
42
- }
43
- }
44
- }));
45
-
46
- vi.mock('@/hooks/useConfig', () => ({
47
- useConfig: () => mocks.configQuery,
48
- useConfigMeta: () => mocks.metaQuery,
49
- useConfigSchema: () => mocks.schemaQuery,
50
- useUpdateModel: () => ({
51
- mutate: mocks.mutate,
52
- isPending: false
53
- })
54
- }));
55
-
56
- describe('ModelConfig', () => {
57
- beforeEach(() => {
58
- mocks.mutate.mockReset();
59
- });
60
-
61
- it('submits the workspace together with the selected model', async () => {
62
- const user = userEvent.setup();
63
-
64
- render(<ModelConfig />);
65
-
66
- const workspaceInput = await screen.findByLabelText('Default Path');
67
- await user.clear(workspaceInput);
68
- await user.type(workspaceInput, '~/new-workspace');
69
- await user.click(screen.getByRole('button', { name: /save/i }));
70
-
71
- await waitFor(() => {
72
- expect(mocks.mutate).toHaveBeenCalledWith({
73
- model: 'openai/gpt-5.2',
74
- workspace: '~/new-workspace'
75
- });
76
- });
77
- });
78
- });