@nextclaw/ui 0.13.8 → 0.13.10-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/assets/{api-BCDb13Uv.js → api-CA2udGyv.js} +5 -5
- package/dist/assets/{app-presenter-provider-C-QMjK7m.js → app-presenter-provider-B9ChYida.js} +2 -2
- package/dist/assets/arrow-left-CI0Ocu9p.js +1 -0
- package/dist/assets/channels-list-page-PrT4a9g4.js +8 -0
- package/dist/assets/chat-page-CImtIhmN.js +105 -0
- package/dist/assets/{config-split-page-BwhGqmUS.js → config-split-page-bO0K6uQv.js} +1 -1
- package/dist/assets/{confirm-dialog-DBkKcQ1q.js → confirm-dialog-l4FYdzga.js} +1 -1
- package/dist/assets/{desktop-update-config-BE9KjiXi.js → desktop-update-config-p3rJIgRg.js} +1 -1
- package/dist/assets/{dist-BI0bX8jv.js → dist-DQoV-62W.js} +1 -1
- package/dist/assets/{dist-C8JRt-tf.js → dist-T6rf86Nf.js} +1 -1
- package/dist/assets/{doc-browser-eqdNEerh.js → doc-browser-CTFB6ACN.js} +1 -1
- package/dist/assets/{doc-browser-context-VHUOIUXL.js → doc-browser-context-CP2tI-Br.js} +1 -1
- package/dist/assets/doc-browser-yi02ASOL.js +1 -0
- package/dist/assets/{ellipsis-C9VB2CxT.js → ellipsis-B7CbNPNr.js} +1 -1
- package/dist/assets/{es2015-BCw3Q7nY.js → es2015-BSPOGpH4.js} +1 -1
- package/dist/assets/{external-link-Bp7_4s9t.js → external-link-CefT35zP.js} +1 -1
- package/dist/assets/{index-DTdPDqto.js → index-DFC_DdT1.js} +5 -5
- package/dist/assets/{key-round-Ccy6oND3.js → key-round-BpM3GZn4.js} +1 -1
- package/dist/assets/{loader-circle-OpH4EErP.js → loader-circle-_edjOFsw.js} +1 -1
- package/dist/assets/mcp-marketplace-page-DNHSmfDf.js +1 -0
- package/dist/assets/{mcp-marketplace-page-B-GxKOqS.js → mcp-marketplace-page-DhqcnhZc.js} +2 -2
- package/dist/assets/{model-config-BXPnV_7n.js → model-config-C9ydDDFh.js} +1 -1
- package/dist/assets/{notice-card-Dsbkl7u7.js → notice-card-BfXlhPjX.js} +1 -1
- package/dist/assets/play-DD0bg50i.js +1 -0
- package/dist/assets/plus-zZ2GC_AJ.js +1 -0
- package/dist/assets/{popover-BTLZE5Iz.js → popover-D_pJfOlM.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DKI9KN7N.js → provider-scoped-model-input-CkeEyTfO.js} +1 -1
- package/dist/assets/providers-list-9eOBQFUn.js +1 -0
- package/dist/assets/{react-BqS8oMyM.js → react-Bl8iJUcF.js} +1 -1
- package/dist/assets/{refresh-cw-BIaj7txb.js → refresh-cw-CqfS1EhK.js} +1 -1
- package/dist/assets/remote-fGlxrJI_.js +1 -0
- package/dist/assets/rotate-cw-C7EvaSlb.js +1 -0
- package/dist/assets/{runtime-config-page-DX_wSK1J.js → runtime-config-page-B30PC6bx.js} +1 -1
- package/dist/assets/{save-BEoORten.js → save-CQERCODg.js} +1 -1
- package/dist/assets/search-C0b2yzmt.js +1 -0
- package/dist/assets/{search-config-DA5By-tE.js → search-config-RN01uPJQ.js} +1 -1
- package/dist/assets/{secrets-config-eDxG7iN4.js → secrets-config-CTZ_TGoA.js} +2 -2
- package/dist/assets/{status-dot-oWBjTovo.js → status-dot-DY0xUUvK.js} +1 -1
- package/dist/assets/{tabs-CGaY0BVO.js → tabs-C77i1w90.js} +1 -1
- package/dist/assets/{tabs-custom-D2fPYb4G.js → tabs-custom-CFpR54-m.js} +1 -1
- package/dist/assets/{tag-chip-TVkgofS1.js → tag-chip-Dnb2VuWh.js} +1 -1
- package/dist/assets/{tooltip-BwHPKBv9.js → tooltip-fs7VQG7d.js} +1 -1
- package/dist/assets/{trash-2-D3psAGrz.js → trash-2-Bb8mTTLl.js} +1 -1
- package/dist/assets/x-BV4iiea8.js +1 -0
- package/dist/index.html +24 -24
- package/package.json +8 -8
- package/src/app/components/layout/runtime-status-entry.test.tsx +5 -0
- package/src/app/components/layout/runtime-status-entry.tsx +8 -0
- package/src/features/chat/components/conversation/chat-message-list.container.tsx +0 -6
- package/src/features/chat/components/workspace/session-cron-job-content.tsx +3 -3
- package/src/features/chat/hooks/use-chat-session-update.test.tsx +2 -1
- package/src/features/chat/managers/chat-session-preference-sync.manager.test.ts +2 -1
- package/src/features/chat/utils/chat-input-bar.utils.ts +0 -2
- package/src/features/chat/utils/chat-session-preference-governance.utils.ts +4 -1
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.test.tsx +2 -1
- package/src/features/panel-apps/components/panel-app-list-item.test.tsx +4 -0
- package/src/features/panel-apps/components/panel-apps-list.tsx +27 -1
- package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +124 -140
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +71 -53
- package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +4 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +4 -0
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +38 -5
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -3
- package/src/features/service-apps/stores/service-action-authorization.store.ts +7 -3
- package/src/features/system-status/components/desktop-update-config.test.tsx +2 -1
- package/src/features/system-status/hooks/use-system-status.ts +1 -4
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +137 -100
- package/src/features/system-status/managers/system-status.manager.ts +38 -20
- package/src/shared/components/cron-config.tsx +1 -5
- package/src/shared/lib/api/index.ts +1 -1
- package/src/shared/lib/api/{ncp-session-query-cache.test.ts → ncp-session-query-cache.utils.test.ts} +23 -0
- package/src/shared/lib/api/{ncp-session-query-cache.ts → ncp-session-query-cache.utils.ts} +9 -11
- package/src/shared/lib/api/types.ts +4 -4
- package/src/shared/lib/cadence/index.ts +7 -0
- package/src/shared/lib/cadence/services/adaptive-cadence.service.test.ts +95 -0
- package/src/shared/lib/cadence/services/adaptive-cadence.service.ts +120 -0
- package/src/shared/lib/cron/cron-job-view.utils.ts +0 -9
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +9 -3
- package/src/shared/lib/i18n/runtime/i18n-language-owner.test.ts +3 -2
- package/dist/assets/arrow-left-rIXFA2Zy.js +0 -1
- package/dist/assets/channels-list-page-DnqwsKGe.js +0 -8
- package/dist/assets/chat-page-CsQx20xL.js +0 -105
- package/dist/assets/doc-browser-lpF_pyx4.js +0 -1
- package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +0 -1
- package/dist/assets/play-CvgYhNbo.js +0 -1
- package/dist/assets/plus-DHcP-K1U.js +0 -1
- package/dist/assets/providers-list-Bko6B7dE.js +0 -1
- package/dist/assets/remote-DHn_b8QU.js +0 -1
- package/dist/assets/rotate-cw-DMqSkRJQ.js +0 -1
- package/dist/assets/search-COFh_SFI.js +0 -1
- package/dist/assets/x-CHAxzyp2.js +0 -1
|
@@ -4,11 +4,15 @@ import { createPanelAppRightPanelResourceTarget } from '@/features/right-panel-r
|
|
|
4
4
|
|
|
5
5
|
function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
6
6
|
return {
|
|
7
|
+
appId: 'demo',
|
|
8
|
+
clientDeclared: false,
|
|
9
|
+
clientGranted: false,
|
|
7
10
|
contentPath: '/api/panel-apps/demo/content',
|
|
8
11
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
9
12
|
favorite: false,
|
|
10
13
|
fileName: 'demo.html',
|
|
11
14
|
id: 'demo',
|
|
15
|
+
kind: 'single-file',
|
|
12
16
|
openCount: 0,
|
|
13
17
|
sizeBytes: 1024,
|
|
14
18
|
title: 'Demo App',
|
|
@@ -39,11 +39,16 @@ export function ServiceActionAuthorizationDialog() {
|
|
|
39
39
|
{pending ? (
|
|
40
40
|
<div className="space-y-3 rounded-md border border-gray-100 bg-gray-50 p-3 text-sm">
|
|
41
41
|
<AuthorizationField label={t('serviceActionAuthorizationSource')} value={pending.panelAppId} />
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
<div className="text-xs font-medium text-gray-500">
|
|
44
|
+
{t('serviceActionAuthorizationActions')}
|
|
45
|
+
</div>
|
|
46
|
+
<div className="max-h-64 space-y-2 overflow-auto">
|
|
47
|
+
{pending.actions.map((action) => (
|
|
48
|
+
<AuthorizationActionItem key={action.actionId} action={action} />
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
47
52
|
{pending.inputPreview ? (
|
|
48
53
|
<AuthorizationField label={t('serviceActionAuthorizationInput')} value={pending.inputPreview} />
|
|
49
54
|
) : null}
|
|
@@ -62,6 +67,34 @@ export function ServiceActionAuthorizationDialog() {
|
|
|
62
67
|
);
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
function AuthorizationActionItem({
|
|
71
|
+
action,
|
|
72
|
+
}: {
|
|
73
|
+
action: {
|
|
74
|
+
actionId: string;
|
|
75
|
+
actionTitle?: string;
|
|
76
|
+
actionDescription?: string;
|
|
77
|
+
risk?: string;
|
|
78
|
+
};
|
|
79
|
+
}) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="space-y-1 rounded border border-gray-200 bg-white px-3 py-2">
|
|
82
|
+
<div className="flex items-start justify-between gap-3">
|
|
83
|
+
<div className="min-w-0 break-words text-xs font-medium text-gray-900">
|
|
84
|
+
{action.actionTitle ?? action.actionId}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="shrink-0 rounded bg-amber-50 px-1.5 py-0.5 text-[11px] font-medium text-amber-700">
|
|
87
|
+
{action.risk ?? 'dangerous'}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="break-words text-[11px] text-gray-500">{action.actionId}</div>
|
|
91
|
+
{action.actionDescription ? (
|
|
92
|
+
<div className="break-words text-xs text-gray-600">{action.actionDescription}</div>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
function AuthorizationField({
|
|
66
99
|
label,
|
|
67
100
|
value,
|
|
@@ -77,9 +77,9 @@ export function ServiceAppsPanel({
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const apps = serviceApps.data?.entries ?? [];
|
|
81
|
-
const actions = serviceActions.data?.actions ?? [];
|
|
82
|
-
const grants = serviceActionGrants.data?.grants ?? [];
|
|
80
|
+
const apps: ServiceAppRecordView[] = serviceApps.data?.entries ?? [];
|
|
81
|
+
const actions: ServiceActionView[] = serviceActions.data?.actions ?? [];
|
|
82
|
+
const grants: ServiceActionGrantView[] = serviceActionGrants.data?.grants ?? [];
|
|
83
83
|
const discover = (appId: string) => {
|
|
84
84
|
void discoverServiceAppActions.mutateAsync(appId).then((result) => {
|
|
85
85
|
setDiscoveredActionsByApp((current) => ({
|
|
@@ -3,13 +3,17 @@ import type { ServiceActionListView } from '@nextclaw/client-sdk';
|
|
|
3
3
|
|
|
4
4
|
type ServiceActionRisk = ServiceActionListView['actions'][number]['risk'];
|
|
5
5
|
|
|
6
|
-
export type
|
|
7
|
-
id: string;
|
|
8
|
-
panelAppId: string;
|
|
6
|
+
export type ServiceActionAuthorizationItem = {
|
|
9
7
|
actionId: string;
|
|
10
8
|
actionTitle?: string;
|
|
11
9
|
actionDescription?: string;
|
|
12
10
|
risk?: ServiceActionRisk;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ServiceActionAuthorizationRequest = {
|
|
14
|
+
id: string;
|
|
15
|
+
panelAppId: string;
|
|
16
|
+
actions: ServiceActionAuthorizationItem[];
|
|
13
17
|
inputPreview?: string;
|
|
14
18
|
};
|
|
15
19
|
|
|
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
import { useRuntimeUpdateStore } from '@/features/system-status';
|
|
5
5
|
import { DesktopUpdateConfig } from '@/features/system-status/components/desktop-update-config';
|
|
6
6
|
import { setLanguage } from '@/shared/lib/i18n';
|
|
7
|
+
import type * as SystemStatusModule from '@/features/system-status';
|
|
7
8
|
|
|
8
9
|
const mocks = vi.hoisted(() => ({
|
|
9
10
|
start: vi.fn(),
|
|
@@ -16,7 +17,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
16
17
|
}));
|
|
17
18
|
|
|
18
19
|
vi.mock('@/features/system-status', async () => {
|
|
19
|
-
const actual = await vi.importActual<typeof
|
|
20
|
+
const actual = await vi.importActual<typeof SystemStatusModule>(
|
|
20
21
|
'@/features/system-status'
|
|
21
22
|
);
|
|
22
23
|
return {
|
|
@@ -17,10 +17,7 @@ export function useSystemStatusSources() {
|
|
|
17
17
|
timeoutMs: 5_000,
|
|
18
18
|
}),
|
|
19
19
|
refetchInterval: (query) => {
|
|
20
|
-
return systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
21
|
-
query.state.data,
|
|
22
|
-
query.state.fetchFailureCount
|
|
23
|
-
);
|
|
20
|
+
return systemStatusManager.getRuntimeBootstrapPollInterval(query.state.data);
|
|
24
21
|
},
|
|
25
22
|
retry: false,
|
|
26
23
|
});
|
|
@@ -1,17 +1,77 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { systemStatusManager } from './system-status.manager';
|
|
3
|
-
import {
|
|
3
|
+
import { appQueryClient } from '@/app-query-client';
|
|
4
|
+
import type { BootstrapStatusView } from '@/shared/lib/api';
|
|
5
|
+
|
|
6
|
+
const initializingBootstrapStatus: BootstrapStatusView = {
|
|
7
|
+
phase: 'shell-ready',
|
|
8
|
+
ncpAgent: {
|
|
9
|
+
state: 'running',
|
|
10
|
+
},
|
|
11
|
+
extensionLoading: {
|
|
12
|
+
state: 'pending',
|
|
13
|
+
loadedExtensionCount: 0,
|
|
14
|
+
totalExtensionCount: 0,
|
|
15
|
+
},
|
|
16
|
+
channels: {
|
|
17
|
+
state: 'pending',
|
|
18
|
+
enabled: [],
|
|
19
|
+
},
|
|
20
|
+
remote: {
|
|
21
|
+
state: 'pending',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const errorBootstrapStatus: BootstrapStatusView = {
|
|
26
|
+
phase: 'error',
|
|
27
|
+
ncpAgent: {
|
|
28
|
+
state: 'error',
|
|
29
|
+
error: 'startup failed',
|
|
30
|
+
},
|
|
31
|
+
extensionLoading: {
|
|
32
|
+
state: 'pending',
|
|
33
|
+
loadedExtensionCount: 0,
|
|
34
|
+
totalExtensionCount: 0,
|
|
35
|
+
},
|
|
36
|
+
channels: {
|
|
37
|
+
state: 'pending',
|
|
38
|
+
enabled: [],
|
|
39
|
+
},
|
|
40
|
+
remote: {
|
|
41
|
+
state: 'pending',
|
|
42
|
+
},
|
|
43
|
+
lastError: 'startup failed',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const readyBootstrapStatus: BootstrapStatusView = {
|
|
47
|
+
phase: 'ready',
|
|
48
|
+
ncpAgent: {
|
|
49
|
+
state: 'ready',
|
|
50
|
+
},
|
|
51
|
+
extensionLoading: {
|
|
52
|
+
state: 'ready',
|
|
53
|
+
loadedExtensionCount: 1,
|
|
54
|
+
totalExtensionCount: 1,
|
|
55
|
+
},
|
|
56
|
+
channels: {
|
|
57
|
+
state: 'ready',
|
|
58
|
+
enabled: [],
|
|
59
|
+
},
|
|
60
|
+
remote: {
|
|
61
|
+
state: 'disabled',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
4
64
|
|
|
5
65
|
describe('getRuntimeBootstrapPollInterval', () => {
|
|
6
66
|
beforeEach(() => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
67
|
+
vi.useFakeTimers();
|
|
68
|
+
systemStatusManager.resetForTests();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
systemStatusManager.resetForTests();
|
|
73
|
+
vi.restoreAllMocks();
|
|
74
|
+
vi.useRealTimers();
|
|
15
75
|
});
|
|
16
76
|
|
|
17
77
|
it('keeps polling while bootstrap status is missing', () => {
|
|
@@ -22,115 +82,92 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
22
82
|
|
|
23
83
|
it('keeps polling while ncp agent is still initializing', () => {
|
|
24
84
|
expect(
|
|
25
|
-
systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
state: 'running',
|
|
29
|
-
},
|
|
30
|
-
extensionLoading: {
|
|
31
|
-
state: 'pending',
|
|
32
|
-
loadedExtensionCount: 0,
|
|
33
|
-
totalExtensionCount: 0,
|
|
34
|
-
},
|
|
35
|
-
channels: {
|
|
36
|
-
state: 'pending',
|
|
37
|
-
enabled: [],
|
|
38
|
-
},
|
|
39
|
-
remote: {
|
|
40
|
-
state: 'pending',
|
|
41
|
-
},
|
|
42
|
-
})
|
|
85
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
86
|
+
initializingBootstrapStatus
|
|
87
|
+
)
|
|
43
88
|
).toBe(1000);
|
|
44
89
|
});
|
|
45
90
|
|
|
46
|
-
it('
|
|
91
|
+
it('starts hot backoff when bootstrap status reports an ncp agent error', () => {
|
|
92
|
+
systemStatusManager.reportBootstrapStatus(errorBootstrapStatus);
|
|
93
|
+
|
|
47
94
|
expect(
|
|
48
|
-
systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
49
|
-
|
|
50
|
-
ncpAgent: {
|
|
51
|
-
state: 'error',
|
|
52
|
-
error: 'startup failed',
|
|
53
|
-
},
|
|
54
|
-
extensionLoading: {
|
|
55
|
-
state: 'pending',
|
|
56
|
-
loadedExtensionCount: 0,
|
|
57
|
-
totalExtensionCount: 0,
|
|
58
|
-
},
|
|
59
|
-
channels: {
|
|
60
|
-
state: 'pending',
|
|
61
|
-
enabled: [],
|
|
62
|
-
},
|
|
63
|
-
remote: {
|
|
64
|
-
state: 'pending',
|
|
65
|
-
},
|
|
66
|
-
lastError: 'startup failed',
|
|
67
|
-
})
|
|
68
|
-
).toBe(2000);
|
|
95
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(errorBootstrapStatus)
|
|
96
|
+
).toBe(500);
|
|
69
97
|
});
|
|
70
98
|
|
|
71
99
|
it('stops polling once the ncp agent is ready', () => {
|
|
72
100
|
expect(
|
|
73
|
-
systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
74
|
-
phase: 'ready',
|
|
75
|
-
ncpAgent: {
|
|
76
|
-
state: 'ready',
|
|
77
|
-
},
|
|
78
|
-
extensionLoading: {
|
|
79
|
-
state: 'ready',
|
|
80
|
-
loadedExtensionCount: 1,
|
|
81
|
-
totalExtensionCount: 1,
|
|
82
|
-
},
|
|
83
|
-
channels: {
|
|
84
|
-
state: 'ready',
|
|
85
|
-
enabled: [],
|
|
86
|
-
},
|
|
87
|
-
remote: {
|
|
88
|
-
state: 'disabled',
|
|
89
|
-
},
|
|
90
|
-
})
|
|
101
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(readyBootstrapStatus)
|
|
91
102
|
).toBe(false);
|
|
92
103
|
});
|
|
93
104
|
|
|
94
105
|
it('keeps polling while the runtime is recovering even if the last bootstrap status was ready', () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...useSystemStatusStore.getState().state,
|
|
98
|
-
lifecyclePhase: 'recovering',
|
|
99
|
-
hasReachedReady: true,
|
|
100
|
-
lastReadyAt: Date.now(),
|
|
101
|
-
recoveryStartedAt: Date.now(),
|
|
102
|
-
},
|
|
103
|
-
});
|
|
106
|
+
systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
|
|
107
|
+
systemStatusManager.handleConnectionInterrupted('websocket error');
|
|
104
108
|
|
|
105
109
|
expect(
|
|
106
|
-
systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
107
|
-
|
|
108
|
-
ncpAgent: {
|
|
109
|
-
state: 'ready',
|
|
110
|
-
},
|
|
111
|
-
extensionLoading: {
|
|
112
|
-
state: 'ready',
|
|
113
|
-
loadedExtensionCount: 1,
|
|
114
|
-
totalExtensionCount: 1,
|
|
115
|
-
},
|
|
116
|
-
channels: {
|
|
117
|
-
state: 'ready',
|
|
118
|
-
enabled: [],
|
|
119
|
-
},
|
|
120
|
-
remote: {
|
|
121
|
-
state: 'disabled',
|
|
122
|
-
},
|
|
123
|
-
})
|
|
124
|
-
).toBe(1000);
|
|
110
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(readyBootstrapStatus)
|
|
111
|
+
).toBe(500);
|
|
125
112
|
});
|
|
126
113
|
|
|
127
114
|
it('backs off polling after transport failures', () => {
|
|
115
|
+
systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
|
|
116
|
+
expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
|
|
117
|
+
500
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
|
|
128
121
|
expect(
|
|
129
|
-
systemStatusManager.getRuntimeBootstrapPollInterval(undefined
|
|
130
|
-
).toBe(
|
|
122
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(undefined)
|
|
123
|
+
).toBe(1000);
|
|
124
|
+
|
|
125
|
+
systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
|
|
131
126
|
expect(
|
|
132
|
-
systemStatusManager.getRuntimeBootstrapPollInterval(undefined
|
|
133
|
-
).toBe(
|
|
127
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(undefined)
|
|
128
|
+
).toBe(2000);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('slows down after the hot recovery and long failure windows', async () => {
|
|
132
|
+
systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
|
|
133
|
+
|
|
134
|
+
await vi.advanceTimersByTimeAsync(31_000);
|
|
135
|
+
expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
|
|
136
|
+
10_000
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
|
|
140
|
+
systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
|
|
141
|
+
expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
|
|
142
|
+
30_000
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await vi.advanceTimersByTimeAsync(5 * 60_000);
|
|
146
|
+
expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
|
|
147
|
+
60_000
|
|
148
|
+
);
|
|
134
149
|
});
|
|
135
150
|
|
|
151
|
+
it('lets manual runtime probe trigger one immediate interval after readiness stopped polling', async () => {
|
|
152
|
+
const refetchQueriesSpy = vi
|
|
153
|
+
.spyOn(appQueryClient, 'refetchQueries')
|
|
154
|
+
.mockResolvedValue(undefined as never);
|
|
155
|
+
|
|
156
|
+
systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
|
|
157
|
+
expect(
|
|
158
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(readyBootstrapStatus)
|
|
159
|
+
).toBe(false);
|
|
160
|
+
|
|
161
|
+
systemStatusManager.requestRuntimeBootstrapProbeNow();
|
|
162
|
+
|
|
163
|
+
expect(refetchQueriesSpy).toHaveBeenCalledWith({
|
|
164
|
+
queryKey: ['runtime-bootstrap-status'],
|
|
165
|
+
});
|
|
166
|
+
expect(
|
|
167
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(readyBootstrapStatus)
|
|
168
|
+
).toBe(0);
|
|
169
|
+
expect(
|
|
170
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(readyBootstrapStatus)
|
|
171
|
+
).toBe(false);
|
|
172
|
+
});
|
|
136
173
|
});
|
|
@@ -22,12 +22,18 @@ import {
|
|
|
22
22
|
useSystemStatusStore,
|
|
23
23
|
} from '@/features/system-status/stores/system-status.store';
|
|
24
24
|
import { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
25
|
+
import { AdaptiveCadence } from '@/shared/lib/cadence';
|
|
25
26
|
|
|
26
27
|
const RECOVERY_TIMEOUT_MS = 30_000;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const RUNTIME_BOOTSTRAP_PROBE_CADENCE = {
|
|
29
|
+
idleDelayMs: 1_000,
|
|
30
|
+
manualTriggerDelayMs: 0,
|
|
31
|
+
successDelayMs: false,
|
|
32
|
+
stages: [
|
|
33
|
+
{ untilElapsedMs: 30_000, delaysMs: [500, 1_000, 2_000, 4_000, 5_000] },
|
|
34
|
+
{ untilElapsedMs: 5 * 60_000, delaysMs: [10_000, 15_000, 30_000] },
|
|
35
|
+
{ delaysMs: [60_000] },
|
|
36
|
+
],
|
|
31
37
|
} as const;
|
|
32
38
|
|
|
33
39
|
function getErrorMessage(error: unknown): string {
|
|
@@ -61,32 +67,29 @@ function resolveActionHelp(action: RuntimeControlAction): string {
|
|
|
61
67
|
|
|
62
68
|
export class SystemStatusManager {
|
|
63
69
|
private recoveryTimeoutId: number | null = null;
|
|
70
|
+
private readonly runtimeBootstrapProbeCadence = new AdaptiveCadence(
|
|
71
|
+
RUNTIME_BOOTSTRAP_PROBE_CADENCE,
|
|
72
|
+
);
|
|
64
73
|
|
|
65
74
|
getRuntimeBootstrapPollInterval = (
|
|
66
75
|
status: BootstrapStatusView | null | undefined,
|
|
67
|
-
fetchFailureCount = 0
|
|
68
76
|
): number | false => {
|
|
69
77
|
const { lifecyclePhase, activeSystemAction } = this.getState();
|
|
70
|
-
|
|
71
|
-
return Math.min(
|
|
72
|
-
RUNTIME_BOOTSTRAP_PROBE_POLICY.maxErrorPollIntervalMs,
|
|
73
|
-
RUNTIME_BOOTSTRAP_PROBE_POLICY.errorPollIntervalMs * fetchFailureCount
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (
|
|
78
|
+
const isRecovering =
|
|
77
79
|
lifecyclePhase === 'recovering' ||
|
|
78
80
|
lifecyclePhase === 'stalled' ||
|
|
79
|
-
activeSystemAction?.lifecycle === 'recovering'
|
|
80
|
-
) {
|
|
81
|
-
return
|
|
81
|
+
activeSystemAction?.lifecycle === 'recovering';
|
|
82
|
+
if (this.runtimeBootstrapProbeCadence.hasManualTrigger()) {
|
|
83
|
+
return this.runtimeBootstrapProbeCadence.getNextDelay({
|
|
84
|
+
consumeManualTrigger: true,
|
|
85
|
+
});
|
|
82
86
|
}
|
|
83
|
-
if (status?.ncpAgent.state === 'ready') {
|
|
87
|
+
if (!isRecovering && status?.ncpAgent.state === 'ready') {
|
|
84
88
|
return false;
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
return RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
|
|
90
|
+
return this.runtimeBootstrapProbeCadence.getNextDelay({
|
|
91
|
+
consumeManualTrigger: true,
|
|
92
|
+
});
|
|
90
93
|
};
|
|
91
94
|
|
|
92
95
|
getRuntimeControl = async (): Promise<RuntimeControlView> => {
|
|
@@ -137,6 +140,7 @@ export class SystemStatusManager {
|
|
|
137
140
|
}
|
|
138
141
|
const state = this.getState();
|
|
139
142
|
if (!state.hasReachedReady) {
|
|
143
|
+
this.runtimeBootstrapProbeCadence.recordFailure();
|
|
140
144
|
this.patchState({
|
|
141
145
|
lastTransportError: message,
|
|
142
146
|
});
|
|
@@ -150,6 +154,7 @@ export class SystemStatusManager {
|
|
|
150
154
|
const state = this.getState();
|
|
151
155
|
const normalizedMessage = message?.trim() || null;
|
|
152
156
|
if (!state.hasReachedReady) {
|
|
157
|
+
this.runtimeBootstrapProbeCadence.recordFailure();
|
|
153
158
|
if (normalizedMessage) {
|
|
154
159
|
this.patchState({
|
|
155
160
|
lastTransportError: normalizedMessage,
|
|
@@ -215,6 +220,7 @@ export class SystemStatusManager {
|
|
|
215
220
|
message: t('runtimeControlRecoveringHelp'),
|
|
216
221
|
},
|
|
217
222
|
});
|
|
223
|
+
this.runtimeBootstrapProbeCadence.requestManualTrigger();
|
|
218
224
|
|
|
219
225
|
const recoveredView = await this.waitForRecovery();
|
|
220
226
|
this.syncRuntimeControlQueryCache(recoveredView);
|
|
@@ -240,8 +246,16 @@ export class SystemStatusManager {
|
|
|
240
246
|
|
|
241
247
|
getStatusView = () => toSystemStatusView(this.getState());
|
|
242
248
|
|
|
249
|
+
requestRuntimeBootstrapProbeNow = (): void => {
|
|
250
|
+
this.runtimeBootstrapProbeCadence.requestManualTrigger();
|
|
251
|
+
void appQueryClient.refetchQueries({
|
|
252
|
+
queryKey: ['runtime-bootstrap-status'],
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
243
256
|
resetForTests = (): void => {
|
|
244
257
|
this.clearRecoveryTimeout();
|
|
258
|
+
this.runtimeBootstrapProbeCadence.reset();
|
|
245
259
|
useSystemStatusStore.setState({
|
|
246
260
|
state: initialSystemStatusState,
|
|
247
261
|
});
|
|
@@ -258,6 +272,7 @@ export class SystemStatusManager {
|
|
|
258
272
|
private transitionToColdStarting = (
|
|
259
273
|
bootstrapStatus: BootstrapStatusView
|
|
260
274
|
): void => {
|
|
275
|
+
this.runtimeBootstrapProbeCadence.recordFailure();
|
|
261
276
|
this.clearRecoveryTimeout();
|
|
262
277
|
this.patchState({
|
|
263
278
|
lifecyclePhase: 'cold-starting',
|
|
@@ -271,6 +286,7 @@ export class SystemStatusManager {
|
|
|
271
286
|
errorMessage: string | null,
|
|
272
287
|
bootstrapStatus?: BootstrapStatusView
|
|
273
288
|
): void => {
|
|
289
|
+
this.runtimeBootstrapProbeCadence.recordFailure();
|
|
274
290
|
this.clearRecoveryTimeout();
|
|
275
291
|
this.patchState({
|
|
276
292
|
lifecyclePhase: 'startup-failed',
|
|
@@ -284,6 +300,7 @@ export class SystemStatusManager {
|
|
|
284
300
|
errorMessage: string | null,
|
|
285
301
|
bootstrapStatus?: BootstrapStatusView
|
|
286
302
|
): void => {
|
|
303
|
+
this.runtimeBootstrapProbeCadence.recordFailure();
|
|
287
304
|
const state = this.getState();
|
|
288
305
|
if (!state.hasReachedReady) {
|
|
289
306
|
this.patchState({
|
|
@@ -321,6 +338,7 @@ export class SystemStatusManager {
|
|
|
321
338
|
private transitionToReady = (
|
|
322
339
|
bootstrapStatus: BootstrapStatusView | null
|
|
323
340
|
): void => {
|
|
341
|
+
this.runtimeBootstrapProbeCadence.recordSuccess();
|
|
324
342
|
const state = this.getState();
|
|
325
343
|
const shouldRefreshQueries =
|
|
326
344
|
state.lifecyclePhase === 'recovering' || state.lifecyclePhase === 'stalled';
|
|
@@ -7,7 +7,6 @@ import { Input } from '@/shared/components/ui/input';
|
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
|
8
8
|
import { Card, CardContent } from '@/shared/components/ui/card';
|
|
9
9
|
import {
|
|
10
|
-
describeCronDelivery,
|
|
11
10
|
describeCronSchedule,
|
|
12
11
|
describeCronSession,
|
|
13
12
|
formatCronDate,
|
|
@@ -26,9 +25,7 @@ function matchQuery(job: CronJobView, query: string): boolean {
|
|
|
26
25
|
job.id,
|
|
27
26
|
job.name,
|
|
28
27
|
job.payload.message,
|
|
29
|
-
job.payload.sessionId ?? ''
|
|
30
|
-
job.payload.channel ?? '',
|
|
31
|
-
job.payload.to ?? ''
|
|
28
|
+
job.payload.sessionId ?? ''
|
|
32
29
|
].join(' ').toLowerCase();
|
|
33
30
|
return haystack.includes(q);
|
|
34
31
|
}
|
|
@@ -64,7 +61,6 @@ function CronJobCard(props: {
|
|
|
64
61
|
<div className="mt-2 text-xs text-gray-500">{t('cronScheduleLabel')}: {describeCronSchedule(job)}</div>
|
|
65
62
|
<div className="mt-2 whitespace-pre-wrap break-words text-sm text-gray-700">{job.payload.message}</div>
|
|
66
63
|
<div className="mt-2 text-xs text-gray-500">{t('cronSessionLabel')}: {describeCronSession(job)}</div>
|
|
67
|
-
<div className="mt-2 text-xs text-gray-500">{t('cronDeliverTo')}: {describeCronDelivery(job)}</div>
|
|
68
64
|
</div>
|
|
69
65
|
<div className="min-w-[220px] space-y-2 text-xs text-gray-500">
|
|
70
66
|
<div><span className="font-medium text-gray-700">{t('cronNextRun')}:</span> {formatCronDate(job.state.nextRunAt)}</div>
|
|
@@ -11,7 +11,7 @@ export * from './utils/mcp-marketplace.utils';
|
|
|
11
11
|
export * from './utils/ncp-attachments.utils';
|
|
12
12
|
export * from './utils/ncp-session.utils';
|
|
13
13
|
export * from './ncp-session.types';
|
|
14
|
-
export * from './ncp-session-query-cache';
|
|
14
|
+
export * from './ncp-session-query-cache.utils';
|
|
15
15
|
export * from './raw-client.utils';
|
|
16
16
|
export * from './utils/remote.utils';
|
|
17
17
|
export * from './remote.types';
|
package/src/shared/lib/api/{ncp-session-query-cache.test.ts → ncp-session-query-cache.utils.test.ts}
RENAMED
|
@@ -131,6 +131,29 @@ describe('ncp-session-query-cache', () => {
|
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
it('keeps the realtime running overlay when a newer persisted idle summary arrives', () => {
|
|
135
|
+
const current = updateNcpSessionRunStatusList(createSessionsList(), {
|
|
136
|
+
sessionKey: 'session-1',
|
|
137
|
+
status: 'running'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const updated = upsertNcpSessionSummaryList(current, {
|
|
141
|
+
sessionId: 'session-1',
|
|
142
|
+
messageCount: 2,
|
|
143
|
+
createdAt: '2026-03-29T08:00:00.000Z',
|
|
144
|
+
updatedAt: '2026-03-29T10:01:00.000Z',
|
|
145
|
+
lastMessageAt: '2026-03-29T10:01:00.000Z',
|
|
146
|
+
status: 'idle',
|
|
147
|
+
metadata: { last_activity_preview: { state: 'running' } }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(updated?.sessions[0]).toMatchObject({
|
|
151
|
+
sessionId: 'session-1',
|
|
152
|
+
messageCount: 2,
|
|
153
|
+
status: 'running'
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
134
157
|
it('applies realtime upsert/delete events to every ncp-sessions query cache entry', () => {
|
|
135
158
|
const queryClient = new QueryClient();
|
|
136
159
|
queryClient.setQueryData(['ncp-sessions', 200], createSessionsList());
|
|
@@ -14,18 +14,11 @@ function shouldReplaceSessionSummary(
|
|
|
14
14
|
next: NcpSessionSummaryView
|
|
15
15
|
): boolean {
|
|
16
16
|
const timeOrder = next.updatedAt.localeCompare(current.updatedAt);
|
|
17
|
-
if (timeOrder
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
if (timeOrder < 0) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (current.status === next.status) {
|
|
25
|
-
return true;
|
|
17
|
+
if (timeOrder !== 0) {
|
|
18
|
+
return timeOrder > 0;
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
return next.status === 'idle';
|
|
21
|
+
return current.status === next.status || next.status === 'idle';
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
export function upsertNcpSessionSummaryList(
|
|
@@ -40,7 +33,12 @@ export function upsertNcpSessionSummaryList(
|
|
|
40
33
|
const nextSessions =
|
|
41
34
|
existingIndex >= 0
|
|
42
35
|
? current.sessions.map((session, index) =>
|
|
43
|
-
index === existingIndex && shouldReplaceSessionSummary(session, summary)
|
|
36
|
+
index === existingIndex && shouldReplaceSessionSummary(session, summary)
|
|
37
|
+
? {
|
|
38
|
+
...summary,
|
|
39
|
+
status: session.status === 'running' && summary.status === 'idle' ? 'running' : summary.status
|
|
40
|
+
}
|
|
41
|
+
: session
|
|
44
42
|
)
|
|
45
43
|
: [...current.sessions, summary];
|
|
46
44
|
const sortedSessions = sortSessionSummaries(nextSessions);
|
|
@@ -363,7 +363,9 @@ export type ServerPathReadView = { requestedPath: string; resolvedPath: string;
|
|
|
363
363
|
|
|
364
364
|
export type PanelAppEntryView = {
|
|
365
365
|
id: string;
|
|
366
|
+
appId: string;
|
|
366
367
|
fileName: string;
|
|
368
|
+
kind: "single-file" | "folder";
|
|
367
369
|
title: string;
|
|
368
370
|
description?: string;
|
|
369
371
|
icon?: string;
|
|
@@ -372,6 +374,8 @@ export type PanelAppEntryView = {
|
|
|
372
374
|
updatedAt: string;
|
|
373
375
|
sizeBytes: number;
|
|
374
376
|
favorite: boolean;
|
|
377
|
+
clientDeclared: boolean;
|
|
378
|
+
clientGranted: boolean;
|
|
375
379
|
lastOpenedAt?: string;
|
|
376
380
|
openCount: number;
|
|
377
381
|
};
|
|
@@ -402,10 +406,6 @@ export type CronPayloadView = {
|
|
|
402
406
|
message: string;
|
|
403
407
|
agentId?: string | null;
|
|
404
408
|
sessionId?: string | null;
|
|
405
|
-
deliver?: boolean;
|
|
406
|
-
channel?: string | null;
|
|
407
|
-
to?: string | null;
|
|
408
|
-
accountId?: string | null;
|
|
409
409
|
};
|
|
410
410
|
|
|
411
411
|
export type CronJobStateView = {
|