@nextclaw/ui 0.14.1-beta.0 → 0.14.1
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 +50 -0
- package/dist/assets/{api-C6EDHBDR.js → api-DPcvCLfa.js} +3 -3
- package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-D6jtziu4.js} +2 -2
- package/dist/assets/chat-page-BBnNJoKG.js +105 -0
- package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-BCJhqdK9.js} +1 -1
- package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-CWh5FfK2.js} +1 -1
- package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-DsRhBizi.js} +1 -1
- package/dist/assets/{dist-CmMro3nV.js → dist-DPrgcEC0.js} +1 -1
- package/dist/assets/{dist-BIYV2-Ia.js → dist-DsE4OsSS.js} +1 -1
- package/dist/assets/doc-browser-C8xOF_9F.js +1 -0
- package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DgQedeAn.js} +1 -1
- package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-B46tQGmO.js} +1 -1
- package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-B81FStLm.js} +1 -1
- package/dist/assets/{external-link-D2cBOCqn.js → external-link-C_dmG-WJ.js} +1 -1
- package/dist/assets/index-7o1WdUbU.js +103 -0
- package/dist/assets/index-DQWmEWzk.css +1 -0
- package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +1 -0
- package/dist/assets/mcp-marketplace-page-Dzull_4-.js +40 -0
- package/dist/assets/model-config-page-Cv0O3BIj.js +1 -0
- package/dist/assets/plus-Bnt56oqG.js +1 -0
- package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +1 -0
- package/dist/assets/providers-config-page-CPDz0Lxc.js +1 -0
- package/dist/assets/{react-DYQXXGbH.js → react-sZz5HSCm.js} +1 -1
- package/dist/assets/remote-B0LMs8u7.js +1 -0
- package/dist/assets/rotate-cw-BfyG_KEF.js +1 -0
- package/dist/assets/runtime-config-page-q0fvv8ME.js +1 -0
- package/dist/assets/{save-BNVTgBVn.js → save-MzjoiHri.js} +1 -1
- package/dist/assets/{search-CKCzwLId.js → search-B29dE4GN.js} +1 -1
- package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-Dl5sZzob.js} +1 -1
- package/dist/assets/secrets-config-page-C_l-IFCK.js +3 -0
- package/dist/assets/{select-CIRwTNP4.js → select-BKmh05hd.js} +2 -2
- package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-BS_7Ueom.js} +1 -1
- package/dist/assets/use-config-DZkBWIdL.js +1 -0
- package/dist/assets/x-CW2KO-Hz.js +1 -0
- package/dist/index.html +14 -14
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +70 -7
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +104 -16
- package/src/features/chat/components/layout/chat-sidebar.tsx +21 -2
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
- package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +88 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
- package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
- package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
- package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +3 -2
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +24 -0
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +27 -1
- package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +17 -4
- package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +15 -3
- package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
- package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
- package/src/features/chat/managers/chat-input.manager.ts +24 -2
- package/src/features/chat/stores/chat-input.store.ts +2 -0
- package/src/features/chat/types/chat-input-bar.types.ts +5 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
- package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
- package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
- package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
- package/src/features/settings/components/config/provider-form.tsx +284 -387
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
- package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
- package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
- package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
- package/src/features/settings/pages/secrets-config-page.tsx +2 -1
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
- package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
- package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
- package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
- package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
- package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
- package/src/features/system-status/components/runtime-security-card.tsx +79 -25
- package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
- package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
- package/src/shared/components/common/agent-avatar.tsx +5 -2
- package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
- package/src/shared/components/common/searchable-model-input.tsx +102 -87
- package/src/shared/components/ui/README.md +7 -0
- package/src/shared/components/ui/popover.tsx +19 -3
- package/src/shared/components/ui/select.tsx +14 -4
- package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
- package/src/shared/lib/api/index.ts +3 -0
- package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
- package/src/shared/lib/api/preferences/preference.types.ts +22 -0
- package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
- package/dist/assets/chat-page-BXngLJY6.js +0 -105
- package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
- package/dist/assets/index-DTb_XVIH.js +0 -103
- package/dist/assets/index-ayiPYhf1.css +0 -1
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
- package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
- package/dist/assets/plus-CYE1onU2.js +0 -1
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
- package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
- package/dist/assets/remote-BlJDbXOI.js +0 -1
- package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
- package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
- package/dist/assets/use-config-DgKM9VIU.js +0 -1
- package/dist/assets/x-Cm3JjYut.js +0 -1
- package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
- package/src/features/chat/components/chat-welcome.tsx +0 -102
|
@@ -1,58 +1,24 @@
|
|
|
1
|
-
import { useRef } from "react";
|
|
1
|
+
import { useRef, type ReactNode } from "react";
|
|
2
2
|
import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
|
|
3
3
|
import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
|
|
4
|
-
import { ChatWelcome } from "@/features/chat/components/chat-welcome";
|
|
5
|
-
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
6
|
-
import { resolveAgentRuntimeSessionType } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
7
|
-
import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
|
|
8
|
-
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
9
4
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
10
|
-
import { useAgents } from "@/shared/hooks/use-agents";
|
|
11
5
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
type ChatConversationContentProps = {
|
|
7
|
+
showWelcome: boolean;
|
|
8
|
+
welcomeSlot?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function ChatConversationContent({
|
|
12
|
+
showWelcome,
|
|
13
|
+
welcomeSlot,
|
|
14
|
+
}: ChatConversationContentProps) {
|
|
20
15
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
21
16
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
22
|
-
const agentsQuery = useAgents();
|
|
23
|
-
const draftAgentId = snapshot.agentId ?? selectedAgentId;
|
|
24
|
-
const availableAgents =
|
|
25
|
-
(agentsQuery.data?.agents?.length ?? 0) > 0
|
|
26
|
-
? (agentsQuery.data?.agents ?? [])
|
|
27
|
-
: [{ id: draftAgentId }];
|
|
28
|
-
const showWelcome =
|
|
29
|
-
!snapshot.canDeleteSession &&
|
|
30
|
-
!snapshot.hasSubmittedDraftMessage &&
|
|
31
|
-
snapshot.messages.length === 0 &&
|
|
32
|
-
!snapshot.isSending;
|
|
33
17
|
const hideEmptyHint =
|
|
34
18
|
snapshot.isHistoryLoading &&
|
|
35
19
|
snapshot.messages.length === 0 &&
|
|
36
20
|
!snapshot.isSending &&
|
|
37
21
|
!snapshot.isAwaitingAssistantOutput;
|
|
38
|
-
const resolveDraftAgent = (agentId: string) =>
|
|
39
|
-
availableAgents.find((agent) => agent.id === agentId) ?? null;
|
|
40
|
-
const createDraftSessionForAgent = () => {
|
|
41
|
-
const sessionType = resolveAgentRuntimeSessionType(
|
|
42
|
-
resolveDraftAgent(draftAgentId),
|
|
43
|
-
defaultSessionType,
|
|
44
|
-
);
|
|
45
|
-
presenter.chatSessionListManager.createSession(sessionType);
|
|
46
|
-
};
|
|
47
|
-
const selectDraftAgent = (agentId: string) => {
|
|
48
|
-
presenter.chatSessionListManager.setSelectedAgentId(agentId);
|
|
49
|
-
presenter.chatInputManager.setPendingSessionType(
|
|
50
|
-
resolveAgentRuntimeSessionType(
|
|
51
|
-
resolveDraftAgent(agentId),
|
|
52
|
-
defaultSessionType,
|
|
53
|
-
),
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
22
|
const { onScroll } = useStickyBottomScroll({
|
|
57
23
|
scrollRef: threadRef,
|
|
58
24
|
resetKey: snapshot.sessionKey,
|
|
@@ -72,12 +38,7 @@ export function ChatConversationContent() {
|
|
|
72
38
|
className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
|
|
73
39
|
>
|
|
74
40
|
{showWelcome ? (
|
|
75
|
-
|
|
76
|
-
onCreateSession={createDraftSessionForAgent}
|
|
77
|
-
agents={availableAgents}
|
|
78
|
-
selectedAgentId={draftAgentId}
|
|
79
|
-
onSelectAgent={selectDraftAgent}
|
|
80
|
-
/>
|
|
41
|
+
welcomeSlot ?? null
|
|
81
42
|
) : hideEmptyHint || !hasMessages ? null : (
|
|
82
43
|
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
|
|
83
44
|
<ChatMessageListContainer
|
|
@@ -6,6 +6,9 @@ import { ChatConversationParentBanner } from "@/features/chat/components/convers
|
|
|
6
6
|
import { ChatConversationSkeleton } from "@/features/chat/components/conversation/chat-conversation-skeleton";
|
|
7
7
|
import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
|
|
8
8
|
import { useNcpChatProviderStateResolved } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
|
|
9
|
+
import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
|
|
10
|
+
import { shouldShowChatWelcome } from "@/features/chat/features/welcome/utils/chat-welcome-visibility.utils";
|
|
11
|
+
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
9
12
|
|
|
10
13
|
type ChatConversationLayoutMode = "desktop" | "mobile";
|
|
11
14
|
|
|
@@ -17,6 +20,9 @@ export function ChatConversationPanel({
|
|
|
17
20
|
onBackToList?: () => void;
|
|
18
21
|
}) {
|
|
19
22
|
const isProviderStateResolved = useNcpChatProviderStateResolved();
|
|
23
|
+
const showWelcome = useChatThreadStore((state) =>
|
|
24
|
+
shouldShowChatWelcome(state.snapshot),
|
|
25
|
+
);
|
|
20
26
|
|
|
21
27
|
if (!isProviderStateResolved) {
|
|
22
28
|
return <ChatConversationSkeleton />;
|
|
@@ -31,8 +37,15 @@ export function ChatConversationPanel({
|
|
|
31
37
|
onBackToList={onBackToList}
|
|
32
38
|
/>
|
|
33
39
|
<ChatConversationAlerts />
|
|
34
|
-
<ChatConversationContent
|
|
35
|
-
|
|
40
|
+
<ChatConversationContent
|
|
41
|
+
showWelcome={showWelcome}
|
|
42
|
+
welcomeSlot={
|
|
43
|
+
<ChatConversationWelcome
|
|
44
|
+
inputSlot={<ChatInputBarContainer surface="embedded" />}
|
|
45
|
+
/>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
{showWelcome ? null : <ChatInputBarContainer />}
|
|
36
49
|
</div>
|
|
37
50
|
|
|
38
51
|
<ChatConversationWorkspaceSection layoutMode={layoutMode} />
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
1
2
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
3
|
import { MemoryRouter } from "react-router-dom";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
@@ -5,7 +6,7 @@ import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
|
|
|
5
6
|
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
6
7
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
7
8
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
8
|
-
import type
|
|
9
|
+
import { PREFERENCE_KEYS, type ChatSessionTypeOptionView } from "@/shared/lib/api";
|
|
9
10
|
|
|
10
11
|
const mocks = vi.hoisted(() => ({
|
|
11
12
|
createSession: vi.fn(() => "draft-session-key"),
|
|
@@ -18,6 +19,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
18
19
|
docOpen: vi.fn(),
|
|
19
20
|
setLanguage: vi.fn(),
|
|
20
21
|
setTheme: vi.fn(),
|
|
22
|
+
fetchPreference: vi.fn(),
|
|
23
|
+
updatePreference: vi.fn(),
|
|
21
24
|
updateNcpSession: vi.fn(),
|
|
22
25
|
agents: [] as Array<{
|
|
23
26
|
id: string;
|
|
@@ -28,6 +31,15 @@ const mocks = vi.hoisted(() => ({
|
|
|
28
31
|
isLoading: false,
|
|
29
32
|
}));
|
|
30
33
|
|
|
34
|
+
vi.mock("@/shared/lib/api", async (importOriginal) => {
|
|
35
|
+
const actual = await importOriginal();
|
|
36
|
+
return {
|
|
37
|
+
...(actual as Record<string, unknown>),
|
|
38
|
+
fetchPreference: mocks.fetchPreference,
|
|
39
|
+
updatePreference: mocks.updatePreference,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
31
43
|
function createSessionItem(
|
|
32
44
|
session: NcpSessionListItemView["session"],
|
|
33
45
|
runStatus?: NcpSessionListItemView["runStatus"],
|
|
@@ -53,10 +65,18 @@ function setSessionTypes(
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
function sidebarElement(variant?: "desktop" | "mobile") {
|
|
68
|
+
const queryClient = new QueryClient({
|
|
69
|
+
defaultOptions: {
|
|
70
|
+
queries: { retry: false },
|
|
71
|
+
mutations: { retry: false },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
56
74
|
return (
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
75
|
+
<QueryClientProvider client={queryClient}>
|
|
76
|
+
<MemoryRouter>
|
|
77
|
+
<ChatSidebar variant={variant} />
|
|
78
|
+
</MemoryRouter>
|
|
79
|
+
</QueryClientProvider>
|
|
60
80
|
);
|
|
61
81
|
}
|
|
62
82
|
|
|
@@ -64,6 +84,11 @@ function renderSidebar(variant?: "desktop" | "mobile") {
|
|
|
64
84
|
return render(sidebarElement(variant));
|
|
65
85
|
}
|
|
66
86
|
|
|
87
|
+
function expectCodexSelectedInSessionTypeMenu() {
|
|
88
|
+
fireEvent.click(screen.getByLabelText("Session Type"));
|
|
89
|
+
expect(screen.getByRole("button", { name: /Codex/i }).getAttribute("aria-pressed")).toBe("true");
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
68
93
|
usePresenter: () => ({
|
|
69
94
|
chatUiManager: {
|
|
@@ -177,6 +202,16 @@ function resetSidebarTestState() {
|
|
|
177
202
|
mocks.docOpen.mockReset();
|
|
178
203
|
mocks.setLanguage.mockReset();
|
|
179
204
|
mocks.setTheme.mockReset();
|
|
205
|
+
mocks.fetchPreference.mockReset();
|
|
206
|
+
mocks.fetchPreference.mockResolvedValue({
|
|
207
|
+
key: PREFERENCE_KEYS.chat.newSessionType,
|
|
208
|
+
value: null,
|
|
209
|
+
});
|
|
210
|
+
mocks.updatePreference.mockReset();
|
|
211
|
+
mocks.updatePreference.mockImplementation(async (key: string, value: string) => ({
|
|
212
|
+
key, value,
|
|
213
|
+
updatedAt: "2026-06-17T00:00:00.000Z",
|
|
214
|
+
}));
|
|
180
215
|
mocks.updateNcpSession.mockReset();
|
|
181
216
|
mocks.updateNcpSession.mockResolvedValue({});
|
|
182
217
|
mocks.agents = [];
|
|
@@ -201,16 +236,44 @@ function resetSidebarTestState() {
|
|
|
201
236
|
describe("ChatSidebar create and list basics", () => {
|
|
202
237
|
beforeEach(resetSidebarTestState);
|
|
203
238
|
|
|
204
|
-
it("
|
|
239
|
+
it("switches the desktop new-session type without creating until the left button is clicked", async () => {
|
|
205
240
|
renderSidebar();
|
|
206
241
|
|
|
207
242
|
fireEvent.click(screen.getByLabelText("Session Type"));
|
|
208
|
-
fireEvent.click(screen.
|
|
243
|
+
fireEvent.click(screen.getByRole("button", { name: /Codex/i }));
|
|
244
|
+
|
|
245
|
+
expect(mocks.createSession).not.toHaveBeenCalled();
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(mocks.updatePreference).toHaveBeenCalledWith(
|
|
248
|
+
PREFERENCE_KEYS.chat.newSessionType,
|
|
249
|
+
"codex",
|
|
250
|
+
);
|
|
251
|
+
expect(screen.getByLabelText("Session Type").textContent).toBe("");
|
|
252
|
+
});
|
|
253
|
+
expectCodexSelectedInSessionTypeMenu();
|
|
254
|
+
|
|
255
|
+
fireEvent.click(screen.getByRole("button", { name: "New Task" }));
|
|
209
256
|
|
|
210
257
|
expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("hydrates the desktop new-session type from stored preferences", async () => {
|
|
261
|
+
mocks.fetchPreference.mockResolvedValue({
|
|
262
|
+
key: PREFERENCE_KEYS.chat.newSessionType,
|
|
263
|
+
value: "codex",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
renderSidebar();
|
|
267
|
+
|
|
211
268
|
await waitFor(() => {
|
|
212
|
-
expect(screen.
|
|
269
|
+
expect(screen.getByLabelText("Session Type").textContent).toBe("");
|
|
213
270
|
});
|
|
271
|
+
expectCodexSelectedInSessionTypeMenu();
|
|
272
|
+
fireEvent.click(screen.getByLabelText("Session Type"));
|
|
273
|
+
|
|
274
|
+
fireEvent.click(screen.getByRole("button", { name: "New Task" }));
|
|
275
|
+
|
|
276
|
+
expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
|
|
214
277
|
});
|
|
215
278
|
|
|
216
279
|
it("does not animate the desktop create task button width during runtime option hydration", () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Button } from "@/shared/components/ui/button";
|
|
1
|
+
import { Button, type ButtonProps } from "@/shared/components/ui/button";
|
|
2
2
|
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
3
3
|
import { Input } from "@/shared/components/ui/input";
|
|
4
4
|
import {
|
|
@@ -6,22 +6,71 @@ import {
|
|
|
6
6
|
PopoverContent,
|
|
7
7
|
PopoverTrigger,
|
|
8
8
|
} from "@/shared/components/ui/popover";
|
|
9
|
-
import {
|
|
9
|
+
import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
|
|
10
|
+
import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
|
|
10
11
|
import type { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store";
|
|
11
12
|
import { cn } from "@/shared/lib/utils";
|
|
12
13
|
import { t } from "@/shared/lib/i18n";
|
|
13
|
-
import { ChevronDown, Plus, Search } from "lucide-react";
|
|
14
|
+
import { Bot, ChevronDown, Plus, Search } from "lucide-react";
|
|
14
15
|
|
|
15
16
|
type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
|
|
17
|
+
type NewSessionActionStyleVariant =
|
|
18
|
+
| "neutralSurface"
|
|
19
|
+
| "brandSoft"
|
|
20
|
+
| "brandTextSurface"
|
|
21
|
+
| "brandSolid";
|
|
22
|
+
|
|
23
|
+
const NEW_SESSION_ACTION_STYLE_VARIANT: NewSessionActionStyleVariant =
|
|
24
|
+
"brandSoft";
|
|
25
|
+
|
|
26
|
+
const NEW_SESSION_ACTION_STYLE_CLASSES: Record<
|
|
27
|
+
NewSessionActionStyleVariant,
|
|
28
|
+
{
|
|
29
|
+
leftVariant: ButtonProps["variant"];
|
|
30
|
+
leftClassName: string;
|
|
31
|
+
rightClassName: string;
|
|
32
|
+
}
|
|
33
|
+
> = {
|
|
34
|
+
neutralSurface: {
|
|
35
|
+
leftVariant: "ghost",
|
|
36
|
+
leftClassName:
|
|
37
|
+
"bg-white/90 text-gray-700 shadow-none transition-[background-color,color,box-shadow] hover:bg-white hover:text-gray-950 hover:shadow-sm active:bg-white/95",
|
|
38
|
+
rightClassName:
|
|
39
|
+
"bg-white/90 text-gray-600 shadow-none transition-[background-color,color,box-shadow] hover:bg-white hover:text-gray-900 hover:shadow-sm active:bg-white/95",
|
|
40
|
+
},
|
|
41
|
+
brandSoft: {
|
|
42
|
+
leftVariant: "ghost",
|
|
43
|
+
leftClassName:
|
|
44
|
+
"bg-primary/10 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/15 hover:text-primary-700 hover:shadow-sm active:bg-primary/20",
|
|
45
|
+
rightClassName:
|
|
46
|
+
"bg-primary/10 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/15 hover:text-primary-700 hover:shadow-sm active:bg-primary/20",
|
|
47
|
+
},
|
|
48
|
+
brandTextSurface: {
|
|
49
|
+
leftVariant: "ghost",
|
|
50
|
+
leftClassName:
|
|
51
|
+
"bg-white/90 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/10 hover:text-primary-700 hover:shadow-sm active:bg-primary/15",
|
|
52
|
+
rightClassName:
|
|
53
|
+
"bg-white/90 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/10 hover:text-primary-700 hover:shadow-sm active:bg-primary/15",
|
|
54
|
+
},
|
|
55
|
+
brandSolid: {
|
|
56
|
+
leftVariant: "primary",
|
|
57
|
+
leftClassName: "",
|
|
58
|
+
rightClassName:
|
|
59
|
+
"bg-primary text-primary-foreground shadow-sm hover:bg-primary-600 hover:text-primary-foreground active:bg-primary-700",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
16
62
|
|
|
17
63
|
type ChatSidebarToolbarProps = {
|
|
18
64
|
query: string;
|
|
19
65
|
defaultSessionType: string;
|
|
20
66
|
sessionTypeOptions: SessionTypeOption[];
|
|
21
67
|
nonDefaultSessionTypeOptions: SessionTypeOption[];
|
|
68
|
+
selectedNewSessionType: string;
|
|
69
|
+
selectedNewSessionTypeOption: SessionTypeOption | null;
|
|
22
70
|
isCreateMenuOpen: boolean;
|
|
23
71
|
onCreateMenuOpenChange: (open: boolean) => void;
|
|
24
72
|
onCreateSession: (sessionType: string) => void;
|
|
73
|
+
onSelectNewSessionType: (sessionType: string) => void;
|
|
25
74
|
onQueryChange: (query: string) => void;
|
|
26
75
|
};
|
|
27
76
|
|
|
@@ -39,58 +88,96 @@ function getMobileCreateOptions(params: {
|
|
|
39
88
|
];
|
|
40
89
|
}
|
|
41
90
|
|
|
91
|
+
function SessionTypeTriggerIcon({
|
|
92
|
+
option,
|
|
93
|
+
}: {
|
|
94
|
+
option: SessionTypeOption | null;
|
|
95
|
+
}) {
|
|
96
|
+
if (option?.icon?.src) {
|
|
97
|
+
return (
|
|
98
|
+
<span className="inline-flex h-4 w-4 shrink-0 items-center justify-center">
|
|
99
|
+
<SessionContextIconNode
|
|
100
|
+
icon={{
|
|
101
|
+
kind: "runtime-image",
|
|
102
|
+
src: option.icon.src,
|
|
103
|
+
alt: option.icon.alt ?? null,
|
|
104
|
+
name: option.label,
|
|
105
|
+
}}
|
|
106
|
+
className="h-4 w-4"
|
|
107
|
+
/>
|
|
108
|
+
</span>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return <Bot className="h-4 w-4 shrink-0" />;
|
|
112
|
+
}
|
|
113
|
+
|
|
42
114
|
export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
43
115
|
const {
|
|
44
116
|
query,
|
|
45
|
-
|
|
46
|
-
|
|
117
|
+
selectedNewSessionType,
|
|
118
|
+
selectedNewSessionTypeOption,
|
|
119
|
+
sessionTypeOptions,
|
|
47
120
|
isCreateMenuOpen,
|
|
48
121
|
onCreateMenuOpenChange,
|
|
49
122
|
onCreateSession,
|
|
123
|
+
onSelectNewSessionType,
|
|
50
124
|
onQueryChange,
|
|
51
125
|
} = props;
|
|
126
|
+
const supportsSessionTypeSwitch = sessionTypeOptions.length > 1;
|
|
127
|
+
const actionStyle =
|
|
128
|
+
NEW_SESSION_ACTION_STYLE_CLASSES[NEW_SESSION_ACTION_STYLE_VARIANT];
|
|
52
129
|
|
|
53
130
|
return (
|
|
54
131
|
<>
|
|
55
132
|
<div className="px-4 pb-3">
|
|
56
133
|
<div className="flex items-center gap-2">
|
|
57
134
|
<Button
|
|
58
|
-
variant=
|
|
135
|
+
variant={actionStyle.leftVariant}
|
|
59
136
|
className={cn(
|
|
60
137
|
"min-w-0 rounded-xl",
|
|
61
|
-
|
|
138
|
+
actionStyle.leftClassName,
|
|
139
|
+
supportsSessionTypeSwitch
|
|
62
140
|
? "flex-1 rounded-r-md"
|
|
63
141
|
: "w-full",
|
|
64
142
|
)}
|
|
65
143
|
onClick={() => {
|
|
66
144
|
onCreateMenuOpenChange(false);
|
|
67
|
-
onCreateSession(
|
|
145
|
+
onCreateSession(selectedNewSessionType);
|
|
68
146
|
}}
|
|
69
147
|
>
|
|
70
148
|
<Plus className="mr-2 h-4 w-4" />
|
|
71
149
|
{t("chatSidebarNewTask")}
|
|
72
150
|
</Button>
|
|
73
|
-
{
|
|
151
|
+
{supportsSessionTypeSwitch ? (
|
|
74
152
|
<Popover
|
|
75
153
|
open={isCreateMenuOpen}
|
|
76
154
|
onOpenChange={onCreateMenuOpenChange}
|
|
77
155
|
>
|
|
78
156
|
<PopoverTrigger asChild>
|
|
79
157
|
<IconActionButton
|
|
80
|
-
icon={
|
|
158
|
+
icon={
|
|
159
|
+
<span className="inline-flex items-center gap-0.5">
|
|
160
|
+
<SessionTypeTriggerIcon option={selectedNewSessionTypeOption} />
|
|
161
|
+
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
162
|
+
</span>
|
|
163
|
+
}
|
|
81
164
|
label={t("chatSessionTypeLabel")}
|
|
82
|
-
tooltip={
|
|
83
|
-
className=
|
|
165
|
+
tooltip={t("chatSessionTypeLabel")}
|
|
166
|
+
className={cn(
|
|
167
|
+
"h-9 w-12 shrink-0 rounded-xl rounded-l-md",
|
|
168
|
+
actionStyle.rightClassName,
|
|
169
|
+
)}
|
|
84
170
|
/>
|
|
85
171
|
</PopoverTrigger>
|
|
86
172
|
<PopoverContent
|
|
87
173
|
align="end"
|
|
88
174
|
className="w-56 rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
|
|
89
175
|
>
|
|
90
|
-
<
|
|
91
|
-
options={
|
|
176
|
+
<ChatSessionTypeMenu
|
|
177
|
+
options={sessionTypeOptions}
|
|
178
|
+
selectedSessionType={selectedNewSessionType}
|
|
92
179
|
onSelect={(sessionType) => {
|
|
93
|
-
|
|
180
|
+
onSelectNewSessionType(sessionType);
|
|
94
181
|
onCreateMenuOpenChange(false);
|
|
95
182
|
}}
|
|
96
183
|
/>
|
|
@@ -160,8 +247,9 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
|
|
|
160
247
|
align="end"
|
|
161
248
|
className="w-60 rounded-3xl border border-gray-200/80 bg-white p-2 shadow-[0_24px_70px_-30px_rgba(15,23,42,0.45)]"
|
|
162
249
|
>
|
|
163
|
-
<
|
|
250
|
+
<ChatSessionTypeMenu
|
|
164
251
|
options={createOptions}
|
|
252
|
+
selectedSessionType={defaultSessionType}
|
|
165
253
|
title={t("chatSidebarNewTask")}
|
|
166
254
|
titleClassName="pb-1.5 text-[11px] font-medium normal-case tracking-normal"
|
|
167
255
|
onSelect={(sessionType) => {
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
DEFAULT_SESSION_TYPE,
|
|
42
42
|
normalizeSessionType,
|
|
43
43
|
} from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
44
|
+
import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
|
|
44
45
|
|
|
45
46
|
const navItems = [
|
|
46
47
|
{
|
|
@@ -173,6 +174,10 @@ export function ChatSidebar({
|
|
|
173
174
|
),
|
|
174
175
|
[defaultSessionType, sessionTypeOptions],
|
|
175
176
|
);
|
|
177
|
+
const newSessionTypePreference = useChatNewSessionTypePreference({
|
|
178
|
+
defaultSessionType,
|
|
179
|
+
sessionTypeOptions,
|
|
180
|
+
});
|
|
176
181
|
const isProjectFirstView = listSnapshot.listMode === "project-first";
|
|
177
182
|
const optimisticReadAtBySessionKey = useChatSessionUnreadState(
|
|
178
183
|
items,
|
|
@@ -250,12 +255,19 @@ export function ChatSidebar({
|
|
|
250
255
|
{isMobileVariant ? (
|
|
251
256
|
<ChatSidebarMobileToolbar
|
|
252
257
|
query={listSnapshot.query}
|
|
253
|
-
defaultSessionType={
|
|
258
|
+
defaultSessionType={newSessionTypePreference.selectedSessionType}
|
|
254
259
|
sessionTypeOptions={sessionTypeOptions}
|
|
255
260
|
nonDefaultSessionTypeOptions={nonDefaultSessionTypeOptions}
|
|
261
|
+
selectedNewSessionType={newSessionTypePreference.selectedSessionType}
|
|
262
|
+
selectedNewSessionTypeOption={
|
|
263
|
+
newSessionTypePreference.selectedSessionTypeOption
|
|
264
|
+
}
|
|
256
265
|
isCreateMenuOpen={isCreateMenuOpen}
|
|
257
266
|
onCreateMenuOpenChange={setIsCreateMenuOpen}
|
|
258
267
|
onCreateSession={createSessionAndOpenIfNeeded}
|
|
268
|
+
onSelectNewSessionType={
|
|
269
|
+
newSessionTypePreference.setSelectedSessionType
|
|
270
|
+
}
|
|
259
271
|
onQueryChange={presenter.chatSessionListManager.setQuery}
|
|
260
272
|
/>
|
|
261
273
|
) : (
|
|
@@ -264,9 +276,16 @@ export function ChatSidebar({
|
|
|
264
276
|
defaultSessionType={defaultSessionType}
|
|
265
277
|
sessionTypeOptions={sessionTypeOptions}
|
|
266
278
|
nonDefaultSessionTypeOptions={nonDefaultSessionTypeOptions}
|
|
279
|
+
selectedNewSessionType={newSessionTypePreference.selectedSessionType}
|
|
280
|
+
selectedNewSessionTypeOption={
|
|
281
|
+
newSessionTypePreference.selectedSessionTypeOption
|
|
282
|
+
}
|
|
267
283
|
isCreateMenuOpen={isCreateMenuOpen}
|
|
268
284
|
onCreateMenuOpenChange={setIsCreateMenuOpen}
|
|
269
285
|
onCreateSession={createSessionAndOpenIfNeeded}
|
|
286
|
+
onSelectNewSessionType={
|
|
287
|
+
newSessionTypePreference.setSelectedSessionType
|
|
288
|
+
}
|
|
270
289
|
onQueryChange={presenter.chatSessionListManager.setQuery}
|
|
271
290
|
/>
|
|
272
291
|
)}
|
|
@@ -308,7 +327,7 @@ export function ChatSidebar({
|
|
|
308
327
|
isProjectFirstView={isProjectFirstView}
|
|
309
328
|
groups={groups}
|
|
310
329
|
projectGroups={projectGroups}
|
|
311
|
-
defaultSessionType={
|
|
330
|
+
defaultSessionType={newSessionTypePreference.selectedSessionType}
|
|
312
331
|
sessionTypeOptions={sessionTypeOptions}
|
|
313
332
|
renderSessionItem={renderSessionItem}
|
|
314
333
|
onCreateSession={createSessionAndOpenIfNeeded}
|