@nextclaw/ui 0.26.2 → 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 (218) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/.vite/manifest.json +366 -365
  3. package/dist/assets/{account-panel-ZM2qzKF4.js → account-panel-DVi_jhkG.js} +1 -1
  4. package/dist/assets/{api-Cjq-o1VN.js → api-BZvNpFOS.js} +2 -2
  5. package/dist/assets/app-presenter-provider-BcYL-VNe.js +124 -0
  6. package/dist/assets/{appearance-settings-page-B4zouKzg.js → appearance-settings-page-AZNBzLmT.js} +1 -1
  7. package/dist/assets/{book-open-AteA89HX.js → book-open-CmRdT3N1.js} +1 -1
  8. package/dist/assets/card-CdRBL1E0.js +1 -0
  9. package/dist/assets/{channels-list-page-fj8kMo-T.js → channels-list-page-eZN74FN3.js} +2 -2
  10. package/dist/assets/{circle-dashed-LSYZTzM7.js → circle-dashed-HOgt3gIJ.js} +1 -1
  11. package/dist/assets/{client-DudCRJw4.js → client-DKKbdJ6Q.js} +1 -1
  12. package/dist/assets/config-split-page-BA3V6b3Q.js +1 -0
  13. package/dist/assets/{confirm-dialog-DShT64F6.js → confirm-dialog-8vqYZ0wy.js} +1 -1
  14. package/dist/assets/context-menu-DHVVdHzN.js +1 -0
  15. package/dist/assets/{createLucideIcon-D7hvIHV-.js → createLucideIcon-DE0sB9Nu.js} +1 -1
  16. package/dist/assets/{desktop-capabilities-page-CzqzFZUY.js → desktop-capabilities-page-B9-B9k1A.js} +1 -1
  17. package/dist/assets/{desktop-update-config-CgeQ0Ffo.js → desktop-update-config-Cfr3IRUe.js} +1 -1
  18. package/dist/assets/{dialog-C7glmAQJ.js → dialog-BUfWO8hr.js} +2 -2
  19. package/dist/assets/{doc-browser-context-BlhBAOLH.js → doc-browser-context-ClLgMbxd.js} +1 -1
  20. package/dist/assets/doc-browser-w8q1j0jL.js +1 -0
  21. package/dist/assets/{download-BYgECE0n.js → download-CKqS1Ksj.js} +1 -1
  22. package/dist/assets/{es2015-CsjAAQ6X.js → es2015-BiB05Sav.js} +1 -1
  23. package/dist/assets/{extensions-qYL-NpL3.js → extensions-vPD4dGqt.js} +1 -1
  24. package/dist/assets/{eye-BBuTzJ53.js → eye-bKzylu3p.js} +1 -1
  25. package/dist/assets/{form-actions-CLGWO52v.js → form-actions-CCoxz33Q.js} +1 -1
  26. package/dist/assets/{i18n-qBGEbcW6.js → i18n-OGmF0Ws_.js} +2 -2
  27. package/dist/assets/{keyboard-shortcuts-settings-page-B-wK_6zG.js → keyboard-shortcuts-settings-page-D8rosOVh.js} +1 -1
  28. package/dist/assets/loader-circle-Clq1Gtb1.js +1 -0
  29. package/dist/assets/{login-page-Cd84lpVV.js → login-page-BT3GV3Sb.js} +1 -1
  30. package/dist/assets/login-page-DKId3By8.js +1 -0
  31. package/dist/assets/main-bEWCGDHr.js +2 -0
  32. package/dist/assets/main-i18n-catalog.config-Bpu5GcwF.js +1 -0
  33. package/dist/assets/main-kqMRTwOs.css +1 -0
  34. package/dist/assets/mcp-marketplace-page-CZIP6j3L.js +1 -0
  35. package/dist/assets/{model-config-page-D0DQlSDa.js → model-config-page-D21a-BO0.js} +1 -1
  36. package/dist/assets/{navigation-link-32iM1-e6.js → navigation-link-DC-Pku0Y.js} +1 -1
  37. package/dist/assets/panel-app-BYfQu_TS.js +1 -0
  38. package/dist/assets/panel-app-i18n-catalog.config-D44qmtfk.js +1 -0
  39. package/dist/assets/panelStandalone-BRg7B1EI.css +1 -0
  40. package/dist/assets/{panelStandalone-C9EKaApN.js → panelStandalone-CX8X2hjh.js} +2 -2
  41. package/dist/assets/{privacy-settings-page-Bxxx12OO.js → privacy-settings-page-e0LFHZJm.js} +1 -1
  42. package/dist/assets/projects-Rki7Fmth.js +1 -0
  43. package/dist/assets/provider-models-DFQgF4hS.js +1 -0
  44. package/dist/assets/provider-scoped-model-input-DaONL0j_.js +1 -0
  45. package/dist/assets/providers-config-page-CEOhQlMe.js +1 -0
  46. package/dist/assets/{puzzle-CWHfFyYN.js → puzzle-D_TiFXOv.js} +1 -1
  47. package/dist/assets/{pwa-CiGUjpcd.js → pwa-C9ac_1dg.js} +1 -1
  48. package/dist/assets/{refresh-cw-y3L6PyLt.js → refresh-cw-BG8VfcdA.js} +1 -1
  49. package/dist/assets/remote-DkSx5x4R.js +1 -0
  50. package/dist/assets/{rotate-cw-CNE9qSHi.js → rotate-cw-DPEegUU2.js} +1 -1
  51. package/dist/assets/{runtime-config-page-5o98T-TD.js → runtime-config-page-DDT1dVLt.js} +1 -1
  52. package/dist/assets/{save-B0p5a0ET.js → save-CPlctBjT.js} +1 -1
  53. package/dist/assets/{search-config-page-CpGcnIzi.js → search-config-page-CDwmd6eX.js} +1 -1
  54. package/dist/assets/{secrets-config-page-C4VTiW3r.js → secrets-config-page-1OvgmwPv.js} +1 -1
  55. package/dist/assets/security-config-Pv4CM-kw.js +1 -0
  56. package/dist/assets/{select-CctTN205.js → select-Y9IEbx9x.js} +1 -1
  57. package/dist/assets/service-apps-panel-DPrLeF3N.js +1 -0
  58. package/dist/assets/{settings-2-EPflrNk6.js → settings-2-CrGx9_De.js} +1 -1
  59. package/dist/assets/{status-dot-CensgJZC.js → status-dot-Cb8ewlUr.js} +1 -1
  60. package/dist/assets/{tag-chip-Bjzb4-qW.js → tag-chip-D-6aPuuh.js} +1 -1
  61. package/dist/assets/{theme-provider-hlle7Skk.js → theme-provider-BjF1Eowc.js} +1 -1
  62. package/dist/assets/{trash-2-C5KH5Rw9.js → trash-2-DER2EJKK.js} +1 -1
  63. package/dist/assets/{use-config-CPl_F79t.js → use-config-BM5jC65L.js} +1 -1
  64. package/dist/assets/{use-desktop-capabilities-CEg9-DrQ.js → use-desktop-capabilities-ezKJwKqg.js} +1 -1
  65. package/dist/assets/{use-viewport-layout-CPZUmTPi.js → use-viewport-layout-vmbQUNT6.js} +1 -1
  66. package/dist/assets/x-DSz9cGhy.js +1 -0
  67. package/dist/index.html +34 -34
  68. package/dist/panel-standalone.html +11 -11
  69. package/package.json +3 -3
  70. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +2 -2
  71. package/src/app/components/layout/page-layout.tsx +5 -2
  72. package/src/app/components/layout/sidebar-rail.styles.ts +3 -3
  73. package/src/app/configs/__tests__/app-navigation.config.test.ts +10 -0
  74. package/src/app/configs/app-navigation.config.ts +6 -2
  75. package/src/app/styles/charcoal-theme.css +0 -3
  76. package/src/app/styles/design-system.css +18 -6
  77. package/src/app/styles/island-theme.css +2 -15
  78. package/src/features/agents/components/agent-details-dialog.tsx +1 -1
  79. package/src/features/agents/components/agents-page.tsx +5 -3
  80. package/src/features/app-data/components/app-data-removal-choice.tsx +1 -1
  81. package/src/features/apps/components/app-package-card.tsx +7 -6
  82. package/src/features/channels/pages/channels-list-page.tsx +1 -1
  83. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +1 -1
  84. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +28 -16
  85. package/src/features/chat/components/conversation/chat-conversation-content.tsx +8 -2
  86. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -3
  87. package/src/features/chat/components/conversation/chat-conversation-header.tsx +4 -4
  88. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +6 -4
  89. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +22 -1
  90. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  91. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +10 -14
  92. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +50 -12
  93. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -4
  94. package/src/features/chat/components/layout/chat-sidebar.tsx +2 -2
  95. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  96. package/src/features/chat/features/message/components/chat-message-list.container.tsx +12 -1
  97. package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +32 -2
  98. package/src/features/chat/features/message/hooks/__tests__/use-chat-message-virtualizer.test.tsx +7 -5
  99. package/src/features/chat/features/message/hooks/use-chat-message-virtualizer.ts +7 -3
  100. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +1 -1
  101. package/src/features/chat/features/session/components/chat-mobile-session-row.tsx +2 -2
  102. package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +1 -1
  103. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +1 -1
  104. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +7 -5
  105. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +45 -10
  106. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +1 -1
  107. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +3 -5
  108. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +4 -3
  109. package/src/features/chat/features/session/components/session-header/chat-session-more-actions-menu.tsx +3 -0
  110. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +6 -6
  111. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +6 -5
  112. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +1 -1
  113. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +0 -1
  114. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  115. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +1 -1
  116. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  117. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  118. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +2 -2
  119. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +1 -1
  120. package/src/features/chat/features/workspace/components/child-sessions/chat-session-child-sessions.tsx +2 -2
  121. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +2 -2
  122. package/src/features/chat/features/workspace/components/overview/chat-session-continuous-attention.tsx +3 -3
  123. package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +1 -1
  124. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +4 -3
  125. package/src/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item.tsx +2 -1
  126. package/src/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar.tsx +3 -1
  127. package/src/features/chat/features/workspace/components/session-cron-job-content.tsx +3 -3
  128. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +2 -2
  129. package/src/features/chat/features/workspace/components/workspace-markdown-outline.tsx +1 -1
  130. package/src/features/cron/components/cron-config.tsx +3 -2
  131. package/src/features/cron/components/cron-job-detail-dialog.tsx +1 -1
  132. package/src/features/cron/components/cron-job-row.tsx +7 -7
  133. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  134. package/src/features/inbox/components/inbox-delivery-content.tsx +1 -1
  135. package/src/features/inbox/pages/inbox-page.tsx +25 -29
  136. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -2
  137. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +2 -2
  138. package/src/features/marketplace/components/marketplace-list-card.tsx +4 -4
  139. package/src/features/marketplace/components/marketplace-page-parts.tsx +4 -3
  140. package/src/features/marketplace/components/marketplace-page.tsx +4 -4
  141. package/src/features/marketplace/components/mcp/mcp-marketplace-card.tsx +2 -2
  142. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +3 -3
  143. package/src/features/panel-apps/components/panel-app-list-item.tsx +3 -3
  144. package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +3 -1
  145. package/src/features/projects/components/project-overview.tsx +1 -1
  146. package/src/features/projects/pages/project-home-page.tsx +11 -2
  147. package/src/features/projects/pages/projects-page.test.tsx +4 -2
  148. package/src/features/pwa/components/pwa-install-entry.tsx +1 -1
  149. package/src/features/right-panel-resources/components/__tests__/page-resource-sidebar-nav.test.tsx +9 -1
  150. package/src/features/right-panel-resources/components/page-resource-sidebar-nav.tsx +1 -1
  151. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  152. package/src/features/service-apps/components/service-action-row.tsx +3 -3
  153. package/src/features/service-apps/components/service-app-list-item.tsx +4 -4
  154. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  155. package/src/features/settings/components/config/provider-model-list.tsx +5 -5
  156. package/src/features/settings/components/config/provider-model-suggestions-panel.tsx +4 -4
  157. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  158. package/src/features/settings/pages/appearance-settings-page.tsx +2 -2
  159. package/src/features/settings/pages/providers-config-page.tsx +6 -6
  160. package/src/features/side-dock/components/side-dock.tsx +4 -4
  161. package/src/index.css +4 -0
  162. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  163. package/src/platforms/mobile/components/mobile-app-shell.tsx +1 -1
  164. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +10 -9
  165. package/src/platforms/mobile/components/mobile-settings-shell.tsx +24 -23
  166. package/src/platforms/mobile/components/mobile-topbar.tsx +10 -11
  167. package/src/shared/components/common/brand-header.tsx +1 -1
  168. package/src/shared/components/common/searchable-model-input.tsx +2 -2
  169. package/src/shared/components/config-split-page.tsx +3 -3
  170. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -2
  171. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +38 -7
  172. package/src/shared/components/doc-browser/doc-browser.tsx +5 -1
  173. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  174. package/src/shared/components/settings/settings-page.tsx +1 -1
  175. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +5 -5
  176. package/src/shared/components/ui/actions/action-feedback.ts +8 -0
  177. package/src/shared/components/ui/actions/icon-action-button.tsx +13 -12
  178. package/src/shared/components/ui/button.tsx +3 -3
  179. package/src/shared/components/ui/card.tsx +1 -1
  180. package/src/shared/components/ui/context-menu/context-menu-items.tsx +6 -5
  181. package/src/shared/components/ui/dialog.tsx +4 -3
  182. package/src/shared/components/ui/input.tsx +7 -2
  183. package/src/shared/components/ui/overlays/sheet.tsx +1 -1
  184. package/src/shared/components/ui/popover.tsx +1 -1
  185. package/src/shared/components/ui/select.tsx +3 -2
  186. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -6
  187. package/src/shared/components/ui/tabs.tsx +3 -3
  188. package/src/shared/components/ui/textarea.tsx +1 -0
  189. package/src/shared/components/workbench/workbench-surface-toolbar.tsx +3 -4
  190. package/src/shared/components/workbench/workbench-surface.tsx +2 -2
  191. package/src/shared/lib/i18n/locales/en-US/agents.json +2 -0
  192. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  193. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +1 -1
  194. package/src/shared/lib/i18n/locales/zh-CN/agents.json +2 -0
  195. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  196. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +1 -1
  197. package/dist/assets/app-presenter-provider-C02YPoMr.js +0 -124
  198. package/dist/assets/card-BLXvBstA.js +0 -1
  199. package/dist/assets/config-split-page-G7Kt2BnV.js +0 -1
  200. package/dist/assets/context-menu-rd7b_kVD.js +0 -1
  201. package/dist/assets/doc-browser-dBiR9Qi0.js +0 -1
  202. package/dist/assets/loader-circle-BA7EMkih.js +0 -1
  203. package/dist/assets/login-page-VdMyNkFf.js +0 -1
  204. package/dist/assets/main-CR0SrTBf.js +0 -2
  205. package/dist/assets/main-DXH6EdXI.css +0 -1
  206. package/dist/assets/main-i18n-catalog.config-Btp_OowF.js +0 -1
  207. package/dist/assets/mcp-marketplace-page-jacRwiI-.js +0 -1
  208. package/dist/assets/panel-app-CC0bVSu4.js +0 -1
  209. package/dist/assets/panel-app-i18n-catalog.config-LUSBp5Ce.js +0 -1
  210. package/dist/assets/panelStandalone-Y91OJkgV.css +0 -1
  211. package/dist/assets/projects-D56yAPNT.js +0 -1
  212. package/dist/assets/provider-models-C3frdXeW.js +0 -1
  213. package/dist/assets/provider-scoped-model-input-CYetrPYZ.js +0 -1
  214. package/dist/assets/providers-config-page-DESB1h8g.js +0 -1
  215. package/dist/assets/remote-DiGFV8nP.js +0 -1
  216. package/dist/assets/security-config-C5-RQVpS.js +0 -1
  217. package/dist/assets/service-apps-panel-CKE5HKNY.js +0 -1
  218. package/dist/assets/x--xxUvHKW.js +0 -1
