@nextclaw/ui 0.8.0 → 0.9.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 +24 -0
- package/dist/assets/ChannelsList-DhvjpZcs.js +1 -0
- package/dist/assets/ChatPage-B8VBaMQm.js +38 -0
- package/dist/assets/{DocBrowser-DDX2HMXW.js → DocBrowser-LpzGe8An.js} +1 -1
- package/dist/assets/{LogoBadge-J53F_3JA.js → LogoBadge-Be4lktJN.js} +1 -1
- package/dist/assets/{MarketplacePage-0BZ4bza0.js → MarketplacePage-Cx9AI3_h.js} +3 -3
- package/dist/assets/{ModelConfig-Wzq9wGHV.js → ModelConfig-DuImUHIX.js} +1 -1
- package/dist/assets/ProvidersList-Ccleg25k.js +1 -0
- package/dist/assets/{RuntimeConfig-N771_AM6.js → RuntimeConfig-C6iqpJR_.js} +1 -1
- package/dist/assets/{SearchConfig-DVt5QVa_.js → SearchConfig-Dvp1TAXu.js} +1 -1
- package/dist/assets/{SecretsConfig-CkwauPa8.js → SecretsConfig-D5Ymlvt9.js} +1 -1
- package/dist/assets/{SessionsConfig-C3mnHzkZ.js → SessionsConfig-CIA_jA1P.js} +2 -2
- package/dist/assets/{chat-message-pxr79GDs.js → chat-message-B60Fh9kI.js} +1 -1
- package/dist/assets/index-BiPDnzv0.js +8 -0
- package/dist/assets/index-C8GsgIUn.css +1 -0
- package/dist/assets/{index-GdpEEKnz.js → index-CPDASUXh.js} +1 -1
- package/dist/assets/{label-CmksBHgc.js → label-D4fGx6Wb.js} +1 -1
- package/dist/assets/{page-layout-Db0GbnhS.js → page-layout-twy8gmBE.js} +1 -1
- package/dist/assets/popover-DYbYpt1j.js +1 -0
- package/dist/assets/{security-config-CjLFME5Q.js → security-config-BcIZ4rpb.js} +1 -1
- package/dist/assets/skeleton-DypBy7jp.js +1 -0
- package/dist/assets/{switch-C24d-UJU.js → switch-DqA6r5XR.js} +1 -1
- package/dist/assets/tabs-custom-C6enKKs1.js +1 -0
- package/dist/assets/{useConfirmDialog-BeP35LcG.js → useConfirmDialog-CHBf5Of7.js} +1 -1
- package/dist/assets/{vendor-psXJBy9u.js → vendor-DKBNiC31.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +6 -6
- package/src/api/config.ts +9 -38
- package/src/api/ncp-session.ts +50 -0
- package/src/api/types.ts +1 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
- package/src/components/chat/ChatConversationPanel.tsx +21 -12
- package/src/components/chat/ChatSidebar.test.tsx +203 -0
- package/src/components/chat/ChatSidebar.tsx +97 -7
- package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -82
- package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
- package/src/components/chat/chat-composer-state.ts +53 -0
- package/src/components/chat/chat-page-data.ts +30 -1
- package/src/components/chat/chat-page-runtime.test.ts +181 -0
- package/src/components/chat/chat-page-runtime.ts +101 -15
- package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
- package/src/components/chat/chat-session-preference-sync.ts +75 -0
- package/src/components/chat/chat-stream/types.ts +3 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +12 -63
- package/src/components/chat/containers/chat-message-list.container.tsx +31 -27
- package/src/components/chat/legacy/LegacyChatPage.tsx +25 -0
- package/src/components/chat/managers/chat-input.manager.ts +48 -13
- package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
- package/src/components/chat/ncp/NcpChatPage.tsx +53 -13
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +48 -12
- package/src/components/chat/ncp/ncp-chat-page-data.ts +34 -2
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +1 -1
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +27 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +2 -0
- package/src/components/chat/stores/chat-input.store.ts +4 -0
- package/src/components/chat/stores/chat-thread.store.ts +2 -0
- package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
- package/src/components/chat/useChatSessionTypeState.ts +25 -8
- package/src/hooks/use-ncp-chat-session-types.ts +11 -0
- package/src/hooks/useConfig.ts +2 -4
- package/src/hooks/useMarketplace.ts +7 -4
- package/src/hooks/useWebSocket.ts +23 -2
- package/dist/assets/ChannelsList-DBcoVJRW.js +0 -1
- package/dist/assets/ChatPage-CD3cxyyM.js +0 -37
- package/dist/assets/ProvidersList-kwzRS8_M.js +0 -1
- package/dist/assets/index-BIvFMkN4.js +0 -1
- package/dist/assets/index-CzkY1reu.js +0 -8
- package/dist/assets/index-RZ0kHHRI.css +0 -1
- package/dist/assets/skeleton-CkpQeVWN.js +0 -1
- package/dist/assets/tabs-custom-D89bh-fc.js +0 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
2
|
import type { SessionEntryView } from '@/api/types';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
4
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
5
5
|
import { StatusBadge } from '@/components/common/StatusBadge';
|
|
6
6
|
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
7
8
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
8
9
|
import { SessionRunBadge } from '@/components/common/SessionRunBadge';
|
|
9
10
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
11
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
10
12
|
import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
|
|
11
13
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
12
14
|
import { cn } from '@/lib/utils';
|
|
@@ -17,7 +19,7 @@ import { useTheme } from '@/components/providers/ThemeProvider';
|
|
|
17
19
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
18
20
|
import { useUiStore } from '@/stores/ui.store';
|
|
19
21
|
import { NavLink } from 'react-router-dom';
|
|
20
|
-
import { AlarmClock, BookOpen, BrainCircuit, Languages, MessageSquareText, Palette, Plus, Search, Settings } from 'lucide-react';
|
|
22
|
+
import { AlarmClock, BookOpen, BrainCircuit, ChevronDown, Languages, MessageSquareText, Palette, Plus, Search, Settings } from 'lucide-react';
|
|
21
23
|
|
|
22
24
|
type DateGroup = {
|
|
23
25
|
label: string;
|
|
@@ -64,6 +66,25 @@ function sessionTitle(session: SessionEntryView): string {
|
|
|
64
66
|
return chunks[chunks.length - 1] || session.key;
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function resolveSessionTypeLabel(
|
|
70
|
+
sessionType: string,
|
|
71
|
+
options: Array<{ value: string; label: string }>
|
|
72
|
+
): string | null {
|
|
73
|
+
const normalized = sessionType.trim().toLowerCase();
|
|
74
|
+
if (!normalized || normalized === 'native') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const matchedOption = options.find((option) => option.value.trim().toLowerCase() === normalized);
|
|
78
|
+
if (matchedOption?.label.trim()) {
|
|
79
|
+
return matchedOption.label.trim();
|
|
80
|
+
}
|
|
81
|
+
return normalized
|
|
82
|
+
.split(/[-_]+/g)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
85
|
+
.join(' ');
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
const navItems = [
|
|
68
89
|
{ target: '/cron', label: () => t('chatSidebarScheduledTasks'), icon: AlarmClock },
|
|
69
90
|
{ target: '/skills', label: () => t('chatSidebarSkills'), icon: BrainCircuit },
|
|
@@ -72,6 +93,8 @@ const navItems = [
|
|
|
72
93
|
export function ChatSidebar() {
|
|
73
94
|
const presenter = usePresenter();
|
|
74
95
|
const docBrowser = useDocBrowser();
|
|
96
|
+
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
97
|
+
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
75
98
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
76
99
|
const runSnapshot = useChatRunStatusStore((state) => state.snapshot);
|
|
77
100
|
const connectionStatus = useUiStore((state) => state.connectionStatus);
|
|
@@ -81,6 +104,11 @@ export function ChatSidebar() {
|
|
|
81
104
|
const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
82
105
|
|
|
83
106
|
const groups = useMemo(() => groupSessionsByDate(listSnapshot.sessions), [listSnapshot.sessions]);
|
|
107
|
+
const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
|
|
108
|
+
const nonDefaultSessionTypeOptions = useMemo(
|
|
109
|
+
() => inputSnapshot.sessionTypeOptions.filter((option) => option.value !== defaultSessionType),
|
|
110
|
+
[defaultSessionType, inputSnapshot.sessionTypeOptions]
|
|
111
|
+
);
|
|
84
112
|
|
|
85
113
|
const handleLanguageSwitch = (nextLang: I18nLanguage) => {
|
|
86
114
|
if (language === nextLang) return;
|
|
@@ -98,10 +126,57 @@ export function ChatSidebar() {
|
|
|
98
126
|
</div>
|
|
99
127
|
|
|
100
128
|
<div className="px-4 pb-3">
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
<div className="flex items-center gap-2">
|
|
130
|
+
<Button
|
|
131
|
+
variant="primary"
|
|
132
|
+
className={cn(
|
|
133
|
+
'min-w-0 rounded-xl',
|
|
134
|
+
nonDefaultSessionTypeOptions.length > 0 ? 'flex-1 rounded-r-md' : 'w-full'
|
|
135
|
+
)}
|
|
136
|
+
onClick={() => {
|
|
137
|
+
setIsCreateMenuOpen(false);
|
|
138
|
+
presenter.chatSessionListManager.createSession(defaultSessionType);
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
142
|
+
{t('chatSidebarNewTask')}
|
|
143
|
+
</Button>
|
|
144
|
+
{nonDefaultSessionTypeOptions.length > 0 ? (
|
|
145
|
+
<Popover open={isCreateMenuOpen} onOpenChange={setIsCreateMenuOpen}>
|
|
146
|
+
<PopoverTrigger asChild>
|
|
147
|
+
<Button
|
|
148
|
+
variant="primary"
|
|
149
|
+
size="icon"
|
|
150
|
+
className="h-9 w-10 shrink-0 rounded-xl rounded-l-md"
|
|
151
|
+
aria-label={t('chatSessionTypeLabel')}
|
|
152
|
+
>
|
|
153
|
+
<ChevronDown className="h-4 w-4" />
|
|
154
|
+
</Button>
|
|
155
|
+
</PopoverTrigger>
|
|
156
|
+
<PopoverContent align="end" className="w-64 p-2">
|
|
157
|
+
<div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wider text-gray-400">
|
|
158
|
+
{t('chatSessionTypeLabel')}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="mt-1 space-y-1">
|
|
161
|
+
{nonDefaultSessionTypeOptions.map((option) => (
|
|
162
|
+
<button
|
|
163
|
+
key={option.value}
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={() => {
|
|
166
|
+
presenter.chatSessionListManager.createSession(option.value);
|
|
167
|
+
setIsCreateMenuOpen(false);
|
|
168
|
+
}}
|
|
169
|
+
className="w-full rounded-xl px-3 py-2 text-left transition-colors hover:bg-gray-100"
|
|
170
|
+
>
|
|
171
|
+
<div className="text-[13px] font-medium text-gray-900">{option.label}</div>
|
|
172
|
+
<div className="mt-0.5 text-[11px] text-gray-500">{t('chatSidebarNewTask')}</div>
|
|
173
|
+
</button>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
</PopoverContent>
|
|
177
|
+
</Popover>
|
|
178
|
+
) : null}
|
|
179
|
+
</div>
|
|
105
180
|
</div>
|
|
106
181
|
|
|
107
182
|
<div className="px-4 pb-3">
|
|
@@ -168,6 +243,7 @@ export function ChatSidebar() {
|
|
|
168
243
|
{group.sessions.map((session) => {
|
|
169
244
|
const active = listSnapshot.selectedSessionKey === session.key;
|
|
170
245
|
const runStatus = runSnapshot.sessionRunStatusByKey.get(session.key);
|
|
246
|
+
const sessionTypeLabel = resolveSessionTypeLabel(session.sessionType, inputSnapshot.sessionTypeOptions);
|
|
171
247
|
return (
|
|
172
248
|
<button
|
|
173
249
|
key={session.key}
|
|
@@ -180,7 +256,21 @@ export function ChatSidebar() {
|
|
|
180
256
|
)}
|
|
181
257
|
>
|
|
182
258
|
<div className="grid grid-cols-[minmax(0,1fr)_0.875rem] items-center gap-1.5">
|
|
183
|
-
<span className="
|
|
259
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
260
|
+
<span className="truncate font-medium">{sessionTitle(session)}</span>
|
|
261
|
+
{sessionTypeLabel ? (
|
|
262
|
+
<span
|
|
263
|
+
className={cn(
|
|
264
|
+
'shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold leading-none',
|
|
265
|
+
active
|
|
266
|
+
? 'border-gray-300 bg-white/80 text-gray-700'
|
|
267
|
+
: 'border-gray-200 bg-gray-100 text-gray-500'
|
|
268
|
+
)}
|
|
269
|
+
>
|
|
270
|
+
{sessionTypeLabel}
|
|
271
|
+
</span>
|
|
272
|
+
) : null}
|
|
273
|
+
</span>
|
|
184
274
|
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
|
185
275
|
{runStatus ? <SessionRunBadge status={runStatus} /> : null}
|
|
186
276
|
</span>
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { ToolInvocationStatus, type UiMessage } from
|
|
2
|
-
import { adaptChatMessages } from
|
|
3
|
-
import type { ChatMessageSource } from
|
|
1
|
+
import { ToolInvocationStatus, type UiMessage } from "@nextclaw/agent-chat";
|
|
2
|
+
import { adaptChatMessages } from "@/components/chat/adapters/chat-message.adapter";
|
|
3
|
+
import type { ChatMessageSource } from "@/components/chat/adapters/chat-message.adapter";
|
|
4
4
|
|
|
5
5
|
function toSource(uiMessages: UiMessage[]): ChatMessageSource[] {
|
|
6
6
|
return uiMessages as unknown as ChatMessageSource[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
describe(
|
|
10
|
-
it(
|
|
9
|
+
describe("adaptChatMessages", () => {
|
|
10
|
+
it("maps markdown, reasoning, and tool parts into UI view models", () => {
|
|
11
11
|
const messages: UiMessage[] = [
|
|
12
12
|
{
|
|
13
|
-
id:
|
|
14
|
-
role:
|
|
13
|
+
id: "assistant-1",
|
|
14
|
+
role: "assistant",
|
|
15
15
|
meta: {
|
|
16
|
-
status:
|
|
17
|
-
timestamp:
|
|
16
|
+
status: "final",
|
|
17
|
+
timestamp: "2026-03-17T10:00:00.000Z",
|
|
18
18
|
},
|
|
19
19
|
parts: [
|
|
20
|
-
{ type:
|
|
20
|
+
{ type: "text", text: "hello world" },
|
|
21
21
|
{
|
|
22
|
-
type:
|
|
23
|
-
reasoning:
|
|
24
|
-
details: []
|
|
22
|
+
type: "reasoning",
|
|
23
|
+
reasoning: "internal reasoning",
|
|
24
|
+
details: [],
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
type:
|
|
27
|
+
type: "tool-invocation",
|
|
28
28
|
toolInvocation: {
|
|
29
29
|
status: ToolInvocationStatus.RESULT,
|
|
30
|
-
toolCallId:
|
|
31
|
-
toolName:
|
|
30
|
+
toolCallId: "call-1",
|
|
31
|
+
toolName: "web_search",
|
|
32
32
|
args: '{"q":"hello"}',
|
|
33
|
-
result: { ok: true }
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}
|
|
33
|
+
result: { ok: true },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
const adapted = adaptChatMessages({
|
|
@@ -42,97 +42,147 @@ describe('adaptChatMessages', () => {
|
|
|
42
42
|
formatTimestamp: (value) => `formatted:${value}`,
|
|
43
43
|
texts: {
|
|
44
44
|
roleLabels: {
|
|
45
|
-
user:
|
|
46
|
-
assistant:
|
|
47
|
-
tool:
|
|
48
|
-
system:
|
|
49
|
-
fallback:
|
|
45
|
+
user: "You",
|
|
46
|
+
assistant: "Assistant",
|
|
47
|
+
tool: "Tool",
|
|
48
|
+
system: "System",
|
|
49
|
+
fallback: "Message",
|
|
50
50
|
},
|
|
51
|
-
reasoningLabel:
|
|
52
|
-
toolCallLabel:
|
|
53
|
-
toolResultLabel:
|
|
54
|
-
toolNoOutputLabel:
|
|
55
|
-
toolOutputLabel:
|
|
56
|
-
unknownPartLabel:
|
|
57
|
-
}
|
|
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
58
|
});
|
|
59
59
|
|
|
60
60
|
expect(adapted).toHaveLength(1);
|
|
61
|
-
expect(adapted[0]?.roleLabel).toBe(
|
|
62
|
-
expect(adapted[0]?.timestampLabel).toBe(
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
});
|
|
65
75
|
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
66
|
-
type:
|
|
76
|
+
type: "tool-card",
|
|
67
77
|
card: {
|
|
68
|
-
titleLabel:
|
|
69
|
-
outputLabel:
|
|
70
|
-
}
|
|
78
|
+
titleLabel: "Tool Result",
|
|
79
|
+
outputLabel: "View Output",
|
|
80
|
+
},
|
|
71
81
|
});
|
|
72
82
|
});
|
|
73
83
|
|
|
74
|
-
it(
|
|
84
|
+
it("maps non-standard roles back to the generic message role", () => {
|
|
75
85
|
const adapted = adaptChatMessages({
|
|
76
86
|
uiMessages: [
|
|
77
87
|
{
|
|
78
|
-
id:
|
|
79
|
-
role:
|
|
80
|
-
parts: [{ type:
|
|
81
|
-
}
|
|
88
|
+
id: "data-1",
|
|
89
|
+
role: "data",
|
|
90
|
+
parts: [{ type: "text", text: "payload" }],
|
|
91
|
+
},
|
|
82
92
|
] as unknown as ChatMessageSource[],
|
|
83
|
-
formatTimestamp: () =>
|
|
93
|
+
formatTimestamp: () => "formatted",
|
|
84
94
|
texts: {
|
|
85
95
|
roleLabels: {
|
|
86
|
-
user:
|
|
87
|
-
assistant:
|
|
88
|
-
tool:
|
|
89
|
-
system:
|
|
90
|
-
fallback:
|
|
96
|
+
user: "You",
|
|
97
|
+
assistant: "Assistant",
|
|
98
|
+
tool: "Tool",
|
|
99
|
+
system: "System",
|
|
100
|
+
fallback: "Message",
|
|
91
101
|
},
|
|
92
|
-
reasoningLabel:
|
|
93
|
-
toolCallLabel:
|
|
94
|
-
toolResultLabel:
|
|
95
|
-
toolNoOutputLabel:
|
|
96
|
-
toolOutputLabel:
|
|
97
|
-
unknownPartLabel:
|
|
98
|
-
}
|
|
102
|
+
reasoningLabel: "Reasoning",
|
|
103
|
+
toolCallLabel: "Tool Call",
|
|
104
|
+
toolResultLabel: "Tool Result",
|
|
105
|
+
toolNoOutputLabel: "No output",
|
|
106
|
+
toolOutputLabel: "View Output",
|
|
107
|
+
unknownPartLabel: "Unknown Part",
|
|
108
|
+
},
|
|
99
109
|
});
|
|
100
110
|
|
|
101
|
-
expect(adapted[0]?.role).toBe(
|
|
102
|
-
expect(adapted[0]?.roleLabel).toBe(
|
|
111
|
+
expect(adapted[0]?.role).toBe("message");
|
|
112
|
+
expect(adapted[0]?.roleLabel).toBe("Message");
|
|
103
113
|
});
|
|
104
114
|
|
|
105
|
-
it(
|
|
115
|
+
it("maps unknown parts into a visible fallback part", () => {
|
|
106
116
|
const adapted = adaptChatMessages({
|
|
107
117
|
uiMessages: [
|
|
108
118
|
{
|
|
109
|
-
id:
|
|
110
|
-
role:
|
|
111
|
-
parts: [{ type:
|
|
112
|
-
}
|
|
119
|
+
id: "x-1",
|
|
120
|
+
role: "assistant",
|
|
121
|
+
parts: [{ type: "step-start", value: "x" }],
|
|
122
|
+
},
|
|
113
123
|
] as unknown as ChatMessageSource[],
|
|
114
|
-
formatTimestamp: () =>
|
|
124
|
+
formatTimestamp: () => "formatted",
|
|
115
125
|
texts: {
|
|
116
126
|
roleLabels: {
|
|
117
|
-
user:
|
|
118
|
-
assistant:
|
|
119
|
-
tool:
|
|
120
|
-
system:
|
|
121
|
-
fallback:
|
|
127
|
+
user: "You",
|
|
128
|
+
assistant: "Assistant",
|
|
129
|
+
tool: "Tool",
|
|
130
|
+
system: "System",
|
|
131
|
+
fallback: "Message",
|
|
122
132
|
},
|
|
123
|
-
reasoningLabel:
|
|
124
|
-
toolCallLabel:
|
|
125
|
-
toolResultLabel:
|
|
126
|
-
toolNoOutputLabel:
|
|
127
|
-
toolOutputLabel:
|
|
128
|
-
unknownPartLabel:
|
|
129
|
-
}
|
|
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
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
143
|
+
type: "unknown",
|
|
144
|
+
rawType: "step-start",
|
|
145
|
+
label: "Unknown Part",
|
|
130
146
|
});
|
|
147
|
+
});
|
|
131
148
|
|
|
149
|
+
it("drops empty and zero-width text parts during adaptation", () => {
|
|
150
|
+
const adapted = adaptChatMessages({
|
|
151
|
+
uiMessages: [
|
|
152
|
+
{
|
|
153
|
+
id: "assistant-mixed",
|
|
154
|
+
role: "assistant",
|
|
155
|
+
parts: [
|
|
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",
|
|
170
|
+
},
|
|
171
|
+
reasoningLabel: "Reasoning",
|
|
172
|
+
toolCallLabel: "Tool Call",
|
|
173
|
+
toolResultLabel: "Tool Result",
|
|
174
|
+
toolNoOutputLabel: "No output",
|
|
175
|
+
toolOutputLabel: "View Output",
|
|
176
|
+
unknownPartLabel: "Unknown Part",
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(adapted).toHaveLength(1);
|
|
181
|
+
expect(adapted[0]?.id).toBe("assistant-mixed");
|
|
182
|
+
expect(adapted[0]?.parts).toHaveLength(1);
|
|
132
183
|
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
133
|
-
type:
|
|
134
|
-
|
|
135
|
-
label: 'Unknown Part'
|
|
184
|
+
type: "markdown",
|
|
185
|
+
text: "\u200Bhello\u200B",
|
|
136
186
|
});
|
|
137
187
|
});
|
|
138
188
|
});
|
|
@@ -61,6 +61,8 @@ export type ChatMessageAdapterTexts = {
|
|
|
61
61
|
unknownPartLabel: string;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
|
|
65
|
+
|
|
64
66
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
65
67
|
return typeof value === 'object' && value !== null;
|
|
66
68
|
}
|
|
@@ -95,7 +97,10 @@ function resolveMessageTimestamp(message: ChatMessageSource): string {
|
|
|
95
97
|
return new Date().toISOString();
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
function resolveRoleLabel(
|
|
100
|
+
function resolveRoleLabel(
|
|
101
|
+
role: string,
|
|
102
|
+
texts: ChatMessageAdapterTexts['roleLabels']
|
|
103
|
+
): string {
|
|
99
104
|
if (role === 'user') {
|
|
100
105
|
return texts.user;
|
|
101
106
|
}
|
|
@@ -118,7 +123,10 @@ function resolveUiRole(role: string): ChatMessageRole {
|
|
|
118
123
|
return 'message';
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
function buildToolCard(
|
|
126
|
+
function buildToolCard(
|
|
127
|
+
toolCard: ToolCard,
|
|
128
|
+
texts: ChatMessageAdapterTexts
|
|
129
|
+
): ChatToolPartViewModel {
|
|
122
130
|
return {
|
|
123
131
|
kind: toolCard.kind,
|
|
124
132
|
toolName: toolCard.name,
|
|
@@ -131,6 +139,15 @@ function buildToolCard(toolCard: ToolCard, texts: ChatMessageAdapterTexts): Chat
|
|
|
131
139
|
};
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
function toRenderableText(value: string): string | null {
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
if (!trimmed) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const visible = trimmed.replace(INVISIBLE_ONLY_TEXT_PATTERN, "").trim();
|
|
148
|
+
return visible ? trimmed : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
134
151
|
export function adaptChatMessages(params: {
|
|
135
152
|
uiMessages: ChatMessageSource[];
|
|
136
153
|
texts: ChatMessageAdapterTexts;
|
|
@@ -145,7 +162,7 @@ export function adaptChatMessages(params: {
|
|
|
145
162
|
parts: message.parts
|
|
146
163
|
.map((part) => {
|
|
147
164
|
if (isTextPart(part)) {
|
|
148
|
-
const text = part.text
|
|
165
|
+
const text = toRenderableText(part.text);
|
|
149
166
|
if (!text) {
|
|
150
167
|
return null;
|
|
151
168
|
}
|
|
@@ -155,7 +172,7 @@ export function adaptChatMessages(params: {
|
|
|
155
172
|
};
|
|
156
173
|
}
|
|
157
174
|
if (isReasoningPart(part)) {
|
|
158
|
-
const text = part.reasoning
|
|
175
|
+
const text = toRenderableText(part.reasoning);
|
|
159
176
|
if (!text) {
|
|
160
177
|
return null;
|
|
161
178
|
}
|
|
@@ -168,11 +185,12 @@ export function adaptChatMessages(params: {
|
|
|
168
185
|
if (isToolInvocationPart(part)) {
|
|
169
186
|
const invocation = part.toolInvocation;
|
|
170
187
|
const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
|
|
171
|
-
const rawResult =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
const rawResult =
|
|
189
|
+
typeof invocation.error === 'string' && invocation.error.trim()
|
|
190
|
+
? invocation.error.trim()
|
|
191
|
+
: invocation.result != null
|
|
192
|
+
? stringifyUnknown(invocation.result).trim()
|
|
193
|
+
: '';
|
|
176
194
|
const hasResult =
|
|
177
195
|
invocation.status === 'result' || invocation.status === 'error' || invocation.status === 'cancelled';
|
|
178
196
|
const card: ToolCard = {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import {
|
|
3
|
+
createChatComposerTokenNode,
|
|
4
|
+
createChatComposerNodesFromText,
|
|
5
|
+
createEmptyChatComposerNodes,
|
|
6
|
+
extractChatComposerTokenKeys,
|
|
7
|
+
normalizeChatComposerNodes,
|
|
8
|
+
removeChatComposerTokenNodes,
|
|
9
|
+
serializeChatComposerPlainText
|
|
10
|
+
} from '@nextclaw/agent-chat-ui';
|
|
11
|
+
|
|
12
|
+
export function createInitialChatComposerNodes(): ChatComposerNode[] {
|
|
13
|
+
return createEmptyChatComposerNodes();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createChatComposerNodesFromDraft(text: string): ChatComposerNode[] {
|
|
17
|
+
return createChatComposerNodesFromText(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveChatComposerDraft(nodes: ChatComposerNode[]): string {
|
|
21
|
+
return serializeChatComposerPlainText(nodes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deriveSelectedSkillsFromComposer(nodes: ChatComposerNode[]): string[] {
|
|
25
|
+
return extractChatComposerTokenKeys(nodes, 'skill');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function syncComposerSkills(
|
|
29
|
+
nodes: ChatComposerNode[],
|
|
30
|
+
nextSkills: string[],
|
|
31
|
+
skillRecords: Array<{ spec: string; label?: string }>
|
|
32
|
+
): ChatComposerNode[] {
|
|
33
|
+
const nextSkillSet = new Set(nextSkills);
|
|
34
|
+
const prunedNodes = removeChatComposerTokenNodes(
|
|
35
|
+
nodes,
|
|
36
|
+
(node) => node.tokenKind === 'skill' && !nextSkillSet.has(node.tokenKey)
|
|
37
|
+
);
|
|
38
|
+
const existingSkills = extractChatComposerTokenKeys(prunedNodes, 'skill');
|
|
39
|
+
const recordMap = new Map(skillRecords.map((record) => [record.spec, record]));
|
|
40
|
+
const appendedNodes = nextSkills
|
|
41
|
+
.filter((skill) => !existingSkills.includes(skill))
|
|
42
|
+
.map((skill) =>
|
|
43
|
+
createChatComposerTokenNode({
|
|
44
|
+
tokenKind: 'skill',
|
|
45
|
+
tokenKey: skill,
|
|
46
|
+
label: recordMap.get(skill)?.label || skill
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return appendedNodes.length === 0
|
|
51
|
+
? prunedNodes
|
|
52
|
+
: normalizeChatComposerNodes([...prunedNodes, ...appendedNodes]);
|
|
53
|
+
}
|
|
@@ -3,7 +3,11 @@ import type { Dispatch, SetStateAction } from 'react';
|
|
|
3
3
|
import type { SessionEntryView, ThinkingLevel } from '@/api/types';
|
|
4
4
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
5
|
import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
resolveSelectedModelValue,
|
|
8
|
+
resolveRecentSessionPreferredModel,
|
|
9
|
+
useSyncSelectedModel
|
|
10
|
+
} from '@/components/chat/chat-page-runtime';
|
|
7
11
|
import {
|
|
8
12
|
useChatCapabilities,
|
|
9
13
|
useChatSessionTypes,
|
|
@@ -98,14 +102,38 @@ export function useChatPageData(params: UseChatPageDataParams) {
|
|
|
98
102
|
setPendingSessionType: params.setPendingSessionType,
|
|
99
103
|
sessionTypesData: sessionTypesQuery.data
|
|
100
104
|
});
|
|
105
|
+
const recentSessionPreferredModel = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
resolveRecentSessionPreferredModel({
|
|
108
|
+
sessions,
|
|
109
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
110
|
+
sessionType: sessionTypeState.selectedSessionType
|
|
111
|
+
}),
|
|
112
|
+
[params.selectedSessionKey, sessionTypeState.selectedSessionType, sessions]
|
|
113
|
+
);
|
|
101
114
|
|
|
102
115
|
useSyncSelectedModel({
|
|
103
116
|
modelOptions,
|
|
117
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
104
118
|
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
119
|
+
fallbackPreferredModel: recentSessionPreferredModel,
|
|
105
120
|
defaultModel: configQuery.data?.agents.defaults.model,
|
|
106
121
|
setSelectedModel: params.setSelectedModel
|
|
107
122
|
});
|
|
108
123
|
|
|
124
|
+
const hydratedSessionModel = useMemo(
|
|
125
|
+
() =>
|
|
126
|
+
resolveSelectedModelValue({
|
|
127
|
+
currentSelectedModel: '',
|
|
128
|
+
modelOptions,
|
|
129
|
+
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
130
|
+
fallbackPreferredModel: recentSessionPreferredModel,
|
|
131
|
+
defaultModel: configQuery.data?.agents.defaults.model,
|
|
132
|
+
preferSessionPreferredModel: true
|
|
133
|
+
}),
|
|
134
|
+
[configQuery.data?.agents.defaults.model, modelOptions, recentSessionPreferredModel, selectedSession?.preferredModel]
|
|
135
|
+
);
|
|
136
|
+
|
|
109
137
|
const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
|
|
110
138
|
const selectedSessionThinkingLevel = useMemo(() => {
|
|
111
139
|
if (!params.selectedSessionKey) {
|
|
@@ -143,6 +171,7 @@ export function useChatPageData(params: UseChatPageDataParams) {
|
|
|
143
171
|
sessions,
|
|
144
172
|
skillRecords,
|
|
145
173
|
selectedSession,
|
|
174
|
+
hydratedSessionModel,
|
|
146
175
|
historyMessages,
|
|
147
176
|
selectedSessionThinkingLevel,
|
|
148
177
|
...sessionTypeState
|