@nextclaw/ui 0.5.26 → 0.5.29

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 (33) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/assets/{ChannelsList-D0Wk08Ki.js → ChannelsList-DEr4kE7H.js} +1 -1
  3. package/dist/assets/ChatPage-DI2euxZy.js +32 -0
  4. package/dist/assets/{CronConfig-D-3Y8kWb.js → CronConfig-DAlt-x5i.js} +1 -1
  5. package/dist/assets/{DocBrowser-BSPKhqrK.js → DocBrowser-TrMsdXgx.js} +1 -1
  6. package/dist/assets/{MarketplacePage-Dkm2FTtN.js → MarketplacePage-Dwm527F7.js} +1 -1
  7. package/dist/assets/{ModelConfig-2cpAmvGq.js → ModelConfig-srggzgfA.js} +1 -1
  8. package/dist/assets/{ProvidersList-Dot21pAy.js → ProvidersList-8kFCDiqC.js} +1 -1
  9. package/dist/assets/{RuntimeConfig-BNw_Ms_Y.js → RuntimeConfig-CLbdKAlo.js} +1 -1
  10. package/dist/assets/{SecretsConfig-z8M3PDJP.js → SecretsConfig-DXCdR0Be.js} +1 -1
  11. package/dist/assets/{SessionsConfig-XVHZ-FG5.js → SessionsConfig-iKpz3Sts.js} +1 -1
  12. package/dist/assets/{action-link-CpPJJN-z.js → action-link-w4jS8X9q.js} +1 -1
  13. package/dist/assets/{card-DsZ2Am92.js → card-CVj65Dvi.js} +1 -1
  14. package/dist/assets/chat-message-D0s61C4e.js +5 -0
  15. package/dist/assets/{dialog-BysNu5hM.js → dialog-lK79rlAw.js} +1 -1
  16. package/dist/assets/{index-Bny21Br0.js → index-BXgULtdk.js} +2 -2
  17. package/dist/assets/{label-q6RASlER.js → label-l-fECYi3.js} +1 -1
  18. package/dist/assets/{page-layout-WiVrFc8t.js → page-layout-BghxFaNt.js} +1 -1
  19. package/dist/assets/{switch-DM_YYUgB.js → switch-B4yFzIbc.js} +1 -1
  20. package/dist/assets/{tabs-custom-mlgm-IGH.js → tabs-custom-B4q02QSV.js} +1 -1
  21. package/dist/assets/useConfig-C9k3TmQk.js +6 -0
  22. package/dist/assets/{useConfirmDialog-DamaA60g.js → useConfirmDialog-C20D5SYn.js} +1 -1
  23. package/dist/index.html +1 -1
  24. package/package.json +1 -1
  25. package/src/api/config.ts +14 -1
  26. package/src/api/types.ts +14 -0
  27. package/src/components/chat/ChatPage.tsx +95 -35
  28. package/src/components/chat/ChatThread.tsx +58 -32
  29. package/src/hooks/useConfig.ts +2 -1
  30. package/src/lib/chat-message.ts +169 -153
  31. package/dist/assets/ChatPage-Deg2lBH4.js +0 -32
  32. package/dist/assets/chat-message-Jxa8JFA_.js +0 -9
  33. package/dist/assets/useConfig-BOn-kp8G.js +0 -6
@@ -1,4 +1,4 @@
1
- import type { SessionMessageView } from '@/api/types';
1
+ import type { SessionEventView, SessionMessageView } from '@/api/types';
2
2
 
3
3
  export type ChatRole = 'user' | 'assistant' | 'tool' | 'system' | 'other';
4
4
 
@@ -11,14 +11,28 @@ export type ToolCard = {
11
11
  hasResult?: boolean;
12
12
  };
13
13
 
