@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/{api-BCDb13Uv.js → api-CA2udGyv.js} +5 -5
  3. package/dist/assets/{app-presenter-provider-C-QMjK7m.js → app-presenter-provider-B9ChYida.js} +2 -2
  4. package/dist/assets/arrow-left-CI0Ocu9p.js +1 -0
  5. package/dist/assets/channels-list-page-PrT4a9g4.js +8 -0
  6. package/dist/assets/chat-page-CImtIhmN.js +105 -0
  7. package/dist/assets/{config-split-page-BwhGqmUS.js → config-split-page-bO0K6uQv.js} +1 -1
  8. package/dist/assets/{confirm-dialog-DBkKcQ1q.js → confirm-dialog-l4FYdzga.js} +1 -1
  9. package/dist/assets/{desktop-update-config-BE9KjiXi.js → desktop-update-config-p3rJIgRg.js} +1 -1
  10. package/dist/assets/{dist-BI0bX8jv.js → dist-DQoV-62W.js} +1 -1
  11. package/dist/assets/{dist-C8JRt-tf.js → dist-T6rf86Nf.js} +1 -1
  12. package/dist/assets/{doc-browser-eqdNEerh.js → doc-browser-CTFB6ACN.js} +1 -1
  13. package/dist/assets/{doc-browser-context-VHUOIUXL.js → doc-browser-context-CP2tI-Br.js} +1 -1
  14. package/dist/assets/doc-browser-yi02ASOL.js +1 -0
  15. package/dist/assets/{ellipsis-C9VB2CxT.js → ellipsis-B7CbNPNr.js} +1 -1
  16. package/dist/assets/{es2015-BCw3Q7nY.js → es2015-BSPOGpH4.js} +1 -1
  17. package/dist/assets/{external-link-Bp7_4s9t.js → external-link-CefT35zP.js} +1 -1
  18. package/dist/assets/{index-DTdPDqto.js → index-DFC_DdT1.js} +5 -5
  19. package/dist/assets/{key-round-Ccy6oND3.js → key-round-BpM3GZn4.js} +1 -1
  20. package/dist/assets/{loader-circle-OpH4EErP.js → loader-circle-_edjOFsw.js} +1 -1
  21. package/dist/assets/mcp-marketplace-page-DNHSmfDf.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-B-GxKOqS.js → mcp-marketplace-page-DhqcnhZc.js} +2 -2
  23. package/dist/assets/{model-config-BXPnV_7n.js → model-config-C9ydDDFh.js} +1 -1
  24. package/dist/assets/{notice-card-Dsbkl7u7.js → notice-card-BfXlhPjX.js} +1 -1
  25. package/dist/assets/play-DD0bg50i.js +1 -0
  26. package/dist/assets/plus-zZ2GC_AJ.js +1 -0
  27. package/dist/assets/{popover-BTLZE5Iz.js → popover-D_pJfOlM.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-DKI9KN7N.js → provider-scoped-model-input-CkeEyTfO.js} +1 -1
  29. package/dist/assets/providers-list-9eOBQFUn.js +1 -0
  30. package/dist/assets/{react-BqS8oMyM.js → react-Bl8iJUcF.js} +1 -1
  31. package/dist/assets/{refresh-cw-BIaj7txb.js → refresh-cw-CqfS1EhK.js} +1 -1
  32. package/dist/assets/remote-fGlxrJI_.js +1 -0
  33. package/dist/assets/rotate-cw-C7EvaSlb.js +1 -0
  34. package/dist/assets/{runtime-config-page-DX_wSK1J.js → runtime-config-page-B30PC6bx.js} +1 -1
  35. package/dist/assets/{save-BEoORten.js → save-CQERCODg.js} +1 -1
  36. package/dist/assets/search-C0b2yzmt.js +1 -0
  37. package/dist/assets/{search-config-DA5By-tE.js → search-config-RN01uPJQ.js} +1 -1
  38. package/dist/assets/{secrets-config-eDxG7iN4.js → secrets-config-CTZ_TGoA.js} +2 -2
  39. package/dist/assets/{status-dot-oWBjTovo.js → status-dot-DY0xUUvK.js} +1 -1
  40. package/dist/assets/{tabs-CGaY0BVO.js → tabs-C77i1w90.js} +1 -1
  41. package/dist/assets/{tabs-custom-D2fPYb4G.js → tabs-custom-CFpR54-m.js} +1 -1
  42. package/dist/assets/{tag-chip-TVkgofS1.js → tag-chip-Dnb2VuWh.js} +1 -1
  43. package/dist/assets/{tooltip-BwHPKBv9.js → tooltip-fs7VQG7d.js} +1 -1
  44. package/dist/assets/{trash-2-D3psAGrz.js → trash-2-Bb8mTTLl.js} +1 -1
  45. package/dist/assets/x-BV4iiea8.js +1 -0
  46. package/dist/index.html +24 -24
  47. package/package.json +8 -8
  48. package/src/app/components/layout/runtime-status-entry.test.tsx +5 -0
  49. package/src/app/components/layout/runtime-status-entry.tsx +8 -0
  50. package/src/features/chat/components/conversation/chat-message-list.container.tsx +0 -6
  51. package/src/features/chat/components/workspace/session-cron-job-content.tsx +3 -3
  52. package/src/features/chat/hooks/use-chat-session-update.test.tsx +2 -1
  53. package/src/features/chat/managers/chat-session-preference-sync.manager.test.ts +2 -1
  54. package/src/features/chat/utils/chat-input-bar.utils.ts +0 -2
  55. package/src/features/chat/utils/chat-session-preference-governance.utils.ts +4 -1
  56. package/src/features/marketplace/components/mcp/mcp-marketplace-page.test.tsx +2 -1
  57. package/src/features/panel-apps/components/panel-app-list-item.test.tsx +4 -0
  58. package/src/features/panel-apps/components/panel-apps-list.tsx +27 -1
  59. package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
  60. package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +124 -140
  61. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +71 -53
  62. package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +4 -0
  63. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +4 -0
  64. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +38 -5
  65. package/src/features/service-apps/components/service-apps-panel.tsx +3 -3
  66. package/src/features/service-apps/stores/service-action-authorization.store.ts +7 -3
  67. package/src/features/system-status/components/desktop-update-config.test.tsx +2 -1
  68. package/src/features/system-status/hooks/use-system-status.ts +1 -4
  69. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +137 -100
  70. package/src/features/system-status/managers/system-status.manager.ts +38 -20
  71. package/src/shared/components/cron-config.tsx +1 -5
  72. package/src/shared/lib/api/index.ts +1 -1
  73. package/src/shared/lib/api/{ncp-session-query-cache.test.ts → ncp-session-query-cache.utils.test.ts} +23 -0
  74. package/src/shared/lib/api/{ncp-session-query-cache.ts → ncp-session-query-cache.utils.ts} +9 -11
  75. package/src/shared/lib/api/types.ts +4 -4
  76. package/src/shared/lib/cadence/index.ts +7 -0
  77. package/src/shared/lib/cadence/services/adaptive-cadence.service.test.ts +95 -0
  78. package/src/shared/lib/cadence/services/adaptive-cadence.service.ts +120 -0
  79. package/src/shared/lib/cron/cron-job-view.utils.ts +0 -9
  80. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +9 -3
  81. package/src/shared/lib/i18n/runtime/i18n-language-owner.test.ts +3 -2
  82. package/dist/assets/arrow-left-rIXFA2Zy.js +0 -1
  83. package/dist/assets/channels-list-page-DnqwsKGe.js +0 -8
  84. package/dist/assets/chat-page-CsQx20xL.js +0 -105
  85. package/dist/assets/doc-browser-lpF_pyx4.js +0 -1
  86. package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +0 -1
  87. package/dist/assets/play-CvgYhNbo.js +0 -1
  88. package/dist/assets/plus-DHcP-K1U.js +0 -1
  89. package/dist/assets/providers-list-Bko6B7dE.js +0 -1
  90. package/dist/assets/remote-DHn_b8QU.js +0 -1
  91. package/dist/assets/rotate-cw-DMqSkRJQ.js +0 -1
  92. package/dist/assets/search-COFh_SFI.js +0 -1
  93. 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
