@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,14 +28,14 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/client-sdk": "0.4.4",
32
- "@nextclaw/ncp-http-agent-client": "0.3.46",
33
- "@nextclaw/ncp-react": "0.4.54",
34
- "@nextclaw/ncp-toolkit": "0.5.39",
35
- "@nextclaw/shared": "0.3.1",
36
- "@nextclaw/agent-chat": "0.2.14",
37
- "@nextclaw/agent-chat-ui": "0.5.1",
38
- "@nextclaw/ncp": "0.6.4"
31
+ "@nextclaw/agent-chat-ui": "0.5.3",
32
+ "@nextclaw/client-sdk": "0.4.6",
33
+ "@nextclaw/ncp": "0.6.5",
34
+ "@nextclaw/ncp-toolkit": "0.5.40",
35
+ "@nextclaw/shared": "0.3.2",
36
+ "@nextclaw/ncp-http-agent-client": "0.3.47",
37
+ "@nextclaw/agent-chat": "0.2.15",
38
+ "@nextclaw/ncp-react": "0.4.55"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@testing-library/react": "^16.3.0",
@@ -1,7 +1,16 @@
1
- import { createContext, useContext, useMemo, type ReactNode } from 'react';
1
+ import { createContext, useContext, useMemo, type Context, type ReactNode } from 'react';
2
2
  import { getAppPresenter, type AppPresenter } from '@/app/presenters/app.presenter';
3
3
 
4
- const AppPresenterContext = createContext<AppPresenter | null>(null);
4
+ type AppPresenterContextGlobal = typeof globalThis & {
5
+ __NEXTCLAW_APP_PRESENTER_CONTEXT__?: Context<AppPresenter | null>;
6
+ };
7
+
8
+ const appPresenterContextGlobal = globalThis as AppPresenterContextGlobal;
9
+ const AppPresenterContext =
10
+ appPresenterContextGlobal.__NEXTCLAW_APP_PRESENTER_CONTEXT__ ??
11
+ createContext<AppPresenter | null>(null);
12
+
13
+ appPresenterContextGlobal.__NEXTCLAW_APP_PRESENTER_CONTEXT__ = AppPresenterContext;
5
14
 
