@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/dist/assets/ChannelsList-Cu_hLbps.js +1 -0
- package/dist/assets/ChatPage-Dmpau_7n.js +41 -0
- package/dist/assets/DocBrowser-C3ijFxFF.js +1 -0
- package/dist/assets/LogoBadge-BgjXmBcw.js +1 -0
- package/dist/assets/MarketplacePage-CAIdEiw8.js +49 -0
- package/dist/assets/McpMarketplacePage-DPtH1xcY.js +40 -0
- package/dist/assets/ModelConfig-D-pqArCg.js +1 -0
- package/dist/assets/ProvidersList-DnWsJqMQ.js +1 -0
- package/dist/assets/RemoteAccessPage-BrXq-x0-.js +1 -0
- package/dist/assets/RuntimeConfig-UE9VaFO7.js +1 -0
- package/dist/assets/SearchConfig-CP-RM3V3.js +1 -0
- package/dist/assets/SecretsConfig-CfN_bazs.js +3 -0
- package/dist/assets/SessionsConfig-CgkKzKGv.js +2 -0
- package/dist/assets/chat-message-CGL3sMsS.js +3 -0
- package/dist/assets/config-hints-CApS3K_7.js +1 -0
- package/dist/assets/config-layout-BHnOoweL.js +1 -0
- package/dist/assets/index-D4alkESd.js +8 -0
- package/dist/assets/index-SGSkQCPi.css +1 -0
- package/dist/assets/label-CbOSodIL.js +1 -0
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/page-layout-BtDnyNLf.js +1 -0
- package/dist/assets/popover-DGlUjPQc.js +1 -0
- package/dist/assets/provider-models-BOeNnjk9.js +1 -0
- package/dist/assets/security-config-D6Bs1yoK.js +1 -0
- package/dist/assets/skeleton-BLV99JbX.js +1 -0
- package/dist/assets/status-dot-C8vM3IN1.js +1 -0
- package/dist/assets/switch-AuwUiga3.js +1 -0
- package/dist/assets/tabs-custom-CTS7SaFG.js +1 -0
- package/dist/assets/useConfirmDialog-DrMAdNfN.js +1 -0
- package/dist/assets/vendor-TJ2hy_Lv.js +441 -0
- package/dist/index.html +18 -0
- package/dist/logo.svg +5 -0
- package/dist/logos/aihubmix.png +0 -0
- package/dist/logos/anthropic.svg +1 -0
- package/dist/logos/dashscope.png +0 -0
- package/dist/logos/deepseek.png +0 -0
- package/dist/logos/dingtalk.svg +1 -0
- package/dist/logos/discord.svg +1 -0
- package/dist/logos/email.svg +1 -0
- package/dist/logos/feishu.svg +12 -0
- package/dist/logos/gemini.svg +1 -0
- package/dist/logos/groq.svg +1 -0
- package/dist/logos/minimax.svg +1 -0
- package/dist/logos/mochat.svg +6 -0
- package/dist/logos/moonshot.png +0 -0
- package/dist/logos/openai.svg +1 -0
- package/dist/logos/openrouter.svg +1 -0
- package/dist/logos/qq.svg +1 -0
- package/dist/logos/slack.svg +1 -0
- package/dist/logos/telegram.svg +1 -0
- package/dist/logos/vllm.svg +1 -0
- package/dist/logos/wecom.svg +11 -0
- package/dist/logos/weixin.svg +5 -0
- package/dist/logos/whatsapp.svg +1 -0
- package/dist/logos/zhipu.svg +15 -0
- package/package.json +16 -17
- package/src/api/config.ts +4 -2
- package/src/components/chat/chat-stream/transport.ts +42 -2
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +0 -9
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +0 -10
- package/src/components/chat/ncp/ncp-session-adapter.ts +1 -0
- package/src/components/config/ModelConfig.tsx +1 -4
- package/src/transport/app-client.ts +6 -22
- package/src/transport/local.transport.ts +5 -7
- package/src/transport/remote.transport.ts +8 -7
- package/src/transport/sse-stream.test.ts +5 -19
- package/src/transport/sse-stream.ts +5 -60
- package/src/transport/transport.types.ts +0 -2
- 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('
|
|
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('
|
|
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).
|
|
39
|
-
expect(events.map((event) => event.name)).toEqual(['ncp-event', '
|
|
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('
|
|
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
|
|
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, (
|
|
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
|
}
|
|
@@ -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
|
-
});
|