@nextclaw/ui 0.14.1 → 0.14.2

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 (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -0,0 +1,15 @@
1
+ export const SIDEBAR_RAIL_WIDTH_PX = 56;
2
+ export const SIDEBAR_RAIL_WIDTH_CLASS = "w-[56px]";
3
+ export const SIDEBAR_RAIL_PADDING_X_CLASS = "px-2";
4
+ export const SIDEBAR_RAIL_CONTROL_CLASS = "h-9 w-9 rounded-xl";
5
+ export const SIDEBAR_RAIL_ICON_CLASS = "h-4 w-4";
6
+ export const SIDEBAR_RAIL_STACK_CLASS = "space-y-1";
7
+ export const SIDEBAR_RAIL_ITEM_GAP_CLASS = "gap-1";
8
+
9
+ export const SIDEBAR_RAIL_SURFACE_CLASS =
10
+ "text-gray-600 transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 active:bg-gray-200/75 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35";
11
+
12
+ export const SIDEBAR_RAIL_ACTIVE_SURFACE_CLASS = "bg-gray-200/70 text-gray-900";
13
+
14
+ export const SIDEBAR_RAIL_PRIMARY_SURFACE_CLASS =
15
+ "bg-primary/10 text-primary transition-colors duration-base hover:bg-primary/15 hover:text-primary-700 active:bg-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35";
@@ -1,28 +1,253 @@
1
- import { cn } from '@/shared/lib/utils';
2
- import { t } from '@/shared/lib/i18n';
3
- import { THEME_OPTIONS, type UiTheme } from '@/shared/lib/theme';
4
- import { MessageCircle, BookOpen, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Bot } from 'lucide-react';
5
- import { NavLink } from 'react-router-dom';
6
- import { useDocBrowser } from '@/shared/components/doc-browser';
7
- import { BrandHeader } from '@/shared/components/common/brand-header';
8
- import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/app/components/layout/sidebar-items';
9
- import { useTheme } from '@/app/components/theme-provider';
10
- import { SelectItem } from '@/shared/components/ui/select';
11
- import { useAppPresenter } from '@/app/components/app-presenter-provider';
12
- import { useRemoteStatus } from '@/features/remote';
13
- import { getSettingsNavItems } from '@/app/configs/app-navigation.config';
14
- import { useLanguagePreference } from '@/features/settings';
15
-
16
- type SidebarMode = 'main' | 'settings';
1
+ import { cn } from "@/shared/lib/utils";
2
+ import { t } from "@/shared/lib/i18n";
3
+ import { THEME_OPTIONS, type UiTheme } from "@/shared/lib/theme";
4
+ import {
5
+ MessageCircle,
6
+ BookOpen,
7
+ BrainCircuit,
8
+ AlarmClock,
9
+ Languages,
10
+ Palette,
11
+ KeyRound,
12
+ Settings,
13
+ ArrowLeft,
14
+ Bot,
15
+ PanelLeftClose,
16
+ PanelLeftOpen,
17
+ type LucideIcon,
18
+ } from "lucide-react";
19
+ import { NavLink } from "react-router-dom";
20
+ import { useDocBrowser } from "@/shared/components/doc-browser";
21
+ import { BrandHeader } from "@/shared/components/common/brand-header";
22
+ import {
23
+ SidebarActionItem,
24
+ SidebarNavLinkItem,
25
+ SidebarSelectItem,
26
+ } from "@/app/components/layout/sidebar-items";
27
+ import { useTheme } from "@/app/components/theme-provider";
28
+ import { SelectItem } from "@/shared/components/ui/select";
29
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
30
+ import { useAppPresenter } from "@/app/components/app-presenter-provider";
31
+ import { useRemoteStatus } from "@/features/remote";
32
+ import { getSettingsNavItems } from "@/app/configs/app-navigation.config";
33
+ import { useLanguagePreference } from "@/features/settings";
34
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
35
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
36
+ import {
37
+ SIDEBAR_RAIL_CONTROL_CLASS,
38
+ SIDEBAR_RAIL_ICON_CLASS,
39
+ SIDEBAR_RAIL_ITEM_GAP_CLASS,
40
+ SIDEBAR_RAIL_PADDING_X_CLASS,
41
+ SIDEBAR_RAIL_STACK_CLASS,
42
+ SIDEBAR_RAIL_SURFACE_CLASS,
43
+ SIDEBAR_RAIL_WIDTH_CLASS,
44
+ } from "@/app/components/layout/sidebar-rail.styles";
45
+
46
+ type SidebarMode = "main" | "settings";
17
47
 
18
48
  type SidebarProps = {
19
49
  mode: SidebarMode;
20
50
  };
21
51
 
52
+ type SidebarNavItem = {
53
+ target: string;
54
+ label: string;
55
+ icon: LucideIcon;
56
+ };
57
+
58
+ function SidebarCollapseToggle({
59
+ isCollapsed,
60
+ onToggle,
61
+ }: {
62
+ isCollapsed: boolean;
63
+ onToggle: () => void;
64
+ }) {
65
+ const label = isCollapsed ? t("sidebarExpand") : t("sidebarCollapse");
66
+ const Icon = isCollapsed ? PanelLeftOpen : PanelLeftClose;
67
+
68
+ return (
69
+ <IconActionButton
70
+ icon={<Icon className={SIDEBAR_RAIL_ICON_CLASS} />}
71
+ label={label}
72
+ className={cn(SIDEBAR_RAIL_SURFACE_CLASS, isCollapsed && SIDEBAR_RAIL_CONTROL_CLASS)}
73
+ onClick={onToggle}
74
+ />
75
+ );
76
+ }
77
+
78
+ function SettingsSidebarHeader({
79
+ isCollapsed,
80
+ onToggle,
81
+ }: {
82
+ isCollapsed: boolean;
83
+ onToggle: () => void;
84
+ }) {
85
+ return (
86
+ <div
87
+ className={cn("shrink-0", isCollapsed ? "px-0 py-1.5" : "px-2 py-2")}
88
+ >
89
+ <div
90
+ className={cn(
91
+ "flex items-center py-1",
92
+ isCollapsed
93
+ ? cn("flex-col px-0", SIDEBAR_RAIL_ITEM_GAP_CLASS)
94
+ : "gap-2 px-1",
95
+ )}
96
+ data-testid="settings-sidebar-header"
97
+ >
98
+ {isCollapsed ? (
99
+ <SidebarCollapseToggle
100
+ isCollapsed={isCollapsed}
101
+ onToggle={onToggle}
102
+ />
103
+ ) : null}
104
+ <NavLink
105
+ to="/chat"
106
+ aria-label={t("backToMain")}
107
+ className={cn(
108
+ "group inline-flex min-w-0 items-center rounded-lg text-[12px] font-medium text-gray-500 transition-colors hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35",
109
+ isCollapsed
110
+ ? cn(
111
+ SIDEBAR_RAIL_CONTROL_CLASS,
112
+ SIDEBAR_RAIL_SURFACE_CLASS,
113
+ "justify-center p-0",
114
+ )
115
+ : "gap-1.5 px-1 py-1",
116
+ )}
117
+ >
118
+ <ArrowLeft
119
+ className={cn(
120
+ isCollapsed ? SIDEBAR_RAIL_ICON_CLASS : "h-3.5 w-3.5",
121
+ "shrink-0 text-gray-500 group-hover:text-gray-800",
122
+ )}
123
+ />
124
+ <span className={isCollapsed ? "sr-only" : "truncate"}>
125
+ {t("backToMain")}
126
+ </span>
127
+ </NavLink>
128
+ {!isCollapsed ? (
129
+ <>
130
+ <span
131
+ className="h-4 w-px shrink-0 bg-[#dddfe6]"
132
+ aria-hidden="true"
133
+ />
134
+ <h1 className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">
135
+ {t("settings")}
136
+ </h1>
137
+ <div className="ml-auto">
138
+ <SidebarCollapseToggle
139
+ isCollapsed={isCollapsed}
140
+ onToggle={onToggle}
141
+ />
142
+ </div>
143
+ </>
144
+ ) : null}
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function MainSidebarHeader({
151
+ isCollapsed,
152
+ onToggle,
153
+ }: {
154
+ isCollapsed: boolean;
155
+ onToggle: () => void;
156
+ }) {
157
+ return (
158
+ <div
159
+ className={cn(
160
+ "shrink-0",
161
+ isCollapsed ? "flex justify-center px-0 py-1.5" : "px-2 py-2",
162
+ )}
163
+ >
164
+ {isCollapsed ? (
165
+ <SidebarCollapseToggle isCollapsed={isCollapsed} onToggle={onToggle} />
166
+ ) : (
167
+ <div className="flex items-center gap-2">
168
+ <BrandHeader className="flex min-w-0 flex-1 items-center gap-2.5 cursor-pointer" />
169
+ <SidebarCollapseToggle
170
+ isCollapsed={isCollapsed}
171
+ onToggle={onToggle}
172
+ />
173
+ </div>
174
+ )}
175
+ </div>
176
+ );
177
+ }
178
+
179
+ function SidebarHeader({
180
+ isSettingsMode,
181
+ isCollapsed,
182
+ onToggle,
183
+ }: {
184
+ isSettingsMode: boolean;
185
+ isCollapsed: boolean;
186
+ onToggle: () => void;
187
+ }) {
188
+ return isSettingsMode ? (
189
+ <SettingsSidebarHeader isCollapsed={isCollapsed} onToggle={onToggle} />
190
+ ) : (
191
+ <MainSidebarHeader isCollapsed={isCollapsed} onToggle={onToggle} />
192
+ );
193
+ }
194
+
195
+ function SidebarNavigation({
196
+ isSettingsMode,
197
+ isCollapsed,
198
+ items,
199
+ density,
200
+ }: {
201
+ isSettingsMode: boolean;
202
+ isCollapsed: boolean;
203
+ items: SidebarNavItem[];
204
+ density: "default" | "compact";
205
+ }) {
206
+ return (
207
+ <nav
208
+ className={cn(
209
+ "custom-scrollbar min-h-0 flex-1 overflow-y-auto",
210
+ isCollapsed ? "pr-0" : "pr-1",
211
+ )}
212
+ >
213
+ <ul
214
+ className={cn(
215
+ isCollapsed
216
+ ? cn(SIDEBAR_RAIL_STACK_CLASS, "pb-3")
217
+ : isSettingsMode
218
+ ? "space-y-0.5 pb-3"
219
+ : "space-y-1 pb-4",
220
+ )}
221
+ >
222
+ {items.map((item) => {
223
+ return (
224
+ <li
225
+ key={item.target}
226
+ className={isCollapsed ? "flex justify-center" : undefined}
227
+ >
228
+ <SidebarNavLinkItem
229
+ to={item.target}
230
+ label={item.label}
231
+ icon={item.icon}
232
+ density={density}
233
+ collapsed={isCollapsed}
234
+ />
235
+ </li>
236
+ );
237
+ })}
238
+ </ul>
239
+ </nav>
240
+ );
241
+ }
242
+
22
243
  export function Sidebar({ mode }: SidebarProps) {
23
244
  const presenter = useAppPresenter();
24
245
  const docBrowser = useDocBrowser();
25
246
  const remoteStatus = useRemoteStatus();
247
+ const isCollapsed = useViewportLayoutStore(
248
+ (state) => state.isSidebarCollapsed,
249
+ );
250
+ const toggleCollapsed = viewportLayoutManager.toggleSidebarCollapsed;
26
251
  const {
27
252
  currentLanguage,
28
253
  currentLanguageLabel,
@@ -30,8 +255,11 @@ export function Sidebar({ mode }: SidebarProps) {
30
255
  selectLanguage,
31
256
  } = useLanguagePreference();
32
257
  const { theme, setTheme } = useTheme();
33
- const isSettingsMode = mode === 'settings';
34
- const currentThemeLabel = t(THEME_OPTIONS.find((option) => option.value === theme)?.labelKey ?? 'themeWarm');
258
+ const isSettingsMode = mode === "settings";
259
+ const currentThemeLabel = t(
260
+ THEME_OPTIONS.find((option) => option.value === theme)?.labelKey ??
261
+ "themeWarm",
262
+ );
35
263
  const accountEmail = remoteStatus.data?.account.email?.trim();
36
264
  const accountConnected = Boolean(remoteStatus.data?.account.loggedIn);
37
265
 
@@ -43,138 +271,167 @@ export function Sidebar({ mode }: SidebarProps) {
43
271
  };
44
272
 
45
273
  // Core navigation items - primary features
46
- const mainNavItems = [
274
+ const mainNavItems: SidebarNavItem[] = [
47
275
  {
48
- target: '/chat',
49
- label: t('chat'),
276
+ target: "/chat",
277
+ label: t("chat"),
50
278
  icon: MessageCircle,
51
279
  },
52
280
  {
53
- target: '/chat/cron',
54
- label: t('cron'),
281
+ target: "/chat/cron",
282
+ label: t("cron"),
55
283
  icon: AlarmClock,
56
284
  },
57
285
  {
58
- target: '/chat/skills',
59
- label: t('marketplaceFilterSkills'),
286
+ target: "/chat/skills",
287
+ label: t("marketplaceFilterSkills"),
60
288
  icon: BrainCircuit,
61
289
  },
62
290
  {
63
- target: '/agents',
64
- label: t('agentsPageTitle'),
291
+ target: "/agents",
292
+ label: t("agentsPageTitle"),
65
293
  icon: Bot,
66
- }
294
+ },
67
295
  ];
68
296
 
69
297
  const settingsNavItems = getSettingsNavItems(t);
70
298
  const navItems = isSettingsMode ? settingsNavItems : mainNavItems;
71
- const sidebarDensity = isSettingsMode ? 'compact' : 'default';
299
+ const sidebarDensity = isSettingsMode ? "compact" : "default";
72
300
 
73
301
  return (
74
- <aside className="w-[240px] shrink-0 flex h-full min-h-0 flex-col overflow-hidden bg-secondary px-4 pb-6">
75
- {isSettingsMode ? (
76
- <div className="shrink-0 px-2 py-2.5">
77
- <div
78
- className="flex items-center gap-2 px-1 py-1"
79
- data-testid="settings-sidebar-header"
80
- >
81
- <NavLink
82
- to="/chat"
83
- className="group inline-flex min-w-0 items-center gap-1.5 rounded-lg px-1 py-1 text-[12px] font-medium text-gray-500 transition-colors hover:bg-gray-200/60 hover:text-gray-900"
84
- >
85
- <ArrowLeft className="h-3.5 w-3.5 shrink-0 text-gray-400 group-hover:text-gray-700" />
86
- <span className="truncate">{t('backToMain')}</span>
87
- </NavLink>
88
- <span className="h-4 w-px shrink-0 bg-[#dddfe6]" aria-hidden="true" />
89
- <h1 className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{t('settings')}</h1>
90
- </div>
91
- </div>
92
- ) : (
93
- <div className="shrink-0 px-2 py-2.5">
94
- <BrandHeader className="flex items-center gap-2.5 cursor-pointer" />
95
- </div>
302
+ <aside
303
+ className={cn(
304
+ "shrink-0 flex h-full min-h-0 flex-col overflow-hidden bg-secondary pb-6 transition-[width] duration-200 ease-out",
305
+ isCollapsed
306
+ ? cn(SIDEBAR_RAIL_WIDTH_CLASS, SIDEBAR_RAIL_PADDING_X_CLASS)
307
+ : "w-[240px] px-4",
96
308
  )}
309
+ data-sidebar-collapsed={isCollapsed ? "true" : "false"}
310
+ >
311
+ <SidebarHeader
312
+ isSettingsMode={isSettingsMode}
313
+ isCollapsed={isCollapsed}
314
+ onToggle={toggleCollapsed}
315
+ />
97
316
 
98
317
  <div className="flex min-h-0 flex-1 flex-col">
99
318
  {/* Navigation */}
100
- <nav className="custom-scrollbar min-h-0 flex-1 overflow-y-auto pr-1">
101
- <ul className={cn(isSettingsMode ? 'space-y-0.5 pb-3' : 'space-y-1 pb-4')}>
102
- {navItems.map((item) => {
103
- return (
104
- <li key={item.target}>
105
- <SidebarNavLinkItem
106
- to={item.target}
107
- label={item.label}
108
- icon={item.icon}
109
- density={sidebarDensity}
110
- />
111
- </li>
112
- );
113
- })}
114
- </ul>
115
- </nav>
319
+ <SidebarNavigation
320
+ isSettingsMode={isSettingsMode}
321
+ isCollapsed={isCollapsed}
322
+ items={navItems}
323
+ density={sidebarDensity}
324
+ />
116
325
 
117
326
  {/* Footer actions stay reachable while the nav scrolls independently. */}
118
- <div className={cn('shrink-0 border-t border-[#dde0ea] bg-secondary', isSettingsMode ? 'mt-2 pt-3' : 'mt-3 pt-3')}>
327
+ <div
328
+ className={cn(
329
+ "shrink-0 border-t border-[#dde0ea] bg-secondary",
330
+ isCollapsed
331
+ ? "mt-2 pt-2"
332
+ : isSettingsMode
333
+ ? "mt-2 pt-3"
334
+ : "mt-3 pt-3",
335
+ )}
336
+ >
119
337
  {isSettingsMode ? (
120
338
  <SidebarActionItem
121
339
  onClick={() => presenter.accountManager.openAccountPanel()}
122
340
  icon={KeyRound}
123
- label={t('remoteAccountEntryManage')}
341
+ label={t("remoteAccountEntryManage")}
124
342
  density="compact"
125
- className="mb-1.5"
126
- trailing={accountConnected ? accountEmail || t('remoteAccountEntryConnected') : t('remoteAccountEntryDisconnected')}
343
+ className={isCollapsed ? "mb-1" : "mb-1.5"}
344
+ trailing={
345
+ accountConnected
346
+ ? accountEmail || t("remoteAccountEntryConnected")
347
+ : t("remoteAccountEntryDisconnected")
348
+ }
127
349
  trailingClassName="max-w-[92px] truncate text-right"
128
350
  testId="settings-sidebar-account-entry"
129
351
  trailingTestId="settings-sidebar-account-status"
352
+ collapsed={isCollapsed}
130
353
  />
131
354
  ) : null}
132
- {mode === 'main' && (
133
- <div className="mb-2">
355
+ {mode === "main" && (
356
+ <div
357
+ className={cn(
358
+ isCollapsed ? "mb-1" : "mb-2",
359
+ isCollapsed ? "flex justify-center" : undefined,
360
+ )}
361
+ >
134
362
  <SidebarNavLinkItem
135
363
  to="/settings"
136
- label={t('settings')}
364
+ label={t("settings")}
137
365
  icon={Settings}
366
+ collapsed={isCollapsed}
138
367
  />
139
368
  </div>
140
369
  )}
141
- <div className="mb-2">
370
+ <div
371
+ className={cn(
372
+ isCollapsed ? "mb-1" : "mb-2",
373
+ isCollapsed ? "flex justify-center" : undefined,
374
+ )}
375
+ >
142
376
  <SidebarSelectItem
143
377
  value={theme}
144
378
  onValueChange={(value) => handleThemeSwitch(value as UiTheme)}
145
379
  icon={Palette}
146
- label={t('theme')}
380
+ label={t("theme")}
147
381
  valueLabel={currentThemeLabel}
148
382
  density={sidebarDensity}
383
+ collapsed={isCollapsed}
149
384
  >
150
385
  {THEME_OPTIONS.map((option) => (
151
- <SelectItem key={option.value} value={option.value} className="text-xs">
386
+ <SelectItem
387
+ key={option.value}
388
+ value={option.value}
389
+ className="text-xs"
390
+ >
152
391
  {t(option.labelKey)}
153
392
  </SelectItem>
154
393
  ))}
155
394
  </SidebarSelectItem>
156
395
  </div>
157
- <div className="mb-2">
396
+ <div
397
+ className={cn(
398
+ isCollapsed ? "mb-1" : "mb-2",
399
+ isCollapsed ? "flex justify-center" : undefined,
400
+ )}
401
+ >
158
402
  <SidebarSelectItem
159
403
  value={currentLanguage}
160
- onValueChange={(value) => selectLanguage(value as typeof currentLanguage)}
404
+ onValueChange={(value) =>
405
+ selectLanguage(value as typeof currentLanguage)
406
+ }
161
407
  icon={Languages}
162
- label={t('language')}
408
+ label={t("language")}
163
409
  valueLabel={currentLanguageLabel}
164
410
  density={sidebarDensity}
411
+ collapsed={isCollapsed}
165
412
  >
166
413
  {languageOptions.map((option) => (
167
- <SelectItem key={option.value} value={option.value} className="text-xs">
414
+ <SelectItem
415
+ key={option.value}
416
+ value={option.value}
417
+ className="text-xs"
418
+ >
168
419
  {option.label}
169
420
  </SelectItem>
170
421
  ))}
171
422
  </SidebarSelectItem>
172
423
  </div>
173
424
  <SidebarActionItem
174
- onClick={() => docBrowser.open(undefined, { kind: 'docs', title: t('docBrowserHelp') })}
425
+ onClick={() =>
426
+ docBrowser.open(undefined, {
427
+ kind: "docs",
428
+ title: t("docBrowserHelp"),
429
+ })
430
+ }
175
431
  icon={BookOpen}
176
- label={t('docBrowserHelp')}
432
+ label={t("docBrowserHelp")}
177
433
  density={sidebarDensity}
434
+ collapsed={isCollapsed}
178
435
  />
179
436
  </div>
180
437
  </div>
@@ -47,6 +47,16 @@ export class ViewportLayoutManager {
47
47
 
48
48
  getSnapshot = (): ViewportLayoutSnapshot => useViewportLayoutStore.getState();
49
49
 
50
+ setSidebarCollapsed = (isSidebarCollapsed: boolean) => {
51
+ useViewportLayoutStore.setState({ isSidebarCollapsed });
52
+ };
53
+
54
+ toggleSidebarCollapsed = () => {
55
+ useViewportLayoutStore.setState((state) => ({
56
+ isSidebarCollapsed: !state.isSidebarCollapsed,
57
+ }));
58
+ };
59
+
50
60
  resetForTests = () => {
51
61
  this.consumerCount = 0;
52
62
  this.stop();
@@ -1,13 +1,21 @@
1
1
  import { create } from "zustand";
2
+ import { createJSONStorage, persist } from "zustand/middleware";
2
3
 
3
4
  export type ViewportLayoutMode = "mobile" | "desktop";
4
5
 
5
6
  export type ViewportLayoutSnapshot = {
6
7
  mode: ViewportLayoutMode;
7
8
  width: number | null;
9
+ isSidebarCollapsed: boolean;
8
10
  };
9
11
 
10
12
  export const MOBILE_VIEWPORT_MAX_WIDTH = 767;
13
+ const VIEWPORT_LAYOUT_STORAGE_KEY = "nextclaw.app.viewport-layout";
14
+ const VIEWPORT_LAYOUT_STORAGE_VERSION = 1;
15
+
16
+ type PersistedViewportLayoutStore = {
17
+ isSidebarCollapsed?: unknown;
18
+ };
11
19
 
12
20
  export function resolveViewportLayoutMode(
13
21
  width: number | null | undefined,
@@ -20,13 +28,41 @@ export function resolveViewportLayoutMode(
20
28
 
21
29
  export function createInitialViewportLayoutSnapshot(): ViewportLayoutSnapshot {
22
30
  const width =
23
- typeof window === "undefined" ? null : window.innerWidth ?? null;
31
+ typeof window === "undefined" ? null : (window.innerWidth ?? null);
24
32
  return {
25
33
  mode: resolveViewportLayoutMode(width),
26
34
  width,
35
+ isSidebarCollapsed: false,
27
36
  };
28
37
  }
29
38
 
30
- export const useViewportLayoutStore = create<ViewportLayoutSnapshot>(() =>
31
- createInitialViewportLayoutSnapshot(),
39
+ function resolvePersistedSidebarCollapsed(
40
+ persistedState: unknown,
41
+ ): boolean | null {
42
+ if (!persistedState || typeof persistedState !== "object") {
43
+ return null;
44
+ }
45
+ const { isSidebarCollapsed } = persistedState as PersistedViewportLayoutStore;
46
+ return typeof isSidebarCollapsed === "boolean" ? isSidebarCollapsed : null;
47
+ }
48
+
49
+ export const useViewportLayoutStore = create<ViewportLayoutSnapshot>()(
50
+ persist(() => createInitialViewportLayoutSnapshot(), {
51
+ name: VIEWPORT_LAYOUT_STORAGE_KEY,
52
+ version: VIEWPORT_LAYOUT_STORAGE_VERSION,
53
+ storage: createJSONStorage(() => window.localStorage),
54
+ partialize: (state): PersistedViewportLayoutStore => ({
55
+ isSidebarCollapsed: state.isSidebarCollapsed,
56
+ }),
57
+ merge: (persistedState, currentState) => {
58
+ const isSidebarCollapsed =
59
+ resolvePersistedSidebarCollapsed(persistedState);
60
+ return isSidebarCollapsed === null
61
+ ? currentState
62
+ : {
63
+ ...currentState,
64
+ isSidebarCollapsed,
65
+ };
66
+ },
67
+ }),
32
68
  );