@nextclaw/ui 0.6.9 → 0.6.11
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/.eslintrc.cjs +10 -0
- package/CHANGELOG.md +15 -0
- package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
- package/dist/assets/ChatPage-DIx05c6s.js +36 -0
- package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
- package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
- package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
- package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
- package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
- package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
- package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
- package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
- package/dist/assets/index-Cn6_2To7.js +8 -0
- package/dist/assets/index-nEYGCJTC.css +1 -0
- package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
- package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
- package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
- package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
- package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
- package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
- package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
- package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/src/App.tsx +1 -2
- package/src/api/config.ts +205 -202
- package/src/api/types.ts +54 -24
- package/src/components/chat/ChatConversationPanel.tsx +102 -121
- package/src/components/chat/ChatPage.tsx +165 -437
- package/src/components/chat/ChatSidebar.tsx +30 -36
- package/src/components/chat/ChatThread.tsx +73 -131
- package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
- package/src/components/chat/chat-input.types.ts +15 -0
- package/src/components/chat/chat-page-data.ts +121 -0
- package/src/components/chat/chat-page-runtime.ts +221 -0
- package/src/components/chat/chat-session-route.ts +59 -0
- package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
- package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
- package/src/components/chat/chat-stream/transport.ts +159 -0
- package/src/components/chat/chat-stream/types.ts +76 -0
- package/src/components/chat/managers/chat-input.manager.ts +142 -0
- package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
- package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
- package/src/components/chat/managers/chat-thread.manager.ts +86 -0
- package/src/components/chat/managers/chat-ui.manager.ts +65 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
- package/src/components/chat/presenter/chat.presenter.ts +32 -0
- package/src/components/chat/stores/chat-input.store.ts +62 -0
- package/src/components/chat/stores/chat-run-status.store.ts +30 -0
- package/src/components/chat/stores/chat-session-list.store.ts +34 -0
- package/src/components/chat/stores/chat-thread.store.ts +52 -0
- package/src/components/chat/useChatRuntimeController.ts +134 -0
- package/src/components/chat/useChatSessionTypeState.ts +148 -0
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/config/ProviderForm.tsx +221 -14
- package/src/hooks/useConfig.ts +33 -2
- package/src/hooks/useObservable.ts +20 -0
- package/src/hooks/useWebSocket.ts +23 -1
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +11 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChatPage-iji0RkTR.js +0 -34
- package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
- package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
- package/dist/assets/index-C_DhisNo.css +0 -1
- package/dist/assets/index-dKTqKCJo.js +0 -7
- package/dist/assets/session-run-status-CllIZxNf.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
package/src/hooks/useConfig.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
fetchChatRun,
|
|
25
25
|
fetchChatRuns,
|
|
26
26
|
fetchChatCapabilities,
|
|
27
|
+
fetchChatSessionTypes,
|
|
27
28
|
fetchCronJobs,
|
|
28
29
|
deleteCronJob,
|
|
29
30
|
setCronJobEnabled,
|
|
@@ -139,7 +140,8 @@ export function useTestProviderConnection() {
|
|
|
139
140
|
|
|
140
141
|
export function useStartProviderAuth() {
|
|
141
142
|
return useMutation({
|
|
142
|
-
mutationFn: ({ provider }: { provider: string }) =>
|
|
143
|
+
mutationFn: ({ provider, data }: { provider: string; data?: unknown }) =>
|
|
144
|
+
startProviderAuth(provider, data as Parameters<typeof startProviderAuth>[1])
|
|
143
145
|
});
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -293,9 +295,26 @@ export function useChatCapabilities(params?: { sessionKey?: string | null; agent
|
|
|
293
295
|
});
|
|
294
296
|
}
|
|
295
297
|
|
|
296
|
-
export function
|
|
298
|
+
export function useChatSessionTypes() {
|
|
299
|
+
return useQuery({
|
|
300
|
+
queryKey: ['chat-session-types'],
|
|
301
|
+
queryFn: fetchChatSessionTypes,
|
|
302
|
+
staleTime: 10_000,
|
|
303
|
+
retry: false
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function useChatRuns(params?: {
|
|
308
|
+
sessionKey?: string | null;
|
|
309
|
+
states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>;
|
|
310
|
+
limit?: number;
|
|
311
|
+
syncActiveStates?: boolean;
|
|
312
|
+
isLocallyRunning?: boolean;
|
|
313
|
+
}) {
|
|
297
314
|
const sessionKey = params?.sessionKey?.trim() || undefined;
|
|
298
315
|
const states = Array.isArray(params?.states) && params.states.length > 0 ? params.states : undefined;
|
|
316
|
+
const isActiveStatesQuery = Boolean(states?.some((state) => state === 'queued' || state === 'running'));
|
|
317
|
+
const shouldSyncActiveStates = Boolean(params?.syncActiveStates && isActiveStatesQuery);
|
|
299
318
|
return useQuery({
|
|
300
319
|
queryKey: ['chat-runs', sessionKey ?? null, states ?? null, params?.limit ?? null],
|
|
301
320
|
queryFn: () => fetchChatRuns({
|
|
@@ -305,6 +324,18 @@ export function useChatRuns(params?: { sessionKey?: string | null; states?: Arra
|
|
|
305
324
|
}),
|
|
306
325
|
enabled: Boolean(sessionKey) || Boolean(states),
|
|
307
326
|
staleTime: 5_000,
|
|
327
|
+
refetchInterval: (query) => {
|
|
328
|
+
if (!shouldSyncActiveStates) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (params?.isLocallyRunning) {
|
|
332
|
+
return 800;
|
|
333
|
+
}
|
|
334
|
+
const data = query.state.data;
|
|
335
|
+
const hasActiveRuns = Array.isArray(data?.runs) && data.runs.length > 0;
|
|
336
|
+
return hasActiveRuns ? 800 : false;
|
|
337
|
+
},
|
|
338
|
+
refetchIntervalInBackground: false,
|
|
308
339
|
retry: false
|
|
309
340
|
});
|
|
310
341
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
export function useValueFromBehaviorSubject<T>(subject: BehaviorSubject<T>): T {
|
|
5
|
+
const [state, setState] = useState<T>(subject.getValue());
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const subscription = subject.subscribe(setState);
|
|
8
|
+
return () => subscription.unsubscribe();
|
|
9
|
+
}, [subject]);
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useValueFromObservable<T>(observable: Observable<T>, defaultValue: T): T {
|
|
14
|
+
const [state, setState] = useState<T>(defaultValue);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const subscription = observable.subscribe(setState);
|
|
17
|
+
return () => subscription.unsubscribe();
|
|
18
|
+
}, [observable]);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
@@ -38,15 +38,30 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
38
38
|
})();
|
|
39
39
|
const client = new ConfigWebSocket(wsUrl);
|
|
40
40
|
|
|
41
|
+
const invalidateSessionQueries = (sessionKey?: string) => {
|
|
42
|
+
if (!queryClient) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
queryClient.invalidateQueries({ queryKey: ['sessions'] });
|
|
46
|
+
if (sessionKey && sessionKey.trim().length > 0) {
|
|
47
|
+
queryClient.invalidateQueries({ queryKey: ['session-history', sessionKey.trim()] });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: ['session-history'] });
|
|
51
|
+
};
|
|
52
|
+
|
|
41
53
|
client.on('connection.open', () => {
|
|
42
54
|
setConnectionStatus('connected');
|
|
43
55
|
});
|
|
44
56
|
|
|
45
|
-
client.on('config.updated', () => {
|
|
57
|
+
client.on('config.updated', (event) => {
|
|
46
58
|
// Trigger refetch of config
|
|
47
59
|
if (queryClient) {
|
|
48
60
|
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
49
61
|
}
|
|
62
|
+
if (event.type === 'config.updated' && event.payload.path.startsWith('session')) {
|
|
63
|
+
invalidateSessionQueries();
|
|
64
|
+
}
|
|
50
65
|
});
|
|
51
66
|
|
|
52
67
|
client.on('run.updated', (event) => {
|
|
@@ -70,6 +85,13 @@ export function useWebSocket(queryClient?: QueryClient) {
|
|
|
70
85
|
}
|
|
71
86
|
});
|
|
72
87
|
|
|
88
|
+
client.on('session.updated', (event) => {
|
|
89
|
+
if (event.type !== 'session.updated') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
invalidateSessionQueries(event.payload.sessionKey);
|
|
93
|
+
});
|
|
94
|
+
|
|
73
95
|
client.on('error', (event) => {
|
|
74
96
|
if (event.type === 'error') {
|
|
75
97
|
console.error('WebSocket error:', event.payload.message);
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -11,37 +11,6 @@ export type ToolCard = {
|
|
|
11
11
|
hasResult?: boolean;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
export type ChatTimelineMessageItem = {
|
|
15
|
-
kind: 'message';
|
|
16
|
-
key: string;
|
|
17
|
-
role: ChatRole;
|
|
18
|
-
timestamp: string;
|
|
19
|
-
message: SessionMessageView;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type ChatTimelineAssistantTurnSegment =
|
|
23
|
-
| {
|
|
24
|
-
kind: 'assistant_message';
|
|
25
|
-
key: string;
|
|
26
|
-
text: string;
|
|
27
|
-
reasoning: string;
|
|
28
|
-
}
|
|
29
|
-
| {
|
|
30
|
-
kind: 'tool_card';
|
|
31
|
-
key: string;
|
|
32
|
-
card: ToolCard;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type ChatTimelineAssistantTurnItem = {
|
|
36
|
-
kind: 'assistant_turn';
|
|
37
|
-
key: string;
|
|
38
|
-
role: 'assistant';
|
|
39
|
-
timestamp: string;
|
|
40
|
-
segments: ChatTimelineAssistantTurnSegment[];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export type ChatTimelineItem = ChatTimelineMessageItem | ChatTimelineAssistantTurnItem;
|
|
44
|
-
|
|
45
14
|
const TOOL_DETAIL_FIELDS = ['cmd', 'command', 'query', 'q', 'path', 'url', 'to', 'channel', 'agentId', 'sessionKey'];
|
|
46
15
|
|
|
47
16
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -55,7 +24,7 @@ function truncateText(value: string, maxChars = 2400): string {
|
|
|
55
24
|
return `${value.slice(0, maxChars)}\n…`;
|
|
56
25
|
}
|
|
57
26
|
|
|
58
|
-
function stringifyUnknown(value: unknown): string {
|
|
27
|
+
export function stringifyUnknown(value: unknown): string {
|
|
59
28
|
if (typeof value === 'string') {
|
|
60
29
|
return value;
|
|
61
30
|
}
|
|
@@ -91,7 +60,7 @@ function parseArgsObject(value: unknown): Record<string, unknown> | null {
|
|
|
91
60
|
}
|
|
92
61
|
}
|
|
93
62
|
|
|
94
|
-
function summarizeToolArgs(args: unknown): string | undefined {
|
|
63
|
+
export function summarizeToolArgs(args: unknown): string | undefined {
|
|
95
64
|
const parsed = parseArgsObject(args);
|
|
96
65
|
if (!parsed) {
|
|
97
66
|
const text = stringifyUnknown(args).trim();
|
|
@@ -212,20 +181,6 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
212
181
|
return cards;
|
|
213
182
|
}
|
|
214
183
|
|
|
215
|
-
function normalizeEvent(event: SessionEventView, index: number): SessionEventView & { _idx: number; _seq: number } {
|
|
216
|
-
const seq = Number.isFinite(event.seq) && event.seq > 0 ? Math.trunc(event.seq) : index + 1;
|
|
217
|
-
const timestamp =
|
|
218
|
-
typeof event.timestamp === 'string' && event.timestamp
|
|
219
|
-
? event.timestamp
|
|
220
|
-
: event.message?.timestamp ?? new Date().toISOString();
|
|
221
|
-
return {
|
|
222
|
-
...event,
|
|
223
|
-
timestamp,
|
|
224
|
-
_idx: index,
|
|
225
|
-
_seq: seq
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
184
|
function inferEventTypeFromMessage(message: SessionMessageView): string {
|
|
230
185
|
const role = normalizeChatRole(message);
|
|
231
186
|
if (role === 'assistant' && hasToolCalls(message)) {
|
|
@@ -245,158 +200,3 @@ export function buildFallbackEventsFromMessages(messages: SessionMessageView[]):
|
|
|
245
200
|
message
|
|
246
201
|
}));
|
|
247
202
|
}
|
|
248
|
-
|
|
249
|
-
function appendText(base: string, next: string): string {
|
|
250
|
-
if (!next) {
|
|
251
|
-
return base;
|
|
252
|
-
}
|
|
253
|
-
if (!base) {
|
|
254
|
-
return next;
|
|
255
|
-
}
|
|
256
|
-
return `${base}\n\n${next}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[] {
|
|
260
|
-
const normalized = events
|
|
261
|
-
.map((event, index) => normalizeEvent(event, index))
|
|
262
|
-
.sort((left, right) => {
|
|
263
|
-
if (left._seq !== right._seq) {
|
|
264
|
-
return left._seq - right._seq;
|
|
265
|
-
}
|
|
266
|
-
const leftTs = Date.parse(left.timestamp);
|
|
267
|
-
const rightTs = Date.parse(right.timestamp);
|
|
268
|
-
if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
|
|
269
|
-
return leftTs - rightTs;
|
|
270
|
-
}
|
|
271
|
-
return left._idx - right._idx;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const timeline: ChatTimelineItem[] = [];
|
|
275
|
-
let activeTurn:
|
|
276
|
-
| {
|
|
277
|
-
item: ChatTimelineAssistantTurnItem;
|
|
278
|
-
cardByCallId: Map<string, ToolCard>;
|
|
279
|
-
}
|
|
280
|
-
| null = null;
|
|
281
|
-
|
|
282
|
-
const closeActiveTurn = () => {
|
|
283
|
-
activeTurn = null;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const ensureActiveTurn = (eventKey: string, timestamp: string) => {
|
|
287
|
-
if (activeTurn) {
|
|
288
|
-
activeTurn.item.timestamp = timestamp;
|
|
289
|
-
return activeTurn;
|
|
290
|
-
}
|
|
291
|
-
const item: ChatTimelineAssistantTurnItem = {
|
|
292
|
-
kind: 'assistant_turn',
|
|
293
|
-
key: `turn-${eventKey}`,
|
|
294
|
-
role: 'assistant',
|
|
295
|
-
timestamp,
|
|
296
|
-
segments: []
|
|
297
|
-
};
|
|
298
|
-
timeline.push(item);
|
|
299
|
-
activeTurn = {
|
|
300
|
-
item,
|
|
301
|
-
cardByCallId: new Map<string, ToolCard>()
|
|
302
|
-
};
|
|
303
|
-
return activeTurn;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const pushAssistantMessageSegment = (
|
|
307
|
-
target: { item: ChatTimelineAssistantTurnItem },
|
|
308
|
-
eventKey: string,
|
|
309
|
-
message: SessionMessageView
|
|
310
|
-
) => {
|
|
311
|
-
const text = extractMessageText(message.content).trim();
|
|
312
|
-
const reasoning =
|
|
313
|
-
typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
|
|
314
|
-
if (!text && !reasoning) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
target.item.segments.push({
|
|
318
|
-
kind: 'assistant_message',
|
|
319
|
-
key: `assistant-${eventKey}-${target.item.segments.length}`,
|
|
320
|
-
text,
|
|
321
|
-
reasoning
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
for (const event of normalized) {
|
|
326
|
-
const message = event.message;
|
|
327
|
-
if (!message) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const role = normalizeChatRole(message);
|
|
332
|
-
const timestamp =
|
|
333
|
-
typeof message.timestamp === 'string' && message.timestamp
|
|
334
|
-
? message.timestamp
|
|
335
|
-
: event.timestamp;
|
|
336
|
-
const eventKey = `${event._seq}-${event._idx}`;
|
|
337
|
-
|
|
338
|
-
if (role === 'assistant') {
|
|
339
|
-
const turn = ensureActiveTurn(eventKey, timestamp);
|
|
340
|
-
pushAssistantMessageSegment(turn, eventKey, message);
|
|
341
|
-
if (!hasToolCalls(message)) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const toolCards = buildToolCallCards(message);
|
|
346
|
-
for (const card of toolCards) {
|
|
347
|
-
turn.item.segments.push({
|
|
348
|
-
kind: 'tool_card',
|
|
349
|
-
key: `tool-call-${eventKey}-${turn.item.segments.length}`,
|
|
350
|
-
card
|
|
351
|
-
});
|
|
352
|
-
if (typeof card.callId === 'string' && card.callId.trim()) {
|
|
353
|
-
turn.cardByCallId.set(card.callId, card);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (role === 'tool') {
|
|
360
|
-
const turn = ensureActiveTurn(eventKey, timestamp);
|
|
361
|
-
const callId =
|
|
362
|
-
typeof message.tool_call_id === 'string' && message.tool_call_id.trim()
|
|
363
|
-
? message.tool_call_id.trim()
|
|
364
|
-
: undefined;
|
|
365
|
-
if (callId && turn.cardByCallId.has(callId)) {
|
|
366
|
-
const card = turn.cardByCallId.get(callId)!;
|
|
367
|
-
const resultText = extractMessageText(message.content).trim();
|
|
368
|
-
card.text = appendText(card.text ?? '', resultText);
|
|
369
|
-
card.hasResult = true;
|
|
370
|
-
if (typeof message.name === 'string' && message.name.trim()) {
|
|
371
|
-
card.name = message.name.trim();
|
|
372
|
-
}
|
|
373
|
-
turn.item.timestamp = timestamp;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
turn.item.segments.push({
|
|
378
|
-
kind: 'tool_card',
|
|
379
|
-
key: `tool-result-${eventKey}-${turn.item.segments.length}`,
|
|
380
|
-
card: {
|
|
381
|
-
kind: 'result',
|
|
382
|
-
name: toToolName(message.name),
|
|
383
|
-
text: extractMessageText(message.content).trim(),
|
|
384
|
-
callId,
|
|
385
|
-
hasResult: true
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
timeline.push({
|
|
392
|
-
kind: 'message',
|
|
393
|
-
key: `message-${event._seq}-${event._idx}`,
|
|
394
|
-
role,
|
|
395
|
-
timestamp,
|
|
396
|
-
message
|
|
397
|
-
});
|
|
398
|
-
closeActiveTurn();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return timeline;
|
|
402
|
-
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { SessionMessageView } from '@/api/types';
|
|
2
|
+
import { extractMessageText } from '@/lib/chat-message';
|
|
3
|
+
import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
|
|
4
|
+
|
|
5
|
+
export { isAbortLikeError, formatSendError, buildLocalAssistantMessage } from '@nextclaw/agent-chat';
|
|
6
|
+
|
|
7
|
+
export function normalizeRequestedSkills(value: string[] | undefined): string[] {
|
|
8
|
+
if (!Array.isArray(value)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const deduped = new Set<string>();
|
|
12
|
+
for (const item of value) {
|
|
13
|
+
const trimmed = item.trim();
|
|
14
|
+
if (trimmed) {
|
|
15
|
+
deduped.add(trimmed);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...deduped];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildUiMessagesFromHistoryMessages(messages: SessionMessageView[]): UIMessage[] {
|
|
22
|
+
const normalizedToolRoles = new Set(['tool', 'tool_result', 'toolresult', 'function']);
|
|
23
|
+
const output: UIMessage[] = [];
|
|
24
|
+
let cursor = 0;
|
|
25
|
+
let assistantIndex = 0;
|
|
26
|
+
let activeAssistant: UIMessage | null = null;
|
|
27
|
+
|
|
28
|
+
const buildId = (role: UIMessage['role'], timestamp: string) => {
|
|
29
|
+
cursor += 1;
|
|
30
|
+
return `history-${role}-${timestamp || 'unknown'}-${cursor}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const parseArgsPayload = (raw: unknown): { args: string; parsedArgs?: unknown } => {
|
|
34
|
+
const args = typeof raw === 'string' ? raw : JSON.stringify(raw ?? {});
|
|
35
|
+
try {
|
|
36
|
+
return { args, parsedArgs: JSON.parse(args) };
|
|
37
|
+
} catch {
|
|
38
|
+
return { args };
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const findToolPartIndex = (parts: UIMessage['parts'], toolCallId: string): number => {
|
|
43
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
const part = parts[index];
|
|
45
|
+
if (part.type === 'tool-invocation' && part.toolInvocation.toolCallId === toolCallId) {
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return -1;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ensureAssistant = (timestamp: string): UIMessage => {
|
|
53
|
+
if (activeAssistant) {
|
|
54
|
+
activeAssistant = {
|
|
55
|
+
...activeAssistant,
|
|
56
|
+
meta: {
|
|
57
|
+
...activeAssistant.meta,
|
|
58
|
+
timestamp
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return activeAssistant;
|
|
62
|
+
}
|
|
63
|
+
assistantIndex += 1;
|
|
64
|
+
activeAssistant = {
|
|
65
|
+
id: `history-assistant-${assistantIndex}-${timestamp || 'unknown'}`,
|
|
66
|
+
role: 'assistant',
|
|
67
|
+
parts: [],
|
|
68
|
+
meta: {
|
|
69
|
+
source: 'history',
|
|
70
|
+
status: 'final',
|
|
71
|
+
timestamp
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return activeAssistant;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const flushAssistant = () => {
|
|
78
|
+
if (!activeAssistant) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (activeAssistant.parts.length > 0) {
|
|
82
|
+
output.push(activeAssistant);
|
|
83
|
+
}
|
|
84
|
+
activeAssistant = null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const appendAssistantText = (timestamp: string, text: string) => {
|
|
88
|
+
if (!text) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const assistant = ensureAssistant(timestamp);
|
|
92
|
+
assistant.parts = [...assistant.parts, { type: 'text', text }];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const appendAssistantReasoning = (timestamp: string, reasoning: string) => {
|
|
96
|
+
if (!reasoning) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const assistant = ensureAssistant(timestamp);
|
|
100
|
+
assistant.parts = [...assistant.parts, { type: 'reasoning', reasoning, details: [] }];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const appendAssistantToolCall = (params: {
|
|
104
|
+
timestamp: string;
|
|
105
|
+
toolCallId: string;
|
|
106
|
+
toolName: string;
|
|
107
|
+
args: string;
|
|
108
|
+
parsedArgs?: unknown;
|
|
109
|
+
}) => {
|
|
110
|
+
const assistant = ensureAssistant(params.timestamp);
|
|
111
|
+
const partIndex = findToolPartIndex(assistant.parts, params.toolCallId);
|
|
112
|
+
const part = {
|
|
113
|
+
type: 'tool-invocation' as const,
|
|
114
|
+
toolInvocation: {
|
|
115
|
+
status: ToolInvocationStatus.CALL,
|
|
116
|
+
toolCallId: params.toolCallId,
|
|
117
|
+
toolName: params.toolName,
|
|
118
|
+
args: params.args,
|
|
119
|
+
parsedArgs: params.parsedArgs
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (partIndex >= 0) {
|
|
123
|
+
assistant.parts = [...assistant.parts.slice(0, partIndex), part, ...assistant.parts.slice(partIndex + 1)];
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
assistant.parts = [...assistant.parts, part];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const appendAssistantToolResult = (params: {
|
|
130
|
+
timestamp: string;
|
|
131
|
+
toolCallId: string;
|
|
132
|
+
toolName: string;
|
|
133
|
+
result: unknown;
|
|
134
|
+
}) => {
|
|
135
|
+
if (!params.toolCallId) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const assistant = ensureAssistant(params.timestamp);
|
|
139
|
+
const partIndex = findToolPartIndex(assistant.parts, params.toolCallId);
|
|
140
|
+
if (partIndex < 0) {
|
|
141
|
+
assistant.parts = [
|
|
142
|
+
...assistant.parts,
|
|
143
|
+
{
|
|
144
|
+
type: 'tool-invocation',
|
|
145
|
+
toolInvocation: {
|
|
146
|
+
status: ToolInvocationStatus.RESULT,
|
|
147
|
+
toolCallId: params.toolCallId,
|
|
148
|
+
toolName: params.toolName,
|
|
149
|
+
args: '{}',
|
|
150
|
+
parsedArgs: undefined,
|
|
151
|
+
result: params.result
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const part = assistant.parts[partIndex];
|
|
158
|
+
if (part.type !== 'tool-invocation') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
assistant.parts = [
|
|
162
|
+
...assistant.parts.slice(0, partIndex),
|
|
163
|
+
{
|
|
164
|
+
...part,
|
|
165
|
+
toolInvocation: {
|
|
166
|
+
...part.toolInvocation,
|
|
167
|
+
status: ToolInvocationStatus.RESULT,
|
|
168
|
+
result: params.result
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
...assistant.parts.slice(partIndex + 1)
|
|
172
|
+
];
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for (const message of messages) {
|
|
176
|
+
const roleValue = message.role?.toLowerCase().trim();
|
|
177
|
+
if (!roleValue) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const timestamp = message.timestamp;
|
|
181
|
+
|
|
182
|
+
if (roleValue === 'user' || roleValue === 'system' || roleValue === 'data') {
|
|
183
|
+
flushAssistant();
|
|
184
|
+
const text = extractMessageText(message.content).trim();
|
|
185
|
+
if (!text) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
output.push({
|
|
189
|
+
id: buildId(roleValue as UIMessage['role'], timestamp),
|
|
190
|
+
role: roleValue as UIMessage['role'],
|
|
191
|
+
parts: [{ type: 'text', text }],
|
|
192
|
+
meta: {
|
|
193
|
+
source: 'history',
|
|
194
|
+
status: 'final',
|
|
195
|
+
timestamp
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (roleValue === 'assistant') {
|
|
202
|
+
const text = extractMessageText(message.content).trim();
|
|
203
|
+
if (text) {
|
|
204
|
+
appendAssistantText(timestamp, text);
|
|
205
|
+
}
|
|
206
|
+
if (typeof message.reasoning_content === 'string' && message.reasoning_content.trim()) {
|
|
207
|
+
appendAssistantReasoning(timestamp, message.reasoning_content.trim());
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(message.tool_calls)) {
|
|
210
|
+
for (const call of message.tool_calls) {
|
|
211
|
+
if (!call || typeof call !== 'object') {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const callRecord = call as Record<string, unknown>;
|
|
215
|
+
const fnValue = callRecord.function;
|
|
216
|
+
const fn = typeof fnValue === 'object' && fnValue ? (fnValue as { name?: unknown; arguments?: unknown }) : null;
|
|
217
|
+
const toolCallId = typeof callRecord.id === 'string' ? callRecord.id.trim() : '';
|
|
218
|
+
if (!toolCallId) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const toolName =
|
|
222
|
+
typeof fn?.name === 'string' ? fn.name : typeof callRecord.name === 'string' ? callRecord.name : 'tool';
|
|
223
|
+
const payload = parseArgsPayload(fn?.arguments ?? callRecord.arguments ?? '');
|
|
224
|
+
appendAssistantToolCall({
|
|
225
|
+
timestamp,
|
|
226
|
+
toolCallId,
|
|
227
|
+
toolName,
|
|
228
|
+
args: payload.args,
|
|
229
|
+
parsedArgs: payload.parsedArgs
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (normalizedToolRoles.has(roleValue)) {
|
|
237
|
+
const toolCallId = typeof message.tool_call_id === 'string' ? message.tool_call_id.trim() : '';
|
|
238
|
+
const toolName = typeof message.name === 'string' && message.name.trim() ? message.name.trim() : 'tool';
|
|
239
|
+
appendAssistantToolResult({
|
|
240
|
+
timestamp,
|
|
241
|
+
toolCallId,
|
|
242
|
+
toolName,
|
|
243
|
+
result: message.content
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
flushAssistant();
|
|
249
|
+
return output;
|
|
250
|
+
}
|
package/src/lib/i18n.ts
CHANGED
|
@@ -263,6 +263,8 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
263
263
|
providerAuthOpenPrompt: { zh: '请在浏览器完成授权,验证码:', en: 'Open browser and complete authorization (code: ' },
|
|
264
264
|
providerAuthOpenPromptSuffix: { zh: '', en: ')' },
|
|
265
265
|
providerAuthStartFailed: { zh: '启动授权失败', en: 'Failed to start authorization' },
|
|
266
|
+
providerAuthMethodLabel: { zh: '授权区域', en: 'Authorization Region' },
|
|
267
|
+
providerAuthMethodPlaceholder: { zh: '请选择授权方式', en: 'Select authorization method' },
|
|
266
268
|
providerAuthImportFromCli: { zh: '从 Qwen CLI 导入', en: 'Import From Qwen CLI' },
|
|
267
269
|
providerAuthImporting: { zh: '导入中...', en: 'Importing...' },
|
|
268
270
|
providerAuthImportSuccess: { zh: '已从 CLI 导入凭证。', en: 'Imported provider credentials from CLI.' },
|
|
@@ -504,6 +506,14 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
504
506
|
chatSelectAgent: { zh: '选择 Agent', en: 'Select Agent' },
|
|
505
507
|
chatModelLabel: { zh: '对话模型', en: 'Chat Model' },
|
|
506
508
|
chatSelectModel: { zh: '选择模型', en: 'Select model' },
|
|
509
|
+
chatSessionTypeLabel: { zh: '会话类型', en: 'Session Type' },
|
|
510
|
+
chatSessionTypeNative: { zh: '原生', en: 'Native' },
|
|
511
|
+
chatSessionTypeCodex: { zh: 'Codex', en: 'Codex' },
|
|
512
|
+
chatSessionTypeClaude: { zh: 'Claude Code', en: 'Claude Code' },
|
|
513
|
+
chatSessionTypeUnavailableSuffix: {
|
|
514
|
+
zh: '当前不可用,请启用对应插件或新建 Native 会话。',
|
|
515
|
+
en: 'is unavailable now. Re-enable the plugin or create a native session.'
|
|
516
|
+
},
|
|
507
517
|
chatModelNoOptions: { zh: '暂无可用模型,请先配置提供商。', en: 'No available models. Configure a provider first.' },
|
|
508
518
|
chatGoConfigureProvider: { zh: '去配置提供商', en: 'Go to Providers' },
|
|
509
519
|
chatProviderSetupTitle: { zh: '开始前先配置提供商', en: 'Configure a Provider First' },
|
|
@@ -537,6 +547,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
537
547
|
chatQueueSend: { zh: '排队发送', en: 'Queue' },
|
|
538
548
|
chatQueuedHintPrefix: { zh: '当前有', en: 'Queued' },
|
|
539
549
|
chatQueuedHintSuffix: { zh: '条消息待发送。', en: 'pending messages.' },
|
|
550
|
+
chatQueueMoveFirst: { zh: '置顶到下一条', en: 'Move to Next' },
|
|
540
551
|
chatDeleteSession: { zh: '删除会话', en: 'Delete Session' },
|
|
541
552
|
chatDeleteSessionConfirm: { zh: '确认删除当前会话?', en: 'Delete the current session?' },
|
|
542
553
|
chatSendFailed: { zh: '发送消息失败', en: 'Failed to send message' },
|
package/tsconfig.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -9,7 +9,8 @@ export default defineConfig({
|
|
|
9
9
|
plugins: [react(), splitVendorChunkPlugin()],
|
|
10
10
|
resolve: {
|
|
11
11
|
alias: {
|
|
12
|
-
'@': path.resolve(__dirname, './src')
|
|
12
|
+
'@': path.resolve(__dirname, './src'),
|
|
13
|
+
'@nextclaw/agent-chat': path.resolve(__dirname, '../nextclaw-agent-chat/src/index.ts')
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
server: {
|