@nextclaw/ui 0.12.5 → 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 +25 -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-QUZ3nfmH.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-CpiIfhJO.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-BUK13xK5.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-BG4T_Pcx.js → McpMarketplacePage-CxPFOgxv.js} +2 -2
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/{ProviderScopedModelInput-DGn6sFEN.js → ProviderScopedModelInput-BYNouw-i.js} +1 -1
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-ff15qO-c.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/{RuntimeConfig-TgPandXF.js → RuntimeConfig-ChdfK4Y_.js} +1 -1
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-Bew4EF2A.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-CJG8Yz3U.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-D5b3Iyas.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-_FMJqZw2.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-B1fpOuON.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-b7gAJWYY.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-Bhy4TwfZ.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-GMeYU9vc.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-C8UbWuMt.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-8HSx9wQj.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-CA4_C7Zg.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-BtvMy4lk.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-xp_Ac8ip.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-Cn4Pp7DZ.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-BTi6UOij.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-BiiN8DME.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-BpsF0N-r.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-BJIwUZjH.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 -19
- package/package.json +5 -5
- package/src/App.tsx +2 -0
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/components/chat/ChatConversationPanel.test.tsx +161 -1
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/chat-child-session-panel.tsx +56 -18
- 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/managers/chat-session-list.manager.test.ts +34 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +13 -0
- 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/session-conversation/use-ncp-child-session-tabs-view.ts +18 -5
- package/src/components/chat/stores/chat-session-list.store.ts +96 -5
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/layout/sidebar.layout.test.tsx +1 -0
- 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/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -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-C6-lh55g.js +0 -8
- package/dist/assets/ChatPage-DOW0gPc2.js +0 -45
- package/dist/assets/DocBrowser-CGyeswYP.js +0 -1
- package/dist/assets/MarketplacePage-BDVwhIYE.js +0 -1
- package/dist/assets/MarketplacePage-LnKKL3xK.js +0 -49
- package/dist/assets/ModelConfig-LtWuogIw.js +0 -1
- package/dist/assets/ProvidersList-ma-_MlLo.js +0 -1
- package/dist/assets/SearchConfig-C9iBt7pl.js +0 -1
- package/dist/assets/SessionsConfig-2r2yAGZg.js +0 -2
- package/dist/assets/chat-session-display-DkAC5OMC.js +0 -1
- package/dist/assets/config-zvnxSXSP.js +0 -1
- package/dist/assets/dist-BCXX7FD-.js +0 -15
- package/dist/assets/i18n-DJg9BPYk.js +0 -1
- package/dist/assets/index-BoJbxdvZ.css +0 -1
- package/dist/assets/index-CtlT4E9Y.js +0 -6
- package/dist/assets/infiniteQueryBehavior-CTcVlD9s.js +0 -1
- package/dist/assets/loader-circle-B60I0hEk.js +0 -1
- package/dist/assets/plus-CR7RfK3H.js +0 -1
- package/dist/assets/react-BB4jko2M.js +0 -1
- package/dist/assets/search-C60UA27E.js +0 -1
- package/dist/assets/security-config-BkFDYZ6j.js +0 -1
- package/dist/assets/skeleton-uxz_5h3A.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +0 -1
- package/dist/assets/useMutation-BjBOKHj_.js +0 -1
- package/dist/assets/x-BfTu-g7D.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
|
|
|
@@ -15,6 +15,8 @@ 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',
|
|
@@ -122,4 +124,36 @@ describe('ChatSessionListManager', () => {
|
|
|
122
124
|
expect(useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
|
|
123
125
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
124
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
|
+
});
|
|
125
159
|
});
|
|
@@ -46,6 +46,19 @@ export class ChatSessionListManager {
|
|
|
46
46
|
useChatSessionListStore.getState().setSnapshot({ listMode: value });
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
markSessionRead = (sessionKey: string | null | undefined, updatedAt: string | null | undefined) => {
|
|
50
|
+
if (!sessionKey) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
useChatSessionListStore.getState().markSessionRead(sessionKey, updatedAt);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
hydrateReadWatermarks = (
|
|
57
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
58
|
+
) => {
|
|
59
|
+
useChatSessionListStore.getState().hydrateReadWatermarks(entries);
|
|
60
|
+
};
|
|
61
|
+
|
|
49
62
|
createSession = (sessionType?: string, projectRoot?: string | null): string => {
|
|
50
63
|
const { snapshot } = useChatInputStore.getState();
|
|
51
64
|
const { snapshot: sessionListSnapshot } = useChatSessionListStore.getState();
|
|
@@ -55,7 +55,7 @@ describe('ncp-app-client-fetch', () => {
|
|
|
55
55
|
'content-type': 'application/json'
|
|
56
56
|
},
|
|
57
57
|
body: JSON.stringify({ sessionId: 's1' })
|
|
58
|
-
})).rejects.toThrow('Failed to fetch');
|
|
58
|
+
})).rejects.toThrow('NCP fetch failed for POST http://127.0.0.1:55667/api/ncp/agent/abort: Error: Failed to fetch');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('preserves native SSE request headers', async () => {
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
type FetchLike = typeof fetch;
|
|
2
2
|
|
|
3
|
+
function formatFetchTarget(input: RequestInfo | URL): string {
|
|
4
|
+
if (typeof input === 'string') {
|
|
5
|
+
return input;
|
|
6
|
+
}
|
|
7
|
+
if (input instanceof URL) {
|
|
8
|
+
return input.toString();
|
|
9
|
+
}
|
|
10
|
+
return input.url;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatUnknownFetchError(error: unknown): string {
|
|
14
|
+
if (error instanceof Error) {
|
|
15
|
+
const name = error.name?.trim();
|
|
16
|
+
const message = error.message?.trim();
|
|
17
|
+
if (name && message) {
|
|
18
|
+
return `${name}: ${message}`;
|
|
19
|
+
}
|
|
20
|
+
return message || name || 'Unknown Error';
|
|
21
|
+
}
|
|
22
|
+
return String(error ?? 'Unknown error');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createErrorWithCause(message: string, cause: unknown): Error {
|
|
26
|
+
const error = new Error(message) as Error & { cause?: unknown };
|
|
27
|
+
if (cause !== undefined) {
|
|
28
|
+
error.cause = cause;
|
|
29
|
+
}
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
|
|
3
33
|
export function createNcpAppClientFetch(): FetchLike {
|
|
4
|
-
return (input, init) =>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
34
|
+
return async (input, init) => {
|
|
35
|
+
try {
|
|
36
|
+
return await fetch(input, {
|
|
37
|
+
credentials: 'include',
|
|
38
|
+
...init
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const method = (init?.method || 'GET').toUpperCase();
|
|
42
|
+
const target = formatFetchTarget(input);
|
|
43
|
+
throw createErrorWithCause(
|
|
44
|
+
`NCP fetch failed for ${method} ${target}: ${formatUnknownFetchError(error)}`,
|
|
45
|
+
error
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
9
49
|
}
|
|
@@ -5,12 +5,15 @@ import { adaptNcpSessionSummaries } from "@/components/chat/ncp/ncp-session-adap
|
|
|
5
5
|
import { resolveSessionTypeLabel } from "@/components/chat/useChatSessionTypeState";
|
|
6
6
|
import type { ChatChildSessionTab } from "@/components/chat/stores/chat-thread.store";
|
|
7
7
|
import { useNcpSessions } from "@/hooks/useConfig";
|
|
8
|
+
import type { SessionRunStatus } from "@/lib/session-run-status";
|
|
8
9
|
|
|
9
10
|
export type ResolvedChildSessionTab = {
|
|
10
11
|
sessionKey: string;
|
|
11
12
|
parentSessionKey: string | null;
|
|
12
13
|
title: string;
|
|
13
14
|
agentId: string | null;
|
|
15
|
+
updatedAt: string | null;
|
|
16
|
+
runStatus?: SessionRunStatus;
|
|
14
17
|
sessionTypeLabel: string | null;
|
|
15
18
|
preferredModel: string | null;
|
|
16
19
|
projectName: string | null;
|
|
@@ -34,24 +37,34 @@ export function useNcpChildSessionTabsView(
|
|
|
34
37
|
tabs: readonly ChatChildSessionTab[],
|
|
35
38
|
): ResolvedChildSessionTab[] {
|
|
36
39
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
40
|
+
const summaries = useMemo(
|
|
41
|
+
() => sessionsQuery.data?.sessions ?? [],
|
|
42
|
+
[sessionsQuery.data?.sessions],
|
|
43
|
+
);
|
|
37
44
|
|
|
38
45
|
const sessionByKey = useMemo(() => {
|
|
39
|
-
const sessions = adaptNcpSessionSummaries(
|
|
40
|
-
sessionsQuery.data?.sessions ?? [],
|
|
41
|
-
);
|
|
46
|
+
const sessions = adaptNcpSessionSummaries(summaries);
|
|
42
47
|
return new Map(sessions.map((session) => [session.key, session]));
|
|
43
|
-
}, [
|
|
48
|
+
}, [summaries]);
|
|
49
|
+
|
|
50
|
+
const summaryByKey = useMemo(
|
|
51
|
+
() => new Map(summaries.map((summary) => [summary.sessionId, summary])),
|
|
52
|
+
[summaries],
|
|
53
|
+
);
|
|
44
54
|
|
|
45
55
|
return useMemo(
|
|
46
56
|
() =>
|
|
47
57
|
tabs.map((tab) => {
|
|
48
58
|
const session = sessionByKey.get(tab.sessionKey) ?? null;
|
|
59
|
+
const summary = summaryByKey.get(tab.sessionKey) ?? null;
|
|
49
60
|
const agentId = tab.agentId?.trim() || session?.agentId || null;
|
|
50
61
|
return {
|
|
51
62
|
sessionKey: tab.sessionKey,
|
|
52
63
|
parentSessionKey: tab.parentSessionKey,
|
|
53
64
|
title: resolveChildSessionTitle(tab, session),
|
|
54
65
|
agentId,
|
|
66
|
+
updatedAt: session?.updatedAt ?? null,
|
|
67
|
+
runStatus: summary?.status === "running" ? "running" : undefined,
|
|
55
68
|
sessionTypeLabel: session?.sessionType
|
|
56
69
|
? resolveSessionTypeLabel(session.sessionType)
|
|
57
70
|
: null,
|
|
@@ -60,6 +73,6 @@ export function useNcpChildSessionTabsView(
|
|
|
60
73
|
projectRoot: session?.projectRoot?.trim() || null,
|
|
61
74
|
};
|
|
62
75
|
}),
|
|
63
|
-
[sessionByKey, tabs],
|
|
76
|
+
[sessionByKey, summaryByKey, tabs],
|
|
64
77
|
);
|
|
65
78
|
}
|