@nextclaw/ui 0.12.23 → 0.12.25

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 (215) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
  6. package/dist/assets/chat-page-D1fMNBrT.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-kk7qvZ-v.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-DsYTOyq7.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-V75WQJ2s.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-D-AAMKCt.js +103 -0
  23. package/dist/assets/index-DnBeV2Xm.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
  26. package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
  29. package/dist/assets/model-config-Bc6VVnxy.js +1 -0
  30. package/dist/assets/{notice-card-D1RNsTn_.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-BMyiifTA.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DN0tvISH.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-C4c1yZSP.js +1 -0
  43. package/dist/assets/secrets-config-zAF30YfO.js +3 -0
  44. package/dist/assets/{select-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-CvKngoNI.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-BywQeHJj.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +56 -25
  76. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
  78. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  79. package/src/features/chat/components/layout/chat-sidebar.test.tsx +119 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
  81. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  82. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  83. package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
  85. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  87. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
  88. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  89. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
  90. package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
  91. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  92. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
  93. package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
  94. package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
  95. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
  96. package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
  97. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  98. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  99. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  100. package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
  101. package/src/features/chat/stores/chat-session-list.store.ts +0 -3
  102. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  103. package/src/features/chat/types/chat-stream.types.ts +1 -1
  104. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  105. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  106. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  107. package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
  108. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  109. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  110. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  112. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  113. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  114. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  116. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  117. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  118. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  119. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  120. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  121. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  122. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  123. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  124. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  125. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  126. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  127. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  128. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  129. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  130. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  131. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  132. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  133. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  134. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  135. package/src/index.css +8 -0
  136. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  137. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  138. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  139. package/src/platforms/desktop/index.ts +6 -0
  140. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  141. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  142. package/src/shared/components/common/brand-header.tsx +36 -16
  143. package/src/shared/components/config/provider-form-support.ts +2 -22
  144. package/src/shared/components/cron-config.tsx +12 -58
  145. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  146. package/src/shared/components/ui/select.tsx +19 -7
  147. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  148. package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
  149. package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
  150. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  151. package/src/shared/lib/api/types.ts +12 -1
  152. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  153. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  154. package/src/shared/lib/cron/index.ts +1 -0
  155. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  156. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  157. package/src/shared/lib/i18n/index.ts +20 -59
  158. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  159. package/src/shared/lib/provider-models/index.test.ts +39 -0
  160. package/src/shared/lib/provider-models/index.ts +1 -3
  161. package/src/shared/lib/ui-document-title/index.ts +0 -1
  162. package/tsconfig.json +1 -0
  163. package/vite.config.ts +1 -1
  164. package/vitest.config.ts +1 -1
  165. package/dist/assets/api-BGd3rgv_.js +0 -15
  166. package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
  167. package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
  168. package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
  169. package/dist/assets/chat-DGM6K3Qs.js +0 -61
  170. package/dist/assets/chat-page-DpmXMWNS.js +0 -1
  171. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  172. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  173. package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
  174. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  175. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  176. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  177. package/dist/assets/folder-CeJKPx5P.js +0 -1
  178. package/dist/assets/hash-BqxRTZW5.js +0 -1
  179. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  180. package/dist/assets/index-BrEdR78s.js +0 -2
  181. package/dist/assets/index-D8MKmXtO.css +0 -1
  182. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  183. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  184. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  185. package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
  186. package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
  187. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
  189. package/dist/assets/message-square-z_osm9c0.js +0 -1
  190. package/dist/assets/model-config-Cmruiqdx.js +0 -1
  191. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  192. package/dist/assets/plus-D8eKFY7h.js +0 -1
  193. package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
  194. package/dist/assets/providers-list-gg7LrfuB.js +0 -1
  195. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  196. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  197. package/dist/assets/remote-Db2M39Cv.js +0 -1
  198. package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
  199. package/dist/assets/search-config-0VTPpz-w.js +0 -1
  200. package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
  201. package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
  202. package/dist/assets/settings-drbWqzA4.js +0 -1
  203. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  204. package/dist/assets/theme-provider-COAwWFv8.js +0 -2
  205. package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
  206. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  207. package/dist/assets/use-config-DTwhNDQE.js +0 -1
  208. package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
  209. package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
  210. package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
  211. package/dist/assets/x-CM-XDMpk.js +0 -1
  212. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  213. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  214. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  215. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
2
+ import { AlarmClock, FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
3
3
  import { Button } from '@/shared/components/ui/button';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
5
  import { useChatSessionProject } from '@/features/chat/hooks/use-chat-session-project';
@@ -7,13 +7,18 @@ import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
7
7
  import { ChatSessionProjectDialog } from './chat-session-project-dialog';
8
8
  import { t } from '@/shared/lib/i18n';
9
9
 
10
+ const SESSION_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5';
11
+ const SESSION_HEADER_ACTION_BUTTON_CLASS = 'h-7 w-7 rounded-lg shrink-0 text-gray-400 hover:text-gray-700';
12
+
10
13
  type ChatSessionHeaderActionsProps = {
11
14
  sessionKey: string;
12
15
  canDeleteSession: boolean;
13
16
  isDeletePending: boolean;
14
17
  projectRoot?: string | null;
15
18
  childSessionCount?: number;
19
+ sessionCronJobCount?: number;
16
20
  onOpenChildSessions?: () => void;
21
+ onOpenSessionCronJobs?: () => void;
17
22
  onDeleteSession: () => void;
18
23
  };
19
24
 
@@ -23,7 +28,9 @@ export function ChatSessionHeaderActions({
23
28
  isDeletePending,
24
29
  projectRoot,
25
30
  childSessionCount = 0,
31
+ sessionCronJobCount = 0,
26
32
  onOpenChildSessions,
33
+ onOpenSessionCronJobs,
27
34
  onDeleteSession,
28
35
  }: ChatSessionHeaderActionsProps) {
29
36
  const updateSessionProject = useChatSessionProject();
@@ -49,13 +56,13 @@ export function ChatSessionHeaderActions({
49
56
  };
50
57
 
51
58
  return (
52
- <>
59
+ <div className={SESSION_HEADER_ACTION_GROUP_CLASS}>
53
60
  {childSessionCount > 0 && onOpenChildSessions ? (
54
61
  <Button
55
62
  type="button"
56
63
  variant="ghost"
57
64
  size="icon"
58
- className="rounded-lg shrink-0 text-gray-400 hover:text-gray-700"
65
+ className={SESSION_HEADER_ACTION_BUTTON_CLASS}
59
66
  aria-label={t('chatSessionOpenChildSessions')}
60
67
  title={t('chatSessionOpenChildSessions')}
61
68
  onClick={onOpenChildSessions}
@@ -64,12 +71,26 @@ export function ChatSessionHeaderActions({
64
71
  <GitBranch className="h-4 w-4" />
65
72
  </Button>
66
73
  ) : null}
74
+ {sessionCronJobCount > 0 && onOpenSessionCronJobs ? (
75
+ <Button
76
+ type="button"
77
+ variant="ghost"
78
+ size="icon"
79
+ className={SESSION_HEADER_ACTION_BUTTON_CLASS}
80
+ aria-label={t('chatSessionOpenCronJobs')}
81
+ title={t('chatSessionOpenCronJobs')}
82
+ onClick={onOpenSessionCronJobs}
83
+ disabled={isBusy}
84
+ >
85
+ <AlarmClock className="h-4 w-4" />
86
+ </Button>
87
+ ) : null}
67
88
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
68
89
  <PopoverTrigger asChild>
69
90
  <Button
70
91
  variant="ghost"
71
92
  size="icon"
72
- className="rounded-lg shrink-0 text-gray-400 hover:text-gray-700"
93
+ className={SESSION_HEADER_ACTION_BUTTON_CLASS}
73
94
  aria-label={t('chatSessionMoreActions')}
74
95
  disabled={isBusy}
75
96
  >
@@ -108,6 +129,6 @@ export function ChatSessionHeaderActions({
108
129
  onOpenChange={setIsDialogOpen}
109
130
  onSave={runProjectUpdate}
110
131
  />
111
- </>
132
+ </div>
112
133
  );
113
134
  }
@@ -0,0 +1,174 @@
1
+ import { NavLink } from 'react-router-dom';
2
+ import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
3
+ import { Select, SelectContent, SelectItem, SelectTrigger } from '@/shared/components/ui/select';
4
+ import { t, type I18nLanguage } from '@/shared/lib/i18n';
5
+ import type { UiTheme } from '@/shared/lib/theme';
6
+ import {
7
+ BookOpen,
8
+ ChevronRight,
9
+ Languages,
10
+ Palette,
11
+ Settings,
12
+ type LucideIcon,
13
+ } from 'lucide-react';
14
+
15
+ type ChatSidebarUtilityOption<Value extends string> = {
16
+ value: Value;
17
+ label: string;
18
+ };
19
+
20
+ type ChatSidebarUtilityMenuProps = {
21
+ isOpen: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ currentTheme: UiTheme;
24
+ currentThemeLabel: string;
25
+ themeOptions: ChatSidebarUtilityOption<UiTheme>[];
26
+ onSelectTheme: (theme: UiTheme) => void;
27
+ currentLanguage: I18nLanguage;
28
+ currentLanguageLabel: string;
29
+ languageOptions: ChatSidebarUtilityOption<I18nLanguage>[];
30
+ onSelectLanguage: (language: I18nLanguage) => void;
31
+ onOpenDocs: () => void;
32
+ };
33
+
34
+ export function ChatSidebarUtilityMenu({
35
+ isOpen,
36
+ onOpenChange,
37
+ currentTheme,
38
+ currentThemeLabel,
39
+ themeOptions,
40
+ onSelectTheme,
41
+ currentLanguage,
42
+ currentLanguageLabel,
43
+ languageOptions,
44
+ onSelectLanguage,
45
+ onOpenDocs,
46
+ }: ChatSidebarUtilityMenuProps) {
47
+ const handleOpenDocs = () => {
48
+ onOpenDocs();
49
+ onOpenChange(false);
50
+ };
51
+
52
+ return (
53
+ <Popover open={isOpen} onOpenChange={onOpenChange}>
54
+ <PopoverTrigger asChild>
55
+ <button
56
+ type="button"
57
+ aria-label={currentLanguage === 'zh' ? '设置菜单' : 'Settings menu'}
58
+ className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-gray-600 transition-all duration-base hover:bg-gray-200/60 hover:text-gray-800"
59
+ >
60
+ <Settings className="h-4 w-4 shrink-0 text-gray-400" />
61
+ <span className="min-w-0 flex-1 text-left">{t('settings')}</span>
62
+ <span className="max-w-[112px] truncate text-[13px] text-gray-500">
63
+ {currentThemeLabel} / {currentLanguageLabel}
64
+ </span>
65
+ </button>
66
+ </PopoverTrigger>
67
+ <PopoverContent side="top" align="start" className="w-64 p-2">
68
+ <div className="space-y-1">
69
+ <NavLink
70
+ to="/settings"
71
+ onClick={() => onOpenChange(false)}
72
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
73
+ >
74
+ <Settings className="h-4 w-4 text-gray-400" />
75
+ <span className="flex-1 text-left">{t('settings')}</span>
76
+ </NavLink>
77
+ <button
78
+ type="button"
79
+ onClick={handleOpenDocs}
80
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
81
+ >
82
+ <BookOpen className="h-4 w-4 text-gray-400" />
83
+ <span className="flex-1 text-left">{t('docBrowserHelp')}</span>
84
+ </button>
85
+ </div>
86
+
87
+ <div className="my-2 h-px bg-gray-200/70" />
88
+
89
+ <ChatSidebarUtilitySelect
90
+ icon={Palette}
91
+ label={t('theme')}
92
+ options={themeOptions}
93
+ value={currentTheme}
94
+ valueLabel={currentThemeLabel}
95
+ onSelect={onSelectTheme}
96
+ onCloseMenu={() => onOpenChange(false)}
97
+ />
98
+
99
+ <ChatSidebarUtilitySelect
100
+ icon={Languages}
101
+ label={t('language')}
102
+ options={languageOptions}
103
+ value={currentLanguage}
104
+ valueLabel={currentLanguageLabel}
105
+ onSelect={onSelectLanguage}
106
+ onCloseMenu={() => onOpenChange(false)}
107
+ />
108
+ </PopoverContent>
109
+ </Popover>
110
+ );
111
+ }
112
+
113
+ function ChatSidebarUtilitySelect<Value extends string>({
114
+ icon: Icon,
115
+ label,
116
+ options,
117
+ value,
118
+ valueLabel,
119
+ onSelect,
120
+ onCloseMenu,
121
+ }: {
122
+ icon: LucideIcon;
123
+ label: string;
124
+ options: ChatSidebarUtilityOption<Value>[];
125
+ value: Value;
126
+ valueLabel: string;
127
+ onSelect: (value: Value) => void;
128
+ onCloseMenu: () => void;
129
+ }) {
130
+ return (
131
+ <Select
132
+ value={value}
133
+ onOpenChange={(open) => {
134
+ if (!open) {
135
+ onCloseMenu();
136
+ }
137
+ }}
138
+ onValueChange={(nextValue) => onSelect(nextValue as Value)}
139
+ >
140
+ <SelectTrigger
141
+ aria-label={label}
142
+ className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-gray-700 shadow-none hover:bg-gray-100 focus:ring-0"
143
+ indicator={<ChevronRight className="h-4 w-4 text-gray-400" />}
144
+ >
145
+ <div className="flex min-w-0 items-center gap-2.5">
146
+ <Icon className="h-4 w-4 shrink-0 text-gray-400" />
147
+ <span className="text-left">{label}</span>
148
+ </div>
149
+ <span className="ml-auto max-w-[96px] truncate text-[13px] text-gray-500">
150
+ {valueLabel}
151
+ </span>
152
+ </SelectTrigger>
153
+ <SelectContent
154
+ side="right"
155
+ align="center"
156
+ sideOffset={6}
157
+ className="z-[var(--z-tooltip)] min-w-[7rem] rounded-lg"
158
+ viewportClassName="h-auto min-w-[7rem] w-auto"
159
+ >
160
+ {options.map((option) => {
161
+ return (
162
+ <SelectItem
163
+ key={option.value}
164
+ value={option.value}
165
+ className="text-[13px]"
166
+ >
167
+ {option.label}
168
+ </SelectItem>
169
+ );
170
+ })}
171
+ </SelectContent>
172
+ </Select>
173
+ );
174
+ }
@@ -9,11 +9,14 @@ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-lis
9
9
  const mocks = vi.hoisted(() => ({
10
10
  createSession: vi.fn(() => 'draft-session-key'),
11
11
  goToSession: vi.fn(),
12
+ goToChatRoot: vi.fn(),
12
13
  setQuery: vi.fn(),
13
14
  setListMode: vi.fn(),
14
15
  selectSession: vi.fn(),
15
16
  openChildSessionPanel: vi.fn(),
16
17
  docOpen: vi.fn(),
18
+ setLanguage: vi.fn(),
19
+ setTheme: vi.fn(),
17
20
  updateNcpSession: vi.fn(),
18
21
  agents: [] as Array<{ id: string; displayName?: string; avatarUrl?: string | null }>,
19
22
  sessionItems: [] as NcpSessionListItemView[],
@@ -31,6 +34,7 @@ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
31
34
  usePresenter: () => ({
32
35
  chatUiManager: {
33
36
  goToSession: mocks.goToSession,
37
+ goToChatRoot: mocks.goToChatRoot,
34
38
  },
35
39
  chatSessionListManager: {
36
40
  createSession: mocks.createSession,
@@ -100,14 +104,14 @@ vi.mock('@/shared/hooks/use-agents', () => ({
100
104
  vi.mock('@/app/components/i18n-provider', () => ({
101
105
  useI18n: () => ({
102
106
  language: 'en',
103
- setLanguage: vi.fn()
107
+ setLanguage: mocks.setLanguage
104
108
  })
105
109
  }));
106
110
 
107
111
  vi.mock('@/app/components/theme-provider', () => ({
108
112
  useTheme: () => ({
109
113
  theme: 'warm',
110
- setTheme: vi.fn()
114
+ setTheme: mocks.setTheme
111
115
  })
112
116
  }));
113
117
 
@@ -121,11 +125,14 @@ function resetSidebarTestState() {
121
125
  mocks.createSession.mockReset();
122
126
  mocks.createSession.mockReturnValue('draft-session-key');
123
127
  mocks.goToSession.mockReset();
128
+ mocks.goToChatRoot.mockReset();
124
129
  mocks.setQuery.mockReset();
125
130
  mocks.setListMode.mockReset();
126
131
  mocks.selectSession.mockReset();
127
132
  mocks.openChildSessionPanel.mockReset();
128
133
  mocks.docOpen.mockReset();
134
+ mocks.setLanguage.mockReset();
135
+ mocks.setTheme.mockReset();
129
136
  mocks.updateNcpSession.mockReset();
130
137
  mocks.updateNcpSession.mockResolvedValue({});
131
138
  mocks.agents = [];
@@ -166,7 +173,7 @@ describe('ChatSidebar create and list basics', () => {
166
173
  fireEvent.click(screen.getByLabelText('Session Type'));
167
174
  fireEvent.click(screen.getByText('Codex'));
168
175
 
169
- expect(mocks.createSession).toHaveBeenCalledWith('codex');
176
+ expect(mocks.createSession).toHaveBeenCalledWith('codex', undefined);
170
177
  await waitFor(() => {
171
178
  expect(screen.queryByText('Codex')).toBeNull();
172
179
  });
@@ -233,8 +240,38 @@ describe('ChatSidebar create and list basics', () => {
233
240
  fireEvent.click(screen.getByText('Codex'));
234
241
 
235
242
  expect(mocks.setQuery).toHaveBeenCalledWith('release notes');
236
- expect(mocks.createSession).toHaveBeenCalledWith('codex');
237
- expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
243
+ expect(mocks.createSession).toHaveBeenCalledWith('codex', undefined);
244
+ expect(mocks.goToChatRoot).toHaveBeenCalledWith();
245
+ });
246
+
247
+ it('keeps low-frequency utility choices behind nested selectors', () => {
248
+ render(
249
+ <MemoryRouter>
250
+ <ChatSidebar />
251
+ </MemoryRouter>
252
+ );
253
+
254
+ expect(screen.queryByRole('button', { name: 'Help Docs' })).toBeNull();
255
+ expect(screen.queryByText('Language')).toBeNull();
256
+ expect(screen.queryByRole('option', { name: 'Cool' })).toBeNull();
257
+
258
+ fireEvent.click(screen.getByRole('button', { name: 'Settings menu' }));
259
+ expect(screen.getByText('Theme')).not.toBeNull();
260
+ expect(screen.queryByRole('option', { name: 'Cool' })).toBeNull();
261
+
262
+ fireEvent.click(screen.getByRole('button', { name: 'Help Docs' }));
263
+
264
+ expect(mocks.docOpen).toHaveBeenCalledWith(undefined, {
265
+ kind: 'docs',
266
+ newTab: true,
267
+ title: 'Docs',
268
+ });
269
+
270
+ fireEvent.click(screen.getByRole('button', { name: 'Settings menu' }));
271
+ fireEvent.click(screen.getByRole('combobox', { name: 'Theme' }));
272
+ fireEvent.click(screen.getByRole('option', { name: 'Cool' }));
273
+
274
+ expect(mocks.setTheme).toHaveBeenCalledWith('cool');
238
275
  });
239
276
 
240
277
  it('creates the default session directly from the compact mobile add button when no menu is needed', () => {
@@ -254,8 +291,8 @@ describe('ChatSidebar create and list basics', () => {
254
291
 
255
292
  fireEvent.click(screen.getByRole('button', { name: 'New Task' }));
256
293
 
257
- expect(mocks.createSession).toHaveBeenCalledWith('native');
258
- expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
294
+ expect(mocks.createSession).toHaveBeenCalledWith('native', undefined);
295
+ expect(mocks.goToChatRoot).toHaveBeenCalledWith();
259
296
  });
260
297
 
261
298
  it('shows a session type badge for non-native sessions in the list', () => {
@@ -341,6 +378,80 @@ describe('ChatSidebar create and list basics', () => {
341
378
  });
342
379
  });
343
380
 
381
+ describe('ChatSidebar activity ordering', () => {
382
+ beforeEach(resetSidebarTestState);
383
+
384
+ it('orders sessions by last message time and ignores metadata updatedAt changes', () => {
385
+ mocks.sessionItems = [
386
+ createSessionItem({
387
+ key: 'session:message-newer',
388
+ createdAt: '2026-03-19T08:00:00.000Z',
389
+ updatedAt: '2026-03-19T09:00:00.000Z',
390
+ lastMessageAt: '2026-03-19T09:00:00.000Z',
391
+ label: 'Message Newer',
392
+ sessionType: 'native',
393
+ sessionTypeMutable: false,
394
+ messageCount: 1
395
+ }),
396
+ createSessionItem({
397
+ key: 'session:metadata-newer',
398
+ createdAt: '2026-03-19T07:00:00.000Z',
399
+ updatedAt: '2026-03-19T12:00:00.000Z',
400
+ lastMessageAt: '2026-03-19T08:00:00.000Z',
401
+ label: 'Metadata Newer',
402
+ sessionType: 'native',
403
+ sessionTypeMutable: false,
404
+ messageCount: 1
405
+ })
406
+ ];
407
+
408
+ render(
409
+ <MemoryRouter>
410
+ <ChatSidebar />
411
+ </MemoryRouter>
412
+ );
413
+
414
+ const messageNewer = screen.getByText('Message Newer');
415
+ const metadataNewer = screen.getByText('Metadata Newer');
416
+
417
+ expect(messageNewer.compareDocumentPosition(metadataNewer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
418
+ });
419
+
420
+ it('uses createdAt for sorting sessions without messages', () => {
421
+ mocks.sessionItems = [
422
+ createSessionItem({
423
+ key: 'session:created-older',
424
+ createdAt: '2026-03-19T08:00:00.000Z',
425
+ updatedAt: '2026-03-19T12:00:00.000Z',
426
+ label: 'Created Older',
427
+ sessionType: 'native',
428
+ sessionTypeMutable: false,
429
+ messageCount: 0
430
+ }),
431
+ createSessionItem({
432
+ key: 'session:created-newer',
433
+ createdAt: '2026-03-19T09:00:00.000Z',
434
+ updatedAt: '2026-03-19T10:00:00.000Z',
435
+ label: 'Created Newer',
436
+ sessionType: 'native',
437
+ sessionTypeMutable: false,
438
+ messageCount: 0
439
+ })
440
+ ];
441
+
442
+ render(
443
+ <MemoryRouter>
444
+ <ChatSidebar />
445
+ </MemoryRouter>
446
+ );
447
+
448
+ const createdNewer = screen.getByText('Created Newer');
449
+ const createdOlder = screen.getByText('Created Older');
450
+
451
+ expect(createdNewer.compareDocumentPosition(createdOlder) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
452
+ });
453
+ });
454
+
344
455
  describe('ChatSidebar project-first mode', () => {
345
456
  beforeEach(resetSidebarTestState);
346
457
 
@@ -490,7 +601,7 @@ describe('ChatSidebar project-first mode', () => {
490
601
  fireEvent.click(screen.getByText('Codex'));
491
602
 
492
603
  expect(mocks.createSession).toHaveBeenCalledWith('codex', '/tmp/project-mobile');
493
- expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
604
+ expect(mocks.goToChatRoot).toHaveBeenCalledWith();
494
605
  });
495
606
  });
496
607