- <AuthorizationField label={t('serviceActionAuthorizationAction')} value={pending.actionTitle ?? pending.actionId} />
43
- {pending.actionDescription ? (
44
- <AuthorizationField label={t('serviceActionAuthorizationPurpose')} value={pending.actionDescription} />
45
- ) : null}
46
- <AuthorizationField label={t('serviceActionAuthorizationRisk')} value={pending.risk ?? 'dangerous'} />
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 ServiceActionAuthorizationRequest = {
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 import('@/features/system-status')>(
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 { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
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
- useSystemStatusStore.setState({
8
- state: {
9
- ...useSystemStatusStore.getState().state,
10
- lifecyclePhase: 'cold-starting',
11
- hasReachedReady: false,
12
- activeSystemAction: null,
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
- phase: 'shell-ready',
27
- ncpAgent: {
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('continues polling even when bootstrap status reports an ncp agent error', () => {
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
- phase: 'error',
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
- useSystemStatusStore.setState({
96
- state: {
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
- phase: 'ready',
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, 1)
130
- ).toBe(2000);
122
+ systemStatusManager.getRuntimeBootstrapPollInterval(undefined)
123
+ ).toBe(1000);
124
+
125
+ systemStatusManager.reportBootstrapQueryError(new Error('Failed to fetch'));
131
126
  expect(
132
- systemStatusManager.getRuntimeBootstrapPollInterval(undefined, 3)
133
- ).toBe(5000);
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 RUNTIME_BOOTSTRAP_PROBE_POLICY = {
28
- activePollIntervalMs: 1_000,
29
- errorPollIntervalMs: 2_000,
30
- maxErrorPollIntervalMs: 5_000,
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
- if (fetchFailureCount > 0) {
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 RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
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
- if (status?.ncpAgent.state === 'error' || status?.phase === 'error') {
87
- return RUNTIME_BOOTSTRAP_PROBE_POLICY.errorPollIntervalMs;
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';
@@ -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 > 0) {
18
- return true;
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) ? summary : session
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 = {