@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
@@ -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
  }
@@ -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` 中拆出,实际降低核心类型文件体积;完整子树化需要同步改造大量公共导出和调用方,适合单独结构治理批次推进。
@@ -1,20 +1,22 @@
1
- export * from './agents';
1
+ export * from './services/agents.service';
2
2
  export * from './api-base';
3
3
  export * from './auth.types';
4
- export * from './channel-auth';
4
+ export * from './services/channel-auth.service';
5
5
  export * from './channel-auth.types';
6
6
  export * from './chat-session-type.types';
7
- export * from './client';
8
- export * from './config';
9
- export * from './marketplace';
10
- export * from './mcp-marketplace';
11
- export * from './ncp-attachments';
12
- export * from './ncp-session';
7
+ export * from './services/client.service';
8
+ export * from './services/config.service';
9
+ export * from './services/marketplace.service';
10
+ export * from './services/mcp-marketplace.service';
11
+ export * from './services/ncp-attachments.service';
12
+ export * from './services/ncp-session.service';
13
+ export * from './ncp-session.types';
13
14
  export * from './ncp-session-query-cache';
14
15
  export * from './raw-client.utils';
15
- export * from './remote';
16
+ export * from './services/remote.service';
16
17
  export * from './remote.types';
17
- export * from './runtime-control';
18
+ export * from './services/runtime-control.service';
18
19
  export * from './runtime-control.types';
19
- export * from './server-path';
20
+ export * from './services/runtime-update.service';
21
+ export * from './services/server-path.service';
20
22
  export * from './types';
@@ -1,37 +1,36 @@
1
- import { fetchNcpSessionSkills } from './ncp-session';
2
- import { api } from './client';
1
+ import { fetchNcpSessionSkills } from './services/ncp-session.service';
2
+ import { nextclawClient } from './services/client.service';
3
3
 
4
- vi.mock('./client', () => ({
5
- api: {
6
- get: vi.fn()
4
+ vi.mock('./services/client.service', () => ({
5
+ nextclawClient: {
6
+ sessions: {
7
+ listSkills: vi.fn()
8
+ }
7
9
  }
8
10
  }));
9
11
 
10
12
  describe('api/ncp-session', () => {
11
13
  beforeEach(() => {
12
- vi.mocked(api.get).mockReset();
13
- vi.mocked(api.get).mockResolvedValue({
14
- ok: true,
15
- data: {
16
- sessionId: 'session-1',
17
- total: 0,
18
- refs: [],
19
- records: []
20
- }
14
+ vi.mocked(nextclawClient.sessions.listSkills).mockReset();
15
+ vi.mocked(nextclawClient.sessions.listSkills).mockResolvedValue({
16
+ sessionId: 'session-1',
17
+ total: 0,
18
+ refs: [],
19
+ records: []
21
20
  });
22
21
  });
23
22
 
24
23
  it('does not send an empty projectRoot query when no override is provided', async () => {
25
24
  await fetchNcpSessionSkills('session-1', { projectRoot: null });
26
25
 
27
- expect(api.get).toHaveBeenCalledWith('/api/ncp/sessions/session-1/skills');
26
+ expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', { projectRoot: null });
28
27
  });
29
28
 
30
29
  it('sends projectRoot only when the override is non-empty', async () => {
31
30
  await fetchNcpSessionSkills('session-1', { projectRoot: ' /tmp/project-alpha ' });
32
31
 
33
- expect(api.get).toHaveBeenCalledWith(
34
- '/api/ncp/sessions/session-1/skills?projectRoot=%2Ftmp%2Fproject-alpha'
35
- );
32
+ expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', {
33
+ projectRoot: ' /tmp/project-alpha '
34
+ });
36
35
  });
37
36
  });
@@ -0,0 +1,92 @@
1
+ import type { NcpMessage, NcpSessionStatus, NcpSessionSummary } from '@nextclaw/ncp';
2
+ import type { ThinkingLevel } from './types';
3
+
4
+ export type SessionTypeIconView = {
5
+ kind: "image";
6
+ src: string;
7
+ alt?: string | null;
8
+ };
9
+
10
+ export type RuntimeEntryView = {
11
+ enabled?: boolean;
12
+ label?: string;
13
+ icon?: SessionTypeIconView | null;
14
+ type: string;
15
+ config?: Record<string, unknown>;
16
+ };
17
+
18
+ export type SessionContextWindowView = {
19
+ usedContextTokens: number;
20
+ totalContextTokens: number;
21
+ prunedUsedContextTokens: number;
22
+ availableContextTokens: number;
23
+ droppedHistoryCount: number;
24
+ truncatedToolResultCount: number;
25
+ truncatedSystemPrompt: boolean;
26
+ truncatedUserMessage: boolean;
27
+ compacted: boolean;
28
+ checkpointId?: string;
29
+ compactedMessageCount: number;
30
+ compactedUsedContextTokens?: number;
31
+ updatedAt: string;
32
+ };
33
+
34
+ export type SessionEntryView = {
35
+ key: string;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ lastMessageAt?: string;
39
+ readAt?: string;
40
+ agentId?: string;
41
+ label?: string;
42
+ channel?: string;
43
+ type?: string;
44
+ preferredModel?: string;
45
+ preferredThinking?: ThinkingLevel | null;
46
+ projectRoot?: string | null;
47
+ projectName?: string | null;
48
+ sessionType: string;
49
+ sessionTypeMutable: boolean;
50
+ isChildSession?: boolean;
51
+ isPromotedChildSession?: boolean;
52
+ parentSessionId?: string | null;
53
+ spawnedByRequestId?: string | null;
54
+ contextWindow?: SessionContextWindowView | null;
55
+ messageCount: number;
56
+ lastRole?: string;
57
+ lastTimestamp?: string;
58
+ };
59
+
60
+ export type SessionMessageView = {
61
+ role: string;
62
+ content: unknown;
63
+ timestamp: string;
64
+ name?: string;
65
+ tool_call_id?: string;
66
+ tool_calls?: Array<Record<string, unknown>>;
67
+ reasoning_content?: string;
68
+ };
69
+
70
+ export type SessionEventView = {
71
+ seq: number;
72
+ type: string;
73
+ timestamp: string;
74
+ message?: SessionMessageView;
75
+ };
76
+
77
+ export type NcpSessionSummaryView = NcpSessionSummary;
78
+
79
+ export type NcpSessionsListView = {
80
+ sessions: NcpSessionSummaryView[];
81
+ total: number;
82
+ };
83
+
84
+ export type NcpMessageView = NcpMessage;
85
+
86
+ export type NcpSessionMessagesView = {
87
+ sessionId: string;
88
+ status: NcpSessionStatus;
89
+ messages: NcpMessageView[];
90
+ contextWindow?: SessionContextWindowView | null;
91
+ total: number;
92
+ };
@@ -1,132 +1,9 @@
1
1
  import { API_BASE } from './api-base';
2
- import type { ApiResponse } from './types';
3
- import { systemStatusManager } from '@/features/system-status';
4
-
5
- function compactSnippet(text: string) {
6
- return text.replace(/\s+/g, ' ').trim().slice(0, 200);
7
- }
8
-
9
- function inferNonJsonHint(endpoint: string, status: number): string | undefined {
10
- if (
11
- status === 404 &&
12
- endpoint.startsWith('/api/config/providers/') &&
13
- endpoint.endsWith('/test')
14
- ) {
15
- return 'Provider test endpoint is missing. This usually means nextclaw runtime version is outdated.';
16
- }
17
- if (status === 401 || status === 403) {
18
- return 'Authentication failed. Check apiKey and custom headers.';
19
- }
20
- if (status === 429) {
21
- return 'Rate limited by upstream provider. Retry later or switch model/provider.';
22
- }
23
- if (status >= 500) {
24
- return 'Upstream service error. Retry later and inspect server logs if it persists.';
25
- }
26
- return undefined;
27
- }
28
-
29
- function formatUnknownFetchError(error: unknown): {
30
- summary: string;
31
- details: Record<string, unknown>;
32
- } {
33
- if (error instanceof Error) {
34
- const name = error.name?.trim() || 'Error';
35
- const message = error.message?.trim() || 'Unknown error';
36
- return {
37
- summary: `${name}: ${message}`,
38
- details: {
39
- errorName: name,
40
- errorMessage: message,
41
- ...(error.stack?.trim() ? { errorStack: error.stack.trim() } : {})
42
- }
43
- };
44
- }
45
- return {
46
- summary: String(error ?? 'Unknown error'),
47
- details: {
48
- errorName: 'NonError',
49
- errorMessage: String(error ?? 'Unknown error')
50
- }
51
- };
52
- }
2
+ import { requestRawApiResponse as requestRawTransportApiResponse } from '@/shared/lib/transport';
53
3
 
54
4
  export async function requestRawApiResponse<T>(
55
5
  endpoint: string,
56
6
  options: RequestInit = {}
57
- ): Promise<ApiResponse<T>> {
58
- const url = `${API_BASE}${endpoint}`;
59
- const method = (options.method || 'GET').toUpperCase();
60
-
61
- let response: Response;
62
- try {
63
- response = await fetch(url, {
64
- credentials: 'include',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- ...options.headers
68
- },
69
- ...options
70
- });
71
- } catch (error) {
72
- const formatted = formatUnknownFetchError(error);
73
- systemStatusManager.reportTransportFailure(formatted.summary);
74
- return {
75
- ok: false,
76
- error: {
77
- code: 'NETWORK_ERROR',
78
- message: `Fetch failed on ${method} ${endpoint} | ${formatted.summary}`,
79
- details: {
80
- method,
81
- endpoint,
82
- url,
83
- ...formatted.details
84
- }
85
- }
86
- };
87
- }
88
-
89
- const text = await response.text();
90
- let data: ApiResponse<T> | null = null;
91
- if (text) {
92
- try {
93
- data = JSON.parse(text) as ApiResponse<T>;
94
- } catch {
95
- // fall through to build a synthetic error response
96
- }
97
- }
98
-
99
- if (!data) {
100
- const snippet = text ? compactSnippet(text) : '';
101
- const hint = inferNonJsonHint(endpoint, response.status);
102
- const parts = [`Non-JSON response (${response.status} ${response.statusText}) on ${method} ${endpoint}`];
103
- if (snippet) {
104
- parts.push(`body=${snippet}`);
105
- }
106
- if (hint) {
107
- parts.push(`hint=${hint}`);
108
- }
109
- return {
110
- ok: false,
111
- error: {
112
- code: 'INVALID_RESPONSE',
113
- message: parts.join(' | '),
114
- details: {
115
- status: response.status,
116
- statusText: response.statusText,
117
- method,
118
- endpoint,
119
- url,
120
- bodySnippet: snippet || undefined,
121
- hint
122
- }
123
- }
124
- };
125
- }
126
-
127
- if (!response.ok) {
128
- return data as ApiResponse<T>;
129
- }
130
-
131
- return data as ApiResponse<T>;
7
+ ) {
8
+ return await requestRawTransportApiResponse<T>(API_BASE, endpoint, options);
132
9
  }