@nextclaw/ui 0.26.3 → 0.26.4

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 (170) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/.vite/manifest.json +329 -329
  3. package/dist/assets/{account-panel-DIdHjKG-.js → account-panel-DVi_jhkG.js} +1 -1
  4. package/dist/assets/{api-DkLJrSCd.js → api-BZvNpFOS.js} +1 -1
  5. package/dist/assets/app-presenter-provider-BcYL-VNe.js +124 -0
  6. package/dist/assets/{appearance-settings-page-DI8aDY2G.js → appearance-settings-page-AZNBzLmT.js} +1 -1
  7. package/dist/assets/{book-open-BPMkKSci.js → book-open-CmRdT3N1.js} +1 -1
  8. package/dist/assets/card-CdRBL1E0.js +1 -0
  9. package/dist/assets/{channels-list-page-CCIaTQu7.js → channels-list-page-eZN74FN3.js} +2 -2
  10. package/dist/assets/{circle-dashed-B48a0t6p.js → circle-dashed-HOgt3gIJ.js} +1 -1
  11. package/dist/assets/{client-imrVphQb.js → client-DKKbdJ6Q.js} +1 -1
  12. package/dist/assets/config-split-page-BA3V6b3Q.js +1 -0
  13. package/dist/assets/{confirm-dialog-10Qb3dpZ.js → confirm-dialog-8vqYZ0wy.js} +1 -1
  14. package/dist/assets/{context-menu-CRqH2SEk.js → context-menu-DHVVdHzN.js} +1 -1
  15. package/dist/assets/{createLucideIcon-B4xj2I2g.js → createLucideIcon-DE0sB9Nu.js} +1 -1
  16. package/dist/assets/{desktop-capabilities-page-CFcT2g8-.js → desktop-capabilities-page-B9-B9k1A.js} +1 -1
  17. package/dist/assets/{desktop-update-config-CRJd_An1.js → desktop-update-config-Cfr3IRUe.js} +1 -1
  18. package/dist/assets/{dialog-BgL3vKJQ.js → dialog-BUfWO8hr.js} +1 -1
  19. package/dist/assets/{doc-browser-context-DH4aWvkS.js → doc-browser-context-ClLgMbxd.js} +1 -1
  20. package/dist/assets/doc-browser-w8q1j0jL.js +1 -0
  21. package/dist/assets/{download-Cr337h4W.js → download-CKqS1Ksj.js} +1 -1
  22. package/dist/assets/{es2015-D-CRf3_Z.js → es2015-BiB05Sav.js} +1 -1
  23. package/dist/assets/{extensions-CsmJPz7g.js → extensions-vPD4dGqt.js} +1 -1
  24. package/dist/assets/{eye-DrQRSAfJ.js → eye-bKzylu3p.js} +1 -1
  25. package/dist/assets/{form-actions-BEeWTGUZ.js → form-actions-CCoxz33Q.js} +1 -1
  26. package/dist/assets/{keyboard-shortcuts-settings-page-DQU56SrW.js → keyboard-shortcuts-settings-page-D8rosOVh.js} +1 -1
  27. package/dist/assets/loader-circle-Clq1Gtb1.js +1 -0
  28. package/dist/assets/{login-page-sgivRYG1.js → login-page-BT3GV3Sb.js} +1 -1
  29. package/dist/assets/login-page-DKId3By8.js +1 -0
  30. package/dist/assets/{main-B-oxUq-o.js → main-bEWCGDHr.js} +2 -2
  31. package/dist/assets/main-kqMRTwOs.css +1 -0
  32. package/dist/assets/mcp-marketplace-page-CZIP6j3L.js +1 -0
  33. package/dist/assets/{model-config-page-CDQBhmVr.js → model-config-page-D21a-BO0.js} +1 -1
  34. package/dist/assets/{navigation-link-D3pzNHeA.js → navigation-link-DC-Pku0Y.js} +1 -1
  35. package/dist/assets/panelStandalone-BRg7B1EI.css +1 -0
  36. package/dist/assets/{panelStandalone-CGkYeMK5.js → panelStandalone-CX8X2hjh.js} +2 -2
  37. package/dist/assets/{privacy-settings-page-CjoAkoDt.js → privacy-settings-page-e0LFHZJm.js} +1 -1
  38. package/dist/assets/{projects-C7Zxpcoi.js → projects-Rki7Fmth.js} +1 -1
  39. package/dist/assets/provider-models-DFQgF4hS.js +1 -0
  40. package/dist/assets/provider-scoped-model-input-DaONL0j_.js +1 -0
  41. package/dist/assets/providers-config-page-CEOhQlMe.js +1 -0
  42. package/dist/assets/{puzzle--RLCeYML.js → puzzle-D_TiFXOv.js} +1 -1
  43. package/dist/assets/{pwa-DcizGowz.js → pwa-C9ac_1dg.js} +1 -1
  44. package/dist/assets/{refresh-cw-syCVj_nw.js → refresh-cw-BG8VfcdA.js} +1 -1
  45. package/dist/assets/remote-DkSx5x4R.js +1 -0
  46. package/dist/assets/{rotate-cw-BlH9El3x.js → rotate-cw-DPEegUU2.js} +1 -1
  47. package/dist/assets/{runtime-config-page-Cco9dS73.js → runtime-config-page-DDT1dVLt.js} +1 -1
  48. package/dist/assets/{save-BWynuRQ2.js → save-CPlctBjT.js} +1 -1
  49. package/dist/assets/{search-config-page-Cc74_SAU.js → search-config-page-CDwmd6eX.js} +1 -1
  50. package/dist/assets/{secrets-config-page-raYDzlFC.js → secrets-config-page-1OvgmwPv.js} +1 -1
  51. package/dist/assets/security-config-Pv4CM-kw.js +1 -0
  52. package/dist/assets/{select-6Uq4chZW.js → select-Y9IEbx9x.js} +1 -1
  53. package/dist/assets/service-apps-panel-DPrLeF3N.js +1 -0
  54. package/dist/assets/{settings-2-COvVf1XX.js → settings-2-CrGx9_De.js} +1 -1
  55. package/dist/assets/{status-dot-DZlxa5zW.js → status-dot-Cb8ewlUr.js} +1 -1
  56. package/dist/assets/{tag-chip-DXHtZ8AS.js → tag-chip-D-6aPuuh.js} +1 -1
  57. package/dist/assets/{theme-provider-C5PCDBPB.js → theme-provider-BjF1Eowc.js} +1 -1
  58. package/dist/assets/{trash-2-DazgBvmJ.js → trash-2-DER2EJKK.js} +1 -1
  59. package/dist/assets/{use-config-DRRGF9Zb.js → use-config-BM5jC65L.js} +1 -1
  60. package/dist/assets/{use-desktop-capabilities-CdfOUjdE.js → use-desktop-capabilities-ezKJwKqg.js} +1 -1
  61. package/dist/assets/{use-viewport-layout-DdxVnCYc.js → use-viewport-layout-vmbQUNT6.js} +1 -1
  62. package/dist/assets/x-DSz9cGhy.js +1 -0
  63. package/dist/index.html +33 -33
  64. package/dist/panel-standalone.html +10 -10
  65. package/package.json +4 -4
  66. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +2 -2
  67. package/src/app/components/layout/sidebar-rail.styles.ts +3 -3
  68. package/src/app/styles/charcoal-theme.css +0 -3
  69. package/src/app/styles/design-system.css +12 -6
  70. package/src/app/styles/island-theme.css +2 -15
  71. package/src/features/app-data/components/app-data-removal-choice.tsx +1 -1
  72. package/src/features/channels/pages/channels-list-page.tsx +1 -1
  73. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +1 -1
  74. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +28 -16
  75. package/src/features/chat/components/conversation/chat-conversation-content.tsx +8 -2
  76. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +6 -4
  77. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +22 -1
  78. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +10 -14
  79. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +38 -7
  80. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -4
  81. package/src/features/chat/components/layout/chat-sidebar.tsx +2 -2
  82. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  83. package/src/features/chat/features/message/components/chat-message-list.container.tsx +12 -1
  84. package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +32 -2
  85. package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +7 -5
  86. package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +7 -3
  87. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +1 -1
  88. package/src/features/chat/features/session/components/chat-mobile-session-row.tsx +1 -1
  89. package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +1 -1
  91. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +7 -5
  92. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +45 -10
  93. package/src/features/chat/features/session/components/session-header/chat-session-more-actions-menu.tsx +3 -0
  94. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +6 -6
  95. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -4
  96. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +1 -1
  97. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +0 -1
  98. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  99. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +1 -1
  100. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +2 -2
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +1 -1
  104. package/src/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions.tsx +2 -2
  105. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +2 -2
  106. package/src/features/chat/features/workspace/components/overview/chat-session-continuous-attention.tsx +3 -3
  107. package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +1 -1
  108. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +4 -3
  109. package/src/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item.tsx +2 -1
  110. package/src/features/chat/features/workspace/components/session-cron-job-content.tsx +3 -3
  111. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +2 -2
  112. package/src/features/chat/features/workspace/components/workspace-markdown-outline.tsx +1 -1
  113. package/src/features/cron/components/cron-config.tsx +2 -2
  114. package/src/features/cron/components/cron-job-detail-dialog.tsx +1 -1
  115. package/src/features/cron/components/cron-job-row.tsx +3 -3
  116. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  117. package/src/features/inbox/pages/inbox-page.tsx +1 -1
  118. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -2
  119. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +2 -2
  120. package/src/features/marketplace/components/marketplace-list-card.tsx +4 -4
  121. package/src/features/marketplace/components/marketplace-page.tsx +2 -2
  122. package/src/features/marketplace/components/mcp/mcp-marketplace-card.tsx +2 -2
  123. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +3 -3
  124. package/src/features/panel-apps/components/panel-app-list-item.tsx +3 -3
  125. package/src/features/pwa/components/pwa-install-entry.tsx +1 -1
  126. package/src/features/right-panel-resources/components/__tests__/page-resource-sidebar-nav.test.tsx +9 -1
  127. package/src/features/right-panel-resources/components/page-resource-sidebar-nav.tsx +1 -1
  128. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  129. package/src/features/service-apps/components/service-action-row.tsx +3 -3
  130. package/src/features/service-apps/components/service-app-list-item.tsx +4 -4
  131. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  132. package/src/features/settings/components/config/provider-model-list.tsx +5 -5
  133. package/src/features/settings/components/config/provider-model-suggestions-panel.tsx +4 -4
  134. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  135. package/src/features/settings/pages/appearance-settings-page.tsx +2 -2
  136. package/src/features/settings/pages/providers-config-page.tsx +6 -6
  137. package/src/features/side-dock/components/side-dock.tsx +4 -4
  138. package/src/index.css +4 -0
  139. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  140. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +3 -3
  141. package/src/shared/components/common/brand-header.tsx +1 -1
  142. package/src/shared/components/common/searchable-model-input.tsx +2 -2
  143. package/src/shared/components/config-split-page.tsx +3 -3
  144. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +2 -2
  145. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  146. package/src/shared/components/ui/button.tsx +2 -2
  147. package/src/shared/components/ui/card.tsx +1 -1
  148. package/src/shared/components/ui/context-menu/context-menu-items.tsx +3 -3
  149. package/src/shared/components/ui/input.tsx +7 -3
  150. package/src/shared/components/ui/overlays/sheet.tsx +1 -1
  151. package/src/shared/components/ui/popover.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +2 -2
  153. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +3 -3
  154. package/src/shared/components/ui/tabs.tsx +1 -1
  155. package/dist/assets/app-presenter-provider-Co8uKOyW.js +0 -124
  156. package/dist/assets/card-Bqu8zWLq.js +0 -1
  157. package/dist/assets/config-split-page-Bd-bGKsw.js +0 -1
  158. package/dist/assets/doc-browser-52t2_rSF.js +0 -1
  159. package/dist/assets/loader-circle-DbOQWT9C.js +0 -1
  160. package/dist/assets/login-page-DW1AZXqX.js +0 -1
  161. package/dist/assets/main-BpL6OjwM.css +0 -1
  162. package/dist/assets/mcp-marketplace-page-DSLRpJEB.js +0 -1
  163. package/dist/assets/panelStandalone-D-mhAGNa.css +0 -1
  164. package/dist/assets/provider-models-YW7rIeOD.js +0 -1
  165. package/dist/assets/provider-scoped-model-input-axJ-pTEu.js +0 -1
  166. package/dist/assets/providers-config-page-_QhBXQ4M.js +0 -1
  167. package/dist/assets/remote-BLQU7aY5.js +0 -1
  168. package/dist/assets/security-config-aE_jOwy-.js +0 -1
  169. package/dist/assets/service-apps-panel-jBWAa4qZ.js +0 -1
  170. package/dist/assets/x-CQIp_nJT.js +0 -1
