@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.
Files changed (83) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
  4. package/dist/assets/ChatPage-DIx05c6s.js +36 -0
  5. package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
  6. package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
  7. package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
  8. package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
  9. package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
  10. package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
  11. package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
  12. package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
  13. package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
  14. package/dist/assets/index-Cn6_2To7.js +8 -0
  15. package/dist/assets/index-nEYGCJTC.css +1 -0
  16. package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
  17. package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
  18. package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
  19. package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
  20. package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
  21. package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
  22. package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
  23. package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
  24. package/dist/index.html +3 -3
  25. package/package.json +4 -2
  26. package/src/App.tsx +1 -2
  27. package/src/api/config.ts +205 -202
  28. package/src/api/types.ts +54 -24
  29. package/src/components/chat/ChatConversationPanel.tsx +102 -121
  30. package/src/components/chat/ChatPage.tsx +165 -437
  31. package/src/components/chat/ChatSidebar.tsx +30 -36
  32. package/src/components/chat/ChatThread.tsx +73 -131
  33. package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
  34. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
  35. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
  36. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
  37. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
  38. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
  39. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
  40. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
  41. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
  42. package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
  43. package/src/components/chat/chat-input.types.ts +15 -0
  44. package/src/components/chat/chat-page-data.ts +121 -0
  45. package/src/components/chat/chat-page-runtime.ts +221 -0
  46. package/src/components/chat/chat-session-route.ts +59 -0
  47. package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
  48. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
  49. package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
  50. package/src/components/chat/chat-stream/transport.ts +159 -0
  51. package/src/components/chat/chat-stream/types.ts +76 -0
  52. package/src/components/chat/managers/chat-input.manager.ts +142 -0
  53. package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
  54. package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
  55. package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
  56. package/src/components/chat/managers/chat-thread.manager.ts +86 -0
  57. package/src/components/chat/managers/chat-ui.manager.ts +65 -0
  58. package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
  59. package/src/components/chat/presenter/chat.presenter.ts +32 -0
  60. package/src/components/chat/stores/chat-input.store.ts +62 -0
  61. package/src/components/chat/stores/chat-run-status.store.ts +30 -0
  62. package/src/components/chat/stores/chat-session-list.store.ts +34 -0
  63. package/src/components/chat/stores/chat-thread.store.ts +52 -0
  64. package/src/components/chat/useChatRuntimeController.ts +134 -0
  65. package/src/components/chat/useChatSessionTypeState.ts +148 -0
  66. package/src/components/common/MaskedInput.tsx +1 -1
  67. package/src/components/config/ProviderForm.tsx +221 -14
  68. package/src/hooks/useConfig.ts +33 -2
  69. package/src/hooks/useObservable.ts +20 -0
  70. package/src/hooks/useWebSocket.ts +23 -1
  71. package/src/lib/chat-message.ts +2 -202
  72. package/src/lib/chat-runtime-utils.ts +250 -0
  73. package/src/lib/i18n.ts +11 -0
  74. package/tsconfig.json +2 -1
  75. package/vite.config.ts +2 -1
  76. package/dist/assets/ChatPage-iji0RkTR.js +0 -34
  77. package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
  78. package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
  79. package/dist/assets/index-C_DhisNo.css +0 -1
  80. package/dist/assets/index-dKTqKCJo.js +0 -7
  81. package/dist/assets/session-run-status-CllIZxNf.js +0 -5
  82. package/src/components/chat/ChatInputBar.tsx +0 -590
  83. package/src/components/chat/useChatStreamController.ts +0 -591
@@ -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 }) => startProviderAuth(provider)
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 useChatRuns(params?: { sessionKey?: string | null; states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>; limit?: number }) {
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);
@@ -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
@@ -11,7 +11,8 @@
11
11
  "types": ["vite/client"],
12
12
  "baseUrl": ".",
13
13
  "paths": {
14
- "@/*": ["./src/*"]
14
+ "@/*": ["./src/*"],
15
+ "@nextclaw/agent-chat": ["../nextclaw-agent-chat/src/index.ts"]
15
16
  }
16
17
  },
17
18
  "include": ["src"]
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: {