@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -1,20 +1,22 @@
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 { desktopPresenceManager } from "@/desktop/managers/desktop-presence.manager";
13
+ import { useDesktopPresenceStore } from "@/desktop/stores/desktop-presence.store";
14
+ import { useRuntimeControl } from "@/hooks/use-runtime-control";
15
+ import { t } from "@/lib/i18n";
9
16
 
10
17
  function PresenceHint(props: { title: string; description: string }) {
11
18
  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
- );
19
+ return <NoticeCard tone="neutral" title={title} description={description} />;
18
20
  }
19
21
 
20
22
  export function RuntimePresenceCard() {
@@ -26,71 +28,83 @@ export function RuntimePresenceCard() {
26
28
  const snapshot = useDesktopPresenceStore((state) => state.snapshot);
27
29
 
28
30
  useEffect(() => {
29
- if (environment === 'desktop-embedded') {
31
+ if (environment === "desktop-embedded") {
30
32
  void desktopPresenceManager.start();
31
33
  return;
32
34
  }
33
35
  desktopPresenceManager.markUnsupported();
34
36
  }, [environment]);
35
37
 
36
- if (environment === 'desktop-embedded') {
38
+ if (environment === "desktop-embedded") {
37
39
  return (
38
40
  <Card>
39
41
  <CardHeader>
40
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
41
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
42
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
43
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
42
44
  </CardHeader>
43
45
  <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>
46
+ <NoticeCard
47
+ tone="neutral"
48
+ title={t("runtimePresenceBehaviorLabel")}
49
+ description={
50
+ snapshot?.closeToBackground
51
+ ? t("runtimePresenceBehaviorBackground")
52
+ : t("runtimePresenceBehaviorQuit")
53
+ }
54
+ className="rounded-xl"
55
+ />
52
56
 
53
57
  {!initialized || (supported && !snapshot) ? (
54
- <p className="text-sm text-gray-500">{t('runtimePresenceLoading')}</p>
58
+ <p className="text-sm text-gray-500">
59
+ {t("runtimePresenceLoading")}
60
+ </p>
55
61
  ) : null}
56
62
 
57
63
  {snapshot ? (
58
64
  <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>
65
+ <SettingRow
66
+ title={t("runtimePresenceCloseToBackground")}
67
+ description={t("runtimePresenceCloseToBackgroundHelp")}
68
+ control={
69
+ <Switch
70
+ id="runtime-presence-close-background"
71
+ aria-label={t("runtimePresenceCloseToBackground")}
72
+ checked={snapshot.closeToBackground}
73
+ disabled={busyAction === "saving-preferences"}
74
+ onCheckedChange={(checked) => {
75
+ void desktopPresenceManager.updatePreferences({
76
+ closeToBackground: checked,
77
+ });
78
+ }}
79
+ />
80
+ }
81
+ />
74
82
 
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>
83
+ <SettingRow
84
+ title={t("runtimePresenceLaunchAtLogin")}
85
+ description={
86
+ snapshot.supportsLaunchAtLogin
87
+ ? t("runtimePresenceLaunchAtLoginHelp")
88
+ : (snapshot.launchAtLoginReason ??
89
+ t("runtimePresenceLaunchAtLoginUnavailable"))
90
+ }
91
+ control={
92
+ <Switch
93
+ id="runtime-presence-launch-login"
94
+ aria-label={t("runtimePresenceLaunchAtLogin")}
95
+ checked={snapshot.launchAtLogin}
96
+ disabled={
97
+ !snapshot.supportsLaunchAtLogin ||
98
+ busyAction === "saving-preferences"
99
+ }
100
+ onCheckedChange={(checked) => {
101
+ void desktopPresenceManager.updatePreferences({
102
+ launchAtLogin: checked,
103
+ });
104
+ }}
105
+ />
106
+ }
107
+ />
94
108
  </div>
95
109
  ) : null}
96
110
  </CardContent>
@@ -98,51 +112,51 @@ export function RuntimePresenceCard() {
98
112
  );
99
113
  }
100
114
 
101
- if (environment === 'managed-local-service') {
115
+ if (environment === "managed-local-service") {
102
116
  return (
103
117
  <Card>
104
118
  <CardHeader>
105
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
106
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
119
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
120
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
107
121
  </CardHeader>
108
122
  <CardContent>
109
123
  <PresenceHint
110
- title={t('runtimePresenceManagedLocalTitle')}
111
- description={t('runtimePresenceManagedLocalDescription')}
124
+ title={t("runtimePresenceManagedLocalTitle")}
125
+ description={t("runtimePresenceManagedLocalDescription")}
112
126
  />
113
127
  </CardContent>
114
128
  </Card>
115
129
  );
116
130
  }
117
131
 
118
- if (environment === 'self-hosted-web') {
132
+ if (environment === "self-hosted-web") {
119
133
  return (
120
134
  <Card>
121
135
  <CardHeader>
122
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
123
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
136
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
137
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
124
138
  </CardHeader>
125
139
  <CardContent>
126
140
  <PresenceHint
127
- title={t('runtimePresenceSelfHostedTitle')}
128
- description={t('runtimePresenceSelfHostedDescription')}
141
+ title={t("runtimePresenceSelfHostedTitle")}
142
+ description={t("runtimePresenceSelfHostedDescription")}
129
143
  />
130
144
  </CardContent>
131
145
  </Card>
132
146
  );
133
147
  }
134
148
 
135
- if (environment === 'shared-web') {
149
+ if (environment === "shared-web") {
136
150
  return (
137
151
  <Card>
138
152
  <CardHeader>
139
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
140
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
153
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
154
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
141
155
  </CardHeader>
142
156
  <CardContent>
143
157
  <PresenceHint
144
- title={t('runtimePresenceSharedTitle')}
145
- description={t('runtimePresenceSharedDescription')}
158
+ title={t("runtimePresenceSharedTitle")}
159
+ description={t("runtimePresenceSharedDescription")}
146
160
  />
147
161
  </CardContent>
148
162
  </Card>
@@ -152,11 +166,11 @@ export function RuntimePresenceCard() {
152
166
  return (
153
167
  <Card>
154
168
  <CardHeader>
155
- <CardTitle>{t('runtimePresenceTitle')}</CardTitle>
156
- <CardDescription>{t('runtimePresenceDescription')}</CardDescription>
169
+ <CardTitle>{t("runtimePresenceTitle")}</CardTitle>
170
+ <CardDescription>{t("runtimePresenceDescription")}</CardDescription>
157
171
  </CardHeader>
158
172
  <CardContent>
159
- <p className="text-sm text-gray-500">{t('runtimePresenceLoading')}</p>
173
+ <p className="text-sm text-gray-500">{t("runtimePresenceLoading")}</p>
160
174
  </CardContent>
161
175
  </Card>
162
176
  );
@@ -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',
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>
@@ -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,157 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import type { ReactNode } from 'react';
5
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { toast } from 'sonner';
7
+ import { RuntimeStatusEntry } from '@/components/layout/runtime-status-entry';
8
+ import { setLanguage } from '@/lib/i18n';
9
+
10
+ const mocks = vi.hoisted(() => ({
11
+ useRuntimeControl: vi.fn(),
12
+ controlService: vi.fn()
13
+ }));
14
+
15
+ vi.mock('sonner', () => ({
16
+ toast: {
17
+ success: vi.fn(),
18
+ error: vi.fn()
19
+ }
20
+ }));
21
+
22
+ vi.mock('@/hooks/use-runtime-control', () => ({
23
+ useRuntimeControl: (...args: unknown[]) => mocks.useRuntimeControl(...args)
24
+ }));
25
+
26
+ vi.mock('@/runtime-control/runtime-control.manager', () => ({
27
+ runtimeControlManager: {
28
+ controlService: (...args: unknown[]) => mocks.controlService(...args)
29
+ }
30
+ }));
31
+
32
+ function createWrapper(queryClient: QueryClient) {
33
+ return function Wrapper({ children }: { children: ReactNode }) {
34
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
35
+ };
36
+ }
37
+
38
+ describe('RuntimeStatusEntry', () => {
39
+ beforeEach(() => {
40
+ setLanguage('zh');
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ it('shows a compact pending-restart entry with reasons and a restart action', async () => {
45
+ const queryClient = new QueryClient();
46
+ const user = userEvent.setup();
47
+ const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
48
+
49
+ mocks.useRuntimeControl.mockReturnValue({
50
+ data: {
51
+ environment: 'managed-local-service',
52
+ lifecycle: 'healthy',
53
+ serviceState: 'running',
54
+ message: 'runtime healthy',
55
+ pendingRestart: {
56
+ changedPaths: ['plugins', 'ui'],
57
+ message: 'Saved changes are waiting for a manual restart.',
58
+ reasons: ['config reload requires restart: plugins, ui'],
59
+ requestedAt: '2026-04-17T12:00:00.000Z'
60
+ },
61
+ canStartService: {
62
+ available: false,
63
+ requiresConfirmation: false,
64
+ impact: 'brief-ui-disconnect'
65
+ },
66
+ canRestartService: {
67
+ available: true,
68
+ requiresConfirmation: false,
69
+ impact: 'brief-ui-disconnect'
70
+ },
71
+ canStopService: {
72
+ available: true,
73
+ requiresConfirmation: true,
74
+ impact: 'brief-ui-disconnect'
75
+ },
76
+ canRestartApp: {
77
+ available: false,
78
+ requiresConfirmation: true,
79
+ impact: 'full-app-relaunch'
80
+ }
81
+ },
82
+ isError: false,
83
+ error: null
84
+ });
85
+ mocks.controlService.mockResolvedValue({
86
+ accepted: true,
87
+ action: 'restart-service',
88
+ lifecycle: 'restarting-service',
89
+ message: 'Restart scheduled. This page may disconnect for a few seconds.'
90
+ });
91
+
92
+ render(<RuntimeStatusEntry />, {
93
+ wrapper: createWrapper(queryClient)
94
+ });
95
+
96
+ await user.click(screen.getByTestId('runtime-status-entry'));
97
+
98
+ expect(screen.getByText('待重启')).toBeTruthy();
99
+ expect(screen.getByText('这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。')).toBeTruthy();
100
+ expect(screen.getByText('plugins 改动将在重启后生效。')).toBeTruthy();
101
+ expect(screen.getByText('ui 改动将在重启后生效。')).toBeTruthy();
102
+
103
+ await user.click(screen.getByRole('button', { name: '立即重启' }));
104
+
105
+ await waitFor(() => {
106
+ expect(mocks.controlService).toHaveBeenCalledWith('restart-service');
107
+ });
108
+ expect(toast.success).toHaveBeenCalledWith('Restart scheduled. This page may disconnect for a few seconds.');
109
+ expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['runtime-control'] });
110
+ });
111
+
112
+ it('shows a healthy status without restart controls when no action is needed', async () => {
113
+ const queryClient = new QueryClient();
114
+ const user = userEvent.setup();
115
+
116
+ mocks.useRuntimeControl.mockReturnValue({
117
+ data: {
118
+ environment: 'managed-local-service',
119
+ lifecycle: 'healthy',
120
+ serviceState: 'running',
121
+ message: 'runtime healthy',
122
+ pendingRestart: null,
123
+ canStartService: {
124
+ available: false,
125
+ requiresConfirmation: false,
126
+ impact: 'brief-ui-disconnect'
127
+ },
128
+ canRestartService: {
129
+ available: true,
130
+ requiresConfirmation: false,
131
+ impact: 'brief-ui-disconnect'
132
+ },
133
+ canStopService: {
134
+ available: true,
135
+ requiresConfirmation: true,
136
+ impact: 'brief-ui-disconnect'
137
+ },
138
+ canRestartApp: {
139
+ available: false,
140
+ requiresConfirmation: true,
141
+ impact: 'full-app-relaunch'
142
+ }
143
+ },
144
+ isError: false,
145
+ error: null
146
+ });
147
+
148
+ render(<RuntimeStatusEntry />, {
149
+ wrapper: createWrapper(queryClient)
150
+ });
151
+
152
+ await user.click(screen.getByTestId('runtime-status-entry'));
153
+
154
+ expect(screen.getByText('系统正常')).toBeTruthy();
155
+ expect(screen.queryByRole('button', { name: '立即重启' })).toBeNull();
156
+ });
157
+ });