@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
@@ -1,8 +1,29 @@
1
- import { NavLink } from 'react-router-dom';
2
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
3
- import { Select, SelectContent, SelectItem, SelectTrigger } from '@/shared/components/ui/select';
4
- import { t, type I18nLanguage } from '@/shared/lib/i18n';
5
- import type { UiTheme } from '@/shared/lib/theme';
1
+ import { NavLink } from "react-router-dom";
2
+ import {
3
+ Popover,
4
+ PopoverContent,
5
+ PopoverTrigger,
6
+ } from "@/shared/components/ui/popover";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ } from "@/shared/components/ui/select";
13
+ import {
14
+ Tooltip,
15
+ TooltipContent,
16
+ TooltipProvider,
17
+ TooltipTrigger,
18
+ } from "@/shared/components/ui/tooltip";
19
+ import { t, type I18nLanguage } from "@/shared/lib/i18n";
20
+ import type { UiTheme } from "@/shared/lib/theme";
21
+ import { cn } from "@/shared/lib/utils";
22
+ import {
23
+ SIDEBAR_RAIL_CONTROL_CLASS,
24
+ SIDEBAR_RAIL_ICON_CLASS,
25
+ SIDEBAR_RAIL_SURFACE_CLASS,
26
+ } from "@/app/components/layout/sidebar-rail.styles";
6
27
  import {
7
28
  BookOpen,
8
29
  ChevronRight,
@@ -11,7 +32,7 @@ import {
11
32
  Palette,
12
33
  Settings,
13
34
  type LucideIcon,
14
- } from 'lucide-react';
35
+ } from "lucide-react";
15
36
 
