@nextclaw/ui 0.12.18 → 0.12.20-beta.0

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 (152) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/assets/api-C412zuay.js +15 -0
  3. package/dist/assets/app-manager-provider-Cm-KiZZG.js +1 -0
  4. package/dist/assets/app-navigation.config-BORqHkbN.js +1 -0
  5. package/dist/assets/{book-open-CUd69I2f.js → book-open-DgLqYpNY.js} +1 -1
  6. package/dist/assets/{channels-list-page-5wQy-UW7.js → channels-list-page-sISO_4Yj.js} +2 -2
  7. package/dist/assets/{chat-C7Ywus_K.js → chat-ChCu7LQD.js} +13 -12
  8. package/dist/assets/chat-page-BCaNZJGT.js +1 -0
  9. package/dist/assets/{chunk-JZWAC4HX-THqEFwu9.js → chunk-JZWAC4HX-DvbcIVPf.js} +1 -1
  10. package/dist/assets/{config-split-page-UJSTBsEU.js → config-split-page-BMRGuCJQ.js} +1 -1
  11. package/dist/assets/{createLucideIcon-DVAlgDOi.js → createLucideIcon-BZkY6emz.js} +1 -1
  12. package/dist/assets/desktop-update-config-BfJ5iSeY.js +1 -0
  13. package/dist/assets/{dialog-t7OAmObC.js → dialog-B-CXiFPZ.js} +1 -1
  14. package/dist/assets/{dist-DWPNydLC.js → dist-DYVfg3q5.js} +1 -1
  15. package/dist/assets/{doc-browser-DznuT-CU.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-zXaTjrpA.js → doc-browser-context-DfLHAWbG.js} +1 -1
  19. package/dist/assets/{es2015-BRVsmfFO.js → es2015-BXroVnPi.js} +1 -1
  20. package/dist/assets/{external-link-D1Xqff6i.js → external-link-Sw3ah_JD.js} +1 -1
  21. package/dist/assets/{folder-B_fuaX3x.js → folder-D7-VTnkz.js} +1 -1
  22. package/dist/assets/{hash-BRvv_UUq.js → hash-zajSTDXZ.js} +1 -1
  23. package/dist/assets/i18n-C5Mibli1.js +1 -0
  24. package/dist/assets/index-CUmk8xFK.css +1 -0
  25. package/dist/assets/index-CqPDhosM.js +2 -0
  26. package/dist/assets/{key-round-DFVNXZcD.js → key-round-CnI1mc9F.js} +1 -1
  27. package/dist/assets/loader-circle-B5i8oMMY.js +1 -0
  28. package/dist/assets/{logo-badge-CRVKkIl9.js → logo-badge-BQgKnVtz.js} +1 -1
  29. package/dist/assets/{logos-BVCi_7_I.js → logos-CqVm0q0W.js} +1 -1
  30. package/dist/assets/marketplace-page-C8uaWkfd.js +1 -0
  31. package/dist/assets/{marketplace-page-apq5LpYx.js → marketplace-page-C9oZ01rM.js} +2 -2
  32. package/dist/assets/mcp-marketplace-page-DuEixgSs.js +40 -0
  33. package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +1 -0
  34. package/dist/assets/message-square-D6Z4NwpG.js +1 -0
  35. package/dist/assets/{model-config-cth12uRn.js → model-config-mfhqEZBG.js} +1 -1
  36. package/dist/assets/{notice-card-CXY09tsa.js → notice-card-CozHB03G.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-CbQxrchk.js → popover-CPUPma-w.js} +1 -1
  40. package/dist/assets/{provider-scoped-model-input-CZEB2m98.js → provider-scoped-model-input-CL9sti2I.js} +1 -1
  41. package/dist/assets/{providers-list-Dsj2BYPm.js → providers-list-HPmL2akJ.js} +1 -1
  42. package/dist/assets/{refresh-ccw-C-ytTHiq.js → refresh-ccw-Bii4w8aB.js} +1 -1
  43. package/dist/assets/refresh-cw-BxojR62w.js +1 -0
  44. package/dist/assets/remote-oDlAdgVA.js +1 -0
  45. package/dist/assets/{rotate-cw-ClSrRUa0.js → rotate-cw-1Xqa7LZ8.js} +1 -1
  46. package/dist/assets/runtime-config-page-BCshTAAE.js +1 -0
  47. package/dist/assets/{save-KxhpE3Zr.js → save--BVI5wZX.js} +1 -1
  48. package/dist/assets/search-config-Bcnk9VlL.js +1 -0
  49. package/dist/assets/{search-Bz3Q64sr.js → search-vChioOoe.js} +1 -1
  50. package/dist/assets/{secrets-config-BOL024Fj.js → secrets-config-Dde-5Y1w.js} +2 -2
  51. package/dist/assets/{select-tRTLG4FK.js → select-BELPuXLW.js} +1 -1
  52. package/dist/assets/{sessions-config-page-Ck6nbIFq.js → sessions-config-page-CG49_0Z6.js} +2 -2
  53. package/dist/assets/{setting-row-DMDgBCC7.js → setting-row-D5DtT6Ny.js} +1 -1
  54. package/dist/assets/{settings-Cto6z-Ij.js → settings-CiRChctQ.js} +1 -1
  55. package/dist/assets/skeleton-CFQRIUzt.js +1 -0
  56. package/dist/assets/{sparkles-xZ74eW0P.js → sparkles-D1ZKWdm4.js} +1 -1
  57. package/dist/assets/{status-dot-Bobpfutv.js → status-dot-Dv_hiUVa.js} +1 -1
  58. package/dist/assets/{tabs-custom-C3Mf-NLb.js → tabs-custom-CsACkVji.js} +1 -1
  59. package/dist/assets/{tag-chip-BZ14i5b1.js → tag-chip-D9BWWgYg.js} +1 -1
  60. package/dist/assets/theme-provider-DeBrTglS.js +1 -0
  61. package/dist/assets/{tooltip-7lsLGcL9.js → tooltip-CI0rpNee.js} +1 -1
  62. package/dist/assets/{trash-2-WFSNa5oj.js → trash-2-rY9ZteZX.js} +1 -1
  63. package/dist/assets/use-config-CrWZ_TSF.js +1 -0
  64. package/dist/assets/{use-confirm-dialog-Df1SozKw.js → use-confirm-dialog-hbynwWf2.js} +1 -1
  65. package/dist/assets/{use-infinite-scroll-loader-BogRuzgz.js → use-infinite-scroll-loader-Cw5qQr3-.js} +1 -1
  66. package/dist/assets/{use-viewport-layout-CWlW5b-T.js → use-viewport-layout-CWHVDC6z.js} +1 -1
  67. package/dist/assets/x-DpTzXQcX.js +1 -0
  68. package/dist/index.html +40 -39
  69. package/package.json +7 -6
  70. package/src/app/index.tsx +7 -1
  71. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +1 -1
  72. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -0
  73. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +9 -4
  74. package/src/features/chat/components/conversation/chat-message-list.container.test.tsx +64 -6
  75. package/src/features/chat/components/conversation/chat-message-list.container.tsx +185 -17
  76. package/src/features/chat/components/session/session-context-icon.tsx +1 -4
  77. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +3 -1
  78. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +74 -2
  79. package/src/features/chat/hooks/use-ncp-session-conversation.ts +32 -10
  80. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +20 -0
  81. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +25 -0
  82. package/src/features/chat/managers/ncp-chat-input.manager.ts +5 -1
  83. package/src/features/chat/pages/ncp-chat-page.tsx +15 -11
  84. package/src/features/chat/stores/chat-thread.store.ts +8 -2
  85. package/src/features/chat/utils/chat-context-window-indicator.utils.ts +50 -0
  86. package/src/features/chat/utils/chat-runtime.utils.ts +1 -1
  87. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +165 -0
  88. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.ts +50 -0
  89. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +27 -0
  90. package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -4
  91. package/src/features/chat/utils/ncp-session-context-metadata.utils.ts +121 -0
  92. package/src/features/chat/utils/session-context.utils.ts +1 -2
  93. package/src/features/system-status/components/config/runtime-config-editor.tsx +6 -0
  94. package/src/features/system-status/components/config/runtime-settings-card.tsx +12 -0
  95. package/src/features/system-status/components/desktop-update-config.test.tsx +17 -7
  96. package/src/features/system-status/components/desktop-update-config.tsx +75 -30
  97. package/src/features/system-status/hooks/use-system-status.ts +0 -11
  98. package/src/features/system-status/index.ts +4 -1
  99. package/src/features/system-status/managers/runtime-update.manager.ts +330 -0
  100. package/src/features/system-status/managers/system-status.manager.test.ts +0 -25
  101. package/src/features/system-status/managers/system-status.manager.ts +1 -30
  102. package/src/features/system-status/stores/runtime-update.store.ts +24 -0
  103. package/src/features/system-status/types/system-status.types.ts +0 -2
  104. package/src/features/system-status/utils/runtime-config-agent.utils.ts +6 -1
  105. package/src/features/system-status/utils/system-status.utils.test.ts +1 -85
  106. package/src/features/system-status/utils/system-status.utils.ts +1 -23
  107. package/src/platforms/desktop/managers/desktop-update.manager.ts +6 -0
  108. package/src/platforms/desktop/types/desktop-update.types.ts +21 -19
  109. package/src/shared/components/common/brand-header.test.tsx +142 -0
  110. package/src/shared/components/common/brand-header.tsx +93 -0
  111. package/src/shared/components/cron-config.tsx +7 -1
  112. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +1 -1
  113. package/src/shared/components/doc-browser/doc-browser.tsx +1 -1
  114. package/src/shared/components/search-config.tsx +3 -3
  115. package/src/shared/lib/api/README.md +3 -0
  116. package/src/shared/lib/api/index.ts +2 -0
  117. package/src/shared/lib/api/ncp-attachments.ts +2 -2
  118. package/src/shared/lib/api/ncp-session.types.ts +92 -0
  119. package/src/shared/lib/api/runtime-update.service.ts +50 -0
  120. package/src/shared/lib/api/types.ts +14 -84
  121. package/src/shared/lib/i18n/{chat.ts → chat-labels.utils.ts} +13 -1
  122. package/src/shared/lib/i18n/desktop-update-labels.utils.ts +65 -0
  123. package/src/shared/lib/i18n/index.ts +5 -6
  124. package/src/shared/lib/i18n/runtime/i18n-language-owner.ts +5 -5
  125. package/src/shared/lib/transport/remote-transport.service.ts +1 -1
  126. package/src/shared/lib/ui-document-title/index.ts +1 -1
  127. package/tsconfig.json +1 -0
  128. package/dist/assets/api-BIg--UMJ.js +0 -15
  129. package/dist/assets/app-manager-provider-BfKiVYea.js +0 -1
  130. package/dist/assets/app-navigation.config-xIjCAn-R.js +0 -1
  131. package/dist/assets/chat-page-DX8OMxQ_.js +0 -1
  132. package/dist/assets/desktop-update-config-DdVgauFR.js +0 -1
  133. package/dist/assets/doc-browser-BIggpN8Z.js +0 -1
  134. package/dist/assets/doc-browser-t96ibd-b.js +0 -1
  135. package/dist/assets/i18n-CF_jgT_-.js +0 -1
  136. package/dist/assets/index-CLxN8vXZ.js +0 -2
  137. package/dist/assets/index-N3hjuljD.css +0 -1
  138. package/dist/assets/loader-circle-DEz3bHGb.js +0 -1
  139. package/dist/assets/marketplace-page-DVmk8dZk.js +0 -1
  140. package/dist/assets/mcp-marketplace-page-CskrJuKU.js +0 -1
  141. package/dist/assets/mcp-marketplace-page-DiqTAdRJ.js +0 -40
  142. package/dist/assets/message-square-CLhDWybk.js +0 -1
  143. package/dist/assets/play-CnnPm8ca.js +0 -1
  144. package/dist/assets/plus-CdYMdiws.js +0 -1
  145. package/dist/assets/remote-DMMC2PSo.js +0 -1
  146. package/dist/assets/runtime-config-page-y8HmA9qr.js +0 -1
  147. package/dist/assets/search-config-C_xRBv_i.js +0 -1
  148. package/dist/assets/skeleton-BwfJfVK3.js +0 -1
  149. package/dist/assets/theme-provider-D6Cgm6i-.js +0 -1
  150. package/dist/assets/use-config-CXu7dFzw.js +0 -1
  151. package/dist/assets/x-BvS2y4e_.js +0 -1
  152. /package/dist/assets/{config-hints-CPNzbMEp.js → config-hints-MogHYQ8G.js} +0 -0
