@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,50 @@
1
+ import { isTransientRuntimeConnectionErrorMessage, type SystemStatusView } from '@/features/system-status';
2
+ import { t } from '@/shared/lib/i18n';
3
+
4
+ type ChatRuntimeStatus = Pick<SystemStatusView, 'activeSystemAction' | 'bootstrapStatus' | 'lastError' | 'lastReadyAt' | 'lifecyclePhase' | 'phase'>;
5
+
6
+ export function isNcpChatRuntimeBlocked(status: Pick<SystemStatusView, 'bootstrapStatus'>): boolean {
7
+ return status.bootstrapStatus?.ncpAgent.state !== 'ready';
8
+ }
9
+
10
+ export function resolveNcpChatRuntimeMessage(
11
+ status: ChatRuntimeStatus
12
+ ): string | null {
13
+ const actionMessage = status.activeSystemAction?.message?.trim();
14
+ if (actionMessage) return actionMessage;
15
+ if (status.lifecyclePhase === 'cold-starting') {
16
+ return t('chatRuntimeInitializing');
17
+ }
18
+ if (status.lifecyclePhase === 'startup-failed') {
19
+ return (
20
+ status.bootstrapStatus?.ncpAgent.error?.trim() ||
21
+ status.bootstrapStatus?.lastError?.trim() ||
22
+ status.lastError?.trim() ||
23
+ t('chatRuntimeInitializationFailed')
24
+ );
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export function resolveNcpChatSendErrorMessage(params: {
30
+ message: string | null | undefined;
31
+ status: ChatRuntimeStatus;
32
+ }): string | null {
33
+ const { message: rawMessage, status } = params;
34
+ const message = rawMessage?.trim();
35
+ if (!message) {
36
+ return resolveNcpChatRuntimeMessage(status);
37
+ }
38
+ const actionMessage = status.activeSystemAction?.message?.trim();
39
+ if (status.phase === 'service-transitioning' && actionMessage) {
40
+ return actionMessage;
41
+ }
42
+ const isTransientTransportError = isTransientRuntimeConnectionErrorMessage(message);
43
+ if (status.phase === 'recovering' && isTransientTransportError) {
44
+ return t('runtimeControlRecoveringHelp');
45
+ }
46
+ if (status.phase === 'stalled' && isTransientTransportError) {
47
+ return null;
48
+ }
49
+ return message;
50
+ }
@@ -69,6 +69,33 @@ describe('adaptNcpSessionSummary', () => {
69
69
  spawnedByRequestId: 'request-1',
70
70
  });
71
71
  });
72
+
73
+ it('does not hydrate context window metadata from persisted session summaries', () => {
74
+ const adapted = adaptNcpSessionSummary(
75
+ createSummary({
76
+ metadata: {
77
+ last_context_window: {
78
+ version: 1,
79
+ usedContextTokens: 76000,
80
+ totalContextTokens: 200000,
81
+ prunedUsedContextTokens: 61200,
82
+ availableContextTokens: 124000,
83
+ droppedHistoryCount: 3,
84
+ truncatedToolResultCount: 1,
85
+ truncatedSystemPrompt: false,
86
+ truncatedUserMessage: false,
87
+ compacted: true,
88
+ checkpointId: 'ctx-20260505123456-8',
89
+ compactedMessageCount: 8,
90
+ compactedUsedContextTokens: 51000,
91
+ updatedAt: '2026-05-05T12:34:56.000Z',
92
+ },
93
+ },
94
+ }),
95
+ );
96
+
97
+ expect(adapted.contextWindow).toBeUndefined();
98
+ });
72
99
  });
73
100
 
