@nextclaw/ui 0.12.19 → 0.12.20-beta.1

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 (185) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/api-BcqDx0tm.js +15 -0
  3. package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
  4. package/dist/assets/app-navigation.config-CMoWvFEI.js +1 -0
  5. package/dist/assets/{book-open-CVEuA0y5.js → book-open-DgLqYpNY.js} +1 -1
  6. package/dist/assets/{channels-list-page-BqhqaBf1.js → channels-list-page-CsoI4OJm.js} +2 -2
  7. package/dist/assets/{chat-D4KecKjB.js → chat-CA3aRmhx.js} +13 -12
  8. package/dist/assets/chat-page-gdSN6Pr6.js +1 -0
  9. package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
  10. package/dist/assets/{config-split-page-BGjVACdO.js → config-split-page-BMRGuCJQ.js} +1 -1
  11. package/dist/assets/{createLucideIcon-PPrXCGK8.js → createLucideIcon-BZkY6emz.js} +1 -1
  12. package/dist/assets/desktop-update-config-CD6-2PfI.js +1 -0
  13. package/dist/assets/{dialog-CTCX7oLf.js → dialog-csshWetU.js} +1 -1
  14. package/dist/assets/{dist-FL5e8mMi.js → dist-Bl94Ahwx.js} +1 -1
  15. package/dist/assets/{doc-browser-C02neCIE.js → doc-browser-BUlCkZo2.js} +1 -1
  16. package/dist/assets/doc-browser-CzCV73NJ.js +1 -0
  17. package/dist/assets/doc-browser-Doh2541x.js +1 -0
  18. package/dist/assets/{doc-browser-context-C-WPOji4.js → doc-browser-context-DfLHAWbG.js} +1 -1
  19. package/dist/assets/{es2015-BNy4R8AC.js → es2015-JCM5-KtW.js} +1 -1
  20. package/dist/assets/{external-link-BNtqJE01.js → external-link-Sw3ah_JD.js} +1 -1
  21. package/dist/assets/{folder-QyJHVUNz.js → folder-D7-VTnkz.js} +1 -1
  22. package/dist/assets/{hash-BGYUE-zr.js → hash-zajSTDXZ.js} +1 -1
  23. package/dist/assets/i18n-C5Mibli1.js +1 -0
  24. package/dist/assets/index-BTDFuKka.js +2 -0
  25. package/dist/assets/index-CUmk8xFK.css +1 -0
  26. package/dist/assets/{key-round-DenCfA2w.js → key-round-CnI1mc9F.js} +1 -1
  27. package/dist/assets/loader-circle-B5i8oMMY.js +1 -0
  28. package/dist/assets/{logo-badge-CKAxvQFc.js → logo-badge-BQgKnVtz.js} +1 -1
  29. package/dist/assets/{logos-CqXnaJIm.js → logos-CqVm0q0W.js} +1 -1
  30. package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
  31. package/dist/assets/{marketplace-page-XnDa2ulT.js → marketplace-page-DxlxHCFm.js} +2 -2
  32. package/dist/assets/mcp-marketplace-page-5UjYRWOR.js +40 -0
  33. package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
  34. package/dist/assets/message-square-D6Z4NwpG.js +1 -0
  35. package/dist/assets/{model-config-ByeL6Toe.js → model-config-PccJ9XyH.js} +1 -1
  36. package/dist/assets/{notice-card-D00-02yg.js → notice-card-CCgk6FvF.js} +1 -1
  37. package/dist/assets/play-D8WJLnJe.js +1 -0
  38. package/dist/assets/plus-Di0KAkiO.js +1 -0
  39. package/dist/assets/{popover-AmJkxio3.js → popover-YAsxDBhY.js} +1 -1
  40. package/dist/assets/{provider-scoped-model-input-CfFJsJp-.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
  41. package/dist/assets/{providers-list-HMQzW2WV.js → providers-list-8qDMER8o.js} +1 -1
  42. package/dist/assets/{refresh-ccw-B-dhb3yS.js → refresh-ccw-Bii4w8aB.js} +1 -1
  43. package/dist/assets/refresh-cw-BxojR62w.js +1 -0
  44. package/dist/assets/remote-D4TtLPAp.js +1 -0
  45. package/dist/assets/{rotate-cw-BWqAG3Fv.js → rotate-cw-1Xqa7LZ8.js} +1 -1
  46. package/dist/assets/runtime-config-page-D-4c5H5z.js +1 -0
  47. package/dist/assets/{save-DpdkGieJ.js → save--BVI5wZX.js} +1 -1
  48. package/dist/assets/search-config-D3a65l3r.js +1 -0
  49. package/dist/assets/{search-CQUdr7j_.js → search-vChioOoe.js} +1 -1
  50. package/dist/assets/{secrets-config-YCsGd1am.js → secrets-config-CoMlR_7i.js} +2 -2
  51. package/dist/assets/{select-DVUtSFHZ.js → select-DIZrwsKU.js} +1 -1
  52. package/dist/assets/{sessions-config-page-BKN-XdKr.js → sessions-config-page-Cc0TJStn.js} +2 -2
  53. package/dist/assets/{setting-row-Cb5-lFs-.js → setting-row-DiQyrE81.js} +1 -1
  54. package/dist/assets/{settings-DgtZZlnF.js → settings-CiRChctQ.js} +1 -1
  55. package/dist/assets/skeleton-CFQRIUzt.js +1 -0
  56. package/dist/assets/{sparkles-DNSCyDhL.js → sparkles-D1ZKWdm4.js} +1 -1
  57. package/dist/assets/{status-dot-X_j51OfA.js → status-dot-Dv_hiUVa.js} +1 -1
  58. package/dist/assets/{tabs-custom-CcWmekaF.js → tabs-custom-CsACkVji.js} +1 -1
  59. package/dist/assets/{tag-chip-fdbK2wE6.js → tag-chip-C3wDBe_-.js} +1 -1
  60. package/dist/assets/theme-provider-aOmrJ9J6.js +1 -0
  61. package/dist/assets/{tooltip-BkZCQcKw.js → tooltip-Dq5Xehpk.js} +1 -1
  62. package/dist/assets/{trash-2-CqciSCsg.js → trash-2-rY9ZteZX.js} +1 -1
  63. package/dist/assets/use-config-BQJjq1mP.js +1 -0
  64. package/dist/assets/{use-confirm-dialog-DSrb9205.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
  65. package/dist/assets/{use-infinite-scroll-loader-DmowtyTI.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
  66. package/dist/assets/{use-viewport-layout-CaALCA51.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
  67. package/dist/assets/x-DpTzXQcX.js +1 -0
  68. package/dist/index.html +40 -39
  69. package/package.json +9 -6
  70. package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
  71. package/src/app/index.tsx +7 -1
  72. package/src/features/channels/components/config/channel-form.tsx +3 -3
  73. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +1 -1
  74. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -0
  75. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +9 -4
  76. package/src/features/chat/components/conversation/chat-message-list.container.test.tsx +64 -6
  77. package/src/features/chat/components/conversation/chat-message-list.container.tsx +185 -17
  78. package/src/features/chat/components/session/session-context-icon.tsx +1 -4
  79. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +3 -1
  80. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
  81. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +74 -2
  82. package/src/features/chat/hooks/use-ncp-session-conversation.ts +32 -10
  83. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +20 -0
  84. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +25 -0
  85. package/src/features/chat/managers/ncp-chat-input.manager.ts +5 -1
  86. package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
  87. package/src/features/chat/pages/ncp-chat-page.tsx +15 -11
  88. package/src/features/chat/stores/chat-thread.store.ts +8 -2
  89. package/src/features/chat/utils/chat-context-window-indicator.utils.ts +50 -0
  90. package/src/features/chat/utils/chat-runtime.utils.ts +1 -1
  91. package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
  92. package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
  93. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +165 -0
  94. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.ts +50 -0
  95. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +27 -0
  96. package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -4
  97. package/src/features/chat/utils/ncp-session-context-metadata.utils.ts +121 -0
  98. package/src/features/chat/utils/session-context.utils.ts +1 -2
  99. package/src/features/system-status/components/config/runtime-config-editor.tsx +6 -0
  100. package/src/features/system-status/components/config/runtime-settings-card.tsx +12 -0
  101. package/src/features/system-status/components/desktop-update-config.test.tsx +17 -7
  102. package/src/features/system-status/components/desktop-update-config.tsx +75 -30
  103. package/src/features/system-status/hooks/use-system-status.ts +0 -11
  104. package/src/features/system-status/index.ts +4 -1
  105. package/src/features/system-status/managers/runtime-update.manager.ts +330 -0
  106. package/src/features/system-status/managers/system-status.manager.test.ts +0 -25
  107. package/src/features/system-status/managers/system-status.manager.ts +1 -30
  108. package/src/features/system-status/stores/runtime-update.store.ts +24 -0
  109. package/src/features/system-status/types/system-status.types.ts +0 -2
  110. package/src/features/system-status/utils/runtime-config-agent.utils.ts +6 -1
  111. package/src/features/system-status/utils/system-status.utils.test.ts +1 -85
  112. package/src/features/system-status/utils/system-status.utils.ts +1 -23
  113. package/src/platforms/desktop/managers/desktop-update.manager.ts +6 -0
  114. package/src/platforms/desktop/types/desktop-update.types.ts +21 -19
  115. package/src/shared/components/common/brand-header.test.tsx +142 -0
  116. package/src/shared/components/common/brand-header.tsx +93 -0
  117. package/src/shared/components/cron-config.tsx +1 -1
  118. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +1 -1
  119. package/src/shared/components/doc-browser/doc-browser.tsx +1 -1
  120. package/src/shared/components/search-config.tsx +3 -3
  121. package/src/shared/lib/api/README.md +3 -0
  122. package/src/shared/lib/api/index.ts +13 -11
  123. package/src/shared/lib/api/ncp-session.test.ts +17 -18
  124. package/src/shared/lib/api/ncp-session.types.ts +92 -0
  125. package/src/shared/lib/api/raw-client.utils.ts +3 -126
  126. package/src/shared/lib/api/services/agents.service.ts +18 -0
  127. package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
  128. package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
  129. package/src/shared/lib/api/services/config.service.ts +171 -0
  130. package/src/shared/lib/api/services/marketplace.service.ts +66 -0
  131. package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
  132. package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
  133. package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
  134. package/src/shared/lib/api/services/remote.service.ts +50 -0
  135. package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
  136. package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
  137. package/src/shared/lib/api/services/server-path.service.ts +16 -0
  138. package/src/shared/lib/api/types.ts +9 -74
  139. package/src/shared/lib/i18n/{chat.ts → chat-labels.utils.ts} +13 -1
  140. package/src/shared/lib/i18n/desktop-update-labels.utils.ts +65 -0
  141. package/src/shared/lib/i18n/index.ts +4 -5
  142. package/src/shared/lib/i18n/runtime/i18n-language-owner.ts +5 -5
  143. package/src/shared/lib/transport/index.ts +1 -0
  144. package/src/shared/lib/transport/local-transport.service.ts +24 -4
  145. package/src/shared/lib/transport/remote-transport.service.ts +2 -2
  146. package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
  147. package/src/shared/lib/transport/transport.types.ts +8 -2
  148. package/src/shared/lib/ui-document-title/index.ts +1 -1
  149. package/tsconfig.json +1 -0
  150. package/dist/assets/api-BurjmW4A.js +0 -15
  151. package/dist/assets/app-manager-provider-DhxUmyTv.js +0 -1
  152. package/dist/assets/app-navigation.config-Bpd16Pem.js +0 -1
  153. package/dist/assets/chat-page-Cc7n80lW.js +0 -1
  154. package/dist/assets/chunk-JZWAC4HX-24FLdHl7.js +0 -3
  155. package/dist/assets/desktop-update-config-fMLlSStv.js +0 -1
  156. package/dist/assets/doc-browser-COj7x090.js +0 -1
  157. package/dist/assets/doc-browser-fyn7eDTp.js +0 -1
  158. package/dist/assets/i18n-CM4y8Mw9.js +0 -1
  159. package/dist/assets/index-CtVSzMPM.js +0 -2
  160. package/dist/assets/index-N3hjuljD.css +0 -1
  161. package/dist/assets/loader-circle-R23uEPkM.js +0 -1
  162. package/dist/assets/marketplace-page-mF-M5mku.js +0 -1
  163. package/dist/assets/mcp-marketplace-page-BArKWcRZ.js +0 -40
  164. package/dist/assets/mcp-marketplace-page-DBUcIIHJ.js +0 -1
  165. package/dist/assets/message-square-Dm34zD6k.js +0 -1
  166. package/dist/assets/play-ul4L6MWm.js +0 -1
  167. package/dist/assets/plus-D14303DH.js +0 -1
  168. package/dist/assets/remote-B4ELSd3u.js +0 -1
  169. package/dist/assets/runtime-config-page-N4FP6H0M.js +0 -1
  170. package/dist/assets/search-config-B62TY-z2.js +0 -1
  171. package/dist/assets/skeleton-BCPi52jT.js +0 -1
  172. package/dist/assets/theme-provider-WTWq_jYq.js +0 -1
  173. package/dist/assets/use-config-CyvhbRhf.js +0 -1
  174. package/dist/assets/x-tYcSDsrY.js +0 -1
  175. package/src/shared/lib/api/agents.ts +0 -34
  176. package/src/shared/lib/api/channel-auth.ts +0 -35
  177. package/src/shared/lib/api/config.ts +0 -362
  178. package/src/shared/lib/api/marketplace.ts +0 -156
  179. package/src/shared/lib/api/mcp-marketplace.ts +0 -138
  180. package/src/shared/lib/api/ncp-attachments.ts +0 -41
  181. package/src/shared/lib/api/ncp-session.ts +0 -78
  182. package/src/shared/lib/api/remote.ts +0 -86
  183. package/src/shared/lib/api/runtime-control.ts +0 -34
  184. package/src/shared/lib/api/server-path.ts +0 -46
  185. /package/dist/assets/{config-hints-CPNzbMEp.js → config-hints-MogHYQ8G.js} +0 -0
@@ -0,0 +1,330 @@
1
+ import type { UpdatePreferences, UpdateSnapshot } from '@nextclaw/kernel';
2
+ import { applyRuntimeUpdate, checkRuntimeUpdate, downloadRuntimeUpdate, fetchRuntimeUpdate, updateRuntimeUpdateChannel, updateRuntimeUpdatePreferences } from '@/shared/lib/api';
3
+ import type { NextClawDesktopBridge } from '@/platforms/desktop';
4
+ import { t } from '@/shared/lib/i18n';
5
+ import { toast } from 'sonner';
6
+ import { useRuntimeUpdateStore, type RuntimeUpdateBusyAction } from '@/features/system-status/stores/runtime-update.store';
7
+
8
+ type RuntimeUpdateSourceKind = 'desktop-bridge' | 'runtime-host';
9
+
10
+ interface RuntimeUpdateSourceBase {
11
+ getState: () => Promise<UpdateSnapshot>;
12
+ checkForUpdates: () => Promise<UpdateSnapshot>;
13
+ downloadUpdate: () => Promise<UpdateSnapshot>;
14
+ applyDownloadedUpdate: () => Promise<UpdateSnapshot>;
15
+ updatePreferences: (preferences: Partial<UpdatePreferences>) => Promise<UpdateSnapshot>;
16
+ updateChannel: (channel: UpdateSnapshot['channel']) => Promise<UpdateSnapshot>;
17
+ }
18
+
19
+ interface DesktopBridgeRuntimeUpdateSourceContract extends RuntimeUpdateSourceBase {
20
+ kind: 'desktop-bridge';
21
+ subscribe: (listener: (snapshot: UpdateSnapshot) => void) => () => void;
22
+ }
23
+
24
+ interface HostRuntimeUpdateSourceContract extends RuntimeUpdateSourceBase {
25
+ kind: 'runtime-host';
26
+ }
27
+
28
+ const RUNTIME_HOST_POLL_INTERVAL_MS = 1000;
29
+
30
+ type RuntimeUpdateSource = DesktopBridgeRuntimeUpdateSourceContract | HostRuntimeUpdateSourceContract;
31
+
32
+ class DesktopBridgeRuntimeUpdateSource implements DesktopBridgeRuntimeUpdateSourceContract {
33
+ readonly kind = 'desktop-bridge' as const;
34
+
35
+ constructor(private readonly desktopApi: NextClawDesktopBridge) {}
36
+
37
+ subscribe = (listener: (snapshot: UpdateSnapshot) => void) => {
38
+ return this.desktopApi.onUpdateStateChanged(listener);
39
+ };
40
+
41
+ getState = async (): Promise<UpdateSnapshot> => {
42
+ return await this.desktopApi.getUpdateState();
43
+ };
44
+
45
+ checkForUpdates = async (): Promise<UpdateSnapshot> => {
46
+ return await this.desktopApi.checkForUpdates();
47
+ };
48
+
49
+ downloadUpdate = async (): Promise<UpdateSnapshot> => {
50
+ return await this.desktopApi.downloadUpdate();
51
+ };
52
+
53
+ applyDownloadedUpdate = async (): Promise<UpdateSnapshot> => {
54
+ return await this.desktopApi.applyDownloadedUpdate();
55
+ };
56
+
57
+ updatePreferences = async (preferences: Partial<UpdatePreferences>): Promise<UpdateSnapshot> => {
58
+ return await this.desktopApi.updatePreferences(preferences);
59
+ };
60
+
61
+ updateChannel = async (channel: UpdateSnapshot['channel']): Promise<UpdateSnapshot> => {
62
+ return await this.desktopApi.updateChannel(channel);
63
+ };
64
+ }
65
+
66
+ class HostRuntimeUpdateSource implements HostRuntimeUpdateSourceContract {
67
+ readonly kind = 'runtime-host' as const;
68
+
69
+ getState = async (): Promise<UpdateSnapshot> => {
70
+ return await fetchRuntimeUpdate();
71
+ };
72
+
73
+ checkForUpdates = async (): Promise<UpdateSnapshot> => {
74
+ return await checkRuntimeUpdate();
75
+ };
76
+
77
+ downloadUpdate = async (): Promise<UpdateSnapshot> => {
78
+ return await downloadRuntimeUpdate();
79
+ };
80
+
81
+ applyDownloadedUpdate = async (): Promise<UpdateSnapshot> => {
82
+ return await applyRuntimeUpdate();
83
+ };
84
+
85
+ updatePreferences = async (preferences: Partial<UpdatePreferences>): Promise<UpdateSnapshot> => {
86
+ return await updateRuntimeUpdatePreferences(preferences);
87
+ };
88
+
89
+ updateChannel = async (channel: UpdateSnapshot['channel']): Promise<UpdateSnapshot> => {
90
+ return await updateRuntimeUpdateChannel(channel);
91
+ };
92
+ }
93
+
94
+ export class RuntimeUpdateManager {
95
+ private unsubscribe: (() => void) | null = null;
96
+ private pollingTimer: number | null = null;
97
+ private subscriptionCount = 0;
98
+ private source: RuntimeUpdateSource | null = null;
99
+
100
+ start = async () => {
101
+ this.subscriptionCount += 1;
102
+ const source = this.resolveSource();
103
+ this.source = source;
104
+ if (!source) {
105
+ useRuntimeUpdateStore.setState({
106
+ supported: false,
107
+ initialized: true,
108
+ snapshot: null
109
+ });
110
+ return;
111
+ }
112
+
113
+ if (source.kind === 'desktop-bridge' && !this.unsubscribe) {
114
+ this.unsubscribe = source.subscribe((snapshot) => {
115
+ useRuntimeUpdateStore.setState({
116
+ supported: true,
117
+ initialized: true,
118
+ snapshot
119
+ });
120
+ });
121
+ }
122
+
123
+ if (source.kind === 'runtime-host') {
124
+ this.ensurePolling();
125
+ }
126
+
127
+ useRuntimeUpdateStore.setState({
128
+ supported: true,
129
+ initialized: false
130
+ });
131
+
132
+ try {
133
+ const snapshot = await source.getState();
134
+ useRuntimeUpdateStore.setState({
135
+ supported: true,
136
+ initialized: true,
137
+ snapshot
138
+ });
139
+ } catch (error) {
140
+ if (source.kind === 'runtime-host' && this.isUnsupportedError(error)) {
141
+ useRuntimeUpdateStore.setState({
142
+ supported: false,
143
+ initialized: true,
144
+ snapshot: null
145
+ });
146
+ return;
147
+ }
148
+ useRuntimeUpdateStore.setState({
149
+ supported: true,
150
+ initialized: true
151
+ });
152
+ toast.error(`${t('runtimeUpdatesLoadFailed')}: ${this.getErrorMessage(error)}`);
153
+ }
154
+ };
155
+
156
+ stop = () => {
157
+ this.subscriptionCount = Math.max(0, this.subscriptionCount - 1);
158
+ if (this.subscriptionCount > 0) {
159
+ return;
160
+ }
161
+ this.unsubscribe?.();
162
+ this.unsubscribe = null;
163
+ if (this.pollingTimer !== null && typeof window !== 'undefined') {
164
+ window.clearInterval(this.pollingTimer);
165
+ }
166
+ this.pollingTimer = null;
167
+ this.source = null;
168
+ };
169
+
170
+ checkForUpdates = async () => {
171
+ let snapshot: UpdateSnapshot;
172
+ try {
173
+ snapshot = await this.runSnapshotCommand('checking', t('runtimeUpdatesCheckFailed'), async (source) => {
174
+ return await source.checkForUpdates();
175
+ });
176
+ } catch {
177
+ return;
178
+ }
179
+
180
+ if (snapshot.status === 'up-to-date') {
181
+ toast.success(t('runtimeUpdatesAlreadyLatest'));
182
+ return;
183
+ }
184
+ if (snapshot.status === 'update-available') {
185
+ toast.success(
186
+ t('runtimeUpdatesAvailable').replace('{version}', snapshot.availableVersion ?? t('runtimeUpdatesUnknownVersion'))
187
+ );
188
+ return;
189
+ }
190
+ if (snapshot.status === 'downloaded') {
191
+ toast.success(t('runtimeUpdatesReadyToApply'));
192
+ }
193
+ };
194
+
195
+ downloadUpdate = async () => {
196
+ try {
197
+ await this.runSnapshotCommand('downloading', t('runtimeUpdatesDownloadFailed'), async (source) => {
198
+ return await source.downloadUpdate();
199
+ });
200
+ } catch {
201
+ return;
202
+ }
203
+ };
204
+
205
+ applyDownloadedUpdate = async () => {
206
+ try {
207
+ await this.runSnapshotCommand('applying', t('runtimeUpdatesApplyFailed'), async (source) => {
208
+ return await source.applyDownloadedUpdate();
209
+ });
210
+ } catch {
211
+ return;
212
+ }
213
+ };
214
+
215
+ updatePreferences = async (preferences: Partial<UpdatePreferences>) => {
216
+ try {
217
+ await this.runSnapshotCommand('saving-preferences', t('runtimeUpdatesPreferencesFailed'), async (source) => {
218
+ return await source.updatePreferences(preferences);
219
+ });
220
+ } catch {
221
+ return;
222
+ }
223
+ };
224
+
225
+ updateChannel = async (channel: UpdateSnapshot['channel']) => {
226
+ const currentChannel = useRuntimeUpdateStore.getState().snapshot?.channel;
227
+ if (currentChannel === channel) {
228
+ return;
229
+ }
230
+
231
+ let snapshot: UpdateSnapshot;
232
+ try {
233
+ snapshot = await this.runSnapshotCommand('switching-channel', t('runtimeUpdatesChannelChangeFailed'), async (source) => {
234
+ return await source.updateChannel(channel);
235
+ });
236
+ } catch {
237
+ return;
238
+ }
239
+
240
+ if (snapshot.status === 'update-available' && snapshot.availableVersion) {
241
+ toast.success(
242
+ t('runtimeUpdatesChannelChangedWithUpdate')
243
+ .replace('{channel}', this.getChannelLabel(channel))
244
+ .replace('{version}', snapshot.availableVersion)
245
+ );
246
+ return;
247
+ }
248
+
249
+ toast.success(t('runtimeUpdatesChannelChanged').replace('{channel}', this.getChannelLabel(channel)));
250
+ };
251
+
252
+ private ensurePolling = () => {
253
+ if (this.pollingTimer !== null || typeof window === 'undefined') {
254
+ return;
255
+ }
256
+ this.pollingTimer = window.setInterval(() => {
257
+ void this.refreshSnapshot();
258
+ }, RUNTIME_HOST_POLL_INTERVAL_MS);
259
+ };
260
+
261
+ private refreshSnapshot = async () => {
262
+ if (!this.source || this.source.kind !== 'runtime-host') {
263
+ return;
264
+ }
265
+ try {
266
+ const snapshot = await this.source.getState();
267
+ useRuntimeUpdateStore.setState({
268
+ supported: true,
269
+ initialized: true,
270
+ snapshot
271
+ });
272
+ } catch {
273
+ // keep the latest successful snapshot visible
274
+ }
275
+ };
276
+
277
+ private runSnapshotCommand = async (
278
+ busyAction: RuntimeUpdateBusyAction,
279
+ fallbackMessage: string,
280
+ job: (source: RuntimeUpdateSource) => Promise<UpdateSnapshot>
281
+ ): Promise<UpdateSnapshot> => {
282
+ const source = this.source ?? this.resolveSource();
283
+ if (!source) {
284
+ throw new Error(t('runtimeUpdatesUnavailableDescription'));
285
+ }
286
+
287
+ this.source = source;
288
+ useRuntimeUpdateStore.setState({ busyAction });
289
+ try {
290
+ const snapshot = await job(source);
291
+ useRuntimeUpdateStore.setState({ snapshot });
292
+ return snapshot;
293
+ } catch (error) {
294
+ toast.error(`${fallbackMessage}: ${this.getErrorMessage(error)}`);
295
+ throw error;
296
+ } finally {
297
+ useRuntimeUpdateStore.setState({ busyAction: null });
298
+ }
299
+ };
300
+
301
+ private resolveSource = (): RuntimeUpdateSource | null => {
302
+ const desktopApi = this.getDesktopApi();
303
+ if (desktopApi) {
304
+ return new DesktopBridgeRuntimeUpdateSource(desktopApi);
305
+ }
306
+ return new HostRuntimeUpdateSource();
307
+ };
308
+
309
+ private getDesktopApi = (): NextClawDesktopBridge | null => {
310
+ if (typeof window === 'undefined') {
311
+ return null;
312
+ }
313
+ return window.nextclawDesktop ?? null;
314
+ };
315
+
316
+ private isUnsupportedError = (error: unknown): boolean => {
317
+ const message = this.getErrorMessage(error).toLowerCase();
318
+ return message.includes('404') || message.includes('not found') || message.includes('endpoint not found');
319
+ };
320
+
321
+ private getErrorMessage = (error: unknown): string => {
322
+ return error instanceof Error ? error.message : t('error');
323
+ };
324
+
325
+ private getChannelLabel = (channel: UpdateSnapshot['channel']): string => {
326
+ return channel === 'beta' ? t('desktopUpdatesChannelBeta') : t('desktopUpdatesChannelStable');
327
+ };
328
+ }
329
+
330
+ export const runtimeUpdateManager = new RuntimeUpdateManager();
@@ -1,7 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type { BootstrapStatusView } from '@/shared/lib/api';
3
3
  import { appQueryClient } from '@/app-query-client';
4
- import { t } from '@/shared/lib/i18n';
5
4
  import {
6
5
  isTransientRuntimeConnectionErrorMessage,
7
6
  systemStatusManager,
@@ -120,30 +119,6 @@ describe('systemStatusManager', () => {
120
119
  expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe('stalled');
121
120
  });
122
121
 
123
- it('maps transient chat errors to friendly recovery copy while recovering', () => {
124
- systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
125
- systemStatusManager.handleConnectionInterrupted('Failed to fetch');
126
-
127
- expect(
128
- systemStatusManager.getDisplayMessage(
129
- 'NCP fetch failed for POST /api/ncp/agent: Error: Failed to fetch'
130
- )
131
- ).toBe(t('runtimeControlRecoveringHelp'));
132
- });
133
-
134
- it('suppresses transient transport errors after recovery stalls', async () => {
135
- systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
136
- systemStatusManager.handleConnectionInterrupted('Failed to fetch');
137
-
138
- await vi.advanceTimersByTimeAsync(30_000);
139
-
140
- expect(
141
- systemStatusManager.getDisplayMessage(
142
- 'NCP fetch failed for POST /api/ncp/agent: Error: Failed to fetch'
143
- )
144
- ).toBeNull();
145
- });
146
-
147
122
  it('restores readiness from stalled once the realtime connection reopens', async () => {
148
123
  systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
149
124
  systemStatusManager.handleConnectionInterrupted('websocket error');
@@ -15,7 +15,6 @@ import type { NextClawDesktopBridge } from '@/platforms/desktop';
15
15
  import { t } from '@/shared/lib/i18n';
16
16
  import {
17
17
  buildActiveSystemActionState,
18
- resolveChatRuntimeMessage,
19
18
  toSystemStatusView,
20
19
  } from '@/features/system-status/utils/system-status.utils';
21
20
  import {
@@ -245,35 +244,7 @@ export class SystemStatusManager {
245
244
  }
246
245
  };
247
246
 
248
- isChatInteractionBlocked = (): boolean => {
249
- return toSystemStatusView(this.getState()).isChatBlocked;
250
- };
251
-
252
- getDisplayMessage = (message: string | null | undefined): string | null => {
253
- if (!message?.trim()) {
254
- return resolveChatRuntimeMessage(this.getState());
255
- }
256
- const { phase } = toSystemStatusView(this.getState());
257
- if (
258
- phase === 'service-transitioning' &&
259
- this.getState().activeSystemAction?.message?.trim()
260
- ) {
261
- return this.getState().activeSystemAction?.message?.trim() ?? null;
262
- }
263
- if (
264
- phase === 'recovering' &&
265
- isTransientRuntimeConnectionErrorMessage(message)
266
- ) {
267
- return t('runtimeControlRecoveringHelp');
268
- }
269
- if (
270
- phase === 'stalled' &&
271
- isTransientRuntimeConnectionErrorMessage(message)
272
- ) {
273
- return null;
274
- }
275
- return message;
276
- };
247
+ getStatusView = () => toSystemStatusView(this.getState());
277
248
 
278
249
  resetForTests = (): void => {
279
250
  this.clearRecoveryTimeout();
@@ -0,0 +1,24 @@
1
+ import type { UpdateSnapshot } from '@nextclaw/kernel';
2
+ import { create } from 'zustand';
3
+
4
+ export type RuntimeUpdateBusyAction =
5
+ | 'checking'
6
+ | 'downloading'
7
+ | 'applying'
8
+ | 'saving-preferences'
9
+ | 'switching-channel'
10
+ | null;
11
+
12
+ type RuntimeUpdateStoreState = {
13
+ supported: boolean;
14
+ initialized: boolean;
15
+ busyAction: RuntimeUpdateBusyAction;
16
+ snapshot: UpdateSnapshot | null;
17
+ };
18
+
19
+ export const useRuntimeUpdateStore = create<RuntimeUpdateStoreState>(() => ({
20
+ supported: false,
21
+ initialized: false,
22
+ busyAction: null,
23
+ snapshot: null
24
+ }));
@@ -46,8 +46,6 @@ export type SystemStatusState = {
46
46
  export type SystemStatusView = SystemStatusState & {
47
47
  phase: SystemStatusPhase;
48
48
  connectionStatus: SystemConnectionStatus;
49
- isChatBlocked: boolean;
50
- chatMessage: string | null;
51
49
  };
52
50
 
53
51
  export type RuntimeStatusTone = 'healthy' | 'attention' | 'inactive';
@@ -4,7 +4,7 @@ import { t } from '@/shared/lib/i18n';
4
4
  export type DmScope = 'main' | 'per-peer' | 'per-channel-peer' | 'per-account-channel-peer';
5
5
  export type PeerKind = '' | 'direct' | 'group' | 'channel';
6
6
  export type RuntimeEntryDraft = RuntimeEntryView & { id: string; configText: string };
7
- export type RuntimeConfigEditorState = { agents: AgentProfileView[]; bindings: AgentBindingView[]; runtimeEntries: RuntimeEntryDraft[]; dmScope: DmScope; defaultContextTokens: number; defaultEngine: string };
7
+ export type RuntimeConfigEditorState = { companionEnabled: boolean; agents: AgentProfileView[]; bindings: AgentBindingView[]; runtimeEntries: RuntimeEntryDraft[]; dmScope: DmScope; defaultContextTokens: number; defaultEngine: string };
8
8
 
9
9
  const DEFAULT_NARP_STDIO_ENTRY_CONFIG = {
10
10
  wireDialect: 'acp',
@@ -56,6 +56,7 @@ export function parseOptionalInt(value: string): number | undefined {
56
56
 
57
57
  export function createRuntimeConfigEditorState(config: ConfigView): RuntimeConfigEditorState {
58
58
  return {
59
+ companionEnabled: config.companion?.enabled === true,
59
60
  agents: (config.agents.list ?? []).map(hydrateRuntimeAgent),
60
61
  bindings: (config.bindings ?? []).map(hydrateRuntimeBinding),
61
62
  runtimeEntries: Object.entries(config.agents.runtimes?.entries ?? {}).map(([id, entry]) => ({ id, enabled: entry.enabled !== false, label: entry.label ?? '', type: entry.type, config: entry.config ?? {}, configText: JSON.stringify(entry.config ?? {}, null, 2) })),
@@ -81,6 +82,7 @@ export function toPersistedRuntimeAgent(agent: AgentProfileView): AgentProfileVi
81
82
  }
82
83
 
83
84
  export function createRuntimeConfigUpdatePayload(input: {
85
+ companionEnabled: boolean;
84
86
  agents: AgentProfileView[];
85
87
  bindings: AgentBindingView[];
86
88
  runtimeEntries: RuntimeEntryDraft[];
@@ -134,6 +136,9 @@ export function createRuntimeConfigUpdatePayload(input: {
134
136
  return entries;
135
137
  }, {});
136
138
  return {
139
+ companion: {
140
+ enabled: input.companionEnabled
141
+ },
137
142
  agents: {
138
143
  defaults: {
139
144
  contextTokens: Math.max(1000, input.defaultContextTokens),
@@ -1,7 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { t } from '@/shared/lib/i18n';
3
2
  import {
4
- resolveChatRuntimeMessage,
5
3
  resolveSystemConnectionStatus,
6
4
  toSystemStatusView,
7
5
  } from './system-status.utils';
@@ -26,88 +24,8 @@ describe('resolveSystemConnectionStatus', () => {
26
24
  });
27
25
  });
28
26
 
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
27
  describe('toSystemStatusView', () => {
110
- it('keeps stalled chat blocked without surfacing a timeout banner', () => {
28
+ it('maps stalled to factual connection and lifecycle view fields', () => {
111
29
  expect(
112
30
  toSystemStatusView({
113
31
  lifecyclePhase: 'stalled',
@@ -123,8 +41,6 @@ describe('toSystemStatusView', () => {
123
41
  lastSystemActionError: null,
124
42
  })
125
43
  ).toMatchObject({
126
- isChatBlocked: true,
127
- chatMessage: null,
128
44
  connectionStatus: 'disconnected',
129
45
  phase: 'stalled',
130
46
  });
@@ -1,9 +1,9 @@
1
- import { t } from '@/shared/lib/i18n';
2
1
  import type {
3
2
  RuntimeControlAction,
4
3
  RuntimeLifecycleState,
5
4
  RuntimeServiceState,
6
5
  } from '@/shared/lib/api';
6
+ import { t } from '@/shared/lib/i18n';
7
7
  import type {
8
8
  RuntimeControlPanelView,
9
9
  RuntimeStatusBadgeView,
@@ -29,26 +29,6 @@ export function resolveSystemConnectionStatus(
29
29
  return 'connecting';
30
30
  }
31
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
32
  export function toSystemStatusView(
53
33
  state: SystemStatusState
54
34
  ): SystemStatusView {
@@ -57,8 +37,6 @@ export function toSystemStatusView(
57
37
  ...state,
58
38
  phase,
59
39
  connectionStatus: resolveSystemConnectionStatus(phase),
60
- isChatBlocked: phase !== 'ready',
61
- chatMessage: resolveChatRuntimeMessage(state),
62
40
  };
63
41
  }
64
42
 
@@ -12,8 +12,10 @@ type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'saving
12
12
 
13
13
  export class DesktopUpdateManager {
14
14
  private unsubscribe: (() => void) | null = null;
15
+ private subscriptionCount = 0;
15
16
 
16
17
  start = async () => {
18
+ this.subscriptionCount += 1;
17
19
  const desktopApi = this.getDesktopApi();
18
20
  if (!desktopApi) {
19
21
  useDesktopUpdateStore.setState({
@@ -56,6 +58,10 @@ export class DesktopUpdateManager {
56
58
  };
57
59
 
58
60
  stop = () => {
61
+ this.subscriptionCount = Math.max(0, this.subscriptionCount - 1);
62
+ if (this.subscriptionCount > 0) {
63
+ return;
64
+ }
59
65
  this.unsubscribe?.();
60
66
  this.unsubscribe = null;
61
67
  };