@@ -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
  };
@@ -1,29 +1,31 @@
1
- export type DesktopUpdateStatus =
2
- | 'idle'
3
- | 'checking'
4
- | 'update-available'
5
- | 'downloading'
6
- | 'downloaded'
7
- | 'up-to-date'
8
- | 'failed';
1
+ import type {
2
+ InstallationKind,
3
+ UpdateBlockReason,
4
+ UpdatePreferences,
5
+ UpdateProgress,
6
+ UpdateSnapshot,
7
+ UpdateStatus,
8
+ } from '@nextclaw/kernel';
9
+
10
+ export type DesktopUpdateStatus = Extract<
11
+ UpdateStatus,
12
+ 'idle' | 'checking' | 'update-available' | 'downloading' | 'downloaded' | 'blocked' | 'up-to-date' | 'failed'
13
+ >;
9
14
 
10
15
  export type DesktopReleaseChannel = 'stable' | 'beta';
11
16
 
12
- export type DesktopUpdatePreferences = {
13
- automaticChecks: boolean;
14
- autoDownload: boolean;
15
- };
17
+ export type DesktopInstallationKind = InstallationKind;
18
+
19
+ export type DesktopUpdateBlockReason = UpdateBlockReason;
20
+
21
+ export type DesktopUpdateProgress = UpdateProgress;
22
+
23
+ export type DesktopUpdatePreferences = UpdatePreferences;
16
24
 
