@nextclaw/ui 0.26.3 → 0.26.5

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 (191) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/.vite/manifest.json +362 -362
  3. package/dist/assets/{account-panel-DIdHjKG-.js → account-panel-BAH7FW6z.js} +1 -1
  4. package/dist/assets/{api-DkLJrSCd.js → api-DjiZvIa0.js} +2 -2
  5. package/dist/assets/app-presenter-provider-CCRFsFYr.js +232 -0
  6. package/dist/assets/{appearance-settings-page-DI8aDY2G.js → appearance-settings-page-CdP86QJ7.js} +1 -1
  7. package/dist/assets/{book-open-BPMkKSci.js → book-open-Bcnn2_uV.js} +1 -1
  8. package/dist/assets/card-Cqs73hGI.js +1 -0
  9. package/dist/assets/{channels-list-page-CCIaTQu7.js → channels-list-page-5qGagKuE.js} +2 -2
  10. package/dist/assets/{circle-dashed-B48a0t6p.js → circle-dashed-EY1ScSMe.js} +1 -1
  11. package/dist/assets/{client-imrVphQb.js → client-CH7w6yRV.js} +1 -1
  12. package/dist/assets/config-split-page-CuEGQL8Z.js +1 -0
  13. package/dist/assets/{confirm-dialog-10Qb3dpZ.js → confirm-dialog-CxfecBzO.js} +1 -1
  14. package/dist/assets/{context-menu-CRqH2SEk.js → context-menu-BsCj2fRs.js} +1 -1
  15. package/dist/assets/{createLucideIcon-B4xj2I2g.js → createLucideIcon-Cfo2ySi5.js} +1 -1
  16. package/dist/assets/{desktop-capabilities-page-CFcT2g8-.js → desktop-capabilities-page-BDYITStl.js} +1 -1
  17. package/dist/assets/{desktop-update-config-CRJd_An1.js → desktop-update-config-e-zhkJuS.js} +1 -1
  18. package/dist/assets/{dialog-BgL3vKJQ.js → dialog-QjQoKbIV.js} +1 -1
  19. package/dist/assets/doc-browser-CLEdkH9z.js +1 -0
  20. package/dist/assets/{doc-browser-context-DH4aWvkS.js → doc-browser-context-DwgG6wrz.js} +1 -1
  21. package/dist/assets/{download-Cr337h4W.js → download-BoaKzDJk.js} +1 -1
  22. package/dist/assets/{es2015-D-CRf3_Z.js → es2015-Bp6cuSGh.js} +1 -1
  23. package/dist/assets/{extensions-CsmJPz7g.js → extensions-CgShSIkd.js} +1 -1
  24. package/dist/assets/{eye-DrQRSAfJ.js → eye-kisejBFH.js} +1 -1
  25. package/dist/assets/{form-actions-BEeWTGUZ.js → form-actions-D9JWSFQu.js} +1 -1
  26. package/dist/assets/{i18n-OGmF0Ws_.js → i18n-Bm3Ov7Fy.js} +2 -2
  27. package/dist/assets/{keyboard-shortcuts-settings-page-DQU56SrW.js → keyboard-shortcuts-settings-page-DZ8A1iLg.js} +1 -1
  28. package/dist/assets/loader-circle-Bd1fTm-x.js +1 -0
  29. package/dist/assets/{login-page-sgivRYG1.js → login-page-C-DvcG3L.js} +1 -1
  30. package/dist/assets/login-page-D99JjH9F.js +1 -0
  31. package/dist/assets/{main-B-oxUq-o.js → main-Bt5r6b21.js} +2 -2
  32. package/dist/assets/main-DXd_aKJV.css +1 -0
  33. package/dist/assets/main-i18n-catalog.config-D0Ll36J_.js +1 -0
  34. package/dist/assets/mcp-marketplace-page-DUoGfSmY.js +1 -0
  35. package/dist/assets/{model-config-page-CDQBhmVr.js → model-config-page-9NNf-mzK.js} +1 -1
  36. package/dist/assets/{navigation-link-D3pzNHeA.js → navigation-link-CjxNC8aT.js} +1 -1
  37. package/dist/assets/panel-app-i18n-catalog.config-Ddf2Kv8A.js +1 -0
  38. package/dist/assets/panelStandalone-BRg7B1EI.css +1 -0
  39. package/dist/assets/{panelStandalone-CGkYeMK5.js → panelStandalone-BaUzY8Wy.js} +2 -2
  40. package/dist/assets/{privacy-settings-page-CjoAkoDt.js → privacy-settings-page-CVMBFuak.js} +1 -1
  41. package/dist/assets/projects-BEsU3box.js +1 -0
  42. package/dist/assets/provider-models-Dm-pmWAA.js +1 -0
  43. package/dist/assets/provider-scoped-model-input-CzrXg4cX.js +1 -0
  44. package/dist/assets/providers-config-page-839uUADZ.js +1 -0
  45. package/dist/assets/{puzzle--RLCeYML.js → puzzle-BGvD2DPZ.js} +1 -1
  46. package/dist/assets/{pwa-DcizGowz.js → pwa-DNNK_8E9.js} +1 -1
  47. package/dist/assets/{refresh-cw-syCVj_nw.js → refresh-cw-BlXDrVP0.js} +1 -1
  48. package/dist/assets/remote-B5qIuUbT.js +1 -0
  49. package/dist/assets/{rotate-cw-BlH9El3x.js → rotate-cw-ei6rPD6z.js} +1 -1
  50. package/dist/assets/{runtime-config-page-Cco9dS73.js → runtime-config-page-Bf-4JYxQ.js} +1 -1
  51. package/dist/assets/{save-BWynuRQ2.js → save-DdF-bXgC.js} +1 -1
  52. package/dist/assets/{search-config-page-Cc74_SAU.js → search-config-page-C0qA3j-T.js} +1 -1
  53. package/dist/assets/{secrets-config-page-raYDzlFC.js → secrets-config-page-DxfZWpnk.js} +1 -1
  54. package/dist/assets/security-config-CItfIQZ4.js +1 -0
  55. package/dist/assets/{select-6Uq4chZW.js → select-CWyjnar7.js} +1 -1
  56. package/dist/assets/service-apps-panel-D3SOkRVv.js +1 -0
  57. package/dist/assets/{settings-2-COvVf1XX.js → settings-2-CrNhfInU.js} +1 -1
  58. package/dist/assets/{status-dot-DZlxa5zW.js → status-dot-C3SRFEta.js} +1 -1
  59. package/dist/assets/{tag-chip-DXHtZ8AS.js → tag-chip-BIuavK2K.js} +1 -1
  60. package/dist/assets/{theme-provider-C5PCDBPB.js → theme-provider-CWUz4yO9.js} +1 -1
  61. package/dist/assets/{trash-2-DazgBvmJ.js → trash-2-hAbJh9VZ.js} +1 -1
  62. package/dist/assets/{use-config-DRRGF9Zb.js → use-config-evzage09.js} +1 -1
  63. package/dist/assets/{use-desktop-capabilities-CdfOUjdE.js → use-desktop-capabilities-BYDjFlpt.js} +1 -1
  64. package/dist/assets/{use-viewport-layout-DdxVnCYc.js → use-viewport-layout-D_9XWGDV.js} +1 -1
  65. package/dist/assets/x-ChPbNv39.js +1 -0
  66. package/dist/index.html +34 -34
  67. package/dist/panel-standalone.html +11 -11
  68. package/package.json +3 -3
  69. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +2 -2
  70. package/src/app/components/layout/sidebar-rail.styles.ts +3 -3
  71. package/src/app/configs/__tests__/app-navigation.config.test.ts +4 -2
  72. package/src/app/configs/app-navigation.config.ts +7 -2
  73. package/src/app/styles/charcoal-theme.css +0 -3
  74. package/src/app/styles/design-system.css +12 -6
  75. package/src/app/styles/island-theme.css +2 -15
  76. package/src/features/app-data/components/app-data-removal-choice.tsx +1 -1
  77. package/src/features/channels/pages/channels-list-page.tsx +1 -1
  78. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +1 -1
  79. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +28 -16
  80. package/src/features/chat/components/conversation/chat-conversation-content.tsx +8 -2
  81. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +6 -4
  82. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +22 -1
  83. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +10 -14
  84. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +38 -7
  85. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -4
  86. package/src/features/chat/components/layout/chat-sidebar.tsx +2 -2
  87. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  88. package/src/features/chat/features/message/components/chat-message-list.container.tsx +12 -3
  89. package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +32 -2
  90. package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +7 -5
  91. package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +7 -3
  92. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +4 -6
  93. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +2 -10
  94. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  95. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +1 -1
  96. package/src/features/chat/features/session/components/chat-mobile-session-row.tsx +1 -1
  97. package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +1 -1
  98. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -1
  99. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +7 -5
  100. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +45 -10
  101. package/src/features/chat/features/session/components/session-header/chat-session-more-actions-menu.tsx +3 -0
  102. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +6 -6
  103. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -4
  104. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +1 -1
  105. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +0 -1
  106. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  107. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +1 -1
  108. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  109. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  110. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +5 -5
  111. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +1 -0
  112. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +1 -1
  113. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +1 -1
  114. package/src/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions.tsx +2 -2
  115. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +2 -2
  116. package/src/features/chat/features/workspace/components/overview/chat-session-continuous-attention.tsx +3 -3
  117. package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +1 -1
  118. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +4 -3
  119. package/src/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item.tsx +2 -1
  120. package/src/features/chat/features/workspace/components/session-cron-job-content.tsx +3 -3
  121. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +2 -2
  122. package/src/features/chat/features/workspace/components/workspace-markdown-outline.tsx +1 -1
  123. package/src/features/cron/components/cron-config.tsx +2 -2
  124. package/src/features/cron/components/cron-job-detail-dialog.tsx +1 -1
  125. package/src/features/cron/components/cron-job-row.tsx +3 -3
  126. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  127. package/src/features/inbox/pages/inbox-page.test.ts +12 -3
  128. package/src/features/inbox/pages/inbox-page.tsx +32 -25
  129. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -2
  130. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +2 -2
  131. package/src/features/marketplace/components/marketplace-list-card.tsx +4 -4
  132. package/src/features/marketplace/components/marketplace-page.tsx +2 -2
  133. package/src/features/marketplace/components/mcp/mcp-marketplace-card.tsx +2 -2
  134. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +3 -3
  135. package/src/features/panel-apps/components/panel-app-list-item.tsx +3 -3
  136. package/src/features/projects/pages/project-home-page.tsx +4 -2
  137. package/src/features/pwa/components/pwa-install-entry.tsx +1 -1
  138. package/src/features/right-panel-resources/components/__tests__/page-resource-sidebar-nav.test.tsx +9 -1
  139. package/src/features/right-panel-resources/components/page-resource-sidebar-nav.tsx +1 -1
  140. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  141. package/src/features/service-apps/components/service-action-row.tsx +3 -3
  142. package/src/features/service-apps/components/service-app-list-item.tsx +4 -4
  143. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  144. package/src/features/settings/components/config/provider-model-list.tsx +5 -5
  145. package/src/features/settings/components/config/provider-model-suggestions-panel.tsx +4 -4
  146. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  147. package/src/features/settings/pages/appearance-settings-page.tsx +2 -2
  148. package/src/features/settings/pages/providers-config-page.tsx +6 -6
  149. package/src/features/side-dock/components/side-dock.tsx +4 -4
  150. package/src/index.css +4 -0
  151. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  152. package/src/platforms/mobile/components/__tests__/mobile-app-shell.test.tsx +9 -0
  153. package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -2
  154. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +3 -3
  155. package/src/platforms/mobile/components/mobile-topbar.tsx +2 -2
  156. package/src/shared/components/common/brand-header.tsx +1 -1
  157. package/src/shared/components/common/searchable-model-input.tsx +2 -2
  158. package/src/shared/components/config-split-page.tsx +3 -3
  159. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +2 -2
  160. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  161. package/src/shared/components/ui/button.tsx +2 -2
  162. package/src/shared/components/ui/card.tsx +1 -1
  163. package/src/shared/components/ui/context-menu/context-menu-items.tsx +3 -3
  164. package/src/shared/components/ui/input.tsx +7 -3
  165. package/src/shared/components/ui/overlays/sheet.tsx +1 -1
  166. package/src/shared/components/ui/popover.tsx +1 -1
  167. package/src/shared/components/ui/select.tsx +2 -2
  168. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -4
  169. package/src/shared/components/ui/tabs.tsx +1 -1
  170. package/src/shared/components/workbench/workbench-surface.tsx +1 -1
  171. package/src/shared/lib/i18n/locales/en-US/chat.json +1 -2
  172. package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -2
  173. package/dist/assets/app-presenter-provider-Co8uKOyW.js +0 -124
  174. package/dist/assets/card-Bqu8zWLq.js +0 -1
  175. package/dist/assets/config-split-page-Bd-bGKsw.js +0 -1
  176. package/dist/assets/doc-browser-52t2_rSF.js +0 -1
  177. package/dist/assets/loader-circle-DbOQWT9C.js +0 -1
  178. package/dist/assets/login-page-DW1AZXqX.js +0 -1
  179. package/dist/assets/main-BpL6OjwM.css +0 -1
  180. package/dist/assets/main-i18n-catalog.config-Bpu5GcwF.js +0 -1
  181. package/dist/assets/mcp-marketplace-page-DSLRpJEB.js +0 -1
  182. package/dist/assets/panel-app-i18n-catalog.config-D44qmtfk.js +0 -1
  183. package/dist/assets/panelStandalone-D-mhAGNa.css +0 -1
  184. package/dist/assets/projects-C7Zxpcoi.js +0 -1
  185. package/dist/assets/provider-models-YW7rIeOD.js +0 -1
  186. package/dist/assets/provider-scoped-model-input-axJ-pTEu.js +0 -1
  187. package/dist/assets/providers-config-page-_QhBXQ4M.js +0 -1
  188. package/dist/assets/remote-BLQU7aY5.js +0 -1
  189. package/dist/assets/security-config-aE_jOwy-.js +0 -1
  190. package/dist/assets/service-apps-panel-jBWAa4qZ.js +0 -1
  191. package/dist/assets/x-CQIp_nJT.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { BrandHeader } from "@/shared/components/common/brand-header";
