@nextclaw/ui 0.12.9 → 0.12.11

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 (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -1,96 +1,112 @@
1
- import { useEffect } from 'react';
2
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
3
- import { Label } from '@/components/ui/label';
4
- import { Switch } from '@/components/ui/switch';
5
- import { desktopPresenceManager } from '@/desktop/managers/desktop-presence.manager';
6
- import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
7
- import { useRuntimeControl } from '@/hooks/use-runtime-control';
8
- import { t } from '@/lib/i18n';
1
+ import { useEffect } from "react";
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "@/components/ui/card";
9
+ import { NoticeCard } from "@/components/ui/notice-card";
10
+ import { SettingRow } from "@/components/ui/setting-row";
11
+ import { Switch } from "@/components/ui/switch";
12
+ import {
13
+ desktopPresenceManager,
14
+ useDesktopPresenceStore,
15
+ } from "@/platforms/desktop";
16
+ import { useSystemStatus } from "@/features/system-status";
17
+ import { t } from "@/lib/i18n";
9
18
 
10
19
  function PresenceHint(props: { title: string; description: string }) {
11
20
  const { description, title } = props;
12
- return (
13
- <div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
14
- <p className="text-sm font-medium text-gray-900">{title}</p>
15
- <p className="mt-2 text-sm leading-6 text-gray-600">{description}</p>
16
- </div>
17
- );
21
+ return <NoticeCard tone="neutral" title={title} description={description} />;
18
22
  }
19
23
 
20
24
  export function RuntimePresenceCard() {
21
- const runtimeControlQuery = useRuntimeControl();
22
- const environment = runtimeControlQuery.data?.environment;
25
+ const systemStatus = useSystemStatus();
26
+ const environment = systemStatus.runtimeControlView?.environment;
23
27
  const supported = useDesktopPresenceStore((state) => state.supported);
24
28
  const initialized = useDesktopPresenceStore((state) => state.initialized);
25
29
  const busyAction = useDesktopPresenceStore((state) => state.busyAction);
26
30
  const snapshot = useDesktopPresenceStore((state) => state.snapshot);
27
31
 
28
32
  useEffect(() => {
29
- if (environment === 'desktop-embedded') {
33
+ if (environment === "desktop-embedded") {
30
34
  void desktopPresenceManager.start();
31
35
  return;
32
36
  }
33
37
  desktopPresenceManager.markUnsupported();
34
38
  }, [environment]);
35
39
 
36
- if (environment === 'desktop-embedded') {
40
+ if (environment === "desktop-embedded") {
37
41
  return (
38
42
  <Card>
39
43
  <CardHeader>
40
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
41
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
44
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
45
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
42
46
  </CardHeader>
43
47
  <CardContent className="space-y-4">
44
- <div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
45
- <p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">
46
- {t('runtimePresenceBehaviorLabel')}
47
- </p>
48
- <p className="mt-2 text-sm font-medium text-gray-900">
49
- {snapshot?.closeToBackground ? t('runtimePresenceBehaviorBackground') : t('runtimePresenceBehaviorQuit')}
50
- </p>
51
- </div>
48
+ <NoticeCard
49
+ tone="neutral"
50
+ title={t("runtimePresenceBehaviorLabel")}
51
+ description={
52
+ snapshot?.closeToBackground
53
+ ? t("runtimePresenceBehaviorBackground")
54
+ : t("runtimePresenceBehaviorQuit")
55
+ }
56
+ className="rounded-xl"
57
+ />
52
58
 
53
59
  {!initialized || (supported && !snapshot) ? (
54
- <p className="text-sm text-gray-500">{t('runtimePresenceLoading')}</p>
60
+ <p className="text-sm text-gray-500">
61
+ {t("runtimePresenceLoading")}
62
+ </p>
55
63
  ) : null}
56
64
 
57
65
  {snapshot ? (
58
66
  <div className="space-y-4">
59
- <div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
60
- <div className="space-y-2">
61
- <Label htmlFor="runtime-presence-close-background">{t('runtimePresenceCloseToBackground')}</Label>
62
- <p className="text-sm text-gray-500">{t('runtimePresenceCloseToBackgroundHelp')}</p>
63
- </div>
64
- <Switch
65
- id="runtime-presence-close-background"
66
- aria-label={t('runtimePresenceCloseToBackground')}
67
- checked={snapshot.closeToBackground}
68
- disabled={busyAction === 'saving-preferences'}
69
- onCheckedChange={(checked) => {
70
- void desktopPresenceManager.updatePreferences({ closeToBackground: checked });
71
- }}
72
- />
73
- </div>
67
+ <SettingRow
68
+ title={t("runtimePresenceCloseToBackground")}
69
+ description={t("runtimePresenceCloseToBackgroundHelp")}
70
+ control={
71
+ <Switch
72
+ id="runtime-presence-close-background"
73
+ aria-label={t("runtimePresenceCloseToBackground")}
74
+ checked={snapshot.closeToBackground}
75
+ disabled={busyAction === "saving-preferences"}
76
+ onCheckedChange={(checked) => {
77
+ void desktopPresenceManager.updatePreferences({
78
+ closeToBackground: checked,
79
+ });
80
+ }}
81
+ />
82
+ }
83
+ />
74
84
 
75
- <div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
76
- <div className="space-y-2">
77
- <Label htmlFor="runtime-presence-launch-login">{t('runtimePresenceLaunchAtLogin')}</Label>
78
- <p className="text-sm text-gray-500">
79
- {snapshot.supportsLaunchAtLogin
80
- ? t('runtimePresenceLaunchAtLoginHelp')
81
- : snapshot.launchAtLoginReason ?? t('runtimePresenceLaunchAtLoginUnavailable')}
82
- </p>
83
- </div>
84
- <Switch
85
- id="runtime-presence-launch-login"
86
- aria-label={t('runtimePresenceLaunchAtLogin')}
87
- checked={snapshot.launchAtLogin}
88
- disabled={!snapshot.supportsLaunchAtLogin || busyAction === 'saving-preferences'}
89
- onCheckedChange={(checked) => {
90
- void desktopPresenceManager.updatePreferences({ launchAtLogin: checked });
91
- }}
92
- />
93
- </div>
85
+ <SettingRow
86
+ title={t("runtimePresenceLaunchAtLogin")}
87
+ description={
88
+ snapshot.supportsLaunchAtLogin
89
+ ? t("runtimePresenceLaunchAtLoginHelp")
90
+ : (snapshot.launchAtLoginReason ??
91
+ t("runtimePresenceLaunchAtLoginUnavailable"))
92
+ }
93
+ control={
94
+ <Switch
95
+ id="runtime-presence-launch-login"
96
+ aria-label={t("runtimePresenceLaunchAtLogin")}
97
+ checked={snapshot.launchAtLogin}
98
+ disabled={
99
+ !snapshot.supportsLaunchAtLogin ||
100
+ busyAction === "saving-preferences"
101
+ }
102
+ onCheckedChange={(checked) => {
103
+ void desktopPresenceManager.updatePreferences({
104
+ launchAtLogin: checked,
105
+ });
106
+ }}
107
+ />
108
+ }
109
+ />
94
110
  </div>
95
111
  ) : null}
96
112
  </CardContent>
@@ -98,51 +114,51 @@ export function RuntimePresenceCard() {
98
114
  );
99
115
  }
100
116
 
101
- if (environment === 'managed-local-service') {
117
+ if (environment === "managed-local-service") {
102
118
  return (
103
119
  <Card>
104
120
  <CardHeader>
105
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
106
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
121
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
122
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
107
123
  </CardHeader>
108
124
  <CardContent>
109
125
  <PresenceHint
110
- title={t('runtimePresenceManagedLocalTitle')}
111
- description={t('runtimePresenceManagedLocalDescription')}
126
+ title={t("runtimePresenceManagedLocalTitle")}
127
+ description={t("runtimePresenceManagedLocalDescription")}
112
128
  />
113
129
  </CardContent>
114
130
  </Card>
115
131
  );
116
132
  }
117
133
 
118
- if (environment === 'self-hosted-web') {
134
+ if (environment === "self-hosted-web") {
119
135
  return (
120
136
  <Card>
121
137
  <CardHeader>
122
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
123
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
138
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
139
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
124
140
  </CardHeader>
125
141
  <CardContent>
126
142
  <PresenceHint
127
- title={t('runtimePresenceSelfHostedTitle')}
128
- description={t('runtimePresenceSelfHostedDescription')}
143
+ title={t("runtimePresenceSelfHostedTitle")}
144
+ description={t("runtimePresenceSelfHostedDescription")}
129
145
  />
130
146
  </CardContent>
131
147
  </Card>
132
148
  );
133
149
  }
134
150
 
135
- if (environment === 'shared-web') {
151
+ if (environment === "shared-web") {
136
152
  return (
137
153
  <Card>
138
154
  <CardHeader>
139
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
140
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
155
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
156
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
141
157
  </CardHeader>
142
158
  <CardContent>
143
159
  <PresenceHint
144
- title={t('runtimePresenceSharedTitle')}
145
- description={t('runtimePresenceSharedDescription')}
160
+ title={t("runtimePresenceSharedTitle")}
161
+ description={t("runtimePresenceSharedDescription")}
146
162
  />
147
163
  </CardContent>
148
164
  </Card>
@@ -152,11 +168,11 @@ export function RuntimePresenceCard() {
152
168
  return (
153
169
  <Card>
154
170
  <CardHeader>
155
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
156
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
171
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
172
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
157
173
  </CardHeader>
158
174
  <CardContent>
159
- <p className="text-sm text-gray-500">{t('runtimePresenceLoading')}</p>
175
+ <p className="text-sm text-gray-500">{t("runtimePresenceLoading")}</p>
160
176
  </CardContent>
161
177
  </Card>
162
178
  );
@@ -1,13 +1,17 @@
1
- import { lazy, Suspense, useEffect } from 'react';
2
- import { useLocation } from 'react-router-dom';
3
- import { Sidebar } from './Sidebar';
4
- import { DocBrowserProvider, useDocBrowser } from '@/components/doc-browser/DocBrowserContext';
5
- import { useDocLinkInterceptor } from '@/components/doc-browser/useDocLinkInterceptor';
6
- import { useI18n } from '@/components/providers/I18nProvider';
7
- import { resolveUiDocumentTitle } from '@/lib/ui-document-title';
8
- import { cn } from '@/lib/utils';
1
+ import { lazy, Suspense, useEffect } from "react";
2
+ import { useLocation } from "react-router-dom";
3
+ import { Sidebar } from "./Sidebar";
4
+ import {
5
+ DocBrowserProvider,
6
+ useDocBrowser,
7
+ } from "@/components/doc-browser/DocBrowserContext";
8
+ import { useDocLinkInterceptor } from "@/components/doc-browser/useDocLinkInterceptor";
9
+ import { useI18n } from "@/components/providers/I18nProvider";
10
+ import { resolveUiDocumentTitle } from "@/lib/ui-document-title";
9
11
 
10
- const DocBrowser = lazy(async () => ({ default: (await import('@/components/doc-browser/DocBrowser')).DocBrowser }));
12
+ const DocBrowser = lazy(async () => ({
13
+ default: (await import("@/components/doc-browser/DocBrowser")).DocBrowser,
14
+ }));
11
15
 
12
16
  interface AppLayoutProps {
13
17
  children: React.ReactNode;
@@ -16,29 +20,23 @@ interface AppLayoutProps {
16
20
  function isMainWorkspaceRoute(pathname: string): boolean {
17
21
  const normalized = pathname.toLowerCase();
18
22
  return (
19
- normalized === '/chat' ||
20
- normalized.startsWith('/chat/') ||
21
- normalized === '/skills' ||
22
- normalized.startsWith('/skills/') ||
23
- normalized === '/cron' ||
24
- normalized.startsWith('/cron/') ||
25
- normalized === '/agents' ||
26
- normalized.startsWith('/agents/')
23
+ normalized === "/chat" ||
24
+ normalized.startsWith("/chat/") ||
25
+ normalized === "/skills" ||
26
+ normalized.startsWith("/skills/") ||
27
+ normalized === "/cron" ||
28
+ normalized.startsWith("/cron/") ||
29
+ normalized === "/agents" ||
30
+ normalized.startsWith("/agents/")
27
31
  );
28
32
  }
29
33
 
30
- function isChannelsRoute(pathname: string): boolean {
31
- const normalized = pathname.toLowerCase();
32
- return normalized === '/channels' || normalized.startsWith('/channels/');
33
- }
34
-
35
34
  function AppLayoutInner({ children }: AppLayoutProps) {
36
35
  const { isOpen, mode } = useDocBrowser();
37
36
  useDocLinkInterceptor();
38
37
  const { pathname } = useLocation();
39
38
  const { language } = useI18n();
40
39
  const isMainRoute = isMainWorkspaceRoute(pathname);
41
- const lockPageScroll = isChannelsRoute(pathname);
42
40
 
43
41
  useEffect(() => {
44
42
  document.title = resolveUiDocumentTitle(pathname);
@@ -52,31 +50,21 @@ function AppLayoutInner({ children }: AppLayoutProps) {
52
50
  {isMainRoute ? (
53
51
  <div className="flex-1 h-full overflow-hidden">{children}</div>
54
52
  ) : (
55
- <main
56
- className={cn(
57
- 'flex-1 custom-scrollbar p-8 pb-24',
58
- lockPageScroll ? 'overflow-auto xl:overflow-hidden' : 'overflow-auto'
59
- )}
60
- >
61
- <div
62
- className={cn(
63
- 'max-w-6xl mx-auto animate-fade-in h-full',
64
- lockPageScroll && 'min-h-0 xl:overflow-hidden'
65
- )}
66
- >
53
+ <main className="flex-1 overflow-auto p-8 pb-16 custom-scrollbar">
54
+ <div className="mx-auto h-full max-w-6xl animate-fade-in">
67
55
  {children}
68
56
  </div>
69
57
  </main>
70
58
  )}
71
59
  </div>
72
60
  {/* Doc Browser: docked mode renders inline, floating mode renders as overlay */}
73
- {isOpen && mode === 'docked' && (
61
+ {isOpen && mode === "docked" && (
74
62
  <Suspense fallback={null}>
75
63
  <DocBrowser />
76
64
  </Suspense>
77
65
  )}
78
66
  </div>
79
- {isOpen && mode === 'floating' && (
67
+ {isOpen && mode === "floating" && (
80
68
  <Suspense fallback={null}>
81
69
  <DocBrowser />
82
70
  </Suspense>
@@ -9,8 +9,8 @@ import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/comp
9
9
  import { useI18n } from '@/components/providers/I18nProvider';
10
10
  import { useTheme } from '@/components/providers/ThemeProvider';
11
11
  import { SelectItem } from '@/components/ui/select';
12
- import { useRemoteStatus } from '@/hooks/useRemoteAccess';
13
- import { useAppPresenter } from '@/presenter/app-presenter-context';
12
+ import { useAppManager } from '@/app/components/app-manager-provider';
13
+ import { useRemoteStatus } from '@/features/remote';
14
14
 
15
15
  type SidebarMode = 'main' | 'settings';
16
16
 
@@ -19,7 +19,7 @@ type SidebarProps = {
19
19
  };
20
20
 
21
21
  export function Sidebar({ mode }: SidebarProps) {
22
- const presenter = useAppPresenter();
22
+ const manager = useAppManager();
23
23
  const docBrowser = useDocBrowser();
24
24
  const remoteStatus = useRemoteStatus();
25
25
  const { language, setLanguage } = useI18n();
@@ -182,7 +182,7 @@ export function Sidebar({ mode }: SidebarProps) {
182
182
  <div className={cn('shrink-0 border-t border-[#dde0ea] bg-secondary', isSettingsMode ? 'mt-2 pt-3' : 'mt-3 pt-3')}>
183
183
  {isSettingsMode ? (
184
184
  <SidebarActionItem
185
- onClick={() => presenter.accountManager.openAccountPanel()}
185
+ onClick={() => manager.accountManager.openAccountPanel()}
186
186
  icon={KeyRound}
187
187
  label={t('remoteAccountEntryManage')}
188
188
  density="compact"
@@ -1,30 +1,62 @@
1
- import { render, screen } from '@testing-library/react';
2
- import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
- import { describe, expect, it } from 'vitest';
4
- import { AppLayout } from '@/components/layout/AppLayout';
5
- import { I18nProvider } from '@/components/providers/I18nProvider';
1
+ import { render, screen } from "@testing-library/react";
2
+ import { MemoryRouter, Route, Routes } from "react-router-dom";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { AppLayout } from "@/components/layout/AppLayout";
5
+ import { I18nProvider } from "@/components/providers/I18nProvider";
6
6
 
7
- describe('AppLayout', () => {
8
- it('treats /agents as a main workspace route instead of the settings shell', () => {
7
+ vi.mock("@/components/layout/Sidebar", () => ({
8
+ Sidebar: () => (
9
+ <aside data-testid="settings-sidebar-header">Settings Sidebar</aside>
10
+ ),
11
+ }));
12
+
13
+ describe("AppLayout", () => {
14
+ it("treats /agents as a main workspace route instead of the settings shell", () => {
9
15
  const { container } = render(
10
16
  <I18nProvider>
11
- <MemoryRouter initialEntries={['/agents']}>
17
+ <MemoryRouter initialEntries={["/agents"]}>
12
18
  <Routes>
13
19
  <Route
14
20
  path="*"
15
- element={(
21
+ element={
16
22
  <AppLayout>
17
23
  <div data-testid="agents-content">Agents Content</div>
18
24
  </AppLayout>
19
- )}
25
+ }
26
+ />
27
+ </Routes>
28
+ </MemoryRouter>
29
+ </I18nProvider>,
30
+ );
31
+
32
+ expect(screen.getByTestId("agents-content")).toBeTruthy();
33
+ expect(screen.queryByTestId("settings-sidebar-header")).toBeNull();
34
+ expect(container.querySelector("aside")).toBeNull();
35
+ });
36
+
37
+ it("keeps settings routes on the shared shell without channel-specific scroll locking", () => {
38
+ const { container } = render(
39
+ <I18nProvider>
40
+ <MemoryRouter initialEntries={["/channels"]}>
41
+ <Routes>
42
+ <Route
43
+ path="*"
44
+ element={
45
+ <AppLayout>
46
+ <div data-testid="channels-content">Channels Content</div>
47
+ </AppLayout>
48
+ }
20
49
  />
21
50
  </Routes>
22
51
  </MemoryRouter>
23
- </I18nProvider>
52
+ </I18nProvider>,
24
53
  );
25
54
 
26
- expect(screen.getByTestId('agents-content')).toBeTruthy();
27
- expect(screen.queryByTestId('settings-sidebar-header')).toBeNull();
28
- expect(container.querySelector('aside')).toBeNull();
55
+ const main = container.querySelector("main");
56
+
57
+ expect(screen.getByTestId("channels-content")).toBeTruthy();
58
+ expect(main).toBeTruthy();
59
+ expect(main?.className).toContain("overflow-auto");
60
+ expect(main?.className).not.toContain("xl:overflow-hidden");
29
61
  });
30
62
  });
@@ -0,0 +1,101 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { toast } from 'sonner';
5
+ import { RuntimeStatusEntry } from '@/components/layout/runtime-status-entry';
6
+ import { setLanguage } from '@/lib/i18n';
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ useRuntimeStatusBadgeView: vi.fn(),
10
+ runRuntimeControlAction: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('sonner', () => ({
14
+ toast: {
15
+ success: vi.fn(),
16
+ error: vi.fn(),
17
+ },
18
+ }));
19
+
20
+ vi.mock('@/features/system-status', () => ({
21
+ useRuntimeStatusBadgeView: (...args: unknown[]) =>
22
+ mocks.useRuntimeStatusBadgeView(...args),
23
+ systemStatusManager: {
24
+ runRuntimeControlAction: (...args: unknown[]) =>
25
+ mocks.runRuntimeControlAction(...args),
26
+ },
27
+ }));
28
+
29
+ describe('RuntimeStatusEntry', () => {
30
+ beforeEach(() => {
31
+ setLanguage('zh');
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ it('shows a compact pending-restart entry with reasons and a restart action', async () => {
36
+ const user = userEvent.setup();
37
+
38
+ mocks.useRuntimeStatusBadgeView.mockReturnValue({
39
+ tone: 'attention',
40
+ title: '待重启',
41
+ description:
42
+ '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。',
43
+ reasonLines: [
44
+ 'plugins 改动将在重启后生效。',
45
+ 'ui 改动将在重启后生效。',
46
+ ],
47
+ actionLabel: '立即重启',
48
+ isBusy: false,
49
+ });
50
+ mocks.runRuntimeControlAction.mockResolvedValue({
51
+ accepted: true,
52
+ action: 'restart-service',
53
+ lifecycle: 'restarting-service',
54
+ message: 'Restart scheduled. This page may disconnect for a few seconds.',
55
+ });
56
+
57
+ render(<RuntimeStatusEntry />);
58
+
59
+ await user.click(screen.getByTestId('runtime-status-entry'));
60
+
61
+ expect(screen.getByText('待重启')).toBeTruthy();
62
+ expect(
63
+ screen.getByText(
64
+ '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。'
65
+ )
66
+ ).toBeTruthy();
67
+ expect(screen.getByText('plugins 改动将在重启后生效。')).toBeTruthy();
68
+ expect(screen.getByText('ui 改动将在重启后生效。')).toBeTruthy();
69
+
70
+ await user.click(screen.getByRole('button', { name: '立即重启' }));
71
+
72
+ await waitFor(() => {
73
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
74
+ 'restart-service'
75
+ );
76
+ });
77
+ expect(toast.success).toHaveBeenCalledWith(
78
+ 'Restart scheduled. This page may disconnect for a few seconds.'
79
+ );
80
+ });
81
+
82
+ it('shows a healthy status without restart controls when no action is needed', async () => {
83
+ const user = userEvent.setup();
84
+
85
+ mocks.useRuntimeStatusBadgeView.mockReturnValue({
86
+ tone: 'healthy',
87
+ title: '系统正常',
88
+ description: '所有系统状态都正常。',
89
+ reasonLines: [],
90
+ actionLabel: null,
91
+ isBusy: false,
92
+ });
93
+
94
+ render(<RuntimeStatusEntry />);
95
+
96
+ await user.click(screen.getByTestId('runtime-status-entry'));
97
+
98
+ expect(screen.getByText('系统正常')).toBeTruthy();
99
+ expect(screen.queryByRole('button', { name: '立即重启' })).toBeNull();
100
+ });
101
+ });
@@ -0,0 +1,95 @@
1
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
2
+ import {
3
+ systemStatusManager,
4
+ useRuntimeStatusBadgeView,
5
+ } from '@/features/system-status';
6
+ import { t } from '@/lib/i18n';
7
+ import { cn } from '@/lib/utils';
8
+ import { toast } from 'sonner';
9
+
10
+ type RuntimeStatusTone = 'healthy' | 'attention' | 'inactive';
11
+
12
+ const runtimeStatusToneStyles: Record<RuntimeStatusTone, string> = {
13
+ healthy: 'bg-emerald-500 shadow-[0_0_0_3px_rgba(16,185,129,0.14)]',
14
+ attention: 'bg-amber-400 shadow-[0_0_0_3px_rgba(251,191,36,0.16)]',
15
+ inactive: 'bg-gray-300 shadow-[0_0_0_3px_rgba(156,163,175,0.12)]'
16
+ };
17
+
18
+ type RuntimeStatusSummary = {
19
+ actionLabel: string | null;
20
+ description: string;
21
+ reasonLines: string[];
22
+ title: string;
23
+ tone: RuntimeStatusTone;
24
+ isBusy: boolean;
25
+ };
26
+
27
+ export function RuntimeStatusEntry() {
28
+ const summary = useRuntimeStatusBadgeView() as RuntimeStatusSummary;
29
+ const canRestart = summary.actionLabel === t('runtimeStatusRestartAction');
30
+
31
+ const handleRestart = async () => {
32
+ if (!canRestart) {
33
+ return;
34
+ }
35
+ try {
36
+ const result =
37
+ await systemStatusManager.runRuntimeControlAction('restart-service');
38
+ toast.success(result.message);
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
41
+ toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <Popover>
47
+ <PopoverTrigger asChild>
48
+ <button
49
+ type="button"
50
+ className="inline-flex items-center justify-center rounded-full p-0.5 transition-transform hover:scale-105"
51
+ aria-label={summary.title}
52
+ title={summary.title}
53
+ data-testid="runtime-status-entry"
54
+ >
55
+ <span className={cn('h-2.5 w-2.5 rounded-full', runtimeStatusToneStyles[summary.tone])} />
56
+ </button>
57
+ </PopoverTrigger>
58
+ <PopoverContent
59
+ align="start"
60
+ sideOffset={10}
61
+ className="w-[290px] space-y-3 rounded-2xl border border-gray-200 bg-white p-4"
62
+ >
63
+ <div className="space-y-1">
64
+ <div className="text-sm font-semibold text-gray-900">{summary.title}</div>
65
+ <p className="text-xs leading-5 text-gray-600">{summary.description}</p>
66
+ </div>
67
+ {summary.reasonLines.length > 0 ? (
68
+ <div className="space-y-2">
69
+ {summary.reasonLines.map((reason) => (
70
+ <div
71
+ key={reason}
72
+ className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-900"
73
+ >
74
+ {reason}
75
+ </div>
76
+ ))}
77
+ </div>
78
+ ) : null}
79
+ {summary.actionLabel ? (
80
+ <div className="flex items-center justify-between border-t border-gray-100 pt-1">
81
+ <span className="text-[11px] text-gray-500">{t('runtimeStatusActionHint')}</span>
82
+ <button
83
+ type="button"
84
+ onClick={() => void handleRestart()}
85
+ disabled={summary.isBusy}
86
+ className="text-sm font-semibold text-sky-600 transition-colors hover:text-sky-700 disabled:text-gray-400"
87
+ >
88
+ {summary.isBusy ? t('runtimeStatusRestartingAction') : summary.actionLabel}
89
+ </button>
90
+ </div>
91
+ ) : null}
92
+ </PopoverContent>
93
+ </Popover>
94
+ );
95
+ }