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