17
- export type DesktopUpdateSnapshot = {
25
+ export type DesktopUpdateSnapshot = UpdateSnapshot & {
18
26
  status: DesktopUpdateStatus;
19
27
  channel: DesktopReleaseChannel;
20
28
  launcherVersion: string;
21
- currentVersion: string | null;
22
- availableVersion: string | null;
23
- downloadedVersion: string | null;
24
- releaseNotesUrl: string | null;
25
- lastCheckedAt: string | null;
26
- errorMessage: string | null;
27
29
  preferences: DesktopUpdatePreferences;
28
30
  };
29
31
 
@@ -0,0 +1,142 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { MemoryRouter } from 'react-router-dom';
5
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { useRuntimeUpdateStore } from '@/features/system-status';
7
+ import { BrandHeader } from '@/shared/components/common/brand-header';
8
+ import { setLanguage } from '@/shared/lib/i18n';
9
+
10
+ const mocks = vi.hoisted(() => ({
11
+ applyDownloadedUpdate: vi.fn(),
12
+ downloadUpdate: vi.fn()
13
+ }));
14
+
15
+ vi.mock('@/features/system-status', async () => {
16
+ const actual = await vi.importActual<typeof import('@/features/system-status')>(
17
+ '@/features/system-status'
18
+ );
19
+ return {
20
+ ...actual,
21
+ runtimeUpdateManager: {
22
+ ...actual.runtimeUpdateManager,
23
+ applyDownloadedUpdate: mocks.applyDownloadedUpdate,
24
+ downloadUpdate: mocks.downloadUpdate
25
+ }
26
+ };
27
+ });
28
+
29
+ function renderBrandHeader() {
30
+ const queryClient = new QueryClient({
31
+ defaultOptions: {
32
+ queries: {
33
+ retry: false
34
+ }
35
+ }
36
+ });
37
+ queryClient.setQueryData(['app-meta'], {
38
+ name: 'NextClaw',
39
+ productVersion: '0.18.11'
40
+ });
41
+ return render(
42
+ <QueryClientProvider client={queryClient}>
43
+ <MemoryRouter>
44
+ <BrandHeader suffix={null} />
45
+ </MemoryRouter>
46
+ </QueryClientProvider>
47
+ );
48
+ }
49
+
50
+ describe('BrandHeader', () => {
51
+ beforeEach(() => {
52
+ setLanguage('zh');
53
+ useRuntimeUpdateStore.setState({
54
+ supported: false,
55
+ initialized: false,
56
+ busyAction: null,
57
+ snapshot: null
58
+ });
59
+ mocks.applyDownloadedUpdate.mockReset();
60
+ mocks.downloadUpdate.mockReset();
61
+ });
62
+
63
+ it('shows update progress next to the product version', () => {
64
+ useRuntimeUpdateStore.setState({
65
+ supported: true,
66
+ initialized: true,
67
+ busyAction: null,
68
+ snapshot: {
69
+ status: 'downloading',
70
+ installationKind: 'desktop-bundle',
71
+ channel: 'stable',
72
+ hostVersion: '0.0.138',
73
+ currentVersion: '0.18.11',
74
+ availableVersion: '0.18.12',
75
+ downloadedVersion: null,
76
+ minimumHostVersion: null,
77
+ releaseNotesUrl: null,
78
+ lastCheckedAt: null,
79
+ progress: {
80
+ downloadedBytes: 50,
81
+ totalBytes: 100,
82
+ percent: 50
83
+ },
84
+ canAutoDownload: true,
85
+ canApplyInApp: false,
86
+ requiresRestart: false,
87
+ blockReason: null,
88
+ recoveryCommand: null,
89
+ errorMessage: null,
90
+ preferences: {
91
+ automaticChecks: true,
92
+ autoDownload: true
93
+ }
94
+ }
95
+ });
96
+
97
+ renderBrandHeader();
98
+
99
+ expect(screen.getByText('v0.18.11')).toBeTruthy();
100
+ expect(screen.getByText('下载 50%')).toBeTruthy();
101
+ expect(screen.queryByRole('button', { name: '更新' })).toBeNull();
102
+ });
103
+
104
+ it('applies the downloaded update from the version-adjacent update button', async () => {
105
+ const user = userEvent.setup();
106
+ useRuntimeUpdateStore.setState({
107
+ supported: true,
108
+ initialized: true,
109
+ busyAction: null,
110
+ snapshot: {
111
+ status: 'downloaded',
112
+ installationKind: 'desktop-bundle',
113
+ channel: 'stable',
114
+ hostVersion: '0.0.138',
115
+ currentVersion: '0.18.11',
116
+ availableVersion: null,
117
+ downloadedVersion: '0.18.12',
118
+ minimumHostVersion: null,
119
+ releaseNotesUrl: null,
120
+ lastCheckedAt: null,
121
+ progress: null,
122
+ canAutoDownload: true,
123
+ canApplyInApp: true,
124
+ requiresRestart: false,
125
+ blockReason: null,
126
+ recoveryCommand: null,
127
+ errorMessage: null,
128
+ preferences: {
129
+ automaticChecks: true,
130
+ autoDownload: true
131
+ }
132
+ }
133
+ });
134
+
135
+ renderBrandHeader();
136
+
137
+ await user.click(screen.getByRole('button', { name: '更新' }));
138
+
139
+ expect(mocks.applyDownloadedUpdate).toHaveBeenCalledTimes(1);
140
+ expect(mocks.downloadUpdate).not.toHaveBeenCalled();
141
+ });
142
+ });
@@ -1,6 +1,10 @@
1
+ import type { UpdateSnapshot } from '@nextclaw/kernel';
2
+ import { runtimeUpdateManager, useRuntimeUpdateStore } from '@/features/system-status';
1
3
  import { useAppMeta } from '@/shared/hooks/use-config';
2
4
  import type { ReactNode } from 'react';
3
5
  import { RuntimeStatusEntry } from '@/app/components/layout/runtime-status-entry';
6
+ import { cn } from '@/shared/lib/utils';
7
+ import { t } from '@/shared/lib/i18n';
4
8
 
5
9
  type BrandHeaderProps = {
6
10
  className?: string;
@@ -21,8 +25,97 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
21
25
  <div className="flex items-baseline gap-2 min-w-0">
22
26
  <span className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
23
27
  {productVersion ? <span className="text-[13px] font-medium text-gray-500">v{productVersion}</span> : null}
28
+ <RuntimeUpdateInlineStatus />
24
29
  {resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
25
30
  </div>
26
31
  </div>
27
32
  );
28
33
  }
34
+
35
+ function RuntimeUpdateInlineStatus() {
36
+ const { supported, busyAction, snapshot } = useRuntimeUpdateStore();
37
+ if (!supported || !snapshot) {
38
+ return null;
39
+ }
40
+ if (snapshot.status === 'downloading' || snapshot.status === 'blocked' || snapshot.status === 'failed') {
41
+ return <RuntimeUpdateInlineBadge snapshot={snapshot} />;
42
+ }
43
+ if (snapshot.status === 'downloaded') {
44
+ return (
45
+ <button
46
+ type="button"
47
+ className={cn(
48
+ 'inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold leading-none ring-1 transition-colors',
49
+ resolveInlineUpdateTone(snapshot.status)
50
+ )}
51
+ disabled={busyAction === 'applying'}
52
+ onClick={() => void runtimeUpdateManager.applyDownloadedUpdate()}
53
+ >
54
+ {busyAction === 'applying' ? t('desktopUpdatesInlineApplying') : t('desktopUpdatesInlineReady')}
55
+ </button>
56
+ );
57
+ }
58
+ if (snapshot.status === 'update-available') {
59
+ return (
60
+ <button
61
+ type="button"
62
+ className={cn(
63
+ 'inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold leading-none ring-1 transition-colors',
64
+ resolveInlineUpdateTone(snapshot.status)
65
+ )}
66
+ disabled={busyAction === 'downloading'}
67
+ onClick={() => void runtimeUpdateManager.downloadUpdate()}
68
+ >
69
+ {busyAction === 'downloading' ? t('desktopUpdatesInlineDownloading') : t('desktopUpdatesInlineDownload')}
70
+ </button>
71
+ );
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function RuntimeUpdateInlineBadge({ snapshot }: { snapshot: UpdateSnapshot }) {
77
+ const label = resolveInlineUpdateLabel(snapshot);
78
+ if (!label) {
79
+ return null;
80
+ }
81
+ return (
82
+ <span
83
+ className={cn(
84
+ 'inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold leading-none ring-1 transition-colors',
85
+ resolveInlineUpdateTone(snapshot.status)
86
+ )}
87
+ title={t('updates')}
88
+ >
89
+ {label}
90
+ </span>
91
+ );
92
+ }
93
+
94
+ function resolveInlineUpdateLabel(snapshot: UpdateSnapshot): string | null {
95
+ if (snapshot.status === 'downloading') {
96
+ const percent = snapshot.progress?.percent;
97
+ return percent === null || percent === undefined
98
+ ? t('desktopUpdatesInlineDownloading')
99
+ : t('desktopUpdatesInlineDownloadingPercent').replace('{percent}', String(percent));
100
+ }
101
+ if (snapshot.status === 'downloaded') {
102
+ return t('desktopUpdatesInlineReady');
103
+ }
104
+ if (snapshot.status === 'update-available') {
105
+ return t('desktopUpdatesInlineDownload');
106
+ }
107
+ if (snapshot.status === 'blocked' || snapshot.status === 'failed') {
108
+ return t('desktopUpdatesInlineAttention');
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function resolveInlineUpdateTone(status: UpdateSnapshot['status']): string {
114
+ if (status === 'downloaded') {
115
+ return 'bg-emerald-50 text-emerald-700 ring-emerald-100 hover:bg-emerald-100 disabled:opacity-70';
116
+ }
117
+ if (status === 'blocked' || status === 'failed') {
118
+ return 'bg-red-50 text-red-700 ring-red-100';
119
+ }
120
+ return 'bg-amber-50 text-amber-700 ring-amber-100 hover:bg-amber-100 disabled:opacity-70';
121
+ }
@@ -39,7 +39,7 @@ function formatEveryDuration(ms?: number | null): string {
39
39
  }
40
40
 
41
41
  function describeSchedule(job: CronJobView): string {
42
- const schedule = job.schedule;
42
+ const { schedule } = job;
43
43
  if (schedule.kind === 'cron') {
44
44
  return schedule.expr ? `cron ${schedule.expr}` : 'cron';
45
45
  }
@@ -61,6 +61,10 @@ function describeDelivery(job: CronJobView): string {
61
61
  return `${channel}:${target}`;
62
62
  }
63
63
 
64
+ function describeSession(job: CronJobView): string {
65
+ return job.payload.sessionId?.trim() || `cron:${job.id}`;
66
+ }
67
+
64
68
  function matchQuery(job: CronJobView, query: string): boolean {
65
69
  const q = query.trim().toLowerCase();
66
70
  if (!q) return true;
@@ -68,6 +72,7 @@ function matchQuery(job: CronJobView, query: string): boolean {
68
72
  job.id,
69
73
  job.name,
70
74
  job.payload.message,
75
+ job.payload.sessionId ?? '',
71
76
  job.payload.channel ?? '',
72
77
  job.payload.to ?? ''
73
78
  ].join(' ').toLowerCase();
@@ -104,6 +109,7 @@ function CronJobCard(props: {
104
109
  </div>
105
110
  <div className="mt-2 text-xs text-gray-500">{t('cronScheduleLabel')}: {describeSchedule(job)}</div>
106
111
  <div className="mt-2 whitespace-pre-wrap break-words text-sm text-gray-700">{job.payload.message}</div>
112
+ <div className="mt-2 text-xs text-gray-500">{t('cronSessionLabel')}: {describeSession(job)}</div>
107
113
  <div className="mt-2 text-xs text-gray-500">{t('cronDeliverTo')}: {describeDelivery(job)}</div>
108
114
  </div>
109
115
  <div className="min-w-[220px] space-y-2 text-xs text-gray-500">
@@ -83,7 +83,7 @@ describe('DocBrowserProvider dedupe keys', () => {
83
83
  title: 'Skill B',
84
84
  });
85
85
  });
86
- const activeTabId = result.current.activeTabId;
86
+ const { activeTabId } = result.current;
87
87
 
88
88
  act(() => {
89
89
  result.current.open('data:text/html,A-loaded', {
@@ -203,7 +203,7 @@ export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
203
203
  e.preventDefault();
204
204
  e.stopPropagation();
205
205
  setIsResizing(true);
206
- const axis = (e.currentTarget as HTMLElement).dataset.axis;
206
+ const { axis } = (e.currentTarget as HTMLElement).dataset;
207
207
  resizeRef.current = {
208
208
  startX: e.clientX,
209
209
  startY: e.clientY,
@@ -179,7 +179,7 @@ function SearchProviderFields(props: {
179
179
  const { draft, provider, search, selectedDocsUrl, updateProviderDraft } = props;
180
180
 
181
181
  if (provider === "bocha") {
182
- const bocha = draft.providers.bocha;
182
+ const { bocha } = draft.providers;
183
183
  return (
184
184
  <>
185
185
  <SearchTextField
@@ -214,7 +214,7 @@ function SearchProviderFields(props: {
214
214
  }
215
215
 
216
216
  if (provider === "tavily") {
217
- const tavily = draft.providers.tavily;
217
+ const { tavily } = draft.providers;
218
218
  return (
219
219
  <>
220
220
  <SearchTextField
@@ -252,7 +252,7 @@ function SearchProviderFields(props: {
252
252
  );
253
253
  }
254
254
 
255
- const brave = draft.providers.brave;
255
+ const { brave } = draft.providers;
256
256
  return (
257
257
  <>
258
258
  <SearchTextField
@@ -0,0 +1,3 @@
1
+ ## 子树边界豁免
2
+
3
+ - 原因:`shared/lib/api` 是历史形成的前端 API facade,当前同时承载 client、endpoint wrapper、query cache、transport helper 与 response view types。本次新增 `ncp-session.types.ts` 是为了把会话类型从超大的 `types.ts` 中拆出,实际降低核心类型文件体积;完整子树化需要同步改造大量公共导出和调用方,适合单独结构治理批次推进。
@@ -10,11 +10,13 @@ export * from './marketplace';
10
10
  export * from './mcp-marketplace';
11
11
  export * from './ncp-attachments';
12
12
  export * from './ncp-session';
13
+ export * from './ncp-session.types';
13
14
  export * from './ncp-session-query-cache';
14
15
  export * from './raw-client.utils';
15
16
  export * from './remote';
16
17
  export * from './remote.types';
17
18
  export * from './runtime-control';
18
19
  export * from './runtime-control.types';
20
+ export * from './runtime-update.service';
19
21
  export * from './server-path';
20
22
  export * from './types';
@@ -6,11 +6,11 @@ function readErrorMessage(payload: unknown, fallback: string): string {
6
6
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
7
7
  return fallback;
8
8
  }
9
- const error = (payload as { error?: unknown }).error;
9
+ const { error } = (payload as { error?: unknown });
10
10
  if (!error || typeof error !== "object" || Array.isArray(error)) {
11
11
  return fallback;
12
12
  }
13
- const message = (error as { message?: unknown }).message;
13
+ const { message } = (error as { message?: unknown });
14
14
  return typeof message === "string" && message.trim().length > 0 ? message : fallback;
15
15
  }
16
16