@nextclaw/ui 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/dist/assets/{ChannelsList-VSRZzxx2.js → ChannelsList-BX7KqEk7.js} +4 -4
- package/dist/assets/ChatPage-zXLBKIAY.js +38 -0
- package/dist/assets/{DocBrowser-C65Hbvnb.js → DocBrowser-Cdbh4cVD.js} +1 -1
- package/dist/assets/{LogoBadge-4qtguXEJ.js → LogoBadge-4801esOJ.js} +1 -1
- package/dist/assets/MarketplacePage-GZgus0Or.js +49 -0
- package/dist/assets/{McpMarketplacePage-CHLkD8yX.js → McpMarketplacePage-CAGGvoMo.js} +1 -1
- package/dist/assets/{ModelConfig-CjsGdmZa.js → ModelConfig-CfLYjQM3.js} +1 -1
- package/dist/assets/ProvidersList-CEo1kdf-.js +1 -0
- package/dist/assets/{RemoteAccessPage-rOZCnH1x.js → RemoteAccessPage-6GYzD7cc.js} +1 -1
- package/dist/assets/{RuntimeConfig-CmJh6g0R.js → RuntimeConfig-BZdbp8mH.js} +1 -1
- package/dist/assets/{SearchConfig-C_hUuzR4.js → SearchConfig-ifvYKix-.js} +1 -1
- package/dist/assets/{SecretsConfig-Bu_zIRlQ.js → SecretsConfig-tDPbhTeR.js} +1 -1
- package/dist/assets/{SessionsConfig-DA_nqkM_.js → SessionsConfig-DhkAIzGm.js} +1 -1
- package/dist/assets/{chat-message-BOdA4h43.js → chat-message-C5Gl-dCH.js} +1 -1
- package/dist/assets/index-BTt_JlNV.css +1 -0
- package/dist/assets/index-JN3V84h_.js +8 -0
- package/dist/assets/{label-BYZ62ajO.js → label-D8zWKdqp.js} +1 -1
- package/dist/assets/{page-layout-UC-h92sU.js → page-layout-qAJ47LNQ.js} +1 -1
- package/dist/assets/{popover-DASCEr3G.js → popover-hyBGxpxS.js} +1 -1
- package/dist/assets/{security-config-Cvujq4fH.js → security-config-BJYZSnCA.js} +1 -1
- package/dist/assets/skeleton-CUQLsNsM.js +1 -0
- package/dist/assets/{status-dot-C1AvPwDD.js → status-dot-DKcoD-iY.js} +1 -1
- package/dist/assets/{switch-D3wVuCSh.js → switch-DtUdQxr_.js} +1 -1
- package/dist/assets/tabs-custom-Dj1BWHGK.js +1 -0
- package/dist/assets/useConfirmDialog-nZdrtETU.js +1 -0
- package/dist/assets/{vendor-DJt0Azq5.js → vendor-CNhxtHCf.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +5 -5
- package/src/components/chat/ChatSidebar.tsx +41 -69
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +32 -1
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +6 -3
- package/src/components/chat/adapters/chat-message.adapter.test.ts +141 -163
- package/src/components/chat/adapters/chat-message.adapter.ts +35 -0
- package/src/components/chat/chat-composer-state.ts +38 -0
- package/src/components/chat/chat-stream/types.ts +2 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +116 -55
- package/src/components/chat/containers/chat-message-list.container.tsx +2 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +16 -1
- package/src/components/chat/managers/chat-session-list.manager.ts +0 -2
- package/src/components/chat/managers/chat-thread.manager.ts +0 -1
- package/src/components/chat/ncp/NcpChatPage.tsx +18 -18
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +50 -33
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +5 -123
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +56 -1
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +8 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +0 -1
- package/src/components/chat/presenter/chat-presenter-context.tsx +6 -0
- package/src/components/chat/stores/chat-input.store.ts +3 -0
- package/src/components/config/ChannelsList.test.tsx +2 -1
- package/src/components/config/weixin-channel-auth-section.test.tsx +2 -1
- package/src/components/layout/Sidebar.tsx +62 -102
- package/src/components/layout/sidebar-items.tsx +172 -0
- package/src/components/layout/sidebar.layout.test.tsx +11 -4
- package/src/lib/i18n.chat.ts +117 -0
- package/src/lib/i18n.remote.ts +1 -1
- package/src/lib/i18n.ts +2 -112
- package/src/transport/remote.transport.test.ts +135 -0
- package/src/transport/remote.transport.ts +11 -1
- package/dist/assets/ChatPage-CX0ZKE5i.js +0 -41
- package/dist/assets/MarketplacePage-DPCYptfD.js +0 -49
- package/dist/assets/ProvidersList-aXp_mo4J.js +0 -1
- package/dist/assets/index-C63mHRbE.css +0 -1
- package/dist/assets/index-DS7D1-KS.js +0 -8
- package/dist/assets/skeleton-DlYEKkkj.js +0 -1
- package/dist/assets/tabs-custom-CbgS7tu0.js +0 -1
- package/dist/assets/useConfirmDialog-BYbFEIbQ.js +0 -1
|
@@ -5,7 +5,7 @@ import { BrandHeader } from '@/components/common/BrandHeader';
|
|
|
5
5
|
import { StatusBadge } from '@/components/common/StatusBadge';
|
|
6
6
|
import { Input } from '@/components/ui/input';
|
|
7
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
8
|
-
import {
|
|
8
|
+
import { SelectItem } from '@/components/ui/select';
|
|
9
9
|
import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
|
|
10
10
|
import { useChatSessionLabelService } from '@/components/chat/chat-session-label.service';
|
|
11
11
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
@@ -19,8 +19,9 @@ import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
|
19
19
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
20
20
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
21
21
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
22
|
+
import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/components/layout/sidebar-items';
|
|
22
23
|
import { useUiStore } from '@/stores/ui.store';
|
|
23
|
-
import {
|
|
24
|
+
import { useLocation } from 'react-router-dom';
|
|
24
25
|
import {
|
|
25
26
|
AlarmClock,
|
|
26
27
|
BookOpen,
|
|
@@ -284,28 +285,14 @@ export function ChatSidebar() {
|
|
|
284
285
|
<div className="px-3 pb-2">
|
|
285
286
|
<ul className="space-y-0.5">
|
|
286
287
|
{navItems.map((item) => {
|
|
287
|
-
const Icon = item.icon;
|
|
288
288
|
return (
|
|
289
289
|
<li key={item.target}>
|
|
290
|
-
<
|
|
290
|
+
<SidebarNavLinkItem
|
|
291
291
|
to={item.target}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
: 'text-gray-600 hover:bg-gray-200/60 hover:text-gray-900'
|
|
297
|
-
)}
|
|
298
|
-
>
|
|
299
|
-
{({ isActive }) => (
|
|
300
|
-
<>
|
|
301
|
-
<Icon className={cn(
|
|
302
|
-
'h-4 w-4 transition-colors',
|
|
303
|
-
isActive ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-800'
|
|
304
|
-
)} />
|
|
305
|
-
<span>{item.label()}</span>
|
|
306
|
-
</>
|
|
307
|
-
)}
|
|
308
|
-
</NavLink>
|
|
292
|
+
label={item.label()}
|
|
293
|
+
icon={item.icon}
|
|
294
|
+
density="compact"
|
|
295
|
+
/>
|
|
309
296
|
</li>
|
|
310
297
|
);
|
|
311
298
|
})}
|
|
@@ -363,57 +350,42 @@ export function ChatSidebar() {
|
|
|
363
350
|
</div>
|
|
364
351
|
|
|
365
352
|
<div className="px-3 py-3 border-t border-gray-200/60 space-y-0.5">
|
|
366
|
-
<
|
|
353
|
+
<SidebarNavLinkItem
|
|
367
354
|
to="/settings"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)}
|
|
374
|
-
>
|
|
375
|
-
{({ isActive }) => (
|
|
376
|
-
<>
|
|
377
|
-
<Settings className={cn('h-4 w-4 transition-colors', isActive ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-800')} />
|
|
378
|
-
<span>{t('settings')}</span>
|
|
379
|
-
</>
|
|
380
|
-
)}
|
|
381
|
-
</NavLink>
|
|
382
|
-
<button
|
|
355
|
+
label={t('settings')}
|
|
356
|
+
icon={Settings}
|
|
357
|
+
density="compact"
|
|
358
|
+
/>
|
|
359
|
+
<SidebarActionItem
|
|
383
360
|
onClick={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
384
|
-
|
|
361
|
+
icon={BookOpen}
|
|
362
|
+
label={t('docBrowserHelp')}
|
|
363
|
+
density="compact"
|
|
364
|
+
/>
|
|
365
|
+
<SidebarSelectItem
|
|
366
|
+
value={theme}
|
|
367
|
+
onValueChange={(value) => setTheme(value as UiTheme)}
|
|
368
|
+
icon={Palette}
|
|
369
|
+
label={t('theme')}
|
|
370
|
+
valueLabel={currentThemeLabel}
|
|
371
|
+
density="compact"
|
|
385
372
|
>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
</Select>
|
|
403
|
-
<Select value={language} onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}>
|
|
404
|
-
<SelectTrigger className="w-full h-auto rounded-xl border-0 bg-transparent shadow-none px-3 py-2 text-[13px] font-medium text-gray-600 hover:bg-gray-200/60 focus:ring-0">
|
|
405
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
406
|
-
<Languages className="h-4 w-4 text-gray-400" />
|
|
407
|
-
<span>{t('language')}</span>
|
|
408
|
-
</div>
|
|
409
|
-
<span className="ml-auto text-[11px] text-gray-500">{currentLanguageLabel}</span>
|
|
410
|
-
</SelectTrigger>
|
|
411
|
-
<SelectContent>
|
|
412
|
-
{LANGUAGE_OPTIONS.map((option) => (
|
|
413
|
-
<SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
|
|
414
|
-
))}
|
|
415
|
-
</SelectContent>
|
|
416
|
-
</Select>
|
|
373
|
+
{THEME_OPTIONS.map((option) => (
|
|
374
|
+
<SelectItem key={option.value} value={option.value} className="text-xs">{t(option.labelKey)}</SelectItem>
|
|
375
|
+
))}
|
|
376
|
+
</SidebarSelectItem>
|
|
377
|
+
<SidebarSelectItem
|
|
378
|
+
value={language}
|
|
379
|
+
onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}
|
|
380
|
+
icon={Languages}
|
|
381
|
+
label={t('language')}
|
|
382
|
+
valueLabel={currentLanguageLabel}
|
|
383
|
+
density="compact"
|
|
384
|
+
>
|
|
385
|
+
{LANGUAGE_OPTIONS.map((option) => (
|
|
386
|
+
<SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
|
|
387
|
+
))}
|
|
388
|
+
</SidebarSelectItem>
|
|
417
389
|
</div>
|
|
418
390
|
</aside>
|
|
419
391
|
);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildChatSlashItems,
|
|
3
|
+
buildModelToolbarSelect,
|
|
4
|
+
buildSelectedSkillItems,
|
|
5
|
+
buildSkillPickerModel
|
|
6
|
+
} from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
2
7
|
import type { ChatSkillRecord } from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
3
8
|
|
|
4
9
|
function createSkillRecord(partial: Partial<ChatSkillRecord>): ChatSkillRecord {
|
|
@@ -78,3 +83,29 @@ describe('buildSkillPickerModel', () => {
|
|
|
78
83
|
});
|
|
79
84
|
});
|
|
80
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('buildModelToolbarSelect', () => {
|
|
88
|
+
it('falls back to the first available option when the selected model is missing', () => {
|
|
89
|
+
const onValueChange = vi.fn();
|
|
90
|
+
const select = buildModelToolbarSelect({
|
|
91
|
+
modelOptions: [
|
|
92
|
+
{
|
|
93
|
+
value: 'minimax/MiniMax-M2.7',
|
|
94
|
+
modelLabel: 'MiniMax-M2.7',
|
|
95
|
+
providerLabel: 'MiniMax'
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
selectedModel: 'dashscope/qwen3-coder-next',
|
|
99
|
+
isModelOptionsLoading: false,
|
|
100
|
+
hasModelOptions: true,
|
|
101
|
+
onValueChange,
|
|
102
|
+
texts: {
|
|
103
|
+
modelSelectPlaceholder: 'Select model',
|
|
104
|
+
modelNoOptionsLabel: 'No models'
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(select.value).toBe('minimax/MiniMax-M2.7');
|
|
109
|
+
expect(select.selectedLabel).toBe('MiniMax/MiniMax-M2.7');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -238,13 +238,16 @@ export function buildModelToolbarSelect(params: {
|
|
|
238
238
|
texts: Pick<ChatInputBarAdapterTexts, 'modelSelectPlaceholder' | 'modelNoOptionsLabel'>;
|
|
239
239
|
}): ChatToolbarSelect {
|
|
240
240
|
const selectedModelOption = params.modelOptions.find((option) => option.value === params.selectedModel);
|
|
241
|
+
const fallbackModelOption = params.modelOptions[0];
|
|
242
|
+
const resolvedModelOption = selectedModelOption ?? fallbackModelOption;
|
|
243
|
+
const resolvedValue = params.hasModelOptions ? resolvedModelOption?.value : undefined;
|
|
241
244
|
|
|
242
245
|
return {
|
|
243
246
|
key: 'model',
|
|
244
|
-
value:
|
|
247
|
+
value: resolvedValue,
|
|
245
248
|
placeholder: params.texts.modelSelectPlaceholder,
|
|
246
|
-
selectedLabel:
|
|
247
|
-
? `${
|
|
249
|
+
selectedLabel: resolvedModelOption
|
|
250
|
+
? `${resolvedModelOption.providerLabel}/${resolvedModelOption.modelLabel}`
|
|
248
251
|
: undefined,
|
|
249
252
|
icon: 'sparkles',
|
|
250
253
|
options: params.modelOptions.map((option) => ({
|
|
@@ -6,183 +6,161 @@ function toSource(uiMessages: UiMessage[]): ChatMessageSource[] {
|
|
|
6
6
|
return uiMessages as unknown as ChatMessageSource[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
type: "tool-invocation",
|
|
28
|
-
toolInvocation: {
|
|
29
|
-
status: ToolInvocationStatus.RESULT,
|
|
30
|
-
toolCallId: "call-1",
|
|
31
|
-
toolName: "web_search",
|
|
32
|
-
args: '{"q":"hello"}',
|
|
33
|
-
result: { ok: true },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
];
|
|
9
|
+
const defaultTexts = {
|
|
10
|
+
roleLabels: {
|
|
11
|
+
user: "You",
|
|
12
|
+
assistant: "Assistant",
|
|
13
|
+
tool: "Tool",
|
|
14
|
+
system: "System",
|
|
15
|
+
fallback: "Message",
|
|
16
|
+
},
|
|
17
|
+
reasoningLabel: "Reasoning",
|
|
18
|
+
toolCallLabel: "Tool Call",
|
|
19
|
+
toolResultLabel: "Tool Result",
|
|
20
|
+
toolNoOutputLabel: "No output",
|
|
21
|
+
toolOutputLabel: "View Output",
|
|
22
|
+
imageAttachmentLabel: "Image attachment",
|
|
23
|
+
fileAttachmentLabel: "File attachment",
|
|
24
|
+
unknownPartLabel: "Unknown Part",
|
|
25
|
+
};
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
user: "You",
|
|
46
|
-
assistant: "Assistant",
|
|
47
|
-
tool: "Tool",
|
|
48
|
-
system: "System",
|
|
49
|
-
fallback: "Message",
|
|
50
|
-
},
|
|
51
|
-
reasoningLabel: "Reasoning",
|
|
52
|
-
toolCallLabel: "Tool Call",
|
|
53
|
-
toolResultLabel: "Tool Result",
|
|
54
|
-
toolNoOutputLabel: "No output",
|
|
55
|
-
toolOutputLabel: "View Output",
|
|
56
|
-
unknownPartLabel: "Unknown Part",
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
expect(adapted).toHaveLength(1);
|
|
61
|
-
expect(adapted[0]?.roleLabel).toBe("Assistant");
|
|
62
|
-
expect(adapted[0]?.timestampLabel).toBe(
|
|
63
|
-
"formatted:2026-03-17T10:00:00.000Z",
|
|
64
|
-
);
|
|
65
|
-
expect(adapted[0]?.parts.map((part) => part.type)).toEqual([
|
|
66
|
-
"markdown",
|
|
67
|
-
"reasoning",
|
|
68
|
-
"tool-card",
|
|
69
|
-
]);
|
|
70
|
-
expect(adapted[0]?.parts[1]).toMatchObject({
|
|
71
|
-
type: "reasoning",
|
|
72
|
-
label: "Reasoning",
|
|
73
|
-
text: "internal reasoning",
|
|
74
|
-
});
|
|
75
|
-
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
76
|
-
type: "tool-card",
|
|
77
|
-
card: {
|
|
78
|
-
titleLabel: "Tool Result",
|
|
79
|
-
outputLabel: "View Output",
|
|
80
|
-
},
|
|
81
|
-
});
|
|
27
|
+
function adapt(uiMessages: ChatMessageSource[]) {
|
|
28
|
+
return adaptChatMessages({
|
|
29
|
+
uiMessages,
|
|
30
|
+
formatTimestamp: (value) => `formatted:${value}`,
|
|
31
|
+
texts: defaultTexts,
|
|
82
32
|
});
|
|
33
|
+
}
|
|
83
34
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
35
|
+
it("maps markdown, reasoning, and tool parts into UI view models", () => {
|
|
36
|
+
const messages: UiMessage[] = [
|
|
37
|
+
{
|
|
38
|
+
id: "assistant-1",
|
|
39
|
+
role: "assistant",
|
|
40
|
+
meta: {
|
|
41
|
+
status: "final",
|
|
42
|
+
timestamp: "2026-03-17T10:00:00.000Z",
|
|
43
|
+
},
|
|
44
|
+
parts: [
|
|
45
|
+
{ type: "text", text: "hello world" },
|
|
87
46
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
47
|
+
type: "reasoning",
|
|
48
|
+
reasoning: "internal reasoning",
|
|
49
|
+
details: [],
|
|
91
50
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
51
|
+
{
|
|
52
|
+
type: "tool-invocation",
|
|
53
|
+
toolInvocation: {
|
|
54
|
+
status: ToolInvocationStatus.RESULT,
|
|
55
|
+
toolCallId: "call-1",
|
|
56
|
+
toolName: "web_search",
|
|
57
|
+
args: '{"q":"hello"}',
|
|
58
|
+
result: { ok: true },
|
|
59
|
+
},
|
|
101
60
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const adapted = adapt(toSource(messages));
|
|
66
|
+
|
|
67
|
+
expect(adapted).toHaveLength(1);
|
|
68
|
+
expect(adapted[0]?.roleLabel).toBe("Assistant");
|
|
69
|
+
expect(adapted[0]?.timestampLabel).toBe(
|
|
70
|
+
"formatted:2026-03-17T10:00:00.000Z",
|
|
71
|
+
);
|
|
72
|
+
expect(adapted[0]?.parts.map((part) => part.type)).toEqual([
|
|
73
|
+
"markdown",
|
|
74
|
+
"reasoning",
|
|
75
|
+
"tool-card",
|
|
76
|
+
]);
|
|
77
|
+
expect(adapted[0]?.parts[1]).toMatchObject({
|
|
78
|
+
type: "reasoning",
|
|
79
|
+
label: "Reasoning",
|
|
80
|
+
text: "internal reasoning",
|
|
81
|
+
});
|
|
82
|
+
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
83
|
+
type: "tool-card",
|
|
84
|
+
card: {
|
|
85
|
+
titleLabel: "Tool Result",
|
|
86
|
+
outputLabel: "View Output",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("maps non-standard roles back to the generic message role", () => {
|
|
92
|
+
const adapted = adapt([
|
|
93
|
+
{
|
|
94
|
+
id: "data-1",
|
|
95
|
+
role: "data",
|
|
96
|
+
parts: [{ type: "text", text: "payload" }],
|
|
97
|
+
},
|
|
98
|
+
] as unknown as ChatMessageSource[]);
|
|
110
99
|
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
expect(adapted[0]?.role).toBe("message");
|
|
101
|
+
expect(adapted[0]?.roleLabel).toBe("Message");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("maps unknown parts into a visible fallback part", () => {
|
|
105
|
+
const adapted = adapt([
|
|
106
|
+
{
|
|
107
|
+
id: "x-1",
|
|
108
|
+
role: "assistant",
|
|
109
|
+
parts: [{ type: "step-start", value: "x" }],
|
|
110
|
+
},
|
|
111
|
+
] as unknown as ChatMessageSource[]);
|
|
112
|
+
|
|
113
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
114
|
+
type: "unknown",
|
|
115
|
+
rawType: "step-start",
|
|
116
|
+
label: "Unknown Part",
|
|
113
117
|
});
|
|
118
|
+
});
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
user: "You",
|
|
128
|
-
assistant: "Assistant",
|
|
129
|
-
tool: "Tool",
|
|
130
|
-
system: "System",
|
|
131
|
-
fallback: "Message",
|
|
132
|
-
},
|
|
133
|
-
reasoningLabel: "Reasoning",
|
|
134
|
-
toolCallLabel: "Tool Call",
|
|
135
|
-
toolResultLabel: "Tool Result",
|
|
136
|
-
toolNoOutputLabel: "No output",
|
|
137
|
-
toolOutputLabel: "View Output",
|
|
138
|
-
unknownPartLabel: "Unknown Part",
|
|
139
|
-
},
|
|
140
|
-
});
|
|
120
|
+
it("drops empty and zero-width text parts during adaptation", () => {
|
|
121
|
+
const adapted = adapt([
|
|
122
|
+
{
|
|
123
|
+
id: "assistant-mixed",
|
|
124
|
+
role: "assistant",
|
|
125
|
+
parts: [
|
|
126
|
+
{ type: "text", text: " " },
|
|
127
|
+
{ type: "text", text: "\u200B\u200B" },
|
|
128
|
+
{ type: "text", text: "\u200Bhello\u200B" },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
] as unknown as ChatMessageSource[]);
|
|
141
132
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
expect(adapted).toHaveLength(1);
|
|
134
|
+
expect(adapted[0]?.id).toBe("assistant-mixed");
|
|
135
|
+
expect(adapted[0]?.parts).toHaveLength(1);
|
|
136
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
137
|
+
type: "markdown",
|
|
138
|
+
text: "\u200Bhello\u200B",
|
|
147
139
|
});
|
|
140
|
+
});
|
|
148
141
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
it("maps file parts into previewable attachment view models", () => {
|
|
143
|
+
const adapted = adapt([
|
|
144
|
+
{
|
|
145
|
+
id: "assistant-file",
|
|
146
|
+
role: "assistant",
|
|
147
|
+
parts: [
|
|
152
148
|
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{ type: "text", text: " " },
|
|
157
|
-
{ type: "text", text: "\u200B\u200B" },
|
|
158
|
-
{ type: "text", text: "\u200Bhello\u200B" },
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
] as unknown as ChatMessageSource[],
|
|
162
|
-
formatTimestamp: () => "formatted",
|
|
163
|
-
texts: {
|
|
164
|
-
roleLabels: {
|
|
165
|
-
user: "You",
|
|
166
|
-
assistant: "Assistant",
|
|
167
|
-
tool: "Tool",
|
|
168
|
-
system: "System",
|
|
169
|
-
fallback: "Message",
|
|
149
|
+
type: "file",
|
|
150
|
+
mimeType: "image/png",
|
|
151
|
+
data: "ZmFrZS1pbWFnZQ==",
|
|
170
152
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
toolNoOutputLabel: "No output",
|
|
175
|
-
toolOutputLabel: "View Output",
|
|
176
|
-
unknownPartLabel: "Unknown Part",
|
|
177
|
-
},
|
|
178
|
-
});
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
] as unknown as ChatMessageSource[]);
|
|
179
156
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
157
|
+
expect(adapted[0]?.parts[0]).toEqual({
|
|
158
|
+
type: "file",
|
|
159
|
+
file: {
|
|
160
|
+
label: "Image attachment",
|
|
161
|
+
mimeType: "image/png",
|
|
162
|
+
dataUrl: "data:image/png;base64,ZmFrZS1pbWFnZQ==",
|
|
163
|
+
isImage: true,
|
|
164
|
+
},
|
|
187
165
|
});
|
|
188
166
|
});
|
|
@@ -14,6 +14,12 @@ export type ChatMessagePartSource =
|
|
|
14
14
|
type: 'text';
|
|
15
15
|
text: string;
|
|
16
16
|
}
|
|
17
|
+
| {
|
|
18
|
+
type: 'file';
|
|
19
|
+
mimeType: string;
|
|
20
|
+
data: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
}
|
|
17
23
|
| {
|
|
18
24
|
type: 'reasoning';
|
|
19
25
|
reasoning: string;
|
|
@@ -58,6 +64,8 @@ export type ChatMessageAdapterTexts = {
|
|
|
58
64
|
toolResultLabel: string;
|
|
59
65
|
toolNoOutputLabel: string;
|
|
60
66
|
toolOutputLabel: string;
|
|
67
|
+
imageAttachmentLabel: string;
|
|
68
|
+
fileAttachmentLabel: string;
|
|
61
69
|
unknownPartLabel: string;
|
|
62
70
|
};
|
|
63
71
|
|
|
@@ -77,6 +85,16 @@ function isReasoningPart(
|
|
|
77
85
|
return part.type === 'reasoning' && typeof part.reasoning === 'string';
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
function isFilePart(
|
|
89
|
+
part: ChatMessagePartSource
|
|
90
|
+
): part is Extract<ChatMessagePartSource, { type: 'file' }> {
|
|
91
|
+
return (
|
|
92
|
+
part.type === 'file' &&
|
|
93
|
+
typeof part.mimeType === 'string' &&
|
|
94
|
+
typeof part.data === 'string'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
function isToolInvocationPart(
|
|
81
99
|
part: ChatMessagePartSource
|
|
82
100
|
): part is Extract<ChatMessagePartSource, { type: 'tool-invocation' }> {
|
|
@@ -182,6 +200,23 @@ export function adaptChatMessages(params: {
|
|
|
182
200
|
label: params.texts.reasoningLabel
|
|
183
201
|
};
|
|
184
202
|
}
|
|
203
|
+
if (isFilePart(part)) {
|
|
204
|
+
const isImage = part.mimeType.startsWith('image/');
|
|
205
|
+
return {
|
|
206
|
+
type: 'file' as const,
|
|
207
|
+
file: {
|
|
208
|
+
label:
|
|
209
|
+
typeof part.name === 'string' && part.name.trim()
|
|
210
|
+
? part.name.trim()
|
|
211
|
+
: isImage
|
|
212
|
+
? params.texts.imageAttachmentLabel
|
|
213
|
+
: params.texts.fileAttachmentLabel,
|
|
214
|
+
mimeType: part.mimeType,
|
|
215
|
+
dataUrl: `data:${part.mimeType};base64,${part.data}`,
|
|
216
|
+
isImage
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
185
220
|
if (isToolInvocationPart(part)) {
|
|
186
221
|
const invocation = part.toolInvocation;
|
|
187
222
|
const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
2
3
|
import {
|
|
3
4
|
createChatComposerTokenNode,
|
|
4
5
|
createChatComposerNodesFromText,
|
|
@@ -25,6 +26,10 @@ export function deriveSelectedSkillsFromComposer(nodes: ChatComposerNode[]): str
|
|
|
25
26
|
return extractChatComposerTokenKeys(nodes, 'skill');
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export function deriveSelectedAttachmentIdsFromComposer(nodes: ChatComposerNode[]): string[] {
|
|
30
|
+
return extractChatComposerTokenKeys(nodes, 'file');
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export function syncComposerSkills(
|
|
29
34
|
nodes: ChatComposerNode[],
|
|
30
35
|
nextSkills: string[],
|
|
@@ -51,3 +56,36 @@ export function syncComposerSkills(
|
|
|
51
56
|
? prunedNodes
|
|
52
57
|
: normalizeChatComposerNodes([...prunedNodes, ...appendedNodes]);
|
|
53
58
|
}
|
|
59
|
+
|
|
60
|
+
export function syncComposerAttachments(
|
|
61
|
+
nodes: ChatComposerNode[],
|
|
62
|
+
attachments: readonly NcpDraftAttachment[]
|
|
63
|
+
): ChatComposerNode[] {
|
|
64
|
+
const nextAttachmentIds = new Set(attachments.map((attachment) => attachment.id));
|
|
65
|
+
const prunedNodes = removeChatComposerTokenNodes(
|
|
66
|
+
nodes,
|
|
67
|
+
(node) => node.tokenKind === 'file' && !nextAttachmentIds.has(node.tokenKey)
|
|
68
|
+
);
|
|
69
|
+
const existingAttachmentIds = extractChatComposerTokenKeys(prunedNodes, 'file');
|
|
70
|
+
const appendedNodes = attachments
|
|
71
|
+
.filter((attachment) => !existingAttachmentIds.includes(attachment.id))
|
|
72
|
+
.map((attachment) =>
|
|
73
|
+
createChatComposerTokenNode({
|
|
74
|
+
tokenKind: 'file',
|
|
75
|
+
tokenKey: attachment.id,
|
|
76
|
+
label: attachment.name
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return appendedNodes.length === 0
|
|
81
|
+
? prunedNodes
|
|
82
|
+
: normalizeChatComposerNodes([...prunedNodes, ...appendedNodes]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function pruneComposerAttachments(
|
|
86
|
+
nodes: ChatComposerNode[],
|
|
87
|
+
attachments: readonly NcpDraftAttachment[]
|
|
88
|
+
): NcpDraftAttachment[] {
|
|
89
|
+
const selectedIds = new Set(deriveSelectedAttachmentIdsFromComposer(nodes));
|
|
90
|
+
return attachments.filter((attachment) => selectedIds.has(attachment.id));
|
|
91
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
2
3
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
3
4
|
import type {
|
|
4
5
|
ChatRunView,
|
|
@@ -18,6 +19,7 @@ export type SendMessageParams = {
|
|
|
18
19
|
model?: string;
|
|
19
20
|
thinkingLevel?: ThinkingLevel;
|
|
20
21
|
requestedSkills?: string[];
|
|
22
|
+
attachments?: NcpDraftAttachment[];
|
|
21
23
|
stopSupported?: boolean;
|
|
22
24
|
stopReason?: string;
|
|
23
25
|
restoreDraftOnError?: boolean;
|