2
2
  import { StatusBadge } from "@/shared/components/common/status-badge";
3
- import { ChatSidebarListModeSwitch } from "@/features/chat/components/chat-sidebar-list-mode-switch";
3
+ import { ChatSidebarListToolbar } from "@/features/chat/components/layout/chat-sidebar-toolbar";
4
4
  import type {
5
5
  groupSessionsByDate,
6
6
  groupSessionsByProject,
@@ -20,7 +20,6 @@ import {
20
20
  Inbox,
21
21
  PanelLeftClose,
22
22
  PanelLeftOpen,
23
- FolderPlus,
24
23
  } from "lucide-react";
25
24
  import { useInboxUnreadCount } from "@/features/inbox";
26
25
  import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
@@ -148,6 +147,8 @@ export function ChatSidebarDesktopNav({
148
147
  }
149
148
 
150
149
  export function ChatSidebarSessionArea({
150
+ query,
151
+ onQueryChange,
151
152
  variant = 'desktop',
152
153
  defaultSessionType,
153
154
  groups,
@@ -162,6 +163,8 @@ export function ChatSidebarSessionArea({
162
163
  renderSessionItem,
163
164
  sessionTypeOptions,
164
165
  }: {
166
+ query: string;
167
+ onQueryChange: (query: string) => void;
165
168
  variant?: 'desktop' | 'mobile';
166
169
  defaultSessionType: string;
167
170
  groups: ReturnType<typeof groupSessionsByDate>;
@@ -189,20 +192,13 @@ export function ChatSidebarSessionArea({
189
192
 
190
193
  return (
191
194
  <>
192
- <div className={variant === 'mobile' ? 'hidden' : 'flex h-8 items-center justify-end gap-1 px-3'}>
193
- {isProjectFirstView ? (
194
- <IconActionButton
195
- icon={<FolderPlus className="h-3.5 w-3.5" />}
196
- label={t("chatProjectAdd")}
197
- tone="surface"
198
- onClick={onAddProject}
199
- />
200
- ) : null}
201
- <ChatSidebarListModeSwitch
195
+ {variant === 'desktop' ? <ChatSidebarListToolbar
196
+ query={query}
197
+ onQueryChange={onQueryChange}
202
198
  isProjectFirstView={isProjectFirstView}
203
199
  onSelectMode={onSelectMode}
204
- />
205
- </div>
200
+ onAddProject={onAddProject}
201
+ /> : null}
206
202
 
207
203
  <div
208
204
  ref={scrollRef}
@@ -1,4 +1,5 @@
1
- import { useState } from 'react';
1
+ import { useId, useRef, useState } from 'react';
2
+ import { ChatSidebarListModeSwitch } from '@/features/chat/components/chat-sidebar-list-mode-switch';
2
3
  import { Link } from 'react-router-dom';
3
4
  import { AlarmClock, ChevronDown, Clock3, Folder, FolderPlus, Plus, Search, X } from 'lucide-react';
4
5
  import { Button } from '@/shared/components/ui/button';
@@ -19,7 +20,7 @@ type ChatSidebarToolbarProps = {
19
20
  collapsed?: boolean;
20
21
  };
21
22
 
22
- export function ChatSidebarDesktopToolbar({ query, onQueryChange, onCreateSession, collapsed }: ChatSidebarToolbarProps) {
23
+ export function ChatSidebarDesktopToolbar({ onCreateSession, collapsed }: Pick<ChatSidebarToolbarProps, 'onCreateSession' | 'collapsed'>) {
23
24
  if (collapsed) {
24
25
  return <div className="px-2 pb-2">
25
26
  <IconActionButton icon={<Plus className={SIDEBAR_RAIL_ICON_CLASS} />} label={t('chatSidebarNewTask')}
@@ -32,14 +33,44 @@ export function ChatSidebarDesktopToolbar({ query, onQueryChange, onCreateSessio
32
33
  <Plus className="mr-1.5 h-4 w-4" />{t('chatSidebarNewTask')}
33
34
  </Button>
34
35
  </div>
35
- <div className="px-4 pb-2">
36
+ </>;
37
+ }
38
+
39
+ export function ChatSidebarListToolbar({ query, onQueryChange, isProjectFirstView, onSelectMode, onAddProject }: {
40
+ query: string;
41
+ onQueryChange: (query: string) => void;
42
+ isProjectFirstView: boolean;
43
+ onSelectMode: (mode: 'time-first' | 'project-first') => void;
44
+ onAddProject: () => void;
45
+ }) {
46
+ const [searchOpen, setSearchOpen] = useState(Boolean(query));
47
+ const searchButtonRef = useRef<HTMLButtonElement>(null);
48
+ const searchId = useId();
49
+ const closeSearch = () => {
50
+ onQueryChange('');
51
+ setSearchOpen(false);
52
+ searchButtonRef.current?.focus();
53
+ };
54
+ return <div className="shrink-0 px-3">
55
+ <div className="flex h-8 items-center justify-between gap-1">
56
+ <ChatSidebarListModeSwitch isProjectFirstView={isProjectFirstView} onSelectMode={onSelectMode} />
57
+ <IconActionGroup>
58
+ {isProjectFirstView ? <IconActionButton icon={<FolderPlus className="h-3.5 w-3.5" />} label={t('chatProjectAdd')} onClick={onAddProject} /> : null}
59
+ <IconActionButton ref={searchButtonRef} icon={<Search className="h-3.5 w-3.5" />} label={t('chatSidebarSearchPlaceholder')}
60
+ aria-expanded={searchOpen} aria-controls={searchId} onClick={() => { if (searchOpen) closeSearch(); else setSearchOpen(true); }} />
61
+ </IconActionGroup>
62
+ </div>
63
+ {searchOpen ? <div id={searchId} className="pb-2 pt-1">
36
64
  <div className="relative">
37
65
  <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/60" />
38
- <Input data-theme-control="chat-search" value={query} onChange={event => onQueryChange(event.target.value)}
39
- placeholder={t('chatSidebarSearchPlaceholder')} className="h-8 rounded-lg border-0 bg-background/55 pl-8 text-xs shadow-none hover:bg-background/75" />
66
+ <Input autoFocus data-theme-control="chat-search" density="compact" value={query} onChange={event => onQueryChange(event.target.value)}
67
+ onKeyDown={event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeSearch(); } }}
68
+ aria-label={t('chatSidebarSearchPlaceholder')} placeholder={t('chatSidebarSearchPlaceholder')} className="h-8 rounded-lg border-0 bg-background/55 pl-8 pr-9 shadow-none hover:bg-[var(--interaction-hover)]" />
69
+ <IconActionButton icon={<X className="h-3.5 w-3.5" />} label={t('chatSidebarCloseSearch')} size="sm"
70
+ className="absolute right-1 top-1/2 -translate-y-1/2" onClick={closeSearch} />
40
71
  </div>
41
- </div>
42
- </>;
72
+ </div> : null}
73
+ </div>;
43
74
  }
44
75
 
45
76
  export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps & {
@@ -102,7 +102,7 @@ export function ChatSidebarUtilityMenu({
102
102
  <button
103
103
  type="button"
104
104
  aria-label={t("settingsMenu")}
105
- className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-background/70 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
105
+ className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-[var(--interaction-hover)] hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
106
106
  >
107
107
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
108
108
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
@@ -118,7 +118,7 @@ export function ChatSidebarUtilityMenu({
118
118
  <NavLink
119
119
  to="/settings"
120
120
  onClick={() => onOpenChange(false)}
121
- 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-gray-200/60 hover:text-gray-900"
121
+ 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-[var(--interaction-hover)] hover:text-gray-900"
122
122
  >
123
123
  <Settings className="h-4 w-4 text-muted-foreground/70" />
124
124
  <span className="flex-1 text-left">{t("settings")}</span>
@@ -134,7 +134,7 @@ export function ChatSidebarUtilityMenu({
134
134
  action();
135
135
  onOpenChange(false);
136
136
  }}
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-gray-200/60 hover:text-gray-900"
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-[var(--interaction-hover)] hover:text-gray-900"
138
138
  >
139
139
  <Icon className="h-4 w-4 text-muted-foreground/70" />
140
140
  <span className="flex-1 text-left">{label}</span>
@@ -197,7 +197,7 @@ function ChatSidebarUtilitySelect<Value extends string>({
197
197
  >
198
198
  <SelectTrigger
199
199
  aria-label={label}
200
- 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-gray-200/60 hover:text-gray-900 focus:ring-0"
200
+ 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-[var(--interaction-hover)] hover:text-gray-900 focus:ring-0"
201
201
  indicator={<ChevronRight className="h-4 w-4 text-muted-foreground/70" />}
202
202
  >
203
203
  <div className="flex min-w-0 items-center gap-2.5">
@@ -304,9 +304,7 @@ export function ChatSidebar({
304
304
  />
305
305
  ) : (
306
306
  <ChatSidebarDesktopToolbar
307
- query={listSnapshot.query}
308
307
  onCreateSession={createSessionAndOpenIfNeeded}
309
- onQueryChange={presenter.chatSessionListManager.setQuery}
310
308
  collapsed={shouldCollapse}
311
309
  />
312
310
  )}
@@ -316,6 +314,8 @@ export function ChatSidebar({
316
314
  ) : null}
317
315
 
318
316
  <ChatSidebarSessionArea
317
+ query={listSnapshot.query}
318
+ onQueryChange={presenter.chatSessionListManager.setQuery}
319
319
  variant={variant}
320
320
  defaultSessionType={newSessionTypePreference.selectedSessionType}
321
321
  groups={groups}
@@ -91,7 +91,7 @@ function QueuedInputIconButton({
91
91
  aria-label={label}
92
92
  disabled={disabled}
93
93
  onClick={onClick}
94
- className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:text-muted-foreground/50"
94
+ className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:text-muted-foreground/50"
95
95
  >
96
96
  {children}
97
97
  </button>
@@ -89,6 +89,7 @@ type ChatMessageListContainerProps = {
89
89
  onLoadMessageDetails?: (messageId: string) => Promise<void>;
90
90
  sessionKey: string | null;
91
91
  scrollRef: RefObject<HTMLDivElement | null>;
92
+ turnAnchorRef?: RefObject<HTMLDivElement>;
92
93
  className?: string;
93
94
  };
94
95
 
@@ -121,8 +122,6 @@ class ChatMessageViewModelAdapter {
121
122
  message,
122
123
  processedLabel,
123
124
  language,
124
- failedLabel: t("chatProcessSummaryError", language),
125
- stoppedLabel: t("chatProcessSummaryStopped", language),
126
125
  });
127
126
  const processSummaryLabel = processSummary?.label ?? null;
128
127
  const executionPresentation = buildChatMessageExecutionPresentation({
@@ -231,6 +230,10 @@ function isAwaitingAssistantOutputRow(
231
230
  return item.kind === "typing" || item.key === activeRowKey;
232
231
  }
233
232
 
233
+ function resolveTurnAnchor(item: ChatTimelineItem, messageId: string | undefined, ref?: RefObject<HTMLDivElement>) {
234
+ return item.kind === "message" && item.message.id === messageId ? ref : undefined;
235
+ }
236
+
234
237
  export function ChatMessageListContainer({
235
238
  canContinue = false,
236
239
  messages: rawMessages,
@@ -241,6 +244,7 @@ export function ChatMessageListContainer({
241
244
  onEditMessage,
242
245
  onLoadMessageDetails,
243
246
  scrollRef,
247
+ turnAnchorRef,
244
248
  sessionKey,
245
249
  className,
246
250
  }: ChatMessageListContainerProps) {
@@ -386,11 +390,13 @@ export function ChatMessageListContainer({
386
390
  );
387
391
  const [focusedRowKey, setFocusedRowKey] = useState<string | null>(null);
388
392
  const activeRowKey = activeAssistantMessage ? `message:${activeAssistantMessage.id}` : null;
393
+ const turnMessageId = messages.findLast((message) => message.role === "user")?.id;
389
394
  const { containerRef, virtualizer } = useChatMessageVirtualizer({
390
395
  rows: virtualRows,
391
396
  scrollRef,
392
397
  activeRowKey,
393
398
  focusedRowKey,
399
+ anchorRowKey: turnAnchorRef && turnMessageId ? `message:${turnMessageId}` : null,
394
400
  });
395
401
  const { handleAttachmentOpen, handleInlineTokenClick } =
396
402
  useChatInlineTokenActions({ selectedSession, sessionKey });
@@ -448,7 +454,10 @@ export function ChatMessageListContainer({
448
454
  ) : item.kind === "observation-event" ? (
449
455
  <ChatMessageObservationEvent event={item.event} />
450
456
  ) : (
451
- <div className={item.kind === "message" ? "pb-5" : undefined}>
457
+ <div
458
+ className={item.kind === "message" ? "pb-5" : undefined}
459
+ ref={resolveTurnAnchor(item, turnMessageId, turnAnchorRef)}
460
+ >
452
461
  <ChatMessageList
453
462
  assistantAvatarIcon={assistantAvatarIcon}
454
463
  showAssistantHeader={showAssistantHeader}
@@ -1,5 +1,5 @@
1
1
  import { Profiler, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { ChatMessageList, type ChatMessageTexts } from "@nextclaw/agent-chat-ui";
2
+ import { ChatMessageList, type ChatMessageTexts, type ChatMessageListProps } from "@nextclaw/agent-chat-ui";
3
3
  import {
4
4
  createChatToolCallStressViewModel,
5
5
  DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG,
@@ -40,6 +40,8 @@ const STRESS_TEXTS: ChatMessageTexts = {
40
40
  },
41
41
  toolActivityFailedLabel: "failed",
42
42
  toolActivityCancelledLabel: "cancelled",
43
+ toolPayloadLoadingLabel: "正在加载详情",
44
+ toolPayloadLoadFailedLabel: "详情加载失败,点击重试",
43
45
  };
44
46
 
45
47
  function readBoundedNumber(value: string, fallback: number, maximum: number): number {
@@ -53,6 +55,18 @@ export function ChatToolCallStressHarness() {
53
55
  const [config, setConfig] = useState(DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG);
54
56
  const [renderVersion, setRenderVersion] = useState(0);
55
57
  const [metric, setMetric] = useState<StressMetric | null>(null);
58
+ const [detailState, setDetailState] = useState<
59
+ ReturnType<NonNullable<ChatMessageListProps["resolveMessageToolPayloadState"]>>
60
+ >("ready");
61
+ const detailTimer = useRef<ReturnType<typeof setTimeout>>();
62
+ const failNextRequest = useRef(false);
63
+ useEffect(() => () => clearTimeout(detailTimer.current), []);
64
+ const resetDetails = (fail: boolean) => {
65
+ clearTimeout(detailTimer.current);
66
+ failNextRequest.current = fail;
67
+ setDetailState("summary");
68
+ setRenderVersion((value) => value + 1);
69
+ };
56
70
  const pendingMeasurement = useRef<{ action: string; startedAt: number } | null>(null);
57
71
  const viewModel = useMemo(
58
72
  () => createChatToolCallStressViewModel(config, `chat-tool-call-stress-message-${renderVersion}`),
@@ -104,6 +118,12 @@ export function ChatToolCallStressHarness() {
104
118
  ))}
105
119
  </div>
106
120
  <div className="flex flex-wrap gap-2">
121
+ <button type="button" className="rounded border border-border px-3 py-1.5 text-sm" onClick={() => resetDetails(false)}>
122
+ 演练慢加载(5 秒)
123
+ </button>
124
+ <button type="button" className="rounded border border-border px-3 py-1.5 text-sm" onClick={() => resetDetails(true)}>
125
+ 演练失败后重试
126
+ </button>
107
127
  <button
108
128
  type="button"
109
129
  className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground"
@@ -165,10 +185,20 @@ export function ChatToolCallStressHarness() {
165
185
  >
166
186
  <section data-stress-message="true" className="rounded-lg border border-border p-4">
167
187
  <ChatMessageList
168
- messages={[viewModel]}
188
+ layout="flat"
189
+ messages={[{ ...viewModel, processSummary: { label: "已处理 2分钟58秒" } }]}
169
190
  isSending={false}
170
191
  hasAssistantDraft={false}
171
192
  texts={STRESS_TEXTS}
193
+ resolveMessageToolPayloadState={() => detailState}
194
+ onMessageToolPayloadRequest={() => {
195
+ if (detailState === "loading" || detailState === "ready") return;
196
+ setDetailState("loading");
197
+ detailTimer.current = setTimeout(() => {
198
+ setDetailState(failNextRequest.current ? "error" : "ready");
199
+ failNextRequest.current = false;
200
+ }, 5_000);
201
+ }}
172
202
  />
173
203
  </section>
174
204
  </Profiler>
@@ -207,9 +207,11 @@ describe("useChatMessageVirtualizer", () => {
207
207
  expect(scrollElement.scrollTop - (anchored?.start ?? 0)).toBe(anchorOffset);
208
208
  });
209
209
 
210
- it.each([11, 20])(
211
- "does not reclaim sticky escape when the active row grows %d px from the bottom",
212
- async (escapeDistance) => {
210
+ it.each([-200, 0, 11, 20].flatMap((distance) => [
211
+ [distance, 100], [distance, -105],
212
+ ]))(
213
+ "leaves bottom following to the scroll owner at distance %d with height delta %d",
214
+ async (escapeDistance, delta) => {
213
215
  const scrollElement = document.createElement("div");
214
216
  Object.defineProperties(scrollElement, {
215
217
  clientHeight: { configurable: true, value: 800 },
@@ -248,14 +250,14 @@ describe("useChatMessageVirtualizer", () => {
248
250
  expect(result.current.virtualizer.scrollOffset).toBe(escapedScrollTop),
249
251
  );
250
252
 
251
- act(() => result.current.virtualizer.resizeItem(9, 1_100));
253
+ act(() => result.current.virtualizer.resizeItem(9, 1_000 + delta));
252
254
 
253
255
  expect(scrollElement.scrollTop).toBe(escapedScrollTop);
254
256
  expect(
255
257
  result.current.virtualizer.getTotalSize() -
256
258
  scrollElement.scrollTop -
257
259
  scrollElement.clientHeight,
258
- ).toBe(escapeDistance + 100);
260
+ ).toBe(escapeDistance + delta);
259
261
  },
260
262
  );
261
263
  });
@@ -12,14 +12,15 @@ export function useChatMessageVirtualizer(params: {
12
12
  scrollRef: RefObject<HTMLDivElement | null>;
13
13
  activeRowKey?: string | null;
14
14
  focusedRowKey?: string | null;
15
+ anchorRowKey?: string | null;
15
16
  }) {
16
- const { activeRowKey, focusedRowKey, rows, scrollRef } = params;
17
+ const { activeRowKey, focusedRowKey, anchorRowKey, rows, scrollRef } = params;
17
18
  const pinnedIndexes = useMemo(
18
19
  () =>
19
20
  rows.flatMap((row, index) =>
20
- row.key === activeRowKey || row.key === focusedRowKey ? [index] : [],
21
+ row.key === activeRowKey || row.key === focusedRowKey || row.key === anchorRowKey ? [index] : [],
21
22
  ),
22
- [activeRowKey, focusedRowKey, rows],
23
+ [activeRowKey, focusedRowKey, anchorRowKey, rows],
23
24
  );
24
25
  const rangeExtractor = useCallback(
25
26
  (range: Parameters<typeof defaultRangeExtractor>[0]) =>
@@ -35,6 +36,9 @@ export function useChatMessageVirtualizer(params: {
35
36
  // eslint-disable-next-line react-hooks/incompatible-library
36
37
  const virtualizer = useVirtualizer({
37
38
  anchorTo: "end",
39
+ // Preserve prepend anchoring, but let useStickyBottomScroll own end following.
40
+ // Virtual content excludes the reserved turn space and cannot detect its bottom.
41
+ scrollEndThreshold: -1,
38
42
  count: rows.length,
39
43
  directDomUpdates: true,
40
44
  estimateSize: () => ESTIMATED_CHAT_MESSAGE_ROW_HEIGHT,
@@ -31,8 +31,6 @@ const baseAssistantMessage = {
31
31
 
32
32
  const summaryLabels = {
33
33
  processedLabel: "已处理",
34
- failedLabel: "处理失败",
35
- stoppedLabel: "已停止",
36
34
  language: "zh" as const,
37
35
  };
38
36
 
@@ -135,8 +133,8 @@ describe("buildChatMessageProcessSummary", () => {
135
133
  });
136
134
 
137
135
  it.each([
138
- ["failed", "处理失败"], ["aborted", "已停止"], ["completed", "已处理"],
139
- ] as const)("uses the recorded %s outcome", (outcome, expected) => {
136
+ ["failed", "已处理"], ["aborted", "已处理"], ["completed", "已处理"],
137
+ ] as const)("keeps the process summary neutral for a %s outcome", (outcome, expected) => {
140
138
  const message: NcpMessage = {
141
139
  ...baseAssistantMessage,
142
140
  metadata: { ai_execution: createUnavailableNcpAiExecutionMetadata({
@@ -146,9 +144,9 @@ describe("buildChatMessageProcessSummary", () => {
146
144
  expect(buildChatMessageProcessSummary({ ...summaryLabels, message })?.label).toBe(expected);
147
145
  });
148
146
 
149
- it("shows failure for an error message without execution metadata", () => {
147
+ it("keeps the process summary neutral for an error message", () => {
150
148
  expect(buildChatMessageProcessSummary({
151
149
  ...summaryLabels, message: { ...baseAssistantMessage, status: "error" },
152
- })?.label).toBe("处理失败");
150
+ })?.label).toBe("已处理");
153
151
  });
154
152
  });
@@ -1,12 +1,10 @@
1
- import { readNcpAiExecutionMetadata, type NcpMessage } from "@nextclaw/ncp";
1
+ import type { NcpMessage } from "@nextclaw/ncp";
2
2
  import type { ChatMessageProcessSummarySource } from "@/features/chat/types/chat-message.types";
3
3
  import { t, type I18nLanguage } from "@/shared/lib/i18n";
4
4
 
5
5
  type BuildChatMessageProcessSummaryParams = {
6
6
  message: NcpMessage;
7
7
  processedLabel: string;
8
- failedLabel: string;
9
- stoppedLabel: string;
10
8
  language: I18nLanguage;
11
9
  };
12
10
 
@@ -78,8 +76,6 @@ function formatLifecycleDuration(message: NcpMessage, language: I18nLanguage): s
78
76
  * and must not be mixed into this summary.
79
77
  */
80
78
  export function buildChatMessageProcessSummary({
81
- failedLabel,
82
- stoppedLabel,
83
79
  message,
84
80
  processedLabel,
85
81
  language,
@@ -88,11 +84,7 @@ export function buildChatMessageProcessSummary({
88
84
  return undefined;
89
85
  }
90
86
  const duration = formatLifecycleDuration(message, language);
91
- const outcome = readNcpAiExecutionMetadata(message.metadata)?.outcome;
92
- const statusLabel = message.status === "error" || outcome === "failed"
93
- ? failedLabel
94
- : outcome === "aborted" ? stoppedLabel : processedLabel;
95
87
  return {
96
- label: duration ? `${statusLabel} ${duration}` : statusLabel,
88
+ label: duration ? `${processedLabel} ${duration}` : processedLabel,
97
89
  };
98
90
  }
@@ -189,6 +189,7 @@ export function buildChatMessageTexts(
189
189
  copiedMessageLabel: t("chatMessageCopied"),
190
190
  excerptCharacterCountTemplate: t("chatWorkspaceExcerptCharacterCount"),
191
191
  mermaidDiagramLabel: t("chatMermaidDiagram"),
192
+ frontmatterLabel: t("chatMarkdownFrontmatter"),
192
193
  mermaidExpandLabel: t("chatMermaidExpand"),
193
194
  mermaidLoadingLabel: t("chatMermaidLoading"),
194
195
  mermaidRenderErrorLabel: t("chatMermaidRenderError"),
@@ -113,7 +113,7 @@ describe("ChatSidebarProjectGroups", () => {
113
113
  const projectRow = projectLink.parentElement?.parentElement;
114
114
  expect(container.firstElementChild?.className).toBe("space-y-0.5");
115
115
  expect(projectRow?.className).toContain("h-8");
116
- expect(projectRow?.className).toContain("hover:bg-[hsl(var(--gray-200)/0.6)]");
116
+ expect(projectRow?.className).toContain("hover:bg-[var(--sidebar-interaction-hover)]");
117
117
  const projectContent = projectLink.parentElement;
118
118
  expect(projectContent?.classList.contains("pr-14")).toBe(false);
119
119
  expect(projectContent?.className).toContain("group-hover/project:pr-20");
@@ -28,7 +28,7 @@ export function ChatMobileSessionRow(props: ChatSidebarSessionItemProps) {
28
28
  onStartEditing, onTogglePinned, onDelete } = props;
29
29
  return (
30
30
  <div className="relative flex min-w-0 items-center">
31
- <button type="button" onClick={onSelect} className="flex min-h-[76px] min-w-0 flex-1 items-center gap-3 py-3 pl-4 pr-1 text-left active:bg-muted">
31
+ <button type="button" onClick={onSelect} className="flex min-h-[76px] min-w-0 flex-1 items-center gap-3 py-3 pl-4 pr-1 text-left active:bg-[var(--interaction-hover)]">
32
32
  <span className="relative shrink-0">
33
33
  {agentAvatarUrl || (agentId && agentId.toLowerCase() !== 'main') ? (
34
34
  <AgentAvatar agentId={agentId || 'main'} displayName={agentLabel} avatarUrl={agentAvatarUrl} className="h-12 w-12 rounded-xl text-lg" />
@@ -30,7 +30,7 @@ export function ChatSidebarContextCard({
30
30
  align="start"
31
31
  sideOffset={10}
32
32
  collisionPadding={12}
33
- className="pointer-events-none w-64 rounded-xl border-border/80 bg-popover p-3 text-popover-foreground shadow-[0_18px_48px_-24px_rgba(15,23,42,0.45)]"
33
+ className="pointer-events-none w-64 p-3"
34
34
  >
35
35
  <div className="truncate text-sm font-semibold leading-5">
36
36
  {title}
@@ -175,6 +175,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
175
175
  {group.projectId ? (
176
176
  <Link
177
177
  to={`/projects/${encodeURIComponent(group.projectId)}/overview`}
178
+ state={{ returnTo: `${location.pathname}${location.search}` }}
178
179
  className="flex h-full min-w-0 flex-1 items-center gap-1.5 rounded-md px-1 text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
179
180
  aria-current={isSelected ? "page" : undefined}
180
181
  title={group.projectRoot}
@@ -233,7 +234,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
233
234
  />
234
235
  }
235
236
  label={pinLabel}
236
- tooltipSide="right"
237
+ tooltipSide="top"
237
238
  onClick={() =>
238
239
  presenter.chatSessionListManager.toggleProjectPinned(
239
240
  group.projectRoot,
@@ -73,6 +73,7 @@ function ChatSidebarSessionEditingView({
73
73
  return (
74
74
  <div className="space-y-2">
75
75
  <Input
76
+ density="compact"
76
77
  value={draftLabel}
77
78
  onChange={(event) => onDraftLabelChange(event.target.value)}
78
79
  onKeyDown={(event) => {
@@ -85,7 +86,7 @@ function ChatSidebarSessionEditingView({
85
86
  }
86
87
  }}
87
88
  placeholder={t("sessionsLabelPlaceholder")}
88
- className="h-8 rounded-lg border-border bg-background text-xs"
89
+ className="h-8 rounded-lg border-border bg-background"
89
90
  autoFocus
90
91
  disabled={isSaving}
91
92
  />
@@ -248,7 +249,7 @@ function ChatSidebarSessionDisplayView({
248
249
  label={t(
249
250
  isPinned ? "chatSidebarUnpinSession" : "chatSidebarPinSession",
250
251
  )}
251
- tooltipSide="right"
252
+ tooltipSide="top"
252
253
  onClick={(event) => {
253
254
  event.stopPropagation();
254
255
  onTogglePinned();
@@ -259,7 +260,7 @@ function ChatSidebarSessionDisplayView({
259
260
  tone="strong"
260
261
  icon={<Pencil className="h-3.5 w-3.5" />}
261
262
  label={t("edit")}
262
- tooltipSide="right"
263
+ tooltipSide="top"
263
264
  onClick={(event) => {
264
265
  event.stopPropagation();
265
266
  onStartEditing();
@@ -270,6 +271,7 @@ function ChatSidebarSessionDisplayView({
270
271
  sessionTitle={title}
271
272
  triggerSize="sm"
272
273
  triggerTone="strong"
274
+ triggerTooltipSide="top"
273
275
  >
274
276
  <ChatSessionHeaderMenuItem
275
277
  icon={Trash2}
@@ -319,8 +321,8 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
319
321
  ? 'w-full border-b border-border/50 bg-background text-left'
320
322
  : cn("w-full px-3 py-2 text-left text-[13px]", SIDEBAR_ITEM_SURFACE_CLASS, active && SIDEBAR_ITEM_ACTIVE_SURFACE_CLASS),
321
323
  variant === 'mobile' && (active
322
- ? "bg-background/90 font-medium text-foreground"
323
- : "text-foreground/80 hover:bg-background/65 hover:text-foreground"),
324
+ ? "bg-[var(--session-interaction-selection)] font-medium text-foreground"
325
+ : "text-foreground/80 hover:bg-[var(--session-interaction-hover)] hover:text-foreground"),
324
326
  active && "font-medium",
325
327
  )}
326
328
  >
@@ -1,5 +1,6 @@
1
- import type { ReactNode } from "react";
2
- import { MessageSquareText } from "lucide-react";
1
+ import { useState, type ReactNode } from "react";
2
+ import { ChatCollapsibleContent } from "@nextclaw/agent-chat-ui";
3
+ import { ChevronRight, MessageSquareText } from "lucide-react";
3
4
  import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
4
5
  import type {
5
6
  ChatSidebarDateGroup,
@@ -8,6 +9,7 @@ import type {
8
9
  import { ChatSidebarProjectGroups } from "@/features/chat/features/session/components/chat-sidebar-project-groups";
9
10
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
10
11
  import { t } from "@/shared/lib/i18n";
12
+ import { cn } from "@/shared/lib/utils";
11
13
 
12
14
  type SessionTypeOption = ChatSessionTypeOption;
13
15
 
@@ -32,6 +34,41 @@ function ChatSidebarEmptyState({ label }: { label: string }) {
32
34
  );
33
35
  }
34
36
 
37
+ function ChatSidebarSessionGroup({ group, variant, renderSessionItem }: {
38
+ group: ChatSidebarDateGroup;
39
+ variant: 'desktop' | 'mobile';
40
+ renderSessionItem: ChatSidebarSessionListProps['renderSessionItem'];
41
+ }) {
42
+ const [open, setOpen] = useState(true);
43
+ return (
44
+ <div>
45
+ <button
46
+ type="button"
47
+ aria-expanded={open}
48
+ onClick={() => setOpen((value) => !value)}
49
+ className={cn(
50
+ 'group/session-heading flex w-full cursor-pointer items-center justify-between gap-2 rounded-xl text-left text-xs font-medium text-muted-foreground outline-none transition-colors duration-base hover:bg-[var(--interaction-hover)] hover:text-foreground',
51
+ variant === 'mobile' ? 'min-h-9 px-4' : 'min-h-7 px-2',
52
+ )}
53
+ >
54
+ <span className="truncate">{group.label}</span>
55
+ <ChevronRight
56
+ aria-hidden="true"
57
+ className={cn(
58
+ 'h-3 w-3 shrink-0 opacity-0 transition-[opacity,transform] duration-200 group-hover/session-heading:opacity-100 motion-reduce:transition-none',
59
+ open && 'rotate-90',
60
+ )}
61
+ />
62
+ </button>
63
+ <ChatCollapsibleContent open={open}>{() => (
64
+ <div className={variant === 'mobile' ? undefined : 'space-y-0.5'}>
65
+ {group.items.map(renderSessionItem)}
66
+ </div>
67
+ )}</ChatCollapsibleContent>
68
+ </div>
69
+ );
70
+ }
71
+
35
72
  export function ChatSidebarSessionList({
36
73
  variant = 'desktop',
37
74
  defaultSessionType,
@@ -72,14 +109,12 @@ export function ChatSidebarSessionList({
72
109
  return (
73
110
  <div className={variant === 'mobile' ? undefined : 'space-y-2'}>
74
111
  {groups.map((group) => (
75
- <div key={group.label}>
76
- <div className={variant === 'mobile' ? 'px-4 py-1 text-[11px] text-muted-foreground' : 'px-2 pb-1 pt-0.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/65'}>
77
- {group.label}
78
- </div>
79
- <div className={variant === 'mobile' ? undefined : 'space-y-0.5'}>
80
- {group.items.map(renderSessionItem)}
81
- </div>
82
- </div>
112
+ <ChatSidebarSessionGroup
113
+ key={group.label}
114
+ group={group}
115
+ variant={variant}
116
+ renderSessionItem={renderSessionItem}
117
+ />
83
118
  ))}
84
119
  </div>
85
120
  );