16
37
  type ChatSidebarUtilityOption<Value extends string> = {
17
38
  value: Value;
@@ -31,6 +52,7 @@ type ChatSidebarUtilityMenuProps = {
31
52
  onSelectLanguage: (language: I18nLanguage) => void;
32
53
  onOpenDocs: () => void;
33
54
  onOpenApps: () => void;
55
+ collapsed?: boolean;
34
56
  };
35
57
 
36
58
  export function ChatSidebarUtilityMenu({
@@ -46,6 +68,7 @@ export function ChatSidebarUtilityMenu({
46
68
  onSelectLanguage,
47
69
  onOpenDocs,
48
70
  onOpenApps,
71
+ collapsed = false,
49
72
  }: ChatSidebarUtilityMenuProps) {
50
73
  const handleOpenDocs = () => {
51
74
  onOpenDocs();
@@ -59,52 +82,86 @@ export function ChatSidebarUtilityMenu({
59
82
 
60
83
  return (
61
84
  <Popover open={isOpen} onOpenChange={onOpenChange}>
62
- <PopoverTrigger asChild>
63
- <button
64
- type="button"
65
- aria-label={t('settingsMenu')}
66
- className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-gray-600 transition-all duration-base hover:bg-gray-200/60 hover:text-gray-800"
67
- >
68
- <Settings className="h-4 w-4 shrink-0 text-gray-400" />
69
- <span className="min-w-0 flex-1 text-left">{t('settings')}</span>
70
- <span className="max-w-[112px] truncate text-[13px] text-gray-500">
71
- {currentThemeLabel} / {currentLanguageLabel}
72
- </span>
73
- </button>
74
- </PopoverTrigger>
75
- <PopoverContent side="top" align="start" className="w-64 p-2">
85
+ {collapsed ? (
86
+ <TooltipProvider delayDuration={250}>
87
+ <Tooltip>
88
+ <TooltipTrigger asChild>
89
+ <PopoverTrigger asChild>
90
+ <button
91
+ type="button"
92
+ aria-label={t("settingsMenu")}
93
+ className={cn(
94
+ "flex items-center justify-center",
95
+ SIDEBAR_RAIL_CONTROL_CLASS,
96
+ SIDEBAR_RAIL_SURFACE_CLASS,
97
+ )}
98
+ >
99
+ <Settings
100
+ className={cn(
101
+ SIDEBAR_RAIL_ICON_CLASS,
102
+ "shrink-0 text-muted-foreground",
103
+ )}
104
+ />
105
+ </button>
106
+ </PopoverTrigger>
107
+ </TooltipTrigger>
108
+ <TooltipContent side="right" className="text-xs">
109
+ {t("settingsMenu")}
110
+ </TooltipContent>
111
+ </Tooltip>
112
+ </TooltipProvider>
113
+ ) : (
114
+ <PopoverTrigger asChild>
115
+ <button
116
+ type="button"
117
+ aria-label={t("settingsMenu")}
118
+ className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-all duration-base hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
119
+ >
120
+ <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
121
+ <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
122
+ <span className="max-w-[112px] truncate text-[13px] text-muted-foreground">
123
+ {currentThemeLabel} / {currentLanguageLabel}
124
+ </span>
125
+ </button>
126
+ </PopoverTrigger>
127
+ )}
128
+ <PopoverContent
129
+ side={collapsed ? "right" : "top"}
130
+ align={collapsed ? "end" : "start"}
131
+ className="w-64 p-2"
132
+ >
76
133
  <div className="space-y-1">
77
134
  <NavLink
78
135
  to="/settings"
79
136
  onClick={() => onOpenChange(false)}
80
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
137
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
81
138
  >
82
- <Settings className="h-4 w-4 text-gray-400" />
83
- <span className="flex-1 text-left">{t('settings')}</span>
139
+ <Settings className="h-4 w-4 text-muted-foreground/70" />
140
+ <span className="flex-1 text-left">{t("settings")}</span>
84
141
  </NavLink>
85
142
  <button
86
143
  type="button"
87
144
  onClick={handleOpenDocs}
88
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
145
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
89
146
  >
90
- <BookOpen className="h-4 w-4 text-gray-400" />
91
- <span className="flex-1 text-left">{t('docBrowserHelp')}</span>
147
+ <BookOpen className="h-4 w-4 text-muted-foreground/70" />
148
+ <span className="flex-1 text-left">{t("docBrowserHelp")}</span>
92
149
  </button>
93
150
  <button
94
151
  type="button"
95
152
  onClick={handleOpenApps}
96
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
153
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
97
154
  >
98
- <Boxes className="h-4 w-4 text-gray-400" />
99
- <span className="flex-1 text-left">{t('appsTitle')}</span>
155
+ <Boxes className="h-4 w-4 text-muted-foreground/70" />
156
+ <span className="flex-1 text-left">{t("appsTitle")}</span>
100
157
  </button>
101
158
  </div>
102
159
 
103
- <div className="my-2 h-px bg-gray-200/70" />
160
+ <div className="my-2 h-px bg-border" />
104
161
 
105
162
  <ChatSidebarUtilitySelect
106
163
  icon={Palette}
107
- label={t('theme')}
164
+ label={t("theme")}
108
165
  options={themeOptions}
109
166
  value={currentTheme}
110
167
  valueLabel={currentThemeLabel}
@@ -114,7 +171,7 @@ export function ChatSidebarUtilityMenu({
114
171
 
115
172
  <ChatSidebarUtilitySelect
116
173
  icon={Languages}
117
- label={t('language')}
174
+ label={t("language")}
118
175
  options={languageOptions}
119
176
  value={currentLanguage}
120
177
  valueLabel={currentLanguageLabel}
@@ -155,14 +212,14 @@ function ChatSidebarUtilitySelect<Value extends string>({
155
212
  >
156
213
  <SelectTrigger
157
214
  aria-label={label}
158
- className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-gray-700 shadow-none hover:bg-gray-100 focus:ring-0"
159
- indicator={<ChevronRight className="h-4 w-4 text-gray-400" />}
215
+ className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-accent hover:text-accent-foreground focus:ring-0"
216
+ indicator={<ChevronRight className="h-4 w-4 text-muted-foreground/70" />}
160
217
  >
161
218
  <div className="flex min-w-0 items-center gap-2.5">
162
- <Icon className="h-4 w-4 shrink-0 text-gray-400" />
219
+ <Icon className="h-4 w-4 shrink-0 text-muted-foreground/70" />
163
220
  <span className="text-left">{label}</span>
164
221
  </div>
165
- <span className="ml-auto max-w-[96px] truncate text-[13px] text-gray-500">
222
+ <span className="ml-auto max-w-[96px] truncate text-[13px] text-muted-foreground">
166
223
  {valueLabel}
167
224
  </span>
168
225
  </SelectTrigger>
@@ -1,7 +1,4 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
- import { BrandHeader } from "@/shared/components/common/brand-header";
3
- import { StatusBadge } from "@/shared/components/common/status-badge";
4
- import { ChatSidebarListModeSwitch } from "@/features/chat/components/chat-sidebar-list-mode-switch";
5
2
  import {
6
3
  getSessionTitle,
7
4
  groupChildSessionsByParentKey,
@@ -25,37 +22,26 @@ import { THEME_OPTIONS } from "@/shared/lib/theme";
25
22
  import { useI18n } from "@/app/components/i18n-provider";
26
23
  import { useTheme } from "@/app/components/theme-provider";
27
24
  import { useDocBrowser } from "@/shared/components/doc-browser";
28
- import { SidebarNavLinkItem } from "@/app/components/layout/sidebar-items";
29
- import { AlarmClock, Bot, BrainCircuit } from "lucide-react";
30
25
  import { ChatSidebarSessionEntry } from "@/features/chat/features/session/components/chat-sidebar-session-entry";
31
- import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
32
26
  import {
33
27
  ChatSidebarDesktopToolbar,
34
28
  ChatSidebarMobileToolbar,
35
29
  } from "@/features/chat/components/layout/chat-sidebar-toolbar";
36
- import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
30
+ import {
31
+ ChatSidebarDesktopFooter,
32
+ ChatSidebarDesktopHeader,
33
+ ChatSidebarDesktopNav,
34
+ ChatSidebarSessionArea,
35
+ } from "@/features/chat/components/layout/chat-sidebar-desktop-layout";
37
36
  import { openApps } from "@/features/panel-apps";
38
- import { isWindowsDesktopHost } from "@/platforms/desktop";
39
37
  import {
40
38
  buildSessionTypeOptions,
41
39
  DEFAULT_SESSION_TYPE,
42
40
  normalizeSessionType,
43
41
  } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
44
42
  import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
45
-
46
- const navItems = [
47
- {
48
- target: "/cron",
49
- label: () => t("chatSidebarScheduledTasks"),
50
- icon: AlarmClock,
51
- },
52
- {
53
- target: "/skills",
54
- label: () => t("chatSidebarSkills"),
55
- icon: BrainCircuit,
56
- },
57
- { target: "/agents", label: () => t("agentsPageTitle"), icon: Bot },
58
- ];
43
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
44
+ import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
59
45
 
60
46
  type ChatSidebarVariant = "desktop" | "mobile";
61
47
 
@@ -104,6 +90,9 @@ export function ChatSidebar({
104
90
  const isMobileVariant = variant === "mobile";
105
91
  const presenter = usePresenter();
106
92
  const docBrowser = useDocBrowser();
93
+ const isSidebarCollapsed = useViewportLayoutStore(
94
+ (state) => state.isSidebarCollapsed,
95
+ );
107
96
  const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
108
97
  const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
109
98
  const listSnapshot = useChatSessionListStore((state) => state.snapshot);
@@ -116,7 +105,7 @@ export function ChatSidebar({
116
105
  const { language, setLanguage } = useI18n();
117
106
  const { theme, setTheme } = useTheme();
118
107
  const currentThemeLabel = t(
119
- THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeWarm",
108
+ THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeNatural",
120
109
  );
121
110
  const currentLanguageLabel =
122
111
  LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
@@ -179,6 +168,7 @@ export function ChatSidebar({
179
168
  sessionTypeOptions,
180
169
  });
181
170
  const isProjectFirstView = listSnapshot.listMode === "project-first";
171
+ const shouldCollapse = !isMobileVariant && isSidebarCollapsed;
182
172
  const optimisticReadAtBySessionKey = useChatSessionUnreadState(
183
173
  items,
184
174
  listSnapshot.selectedSessionKey,
@@ -237,19 +227,23 @@ export function ChatSidebar({
237
227
  return (
238
228
  <aside
239
229
  className={cn(
240
- "flex h-full min-h-0 flex-col bg-secondary",
230
+ "flex h-full min-h-0 flex-col bg-secondary transition-[width] duration-200 ease-out",
241
231
  isMobileVariant
242
232
  ? "flex-1 overflow-hidden"
243
- : "w-[280px] shrink-0 border-r border-gray-200/60",
233
+ : shouldCollapse
234
+ ? cn(
235
+ SIDEBAR_RAIL_WIDTH_CLASS,
236
+ "shrink-0 border-r border-border/70",
237
+ )
238
+ : "w-[280px] shrink-0 border-r border-border/70",
244
239
  )}
240
+ data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
245
241
  >
246
- {!isMobileVariant && !isWindowsDesktopHost() ? (
247
- <div className="px-5 py-2.5">
248
- <BrandHeader
249
- className="flex min-w-0 items-center gap-2"
250
- suffix={<StatusBadge status={systemStatus.connectionStatus} />}
251
- />
252
- </div>
242
+ {!isMobileVariant ? (
243
+ <ChatSidebarDesktopHeader
244
+ connectionStatus={systemStatus.connectionStatus}
245
+ isCollapsed={shouldCollapse}
246
+ />
253
247
  ) : null}
254
248
 
255
249
  {isMobileVariant ? (
@@ -287,75 +281,48 @@ export function ChatSidebar({
287
281
  newSessionTypePreference.setSelectedSessionType
288
282
  }
289
283
  onQueryChange={presenter.chatSessionListManager.setQuery}
284
+ collapsed={shouldCollapse}
290
285
  />
291
286
  )}
292
287
 
293
288
  {!isMobileVariant ? (
294
- <div className="px-3 pb-2">
295
- <ul className="space-y-0.5">
296
- {navItems.map((item) => (
297
- <li key={item.target}>
298
- <SidebarNavLinkItem
299
- to={item.target}
300
- label={item.label()}
301
- icon={item.icon}
302
- density="compact"
303
- />
304
- </li>
305
- ))}
306
- </ul>
307
- </div>
308
- ) : null}
309
-
310
- {!isMobileVariant ? (
311
- <div className="mx-4 border-t border-gray-200/60" />
289
+ <ChatSidebarDesktopNav isCollapsed={shouldCollapse} />
312
290
  ) : null}
313
291
 
314
- <div className="flex items-center justify-between px-5 pb-2 pt-3">
315
- <div className="text-[11px] font-medium uppercase tracking-wider text-gray-400">
316
- {t("chatSidebarTaskRecords")}
317
- </div>
318
- <ChatSidebarListModeSwitch
319
- isProjectFirstView={isProjectFirstView}
320
- onSelectMode={presenter.chatSessionListManager.setListMode}
321
- />
322
- </div>
323
-
324
- <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
325
- <ChatSidebarSessionList
326
- isLoading={isLoading}
327
- isProjectFirstView={isProjectFirstView}
328
- groups={groups}
329
- projectGroups={projectGroups}
330
- defaultSessionType={newSessionTypePreference.selectedSessionType}
331
- sessionTypeOptions={sessionTypeOptions}
332
- renderSessionItem={renderSessionItem}
333
- onCreateSession={createSessionAndOpenIfNeeded}
334
- />
335
- </div>
292
+ <ChatSidebarSessionArea
293
+ defaultSessionType={newSessionTypePreference.selectedSessionType}
294
+ groups={groups}
295
+ isCollapsed={shouldCollapse}
296
+ isLoading={isLoading}
297
+ isProjectFirstView={isProjectFirstView}
298
+ onCreateSession={createSessionAndOpenIfNeeded}
299
+ onSelectMode={presenter.chatSessionListManager.setListMode}
300
+ projectGroups={projectGroups}
301
+ renderSessionItem={renderSessionItem}
302
+ sessionTypeOptions={sessionTypeOptions}
303
+ />
336
304
 
337
305
  {!isMobileVariant ? (
338
- <div className="px-3 py-3 border-t border-gray-200/60">
339
- <ChatSidebarUtilityMenu
340
- isOpen={isUtilityMenuOpen}
341
- onOpenChange={setIsUtilityMenuOpen}
342
- currentTheme={theme}
343
- currentThemeLabel={currentThemeLabel}
344
- themeOptions={utilityThemeOptions}
345
- onSelectTheme={setTheme}
346
- currentLanguage={language}
347
- currentLanguageLabel={currentLanguageLabel}
348
- languageOptions={utilityLanguageOptions}
349
- onSelectLanguage={handleLanguageSwitch}
350
- onOpenDocs={() =>
351
- docBrowser.open(undefined, {
352
- kind: "docs",
353
- title: t("docBrowserHelp"),
354
- })
355
- }
356
- onOpenApps={() => openApps(docBrowser)}
357
- />
358
- </div>
306
+ <ChatSidebarDesktopFooter
307
+ currentLanguage={language}
308
+ currentLanguageLabel={currentLanguageLabel}
309
+ currentTheme={theme}
310
+ currentThemeLabel={currentThemeLabel}
311
+ isCollapsed={shouldCollapse}
312
+ isOpen={isUtilityMenuOpen}
313
+ languageOptions={utilityLanguageOptions}
314
+ onOpenApps={() => openApps(docBrowser)}
315
+ onOpenChange={setIsUtilityMenuOpen}
316
+ onOpenDocs={() =>
317
+ docBrowser.open(undefined, {
318
+ kind: "docs",
319
+ title: t("docBrowserHelp"),
320
+ })
321
+ }
322
+ onSelectLanguage={handleLanguageSwitch}
323
+ onSelectTheme={setTheme}
324
+ themeOptions={utilityThemeOptions}
325
+ />
359
326
  ) : null}
360
327
  </aside>
361
328
  );
@@ -2,25 +2,19 @@ import { createContext, useContext, type ReactNode } from 'react';
2
2
  import type {
3
3
  ChatSessionListManager,
4
4
  } from '@/features/chat/managers/chat-session-list.manager';
5
- import type { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
6
5
  import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
7
- import type { ChatInputManager } from '@/features/chat/managers/chat-input.manager';
8
6
  import type { ChatQueryManager } from '@/features/chat/managers/chat-query.manager';
9
7
  import type { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
10
8
 
11
9
  type PublicManager<T extends object> = Pick<T, keyof T>;
12
10
 
13
- export type ChatInputManagerLike = PublicManager<ChatInputManager>;
14
11
  export type ChatThreadManagerLike = PublicManager<ChatThreadManager>;
15
12
 
16
13
  export type ChatPresenterLike = {
17
14
  chatUiManager: ChatUiManager;
18
- chatRunManager: ChatRunManager;
19
- chatInputManager: ChatInputManagerLike;
20
15
  chatQueryManager: ChatQueryManager;
21
16
  chatSessionListManager: ChatSessionListManager;
22
17
  chatThreadManager: ChatThreadManagerLike;
23
- startAgentCreationDraft: (prompt: string) => void;
24
18
  };
25
19
 
26
20
  const ChatPresenterContext = createContext<ChatPresenterLike | null>(null);
@@ -0,0 +1,225 @@
1
+ import { memo, type ReactNode } from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
5
+
6
+ import { SessionConversationArea } from '@/features/chat/features/conversation/components/session-conversation-area';
7
+
8
+ const mocks = vi.hoisted(() => {
9
+ const inputRenderSpy = vi.fn();
10
+ const inputActions = {
11
+ update: vi.fn(),
12
+ syncComposer: vi.fn(),
13
+ resetComposer: vi.fn(),
14
+ restoreComposer: vi.fn(),
15
+ applyPromptSuggestion: vi.fn(),
16
+ requestComposerFocusAtEnd: vi.fn(),
17
+ consumeComposerFocusRequest: vi.fn(),
18
+ setAttachments: vi.fn(),
19
+ addAttachments: vi.fn(() => []),
20
+ removeAttachment: vi.fn(),
21
+ setSelectedModel: vi.fn(),
22
+ setSelectedThinkingLevel: vi.fn(),
23
+ setPendingSessionType: vi.fn(),
24
+ setPendingProjectRoot: vi.fn(),
25
+ setSelectedSkills: vi.fn(),
26
+ setSendError: vi.fn(),
27
+ };
28
+ const inputSnapshot = {
29
+ text: '',
30
+ nodes: [],
31
+ selectedSkills: [],
32
+ skillRecords: [],
33
+ attachments: [],
34
+ selectedModel: undefined,
35
+ selectedThinkingLevel: null,
36
+ pendingSessionType: 'default',
37
+ selectedSessionType: 'default',
38
+ pendingProjectRoot: null,
39
+ composerFocusRequestId: 0,
40
+ sendError: null,
41
+ };
42
+ const inputQuery = {
43
+ defaultModel: undefined,
44
+ defaultProjectRoot: null,
45
+ fallbackPreferredModel: undefined,
46
+ fallbackPreferredThinking: null,
47
+ isProviderStateResolved: true,
48
+ isSkillsLoading: false,
49
+ modelOptions: [],
50
+ selectedSession: null,
51
+ selectedSessionKey: null,
52
+ sessionTypeState: {
53
+ sessionTypeOptions: [],
54
+ selectedSessionTypeOption: null,
55
+ defaultSessionType: 'default',
56
+ selectedSessionType: 'default',
57
+ canEditSessionType: true,
58
+ sessionTypeUnavailable: false,
59
+ sessionTypeUnavailableMessage: null,
60
+ },
61
+ skillRecords: [],
62
+ };
63
+ const controller = {
64
+ canStopGeneration: true,
65
+ hasSendableDraft: true,
66
+ isSending: true,
67
+ send: vi.fn(),
68
+ sendDisabled: true,
69
+ stop: vi.fn(),
70
+ stopDisabled: false,
71
+ };
72
+ const agent = {
73
+ visibleMessages: [] as unknown[],
74
+ isHydrating: false,
75
+ isRunning: true,
76
+ isSending: true,
77
+ hydrateError: null,
78
+ snapshot: {
79
+ activeRun: null,
80
+ contextWindow: null,
81
+ error: null,
82
+ },
83
+ send: vi.fn(),
84
+ abort: vi.fn(),
85
+ };
86
+
87
+ return {
88
+ agent,
89
+ controller,
90
+ inputActions,
91
+ inputQuery,
92
+ inputRenderSpy,
93
+ inputSnapshot,
94
+ presenter: {
95
+ chatUiManager: {
96
+ goToProviders: vi.fn(),
97
+ },
98
+ chatSessionListManager: {
99
+ createSession: vi.fn(),
100
+ setSelectedAgentId: vi.fn(),
101
+ },
102
+ },
103
+ appPresenter: {
104
+ chatDraftIntentManager: {
105
+ consumePending: vi.fn(() => null),
106
+ markConsumed: vi.fn(),
107
+ subscribe: vi.fn(() => vi.fn()),
108
+ },
109
+ },
110
+ };
111
+ });
112
+
113
+ vi.mock('@/app/components/app-presenter-provider', () => ({
114
+ useAppPresenter: () => mocks.appPresenter,
115
+ }));
116
+
117
+ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
118
+ usePresenter: () => mocks.presenter,
119
+ }));
120
+
121
+ vi.mock('@/features/chat/components/conversation/chat-conversation-content', () => ({
122
+ ChatConversationContent: ({
123
+ messages,
124
+ showWelcome,
125
+ welcomeSlot,
126
+ }: {
127
+ messages: readonly unknown[];
128
+ showWelcome: boolean;
129
+ welcomeSlot?: ReactNode;
130
+ }) => (
131
+ <div data-testid="conversation-content" data-show-welcome={String(showWelcome)}>
132
+ {showWelcome ? welcomeSlot : <div data-testid="message-count">{messages.length}</div>}
133
+ </div>
134
+ ),
135
+ }));
136
+
137
+ vi.mock('@/features/chat/features/welcome/components/chat-conversation-welcome', () => ({
138
+ ChatConversationWelcome: ({ inputSlot }: { inputSlot: ReactNode }) => (
139
+ <div data-testid="welcome">{inputSlot}</div>
140
+ ),
141
+ }));
142
+
143
+ vi.mock('@/features/chat/features/ncp/hooks/use-ncp-session-conversation', () => ({
144
+ isNcpAgentStartupUnavailableErrorMessage: () => false,
145
+ useNcpSessionConversation: () => mocks.agent,
146
+ }));
147
+
148
+ vi.mock('@/features/chat/features/runtime/utils/ncp-chat-runtime-availability.utils', () => ({
149
+ isNcpChatRuntimeBlocked: () => false,
150
+ resolveNcpChatSendErrorMessage: ({ message }: { message: string | null }) => message,
151
+ }));
152
+
153
+ vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-input-state', () => ({
154
+ useSessionConversationInputState: () => ({
155
+ inputActions: mocks.inputActions,
156
+ inputSnapshot: mocks.inputSnapshot,
157
+ }),
158
+ }));
159
+
160
+ vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-input-query', () => ({
161
+ useSessionConversationInputQuery: () => mocks.inputQuery,
162
+ }));
163
+
164
+ vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-controller', () => ({
165
+ useSessionConversationController: () => mocks.controller,
166
+ }));
167
+
168
+ vi.mock('@/features/chat/stores/chat-session-list.store', () => ({
169
+ useChatSessionListStore: (selector: (state: { snapshot: { selectedAgentId: string } }) => unknown) =>
170
+ selector({ snapshot: { selectedAgentId: 'main' } }),
171
+ }));
172
+
173
+ vi.mock('@/features/system-status', () => ({
174
+ useSystemStatus: () => ({ phase: 'ready', lastReadyAt: 1 }),
175
+ }));
176
+
177
+ vi.mock('../session-conversation-input', () => ({
178
+ SessionConversationInput: memo((props: unknown) => {
179
+ mocks.inputRenderSpy(props);
180
+ return <div data-testid="conversation-input" />;
181
+ }),
182
+ }));
183
+
184
+ function renderArea(sessionKey: string | null = 'session-1') {
185
+ return render(
186
+ <MemoryRouter>
187
+ <SessionConversationArea sessionKey={sessionKey} />
188
+ </MemoryRouter>,
189
+ );
190
+ }
191
+
192
+ describe('SessionConversationArea input boundary', () => {
193
+ beforeEach(() => {
194
+ vi.clearAllMocks();
195
+ mocks.agent.visibleMessages = [];
196
+ mocks.agent.isHydrating = false;
197
+ mocks.agent.isRunning = true;
198
+ mocks.agent.isSending = true;
199
+ mocks.agent.snapshot.contextWindow = null;
200
+ });
201
+
202
+ it('keeps the composer input subtree stable when only streamed messages change', () => {
203
+ const rendered = renderArea('session-1');
204
+
205
+ expect(mocks.inputRenderSpy).toHaveBeenCalledOnce();
206
+
207
+ mocks.agent.visibleMessages = [{ id: 'message-1' }];
208
+ rendered.rerender(
209
+ <MemoryRouter>
210
+ <SessionConversationArea sessionKey="session-1" />
211
+ </MemoryRouter>,
212
+ );
213
+
214
+ expect(screen.getByTestId('message-count').textContent).toBe('1');
215
+ expect(mocks.inputRenderSpy).toHaveBeenCalledOnce();
216
+ });
217
+
218
+ it('does not replace the welcome composer just because draft send starts', () => {
219
+ renderArea(null);
220
+
221
+ expect(screen.getByTestId('conversation-content').dataset.showWelcome).toBe('true');
222
+ expect(screen.getByTestId('welcome')).toBeTruthy();
223
+ expect(screen.getByTestId('conversation-input')).toBeTruthy();
224
+ });
225
+ });