@nextclaw/ui 0.12.24 → 0.12.26

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 (210) hide show
  1. package/CHANGELOG.md +136 -29
  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-HgLgrEg4.js +8 -0
  6. package/dist/assets/chat-page-DAKMFDrS.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-DVUbOWbR.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.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-xqN1slyW.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-Cuwst6cc.js +100 -0
  23. package/dist/assets/index-dlcqieQ0.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BeFbwxR-.js +105 -0
  26. package/dist/assets/marketplace-page-CR4xq-TM.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DwnaLNTx.js +40 -0
  29. package/dist/assets/model-config-L2l6YAlQ.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.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-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DYAEunOp.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-BdeU8PEK.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-CQUhd5RU.js +1 -0
  43. package/dist/assets/secrets-config-D-NWlW9q.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.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-FrkmkT8r.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-CFVdPpNv.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 +53 -35
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +16 -12
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +74 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +8 -2
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +262 -114
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +210 -174
  76. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  78. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +27 -6
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  80. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  81. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  82. package/src/features/chat/components/providers/chat-presenter.provider.tsx +4 -0
  83. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +153 -80
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  87. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  88. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  89. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  90. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  91. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -2
  92. package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
  93. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +7 -0
  94. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  95. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  96. package/src/features/chat/pages/ncp-chat-page.tsx +9 -5
  97. package/src/features/chat/stores/chat-input.store.ts +3 -1
  98. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  99. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  100. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  101. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  102. package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
  103. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  104. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  105. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  106. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  107. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  108. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  109. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  110. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  111. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  112. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  113. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  114. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  115. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  116. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  117. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  118. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  119. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  120. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  121. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  122. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  123. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  124. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  125. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  126. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  127. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  128. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  129. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  130. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  131. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  132. package/src/index.css +8 -0
  133. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +68 -0
  134. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  135. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  136. package/src/platforms/desktop/index.ts +6 -0
  137. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  138. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  139. package/src/shared/components/common/brand-header.tsx +36 -16
  140. package/src/shared/components/config/provider-form-support.ts +2 -22
  141. package/src/shared/components/cron-config.tsx +12 -58
  142. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  143. package/src/shared/components/ui/select.tsx +19 -7
  144. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  145. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  146. package/src/shared/lib/api/types.ts +12 -1
  147. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  148. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  149. package/src/shared/lib/cron/index.ts +1 -0
  150. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  151. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  152. package/src/shared/lib/i18n/index.ts +20 -59
  153. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  154. package/src/shared/lib/provider-models/index.test.ts +39 -0
  155. package/src/shared/lib/provider-models/index.ts +1 -3
  156. package/src/shared/lib/ui-document-title/index.ts +0 -1
  157. package/tsconfig.json +1 -0
  158. package/vite.config.ts +1 -1
  159. package/vitest.config.ts +1 -1
  160. package/dist/assets/api-D2xRKmZd.js +0 -15
  161. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  162. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  163. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  164. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  165. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  166. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  167. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  168. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  169. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  170. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  171. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  172. package/dist/assets/folder-CeJKPx5P.js +0 -1
  173. package/dist/assets/hash-BqxRTZW5.js +0 -1
  174. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  175. package/dist/assets/index-D8MKmXtO.css +0 -1
  176. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  177. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  178. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  179. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  180. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  181. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  182. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  183. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  184. package/dist/assets/message-square-z_osm9c0.js +0 -1
  185. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  186. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  187. package/dist/assets/plus-D8eKFY7h.js +0 -1
  188. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  189. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  190. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  191. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  192. package/dist/assets/remote-BOxo9iwd.js +0 -1
  193. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  194. package/dist/assets/search-config-J4Htco-P.js +0 -1
  195. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  196. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  197. package/dist/assets/settings-drbWqzA4.js +0 -1
  198. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  199. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  200. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  201. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  202. package/dist/assets/use-config-38Ur-89i.js +0 -1
  203. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  204. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  205. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  206. package/dist/assets/x-CM-XDMpk.js +0 -1
  207. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  208. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  209. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  210. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -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', () => {
@@ -564,7 +601,7 @@ describe('ChatSidebar project-first mode', () => {
564
601
  fireEvent.click(screen.getByText('Codex'));
565
602
 
566
603
  expect(mocks.createSession).toHaveBeenCalledWith('codex', '/tmp/project-mobile');
567
- expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
604
+ expect(mocks.goToChatRoot).toHaveBeenCalledWith();
568
605
  });