@@ -66,21 +66,21 @@ export function ChatConversationHeader({
66
66
  "bg-background/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-colors duration-200",
67
67
  isMobileLayout ? "px-3 sm:px-3" : "px-4 sm:px-5",
68
68
  shouldShow ? "opacity-100" : "h-0 py-0 opacity-0",
69
- shouldShow && (isMobileLayout ? "min-h-12 pb-2 pt-2" : "h-[52px]"),
69
+ shouldShow && (isMobileLayout ? "min-h-[var(--mobile-header-height)] pb-0.5 pt-0.5" : "h-[52px]"),
70
70
  )}
71
71
  style={
72
72
  isMobileLayout && shouldShow
73
- ? { paddingTop: "calc(env(safe-area-inset-top, 0px) + 0.5rem)" }
73
+ ? { paddingTop: "calc(env(safe-area-inset-top, 0px) + 0.125rem)" }
74
74
  : undefined
75
75
  }
76
76
  >
77
- <div className="min-w-0 flex-1 flex items-center gap-2">
77
+ <div className="min-w-0 flex-1 flex items-center gap-2 max-md:gap-0">
78
78
  {isMobileLayout && onBackToList ? (
79
79
  <IconActionButton
80
80
  icon={<ArrowLeft className="h-4 w-4" />}
81
81
  label={t("chat")}
82
82
  onClick={onBackToList}
83
- className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
83
+ size="lg"
84
84
  />
85
85
  ) : null}
