@nextclaw/ui 0.12.22 → 0.12.24
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.
- package/CHANGELOG.md +103 -0
- package/dist/assets/{api-lwyw9j7i.js → api-D2xRKmZd.js} +5 -5
- package/dist/assets/app-manager-provider-CNaZboG4.js +1 -0
- package/dist/assets/{app-navigation.config-DgiR0c5_.js → app-navigation.config-Ihhrrt--.js} +1 -1
- package/dist/assets/{book-open-DgLqYpNY.js → book-open-DDlN5MvX.js} +1 -1
- package/dist/assets/{channels-list-page-Dl839n02.js → channels-list-page-p26lgxLk.js} +2 -2
- package/dist/assets/{chat-DwUf7AKR.js → chat-Dkh2qtuz.js} +8 -8
- package/dist/assets/{chat-page-B-FvPmA7.js → chat-page-DoTmE2wx.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-u4uYphxM.js → chunk-JZWAC4HX-Kydj4yEz.js} +1 -1
- package/dist/assets/{config-split-page-BMRGuCJQ.js → config-split-page-DIOCjj2Q.js} +1 -1
- package/dist/assets/{createLucideIcon-BZkY6emz.js → createLucideIcon-BLMK3QUd.js} +1 -1
- package/dist/assets/{desktop-update-config-D5g_gPak.js → desktop-update-config-DlpzDfKM.js} +1 -1
- package/dist/assets/{dialog-CdtCU2xX.js → dialog-C3D7Be0p.js} +1 -1
- package/dist/assets/{dist-CuqvE--P.js → dist-CPlbUgwU.js} +1 -1
- package/dist/assets/{doc-browser-BUlCkZo2.js → doc-browser-C8FM5fC0.js} +1 -1
- package/dist/assets/doc-browser-RJUOL_GO.js +1 -0
- package/dist/assets/{doc-browser-context-DfLHAWbG.js → doc-browser-context-BJuMaI3o.js} +1 -1
- package/dist/assets/{doc-browser-CzCV73NJ.js → doc-browser-p82AdNO-.js} +1 -1
- package/dist/assets/{es2015-yYU5Ad5w.js → es2015-xqN1slyW.js} +1 -1
- package/dist/assets/{external-link-Sw3ah_JD.js → external-link-DwfSfTLB.js} +1 -1
- package/dist/assets/{folder-D7-VTnkz.js → folder-CeJKPx5P.js} +1 -1
- package/dist/assets/{hash-zajSTDXZ.js → hash-BqxRTZW5.js} +1 -1
- package/dist/assets/i18n-DnTGDIRw.js +1 -0
- package/dist/assets/{index-Doxyk7L2.js → index-pBvbJ5Mt.js} +2 -2
- package/dist/assets/{key-round-CnI1mc9F.js → key-round-CJ5gDAAG.js} +1 -1
- package/dist/assets/loader-circle-fd-vQKtW.js +1 -0
- package/dist/assets/{logo-badge-BQgKnVtz.js → logo-badge-KAe-7d8c.js} +1 -1
- package/dist/assets/{logos-CqVm0q0W.js → logos-C4sYP1Vl.js} +1 -1
- package/dist/assets/marketplace-page-Cql0kDi-.js +1 -0
- package/dist/assets/{marketplace-page-CawcdL6Y.js → marketplace-page-m4P5g_Ht.js} +1 -1
- package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DEGfJ_70.js → mcp-marketplace-page-ByzBQZcx.js} +1 -1
- package/dist/assets/message-square-z_osm9c0.js +1 -0
- package/dist/assets/{model-config-r-1RPSrZ.js → model-config-Dbr_0APb.js} +1 -1
- package/dist/assets/{notice-card-BPtCVEKW.js → notice-card-BFDbKQDA.js} +1 -1
- package/dist/assets/play-Dv6Nr1Ew.js +1 -0
- package/dist/assets/plus-D8eKFY7h.js +1 -0
- package/dist/assets/{popover-jbfQhYQh.js → popover-B86Dbfhf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-gdk2lmRi.js → provider-scoped-model-input-DFm6N2f7.js} +1 -1
- package/dist/assets/{providers-list-DpISIr3M.js → providers-list-BJcLOjun.js} +1 -1
- package/dist/assets/{refresh-ccw-Bii4w8aB.js → refresh-ccw-ByVwmnN_.js} +1 -1
- package/dist/assets/{refresh-cw-BxojR62w.js → refresh-cw-PcqoYB3K.js} +1 -1
- package/dist/assets/remote-BOxo9iwd.js +1 -0
- package/dist/assets/{rotate-cw-1Xqa7LZ8.js → rotate-cw-BZ2JObNs.js} +1 -1
- package/dist/assets/runtime-config-page-CjLhnbSl.js +1 -0
- package/dist/assets/{save--BVI5wZX.js → save-euRxl8pI.js} +1 -1
- package/dist/assets/{search-vChioOoe.js → search-CLd7m0M7.js} +1 -1
- package/dist/assets/{search-config-BWqz8nqY.js → search-config-J4Htco-P.js} +1 -1
- package/dist/assets/{secrets-config-CjzSNg0Y.js → secrets-config-CUdERjco.js} +1 -1
- package/dist/assets/{select-Cw5Zkb1w.js → select-CJ0wbo3D.js} +1 -1
- package/dist/assets/{sessions-config-page-beoDPtII.js → sessions-config-page-DpK991fs.js} +2 -2
- package/dist/assets/{setting-row-Cjl2d40s.js → setting-row-D1Yygqp7.js} +1 -1
- package/dist/assets/{settings-CiRChctQ.js → settings-drbWqzA4.js} +1 -1
- package/dist/assets/skeleton-BK1SOSRA.js +1 -0
- package/dist/assets/{sparkles-D1ZKWdm4.js → sparkles-DVfeSVJQ.js} +1 -1
- package/dist/assets/{status-dot-Dv_hiUVa.js → status-dot-ChvPCib9.js} +1 -1
- package/dist/assets/{tabs-custom-CsACkVji.js → tabs-custom-Hia_ong0.js} +1 -1
- package/dist/assets/{tag-chip-CoWHxYJj.js → tag-chip-FrkmkT8r.js} +1 -1
- package/dist/assets/theme-provider-0hxjiPc_.js +2 -0
- package/dist/assets/{tooltip-GYzH-Hfq.js → tooltip-Cj4yA0gH.js} +1 -1
- package/dist/assets/{trash-2-rY9ZteZX.js → trash-2-CBsHCfqq.js} +1 -1
- package/dist/assets/{use-config-BhJHD3-G.js → use-config-38Ur-89i.js} +1 -1
- package/dist/assets/{use-confirm-dialog-Bqgy3Gi-.js → use-confirm-dialog-DPQThaeU.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-BfexitoF.js → use-infinite-scroll-loader-5Gf1xQi7.js} +1 -1
- package/dist/assets/{use-viewport-layout-D33zVbr5.js → use-viewport-layout-D1XzKeip.js} +1 -1
- package/dist/assets/x-CM-XDMpk.js +1 -0
- package/dist/index.html +39 -39
- package/package.json +9 -9
- package/src/features/account/hooks/use-auth.test.ts +7 -5
- package/src/features/account/hooks/use-auth.ts +23 -20
- package/src/features/chat/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -2
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +74 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +28 -29
- package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +158 -69
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
- package/src/features/chat/managers/chat-session-list.manager.test.ts +19 -16
- package/src/features/chat/managers/chat-session-list.manager.ts +20 -24
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +23 -12
- package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
- package/src/features/chat/pages/ncp-chat-page.tsx +23 -13
- package/src/features/chat/stores/chat-session-list.store.ts +2 -3
- package/src/features/chat/types/chat-stream.types.ts +1 -1
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +1 -1
- package/src/features/system-status/hooks/use-system-status.ts +6 -28
- package/src/features/system-status/index.ts +2 -1
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +14 -4
- package/src/features/system-status/managers/system-status.manager.test.ts +2 -8
- package/src/features/system-status/managers/system-status.manager.ts +20 -30
- package/src/shared/components/common/brand-header.test.tsx +84 -3
- package/src/shared/components/common/brand-header.tsx +37 -39
- package/src/shared/lib/api/managers/client.manager.ts +30 -2
- package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
- package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
- package/src/shared/lib/api/utils/config.utils.ts +6 -4
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +3 -1
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/transport.types.ts +20 -0
- package/dist/assets/app-manager-provider-C0ONQxUg.js +0 -1
- package/dist/assets/doc-browser-Doh2541x.js +0 -1
- package/dist/assets/i18n-C5Mibli1.js +0 -1
- package/dist/assets/loader-circle-B5i8oMMY.js +0 -1
- package/dist/assets/marketplace-page-BRHkZaO5.js +0 -1
- package/dist/assets/mcp-marketplace-page-CL7BF4dD.js +0 -1
- package/dist/assets/message-square-D6Z4NwpG.js +0 -1
- package/dist/assets/play-D8WJLnJe.js +0 -1
- package/dist/assets/plus-Di0KAkiO.js +0 -1
- package/dist/assets/remote-BnRNqMlb.js +0 -1
- package/dist/assets/runtime-config-page-DQ8YY8Lc.js +0 -1
- package/dist/assets/skeleton-CFQRIUzt.js +0 -1
- package/dist/assets/theme-provider-B5XReW_-.js +0 -1
- package/dist/assets/x-DpTzXQcX.js +0 -1
|
@@ -77,7 +77,7 @@ describe('NcpChatInputManager', () => {
|
|
|
77
77
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
78
78
|
const sessionListManager = {
|
|
79
79
|
ensureDraftSession: vi.fn(() => 'draft-session'),
|
|
80
|
-
|
|
80
|
+
materializeRootSessionRoute: vi.fn(),
|
|
81
81
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
82
82
|
const manager = new NcpChatInputManager(
|
|
83
83
|
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
@@ -95,14 +95,21 @@ describe('NcpChatInputManager', () => {
|
|
|
95
95
|
}),
|
|
96
96
|
);
|
|
97
97
|
expect(sessionListManager.ensureDraftSession).not.toHaveBeenCalled();
|
|
98
|
-
expect(sessionListManager.
|
|
98
|
+
expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
it('
|
|
101
|
+
it('sends without a session key while /chat is still in blank-draft mode', async () => {
|
|
102
102
|
useChatThreadStore.setState({
|
|
103
103
|
snapshot: {
|
|
104
104
|
...useChatThreadStore.getState().snapshot,
|
|
105
|
-
sessionKey:
|
|
105
|
+
sessionKey: null,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
useChatSessionListStore.setState({
|
|
109
|
+
snapshot: {
|
|
110
|
+
...useChatSessionListStore.getState().snapshot,
|
|
111
|
+
selectedSessionKey: null,
|
|
112
|
+
draftSessionKey: null,
|
|
106
113
|
},
|
|
107
114
|
});
|
|
108
115
|
const streamActionsManager = {
|
|
@@ -111,7 +118,7 @@ describe('NcpChatInputManager', () => {
|
|
|
111
118
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
112
119
|
const sessionListManager = {
|
|
113
120
|
ensureDraftSession: vi.fn(() => 'materialized-draft-session'),
|
|
114
|
-
|
|
121
|
+
materializeRootSessionRoute: vi.fn(),
|
|
115
122
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
116
123
|
const manager = new NcpChatInputManager(
|
|
117
124
|
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
@@ -121,14 +128,18 @@ describe('NcpChatInputManager', () => {
|
|
|
121
128
|
|
|
122
129
|
await manager.send();
|
|
123
130
|
|
|
124
|
-
expect(sessionListManager.ensureDraftSession).
|
|
131
|
+
expect(sessionListManager.ensureDraftSession).toHaveBeenCalledWith('native');
|
|
132
|
+
expect(streamActionsManager.sendMessage).toHaveBeenCalledWith(
|
|
133
|
+
expect.not.objectContaining({
|
|
134
|
+
sessionKey: expect.any(String),
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
125
137
|
expect(streamActionsManager.sendMessage).toHaveBeenCalledWith(
|
|
126
138
|
expect.objectContaining({
|
|
127
|
-
sessionKey: 'draft-root-session',
|
|
128
139
|
message: 'hello from current thread',
|
|
129
140
|
}),
|
|
130
141
|
);
|
|
131
|
-
expect(sessionListManager.
|
|
142
|
+
expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
|
|
132
143
|
});
|
|
133
144
|
|
|
134
145
|
it('does not send while the runtime is still blocked during startup', async () => {
|
|
@@ -158,7 +169,7 @@ describe('NcpChatInputManager', () => {
|
|
|
158
169
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
159
170
|
const sessionListManager = {
|
|
160
171
|
ensureDraftSession: vi.fn(() => 'draft-session'),
|
|
161
|
-
|
|
172
|
+
materializeRootSessionRoute: vi.fn(),
|
|
162
173
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
163
174
|
const manager = new NcpChatInputManager(
|
|
164
175
|
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
@@ -169,7 +180,7 @@ describe('NcpChatInputManager', () => {
|
|
|
169
180
|
await manager.send();
|
|
170
181
|
|
|
171
182
|
expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
|
|
172
|
-
expect(sessionListManager.
|
|
183
|
+
expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
|
|
173
184
|
});
|
|
174
185
|
|
|
175
186
|
it('still attempts to send when provider metadata is stale or the session type is marked unavailable', async () => {
|
|
@@ -187,7 +198,7 @@ describe('NcpChatInputManager', () => {
|
|
|
187
198
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
188
199
|
const sessionListManager = {
|
|
189
200
|
ensureDraftSession: vi.fn(() => 'draft-session'),
|
|
190
|
-
|
|
201
|
+
materializeRootSessionRoute: vi.fn(),
|
|
191
202
|
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
192
203
|
const manager = new NcpChatInputManager(
|
|
193
204
|
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
@@ -198,6 +209,6 @@ describe('NcpChatInputManager', () => {
|
|
|
198
209
|
await manager.send();
|
|
199
210
|
|
|
200
211
|
expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
|
|
201
|
-
expect(sessionListManager.
|
|
212
|
+
expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
|
|
202
213
|
});
|
|
203
214
|
});
|
|
@@ -197,11 +197,14 @@ export class NcpChatInputManager {
|
|
|
197
197
|
const sessionKey =
|
|
198
198
|
threadSnapshot.sessionKey ??
|
|
199
199
|
sessionSnapshot.selectedSessionKey ??
|
|
200
|
+
null;
|
|
201
|
+
if (!sessionKey && inputSnapshot.selectedSessionType?.trim()) {
|
|
200
202
|
this.sessionListManager.ensureDraftSession(inputSnapshot.selectedSessionType);
|
|
203
|
+
}
|
|
201
204
|
this.setComposerNodes(createInitialChatComposerNodes());
|
|
202
205
|
await this.streamActionsManager.sendMessage({
|
|
203
206
|
message,
|
|
204
|
-
sessionKey,
|
|
207
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
205
208
|
agentId: sessionSnapshot.selectedAgentId,
|
|
206
209
|
sessionType: inputSnapshot.selectedSessionType,
|
|
207
210
|
model: inputSnapshot.selectedModel || undefined,
|
|
@@ -213,7 +216,6 @@ export class NcpChatInputManager {
|
|
|
213
216
|
restoreDraftOnError: true,
|
|
214
217
|
composerNodes
|
|
215
218
|
});
|
|
216
|
-
this.sessionListManager.promoteRootDraftSessionRoute(sessionKey);
|
|
217
219
|
};
|
|
218
220
|
|
|
219
221
|
stop = async () => {
|
|
@@ -106,9 +106,6 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
|
|
|
106
106
|
const selectedSessionKey = useChatSessionListStore(
|
|
107
107
|
(state) => state.snapshot.selectedSessionKey,
|
|
108
108
|
);
|
|
109
|
-
const draftSessionKey = useChatSessionListStore(
|
|
110
|
-
(state) => state.snapshot.draftSessionKey,
|
|
111
|
-
);
|
|
112
109
|
const selectedAgentId = useChatSessionListStore(
|
|
113
110
|
(state) => state.snapshot.selectedAgentId,
|
|
114
111
|
);
|
|
@@ -137,16 +134,16 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
|
|
|
137
134
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
138
135
|
[routeSessionIdParam],
|
|
139
136
|
);
|
|
140
|
-
const sessionKey = routeSessionKey ??
|
|
137
|
+
const sessionKey = routeSessionKey ?? undefined;
|
|
141
138
|
const hasSessionProjectRootOverride =
|
|
142
139
|
pendingProjectRoot !== null &&
|
|
143
|
-
pendingProjectRootSessionKey === sessionKey;
|
|
140
|
+
(!sessionKey || pendingProjectRootSessionKey === sessionKey);
|
|
144
141
|
const sessionProjectRootOverride = hasSessionProjectRootOverride
|
|
145
142
|
? pendingProjectRoot
|
|
146
143
|
: undefined;
|
|
147
144
|
const pageData = useNcpChatPageData({
|
|
148
145
|
query,
|
|
149
|
-
sessionKey,
|
|
146
|
+
sessionKey: sessionKey ?? null,
|
|
150
147
|
projectRootOverride: sessionProjectRootOverride,
|
|
151
148
|
currentSelectedModel,
|
|
152
149
|
pendingSessionType,
|
|
@@ -213,7 +210,7 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
|
|
|
213
210
|
? (agentsQuery.data?.agents ?? [])
|
|
214
211
|
: [{ id: selectedSession?.agentId ?? selectedAgentId }];
|
|
215
212
|
const derivedState = useNcpChatDerivedState({
|
|
216
|
-
sessionKey,
|
|
213
|
+
sessionKey: sessionKey ?? null,
|
|
217
214
|
selectedSession,
|
|
218
215
|
selectedAgentId,
|
|
219
216
|
availableAgents,
|
|
@@ -251,14 +248,13 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
|
|
|
251
248
|
pendingProjectRootSessionKey,
|
|
252
249
|
presenter,
|
|
253
250
|
selectedSession,
|
|
254
|
-
selectedSessionKey,
|
|
255
251
|
selectedSessionKeyRef,
|
|
256
252
|
sessionKey,
|
|
257
253
|
} = params;
|
|
258
254
|
useEffect(() => {
|
|
259
255
|
presenter.chatStreamActionsManager.bind({
|
|
260
256
|
sendMessage: async (payload) => {
|
|
261
|
-
if (payload.sessionKey !== sessionKey) {
|
|
257
|
+
if ((payload.sessionKey ?? null) !== (sessionKey ?? null)) {
|
|
262
258
|
return;
|
|
263
259
|
}
|
|
264
260
|
const metadata = buildNcpSendMetadata({
|
|
@@ -267,7 +263,7 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
|
|
|
267
263
|
thinkingLevel: payload.thinkingLevel,
|
|
268
264
|
sessionType: payload.sessionType,
|
|
269
265
|
projectRoot:
|
|
270
|
-
payload.sessionKey === pendingProjectRootSessionKey
|
|
266
|
+
!payload.sessionKey || payload.sessionKey === pendingProjectRootSessionKey
|
|
271
267
|
? pendingProjectRoot
|
|
272
268
|
: (selectedSession?.projectRoot ?? null),
|
|
273
269
|
requestedSkills: payload.requestedSkills,
|
|
@@ -322,7 +318,6 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
|
|
|
322
318
|
pendingProjectRoot,
|
|
323
319
|
pendingProjectRootSessionKey,
|
|
324
320
|
presenter,
|
|
325
|
-
selectedSessionKey,
|
|
326
321
|
selectedSession?.projectRoot,
|
|
327
322
|
selectedSessionKeyRef,
|
|
328
323
|
sessionKey,
|
|
@@ -336,7 +331,6 @@ function usePendingProjectRootOverrideCleanup(
|
|
|
336
331
|
pendingProjectRoot,
|
|
337
332
|
pendingProjectRootSessionKey,
|
|
338
333
|
selectedSession,
|
|
339
|
-
selectedSessionKey,
|
|
340
334
|
} = params;
|
|
341
335
|
useEffect(() => {
|
|
342
336
|
if (
|
|
@@ -358,7 +352,6 @@ function usePendingProjectRootOverrideCleanup(
|
|
|
358
352
|
pendingProjectRoot,
|
|
359
353
|
pendingProjectRootSessionKey,
|
|
360
354
|
selectedSession,
|
|
361
|
-
selectedSessionKey,
|
|
362
355
|
]);
|
|
363
356
|
}
|
|
364
357
|
|
|
@@ -385,6 +378,22 @@ function useSelectedSessionAgentSync(params: ReturnType<typeof useNcpChatPageSta
|
|
|
385
378
|
}, [presenter, selectedAgentId, selectedSession?.agentId]);
|
|
386
379
|
}
|
|
387
380
|
|
|
381
|
+
function useMaterializedRootSessionRouteSync(
|
|
382
|
+
params: ReturnType<typeof useNcpChatPageState>,
|
|
383
|
+
) {
|
|
384
|
+
const { agent, presenter, routeSessionKey } = params;
|
|
385
|
+
const materializedSessionKey =
|
|
386
|
+
agent.snapshot.activeRun?.sessionId ??
|
|
387
|
+
agent.visibleMessages.find((message) => message.sessionId.trim())?.sessionId ??
|
|
388
|
+
null;
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
if (routeSessionKey || !materializedSessionKey) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
presenter.chatSessionListManager.materializeRootSessionRoute(materializedSessionKey);
|
|
394
|
+
}, [materializedSessionKey, presenter, routeSessionKey]);
|
|
395
|
+
}
|
|
396
|
+
|
|
388
397
|
export function NcpChatPage({ view }: ChatPageProps) {
|
|
389
398
|
const [presenter] = useState(() => new NcpChatPresenter());
|
|
390
399
|
const state = useNcpChatPageState(presenter);
|
|
@@ -392,6 +401,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
392
401
|
usePendingProjectRootOverrideCleanup(state);
|
|
393
402
|
useNcpChatUiBindings(state);
|
|
394
403
|
useSelectedSessionAgentSync(state);
|
|
404
|
+
useMaterializedRootSessionRouteSync(state);
|
|
395
405
|
useChatSessionSync({
|
|
396
406
|
view,
|
|
397
407
|
routeSessionKey: state.routeSessionKey,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { create, type StateCreator } from 'zustand';
|
|
2
|
-
import { createNcpSessionId } from '@/features/chat/utils/ncp-session-adapter.utils';
|
|
3
2
|
import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
|
|
4
3
|
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
5
4
|
export type ChatSessionListSnapshot = {
|
|
6
5
|
selectedSessionKey: string | null;
|
|
7
|
-
draftSessionKey: string;
|
|
6
|
+
draftSessionKey: string | null;
|
|
8
7
|
selectedAgentId: string;
|
|
9
8
|
query: string;
|
|
10
9
|
listMode: ChatSessionListMode;
|
|
@@ -50,7 +49,7 @@ type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0]
|
|
|
50
49
|
|
|
51
50
|
const initialSnapshot: ChatSessionListSnapshot = {
|
|
52
51
|
selectedSessionKey: null,
|
|
53
|
-
draftSessionKey:
|
|
52
|
+
draftSessionKey: null,
|
|
54
53
|
selectedAgentId: 'main',
|
|
55
54
|
query: '',
|
|
56
55
|
listMode: 'time-first'
|
|
@@ -288,7 +288,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
288
288
|
const isPromotedChildSession = readPromotedChildSession(summary);
|
|
289
289
|
return {
|
|
290
290
|
key: summary.sessionId,
|
|
291
|
-
createdAt: summary.updatedAt,
|
|
291
|
+
createdAt: summary.createdAt ?? summary.updatedAt,
|
|
292
292
|
updatedAt: summary.updatedAt,
|
|
293
293
|
...(lastMessageAt ? { lastMessageAt } : {}),
|
|
294
294
|
...(readAt ? { readAt } : {}),
|
|
@@ -10,46 +10,24 @@ import {
|
|
|
10
10
|
import { systemStatusManager } from '@/features/system-status/managers/system-status.manager';
|
|
11
11
|
import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
|
|
12
12
|
|
|
13
|
-
function createPendingBootstrapStatus(): BootstrapStatusView {
|
|
14
|
-
return {
|
|
15
|
-
phase: 'kernel-starting',
|
|
16
|
-
ncpAgent: {
|
|
17
|
-
state: 'pending',
|
|
18
|
-
},
|
|
19
|
-
pluginHydration: {
|
|
20
|
-
state: 'pending',
|
|
21
|
-
loadedPluginCount: 0,
|
|
22
|
-
totalPluginCount: 0,
|
|
23
|
-
},
|
|
24
|
-
channels: {
|
|
25
|
-
state: 'pending',
|
|
26
|
-
enabled: [],
|
|
27
|
-
},
|
|
28
|
-
remote: {
|
|
29
|
-
state: 'pending',
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
13
|
export function useSystemStatusSources() {
|
|
35
|
-
const runtimeBootstrapStatus = useQuery({
|
|
14
|
+
const runtimeBootstrapStatus = useQuery<BootstrapStatusView>({
|
|
36
15
|
queryKey: ['runtime-bootstrap-status'],
|
|
37
|
-
queryFn: fetchBootstrapStatus
|
|
38
|
-
|
|
16
|
+
queryFn: () => fetchBootstrapStatus({
|
|
17
|
+
timeoutMs: 5_000,
|
|
18
|
+
}),
|
|
39
19
|
refetchInterval: (query) => {
|
|
40
20
|
return systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
41
|
-
query.state.data
|
|
21
|
+
query.state.data,
|
|
22
|
+
query.state.fetchFailureCount
|
|
42
23
|
);
|
|
43
24
|
},
|
|
44
|
-
refetchIntervalInBackground: true,
|
|
45
25
|
retry: false,
|
|
46
|
-
refetchOnWindowFocus: true,
|
|
47
26
|
});
|
|
48
27
|
const runtimeControl = useQuery({
|
|
49
28
|
queryKey: ['runtime-control'],
|
|
50
29
|
queryFn: async () => await systemStatusManager.getRuntimeControl(),
|
|
51
30
|
staleTime: 5_000,
|
|
52
|
-
refetchOnWindowFocus: true,
|
|
53
31
|
});
|
|
54
32
|
|
|
55
33
|
useEffect(() => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useRuntimeControlPanelView, useRuntimeStatusBadgeView, useSystemStatus, useSystemStatusSources } from './hooks/use-system-status';
|
|
2
|
-
export {
|
|
2
|
+
export { systemStatusManager } from './managers/system-status.manager';
|
|
3
|
+
export { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
3
4
|
export { runtimeUpdateManager } from './managers/runtime-update.manager';
|
|
4
5
|
export type { SystemStatusState, SystemStatusView } from './types/system-status.types';
|
|
5
6
|
export { useSystemStatusStore } from './stores/system-status.store';
|
|
@@ -16,7 +16,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
16
16
|
|
|
17
17
|
it('keeps polling while bootstrap status is missing', () => {
|
|
18
18
|
expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
|
|
19
|
-
|
|
19
|
+
1000
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
40
40
|
state: 'pending',
|
|
41
41
|
},
|
|
42
42
|
})
|
|
43
|
-
).toBe(
|
|
43
|
+
).toBe(1000);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('continues polling even when bootstrap status reports an ncp agent error', () => {
|
|
@@ -65,7 +65,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
65
65
|
},
|
|
66
66
|
lastError: 'startup failed',
|
|
67
67
|
})
|
|
68
|
-
).toBe(
|
|
68
|
+
).toBe(2000);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('stops polling once the ncp agent is ready', () => {
|
|
@@ -121,6 +121,16 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
121
121
|
state: 'disabled',
|
|
122
122
|
},
|
|
123
123
|
})
|
|
124
|
-
).toBe(
|
|
124
|
+
).toBe(1000);
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
it('backs off polling after transport failures', () => {
|
|
128
|
+
expect(
|
|
129
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(undefined, 1)
|
|
130
|
+
).toBe(2000);
|
|
131
|
+
expect(
|
|
132
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(undefined, 3)
|
|
133
|
+
).toBe(5000);
|
|
134
|
+
});
|
|
135
|
+
|
|
126
136
|
});
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { BootstrapStatusView } from '@/shared/lib/api';
|
|
3
3
|
import { appQueryClient } from '@/app-query-client';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
systemStatusManager,
|
|
7
|
-
} from './system-status.manager';
|
|
4
|
+
import { systemStatusManager } from './system-status.manager';
|
|
5
|
+
import { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
8
6
|
import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
|
|
9
7
|
|
|
10
8
|
const readyBootstrapStatus: BootstrapStatusView = {
|
|
@@ -76,9 +74,6 @@ describe('systemStatusManager', () => {
|
|
|
76
74
|
});
|
|
77
75
|
|
|
78
76
|
it('enters recovering only after the page has previously reached ready', async () => {
|
|
79
|
-
const invalidateQueriesSpy = vi
|
|
80
|
-
.spyOn(appQueryClient, 'invalidateQueries')
|
|
81
|
-
.mockResolvedValue(undefined as never);
|
|
82
77
|
const refetchQueriesSpy = vi
|
|
83
78
|
.spyOn(appQueryClient, 'refetchQueries')
|
|
84
79
|
.mockResolvedValue(undefined as never);
|
|
@@ -93,7 +88,6 @@ describe('systemStatusManager', () => {
|
|
|
93
88
|
systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
|
|
94
89
|
|
|
95
90
|
expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe('ready');
|
|
96
|
-
expect(invalidateQueriesSpy).toHaveBeenCalled();
|
|
97
91
|
expect(refetchQueriesSpy).toHaveBeenCalledWith({ type: 'active' });
|
|
98
92
|
});
|
|
99
93
|
|
|
@@ -21,8 +21,14 @@ import {
|
|
|
21
21
|
initialSystemStatusState,
|
|
22
22
|
useSystemStatusStore,
|
|
23
23
|
} from '@/features/system-status/stores/system-status.store';
|
|
24
|
+
import { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
24
25
|
|
|
25
26
|
const RECOVERY_TIMEOUT_MS = 30_000;
|
|
27
|
+
const RUNTIME_BOOTSTRAP_PROBE_POLICY = {
|
|
28
|
+
activePollIntervalMs: 1_000,
|
|
29
|
+
errorPollIntervalMs: 2_000,
|
|
30
|
+
maxErrorPollIntervalMs: 5_000,
|
|
31
|
+
} as const;
|
|
26
32
|
|
|
27
33
|
function getErrorMessage(error: unknown): string {
|
|
28
34
|
if (error instanceof Error) {
|
|
@@ -53,46 +59,34 @@ function resolveActionHelp(action: RuntimeControlAction): string {
|
|
|
53
59
|
return t('runtimeControlRestartingAppHelp');
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
export function isTransientRuntimeConnectionErrorMessage(
|
|
57
|
-
message: string
|
|
58
|
-
): boolean {
|
|
59
|
-
const normalized = message.trim().toLowerCase();
|
|
60
|
-
if (!normalized) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
return (
|
|
64
|
-
normalized.includes('failed to fetch') ||
|
|
65
|
-
normalized.includes('networkerror') ||
|
|
66
|
-
normalized.includes('network request failed') ||
|
|
67
|
-
normalized.includes('load failed') ||
|
|
68
|
-
normalized.includes('request timed out') ||
|
|
69
|
-
normalized.includes('timed out waiting for remote request response') ||
|
|
70
|
-
normalized.includes('remote transport connection closed') ||
|
|
71
|
-
normalized.includes('websocket error') ||
|
|
72
|
-
normalized.includes('fetch failed on ') ||
|
|
73
|
-
normalized.includes('stream request failed for ') ||
|
|
74
|
-
normalized.includes('ncp fetch failed for ')
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
62
|
export class SystemStatusManager {
|
|
79
63
|
private recoveryTimeoutId: number | null = null;
|
|
80
64
|
|
|
81
65
|
getRuntimeBootstrapPollInterval = (
|
|
82
|
-
status: BootstrapStatusView | null | undefined
|
|
66
|
+
status: BootstrapStatusView | null | undefined,
|
|
67
|
+
fetchFailureCount = 0
|
|
83
68
|
): number | false => {
|
|
84
69
|
const { lifecyclePhase, activeSystemAction } = this.getState();
|
|
70
|
+
if (fetchFailureCount > 0) {
|
|
71
|
+
return Math.min(
|
|
72
|
+
RUNTIME_BOOTSTRAP_PROBE_POLICY.maxErrorPollIntervalMs,
|
|
73
|
+
RUNTIME_BOOTSTRAP_PROBE_POLICY.errorPollIntervalMs * fetchFailureCount
|
|
74
|
+
);
|
|
75
|
+
}
|
|
85
76
|
if (
|
|
86
77
|
lifecyclePhase === 'recovering' ||
|
|
87
78
|
lifecyclePhase === 'stalled' ||
|
|
88
79
|
activeSystemAction?.lifecycle === 'recovering'
|
|
89
80
|
) {
|
|
90
|
-
return
|
|
81
|
+
return RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
|
|
91
82
|
}
|
|
92
83
|
if (status?.ncpAgent.state === 'ready') {
|
|
93
84
|
return false;
|
|
94
85
|
}
|
|
95
|
-
|
|
86
|
+
if (status?.ncpAgent.state === 'error' || status?.phase === 'error') {
|
|
87
|
+
return RUNTIME_BOOTSTRAP_PROBE_POLICY.errorPollIntervalMs;
|
|
88
|
+
}
|
|
89
|
+
return RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
|
|
96
90
|
};
|
|
97
91
|
|
|
98
92
|
getRuntimeControl = async (): Promise<RuntimeControlView> => {
|
|
@@ -343,10 +337,7 @@ export class SystemStatusManager {
|
|
|
343
337
|
});
|
|
344
338
|
|
|
345
339
|
if (shouldRefreshQueries) {
|
|
346
|
-
void
|
|
347
|
-
appQueryClient.invalidateQueries(),
|
|
348
|
-
appQueryClient.refetchQueries({ type: 'active' }),
|
|
349
|
-
]);
|
|
340
|
+
void appQueryClient.refetchQueries({ type: 'active' });
|
|
350
341
|
}
|
|
351
342
|
};
|
|
352
343
|
|
|
@@ -413,7 +404,6 @@ export class SystemStatusManager {
|
|
|
413
404
|
const view = await this.getRuntimeControl();
|
|
414
405
|
this.syncRuntimeControlQueryCache(view);
|
|
415
406
|
this.reportRuntimeControlView(view);
|
|
416
|
-
await appQueryClient.invalidateQueries({ queryKey: ['runtime-control'] });
|
|
417
407
|
} catch (error) {
|
|
418
408
|
this.reportRuntimeControlError(error);
|
|
419
409
|
}
|
|
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
3
3
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
4
|
import { MemoryRouter } from 'react-router-dom';
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import type * as SystemStatusModule from '@/features/system-status';
|
|
6
7
|
import { useRuntimeUpdateStore } from '@/features/system-status';
|
|
7
8
|
import { BrandHeader } from '@/shared/components/common/brand-header';
|
|
8
9
|
import { setLanguage } from '@/shared/lib/i18n';
|
|
@@ -13,9 +14,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
13
14
|
}));
|
|
14
15
|
|
|
15
16
|
vi.mock('@/features/system-status', async () => {
|
|
16
|
-
const actual = await vi.importActual<typeof
|
|
17
|
-
'@/features/system-status'
|
|
18
|
-
);
|
|
17
|
+
const actual = await vi.importActual<typeof SystemStatusModule>('@/features/system-status');
|
|
19
18
|
return {
|
|
20
19
|
...actual,
|
|
21
20
|
runtimeUpdateManager: {
|
|
@@ -144,4 +143,86 @@ describe('BrandHeader', () => {
|
|
|
144
143
|
expect(mocks.applyDownloadedUpdate).toHaveBeenCalledTimes(1);
|
|
145
144
|
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
|
|
146
145
|
});
|
|
146
|
+
|
|
147
|
+
it('shows a warning icon with the blocked update reason instead of a visible failure label', async () => {
|
|
148
|
+
useRuntimeUpdateStore.setState({
|
|
149
|
+
supported: true,
|
|
150
|
+
initialized: true,
|
|
151
|
+
busyAction: null,
|
|
152
|
+
snapshot: {
|
|
153
|
+
status: 'blocked',
|
|
154
|
+
installationKind: 'npm-runtime-bundle',
|
|
155
|
+
channel: 'stable',
|
|
156
|
+
hostVersion: '0.19.4',
|
|
157
|
+
currentVersion: '0.19.4',
|
|
158
|
+
availableVersion: null,
|
|
159
|
+
downloadedVersion: null,
|
|
160
|
+
minimumHostVersion: null,
|
|
161
|
+
releaseNotesUrl: null,
|
|
162
|
+
lastCheckedAt: null,
|
|
163
|
+
progress: null,
|
|
164
|
+
canAutoDownload: true,
|
|
165
|
+
canApplyInApp: false,
|
|
166
|
+
requiresRestart: false,
|
|
167
|
+
blockReason: 'signature-verification-unavailable',
|
|
168
|
+
recoveryCommand: 'Set NEXTCLAW_UPDATE_BUNDLE_PUBLIC_KEY',
|
|
169
|
+
errorMessage: 'Runtime bundle updates require a configured update public key.',
|
|
170
|
+
preferences: {
|
|
171
|
+
automaticChecks: true,
|
|
172
|
+
autoDownload: true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
renderBrandHeader();
|
|
178
|
+
|
|
179
|
+
expect(screen.queryByText('更新异常')).toBeNull();
|
|
180
|
+
const issueIcon = screen.getByLabelText('更新被阻塞');
|
|
181
|
+
|
|
182
|
+
expect(issueIcon.textContent).toBe('!');
|
|
183
|
+
expect(issueIcon.getAttribute('title')).toContain('更新被阻塞');
|
|
184
|
+
expect(issueIcon.getAttribute('title')).toContain('根因:缺少更新签名公钥,无法验证更新包来源');
|
|
185
|
+
expect(issueIcon.getAttribute('title')).toContain('Runtime bundle updates require a configured update public key.');
|
|
186
|
+
expect(issueIcon.getAttribute('title')).toContain('Set NEXTCLAW_UPDATE_BUNDLE_PUBLIC_KEY');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('uses the failed update wording only for failed snapshots', async () => {
|
|
190
|
+
useRuntimeUpdateStore.setState({
|
|
191
|
+
supported: true,
|
|
192
|
+
initialized: true,
|
|
193
|
+
busyAction: null,
|
|
194
|
+
snapshot: {
|
|
195
|
+
status: 'failed',
|
|
196
|
+
installationKind: 'npm-runtime-bundle',
|
|
197
|
+
channel: 'stable',
|
|
198
|
+
hostVersion: '0.19.4',
|
|
199
|
+
currentVersion: '0.19.3',
|
|
200
|
+
availableVersion: '0.19.4',
|
|
201
|
+
downloadedVersion: null,
|
|
202
|
+
minimumHostVersion: null,
|
|
203
|
+
releaseNotesUrl: null,
|
|
204
|
+
lastCheckedAt: null,
|
|
205
|
+
progress: null,
|
|
206
|
+
canAutoDownload: true,
|
|
207
|
+
canApplyInApp: false,
|
|
208
|
+
requiresRestart: false,
|
|
209
|
+
blockReason: null,
|
|
210
|
+
recoveryCommand: null,
|
|
211
|
+
errorMessage: 'runtime bundle sha256 mismatch',
|
|
212
|
+
preferences: {
|
|
213
|
+
automaticChecks: true,
|
|
214
|
+
autoDownload: true
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
renderBrandHeader();
|
|
220
|
+
|
|
221
|
+
const issueIcon = screen.getByLabelText('更新失败');
|
|
222
|
+
|
|
223
|
+
expect(issueIcon.textContent).toBe('!');
|
|
224
|
+
expect(issueIcon.getAttribute('title')).toContain('更新失败');
|
|
225
|
+
expect(issueIcon.getAttribute('title')).toContain('runtime bundle sha256 mismatch');
|
|
226
|
+
expect(screen.queryByText('更新被阻塞')).toBeNull();
|
|
227
|
+
});
|
|
147
228
|
});
|