74
101
  describe('adaptNcpMessageToUiMessage file rendering', () => {
@@ -1,6 +1,11 @@
1
1
  import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
2
2
  import type { NcpMessagePart } from '@nextclaw/ncp';
3
- import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/shared/lib/api';
3
+ import type {
4
+ NcpMessageView,
5
+ NcpSessionSummaryView,
6
+ SessionEntryView,
7
+ ThinkingLevel
8
+ } from '@/shared/lib/api';
4
9
  import { API_BASE } from '@/shared/lib/api';
5
10
  import {
6
11
  getSessionProjectName,
@@ -132,9 +137,6 @@ function readPromotedChildSession(summary: NcpSessionSummaryView): boolean {
132
137
  }
133
138
 
134
139
  function parseSessionContext(sessionKey: string): { channel?: string; type?: string } {
135
- if (sessionKey === 'heartbeat') {
136
- return { type: 'heartbeat' };
137
- }
138
140
  if (sessionKey.startsWith('cron:')) {
139
141
  return { type: 'cron' };
140
142
  }
@@ -0,0 +1,121 @@
1
+ import type {
2
+ NcpMessageView,
3
+ SessionContextWindowView,
4
+ } from '@/shared/lib/api';
5
+
6
+ function readOptionalString(value: unknown): string | null {
7
+ if (typeof value !== 'string') {
8
+ return null;
9
+ }
10
+ const trimmed = value.trim();
11
+ return trimmed.length > 0 ? trimmed : null;
12
+ }
13
+
14
+ function readNonNegativeInteger(value: unknown): number | null {
15
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
16
+ return null;
17
+ }
18
+ const normalized = Math.trunc(value);
19
+ return normalized >= 0 ? normalized : null;
20
+ }
21
+
22
+ function readBoolean(value: unknown): boolean {
23
+ return value === true;
24
+ }
25
+
26
+ export function readNcpContextWindowValue(value: unknown): SessionContextWindowView | null {
27
+ const rawContextWindow = value;
28
+ if (!rawContextWindow || typeof rawContextWindow !== 'object' || Array.isArray(rawContextWindow)) {
29
+ return null;
30
+ }
31
+ const contextWindow = rawContextWindow as Record<string, unknown>;
32
+ const usedContextTokens = readNonNegativeInteger(contextWindow.usedContextTokens);
33
+ const totalContextTokens = readNonNegativeInteger(contextWindow.totalContextTokens);
34
+ const prunedUsedContextTokens = readNonNegativeInteger(contextWindow.prunedUsedContextTokens);
35
+ const updatedAt = readOptionalString(contextWindow.updatedAt);
36
+ if (usedContextTokens === null || totalContextTokens === null || prunedUsedContextTokens === null || !updatedAt) {
37
+ return null;
38
+ }
39
+ const compactedUsedContextTokens = readNonNegativeInteger(contextWindow.compactedUsedContextTokens);
40
+ return {
41
+ usedContextTokens,
42
+ totalContextTokens,
43
+ prunedUsedContextTokens,
44
+ availableContextTokens: readNonNegativeInteger(contextWindow.availableContextTokens) ?? Math.max(0, totalContextTokens - usedContextTokens),
45
+ droppedHistoryCount: readNonNegativeInteger(contextWindow.droppedHistoryCount) ?? 0,
46
+ truncatedToolResultCount: readNonNegativeInteger(contextWindow.truncatedToolResultCount) ?? 0,
47
+ truncatedSystemPrompt: readBoolean(contextWindow.truncatedSystemPrompt),
48
+ truncatedUserMessage: readBoolean(contextWindow.truncatedUserMessage),
49
+ compacted: readBoolean(contextWindow.compacted),
50
+ ...(readOptionalString(contextWindow.checkpointId)
51
+ ? { checkpointId: readOptionalString(contextWindow.checkpointId) ?? undefined }
52
+ : {}),
53
+ compactedMessageCount: readNonNegativeInteger(contextWindow.compactedMessageCount) ?? 0,
54
+ ...(compactedUsedContextTokens !== null
55
+ ? { compactedUsedContextTokens }
56
+ : {}),
57
+ updatedAt,
58
+ };
59
+ }
60
+
61
+ export const NEXTCLAW_TIMELINE_KIND_METADATA_KEY = 'nextclaw_timeline_kind';
62
+ export const CONTEXT_COMPACTION_TIMELINE_KIND = 'context_compaction';
63
+
64
+ export type ContextCompactionTimelineView = {
65
+ id: string;
66
+ status: 'compressing' | 'compressed';
67
+ summary: string;
68
+ coveredMessageCount: number;
69
+ coveredSessionMessageCount: number;
70
+ originalEstimatedTokens: number;
71
+ projectedEstimatedTokens: number;
72
+ createdAt: string;
73
+ updatedAt: string;
74
+ };
75
+
76
+ export function readContextCompactionTimeline(message: Pick<NcpMessageView, 'metadata'>): ContextCompactionTimelineView | null {
77
+ const { metadata } = message;
78
+ if (!metadata || metadata[NEXTCLAW_TIMELINE_KIND_METADATA_KEY] !== CONTEXT_COMPACTION_TIMELINE_KIND) {
79
+ return null;
80
+ }
81
+ const rawCheckpoint =
82
+ metadata.checkpoint && typeof metadata.checkpoint === 'object' && !Array.isArray(metadata.checkpoint)
83
+ ? (metadata.checkpoint as Record<string, unknown>)
84
+ : null;
85
+ if (!rawCheckpoint) {
86
+ return null;
87
+ }
88
+ const id = readOptionalString(rawCheckpoint.id);
89
+ const status = rawCheckpoint.status === 'compressing' ? 'compressing' : rawCheckpoint.status === 'compressed' ? 'compressed' : null;
90
+ const summary = readOptionalString(rawCheckpoint.summary);
91
+ const coveredMessageCount = readNonNegativeInteger(rawCheckpoint.coveredMessageCount);
92
+ const coveredSessionMessageCount = readNonNegativeInteger(rawCheckpoint.coveredSessionMessageCount);
93
+ const originalEstimatedTokens = readNonNegativeInteger(rawCheckpoint.originalEstimatedTokens);
94
+ const projectedEstimatedTokens = readNonNegativeInteger(rawCheckpoint.projectedEstimatedTokens);
95
+ const createdAt = readOptionalString(rawCheckpoint.createdAt);
96
+ const updatedAt = readOptionalString(rawCheckpoint.updatedAt);
97
+ if (
98
+ !id ||
99
+ !status ||
100
+ !summary ||
101
+ coveredMessageCount === null ||
102
+ coveredSessionMessageCount === null ||
103
+ originalEstimatedTokens === null ||
104
+ projectedEstimatedTokens === null ||
105
+ !createdAt ||
106
+ !updatedAt
107
+ ) {
108
+ return null;
109
+ }
110
+ return {
111
+ id,
112
+ status,
113
+ summary,
114
+ coveredMessageCount,
115
+ coveredSessionMessageCount,
116
+ originalEstimatedTokens,
117
+ projectedEstimatedTokens,
118
+ createdAt,
119
+ updatedAt,
120
+ };
121
+ }
@@ -2,7 +2,7 @@ import type { SessionEntryView, SessionTypeIconView } from '@/shared/lib/api';
2
2
  import { t } from '@/shared/lib/i18n';
3
3
  import { getChannelLogo } from '@/shared/lib/logos';
4
4
 
5
- type SessionContextSymbolIcon = 'heartbeat' | 'cron';
5
+ type SessionContextSymbolIcon = 'cron';
6
6
 
7
7
  export type SessionContextIcon =
8
8
  | { kind: 'channel-logo'; channel: string }
@@ -20,7 +20,6 @@ const CHANNEL_ALIAS_REGISTRY: Record<string, string> = {
20
20
  };
21
21
 
22
22
  const TYPE_CONTEXT_REGISTRY: Record<string, { icon: SessionContextSymbolIcon }> = {
23
- heartbeat: { icon: 'heartbeat' },
24
23
  cron: { icon: 'cron' },
25
24
  };
26
25
 
@@ -35,6 +35,7 @@ export function RuntimeConfigEditor(props: {
35
35
  const [bindings, setBindings] = useState(initialState.bindings);
36
36
  const [runtimeEntries, setRuntimeEntries] = useState(initialState.runtimeEntries);
37
37
  const [dmScope, setDmScope] = useState<DmScope>(initialState.dmScope);
38
+ const [companionEnabled, setCompanionEnabled] = useState(initialState.companionEnabled);
38
39
  const [defaultContextTokens, setDefaultContextTokens] = useState(initialState.defaultContextTokens);
39
40
  const [defaultEngine, setDefaultEngine] = useState(initialState.defaultEngine);
40
41
 
@@ -60,6 +61,7 @@ export function RuntimeConfigEditor(props: {
60
61
  const handleSave = () => {
61
62
  try {
62
63
  const data = createRuntimeConfigUpdatePayload({
64
+ companionEnabled,
63
65
  agents,
64
66
  bindings,
65
67
  runtimeEntries,
@@ -78,12 +80,16 @@ export function RuntimeConfigEditor(props: {
78
80
  <PageLayout className="space-y-6">
79
81
  <RuntimeConfigOverview />
80
82
  <RuntimeSettingsCard
83
+ companionEnabled={companionEnabled}
81
84
  dmScope={dmScope}
82
85
  defaultContextTokens={defaultContextTokens}
83
86
  defaultEngine={defaultEngine}
87
+ onCompanionEnabledChange={setCompanionEnabled}
84
88
  onDmScopeChange={setDmScope}
85
89
  onDefaultContextTokensChange={setDefaultContextTokens}
86
90
  onDefaultEngineChange={setDefaultEngine}
91
+ companionEnabledLabel={hintForPath('companion.enabled', props.uiHints)?.label}
92
+ companionEnabledHelp={hintForPath('companion.enabled', props.uiHints)?.help}
87
93
  dmScopeLabel={hintForPath('session.dmScope', props.uiHints)?.label}
88
94
  dmScopeHelp={hintForPath('session.dmScope', props.uiHints)?.help}
89
95
  defaultContextTokensLabel={hintForPath('agents.defaults.contextTokens', props.uiHints)?.label}
@@ -1,6 +1,7 @@
1
1
  import { Input } from '@/shared/components/ui/input';
2
2
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
3
3
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
4
+ import { Switch } from '@/shared/components/ui/switch';
4
5
  import { t } from '@/shared/lib/i18n';
5
6
  import type { DmScope } from '@/features/system-status/utils/runtime-config-agent.utils';
6
7
 
@@ -13,11 +14,15 @@ const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
13
14
 
14
15
  export function RuntimeSettingsCard(props: {
15
16
  dmScope: DmScope;
17
+ companionEnabled: boolean;
16
18
  defaultContextTokens: number;
17
19
  defaultEngine: string;
20
+ onCompanionEnabledChange: (value: boolean) => void;
18
21
  onDmScopeChange: (value: DmScope) => void;
19
22
  onDefaultContextTokensChange: (value: number) => void;
20
23
  onDefaultEngineChange: (value: string) => void;
24
+ companionEnabledLabel?: string;
25
+ companionEnabledHelp?: string;
21
26
  dmScopeLabel?: string;
22
27
  dmScopeHelp?: string;
23
28
  defaultContextTokensLabel?: string;
@@ -32,6 +37,13 @@ export function RuntimeSettingsCard(props: {
32
37
  <CardDescription>{props.dmScopeHelp ?? t('dmScopeHelp')}</CardDescription>
33
38
  </CardHeader>
34
39
  <CardContent className="space-y-4">
40
+ <div className="flex items-start justify-between gap-4 rounded-md border border-gray-200 px-4 py-3">
41
+ <div className="space-y-1">
42
+ <div className="text-sm font-medium text-gray-800">{props.companionEnabledLabel ?? t('runtimeCompanionEnabled')}</div>
43
+ <p className="text-xs text-gray-500">{props.companionEnabledHelp ?? t('runtimeCompanionEnabledHelp')}</p>
44
+ </div>
45
+ <Switch checked={props.companionEnabled} onCheckedChange={props.onCompanionEnabledChange} />
46
+ </div>
35
47
  <div className="space-y-2">
36
48
  <label className="text-sm font-medium text-gray-800">{props.defaultContextTokensLabel ?? t('defaultContextTokens')}</label>
37
49
  <Input
@@ -1,9 +1,9 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { useRuntimeUpdateStore } from '@/features/system-status';
4
5
  import { DesktopUpdateConfig } from '@/features/system-status/components/desktop-update-config';
5
6
  import { setLanguage } from '@/shared/lib/i18n';
6
- import { useDesktopUpdateStore } from '@/platforms/desktop';
7
7
 
8
8
  const mocks = vi.hoisted(() => ({
9
9
  start: vi.fn(),
@@ -15,13 +15,13 @@ const mocks = vi.hoisted(() => ({
15
15
  updateChannel: vi.fn()
16
16
  }));
17
17
 
18
- vi.mock('@/platforms/desktop', async () => {
19
- const actual = await vi.importActual<typeof import('@/platforms/desktop')>(
20
- '@/platforms/desktop'
18
+ vi.mock('@/features/system-status', async () => {
19
+ const actual = await vi.importActual<typeof import('@/features/system-status')>(
20
+ '@/features/system-status'
21
21
  );
22
22
  return {
23
23
  ...actual,
24
- desktopUpdateManager: mocks,
24
+ runtimeUpdateManager: mocks,
25
25
  };
26
26
  });
27
27
 
@@ -46,19 +46,27 @@ describe('DesktopUpdateConfig', () => {
46
46
  HTMLElement.prototype.releasePointerCapture = () => {};
47
47
  }
48
48
 
49
- useDesktopUpdateStore.setState({
49
+ useRuntimeUpdateStore.setState({
50
50
  supported: true,
51
51
  initialized: true,
52
52
  busyAction: null,
53
53
  snapshot: {
54
54
  status: 'idle',
55
+ installationKind: 'npm-runtime-bundle',
55
56
  channel: 'beta',
56
- launcherVersion: '0.0.138',
57
+ hostVersion: '0.0.138',
57
58
  currentVersion: '0.18.0',
58
59
  availableVersion: '0.18.2-beta.1',
59
60
  downloadedVersion: null,
61
+ minimumHostVersion: null,
60
62
  releaseNotesUrl: 'https://example.com/release-notes',
61
63
  lastCheckedAt: '2026-04-13T12:00:00.000Z',
64
+ progress: null,
65
+ canAutoDownload: false,
66
+ canApplyInApp: false,
67
+ requiresRestart: false,
68
+ blockReason: null,
69
+ recoveryCommand: null,
62
70
  errorMessage: null,
63
71
  preferences: {
64
72
  automaticChecks: true,
@@ -72,6 +80,8 @@ describe('DesktopUpdateConfig', () => {
72
80
  render(<DesktopUpdateConfig />);
73
81
 
74
82
  expect(mocks.start).toHaveBeenCalledTimes(1);
83
+ expect(screen.getByText('版本更新')).toBeTruthy();
84
+ expect(screen.getByText('宿主版本')).toBeTruthy();
75
85
  expect(screen.getByText('当前更新通道')).toBeTruthy();
76
86
  expect(screen.getAllByText('Beta').length).toBeGreaterThan(0);
77
87
  expect(screen.getByText('当前正在跟随 Beta 通道')).toBeTruthy();
@@ -1,4 +1,6 @@
1
+ import type { UpdateSnapshot } from '@nextclaw/kernel';
1
2
  import { useEffect } from 'react';
3
+ import { runtimeUpdateManager, useRuntimeUpdateStore } from '@/features/system-status';
2
4
  import { Button } from '@/shared/components/ui/button';
3
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
4
6
  import { Label } from '@/shared/components/ui/label';
@@ -7,11 +9,6 @@ import { Switch } from '@/shared/components/ui/switch';
7
9
  import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
8
10
  import { formatDateTime, t } from '@/shared/lib/i18n';
9
11
  import { cn } from '@/shared/lib/utils';
10
- import {
11
- desktopUpdateManager,
12
- type DesktopReleaseChannel,
13
- useDesktopUpdateStore,
14
- } from '@/platforms/desktop';
15
12
  import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
16
13
 
17
14
  const STATUS_LABEL_KEYS: Record<string, string> = {
@@ -20,6 +17,7 @@ const STATUS_LABEL_KEYS: Record<string, string> = {
20
17
  downloading: 'desktopUpdatesStatusDownloading',
21
18
  downloaded: 'desktopUpdatesStatusDownloaded',
22
19
  'up-to-date': 'desktopUpdatesStatusUpToDate',
20
+ blocked: 'desktopUpdatesStatusBlocked',
23
21
  failed: 'desktopUpdatesStatusFailed',
24
22
  };
25
23
 
@@ -31,6 +29,28 @@ function OverviewStat({ label, value }: { label: string; value: string }) {
31
29
  return <div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4"><p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{label}</p><p className="mt-2 text-base font-semibold text-gray-900">{value}</p></div>;
32
30
  }
33
31
 
32
+ function DownloadProgress({ snapshot }: { snapshot: UpdateSnapshot }) {
33
+ if (snapshot.status !== 'downloading') {
34
+ return null;
35
+ }
36
+ const percent = snapshot.progress?.percent;
37
+ const progressLabel = percent === null || percent === undefined
38
+ ? t('desktopUpdatesDownloadProgressUnknown')
39
+ : t('desktopUpdatesDownloadProgressPercent').replace('{percent}', String(percent));
40
+ const byteLabel = formatDownloadBytes(snapshot.progress?.downloadedBytes ?? 0, snapshot.progress?.totalBytes ?? null);
41
+ return (
42
+ <div className="rounded-2xl border border-amber-200 bg-amber-50/70 p-4">
43
+ <div className="flex items-center justify-between gap-4">
44
+ <p className="text-sm font-semibold text-amber-800">{progressLabel}</p>
45
+ <p className="text-xs font-medium text-amber-700">{byteLabel}</p>
46
+ </div>
47
+ <div className="mt-3 h-2 overflow-hidden rounded-full bg-amber-100">
48
+ <div className="h-full rounded-full bg-amber-500 transition-[width]" style={{ width: `${percent ?? 0}%` }} />
49
+ </div>
50
+ </div>
51
+ );
52
+ }
53
+
34
54
  function PreferenceToggle({
35
55
  label,
36
56
  help,
@@ -61,7 +81,24 @@ function formatVersion(value: string | null): string {
61
81
  function formatLastCheckedAt(value: string | null): string {
62
82
  return value ? formatDateTime(value) : '-';
63
83
  }
64
- function getChannelLabel(channel: DesktopReleaseChannel): string {
84
+ function formatDownloadBytes(downloadedBytes: number, totalBytes: number | null): string {
85
+ const downloaded = formatBytes(downloadedBytes);
86
+ return totalBytes && totalBytes > 0 ? `${downloaded} / ${formatBytes(totalBytes)}` : downloaded;
87
+ }
88
+ function formatBytes(value: number): string {
89
+ if (!Number.isFinite(value) || value <= 0) {
90
+ return '0 B';
91
+ }
92
+ const units = ['B', 'KB', 'MB', 'GB'];
93
+ let cursor = value;
94
+ let unitIndex = 0;
95
+ while (cursor >= 1024 && unitIndex < units.length - 1) {
96
+ cursor /= 1024;
97
+ unitIndex += 1;
98
+ }
99
+ return `${cursor >= 10 || unitIndex === 0 ? cursor.toFixed(0) : cursor.toFixed(1)} ${units[unitIndex]}`;
100
+ }
101
+ function getChannelLabel(channel: UpdateSnapshot['channel']): string {
65
102
  return channel === 'beta' ? t('desktopUpdatesChannelBeta') : t('desktopUpdatesChannelStable');
66
103
  }
67
104
  function getStatusLabel(status: string): string {
@@ -74,40 +111,40 @@ function getStatusTone(status: string): string {
74
111
  if (status === 'update-available' || status === 'downloading' || status === 'checking') {
75
112
  return 'bg-amber-50 text-amber-700 ring-amber-100';
76
113
  }
77
- if (status === 'failed') {
114
+ if (status === 'failed' || status === 'blocked') {
78
115
  return 'bg-red-50 text-red-700 ring-red-100';
79
116
  }
80
117
  return 'bg-gray-100 text-gray-700 ring-gray-200';
81
118
  }
82
119
 
83
- function DesktopOnlyState() {
120
+ function RuntimeUpdateUnavailableState() {
84
121
  return (
85
122
  <PageLayout className="space-y-6">
86
- <PageHeader title={t('desktopUpdatesPageTitle')} description={t('desktopUpdatesPageDescription')} />
123
+ <PageHeader title={t('runtimeUpdatesPageTitle')} description={t('runtimeUpdatesPageDescription')} />
87
124
  <Card>
88
125
  <CardHeader>
89
- <CardTitle>{t('desktopUpdatesDesktopOnlyTitle')}</CardTitle>
90
- <CardDescription>{t('desktopUpdatesDesktopOnlyDescription')}</CardDescription>
126
+ <CardTitle>{t('runtimeUpdatesUnavailableTitle')}</CardTitle>
127
+ <CardDescription>{t('runtimeUpdatesUnavailableDescription')}</CardDescription>
91
128
  </CardHeader>
92
- <CardContent><p className="text-sm text-gray-500">{t('desktopUpdatesDesktopOnlyFutureHint')}</p></CardContent>
129
+ <CardContent><p className="text-sm text-gray-500">{t('runtimeUpdatesUnavailableHint')}</p></CardContent>
93
130
  </Card>
94
131
  </PageLayout>
95
132
  );
96
133
  }
97
134
 
98
135
  export function DesktopUpdateConfig() {
99
- const { supported, initialized, busyAction, snapshot } = useDesktopUpdateStore();
136
+ const { supported, initialized, busyAction, snapshot } = useRuntimeUpdateStore();
100
137
  useEffect(() => {
101
- void desktopUpdateManager.start();
138
+ void runtimeUpdateManager.start();
102
139
  return () => {
103
- desktopUpdateManager.stop();
140
+ runtimeUpdateManager.stop();
104
141
  };
105
142
  }, []);
106
143
  if (!initialized) {
107
144
  return <div className="p-8 text-gray-400">{t('loading')}</div>;
108
145
  }
109
146
  if (!supported || !snapshot) {
110
- return <DesktopOnlyState />;
147
+ return <RuntimeUpdateUnavailableState />;
111
148
  }
112
149
  const isChecking = busyAction === 'checking';
113
150
  const isDownloading = busyAction === 'downloading';
@@ -117,7 +154,7 @@ export function DesktopUpdateConfig() {
117
154
  const canDownload = snapshot.status === 'update-available' && !isDownloading && !isApplying;
118
155
  const canApply = snapshot.status === 'downloaded' && !isApplying;
119
156
  const overviewStats = [
120
- [t('desktopUpdatesLauncherVersion'), formatVersion(snapshot.launcherVersion)],
157
+ [t('runtimeUpdatesHostVersion'), formatVersion(snapshot.hostVersion)],
121
158
  [t('desktopUpdatesCurrentBundleVersion'), formatVersion(snapshot.currentVersion)],
122
159
  [t('desktopUpdatesAvailableVersion'), formatVersion(snapshot.availableVersion)],
123
160
  [t('desktopUpdatesLastCheckedAt'), formatLastCheckedAt(snapshot.lastCheckedAt)],
@@ -126,9 +163,9 @@ export function DesktopUpdateConfig() {
126
163
  return (
127
164
  <PageLayout className="space-y-6">
128
165
  <PageHeader
129
- title={t('desktopUpdatesPageTitle')}
130
- description={t('desktopUpdatesPageDescription')}
131
- actions={<Button variant="outline" onClick={() => void desktopUpdateManager.checkForUpdates()} disabled={isChecking || isDownloading || isApplying}><RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />{t('desktopUpdatesCheckNow')}</Button>}
166
+ title={t('runtimeUpdatesPageTitle')}
167
+ description={t('runtimeUpdatesPageDescription')}
168
+ actions={<Button variant="outline" onClick={() => void runtimeUpdateManager.checkForUpdates()} disabled={isChecking || isDownloading || isApplying}><RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />{t('desktopUpdatesCheckNow')}</Button>}
132
169
  />
133
170
  <Card>
134
171
  <CardHeader>
@@ -147,10 +184,18 @@ export function DesktopUpdateConfig() {
147
184
  {snapshot.downloadedVersion ? (
148
185
  <div className="rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4">
149
186
  <p className="text-sm font-semibold text-emerald-800">{t('desktopUpdatesDownloadedBannerTitle')}</p>
150
- <p className="mt-1 text-sm text-emerald-700">{t('desktopUpdatesDownloadedBannerDescription').replace('{version}', snapshot.downloadedVersion)}</p>
187
+ <p className="mt-1 text-sm text-emerald-700">{t('runtimeUpdatesDownloadedBannerDescription').replace('{version}', snapshot.downloadedVersion)}</p>
188
+ </div>
189
+ ) : null}
190
+ <DownloadProgress snapshot={snapshot} />
191
+ {snapshot.status === 'blocked' ? (
192
+ <div className="rounded-2xl border border-red-200 bg-red-50/70 p-4">
193
+ <p className="text-sm font-semibold text-red-800">{t('desktopUpdatesBlockedTitle')}</p>
194
+ <p className="mt-1 text-sm text-red-700">{snapshot.errorMessage ?? t('desktopUpdatesBlockedDescription')}</p>
195
+ {snapshot.recoveryCommand ? <code className="mt-3 block rounded-lg bg-white/70 px-3 py-2 text-xs text-red-800">{snapshot.recoveryCommand}</code> : null}
151
196
  </div>
152
197
  ) : null}
153
- {snapshot.errorMessage ? <div className="rounded-2xl border border-red-200 bg-red-50/70 p-4 text-sm text-red-700">{snapshot.errorMessage}</div> : null}
198
+ {snapshot.errorMessage && snapshot.status !== 'blocked' ? <div className="rounded-2xl border border-red-200 bg-red-50/70 p-4 text-sm text-red-700">{snapshot.errorMessage}</div> : null}
154
199
  </CardContent>
155
200
  </Card>
156
201
  <Card>
@@ -165,7 +210,7 @@ export function DesktopUpdateConfig() {
165
210
  <Label>{t('desktopUpdatesReleaseChannel')}</Label>
166
211
  <p className="text-sm text-gray-500">{t('desktopUpdatesReleaseChannelHelp')}</p>
167
212
  </div>
168
- <Select value={snapshot.channel} disabled={isSwitchingChannel || isChecking || isDownloading || isApplying} onValueChange={(value) => void desktopUpdateManager.updateChannel(value as DesktopReleaseChannel)}>
213
+ <Select value={snapshot.channel} disabled={isSwitchingChannel || isChecking || isDownloading || isApplying} onValueChange={(value) => void runtimeUpdateManager.updateChannel(value as UpdateSnapshot['channel'])}>
169
214
  <SelectTrigger className="w-full max-w-sm">
170
215
  <SelectValue placeholder={t('desktopUpdatesReleaseChannel')} />
171
216
  </SelectTrigger>
@@ -182,34 +227,34 @@ export function DesktopUpdateConfig() {
182
227
  help={t('desktopUpdatesAutomaticChecksHelp')}
183
228
  checked={snapshot.preferences.automaticChecks}
184
229
  disabled={isSavingPreferences || isSwitchingChannel}
185
- onCheckedChange={(checked) => void desktopUpdateManager.updatePreferences({ automaticChecks: checked })}
230
+ onCheckedChange={(checked) => void runtimeUpdateManager.updatePreferences({ automaticChecks: checked })}
186
231
  />
187
232
  <PreferenceToggle
188
233
  label={t('desktopUpdatesAutoDownload')}
189
234
  help={t('desktopUpdatesAutoDownloadHelp')}
190
235
  checked={snapshot.preferences.autoDownload}
191
236
  disabled={isSavingPreferences || isSwitchingChannel}
192
- onCheckedChange={(checked) => void desktopUpdateManager.updatePreferences({ autoDownload: checked })}
237
+ onCheckedChange={(checked) => void runtimeUpdateManager.updatePreferences({ autoDownload: checked })}
193
238
  />
194
239
  </CardContent>
195
240
  </Card>
196
241
  <Card>
197
242
  <CardHeader>
198
243
  <CardTitle>{t('desktopUpdatesActionsTitle')}</CardTitle>
199
- <CardDescription>{t('desktopUpdatesActionsDescription')}</CardDescription>
244
+ <CardDescription>{t('runtimeUpdatesActionsDescription')}</CardDescription>
200
245
  </CardHeader>
201
246
  <CardContent className="flex flex-wrap items-center gap-3">
202
- <Button variant="outline" onClick={() => void desktopUpdateManager.checkForUpdates()} disabled={isChecking || isDownloading || isApplying}>
247
+ <Button variant="outline" onClick={() => void runtimeUpdateManager.checkForUpdates()} disabled={isChecking || isDownloading || isApplying}>
203
248
  <RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />
204
249
  {t('desktopUpdatesCheckNow')}
205
250
  </Button>
206
- <Button onClick={() => void desktopUpdateManager.downloadUpdate()} disabled={!canDownload}>
251
+ <Button onClick={() => void runtimeUpdateManager.downloadUpdate()} disabled={!canDownload}>
207
252
  <Download className={cn('mr-2 h-4 w-4', isDownloading && 'animate-bounce')} />
208
253
  {t('desktopUpdatesDownloadNow')}
209
254
  </Button>
210
- <Button variant="secondary" onClick={() => void desktopUpdateManager.applyDownloadedUpdate()} disabled={!canApply}>
255
+ <Button variant="secondary" onClick={() => void runtimeUpdateManager.applyDownloadedUpdate()} disabled={!canApply}>
211
256
  <RotateCw className={cn('mr-2 h-4 w-4', isApplying && 'animate-spin')} />
212
- {t('desktopUpdatesApplyNow')}
257
+ {t('runtimeUpdatesApplyNow')}
213
258
  </Button>
214
259
  {snapshot.releaseNotesUrl ? <Button variant="ghost" onClick={() => window.open(snapshot.releaseNotesUrl ?? '', '_blank', 'noopener,noreferrer')}><ExternalLink className="mr-2 h-4 w-4" />{t('desktopUpdatesReleaseNotes')}</Button> : null}
215
260
  </CardContent>
@@ -82,17 +82,6 @@ export function useSystemStatus() {
82
82
  return toSystemStatusView(state);
83
83
  }
84
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
85
  export function useRuntimeStatusBadgeView() {
97
86
  const state = useSystemStatusStore((store) => store.state);
98
87
  return toRuntimeStatusBadgeView(state);
@@ -1,4 +1,7 @@
1
- export { useChatRuntimeAvailability, useRuntimeControlPanelView, useRuntimeStatusBadgeView, useSystemStatus, useSystemStatusSources } from './hooks/use-system-status';
1
+ export { useRuntimeControlPanelView, useRuntimeStatusBadgeView, useSystemStatus, useSystemStatusSources } from './hooks/use-system-status';
2
2
  export { isTransientRuntimeConnectionErrorMessage, systemStatusManager } from './managers/system-status.manager';
3
+ export { runtimeUpdateManager } from './managers/runtime-update.manager';
4
+ export type { SystemStatusState, SystemStatusView } from './types/system-status.types';
3
5
  export { useSystemStatusStore } from './stores/system-status.store';
6
+ export { useRuntimeUpdateStore } from './stores/runtime-update.store';
4
7
  export { SecurityConfig } from './components/security-config';