@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.
- package/CHANGELOG.md +14 -0
- package/dist/assets/{api-BCDb13Uv.js → api-CIJxdHmy.js} +5 -5
- package/dist/assets/app-presenter-provider-CKZqZfv5.js +3 -0
- package/dist/assets/arrow-left-DHcD-tbC.js +1 -0
- package/dist/assets/channels-list-page-C_Xe0CNX.js +8 -0
- package/dist/assets/chat-page-CXRtoQR2.js +105 -0
- package/dist/assets/{config-split-page-BwhGqmUS.js → config-split-page-DBAcw9mJ.js} +1 -1
- package/dist/assets/{confirm-dialog-DBkKcQ1q.js → confirm-dialog-DwPTxq8C.js} +1 -1
- package/dist/assets/{desktop-update-config-BE9KjiXi.js → desktop-update-config-BqABa_1C.js} +1 -1
- package/dist/assets/{dist-BI0bX8jv.js → dist-BfcibKyv.js} +1 -1
- package/dist/assets/{dist-C8JRt-tf.js → dist-WjhKB-pc.js} +1 -1
- package/dist/assets/{doc-browser-eqdNEerh.js → doc-browser-8EEDwGCJ.js} +1 -1
- package/dist/assets/doc-browser-DlT90zQD.js +1 -0
- package/dist/assets/{doc-browser-context-VHUOIUXL.js → doc-browser-context-DLJOy6Am.js} +1 -1
- package/dist/assets/{ellipsis-C9VB2CxT.js → ellipsis-H-4aNwgt.js} +1 -1
- package/dist/assets/{es2015-BCw3Q7nY.js → es2015-B5k1IpKX.js} +1 -1
- package/dist/assets/{external-link-Bp7_4s9t.js → external-link-BN1wd-eI.js} +1 -1
- package/dist/assets/{index-DTdPDqto.js → index-WJt0X9wC.js} +5 -5
- package/dist/assets/{key-round-Ccy6oND3.js → key-round-D-7IaAQS.js} +1 -1
- package/dist/assets/{loader-circle-OpH4EErP.js → loader-circle-CB38Q7Ws.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CmZwt64F.js +1 -0
- package/dist/assets/{mcp-marketplace-page-B-GxKOqS.js → mcp-marketplace-page-ju1KH3TG.js} +2 -2
- package/dist/assets/{model-config-BXPnV_7n.js → model-config-CgS6dp65.js} +1 -1
- package/dist/assets/{notice-card-Dsbkl7u7.js → notice-card-DW9BLKpG.js} +1 -1
- package/dist/assets/play-DGpH8jeI.js +1 -0
- package/dist/assets/plus-CkcQjwuH.js +1 -0
- package/dist/assets/{popover-BTLZE5Iz.js → popover-ofN845gZ.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DKI9KN7N.js → provider-scoped-model-input-DaDqi8Zy.js} +1 -1
- package/dist/assets/providers-list-CZNQXY4n.js +1 -0
- package/dist/assets/{react-BqS8oMyM.js → react-Bq34NL59.js} +1 -1
- package/dist/assets/{refresh-cw-BIaj7txb.js → refresh-cw-DJttSXS8.js} +1 -1
- package/dist/assets/remote-CYmsM9DD.js +1 -0
- package/dist/assets/rotate-cw-D_xslHna.js +1 -0
- package/dist/assets/{runtime-config-page-DX_wSK1J.js → runtime-config-page-gUjM6Z_A.js} +1 -1
- package/dist/assets/{save-BEoORten.js → save-DtAFbS7x.js} +1 -1
- package/dist/assets/search-CNMRwAFx.js +1 -0
- package/dist/assets/{search-config-DA5By-tE.js → search-config-C04PrmHj.js} +1 -1
- package/dist/assets/{secrets-config-eDxG7iN4.js → secrets-config-DvxHdz9h.js} +2 -2
- package/dist/assets/{status-dot-oWBjTovo.js → status-dot-D4jbKDLB.js} +1 -1
- package/dist/assets/{tabs-CGaY0BVO.js → tabs-Ca2vw7YQ.js} +1 -1
- package/dist/assets/{tabs-custom-D2fPYb4G.js → tabs-custom-DzGDjYxf.js} +1 -1
- package/dist/assets/{tag-chip-TVkgofS1.js → tag-chip-CW-_pHjH.js} +1 -1
- package/dist/assets/{tooltip-BwHPKBv9.js → tooltip-Cq2mERPk.js} +1 -1
- package/dist/assets/{trash-2-D3psAGrz.js → trash-2-rumaEu9l.js} +1 -1
- package/dist/assets/x-DGqjdMUt.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/managers/panel-app-bridge.manager.test.ts +125 -119
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +47 -17
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +38 -5
- 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/types.ts +0 -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 +4 -3
- package/src/shared/lib/i18n/runtime/i18n-language-owner.test.ts +3 -2
- package/dist/assets/app-presenter-provider-C-QMjK7m.js +0 -3
- 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
|
@@ -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('
|
|
41
|
-
const manager =
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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.
|
|
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 =
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 =
|
|
200
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
224
|
+
const actions = await this.listGrantCandidateActions(session, actionId);
|
|
223
225
|
const allowed = await this.authorizationManager.requestAuthorization({
|
|
224
226
|
panelAppId: session.panelAppId,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
|
252
|
+
private listGrantCandidateActions = async (
|
|
244
253
|
session: PanelAppBridgeSessionView,
|
|
245
254
|
actionId: string,
|
|
246
|
-
): Promise<ServiceActionListView['actions'][number]
|
|
247
|
-
const
|
|
255
|
+
): Promise<Array<ServiceActionListView['actions'][number]>> => {
|
|
256
|
+
const actionList = await nextclawClient.serviceApps.listServiceActions({
|
|
248
257
|
bridgeSessionToken: session.token,
|
|
249
258
|
});
|
|
250
|
-
|
|
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
|
-
<
|
|
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,
|
|
@@ -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
|
});
|