@nextclaw/ui 0.12.10 → 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 (169) hide show
  1. package/CHANGELOG.md +51 -10
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-DMfr0Oow.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-BXydqby-.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-hO7tY7hE.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-CvjxU40H.js +2 -0
  14. package/dist/assets/{book-open-DzdUViDm.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-C5dEc8hV.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/{config-split-page-BUout_Ak.js → config-split-page-BAGSzUR3.js} +1 -1
  20. package/dist/assets/{createLucideIcon-dy5ie7Ox.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-Cy7_j6hA.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BD0ETkB-.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-kZSAO8nT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-BHJC2Ovu.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-CpTZLchQ.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-B7gRObP8.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-Bcv40SXy.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-EqJPOF0G.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-CM29eCAR.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-YcZUWn3o.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-mJT6oWa2.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-CNcz2fgt.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/module-structure.config.json +7 -0
  59. package/package.json +5 -5
  60. package/src/api/config.ts +10 -0
  61. package/src/api/raw-client.test.ts +1 -1
  62. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  63. package/src/api/types.ts +40 -0
  64. package/src/app/components/app-manager-provider.tsx +20 -0
  65. package/src/app/managers/app.manager.ts +12 -0
  66. package/src/app.tsx +8 -8
  67. package/src/components/chat/chat-conversation-panel.test.tsx +10 -0
  68. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  69. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  70. package/src/components/chat/chat-page-shell.tsx +1 -1
  71. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  72. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  73. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +13 -37
  74. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  75. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  76. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  77. package/src/components/chat/ncp/ncp-chat-page.tsx +21 -2
  78. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  79. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  80. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  81. package/src/components/config/desktop-update-config.test.tsx +10 -4
  82. package/src/components/config/desktop-update-config.tsx +5 -3
  83. package/src/components/config/runtime-control-card.test.tsx +119 -197
  84. package/src/components/config/runtime-control-card.tsx +20 -70
  85. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  86. package/src/components/config/runtime-presence-card.tsx +7 -5
  87. package/src/components/layout/Sidebar.tsx +4 -4
  88. package/src/components/layout/runtime-status-entry.test.tsx +45 -101
  89. package/src/components/layout/runtime-status-entry.tsx +15 -63
  90. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  91. package/src/{account → features/account}/components/account-panel.tsx +13 -13
  92. package/src/features/account/index.ts +6 -0
  93. package/src/{account → features/account}/managers/account.manager.ts +3 -3
  94. package/src/{components/remote → features/remote/components}/remote-access-page.test.tsx +4 -5
  95. package/src/{components/remote → features/remote/components}/remote-access-page.tsx +15 -13
  96. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  97. package/src/features/remote/index.ts +27 -0
  98. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  99. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  100. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  101. package/src/features/system-status/index.ts +12 -0
  102. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  103. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  104. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  105. package/src/features/system-status/stores/system-status.store.ts +32 -0
  106. package/src/features/system-status/types/system-status.types.ts +73 -0
  107. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  108. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  109. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  110. package/src/lib/i18n.chat.ts +8 -0
  111. package/src/platforms/desktop/index.ts +20 -0
  112. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  113. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  114. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  115. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  116. package/src/stores/ui.store.ts +0 -9
  117. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  118. package/src/transport/app-client.test.ts +9 -5
  119. package/src/transport/index.ts +1 -1
  120. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  121. package/dist/assets/ChannelsList-M9FTK1Ak.js +0 -8
  122. package/dist/assets/DocBrowser-CH7-GxlL.js +0 -1
  123. package/dist/assets/ModelConfig-CNIgLf0e.js +0 -1
  124. package/dist/assets/ProviderScopedModelInput-B3HWP4oz.js +0 -1
  125. package/dist/assets/ProvidersList-CHjMnRhX.js +0 -1
  126. package/dist/assets/RuntimeConfig-psp8nMSG.js +0 -1
  127. package/dist/assets/SearchConfig-CSoKip1f.js +0 -1
  128. package/dist/assets/SecretsConfig-MEt6MjuD.js +0 -3
  129. package/dist/assets/SessionsConfig-DifCiXwR.js +0 -2
  130. package/dist/assets/app-query-client-9jNewezV.js +0 -1
  131. package/dist/assets/chat-page-CLp0UV0Y.js +0 -58
  132. package/dist/assets/chat-session-display-DsYHx0RZ.js +0 -1
  133. package/dist/assets/client-C-8fH7-c.js +0 -7
  134. package/dist/assets/config-CBScxsdV.js +0 -1
  135. package/dist/assets/desktop-update-config-2BS6BMkW.js +0 -1
  136. package/dist/assets/dist-BruyLa92.js +0 -9
  137. package/dist/assets/index-mW8W2FUu.css +0 -1
  138. package/dist/assets/index-zDZfXoI4.js +0 -6
  139. package/dist/assets/infiniteQueryBehavior-CyER9hv0.js +0 -1
  140. package/dist/assets/loader-circle-Bc2gCU33.js +0 -1
  141. package/dist/assets/marketplace-page-3qVMnF3d.js +0 -1
  142. package/dist/assets/marketplace-page-BhFIeQzI.js +0 -49
  143. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +0 -40
  144. package/dist/assets/page-layout-0UcO9H9Z.js +0 -1
  145. package/dist/assets/play-CKDjSQFL.js +0 -1
  146. package/dist/assets/plus-CG0QrVY_.js +0 -1
  147. package/dist/assets/refresh-ccw-COVhNHtN.js +0 -1
  148. package/dist/assets/remote-access-page-CWHG-sug.js +0 -1
  149. package/dist/assets/rotate-cw-oHMKJMC8.js +0 -1
  150. package/dist/assets/search-BCAlB8nz.js +0 -1
  151. package/dist/assets/security-config-Slh0Mayz.js +0 -1
  152. package/dist/assets/select-CVz0t7MF.js +0 -41
  153. package/dist/assets/setting-row-CbVHAuQt.js +0 -1
  154. package/dist/assets/skeleton-D5rdKvzy.js +0 -1
  155. package/dist/assets/status-dot-DpPtVzQT.js +0 -1
  156. package/dist/assets/tag-chip-DMXdnLcj.js +0 -1
  157. package/dist/assets/use-infinite-scroll-loader-DJ1L81Dz.js +0 -1
  158. package/dist/assets/useConfirmDialog-BsVuqu1x.js +0 -1
  159. package/dist/assets/x-Czwxm82I.js +0 -1
  160. package/src/hooks/use-runtime-control.ts +0 -24
  161. package/src/presenter/app-presenter-context.tsx +0 -20
  162. package/src/presenter/app.presenter.ts +0 -12
  163. package/src/runtime-control/runtime-control.manager.ts +0 -118
  164. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  165. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  166. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  167. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  168. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  169. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -9,10 +9,10 @@ import {
9
9
  import { Input } from "@/components/ui/input";
10
10
  import { Label } from "@/components/ui/label";
11
11
  import { NoticeCard } from "@/components/ui/notice-card";
12
- import { useRemoteStatus } from "@/hooks/useRemoteAccess";
12
+ import { useRemoteStatus } from "@/features/remote";
13
13
  import { formatDateTime, t } from "@/lib/i18n";
14
- import { useAccountStore } from "@/account/stores/account.store";
15
- import { useAppPresenter } from "@/presenter/app-presenter-context";
14
+ import { useAppManager } from "@/app/components/app-manager-provider";
15
+ import { useAccountStore } from "@/features/account/stores/account.store";
16
16
  import { KeyRound, LogOut, SquareArrowOutUpRight } from "lucide-react";
17
17
  import { useEffect, useState } from "react";
18
18
 
@@ -175,7 +175,7 @@ function SignedOutAccountSection(props: {
175
175
  }
176
176
 
177
177
  export function AccountPanel() {
178
- const presenter = useAppPresenter();
178
+ const manager = useAppManager();
179
179
  const remoteStatus = useRemoteStatus();
180
180
  const panelOpen = useAccountStore((state) => state.panelOpen);
181
181
  const authSessionId = useAccountStore((state) => state.authSessionId);
@@ -189,8 +189,8 @@ export function AccountPanel() {
189
189
  const [savingUsername, setSavingUsername] = useState(false);
190
190
 
191
191
  useEffect(() => {
192
- presenter.accountManager.syncRemoteStatus(status);
193
- }, [presenter, status]);
192
+ manager.accountManager.syncRemoteStatus(status);
193
+ }, [manager, status]);
194
194
 
195
195
  const canSubmitUsername =
196
196
  !savingUsername &&
@@ -202,8 +202,8 @@ export function AccountPanel() {
202
202
  open={panelOpen}
203
203
  onOpenChange={(open) =>
204
204
  open
205
- ? presenter.accountManager.openAccountPanel()
206
- : presenter.accountManager.closeAccountPanel()
205
+ ? manager.accountManager.openAccountPanel()
206
+ : manager.accountManager.closeAccountPanel()
207
207
  }
208
208
  >
209
209
  <DialogContent className="max-w-xl">
@@ -227,13 +227,13 @@ export function AccountPanel() {
227
227
  onSubmitUsername={async () => {
228
228
  setSavingUsername(true);
229
229
  try {
230
- await presenter.accountManager.updateUsername(usernameDraft);
230
+ await manager.accountManager.updateUsername(usernameDraft);
231
231
  } finally {
232
232
  setSavingUsername(false);
233
233
  }
234
234
  }}
235
- onOpenDeviceList={() => presenter.accountManager.openNextClawWeb('/account')}
236
- onLogout={() => presenter.accountManager.logout()}
235
+ onOpenDeviceList={() => manager.accountManager.openNextClawWeb('/account')}
236
+ onLogout={() => manager.accountManager.logout()}
237
237
  />
238
238
  ) : (
239
239
  <SignedOutAccountSection
@@ -242,10 +242,10 @@ export function AccountPanel() {
242
242
  authStatusMessage={authStatusMessage}
243
243
  authVerificationUri={authVerificationUri}
244
244
  onStartBrowserSignIn={() =>
245
- presenter.accountManager.startBrowserSignIn()
245
+ manager.accountManager.startBrowserSignIn()
246
246
  }
247
247
  onResumeBrowserSignIn={() =>
248
- presenter.accountManager.resumeBrowserSignIn()
248
+ manager.accountManager.resumeBrowserSignIn()
249
249
  }
250
250
  />
251
251
  )}
@@ -0,0 +1,6 @@
1
+ export { AccountPanel } from './components/account-panel';
2
+ export { AccountManager } from './managers/account.manager';
3
+ export {
4
+ useAccountStore,
5
+ type AccountPendingAction,
6
+ } from './stores/account.store';
@@ -1,15 +1,15 @@
1
1
  import { logoutRemote, pollRemoteBrowserAuth, startRemoteBrowserAuth, updateRemoteAccountProfile } from '@/api/remote';
2
2
  import type { RemoteAccessView } from '@/api/remote.types';
3
- import type { AccountPendingAction } from '@/account/stores/account.store';
4
- import { useAccountStore } from '@/account/stores/account.store';
5
3
  import {
6
4
  ensureRemoteStatus,
7
5
  refreshRemoteStatus,
8
6
  resolveRemotePlatformApiBase,
9
7
  resolveRemoteWebBase
10
- } from '@/remote/remote-access.query';
8
+ } from '@/features/remote';
11
9
  import { formatDateTime, t } from '@/lib/i18n';
12
10
  import { toast } from 'sonner';
11
+ import type { AccountPendingAction } from '@/features/account/stores/account.store';
12
+ import { useAccountStore } from '@/features/account/stores/account.store';
13
13
 
14
14
  type SignedInContinuation = (action: AccountPendingAction, status: RemoteAccessView) => Promise<void>;
15
15
 
@@ -1,8 +1,7 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
- import { RemoteAccessPage } from "@/components/remote/remote-access-page";
3
+ import { RemoteAccessPage, useRemoteAccessStore } from "@/features/remote";
4
4
  import { setLanguage } from "@/lib/i18n";
5
- import { useRemoteAccessStore } from "@/remote/stores/remote-access.store";
6
5
 
7
6
  const mocks = vi.hoisted(() => ({
8
7
  reauthorizeRemoteAccess: vi.fn(),
@@ -17,12 +16,12 @@ const mocks = vi.hoisted(() => ({
17
16
  },
18
17
  }));
19
18
 
20
- vi.mock("@/hooks/useRemoteAccess", () => ({
19
+ vi.mock("@/features/remote/hooks/use-remote-access", () => ({
21
20
  useRemoteStatus: () => mocks.statusQuery,
22
21
  }));
23
22
 
24
- vi.mock("@/presenter/app-presenter-context", () => ({
25
- useAppPresenter: () => ({
23
+ vi.mock("@/app/components/app-manager-provider", () => ({
24
+ useAppManager: () => ({
26
25
  remoteAccessManager: {
27
26
  reauthorizeRemoteAccess: mocks.reauthorizeRemoteAccess,
28
27
  repairRemoteAccess: mocks.repairRemoteAccess,
@@ -9,12 +9,14 @@ import {
9
9
  } from "@/components/ui/card";
10
10
  import { NoticeCard } from "@/components/ui/notice-card";
11
11
  import { StatusDot } from "@/components/ui/status-dot";
12
- import { useRemoteStatus } from "@/hooks/useRemoteAccess";
12
+ import { useAppManager } from "@/app/components/app-manager-provider";
13
+ import {
14
+ buildRemoteAccessFeedbackView,
15
+ resolveRemoteWebBase,
16
+ useRemoteAccessStore,
17
+ } from "@/features/remote";
18
+ import { useRemoteStatus } from "@/features/remote/hooks/use-remote-access";
13
19
  import { formatDateTime, t } from "@/lib/i18n";
14
- import { useAppPresenter } from "@/presenter/app-presenter-context";
15
- import { resolveRemoteWebBase } from "@/remote/remote-access.query";
16
- import { buildRemoteAccessFeedbackView } from "@/remote/remote-access-feedback.service";
17
- import { useRemoteAccessStore } from "@/remote/stores/remote-access.store";
18
20
  import { Laptop, RefreshCcw, SquareArrowOutUpRight } from "lucide-react";
19
21
  import { useEffect, useMemo } from "react";
20
22
 
@@ -43,7 +45,7 @@ function KeyValueRow(props: {
43
45
  }
44
46
 
45
47
  export function RemoteAccessPage() {
46
- const presenter = useAppPresenter();
48
+ const manager = useAppManager();
47
49
  const remoteStatus = useRemoteStatus();
48
50
  const status = remoteStatus.data;
49
51
  const actionLabel = useRemoteAccessStore((state) => state.actionLabel);
@@ -62,8 +64,8 @@ export function RemoteAccessPage() {
62
64
  const { hero: heroView, issueHint } = feedbackView;
63
65
 
64
66
  useEffect(() => {
65
- presenter.remoteAccessManager.syncStatus(status);
66
- }, [presenter, status]);
67
+ manager.remoteAccessManager.syncStatus(status);
68
+ }, [manager, status]);
67
69
 
68
70
  if (remoteStatus.isLoading && !status) {
69
71
  return <div className="p-8 text-gray-400">{t("remoteLoading")}</div>;
@@ -115,18 +117,18 @@ export function RemoteAccessPage() {
115
117
  <Button
116
118
  onClick={() => {
117
119
  if (feedbackView.primaryAction?.kind === "reauthorize") {
118
- void presenter.remoteAccessManager.reauthorizeRemoteAccess(
120
+ void manager.remoteAccessManager.reauthorizeRemoteAccess(
119
121
  status,
120
122
  );
121
123
  return;
122
124
  }
123
125
  if (feedbackView.primaryAction?.kind === "repair") {
124
- void presenter.remoteAccessManager.repairRemoteAccess(
126
+ void manager.remoteAccessManager.repairRemoteAccess(
125
127
  status,
126
128
  );
127
129
  return;
128
130
  }
129
- void presenter.remoteAccessManager.enableRemoteAccess(
131
+ void manager.remoteAccessManager.enableRemoteAccess(
130
132
  status,
131
133
  );
132
134
  }}
@@ -141,7 +143,7 @@ export function RemoteAccessPage() {
141
143
 
142
144
  <Button
143
145
  variant="outline"
144
- onClick={() => void presenter.accountManager.openNextClawWeb()}
146
+ onClick={() => void manager.accountManager.openNextClawWeb()}
145
147
  disabled={busy || !canOpenDeviceList}
146
148
  >
147
149
  <SquareArrowOutUpRight className="mr-2 h-4 w-4" />
@@ -152,7 +154,7 @@ export function RemoteAccessPage() {
152
154
  <Button
153
155
  variant="outline"
154
156
  onClick={() =>
155
- void presenter.remoteAccessManager.disableRemoteAccess(
157
+ void manager.remoteAccessManager.disableRemoteAccess(
156
158
  status,
157
159
  )
158
160
  }
@@ -9,8 +9,8 @@ import {
9
9
  startRemoteBrowserAuth,
10
10
  updateRemoteSettings
11
11
  } from '@/api/remote';
12
+ import { REMOTE_STATUS_QUERY_KEY } from '@/features/remote';
12
13
  import { t } from '@/lib/i18n';
13
- import { REMOTE_STATUS_QUERY_KEY } from '@/remote/remote-access.query';
14
14
  import { toast } from 'sonner';
15
15
 
16
16
  export function useRemoteStatus() {
@@ -0,0 +1,27 @@
1
+ export { RemoteAccessPage } from './components/remote-access-page';
2
+ export {
3
+ useRemoteBrowserAuthPoll,
4
+ useRemoteBrowserAuthStart,
5
+ useRemoteDoctor,
6
+ useRemoteLogin,
7
+ useRemoteLogout,
8
+ useRemoteServiceControl,
9
+ useRemoteSettings,
10
+ useRemoteStatus,
11
+ } from './hooks/use-remote-access';
12
+ export { RemoteAccessManager } from './managers/remote-access.manager';
13
+ export {
14
+ buildRemoteAccessFeedbackView,
15
+ requiresRemoteReauthorization,
16
+ type RemoteAccessFeedbackView,
17
+ } from './services/remote-access-feedback.service';
18
+ export {
19
+ REMOTE_STATUS_QUERY_KEY,
20
+ ensureRemoteStatus,
21
+ getRemoteStatusSnapshot,
22
+ refreshRemoteStatus,
23
+ resolveRemotePlatformApiBase,
24
+ resolveRemotePlatformBase,
25
+ resolveRemoteWebBase,
26
+ } from './services/remote-access-query.service';
27
+ export { useRemoteAccessStore } from './stores/remote-access.store';
@@ -1,11 +1,10 @@
1
1
  import { controlRemoteService, fetchRemoteDoctor, updateRemoteSettings } from '@/api/remote';
2
2
  import type { RemoteAccessView } from '@/api/remote.types';
3
- import type { AccountPendingAction } from '@/account/stores/account.store';
4
- import type { AccountManager } from '@/account/managers/account.manager';
5
- import { useRemoteAccessStore } from '@/remote/stores/remote-access.store';
6
- import { refreshRemoteStatus } from '@/remote/remote-access.query';
3
+ import type { AccountManager, AccountPendingAction } from '@/features/account';
7
4
  import { t } from '@/lib/i18n';
8
5
  import { toast } from 'sonner';
6
+ import { refreshRemoteStatus } from '@/features/remote/services/remote-access-query.service';
7
+ import { useRemoteAccessStore } from '@/features/remote/stores/remote-access.store';
9
8
 
10
9
  export class RemoteAccessManager {
11
10
  private accountManager: AccountManager | null = null;
@@ -1,7 +1,7 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest';
2
2
  import type { RemoteAccessView } from '@/api/remote.types';
3
3
  import { setLanguage } from '@/lib/i18n';
4
- import { buildRemoteAccessFeedbackView, requiresRemoteReauthorization } from '@/remote/remote-access-feedback.service';
4
+ import { buildRemoteAccessFeedbackView, requiresRemoteReauthorization } from './remote-access-feedback.service';
5
5
 
6
6
  function createRemoteAccessView(overrides: Partial<RemoteAccessView> = {}): RemoteAccessView {
7
7
  return {
@@ -0,0 +1,104 @@
1
+ import { useEffect } from 'react';
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import { fetchBootstrapStatus } from '@/api/config';
4
+ import type { BootstrapStatusView } from '@/api/types';
5
+ import {
6
+ toRuntimeControlPanelView,
7
+ toRuntimeStatusBadgeView,
8
+ toSystemStatusView,
9
+ } from '@/features/system-status/utils/system-status.utils';
10
+ import { systemStatusManager } from '@/features/system-status/managers/system-status.manager';
11
+ import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
12
+
13
+ function createPendingBootstrapStatus(): BootstrapStatusView {
14
+ return {
15
+ phase: 'kernel-starting',
16
+ ncpAgent: {
17
+ state: 'pending',
18
+ },
19
+ pluginHydration: {
20
+ state: 'pending',
21
+ loadedPluginCount: 0,
22
+ totalPluginCount: 0,
23
+ },
24
+ channels: {
25
+ state: 'pending',
26
+ enabled: [],
27
+ },
28
+ remote: {
29
+ state: 'pending',
30
+ },
31
+ };
32
+ }
33
+
34
+ export function useSystemStatusSources() {
35
+ const runtimeBootstrapStatus = useQuery({
36
+ queryKey: ['runtime-bootstrap-status'],
37
+ queryFn: fetchBootstrapStatus,
38
+ placeholderData: createPendingBootstrapStatus,
39
+ refetchInterval: (query) => {
40
+ return systemStatusManager.getRuntimeBootstrapPollInterval(
41
+ query.state.data
42
+ );
43
+ },
44
+ refetchIntervalInBackground: true,
45
+ retry: false,
46
+ refetchOnWindowFocus: true,
47
+ });
48
+ const runtimeControl = useQuery({
49
+ queryKey: ['runtime-control'],
50
+ queryFn: async () => await systemStatusManager.getRuntimeControl(),
51
+ staleTime: 5_000,
52
+ refetchOnWindowFocus: true,
53
+ });
54
+
55
+ useEffect(() => {
56
+ if (runtimeBootstrapStatus.data) {
57
+ systemStatusManager.reportBootstrapStatus(runtimeBootstrapStatus.data);
58
+ }
59
+ }, [runtimeBootstrapStatus.data]);
60
+
61
+ useEffect(() => {
62
+ if (runtimeBootstrapStatus.error) {
63
+ systemStatusManager.reportBootstrapQueryError(runtimeBootstrapStatus.error);
64
+ }
65
+ }, [runtimeBootstrapStatus.error]);
66
+
67
+ useEffect(() => {
68
+ if (runtimeControl.data) {
69
+ systemStatusManager.reportRuntimeControlView(runtimeControl.data);
70
+ }
71
+ }, [runtimeControl.data]);
72
+
73
+ useEffect(() => {
74
+ if (runtimeControl.error) {
75
+ systemStatusManager.reportRuntimeControlError(runtimeControl.error);
76
+ }
77
+ }, [runtimeControl.error]);
78
+ }
79
+
80
+ export function useSystemStatus() {
81
+ const state = useSystemStatusStore((store) => store.state);
82
+ return toSystemStatusView(state);
83
+ }
84
+
85
+ export function useChatRuntimeAvailability() {
86
+ const state = useSystemStatusStore((store) => store.state);
87
+ const view = toSystemStatusView(state);
88
+ return {
89
+ isBlocked: view.isChatBlocked,
90
+ message: view.chatMessage,
91
+ phase: view.phase,
92
+ lastReadyAt: view.lastReadyAt,
93
+ };
94
+ }
95
+
96
+ export function useRuntimeStatusBadgeView() {
97
+ const state = useSystemStatusStore((store) => store.state);
98
+ return toRuntimeStatusBadgeView(state);
99
+ }
100
+
101
+ export function useRuntimeControlPanelView() {
102
+ const state = useSystemStatusStore((store) => store.state);
103
+ return toRuntimeControlPanelView(state);
104
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ useChatRuntimeAvailability,
3
+ useRuntimeControlPanelView,
4
+ useRuntimeStatusBadgeView,
5
+ useSystemStatus,
6
+ useSystemStatusSources,
7
+ } from './hooks/use-system-status';
8
+ export {
9
+ isTransientRuntimeConnectionErrorMessage,
10
+ systemStatusManager,
11
+ } from './managers/system-status.manager';
12
+ export { useSystemStatusStore } from './stores/system-status.store';
@@ -0,0 +1,126 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { systemStatusManager } from './system-status.manager';
3
+ import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
4
+
5
+ describe('getRuntimeBootstrapPollInterval', () => {
6
+ beforeEach(() => {
7
+ useSystemStatusStore.setState({
8
+ state: {
9
+ ...useSystemStatusStore.getState().state,
10
+ lifecyclePhase: 'cold-starting',
11
+ hasReachedReady: false,
12
+ activeSystemAction: null,
13
+ },
14
+ });
15
+ });
16
+
17
+ it('keeps polling while bootstrap status is missing', () => {
18
+ expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
19
+ 500
20
+ );
21
+ });
22
+
23
+ it('keeps polling while ncp agent is still initializing', () => {
24
+ expect(
25
+ systemStatusManager.getRuntimeBootstrapPollInterval({
26
+ phase: 'shell-ready',
27
+ ncpAgent: {
28
+ state: 'running',
29
+ },
30
+ pluginHydration: {
31
+ state: 'pending',
32
+ loadedPluginCount: 0,
33
+ totalPluginCount: 0,
34
+ },
35
+ channels: {
36
+ state: 'pending',
37
+ enabled: [],
38
+ },
39
+ remote: {
40
+ state: 'pending',
41
+ },
42
+ })
43
+ ).toBe(500);
44
+ });
45
+
46
+ it('continues polling even when bootstrap status reports an ncp agent error', () => {
47
+ expect(
48
+ systemStatusManager.getRuntimeBootstrapPollInterval({
49
+ phase: 'error',
50
+ ncpAgent: {
51
+ state: 'error',
52
+ error: 'startup failed',
53
+ },
54
+ pluginHydration: {
55
+ state: 'pending',
56
+ loadedPluginCount: 0,
57
+ totalPluginCount: 0,
58
+ },
59
+ channels: {
60
+ state: 'pending',
61
+ enabled: [],
62
+ },
63
+ remote: {
64
+ state: 'pending',
65
+ },
66
+ lastError: 'startup failed',
67
+ })
68
+ ).toBe(500);
69
+ });
70
+
71
+ it('stops polling once the ncp agent is ready', () => {
72
+ expect(
73
+ systemStatusManager.getRuntimeBootstrapPollInterval({
74
+ phase: 'ready',
75
+ ncpAgent: {
76
+ state: 'ready',
77
+ },
78
+ pluginHydration: {
79
+ state: 'ready',
80
+ loadedPluginCount: 1,
81
+ totalPluginCount: 1,
82
+ },
83
+ channels: {
84
+ state: 'ready',
85
+ enabled: [],
86
+ },
87
+ remote: {
88
+ state: 'disabled',
89
+ },
90
+ })
91
+ ).toBe(false);
92
+ });
93
+
94
+ it('keeps polling while the runtime is recovering even if the last bootstrap status was ready', () => {
95
+ useSystemStatusStore.setState({
96
+ state: {
97
+ ...useSystemStatusStore.getState().state,
98
+ lifecyclePhase: 'recovering',
99
+ hasReachedReady: true,
100
+ lastReadyAt: Date.now(),
101
+ recoveryStartedAt: Date.now(),
102
+ },
103
+ });
104
+
105
+ expect(
106
+ systemStatusManager.getRuntimeBootstrapPollInterval({
107
+ phase: 'ready',
108
+ ncpAgent: {
109
+ state: 'ready',
110
+ },
111
+ pluginHydration: {
112
+ state: 'ready',
113
+ loadedPluginCount: 1,
114
+ totalPluginCount: 1,
115
+ },
116
+ channels: {
117
+ state: 'ready',
118
+ enabled: [],
119
+ },
120
+ remote: {
121
+ state: 'disabled',
122
+ },
123
+ })
124
+ ).toBe(500);
125
+ });
126
+ });
@@ -0,0 +1,142 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { BootstrapStatusView } from '@/api/types';
3
+ import { appQueryClient } from '@/app-query-client';
4
+ import { t } from '@/lib/i18n';
5
+ import {
6
+ isTransientRuntimeConnectionErrorMessage,
7
+ systemStatusManager,
8
+ } from './system-status.manager';
9
+ import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
10
+
11
+ const readyBootstrapStatus: BootstrapStatusView = {
12
+ phase: 'ready',
13
+ ncpAgent: {
14
+ state: 'ready',
15
+ },
16
+ pluginHydration: {
17
+ state: 'ready',
18
+ loadedPluginCount: 1,
19
+ totalPluginCount: 1,
20
+ },
21
+ channels: {
22
+ state: 'ready',
23
+ enabled: [],
24
+ },
25
+ remote: {
26
+ state: 'disabled',
27
+ },
28
+ };
29
+
30
+ describe('systemStatusManager', () => {
31
+ beforeEach(() => {
32
+ vi.useFakeTimers();
33
+ systemStatusManager.resetForTests();
34
+ });
35
+
36
+ afterEach(() => {
37
+ systemStatusManager.resetForTests();
38
+ vi.restoreAllMocks();
39
+ vi.useRealTimers();
40
+ });
41
+
42
+ it('keeps cold start in cold-starting when transport fails before the first ready state', () => {
43
+ expect(
44
+ systemStatusManager.reportTransportFailure(new Error('Failed to fetch'))
45
+ ).toBe(true);
46
+
47
+ expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe(
48
+ 'cold-starting'
49
+ );
50
+ });
51
+
52
+ it('enters startup-failed when bootstrap explicitly reports an error before the first ready state', () => {
53
+ systemStatusManager.reportBootstrapStatus({
54
+ phase: 'error',
55
+ ncpAgent: {
56
+ state: 'error',
57
+ error: 'startup failed',
58
+ },
59
+ pluginHydration: {
60
+ state: 'pending',
61
+ loadedPluginCount: 0,
62
+ totalPluginCount: 0,
63
+ },
64
+ channels: {
65
+ state: 'pending',
66
+ enabled: [],
67
+ },
68
+ remote: {
69
+ state: 'pending',
70
+ },
71
+ lastError: 'startup failed',
72
+ });
73
+
74
+ expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe(
75
+ 'startup-failed'
76
+ );
77
+ });
78
+
79
+ it('enters recovering only after the page has previously reached ready', async () => {
80
+ const invalidateQueriesSpy = vi
81
+ .spyOn(appQueryClient, 'invalidateQueries')
82
+ .mockResolvedValue(undefined as never);
83
+ const refetchQueriesSpy = vi
84
+ .spyOn(appQueryClient, 'refetchQueries')
85
+ .mockResolvedValue(undefined as never);
86
+
87
+ systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
88
+ systemStatusManager.handleConnectionInterrupted('websocket error');
89
+
90
+ expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe(
91
+ 'recovering'
92
+ );
93
+
94
+ systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
95
+
96
+ expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe('ready');
97
+ expect(invalidateQueriesSpy).toHaveBeenCalled();
98
+ expect(refetchQueriesSpy).toHaveBeenCalledWith({ type: 'active' });
99
+ });
100
+
101
+ it('marks recovery as stalled after the timeout window elapses', async () => {
102
+ systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
103
+ systemStatusManager.handleConnectionInterrupted('websocket error');
104
+
105
+ await vi.advanceTimersByTimeAsync(30_000);
106
+
107
+ expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe('stalled');
108
+ });
109
+
110
+ it('maps transient chat errors to friendly recovery copy while recovering', () => {
111
+ systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
112
+ systemStatusManager.handleConnectionInterrupted('Failed to fetch');
113
+
114
+ expect(
115
+ systemStatusManager.getDisplayMessage(
116
+ 'NCP fetch failed for POST /api/ncp/agent: Error: Failed to fetch'
117
+ )
118
+ ).toBe(t('runtimeControlRecoveringHelp'));
119
+ });
120
+
121
+ it('suppresses transient transport errors after recovery stalls', async () => {
122
+ systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
123
+ systemStatusManager.handleConnectionInterrupted('Failed to fetch');
124
+
125
+ await vi.advanceTimersByTimeAsync(30_000);
126
+
127
+ expect(
128
+ systemStatusManager.getDisplayMessage(
129
+ 'NCP fetch failed for POST /api/ncp/agent: Error: Failed to fetch'
130
+ )
131
+ ).toBeNull();
132
+ });
133
+
134
+ it('keeps only transport-level failures in the recovery flow', () => {
135
+ expect(isTransientRuntimeConnectionErrorMessage('Failed to fetch')).toBe(
136
+ true
137
+ );
138
+ expect(
139
+ isTransientRuntimeConnectionErrorMessage('HTTP 500 internal server error')
140
+ ).toBe(false);
141
+ });
142
+ });