@nextclaw/ui 0.6.10 → 0.6.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 (90) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/assets/ChannelsList-DBDjwf-X.js +1 -0
  4. package/dist/assets/ChatPage-C18sGGk1.js +36 -0
  5. package/dist/assets/DocBrowser-ZOplDEMS.js +1 -0
  6. package/dist/assets/LogoBadge-2LMzEMwe.js +1 -0
  7. package/dist/assets/MarketplacePage-D4JHYcB5.js +49 -0
  8. package/dist/assets/ModelConfig-DZVvdLFq.js +1 -0
  9. package/dist/assets/ProvidersList-Dum31480.js +1 -0
  10. package/dist/assets/{RuntimeConfig-BO6s-ls-.js → RuntimeConfig-4sb3mpkd.js} +1 -1
  11. package/dist/assets/SearchConfig-B4u_MxRG.js +1 -0
  12. package/dist/assets/{SecretsConfig-mayFdxpM.js → SecretsConfig-BQXblZvb.js} +2 -2
  13. package/dist/assets/SessionsConfig-Jk29xjQU.js +2 -0
  14. package/dist/assets/{card-BP5YnL-G.js → card-BekAnCgX.js} +1 -1
  15. package/dist/assets/config-layout-BHnOoweL.js +1 -0
  16. package/dist/assets/index-BXwjfCEO.css +1 -0
  17. package/dist/assets/index-Dl6t70wA.js +8 -0
  18. package/dist/assets/{input-B1D2QX0O.js → input-MMn_Na9q.js} +1 -1
  19. package/dist/assets/{label-DW0j-fXA.js → label-Dg2ydpN0.js} +1 -1
  20. package/dist/assets/{page-layout-Ch-H9gD-.js → page-layout-7K0rcz0I.js} +1 -1
  21. package/dist/assets/session-run-status-CAdjSqeb.js +3 -0
  22. package/dist/assets/{switch-_cZHlGKB.js → switch-DnDMlDVu.js} +1 -1
  23. package/dist/assets/{tabs-custom-ARxqYYjG.js → tabs-custom-khLM8lWj.js} +1 -1
  24. package/dist/assets/{useConfirmDialog-BaU7nIat.js → useConfirmDialog-BYA1XnVU.js} +2 -2
  25. package/dist/assets/{vendor-C--HHaLf.js → vendor-d7E8OgNx.js} +84 -84
  26. package/dist/index.html +3 -3
  27. package/package.json +4 -2
  28. package/src/App.tsx +3 -2
  29. package/src/api/config.ts +212 -200
  30. package/src/api/types.ts +93 -24
  31. package/src/components/chat/ChatConversationPanel.tsx +102 -121
  32. package/src/components/chat/ChatPage.tsx +165 -437
  33. package/src/components/chat/ChatSidebar.tsx +30 -36
  34. package/src/components/chat/ChatThread.tsx +73 -131
  35. package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
  36. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
  37. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
  38. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
  39. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
  40. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
  41. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
  42. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
  43. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
  44. package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
  45. package/src/components/chat/chat-input.types.ts +15 -0
  46. package/src/components/chat/chat-page-data.ts +121 -0
  47. package/src/components/chat/chat-page-runtime.ts +221 -0
  48. package/src/components/chat/chat-session-route.ts +59 -0
  49. package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
  50. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
  51. package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
  52. package/src/components/chat/chat-stream/transport.ts +159 -0
  53. package/src/components/chat/chat-stream/types.ts +76 -0
  54. package/src/components/chat/managers/chat-input.manager.ts +142 -0
  55. package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
  56. package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
  57. package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
  58. package/src/components/chat/managers/chat-thread.manager.ts +86 -0
  59. package/src/components/chat/managers/chat-ui.manager.ts +65 -0
  60. package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
  61. package/src/components/chat/presenter/chat.presenter.ts +32 -0
  62. package/src/components/chat/stores/chat-input.store.ts +62 -0
  63. package/src/components/chat/stores/chat-run-status.store.ts +30 -0
  64. package/src/components/chat/stores/chat-session-list.store.ts +34 -0
  65. package/src/components/chat/stores/chat-thread.store.ts +52 -0
  66. package/src/components/chat/useChatRuntimeController.ts +134 -0
  67. package/src/components/chat/useChatSessionTypeState.ts +148 -0
  68. package/src/components/common/MaskedInput.tsx +1 -1
  69. package/src/components/config/SearchConfig.tsx +297 -0
  70. package/src/components/layout/Sidebar.tsx +6 -1
  71. package/src/hooks/useConfig.ts +48 -1
  72. package/src/hooks/useObservable.ts +20 -0
  73. package/src/lib/chat-message.ts +2 -202
  74. package/src/lib/chat-runtime-utils.ts +250 -0
  75. package/src/lib/i18n.ts +31 -0
  76. package/tsconfig.json +2 -1
  77. package/vite.config.ts +2 -1
  78. package/dist/assets/ChannelsList-TyMb5Mgz.js +0 -1
  79. package/dist/assets/ChatPage-CQerYqvy.js +0 -34
  80. package/dist/assets/DocBrowser-CNtrA0ps.js +0 -1
  81. package/dist/assets/LogoBadge-BLqiOM5D.js +0 -1
  82. package/dist/assets/MarketplacePage-CotZxxNe.js +0 -49
  83. package/dist/assets/ModelConfig-CCsQ8KFq.js +0 -1
  84. package/dist/assets/ProvidersList-BYYX5K_g.js +0 -1
  85. package/dist/assets/SessionsConfig-DAIczdBj.js +0 -2
  86. package/dist/assets/index-BUiahmWm.css +0 -1
  87. package/dist/assets/index-D6_5HaDl.js +0 -7
  88. package/dist/assets/session-run-status-BUYsQeWs.js +0 -5
  89. package/src/components/chat/ChatInputBar.tsx +0 -590
  90. package/src/components/chat/useChatStreamController.ts +0 -591