86
86
  {leading}
@@ -8,6 +8,8 @@ function renderSessionArea(isProjectFirstView: boolean, onScrollNearEnd = vi.fn(
8
8
 
9
9
  render(
10
10
  <ChatSidebarSessionArea
11
+ query=""
12
+ onQueryChange={vi.fn()}
11
13
  defaultSessionType="native"
12
14
  groups={[]}
13
15
  isCollapsed={false}
@@ -50,9 +52,9 @@ describe("ChatSidebarSessionArea", () => {
50
52
  expect(projectButton.className).toContain("rounded-full");
51
53
  expect(timeButton.querySelector("svg")).not.toBeNull();
52
54
  expect(projectButton.querySelector("svg")).not.toBeNull();
53
- expect(modeGroup.parentElement?.className).toContain("justify-end");
55
+ expect(modeGroup.parentElement?.className).toContain("justify-between");
54
56
  expect(modeGroup.parentElement?.className).toContain("h-8");
55
- expect(modeGroup.parentElement?.lastElementChild).toBe(modeGroup);
57
+ expect(modeGroup.parentElement?.firstElementChild).toBe(modeGroup);
56
58
 
57
59
  fireEvent.click(projectButton);
58
60
 
@@ -73,7 +75,7 @@ describe("ChatSidebarSessionArea", () => {
73
75
  .querySelector("svg")
74
76
  ?.classList.contains("lucide-folder-plus"),
75
77
  ).toBe(true);
76
- expect(addProjectButton.className).toContain("hover:bg-gray-200/60");
78
+ expect(addProjectButton.className).toContain("hover:before:bg-[var(--interaction-hover)]");
77
79
  expect(modeIndicator?.className).toContain("translate-x-full");
78
80
  expect(modeIndicator?.className).toContain("motion-reduce:transition-none");
79
81
  expect(
@@ -81,7 +83,7 @@ describe("ChatSidebarSessionArea", () => {
81
83
  .getByRole("button", { name: "Project" })
82
84
  .getAttribute("aria-pressed"),
83
85
  ).toBe("true");
84
- expect(modeIndicator?.parentElement?.previousElementSibling).toBe(
86
+ expect(screen.getByRole("button", { name: "Search conversations..." }).previousElementSibling).toBe(
85
87
  addProjectButton,
86
88
  );
87
89
  fireEvent.click(addProjectButton);
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
3
3
  import { describe, expect, it, vi } from "vitest";
4
4
  import {
5
5
  ChatSidebarDesktopToolbar,
6
+ ChatSidebarListToolbar,
6
7
  ChatSidebarMobileToolbar,
7
8
  } from "@/features/chat/components/layout/chat-sidebar-toolbar";
8
9
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
@@ -34,8 +35,18 @@ function searchIconClassName() {
34
35
  }
35
36
 
36
37
  describe("ChatSidebarToolbar", () => {
37
- it("keeps desktop search icon transparent to pointer input", () => {
38
+ it("does not reserve a search row in the desktop header", () => {
38
39
  render(<ChatSidebarDesktopToolbar {...toolbarProps} />);
40
+ expect(screen.queryByRole("textbox")).toBeNull();
41
+ });
42
+ it("reveals desktop search below the list controls and clears it on close", async () => {
43
+ const user = userEvent.setup();
44
+ const onQueryChange = vi.fn();
45
+ render(<ChatSidebarListToolbar {...toolbarProps} onQueryChange={onQueryChange} isProjectFirstView={false} onSelectMode={vi.fn()} onAddProject={vi.fn()} />);
46
+ expect(screen.queryByRole("textbox")).toBeNull();
47
+ const trigger = screen.getByRole("button", { name: "Search conversations..." });
48
+ await user.click(trigger);
49
+ expect(document.activeElement).toBe(screen.getByRole("textbox"));
39
50
 
40
51
  expect(searchIconClassName()).toContain("pointer-events-none");
41
52
  expect(
@@ -46,6 +57,16 @@ describe("ChatSidebarToolbar", () => {
46
57
  .getByPlaceholderText("Search conversations...")
47
58
  .getAttribute("data-theme-control"),
48
59
  ).toBe("chat-search");
60
+ await user.type(screen.getByRole("textbox"), "a");
61
+ expect(onQueryChange).toHaveBeenLastCalledWith("a");
62
+ await user.click(screen.getByRole("button", { name: "Close search" }));
63
+ expect(onQueryChange).toHaveBeenLastCalledWith("");
64
+ expect(screen.queryByRole("textbox")).toBeNull();
65
+ expect(document.activeElement).toBe(trigger);
66
+ await user.click(trigger);
67
+ await user.keyboard("{Escape}");
68
+ expect(screen.queryByRole("textbox")).toBeNull();
69
+ expect(document.activeElement).toBe(trigger);
49
70
  });
50
71
 
51
72
  it('reveals mobile search on demand and clears the filter when closed', async () => {
@@ -55,7 +55,7 @@ function ManagementPageContent({ view }: ChatPageProps) {
55
55
  case 'resource': return <ResourcePage />;
56
56
  case 'projects': return <Suspense fallback={<div className="h-full animate-pulse bg-card/30" />}><ProjectsPage /></Suspense>;
57
57
  case 'panel-app': return <PanelAppMainPage />;
58
- case 'inbox': return <div className={`mx-auto flex h-full min-h-0 w-full flex-col py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}><InboxPage /></div>;
58
+ case 'inbox': return <div className={`mx-auto flex h-full min-h-0 w-full flex-col md:px-6 md:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}><InboxPage /></div>;
59
59
  case 'cron':
60
60
  case 'agents': return <div ref={scrollRef} onScroll={onScroll} className="h-full overflow-auto custom-scrollbar">
61
61
  <div className={`mx-auto w-full px-4 py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
@@ -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,7 +1,10 @@
1
- import { useState } from 'react';
2
- import { ChevronDown, Clock3, Folder, FolderPlus, Plus, Search, X } from 'lucide-react';
1
+ import { useId, useRef, useState } from 'react';
2
+ import { ChatSidebarListModeSwitch } from '@/features/chat/components/chat-sidebar-list-mode-switch';
3
+ import { Link } from 'react-router-dom';
4
+ import { AlarmClock, ChevronDown, Clock3, Folder, FolderPlus, Plus, Search, X } from 'lucide-react';
3
5
  import { Button } from '@/shared/components/ui/button';
4
- import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
6
+ import { IconActionButton, IconActionGroup } from '@/shared/components/ui/actions/icon-action-button';
7
+ import { ACTION_FEEDBACK, ACTION_MENU_ITEM_CLASS } from '@/shared/components/ui/actions/action-feedback';
5
8
  import { Input } from '@/shared/components/ui/input';
6
9
  import { Popover, PopoverTrigger } from '@/shared/components/ui/popover';
7
10
  import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
@@ -17,7 +20,7 @@ type ChatSidebarToolbarProps = {
17
20
  collapsed?: boolean;
18
21
  };
19
22
 
20
- export function ChatSidebarDesktopToolbar({ query, onQueryChange, onCreateSession, collapsed }: ChatSidebarToolbarProps) {
23
+ export function ChatSidebarDesktopToolbar({ onCreateSession, collapsed }: Pick<ChatSidebarToolbarProps, 'onCreateSession' | 'collapsed'>) {
21
24
  if (collapsed) {
22
25
  return <div className="px-2 pb-2">
23
26
  <IconActionButton icon={<Plus className={SIDEBAR_RAIL_ICON_CLASS} />} label={t('chatSidebarNewTask')}
@@ -30,14 +33,44 @@ export function ChatSidebarDesktopToolbar({ query, onQueryChange, onCreateSessio
30
33
  <Plus className="mr-1.5 h-4 w-4" />{t('chatSidebarNewTask')}
31
34
  </Button>
32
35
  </div>
33
- <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">
34
64
  <div className="relative">
35
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" />
36
- <Input data-theme-control="chat-search" value={query} onChange={event => onQueryChange(event.target.value)}
37
- 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} />
38
71
  </div>
39
- </div>
40
- </>;
72
+ </div> : null}
73
+ </div>;
41
74
  }
42
75
 
43
76
  export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps & {
@@ -50,7 +83,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps & {
50
83
  const [viewMenuOpen, setViewMenuOpen] = useState(false);
51
84
  return (
52
85
  <div className="shrink-0 border-b border-border/50 bg-background px-3 text-foreground" style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}>
53
- <div className="flex h-14 items-center gap-1">
86
+ <div className="flex h-[var(--mobile-header-height)] items-center gap-1">
54
87
  <Popover open={viewMenuOpen} onOpenChange={setViewMenuOpen}>
55
88
  <h1>
56
89
  <PopoverTrigger asChild>
@@ -65,14 +98,19 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps & {
65
98
  <ChatSessionHeaderMenuItem icon={Clock3} label={t('chatSidebarViewTime')} onClick={() => onSelectMode('time-first')} />
66
99
  <ChatSessionHeaderMenuItem icon={Folder} label={t('chatSidebarViewProject')} onClick={() => onSelectMode('project-first')} />
67
100
  <ChatSessionHeaderMenuItem icon={FolderPlus} label={t('chatProjectAdd')} onClick={onAddProject} />
101
+ <Link to="/cron" className={`${ACTION_MENU_ITEM_CLASS} text-foreground ${ACTION_FEEDBACK.item}`}>
102
+ <AlarmClock className="h-4 w-4 text-muted-foreground" />{t('cron')}
103
+ </Link>
68
104
  </div>
69
105
  </ChatPopoverContent>
70
106
  </Popover>
71
107
  <div className="flex-1" />
108
+ <IconActionGroup>
72
109
  <IconActionButton icon={searchOpen ? <X className="h-5 w-5" /> : <Search className="h-5 w-5" />}
73
- label={t(searchOpen ? 'chatSidebarCloseSearch' : 'chatSidebarSearchPlaceholder')} className="h-11 w-11" tooltip={false}
110
+ label={t(searchOpen ? 'chatSidebarCloseSearch' : 'chatSidebarSearchPlaceholder')} tooltip={false}
74
111
  onClick={() => { setSearchOpen(!searchOpen); if (searchOpen) onQueryChange(''); }} />
75
- <IconActionButton icon={<Plus className="h-5 w-5" />} label={t('chatSidebarNewTask')} className="h-11 w-11" tooltip={false} onClick={onCreateSession} />
112
+ <IconActionButton icon={<Plus className="h-5 w-5" />} label={t('chatSidebarNewTask')} tooltip={false} onClick={onCreateSession} />
113
+ </IconActionGroup>
76
114
  </div>
77
115
  {searchOpen ? <div className="pb-2">
78
116
  <Input autoFocus value={query} onChange={event => onQueryChange(event.target.value)} placeholder={t('chatSidebarSearchPlaceholder')}
@@ -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" />
@@ -51,7 +51,7 @@ export function ChatMobileSessionRow(props: ChatSidebarSessionItemProps) {
51
51
  </span>
52
52
  </span>
53
53
  </button>
54
- <ChatSessionMoreActionsMenu sessionKey={sessionKey} sessionTitle={title} className="h-11 w-9 shrink-0 rounded-none text-muted-foreground">
54
+ <ChatSessionMoreActionsMenu sessionKey={sessionKey} sessionTitle={title}>
55
55
  <ChatSessionHeaderMenuItem icon={Pin} label={t(isPinned ? 'chatSidebarUnpinSession' : 'chatSidebarPinSession')} onClick={onTogglePinned} />
56
56
  <ChatSessionHeaderMenuItem icon={Pencil} label={t('edit')} onClick={onStartEditing} />
57
57
  <ChatSessionHeaderMenuItem icon={Trash2} label={t('delete')} onClick={onDelete} destructive />
@@ -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
  >