@nextclaw/ui 0.14.1-beta.1 → 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 +32 -0
- package/dist/assets/{api-dQ6uQSZG.js → api-DPcvCLfa.js} +3 -3
- package/dist/assets/{channels-list-page-DfpjB8XB.js → channels-list-page-D6jtziu4.js} +2 -2
- package/dist/assets/chat-page-BBnNJoKG.js +105 -0
- package/dist/assets/{config-split-page-CeFfbaqc.js → config-split-page-BCJhqdK9.js} +1 -1
- package/dist/assets/{confirm-dialog-C95Y4QPC.js → confirm-dialog-CWh5FfK2.js} +1 -1
- package/dist/assets/{desktop-update-config-CcK6jiEb.js → desktop-update-config-DsRhBizi.js} +1 -1
- package/dist/assets/{dist-DTGzeil6.js → dist-DPrgcEC0.js} +1 -1
- package/dist/assets/{dist-fGogBmPA.js → dist-DsE4OsSS.js} +1 -1
- package/dist/assets/doc-browser-C8xOF_9F.js +1 -0
- package/dist/assets/{doc-browser-DVHhEb96.js → doc-browser-DgQedeAn.js} +1 -1
- package/dist/assets/{doc-browser-context-BfMXwnrz.js → doc-browser-context-B46tQGmO.js} +1 -1
- package/dist/assets/{ellipsis-D2F4KMiU.js → ellipsis-B81FStLm.js} +1 -1
- package/dist/assets/{external-link-CcgtBK26.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-aAUkZkSX.js → model-config-page-Cv0O3BIj.js} +1 -1
- package/dist/assets/plus-Bnt56oqG.js +1 -0
- package/dist/assets/{provider-scoped-model-input-we4_Z4gP.js → provider-scoped-model-input-Duuh7i3h.js} +1 -1
- package/dist/assets/{providers-config-page-CtMkPSyu.js → providers-config-page-CPDz0Lxc.js} +1 -1
- package/dist/assets/{react-CTqdhFMk.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-TD1UrTbG.js → runtime-config-page-q0fvv8ME.js} +1 -1
- package/dist/assets/{save-C9fKWfVB.js → save-MzjoiHri.js} +1 -1
- package/dist/assets/{search-D5FQ5q0X.js → search-B29dE4GN.js} +1 -1
- package/dist/assets/{search-config-page-AMEJJNIS.js → search-config-page-Dl5sZzob.js} +1 -1
- package/dist/assets/{secrets-config-page-CcgJnvBO.js → secrets-config-page-C_l-IFCK.js} +2 -2
- package/dist/assets/{select-Cu5n0E-3.js → select-BKmh05hd.js} +1 -1
- package/dist/assets/{tag-chip-C5aOtjxu.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/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/hooks/use-chat-model-favorites.ts +2 -1
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +3 -2
- package/src/features/chat/features/session-type/components/__tests__/{chat-sidebar-create-menu.test.tsx → chat-session-type-menu.test.tsx} +3 -3
- 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} +11 -7
- 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/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/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
- package/src/shared/components/common/agent-avatar.tsx +5 -2
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
- package/dist/assets/chat-page-D3QaEqjs.js +0 -105
- package/dist/assets/doc-browser-CuBTcBEZ.js +0 -1
- package/dist/assets/index-ByJCbQqr.css +0 -1
- package/dist/assets/index-DjXqiloY.js +0 -103
- package/dist/assets/mcp-marketplace-page-98-V__3u.js +0 -1
- package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +0 -40
- package/dist/assets/plus-1Rxl5FlV.js +0 -1
- package/dist/assets/remote-fQ7N_r8u.js +0 -1
- package/dist/assets/rotate-cw-CzfIeAJj.js +0 -1
- package/dist/assets/use-config-D8TESeo6.js +0 -1
- package/dist/assets/x-1qETdQ70.js +0 -1
|
@@ -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}
|
|
@@ -2,12 +2,13 @@ import { useCallback, useMemo } from 'react';
|
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
3
|
import {
|
|
4
4
|
fetchPreference,
|
|
5
|
+
PREFERENCE_KEYS,
|
|
5
6
|
updatePreference,
|
|
6
7
|
type PreferenceEntryView,
|
|
7
8
|
type PreferenceJsonValue,
|
|
8
9
|
} from '@/shared/lib/api';
|
|
9
10
|
|
|
10
|
-
const CHAT_MODEL_FAVORITES_PREFERENCE_KEY =
|
|
11
|
+
const CHAT_MODEL_FAVORITES_PREFERENCE_KEY = PREFERENCE_KEYS.chat.modelFavorites;
|
|
11
12
|
const chatModelFavoritesQueryKey = ['preference', CHAT_MODEL_FAVORITES_PREFERENCE_KEY] as const;
|
|
12
13
|
|
|
13
14
|
function normalizeFavoriteModelValues(value: PreferenceJsonValue | null | undefined): string[] {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { Plus } from 'lucide-react';
|
|
3
|
-
import {
|
|
3
|
+
import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
|
|
4
4
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
5
5
|
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
6
6
|
import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
|
|
@@ -77,8 +77,9 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
77
77
|
align="end"
|
|
78
78
|
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)]"
|
|
79
79
|
>
|
|
80
|
-
<
|
|
80
|
+
<ChatSessionTypeMenu
|
|
81
81
|
options={sessionTypeOptions}
|
|
82
|
+
selectedSessionType={preferredSessionType}
|
|
82
83
|
onSelect={(sessionType) => {
|
|
83
84
|
onCreateSession(sessionType, group.projectRoot);
|
|
84
85
|
setOpenProjectRoot(null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
|
|
4
4
|
import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
|
|
5
5
|
|
|
6
6
|
const options = [
|
|
@@ -8,9 +8,9 @@ const options = [
|
|
|
8
8
|
{ value: "codex", label: "Codex", ready: true },
|
|
9
9
|
];
|
|
10
10
|
|
|
11
|
-
describe("
|
|
11
|
+
describe("ChatSessionTypeMenu", () => {
|
|
12
12
|
it("keeps the session type menu height bounded", () => {
|
|
13
|
-
render(<
|
|
13
|
+
render(<ChatSessionTypeMenu options={options} onSelect={vi.fn()} />);
|
|
14
14
|
|
|
15
15
|
const createMenu = screen.getByText("Session Type").parentElement;
|
|
16
16
|
expect(createMenu?.style.maxHeight).toBe(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { ChatSessionTypeOptionItem } from "@/features/chat/features/session-type/components/chat-session-type-option-item";
|
|
4
4
|
|
|
@@ -59,4 +59,30 @@ describe("ChatSessionTypeOptionItem", () => {
|
|
|
59
59
|
expect(screen.getByRole("button", { name: /native/i })).toBeTruthy();
|
|
60
60
|
expect(screen.getByText("Native")).toBeTruthy();
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
it("marks selected options and does not call select when disabled", () => {
|
|
64
|
+
const onSelect = vi.fn();
|
|
65
|
+
|
|
66
|
+
render(
|
|
67
|
+
<ChatSessionTypeOptionItem
|
|
68
|
+
disabled
|
|
69
|
+
selected
|
|
70
|
+
option={{
|
|
71
|
+
value: "claude",
|
|
72
|
+
label: "Claude",
|
|
73
|
+
ready: false,
|
|
74
|
+
reasonMessage: "Configure a provider API key first.",
|
|
75
|
+
}}
|
|
76
|
+
onSelect={onSelect}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const optionButton = screen.getByRole("button", { name: /claude/i });
|
|
81
|
+
expect(optionButton.getAttribute("aria-pressed")).toBe("true");
|
|
82
|
+
expect((optionButton as HTMLButtonElement).disabled).toBe(true);
|
|
83
|
+
|
|
84
|
+
fireEvent.click(optionButton);
|
|
85
|
+
|
|
86
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
62
88
|
});
|
|
@@ -5,31 +5,33 @@ import { t } from "@/shared/lib/i18n";
|
|
|
5
5
|
import { cn } from "@/shared/lib/utils";
|
|
6
6
|
|
|
7
7
|
type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
|
|
8
|
-
const
|
|
8
|
+
const CHAT_SESSION_TYPE_MENU_MAX_HEIGHT =
|
|
9
9
|
createPopoverAvailableHeightLimit("18rem");
|
|
10
|
-
const
|
|
11
|
-
maxHeight:
|
|
10
|
+
const CHAT_SESSION_TYPE_MENU_STYLE = {
|
|
11
|
+
maxHeight: CHAT_SESSION_TYPE_MENU_MAX_HEIGHT,
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
type
|
|
14
|
+
type ChatSessionTypeMenuProps = {
|
|
15
15
|
options: readonly SessionTypeOption[];
|
|
16
16
|
onSelect: (sessionType: string) => void;
|
|
17
|
+
selectedSessionType?: string;
|
|
17
18
|
title?: string;
|
|
18
19
|
className?: string;
|
|
19
20
|
titleClassName?: string;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
export function
|
|
23
|
+
export function ChatSessionTypeMenu({
|
|
23
24
|
className,
|
|
24
25
|
onSelect,
|
|
25
26
|
options,
|
|
27
|
+
selectedSessionType,
|
|
26
28
|
title = t("chatSessionTypeLabel"),
|
|
27
29
|
titleClassName,
|
|
28
|
-
}:
|
|
30
|
+
}: ChatSessionTypeMenuProps) {
|
|
29
31
|
return (
|
|
30
32
|
<div
|
|
31
33
|
className={cn("space-y-1 overflow-y-auto overscroll-contain", className)}
|
|
32
|
-
style={
|
|
34
|
+
style={CHAT_SESSION_TYPE_MENU_STYLE}
|
|
33
35
|
>
|
|
34
36
|
<div
|
|
35
37
|
className={cn(
|
|
@@ -42,7 +44,9 @@ export function ChatSidebarCreateMenu({
|
|
|
42
44
|
{options.map((option) => (
|
|
43
45
|
<ChatSessionTypeOptionItem
|
|
44
46
|
key={option.value}
|
|
47
|
+
disabled={option.ready === false}
|
|
45
48
|
option={option}
|
|
49
|
+
selected={option.value === selectedSessionType}
|
|
46
50
|
onSelect={() => onSelect(option.value)}
|
|
47
51
|
/>
|
|
48
52
|
))}
|
package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx
CHANGED
|
@@ -2,15 +2,18 @@ import type { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store"
|
|
|
2
2
|
import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
|
|
3
3
|
import { StatusDot } from "@/shared/components/status/status-dot";
|
|
4
4
|
import { t } from "@/shared/lib/i18n";
|
|
5
|
-
import {
|
|
5
|
+
import { cn } from "@/shared/lib/utils";
|
|
6
|
+
import { Bot, Check } from "lucide-react";
|
|
6
7
|
|
|
7
8
|
type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
|
|
8
9
|
|
|
9
10
|
export function ChatSessionTypeOptionItem(props: {
|
|
10
11
|
option: SessionTypeOption;
|
|
12
|
+
selected?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
11
14
|
onSelect: () => void;
|
|
12
15
|
}) {
|
|
13
|
-
const { option, onSelect } = props;
|
|
16
|
+
const { disabled = false, option, onSelect, selected = false } = props;
|
|
14
17
|
const helperText =
|
|
15
18
|
option.ready === false
|
|
16
19
|
? option.reasonMessage?.trim() || t("statusSetup")
|
|
@@ -19,8 +22,14 @@ export function ChatSessionTypeOptionItem(props: {
|
|
|
19
22
|
return (
|
|
20
23
|
<button
|
|
21
24
|
type="button"
|
|
25
|
+
aria-pressed={selected}
|
|
26
|
+
disabled={disabled}
|
|
22
27
|
onClick={onSelect}
|
|
23
|
-
className=
|
|
28
|
+
className={cn(
|
|
29
|
+
"w-full rounded-2xl px-3 py-2.5 text-left transition-colors",
|
|
30
|
+
selected ? "bg-gray-50" : "hover:bg-gray-50",
|
|
31
|
+
disabled ? "cursor-not-allowed opacity-70" : null,
|
|
32
|
+
)}
|
|
24
33
|
>
|
|
25
34
|
<div className="flex items-start gap-3">
|
|
26
35
|
<div className="flex min-w-0 flex-1 items-start gap-2.5">
|
|
@@ -56,6 +65,9 @@ export function ChatSessionTypeOptionItem(props: {
|
|
|
56
65
|
) : null}
|
|
57
66
|
</div>
|
|
58
67
|
</div>
|
|
68
|
+
{selected ? (
|
|
69
|
+
<Check className="mt-1 h-3.5 w-3.5 shrink-0 text-primary" />
|
|
70
|
+
) : null}
|
|
59
71
|
</div>
|
|
60
72
|
</button>
|
|
61
73
|
);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { PREFERENCE_KEYS } from "@/shared/lib/api";
|
|
6
|
+
import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
|
|
7
|
+
import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
fetchPreference: vi.fn(),
|
|
11
|
+
updatePreference: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/shared/lib/api", async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal();
|
|
16
|
+
return {
|
|
17
|
+
...(actual as Record<string, unknown>),
|
|
18
|
+
fetchPreference: mocks.fetchPreference,
|
|
19
|
+
updatePreference: mocks.updatePreference,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const sessionTypeOptions: ChatSessionTypeOption[] = [
|
|
24
|
+
{
|
|
25
|
+
value: "native",
|
|
26
|
+
label: "Native",
|
|
27
|
+
icon: null,
|
|
28
|
+
ready: true,
|
|
29
|
+
reason: null,
|
|
30
|
+
reasonMessage: null,
|
|
31
|
+
supportedModels: undefined,
|
|
32
|
+
recommendedModel: null,
|
|
33
|
+
modelSelectionMode: "nextclaw",
|
|
34
|
+
runtimeDefaultThinking: null,
|
|
35
|
+
cta: null,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: "codex",
|
|
39
|
+
label: "Codex",
|
|
40
|
+
icon: null,
|
|
41
|
+
ready: true,
|
|
42
|
+
reason: null,
|
|
43
|
+
reasonMessage: null,
|
|
44
|
+
supportedModels: undefined,
|
|
45
|
+
recommendedModel: null,
|
|
46
|
+
modelSelectionMode: "runtime-default",
|
|
47
|
+
runtimeDefaultThinking: null,
|
|
48
|
+
cta: null,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function createWrapper() {
|
|
53
|
+
const queryClient = new QueryClient({
|
|
54
|
+
defaultOptions: {
|
|
55
|
+
queries: { retry: false },
|
|
56
|
+
mutations: { retry: false },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
60
|
+
return (
|
|
61
|
+
<QueryClientProvider client={queryClient}>
|
|
62
|
+
{children}
|
|
63
|
+
</QueryClientProvider>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("useChatNewSessionTypePreference", () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
mocks.fetchPreference.mockReset();
|
|
71
|
+
mocks.updatePreference.mockReset();
|
|
72
|
+
mocks.fetchPreference.mockResolvedValue({
|
|
73
|
+
key: PREFERENCE_KEYS.chat.newSessionType,
|
|
74
|
+
value: null,
|
|
75
|
+
});
|
|
76
|
+
mocks.updatePreference.mockImplementation(
|
|
77
|
+
async (key: string, value: string) => ({
|
|
78
|
+
key,
|
|
79
|
+
value,
|
|
80
|
+
updatedAt: "2026-06-17T00:00:00.000Z",
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("uses the stored new-session type when it is selectable", async () => {
|
|
86
|
+
mocks.fetchPreference.mockResolvedValue({
|
|
87
|
+
key: PREFERENCE_KEYS.chat.newSessionType,
|
|
88
|
+
value: "codex",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { result } = renderHook(
|
|
92
|
+
() =>
|
|
93
|
+
useChatNewSessionTypePreference({
|
|
94
|
+
defaultSessionType: "native",
|
|
95
|
+
sessionTypeOptions,
|
|
96
|
+
}),
|
|
97
|
+
{ wrapper: createWrapper() },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(result.current.selectedSessionType).toBe("codex");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to the default type when the stored type is not selectable", async () => {
|
|
106
|
+
mocks.fetchPreference.mockResolvedValue({
|
|
107
|
+
key: PREFERENCE_KEYS.chat.newSessionType,
|
|
108
|
+
value: "claude",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const { result } = renderHook(
|
|
112
|
+
() =>
|
|
113
|
+
useChatNewSessionTypePreference({
|
|
114
|
+
defaultSessionType: "native",
|
|
115
|
+
sessionTypeOptions,
|
|
116
|
+
}),
|
|
117
|
+
{ wrapper: createWrapper() },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(result.current.selectedSessionType).toBe("native");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("persists only selectable session types", async () => {
|
|
126
|
+
const { result } = renderHook(
|
|
127
|
+
() =>
|
|
128
|
+
useChatNewSessionTypePreference({
|
|
129
|
+
defaultSessionType: "native",
|
|
130
|
+
sessionTypeOptions,
|
|
131
|
+
}),
|
|
132
|
+
{ wrapper: createWrapper() },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
act(() => {
|
|
136
|
+
result.current.setSelectedSessionType("codex");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(mocks.updatePreference).toHaveBeenCalledWith(
|
|
141
|
+
PREFERENCE_KEYS.chat.newSessionType,
|
|
142
|
+
"codex",
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
act(() => {
|
|
147
|
+
result.current.setSelectedSessionType("claude");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(mocks.updatePreference).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
});
|