@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.
- package/CHANGELOG.md +39 -0
- package/dist/assets/api-BcqDx0tm.js +15 -0
- package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
- package/dist/assets/app-navigation.config-CMoWvFEI.js +1 -0
- package/dist/assets/{book-open-CVEuA0y5.js → book-open-DgLqYpNY.js} +1 -1
- package/dist/assets/{channels-list-page-BqhqaBf1.js → channels-list-page-CsoI4OJm.js} +2 -2
- package/dist/assets/{chat-D4KecKjB.js → chat-CA3aRmhx.js} +13 -12
- package/dist/assets/chat-page-gdSN6Pr6.js +1 -0
- package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
- package/dist/assets/{config-split-page-BGjVACdO.js → config-split-page-BMRGuCJQ.js} +1 -1
- package/dist/assets/{createLucideIcon-PPrXCGK8.js → createLucideIcon-BZkY6emz.js} +1 -1
- package/dist/assets/desktop-update-config-CD6-2PfI.js +1 -0
- package/dist/assets/{dialog-CTCX7oLf.js → dialog-csshWetU.js} +1 -1
- package/dist/assets/{dist-FL5e8mMi.js → dist-Bl94Ahwx.js} +1 -1
- package/dist/assets/{doc-browser-C02neCIE.js → doc-browser-BUlCkZo2.js} +1 -1
- package/dist/assets/doc-browser-CzCV73NJ.js +1 -0
- package/dist/assets/doc-browser-Doh2541x.js +1 -0
- package/dist/assets/{doc-browser-context-C-WPOji4.js → doc-browser-context-DfLHAWbG.js} +1 -1
- package/dist/assets/{es2015-BNy4R8AC.js → es2015-JCM5-KtW.js} +1 -1
- package/dist/assets/{external-link-BNtqJE01.js → external-link-Sw3ah_JD.js} +1 -1
- package/dist/assets/{folder-QyJHVUNz.js → folder-D7-VTnkz.js} +1 -1
- package/dist/assets/{hash-BGYUE-zr.js → hash-zajSTDXZ.js} +1 -1
- package/dist/assets/i18n-C5Mibli1.js +1 -0
- package/dist/assets/index-BTDFuKka.js +2 -0
- package/dist/assets/index-CUmk8xFK.css +1 -0
- package/dist/assets/{key-round-DenCfA2w.js → key-round-CnI1mc9F.js} +1 -1
- package/dist/assets/loader-circle-B5i8oMMY.js +1 -0
- package/dist/assets/{logo-badge-CKAxvQFc.js → logo-badge-BQgKnVtz.js} +1 -1
- package/dist/assets/{logos-CqXnaJIm.js → logos-CqVm0q0W.js} +1 -1
- package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
- package/dist/assets/{marketplace-page-XnDa2ulT.js → marketplace-page-DxlxHCFm.js} +2 -2
- package/dist/assets/mcp-marketplace-page-5UjYRWOR.js +40 -0
- package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
- package/dist/assets/message-square-D6Z4NwpG.js +1 -0
- package/dist/assets/{model-config-ByeL6Toe.js → model-config-PccJ9XyH.js} +1 -1
- package/dist/assets/{notice-card-D00-02yg.js → notice-card-CCgk6FvF.js} +1 -1
- package/dist/assets/play-D8WJLnJe.js +1 -0
- package/dist/assets/plus-Di0KAkiO.js +1 -0
- package/dist/assets/{popover-AmJkxio3.js → popover-YAsxDBhY.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CfFJsJp-.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
- package/dist/assets/{providers-list-HMQzW2WV.js → providers-list-8qDMER8o.js} +1 -1
- package/dist/assets/{refresh-ccw-B-dhb3yS.js → refresh-ccw-Bii4w8aB.js} +1 -1
- package/dist/assets/refresh-cw-BxojR62w.js +1 -0
- package/dist/assets/remote-D4TtLPAp.js +1 -0
- package/dist/assets/{rotate-cw-BWqAG3Fv.js → rotate-cw-1Xqa7LZ8.js} +1 -1
- package/dist/assets/runtime-config-page-D-4c5H5z.js +1 -0
- package/dist/assets/{save-DpdkGieJ.js → save--BVI5wZX.js} +1 -1
- package/dist/assets/search-config-D3a65l3r.js +1 -0
- package/dist/assets/{search-CQUdr7j_.js → search-vChioOoe.js} +1 -1
- package/dist/assets/{secrets-config-YCsGd1am.js → secrets-config-CoMlR_7i.js} +2 -2
- package/dist/assets/{select-DVUtSFHZ.js → select-DIZrwsKU.js} +1 -1
- package/dist/assets/{sessions-config-page-BKN-XdKr.js → sessions-config-page-Cc0TJStn.js} +2 -2
- package/dist/assets/{setting-row-Cb5-lFs-.js → setting-row-DiQyrE81.js} +1 -1
- package/dist/assets/{settings-DgtZZlnF.js → settings-CiRChctQ.js} +1 -1
- package/dist/assets/skeleton-CFQRIUzt.js +1 -0
- package/dist/assets/{sparkles-DNSCyDhL.js → sparkles-D1ZKWdm4.js} +1 -1
- package/dist/assets/{status-dot-X_j51OfA.js → status-dot-Dv_hiUVa.js} +1 -1
- package/dist/assets/{tabs-custom-CcWmekaF.js → tabs-custom-CsACkVji.js} +1 -1
- package/dist/assets/{tag-chip-fdbK2wE6.js → tag-chip-C3wDBe_-.js} +1 -1
- package/dist/assets/theme-provider-aOmrJ9J6.js +1 -0
- package/dist/assets/{tooltip-BkZCQcKw.js → tooltip-Dq5Xehpk.js} +1 -1
- package/dist/assets/{trash-2-CqciSCsg.js → trash-2-rY9ZteZX.js} +1 -1
- package/dist/assets/use-config-BQJjq1mP.js +1 -0
- package/dist/assets/{use-confirm-dialog-DSrb9205.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-DmowtyTI.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CaALCA51.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
- package/dist/assets/x-DpTzXQcX.js +1 -0
- package/dist/index.html +40 -39
- package/package.json +9 -6
- package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
- package/src/app/index.tsx +7 -1
- package/src/features/channels/components/config/channel-form.tsx +3 -3
- package/src/features/channels/components/config/weixin-channel-auth-section.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -0
- package/src/features/chat/components/conversation/chat-input-bar.container.tsx +9 -4
- package/src/features/chat/components/conversation/chat-message-list.container.test.tsx +64 -6
- package/src/features/chat/components/conversation/chat-message-list.container.tsx +185 -17
- package/src/features/chat/components/session/session-context-icon.tsx +1 -4
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +3 -1
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +74 -2
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +32 -10
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +20 -0
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +25 -0
- package/src/features/chat/managers/ncp-chat-input.manager.ts +5 -1
- package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
- package/src/features/chat/pages/ncp-chat-page.tsx +15 -11
- package/src/features/chat/stores/chat-thread.store.ts +8 -2
- package/src/features/chat/utils/chat-context-window-indicator.utils.ts +50 -0
- package/src/features/chat/utils/chat-runtime.utils.ts +1 -1
- package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
- package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
- package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +165 -0
- package/src/features/chat/utils/ncp-chat-runtime-availability.utils.ts +50 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +27 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -4
- package/src/features/chat/utils/ncp-session-context-metadata.utils.ts +121 -0
- package/src/features/chat/utils/session-context.utils.ts +1 -2
- package/src/features/system-status/components/config/runtime-config-editor.tsx +6 -0
- package/src/features/system-status/components/config/runtime-settings-card.tsx +12 -0
- package/src/features/system-status/components/desktop-update-config.test.tsx +17 -7
- package/src/features/system-status/components/desktop-update-config.tsx +75 -30
- package/src/features/system-status/hooks/use-system-status.ts +0 -11
- package/src/features/system-status/index.ts +4 -1
- package/src/features/system-status/managers/runtime-update.manager.ts +330 -0
- package/src/features/system-status/managers/system-status.manager.test.ts +0 -25
- package/src/features/system-status/managers/system-status.manager.ts +1 -30
- package/src/features/system-status/stores/runtime-update.store.ts +24 -0
- package/src/features/system-status/types/system-status.types.ts +0 -2
- package/src/features/system-status/utils/runtime-config-agent.utils.ts +6 -1
- package/src/features/system-status/utils/system-status.utils.test.ts +1 -85
- package/src/features/system-status/utils/system-status.utils.ts +1 -23
- package/src/platforms/desktop/managers/desktop-update.manager.ts +6 -0
- package/src/platforms/desktop/types/desktop-update.types.ts +21 -19
- package/src/shared/components/common/brand-header.test.tsx +142 -0
- package/src/shared/components/common/brand-header.tsx +93 -0
- package/src/shared/components/cron-config.tsx +1 -1
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +1 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +1 -1
- package/src/shared/components/search-config.tsx +3 -3
- package/src/shared/lib/api/README.md +3 -0
- package/src/shared/lib/api/index.ts +13 -11
- package/src/shared/lib/api/ncp-session.test.ts +17 -18
- package/src/shared/lib/api/ncp-session.types.ts +92 -0
- package/src/shared/lib/api/raw-client.utils.ts +3 -126
- package/src/shared/lib/api/services/agents.service.ts +18 -0
- package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
- package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
- package/src/shared/lib/api/services/config.service.ts +171 -0
- package/src/shared/lib/api/services/marketplace.service.ts +66 -0
- package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
- package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
- package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
- package/src/shared/lib/api/services/remote.service.ts +50 -0
- package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
- package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
- package/src/shared/lib/api/services/server-path.service.ts +16 -0
- package/src/shared/lib/api/types.ts +9 -74
- package/src/shared/lib/i18n/{chat.ts → chat-labels.utils.ts} +13 -1
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +65 -0
- package/src/shared/lib/i18n/index.ts +4 -5
- package/src/shared/lib/i18n/runtime/i18n-language-owner.ts +5 -5
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/local-transport.service.ts +24 -4
- package/src/shared/lib/transport/remote-transport.service.ts +2 -2
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
- package/src/shared/lib/transport/transport.types.ts +8 -2
- package/src/shared/lib/ui-document-title/index.ts +1 -1
- package/tsconfig.json +1 -0
- package/dist/assets/api-BurjmW4A.js +0 -15
- package/dist/assets/app-manager-provider-DhxUmyTv.js +0 -1
- package/dist/assets/app-navigation.config-Bpd16Pem.js +0 -1
- package/dist/assets/chat-page-Cc7n80lW.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-24FLdHl7.js +0 -3
- package/dist/assets/desktop-update-config-fMLlSStv.js +0 -1
- package/dist/assets/doc-browser-COj7x090.js +0 -1
- package/dist/assets/doc-browser-fyn7eDTp.js +0 -1
- package/dist/assets/i18n-CM4y8Mw9.js +0 -1
- package/dist/assets/index-CtVSzMPM.js +0 -2
- package/dist/assets/index-N3hjuljD.css +0 -1
- package/dist/assets/loader-circle-R23uEPkM.js +0 -1
- package/dist/assets/marketplace-page-mF-M5mku.js +0 -1
- package/dist/assets/mcp-marketplace-page-BArKWcRZ.js +0 -40
- package/dist/assets/mcp-marketplace-page-DBUcIIHJ.js +0 -1
- package/dist/assets/message-square-Dm34zD6k.js +0 -1
- package/dist/assets/play-ul4L6MWm.js +0 -1
- package/dist/assets/plus-D14303DH.js +0 -1
- package/dist/assets/remote-B4ELSd3u.js +0 -1
- package/dist/assets/runtime-config-page-N4FP6H0M.js +0 -1
- package/dist/assets/search-config-B62TY-z2.js +0 -1
- package/dist/assets/skeleton-BCPi52jT.js +0 -1
- package/dist/assets/theme-provider-WTWq_jYq.js +0 -1
- package/dist/assets/use-config-CyvhbRhf.js +0 -1
- package/dist/assets/x-tYcSDsrY.js +0 -1
- package/src/shared/lib/api/agents.ts +0 -34
- package/src/shared/lib/api/channel-auth.ts +0 -35
- package/src/shared/lib/api/config.ts +0 -362
- package/src/shared/lib/api/marketplace.ts +0 -156
- package/src/shared/lib/api/mcp-marketplace.ts +0 -138
- package/src/shared/lib/api/ncp-attachments.ts +0 -41
- package/src/shared/lib/api/ncp-session.ts +0 -78
- package/src/shared/lib/api/remote.ts +0 -86
- package/src/shared/lib/api/runtime-control.ts +0 -34
- package/src/shared/lib/api/server-path.ts +0 -46
- /package/dist/assets/{config-hints-CPNzbMEp.js → config-hints-MogHYQ8G.js} +0 -0
|
@@ -22,6 +22,7 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
|
|
|
22
22
|
import type { ChatSessionListManager } from '@/features/chat/managers/chat-session-list.manager';
|
|
23
23
|
import { ChatSessionPreferenceSync } from '@/features/chat/managers/chat-session-preference-sync.manager';
|
|
24
24
|
import { isNcpChatSendDisabled } from '@/features/chat/utils/ncp-chat-input-availability.utils';
|
|
25
|
+
import { isNcpChatRuntimeBlocked } from '@/features/chat/utils/ncp-chat-runtime-availability.utils';
|
|
25
26
|
import { chatRecentModelsManager } from '@/features/chat/managers/chat-recent-models.manager';
|
|
26
27
|
import { chatRecentSkillsManager } from '@/features/chat/managers/chat-recent-skills.manager';
|
|
27
28
|
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
@@ -67,6 +68,9 @@ export class NcpChatInputManager {
|
|
|
67
68
|
private isSameStringArray = (left: string[], right: string[]): boolean =>
|
|
68
69
|
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
69
70
|
|
|
71
|
+
private isRuntimeBlockedForSend = (): boolean =>
|
|
72
|
+
isNcpChatRuntimeBlocked(systemStatusManager.getStatusView());
|
|
73
|
+
|
|
70
74
|
private syncComposerSnapshot = (nodes: ChatComposerNode[]) => {
|
|
71
75
|
const currentAttachments = useChatInputStore.getState().snapshot.attachments;
|
|
72
76
|
const attachments = pruneComposerAttachments(nodes, currentAttachments);
|
|
@@ -184,7 +188,7 @@ export class NcpChatInputManager {
|
|
|
184
188
|
isNcpChatSendDisabled({
|
|
185
189
|
snapshot: inputSnapshot,
|
|
186
190
|
hasSendableDraft: hasSendableContent,
|
|
187
|
-
isRuntimeBlocked:
|
|
191
|
+
isRuntimeBlocked: this.isRuntimeBlockedForSend(),
|
|
188
192
|
})
|
|
189
193
|
) {
|
|
190
194
|
return;
|
|
@@ -2,8 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import type { SessionEntryView, ThinkingLevel } from '@/shared/lib/api';
|
|
3
3
|
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
resolveRecentSessionPreferredThinking,
|
|
5
|
+
resolveRecentSessionPreferredValue,
|
|
7
6
|
resolveSelectedModelValue,
|
|
8
7
|
resolveSelectedThinkingLevelValue
|
|
9
8
|
} from '@/features/chat/utils/chat-session-preference-governance.utils';
|
|
@@ -25,6 +24,18 @@ const modelOptions: ChatModelOption[] = [
|
|
|
25
24
|
modelLabel: 'gpt-5',
|
|
26
25
|
providerLabel: 'OpenAI',
|
|
27
26
|
thinkingCapability: null
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: 'minimax/MiniMax-M2.7',
|
|
30
|
+
modelLabel: 'MiniMax-M2.7',
|
|
31
|
+
providerLabel: 'MiniMax',
|
|
32
|
+
thinkingCapability: null
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'deepseek/deepseek-v4-flash',
|
|
36
|
+
modelLabel: 'deepseek-v4-flash',
|
|
37
|
+
providerLabel: 'DeepSeek',
|
|
38
|
+
thinkingCapability: null
|
|
28
39
|
}
|
|
29
40
|
];
|
|
30
41
|
|
|
@@ -270,10 +281,11 @@ describe('resolveRecentSessionPreferredModel', () => {
|
|
|
270
281
|
];
|
|
271
282
|
|
|
272
283
|
expect(
|
|
273
|
-
|
|
284
|
+
resolveRecentSessionPreferredValue<string>({
|
|
274
285
|
sessions,
|
|
275
286
|
selectedSessionKey: 'draft',
|
|
276
|
-
sessionType: 'codex'
|
|
287
|
+
sessionType: 'codex',
|
|
288
|
+
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
277
289
|
})
|
|
278
290
|
).toBe('openai/gpt-5');
|
|
279
291
|
});
|
|
@@ -300,10 +312,11 @@ describe('resolveRecentSessionPreferredModel', () => {
|
|
|
300
312
|
];
|
|
301
313
|
|
|
302
314
|
expect(
|
|
303
|
-
|
|
315
|
+
resolveRecentSessionPreferredValue<string>({
|
|
304
316
|
sessions,
|
|
305
317
|
selectedSessionKey: 'codex-current',
|
|
306
|
-
sessionType: 'codex'
|
|
318
|
+
sessionType: 'codex',
|
|
319
|
+
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
307
320
|
})
|
|
308
321
|
).toBe('anthropic/claude-sonnet-4');
|
|
309
322
|
});
|
|
@@ -384,10 +397,11 @@ describe('resolveRecentSessionPreferredThinking', () => {
|
|
|
384
397
|
];
|
|
385
398
|
|
|
386
399
|
expect(
|
|
387
|
-
|
|
400
|
+
resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
388
401
|
sessions,
|
|
389
402
|
selectedSessionKey: 'draft',
|
|
390
|
-
sessionType: 'codex'
|
|
403
|
+
sessionType: 'codex',
|
|
404
|
+
readPreference: (session) => session.preferredThinking ?? undefined
|
|
391
405
|
})
|
|
392
406
|
).toBe('high');
|
|
393
407
|
});
|
|
@@ -32,10 +32,8 @@ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-lis
|
|
|
32
32
|
import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
|
|
33
33
|
import { useAgents } from "@/shared/hooks/use-agents";
|
|
34
34
|
import { normalizeRequestedSkills } from "@/features/chat/utils/chat-runtime.utils";
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
useChatRuntimeAvailability,
|
|
38
|
-
} from "@/features/system-status";
|
|
35
|
+
import { useSystemStatus } from "@/features/system-status";
|
|
36
|
+
import { isNcpChatRuntimeBlocked, resolveNcpChatSendErrorMessage } from "@/features/chat/utils/ncp-chat-runtime-availability.utils";
|
|
39
37
|
import {
|
|
40
38
|
getSessionProjectName,
|
|
41
39
|
normalizeSessionProjectRootValue,
|
|
@@ -126,7 +124,8 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
|
|
|
126
124
|
const currentSelectedModel = useChatInputStore(
|
|
127
125
|
(state) => state.snapshot.selectedModel,
|
|
128
126
|
);
|
|
129
|
-
const
|
|
127
|
+
const systemStatus = useSystemStatus();
|
|
128
|
+
const isRuntimeBlocked = isNcpChatRuntimeBlocked(systemStatus);
|
|
130
129
|
const agentsQuery = useAgents();
|
|
131
130
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
132
131
|
const location = useLocation();
|
|
@@ -163,7 +162,8 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
|
|
|
163
162
|
selectedAgentId,
|
|
164
163
|
pendingProjectRoot,
|
|
165
164
|
pendingProjectRootSessionKey,
|
|
166
|
-
|
|
165
|
+
systemStatus,
|
|
166
|
+
isRuntimeBlocked,
|
|
167
167
|
agentsQuery,
|
|
168
168
|
confirm,
|
|
169
169
|
ConfirmDialog,
|
|
@@ -186,7 +186,8 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
|
|
|
186
186
|
agentsQuery,
|
|
187
187
|
hasSessionProjectRootOverride,
|
|
188
188
|
pendingProjectRoot,
|
|
189
|
-
|
|
189
|
+
isRuntimeBlocked,
|
|
190
|
+
systemStatus,
|
|
190
191
|
selectedAgentId,
|
|
191
192
|
selectedSession,
|
|
192
193
|
selectedSessionType,
|
|
@@ -204,7 +205,7 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
|
|
|
204
205
|
const rawLastSendError =
|
|
205
206
|
agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
206
207
|
const filteredLastSendError =
|
|
207
|
-
|
|
208
|
+
systemStatus.phase === "ready" &&
|
|
208
209
|
isNcpAgentStartupUnavailableErrorMessage(rawLastSendError)
|
|
209
210
|
? null
|
|
210
211
|
: rawLastSendError;
|
|
@@ -231,11 +232,14 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
|
|
|
231
232
|
canStopCurrentRun: agent.isRunning,
|
|
232
233
|
stopDisabledReason: agent.isRunning ? null : "__preparing__",
|
|
233
234
|
lastSendError:
|
|
234
|
-
|
|
235
|
+
isRuntimeBlocked
|
|
235
236
|
? null
|
|
236
|
-
:
|
|
237
|
+
: systemStatus.phase === "ready"
|
|
237
238
|
? filteredLastSendError
|
|
238
|
-
:
|
|
239
|
+
: resolveNcpChatSendErrorMessage({
|
|
240
|
+
message: filteredLastSendError,
|
|
241
|
+
status: systemStatus,
|
|
242
|
+
}),
|
|
239
243
|
...derivedState,
|
|
240
244
|
};
|
|
241
245
|
}
|
|
@@ -2,7 +2,11 @@ import { create } from 'zustand';
|
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
3
|
import type { NcpMessage } from '@nextclaw/ncp';
|
|
4
4
|
import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AgentProfileView,
|
|
7
|
+
SessionContextWindowView,
|
|
8
|
+
SessionTypeIconView
|
|
9
|
+
} from '@/shared/lib/api';
|
|
6
10
|
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
7
11
|
|
|
8
12
|
export type ChatChildSessionTab = {
|
|
@@ -58,6 +62,7 @@ export type ChatThreadSnapshot = {
|
|
|
58
62
|
activeChildSessionKey?: string | null;
|
|
59
63
|
workspaceFileTabs: ChatWorkspaceFileTab[];
|
|
60
64
|
activeWorkspaceFileKey?: string | null;
|
|
65
|
+
contextWindow?: SessionContextWindowView | null;
|
|
61
66
|
};
|
|
62
67
|
|
|
63
68
|
type ChatThreadStore = {
|
|
@@ -93,7 +98,8 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
93
98
|
childSessionTabs: [],
|
|
94
99
|
activeChildSessionKey: null,
|
|
95
100
|
workspaceFileTabs: [],
|
|
96
|
-
activeWorkspaceFileKey: null
|
|
101
|
+
activeWorkspaceFileKey: null,
|
|
102
|
+
contextWindow: null
|
|
97
103
|
};
|
|
98
104
|
|
|
99
105
|
export const useChatThreadStore = create<ChatThreadStore>((set) => ({
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ChatContextWindowIndicator } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import type { SessionContextWindowView } from '@/shared/lib/api';
|
|
3
|
+
import { t } from '@/shared/lib/i18n';
|
|
4
|
+
|
|
5
|
+
function formatTokenCount(value: number): string {
|
|
6
|
+
if (value >= 1_000_000) {
|
|
7
|
+
return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}M`;
|
|
8
|
+
}
|
|
9
|
+
if (value >= 1_000) {
|
|
10
|
+
return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
|
11
|
+
}
|
|
12
|
+
return String(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildChatContextWindowIndicator(
|
|
16
|
+
contextWindow: SessionContextWindowView | null | undefined
|
|
17
|
+
): ChatContextWindowIndicator | null {
|
|
18
|
+
if (!contextWindow || contextWindow.totalContextTokens <= 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const ratio = contextWindow.usedContextTokens / contextWindow.totalContextTokens;
|
|
22
|
+
const clampedRatio = Math.max(0, Math.min(1, ratio));
|
|
23
|
+
const percentLabel = `${Math.round(clampedRatio * 100)}%`;
|
|
24
|
+
const tone: ChatContextWindowIndicator['tone'] =
|
|
25
|
+
clampedRatio >= 0.9 ? 'danger' : clampedRatio >= 0.75 ? 'warning' : 'neutral';
|
|
26
|
+
const details: ChatContextWindowIndicator['details'] = [
|
|
27
|
+
{ label: t('chatContextWindowUsed'), value: formatTokenCount(contextWindow.usedContextTokens) },
|
|
28
|
+
{ label: t('chatContextWindowTotal'), value: formatTokenCount(contextWindow.totalContextTokens) },
|
|
29
|
+
{ label: t('chatContextWindowAvailable'), value: formatTokenCount(contextWindow.availableContextTokens) }
|
|
30
|
+
];
|
|
31
|
+
if (contextWindow.prunedUsedContextTokens !== contextWindow.usedContextTokens) {
|
|
32
|
+
details.push({
|
|
33
|
+
label: t('chatContextWindowPruned'),
|
|
34
|
+
value: formatTokenCount(contextWindow.prunedUsedContextTokens)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (contextWindow.droppedHistoryCount > 0) {
|
|
38
|
+
details.push({ label: t('chatContextWindowDroppedHistory'), value: String(contextWindow.droppedHistoryCount) });
|
|
39
|
+
}
|
|
40
|
+
if (contextWindow.truncatedToolResultCount > 0) {
|
|
41
|
+
details.push({ label: t('chatContextWindowTruncatedTools'), value: String(contextWindow.truncatedToolResultCount) });
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
label: t('chatContextWindow'),
|
|
45
|
+
percentLabel,
|
|
46
|
+
ratio: clampedRatio,
|
|
47
|
+
tone,
|
|
48
|
+
details
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -179,7 +179,7 @@ class HistoryMessageBuilder {
|
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
private appendAssistantMessage = (message: SessionMessageView): void => {
|
|
182
|
-
const timestamp = message
|
|
182
|
+
const { timestamp } = message;
|
|
183
183
|
const text = extractMessageText(message.content).trim();
|
|
184
184
|
if (text) {
|
|
185
185
|
this.appendAssistantText(timestamp, text);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
5
|
+
import { useSyncSelectedModel } from '@/features/chat/utils/chat-session-preference-governance.utils';
|
|
6
|
+
|
|
7
|
+
const modelOptions: ChatModelOption[] = [
|
|
8
|
+
{
|
|
9
|
+
value: 'minimax/MiniMax-M2.7',
|
|
10
|
+
modelLabel: 'MiniMax-M2.7',
|
|
11
|
+
providerLabel: 'MiniMax',
|
|
12
|
+
thinkingCapability: null
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: 'deepseek/deepseek-v4-flash',
|
|
16
|
+
modelLabel: 'deepseek-v4-flash',
|
|
17
|
+
providerLabel: 'DeepSeek',
|
|
18
|
+
thinkingCapability: null
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: 'openai/gpt-5',
|
|
22
|
+
modelLabel: 'gpt-5',
|
|
23
|
+
providerLabel: 'OpenAI',
|
|
24
|
+
thinkingCapability: null
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
type HookProps = {
|
|
29
|
+
fallbackPreferredModel?: string;
|
|
30
|
+
defaultModel: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('useSyncSelectedModel', () => {
|
|
34
|
+
it('replaces an auto-selected global default with the later-arriving recent same-runtime model for a fresh draft session', async () => {
|
|
35
|
+
const initialProps: HookProps = {
|
|
36
|
+
fallbackPreferredModel: undefined,
|
|
37
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
38
|
+
};
|
|
39
|
+
const { result, rerender } = renderHook(
|
|
40
|
+
(props: HookProps) => {
|
|
41
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
42
|
+
useSyncSelectedModel({
|
|
43
|
+
modelOptions,
|
|
44
|
+
selectedSessionKey: 'draft-session',
|
|
45
|
+
selectedSessionExists: false,
|
|
46
|
+
fallbackPreferredModel: props.fallbackPreferredModel,
|
|
47
|
+
defaultModel: props.defaultModel,
|
|
48
|
+
setSelectedModel
|
|
49
|
+
});
|
|
50
|
+
return selectedModel;
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
initialProps
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(result.current).toBe('minimax/MiniMax-M2.7');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
rerender({
|
|
62
|
+
fallbackPreferredModel: 'deepseek/deepseek-v4-flash',
|
|
63
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(result.current).toBe('deepseek/deepseek-v4-flash');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not override a manual model selection when recent same-runtime model data arrives later', async () => {
|
|
72
|
+
const initialProps: HookProps = {
|
|
73
|
+
fallbackPreferredModel: undefined,
|
|
74
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
75
|
+
};
|
|
76
|
+
const { result, rerender } = renderHook(
|
|
77
|
+
(props: HookProps) => {
|
|
78
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
79
|
+
useSyncSelectedModel({
|
|
80
|
+
modelOptions,
|
|
81
|
+
selectedSessionKey: 'draft-session',
|
|
82
|
+
selectedSessionExists: false,
|
|
83
|
+
fallbackPreferredModel: props.fallbackPreferredModel,
|
|
84
|
+
defaultModel: props.defaultModel,
|
|
85
|
+
setSelectedModel
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
selectedModel,
|
|
89
|
+
setSelectedModel
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
initialProps
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(result.current.selectedModel).toBe('minimax/MiniMax-M2.7');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
result.current.setSelectedModel('openai/gpt-5');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
rerender({
|
|
106
|
+
fallbackPreferredModel: 'deepseek/deepseek-v4-flash',
|
|
107
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(result.current.selectedModel).toBe('openai/gpt-5');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -160,37 +160,14 @@ export function resolveRecentSessionPreferredValue<T>(params: {
|
|
|
160
160
|
return bestValue;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
selectedSessionKey?: string | null;
|
|
166
|
-
sessionType?: string | null;
|
|
167
|
-
}): string | undefined {
|
|
168
|
-
const { sessions, selectedSessionKey, sessionType } = params;
|
|
169
|
-
return resolveRecentSessionPreferredValue<string>({
|
|
170
|
-
sessions,
|
|
171
|
-
selectedSessionKey,
|
|
172
|
-
sessionType,
|
|
173
|
-
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function resolveRecentSessionPreferredThinking(params: {
|
|
178
|
-
sessions: readonly SessionEntryView[];
|
|
179
|
-
selectedSessionKey?: string | null;
|
|
180
|
-
sessionType?: string | null;
|
|
181
|
-
}): ThinkingLevel | undefined {
|
|
182
|
-
const { sessions, selectedSessionKey, sessionType } = params;
|
|
183
|
-
return resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
184
|
-
sessions,
|
|
185
|
-
selectedSessionKey,
|
|
186
|
-
sessionType,
|
|
187
|
-
readPreference: (session) => session.preferredThinking ?? undefined
|
|
188
|
-
});
|
|
163
|
+
function buildSyncKey(parts: unknown[]): string {
|
|
164
|
+
return parts.map((part) => (part == null ? '' : String(part))).join('\u0002');
|
|
189
165
|
}
|
|
190
166
|
|
|
191
167
|
type UseSyncSessionPreferenceParams<T> = {
|
|
192
168
|
isPreferenceAvailable: boolean;
|
|
193
169
|
emptyValue: T;
|
|
170
|
+
syncKey: string;
|
|
194
171
|
selectedSessionKey?: string | null;
|
|
195
172
|
selectedSessionExists?: boolean;
|
|
196
173
|
setValue: Dispatch<SetStateAction<T>>;
|
|
@@ -201,6 +178,7 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
|
|
|
201
178
|
const {
|
|
202
179
|
isPreferenceAvailable,
|
|
203
180
|
emptyValue,
|
|
181
|
+
syncKey,
|
|
204
182
|
selectedSessionKey,
|
|
205
183
|
selectedSessionExists = false,
|
|
206
184
|
setValue,
|
|
@@ -208,27 +186,31 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
|
|
|
208
186
|
} = params;
|
|
209
187
|
const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
|
|
210
188
|
const resolveValueRef = useRef(resolveValue);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
resolveValueRef.current = resolveValue;
|
|
214
|
-
}, [resolveValue]);
|
|
189
|
+
const lastSyncedValueRef = useRef<T>(emptyValue);
|
|
190
|
+
resolveValueRef.current = resolveValue;
|
|
215
191
|
|
|
216
192
|
useEffect(() => {
|
|
217
193
|
const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
|
|
218
194
|
if (!isPreferenceAvailable) {
|
|
219
195
|
setValue(emptyValue);
|
|
196
|
+
lastSyncedValueRef.current = emptyValue;
|
|
220
197
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
221
198
|
return;
|
|
222
199
|
}
|
|
223
|
-
setValue((prev) =>
|
|
224
|
-
resolveValueRef.current({
|
|
225
|
-
currentValue:
|
|
200
|
+
setValue((prev) => {
|
|
201
|
+
const next = resolveValueRef.current({
|
|
202
|
+
currentValue:
|
|
203
|
+
!sessionChanged && Object.is(prev, lastSyncedValueRef.current)
|
|
204
|
+
? emptyValue
|
|
205
|
+
: prev,
|
|
226
206
|
sessionChanged,
|
|
227
207
|
preserveCurrentValueOnSessionChange: sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
|
|
228
|
-
})
|
|
229
|
-
|
|
208
|
+
});
|
|
209
|
+
lastSyncedValueRef.current = next;
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
230
212
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
231
|
-
}, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue]);
|
|
213
|
+
}, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue, syncKey]);
|
|
232
214
|
}
|
|
233
215
|
|
|
234
216
|
export function useSyncSelectedModel(params: {
|
|
@@ -244,6 +226,12 @@ export function useSyncSelectedModel(params: {
|
|
|
244
226
|
useSyncSessionPreference<string>({
|
|
245
227
|
isPreferenceAvailable: modelOptions.length > 0,
|
|
246
228
|
emptyValue: '',
|
|
229
|
+
syncKey: buildSyncKey([
|
|
230
|
+
modelOptions.map((option) => option.value).join('\u0001'),
|
|
231
|
+
selectedSessionPreferredModel,
|
|
232
|
+
fallbackPreferredModel,
|
|
233
|
+
defaultModel
|
|
234
|
+
]),
|
|
247
235
|
selectedSessionKey,
|
|
248
236
|
selectedSessionExists,
|
|
249
237
|
setValue: setSelectedModel,
|
|
@@ -273,6 +261,12 @@ export function useSyncSelectedThinking(params: {
|
|
|
273
261
|
useSyncSessionPreference<ThinkingLevel | null>({
|
|
274
262
|
isPreferenceAvailable: supportedThinkingLevels.length > 0,
|
|
275
263
|
emptyValue: null,
|
|
264
|
+
syncKey: buildSyncKey([
|
|
265
|
+
supportedThinkingLevels.join('\u0001'),
|
|
266
|
+
selectedSessionPreferredThinking,
|
|
267
|
+
fallbackPreferredThinking,
|
|
268
|
+
defaultThinkingLevel
|
|
269
|
+
]),
|
|
276
270
|
selectedSessionKey,
|
|
277
271
|
selectedSessionExists,
|
|
278
272
|
setValue: setSelectedThinkingLevel,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { t } from '@/shared/lib/i18n';
|
|
3
|
+
import type { SystemStatusView } from '@/features/system-status';
|
|
4
|
+
import {
|
|
5
|
+
isNcpChatRuntimeBlocked,
|
|
6
|
+
resolveNcpChatRuntimeMessage,
|
|
7
|
+
resolveNcpChatSendErrorMessage,
|
|
8
|
+
} from './ncp-chat-runtime-availability.utils';
|
|
9
|
+
|
|
10
|
+
function createStatus(overrides: Partial<SystemStatusView> = {}): SystemStatusView {
|
|
11
|
+
return {
|
|
12
|
+
lifecyclePhase: 'ready',
|
|
13
|
+
phase: 'ready',
|
|
14
|
+
connectionStatus: 'connected',
|
|
15
|
+
hasReachedReady: true,
|
|
16
|
+
lastReadyAt: Date.now(),
|
|
17
|
+
recoveryStartedAt: null,
|
|
18
|
+
bootstrapStatus: {
|
|
19
|
+
phase: 'ready',
|
|
20
|
+
ncpAgent: {
|
|
21
|
+
state: 'ready',
|
|
22
|
+
},
|
|
23
|
+
pluginHydration: {
|
|
24
|
+
state: 'ready',
|
|
25
|
+
loadedPluginCount: 1,
|
|
26
|
+
totalPluginCount: 1,
|
|
27
|
+
},
|
|
28
|
+
channels: {
|
|
29
|
+
state: 'ready',
|
|
30
|
+
enabled: [],
|
|
31
|
+
},
|
|
32
|
+
remote: {
|
|
33
|
+
state: 'pending',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
lastError: null,
|
|
37
|
+
lastTransportError: null,
|
|
38
|
+
runtimeControlView: null,
|
|
39
|
+
runtimeControlError: null,
|
|
40
|
+
activeSystemAction: null,
|
|
41
|
+
lastSystemActionError: null,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('ncp-chat-runtime-availability.utils', () => {
|
|
47
|
+
it('allows chat send when the NCP agent is ready even if the aggregate phase is stalled', () => {
|
|
48
|
+
expect(isNcpChatRuntimeBlocked(createStatus())).toBe(false);
|
|
49
|
+
expect(
|
|
50
|
+
isNcpChatRuntimeBlocked(
|
|
51
|
+
createStatus({
|
|
52
|
+
lifecyclePhase: 'stalled',
|
|
53
|
+
phase: 'stalled',
|
|
54
|
+
connectionStatus: 'disconnected',
|
|
55
|
+
recoveryStartedAt: Date.now(),
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('blocks chat send while the NCP agent is not ready', () => {
|
|
62
|
+
expect(
|
|
63
|
+
isNcpChatRuntimeBlocked(
|
|
64
|
+
createStatus({
|
|
65
|
+
bootstrapStatus: null,
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses the startup message during cold start', () => {
|
|
72
|
+
expect(
|
|
73
|
+
resolveNcpChatRuntimeMessage(
|
|
74
|
+
createStatus({
|
|
75
|
+
lifecyclePhase: 'cold-starting',
|
|
76
|
+
phase: 'cold-starting',
|
|
77
|
+
connectionStatus: 'connecting',
|
|
78
|
+
hasReachedReady: false,
|
|
79
|
+
lastReadyAt: null,
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
).toBe(t('chatRuntimeInitializing'));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses the bootstrap error when startup failed', () => {
|
|
86
|
+
expect(
|
|
87
|
+
resolveNcpChatRuntimeMessage(
|
|
88
|
+
createStatus({
|
|
89
|
+
lifecyclePhase: 'startup-failed',
|
|
90
|
+
phase: 'startup-failed',
|
|
91
|
+
connectionStatus: 'disconnected',
|
|
92
|
+
hasReachedReady: false,
|
|
93
|
+
lastReadyAt: null,
|
|
94
|
+
bootstrapStatus: {
|
|
95
|
+
phase: 'error',
|
|
96
|
+
ncpAgent: {
|
|
97
|
+
state: 'error',
|
|
98
|
+
error: 'boom',
|
|
99
|
+
},
|
|
100
|
+
pluginHydration: {
|
|
101
|
+
state: 'pending',
|
|
102
|
+
loadedPluginCount: 0,
|
|
103
|
+
totalPluginCount: 0,
|
|
104
|
+
},
|
|
105
|
+
channels: {
|
|
106
|
+
state: 'pending',
|
|
107
|
+
enabled: [],
|
|
108
|
+
},
|
|
109
|
+
remote: {
|
|
110
|
+
state: 'pending',
|
|
111
|
+
},
|
|
112
|
+
lastError: 'boom',
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
).toBe('boom');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('prefers the centralized action message while a system action is running', () => {
|
|
120
|
+
expect(
|
|
121
|
+
resolveNcpChatRuntimeMessage(
|
|
122
|
+
createStatus({
|
|
123
|
+
activeSystemAction: {
|
|
124
|
+
action: 'restart-service',
|
|
125
|
+
lifecycle: 'recovering',
|
|
126
|
+
serviceState: null,
|
|
127
|
+
message: 'NextClaw 正在恢复连接',
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
).toBe('NextClaw 正在恢复连接');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('maps transient chat errors to friendly recovery copy while recovering', () => {
|
|
135
|
+
expect(
|
|
136
|
+
resolveNcpChatSendErrorMessage({
|
|
137
|
+
message: 'NCP fetch failed for POST /api/ncp/agent: Error: Failed to fetch',
|
|
138
|
+
status: createStatus({
|
|
139
|
+
lifecyclePhase: 'recovering',
|
|
140
|
+
phase: 'recovering',
|
|
141
|
+
connectionStatus: 'connecting',
|
|
142
|
+
recoveryStartedAt: Date.now(),
|
|
143
|
+
lastError: 'Failed to fetch',
|
|
144
|
+
lastTransportError: 'Failed to fetch',
|
|
145
|
+
}),
|
|
146
|
+
})
|
|
147
|
+
).toBe(t('runtimeControlRecoveringHelp'));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('suppresses transient transport errors after recovery stalls', () => {
|
|
151
|
+
expect(
|
|
152
|
+
resolveNcpChatSendErrorMessage({
|
|
153
|
+
message: 'NCP fetch failed for POST /api/ncp/agent: Error: Failed to fetch',
|
|
154
|
+
status: createStatus({
|
|
155
|
+
lifecyclePhase: 'stalled',
|
|
156
|
+
phase: 'stalled',
|
|
157
|
+
connectionStatus: 'disconnected',
|
|
158
|
+
recoveryStartedAt: Date.now(),
|
|
159
|
+
lastError: 'Failed to fetch',
|
|
160
|
+
lastTransportError: 'Failed to fetch',
|
|
161
|
+
}),
|
|
162
|
+
})
|
|
163
|
+
).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|