@nextclaw/ui 0.14.1 → 0.14.3

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.
Files changed (224) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-BP_8IaxB.js} +7 -7
  3. package/dist/assets/channels-list-page-DEh5Jjqx.js +8 -0
  4. package/dist/assets/chat-page-BRB0N5WW.js +105 -0
  5. package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-DsivFJ4r.js} +2 -2
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-B0w9p2ep.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-CikkhOlq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-PVA_xL1D.js} +1 -1
  10. package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
  11. package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
  12. package/dist/assets/doc-browser-context-MTw4EvKd.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-y6vDtq4M.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-DVJIQf_G.js} +1 -1
  15. package/dist/assets/index-BiE-K8-q.css +1 -0
  16. package/dist/assets/index-D9BAE_L_.js +105 -0
  17. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-DotaTUHb.js +1 -0
  21. package/dist/assets/plus-BbZqquJe.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +1 -0
  23. package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-VNqT5Zp7.js} +1 -1
  25. package/dist/assets/remote-B8TiUfwW.js +1 -0
  26. package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
  27. package/dist/assets/runtime-config-page-S6ErRHR8.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-BOJ477H-.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BEti5beA.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-DGj5am03.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page--IzrIOid.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-hoj8dK4K.js} +2 -2
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-T8-h-die.js} +1 -1
  34. package/dist/assets/use-config-CjRV2hwL.js +1 -0
  35. package/dist/assets/x-D9PXs_xP.js +1 -0
  36. package/dist/index.html +70 -19
  37. package/index.html +55 -5
  38. package/package.json +9 -9
  39. package/src/app/components/app-presenter-provider.tsx +11 -2
  40. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  41. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  42. package/src/app/components/layout/app-layout.tsx +18 -4
  43. package/src/app/components/layout/header.tsx +3 -3
  44. package/src/app/components/layout/page-layout.tsx +2 -2
  45. package/src/app/components/layout/runtime-status-entry.tsx +7 -7
  46. package/src/app/components/layout/sidebar-items.tsx +194 -71
  47. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  48. package/src/app/components/layout/sidebar.tsx +340 -83
  49. package/src/app/managers/viewport-layout.manager.ts +10 -0
  50. package/src/app/stores/viewport-layout.store.ts +39 -3
  51. package/src/app/styles/design-system.css +388 -26
  52. package/src/app/test/vitest-setup.ts +31 -0
  53. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  54. package/src/features/agents/components/agents-page.tsx +10 -6
  55. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
  56. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  57. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  60. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  61. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +15 -16
  62. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
  63. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +19 -19
  64. package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
  65. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  66. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +18 -16
  68. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  69. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  70. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +93 -20
  71. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +93 -36
  72. package/src/features/chat/components/layout/chat-sidebar.tsx +60 -93
  73. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  74. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
  76. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +308 -0
  77. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +439 -0
  78. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +194 -0
  79. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  80. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +51 -20
  81. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  82. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  83. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  84. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  85. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  86. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  87. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  88. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  89. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  90. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  91. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  92. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  93. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  94. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  95. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  96. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  97. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  98. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  99. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
  100. package/src/features/chat/features/message/components/chat-message-list.container.tsx +99 -18
  101. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  102. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  103. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  104. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +7 -7
  105. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +6 -6
  106. package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
  107. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
  108. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  111. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  112. package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
  113. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  114. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  115. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  116. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  117. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  118. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  119. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  120. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
  121. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  122. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -9
  123. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  124. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
  125. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
  126. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
  127. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +5 -5
  128. package/src/features/chat/features/welcome/components/chat-welcome.tsx +6 -6
  129. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
  130. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
  131. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  132. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  133. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  134. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  135. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  136. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  137. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  138. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  139. package/src/features/chat/index.ts +0 -1
  140. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  141. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  142. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  143. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  144. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  145. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  146. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  147. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  148. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  149. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  150. package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
  151. package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
  152. package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
  153. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  154. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
  155. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
  156. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
  157. package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
  158. package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
  159. package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
  160. package/src/features/settings/components/config/provider-form.tsx +10 -10
  161. package/src/features/settings/components/config/provider-models-section.tsx +16 -16
  162. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  163. package/src/features/settings/pages/providers-config-page.tsx +17 -17
  164. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  165. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  166. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  167. package/src/shared/components/config-split-page.tsx +11 -11
  168. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
  169. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
  170. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
  171. package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
  172. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
  173. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  174. package/src/shared/components/ui/button.tsx +4 -4
  175. package/src/shared/components/ui/card.tsx +4 -4
  176. package/src/shared/components/ui/dialog.tsx +5 -5
  177. package/src/shared/components/ui/input.tsx +1 -1
  178. package/src/shared/components/ui/label.tsx +1 -1
  179. package/src/shared/components/ui/popover.tsx +1 -1
  180. package/src/shared/components/ui/select.tsx +3 -3
  181. package/src/shared/components/ui/switch.tsx +3 -3
  182. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  183. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  184. package/src/shared/components/ui/tabs.tsx +4 -4
  185. package/src/shared/components/ui/textarea.tsx +1 -1
  186. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  187. package/src/shared/lib/i18n/locales/en-US/core.json +9 -2
  188. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  189. package/src/shared/lib/i18n/locales/zh-CN/core.json +9 -2
  190. package/src/shared/lib/theme/index.ts +94 -52
  191. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  192. package/src/shared/lib/ui-document-title/index.ts +50 -21
  193. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  194. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  195. package/dist/assets/config-split-page-BCJhqdK9.js +0 -1
  196. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  197. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  198. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  199. package/dist/assets/index-7o1WdUbU.js +0 -103
  200. package/dist/assets/index-DQWmEWzk.css +0 -1
  201. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  202. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  203. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  204. package/dist/assets/plus-Bnt56oqG.js +0 -1
  205. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  206. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  207. package/dist/assets/remote-B0LMs8u7.js +0 -1
  208. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  209. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  210. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  211. package/dist/assets/x-CW2KO-Hz.js +0 -1
  212. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  213. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  214. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  215. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  216. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  217. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  218. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  219. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  220. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  221. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  222. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  223. package/src/features/chat/stores/chat-input.store.ts +0 -104
  224. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -0,0 +1,132 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PersistStorage, StorageValue } from "zustand/middleware";
