@nextclaw/ui 0.13.8 → 0.13.9

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 (85) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/assets/{api-BCDb13Uv.js → api-CIJxdHmy.js} +5 -5
  3. package/dist/assets/app-presenter-provider-CKZqZfv5.js +3 -0
  4. package/dist/assets/arrow-left-DHcD-tbC.js +1 -0
  5. package/dist/assets/channels-list-page-C_Xe0CNX.js +8 -0
  6. package/dist/assets/chat-page-CXRtoQR2.js +105 -0
  7. package/dist/assets/{config-split-page-BwhGqmUS.js → config-split-page-DBAcw9mJ.js} +1 -1
  8. package/dist/assets/{confirm-dialog-DBkKcQ1q.js → confirm-dialog-DwPTxq8C.js} +1 -1
  9. package/dist/assets/{desktop-update-config-BE9KjiXi.js → desktop-update-config-BqABa_1C.js} +1 -1
  10. package/dist/assets/{dist-BI0bX8jv.js → dist-BfcibKyv.js} +1 -1
  11. package/dist/assets/{dist-C8JRt-tf.js → dist-WjhKB-pc.js} +1 -1
  12. package/dist/assets/{doc-browser-eqdNEerh.js → doc-browser-8EEDwGCJ.js} +1 -1
  13. package/dist/assets/doc-browser-DlT90zQD.js +1 -0
  14. package/dist/assets/{doc-browser-context-VHUOIUXL.js → doc-browser-context-DLJOy6Am.js} +1 -1
  15. package/dist/assets/{ellipsis-C9VB2CxT.js → ellipsis-H-4aNwgt.js} +1 -1
  16. package/dist/assets/{es2015-BCw3Q7nY.js → es2015-B5k1IpKX.js} +1 -1
  17. package/dist/assets/{external-link-Bp7_4s9t.js → external-link-BN1wd-eI.js} +1 -1
  18. package/dist/assets/{index-DTdPDqto.js → index-WJt0X9wC.js} +5 -5
  19. package/dist/assets/{key-round-Ccy6oND3.js → key-round-D-7IaAQS.js} +1 -1
  20. package/dist/assets/{loader-circle-OpH4EErP.js → loader-circle-CB38Q7Ws.js} +1 -1
  21. package/dist/assets/mcp-marketplace-page-CmZwt64F.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-B-GxKOqS.js → mcp-marketplace-page-ju1KH3TG.js} +2 -2
  23. package/dist/assets/{model-config-BXPnV_7n.js → model-config-CgS6dp65.js} +1 -1
  24. package/dist/assets/{notice-card-Dsbkl7u7.js → notice-card-DW9BLKpG.js} +1 -1
  25. package/dist/assets/play-DGpH8jeI.js +1 -0
  26. package/dist/assets/plus-CkcQjwuH.js +1 -0
  27. package/dist/assets/{popover-BTLZE5Iz.js → popover-ofN845gZ.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-DKI9KN7N.js → provider-scoped-model-input-DaDqi8Zy.js} +1 -1
  29. package/dist/assets/providers-list-CZNQXY4n.js +1 -0
  30. package/dist/assets/{react-BqS8oMyM.js → react-Bq34NL59.js} +1 -1
  31. package/dist/assets/{refresh-cw-BIaj7txb.js → refresh-cw-DJttSXS8.js} +1 -1
  32. package/dist/assets/remote-CYmsM9DD.js +1 -0
  33. package/dist/assets/rotate-cw-D_xslHna.js +1 -0
  34. package/dist/assets/{runtime-config-page-DX_wSK1J.js → runtime-config-page-gUjM6Z_A.js} +1 -1
  35. package/dist/assets/{save-BEoORten.js → save-DtAFbS7x.js} +1 -1
  36. package/dist/assets/search-CNMRwAFx.js +1 -0
  37. package/dist/assets/{search-config-DA5By-tE.js → search-config-C04PrmHj.js} +1 -1
  38. package/dist/assets/{secrets-config-eDxG7iN4.js → secrets-config-DvxHdz9h.js} +2 -2
  39. package/dist/assets/{status-dot-oWBjTovo.js → status-dot-D4jbKDLB.js} +1 -1
  40. package/dist/assets/{tabs-CGaY0BVO.js → tabs-Ca2vw7YQ.js} +1 -1
  41. package/dist/assets/{tabs-custom-D2fPYb4G.js → tabs-custom-DzGDjYxf.js} +1 -1
  42. package/dist/assets/{tag-chip-TVkgofS1.js → tag-chip-CW-_pHjH.js} +1 -1
  43. package/dist/assets/{tooltip-BwHPKBv9.js → tooltip-Cq2mERPk.js} +1 -1
  44. package/dist/assets/{trash-2-D3psAGrz.js → trash-2-rumaEu9l.js} +1 -1
  45. package/dist/assets/x-DGqjdMUt.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/managers/panel-app-bridge.manager.test.ts +125 -119
  58. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +47 -17
  59. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +38 -5
  60. package/src/features/service-apps/stores/service-action-authorization.store.ts +7 -3
  61. package/src/features/system-status/components/desktop-update-config.test.tsx +2 -1
  62. package/src/features/system-status/hooks/use-system-status.ts +1 -4
  63. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +137 -100
  64. package/src/features/system-status/managers/system-status.manager.ts +38 -20
  65. package/src/shared/components/cron-config.tsx +1 -5
  66. package/src/shared/lib/api/types.ts +0 -4
  67. package/src/shared/lib/cadence/index.ts +7 -0
  68. package/src/shared/lib/cadence/services/adaptive-cadence.service.test.ts +95 -0
  69. package/src/shared/lib/cadence/services/adaptive-cadence.service.ts +120 -0
  70. package/src/shared/lib/cron/cron-job-view.utils.ts +0 -9
  71. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +4 -3
  72. package/src/shared/lib/i18n/runtime/i18n-language-owner.test.ts +3 -2
  73. package/dist/assets/app-presenter-provider-C-QMjK7m.js +0 -3
  74. package/dist/assets/arrow-left-rIXFA2Zy.js +0 -1
  75. package/dist/assets/channels-list-page-DnqwsKGe.js +0 -8
  76. package/dist/assets/chat-page-CsQx20xL.js +0 -105
  77. package/dist/assets/doc-browser-lpF_pyx4.js +0 -1
  78. package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +0 -1
  79. package/dist/assets/play-CvgYhNbo.js +0 -1
  80. package/dist/assets/plus-DHcP-K1U.js +0 -1
  81. package/dist/assets/providers-list-Bko6B7dE.js +0 -1
  82. package/dist/assets/remote-DHn_b8QU.js +0 -1
  83. package/dist/assets/rotate-cw-DMqSkRJQ.js +0 -1
  84. package/dist/assets/search-COFh_SFI.js +0 -1
  85. package/dist/assets/x-CHAxzyp2.js +0 -1
