@nextclaw/ui 0.7.0 → 0.9.0
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 +30 -0
- package/dist/assets/ChannelsList-C7F_As4r.js +1 -0
- package/dist/assets/ChatPage-Oo7-OUsx.js +37 -0
- package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-Dsd8Dlq8.js} +1 -1
- package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-2ChEc_oz.js} +1 -1
- package/dist/assets/MarketplacePage-BXck6-X3.js +49 -0
- package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-CgHRSD0b.js} +1 -1
- package/dist/assets/ProvidersList-PPfZucvS.js +1 -0
- package/dist/assets/RuntimeConfig-ClLEKNTN.js +1 -0
- package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-CuXVCbrf.js} +1 -1
- package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-udJz6Ake.js} +2 -2
- package/dist/assets/SessionsConfig-C1XnFfiC.js +2 -0
- package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-BETwXLD4.js} +3 -3
- package/dist/assets/{index-uMsNsQX6.js → index-COJdlL0e.js} +1 -1
- package/dist/assets/index-CsvP4CER.js +8 -0
- package/dist/assets/index-D-bXl7qL.css +1 -0
- package/dist/assets/{label-D8ly4a2P.js → label-BGL-ztxh.js} +1 -1
- package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-aw88k7tG.js} +1 -1
- package/dist/assets/popover-DyEvzhmV.js +1 -0
- package/dist/assets/security-config-BuPAQn82.js +1 -0
- package/dist/assets/skeleton-drzO_tdU.js +1 -0
- package/dist/assets/{switch-Ce_g9lpN.js → switch-BK8jIzto.js} +1 -1
- package/dist/assets/{tabs-custom-Cf5azvT5.js → tabs-custom-Da3cEOji.js} +1 -1
- package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-z0CE92iS.js} +2 -2
- package/dist/assets/{vendor-B7ozqnFC.js → vendor-CkJHmX1g.js} +65 -70
- package/dist/index.html +3 -3
- package/package.json +5 -2
- package/src/api/config.ts +9 -0
- package/src/api/ncp-session.ts +50 -0
- package/src/api/types.ts +20 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
- package/src/components/chat/ChatConversationPanel.tsx +21 -12
- package/src/components/chat/ChatPage.tsx +10 -324
- 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 -81
- package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
- package/src/components/chat/chat-chain.test.ts +22 -0
- package/src/components/chat/chat-chain.ts +23 -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-page-shell.tsx +103 -0
- 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/containers/chat-input-bar.container.tsx +0 -22
- package/src/components/chat/containers/chat-message-list.container.tsx +34 -26
- package/src/components/chat/legacy/LegacyChatPage.tsx +252 -0
- package/src/components/chat/managers/chat-input.manager.ts +5 -0
- 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 +381 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +179 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +166 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
- package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +75 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +214 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
- 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 +41 -1
- package/src/hooks/useMarketplace.ts +7 -4
- package/src/hooks/useWebSocket.ts +23 -2
- package/src/lib/i18n.ts +1 -1
- package/tailwind.config.js +8 -3
- package/tsconfig.json +4 -1
- package/dist/assets/ChannelsList-DF2U-LY1.js +0 -1
- package/dist/assets/ChatPage-BX39y0U5.js +0 -36
- package/dist/assets/MarketplacePage-DG5mHWJ8.js +0 -49
- package/dist/assets/ProvidersList-CH5z00YT.js +0 -1
- package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
- package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
- package/dist/assets/index-BLeJkJ0o.css +0 -1
- package/dist/assets/index-DK4TS5ev.js +0 -8
- package/dist/assets/index-X5J6Mm--.js +0 -1
- package/dist/assets/security-config-DlKEYHNN.js +0 -1
- package/dist/assets/skeleton-CWbsNx2h.js +0 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ChatSidebar } from '@/components/chat/ChatSidebar';
|
|
5
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
6
|
+
import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
|
|
7
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
createSession: vi.fn(),
|
|
11
|
+
setQuery: vi.fn(),
|
|
12
|
+
selectSession: vi.fn(),
|
|
13
|
+
docOpen: vi.fn()
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
17
|
+
usePresenter: () => ({
|
|
18
|
+
chatSessionListManager: {
|
|
19
|
+
createSession: mocks.createSession,
|
|
20
|
+
setQuery: mocks.setQuery,
|
|
21
|
+
selectSession: mocks.selectSession
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('@/components/doc-browser', () => ({
|
|
27
|
+
useDocBrowser: () => ({
|
|
28
|
+
open: mocks.docOpen
|
|
29
|
+
})
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@/components/common/BrandHeader', () => ({
|
|
33
|
+
BrandHeader: () => <div data-testid="brand-header" />
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('@/components/common/StatusBadge', () => ({
|
|
37
|
+
StatusBadge: () => <div data-testid="status-badge" />
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock('@/components/providers/I18nProvider', () => ({
|
|
41
|
+
useI18n: () => ({
|
|
42
|
+
language: 'en',
|
|
43
|
+
setLanguage: vi.fn()
|
|
44
|
+
})
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock('@/components/providers/ThemeProvider', () => ({
|
|
48
|
+
useTheme: () => ({
|
|
49
|
+
theme: 'warm',
|
|
50
|
+
setTheme: vi.fn()
|
|
51
|
+
})
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock('@/stores/ui.store', () => ({
|
|
55
|
+
useUiStore: (selector: (state: { connectionStatus: string }) => unknown) =>
|
|
56
|
+
selector({ connectionStatus: 'connected' })
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
describe('ChatSidebar', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
mocks.createSession.mockReset();
|
|
62
|
+
mocks.setQuery.mockReset();
|
|
63
|
+
mocks.selectSession.mockReset();
|
|
64
|
+
mocks.docOpen.mockReset();
|
|
65
|
+
|
|
66
|
+
useChatInputStore.setState({
|
|
67
|
+
snapshot: {
|
|
68
|
+
...useChatInputStore.getState().snapshot,
|
|
69
|
+
defaultSessionType: 'native',
|
|
70
|
+
sessionTypeOptions: [
|
|
71
|
+
{ value: 'native', label: 'Native' },
|
|
72
|
+
{ value: 'codex', label: 'Codex' }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
useChatSessionListStore.setState({
|
|
77
|
+
snapshot: {
|
|
78
|
+
...useChatSessionListStore.getState().snapshot,
|
|
79
|
+
sessions: [],
|
|
80
|
+
query: '',
|
|
81
|
+
isLoading: false
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
useChatRunStatusStore.setState({
|
|
85
|
+
snapshot: {
|
|
86
|
+
...useChatRunStatusStore.getState().snapshot,
|
|
87
|
+
sessionRunStatusByKey: new Map()
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('closes the create-session menu after choosing a non-default session type', async () => {
|
|
93
|
+
render(
|
|
94
|
+
<MemoryRouter>
|
|
95
|
+
<ChatSidebar />
|
|
96
|
+
</MemoryRouter>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
fireEvent.click(screen.getByLabelText('Session Type'));
|
|
100
|
+
fireEvent.click(screen.getByText('Codex'));
|
|
101
|
+
|
|
102
|
+
expect(mocks.createSession).toHaveBeenCalledWith('codex');
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(screen.queryByText('Codex')).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('shows a session type badge for non-native sessions in the list', () => {
|
|
109
|
+
useChatSessionListStore.setState({
|
|
110
|
+
snapshot: {
|
|
111
|
+
...useChatSessionListStore.getState().snapshot,
|
|
112
|
+
sessions: [
|
|
113
|
+
{
|
|
114
|
+
key: 'session:codex-1',
|
|
115
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
116
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
117
|
+
label: 'Codex Task',
|
|
118
|
+
sessionType: 'codex',
|
|
119
|
+
sessionTypeMutable: false,
|
|
120
|
+
messageCount: 2
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
render(
|
|
127
|
+
<MemoryRouter>
|
|
128
|
+
<ChatSidebar />
|
|
129
|
+
</MemoryRouter>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(screen.getByText('Codex Task')).not.toBeNull();
|
|
133
|
+
expect(screen.getByText('Codex')).not.toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('formats non-native session badges generically when the type is no longer in the available options', () => {
|
|
137
|
+
useChatInputStore.setState({
|
|
138
|
+
snapshot: {
|
|
139
|
+
...useChatInputStore.getState().snapshot,
|
|
140
|
+
sessionTypeOptions: [{ value: 'native', label: 'Native' }]
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
useChatSessionListStore.setState({
|
|
144
|
+
snapshot: {
|
|
145
|
+
...useChatSessionListStore.getState().snapshot,
|
|
146
|
+
sessions: [
|
|
147
|
+
{
|
|
148
|
+
key: 'session:workspace-agent-1',
|
|
149
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
150
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
151
|
+
label: 'Workspace Task',
|
|
152
|
+
sessionType: 'workspace-agent',
|
|
153
|
+
sessionTypeMutable: false,
|
|
154
|
+
messageCount: 2
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
render(
|
|
161
|
+
<MemoryRouter>
|
|
162
|
+
<ChatSidebar />
|
|
163
|
+
</MemoryRouter>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(screen.getByText('Workspace Task')).not.toBeNull();
|
|
167
|
+
expect(screen.getByText('Workspace Agent')).not.toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('does not show a session type badge for native sessions in the list', () => {
|
|
171
|
+
useChatInputStore.setState({
|
|
172
|
+
snapshot: {
|
|
173
|
+
...useChatInputStore.getState().snapshot,
|
|
174
|
+
sessionTypeOptions: [{ value: 'native', label: 'Native' }]
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
useChatSessionListStore.setState({
|
|
178
|
+
snapshot: {
|
|
179
|
+
...useChatSessionListStore.getState().snapshot,
|
|
180
|
+
sessions: [
|
|
181
|
+
{
|
|
182
|
+
key: 'session:native-1',
|
|
183
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
184
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
185
|
+
label: 'Native Task',
|
|
186
|
+
sessionType: 'native',
|
|
187
|
+
sessionTypeMutable: false,
|
|
188
|
+
messageCount: 1
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
render(
|
|
195
|
+
<MemoryRouter>
|
|
196
|
+
<ChatSidebar />
|
|
197
|
+
</MemoryRouter>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(screen.getByText('Native Task')).not.toBeNull();
|
|
201
|
+
expect(screen.queryByText('Native')).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -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,96 +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
|
-
|
|
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
|
+
});
|
|
64
75
|
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
65
|
-
type:
|
|
76
|
+
type: "tool-card",
|
|
66
77
|
card: {
|
|
67
|
-
titleLabel:
|
|
68
|
-
outputLabel:
|
|
69
|
-
}
|
|
78
|
+
titleLabel: "Tool Result",
|
|
79
|
+
outputLabel: "View Output",
|
|
80
|
+
},
|
|
70
81
|
});
|
|
71
82
|
});
|
|
72
83
|
|
|
73
|
-
it(
|
|
84
|
+
it("maps non-standard roles back to the generic message role", () => {
|
|
74
85
|
const adapted = adaptChatMessages({
|
|
75
86
|
uiMessages: [
|
|
76
87
|
{
|
|
77
|
-
id:
|
|
78
|
-
role:
|
|
79
|
-
parts: [{ type:
|
|
80
|
-
}
|
|
88
|
+
id: "data-1",
|
|
89
|
+
role: "data",
|
|
90
|
+
parts: [{ type: "text", text: "payload" }],
|
|
91
|
+
},
|
|
81
92
|
] as unknown as ChatMessageSource[],
|
|
82
|
-
formatTimestamp: () =>
|
|
93
|
+
formatTimestamp: () => "formatted",
|
|
83
94
|
texts: {
|
|
84
95
|
roleLabels: {
|
|
85
|
-
user:
|
|
86
|
-
assistant:
|
|
87
|
-
tool:
|
|
88
|
-
system:
|
|
89
|
-
fallback:
|
|
96
|
+
user: "You",
|
|
97
|
+
assistant: "Assistant",
|
|
98
|
+
tool: "Tool",
|
|
99
|
+
system: "System",
|
|
100
|
+
fallback: "Message",
|
|
90
101
|
},
|
|
91
|
-
reasoningLabel:
|
|
92
|
-
toolCallLabel:
|
|
93
|
-
toolResultLabel:
|
|
94
|
-
toolNoOutputLabel:
|
|
95
|
-
toolOutputLabel:
|
|
96
|
-
unknownPartLabel:
|
|
97
|
-
}
|
|
102
|
+
reasoningLabel: "Reasoning",
|
|
103
|
+
toolCallLabel: "Tool Call",
|
|
104
|
+
toolResultLabel: "Tool Result",
|
|
105
|
+
toolNoOutputLabel: "No output",
|
|
106
|
+
toolOutputLabel: "View Output",
|
|
107
|
+
unknownPartLabel: "Unknown Part",
|
|
108
|
+
},
|
|
98
109
|
});
|
|
99
110
|
|
|
100
|
-
expect(adapted[0]?.role).toBe(
|
|
101
|
-
expect(adapted[0]?.roleLabel).toBe(
|
|
111
|
+
expect(adapted[0]?.role).toBe("message");
|
|
112
|
+
expect(adapted[0]?.roleLabel).toBe("Message");
|
|
102
113
|
});
|
|
103
114
|
|
|
104
|
-
it(
|
|
115
|
+
it("maps unknown parts into a visible fallback part", () => {
|
|
105
116
|
const adapted = adaptChatMessages({
|
|
106
117
|
uiMessages: [
|
|
107
118
|
{
|
|
108
|
-
id:
|
|
109
|
-
role:
|
|
110
|
-
parts: [{ type:
|
|
111
|
-
}
|
|
119
|
+
id: "x-1",
|
|
120
|
+
role: "assistant",
|
|
121
|
+
parts: [{ type: "step-start", value: "x" }],
|
|
122
|
+
},
|
|
112
123
|
] as unknown as ChatMessageSource[],
|
|
113
|
-
formatTimestamp: () =>
|
|
124
|
+
formatTimestamp: () => "formatted",
|
|
114
125
|
texts: {
|
|
115
126
|
roleLabels: {
|
|
116
|
-
user:
|
|
117
|
-
assistant:
|
|
118
|
-
tool:
|
|
119
|
-
system:
|
|
120
|
-
fallback:
|
|
127
|
+
user: "You",
|
|
128
|
+
assistant: "Assistant",
|
|
129
|
+
tool: "Tool",
|
|
130
|
+
system: "System",
|
|
131
|
+
fallback: "Message",
|
|
121
132
|
},
|
|
122
|
-
reasoningLabel:
|
|
123
|
-
toolCallLabel:
|
|
124
|
-
toolResultLabel:
|
|
125
|
-
toolNoOutputLabel:
|
|
126
|
-
toolOutputLabel:
|
|
127
|
-
unknownPartLabel:
|
|
128
|
-
}
|
|
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",
|
|
129
146
|
});
|
|
147
|
+
});
|
|
130
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);
|
|
131
183
|
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
132
|
-
type:
|
|
133
|
-
|
|
134
|
-
label: 'Unknown Part'
|
|
184
|
+
type: "markdown",
|
|
185
|
+
text: "\u200Bhello\u200B",
|
|
135
186
|
});
|
|
136
187
|
});
|
|
137
188
|
});
|