569
606
  });
570
607
 
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react';
2
2
  import type { SessionEntryView } from '@/shared/lib/api';
3
3
  import { BrandHeader } from '@/shared/components/common/brand-header';
4
4
  import { StatusBadge } from '@/shared/components/common/status-badge';
5
- import { SelectItem } from '@/shared/components/ui/select';
6
5
  import { ChatSidebarListModeSwitch, ChatSidebarProjectGroups, type ChatSidebarProjectGroup } from '@/features/chat';
7
6
  import { useChatSidebarSessionLabelEditor } from '@/features/chat/hooks/use-chat-sidebar-session-label-editor';
8
7
  import { useNcpSessionListView, type NcpSessionListItemView } from '@/features/chat/hooks/use-ncp-session-list-view';
@@ -14,26 +13,24 @@ import { useAgents } from '@/shared/hooks/use-agents';
14
13
  import { getSessionProjectName } from '@/shared/lib/session-project';
15
14
  import { cn } from '@/shared/lib/utils';
16
15
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/shared/lib/i18n';
17
- import { THEME_OPTIONS, type UiTheme } from '@/shared/lib/theme';
16
+ import { THEME_OPTIONS } from '@/shared/lib/theme';
18
17
  import { useI18n } from '@/app/components/i18n-provider';
19
18
  import { useTheme } from '@/app/components/theme-provider';
20
19
  import { useDocBrowser } from '@/shared/components/doc-browser';
21
- import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/app/components/layout/sidebar-items';
20
+ import { SidebarNavLinkItem } from '@/app/components/layout/sidebar-items';
22
21
  import {
23
22
  AlarmClock,
24
23
  Bot,
25
- BookOpen,
26
24
  BrainCircuit,
27
- Languages,
28
25
  MessageSquareText,
29
- Palette,
30
- Settings
31
26
  } from 'lucide-react';
32
27
  import { ChatSidebarSessionEntry } from '@/features/chat/components/layout/chat-sidebar-session-entry';
33
28
  import {
34
29
  ChatSidebarDesktopToolbar,
35
30
  ChatSidebarMobileToolbar,
36
31
  } from '@/features/chat/components/layout/chat-sidebar-toolbar';
32
+ import { ChatSidebarUtilityMenu } from '@/features/chat/components/layout/chat-sidebar-utility-menu';
33
+ import { isMacDesktopHost, isWindowsDesktopHost } from '@/platforms/desktop';
37
34
 
