@nextclaw/ui 0.12.4 → 0.12.6
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 +66 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
- package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
- package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
- package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
- package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
- package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
- package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
- package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
- package/dist/assets/client-CSk58DcF.js +7 -0
- package/dist/assets/config-D8KzikVB.js +1 -0
- package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
- package/dist/assets/dist-aTmhMDVh.js +9 -0
- package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
- package/dist/assets/i18n-C3jb83S6.js +1 -0
- package/dist/assets/index-CE4N7ItL.css +1 -0
- package/dist/assets/index-riX7Sg0_.js +6 -0
- package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
- package/dist/assets/loader-circle-BjMg63eu.js +1 -0
- package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
- package/dist/assets/search-B_Qr0f6C.js +1 -0
- package/dist/assets/security-config-BGWYwxNr.js +1 -0
- package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
- package/dist/assets/useMutation-BdZm-9PL.js +1 -0
- package/dist/assets/x-B8Tho_xC.js +1 -0
- package/dist/index.html +20 -18
- package/package.json +6 -6
- package/src/App.tsx +2 -0
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +155 -59
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
- package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
- package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
- package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
- package/src/components/chat/stores/chat-session-list.store.ts +99 -5
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +19 -14
- package/src/components/layout/sidebar.layout.test.tsx +33 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/desktop/desktop-update.types.ts +36 -0
- package/src/desktop/managers/desktop-update.manager.ts +163 -0
- package/src/desktop/stores/desktop-update.store.ts +18 -0
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/src/lib/i18n.ts +3 -9
- package/src/lib/ui-document-title.ts +1 -0
- package/src/transport/local.transport.ts +57 -18
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
- package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
- package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
- package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
- package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
- package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
- package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
- package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
- package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
- package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
- package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
- package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
- package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
- package/dist/assets/config-CMiW0yaK.js +0 -1
- package/dist/assets/dist-BFc_H-lY.js +0 -15
- package/dist/assets/i18n-C_2dKw6w.js +0 -1
- package/dist/assets/index-ChUXhq0G.css +0 -1
- package/dist/assets/index-DAE8Srx-.js +0 -6
- package/dist/assets/label-D8yyejJS.js +0 -1
- package/dist/assets/loader-circle-B0sKKO29.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CYXs3JtZ.js +0 -1
- package/dist/assets/react-8EIEQjMP.js +0 -1
- package/dist/assets/search-DOsLw-P9.js +0 -1
- package/dist/assets/security-config-CM_tQRXQ.js +0 -1
- package/dist/assets/skeleton-GbHLjPC0.js +0 -1
- package/dist/assets/useMutation-DSinpgEq.js +0 -1
- package/dist/assets/x-Bnco_K8b.js +0 -1
- package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
- /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
- /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
|
@@ -13,6 +13,7 @@ import { Check, Pencil, X } from 'lucide-react';
|
|
|
13
13
|
type ChatSidebarSessionItemProps = {
|
|
14
14
|
session: SessionEntryView;
|
|
15
15
|
active: boolean;
|
|
16
|
+
showUnreadDot: boolean;
|
|
16
17
|
runStatus?: SessionRunStatus;
|
|
17
18
|
context: SessionContextView;
|
|
18
19
|
title: string;
|
|
@@ -29,32 +30,180 @@ type ChatSidebarSessionItemProps = {
|
|
|
29
30
|
onCancel: () => void;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
runStatus,
|
|
37
|
-
context,
|
|
38
|
-
title,
|
|
39
|
-
agentId,
|
|
40
|
-
agentLabel,
|
|
41
|
-
agentAvatarUrl,
|
|
42
|
-
isEditing,
|
|
43
|
-
draftLabel,
|
|
44
|
-
isSaving,
|
|
45
|
-
onSelect,
|
|
46
|
-
onStartEditing,
|
|
47
|
-
onDraftLabelChange,
|
|
48
|
-
onSave,
|
|
49
|
-
onCancel
|
|
50
|
-
} = props;
|
|
33
|
+
type ChatSidebarSessionEditingViewProps = Pick<
|
|
34
|
+
ChatSidebarSessionItemProps,
|
|
35
|
+
'session' | 'draftLabel' | 'isSaving' | 'onDraftLabelChange' | 'onSave' | 'onCancel'
|
|
36
|
+
>;
|
|
51
37
|
|
|
38
|
+
function ChatSidebarSessionEditingView({
|
|
39
|
+
session,
|
|
40
|
+
draftLabel,
|
|
41
|
+
isSaving,
|
|
42
|
+
onDraftLabelChange,
|
|
43
|
+
onSave,
|
|
44
|
+
onCancel
|
|
45
|
+
}: ChatSidebarSessionEditingViewProps) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-2">
|
|
48
|
+
<Input
|
|
49
|
+
value={draftLabel}
|
|
50
|
+
onChange={(event) => onDraftLabelChange(event.target.value)}
|
|
51
|
+
onKeyDown={(event) => {
|
|
52
|
+
if (event.key === 'Enter') {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
void onSave();
|
|
55
|
+
} else if (event.key === 'Escape') {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
onCancel();
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
placeholder={t('sessionsLabelPlaceholder')}
|
|
61
|
+
className="h-8 rounded-lg border-gray-300 bg-white text-xs"
|
|
62
|
+
autoFocus
|
|
63
|
+
disabled={isSaving}
|
|
64
|
+
/>
|
|
65
|
+
<div className="flex items-center justify-between gap-2">
|
|
66
|
+
<div className="min-w-0 text-[11px] text-gray-400 truncate">{session.key}</div>
|
|
67
|
+
<div className="flex items-center gap-1">
|
|
68
|
+
<Button
|
|
69
|
+
type="button"
|
|
70
|
+
size="icon"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
|
|
73
|
+
onClick={() => void onSave()}
|
|
74
|
+
disabled={isSaving}
|
|
75
|
+
aria-label={t('save')}
|
|
76
|
+
>
|
|
77
|
+
<Check className="h-3.5 w-3.5" />
|
|
78
|
+
</Button>
|
|
79
|
+
<Button
|
|
80
|
+
type="button"
|
|
81
|
+
size="icon"
|
|
82
|
+
variant="ghost"
|
|
83
|
+
className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
|
|
84
|
+
onClick={onCancel}
|
|
85
|
+
disabled={isSaving}
|
|
86
|
+
aria-label={t('cancel')}
|
|
87
|
+
>
|
|
88
|
+
<X className="h-3.5 w-3.5" />
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type ChatSidebarSessionDisplayViewProps = Omit<
|
|
97
|
+
ChatSidebarSessionItemProps,
|
|
98
|
+
'isEditing' | 'draftLabel' | 'isSaving' | 'onDraftLabelChange' | 'onSave' | 'onCancel'
|
|
99
|
+
>;
|
|
100
|
+
|
|
101
|
+
function ChatSidebarSessionDisplayView({
|
|
102
|
+
session,
|
|
103
|
+
active,
|
|
104
|
+
showUnreadDot,
|
|
105
|
+
runStatus,
|
|
106
|
+
context,
|
|
107
|
+
title,
|
|
108
|
+
agentId,
|
|
109
|
+
agentLabel,
|
|
110
|
+
agentAvatarUrl,
|
|
111
|
+
onSelect,
|
|
112
|
+
onStartEditing
|
|
113
|
+
}: ChatSidebarSessionDisplayViewProps) {
|
|
52
114
|
const iconTone = active ? 'text-gray-700' : 'text-gray-500';
|
|
53
115
|
const normalizedAgentId = agentId?.trim() ?? '';
|
|
54
116
|
const shouldShowAgentAvatar = Boolean(
|
|
55
117
|
normalizedAgentId && normalizedAgentId.toLowerCase() !== 'main',
|
|
56
118
|
);
|
|
57
119
|
|
|
120
|
+
return (
|
|
121
|
+
<div className="group/session relative">
|
|
122
|
+
<button type="button" onClick={onSelect} className="w-full text-left">
|
|
123
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1.5 pr-8">
|
|
124
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
125
|
+
{shouldShowAgentAvatar ? (
|
|
126
|
+
<AgentAvatar
|
|
127
|
+
agentId={normalizedAgentId}
|
|
128
|
+
displayName={agentLabel}
|
|
129
|
+
avatarUrl={agentAvatarUrl}
|
|
130
|
+
className="h-5 w-5 shrink-0"
|
|
131
|
+
/>
|
|
132
|
+
) : null}
|
|
133
|
+
<span className="truncate font-medium">{title}</span>
|
|
134
|
+
{context.label ? (
|
|
135
|
+
<span
|
|
136
|
+
className={cn(
|
|
137
|
+
'shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold leading-none',
|
|
138
|
+
active
|
|
139
|
+
? 'border-gray-300 bg-white/80 text-gray-700'
|
|
140
|
+
: 'border-gray-200 bg-gray-100 text-gray-500'
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{context.label}
|
|
144
|
+
</span>
|
|
145
|
+
) : null}
|
|
146
|
+
{context.icon ? (
|
|
147
|
+
<span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
|
|
148
|
+
<SessionContextIconNode icon={context.icon} className={iconTone} />
|
|
149
|
+
</span>
|
|
150
|
+
) : null}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="inline-flex shrink-0 items-center justify-end gap-1.5">
|
|
153
|
+
{showUnreadDot ? (
|
|
154
|
+
<span
|
|
155
|
+
aria-label={t('chatSessionUnread')}
|
|
156
|
+
className="h-2 w-2 rounded-full bg-primary"
|
|
157
|
+
/>
|
|
158
|
+
) : null}
|
|
159
|
+
<span className="inline-flex h-3.5 w-3.5 items-center justify-center">
|
|
160
|
+
{runStatus ? <SessionRunBadge status={runStatus} /> : null}
|
|
161
|
+
</span>
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div className="mt-0.5 text-[11px] text-gray-400 truncate">
|
|
165
|
+
{agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount} · {formatDateTime(session.updatedAt)}
|
|
166
|
+
</div>
|
|
167
|
+
</button>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={(event) => {
|
|
171
|
+
event.stopPropagation();
|
|
172
|
+
onStartEditing();
|
|
173
|
+
}}
|
|
174
|
+
className={cn(
|
|
175
|
+
'absolute right-0 top-0 inline-flex h-7 w-7 items-center justify-center rounded-lg text-gray-400 transition-all hover:bg-white hover:text-gray-900',
|
|
176
|
+
active
|
|
177
|
+
? 'opacity-100'
|
|
178
|
+
: 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
|
|
179
|
+
)}
|
|
180
|
+
aria-label={t('edit')}
|
|
181
|
+
>
|
|
182
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function ChatSidebarSessionItem({
|
|
189
|
+
session,
|
|
190
|
+
active,
|
|
191
|
+
showUnreadDot,
|
|
192
|
+
runStatus,
|
|
193
|
+
context,
|
|
194
|
+
title,
|
|
195
|
+
agentId,
|
|
196
|
+
agentLabel,
|
|
197
|
+
agentAvatarUrl,
|
|
198
|
+
isEditing,
|
|
199
|
+
draftLabel,
|
|
200
|
+
isSaving,
|
|
201
|
+
onSelect,
|
|
202
|
+
onStartEditing,
|
|
203
|
+
onDraftLabelChange,
|
|
204
|
+
onSave,
|
|
205
|
+
onCancel
|
|
206
|
+
}: ChatSidebarSessionItemProps) {
|
|
58
207
|
return (
|
|
59
208
|
<div
|
|
60
209
|
className={cn(
|
|
@@ -65,109 +214,28 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
65
214
|
)}
|
|
66
215
|
>
|
|
67
216
|
{isEditing ? (
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} else if (event.key === 'Escape') {
|
|
77
|
-
event.preventDefault();
|
|
78
|
-
onCancel();
|
|
79
|
-
}
|
|
80
|
-
}}
|
|
81
|
-
placeholder={t('sessionsLabelPlaceholder')}
|
|
82
|
-
className="h-8 rounded-lg border-gray-300 bg-white text-xs"
|
|
83
|
-
autoFocus
|
|
84
|
-
disabled={isSaving}
|
|
85
|
-
/>
|
|
86
|
-
<div className="flex items-center justify-between gap-2">
|
|
87
|
-
<div className="min-w-0 text-[11px] text-gray-400 truncate">{session.key}</div>
|
|
88
|
-
<div className="flex items-center gap-1">
|
|
89
|
-
<Button
|
|
90
|
-
type="button"
|
|
91
|
-
size="icon"
|
|
92
|
-
variant="ghost"
|
|
93
|
-
className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
|
|
94
|
-
onClick={() => void onSave()}
|
|
95
|
-
disabled={isSaving}
|
|
96
|
-
aria-label={t('save')}
|
|
97
|
-
>
|
|
98
|
-
<Check className="h-3.5 w-3.5" />
|
|
99
|
-
</Button>
|
|
100
|
-
<Button
|
|
101
|
-
type="button"
|
|
102
|
-
size="icon"
|
|
103
|
-
variant="ghost"
|
|
104
|
-
className="h-7 w-7 rounded-lg text-gray-500 hover:bg-white hover:text-gray-900"
|
|
105
|
-
onClick={onCancel}
|
|
106
|
-
disabled={isSaving}
|
|
107
|
-
aria-label={t('cancel')}
|
|
108
|
-
>
|
|
109
|
-
<X className="h-3.5 w-3.5" />
|
|
110
|
-
</Button>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
217
|
+
<ChatSidebarSessionEditingView
|
|
218
|
+
session={session}
|
|
219
|
+
draftLabel={draftLabel}
|
|
220
|
+
isSaving={isSaving}
|
|
221
|
+
onDraftLabelChange={onDraftLabelChange}
|
|
222
|
+
onSave={onSave}
|
|
223
|
+
onCancel={onCancel}
|
|
224
|
+
/>
|
|
114
225
|
) : (
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{context.label ? (
|
|
129
|
-
<span
|
|
130
|
-
className={cn(
|
|
131
|
-
'shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold leading-none',
|
|
132
|
-
active
|
|
133
|
-
? 'border-gray-300 bg-white/80 text-gray-700'
|
|
134
|
-
: 'border-gray-200 bg-gray-100 text-gray-500'
|
|
135
|
-
)}
|
|
136
|
-
>
|
|
137
|
-
{context.label}
|
|
138
|
-
</span>
|
|
139
|
-
) : null}
|
|
140
|
-
{context.icon ? (
|
|
141
|
-
<span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
|
|
142
|
-
<SessionContextIconNode icon={context.icon} className={iconTone} />
|
|
143
|
-
</span>
|
|
144
|
-
) : null}
|
|
145
|
-
</span>
|
|
146
|
-
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
|
147
|
-
{runStatus ? <SessionRunBadge status={runStatus} /> : null}
|
|
148
|
-
</span>
|
|
149
|
-
</div>
|
|
150
|
-
<div className="mt-0.5 text-[11px] text-gray-400 truncate">
|
|
151
|
-
{agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount} · {formatDateTime(session.updatedAt)}
|
|
152
|
-
</div>
|
|
153
|
-
</button>
|
|
154
|
-
<button
|
|
155
|
-
type="button"
|
|
156
|
-
onClick={(event) => {
|
|
157
|
-
event.stopPropagation();
|
|
158
|
-
onStartEditing();
|
|
159
|
-
}}
|
|
160
|
-
className={cn(
|
|
161
|
-
'absolute right-0 top-0 inline-flex h-7 w-7 items-center justify-center rounded-lg text-gray-400 transition-all hover:bg-white hover:text-gray-900',
|
|
162
|
-
active
|
|
163
|
-
? 'opacity-100'
|
|
164
|
-
: 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
|
|
165
|
-
)}
|
|
166
|
-
aria-label={t('edit')}
|
|
167
|
-
>
|
|
168
|
-
<Pencil className="h-3.5 w-3.5" />
|
|
169
|
-
</button>
|
|
170
|
-
</div>
|
|
226
|
+
<ChatSidebarSessionDisplayView
|
|
227
|
+
session={session}
|
|
228
|
+
active={active}
|
|
229
|
+
showUnreadDot={showUnreadDot}
|
|
230
|
+
runStatus={runStatus}
|
|
231
|
+
context={context}
|
|
232
|
+
title={title}
|
|
233
|
+
agentId={agentId}
|
|
234
|
+
agentLabel={agentLabel}
|
|
235
|
+
agentAvatarUrl={agentAvatarUrl}
|
|
236
|
+
onSelect={onSelect}
|
|
237
|
+
onStartEditing={onStartEditing}
|
|
238
|
+
/>
|
|
171
239
|
)}
|
|
172
240
|
</div>
|
|
173
241
|
);
|
|
@@ -4,18 +4,19 @@ import { beforeEach, expect, it, vi } from "vitest";
|
|
|
4
4
|
import { ChatMessageListContainer } from "./chat-message-list.container";
|
|
5
5
|
|
|
6
6
|
const captures = vi.hoisted(() => ({
|
|
7
|
-
renders: [] as Array<{ messages: unknown[] }>,
|
|
7
|
+
renders: [] as Array<{ messages: unknown[]; texts?: Record<string, unknown> }>,
|
|
8
|
+
language: "en",
|
|
8
9
|
}));
|
|
9
10
|
|
|
10
11
|
vi.mock("@nextclaw/agent-chat-ui", () => ({
|
|
11
|
-
ChatMessageList: (props: { messages: unknown[] }) => {
|
|
12
|
+
ChatMessageList: (props: { messages: unknown[]; texts?: Record<string, unknown> }) => {
|
|
12
13
|
captures.renders.push(props);
|
|
13
14
|
return <div data-testid="chat-message-list" />;
|
|
14
15
|
},
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
18
|
vi.mock("@/components/providers/I18nProvider", () => ({
|
|
18
|
-
useI18n: () => ({ language:
|
|
19
|
+
useI18n: () => ({ language: captures.language }),
|
|
19
20
|
}));
|
|
20
21
|
|
|
21
22
|
vi.mock("@/lib/i18n", () => ({
|
|
@@ -25,6 +26,7 @@ vi.mock("@/lib/i18n", () => ({
|
|
|
25
26
|
|
|
26
27
|
beforeEach(() => {
|
|
27
28
|
captures.renders = [];
|
|
29
|
+
captures.language = "en";
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
it("reuses adapted message references when the source message object is unchanged", () => {
|
|
@@ -144,3 +146,19 @@ it("adapts persisted inline token metadata into rich message parts", () => {
|
|
|
144
146
|
],
|
|
145
147
|
});
|
|
146
148
|
});
|
|
149
|
+
|
|
150
|
+
it("passes localized attachment card texts to the shared chat UI", () => {
|
|
151
|
+
captures.language = "zh";
|
|
152
|
+
|
|
153
|
+
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
154
|
+
|
|
155
|
+
expect(captures.renders[captures.renders.length - 1]?.texts).toMatchObject({
|
|
156
|
+
attachmentOpenLabel: "chatAttachmentOpen",
|
|
157
|
+
attachmentAttachedLabel: "chatAttachmentAttached",
|
|
158
|
+
attachmentCategoryLabels: {
|
|
159
|
+
archive: "chatAttachmentCategoryArchive",
|
|
160
|
+
pdf: "chatAttachmentCategoryPdf",
|
|
161
|
+
generic: "chatAttachmentCategoryGeneric",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -65,6 +65,20 @@ function buildChatMessageTexts(language: string) {
|
|
|
65
65
|
copyMessageLabel: t("chatMessageCopy"),
|
|
66
66
|
copiedMessageLabel: t("chatMessageCopied"),
|
|
67
67
|
typingLabel: t("chatTyping"),
|
|
68
|
+
attachmentOpenLabel: t("chatAttachmentOpen"),
|
|
69
|
+
attachmentAttachedLabel: t("chatAttachmentAttached"),
|
|
70
|
+
attachmentCategoryLabels: {
|
|
71
|
+
archive: t("chatAttachmentCategoryArchive"),
|
|
72
|
+
audio: t("chatAttachmentCategoryAudio"),
|
|
73
|
+
code: t("chatAttachmentCategoryCode"),
|
|
74
|
+
data: t("chatAttachmentCategoryData"),
|
|
75
|
+
document: t("chatAttachmentCategoryDocument"),
|
|
76
|
+
generic: t("chatAttachmentCategoryGeneric"),
|
|
77
|
+
image: t("chatAttachmentCategoryImage"),
|
|
78
|
+
pdf: t("chatAttachmentCategoryPdf"),
|
|
79
|
+
sheet: t("chatAttachmentCategorySheet"),
|
|
80
|
+
video: t("chatAttachmentCategoryVideo"),
|
|
81
|
+
},
|
|
68
82
|
};
|
|
69
83
|
}
|
|
70
84
|
|
|
@@ -71,7 +71,7 @@ describe('useChatSessionProject', () => {
|
|
|
71
71
|
expect(toast.success).toHaveBeenCalledTimes(1);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
it('persists to the server
|
|
74
|
+
it('persists to the server without reusing the draft override state for an existing session', async () => {
|
|
75
75
|
const { result } = renderHook(() => useChatSessionProject());
|
|
76
76
|
|
|
77
77
|
await act(async () => {
|
|
@@ -88,12 +88,12 @@ describe('useChatSessionProject', () => {
|
|
|
88
88
|
successMessage: 'Project directory updated',
|
|
89
89
|
});
|
|
90
90
|
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
91
|
-
pendingProjectRoot:
|
|
92
|
-
pendingProjectRootSessionKey:
|
|
91
|
+
pendingProjectRoot: null,
|
|
92
|
+
pendingProjectRootSessionKey: null,
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it('persists clearing to the server
|
|
96
|
+
it('persists clearing to the server without keeping a session-scoped local override', async () => {
|
|
97
97
|
const { result } = renderHook(() => useChatSessionProject());
|
|
98
98
|
|
|
99
99
|
await act(async () => {
|
|
@@ -111,7 +111,7 @@ describe('useChatSessionProject', () => {
|
|
|
111
111
|
});
|
|
112
112
|
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
113
113
|
pendingProjectRoot: null,
|
|
114
|
-
pendingProjectRootSessionKey:
|
|
114
|
+
pendingProjectRootSessionKey: null,
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
117
|
});
|
|
@@ -31,10 +31,5 @@ export function useChatSessionProject() {
|
|
|
31
31
|
patch: { projectRoot: params.projectRoot },
|
|
32
32
|
successMessage,
|
|
33
33
|
});
|
|
34
|
-
|
|
35
|
-
useChatInputStore.getState().setSnapshot({
|
|
36
|
-
pendingProjectRoot: params.projectRoot,
|
|
37
|
-
pendingProjectRootSessionKey: params.sessionKey,
|
|
38
|
-
});
|
|
39
34
|
};
|
|
40
35
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { useChatSessionUpdate } from '@/components/chat/hooks/use-chat-session-update';
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
updateNcpSession: vi.fn(),
|
|
10
|
+
upsertNcpSessionSummaryInQueryClient: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('sonner', () => ({
|
|
14
|
+
toast: {
|
|
15
|
+
success: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@/api/ncp-session', () => ({
|
|
21
|
+
updateNcpSession: (...args: unknown[]) => mocks.updateNcpSession(...args),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@/api/ncp-session-query-cache', () => ({
|
|
25
|
+
upsertNcpSessionSummaryInQueryClient: (...args: unknown[]) =>
|
|
26
|
+
mocks.upsertNcpSessionSummaryInQueryClient(...args),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function createWrapper(queryClient: QueryClient) {
|
|
30
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
31
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('useChatSessionUpdate', () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('updates the session summary and invalidates the matching session skills queries', async () => {
|
|
41
|
+
const queryClient = new QueryClient();
|
|
42
|
+
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
43
|
+
const updatedSession = {
|
|
44
|
+
sessionId: 'session-1',
|
|
45
|
+
updatedAt: '2026-04-09T00:00:00.000Z',
|
|
46
|
+
status: 'idle',
|
|
47
|
+
metadata: { project_root: '/tmp/project-alpha' },
|
|
48
|
+
};
|
|
49
|
+
mocks.updateNcpSession.mockResolvedValue(updatedSession);
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useChatSessionUpdate(), {
|
|
52
|
+
wrapper: createWrapper(queryClient),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await act(async () => {
|
|
56
|
+
await result.current({
|
|
57
|
+
sessionKey: 'session-1',
|
|
58
|
+
patch: { projectRoot: '/tmp/project-alpha' },
|
|
59
|
+
successMessage: 'Project directory updated',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(mocks.updateNcpSession).toHaveBeenCalledWith('session-1', {
|
|
64
|
+
projectRoot: '/tmp/project-alpha',
|
|
65
|
+
});
|
|
66
|
+
expect(mocks.upsertNcpSessionSummaryInQueryClient).toHaveBeenCalledWith(
|
|
67
|
+
queryClient,
|
|
68
|
+
updatedSession,
|
|
69
|
+
);
|
|
70
|
+
expect(invalidateQueriesSpy).toHaveBeenCalledWith({
|
|
71
|
+
queryKey: ['ncp-session-skills', 'session-1'],
|
|
72
|
+
});
|
|
73
|
+
expect(toast.success).toHaveBeenCalledWith('Project directory updated');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -15,10 +15,12 @@ export function useChatSessionUpdate() {
|
|
|
15
15
|
const queryClient = useQueryClient();
|
|
16
16
|
|
|
17
17
|
return async (params: UpdateChatSessionParams): Promise<void> => {
|
|
18
|
+
const { sessionKey, patch, successMessage } = params;
|
|
18
19
|
try {
|
|
19
|
-
const updated = await updateNcpSession(
|
|
20
|
+
const updated = await updateNcpSession(sessionKey, patch);
|
|
20
21
|
upsertNcpSessionSummaryInQueryClient(queryClient, updated);
|
|
21
|
-
|
|
22
|
+
await queryClient.invalidateQueries({ queryKey: ['ncp-session-skills', sessionKey] });
|
|
23
|
+
toast.success(successMessage ?? t('configSavedApplied'));
|
|
22
24
|
} catch (error) {
|
|
23
25
|
toast.error(
|
|
24
26
|
t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)),
|
|
@@ -15,9 +15,12 @@ describe('ChatSessionListManager', () => {
|
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
useChatSessionListStore.setState({
|
|
18
|
+
readUpdatedAtBySessionKey: {},
|
|
19
|
+
hasHydratedReadWatermarks: false,
|
|
18
20
|
snapshot: {
|
|
19
21
|
...useChatSessionListStore.getState().snapshot,
|
|
20
22
|
selectedSessionKey: 'session-1',
|
|
23
|
+
draftSessionKey: 'draft-root-1',
|
|
21
24
|
listMode: 'time-first'
|
|
22
25
|
}
|
|
23
26
|
});
|
|
@@ -25,7 +28,7 @@ describe('ChatSessionListManager', () => {
|
|
|
25
28
|
|
|
26
29
|
it('applies the requested session type when creating a session', () => {
|
|
27
30
|
const uiManager = {
|
|
28
|
-
|
|
31
|
+
goToSession: vi.fn()
|
|
29
32
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
30
33
|
const streamActionsManager = {
|
|
31
34
|
resetStreamState: vi.fn()
|
|
@@ -35,8 +38,9 @@ describe('ChatSessionListManager', () => {
|
|
|
35
38
|
manager.createSession('codex');
|
|
36
39
|
|
|
37
40
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
38
|
-
expect(uiManager.
|
|
39
|
-
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).
|
|
41
|
+
expect(uiManager.goToSession).toHaveBeenCalledWith('draft-root-1');
|
|
42
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
43
|
+
expect(useChatSessionListStore.getState().snapshot.draftSessionKey).not.toBe('draft-root-1');
|
|
40
44
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
41
45
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
42
46
|
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
@@ -44,7 +48,7 @@ describe('ChatSessionListManager', () => {
|
|
|
44
48
|
|
|
45
49
|
it('hydrates the draft project root when creating a session inside a project group', () => {
|
|
46
50
|
const uiManager = {
|
|
47
|
-
|
|
51
|
+
goToSession: vi.fn()
|
|
48
52
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
49
53
|
const streamActionsManager = {
|
|
50
54
|
resetStreamState: vi.fn()
|
|
@@ -54,7 +58,45 @@ describe('ChatSessionListManager', () => {
|
|
|
54
58
|
manager.createSession('native', '/tmp/project-alpha');
|
|
55
59
|
|
|
56
60
|
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
|
|
57
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).
|
|
61
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe('draft-root-1');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('promotes the current root draft when send flow needs a concrete session key', () => {
|
|
65
|
+
useChatSessionListStore.setState({
|
|
66
|
+
snapshot: {
|
|
67
|
+
...useChatSessionListStore.getState().snapshot,
|
|
68
|
+
selectedSessionKey: null,
|
|
69
|
+
draftSessionKey: 'draft-root-2'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const uiManager = {
|
|
73
|
+
goToSession: vi.fn()
|
|
74
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
75
|
+
const streamActionsManager = {
|
|
76
|
+
resetStreamState: vi.fn()
|
|
77
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
78
|
+
|
|
79
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
80
|
+
const sessionKey = manager.ensureDraftSession('native');
|
|
81
|
+
|
|
82
|
+
expect(sessionKey).toBe('draft-root-2');
|
|
83
|
+
expect(uiManager.goToSession).toHaveBeenCalledWith('draft-root-2');
|
|
84
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not eagerly replace the old selected session before the route finishes switching', () => {
|
|
88
|
+
const uiManager = {
|
|
89
|
+
goToSession: vi.fn()
|
|
90
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
91
|
+
const streamActionsManager = {
|
|
92
|
+
resetStreamState: vi.fn()
|
|
93
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
94
|
+
|
|
95
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
96
|
+
manager.createSession('native', '/tmp/project-alpha');
|
|
97
|
+
|
|
98
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
99
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe('draft-root-1');
|
|
58
100
|
});
|
|
59
101
|
|
|
60
102
|
it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
|
|
@@ -82,4 +124,36 @@ describe('ChatSessionListManager', () => {
|
|
|
82
124
|
expect(useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
|
|
83
125
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
84
126
|
});
|
|
127
|
+
|
|
128
|
+
it('marks a session as read through the session list owner boundary', () => {
|
|
129
|
+
const manager = new ChatSessionListManager(
|
|
130
|
+
{} as ConstructorParameters<typeof ChatSessionListManager>[0],
|
|
131
|
+
{} as ConstructorParameters<typeof ChatSessionListManager>[1]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
manager.markSessionRead('session-2', '2026-04-10T10:00:00.000Z');
|
|
135
|
+
|
|
136
|
+
expect(useChatSessionListStore.getState().readUpdatedAtBySessionKey['session-2']).toBe(
|
|
137
|
+
'2026-04-10T10:00:00.000Z'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('hydrates the initial unread baseline through the session list owner boundary', () => {
|
|
142
|
+
const manager = new ChatSessionListManager(
|
|
143
|
+
{} as ConstructorParameters<typeof ChatSessionListManager>[0],
|
|
144
|
+
{} as ConstructorParameters<typeof ChatSessionListManager>[1]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
manager.hydrateReadWatermarks([
|
|
148
|
+
{
|
|
149
|
+
sessionKey: 'session-2',
|
|
150
|
+
updatedAt: '2026-04-10T10:00:00.000Z'
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
expect(useChatSessionListStore.getState().readUpdatedAtBySessionKey['session-2']).toBe(
|
|
155
|
+
'2026-04-10T10:00:00.000Z'
|
|
156
|
+
);
|
|
157
|
+
expect(useChatSessionListStore.getState().hasHydratedReadWatermarks).toBe(true);
|
|
158
|
+
});
|
|
85
159
|
});
|