@@ -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
 
@@ -231,6 +232,10 @@ function isAwaitingAssistantOutputRow(
231
232
  return item.kind === "typing" || item.key === activeRowKey;
232
233
  }
233
234
 
235
+ function resolveTurnAnchor(item: ChatTimelineItem, messageId: string | undefined, ref?: RefObject<HTMLDivElement>) {
236
+ return item.kind === "message" && item.message.id === messageId ? ref : undefined;
237
+ }
238
+
234
239
  export function ChatMessageListContainer({
235
240
  canContinue = false,
236
241
  messages: rawMessages,
@@ -241,6 +246,7 @@ export function ChatMessageListContainer({
241
246
  onEditMessage,
242
247
  onLoadMessageDetails,
243
248
  scrollRef,
249
+ turnAnchorRef,
244
250
  sessionKey,
245
251
  className,
246
252
  }: ChatMessageListContainerProps) {
@@ -386,11 +392,13 @@ export function ChatMessageListContainer({
386
392
  );
387
393
  const [focusedRowKey, setFocusedRowKey] = useState<string | null>(null);
388
394
  const activeRowKey = activeAssistantMessage ? `message:${activeAssistantMessage.id}` : null;
395
+ const turnMessageId = messages.findLast((message) => message.role === "user")?.id;
389
396
  const { containerRef, virtualizer } = useChatMessageVirtualizer({
390
397
  rows: virtualRows,
391
398
  scrollRef,
392
399
  activeRowKey,
393
400
  focusedRowKey,
401
+ anchorRowKey: turnAnchorRef && turnMessageId ? `message:${turnMessageId}` : null,
394
402
  });
395
403
  const { handleAttachmentOpen, handleInlineTokenClick } =
396
404
  useChatInlineTokenActions({ selectedSession, sessionKey });
@@ -448,7 +456,10 @@ export function ChatMessageListContainer({
448
456
  ) : item.kind === "observation-event" ? (
449
457
  <ChatMessageObservationEvent event={item.event} />
450
458
  ) : (
451
- <div className={item.kind === "message" ? "pb-5" : undefined}>
459
+ <div
460
+ className={item.kind === "message" ? "pb-5" : undefined}
461
+ ref={resolveTurnAnchor(item, turnMessageId, turnAnchorRef)}
462
+ >
452
463
  <ChatMessageList
453
464
  assistantAvatarIcon={assistantAvatarIcon}
454
465
  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,
@@ -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}
@@ -233,7 +233,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
233
233
  />
234
234
  }
235
235
  label={pinLabel}
236
- tooltipSide="right"
236
+ tooltipSide="top"
237
237
  onClick={() =>
238
238
  presenter.chatSessionListManager.toggleProjectPinned(
239
239
  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
  );
@@ -22,6 +22,7 @@ type ChatSessionMoreActionsMenuProps = {
22
22
  disabled?: boolean;
23
23
  triggerSize?: IconActionButtonSize;
24
24
  triggerTone?: IconActionButtonTone;
25
+ triggerTooltipSide?: 'top' | 'bottom';
25
26
  className?: string;
26
27
  };
27
28
 
@@ -39,6 +40,7 @@ export function ChatSessionMoreActionsMenu({
39
40
  disabled = false,
40
41
  triggerSize = 'md',
41
42
  triggerTone = 'default',
43
+ triggerTooltipSide = 'bottom',
42
44
  className,
43
45
  }: ChatSessionMoreActionsMenuProps) {
44
46
  const [isOpen, setIsOpen] = useState(false);
@@ -59,6 +61,7 @@ export function ChatSessionMoreActionsMenu({
59
61
  label={t('chatSessionMoreActions')}
60
62
  size={triggerSize}
61
63
  tone={triggerTone}
64
+ tooltipSide={triggerTooltipSide}
62
65
  className={className}
63
66
  disabled={disabled}
64
67
  onClick={(event) => event.stopPropagation()}
@@ -66,9 +66,9 @@ function ChatSessionSwitchItem({
66
66
  type="button"
67
67
  aria-current={active ? "page" : undefined}
68
68
  className={cn(
69
- "flex w-full min-w-0 items-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
69
+ "flex w-full min-w-0 items-start gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
70
70
  active
71
- ? "bg-accent text-accent-foreground"
71
+ ? "bg-accent/60 text-accent-foreground"
72
72
  : "text-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground",
73
73
  )}
74
74
  onClick={() => onSelect(session.key)}
@@ -101,7 +101,7 @@ function ChatSessionSwitchItem({
101
101
  </span>
102
102
  ) : null}
103
103
  </span>
104
- <span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
104
+ <span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground/75">
105
105
  <span className="min-w-0 truncate">{previewText}</span>
106
106
  <span className="shrink-0">{trailingText}</span>
107
107
  </span>
@@ -177,12 +177,12 @@ function ChatSessionSwitchProjectGroup({
177
177
  aria-controls={contentId}
178
178
  aria-expanded={!isCollapsed}
179
179
  aria-label={actionLabel}
180
- className="group flex h-10 w-full min-w-0 items-center gap-1.5 rounded-lg px-2 text-left text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
180
+ className="group flex h-8 w-full min-w-0 items-center gap-1.5 rounded-lg px-2 text-left text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
181
181
  onClick={() => onToggleCollapsed(group.projectRoot)}
182
182
  >
183
183
  <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
184
184
  <span
185
- className="min-w-0 flex-1 truncate text-[11px] font-medium uppercase tracking-wider"
185
+ className="min-w-0 flex-1 truncate text-[13px] font-medium"
186
186
  title={group.projectRoot}
187
187
  >
188
188
  {group.projectName}
@@ -222,7 +222,7 @@ export function ChatSessionSwitcherGroups({
222
222
  projectGroups: ChatSidebarProjectGroup[];
223
223
  }) {
224
224
  return (
225
- <div className="space-y-3">
225
+ <div className="space-y-0.5">
226
226
  {isProjectFirstView
227
227
  ? projectGroups.map((group) => (
228
228
  <ChatSessionSwitchProjectGroup
@@ -165,9 +165,9 @@ function ChatSessionTitleSwitcherPopover({
165
165
  </PopoverTrigger>
166
166
  <ChatPopoverContent
167
167
  align="start"
168
- className="w-[22rem] max-w-[calc(100vw-2rem)] p-0"
168
+ className="flex w-[22rem] max-w-[calc(100vw-2rem)] flex-col overflow-hidden p-0"
169
169
  >
170
- <div className="space-y-2 border-b border-border px-3 py-2">
170
+ <div className="shrink-0 space-y-2 border-b border-border/40 px-3 py-2">
171
171
  <div className="flex items-center justify-between gap-3">
172
172
  <div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
173
173
  {t("chatSessionSwitcherTitle")}
@@ -180,16 +180,17 @@ function ChatSessionTitleSwitcherPopover({
180
180
  <div className="relative">
181
181
  <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/65" />
182
182
  <Input
183
+ density="compact"
183
184
  value={searchQuery}
184
185
  onChange={(event) => setSearchQuery(event.target.value)}
185
186
  placeholder={t("chatSearchSessionPlaceholder")}
186
187
  aria-label={t("chatSearchSessionPlaceholder")}
187
- className="h-8 rounded-lg bg-background pl-8 pr-2 text-xs"
188
+ className="h-8 rounded-lg border-0 bg-muted/50 pl-8 pr-2"
188
189
  autoFocus
189
190
  />
190
191
  </div>
191
192
  </div>
192
- <div className="max-h-80 overflow-y-auto p-1.5">
193
+ <div className="min-h-0 max-h-80 overflow-y-auto overscroll-contain p-1.5">
193
194
  {isLoading ? (
194
195
  <ChatSessionTitleSwitcherLoadingState />
195
196
  ) : hasVisibleGroups ? (
@@ -27,7 +27,7 @@ export function ChatSessionTypeOptionItem(props: {
27
27
  onClick={onSelect}
28
28
  className={cn(
29
29
  "w-full rounded-2xl px-3 py-2.5 text-left transition-colors",
30
- selected ? "bg-gray-50" : "hover:bg-gray-50",
30
+ selected ? "bg-[var(--interaction-selection)]" : "hover:bg-[var(--interaction-hover)]",
31
31
  disabled ? "cursor-not-allowed opacity-70" : null,
32
32
  )}
33
33
  >
@@ -48,7 +48,6 @@ export function ChatWelcomeAgentPicker({
48
48
  </div>
49
49
  </SelectTrigger>
50
50
  <SelectContent
51
- className="rounded-xl border-border shadow-lg"
52
51
  style={{ maxHeight: AGENT_PICKER_MAX_HEIGHT }}
53
52
  >
54
53
  {agents.map((agent) => (
@@ -55,7 +55,7 @@ export function ChatWelcomeProjectPicker({
55
55
  </PopoverTrigger>
56
56
  <ChatPopoverContent
57
57
  align="start"
58
- className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-popover p-0 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
58
+ className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden p-0"
59
59
  style={{ maxHeight: PROJECT_PICKER_MAX_HEIGHT }}
60
60
  >
61
61
  <div className="shrink-0 px-3 pb-2 pt-3 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
@@ -27,7 +27,7 @@ export function ChatWelcomePromptSuggestions({
27
27
  key={suggestion.labelKey}
28
28
  type="button"
29
29
  onClick={() => onSelectPrompt(t(suggestion.promptKey))}
30
- className="rounded-full bg-muted/55 px-3.5 py-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
30
+ className="rounded-full bg-muted/55 px-3.5 py-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
31
31
  >
32
32
  {t(suggestion.labelKey)}
33
33
  </button>