38
35
  type DateGroup = {
39
36
  label: string;
@@ -186,6 +183,7 @@ export function ChatSidebar({
186
183
  const presenter = usePresenter();
187
184
  const docBrowser = useDocBrowser();
188
185
  const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
186
+ const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
189
187
  const inputSnapshot = useChatInputStore((state) => state.snapshot);
190
188
  const listSnapshot = useChatSessionListStore((state) => state.snapshot);
191
189
  const systemStatus = useSystemStatus();
@@ -195,6 +193,14 @@ export function ChatSidebar({
195
193
  const { theme, setTheme } = useTheme();
196
194
  const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
197
195
  const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
196
+ const utilityThemeOptions = useMemo(
197
+ () => THEME_OPTIONS.map((option) => ({ value: option.value, label: t(option.labelKey) })),
198
+ [],
199
+ );
200
+ const utilityLanguageOptions = useMemo(
201
+ () => LANGUAGE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
202
+ [],
203
+ );
198
204
  const agentsById = useMemo(
199
205
  () => new Map((agentsQuery.data?.agents ?? []).map((agent) => [agent.id, agent])),
200
206
  [agentsQuery.data?.agents]
@@ -262,10 +268,10 @@ export function ChatSidebar({
262
268
  : 'w-[280px] shrink-0 border-r border-gray-200/60',
263
269
  )}
264
270
  >
265
- {!isMobileVariant ? (
266
- <div className="px-5 pt-5 pb-3">
271
+ {!isMobileVariant && !isWindowsDesktopHost() ? (
272
+ <div className={cn('px-5 pb-3', isMacDesktopHost() ? 'pt-1.5' : 'pt-5')}>
267
273
  <BrandHeader
268
- className="flex items-center gap-2.5 min-w-0"
274
+ className="flex min-w-0 items-center gap-2"
269
275
  suffix={<StatusBadge status={systemStatus.connectionStatus} />}
270
276
  />
271
277
  </div>
@@ -361,43 +367,20 @@ export function ChatSidebar({
361
367
  </div>
362
368
 
363
369
  {!isMobileVariant ? (
364
- <div className="px-3 py-3 border-t border-gray-200/60 space-y-0.5">
365
- <SidebarNavLinkItem
366
- to="/settings"
367
- label={t('settings')}
368
- icon={Settings}
369
- density="compact"
370
+ <div className="px-3 py-3 border-t border-gray-200/60">
371
+ <ChatSidebarUtilityMenu
372
+ isOpen={isUtilityMenuOpen}
373
+ onOpenChange={setIsUtilityMenuOpen}
374
+ currentTheme={theme}
375
+ currentThemeLabel={currentThemeLabel}
376
+ themeOptions={utilityThemeOptions}
377
+ onSelectTheme={setTheme}
378
+ currentLanguage={language}
379
+ currentLanguageLabel={currentLanguageLabel}
380
+ languageOptions={utilityLanguageOptions}
381
+ onSelectLanguage={handleLanguageSwitch}
382
+ onOpenDocs={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
370
383
  />
371
- <SidebarActionItem
372
- onClick={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
373
- icon={BookOpen}
374
- label={t('docBrowserHelp')}
375
- density="compact"
376
- />
377
- <SidebarSelectItem
378
- value={theme}
379
- onValueChange={(value) => setTheme(value as UiTheme)}
380
- icon={Palette}
381
- label={t('theme')}
382
- valueLabel={currentThemeLabel}
383
- density="compact"
384
- >
385
- {THEME_OPTIONS.map((option) => (
386
- <SelectItem key={option.value} value={option.value} className="text-xs">{t(option.labelKey)}</SelectItem>
387
- ))}
388
- </SidebarSelectItem>
389
- <SidebarSelectItem
390
- value={language}
391
- onValueChange={(value) => handleLanguageSwitch(value as I18nLanguage)}
392
- icon={Languages}
393
- label={t('language')}
394
- valueLabel={currentLanguageLabel}
395
- density="compact"
396
- >
397
- {LANGUAGE_OPTIONS.map((option) => (
398
- <SelectItem key={option.value} value={option.value} className="text-xs">{option.label}</SelectItem>
399
- ))}
400
- </SidebarSelectItem>
401
384
  </div>
402
385
  ) : null}
403
386
  </aside>
@@ -10,6 +10,8 @@ import type { ChatThreadSnapshot } from '@/features/chat/stores/chat-thread.stor
10
10
  export type ChatInputManagerLike = {
11
11
  syncSnapshot: (patch: Record<string, unknown>) => void;
12
12
  setDraft: (next: SetStateAction<string>) => void;
13
+ requestComposerFocusAtEnd: () => void;
14
+ consumeComposerFocusRequest: (requestId: number) => void;
13
15
  setComposerNodes: (next: SetStateAction<ChatComposerNode[]>) => void;
14
16
  addAttachments?: (attachments: NcpDraftAttachment[]) => NcpDraftAttachment[];
15
17
  restoreComposerState?: (nodes: ChatComposerNode[], attachments: NcpDraftAttachment[]) => void;
@@ -33,6 +35,7 @@ export type ChatThreadManagerLike = {
33
35
  createSession: () => void;
34
36
  goToProviders: () => void;
35
37
  openChildSessionPanel: (params: { parentSessionKey: string; activeChildSessionKey?: string | null }) => void;
38
+ openSessionCronPanel: (sessionKey: string) => void;
36
39
  openFilePreview: (action: ChatFileOpenActionViewModel) => void;
37
40
  openSessionFromToolAction: (action: ChatToolActionViewModel) => void;
38
41
  selectChildSessionDetail: (sessionKey: string) => void;
@@ -48,6 +51,7 @@ export type ChatPresenterLike = {
48
51
  chatInputManager: ChatInputManagerLike;
49
52
  chatSessionListManager: ChatSessionListManager;
50
53
  chatThreadManager: ChatThreadManagerLike;
54
+ startAgentCreationDraft: (prompt: string) => void;
51
55
  };
52
56
 
53
57
  const ChatPresenterContext = createContext<ChatPresenterLike | null>(null);
@@ -0,0 +1,103 @@
1
+ import { Trash2 } from "lucide-react";
2
+ import type { CronJobView } from "@/shared/lib/api";
3
+ import { Button } from "@/shared/components/ui/button";
4
+ import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
5
+ import { useDeleteCronJob } from "@/shared/hooks/use-config";
6
+ import {
7
+ describeCronDelivery,
8
+ describeCronSchedule,
9
+ formatCronDate,
10
+ } from "@/shared/lib/cron";
11
+ import { t } from "@/shared/lib/i18n";
12
+
13
+ export function SessionCronJobContent({ jobs }: { jobs: readonly CronJobView[] }) {
14
+ const deleteCronJob = useDeleteCronJob();
15
+ const { confirm, ConfirmDialog } = useConfirmDialog();
16
+
17
+ const handleDelete = async (job: CronJobView) => {
18
+ const confirmed = await confirm({
19
+ title: `${t("cronDeleteConfirm")}?`,
20
+ description: job.name ? `${job.name} (${job.id})` : job.id,
21
+ confirmLabel: t("delete"),
22
+ });
23
+ if (!confirmed) {
24
+ return;
25
+ }
26
+ deleteCronJob.mutate({ id: job.id });
27
+ };
28
+
29
+ return (
30
+ <div className="h-full overflow-y-auto custom-scrollbar px-4 py-4">
31
+ <div className="mb-4">
32
+ <div className="text-sm font-semibold text-gray-900">
33
+ {t("chatWorkspaceSessionCronJobs")}
34
+ </div>
35
+ <div className="mt-1 text-xs text-gray-500">
36
+ {t("cronTotalLabel")}: {jobs.length}
37
+ </div>
38
+ </div>
39
+ {jobs.length === 0 ? (
40
+ <div className="py-8 text-center text-sm text-gray-500">
41
+ {t("chatWorkspaceCronJobEmpty")}
42
+ </div>
43
+ ) : (
44
+ <div className="space-y-3">
45
+ {jobs.map((job) => (
46
+ <div
47
+ key={job.id}
48
+ className="rounded-lg border border-gray-200 bg-white px-3 py-3"
49
+ >
50
+ <div className="flex items-start justify-between gap-3">
51
+ <div className="min-w-0">
52
+ <div className="truncate text-sm font-semibold text-gray-900">
53
+ {job.name || job.id}
54
+ </div>
55
+ <div className="mt-1 text-[11px] text-gray-400">
56
+ {job.id}
57
+ </div>
58
+ </div>
59
+ <Button
60
+ type="button"
61
+ variant="subtle"
62
+ size="sm"
63
+ className="h-7 gap-1 rounded-lg px-2"
64
+ onClick={() => void handleDelete(job)}
65
+ disabled={deleteCronJob.isPending}
66
+ >
67
+ <Trash2 className="h-3.5 w-3.5" />
68
+ {t("delete")}
69
+ </Button>
70
+ </div>
71
+ <div className="mt-3 space-y-1.5 text-xs text-gray-500">
72
+ <div>
73
+ <span className="font-medium text-gray-700">{t("cronScheduleLabel")}:</span>{" "}
74
+ {describeCronSchedule(job)}
75
+ </div>
76
+ <div>
77
+ <span className="font-medium text-gray-700">{t("cronNextRun")}:</span>{" "}
78
+ {formatCronDate(job.state.nextRunAt)}
79
+ </div>
80
+ <div>
81
+ <span className="font-medium text-gray-700">{t("cronLastRun")}:</span>{" "}
82
+ {formatCronDate(job.state.lastRunAt)}
83
+ </div>
84
+ <div>
85
+ <span className="font-medium text-gray-700">{t("cronLastStatus")}:</span>{" "}
86
+ {job.state.lastStatus ?? "-"}
87
+ </div>
88
+ <div>
89
+ <span className="font-medium text-gray-700">{t("cronDeliverTo")}:</span>{" "}
90
+ {describeCronDelivery(job)}
91
+ </div>
92
+ </div>
93
+ <div className="mt-3 whitespace-pre-wrap break-words text-sm text-gray-700">
94
+ {job.payload.message}
95
+ </div>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ )}
100
+ <ConfirmDialog />
101
+ </div>
102
+ );
103
+ }