4
+ import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
5
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
6
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
7
+ import type { NcpSessionSummaryView } from "@/shared/lib/api";
8
+
9
+ const mocks = vi.hoisted(() => ({
10
+ panelProps: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("@/features/chat/features/workspace/components/chat-session-workspace-panel", () => ({
14
+ ChatSessionWorkspacePanel: ({
15
+ sessionProjectRoot,
16
+ sessionWorkingDir,
17
+ }: {
18
+ sessionProjectRoot: string | null;
19
+ sessionWorkingDir: string | null;
20
+ }) => {
21
+ mocks.panelProps({ sessionProjectRoot, sessionWorkingDir });
22
+ return (
23
+ <div
24
+ data-testid="workspace-panel"
25
+ data-project-root={sessionProjectRoot ?? ""}
26
+ data-working-dir={sessionWorkingDir ?? ""}
27
+ />
28
+ );
29
+ },
30
+ }));
31
+
32
+ vi.mock("@/features/cron", () => ({
33
+ useCronJobs: () => ({ data: { jobs: [] } }),
34
+ }));
35
+
36
+ class MemoryPersistStorage implements PersistStorage<unknown> {
37
+ private readonly values = new Map<string, StorageValue<unknown>>();
38
+
39
+ getItem = (name: string) => this.values.get(name) ?? null;
40
+
41
+ setItem = (name: string, value: StorageValue<unknown>) => {
42
+ this.values.set(name, value);
43
+ };
44
+
45
+ removeItem = (name: string) => {
46
+ this.values.delete(name);
47
+ };
48
+ }
49
+
50
+ function createSummary(
51
+ overrides: Partial<NcpSessionSummaryView> & Pick<NcpSessionSummaryView, "sessionId">,
52
+ ): NcpSessionSummaryView {
53
+ return {
54
+ messageCount: 1,
55
+ status: "idle",
56
+ updatedAt: "2026-06-19T00:00:00.000Z",
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ describe("ChatConversationWorkspaceSection", () => {
62
+ beforeEach(() => {
63
+ mocks.panelProps.mockReset();
64
+ useChatThreadStore.persist.setOptions({
65
+ storage: new MemoryPersistStorage(),
66
+ });
67
+ useChatThreadStore.setState({
68
+ snapshot: {
69
+ ...useChatThreadStore.getState().snapshot,
70
+ workspacePanelParentKey: "session-1",
71
+ activeWorkspacePanelKind: "file",
72
+ workspaceFileTabs: [
73
+ {
74
+ key: "file-tab",
75
+ parentSessionKey: "session-1",
76
+ path: "docs/designs/2026-06-10-core-kernel-responsibility.design.md",
77
+ label: "core-kernel-responsibility",
78
+ viewMode: "preview",
79
+ },
80
+ ],
81
+ activeWorkspaceFileKey: "file-tab",
82
+ childSessionTabs: [],
83
+ activeChildSessionKey: null,
84
+ workspaceNavigationHistory: [],
85
+ workspaceNavigationHistoryIndex: 0,
86
+ sessionProjectRoot: null,
87
+ sessionWorkingDir: null,
88
+ },
89
+ });
90
+ useChatQueryStore.setState({
91
+ snapshot: {
92
+ sessionsQuery: {
93
+ data: {
94
+ sessions: [
95
+ createSummary({
96
+ sessionId: "session-1",
97
+ workingDir: "/Users/peiwang/Projects/nextbot",
98
+ metadata: {
99
+ project_root: "/Users/peiwang/Projects/nextbot",
100
+ },
101
+ }),
102
+ ],
103
+ total: 1,
104
+ },
105
+ } as never,
106
+ },
107
+ });
108
+ });
109
+
110
+ it("uses selected session workingDir as the workspace file preview base path", () => {
111
+ render(
112
+ <ChatConversationWorkspaceSection
113
+ layoutMode="desktop"
114
+ sessionKey="session-1"
115
+ />,
116
+ );
117
+
118
+ const panel = screen.getByTestId("workspace-panel");
119
+ expect(panel.getAttribute("data-project-root")).toBe(
120
+ "/Users/peiwang/Projects/nextbot",
121
+ );
122
+ expect(panel.getAttribute("data-working-dir")).toBe(
123
+ "/Users/peiwang/Projects/nextbot",
124
+ );
125
+ expect(mocks.panelProps).toHaveBeenCalledWith(
126
+ expect.objectContaining({
127
+ sessionProjectRoot: "/Users/peiwang/Projects/nextbot",
128
+ sessionWorkingDir: "/Users/peiwang/Projects/nextbot",
129
+ }),
130
+ );
131
+ });
132
+ });
@@ -1,34 +1,41 @@
1
1
  import { useRef, type ReactNode } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
+ import type { NcpMessage } from "@nextclaw/ncp";
3
4
  import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
4
- import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
5
5
 
6
6
  type ChatConversationContentProps = {
7
+ isAwaitingAssistantOutput: boolean;
8
+ isHistoryLoading: boolean;
9
+ isSending: boolean;
10
+ messages: readonly NcpMessage[];
11
+ sessionKey: string | null;
7
12
  showWelcome: boolean;
8
13
  welcomeSlot?: ReactNode;
9
14
  };
10
15
 
11
16
  export function ChatConversationContent({
17
+ isAwaitingAssistantOutput,
18
+ isHistoryLoading,
19
+ isSending,
20
+ messages,
21
+ sessionKey,
12
22
  showWelcome,
13
23
  welcomeSlot,
14
24
  }: ChatConversationContentProps) {
15
- const snapshot = useChatThreadStore((state) => state.snapshot);
16
25
  const threadRef = useRef<HTMLDivElement | null>(null);
17
26
  const hideEmptyHint =
18
- snapshot.isHistoryLoading &&
19
- snapshot.messages.length === 0 &&
20
- !snapshot.isSending &&
21
- !snapshot.isAwaitingAssistantOutput;
27
+ isHistoryLoading &&
28
+ messages.length === 0 &&
29
+ !isSending &&
30
+ !isAwaitingAssistantOutput;
22
31
  const { onScroll } = useStickyBottomScroll({
23
32
  scrollRef: threadRef,
24
- resetKey: snapshot.sessionKey,
25
- isLoading: snapshot.isHistoryLoading,
26
- hasContent: snapshot.messages.length > 0,
27
- contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
33
+ resetKey: sessionKey,
34
+ isLoading: isHistoryLoading,
35
+ hasContent: messages.length > 0,
36
+ contentVersion: messages[messages.length - 1] ?? null,
28
37
  });
29
- const hasMessages = snapshot.messages.length > 0;
30
- const isAwaitingAssistantOutput =
31
- hasMessages && snapshot.isSending && snapshot.isAwaitingAssistantOutput;
38
+ const hasMessages = messages.length > 0;
32
39
 
33
40
  return (
34
41
  <div
@@ -42,8 +49,8 @@ export function ChatConversationContent({
42
49
  ) : hideEmptyHint || !hasMessages ? null : (
43
50
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
44
51
  <ChatMessageListContainer
45
- messages={snapshot.messages}
46
- isSending={isAwaitingAssistantOutput}
52
+ messages={messages}
53
+ isSending={hasMessages && isSending && isAwaitingAssistantOutput}
47
54
  />
48
55
  </div>
49
56
  )}
@@ -9,10 +9,10 @@ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/us
9
9
  import { sessionDisplayName } from "@/features/chat/features/session/utils/chat-session-display.utils";
10
10
  import {
11
11
  buildSessionTypeOptions,
12
+ DEFAULT_SESSION_TYPE,
12
13
  normalizeSessionType,
13
14
  resolveSessionTypeLabel,
14
15
  } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
15
- import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
16
16
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
17
17
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
18
18
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
@@ -34,9 +34,8 @@ export function ChatConversationHeaderSection({
34
34
  const sessionKey = useChatSessionListStore(
35
35
  (state) => state.snapshot.selectedSessionKey,
36
36
  );
37
- const effectiveSessionKey = sessionKey ?? snapshot.sessionKey;
38
- const pendingSessionType = useChatInputStore(
39
- (state) => state.snapshot.pendingSessionType,
37
+ const selectedAgentId = useChatSessionListStore(
38
+ (state) => state.snapshot.selectedAgentId,
40
39
  );
41
40
  const sessionTypesData = useChatQueryStore(
42
41
  (state) => state.snapshot.sessionTypesQuery?.data ?? null,
@@ -47,7 +46,7 @@ export function ChatConversationHeaderSection({
47
46
  [sessionTypesData?.options],
48
47
  );
49
48
  const sessionType = normalizeSessionType(
50
- selectedSession?.sessionType ?? pendingSessionType,
49
+ selectedSession?.sessionType ?? sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE,
51
50
  );
52
51
  const sessionTypeOption =
53
52
  sessionTypeOptions.find((option) => option.value === sessionType) ?? null;
@@ -58,8 +57,8 @@ export function ChatConversationHeaderSection({
58
57
  selectedSession?.projectName ?? getSessionProjectName(sessionProjectRoot);
59
58
  const canDeleteSession = Boolean(selectedSession);
60
59
  const { childSessionTabs, sessionCronJobs } =
61
- useChatConversationWorkspaceState(snapshot);
62
- const shouldShowSessionHeader = Boolean(effectiveSessionKey || sessionTypeLabel);
60
+ useChatConversationWorkspaceState(snapshot, sessionKey);
61
+ const shouldShowSessionHeader = Boolean(sessionKey || sessionTypeLabel);
63
62
  const sessionHeaderTitle =
64
63
  (selectedSession ? sessionDisplayName(selectedSession) : undefined) ||
65
64
  (canDeleteSession && sessionKey
@@ -67,23 +66,23 @@ export function ChatConversationHeaderSection({
67
66
  : null) ||
68
67
  t("chatSidebarNewTask");
69
68
  const normalizedAgentId =
70
- selectedSession?.agentId?.trim() ?? snapshot.agentId?.trim() ?? "";
69
+ selectedSession?.agentId?.trim() ?? selectedAgentId?.trim() ?? "";
71
70
  const shouldShowHeaderAgentAvatar =
72
71
  normalizedAgentId.length > 0 && normalizedAgentId.toLowerCase() !== "main";
73
72
  const openChildSessions = () => {
74
- if (!effectiveSessionKey) {
73
+ if (!sessionKey) {
75
74
  return;
76
75
  }
77
76
  presenter.chatThreadManager.openChildSessionPanel({
78
- parentSessionKey: effectiveSessionKey,
77
+ parentSessionKey: sessionKey,
79
78
  activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
80
79
  });
81
80
  };
82
81
  const openSessionCronJobs = () => {
83
- if (!effectiveSessionKey || sessionCronJobs.length === 0) {
82
+ if (!sessionKey || sessionCronJobs.length === 0) {
84
83
  return;
85
84
  }
86
- presenter.chatThreadManager.openSessionCronPanel(effectiveSessionKey);
85
+ presenter.chatThreadManager.openSessionCronPanel(sessionKey);
87
86
  };
88
87
 
89
88
  return (
@@ -104,7 +103,7 @@ export function ChatConversationHeaderSection({
104
103
  }
105
104
  sessionTypeBadge={
106
105
  sessionTypeLabel ? (
107
- <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
106
+ <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
108
107
  {sessionTypeOption?.icon?.src ? (
109
108
  <span className="inline-flex h-[1.125rem] w-[1.125rem] items-center justify-center">
110
109
  <SessionContextIconNode
@@ -124,7 +123,7 @@ export function ChatConversationHeaderSection({
124
123
  projectBadge={
125
124
  sessionProjectName ? (
126
125
  <ChatSessionProjectBadge
127
- sessionKey={effectiveSessionKey ?? "draft"}
126
+ sessionKey={sessionKey ?? "draft"}
128
127
  projectName={sessionProjectName}
129
128
  projectRoot={sessionProjectRoot}
130
129
  persistToServer={canDeleteSession}
@@ -132,9 +131,9 @@ export function ChatConversationHeaderSection({
132
131
  ) : null
133
132
  }
134
133
  actions={
135
- effectiveSessionKey ? (
134
+ sessionKey ? (
136
135
  <ChatSessionHeaderActions
137
- sessionKey={effectiveSessionKey}
136
+ sessionKey={sessionKey}
138
137
  canDeleteSession={canDeleteSession}
139
138
  isDeletePending={snapshot.isDeletePending}
140
139
  projectRoot={sessionProjectRoot}
@@ -16,11 +16,11 @@ export function ChatParentSessionBanner({
16
16
  }
17
17
  const trimmedLabel = parentSessionLabel.trim();
18
18
  return (
19
- <div className="border-b border-gray-200/60 bg-white/75 px-4 py-2 backdrop-blur-sm sm:px-5">
19
+ <div className="bg-background/75 px-4 py-2 backdrop-blur-sm sm:px-5">
20
20
  <button
21
21
  type="button"
22
22
  onClick={onGoToParentSession}
23
- className="inline-flex items-center gap-2 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900"
23
+ className="inline-flex items-center gap-2 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
24
24
  >
25
25
  <ArrowLeft className="h-3.5 w-3.5" />
26
26
  <span>
@@ -55,10 +55,11 @@ export function ChatConversationHeader({
55
55
 
56
56
  return (
57
57
  <div
58
+ data-testid="chat-conversation-header"
58
59
  className={cn(
59
- "border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-colors duration-200",
60
+ "bg-background/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-colors duration-200",
60
61
  isMobileLayout ? "px-3 sm:px-3" : "px-4 sm:px-5",
61
- shouldShow ? "opacity-100" : "h-0 py-0 opacity-0 border-b-0",
62
+ shouldShow ? "opacity-100" : "h-0 py-0 opacity-0",
62
63
  shouldShow && (isMobileLayout ? "min-h-12 pb-2 pt-2" : "h-[52px]"),
63
64
  )}
64
65
  style={
@@ -73,11 +74,11 @@ export function ChatConversationHeader({
73
74
  icon={<ArrowLeft className="h-4 w-4" />}
74
75
  label={t("chat")}
75
76
  onClick={onBackToList}
76
- className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
77
+ className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
77
78
  />
78
79
  ) : null}
79
80
  {leading}
80
- <span className="text-sm font-medium text-gray-700 truncate">
81
+ <span className="text-sm font-medium text-foreground truncate">
81
82
  {title}
82
83
  </span>
83
84
  {sessionTypeBadge}
@@ -1,14 +1,12 @@
1
- import { ChatInputBarContainer } from "@/features/chat/features/input/components/chat-input-bar.container";
2
- import { ChatConversationAlerts } from "@/features/chat/components/conversation/chat-conversation-alerts";
3
- import { ChatConversationContent } from "@/features/chat/components/conversation/chat-conversation-content";
1
+ import { useCallback } from "react";
4
2
  import { ChatConversationHeaderSection } from "@/features/chat/components/conversation/chat-conversation-header-section";
5
3
  import { ChatConversationParentBanner } from "@/features/chat/components/conversation/chat-conversation-parent-banner";
6
4
  import { ChatConversationSkeleton } from "@/features/chat/components/conversation/chat-conversation-skeleton";
7
5
  import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
6
+ import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
7
+ import { SessionConversationArea } from "@/features/chat/features/conversation/components/session-conversation-area";
8
8
  import { useNcpChatProviderStateResolved } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
9
- import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
10
- import { shouldShowChatWelcome } from "@/features/chat/features/welcome/utils/chat-welcome-visibility.utils";
11
- import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
9
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
12
10
 
13
11
  type ChatConversationLayoutMode = "desktop" | "mobile";
14
12
 
@@ -19,36 +17,38 @@ export function ChatConversationPanel({
19
17
  layoutMode?: ChatConversationLayoutMode;
20
18
  onBackToList?: () => void;
21
19
  }) {
20
+ const presenter = usePresenter();
22
21
  const isProviderStateResolved = useNcpChatProviderStateResolved();
23
- const showWelcome = useChatThreadStore((state) =>
24
- shouldShowChatWelcome(state.snapshot),
22
+ const sessionKey = useChatSessionListStore(
23
+ (state) => state.snapshot.selectedSessionKey,
25
24
  );
25
+ const handleSessionMaterialized = useCallback((materializedSessionKey: string) => {
26
+ if (!presenter.chatUiManager.isAtChatRoot()) {
27
+ return;
28
+ }
29
+ presenter.chatUiManager.goToSession(materializedSessionKey, { replace: true });
30
+ }, [presenter]);
26
31
 
27
32
  if (!isProviderStateResolved) {
28
33
  return <ChatConversationSkeleton />;
29
34
  }
30
35
 
31
36
  return (
32
- <section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
37
+ <section className="flex-1 min-h-0 flex overflow-hidden bg-background">
33
38
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
34
39
  <ChatConversationParentBanner />
35
40
  <ChatConversationHeaderSection
36
41
  layoutMode={layoutMode}
37
42
  onBackToList={onBackToList}
38
43
  />
39
- <ChatConversationAlerts />
40
- <ChatConversationContent
41
- showWelcome={showWelcome}
42
- welcomeSlot={
43
- <ChatConversationWelcome
44
- inputSlot={<ChatInputBarContainer surface="embedded" />}
45
- />
46
- }
44
+ <SessionConversationArea
45
+ consumeDraftIntent
46
+ sessionKey={sessionKey}
47
+ onSessionMaterialized={handleSessionMaterialized}
47
48
  />
48
- {showWelcome ? null : <ChatInputBarContainer />}
49
49
  </div>
50
50
 
51
- <ChatConversationWorkspaceSection layoutMode={layoutMode} />
51
+ <ChatConversationWorkspaceSection layoutMode={layoutMode} sessionKey={sessionKey} />
52
52
  </section>
53
53
  );
54
54
  }
@@ -28,13 +28,13 @@ export function ChatConversationSkeleton() {
28
28
  return (
29
29
  <section
30
30
  data-testid="chat-conversation-skeleton"
31
- className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white"
31
+ className="flex-1 min-h-0 flex flex-col overflow-hidden bg-background"
32
32
  >
33
33
  <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
34
34
  <div className="mx-auto flex min-h-full w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
35
35
  <div className="flex flex-1 flex-col gap-8">
36
36
  <div className="space-y-6">
37
- <Skeleton className="h-6 w-52 rounded-lg bg-gray-200/90" />
37
+ <Skeleton className="h-6 w-52 rounded-lg bg-muted" />
38
38
  <div className="space-y-5">
39
39
  {CHAT_CONVERSATION_SKELETON_BUBBLES.map((bubble) => (
40
40
  <div
@@ -44,7 +44,7 @@ export function ChatConversationSkeleton() {
44
44
  <Skeleton
45
45
  data-testid="chat-conversation-skeleton-bubble"
46
46
  className={cn(
47
- "w-full bg-gray-200/80",
47
+ "w-full bg-muted",
48
48
  bubble.bubbleClassName,
49
49
  )}
50
50
  />
@@ -54,29 +54,29 @@ export function ChatConversationSkeleton() {
54
54
  </div>
55
55
  <div className="mt-auto grid gap-4 pb-2 sm:grid-cols-[minmax(0,1fr)_minmax(180px,240px)] sm:items-end">
56
56
  <div className="space-y-3">
57
- <Skeleton className="h-4 w-40 rounded-full bg-gray-200/70" />
58
- <Skeleton className="h-[112px] w-full rounded-[30px] bg-gray-200/70" />
57
+ <Skeleton className="h-4 w-40 rounded-full bg-muted" />
58
+ <Skeleton className="h-[112px] w-full rounded-[30px] bg-muted" />
59
59
  </div>
60
60
  <div className="hidden justify-end sm:flex">
61
- <Skeleton className="h-10 w-36 rounded-full bg-gray-200/75" />
61
+ <Skeleton className="h-10 w-36 rounded-full bg-muted" />
62
62
  </div>
63
63
  </div>
64
64
  </div>
65
65
  </div>
66
66
  </div>
67
- <div className="border-t border-gray-200/80 bg-white p-4">
67
+ <div className="bg-background p-4">
68
68
  <div className="mx-auto w-full max-w-[min(1120px,100%)]">
69
- <div className="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-card">
69
+ <div className="overflow-hidden rounded-2xl border border-border bg-card shadow-card">
70
70
  <div className="px-4 py-2.5">
71
- <Skeleton className="h-[84px] w-full rounded-[28px] bg-gray-200/80" />
71
+ <Skeleton className="h-[84px] w-full rounded-[28px] bg-muted" />
72
72
  </div>
73
73
  <div className="flex items-center justify-between gap-3 px-3 pb-3">
74
74
  <div className="flex items-center gap-2">
75
- <Skeleton className="h-8 w-20 rounded-full bg-gray-200/75" />
76
- <Skeleton className="h-8 w-28 rounded-full bg-gray-200/75" />
77
- <Skeleton className="hidden h-8 w-24 rounded-full bg-gray-200/70 sm:block" />
75
+ <Skeleton className="h-8 w-20 rounded-full bg-muted" />
76
+ <Skeleton className="h-8 w-28 rounded-full bg-muted" />
77
+ <Skeleton className="hidden h-8 w-24 rounded-full bg-muted sm:block" />
78
78
  </div>
79
- <Skeleton className="h-8 w-8 rounded-full bg-gray-200/85" />
79
+ <Skeleton className="h-8 w-8 rounded-full bg-muted" />
80
80
  </div>
81
81
  </div>
82
82
  </div>
@@ -1,21 +1,26 @@
1
1
  import { ChatSessionWorkspacePanel } from "@/features/chat/features/workspace/components/chat-session-workspace-panel";
2
+ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
2
3
  import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
3
4
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
4
5
 
5
6
  type ChatConversationWorkspaceSectionProps = {
6
7
  layoutMode: "desktop" | "mobile";
8
+ sessionKey: string | null;
7
9
  };
8
10
 
9
11
  export function ChatConversationWorkspaceSection({
10
12
  layoutMode,
13
+ sessionKey,
11
14
  }: ChatConversationWorkspaceSectionProps) {
12
15
  const snapshot = useChatThreadStore((state) => state.snapshot);
16
+ const selectedSession = useNcpChatSelectedSession(sessionKey);
13
17
  const {
14
18
  childSessionTabs,
19
+ activeSideChatDraft,
15
20
  workspaceFileTabs,
16
21
  sessionCronJobs,
17
22
  showWorkspacePanel,
18
- } = useChatConversationWorkspaceState(snapshot);
23
+ } = useChatConversationWorkspaceState(snapshot, sessionKey);
19
24
 
20
25
  if (!showWorkspacePanel) {
21
26
  return null;
@@ -23,18 +28,19 @@ export function ChatConversationWorkspaceSection({
23
28
 
24
29
  return (
25
30
  <ChatSessionWorkspacePanel
26
- sessionKey={snapshot.sessionKey}
31
+ sessionKey={sessionKey}
27
32
  childSessionTabs={childSessionTabs}
28
33
  activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
34
+ activeSideChatDraft={activeSideChatDraft}
29
35
  workspaceFileTabs={workspaceFileTabs}
30
36
  activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
31
37
  workspaceNavigationHistory={snapshot.workspaceNavigationHistory}
32
38
  workspaceNavigationHistoryIndex={snapshot.workspaceNavigationHistoryIndex}
33
39
  activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
34
40
  sessionCronJobs={sessionCronJobs}
35
- sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
41
+ sessionProjectRoot={selectedSession?.projectRoot ?? null}
36
42
  sessionWorkingDir={
37
- snapshot.sessionWorkingDir ?? snapshot.sessionProjectRoot ?? null
43
+ selectedSession?.workingDir ?? selectedSession?.projectRoot ?? null
38
44
  }
39
45
  displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
40
46
  />
@@ -1,10 +1,12 @@
1
- import { render, waitFor } from "@testing-library/react";
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
3
  import { MemoryRouter } from "react-router-dom";
3
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
5
  import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
5
6
  import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
6
7
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
7
8
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
8
10
 
9
11
  const mocks = vi.hoisted(() => ({
10
12
  sessionItems: [] as NcpSessionListItemView[],
@@ -107,6 +109,8 @@ function createRunningSessionItem(
107
109
  }
108
110
 
109
111
  function resetReadStateTestState() {
112
+ window.localStorage.clear();
113
+ viewportLayoutManager.resetForTests();
110
114
  mocks.sessionItems = [];
111
115
  useChatQueryStore.setState({
112
116
  snapshot: {
@@ -130,16 +134,36 @@ function resetReadStateTestState() {
130
134
  });
131
135
  }
132
136
 
137
+ function createTestQueryClient() {
138
+ return new QueryClient({
139
+ defaultOptions: {
140
+ queries: { retry: false },
141
+ mutations: { retry: false },
142
+ },
143
+ });
144
+ }
145
+
146
+ function createReadStateSidebarElement(queryClient: QueryClient) {
147
+ return (
148
+ <QueryClientProvider client={queryClient}>
149
+ <MemoryRouter>
150
+ <ChatSidebar />
151
+ </MemoryRouter>
152
+ </QueryClientProvider>
153
+ );
154
+ }
155
+
156
+ function renderReadStateSidebar(queryClient: QueryClient) {
157
+ return render(createReadStateSidebarElement(queryClient));
158
+ }
159
+
133
160
  describe("ChatSidebar read state sync", () => {
134
161
  beforeEach(resetReadStateTestState);
135
162
 
136
163
  it("waits until the active running session is idle before persisting the read watermark", async () => {
137
164
  mocks.sessionItems = [createRunningSessionItem("running")];
138
- const { rerender } = render(
139
- <MemoryRouter>
140
- <ChatSidebar />
141
- </MemoryRouter>,
142
- );
165
+ const queryClient = createTestQueryClient();
166
+ const { rerender } = renderReadStateSidebar(queryClient);
143
167
 
144
168
  await waitFor(() => {
145
169
  expect(
@@ -150,11 +174,7 @@ describe("ChatSidebar read state sync", () => {
150
174
  });
151
175
 
152
176
  mocks.sessionItems = [createRunningSessionItem()];
153
- rerender(
154
- <MemoryRouter>
155
- <ChatSidebar />
156
- </MemoryRouter>,
157
- );
177
+ rerender(createReadStateSidebarElement(queryClient));
158
178
 
159
179
  await waitFor(() => {
160
180
  expect(
@@ -164,4 +184,25 @@ describe("ChatSidebar read state sync", () => {
164
184
  ).toBe("2026-03-19T09:05:00.000Z");
165
185
  });
166
186
  });
187
+
188
+ it("collapses the desktop sidebar into accessible icon actions", () => {
189
+ const { container } = renderReadStateSidebar(createTestQueryClient());
190
+
191
+ fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
192
+
193
+ const persistedState = JSON.parse(
194
+ window.localStorage.getItem("nextclaw.app.viewport-layout") ?? "{}",
195
+ ) as { state?: { isSidebarCollapsed?: boolean } };
196
+
197
+ expect(
198
+ container.querySelector("aside")?.getAttribute("data-sidebar-collapsed"),
199
+ ).toBe("true");
200
+ expect(persistedState.state?.isSidebarCollapsed).toBe(true);
201
+ expect(screen.getByRole("button", { name: "New Task" }).textContent).toBe(
202
+ "",
203
+ );
204
+ expect(screen.queryByPlaceholderText("Search conversations...")).toBeNull();
205
+ expect(screen.getByRole("link", { name: "Scheduled Tasks" })).toBeTruthy();
206
+ expect(screen.getByRole("button", { name: "Settings menu" })).toBeTruthy();
207
+ });
167
208
  });