@nextclaw/ui 0.12.9 → 0.12.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { t } from '@/lib/i18n';
3
+ import {
4
+ resolveChatRuntimeMessage,
5
+ resolveSystemConnectionStatus,
6
+ toSystemStatusView,
7
+ } from './system-status.utils';
8
+
9
+ describe('resolveSystemConnectionStatus', () => {
10
+ it('maps cold-starting to connecting', () => {
11
+ expect(resolveSystemConnectionStatus('cold-starting')).toBe('connecting');
12
+ });
13
+
14
+ it('maps ready to connected', () => {
15
+ expect(resolveSystemConnectionStatus('ready')).toBe('connected');
16
+ });
17
+
18
+ it('maps stalled to disconnected', () => {
19
+ expect(resolveSystemConnectionStatus('stalled')).toBe('disconnected');
20
+ });
21
+
22
+ it('maps service-transitioning to connecting', () => {
23
+ expect(resolveSystemConnectionStatus('service-transitioning')).toBe(
24
+ 'connecting'
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('resolveChatRuntimeMessage', () => {
30
+ it('uses the startup message during cold start', () => {
31
+ expect(
32
+ resolveChatRuntimeMessage({
33
+ lifecyclePhase: 'cold-starting',
34
+ hasReachedReady: false,
35
+ lastReadyAt: null,
36
+ recoveryStartedAt: null,
37
+ bootstrapStatus: null,
38
+ lastError: null,
39
+ lastTransportError: null,
40
+ runtimeControlView: null,
41
+ runtimeControlError: null,
42
+ activeSystemAction: null,
43
+ lastSystemActionError: null,
44
+ })
45
+ ).toBe(t('chatRuntimeInitializing'));
46
+ });
47
+
48
+ it('uses the bootstrap error when startup failed', () => {
49
+ expect(
50
+ resolveChatRuntimeMessage({
51
+ lifecyclePhase: 'startup-failed',
52
+ hasReachedReady: false,
53
+ lastReadyAt: null,
54
+ recoveryStartedAt: null,
55
+ bootstrapStatus: {
56
+ phase: 'error',
57
+ ncpAgent: {
58
+ state: 'error',
59
+ error: 'boom',
60
+ },
61
+ pluginHydration: {
62
+ state: 'pending',
63
+ loadedPluginCount: 0,
64
+ totalPluginCount: 0,
65
+ },
66
+ channels: {
67
+ state: 'pending',
68
+ enabled: [],
69
+ },
70
+ remote: {
71
+ state: 'pending',
72
+ },
73
+ lastError: 'boom',
74
+ },
75
+ lastError: null,
76
+ lastTransportError: null,
77
+ runtimeControlView: null,
78
+ runtimeControlError: null,
79
+ activeSystemAction: null,
80
+ lastSystemActionError: null,
81
+ })
82
+ ).toBe('boom');
83
+ });
84
+
85
+ it('prefers the centralized action message while a system action is running', () => {
86
+ expect(
87
+ resolveChatRuntimeMessage({
88
+ lifecyclePhase: 'ready',
89
+ hasReachedReady: true,
90
+ lastReadyAt: Date.now(),
91
+ recoveryStartedAt: null,
92
+ bootstrapStatus: null,
93
+ lastError: null,
94
+ lastTransportError: null,
95
+ runtimeControlView: null,
96
+ runtimeControlError: null,
97
+ activeSystemAction: {
98
+ action: 'restart-service',
99
+ lifecycle: 'recovering',
100
+ serviceState: null,
101
+ message: 'NextClaw 正在恢复连接',
102
+ },
103
+ lastSystemActionError: null,
104
+ })
105
+ ).toBe('NextClaw 正在恢复连接');
106
+ });
107
+ });
108
+
109
+ describe('toSystemStatusView', () => {
110
+ it('keeps stalled chat blocked without surfacing a timeout banner', () => {
111
+ expect(
112
+ toSystemStatusView({
113
+ lifecyclePhase: 'stalled',
114
+ hasReachedReady: true,
115
+ lastReadyAt: Date.now(),
116
+ recoveryStartedAt: Date.now(),
117
+ bootstrapStatus: null,
118
+ lastError: 'Failed to fetch',
119
+ lastTransportError: 'Failed to fetch',
120
+ runtimeControlView: null,
121
+ runtimeControlError: null,
122
+ activeSystemAction: null,
123
+ lastSystemActionError: null,
124
+ })
125
+ ).toMatchObject({
126
+ isChatBlocked: true,
127
+ chatMessage: null,
128
+ connectionStatus: 'disconnected',
129
+ phase: 'stalled',
130
+ });
131
+ });
132
+ });
@@ -0,0 +1,202 @@
1
+ import { t } from '@/lib/i18n';
2
+ import type {
3
+ RuntimeControlAction,
4
+ RuntimeLifecycleState,
5
+ RuntimeServiceState,
6
+ } from '@/api/runtime-control.types';
7
+ import type {
8
+ RuntimeControlPanelView,
9
+ RuntimeStatusBadgeView,
10
+ SystemConnectionStatus,
11
+ SystemStatusPhase,
12
+ SystemStatusState,
13
+ SystemStatusView,
14
+ } from '@/features/system-status/types/system-status.types';
15
+
16
+ function resolveSystemStatusPhase(state: SystemStatusState): SystemStatusPhase {
17
+ return state.activeSystemAction ? 'service-transitioning' : state.lifecyclePhase;
18
+ }
19
+
20
+ export function resolveSystemConnectionStatus(
21
+ phase: SystemStatusPhase
22
+ ): SystemConnectionStatus {
23
+ if (phase === 'ready') {
24
+ return 'connected';
25
+ }
26
+ if (phase === 'startup-failed' || phase === 'stalled') {
27
+ return 'disconnected';
28
+ }
29
+ return 'connecting';
30
+ }
31
+
32
+ export function resolveChatRuntimeMessage(
33
+ state: SystemStatusState
34
+ ): string | null {
35
+ if (state.activeSystemAction?.message?.trim()) {
36
+ return state.activeSystemAction.message.trim();
37
+ }
38
+ if (state.lifecyclePhase === 'cold-starting') {
39
+ return t('chatRuntimeInitializing');
40
+ }
41
+ if (state.lifecyclePhase === 'startup-failed') {
42
+ return (
43
+ state.bootstrapStatus?.ncpAgent.error?.trim() ||
44
+ state.bootstrapStatus?.lastError?.trim() ||
45
+ state.lastError?.trim() ||
46
+ t('chatRuntimeInitializationFailed')
47
+ );
48
+ }
49
+ return null;
50
+ }
51
+
52
+ export function toSystemStatusView(
53
+ state: SystemStatusState
54
+ ): SystemStatusView {
55
+ const phase = resolveSystemStatusPhase(state);
56
+ return {
57
+ ...state,
58
+ phase,
59
+ connectionStatus: resolveSystemConnectionStatus(phase),
60
+ isChatBlocked: phase !== 'ready',
61
+ chatMessage: resolveChatRuntimeMessage(state),
62
+ };
63
+ }
64
+
65
+ function resolveActionLifecycleLabel(
66
+ action: RuntimeControlAction
67
+ ): RuntimeLifecycleState {
68
+ if (action === 'start-service') {
69
+ return 'starting-service';
70
+ }
71
+ if (action === 'stop-service') {
72
+ return 'stopping-service';
73
+ }
74
+ if (action === 'restart-service') {
75
+ return 'restarting-service';
76
+ }
77
+ return 'restarting-app';
78
+ }
79
+
80
+ function resolveActionServiceState(
81
+ action: RuntimeControlAction
82
+ ): RuntimeServiceState | null {
83
+ if (action === 'start-service') {
84
+ return 'starting';
85
+ }
86
+ if (action === 'stop-service') {
87
+ return 'stopping';
88
+ }
89
+ if (action === 'restart-service') {
90
+ return 'restarting';
91
+ }
92
+ return null;
93
+ }
94
+
95
+ export function buildActiveSystemActionState(params: {
96
+ action: RuntimeControlAction;
97
+ message: string | null;
98
+ }): SystemStatusState['activeSystemAction'] {
99
+ const { action, message } = params;
100
+ return {
101
+ action,
102
+ lifecycle: resolveActionLifecycleLabel(action),
103
+ serviceState: resolveActionServiceState(action),
104
+ message,
105
+ };
106
+ }
107
+
108
+ export function toRuntimeStatusBadgeView(
109
+ state: SystemStatusState
110
+ ): RuntimeStatusBadgeView {
111
+ if (state.runtimeControlError) {
112
+ return {
113
+ tone: 'inactive',
114
+ title: t('runtimeControlLoadFailed'),
115
+ description: state.runtimeControlError,
116
+ reasonLines: [],
117
+ actionLabel: null,
118
+ isBusy: false,
119
+ };
120
+ }
121
+
122
+ if (!state.runtimeControlView) {
123
+ return {
124
+ tone: 'inactive',
125
+ title: t('runtimeStatusLoadingTitle'),
126
+ description: t('runtimeStatusLoadingDescription'),
127
+ reasonLines: [],
128
+ actionLabel: null,
129
+ isBusy: Boolean(state.activeSystemAction),
130
+ };
131
+ }
132
+
133
+ if (state.activeSystemAction) {
134
+ return {
135
+ tone: 'attention',
136
+ title: t('runtimeControlTitle'),
137
+ description:
138
+ state.activeSystemAction.message ||
139
+ state.runtimeControlView.message ||
140
+ t('runtimeControlDescription'),
141
+ reasonLines: [],
142
+ actionLabel: null,
143
+ isBusy: true,
144
+ };
145
+ }
146
+
147
+ const view = state.runtimeControlView;
148
+ if (view.pendingRestart) {
149
+ return {
150
+ tone: 'attention',
151
+ title: t('runtimeStatusPendingRestartTitle'),
152
+ description: t('runtimeStatusPendingRestartDescription'),
153
+ reasonLines:
154
+ view.pendingRestart.changedPaths.length > 0
155
+ ? view.pendingRestart.changedPaths.map((path: string) =>
156
+ t('runtimeStatusPendingRestartReasonItem').replace('{path}', path)
157
+ )
158
+ : [view.pendingRestart.message],
159
+ actionLabel: view.canRestartService.available
160
+ ? t('runtimeStatusRestartAction')
161
+ : null,
162
+ isBusy: false,
163
+ };
164
+ }
165
+
166
+ return {
167
+ tone: view.lifecycle === 'healthy' ? 'healthy' : 'inactive',
168
+ title: t('runtimeStatusHealthyTitle'),
169
+ description: t('runtimeStatusHealthyDescription'),
170
+ reasonLines: [],
171
+ actionLabel: null,
172
+ isBusy: false,
173
+ };
174
+ }
175
+
176
+ export function toRuntimeControlPanelView(
177
+ state: SystemStatusState
178
+ ): RuntimeControlPanelView {
179
+ const action = state.activeSystemAction;
180
+ const controlView = state.runtimeControlView;
181
+ const visibleLifecycle =
182
+ action?.lifecycle ?? controlView?.lifecycle ?? 'healthy';
183
+ const visibleServiceState =
184
+ action?.serviceState ?? controlView?.serviceState ?? 'unknown';
185
+ const visibleMessage =
186
+ action?.message ||
187
+ state.lastSystemActionError ||
188
+ controlView?.message ||
189
+ t('runtimeControlDescription');
190
+
191
+ return {
192
+ controlView,
193
+ visibleLifecycle,
194
+ visibleServiceState,
195
+ visibleMessage,
196
+ busyAction: action?.action ?? null,
197
+ busy: Boolean(action),
198
+ pendingRestart: controlView?.pendingRestart ?? null,
199
+ errorMessage:
200
+ state.lastSystemActionError || state.runtimeControlError || null,
201
+ };
202
+ }
@@ -1,12 +1,9 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { applyNcpSessionRealtimeEvent } from '@/api/ncp-session-query-cache';
3
+ import { systemStatusManager } from '@/features/system-status';
3
4
  import { appClient } from '@/transport';
4
- import { useUiStore } from '@/stores/ui.store';
5
5
  import type { QueryClient } from '@tanstack/react-query';
6
6
 
7
- type ConnectionStatus = 'connected' | 'disconnected' | 'connecting';
8
- type SetConnectionStatus = (status: ConnectionStatus) => void;
9
-
10
7
  function shouldInvalidateConfigQuery(configPath: string) {
11
8
  const normalized = configPath.trim().toLowerCase();
12
9
  if (!normalized) {
@@ -39,22 +36,34 @@ function handleConfigUpdatedEvent(queryClient: QueryClient | undefined, path: st
39
36
  }
40
37
 
41
38
  function handleRealtimeEvent(
42
- queryClient: QueryClient | undefined,
43
- setConnectionStatus: SetConnectionStatus,
44
- shouldResyncSessionsRef: { current: boolean },
45
- event: Parameters<Parameters<typeof appClient.subscribe>[0]>[0]
39
+ params: {
40
+ queryClient: QueryClient | undefined;
41
+ shouldResyncSessions: boolean;
42
+ clearShouldResyncSessions: () => void;
43
+ markShouldResyncSessions: () => void;
44
+ event: Parameters<Parameters<typeof appClient.subscribe>[0]>[0];
45
+ }
46
46
  ): void {
47
+ const {
48
+ queryClient,
49
+ shouldResyncSessions,
50
+ clearShouldResyncSessions,
51
+ markShouldResyncSessions,
52
+ event,
53
+ } = params;
47
54
  if (event.type === 'connection.open') {
48
- setConnectionStatus('connected');
49
- if (shouldResyncSessionsRef.current) {
50
- shouldResyncSessionsRef.current = false;
55
+ systemStatusManager.handleConnectionRestored();
56
+ if (shouldResyncSessions) {
57
+ clearShouldResyncSessions();
51
58
  queryClient?.invalidateQueries({ queryKey: ['ncp-sessions'] });
52
59
  }
53
60
  return;
54
61
  }
55
62
  if (event.type === 'connection.close' || event.type === 'connection.error') {
56
- setConnectionStatus('disconnected');
57
- shouldResyncSessionsRef.current = true;
63
+ systemStatusManager.handleConnectionInterrupted(
64
+ event.type === 'connection.error' ? event.payload?.message : null
65
+ );
66
+ markShouldResyncSessions();
58
67
  return;
59
68
  }
60
69
  if (event.type === 'config.updated') {
@@ -76,14 +85,21 @@ function handleRealtimeEvent(
76
85
  }
77
86
 
78
87
  export function useRealtimeQueryBridge(queryClient?: QueryClient) {
79
- const { setConnectionStatus } = useUiStore();
80
88
  const shouldResyncSessionsRef = useRef(false);
81
89
 
82
90
  useEffect(() => {
83
- setConnectionStatus('connecting');
84
-
85
91
  return appClient.subscribe((event) =>
86
- handleRealtimeEvent(queryClient, setConnectionStatus, shouldResyncSessionsRef, event)
92
+ handleRealtimeEvent({
93
+ queryClient,
94
+ shouldResyncSessions: shouldResyncSessionsRef.current,
95
+ clearShouldResyncSessions: () => {
96
+ shouldResyncSessionsRef.current = false;
97
+ },
98
+ markShouldResyncSessions: () => {
99
+ shouldResyncSessionsRef.current = true;
100
+ },
101
+ event,
102
+ })
87
103
  );
88
- }, [queryClient, setConnectionStatus]);
104
+ }, [queryClient]);
89
105
  }
@@ -185,7 +185,8 @@ export function useUpdateChannel() {
185
185
  updateChannel(channel, data as Parameters<typeof updateChannel>[1]),
186
186
  onSuccess: () => {
187
187
  queryClient.invalidateQueries({ queryKey: ['config'] });
188
- toast.success(t('configSavedApplied'));
188
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
189
+ toast.success(t('configSavedApplying'));
189
190
  },
190
191
  onError: (error: Error) => {
191
192
  toast.error(t('configSaveFailed') + ': ' + error.message);
package/src/index.css CHANGED
@@ -59,6 +59,30 @@
59
59
  background: hsl(var(--gray-400));
60
60
  }
61
61
 
62
+ .workspace-horizontal-scrollbar {
63
+ scrollbar-width: thin;
64
+ scrollbar-color: hsl(var(--gray-300) / 0.38) transparent;
65
+ scrollbar-gutter: stable;
66
+ }
67
+
68
+ .workspace-horizontal-scrollbar::-webkit-scrollbar {
69
+ width: 3px;
70
+ height: 2px;
71
+ }
72
+
73
+ .workspace-horizontal-scrollbar::-webkit-scrollbar-track {
74
+ background: transparent;
75
+ }
76
+
77
+ .workspace-horizontal-scrollbar::-webkit-scrollbar-thumb {
78
+ background: hsl(var(--gray-300) / 0.38);
79
+ border-radius: 999px;
80
+ }
81
+
82
+ .workspace-horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
83
+ background: hsl(var(--gray-400) / 0.48);
84
+ }
85
+
62
86
  /* ========================================
63
87
  GLASSMORPHISM
64
88
  ======================================== */
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveAppResourceUri } from "@/lib/app-resource-uri";
3
+
4
+ describe("resolveAppResourceUri", () => {
5
+ it("maps app resource uris to public app paths", () => {
6
+ expect(resolveAppResourceUri("app://runtime-icons/codex-openai.svg")).toBe(
7
+ "/runtime-icons/codex-openai.svg",
8
+ );
9
+ });
10
+
11
+ it("passes through ordinary image src values for compatibility", () => {
12
+ expect(resolveAppResourceUri("https://example.com/icon.png")).toBe(
13
+ "https://example.com/icon.png",
14
+ );
15
+ });
16
+
17
+ it("rejects app resource uris that escape the app resource directory", () => {
18
+ expect(resolveAppResourceUri("app://../icon.png")).toBeNull();
19
+ });
20
+ });
@@ -0,0 +1,29 @@
1
+ const APP_RESOURCE_URI_PREFIX = "app://";
2
+
3
+ function normalizeAppResourcePath(value: string): string | null {
4
+ const normalized = value.trim().replace(/^\/+/, "");
5
+ if (!normalized) {
6
+ return null;
7
+ }
8
+ const segments = normalized.split("/");
9
+ if (
10
+ segments.some((segment) => segment.trim().length === 0 || segment === "." || segment === "..")
11
+ ) {
12
+ return null;
13
+ }
14
+ return segments.join("/");
15
+ }
16
+
17
+ export function resolveAppResourceUri(uri: string): string | null {
18
+ const normalized = uri.trim();
19
+ if (!normalized) {
20
+ return null;
21
+ }
22
+ if (!normalized.startsWith(APP_RESOURCE_URI_PREFIX)) {
23
+ return normalized;
24
+ }
25
+ const appResourcePath = normalizeAppResourcePath(
26
+ normalized.slice(APP_RESOURCE_URI_PREFIX.length),
27
+ );
28
+ return appResourcePath ? `/${appResourcePath}` : null;
29
+ }
@@ -59,6 +59,14 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
59
59
  chatWorkspacePreviewTruncated: { zh: '内容已截断', en: 'Preview truncated' },
60
60
  chatSessionUnread: { zh: '会话有未读更新', en: 'Session has unread updates' },
61
61
  chatTyping: { zh: 'Agent 正在思考...', en: 'Agent is thinking...' },
62
+ chatRuntimeInitializing: {
63
+ zh: '聊天能力正在初始化。你可以先输入内容,完成后即可发送。',
64
+ en: 'Chat is still initializing. You can keep typing and send once startup finishes.'
65
+ },
66
+ chatRuntimeInitializationFailed: {
67
+ zh: '聊天能力启动失败,请稍后重试或检查服务日志。',
68
+ en: 'Chat startup failed. Please retry in a moment or inspect the service logs.'
69
+ },
62
70
  chatInputPlaceholder: { zh: '输入消息,输入 / 选择技能,Enter 发送,Shift + Enter 换行', en: 'Type a message, type / to select skills, Enter to send, Shift + Enter for newline' },
63
71
  chatInputHint: { zh: '支持多轮上下文,默认走当前会话。', en: 'Multi-turn context is preserved in the current session.' },
64
72
  chatSlashSectionCommands: { zh: '命令', en: 'Commands' },
@@ -5,7 +5,7 @@ export const REMOTE_LABELS: Record<string, { zh: string; en: string }> = {
5
5
  en: 'Make this device appear in your NextClaw Platform device list and open it from the web.'
6
6
  },
7
7
  remoteOpenWeb: { zh: '前往 NextClaw Web', en: 'Open NextClaw Web' },
8
- remoteOpenDeviceList: { zh: '查看我的设备', en: 'View My Devices' },
8
+ remoteOpenDeviceList: { zh: '前往 NextClaw Web', en: 'Open NextClaw Web' },
9
9
  remoteOpenWebHint: {
10
10
  zh: '开启后,这台设备会出现在 NextClaw Web 中,你可以在那里点击打开并继续使用。',
11
11
  en: 'Once enabled, this device appears in NextClaw Web, where you can open it and keep working.'
@@ -75,6 +75,37 @@ export const RUNTIME_CONTROL_LABELS: Record<string, { zh: string; en: string }>
75
75
  runtimeControlRestartService: { zh: '重启服务', en: 'Restart Service' },
76
76
  runtimeControlStopService: { zh: '停止服务', en: 'Stop Service' },
77
77
  runtimeControlRestartApp: { zh: '重启应用', en: 'Restart App' },
78
+ runtimeControlPendingRestartTitle: { zh: '待重启', en: 'Pending Restart' },
79
+ runtimeControlPendingRestartDescription: {
80
+ zh: '这次改动已经保存,但系统不会自动重启。请在你方便的时候手动重启,重启完成后该提示会自动清空。',
81
+ en: 'These changes are saved, but the system will not restart automatically. Restart manually when you are ready, and this notice clears after the restart finishes.'
82
+ },
83
+ runtimeControlPendingRestartPaths: { zh: '待生效项', en: 'Changes Waiting For Restart' },
84
+ runtimeStatusLoadingTitle: { zh: '读取状态中', en: 'Loading status' },
85
+ runtimeStatusLoadingDescription: {
86
+ zh: '正在读取当前系统状态。',
87
+ en: 'Loading the current system status.'
88
+ },
89
+ runtimeStatusHealthyTitle: { zh: '系统正常', en: 'System healthy' },
90
+ runtimeStatusHealthyDescription: {
91
+ zh: '当前没有需要你立即处理的系统动作。',
92
+ en: 'There is no system action that needs your attention right now.'
93
+ },
94
+ runtimeStatusPendingRestartTitle: { zh: '待重启', en: 'Restart required' },
95
+ runtimeStatusPendingRestartDescription: {
96
+ zh: '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。',
97
+ en: 'These changes are saved, but the system will not restart automatically. Review the reason here and restart when you are ready.'
98
+ },
99
+ runtimeStatusPendingRestartReasonItem: {
100
+ zh: '{path} 改动将在重启后生效。',
101
+ en: 'Changes in {path} will apply after restart.'
102
+ },
103
+ runtimeStatusActionHint: {
104
+ zh: '准备好时再执行',
105
+ en: 'Run when you are ready'
106
+ },
107
+ runtimeStatusRestartAction: { zh: '立即重启', en: 'Restart now' },
108
+ runtimeStatusRestartingAction: { zh: '重启中...', en: 'Restarting...' },
78
109
  runtimeControlStartingServiceHelp: {
79
110
  zh: '正在启动 NextClaw 服务,页面可能会在服务恢复后重新连接。',
80
111
  en: 'Starting the NextClaw service. The page may reconnect after the service becomes available.'
package/src/lib/i18n.ts CHANGED
@@ -497,22 +497,19 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
497
497
 
498
498
  // Remote & Status
499
499
  ...REMOTE_LABELS,
500
-
501
- // Action labels
502
500
  actionConfigure: { zh: '配置', en: 'Configure' },
503
501
  actionAddProvider: { zh: '添加提供商', en: 'Add Provider' },
504
502
  actionEnable: { zh: '启用', en: 'Enable' },
505
-
506
- // Messages
507
503
  configSaved: { zh: '配置已保存', en: 'Configuration saved' },
504
+ configSavedApplying: { zh: '配置已保存,正在应用', en: 'Configuration saved, applying changes' },
508
505
  configSavedApplied: { zh: '配置已保存并已应用', en: 'Configuration saved and applied' },
509
506
  configSaveFailed: { zh: '保存配置失败', en: 'Failed to save configuration' },
510
507
  configReloaded: { zh: '配置已重载', en: 'Configuration reloaded' },
511
508
  configReloadFailed: { zh: '重载配置失败', en: 'Failed to reload configuration' },
512
- feishuVerifySuccess: {
513
- zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。',
514
- en: 'Verified. Please finish Feishu event subscription and app publishing before using.'
515
- },
509
+ channelConfigApplying: { zh: '渠道配置正在应用', en: 'Channel configuration is applying' },
510
+ channelConfigApplied: { zh: '渠道配置已应用', en: 'Channel configuration applied' },
511
+ channelConfigApplyFailed: { zh: '渠道配置应用失败', en: 'Failed to apply channel configuration' },
512
+ feishuVerifySuccess: { zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。', en: 'Verified. Please finish Feishu event subscription and app publishing before using.' },
516
513
  feishuVerifyFailed: { zh: '验证失败', en: 'Verification failed' },
517
514
  enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
518
515
  headerName: { zh: 'Header 名称', en: 'Header Name' },
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { resolveSessionContextView } from "@/lib/session-context.utils";
3
+
4
+ vi.mock("@/lib/logos", () => ({
5
+ getChannelLogo: vi.fn(() => null),
6
+ }));
7
+
8
+ vi.mock("@/lib/i18n", () => ({
9
+ t: (key: string) => key,
10
+ }));
11
+
12
+ describe("resolveSessionContextView", () => {
13
+ it("prefers a declared runtime icon before falling back to a text label", () => {
14
+ const view = resolveSessionContextView(
15
+ {
16
+ key: "session-1",
17
+ createdAt: "2026-04-18T00:00:00.000Z",
18
+ updatedAt: "2026-04-18T00:00:00.000Z",
19
+ sessionType: "hermes",
20
+ sessionTypeMutable: true,
21
+ messageCount: 1,
22
+ },
23
+ [
24
+ {
25
+ value: "hermes",
26
+ label: "Hermes",
27
+ icon: {
28
+ kind: "image",
29
+ src: "app://runtime-icons/hermes-agent.png",
30
+ alt: "Hermes",
31
+ },
32
+ },
33
+ ],
34
+ );
35
+
36
+ expect(view).toEqual({
37
+ icon: {
38
+ kind: "runtime-image",
39
+ src: "app://runtime-icons/hermes-agent.png",
40
+ alt: "Hermes",
41
+ name: "Hermes",
42
+ },
43
+ label: null,
44
+ });
45
+ });
46
+
47
+ it("falls back to the resolved runtime label when no runtime icon is available", () => {
48
+ const view = resolveSessionContextView(
49
+ {
50
+ key: "session-2",
51
+ createdAt: "2026-04-18T00:00:00.000Z",
52
+ updatedAt: "2026-04-18T00:00:00.000Z",
53
+ sessionType: "custom-runtime",
54
+ sessionTypeMutable: true,
55
+ messageCount: 2,
56
+ },
57
+ [
58
+ {
59
+ value: "custom-runtime",
60
+ label: "Custom Runtime",
61
+ icon: null,
62
+ },
63
+ ],
64
+ );
65
+
66
+ expect(view).toEqual({
67
+ icon: null,
68
+ label: "Custom Runtime",
69
+ });
70
+ });
71
+ });