14
- export type GroupedChatMessage = {
14
+ export type ChatTimelineMessageItem = {
15
+ kind: 'message';
15
16
  key: string;
16
17
  role: ChatRole;
17
- messages: SessionMessageView[];
18
18
  timestamp: string;
19
+ message: SessionMessageView;
19
20
  };
20
21
 
21
- const MERGE_WINDOW_MS = 2 * 60 * 1000;
22
+ export type ChatTimelineAssistantFlowItem = {
23
+ kind: 'assistant_flow';
24
+ key: string;
25
+ role: 'assistant';
26
+ timestamp: string;
27
+ primaryText: string;
28
+ primaryReasoning: string;
29
+ followupText: string;
30
+ followupReasoning: string;
31
+ toolCards: ToolCard[];
32
+ };
33
+
34
+ export type ChatTimelineItem = ChatTimelineMessageItem | ChatTimelineAssistantFlowItem;
35
+
22
36
  const TOOL_DETAIL_FIELDS = ['cmd', 'command', 'query', 'q', 'path', 'url', 'to', 'channel', 'agentId', 'sessionKey'];
23
37
 
24
38
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -104,30 +118,6 @@ function hasToolCalls(message: SessionMessageView): boolean {
104
118
  return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
105
119
  }
106
120
 
107
- function mergeMessageContent(base: unknown, addition: unknown): unknown {
108
- const left = extractMessageText(base).trim();
109
- const right = extractMessageText(addition).trim();
110
- if (!left) {
111
- return right;
112
- }
113
- if (!right) {
114
- return left;
115
- }
116
- return `${left}\n\n${right}`;
117
- }
118
-
119
- function mergeReasoningContent(base: unknown, addition: unknown): string | undefined {
120
- const left = typeof base === 'string' ? base.trim() : '';
121
- const right = typeof addition === 'string' ? addition.trim() : '';
122
- if (!left) {
123
- return right || undefined;
124
- }
125
- if (!right) {
126
- return left;
127
- }
128
- return `${left}\n\n${right}`;
129
- }
130
-
131
121
  export function normalizeChatRole(message: Pick<SessionMessageView, 'role' | 'name' | 'tool_call_id' | 'tool_calls'>): ChatRole {
132
122
  const role = message.role.toLowerCase().trim();
133
123
  if (role === 'user') {
@@ -177,7 +167,7 @@ export function extractMessageText(content: unknown): string {
177
167
  return stringifyUnknown(content);
178
168
  }
179
169
 
180
- export function extractToolCards(message: SessionMessageView): ToolCard[] {
170
+ function buildToolCallCards(message: SessionMessageView): ToolCard[] {
181
171
  const cards: ToolCard[] = [];
182
172
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
183
173
  for (const call of toolCalls) {
@@ -187,176 +177,202 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
187
177
  const fn = isRecord(call.function) ? call.function : null;
188
178
  const name = toToolName(fn?.name ?? call.name);
189
179
  const args = fn?.arguments ?? call.arguments;
190
- const resultText = typeof call.result_text === 'string' ? call.result_text.trim() : '';
191
- const hasResult = call.has_result === true || typeof call.result_text === 'string';
192
180
  cards.push({
193
181
  kind: 'call',
194
182
  name,
195
183
  detail: summarizeToolArgs(args),
196
- callId: typeof call.id === 'string' ? call.id : undefined,
197
- text: resultText,
198
- hasResult
184
+ callId: typeof call.id === 'string' && call.id.trim() ? call.id : undefined,
185
+ hasResult: false
199
186
  });
200
187
  }
188
+ return cards;
189
+ }
201
190
 
191
+ export function extractToolCards(message: SessionMessageView): ToolCard[] {
192
+ const cards = buildToolCallCards(message);
202
193
  const role = normalizeChatRole(message);
203
194
  if (role === 'tool' || typeof message.tool_call_id === 'string') {
204
- const text = extractMessageText(message.content).trim();
205
195
  cards.push({
206
196
  kind: 'result',
207
197
  name: toToolName(message.name ?? cards[0]?.name),
208
- text,
198
+ text: extractMessageText(message.content).trim(),
209
199
  callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined,
210
200
  hasResult: true
211
201
  });
212
202
  }
213
-
214
203
  return cards;
215
204
  }
216
205
 
217
- type ToolResultBucket = {
218
- name?: string;
219
- texts: string[];
220
- };
221
-
222
- function cloneMessageForMerge(message: SessionMessageView): SessionMessageView {
206
+ function normalizeEvent(event: SessionEventView, index: number): SessionEventView & { _idx: number; _seq: number } {
207
+ const seq = Number.isFinite(event.seq) && event.seq > 0 ? Math.trunc(event.seq) : index + 1;
208
+ const timestamp =
209
+ typeof event.timestamp === 'string' && event.timestamp
210
+ ? event.timestamp
211
+ : event.message?.timestamp ?? new Date().toISOString();
223
212
  return {
224
- ...message,
225
- tool_calls: Array.isArray(message.tool_calls)
226
- ? message.tool_calls.map((call) => (isRecord(call) ? { ...call } : call))
227
- : message.tool_calls
213
+ ...event,
214
+ timestamp,
215
+ _idx: index,
216
+ _seq: seq
228
217
  };
229
218
  }
230
219
 
231
- export function combineToolCallAndResults(messages: SessionMessageView[]): SessionMessageView[] {
232
- const cloned = messages.map(cloneMessageForMerge);
233
- const resultByCallId = new Map<string, ToolResultBucket>();
234
-
235
- for (const message of cloned) {
236
- if (normalizeChatRole(message) !== 'tool') {
237
- continue;
238
- }
239
- if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
240
- continue;
241
- }
242
-
243
- const callId = message.tool_call_id.trim();
244
- const text = extractMessageText(message.content).trim();
245
- const existing = resultByCallId.get(callId) ?? { texts: [] };
246
- if (typeof message.name === 'string' && message.name.trim()) {
247
- existing.name = message.name.trim();
248
- }
249
- existing.texts.push(text);
250
- resultByCallId.set(callId, existing);
220
+ function inferEventTypeFromMessage(message: SessionMessageView): string {
221
+ const role = normalizeChatRole(message);
222
+ if (role === 'assistant' && hasToolCalls(message)) {
223
+ return 'assistant.tool_call';
224
+ }
225
+ if (role === 'tool') {
226
+ return 'tool.result';
251
227
  }
228
+ return `message.${role}`;
229
+ }
252
230
 
253
- const consumedCallIds = new Set<string>();
231
+ export function buildFallbackEventsFromMessages(messages: SessionMessageView[]): SessionEventView[] {
232
+ return messages.map((message, index) => ({
233
+ seq: index + 1,
234
+ type: inferEventTypeFromMessage(message),
235
+ timestamp: message.timestamp,
236
+ message
237
+ }));
238
+ }
254
239
 
255
- for (const message of cloned) {
256
- if (normalizeChatRole(message) !== 'assistant' || !hasToolCalls(message)) {
257
- continue;
258
- }
240
+ function appendText(base: string, next: string): string {
241
+ if (!next) {
242
+ return base;
243
+ }
244
+ if (!base) {
245
+ return next;
246
+ }
247
+ return `${base}\n\n${next}`;
248
+ }
259
249
 
260
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
261
- message.tool_calls = toolCalls.map((call) => {
262
- if (!isRecord(call) || typeof call.id !== 'string') {
263
- return call;
250
+ export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[] {
251
+ const normalized = events
252
+ .map((event, index) => normalizeEvent(event, index))
253
+ .sort((left, right) => {
254
+ if (left._seq !== right._seq) {
255
+ return left._seq - right._seq;
264
256
  }
265
- const result = resultByCallId.get(call.id);
266
- if (!result) {
267
- return call;
257
+ const leftTs = Date.parse(left.timestamp);
258
+ const rightTs = Date.parse(right.timestamp);
259
+ if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
260
+ return leftTs - rightTs;
268
261
  }
269
- consumedCallIds.add(call.id);
270
- return {
271
- ...call,
272
- result_text: result.texts.filter(Boolean).join('\n\n'),
273
- has_result: true,
274
- result_name: result.name
275
- };
276
- }) as Array<Record<string, unknown>>;
277
- }
262
+ return left._idx - right._idx;
263
+ });
278
264
 
279
- const mergedToolResults = cloned.filter((message) => {
280
- if (normalizeChatRole(message) !== 'tool') {
281
- return true;
282
- }
283
- if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
284
- return true;
285
- }
286
- return !consumedCallIds.has(message.tool_call_id.trim());
287
- });
288
-
289
- const mergedFollowups: SessionMessageView[] = [];
290
- for (let index = 0; index < mergedToolResults.length; index += 1) {
291
- const current = mergedToolResults[index];
292
- if (normalizeChatRole(current) !== 'assistant' || !hasToolCalls(current)) {
293
- mergedFollowups.push(current);
265
+ const timeline: ChatTimelineItem[] = [];
266
+ let activeFlow:
267
+ | {
268
+ item: ChatTimelineAssistantFlowItem;
269
+ cardByCallId: Map<string, ToolCard>;
270
+ pendingCallIds: Set<string>;
271
+ awaitingFollowup: boolean;
272
+ }
273
+ | null = null;
274
+
275
+ const closeActiveFlow = () => {
276
+ activeFlow = null;
277
+ };
278
+
279
+ for (const event of normalized) {
280
+ const message = event.message;
281
+ if (!message) {
294
282
  continue;
295
283
  }
296
284
 
297
- let merged = current;
298
- let cursor = index + 1;
299
- while (cursor < mergedToolResults.length) {
300
- const candidate = mergedToolResults[cursor];
301
- if (normalizeChatRole(candidate) !== 'assistant') {
302
- break;
303
- }
304
- if (hasToolCalls(candidate) || typeof candidate.tool_call_id === 'string') {
305
- break;
306
- }
285
+ const role = normalizeChatRole(message);
286
+ const timestamp =
287
+ typeof message.timestamp === 'string' && message.timestamp
288
+ ? message.timestamp
289
+ : event.timestamp;
290
+
291
+ if (role === 'assistant' && hasToolCalls(message)) {
292
+ closeActiveFlow();
293
+ const toolCards = buildToolCallCards(message);
294
+ const item: ChatTimelineAssistantFlowItem = {
295
+ kind: 'assistant_flow',
296
+ key: `flow-${event._seq}-${event._idx}`,
297
+ role: 'assistant',
298
+ timestamp,
299
+ primaryText: extractMessageText(message.content).trim(),
300
+ primaryReasoning:
301
+ typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '',
302
+ followupText: '',
303
+ followupReasoning: '',
304
+ toolCards
305
+ };
307
306
 
308
- const candidateText = extractMessageText(candidate.content).trim();
309
- const candidateReasoning = typeof candidate.reasoning_content === 'string' && candidate.reasoning_content.trim();
310
- if (!candidateText && !candidateReasoning) {
311
- break;
307
+ const cardByCallId = new Map<string, ToolCard>();
308
+ const pendingCallIds = new Set<string>();
309
+ for (const card of toolCards) {
310
+ if (typeof card.callId === 'string' && card.callId.trim()) {
311
+ cardByCallId.set(card.callId, card);
312
+ pendingCallIds.add(card.callId);
313
+ }
312
314
  }
313
315
 
314
- merged = {
315
- ...merged,
316
- content: mergeMessageContent(merged.content, candidate.content),
317
- reasoning_content: mergeReasoningContent(merged.reasoning_content, candidate.reasoning_content),
318
- timestamp: candidate.timestamp
316
+ timeline.push(item);
317
+ activeFlow = {
318
+ item,
319
+ cardByCallId,
320
+ pendingCallIds,
321
+ awaitingFollowup: pendingCallIds.size === 0
319
322
  };
320
- cursor += 1;
323
+ continue;
321
324
  }
322
325
 
323
- mergedFollowups.push(merged);
324
- index = cursor - 1;
325
- }
326
-
327
- return mergedFollowups;
328
- }
326
+ if (role === 'tool') {
327
+ const callId =
328
+ typeof message.tool_call_id === 'string' && message.tool_call_id.trim()
329
+ ? message.tool_call_id.trim()
330
+ : undefined;
331
+ if (activeFlow && callId && activeFlow.cardByCallId.has(callId)) {
332
+ const card = activeFlow.cardByCallId.get(callId)!;
333
+ const resultText = extractMessageText(message.content).trim();
334
+ card.text = appendText(card.text ?? '', resultText);
335
+ card.hasResult = true;
336
+ if (typeof message.name === 'string' && message.name.trim()) {
337
+ card.name = message.name.trim();
338
+ }
339
+ activeFlow.pendingCallIds.delete(callId);
340
+ activeFlow.awaitingFollowup = activeFlow.pendingCallIds.size === 0;
341
+ activeFlow.item.timestamp = timestamp;
342
+ continue;
343
+ }
329
344
 
330
- export function groupChatMessages(messages: SessionMessageView[]): GroupedChatMessage[] {
331
- const groups: GroupedChatMessage[] = [];
332
- let lastTs = 0;
345
+ timeline.push({
346
+ kind: 'message',
347
+ key: `message-${event._seq}-${event._idx}`,
348
+ role,
349
+ timestamp,
350
+ message
351
+ });
352
+ closeActiveFlow();
353
+ continue;
354
+ }
333
355
 
334
- for (let index = 0; index < messages.length; index += 1) {
335
- const message = messages[index];
336
- const role = normalizeChatRole(message);
337
- const parsedTs = Date.parse(message.timestamp);
338
- const ts = Number.isFinite(parsedTs) ? parsedTs : Date.now();
339
- const previous = groups[groups.length - 1];
340
- const canMerge =
341
- previous &&
342
- previous.role === role &&
343
- Math.abs(ts - lastTs) <= MERGE_WINDOW_MS;
344
-
345
- if (canMerge) {
346
- previous.messages.push(message);
347
- previous.timestamp = message.timestamp;
348
- lastTs = ts;
356
+ if (role === 'assistant' && activeFlow && activeFlow.awaitingFollowup && !hasToolCalls(message)) {
357
+ const text = extractMessageText(message.content).trim();
358
+ const reasoning =
359
+ typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
360
+ activeFlow.item.followupText = appendText(activeFlow.item.followupText, text);
361
+ activeFlow.item.followupReasoning = appendText(activeFlow.item.followupReasoning, reasoning);
362
+ activeFlow.item.timestamp = timestamp;
363
+ closeActiveFlow();
349
364
  continue;
350
365
  }
351
366
 
352
- groups.push({
353
- key: `${message.timestamp}-${index}-${role}`,
367
+ timeline.push({
368
+ kind: 'message',
369
+ key: `message-${event._seq}-${event._idx}`,
354
370
  role,
355
- messages: [message],
356
- timestamp: message.timestamp
371
+ timestamp,
372
+ message
357
373
  });
358
- lastTs = ts;
374
+ closeActiveFlow();
359
375
  }
360
376
 
361
- return groups;
377
+ return timeline;
362
378
  }