@nextclaw/ui 0.12.9 → 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 (178) hide show
  1. package/CHANGELOG.md +61 -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-6ReNjvzF.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-Da7khnBA.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-D281Rotl.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-VnFElj4E.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-BdcxxoQu.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-DK5HPmIK.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-_i4MU2bB.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-DtIQwrHF.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-ccBFUi-o.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-6TrrnPCR.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/{download-BhDxnyvU.js → download-BD0ETkB-.js} +1 -1
  27. package/dist/assets/{external-link-BgErLCNT.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-Bl7dr_UG.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/{i18n-eDHeDY0n.js → i18n-CpTZLchQ.js} +1 -1
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-ZDS92Qpp.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-x89HbrZ4.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-vZnghcFy.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-DT98i__E.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-C47QSEwg.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-JtFzpNn6.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-3S6-H3Xw.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-vbanNPFU.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-BsLtHOH-.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-G48scll7.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-DkNhD-42.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-BkvTN-vd.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +22 -22
  61. package/dist/runtime-icons/claude.ico +0 -0
  62. package/dist/runtime-icons/codex-openai.svg +6 -0
  63. package/dist/runtime-icons/hermes-agent.png +0 -0
  64. package/package.json +6 -6
  65. package/public/runtime-icons/claude.ico +0 -0
  66. package/public/runtime-icons/codex-openai.svg +6 -0
  67. package/public/runtime-icons/hermes-agent.png +0 -0
  68. package/src/account/components/account-panel.tsx +217 -97
  69. package/src/account/managers/account.manager.ts +3 -2
  70. package/src/api/chat-session-type.types.ts +7 -0
  71. package/src/api/runtime-control.types.ts +8 -0
  72. package/src/api/types.ts +8 -0
  73. package/src/app.tsx +221 -57
  74. package/src/components/agents/agent-dialogs.tsx +499 -0
  75. package/src/components/agents/agents-page.test.tsx +238 -0
  76. package/src/components/agents/agents-page.tsx +435 -0
  77. package/src/components/chat/ChatSidebar.tsx +11 -35
  78. package/src/components/chat/chat-conversation-panel.test.tsx +20 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-page-shell.tsx +19 -13
  81. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  82. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  84. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  85. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  86. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  87. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  88. package/src/components/chat/ncp/ncp-chat-page.tsx +2 -0
  89. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  90. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  91. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  92. package/src/components/chat/stores/chat-input.store.ts +2 -1
  93. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  94. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  95. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  96. package/src/components/common/BrandHeader.tsx +3 -1
  97. package/src/components/common/session-context-icon.tsx +15 -2
  98. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  99. package/src/components/config/ChannelForm.test.tsx +89 -3
  100. package/src/components/config/ChannelForm.tsx +157 -188
  101. package/src/components/config/ChannelsList.test.tsx +163 -119
  102. package/src/components/config/ChannelsList.tsx +90 -101
  103. package/src/components/config/ProviderForm.tsx +108 -146
  104. package/src/components/config/ProvidersList.tsx +100 -123
  105. package/src/components/config/SearchConfig.tsx +423 -393
  106. package/src/components/config/channel-form-fields-section.tsx +70 -37
  107. package/src/components/config/config-split-page.tsx +109 -0
  108. package/src/components/config/provider-enabled-field.tsx +17 -10
  109. package/src/components/config/runtime-control-card.test.tsx +56 -0
  110. package/src/components/config/runtime-control-card.tsx +25 -0
  111. package/src/components/config/runtime-presence-card.tsx +93 -79
  112. package/src/components/layout/AppLayout.tsx +25 -37
  113. package/src/components/layout/app-layout.test.tsx +46 -14
  114. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  115. package/src/components/layout/runtime-status-entry.tsx +143 -0
  116. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  117. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  118. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  119. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  120. package/src/components/marketplace/marketplace-page.tsx +596 -0
  121. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  122. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  123. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  124. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  125. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  126. package/src/components/remote/remote-access-page.test.tsx +105 -0
  127. package/src/components/remote/remote-access-page.tsx +248 -0
  128. package/src/components/ui/notice-card.tsx +129 -0
  129. package/src/components/ui/setting-row.tsx +51 -0
  130. package/src/components/ui/tag-chip.tsx +39 -0
  131. package/src/components/ui/textarea.tsx +19 -0
  132. package/src/hooks/useConfig.ts +2 -1
  133. package/src/index.css +24 -0
  134. package/src/lib/app-resource-uri.test.ts +20 -0
  135. package/src/lib/app-resource-uri.ts +29 -0
  136. package/src/lib/i18n.remote.ts +1 -1
  137. package/src/lib/i18n.runtime-control.ts +31 -0
  138. package/src/lib/i18n.ts +5 -8
  139. package/src/lib/session-context.utils.test.ts +71 -0
  140. package/src/lib/session-context.utils.ts +28 -3
  141. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  142. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  143. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  144. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  145. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  146. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  147. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  148. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  149. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  150. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  151. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  152. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  153. package/dist/assets/SessionsConfig-ChHQ7M5c.js +0 -2
  154. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  155. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  156. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  157. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  158. package/dist/assets/index-CF9xve0E.js +0 -6
  159. package/dist/assets/index-FgA52VBt.css +0 -1
  160. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  161. package/dist/assets/play-CFUwCA2E.js +0 -1
  162. package/dist/assets/plus-rYsv72JG.js +0 -1
  163. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  164. package/dist/assets/search-3kFR_zh9.js +0 -1
  165. package/dist/assets/security-config-BWaiARNk.js +0 -1
  166. package/dist/assets/select-DJ2MUjBB.js +0 -41
  167. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  168. package/dist/assets/x-ByDbItbq.js +0 -1
  169. package/src/components/agents/AgentDialogs.tsx +0 -400
  170. package/src/components/agents/AgentsPage.test.tsx +0 -217
  171. package/src/components/agents/AgentsPage.tsx +0 -352
  172. package/src/components/config/config-layout.ts +0 -10
  173. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  174. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  175. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  176. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  177. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  178. package/src/components/remote/RemoteAccessPage.tsx +0 -144
@@ -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 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>
@@ -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
+ });