@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
9
9
  generateAgentObject: vi.fn(),
10
10
  grantAgentCapability: vi.fn(),
11
11
  grantServiceAction: vi.fn(),
12
+ grantServiceActions: vi.fn(),
12
13
  invokeServiceAction: vi.fn(),
13
14
  listServiceActions: vi.fn(),
14
15
  sendAgentMessage: vi.fn(),
@@ -25,6 +26,7 @@ vi.mock('@/shared/lib/api', () => ({
25
26
  },
26
27
  serviceApps: {
27
28
  grantServiceAction: mocks.grantServiceAction,
29
+ grantServiceActions: mocks.grantServiceActions,
28
30
  invokeServiceAction: mocks.invokeServiceAction,
29
31
  listServiceActions: mocks.listServiceActions,
30
32
  revokeServiceAction: vi.fn(),
@@ -36,22 +38,54 @@ afterEach(() => {
36
38
  vi.clearAllMocks();
37
39
  });
38
40
 
41
+ function createManager(): PanelAppBridgeManager {
42
+ return new PanelAppBridgeManager({
43
+ requestAuthorization: mocks.requestAuthorization,
44
+ } as unknown as ServiceActionAuthorizationManager);
45
+ }
46
+
47
+ function createIframeHarness(manager: PanelAppBridgeManager) {
48
+ const postMessage = vi.fn();
49
+ const contentWindow = { postMessage } as unknown as Window;
50
+ const iframe = { contentWindow } as HTMLIFrameElement;
51
+ return {
52
+ postMessage,
53
+ send: (
54
+ data: unknown,
55
+ iframeInstanceId = 'tab-1:0:0',
56
+ ) => manager.handleIframeMessage({
57
+ event: { data, source: contentWindow } as MessageEvent,
58
+ iframe,
59
+ iframeInstanceId,
60
+ tab: {
61
+ currentUrl: '/api/panel-apps/mood-calendar/content',
62
+ history: [],
63
+ historyIndex: 0,
64
+ id: 'tab-1',
65
+ kind: 'content',
66
+ navVersion: 0,
67
+ title: 'Mood Calendar',
68
+ },
69
+ }),
70
+ };
71
+ }
72
+
73
+ function mockBridgeSession(token = 'token-1') {
74
+ mocks.createBridgeSession.mockResolvedValue({
75
+ expiresAt: '2026-05-28T00:00:00.000Z',
76
+ id: `session-${token}`,
77
+ panelAppId: 'mood-calendar',
78
+ tabId: 'tab-1',
79
+ token,
80
+ });
81
+ }
82
+
39
83
  describe('PanelAppBridgeManager', () => {
40
- it('opens the authorization flow before retrying a protected service action', async () => {
41
- const manager = new PanelAppBridgeManager({
42
- requestAuthorization: mocks.requestAuthorization,
43
- } as unknown as ServiceActionAuthorizationManager);
44
- const postMessage = vi.fn();
45
- const contentWindow = { postMessage } as unknown as Window;
46
- const iframe = { contentWindow } as HTMLIFrameElement;
84
+ it('authorizes missing service actions in one flow before retrying a protected action', async () => {
85
+ const manager = createManager();
86
+ const { postMessage, send } = createIframeHarness(manager);
47
87
  const actionId = 'mood-tracker.saveMood';
48
- mocks.createBridgeSession.mockResolvedValue({
49
- expiresAt: '2026-05-28T00:00:00.000Z',
50
- id: 'session-1',
51
- panelAppId: 'mood-calendar',
52
- tabId: 'tab-1',
53
- token: 'token-1',
54
- });
88
+ mockBridgeSession();
55
89
  mocks.invokeServiceAction
56
90
  .mockRejectedValueOnce(new NextClawClientError({
57
91
  code: 'AUTHORIZATION_REQUIRED',
@@ -59,58 +93,77 @@ describe('PanelAppBridgeManager', () => {
59
93
  }))
60
94
  .mockResolvedValueOnce({ actionId, result: { saved: true } });
61
95
  mocks.listServiceActions.mockResolvedValue({
62
- actions: [{
63
- appId: 'mood-tracker',
64
- description: 'Save daily mood entries',
65
- id: actionId,
66
- name: 'saveMood',
67
- risk: 'write',
68
- title: 'Save mood',
69
- }],
96
+ actions: [
97
+ {
98
+ appId: 'mood-tracker',
99
+ description: 'Save daily mood entries',
100
+ grantState: 'not-granted',
101
+ id: actionId,
102
+ name: 'saveMood',
103
+ risk: 'write',
104
+ title: 'Save mood',
105
+ },
106
+ {
107
+ appId: 'mood-tracker',
108
+ description: 'Read mood history',
109
+ grantState: 'not-granted',
110
+ id: 'mood-tracker.listMoods',
111
+ name: 'listMoods',
112
+ risk: 'read',
113
+ title: 'List moods',
114
+ },
115
+ ],
70
116
  });
71
117
  mocks.requestAuthorization.mockResolvedValue(true);
72
- mocks.grantServiceAction.mockResolvedValue({
73
- actionId,
74
- caller: { surface: 'panel-app', appId: 'mood-calendar' },
75
- grantedAt: '2026-05-28T00:00:00.000Z',
76
- risk: 'write',
118
+ mocks.grantServiceActions.mockResolvedValue({
119
+ grants: [
120
+ {
121
+ actionId,
122
+ caller: { surface: 'panel-app', appId: 'mood-calendar' },
123
+ grantedAt: '2026-05-28T00:00:00.000Z',
124
+ risk: 'write',
125
+ },
126
+ {
127
+ actionId: 'mood-tracker.listMoods',
128
+ caller: { surface: 'panel-app', appId: 'mood-calendar' },
129
+ grantedAt: '2026-05-28T00:00:00.000Z',
130
+ risk: 'read',
131
+ },
132
+ ],
77
133
  });
78
134
 
79
- manager.handleIframeMessage({
80
- event: {
81
- data: {
82
- method: 'invoke',
83
- payload: { actionId, input: { mood: 'happy' } },
84
- requestId: 'request-1',
85
- type: 'nextclaw:panel-app-service-actions:request',
86
- },
87
- source: contentWindow,
88
- } as MessageEvent,
89
- iframe,
90
- iframeInstanceId: 'tab-1:0:0',
91
- tab: {
92
- currentUrl: '/api/panel-apps/mood-calendar/content',
93
- history: [],
94
- historyIndex: 0,
95
- id: 'tab-1',
96
- kind: 'content',
97
- navVersion: 0,
98
- title: 'Mood Calendar',
99
- },
135
+ send({
136
+ method: 'invoke',
137
+ payload: { actionId, input: { mood: 'happy' } },
138
+ requestId: 'request-1',
139
+ type: 'nextclaw:panel-app-service-actions:request',
100
140
  });
101
141
 
102
142
  await waitFor(() => expect(postMessage).toHaveBeenCalled());
103
143
 
104
144
  expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
105
- actionId,
106
- actionTitle: 'Save mood',
145
+ actions: [
146
+ expect.objectContaining({
147
+ actionId,
148
+ actionTitle: 'Save mood',
149
+ risk: 'write',
150
+ }),
151
+ expect.objectContaining({
152
+ actionId: 'mood-tracker.listMoods',
153
+ actionTitle: 'List moods',
154
+ risk: 'read',
155
+ }),
156
+ ],
107
157
  inputPreview: expect.stringContaining('happy'),
108
158
  panelAppId: 'mood-calendar',
109
- risk: 'write',
110
159
  }));
111
- expect(mocks.grantServiceAction).toHaveBeenCalledWith(actionId, {
160
+ expect(mocks.grantServiceActions).toHaveBeenCalledWith([
161
+ actionId,
162
+ 'mood-tracker.listMoods',
163
+ ], {
112
164
  bridgeSessionToken: 'token-1',
113
165
  });
166
+ expect(mocks.grantServiceAction).not.toHaveBeenCalled();
114
167
  expect(mocks.invokeServiceAction).toHaveBeenCalledTimes(2);
115
168
  expect(postMessage).toHaveBeenCalledWith({
116
169
  data: { actionId, result: { saved: true } },
@@ -121,19 +174,9 @@ describe('PanelAppBridgeManager', () => {
121
174
  });
122
175
 
123
176
  it('opens the authorization flow before retrying a protected generateObject call', async () => {
124
- const manager = new PanelAppBridgeManager({
125
- requestAuthorization: mocks.requestAuthorization,
126
- } as unknown as ServiceActionAuthorizationManager);
127
- const postMessage = vi.fn();
128
- const contentWindow = { postMessage } as unknown as Window;
129
- const iframe = { contentWindow } as HTMLIFrameElement;
130
- mocks.createBridgeSession.mockResolvedValue({
131
- expiresAt: '2026-05-28T00:00:00.000Z',
132
- id: 'session-1',
133
- panelAppId: 'mood-calendar',
134
- tabId: 'tab-1',
135
- token: 'token-1',
136
- });
177
+ const manager = createManager();
178
+ const { postMessage, send } = createIframeHarness(manager);
179
+ mockBridgeSession();
137
180
  mocks.generateAgentObject
138
181
  .mockRejectedValueOnce(new NextClawClientError({
139
182
  code: 'AUTHORIZATION_REQUIRED',
@@ -147,41 +190,24 @@ describe('PanelAppBridgeManager', () => {
147
190
  grantedAt: '2026-05-28T00:00:00.000Z',
148
191
  });
149
192
 
150
- manager.handleIframeMessage({
151
- event: {
152
- data: {
153
- method: 'agent.generateObject',
154
- payload: {
155
- input: {
156
- peerId: 'mood-summary',
157
- prompt: 'summarize',
158
- schema: { type: 'object' },
159
- },
160
- },
161
- requestId: 'request-2',
162
- type: 'nextclaw:panel-app-service-actions:request',
193
+ send({
194
+ method: 'agent.generateObject',
195
+ payload: {
196
+ input: {
197
+ peerId: 'mood-summary',
198
+ prompt: 'summarize',
199
+ schema: { type: 'object' },
163
200
  },
164
- source: contentWindow,
165
- } as MessageEvent,
166
- iframe,
167
- iframeInstanceId: 'tab-1:0:0',
168
- tab: {
169
- currentUrl: '/api/panel-apps/mood-calendar/content',
170
- history: [],
171
- historyIndex: 0,
172
- id: 'tab-1',
173
- kind: 'content',
174
- navVersion: 0,
175
- title: 'Mood Calendar',
176
201
  },
202
+ requestId: 'request-2',
203
+ type: 'nextclaw:panel-app-service-actions:request',
177
204
  });
178
205
 
179
206
  await waitFor(() => expect(postMessage).toHaveBeenCalled());
180
207
 
181
208
  expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
182
- actionId: 'agent:generateObject',
209
+ actions: [expect.objectContaining({ actionId: 'agent:generateObject' })],
183
210
  panelAppId: 'mood-calendar',
184
- risk: 'write',
185
211
  }));
186
212
  expect(mocks.grantAgentCapability).toHaveBeenCalledWith('agent:generateObject', {
187
213
  bridgeSessionToken: 'token-1',
@@ -196,12 +222,8 @@ describe('PanelAppBridgeManager', () => {
196
222
  });
197
223
 
198
224
  it('creates a fresh bridge session for each iframe instance', async () => {
199
- const manager = new PanelAppBridgeManager({
200
- requestAuthorization: mocks.requestAuthorization,
201
- } as unknown as ServiceActionAuthorizationManager);
202
- const postMessage = vi.fn();
203
- const contentWindow = { postMessage } as unknown as Window;
204
- const iframe = { contentWindow } as HTMLIFrameElement;
225
+ const manager = createManager();
226
+ const { postMessage, send } = createIframeHarness(manager);
205
227
  mocks.createBridgeSession
206
228
  .mockResolvedValueOnce({
207
229
  expiresAt: '2026-05-28T00:00:00.000Z',
@@ -224,27 +246,11 @@ describe('PanelAppBridgeManager', () => {
224
246
  ['request-2', 'tab-1:0:0'],
225
247
  ['request-3', 'tab-1:0:1'],
226
248
  ]) {
227
- manager.handleIframeMessage({
228
- event: {
229
- data: {
230
- method: 'list',
231
- requestId,
232
- type: 'nextclaw:panel-app-service-actions:request',
233
- },
234
- source: contentWindow,
235
- } as MessageEvent,
236
- iframe,
237
- iframeInstanceId,
238
- tab: {
239
- currentUrl: '/api/panel-apps/mood-calendar/content',
240
- history: [],
241
- historyIndex: 0,
242
- id: 'tab-1',
243
- kind: 'content',
244
- navVersion: 0,
245
- title: 'Mood Calendar',
246
- },
247
- });
249
+ send({
250
+ method: 'list',
251
+ requestId,
252
+ type: 'nextclaw:panel-app-service-actions:request',
253
+ }, iframeInstanceId);
248
254
  }
249
255
 
250
256
  await waitFor(() => expect(postMessage).toHaveBeenCalledTimes(3));
@@ -195,12 +195,14 @@ export class PanelAppBridgeManager {
195
195
  ): Promise<PanelAppCapabilityGrantView> => {
196
196
  const allowed = await this.authorizationManager.requestAuthorization({
197
197
  panelAppId: session.panelAppId,
198
- actionId: capability,
199
- actionTitle: capability === 'agent:send' ? 'Send agent message' : 'Generate object',
200
- actionDescription: capability === 'agent:send'
201
- ? 'Send a message to a NextClaw Agent session.'
202
- : 'Send context to a NextClaw Agent session and receive a structured object.',
203
- risk: 'write',
198
+ actions: [{
199
+ actionId: capability,
200
+ actionTitle: capability === 'agent:send' ? 'Send agent message' : 'Generate object',
201
+ actionDescription: capability === 'agent:send'
202
+ ? 'Send a message to a NextClaw Agent session.'
203
+ : 'Send context to a NextClaw Agent session and receive a structured object.',
204
+ risk: 'write',
205
+ }],
204
206
  });
205
207
  if (!allowed) {
206
208
  throw new NextClawClientError({
@@ -219,13 +221,15 @@ export class PanelAppBridgeManager {
219
221
  actionId: string,
220
222
  input?: Record<string, unknown>,
221
223
  ): Promise<ServiceActionGrantView> => {
222
- const action = await this.findAction(session, actionId);
224
+ const actions = await this.listGrantCandidateActions(session, actionId);
223
225
  const allowed = await this.authorizationManager.requestAuthorization({
224
226
  panelAppId: session.panelAppId,
225
- actionId,
226
- actionTitle: action?.title,
227
- actionDescription: action?.description,
228
- risk: action?.risk,
227
+ actions: actions.map((action) => ({
228
+ actionId: action.id,
229
+ actionTitle: action.title,
230
+ actionDescription: action.description,
231
+ risk: action.risk,
232
+ })),
229
233
  inputPreview: this.createInputPreview(input),
230
234
  });
231
235
  if (!allowed) {
@@ -234,22 +238,48 @@ export class PanelAppBridgeManager {
234
238
  message: `Permission rejected for ${actionId}.`,
235
239
  });
236
240
  }
237
- return await nextclawClient.serviceApps.grantServiceAction(
238
- actionId,
241
+ const result = await nextclawClient.serviceApps.grantServiceActions(
242
+ actions.map((action) => action.id),
239
243
  { bridgeSessionToken: session.token },
240
244
  );
245
+ const grant = result.grants.find((entry) => entry.actionId === actionId) ?? result.grants[0];
246
+ if (!grant) {
247
+ throw new Error(`No permission grant returned for ${actionId}.`);
248
+ }
249
+ return grant;
241
250
  };
242
251
 
243
- private findAction = async (
252
+ private listGrantCandidateActions = async (
244
253
  session: PanelAppBridgeSessionView,
245
254
  actionId: string,
246
- ): Promise<ServiceActionListView['actions'][number] | undefined> => {
247
- const actions = await nextclawClient.serviceApps.listServiceActions({
255
+ ): Promise<Array<ServiceActionListView['actions'][number]>> => {
256
+ const actionList = await nextclawClient.serviceApps.listServiceActions({
248
257
  bridgeSessionToken: session.token,
249
258
  });
250
- return actions.actions.find(
259
+ const target = actionList.actions.find(
251
260
  (action: ServiceActionListView['actions'][number]) => action.id === actionId,
252
261
  );
262
+ if (target && target.grantState !== 'not-granted') {
263
+ return [target];
264
+ }
265
+ const ungrantedActions = actionList.actions.filter((action) =>
266
+ action.grantState === 'not-granted'
267
+ );
268
+ const orderedActions = [
269
+ ...ungrantedActions.filter((action) => action.id === actionId),
270
+ ...ungrantedActions.filter((action) => action.id !== actionId),
271
+ ];
272
+ if (orderedActions.length > 0) {
273
+ return orderedActions;
274
+ }
275
+ return [{
276
+ appId: target?.appId ?? actionId.split('.')[0] ?? actionId,
277
+ description: target?.description,
278
+ id: actionId,
279
+ name: target?.name ?? actionId,
280
+ risk: target?.risk ?? 'dangerous',
281
+ title: target?.title,
282
+ }];
253
283
  };
254
284
 
255
285
  private createInputPreview = (input: Record<string, unknown> | undefined): string | undefined => {
@@ -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,
@@ -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
  });