@nextclaw/ui 0.12.3 → 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 +49 -0
- package/dist/assets/{ChannelsList-DZWam3Ob.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-C7-1sXqo.js → DocBrowser-QUZ3nfmH.js} +1 -1
- package/dist/assets/{DocBrowserContext-DN5tjUoS.js → DocBrowserContext-CpiIfhJO.js} +1 -1
- package/dist/assets/{LogoBadge-DDS1sU_U.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-COnjm8_x.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-Cefg1LFJ.js → SecretsConfig-Bew4EF2A.js} +2 -2
- package/dist/assets/{SessionsConfig-BZnmVTIu.js → SessionsConfig-2r2yAGZg.js} +2 -2
- package/dist/assets/{book-open-DvWqOode.js → book-open-CJG8Yz3U.js} +1 -1
- package/dist/assets/{chat-session-display-D4bYa0b8.js → chat-session-display-DkAC5OMC.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-CxfKRD7X.js → chunk-JZWAC4HX-D5b3Iyas.js} +1 -1
- package/dist/assets/{config-BeGwf2Ao.js → config-zvnxSXSP.js} +1 -1
- package/dist/assets/{createLucideIcon-C7MmdIX3.js → createLucideIcon-_FMJqZw2.js} +1 -1
- package/dist/assets/{dist-B6VMuIQN.js → dist-B1fpOuON.js} +1 -1
- package/dist/assets/{dist-RWNFhxvR.js → dist-BCXX7FD-.js} +2 -2
- package/dist/assets/{external-link-U86Acd1t.js → external-link-b7gAJWYY.js} +1 -1
- package/dist/assets/{hash-D-OVfV3Z.js → hash-Bhy4TwfZ.js} +1 -1
- package/dist/assets/i18n-DJg9BPYk.js +1 -0
- 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-U1_qDA3U.js → logos-GMeYU9vc.js} +1 -1
- package/dist/assets/{page-layout-Z1klaUFW.js → page-layout-C8UbWuMt.js} +1 -1
- package/dist/assets/plus-CR7RfK3H.js +1 -0
- package/dist/assets/{popover-xWbqMnIN.js → popover-8HSx9wQj.js} +1 -1
- package/dist/assets/react-BB4jko2M.js +1 -0
- package/dist/assets/{refresh-ccw-JQh1lwq-.js → refresh-ccw-CA4_C7Zg.js} +1 -1
- package/dist/assets/{save-4VRlzkii.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-DF-AUoie.js → select-xp_Ac8ip.js} +1 -1
- package/dist/assets/skeleton-uxz_5h3A.js +1 -0
- package/dist/assets/{status-dot-Bq_8Ojvv.js → status-dot-Cn4Pp7DZ.js} +1 -1
- package/dist/assets/{switch-D7JF_RZ-.js → switch-BTi6UOij.js} +1 -1
- package/dist/assets/{tabs-custom-CLksZ2bO.js → tabs-custom-BiiN8DME.js} +1 -1
- package/dist/assets/{trash-2-VV8jvziy.js → trash-2-BpsF0N-r.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +1 -0
- package/dist/assets/{useConfirmDialog-CuQqiPx7.js → useConfirmDialog-BJIwUZjH.js} +1 -1
- package/dist/assets/{useMutation-DBTWPbTg.js → useMutation-BjBOKHj_.js} +1 -1
- package/dist/assets/x-BfTu-g7D.js +1 -0
- package/dist/index.html +19 -18
- package/package.json +5 -5
- 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/ChatSidebar.test.tsx +168 -28
- package/src/components/chat/ChatSidebar.tsx +103 -28
- 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/chat-sidebar-list-mode-switch.tsx +43 -0
- package/src/components/chat/chat-sidebar-project-groups.tsx +152 -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 +46 -6
- package/src/components/chat/managers/chat-session-list.manager.ts +19 -6
- package/src/components/chat/ncp/NcpChatPage.tsx +33 -38
- 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 +2 -16
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +20 -7
- package/src/components/chat/session-header/chat-session-project-badge.test.tsx +16 -0
- package/src/components/chat/session-header/chat-session-project-badge.tsx +2 -2
- package/src/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.chat.ts +3 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/dist/assets/ChatPage-YBL7iJ1X.js +0 -43
- package/dist/assets/DocBrowser-DQjtSsY3.js +0 -1
- package/dist/assets/MarketplacePage-2tWWgwAb.js +0 -49
- package/dist/assets/MarketplacePage-BorWJftJ.js +0 -1
- package/dist/assets/McpMarketplacePage-N-fB4HID.js +0 -40
- package/dist/assets/ModelConfig-DvsBTUiE.js +0 -1
- package/dist/assets/ProviderScopedModelInput-D9woCARc.js +0 -1
- package/dist/assets/ProvidersList-D-qPGgC4.js +0 -1
- package/dist/assets/RuntimeConfig-BHpqcaHm.js +0 -1
- package/dist/assets/SearchConfig-DIT6M65Q.js +0 -1
- package/dist/assets/i18n-hM3v-3YG.js +0 -1
- package/dist/assets/index-CpxuJa9o.css +0 -1
- package/dist/assets/index-DHmCjcxq.js +0 -6
- package/dist/assets/label-CHJ1ATds.js +0 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CrkO1kob.js +0 -1
- package/dist/assets/react-3YE87-lE.js +0 -1
- package/dist/assets/search-EX-Papzl.js +0 -1
- package/dist/assets/security-config-DEgOD4VX.js +0 -1
- package/dist/assets/skeleton-B0mmt1vo.js +0 -1
- package/dist/assets/x-B4sxJkGY.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,15 +134,13 @@ 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 ??
|
|
141
|
+
const sessionKey = routeSessionKey ?? selectedSessionKey ?? draftSessionKey;
|
|
134
142
|
const hasSessionProjectRootOverride =
|
|
143
|
+
pendingProjectRoot !== null &&
|
|
135
144
|
pendingProjectRootSessionKey === sessionKey;
|
|
136
145
|
const sessionProjectRootOverride = hasSessionProjectRootOverride
|
|
137
146
|
? pendingProjectRoot
|
|
@@ -163,25 +172,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
163
172
|
|
|
164
173
|
const agent = useNcpSessionConversation(sessionKey);
|
|
165
174
|
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
presenter.setDraftSessionId(draftSessionId);
|
|
168
|
-
}, [draftSessionId, presenter]);
|
|
169
|
-
|
|
170
|
-
useEffect(() => {
|
|
171
|
-
if (
|
|
172
|
-
shouldRefreshDraftSessionId({
|
|
173
|
-
previousSelectedSessionKey:
|
|
174
|
-
previousSelectedSessionKeyRef.current,
|
|
175
|
-
nextSelectedSessionKey: selectedSessionKey,
|
|
176
|
-
})
|
|
177
|
-
) {
|
|
178
|
-
const nextDraftSessionId = createNcpSessionId();
|
|
179
|
-
setDraftSessionId(nextDraftSessionId);
|
|
180
|
-
presenter.setDraftSessionId(nextDraftSessionId);
|
|
181
|
-
}
|
|
182
|
-
previousSelectedSessionKeyRef.current = selectedSessionKey;
|
|
183
|
-
}, [presenter, selectedSessionKey]);
|
|
184
|
-
|
|
185
175
|
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|
|
186
176
|
? pendingProjectRoot
|
|
187
177
|
: (selectedSession?.projectRoot ?? null);
|
|
@@ -265,6 +255,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
265
255
|
pendingProjectRoot,
|
|
266
256
|
pendingProjectRootSessionKey,
|
|
267
257
|
presenter,
|
|
258
|
+
selectedSessionKey,
|
|
268
259
|
selectedSession?.projectRoot,
|
|
269
260
|
sessionKey,
|
|
270
261
|
]);
|
|
@@ -272,8 +263,12 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
272
263
|
useEffect(() => {
|
|
273
264
|
if (
|
|
274
265
|
!selectedSession ||
|
|
275
|
-
|
|
276
|
-
|
|
266
|
+
!shouldClearPendingProjectRootOverride({
|
|
267
|
+
pendingProjectRoot,
|
|
268
|
+
pendingProjectRootSessionKey,
|
|
269
|
+
sessionKey: selectedSession.key,
|
|
270
|
+
selectedSessionProjectRoot: selectedSession.projectRoot ?? null,
|
|
271
|
+
})
|
|
277
272
|
) {
|
|
278
273
|
return;
|
|
279
274
|
}
|
|
@@ -281,7 +276,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
281
276
|
pendingProjectRoot: null,
|
|
282
277
|
pendingProjectRootSessionKey: null,
|
|
283
278
|
});
|
|
284
|
-
}, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession]);
|
|
279
|
+
}, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession, selectedSessionKey]);
|
|
285
280
|
|
|
286
281
|
useChatSessionSync({
|
|
287
282
|
view,
|
|
@@ -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
|
|
@@ -7,29 +7,15 @@ import { NcpChatThreadManager } from '@/components/chat/ncp/ncp-chat-thread.mana
|
|
|
7
7
|
export class NcpChatPresenter {
|
|
8
8
|
chatUiManager = new ChatUiManager();
|
|
9
9
|
chatStreamActionsManager = new ChatStreamActionsManager();
|
|
10
|
-
chatSessionListManager = new ChatSessionListManager(
|
|
11
|
-
this.chatUiManager,
|
|
12
|
-
this.chatStreamActionsManager,
|
|
13
|
-
() => this.getDraftSessionId()
|
|
14
|
-
);
|
|
10
|
+
chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatStreamActionsManager);
|
|
15
11
|
chatInputManager = new NcpChatInputManager(
|
|
16
12
|
this.chatUiManager,
|
|
17
13
|
this.chatStreamActionsManager,
|
|
18
|
-
|
|
14
|
+
this.chatSessionListManager
|
|
19
15
|
);
|
|
20
16
|
chatThreadManager = new NcpChatThreadManager(
|
|
21
17
|
this.chatUiManager,
|
|
22
18
|
this.chatSessionListManager,
|
|
23
19
|
this.chatStreamActionsManager
|
|
24
20
|
);
|
|
25
|
-
|
|
26
|
-
private draftSessionId = '';
|
|
27
|
-
|
|
28
|
-
setDraftSessionId = (sessionId: string) => {
|
|
29
|
-
this.draftSessionId = sessionId;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
private getDraftSessionId(): string {
|
|
33
|
-
return this.draftSessionId;
|
|
34
|
-
}
|
|
35
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],
|
|
@@ -40,6 +40,22 @@ describe('ChatSessionProjectBadge', () => {
|
|
|
40
40
|
expect(screen.getByText('/tmp/project-alpha')).toBeTruthy();
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('uses the neutral header tag styling instead of a highlighted accent color', () => {
|
|
44
|
+
render(
|
|
45
|
+
<ChatSessionProjectBadge
|
|
46
|
+
sessionKey="session-1"
|
|
47
|
+
projectName="project-alpha"
|
|
48
|
+
projectRoot="/tmp/project-alpha"
|
|
49
|
+
persistToServer
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const trigger = screen.getByRole('button', { name: 'Set Project Directory' });
|
|
54
|
+
expect(trigger.className).toContain('border-gray-200');
|
|
55
|
+
expect(trigger.className).toContain('text-gray-600');
|
|
56
|
+
expect(trigger.className).not.toContain('emerald');
|
|
57
|
+
});
|
|
58
|
+
|
|
43
59
|
it('clears the current project from the badge popover', async () => {
|
|
44
60
|
const user = userEvent.setup();
|
|
45
61
|
|
|
@@ -46,7 +46,7 @@ export function ChatSessionProjectBadge({
|
|
|
46
46
|
<button
|
|
47
47
|
type="button"
|
|
48
48
|
title={projectRoot ?? undefined}
|
|
49
|
-
className="min-w-0 max-w-[320px] shrink rounded-full border border-
|
|
49
|
+
className="min-w-0 max-w-[320px] shrink rounded-full border border-gray-200 bg-gray-100/90 px-2 py-0.5 text-[11px] font-medium text-gray-600 transition-colors hover:border-gray-300 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60"
|
|
50
50
|
aria-label={t('chatSessionSetProject')}
|
|
51
51
|
disabled={isProjectPending}
|
|
52
52
|
>
|
|
@@ -59,7 +59,7 @@ export function ChatSessionProjectBadge({
|
|
|
59
59
|
</PopoverTrigger>
|
|
60
60
|
<PopoverContent align="start" className="w-72 p-2">
|
|
61
61
|
<div className="px-3 pb-2 pt-1">
|
|
62
|
-
<div className="text-[11px] font-medium uppercase tracking-wider text-
|
|
62
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
|
63
63
|
{projectName}
|
|
64
64
|
</div>
|
|
65
65
|
{projectRoot ? (
|
|
@@ -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
|
+
});
|