@nextclaw/ui 0.12.4 → 0.12.5
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 +41 -0
- package/dist/assets/{ChannelsList-CobWeI2V.js → ChannelsList-C6-lh55g.js} +2 -2
- package/dist/assets/ChatPage-DOW0gPc2.js +45 -0
- package/dist/assets/DocBrowser-CGyeswYP.js +1 -0
- package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-QUZ3nfmH.js} +1 -1
- package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-CpiIfhJO.js} +1 -1
- package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BUK13xK5.js} +1 -1
- package/dist/assets/MarketplacePage-BDVwhIYE.js +1 -0
- package/dist/assets/MarketplacePage-LnKKL3xK.js +49 -0
- package/dist/assets/McpMarketplacePage-BG4T_Pcx.js +40 -0
- package/dist/assets/ModelConfig-LtWuogIw.js +1 -0
- package/dist/assets/ProviderScopedModelInput-DGn6sFEN.js +1 -0
- package/dist/assets/ProvidersList-ma-_MlLo.js +1 -0
- package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-ff15qO-c.js} +1 -1
- package/dist/assets/RuntimeConfig-TgPandXF.js +1 -0
- package/dist/assets/SearchConfig-C9iBt7pl.js +1 -0
- package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-Bew4EF2A.js} +2 -2
- package/dist/assets/{SessionsConfig-vYrvc2Fk.js → SessionsConfig-2r2yAGZg.js} +2 -2
- package/dist/assets/{book-open-C7TAghTk.js → book-open-CJG8Yz3U.js} +1 -1
- package/dist/assets/{chat-session-display-5dVFkJyw.js → chat-session-display-DkAC5OMC.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-D5b3Iyas.js} +1 -1
- package/dist/assets/{config-CMiW0yaK.js → config-zvnxSXSP.js} +1 -1
- package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-_FMJqZw2.js} +1 -1
- package/dist/assets/{dist-DP-JKR4G.js → dist-B1fpOuON.js} +1 -1
- package/dist/assets/{dist-BFc_H-lY.js → dist-BCXX7FD-.js} +2 -2
- package/dist/assets/{external-link-BkJkiWbH.js → external-link-b7gAJWYY.js} +1 -1
- package/dist/assets/{hash-CbP6-6R9.js → hash-Bhy4TwfZ.js} +1 -1
- package/dist/assets/{i18n-C_2dKw6w.js → i18n-DJg9BPYk.js} +1 -1
- package/dist/assets/index-BoJbxdvZ.css +1 -0
- package/dist/assets/index-CtlT4E9Y.js +6 -0
- package/dist/assets/infiniteQueryBehavior-CTcVlD9s.js +1 -0
- package/dist/assets/loader-circle-B60I0hEk.js +1 -0
- package/dist/assets/{logos-N3dbS6-I.js → logos-GMeYU9vc.js} +1 -1
- package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-C8UbWuMt.js} +1 -1
- package/dist/assets/plus-CR7RfK3H.js +1 -0
- package/dist/assets/{popover-BKKWGUaG.js → popover-8HSx9wQj.js} +1 -1
- package/dist/assets/react-BB4jko2M.js +1 -0
- package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-CA4_C7Zg.js} +1 -1
- package/dist/assets/{save-Dh4GQzzX.js → save-BtvMy4lk.js} +1 -1
- package/dist/assets/search-C60UA27E.js +1 -0
- package/dist/assets/security-config-BkFDYZ6j.js +1 -0
- package/dist/assets/{select-BtIi5fnh.js → select-xp_Ac8ip.js} +1 -1
- package/dist/assets/skeleton-uxz_5h3A.js +1 -0
- package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-Cn4Pp7DZ.js} +1 -1
- package/dist/assets/{switch-DPegGIa_.js → switch-BTi6UOij.js} +1 -1
- package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-BiiN8DME.js} +1 -1
- package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-BpsF0N-r.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +1 -0
- package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-BJIwUZjH.js} +1 -1
- package/dist/assets/{useMutation-DSinpgEq.js → useMutation-BjBOKHj_.js} +1 -1
- package/dist/assets/x-BfTu-g7D.js +1 -0
- package/dist/index.html +19 -18
- package/package.json +4 -4
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +183 -141
- 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 +103 -45
- 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/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 +45 -5
- package/src/components/chat/managers/chat-session-list.manager.ts +18 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
- 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 +20 -7
- package/src/components/chat/stores/chat-session-list.store.ts +3 -0
- 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/ProvidersList.tsx +17 -3
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +13 -13
- package/src/components/layout/sidebar.layout.test.tsx +32 -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/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/i18n.remote.ts +15 -0
- 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/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/x-Bnco_K8b.js +0 -1
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
useEffect,
|
|
3
3
|
useMemo,
|
|
4
4
|
useRef,
|
|
5
|
-
useState,
|
|
6
5
|
} from "react";
|
|
7
6
|
import {
|
|
8
7
|
buildNcpRequestEnvelope,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
} from "@/components/chat/chat-session-route";
|
|
23
22
|
import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
|
|
24
23
|
import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
|
|
25
|
-
import { createNcpSessionId } from "@/components/chat/ncp/ncp-session-adapter";
|
|
26
24
|
import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
|
|
27
25
|
import { useNcpChatDerivedState, useNcpChatSnapshotSync } from "@/components/chat/ncp/page/ncp-chat-derived-state";
|
|
28
26
|
import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
|
|
@@ -79,26 +77,39 @@ export function buildNcpSendMetadata(payload: {
|
|
|
79
77
|
return metadata;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
export function
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
export function shouldClearPendingProjectRootOverride(params: {
|
|
81
|
+
pendingProjectRoot: string | null;
|
|
82
|
+
pendingProjectRootSessionKey: string | null;
|
|
83
|
+
sessionKey: string | null | undefined;
|
|
84
|
+
selectedSessionProjectRoot: string | null | undefined;
|
|
85
85
|
}): boolean {
|
|
86
|
+
const {
|
|
87
|
+
pendingProjectRoot,
|
|
88
|
+
pendingProjectRootSessionKey,
|
|
89
|
+
sessionKey,
|
|
90
|
+
selectedSessionProjectRoot,
|
|
91
|
+
} = params;
|
|
86
92
|
return (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
pendingProjectRoot !== null &&
|
|
94
|
+
pendingProjectRootSessionKey !== null &&
|
|
95
|
+
sessionKey === pendingProjectRootSessionKey &&
|
|
96
|
+
(selectedSessionProjectRoot ?? null) === pendingProjectRoot
|
|
90
97
|
);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
export function NcpChatPage({ view }: ChatPageProps) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const presenterRef = useRef<NcpChatPresenter | null>(null);
|
|
102
|
+
if (!presenterRef.current) {
|
|
103
|
+
presenterRef.current = new NcpChatPresenter();
|
|
104
|
+
}
|
|
105
|
+
const presenter = presenterRef.current;
|
|
98
106
|
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
99
107
|
const selectedSessionKey = useChatSessionListStore(
|
|
100
108
|
(state) => state.snapshot.selectedSessionKey,
|
|
101
109
|
);
|
|
110
|
+
const draftSessionKey = useChatSessionListStore(
|
|
111
|
+
(state) => state.snapshot.draftSessionKey,
|
|
112
|
+
);
|
|
102
113
|
const selectedAgentId = useChatSessionListStore(
|
|
103
114
|
(state) => state.snapshot.selectedAgentId,
|
|
104
115
|
);
|
|
@@ -123,21 +134,14 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
123
134
|
}>();
|
|
124
135
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
125
136
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
126
|
-
const previousSelectedSessionKeyRef = useRef<string | null | undefined>(
|
|
127
|
-
undefined,
|
|
128
|
-
);
|
|
129
137
|
const routeSessionKey = useMemo(
|
|
130
138
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
131
139
|
[routeSessionIdParam],
|
|
132
140
|
);
|
|
133
|
-
const sessionKey = selectedSessionKey ??
|
|
134
|
-
const hasDraftProjectRootOverride =
|
|
135
|
-
pendingProjectRoot !== null &&
|
|
136
|
-
pendingProjectRootSessionKey === null &&
|
|
137
|
-
selectedSessionKey === null;
|
|
141
|
+
const sessionKey = routeSessionKey ?? selectedSessionKey ?? draftSessionKey;
|
|
138
142
|
const hasSessionProjectRootOverride =
|
|
139
143
|
pendingProjectRoot !== null &&
|
|
140
|
-
|
|
144
|
+
pendingProjectRootSessionKey === sessionKey;
|
|
141
145
|
const sessionProjectRootOverride = hasSessionProjectRootOverride
|
|
142
146
|
? pendingProjectRoot
|
|
143
147
|
: undefined;
|
|
@@ -168,25 +172,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
168
172
|
|
|
169
173
|
const agent = useNcpSessionConversation(sessionKey);
|
|
170
174
|
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
presenter.setDraftSessionId(draftSessionId);
|
|
173
|
-
}, [draftSessionId, presenter]);
|
|
174
|
-
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
if (
|
|
177
|
-
shouldRefreshDraftSessionId({
|
|
178
|
-
previousSelectedSessionKey:
|
|
179
|
-
previousSelectedSessionKeyRef.current,
|
|
180
|
-
nextSelectedSessionKey: selectedSessionKey,
|
|
181
|
-
})
|
|
182
|
-
) {
|
|
183
|
-
const nextDraftSessionId = createNcpSessionId();
|
|
184
|
-
setDraftSessionId(nextDraftSessionId);
|
|
185
|
-
presenter.setDraftSessionId(nextDraftSessionId);
|
|
186
|
-
}
|
|
187
|
-
previousSelectedSessionKeyRef.current = selectedSessionKey;
|
|
188
|
-
}, [presenter, selectedSessionKey]);
|
|
189
|
-
|
|
190
175
|
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|
|
191
176
|
? pendingProjectRoot
|
|
192
177
|
: (selectedSession?.projectRoot ?? null);
|
|
@@ -215,10 +200,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
215
200
|
thinkingLevel: payload.thinkingLevel,
|
|
216
201
|
sessionType: payload.sessionType,
|
|
217
202
|
projectRoot:
|
|
218
|
-
payload.sessionKey === pendingProjectRootSessionKey
|
|
219
|
-
(pendingProjectRoot !== null &&
|
|
220
|
-
pendingProjectRootSessionKey === null &&
|
|
221
|
-
selectedSessionKey === null)
|
|
203
|
+
payload.sessionKey === pendingProjectRootSessionKey
|
|
222
204
|
? pendingProjectRoot
|
|
223
205
|
: (selectedSession?.projectRoot ?? null),
|
|
224
206
|
requestedSkills: payload.requestedSkills,
|
|
@@ -279,15 +261,14 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
279
261
|
]);
|
|
280
262
|
|
|
281
263
|
useEffect(() => {
|
|
282
|
-
const matchesPendingProjectSession =
|
|
283
|
-
pendingProjectRootSessionKey === null
|
|
284
|
-
? selectedSessionKey !== null
|
|
285
|
-
: pendingProjectRootSessionKey === selectedSession?.key;
|
|
286
264
|
if (
|
|
287
265
|
!selectedSession ||
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
266
|
+
!shouldClearPendingProjectRootOverride({
|
|
267
|
+
pendingProjectRoot,
|
|
268
|
+
pendingProjectRootSessionKey,
|
|
269
|
+
sessionKey: selectedSession.key,
|
|
270
|
+
selectedSessionProjectRoot: selectedSession.projectRoot ?? null,
|
|
271
|
+
})
|
|
291
272
|
) {
|
|
292
273
|
return;
|
|
293
274
|
}
|
|
@@ -18,6 +18,7 @@ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-l
|
|
|
18
18
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
19
19
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
20
20
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
21
|
+
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
21
22
|
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
22
23
|
import { chatRecentModelsManager } from '@/components/chat/chat-recent-models.manager';
|
|
23
24
|
import { chatRecentSkillsManager } from '@/components/chat/chat-recent-skills.manager';
|
|
@@ -40,7 +41,7 @@ export class NcpChatInputManager {
|
|
|
40
41
|
constructor(
|
|
41
42
|
private uiManager: ChatUiManager,
|
|
42
43
|
private streamActionsManager: ChatStreamActionsManager,
|
|
43
|
-
private
|
|
44
|
+
private sessionListManager: ChatSessionListManager
|
|
44
45
|
) {}
|
|
45
46
|
|
|
46
47
|
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
@@ -179,10 +180,7 @@ export class NcpChatInputManager {
|
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
182
|
-
const sessionKey = sessionSnapshot.selectedSessionKey ?? this.
|
|
183
|
-
if (!sessionSnapshot.selectedSessionKey) {
|
|
184
|
-
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
185
|
-
}
|
|
183
|
+
const sessionKey = sessionSnapshot.selectedSessionKey ?? this.sessionListManager.ensureDraftSession(inputSnapshot.selectedSessionType);
|
|
186
184
|
this.setComposerNodes(createInitialChatComposerNodes());
|
|
187
185
|
await this.streamActionsManager.sendMessage({
|
|
188
186
|
message,
|
|
@@ -116,7 +116,6 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
116
116
|
);
|
|
117
117
|
const sessionTypeState = useChatSessionTypeState({
|
|
118
118
|
selectedSession,
|
|
119
|
-
selectedSessionKey: params.sessionKey,
|
|
120
119
|
pendingSessionType: params.pendingSessionType,
|
|
121
120
|
setPendingSessionType: params.setPendingSessionType,
|
|
122
121
|
sessionTypesData: sessionTypesQuery.data
|
|
@@ -11,21 +11,11 @@ export class NcpChatPresenter {
|
|
|
11
11
|
chatInputManager = new NcpChatInputManager(
|
|
12
12
|
this.chatUiManager,
|
|
13
13
|
this.chatStreamActionsManager,
|
|
14
|
-
|
|
14
|
+
this.chatSessionListManager
|
|
15
15
|
);
|
|
16
16
|
chatThreadManager = new NcpChatThreadManager(
|
|
17
17
|
this.chatUiManager,
|
|
18
18
|
this.chatSessionListManager,
|
|
19
19
|
this.chatStreamActionsManager
|
|
20
20
|
);
|
|
21
|
-
|
|
22
|
-
private draftSessionId = '';
|
|
23
|
-
|
|
24
|
-
setDraftSessionId = (sessionId: string) => {
|
|
25
|
-
this.draftSessionId = sessionId;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
private getDraftSessionId(): string {
|
|
29
|
-
return this.draftSessionId;
|
|
30
|
-
}
|
|
31
21
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { useMemo } from
|
|
2
|
-
import type { SessionEntryView } from
|
|
3
|
-
import { sessionDisplayName } from
|
|
4
|
-
import { adaptNcpSessionSummaries } from
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { SessionEntryView } from "@/api/types";
|
|
3
|
+
import { sessionDisplayName } from "@/components/chat/chat-session-display";
|
|
4
|
+
import { adaptNcpSessionSummaries } from "@/components/chat/ncp/ncp-session-adapter";
|
|
5
|
+
import { resolveSessionTypeLabel } from "@/components/chat/useChatSessionTypeState";
|
|
6
|
+
import type { ChatChildSessionTab } from "@/components/chat/stores/chat-thread.store";
|
|
7
|
+
import { useNcpSessions } from "@/hooks/useConfig";
|
|
7
8
|
|
|
8
9
|
export type ResolvedChildSessionTab = {
|
|
9
10
|
sessionKey: string;
|
|
10
11
|
parentSessionKey: string | null;
|
|
11
12
|
title: string;
|
|
12
13
|
agentId: string | null;
|
|
14
|
+
sessionTypeLabel: string | null;
|
|
15
|
+
preferredModel: string | null;
|
|
16
|
+
projectName: string | null;
|
|
17
|
+
projectRoot: string | null;
|
|
13
18
|
};
|
|
14
19
|
|
|
15
20
|
function resolveChildSessionTitle(
|
|
@@ -31,7 +36,9 @@ export function useNcpChildSessionTabsView(
|
|
|
31
36
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
32
37
|
|
|
33
38
|
const sessionByKey = useMemo(() => {
|
|
34
|
-
const sessions = adaptNcpSessionSummaries(
|
|
39
|
+
const sessions = adaptNcpSessionSummaries(
|
|
40
|
+
sessionsQuery.data?.sessions ?? [],
|
|
41
|
+
);
|
|
35
42
|
return new Map(sessions.map((session) => [session.key, session]));
|
|
36
43
|
}, [sessionsQuery.data?.sessions]);
|
|
37
44
|
|
|
@@ -45,6 +52,12 @@ export function useNcpChildSessionTabsView(
|
|
|
45
52
|
parentSessionKey: tab.parentSessionKey,
|
|
46
53
|
title: resolveChildSessionTitle(tab, session),
|
|
47
54
|
agentId,
|
|
55
|
+
sessionTypeLabel: session?.sessionType
|
|
56
|
+
? resolveSessionTypeLabel(session.sessionType)
|
|
57
|
+
: null,
|
|
58
|
+
preferredModel: session?.preferredModel?.trim() || null,
|
|
59
|
+
projectName: session?.projectName?.trim() || null,
|
|
60
|
+
projectRoot: session?.projectRoot?.trim() || null,
|
|
48
61
|
};
|
|
49
62
|
}),
|
|
50
63
|
[sessionByKey, tabs],
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
+
import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
2
3
|
|
|
3
4
|
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
4
5
|
|
|
5
6
|
export type ChatSessionListSnapshot = {
|
|
6
7
|
selectedSessionKey: string | null;
|
|
8
|
+
draftSessionKey: string;
|
|
7
9
|
selectedAgentId: string;
|
|
8
10
|
query: string;
|
|
9
11
|
listMode: ChatSessionListMode;
|
|
@@ -16,6 +18,7 @@ type ChatSessionListStore = {
|
|
|
16
18
|
|
|
17
19
|
const initialSnapshot: ChatSessionListSnapshot = {
|
|
18
20
|
selectedSessionKey: null,
|
|
21
|
+
draftSessionKey: createNcpSessionId(),
|
|
19
22
|
selectedAgentId: 'main',
|
|
20
23
|
query: '',
|
|
21
24
|
listMode: 'time-first'
|
|
@@ -17,7 +17,6 @@ describe('useChatSessionTypeState', () => {
|
|
|
17
17
|
const { result } = renderHook(() =>
|
|
18
18
|
useChatSessionTypeState({
|
|
19
19
|
selectedSession: null,
|
|
20
|
-
selectedSessionKey: null,
|
|
21
20
|
pendingSessionType: 'codex-sdk',
|
|
22
21
|
setPendingSessionType,
|
|
23
22
|
sessionTypesData: {
|
|
@@ -40,7 +39,6 @@ describe('useChatSessionTypeState', () => {
|
|
|
40
39
|
renderHook(() =>
|
|
41
40
|
useChatSessionTypeState({
|
|
42
41
|
selectedSession: null,
|
|
43
|
-
selectedSessionKey: null,
|
|
44
42
|
pendingSessionType: '',
|
|
45
43
|
setPendingSessionType,
|
|
46
44
|
sessionTypesData: {
|
|
@@ -62,7 +60,6 @@ describe('useChatSessionTypeState', () => {
|
|
|
62
60
|
const { result } = renderHook(() =>
|
|
63
61
|
useChatSessionTypeState({
|
|
64
62
|
selectedSession: null,
|
|
65
|
-
selectedSessionKey: null,
|
|
66
63
|
pendingSessionType: 'claude',
|
|
67
64
|
setPendingSessionType,
|
|
68
65
|
sessionTypesData: {
|
|
@@ -22,7 +22,6 @@ export type ChatSessionTypeOption = {
|
|
|
22
22
|
|
|
23
23
|
type UseChatSessionTypeStateParams = {
|
|
24
24
|
selectedSession: SessionEntryView | null;
|
|
25
|
-
selectedSessionKey: string | null;
|
|
26
25
|
pendingSessionType: string;
|
|
27
26
|
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
28
27
|
sessionTypesData?: {
|
|
@@ -114,7 +113,6 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
114
113
|
} {
|
|
115
114
|
const {
|
|
116
115
|
selectedSession,
|
|
117
|
-
selectedSessionKey,
|
|
118
116
|
pendingSessionType,
|
|
119
117
|
setPendingSessionType,
|
|
120
118
|
sessionTypesData
|
|
@@ -164,7 +162,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
164
162
|
);
|
|
165
163
|
|
|
166
164
|
useEffect(() => {
|
|
167
|
-
if (
|
|
165
|
+
if (selectedSession) {
|
|
168
166
|
return;
|
|
169
167
|
}
|
|
170
168
|
const rawPending = typeof pendingSessionType === 'string' ? pendingSessionType.trim() : '';
|
|
@@ -181,9 +179,9 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
181
179
|
return;
|
|
182
180
|
}
|
|
183
181
|
setPendingSessionType(defaultSessionType);
|
|
184
|
-
}, [defaultSessionType, pendingSessionType,
|
|
182
|
+
}, [defaultSessionType, pendingSessionType, selectedSession, setPendingSessionType]);
|
|
185
183
|
|
|
186
|
-
const canEditSessionType = !
|
|
184
|
+
const canEditSessionType = !selectedSession || Boolean(selectedSession.sessionTypeMutable);
|
|
187
185
|
const availableSessionTypeSet = useMemo(
|
|
188
186
|
() => new Set(runtimeSessionTypeOptions.map((option) => option.value)),
|
|
189
187
|
[runtimeSessionTypeOptions]
|
|
@@ -100,6 +100,31 @@ describe('ChannelsList', () => {
|
|
|
100
100
|
mocks.updateChannelMutateAsync.mockReset();
|
|
101
101
|
mocks.startChannelAuthMutateAsync.mockReset();
|
|
102
102
|
mocks.pollChannelAuthMutateAsync.mockReset();
|
|
103
|
+
mocks.configQuery.data = {
|
|
104
|
+
channels: {
|
|
105
|
+
weixin: {
|
|
106
|
+
enabled: false,
|
|
107
|
+
defaultAccountId: '1344b2b24720@im.bot',
|
|
108
|
+
baseUrl: 'https://ilinkai.weixin.qq.com',
|
|
109
|
+
pollTimeoutMs: 35000,
|
|
110
|
+
allowFrom: ['o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat'],
|
|
111
|
+
accounts: {
|
|
112
|
+
'1344b2b24720@im.bot': {
|
|
113
|
+
enabled: true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
mocks.metaQuery.data = {
|
|
120
|
+
channels: [
|
|
121
|
+
{
|
|
122
|
+
name: 'weixin',
|
|
123
|
+
displayName: 'Weixin',
|
|
124
|
+
enabled: false
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
};
|
|
103
128
|
});
|
|
104
129
|
|
|
105
130
|
it('renders weixin qr auth card and starts channel auth', async () => {
|
|
@@ -140,6 +165,49 @@ describe('ChannelsList', () => {
|
|
|
140
165
|
});
|
|
141
166
|
});
|
|
142
167
|
|
|
168
|
+
it('keeps Weixin, Feishu, Discord, and QQ at the front of the channel list', async () => {
|
|
169
|
+
const user = userEvent.setup();
|
|
170
|
+
mocks.configQuery.data = {
|
|
171
|
+
channels: {
|
|
172
|
+
telegram: { enabled: false },
|
|
173
|
+
qq: { enabled: false },
|
|
174
|
+
discord: { enabled: false },
|
|
175
|
+
weixin: { enabled: false },
|
|
176
|
+
feishu: { enabled: false }
|
|
177
|
+
}
|
|
178
|
+
} as unknown as typeof mocks.configQuery.data;
|
|
179
|
+
mocks.metaQuery.data = {
|
|
180
|
+
channels: [
|
|
181
|
+
{ name: 'telegram', displayName: 'Telegram', enabled: false },
|
|
182
|
+
{ name: 'qq', displayName: 'QQ', enabled: false },
|
|
183
|
+
{ name: 'discord', displayName: 'Discord', enabled: false },
|
|
184
|
+
{ name: 'weixin', displayName: 'Weixin', enabled: false },
|
|
185
|
+
{ name: 'feishu', displayName: 'Feishu', enabled: false }
|
|
186
|
+
]
|
|
187
|
+
} as typeof mocks.metaQuery.data;
|
|
188
|
+
|
|
189
|
+
const { container } = render(<ChannelsList />);
|
|
190
|
+
|
|
191
|
+
await user.click(await screen.findByRole('button', { name: /All Channels/i }));
|
|
192
|
+
|
|
193
|
+
const sidebarSection = container.querySelector('section');
|
|
194
|
+
if (!(sidebarSection instanceof HTMLElement)) {
|
|
195
|
+
throw new Error('channel sidebar not found');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const channelButtons = Array.from(sidebarSection.querySelectorAll('button[type="button"]')).filter((button) => (
|
|
199
|
+
['Weixin', 'Feishu', 'Discord', 'QQ', 'Telegram'].some((label) => button.textContent?.includes(label))
|
|
200
|
+
));
|
|
201
|
+
|
|
202
|
+
expect(channelButtons.map((button) => button.textContent)).toEqual([
|
|
203
|
+
expect.stringContaining('Weixin'),
|
|
204
|
+
expect.stringContaining('Feishu'),
|
|
205
|
+
expect.stringContaining('Discord'),
|
|
206
|
+
expect.stringContaining('QQ'),
|
|
207
|
+
expect.stringContaining('Telegram')
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
143
211
|
it('saves weixin advanced settings from the advanced section', async () => {
|
|
144
212
|
const user = userEvent.setup();
|
|
145
213
|
|
|
@@ -24,6 +24,24 @@ const channelDescriptionKeys: Record<string, string> = {
|
|
|
24
24
|
weixin: 'channelDescWeixin'
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const prioritizedChannelNames = ['weixin', 'feishu', 'discord', 'qq'] as const;
|
|
28
|
+
|
|
29
|
+
function sortChannelsForDisplay<T extends { name: string }>(channels: T[]): T[] {
|
|
30
|
+
const priorityByName = new Map<string, number>(prioritizedChannelNames.map((name, index) => [name, index]));
|
|
31
|
+
|
|
32
|
+
return channels
|
|
33
|
+
.map((channel, index) => ({ channel, index }))
|
|
34
|
+
.sort((left, right) => {
|
|
35
|
+
const leftPriority = priorityByName.get(left.channel.name) ?? Number.POSITIVE_INFINITY;
|
|
36
|
+
const rightPriority = priorityByName.get(right.channel.name) ?? Number.POSITIVE_INFINITY;
|
|
37
|
+
if (leftPriority !== rightPriority) {
|
|
38
|
+
return leftPriority - rightPriority;
|
|
39
|
+
}
|
|
40
|
+
return left.index - right.index;
|
|
41
|
+
})
|
|
42
|
+
.map(({ channel }) => channel);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
export function ChannelsList() {
|
|
28
46
|
const { data: config } = useConfig();
|
|
29
47
|
const { data: meta } = useConfigMeta();
|
|
@@ -32,17 +50,17 @@ export function ChannelsList() {
|
|
|
32
50
|
const [selectedChannel, setSelectedChannel] = useState<string | undefined>();
|
|
33
51
|
const [query, setQuery] = useState('');
|
|
34
52
|
const uiHints = schema?.uiHints;
|
|
35
|
-
const channels = meta?.channels;
|
|
53
|
+
const channels = useMemo(() => sortChannelsForDisplay(meta?.channels ?? []), [meta?.channels]);
|
|
36
54
|
const channelConfigs = config?.channels;
|
|
37
55
|
|
|
38
56
|
const tabs = [
|
|
39
|
-
{ id: 'enabled', label: t('channelsTabEnabled'), count:
|
|
40
|
-
{ id: 'all', label: t('channelsTabAll'), count:
|
|
57
|
+
{ id: 'enabled', label: t('channelsTabEnabled'), count: channels.filter((c) => channelConfigs?.[c.name]?.enabled).length },
|
|
58
|
+
{ id: 'all', label: t('channelsTabAll'), count: channels.length }
|
|
41
59
|
];
|
|
42
60
|
|
|
43
61
|
const filteredChannels = useMemo(() => {
|
|
44
62
|
const keyword = query.trim().toLowerCase();
|
|
45
|
-
return
|
|
63
|
+
return channels
|
|
46
64
|
.filter((channel) => {
|
|
47
65
|
const enabled = channelConfigs?.[channel.name]?.enabled || false;
|
|
48
66
|
if (activeTab === 'enabled') {
|
|
@@ -26,6 +26,20 @@ function formatBasePreview(base?: string | null): string | null {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function sortProvidersForDisplay<T extends { name: string }>(providers: T[]): T[] {
|
|
30
|
+
return providers
|
|
31
|
+
.map((provider, index) => ({ provider, index }))
|
|
32
|
+
.sort((left, right) => {
|
|
33
|
+
const leftPriority = left.provider.name === 'nextclaw' ? 1 : 0;
|
|
34
|
+
const rightPriority = right.provider.name === 'nextclaw' ? 1 : 0;
|
|
35
|
+
if (leftPriority !== rightPriority) {
|
|
36
|
+
return leftPriority - rightPriority;
|
|
37
|
+
}
|
|
38
|
+
return left.index - right.index;
|
|
39
|
+
})
|
|
40
|
+
.map(({ provider }) => provider);
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
export function ProvidersList() {
|
|
30
44
|
const { data: config } = useConfig();
|
|
31
45
|
const { data: meta } = useConfigMeta();
|
|
@@ -37,7 +51,7 @@ export function ProvidersList() {
|
|
|
37
51
|
const [query, setQuery] = useState('');
|
|
38
52
|
|
|
39
53
|
const uiHints = schema?.uiHints;
|
|
40
|
-
const providers = meta?.providers ?? [];
|
|
54
|
+
const providers = useMemo(() => sortProvidersForDisplay(meta?.providers ?? []), [meta?.providers]);
|
|
41
55
|
const providersConfig = config?.providers ?? {};
|
|
42
56
|
const configuredCount = providers.filter((provider) => {
|
|
43
57
|
const current = providersConfig[provider.name];
|
|
@@ -50,7 +64,7 @@ export function ProvidersList() {
|
|
|
50
64
|
];
|
|
51
65
|
|
|
52
66
|
const filteredProviders = useMemo(() => {
|
|
53
|
-
const baseProviders =
|
|
67
|
+
const baseProviders = providers;
|
|
54
68
|
const baseConfig = config?.providers ?? {};
|
|
55
69
|
const keyword = query.trim().toLowerCase();
|
|
56
70
|
return baseProviders
|
|
@@ -69,7 +83,7 @@ export function ProvidersList() {
|
|
|
69
83
|
const display = (configDisplayName || provider.displayName || provider.name).toLowerCase();
|
|
70
84
|
return display.includes(keyword) || provider.name.toLowerCase().includes(keyword);
|
|
71
85
|
});
|
|
72
|
-
}, [
|
|
86
|
+
}, [providers, config, activeTab, query]);
|
|
73
87
|
|
|
74
88
|
useEffect(() => {
|
|
75
89
|
if (filteredProviders.length === 0) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ProvidersList } from '@/components/config/ProvidersList';
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
createProviderMutateAsync: vi.fn(),
|
|
7
|
+
configQuery: {
|
|
8
|
+
data: {
|
|
9
|
+
providers: {
|
|
10
|
+
openai: { enabled: true, apiKeySet: true },
|
|
11
|
+
anthropic: { enabled: true, apiKeySet: true },
|
|
12
|
+
nextclaw: { enabled: true, apiKeySet: true }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
isLoading: false
|
|
16
|
+
},
|
|
17
|
+
metaQuery: {
|
|
18
|
+
data: {
|
|
19
|
+
providers: [
|
|
20
|
+
{ name: 'nextclaw', displayName: 'NextClaw Builtin', defaultApiBase: 'https://ai-gateway-api.nextclaw.io/v1' },
|
|
21
|
+
{ name: 'openai', displayName: 'OpenAI', defaultApiBase: 'https://api.openai.com/v1' },
|
|
22
|
+
{ name: 'anthropic', displayName: 'Anthropic', defaultApiBase: 'https://api.anthropic.com/v1' }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
schemaQuery: {
|
|
27
|
+
data: {
|
|
28
|
+
uiHints: {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('@/hooks/useConfig', () => ({
|
|
34
|
+
useConfig: () => mocks.configQuery,
|
|
35
|
+
useConfigMeta: () => mocks.metaQuery,
|
|
36
|
+
useConfigSchema: () => mocks.schemaQuery,
|
|
37
|
+
useCreateProvider: () => ({
|
|
38
|
+
mutateAsync: mocks.createProviderMutateAsync,
|
|
39
|
+
isPending: false
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('@/components/config/ProviderForm', () => ({
|
|
44
|
+
ProviderForm: ({ providerName }: { providerName?: string }) => (
|
|
45
|
+
<div data-testid="provider-form">{providerName ?? 'none'}</div>
|
|
46
|
+
)
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
describe('ProvidersList', () => {
|
|
50
|
+
it('keeps the nextclaw builtin provider at the end of the list', () => {
|
|
51
|
+
const { container } = render(<ProvidersList />);
|
|
52
|
+
|
|
53
|
+
const sidebarSection = container.querySelector('section');
|
|
54
|
+
if (!(sidebarSection instanceof HTMLElement)) {
|
|
55
|
+
throw new Error('provider sidebar not found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const providerButtons = Array.from(sidebarSection.querySelectorAll('button[type="button"]')).filter((button) => (
|
|
59
|
+
['OpenAI', 'Anthropic', 'NextClaw Builtin'].some((label) => button.textContent?.includes(label))
|
|
60
|
+
));
|
|
61
|
+
|
|
62
|
+
expect(providerButtons.map((button) => button.textContent)).toEqual([
|
|
63
|
+
expect.stringContaining('OpenAI'),
|
|
64
|
+
expect.stringContaining('Anthropic'),
|
|
65
|
+
expect.stringContaining('NextClaw Builtin')
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -80,15 +80,25 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
80
80
|
label: t('providers'),
|
|
81
81
|
icon: Sparkles,
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
target: '/channels',
|
|
85
|
+
label: t('channels'),
|
|
86
|
+
icon: MessageSquare,
|
|
87
|
+
},
|
|
83
88
|
{
|
|
84
89
|
target: '/search',
|
|
85
90
|
label: t('searchChannels'),
|
|
86
91
|
icon: Search,
|
|
87
92
|
},
|
|
88
93
|
{
|
|
89
|
-
target: '/
|
|
90
|
-
label: t('
|
|
91
|
-
icon:
|
|
94
|
+
target: '/marketplace/plugins',
|
|
95
|
+
label: t('marketplaceFilterPlugins'),
|
|
96
|
+
icon: Plug,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
target: '/marketplace/mcp',
|
|
100
|
+
label: t('marketplaceFilterMcp'),
|
|
101
|
+
icon: Wrench,
|
|
92
102
|
},
|
|
93
103
|
{
|
|
94
104
|
target: '/runtime',
|
|
@@ -114,16 +124,6 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
114
124
|
target: '/secrets',
|
|
115
125
|
label: t('secrets'),
|
|
116
126
|
icon: KeyRound,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
target: '/marketplace/plugins',
|
|
120
|
-
label: t('marketplaceFilterPlugins'),
|
|
121
|
-
icon: Plug,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
target: '/marketplace/mcp',
|
|
125
|
-
label: t('marketplaceFilterMcp'),
|
|
126
|
-
icon: Wrench,
|
|
127
127
|
}
|
|
128
128
|
];
|
|
129
129
|
const navItems = isSettingsMode ? settingsNavItems : mainNavItems;
|