@nextclaw/ui 0.15.5 → 0.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/assets/api-V8m62-13.js +13 -0
- package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
- package/dist/assets/index-DtWf7kk9.css +1 -0
- package/dist/assets/index-UnY4HtVa.js +2 -0
- package/dist/assets/loader-circle-BRSfaztA.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
- package/dist/assets/remote-C1y3ARHu.js +1 -0
- package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
- package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/use-config-CY4uF2Ou.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
- package/dist/assets/x-C6cM8wkT.js +1 -0
- package/dist/index.html +30 -29
- package/index.html +1 -0
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
- 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 +70 -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 +82 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -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 +6 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -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 +67 -5
- 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 +83 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
- package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -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 +82 -128
- 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/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
- package/src/features/marketplace/components/marketplace-page.tsx +10 -2
- package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
- package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
- 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 +40 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -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/index-CT0iYlxV.js +0 -2
- 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
|
}));
|
|
@@ -101,7 +105,7 @@ function createSessionListItem(
|
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
function renderHeaderSection() {
|
|
108
|
+
function renderHeaderSection(layoutMode: "desktop" | "mobile" = "desktop") {
|
|
105
109
|
const queryClient = new QueryClient({
|
|
106
110
|
defaultOptions: {
|
|
107
111
|
queries: { retry: false },
|
|
@@ -110,7 +114,7 @@ function renderHeaderSection() {
|
|
|
110
114
|
});
|
|
111
115
|
return render(
|
|
112
116
|
<QueryClientProvider client={queryClient}>
|
|
113
|
-
<ChatConversationHeaderSection layoutMode=
|
|
117
|
+
<ChatConversationHeaderSection layoutMode={layoutMode} />
|
|
114
118
|
</QueryClientProvider>,
|
|
115
119
|
);
|
|
116
120
|
}
|
|
@@ -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,79 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
229
250
|
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
230
251
|
});
|
|
231
252
|
|
|
253
|
+
it("uses the mobile title as a session switcher", async () => {
|
|
254
|
+
const user = userEvent.setup();
|
|
255
|
+
|
|
256
|
+
renderHeaderSection("mobile");
|
|
257
|
+
|
|
258
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
259
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
260
|
+
|
|
261
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("uses lightweight static groups for pinned and activity dates", async () => {
|
|
265
|
+
const user = userEvent.setup();
|
|
266
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
267
|
+
useChatSessionListStore.setState({
|
|
268
|
+
snapshot: {
|
|
269
|
+
...useChatSessionListStore.getState().snapshot,
|
|
270
|
+
pinnedSessionKeys: ["session:ncp-2"],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
renderHeaderSection();
|
|
275
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
276
|
+
|
|
277
|
+
const pinnedGroup = screen.getByRole("region", { name: "Pinned" });
|
|
278
|
+
const pinnedTitle = within(pinnedGroup).getByText("Pinned");
|
|
279
|
+
expect(pinnedTitle.className).toContain("uppercase");
|
|
280
|
+
expect(pinnedTitle.className).not.toContain("bg-muted");
|
|
281
|
+
expect(within(pinnedGroup).queryByRole("button", { name: "Pinned" })).toBeNull();
|
|
282
|
+
expect(within(pinnedGroup).getByText("Background Task")).toBeTruthy();
|
|
283
|
+
expect(
|
|
284
|
+
within(screen.getByRole("region", { name: "Older" })).getByText("Parent Task"),
|
|
285
|
+
).toBeTruthy();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("switches to collapsible project groups inside the header popover", async () => {
|
|
289
|
+
const user = userEvent.setup();
|
|
290
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
291
|
+
mocks.sessionItems = [
|
|
292
|
+
createSessionListItem({
|
|
293
|
+
key: "parent-session-1",
|
|
294
|
+
label: "Parent Task",
|
|
295
|
+
projectName: "Alpha",
|
|
296
|
+
projectRoot: "/workspace/alpha",
|
|
297
|
+
}),
|
|
298
|
+
createSessionListItem({
|
|
299
|
+
key: "session:ncp-2",
|
|
300
|
+
label: "Background Task",
|
|
301
|
+
projectName: "Beta",
|
|
302
|
+
projectRoot: "/workspace/beta",
|
|
303
|
+
}),
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
renderHeaderSection();
|
|
307
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
308
|
+
await user.click(screen.getByRole("button", { name: "Project" }));
|
|
309
|
+
|
|
310
|
+
expect(mocks.setListMode).toHaveBeenCalledWith("project-first");
|
|
311
|
+
const alphaGroup = screen.getByRole("region", { name: "Alpha" });
|
|
312
|
+
const alphaToggle = within(alphaGroup).getByRole("button", {
|
|
313
|
+
name: "Collapse project · Alpha",
|
|
314
|
+
});
|
|
315
|
+
expect(alphaToggle.getAttribute("aria-expanded")).toBe("true");
|
|
316
|
+
expect(alphaToggle.className).not.toContain("bg-muted");
|
|
317
|
+
expect(within(alphaGroup).getByText("Parent Task")).toBeTruthy();
|
|
318
|
+
|
|
319
|
+
await user.click(alphaToggle);
|
|
320
|
+
expect(mocks.toggleProjectCollapsed).toHaveBeenCalledWith("/workspace/alpha");
|
|
321
|
+
expect(
|
|
322
|
+
within(screen.getByRole("region", { name: "Alpha" })).queryByText("Parent Task"),
|
|
323
|
+
).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
232
326
|
it("keeps the desktop title plain while the sidebar is expanded", () => {
|
|
233
327
|
renderHeaderSection();
|
|
234
328
|
|
|
@@ -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(),
|
|
@@ -54,6 +54,24 @@ vi.mock('@/features/chat/features/input/hooks/use-chat-model-favorites', () => (
|
|
|
54
54
|
}),
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
|
+
vi.mock('@/shared/hooks/use-server-path-browse', () => ({
|
|
58
|
+
useServerPathBrowse: () => ({
|
|
59
|
+
data: { entries: [] },
|
|
60
|
+
error: null,
|
|
61
|
+
isFetching: false,
|
|
62
|
+
isLoading: false,
|
|
63
|
+
}),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
vi.mock('@/shared/hooks/use-server-path-search', () => ({
|
|
67
|
+
useServerPathSearch: () => ({
|
|
68
|
+
data: { entries: [] },
|
|
69
|
+
error: null,
|
|
70
|
+
isFetching: false,
|
|
71
|
+
isLoading: false,
|
|
72
|
+
}),
|
|
73
|
+
}));
|
|
74
|
+
|
|
57
75
|
vi.mock('@/shared/lib/api', async (importOriginal) => {
|
|
58
76
|
const actual = await importOriginal();
|
|
59
77
|
return {
|
|
@@ -110,6 +128,42 @@ function createStreamingInputSnapshot(
|
|
|
110
128
|
};
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
function setImeDomText(textbox: HTMLElement, text: string): HTMLParagraphElement {
|
|
132
|
+
const paragraph = textbox.querySelector('p');
|
|
133
|
+
if (!paragraph) {
|
|
134
|
+
throw new Error('Expected the Lexical composer paragraph to exist.');
|
|
135
|
+
}
|
|
136
|
+
const range = document.createRange();
|
|
137
|
+
const lexicalText = paragraph.querySelector('[data-lexical-text="true"]')?.firstChild;
|
|
138
|
+
if (lexicalText?.nodeType === Node.TEXT_NODE) {
|
|
139
|
+
lexicalText.nodeValue = text;
|
|
140
|
+
range.setStart(lexicalText, text.length);
|
|
141
|
+
range.collapse(true);
|
|
142
|
+
} else {
|
|
143
|
+
paragraph.textContent = text;
|
|
144
|
+
range.selectNodeContents(paragraph);
|
|
145
|
+
range.collapse(false);
|
|
146
|
+
}
|
|
147
|
+
const selection = window.getSelection();
|
|
148
|
+
selection?.removeAllRanges();
|
|
149
|
+
selection?.addRange(range);
|
|
150
|
+
return paragraph;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function insertText(textbox: HTMLElement, text: string): Promise<void> {
|
|
154
|
+
await act(async () => {
|
|
155
|
+
for (const character of text) {
|
|
156
|
+
textbox.dispatchEvent(new InputEvent('beforeinput', {
|
|
157
|
+
bubbles: true,
|
|
158
|
+
cancelable: true,
|
|
159
|
+
data: character,
|
|
160
|
+
inputType: 'insertText',
|
|
161
|
+
}));
|
|
162
|
+
await Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
113
167
|
function StreamingSessionConversationInputHarness({
|
|
114
168
|
controllerOverride = controller,
|
|
115
169
|
controlRef,
|
|
@@ -145,6 +199,7 @@ function StreamingSessionConversationInputHarness({
|
|
|
145
199
|
setSelectedModel: vi.fn(),
|
|
146
200
|
setSelectedSkills: vi.fn(),
|
|
147
201
|
setSelectedThinkingLevel: vi.fn(),
|
|
202
|
+
syncSessionPreferences: vi.fn(),
|
|
148
203
|
setSendError: vi.fn(),
|
|
149
204
|
syncComposer: vi.fn(),
|
|
150
205
|
update: (patch: SessionConversationInputPatch) => {
|
|
@@ -209,23 +264,36 @@ describe('SessionConversationInput streaming stability', () => {
|
|
|
209
264
|
expect(track?.firstElementChild?.className).toContain('px-0');
|
|
210
265
|
});
|
|
211
266
|
|
|
212
|
-
it('keeps IME
|
|
267
|
+
it('keeps a numbered IME candidate commit stable while streamed output rerenders the owner', async () => {
|
|
213
268
|
const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
|
|
214
269
|
render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
|
|
215
270
|
|
|
216
271
|
const textbox = screen.getByRole('textbox');
|
|
217
272
|
fireEvent.focus(textbox);
|
|
273
|
+
await insertText(textbox, 'n');
|
|
218
274
|
fireEvent.compositionStart(textbox);
|
|
275
|
+
const paragraph = setImeDomText(textbox, 'ni');
|
|
219
276
|
|
|
220
277
|
act(() => {
|
|
221
278
|
controlRef.current?.bumpStream();
|
|
222
279
|
controlRef.current?.bumpStream();
|
|
223
280
|
});
|
|
224
281
|
expect(screen.getByTestId('stream-chunk').textContent).toBe('2');
|
|
225
|
-
|
|
282
|
+
expect(textbox.querySelector('p')).toBe(paragraph);
|
|
283
|
+
expect(textbox.textContent).toBe('ni');
|
|
284
|
+
|
|
285
|
+
fireEvent.keyDown(textbox, { key: '1', isComposing: true });
|
|
286
|
+
setImeDomText(textbox, '你');
|
|
287
|
+
fireEvent.input(textbox, {
|
|
288
|
+
data: '你',
|
|
289
|
+
inputType: 'insertCompositionText',
|
|
290
|
+
isComposing: true,
|
|
291
|
+
});
|
|
226
292
|
fireEvent.compositionEnd(textbox, { data: '你' });
|
|
227
293
|
|
|
228
294
|
await waitFor(() => expect(textbox.textContent).toBe('你'));
|
|
295
|
+
expect(textbox.querySelector('p')).toBe(paragraph);
|
|
296
|
+
await waitFor(() => expect(window.getSelection()?.anchorOffset).toBe(1));
|
|
229
297
|
});
|
|
230
298
|
|
|
231
299
|
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,
|