@nextclaw/ui 0.12.3 → 0.12.4
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 +8 -0
- package/dist/assets/{ChannelsList-DZWam3Ob.js → ChannelsList-CobWeI2V.js} +1 -1
- package/dist/assets/ChatPage-ZIdFFVAv.js +43 -0
- package/dist/assets/DocBrowser-D55C0iyl.js +1 -0
- package/dist/assets/{DocBrowser-C7-1sXqo.js → DocBrowser-NSzgVKka.js} +1 -1
- package/dist/assets/{DocBrowserContext-DN5tjUoS.js → DocBrowserContext-DpgVdRgk.js} +1 -1
- package/dist/assets/{LogoBadge-DDS1sU_U.js → LogoBadge-CHS4YNLw.js} +1 -1
- package/dist/assets/{MarketplacePage-2tWWgwAb.js → MarketplacePage-BFYsRss_.js} +1 -1
- package/dist/assets/MarketplacePage-DII-q-Y1.js +1 -0
- package/dist/assets/{McpMarketplacePage-N-fB4HID.js → McpMarketplacePage-CPqsGJzz.js} +1 -1
- package/dist/assets/{ModelConfig-DvsBTUiE.js → ModelConfig-Bvuo_IpS.js} +1 -1
- package/dist/assets/{ProviderScopedModelInput-D9woCARc.js → ProviderScopedModelInput-BfY8rGsf.js} +1 -1
- package/dist/assets/{ProvidersList-D-qPGgC4.js → ProvidersList-3tlaqwSS.js} +1 -1
- package/dist/assets/{RemoteAccessPage-COnjm8_x.js → RemoteAccessPage-yfbrveNQ.js} +1 -1
- package/dist/assets/{RuntimeConfig-BHpqcaHm.js → RuntimeConfig-CAd5Kta3.js} +1 -1
- package/dist/assets/{SearchConfig-DIT6M65Q.js → SearchConfig-DFwgaAa7.js} +1 -1
- package/dist/assets/{SecretsConfig-Cefg1LFJ.js → SecretsConfig-CLFSSoTl.js} +1 -1
- package/dist/assets/{SessionsConfig-BZnmVTIu.js → SessionsConfig-vYrvc2Fk.js} +1 -1
- package/dist/assets/{book-open-DvWqOode.js → book-open-C7TAghTk.js} +1 -1
- package/dist/assets/{chat-session-display-D4bYa0b8.js → chat-session-display-5dVFkJyw.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-CxfKRD7X.js → chunk-JZWAC4HX-DbL4EmiT.js} +1 -1
- package/dist/assets/{config-BeGwf2Ao.js → config-CMiW0yaK.js} +1 -1
- package/dist/assets/{createLucideIcon-C7MmdIX3.js → createLucideIcon-BRLFtf-8.js} +1 -1
- package/dist/assets/{dist-RWNFhxvR.js → dist-BFc_H-lY.js} +1 -1
- package/dist/assets/{dist-B6VMuIQN.js → dist-DP-JKR4G.js} +1 -1
- package/dist/assets/{external-link-U86Acd1t.js → external-link-BkJkiWbH.js} +1 -1
- package/dist/assets/{hash-D-OVfV3Z.js → hash-CbP6-6R9.js} +1 -1
- package/dist/assets/i18n-C_2dKw6w.js +1 -0
- package/dist/assets/index-ChUXhq0G.css +1 -0
- package/dist/assets/{index-DHmCjcxq.js → index-DAE8Srx-.js} +3 -3
- package/dist/assets/{label-CHJ1ATds.js → label-D8yyejJS.js} +1 -1
- package/dist/assets/loader-circle-B0sKKO29.js +1 -0
- package/dist/assets/{logos-U1_qDA3U.js → logos-N3dbS6-I.js} +1 -1
- package/dist/assets/{page-layout-Z1klaUFW.js → page-layout-DyuvlNrg.js} +1 -1
- package/dist/assets/plus-CYXs3JtZ.js +1 -0
- package/dist/assets/{popover-xWbqMnIN.js → popover-BKKWGUaG.js} +1 -1
- package/dist/assets/{react-3YE87-lE.js → react-8EIEQjMP.js} +1 -1
- package/dist/assets/{refresh-ccw-JQh1lwq-.js → refresh-ccw-BGMdiNGq.js} +1 -1
- package/dist/assets/{save-4VRlzkii.js → save-Dh4GQzzX.js} +1 -1
- package/dist/assets/search-DOsLw-P9.js +1 -0
- package/dist/assets/{security-config-DEgOD4VX.js → security-config-CM_tQRXQ.js} +1 -1
- package/dist/assets/{select-DF-AUoie.js → select-BtIi5fnh.js} +1 -1
- package/dist/assets/skeleton-GbHLjPC0.js +1 -0
- package/dist/assets/{status-dot-Bq_8Ojvv.js → status-dot-C4O-2jZP.js} +1 -1
- package/dist/assets/{switch-D7JF_RZ-.js → switch-DPegGIa_.js} +1 -1
- package/dist/assets/{tabs-custom-CLksZ2bO.js → tabs-custom-x5GZexrF.js} +1 -1
- package/dist/assets/{trash-2-VV8jvziy.js → trash-2-CU3LYIpQ.js} +1 -1
- package/dist/assets/{useConfirmDialog-CuQqiPx7.js → useConfirmDialog-S5WsGOGf.js} +1 -1
- package/dist/assets/{useMutation-DBTWPbTg.js → useMutation-DSinpgEq.js} +1 -1
- package/dist/assets/x-Bnco_K8b.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +5 -5
- package/src/components/chat/ChatSidebar.test.tsx +168 -28
- package/src/components/chat/ChatSidebar.tsx +103 -28
- package/src/components/chat/chat-sidebar-list-mode-switch.tsx +43 -0
- package/src/components/chat/chat-sidebar-project-groups.tsx +152 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +4 -4
- package/src/components/chat/managers/chat-session-list.manager.ts +3 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +18 -4
- package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -5
- package/src/components/chat/session-header/chat-session-project-badge.test.tsx +16 -0
- package/src/components/chat/session-header/chat-session-project-badge.tsx +2 -2
- package/src/lib/i18n.chat.ts +3 -0
- package/dist/assets/ChatPage-YBL7iJ1X.js +0 -43
- package/dist/assets/DocBrowser-DQjtSsY3.js +0 -1
- package/dist/assets/MarketplacePage-BorWJftJ.js +0 -1
- package/dist/assets/i18n-hM3v-3YG.js +0 -1
- package/dist/assets/index-CpxuJa9o.css +0 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +0 -1
- package/dist/assets/plus-CrkO1kob.js +0 -1
- package/dist/assets/search-EX-Papzl.js +0 -1
- package/dist/assets/skeleton-B0mmt1vo.js +0 -1
- package/dist/assets/x-B4sxJkGY.js +0 -1
|
@@ -7,6 +7,11 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
8
8
|
import { SelectItem } from '@/components/ui/select';
|
|
9
9
|
import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
|
|
10
|
+
import { ChatSidebarListModeSwitch } from '@/components/chat/chat-sidebar-list-mode-switch';
|
|
11
|
+
import {
|
|
12
|
+
ChatSidebarProjectGroups,
|
|
13
|
+
type ChatSidebarProjectGroup
|
|
14
|
+
} from '@/components/chat/chat-sidebar-project-groups';
|
|
10
15
|
import { resolveSessionContextView } from '@/lib/session-context.utils';
|
|
11
16
|
import { useChatSessionLabel } from '@/components/chat/hooks/use-chat-session-label';
|
|
12
17
|
import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
@@ -14,6 +19,7 @@ import { usePresenter } from '@/components/chat/presenter/chat-presenter-context
|
|
|
14
19
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
15
20
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
16
21
|
import { useAgents } from '@/hooks/agents/useAgents';
|
|
22
|
+
import { getSessionProjectName } from '@/lib/session-project/session-project.utils';
|
|
17
23
|
import { cn } from '@/lib/utils';
|
|
18
24
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
19
25
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
@@ -41,6 +47,14 @@ type DateGroup = {
|
|
|
41
47
|
items: NcpSessionListItemView[];
|
|
42
48
|
};
|
|
43
49
|
|
|
50
|
+
function getSessionUpdatedAtTimestamp(item: NcpSessionListItemView): number {
|
|
51
|
+
return new Date(item.session.updatedAt).getTime();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sortSessionItemsByUpdatedAtDesc(items: NcpSessionListItemView[]): NcpSessionListItemView[] {
|
|
55
|
+
return [...items].sort((left, right) => getSessionUpdatedAtTimestamp(right) - getSessionUpdatedAtTimestamp(left));
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
|
|
45
59
|
const now = new Date();
|
|
46
60
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
@@ -74,6 +88,37 @@ function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
|
|
|
74
88
|
return groups;
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
function groupSessionsByProject(items: NcpSessionListItemView[]): ChatSidebarProjectGroup[] {
|
|
92
|
+
const grouped = new Map<string, ChatSidebarProjectGroup>();
|
|
93
|
+
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
const projectRoot = item.session.projectRoot?.trim();
|
|
96
|
+
if (!projectRoot) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const existingGroup = grouped.get(projectRoot);
|
|
100
|
+
const updatedAt = getSessionUpdatedAtTimestamp(item);
|
|
101
|
+
if (existingGroup) {
|
|
102
|
+
existingGroup.items.push(item);
|
|
103
|
+
existingGroup.latestUpdatedAt = Math.max(existingGroup.latestUpdatedAt, updatedAt);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
grouped.set(projectRoot, {
|
|
107
|
+
projectRoot,
|
|
108
|
+
projectName: item.session.projectName?.trim() || getSessionProjectName(projectRoot) || projectRoot,
|
|
109
|
+
items: [item],
|
|
110
|
+
latestUpdatedAt: updatedAt
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return [...grouped.values()]
|
|
115
|
+
.map((group) => ({
|
|
116
|
+
...group,
|
|
117
|
+
items: sortSessionItemsByUpdatedAtDesc(group.items)
|
|
118
|
+
}))
|
|
119
|
+
.sort((left, right) => right.latestUpdatedAt - left.latestUpdatedAt);
|
|
120
|
+
}
|
|
121
|
+
|
|
77
122
|
function sessionTitle(session: SessionEntryView): string {
|
|
78
123
|
if (session.label && session.label.trim()) {
|
|
79
124
|
return session.label.trim();
|
|
@@ -120,12 +165,15 @@ export function ChatSidebar() {
|
|
|
120
165
|
[agentsQuery.data?.agents]
|
|
121
166
|
);
|
|
122
167
|
|
|
123
|
-
const
|
|
168
|
+
const sortedItems = useMemo(() => sortSessionItemsByUpdatedAtDesc(items), [items]);
|
|
169
|
+
const groups = useMemo(() => groupSessionsByDate(sortedItems), [sortedItems]);
|
|
170
|
+
const projectGroups = useMemo(() => groupSessionsByProject(sortedItems), [sortedItems]);
|
|
124
171
|
const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
|
|
125
172
|
const nonDefaultSessionTypeOptions = useMemo(
|
|
126
173
|
() => inputSnapshot.sessionTypeOptions.filter((option) => option.value !== defaultSessionType),
|
|
127
174
|
[defaultSessionType, inputSnapshot.sessionTypeOptions]
|
|
128
175
|
);
|
|
176
|
+
const isProjectFirstView = listSnapshot.listMode === 'project-first';
|
|
129
177
|
|
|
130
178
|
const handleLanguageSwitch = (nextLang: I18nLanguage) => {
|
|
131
179
|
if (language === nextLang) return;
|
|
@@ -164,6 +212,34 @@ export function ChatSidebar() {
|
|
|
164
212
|
}
|
|
165
213
|
};
|
|
166
214
|
|
|
215
|
+
const renderSessionItem = ({ session, runStatus }: NcpSessionListItemView) => {
|
|
216
|
+
const active = listSnapshot.selectedSessionKey === session.key;
|
|
217
|
+
const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
|
|
218
|
+
const isEditing = editingSessionKey === session.key;
|
|
219
|
+
const isSaving = savingSessionKey === session.key;
|
|
220
|
+
return (
|
|
221
|
+
<ChatSidebarSessionItem
|
|
222
|
+
key={session.key}
|
|
223
|
+
session={session}
|
|
224
|
+
active={active}
|
|
225
|
+
runStatus={runStatus}
|
|
226
|
+
context={context}
|
|
227
|
+
title={sessionTitle(session)}
|
|
228
|
+
agentId={session.agentId ?? null}
|
|
229
|
+
agentLabel={session.agentId ? (agentsById.get(session.agentId)?.displayName ?? session.agentId) : null}
|
|
230
|
+
agentAvatarUrl={session.agentId ? (agentsById.get(session.agentId)?.avatarUrl ?? null) : null}
|
|
231
|
+
isEditing={isEditing}
|
|
232
|
+
draftLabel={draftLabel}
|
|
233
|
+
isSaving={isSaving}
|
|
234
|
+
onSelect={() => presenter.chatSessionListManager.selectSession(session.key)}
|
|
235
|
+
onStartEditing={() => startEditingSessionLabel(session)}
|
|
236
|
+
onDraftLabelChange={setDraftLabel}
|
|
237
|
+
onSave={() => saveSessionLabel(session)}
|
|
238
|
+
onCancel={cancelEditingSessionLabel}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
167
243
|
return (
|
|
168
244
|
<aside className="w-[280px] shrink-0 flex flex-col h-full bg-secondary border-r border-gray-200/60">
|
|
169
245
|
<div className="px-5 pt-5 pb-3">
|
|
@@ -272,9 +348,34 @@ export function ChatSidebar() {
|
|
|
272
348
|
|
|
273
349
|
<div className="mx-4 border-t border-gray-200/60" />
|
|
274
350
|
|
|
351
|
+
<div className="flex items-center justify-between px-5 pb-2 pt-3">
|
|
352
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-gray-400">
|
|
353
|
+
{t('chatSidebarTaskRecords')}
|
|
354
|
+
</div>
|
|
355
|
+
<ChatSidebarListModeSwitch
|
|
356
|
+
isProjectFirstView={isProjectFirstView}
|
|
357
|
+
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
275
361
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
|
|
276
362
|
{isLoading ? (
|
|
277
363
|
<div className="text-xs text-gray-500 p-3">{t('sessionsLoading')}</div>
|
|
364
|
+
) : isProjectFirstView ? (
|
|
365
|
+
projectGroups.length === 0 ? (
|
|
366
|
+
<div className="p-4 text-center">
|
|
367
|
+
<MessageSquareText className="h-6 w-6 mx-auto mb-2 text-gray-300" />
|
|
368
|
+
<div className="text-xs text-gray-500">{t('chatSidebarProjectViewEmpty')}</div>
|
|
369
|
+
</div>
|
|
370
|
+
) : (
|
|
371
|
+
<ChatSidebarProjectGroups
|
|
372
|
+
groups={projectGroups}
|
|
373
|
+
defaultSessionType={defaultSessionType}
|
|
374
|
+
sessionTypeOptions={inputSnapshot.sessionTypeOptions}
|
|
375
|
+
renderSessionItem={renderSessionItem}
|
|
376
|
+
onCreateSession={presenter.chatSessionListManager.createSession}
|
|
377
|
+
/>
|
|
378
|
+
)
|
|
278
379
|
) : groups.length === 0 ? (
|
|
279
380
|
<div className="p-4 text-center">
|
|
280
381
|
<MessageSquareText className="h-6 w-6 mx-auto mb-2 text-gray-300" />
|
|
@@ -288,33 +389,7 @@ export function ChatSidebar() {
|
|
|
288
389
|
{group.label}
|
|
289
390
|
</div>
|
|
290
391
|
<div className="space-y-0.5">
|
|
291
|
-
{group.items.map(
|
|
292
|
-
const active = listSnapshot.selectedSessionKey === session.key;
|
|
293
|
-
const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
|
|
294
|
-
const isEditing = editingSessionKey === session.key;
|
|
295
|
-
const isSaving = savingSessionKey === session.key;
|
|
296
|
-
return (
|
|
297
|
-
<ChatSidebarSessionItem
|
|
298
|
-
key={session.key}
|
|
299
|
-
session={session}
|
|
300
|
-
active={active}
|
|
301
|
-
runStatus={runStatus}
|
|
302
|
-
context={context}
|
|
303
|
-
title={sessionTitle(session)}
|
|
304
|
-
agentId={session.agentId ?? null}
|
|
305
|
-
agentLabel={session.agentId ? (agentsById.get(session.agentId)?.displayName ?? session.agentId) : null}
|
|
306
|
-
agentAvatarUrl={session.agentId ? (agentsById.get(session.agentId)?.avatarUrl ?? null) : null}
|
|
307
|
-
isEditing={isEditing}
|
|
308
|
-
draftLabel={draftLabel}
|
|
309
|
-
isSaving={isSaving}
|
|
310
|
-
onSelect={() => presenter.chatSessionListManager.selectSession(session.key)}
|
|
311
|
-
onStartEditing={() => startEditingSessionLabel(session)}
|
|
312
|
-
onDraftLabelChange={setDraftLabel}
|
|
313
|
-
onSave={() => saveSessionLabel(session)}
|
|
314
|
-
onCancel={cancelEditingSessionLabel}
|
|
315
|
-
/>
|
|
316
|
-
);
|
|
317
|
-
})}
|
|
392
|
+
{group.items.map(renderSessionItem)}
|
|
318
393
|
</div>
|
|
319
394
|
</div>
|
|
320
395
|
))}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { t } from '@/lib/i18n';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
type ChatSidebarListModeSwitchProps = {
|
|
5
|
+
isProjectFirstView: boolean;
|
|
6
|
+
onSelectMode: (mode: 'time-first' | 'project-first') => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function ChatSidebarListModeSwitch(props: ChatSidebarListModeSwitchProps) {
|
|
10
|
+
const { isProjectFirstView, onSelectMode } = props;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex items-center gap-1.5 text-[11px]">
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
aria-pressed={!isProjectFirstView}
|
|
17
|
+
onClick={() => onSelectMode('time-first')}
|
|
18
|
+
className={cn(
|
|
19
|
+
'transition-colors',
|
|
20
|
+
isProjectFirstView
|
|
21
|
+
? 'text-gray-400 hover:text-gray-600'
|
|
22
|
+
: 'font-medium text-gray-600'
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
{t('chatSidebarViewTime')}
|
|
26
|
+
</button>
|
|
27
|
+
<span className="text-gray-300">/</span>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
aria-pressed={isProjectFirstView}
|
|
31
|
+
onClick={() => onSelectMode('project-first')}
|
|
32
|
+
className={cn(
|
|
33
|
+
'transition-colors',
|
|
34
|
+
isProjectFirstView
|
|
35
|
+
? 'font-medium text-gray-600'
|
|
36
|
+
: 'text-gray-400 hover:text-gray-600'
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{t('chatSidebarViewProject')}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
import { Plus } from 'lucide-react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
5
|
+
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
6
|
+
import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
7
|
+
import { t } from '@/lib/i18n';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
export type ChatSidebarProjectGroup = {
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
projectName: string;
|
|
13
|
+
items: NcpSessionListItemView[];
|
|
14
|
+
latestUpdatedAt: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
|
|
18
|
+
|
|
19
|
+
type ChatSidebarProjectGroupsProps = {
|
|
20
|
+
groups: ChatSidebarProjectGroup[];
|
|
21
|
+
defaultSessionType: string;
|
|
22
|
+
sessionTypeOptions: SessionTypeOption[];
|
|
23
|
+
renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
|
|
24
|
+
onCreateSession: (sessionType: string, projectRoot: string) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function resolveProjectGroupDefaultSessionType(
|
|
28
|
+
defaultSessionType: string,
|
|
29
|
+
sessionTypeOptions: SessionTypeOption[]
|
|
30
|
+
): string {
|
|
31
|
+
if (sessionTypeOptions.some((option) => option.value === defaultSessionType)) {
|
|
32
|
+
return defaultSessionType;
|
|
33
|
+
}
|
|
34
|
+
return sessionTypeOptions[0]?.value ?? defaultSessionType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveSessionTypeStatusText(option: {
|
|
38
|
+
ready?: boolean;
|
|
39
|
+
reasonMessage?: string | null;
|
|
40
|
+
}): string {
|
|
41
|
+
if (option.ready === false) {
|
|
42
|
+
return option.reasonMessage?.trim() || t('statusSetup');
|
|
43
|
+
}
|
|
44
|
+
return t('statusReady');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
48
|
+
const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem, onCreateSession } = props;
|
|
49
|
+
const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
|
|
50
|
+
const preferredSessionType = useMemo(
|
|
51
|
+
() => resolveProjectGroupDefaultSessionType(defaultSessionType, sessionTypeOptions),
|
|
52
|
+
[defaultSessionType, sessionTypeOptions]
|
|
53
|
+
);
|
|
54
|
+
const supportsSessionTypeChoice = sessionTypeOptions.length > 1;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="space-y-3">
|
|
58
|
+
{groups.map((group) => {
|
|
59
|
+
const actionLabel = `${t('chatSidebarNewTask')} · ${group.projectName}`;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div key={group.projectRoot}>
|
|
63
|
+
<div className="flex items-center justify-between gap-2 px-2 py-0.5">
|
|
64
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
65
|
+
<div
|
|
66
|
+
className="truncate text-[11px] font-medium uppercase tracking-wider text-gray-500"
|
|
67
|
+
title={group.projectRoot}
|
|
68
|
+
>
|
|
69
|
+
{group.projectName}
|
|
70
|
+
</div>
|
|
71
|
+
<span className="shrink-0 text-[10px] text-gray-400">
|
|
72
|
+
{group.items.length}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
{supportsSessionTypeChoice ? (
|
|
76
|
+
<Popover
|
|
77
|
+
open={openProjectRoot === group.projectRoot}
|
|
78
|
+
onOpenChange={(nextOpen) => {
|
|
79
|
+
setOpenProjectRoot(nextOpen ? group.projectRoot : null);
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<PopoverTrigger asChild>
|
|
83
|
+
<Button
|
|
84
|
+
type="button"
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="icon"
|
|
87
|
+
className="h-7 w-7 shrink-0 rounded-lg text-gray-400 hover:bg-white hover:text-gray-900"
|
|
88
|
+
aria-label={actionLabel}
|
|
89
|
+
title={actionLabel}
|
|
90
|
+
>
|
|
91
|
+
<Plus className="h-3.5 w-3.5" />
|
|
92
|
+
</Button>
|
|
93
|
+
</PopoverTrigger>
|
|
94
|
+
<PopoverContent align="end" className="w-64 p-2">
|
|
95
|
+
<div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wider text-gray-400">
|
|
96
|
+
{t('chatSessionTypeLabel')}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="mt-1 space-y-1">
|
|
99
|
+
{sessionTypeOptions.map((option) => (
|
|
100
|
+
<button
|
|
101
|
+
key={`${group.projectRoot}:${option.value}`}
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={() => {
|
|
104
|
+
onCreateSession(option.value, group.projectRoot);
|
|
105
|
+
setOpenProjectRoot(null);
|
|
106
|
+
}}
|
|
107
|
+
className="w-full rounded-xl px-3 py-2 text-left transition-colors hover:bg-gray-100"
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center justify-between gap-3">
|
|
110
|
+
<div className="text-[13px] font-medium text-gray-900">{option.label}</div>
|
|
111
|
+
<span
|
|
112
|
+
className={cn(
|
|
113
|
+
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold',
|
|
114
|
+
option.ready === false
|
|
115
|
+
? 'bg-amber-100 text-amber-800'
|
|
116
|
+
: 'bg-emerald-100 text-emerald-700'
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{option.ready === false ? t('statusSetup') : t('statusReady')}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="mt-0.5 text-[11px] text-gray-500">
|
|
123
|
+
{resolveSessionTypeStatusText(option)}
|
|
124
|
+
</div>
|
|
125
|
+
</button>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
</PopoverContent>
|
|
129
|
+
</Popover>
|
|
130
|
+
) : (
|
|
131
|
+
<Button
|
|
132
|
+
type="button"
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="icon"
|
|
135
|
+
className="h-7 w-7 shrink-0 rounded-lg text-gray-400 hover:bg-white hover:text-gray-900"
|
|
136
|
+
onClick={() => onCreateSession(preferredSessionType, group.projectRoot)}
|
|
137
|
+
aria-label={actionLabel}
|
|
138
|
+
title={actionLabel}
|
|
139
|
+
>
|
|
140
|
+
<Plus className="h-3.5 w-3.5" />
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
<div className="space-y-0.5 pl-2">
|
|
145
|
+
{group.items.map(renderSessionItem)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
})}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -31,12 +31,12 @@ describe('ChatSessionListManager', () => {
|
|
|
31
31
|
resetStreamState: vi.fn()
|
|
32
32
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
33
33
|
|
|
34
|
-
const manager = new ChatSessionListManager(uiManager, streamActionsManager
|
|
34
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
35
35
|
manager.createSession('codex');
|
|
36
36
|
|
|
37
37
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
38
38
|
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
39
|
-
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).
|
|
39
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
40
40
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
41
41
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
42
42
|
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
@@ -50,11 +50,11 @@ describe('ChatSessionListManager', () => {
|
|
|
50
50
|
resetStreamState: vi.fn()
|
|
51
51
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
52
52
|
|
|
53
|
-
const manager = new ChatSessionListManager(uiManager, streamActionsManager
|
|
53
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
54
54
|
manager.createSession('native', '/tmp/project-alpha');
|
|
55
55
|
|
|
56
56
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
|
|
57
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).
|
|
57
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
|
|
@@ -8,8 +8,7 @@ import { normalizeSessionProjectRootValue } from '@/lib/session-project/session-
|
|
|
8
8
|
export class ChatSessionListManager {
|
|
9
9
|
constructor(
|
|
10
10
|
private uiManager: ChatUiManager,
|
|
11
|
-
private streamActionsManager: ChatStreamActionsManager
|
|
12
|
-
private getDraftSessionId: () => string = () => ''
|
|
11
|
+
private streamActionsManager: ChatStreamActionsManager
|
|
13
12
|
) {}
|
|
14
13
|
|
|
15
14
|
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
@@ -55,12 +54,12 @@ export class ChatSessionListManager {
|
|
|
55
54
|
? sessionType.trim()
|
|
56
55
|
: defaultSessionType;
|
|
57
56
|
const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
|
|
58
|
-
const draftSessionId = normalizedProjectRoot ? this.getDraftSessionId() : null;
|
|
59
57
|
this.streamActionsManager.resetStreamState();
|
|
58
|
+
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: null });
|
|
60
59
|
useChatInputStore.getState().setSnapshot({
|
|
61
60
|
pendingSessionType: nextSessionType,
|
|
62
61
|
pendingProjectRoot: normalizedProjectRoot,
|
|
63
|
-
pendingProjectRootSessionKey:
|
|
62
|
+
pendingProjectRootSessionKey: null
|
|
64
63
|
});
|
|
65
64
|
this.uiManager.goToChatRoot();
|
|
66
65
|
};
|
|
@@ -131,8 +131,13 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
131
131
|
[routeSessionIdParam],
|
|
132
132
|
);
|
|
133
133
|
const sessionKey = selectedSessionKey ?? draftSessionId;
|
|
134
|
+
const hasDraftProjectRootOverride =
|
|
135
|
+
pendingProjectRoot !== null &&
|
|
136
|
+
pendingProjectRootSessionKey === null &&
|
|
137
|
+
selectedSessionKey === null;
|
|
134
138
|
const hasSessionProjectRootOverride =
|
|
135
|
-
|
|
139
|
+
pendingProjectRoot !== null &&
|
|
140
|
+
(pendingProjectRootSessionKey === sessionKey || hasDraftProjectRootOverride);
|
|
136
141
|
const sessionProjectRootOverride = hasSessionProjectRootOverride
|
|
137
142
|
? pendingProjectRoot
|
|
138
143
|
: undefined;
|
|
@@ -210,7 +215,10 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
210
215
|
thinkingLevel: payload.thinkingLevel,
|
|
211
216
|
sessionType: payload.sessionType,
|
|
212
217
|
projectRoot:
|
|
213
|
-
payload.sessionKey === pendingProjectRootSessionKey
|
|
218
|
+
payload.sessionKey === pendingProjectRootSessionKey ||
|
|
219
|
+
(pendingProjectRoot !== null &&
|
|
220
|
+
pendingProjectRootSessionKey === null &&
|
|
221
|
+
selectedSessionKey === null)
|
|
214
222
|
? pendingProjectRoot
|
|
215
223
|
: (selectedSession?.projectRoot ?? null),
|
|
216
224
|
requestedSkills: payload.requestedSkills,
|
|
@@ -265,14 +273,20 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
265
273
|
pendingProjectRoot,
|
|
266
274
|
pendingProjectRootSessionKey,
|
|
267
275
|
presenter,
|
|
276
|
+
selectedSessionKey,
|
|
268
277
|
selectedSession?.projectRoot,
|
|
269
278
|
sessionKey,
|
|
270
279
|
]);
|
|
271
280
|
|
|
272
281
|
useEffect(() => {
|
|
282
|
+
const matchesPendingProjectSession =
|
|
283
|
+
pendingProjectRootSessionKey === null
|
|
284
|
+
? selectedSessionKey !== null
|
|
285
|
+
: pendingProjectRootSessionKey === selectedSession?.key;
|
|
273
286
|
if (
|
|
274
287
|
!selectedSession ||
|
|
275
|
-
|
|
288
|
+
pendingProjectRoot === null ||
|
|
289
|
+
!matchesPendingProjectSession ||
|
|
276
290
|
(selectedSession.projectRoot ?? null) !== pendingProjectRoot
|
|
277
291
|
) {
|
|
278
292
|
return;
|
|
@@ -281,7 +295,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
281
295
|
pendingProjectRoot: null,
|
|
282
296
|
pendingProjectRootSessionKey: null,
|
|
283
297
|
});
|
|
284
|
-
}, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession]);
|
|
298
|
+
}, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession, selectedSessionKey]);
|
|
285
299
|
|
|
286
300
|
useChatSessionSync({
|
|
287
301
|
view,
|
|
@@ -7,11 +7,7 @@ import { NcpChatThreadManager } from '@/components/chat/ncp/ncp-chat-thread.mana
|
|
|
7
7
|
export class NcpChatPresenter {
|
|
8
8
|
chatUiManager = new ChatUiManager();
|
|
9
9
|
chatStreamActionsManager = new ChatStreamActionsManager();
|
|
10
|
-
chatSessionListManager = new ChatSessionListManager(
|
|
11
|
-
this.chatUiManager,
|
|
12
|
-
this.chatStreamActionsManager,
|
|
13
|
-
() => this.getDraftSessionId()
|
|
14
|
-
);
|
|
10
|
+
chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatStreamActionsManager);
|
|
15
11
|
chatInputManager = new NcpChatInputManager(
|
|
16
12
|
this.chatUiManager,
|
|
17
13
|
this.chatStreamActionsManager,
|
|
@@ -40,6 +40,22 @@ describe('ChatSessionProjectBadge', () => {
|
|
|
40
40
|
expect(screen.getByText('/tmp/project-alpha')).toBeTruthy();
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('uses the neutral header tag styling instead of a highlighted accent color', () => {
|
|
44
|
+
render(
|
|
45
|
+
<ChatSessionProjectBadge
|
|
46
|
+
sessionKey="session-1"
|
|
47
|
+
projectName="project-alpha"
|
|
48
|
+
projectRoot="/tmp/project-alpha"
|
|
49
|
+
persistToServer
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const trigger = screen.getByRole('button', { name: 'Set Project Directory' });
|
|
54
|
+
expect(trigger.className).toContain('border-gray-200');
|
|
55
|
+
expect(trigger.className).toContain('text-gray-600');
|
|
56
|
+
expect(trigger.className).not.toContain('emerald');
|
|
57
|
+
});
|
|
58
|
+
|
|
43
59
|
it('clears the current project from the badge popover', async () => {
|
|
44
60
|
const user = userEvent.setup();
|
|
45
61
|
|
|
@@ -46,7 +46,7 @@ export function ChatSessionProjectBadge({
|
|
|
46
46
|
<button
|
|
47
47
|
type="button"
|
|
48
48
|
title={projectRoot ?? undefined}
|
|
49
|
-
className="min-w-0 max-w-[320px] shrink rounded-full border border-
|
|
49
|
+
className="min-w-0 max-w-[320px] shrink rounded-full border border-gray-200 bg-gray-100/90 px-2 py-0.5 text-[11px] font-medium text-gray-600 transition-colors hover:border-gray-300 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60"
|
|
50
50
|
aria-label={t('chatSessionSetProject')}
|
|
51
51
|
disabled={isProjectPending}
|
|
52
52
|
>
|
|
@@ -59,7 +59,7 @@ export function ChatSessionProjectBadge({
|
|
|
59
59
|
</PopoverTrigger>
|
|
60
60
|
<PopoverContent align="start" className="w-72 p-2">
|
|
61
61
|
<div className="px-3 pb-2 pt-1">
|
|
62
|
-
<div className="text-[11px] font-medium uppercase tracking-wider text-
|
|
62
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
|
63
63
|
{projectName}
|
|
64
64
|
</div>
|
|
65
65
|
{projectRoot ? (
|
package/src/lib/i18n.chat.ts
CHANGED
|
@@ -125,10 +125,13 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
125
125
|
chatSidebarScheduledTasks: { zh: '定时任务', en: 'Scheduled Tasks' },
|
|
126
126
|
chatSidebarSkills: { zh: '技能', en: 'Skills' },
|
|
127
127
|
chatSidebarTaskRecords: { zh: '会话记录', en: 'Sessions' },
|
|
128
|
+
chatSidebarViewTime: { zh: '时间', en: 'Time' },
|
|
129
|
+
chatSidebarViewProject: { zh: '项目', en: 'Project' },
|
|
128
130
|
chatSidebarToday: { zh: '今天', en: 'Today' },
|
|
129
131
|
chatSidebarYesterday: { zh: '昨天', en: 'Yesterday' },
|
|
130
132
|
chatSidebarPrevious7Days: { zh: '近 7 天', en: 'Previous 7 Days' },
|
|
131
133
|
chatSidebarOlder: { zh: '更早', en: 'Older' },
|
|
134
|
+
chatSidebarProjectViewEmpty: { zh: '还没有绑定项目的会话', en: 'No project conversations yet' },
|
|
132
135
|
chatWelcomeTitle: { zh: '你好,有什么可以帮你的吗?', en: 'Hello, how can I help you?' },
|
|
133
136
|
chatWelcomeSubtitle: { zh: '开始一个新任务或选择已有对话', en: 'Start a new task or select an existing conversation' },
|
|
134
137
|
chatWelcomeCapability1Title: { zh: '智能对话', en: 'Smart Conversations' },
|