@nextclaw/ui 0.15.5 → 0.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/assets/api-Dyb5rgh0.js +13 -0
- package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
- package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
- package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
- package/dist/assets/index-BVn702m4.css +1 -0
- package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
- package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
- package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
- package/dist/assets/plus-TmFdwO3P.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
- package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
- package/dist/assets/remote-B9F2uCk9.js +1 -0
- package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
- package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
- package/dist/assets/security-config-Cr2ERKLu.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
- package/dist/assets/use-config-CwDEAl6f.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
- package/dist/assets/x-DdEDL6ju.js +1 -0
- package/dist/index.html +29 -29
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import type { PersistStorage, StorageValue } from "zustand/middleware";
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
@@ -15,6 +15,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
15
15
|
deleteSession: vi.fn(),
|
|
16
16
|
toggleWorkspacePanel: vi.fn(),
|
|
17
17
|
selectSession: vi.fn(),
|
|
18
|
+
setListMode: vi.fn(),
|
|
19
|
+
toggleProjectCollapsed: vi.fn(),
|
|
18
20
|
sessionItems: [] as NcpSessionListItemView[],
|
|
19
21
|
isSessionListLoading: false,
|
|
20
22
|
}));
|
|
@@ -27,6 +29,8 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
27
29
|
},
|
|
28
30
|
chatSessionListManager: {
|
|
29
31
|
selectSession: mocks.selectSession,
|
|
32
|
+
setListMode: mocks.setListMode,
|
|
33
|
+
toggleProjectCollapsed: mocks.toggleProjectCollapsed,
|
|
30
34
|
},
|
|
31
35
|
}),
|
|
32
36
|
}));
|
|
@@ -120,6 +124,8 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
120
124
|
mocks.deleteSession.mockReset();
|
|
121
125
|
mocks.toggleWorkspacePanel.mockReset();
|
|
122
126
|
mocks.selectSession.mockReset();
|
|
127
|
+
mocks.setListMode.mockReset();
|
|
128
|
+
mocks.toggleProjectCollapsed.mockReset();
|
|
123
129
|
mocks.isSessionListLoading = false;
|
|
124
130
|
viewportLayoutManager.resetForTests();
|
|
125
131
|
useChatSessionListStore.persist.setOptions({
|
|
@@ -131,10 +137,25 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
131
137
|
useChatSessionListStore.setState({
|
|
132
138
|
snapshot: {
|
|
133
139
|
...useChatSessionListStore.getState().snapshot,
|
|
140
|
+
collapsedProjectRoots: [],
|
|
141
|
+
listMode: "time-first",
|
|
142
|
+
pinnedProjectRoots: [],
|
|
143
|
+
pinnedSessionKeys: [],
|
|
134
144
|
selectedAgentId: "main",
|
|
135
145
|
selectedSessionKey: "parent-session-1",
|
|
136
146
|
},
|
|
137
147
|
});
|
|
148
|
+
mocks.setListMode.mockImplementation((listMode) => {
|
|
149
|
+
useChatSessionListStore.getState().setSnapshot({ listMode });
|
|
150
|
+
});
|
|
151
|
+
mocks.toggleProjectCollapsed.mockImplementation((projectRoot) => {
|
|
152
|
+
const { collapsedProjectRoots } = useChatSessionListStore.getState().snapshot;
|
|
153
|
+
useChatSessionListStore.getState().setSnapshot({
|
|
154
|
+
collapsedProjectRoots: collapsedProjectRoots.includes(projectRoot)
|
|
155
|
+
? collapsedProjectRoots.filter((root) => root !== projectRoot)
|
|
156
|
+
: [...collapsedProjectRoots, projectRoot],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
138
159
|
useChatThreadStore.setState({
|
|
139
160
|
snapshot: {
|
|
140
161
|
...useChatThreadStore.getState().snapshot,
|
|
@@ -229,6 +250,68 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
229
250
|
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
230
251
|
});
|
|
231
252
|
|
|
253
|
+
it("uses lightweight static groups for pinned and activity dates", async () => {
|
|
254
|
+
const user = userEvent.setup();
|
|
255
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
256
|
+
useChatSessionListStore.setState({
|
|
257
|
+
snapshot: {
|
|
258
|
+
...useChatSessionListStore.getState().snapshot,
|
|
259
|
+
pinnedSessionKeys: ["session:ncp-2"],
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
renderHeaderSection();
|
|
264
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
265
|
+
|
|
266
|
+
const pinnedGroup = screen.getByRole("region", { name: "Pinned" });
|
|
267
|
+
const pinnedTitle = within(pinnedGroup).getByText("Pinned");
|
|
268
|
+
expect(pinnedTitle.className).toContain("uppercase");
|
|
269
|
+
expect(pinnedTitle.className).not.toContain("bg-muted");
|
|
270
|
+
expect(within(pinnedGroup).queryByRole("button", { name: "Pinned" })).toBeNull();
|
|
271
|
+
expect(within(pinnedGroup).getByText("Background Task")).toBeTruthy();
|
|
272
|
+
expect(
|
|
273
|
+
within(screen.getByRole("region", { name: "Older" })).getByText("Parent Task"),
|
|
274
|
+
).toBeTruthy();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("switches to collapsible project groups inside the header popover", async () => {
|
|
278
|
+
const user = userEvent.setup();
|
|
279
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
280
|
+
mocks.sessionItems = [
|
|
281
|
+
createSessionListItem({
|
|
282
|
+
key: "parent-session-1",
|
|
283
|
+
label: "Parent Task",
|
|
284
|
+
projectName: "Alpha",
|
|
285
|
+
projectRoot: "/workspace/alpha",
|
|
286
|
+
}),
|
|
287
|
+
createSessionListItem({
|
|
288
|
+
key: "session:ncp-2",
|
|
289
|
+
label: "Background Task",
|
|
290
|
+
projectName: "Beta",
|
|
291
|
+
projectRoot: "/workspace/beta",
|
|
292
|
+
}),
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
renderHeaderSection();
|
|
296
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
297
|
+
await user.click(screen.getByRole("button", { name: "Project" }));
|
|
298
|
+
|
|
299
|
+
expect(mocks.setListMode).toHaveBeenCalledWith("project-first");
|
|
300
|
+
const alphaGroup = screen.getByRole("region", { name: "Alpha" });
|
|
301
|
+
const alphaToggle = within(alphaGroup).getByRole("button", {
|
|
302
|
+
name: "Collapse project · Alpha",
|
|
303
|
+
});
|
|
304
|
+
expect(alphaToggle.getAttribute("aria-expanded")).toBe("true");
|
|
305
|
+
expect(alphaToggle.className).not.toContain("bg-muted");
|
|
306
|
+
expect(within(alphaGroup).getByText("Parent Task")).toBeTruthy();
|
|
307
|
+
|
|
308
|
+
await user.click(alphaToggle);
|
|
309
|
+
expect(mocks.toggleProjectCollapsed).toHaveBeenCalledWith("/workspace/alpha");
|
|
310
|
+
expect(
|
|
311
|
+
within(screen.getByRole("region", { name: "Alpha" })).queryByText("Parent Task"),
|
|
312
|
+
).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
232
315
|
it("keeps the desktop title plain while the sidebar is expanded", () => {
|
|
233
316
|
renderHeaderSection();
|
|
234
317
|
|
|
@@ -17,7 +17,10 @@ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-lis
|
|
|
17
17
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
18
18
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
19
19
|
import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getSessionProjectName,
|
|
22
|
+
normalizeSessionProjectRootValue,
|
|
23
|
+
} from "@/shared/lib/session-project";
|
|
21
24
|
import { t } from "@/shared/lib/i18n";
|
|
22
25
|
|
|
23
26
|
type ChatConversationHeaderSectionProps = {
|
|
@@ -40,6 +43,9 @@ export function ChatConversationHeaderSection({
|
|
|
40
43
|
const sessionTypesData = useChatQueryStore(
|
|
41
44
|
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
42
45
|
);
|
|
46
|
+
const config = useChatQueryStore(
|
|
47
|
+
(state) => state.snapshot.configQuery?.data ?? null,
|
|
48
|
+
);
|
|
43
49
|
const selectedSession = useNcpChatSelectedSession(sessionKey);
|
|
44
50
|
const sessionTypeOptions = useMemo(
|
|
45
51
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
@@ -55,6 +61,9 @@ export function ChatConversationHeaderSection({
|
|
|
55
61
|
const shouldShowSessionTypeBadge =
|
|
56
62
|
sessionType !== DEFAULT_SESSION_TYPE && Boolean(sessionTypeLabel);
|
|
57
63
|
const sessionProjectRoot = selectedSession?.projectRoot ?? null;
|
|
64
|
+
const defaultWorkspacePath = normalizeSessionProjectRootValue(
|
|
65
|
+
config?.agents.defaults.workspace,
|
|
66
|
+
);
|
|
58
67
|
const sessionProjectName =
|
|
59
68
|
selectedSession?.projectName ?? getSessionProjectName(sessionProjectRoot);
|
|
60
69
|
const canDeleteSession = Boolean(selectedSession);
|
|
@@ -129,6 +138,7 @@ export function ChatConversationHeaderSection({
|
|
|
129
138
|
sessionKey={sessionKey ?? "draft"}
|
|
130
139
|
projectName={sessionProjectName}
|
|
131
140
|
projectRoot={sessionProjectRoot}
|
|
141
|
+
defaultWorkspacePath={defaultWorkspacePath}
|
|
132
142
|
persistToServer={canDeleteSession}
|
|
133
143
|
/>
|
|
134
144
|
) : null
|
|
@@ -139,7 +149,8 @@ export function ChatConversationHeaderSection({
|
|
|
139
149
|
sessionKey={sessionKey}
|
|
140
150
|
canDeleteSession={canDeleteSession}
|
|
141
151
|
isDeletePending={snapshot.isDeletePending}
|
|
142
|
-
|
|
152
|
+
currentPath={sessionProjectRoot ?? selectedSession?.workingDir ?? null}
|
|
153
|
+
defaultWorkspacePath={defaultWorkspacePath}
|
|
143
154
|
metadata={selectedSession?.metadata ?? null}
|
|
144
155
|
isWorkspaceOpen={isWorkspaceOpen}
|
|
145
156
|
onToggleWorkspace={toggleWorkspace}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NcpSessionListItemView } from '@/features/chat/features/ncp/hooks/use-ncp-session-list-view';
|
|
2
|
+
import { useChatQueryStore } from '@/features/chat/stores/ncp-chat-query.store';
|
|
3
|
+
import type { ChatSessionTypeOptionView } from '@/shared/lib/api';
|
|
4
|
+
|
|
5
|
+
export function createSidebarSessionItem(
|
|
6
|
+
session: NcpSessionListItemView['session'],
|
|
7
|
+
runStatus?: NcpSessionListItemView['runStatus'],
|
|
8
|
+
): NcpSessionListItemView {
|
|
9
|
+
return { session, runStatus };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setSidebarSessionTypes(
|
|
13
|
+
options: ChatSessionTypeOptionView[],
|
|
14
|
+
defaultType = 'native',
|
|
15
|
+
) {
|
|
16
|
+
useChatQueryStore.setState({
|
|
17
|
+
snapshot: {
|
|
18
|
+
...useChatQueryStore.getState().snapshot,
|
|
19
|
+
sessionTypesQuery: { data: { defaultType, options } } as never,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -3,11 +3,13 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import { MemoryRouter } from "react-router-dom";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
|
|
6
|
+
import {
|
|
7
|
+
createSidebarSessionItem as createSessionItem,
|
|
8
|
+
setSidebarSessionTypes as setSessionTypes,
|
|
9
|
+
} from "@/features/chat/components/layout/__tests__/chat-sidebar-test.utils";
|
|
6
10
|
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
7
|
-
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
8
11
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
9
12
|
import { PREFERENCE_KEYS } from "@/shared/lib/api";
|
|
10
|
-
import type { ChatSessionTypeOptionView } from "@/shared/lib/api";
|
|
11
13
|
import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
|
|
12
14
|
const mocks = vi.hoisted(() => ({
|
|
13
15
|
createSession: vi.fn(() => "draft-session-key"),
|
|
@@ -39,28 +41,15 @@ vi.mock("@/shared/lib/api", async (importOriginal) => {
|
|
|
39
41
|
updatePreference: mocks.updatePreference,
|
|
40
42
|
};
|
|
41
43
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
) {
|
|
52
|
-
useChatQueryStore.setState({
|
|
53
|
-
snapshot: {
|
|
54
|
-
...useChatQueryStore.getState().snapshot,
|
|
55
|
-
sessionTypesQuery: {
|
|
56
|
-
data: {
|
|
57
|
-
defaultType,
|
|
58
|
-
options,
|
|
59
|
-
},
|
|
60
|
-
} as never,
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
}
|
|
44
|
+
vi.mock("@/shared/hooks/use-projects", () => ({
|
|
45
|
+
useProjects: () => ({ data: { projects: [], templates: [] } }),
|
|
46
|
+
useCreateProject: () => ({
|
|
47
|
+
mutateAsync: vi.fn(),
|
|
48
|
+
reset: vi.fn(),
|
|
49
|
+
isPending: false,
|
|
50
|
+
error: null,
|
|
51
|
+
}),
|
|
52
|
+
}));
|
|
64
53
|
function sidebarElement(variant?: "desktop" | "mobile") {
|
|
65
54
|
const queryClient = new QueryClient({
|
|
66
55
|
defaultOptions: {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
BrainCircuit,
|
|
19
19
|
PanelLeftClose,
|
|
20
20
|
PanelLeftOpen,
|
|
21
|
+
Plus,
|
|
21
22
|
} from "lucide-react";
|
|
22
23
|
import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
|
|
23
24
|
import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
|
|
@@ -134,6 +135,7 @@ export function ChatSidebarSessionArea({
|
|
|
134
135
|
isCollapsed,
|
|
135
136
|
isLoading,
|
|
136
137
|
isProjectFirstView,
|
|
138
|
+
onCreateProject,
|
|
137
139
|
onSelectMode,
|
|
138
140
|
projectGroups,
|
|
139
141
|
renderSessionItem,
|
|
@@ -144,6 +146,7 @@ export function ChatSidebarSessionArea({
|
|
|
144
146
|
isCollapsed: boolean;
|
|
145
147
|
isLoading: boolean;
|
|
146
148
|
isProjectFirstView: boolean;
|
|
149
|
+
onCreateProject: () => void;
|
|
147
150
|
onSelectMode: (mode: "time-first" | "project-first") => void;
|
|
148
151
|
projectGroups: ReturnType<typeof groupSessionsByProject>;
|
|
149
152
|
renderSessionItem: (item: NcpSessionListItemView) => JSX.Element;
|
|
@@ -161,10 +164,19 @@ export function ChatSidebarSessionArea({
|
|
|
161
164
|
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
|
|
162
165
|
{t("chatSidebarTaskRecords")}
|
|
163
166
|
</div>
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
<div className="flex items-center gap-1">
|
|
168
|
+
{isProjectFirstView ? (
|
|
169
|
+
<IconActionButton
|
|
170
|
+
icon={<Plus className="h-3.5 w-3.5" />}
|
|
171
|
+
label={t("chatProjectCreate")}
|
|
172
|
+
onClick={onCreateProject}
|
|
173
|
+
/>
|
|
174
|
+
) : null}
|
|
175
|
+
<ChatSidebarListModeSwitch
|
|
176
|
+
isProjectFirstView={isProjectFirstView}
|
|
177
|
+
onSelectMode={onSelectMode}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
168
180
|
</div>
|
|
169
181
|
|
|
170
182
|
<div
|
|
@@ -16,6 +16,9 @@ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-lis
|
|
|
16
16
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
17
17
|
import { useSystemStatus } from "@/features/system-status";
|
|
18
18
|
import { useAgents } from "@/shared/hooks/use-agents";
|
|
19
|
+
import { useCreateProject, useProjects } from "@/shared/hooks/use-projects";
|
|
20
|
+
import type { ProjectCreateRequest } from "@/shared/lib/api";
|
|
21
|
+
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
19
22
|
import { cn } from "@/shared/lib/utils";
|
|
20
23
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from "@/shared/lib/i18n";
|
|
21
24
|
import { THEME_OPTIONS } from "@/shared/lib/theme";
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
|
|
43
46
|
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
44
47
|
import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
|
|
48
|
+
import { ChatProjectCreateDialog } from "@/features/chat/features/project/components/chat-project-create-dialog";
|
|
45
49
|
|
|
46
50
|
type ChatSidebarVariant = "desktop" | "mobile";
|
|
47
51
|
|
|
@@ -94,13 +98,19 @@ export function ChatSidebar({
|
|
|
94
98
|
(state) => state.isSidebarCollapsed,
|
|
95
99
|
);
|
|
96
100
|
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
101
|
+
const [isProjectCreateOpen, setIsProjectCreateOpen] = useState(false);
|
|
97
102
|
const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
|
|
98
103
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
99
104
|
const sessionTypesData = useChatQueryStore(
|
|
100
105
|
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
101
106
|
);
|
|
107
|
+
const config = useChatQueryStore(
|
|
108
|
+
(state) => state.snapshot.configQuery?.data ?? null,
|
|
109
|
+
);
|
|
102
110
|
const systemStatus = useSystemStatus();
|
|
103
111
|
const agentsQuery = useAgents();
|
|
112
|
+
const projectsQuery = useProjects();
|
|
113
|
+
const projectCreateMutation = useCreateProject();
|
|
104
114
|
const { isLoading, items } = useNcpSessionListView();
|
|
105
115
|
const { language, setLanguage } = useI18n();
|
|
106
116
|
const { theme, setTheme } = useTheme();
|
|
@@ -153,8 +163,13 @@ export function ChatSidebar({
|
|
|
153
163
|
[pinnedSessionKeys, sortedItems],
|
|
154
164
|
);
|
|
155
165
|
const projectGroups = useMemo(
|
|
156
|
-
() => groupSessionsByProject(
|
|
157
|
-
|
|
166
|
+
() => groupSessionsByProject(
|
|
167
|
+
sortedItems,
|
|
168
|
+
pinnedSessionKeys,
|
|
169
|
+
pinnedProjectRoots,
|
|
170
|
+
projectsQuery.data?.projects ?? [],
|
|
171
|
+
),
|
|
172
|
+
[pinnedProjectRoots, pinnedSessionKeys, projectsQuery.data?.projects, sortedItems],
|
|
158
173
|
);
|
|
159
174
|
const sessionTypeOptions = useMemo(
|
|
160
175
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
@@ -238,6 +253,14 @@ export function ChatSidebar({
|
|
|
238
253
|
typeof projectRoot === "string" ? projectRoot : undefined,
|
|
239
254
|
);
|
|
240
255
|
};
|
|
256
|
+
const openProjectCreate = () => {
|
|
257
|
+
projectCreateMutation.reset();
|
|
258
|
+
setIsProjectCreateOpen(true);
|
|
259
|
+
};
|
|
260
|
+
const createProjectFromSidebar = async (input: ProjectCreateRequest): Promise<void> => {
|
|
261
|
+
await projectCreateMutation.mutateAsync(input);
|
|
262
|
+
setIsProjectCreateOpen(false);
|
|
263
|
+
};
|
|
241
264
|
|
|
242
265
|
return (
|
|
243
266
|
<aside
|
|
@@ -310,6 +333,7 @@ export function ChatSidebar({
|
|
|
310
333
|
isCollapsed={shouldCollapse}
|
|
311
334
|
isLoading={isLoading}
|
|
312
335
|
isProjectFirstView={isProjectFirstView}
|
|
336
|
+
onCreateProject={openProjectCreate}
|
|
313
337
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
314
338
|
projectGroups={projectGroups}
|
|
315
339
|
renderSessionItem={renderSessionItem}
|
|
@@ -338,6 +362,18 @@ export function ChatSidebar({
|
|
|
338
362
|
themeOptions={utilityThemeOptions}
|
|
339
363
|
/>
|
|
340
364
|
) : null}
|
|
365
|
+
|
|
366
|
+
<ChatProjectCreateDialog
|
|
367
|
+
open={isProjectCreateOpen}
|
|
368
|
+
defaultWorkspacePath={normalizeSessionProjectRootValue(
|
|
369
|
+
config?.agents.defaults.workspace,
|
|
370
|
+
)}
|
|
371
|
+
templates={projectsQuery.data?.templates ?? []}
|
|
372
|
+
isCreating={projectCreateMutation.isPending}
|
|
373
|
+
errorMessage={projectCreateMutation.error?.message}
|
|
374
|
+
onOpenChange={setIsProjectCreateOpen}
|
|
375
|
+
onCreate={createProjectFromSidebar}
|
|
376
|
+
/>
|
|
341
377
|
</aside>
|
|
342
378
|
);
|
|
343
379
|
}
|
|
@@ -21,6 +21,7 @@ const mocks = vi.hoisted(() => {
|
|
|
21
21
|
removeAttachment: vi.fn(),
|
|
22
22
|
setSelectedModel: vi.fn(),
|
|
23
23
|
setSelectedThinkingLevel: vi.fn(),
|
|
24
|
+
syncSessionPreferences: vi.fn(),
|
|
24
25
|
setPendingSessionType: vi.fn(),
|
|
25
26
|
setPendingProjectRoot: vi.fn(),
|
|
26
27
|
setSelectedSkills: vi.fn(),
|
|
@@ -110,6 +110,42 @@ function createStreamingInputSnapshot(
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function setImeDomText(textbox: HTMLElement, text: string): HTMLParagraphElement {
|
|
114
|
+
const paragraph = textbox.querySelector('p');
|
|
115
|
+
if (!paragraph) {
|
|
116
|
+
throw new Error('Expected the Lexical composer paragraph to exist.');
|
|
117
|
+
}
|
|
118
|
+
const range = document.createRange();
|
|
119
|
+
const lexicalText = paragraph.querySelector('[data-lexical-text="true"]')?.firstChild;
|
|
120
|
+
if (lexicalText?.nodeType === Node.TEXT_NODE) {
|
|
121
|
+
lexicalText.nodeValue = text;
|
|
122
|
+
range.setStart(lexicalText, text.length);
|
|
123
|
+
range.collapse(true);
|
|
124
|
+
} else {
|
|
125
|
+
paragraph.textContent = text;
|
|
126
|
+
range.selectNodeContents(paragraph);
|
|
127
|
+
range.collapse(false);
|
|
128
|
+
}
|
|
129
|
+
const selection = window.getSelection();
|
|
130
|
+
selection?.removeAllRanges();
|
|
131
|
+
selection?.addRange(range);
|
|
132
|
+
return paragraph;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function insertText(textbox: HTMLElement, text: string): Promise<void> {
|
|
136
|
+
await act(async () => {
|
|
137
|
+
for (const character of text) {
|
|
138
|
+
textbox.dispatchEvent(new InputEvent('beforeinput', {
|
|
139
|
+
bubbles: true,
|
|
140
|
+
cancelable: true,
|
|
141
|
+
data: character,
|
|
142
|
+
inputType: 'insertText',
|
|
143
|
+
}));
|
|
144
|
+
await Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
113
149
|
function StreamingSessionConversationInputHarness({
|
|
114
150
|
controllerOverride = controller,
|
|
115
151
|
controlRef,
|
|
@@ -145,6 +181,7 @@ function StreamingSessionConversationInputHarness({
|
|
|
145
181
|
setSelectedModel: vi.fn(),
|
|
146
182
|
setSelectedSkills: vi.fn(),
|
|
147
183
|
setSelectedThinkingLevel: vi.fn(),
|
|
184
|
+
syncSessionPreferences: vi.fn(),
|
|
148
185
|
setSendError: vi.fn(),
|
|
149
186
|
syncComposer: vi.fn(),
|
|
150
187
|
update: (patch: SessionConversationInputPatch) => {
|
|
@@ -209,23 +246,36 @@ describe('SessionConversationInput streaming stability', () => {
|
|
|
209
246
|
expect(track?.firstElementChild?.className).toContain('px-0');
|
|
210
247
|
});
|
|
211
248
|
|
|
212
|
-
it('keeps IME
|
|
249
|
+
it('keeps a numbered IME candidate commit stable while streamed output rerenders the owner', async () => {
|
|
213
250
|
const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
|
|
214
251
|
render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
|
|
215
252
|
|
|
216
253
|
const textbox = screen.getByRole('textbox');
|
|
217
254
|
fireEvent.focus(textbox);
|
|
255
|
+
await insertText(textbox, 'n');
|
|
218
256
|
fireEvent.compositionStart(textbox);
|
|
257
|
+
const paragraph = setImeDomText(textbox, 'ni');
|
|
219
258
|
|
|
220
259
|
act(() => {
|
|
221
260
|
controlRef.current?.bumpStream();
|
|
222
261
|
controlRef.current?.bumpStream();
|
|
223
262
|
});
|
|
224
263
|
expect(screen.getByTestId('stream-chunk').textContent).toBe('2');
|
|
225
|
-
|
|
264
|
+
expect(textbox.querySelector('p')).toBe(paragraph);
|
|
265
|
+
expect(textbox.textContent).toBe('ni');
|
|
266
|
+
|
|
267
|
+
fireEvent.keyDown(textbox, { key: '1', isComposing: true });
|
|
268
|
+
setImeDomText(textbox, '你');
|
|
269
|
+
fireEvent.input(textbox, {
|
|
270
|
+
data: '你',
|
|
271
|
+
inputType: 'insertCompositionText',
|
|
272
|
+
isComposing: true,
|
|
273
|
+
});
|
|
226
274
|
fireEvent.compositionEnd(textbox, { data: '你' });
|
|
227
275
|
|
|
228
276
|
await waitFor(() => expect(textbox.textContent).toBe('你'));
|
|
277
|
+
expect(textbox.querySelector('p')).toBe(paragraph);
|
|
278
|
+
await waitFor(() => expect(window.getSelection()?.anchorOffset).toBe(1));
|
|
229
279
|
});
|
|
230
280
|
|
|
231
281
|
it('renders queued inputs above the composer with edit and delete actions', () => {
|
|
@@ -188,6 +188,28 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
|
|
|
188
188
|
inputSnapshot,
|
|
189
189
|
setPendingSessionType: inputActions.setPendingSessionType,
|
|
190
190
|
});
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
inputActions.syncSessionPreferences({
|
|
193
|
+
defaultModel: inputQuery.defaultModel,
|
|
194
|
+
fallbackPreferredModel: inputQuery.fallbackPreferredModel,
|
|
195
|
+
fallbackPreferredThinking: inputQuery.fallbackPreferredThinking,
|
|
196
|
+
modelOptions: inputQuery.modelOptions,
|
|
197
|
+
selectedSessionExists: Boolean(inputQuery.selectedSession),
|
|
198
|
+
selectedSessionKey: inputQuery.selectedSessionKey,
|
|
199
|
+
selectedSessionPreferredModel:
|
|
200
|
+
inputQuery.selectedSession?.preferredModel ?? undefined,
|
|
201
|
+
selectedSessionPreferredThinking:
|
|
202
|
+
inputQuery.selectedSession?.preferredThinking ?? null,
|
|
203
|
+
});
|
|
204
|
+
}, [
|
|
205
|
+
inputActions,
|
|
206
|
+
inputQuery.defaultModel,
|
|
207
|
+
inputQuery.fallbackPreferredModel,
|
|
208
|
+
inputQuery.fallbackPreferredThinking,
|
|
209
|
+
inputQuery.modelOptions,
|
|
210
|
+
inputQuery.selectedSession,
|
|
211
|
+
inputQuery.selectedSessionKey,
|
|
212
|
+
]);
|
|
191
213
|
useSessionConversationDraftRouteState({
|
|
192
214
|
sessionKey,
|
|
193
215
|
setPendingProjectRoot: inputActions.setPendingProjectRoot,
|
|
@@ -249,6 +249,9 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
|
|
|
249
249
|
}),
|
|
250
250
|
[presenter.chatUiManager],
|
|
251
251
|
);
|
|
252
|
+
const contextReferenceProjectRoot = inputQuery.selectedSessionKey
|
|
253
|
+
? inputQuery.selectedSession?.projectRoot ?? inputQuery.defaultProjectRoot ?? ''
|
|
254
|
+
: inputSnapshot.pendingProjectRoot ?? inputQuery.defaultProjectRoot ?? '';
|
|
252
255
|
const { inputSurfaceState, setInputSurfaceTrigger } = useChatInputSurfaceState({
|
|
253
256
|
commands: slashCommands,
|
|
254
257
|
isSkillsLoading: inputQuery.isSkillsLoading,
|
|
@@ -258,6 +261,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
|
|
|
258
261
|
language,
|
|
259
262
|
onSelectPanelApp: handleSlashPanelAppSelect,
|
|
260
263
|
onSelectSkill: chatRecentSkillsManager.remember,
|
|
264
|
+
projectRoot: contextReferenceProjectRoot,
|
|
261
265
|
recentSkillValues,
|
|
262
266
|
skillRecords,
|
|
263
267
|
});
|
|
@@ -269,15 +273,9 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
|
|
|
269
273
|
favoriteModelValues,
|
|
270
274
|
setModelFavorite,
|
|
271
275
|
} = useChatModelFavorites(modelRecordValues);
|
|
272
|
-
const selectedModel =
|
|
273
|
-
inputSnapshot.selectedModel ??
|
|
274
|
-
inputQuery.fallbackPreferredModel ??
|
|
275
|
-
inputQuery.defaultModel ??
|
|
276
|
-
'';
|
|
276
|
+
const selectedModel = inputSnapshot.selectedModel ?? '';
|
|
277
277
|
const selectedThinkingLevel = (
|
|
278
|
-
inputSnapshot.selectedThinkingLevel
|
|
279
|
-
inputQuery.fallbackPreferredThinking ??
|
|
280
|
-
null
|
|
278
|
+
inputSnapshot.selectedThinkingLevel
|
|
281
279
|
) as ChatThinkingLevel | null;
|
|
282
280
|
const availabilitySnapshot = {
|
|
283
281
|
isProviderStateResolved: inputQuery.isProviderStateResolved,
|
|
@@ -408,6 +406,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
|
|
|
408
406
|
skillRecords,
|
|
409
407
|
selectedSkills: inputSnapshot.selectedSkills,
|
|
410
408
|
});
|
|
409
|
+
const composerNodes = useMemo(() => [...inputSnapshot.nodes], [inputSnapshot.nodes]);
|
|
411
410
|
|
|
412
411
|
const useReadingTrack = surface === 'default' && messageLayout === 'flat';
|
|
413
412
|
const inputBar = (
|
|
@@ -420,7 +419,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
|
|
|
420
419
|
? <SessionQueuedInputRows controller={controller} />
|
|
421
420
|
: null}
|
|
422
421
|
composer={{
|
|
423
|
-
nodes:
|
|
422
|
+
nodes: composerNodes,
|
|
424
423
|
placeholder: textareaPlaceholder,
|
|
425
424
|
disabled: inputDisabled,
|
|
426
425
|
onNodesChange: handleNodesChange,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { useSessionConversationInputState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
|
|
5
|
+
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
6
|
+
|
|
7
|
+
const MODEL_OPTIONS: ChatModelOption[] = [
|
|
8
|
+
{
|
|
9
|
+
value: 'deepseek/deepseek-v4-flash',
|
|
10
|
+
modelLabel: 'DeepSeek V4 Flash',
|
|
11
|
+
providerLabel: 'DeepSeek',
|
|
12
|
+
thinkingCapability: null,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: 'minimax/MiniMax-M3',
|
|
16
|
+
modelLabel: 'MiniMax M3',
|
|
17
|
+
providerLabel: 'MiniMax',
|
|
18
|
+
thinkingCapability: null,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe('useSessionConversationInputState session preferences', () => {
|
|
23
|
+
it('restores the switched session model after its metadata becomes available', () => {
|
|
24
|
+
const { result } = renderHook(() => useSessionConversationInputState());
|
|
25
|
+
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current.inputActions.syncSessionPreferences({
|
|
28
|
+
modelOptions: MODEL_OPTIONS,
|
|
29
|
+
selectedSessionExists: true,
|
|
30
|
+
selectedSessionKey: 'session-a',
|
|
31
|
+
selectedSessionPreferredModel: 'deepseek/deepseek-v4-flash',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
|
|
35
|
+
|
|
36
|
+
act(() => {
|
|
37
|
+
result.current.inputActions.syncSessionPreferences({
|
|
38
|
+
modelOptions: MODEL_OPTIONS,
|
|
39
|
+
selectedSessionExists: false,
|
|
40
|
+
selectedSessionKey: 'session-b',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
|
|
44
|
+
|
|
45
|
+
act(() => {
|
|
46
|
+
result.current.inputActions.syncSessionPreferences({
|
|
47
|
+
modelOptions: MODEL_OPTIONS,
|
|
48
|
+
selectedSessionExists: true,
|
|
49
|
+
selectedSessionKey: 'session-b',
|
|
50
|
+
selectedSessionPreferredModel: 'minimax/MiniMax-M3',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
expect(result.current.inputSnapshot.selectedModel).toBe('minimax/MiniMax-M3');
|
|
54
|
+
});
|
|
55
|
+
});
|