6
15
  type AppPresenterProviderProps = {
7
16
  children: ReactNode;
@@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes } from "react-router-dom";
5
5
  import { beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { AppLayout } from "@/app/components/layout/app-layout";
7
7
  import { I18nProvider } from "@/app/components/i18n-provider";
8
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
8
9
 
9
10
  const { useViewportLayoutMock } = vi.hoisted(() => ({
10
11
  useViewportLayoutMock: vi.fn(() => ({
@@ -44,6 +45,8 @@ vi.mock("@/platforms/mobile", () => ({
44
45
 
45
46
  describe("AppLayout", () => {
46
47
  beforeEach(() => {
48
+ window.localStorage.clear();
49
+ viewportLayoutManager.resetForTests();
47
50
  useViewportLayoutMock.mockReturnValue({
48
51
  mode: "desktop",
49
52
  isMobile: false,
@@ -171,7 +174,9 @@ describe("AppLayout", () => {
171
174
 
172
175
  expect(screen.getByTestId("chat-content")).toBeTruthy();
173
176
  expect(screen.getByTestId("desktop-window-chrome")).toBeTruthy();
174
- expect(screen.getByTestId("desktop-window-chrome-sidebar").className).toContain("desktop-window-drag");
177
+ expect(
178
+ screen.getByTestId("desktop-window-chrome-sidebar").className,
179
+ ).toContain("desktop-window-drag");
175
180
  expect(screen.getByTestId("mobile-bottom-nav")).toBeTruthy();
176
181
  });
177
182
 
@@ -203,7 +208,11 @@ describe("AppLayout", () => {
203
208
  );
204
209
 
205
210
  expect(screen.getByTestId("chat-content")).toBeTruthy();
206
- expect(screen.getByTestId("mobile-app-shell").getAttribute("data-topbar-leading-inset")).toBe("4.75rem");
211
+ expect(
212
+ screen
213
+ .getByTestId("mobile-app-shell")
214
+ .getAttribute("data-topbar-leading-inset"),
215
+ ).toBe("4.75rem");
207
216
  expect(screen.queryByTestId("desktop-window-chrome")).toBeNull();
208
217
  });
209
218
  });
@@ -1,8 +1,10 @@
1
- import { render, screen, within } from '@testing-library/react';
2
- import { MemoryRouter } from 'react-router-dom';
3
- import { describe, expect, it, vi } from 'vitest';
4
- import { Sidebar } from '@/app/components/layout/sidebar';
5
- import type * as RemoteFeature from '@/features/remote';
1
+ import { render, screen, within } from "@testing-library/react";
2
+ import { fireEvent } from "@testing-library/react";
3
+ import { MemoryRouter } from "react-router-dom";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { Sidebar } from "@/app/components/layout/sidebar";
6
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
7
+ import type * as RemoteFeature from "@/features/remote";
6
8
 
7
9
  const mocks = vi.hoisted(() => ({
8
10
  openAccountPanel: vi.fn(),
@@ -11,133 +13,162 @@ const mocks = vi.hoisted(() => ({
11
13
  data: {
12
14
  account: {
13
15
  loggedIn: true,
14
- email: 'user@example.com'
15
- }
16
- }
17
- }
16
+ email: "user@example.com",
17
+ },
18
+ },
19
+ },
18
20
  }));
19
21
 
20
- vi.mock('@/shared/components/doc-browser', () => ({
22
+ vi.mock("@/shared/components/doc-browser", () => ({
21
23
  useDocBrowser: () => ({
22
- open: mocks.docOpen
23
- })
24
+ open: mocks.docOpen,
25
+ }),
24
26
  }));
25
27
 
26
- vi.mock('@/app/components/app-presenter-provider', () => ({
28
+ vi.mock("@/app/components/app-presenter-provider", () => ({
27
29
  useAppPresenter: () => ({
28
30
  accountManager: {
29
- openAccountPanel: mocks.openAccountPanel
30
- }
31
- })
31
+ openAccountPanel: mocks.openAccountPanel,
32
+ },
33
+ }),
32
34
  }));
33
35
 
34
- vi.mock('@/features/remote', async () => {
35
- const actual = await vi.importActual<typeof RemoteFeature>('@/features/remote');
36
+ vi.mock("@/features/remote", async () => {
37
+ const actual =
38
+ await vi.importActual<typeof RemoteFeature>("@/features/remote");
36
39
  return {
37
40
  ...actual,
38
- useRemoteStatus: () => mocks.remoteStatus
41
+ useRemoteStatus: () => mocks.remoteStatus,
39
42
  };
40
43
  });
41
44
 
42
- vi.mock('@/app/components/i18n-provider', () => ({
45
+ vi.mock("@/app/components/i18n-provider", () => ({
43
46
  useI18n: () => ({
44
- language: 'en',
45
- setLanguage: vi.fn()
46
- })
47
+ language: "en",
48
+ setLanguage: vi.fn(),
49
+ }),
47
50
  }));
48
51
 
49
- vi.mock('@/app/components/theme-provider', () => ({
52
+ vi.mock("@/app/components/theme-provider", () => ({
50
53
  useTheme: () => ({
51
- theme: 'warm',
52
- setTheme: vi.fn()
53
- })
54
+ theme: "warm",
55
+ setTheme: vi.fn(),
56
+ }),
54
57
  }));
55
58
 
56
- describe('Sidebar', () => {
57
- it('keeps the settings sidebar bounded and lets the navigation scroll independently', () => {
59
+ describe("Sidebar", () => {
60
+ beforeEach(() => {
61
+ window.localStorage.clear();
62
+ viewportLayoutManager.resetForTests();
63
+ });
64
+
65
+ it("keeps the settings sidebar bounded and lets the navigation scroll independently", () => {
58
66
  const { container } = render(
59
- <MemoryRouter initialEntries={['/model']}>
67
+ <MemoryRouter initialEntries={["/model"]}>
60
68
  <Sidebar mode="settings" />
61
- </MemoryRouter>
69
+ </MemoryRouter>,
62
70
  );
63
71
 
64
- const aside = container.querySelector('aside');
65
- const nav = container.querySelector('nav');
66
-
67
- expect(aside?.className).toContain('min-h-0');
68
- expect(aside?.className).toContain('overflow-hidden');
69
- expect(screen.getByTestId('settings-sidebar-header').parentElement?.className).toContain('py-2.5');
70
- expect(aside?.className).not.toContain('py-6');
71
- expect(nav?.className).toContain('flex-1');
72
- expect(nav?.className).toContain('min-h-0');
73
- expect(nav?.className).toContain('overflow-y-auto');
74
- expect(screen.getByRole('link', { current: 'page' }).className).not.toContain('font-semibold');
72
+ const aside = container.querySelector("aside");
73
+ const nav = container.querySelector("nav");
74
+
75
+ expect(aside?.className).toContain("min-h-0");
76
+ expect(aside?.className).toContain("overflow-hidden");
77
+ expect(
78
+ screen.getByTestId("settings-sidebar-header").parentElement?.className,
79
+ ).toMatch(/(?:^|\s)py-2(?:\s|$)/);
80
+ expect(aside?.className).not.toContain("py-6");
81
+ expect(nav?.className).toContain("flex-1");
82
+ expect(nav?.className).toContain("min-h-0");
83
+ expect(nav?.className).toContain("overflow-y-auto");
84
+ expect(
85
+ screen.getByRole("link", { current: "page" }).className,
86
+ ).not.toContain("font-semibold");
75
87
  });
76
88
 
77
- it('keeps the original compact single-row header in settings mode', () => {
89
+ it("keeps the original compact single-row header in settings mode", () => {
78
90
  render(
79
- <MemoryRouter initialEntries={['/model']}>
91
+ <MemoryRouter initialEntries={["/model"]}>
80
92
  <Sidebar mode="settings" />
81
- </MemoryRouter>
93
+ </MemoryRouter>,
82
94
  );
83
95
 
84
- const header = screen.getByTestId('settings-sidebar-header');
85
- const backLink = screen.getByRole('link', { name: 'Back to Main' });
96
+ const header = screen.getByTestId("settings-sidebar-header");
97
+ const backLink = screen.getByRole("link", { name: "Back to Main" });
86
98
 
87
99
  expect(header).toBeTruthy();
88
- expect(screen.getByRole('heading', { name: 'Settings' })).toBeTruthy();
100
+ expect(screen.getByRole("heading", { name: "Settings" })).toBeTruthy();
89
101
  expect(backLink).toBeTruthy();
90
- expect(header.className).not.toContain('bg-white');
91
- expect(header.className).not.toContain('rounded-2xl');
92
- expect(backLink.className).toContain('hover:bg-gray-200/60');
102
+ expect(header.className).not.toContain("bg-white");
103
+ expect(header.className).not.toContain("rounded-2xl");
104
+ expect(backLink.className).toContain("hover:bg-accent");
93
105
  });
94
106
 
95
- it('keeps the settings navigation in the expected product order', () => {
107
+ it("keeps the settings navigation in the expected product order", () => {
96
108
  const { container } = render(
97
- <MemoryRouter initialEntries={['/model']}>
109
+ <MemoryRouter initialEntries={["/model"]}>
98
110
  <Sidebar mode="settings" />
99
- </MemoryRouter>
111
+ </MemoryRouter>,
100
112
  );
101
113
 
102
- const nav = container.querySelector('nav');
114
+ const nav = container.querySelector("nav");
103
115
  if (!(nav instanceof HTMLElement)) {
104
- throw new Error('settings nav not found');
116
+ throw new Error("settings nav not found");
105
117
  }
106
118
 
107
119
  const linkTexts = within(nav)
108
- .getAllByRole('link')
109
- .map((link) => link.textContent?.trim() || '');
120
+ .getAllByRole("link")
121
+ .map((link) => link.textContent?.trim() || "");
110
122
 
111
123
  expect(linkTexts).toEqual([
112
- 'Model',
113
- 'Providers',
114
- 'Channels',
115
- 'Search Channels',
116
- 'Routing & Runtime',
117
- 'Updates',
118
- 'Remote Access',
119
- 'Security',
120
- 'Secrets',
121
- 'MCP'
124
+ "Model",
125
+ "Providers",
126
+ "Channels",
127
+ "Search Channels",
128
+ "Routing & Runtime",
129
+ "Updates",
130
+ "Remote Access",
131
+ "Security",
132
+ "Secrets",
133
+ "MCP",
122
134
  ]);
123
135
  });
124
136
 
125
- it('keeps the footer utilities compact without changing the top header structure', () => {
137
+ it("keeps the footer utilities compact without changing the top header structure", () => {
126
138
  render(
127
- <MemoryRouter initialEntries={['/model']}>
139
+ <MemoryRouter initialEntries={["/model"]}>
128
140
  <Sidebar mode="settings" />
129
- </MemoryRouter>
141
+ </MemoryRouter>,
130
142
  );
131
143
 
132
- const accountEntry = screen.getByTestId('settings-sidebar-account-entry');
133
- const accountStatus = screen.getByTestId('settings-sidebar-account-status');
144
+ const accountEntry = screen.getByTestId("settings-sidebar-account-entry");
145
+ const accountStatus = screen.getByTestId("settings-sidebar-account-status");
134
146
 
135
147
  expect(accountEntry).toBeTruthy();
136
- expect(accountEntry.textContent).toContain('Account');
137
- expect(screen.getByText('user@example.com')).toBeTruthy();
138
- expect(accountEntry.className).toContain('py-2');
139
- expect(accountEntry.className).toContain('text-gray-600');
140
- expect(accountEntry.className).toContain('hover:bg-gray-200/60');
141
- expect(accountStatus.className).toContain('text-[11px]');
148
+ expect(accountEntry.textContent).toContain("Account");
149
+ expect(screen.getByText("user@example.com")).toBeTruthy();
150
+ expect(accountEntry.className).toContain("py-2");
151
+ expect(accountEntry.className).toContain("text-muted-foreground");
152
+ expect(accountEntry.className).toContain("hover:bg-accent");
153
+ expect(accountStatus.className).toContain("text-[11px]");
154
+ });
155
+
156
+ it("persists the shared sidebar collapsed state through the viewport layout store", () => {
157
+ const { container } = render(
158
+ <MemoryRouter initialEntries={["/model"]}>
159
+ <Sidebar mode="settings" />
160
+ </MemoryRouter>,
161
+ );
162
+
163
+ fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
164
+
165
+ const aside = container.querySelector("aside");
166
+ const persistedState = JSON.parse(
167
+ window.localStorage.getItem("nextclaw.app.viewport-layout") ?? "{}",
168
+ ) as { state?: { isSidebarCollapsed?: boolean } };
169
+
170
+ expect(aside?.getAttribute("data-sidebar-collapsed")).toBe("true");
171
+ expect(persistedState.state?.isSidebarCollapsed).toBe(true);
172
+ expect(screen.getByRole("button", { name: "Expand sidebar" })).toBeTruthy();
142
173
  });
143
174
  });
@@ -10,7 +10,11 @@ import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
10
10
  import { DesktopAppShell, getDesktopHostPlatform } from "@/platforms/desktop";
11
11
  import { MobileAppShell } from "@/platforms/mobile";
12
12
  import { PANEL_APPS_DOC_BROWSER_RENDERERS } from "@/features/panel-apps";
13
- import { SideDock, type SideDockManager, useSideDockStore } from "@/features/side-dock";
13
+ import {
14
+ SideDock,
15
+ type SideDockManager,
16
+ useSideDockStore,
17
+ } from "@/features/side-dock";
14
18
  import { getPresenter } from "@/app/presenters/app.presenter";
15
19
  import { resolveUiDocumentTitle } from "@/shared/lib/ui-document-title";
16
20
  import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
@@ -37,12 +41,20 @@ function AppLayoutInner({
37
41
  };
38
42
 
39
43
  useEffect(() => {
40
- document.title = resolveUiDocumentTitle(pathname);
44
+ document.title = resolveUiDocumentTitle(pathname, window.location);
41
45
  }, [pathname, language]);
42
46
 
43
47
  if (isMobile && desktopHostPlatform !== "win32") {
44
48
  return (
45
- <MobileAppShell pathname={pathname} isDocBrowserOpen={isOpen} docBrowserDockControls={docBrowserDockControls} docBrowserRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} topbarLeadingInset={desktopHostPlatform === "darwin" ? "4.75rem" : undefined}>
49
+ <MobileAppShell
50
+ pathname={pathname}
51
+ isDocBrowserOpen={isOpen}
52
+ docBrowserDockControls={docBrowserDockControls}
53
+ docBrowserRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS}
54
+ topbarLeadingInset={
55
+ desktopHostPlatform === "darwin" ? "4.75rem" : undefined
56
+ }
57
+ >
46
58
  {children}
47
59
  </MobileAppShell>
48
60
  );
@@ -68,7 +80,9 @@ export function AppLayout({ children }: AppLayoutProps) {
68
80
 
69
81
  return (
70
82
  <DocBrowserProvider manager={presenter.docBrowserManager}>
71
- <AppLayoutInner sideDockManager={presenter.sideDockManager}>{children}</AppLayoutInner>
83
+ <AppLayoutInner sideDockManager={presenter.sideDockManager}>
84
+ {children}
85
+ </AppLayoutInner>
72
86
  </DocBrowserProvider>
73
87
  );
74
88
  }
@@ -5,13 +5,13 @@ interface HeaderProps {
5
5
 
6
6
  export function Header({ title, description }: HeaderProps) {
7
7
  return (
8
- <header className="h-14 bg-white/90 backdrop-blur-sm sticky top-0 z-10 flex items-center px-6 transition-all duration-base">
8
+ <header className="sticky top-0 z-10 flex h-14 items-center bg-background/90 px-6 backdrop-blur-sm transition-all duration-base">
9
9
  <div className="flex items-center gap-3">
10
10
  {title && (
11
11
  <div>
12
- <h2 className="text-[15px] font-semibold text-gray-900">{title}</h2>
12
+ <h2 className="text-[15px] font-semibold text-foreground">{title}</h2>
13
13
  {description && (
14
- <p className="text-xs text-gray-400 mt-0.5">{description}</p>
14
+ <p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
15
15
  )}
16
16
  </div>
17
17
  )}
@@ -43,9 +43,9 @@ export function PageHeader({ title, description, actions, className }: PageHeade
43
43
  return (
44
44
  <div className={cn('flex items-center justify-between mb-6 shrink-0', className)}>
45
45
  <div>
46
- <h2 className="text-xl font-semibold text-gray-900">{title}</h2>
46
+ <h2 className="text-xl font-semibold text-foreground">{title}</h2>
47
47
  {description && (
48
- <p className="text-sm text-gray-500 mt-1">{description}</p>
48
+ <p className="text-sm text-muted-foreground mt-1">{description}</p>
49
49
  )}
50
50
  </div>
51
51
  {actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
@@ -12,7 +12,7 @@ type RuntimeStatusTone = 'healthy' | 'attention' | 'inactive';
12
12
  const runtimeStatusToneStyles: Record<RuntimeStatusTone, string> = {
13
13
  healthy: 'bg-emerald-500 shadow-[0_0_0_3px_rgba(16,185,129,0.14)]',
14
14
  attention: 'bg-amber-400 shadow-[0_0_0_3px_rgba(251,191,36,0.16)]',
15
- inactive: 'bg-gray-300 shadow-[0_0_0_3px_rgba(156,163,175,0.12)]'
15
+ inactive: 'bg-muted-foreground/45 shadow-[0_0_0_3px_rgba(156,163,175,0.12)]'
16
16
  };
17
17
 
18
18
  type RuntimeStatusSummary = {
@@ -66,11 +66,11 @@ export function RuntimeStatusEntry() {
66
66
  <PopoverContent
67
67
  align="start"
68
68
  sideOffset={10}
69
- className="w-[290px] space-y-3 rounded-2xl border border-gray-200 bg-white p-4"
69
+ className="w-[290px] space-y-3 rounded-2xl border border-border bg-popover p-4 text-popover-foreground"
70
70
  >
71
71
  <div className="space-y-1">
72
- <div className="text-sm font-semibold text-gray-900">{summary.title}</div>
73
- <p className="text-xs leading-5 text-gray-600">{summary.description}</p>
72
+ <div className="text-sm font-semibold text-foreground">{summary.title}</div>
73
+ <p className="text-xs leading-5 text-muted-foreground">{summary.description}</p>
74
74
  </div>
75
75
  {summary.reasonLines.length > 0 ? (
76
76
  <div className="space-y-2">
@@ -85,13 +85,13 @@ export function RuntimeStatusEntry() {
85
85
  </div>
86
86
  ) : null}
87
87
  {summary.actionLabel ? (
88
- <div className="flex items-center justify-between border-t border-gray-100 pt-1">
89
- <span className="text-[11px] text-gray-500">{t('runtimeStatusActionHint')}</span>
88
+ <div className="flex items-center justify-between border-t border-border/70 pt-1">
89
+ <span className="text-[11px] text-muted-foreground">{t('runtimeStatusActionHint')}</span>
90
90
  <button
91
91
  type="button"
92
92
  onClick={() => void handleRestart()}
93
93
  disabled={summary.isBusy}
94
- className="text-sm font-semibold text-sky-600 transition-colors hover:text-sky-700 disabled:text-gray-400"
94
+ className="text-sm font-semibold text-primary transition-colors hover:text-primary-hover disabled:text-muted-foreground/45"
95
95
  >
96
96
  {summary.isBusy ? t('runtimeStatusRestartingAction') : summary.actionLabel}
97
97
  </button>