@@ -1,7 +1,7 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
3
3
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
4
- import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft } from 'lucide-react';
4
+ import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search } from 'lucide-react';
5
5
  import { NavLink } from 'react-router-dom';
6
6
  import { useDocBrowser } from '@/components/doc-browser';
7
7
  import { BrandHeader } from '@/components/common/BrandHeader';
@@ -67,6 +67,11 @@ export function Sidebar({ mode }: SidebarProps) {
67
67
  label: t('providers'),
68
68
  icon: Sparkles,
69
69
  },
70
+ {
71
+ target: '/search',
72
+ label: t('searchChannels'),
73
+ icon: Search,
74
+ },
70
75
  {
71
76
  target: '/channels',
72
77
  label: t('channels'),
@@ -5,6 +5,7 @@ import {
5
5
  fetchConfigMeta,
6
6
  fetchConfigSchema,
7
7
  updateModel,
8
+ updateSearch,
8
9
  createProvider,
9
10
  deleteProvider,
10
11
  updateProvider,
@@ -24,6 +25,7 @@ import {
24
25
  fetchChatRun,
25
26
  fetchChatRuns,
26
27
  fetchChatCapabilities,
28
+ fetchChatSessionTypes,
27
29
  fetchCronJobs,
28
30
  deleteCronJob,
29
31
  setCronJobEnabled,
@@ -80,6 +82,22 @@ export function useUpdateModel() {
80
82
  });
81
83
  }
82
84
 
85
+ export function useUpdateSearch() {
86
+ const queryClient = useQueryClient();
87
+
88
+ return useMutation({
89
+ mutationFn: ({ data }: { data: Parameters<typeof updateSearch>[0] }) => updateSearch(data),
90
+ onSuccess: () => {
91
+ queryClient.invalidateQueries({ queryKey: ['config'] });
92
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
93
+ toast.success(t('configSavedApplied'));
94
+ },
95
+ onError: (error: Error) => {
96
+ toast.error(t('configSaveFailed') + ': ' + error.message);
97
+ }
98
+ });
99
+ }
100
+
83
101
  export function useUpdateProvider() {
84
102
  const queryClient = useQueryClient();
85
103
 
@@ -294,9 +312,26 @@ export function useChatCapabilities(params?: { sessionKey?: string | null; agent
294
312
  });
295
313
  }
296
314
 
297
- export function useChatRuns(params?: { sessionKey?: string | null; states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>; limit?: number }) {
315
+ export function useChatSessionTypes() {
316
+ return useQuery({
317
+ queryKey: ['chat-session-types'],
318
+ queryFn: fetchChatSessionTypes,
319
+ staleTime: 10_000,
320
+ retry: false
321
+ });
322
+ }
323
+
324
+ export function useChatRuns(params?: {
325
+ sessionKey?: string | null;
326
+ states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>;
327
+ limit?: number;
328
+ syncActiveStates?: boolean;
329
+ isLocallyRunning?: boolean;
330
+ }) {
298
331
  const sessionKey = params?.sessionKey?.trim() || undefined;
299
332
  const states = Array.isArray(params?.states) && params.states.length > 0 ? params.states : undefined;
333
+ const isActiveStatesQuery = Boolean(states?.some((state) => state === 'queued' || state === 'running'));
334
+ const shouldSyncActiveStates = Boolean(params?.syncActiveStates && isActiveStatesQuery);
300
335
  return useQuery({
301
336
  queryKey: ['chat-runs', sessionKey ?? null, states ?? null, params?.limit ?? null],
302
337
  queryFn: () => fetchChatRuns({
@@ -306,6 +341,18 @@ export function useChatRuns(params?: { sessionKey?: string | null; states?: Arra
306
341
  }),
307
342
  enabled: Boolean(sessionKey) || Boolean(states),
308
343
  staleTime: 5_000,
344
+ refetchInterval: (query) => {
345
+ if (!shouldSyncActiveStates) {
346
+ return false;
347
+ }
348
+ if (params?.isLocallyRunning) {
349
+ return 800;
350
+ }
351
+ const data = query.state.data;
352
+ const hasActiveRuns = Array.isArray(data?.runs) && data.runs.length > 0;
353
+ return hasActiveRuns ? 800 : false;
354
+ },
355
+ refetchIntervalInBackground: false,
309
356
  retry: false
310
357
  });
311
358
  }
@@ -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
+ }
@@ -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
@@ -125,6 +125,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
125
125
  // Navigation
126
126
  chat: { zh: '对话', en: 'Chat' },
127
127
  model: { zh: '模型', en: 'Model' },
128
+ searchChannels: { zh: '搜索渠道', en: 'Search Channels' },
128
129
  providers: { zh: '提供商', en: 'Providers' },
129
130
  channels: { zh: '渠道', en: 'Channels' },
130
131
  cron: { zh: '定时任务', en: 'Cron Jobs' },
@@ -182,6 +183,27 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
182
183
  saveChanges: { zh: '保存变更', en: 'Save Changes' },
183
184
 
184
185
  // Provider
186
+ searchPageTitle: { zh: '搜索渠道', en: 'Search Channels' },
187
+ searchPageDescription: { zh: '配置网页搜索提供商', en: 'Configure web search providers.' },
188
+ searchActiveProvider: { zh: '当前搜索提供商', en: 'Active Search Provider' },
189
+ searchDefaultMaxResults: { zh: '默认返回条数', en: 'Default Result Count' },
190
+ searchProviderSummary: { zh: '结果摘要', en: 'Result Summary' },
191
+ searchProviderFreshness: { zh: '时间范围', en: 'Freshness' },
192
+ searchProviderBaseUrl: { zh: '接口地址', en: 'API Base URL' },
193
+ searchProviderOpenDocs: { zh: '获取博查 API', en: 'Get Bocha API' },
194
+ searchProviderActivate: { zh: '激活', en: 'Activate' },
195
+ searchProviderActivated: { zh: '已激活', en: 'Activated' },
196
+ searchProviderDeactivate: { zh: '取消激活', en: 'Deactivate' },
197
+ searchProviderBochaDescription: { zh: '更适合中国大陆用户的 AI 搜索。', en: 'AI-ready search that works better for mainland China users.' },
198
+ searchProviderBraveDescription: { zh: '保留 Brave 作为可选 provider。', en: 'Keep Brave as an optional provider.' },
199
+ searchStatusConfigured: { zh: '已配置', en: 'Configured' },
200
+ searchStatusNeedsSetup: { zh: '待配置', en: 'Needs Setup' },
201
+ searchFreshnessNoLimit: { zh: '不限', en: 'No Limit' },
202
+ searchFreshnessOneDay: { zh: '一天内', en: 'One Day' },
203
+ searchFreshnessOneWeek: { zh: '一周内', en: 'One Week' },
204
+ searchFreshnessOneMonth: { zh: '一个月内', en: 'One Month' },
205
+ searchFreshnessOneYear: { zh: '一年内', en: 'One Year' },
206
+ searchNoProviderSelected: { zh: '请选择左侧搜索 provider', en: 'Select a search provider from the left.' },
185
207
  providersPageTitle: { zh: 'AI 提供商', en: 'AI Providers' },
186
208
  providersPageDescription: { zh: '在一个页面内完成提供商切换、配置与保存。', en: 'Switch, configure, and save providers in one continuous workspace.' },
187
209
  providersLoading: { zh: '加载中...', en: 'Loading...' },
@@ -506,6 +528,14 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
506
528
  chatSelectAgent: { zh: '选择 Agent', en: 'Select Agent' },
507
529
  chatModelLabel: { zh: '对话模型', en: 'Chat Model' },
508
530
  chatSelectModel: { zh: '选择模型', en: 'Select model' },
531
+ chatSessionTypeLabel: { zh: '会话类型', en: 'Session Type' },
532
+ chatSessionTypeNative: { zh: '原生', en: 'Native' },
533
+ chatSessionTypeCodex: { zh: 'Codex', en: 'Codex' },
534
+ chatSessionTypeClaude: { zh: 'Claude Code', en: 'Claude Code' },
535
+ chatSessionTypeUnavailableSuffix: {
536
+ zh: '当前不可用,请启用对应插件或新建 Native 会话。',
537
+ en: 'is unavailable now. Re-enable the plugin or create a native session.'
538
+ },
509
539
  chatModelNoOptions: { zh: '暂无可用模型,请先配置提供商。', en: 'No available models. Configure a provider first.' },
510
540
  chatGoConfigureProvider: { zh: '去配置提供商', en: 'Go to Providers' },
511
541
  chatProviderSetupTitle: { zh: '开始前先配置提供商', en: 'Configure a Provider First' },
@@ -539,6 +569,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
539
569
  chatQueueSend: { zh: '排队发送', en: 'Queue' },
540
570
  chatQueuedHintPrefix: { zh: '当前有', en: 'Queued' },
541
571
  chatQueuedHintSuffix: { zh: '条消息待发送。', en: 'pending messages.' },
572
+ chatQueueMoveFirst: { zh: '置顶到下一条', en: 'Move to Next' },
542
573
  chatDeleteSession: { zh: '删除会话', en: 'Delete Session' },
543
574
  chatDeleteSessionConfirm: { zh: '确认删除当前会话?', en: 'Delete the current session?' },
544
575
  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: {