@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { HelpCircle, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
3
4
|
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
4
|
-
import { useDeletePanelApp, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
|
+
import { useDeletePanelApp, useGrantPanelAppClient, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
6
|
import { getPanelAppViewEntries } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
6
7
|
import type { PanelAppViewMode } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
7
8
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
@@ -20,6 +21,8 @@ export function PanelAppsList({
|
|
|
20
21
|
const deletePanelApp = useDeletePanelApp();
|
|
21
22
|
const updatePreferences = useUpdatePanelAppPreferences();
|
|
22
23
|
const recordOpened = useRecordPanelAppOpened();
|
|
24
|
+
const grantClient = useGrantPanelAppClient();
|
|
25
|
+
const presenter = useAppPresenter();
|
|
23
26
|
const [viewMode, setViewMode] = useState<PanelAppViewMode>('smart');
|
|
24
27
|
const entries = useMemo(
|
|
25
28
|
() => getPanelAppViewEntries(panelApps.data?.entries ?? [], viewMode),
|
|
@@ -27,6 +30,9 @@ export function PanelAppsList({
|
|
|
27
30
|
);
|
|
28
31
|
|
|
29
32
|
const openPanelApp = async (entry: PanelAppEntryView) => {
|
|
33
|
+
if (!(await ensurePanelAppClientGrant(entry))) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
30
36
|
try {
|
|
31
37
|
onOpenPanelApp(await recordOpened.mutateAsync(entry.id));
|
|
32
38
|
} catch {
|
|
@@ -34,6 +40,26 @@ export function PanelAppsList({
|
|
|
34
40
|
}
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
const ensurePanelAppClientGrant = async (entry: PanelAppEntryView): Promise<boolean> => {
|
|
44
|
+
if (!entry.clientDeclared || entry.clientGranted) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const allowed = await presenter.serviceActionAuthorizationManager.requestAuthorization({
|
|
48
|
+
panelAppId: entry.appId,
|
|
49
|
+
actions: [{
|
|
50
|
+
actionId: 'nextclaw.client',
|
|
51
|
+
actionTitle: t('panelAppsClientGrantTitle'),
|
|
52
|
+
actionDescription: t('panelAppsClientGrantDescription'),
|
|
53
|
+
risk: 'dangerous',
|
|
54
|
+
}],
|
|
55
|
+
});
|
|
56
|
+
if (!allowed) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
await grantClient.mutateAsync(entry.appId);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
37
63
|
const toggleFavorite = (entry: PanelAppEntryView) => {
|
|
38
64
|
updatePreferences.mutate({
|
|
39
65
|
id: entry.id,
|
|
@@ -38,6 +38,16 @@ export function useRecordPanelAppOpened() {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function useGrantPanelAppClient() {
|
|
42
|
+
const queryClient = useQueryClient();
|
|
43
|
+
return useMutation({
|
|
44
|
+
mutationFn: (appId: string) => nextclawClient.panelApps.grantClient(appId),
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
void queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export function useDeletePanelApp() {
|
|
42
52
|
const queryClient = useQueryClient();
|
|
43
53
|
return useMutation({
|
|
@@ -5,10 +5,10 @@ import { PanelAppBridgeManager } from '@/features/panel-apps/managers/panel-app-
|
|
|
5
5
|
import type { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
6
6
|
|
|
7
7
|
const mocks = vi.hoisted(() => ({
|
|
8
|
-
createBridgeSession: vi.fn(),
|
|
9
8
|
generateAgentObject: vi.fn(),
|
|
10
9
|
grantAgentCapability: vi.fn(),
|
|
11
10
|
grantServiceAction: vi.fn(),
|
|
11
|
+
grantServiceActions: vi.fn(),
|
|
12
12
|
invokeServiceAction: vi.fn(),
|
|
13
13
|
listServiceActions: vi.fn(),
|
|
14
14
|
sendAgentMessage: vi.fn(),
|
|
@@ -18,13 +18,13 @@ const mocks = vi.hoisted(() => ({
|
|
|
18
18
|
vi.mock('@/shared/lib/api', () => ({
|
|
19
19
|
nextclawClient: {
|
|
20
20
|
panelApps: {
|
|
21
|
-
createBridgeSession: mocks.createBridgeSession,
|
|
22
21
|
generateAgentObject: mocks.generateAgentObject,
|
|
23
22
|
grantAgentCapability: mocks.grantAgentCapability,
|
|
24
23
|
sendAgentMessage: mocks.sendAgentMessage,
|
|
25
24
|
},
|
|
26
25
|
serviceApps: {
|
|
27
26
|
grantServiceAction: mocks.grantServiceAction,
|
|
27
|
+
grantServiceActions: mocks.grantServiceActions,
|
|
28
28
|
invokeServiceAction: mocks.invokeServiceAction,
|
|
29
29
|
listServiceActions: mocks.listServiceActions,
|
|
30
30
|
revokeServiceAction: vi.fn(),
|
|
@@ -36,58 +36,32 @@ afterEach(() => {
|
|
|
36
36
|
vi.clearAllMocks();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const postMessage = vi.fn();
|
|
45
|
-
const contentWindow = { postMessage } as unknown as Window;
|
|
46
|
-
const iframe = { contentWindow } as HTMLIFrameElement;
|
|
47
|
-
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
|
-
});
|
|
55
|
-
mocks.invokeServiceAction
|
|
56
|
-
.mockRejectedValueOnce(new NextClawClientError({
|
|
57
|
-
code: 'AUTHORIZATION_REQUIRED',
|
|
58
|
-
message: 'authorization required',
|
|
59
|
-
}))
|
|
60
|
-
.mockResolvedValueOnce({ actionId, result: { saved: true } });
|
|
61
|
-
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
|
-
}],
|
|
70
|
-
});
|
|
71
|
-
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',
|
|
77
|
-
});
|
|
39
|
+
function createManager(): PanelAppBridgeManager {
|
|
40
|
+
return new PanelAppBridgeManager({
|
|
41
|
+
requestAuthorization: mocks.requestAuthorization,
|
|
42
|
+
} as unknown as ServiceActionAuthorizationManager);
|
|
43
|
+
}
|
|
78
44
|
|
|
79
|
-
|
|
45
|
+
function createIframeHarness(manager: PanelAppBridgeManager) {
|
|
46
|
+
const postMessage = vi.fn();
|
|
47
|
+
const contentWindow = { postMessage } as unknown as Window;
|
|
48
|
+
const iframe = { contentWindow } as HTMLIFrameElement;
|
|
49
|
+
return {
|
|
50
|
+
postMessage,
|
|
51
|
+
send: (
|
|
52
|
+
data: Record<string, unknown>,
|
|
53
|
+
iframeInstanceId = 'tab-1:0:0',
|
|
54
|
+
) => manager.handleIframeMessage({
|
|
80
55
|
event: {
|
|
81
56
|
data: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
type: 'nextclaw:panel-app-service-actions:request',
|
|
57
|
+
appId: 'mood-calendar',
|
|
58
|
+
runtimeToken: 'token-1',
|
|
59
|
+
...data,
|
|
86
60
|
},
|
|
87
61
|
source: contentWindow,
|
|
88
62
|
} as MessageEvent,
|
|
89
63
|
iframe,
|
|
90
|
-
iframeInstanceId
|
|
64
|
+
iframeInstanceId,
|
|
91
65
|
tab: {
|
|
92
66
|
currentUrl: '/api/panel-apps/mood-calendar/content',
|
|
93
67
|
history: [],
|
|
@@ -97,20 +71,93 @@ describe('PanelAppBridgeManager', () => {
|
|
|
97
71
|
navVersion: 0,
|
|
98
72
|
title: 'Mood Calendar',
|
|
99
73
|
},
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('PanelAppBridgeManager', () => {
|
|
79
|
+
it('authorizes missing service actions in one flow before retrying a protected action', async () => {
|
|
80
|
+
const manager = createManager();
|
|
81
|
+
const { postMessage, send } = createIframeHarness(manager);
|
|
82
|
+
const actionId = 'mood-tracker.saveMood';
|
|
83
|
+
mocks.invokeServiceAction
|
|
84
|
+
.mockRejectedValueOnce(new NextClawClientError({
|
|
85
|
+
code: 'AUTHORIZATION_REQUIRED',
|
|
86
|
+
message: 'authorization required',
|
|
87
|
+
}))
|
|
88
|
+
.mockResolvedValueOnce({ actionId, result: { saved: true } });
|
|
89
|
+
mocks.listServiceActions.mockResolvedValue({
|
|
90
|
+
actions: [
|
|
91
|
+
{
|
|
92
|
+
appId: 'mood-tracker',
|
|
93
|
+
description: 'Save daily mood entries',
|
|
94
|
+
grantState: 'not-granted',
|
|
95
|
+
id: actionId,
|
|
96
|
+
name: 'saveMood',
|
|
97
|
+
risk: 'write',
|
|
98
|
+
title: 'Save mood',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
appId: 'mood-tracker',
|
|
102
|
+
description: 'Read mood history',
|
|
103
|
+
grantState: 'not-granted',
|
|
104
|
+
id: 'mood-tracker.listMoods',
|
|
105
|
+
name: 'listMoods',
|
|
106
|
+
risk: 'read',
|
|
107
|
+
title: 'List moods',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
mocks.requestAuthorization.mockResolvedValue(true);
|
|
112
|
+
mocks.grantServiceActions.mockResolvedValue({
|
|
113
|
+
grants: [
|
|
114
|
+
{
|
|
115
|
+
actionId,
|
|
116
|
+
caller: { surface: 'panel-app', appId: 'mood-calendar' },
|
|
117
|
+
grantedAt: '2026-05-28T00:00:00.000Z',
|
|
118
|
+
risk: 'write',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
actionId: 'mood-tracker.listMoods',
|
|
122
|
+
caller: { surface: 'panel-app', appId: 'mood-calendar' },
|
|
123
|
+
grantedAt: '2026-05-28T00:00:00.000Z',
|
|
124
|
+
risk: 'read',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
send({
|
|
130
|
+
method: 'invoke',
|
|
131
|
+
payload: { actionId, input: { mood: 'happy' } },
|
|
132
|
+
requestId: 'request-1',
|
|
133
|
+
type: 'nextclaw:panel-app-service-actions:request',
|
|
100
134
|
});
|
|
101
135
|
|
|
102
136
|
await waitFor(() => expect(postMessage).toHaveBeenCalled());
|
|
103
137
|
|
|
104
138
|
expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
actions: [
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
actionId,
|
|
142
|
+
actionTitle: 'Save mood',
|
|
143
|
+
risk: 'write',
|
|
144
|
+
}),
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
actionId: 'mood-tracker.listMoods',
|
|
147
|
+
actionTitle: 'List moods',
|
|
148
|
+
risk: 'read',
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
107
151
|
inputPreview: expect.stringContaining('happy'),
|
|
108
152
|
panelAppId: 'mood-calendar',
|
|
109
|
-
risk: 'write',
|
|
110
153
|
}));
|
|
111
|
-
expect(mocks.
|
|
154
|
+
expect(mocks.grantServiceActions).toHaveBeenCalledWith([
|
|
155
|
+
actionId,
|
|
156
|
+
'mood-tracker.listMoods',
|
|
157
|
+
], {
|
|
112
158
|
bridgeSessionToken: 'token-1',
|
|
113
159
|
});
|
|
160
|
+
expect(mocks.grantServiceAction).not.toHaveBeenCalled();
|
|
114
161
|
expect(mocks.invokeServiceAction).toHaveBeenCalledTimes(2);
|
|
115
162
|
expect(postMessage).toHaveBeenCalledWith({
|
|
116
163
|
data: { actionId, result: { saved: true } },
|
|
@@ -121,19 +168,8 @@ describe('PanelAppBridgeManager', () => {
|
|
|
121
168
|
});
|
|
122
169
|
|
|
123
170
|
it('opens the authorization flow before retrying a protected generateObject call', async () => {
|
|
124
|
-
const manager =
|
|
125
|
-
|
|
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
|
-
});
|
|
171
|
+
const manager = createManager();
|
|
172
|
+
const { postMessage, send } = createIframeHarness(manager);
|
|
137
173
|
mocks.generateAgentObject
|
|
138
174
|
.mockRejectedValueOnce(new NextClawClientError({
|
|
139
175
|
code: 'AUTHORIZATION_REQUIRED',
|
|
@@ -147,41 +183,24 @@ describe('PanelAppBridgeManager', () => {
|
|
|
147
183
|
grantedAt: '2026-05-28T00:00:00.000Z',
|
|
148
184
|
});
|
|
149
185
|
|
|
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',
|
|
186
|
+
send({
|
|
187
|
+
method: 'agent.generateObject',
|
|
188
|
+
payload: {
|
|
189
|
+
input: {
|
|
190
|
+
peerId: 'mood-summary',
|
|
191
|
+
prompt: 'summarize',
|
|
192
|
+
schema: { type: 'object' },
|
|
163
193
|
},
|
|
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
194
|
},
|
|
195
|
+
requestId: 'request-2',
|
|
196
|
+
type: 'nextclaw:panel-app-service-actions:request',
|
|
177
197
|
});
|
|
178
198
|
|
|
179
199
|
await waitFor(() => expect(postMessage).toHaveBeenCalled());
|
|
180
200
|
|
|
181
201
|
expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
182
|
-
actionId: 'agent:generateObject',
|
|
202
|
+
actions: [expect.objectContaining({ actionId: 'agent:generateObject' })],
|
|
183
203
|
panelAppId: 'mood-calendar',
|
|
184
|
-
risk: 'write',
|
|
185
204
|
}));
|
|
186
205
|
expect(mocks.grantAgentCapability).toHaveBeenCalledWith('agent:generateObject', {
|
|
187
206
|
bridgeSessionToken: 'token-1',
|
|
@@ -195,69 +214,34 @@ describe('PanelAppBridgeManager', () => {
|
|
|
195
214
|
}, '*');
|
|
196
215
|
});
|
|
197
216
|
|
|
198
|
-
it('
|
|
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;
|
|
205
|
-
mocks.createBridgeSession
|
|
206
|
-
.mockResolvedValueOnce({
|
|
207
|
-
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
208
|
-
id: 'session-1',
|
|
209
|
-
panelAppId: 'mood-calendar',
|
|
210
|
-
tabId: 'tab-1',
|
|
211
|
-
token: 'token-1',
|
|
212
|
-
})
|
|
213
|
-
.mockResolvedValueOnce({
|
|
214
|
-
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
215
|
-
id: 'session-2',
|
|
216
|
-
panelAppId: 'mood-calendar',
|
|
217
|
-
tabId: 'tab-1',
|
|
218
|
-
token: 'token-2',
|
|
219
|
-
});
|
|
217
|
+
it('uses the injected runtime token without creating a bridge session', async () => {
|
|
218
|
+
const manager = createManager();
|
|
219
|
+
const { postMessage, send } = createIframeHarness(manager);
|
|
220
220
|
mocks.listServiceActions.mockResolvedValue({ actions: [] });
|
|
221
221
|
|
|
222
|
-
for (const [requestId,
|
|
223
|
-
['request-1', '
|
|
224
|
-
['request-2', '
|
|
225
|
-
['request-3', '
|
|
222
|
+
for (const [requestId, runtimeToken] of [
|
|
223
|
+
['request-1', 'token-1'],
|
|
224
|
+
['request-2', 'token-2'],
|
|
225
|
+
['request-3', 'token-3'],
|
|
226
226
|
]) {
|
|
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
|
-
},
|
|
227
|
+
send({
|
|
228
|
+
method: 'list',
|
|
229
|
+
requestId,
|
|
230
|
+
runtimeToken,
|
|
231
|
+
type: 'nextclaw:panel-app-service-actions:request',
|
|
247
232
|
});
|
|
248
233
|
}
|
|
249
234
|
|
|
250
235
|
await waitFor(() => expect(postMessage).toHaveBeenCalledTimes(3));
|
|
251
236
|
|
|
252
|
-
expect(mocks.createBridgeSession).toHaveBeenCalledTimes(2);
|
|
253
237
|
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(1, {
|
|
254
238
|
bridgeSessionToken: 'token-1',
|
|
255
239
|
});
|
|
256
240
|
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(2, {
|
|
257
|
-
bridgeSessionToken: 'token-
|
|
241
|
+
bridgeSessionToken: 'token-2',
|
|
258
242
|
});
|
|
259
243
|
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(3, {
|
|
260
|
-
bridgeSessionToken: 'token-
|
|
244
|
+
bridgeSessionToken: 'token-3',
|
|
261
245
|
});
|
|
262
246
|
});
|
|
263
247
|
});
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
PanelAppAgentGenerateObjectResultView,
|
|
6
6
|
PanelAppAgentSendRequestView,
|
|
7
7
|
PanelAppAgentSendResultView,
|
|
8
|
-
PanelAppBridgeSessionView,
|
|
9
8
|
PanelAppCapabilityGrantView,
|
|
10
9
|
ServiceActionGrantView,
|
|
11
10
|
ServiceActionInvokeResultView,
|
|
@@ -18,6 +17,8 @@ import { nextclawClient } from '@/shared/lib/api';
|
|
|
18
17
|
type PanelAppBridgeRequest = {
|
|
19
18
|
type: 'nextclaw:panel-app-service-actions:request';
|
|
20
19
|
requestId: string;
|
|
20
|
+
appId: string;
|
|
21
|
+
runtimeToken: string;
|
|
21
22
|
method:
|
|
22
23
|
| 'agent.generateObject'
|
|
23
24
|
| 'agent.send'
|
|
@@ -32,6 +33,11 @@ type PanelAppBridgeRequest = {
|
|
|
32
33
|
};
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
type PanelAppBridgeContext = {
|
|
37
|
+
appId: string;
|
|
38
|
+
token: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
type PanelAppBridgeResponse =
|
|
36
42
|
| {
|
|
37
43
|
type: 'nextclaw:panel-app-service-actions:response';
|
|
@@ -51,8 +57,6 @@ type PanelAppBridgeResponse =
|
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
export class PanelAppBridgeManager {
|
|
54
|
-
private readonly sessions = new Map<string, Promise<PanelAppBridgeSessionView>>();
|
|
55
|
-
|
|
56
60
|
constructor(private readonly authorizationManager: ServiceActionAuthorizationManager) {}
|
|
57
61
|
|
|
58
62
|
handleIframeMessage = ({ event, iframe, iframeInstanceId, tab }: DocBrowserIframeMessageParams): void => {
|
|
@@ -70,7 +74,7 @@ export class PanelAppBridgeManager {
|
|
|
70
74
|
request: PanelAppBridgeRequest,
|
|
71
75
|
): Promise<void> => {
|
|
72
76
|
try {
|
|
73
|
-
const session =
|
|
77
|
+
const session = this.getBridgeContext(request);
|
|
74
78
|
const data = await this.dispatchRequest(session, request);
|
|
75
79
|
this.postResponse(params, {
|
|
76
80
|
type: 'nextclaw:panel-app-service-actions:response',
|
|
@@ -89,7 +93,7 @@ export class PanelAppBridgeManager {
|
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
private dispatchRequest = async (
|
|
92
|
-
session:
|
|
96
|
+
session: PanelAppBridgeContext,
|
|
93
97
|
request: PanelAppBridgeRequest,
|
|
94
98
|
): Promise<
|
|
95
99
|
| PanelAppAgentGenerateObjectResultView
|
|
@@ -124,7 +128,7 @@ export class PanelAppBridgeManager {
|
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
private invokeWithAuthorization = async (
|
|
127
|
-
session:
|
|
131
|
+
session: PanelAppBridgeContext,
|
|
128
132
|
request: PanelAppBridgeRequest,
|
|
129
133
|
): Promise<ServiceActionInvokeResultView> => {
|
|
130
134
|
const actionId = this.requireActionId(request);
|
|
@@ -148,7 +152,7 @@ export class PanelAppBridgeManager {
|
|
|
148
152
|
};
|
|
149
153
|
|
|
150
154
|
private sendAgentMessageWithAuthorization = async (
|
|
151
|
-
session:
|
|
155
|
+
session: PanelAppBridgeContext,
|
|
152
156
|
request: PanelAppBridgeRequest,
|
|
153
157
|
): Promise<PanelAppAgentSendResultView> => {
|
|
154
158
|
try {
|
|
@@ -169,7 +173,7 @@ export class PanelAppBridgeManager {
|
|
|
169
173
|
};
|
|
170
174
|
|
|
171
175
|
private generateAgentObjectWithAuthorization = async (
|
|
172
|
-
session:
|
|
176
|
+
session: PanelAppBridgeContext,
|
|
173
177
|
request: PanelAppBridgeRequest,
|
|
174
178
|
): Promise<PanelAppAgentGenerateObjectResultView> => {
|
|
175
179
|
try {
|
|
@@ -190,17 +194,19 @@ export class PanelAppBridgeManager {
|
|
|
190
194
|
};
|
|
191
195
|
|
|
192
196
|
private confirmAndGrantAgentCapability = async (
|
|
193
|
-
session:
|
|
197
|
+
session: PanelAppBridgeContext,
|
|
194
198
|
capability: PanelAppAgentCapabilityView,
|
|
195
199
|
): Promise<PanelAppCapabilityGrantView> => {
|
|
196
200
|
const allowed = await this.authorizationManager.requestAuthorization({
|
|
197
|
-
panelAppId: session.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
panelAppId: session.appId,
|
|
202
|
+
actions: [{
|
|
203
|
+
actionId: capability,
|
|
204
|
+
actionTitle: capability === 'agent:send' ? 'Send agent message' : 'Generate object',
|
|
205
|
+
actionDescription: capability === 'agent:send'
|
|
206
|
+
? 'Send a message to a NextClaw Agent session.'
|
|
207
|
+
: 'Send context to a NextClaw Agent session and receive a structured object.',
|
|
208
|
+
risk: 'write',
|
|
209
|
+
}],
|
|
204
210
|
});
|
|
205
211
|
if (!allowed) {
|
|
206
212
|
throw new NextClawClientError({
|
|
@@ -215,17 +221,19 @@ export class PanelAppBridgeManager {
|
|
|
215
221
|
};
|
|
216
222
|
|
|
217
223
|
private confirmAndGrant = async (
|
|
218
|
-
session:
|
|
224
|
+
session: PanelAppBridgeContext,
|
|
219
225
|
actionId: string,
|
|
220
226
|
input?: Record<string, unknown>,
|
|
221
227
|
): Promise<ServiceActionGrantView> => {
|
|
222
|
-
const
|
|
228
|
+
const actions = await this.listGrantCandidateActions(session, actionId);
|
|
223
229
|
const allowed = await this.authorizationManager.requestAuthorization({
|
|
224
|
-
panelAppId: session.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
panelAppId: session.appId,
|
|
231
|
+
actions: actions.map((action) => ({
|
|
232
|
+
actionId: action.id,
|
|
233
|
+
actionTitle: action.title,
|
|
234
|
+
actionDescription: action.description,
|
|
235
|
+
risk: action.risk,
|
|
236
|
+
})),
|
|
229
237
|
inputPreview: this.createInputPreview(input),
|
|
230
238
|
});
|
|
231
239
|
if (!allowed) {
|
|
@@ -234,22 +242,48 @@ export class PanelAppBridgeManager {
|
|
|
234
242
|
message: `Permission rejected for ${actionId}.`,
|
|
235
243
|
});
|
|
236
244
|
}
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
const result = await nextclawClient.serviceApps.grantServiceActions(
|
|
246
|
+
actions.map((action) => action.id),
|
|
239
247
|
{ bridgeSessionToken: session.token },
|
|
240
248
|
);
|
|
249
|
+
const grant = result.grants.find((entry) => entry.actionId === actionId) ?? result.grants[0];
|
|
250
|
+
if (!grant) {
|
|
251
|
+
throw new Error(`No permission grant returned for ${actionId}.`);
|
|
252
|
+
}
|
|
253
|
+
return grant;
|
|
241
254
|
};
|
|
242
255
|
|
|
243
|
-
private
|
|
244
|
-
session:
|
|
256
|
+
private listGrantCandidateActions = async (
|
|
257
|
+
session: PanelAppBridgeContext,
|
|
245
258
|
actionId: string,
|
|
246
|
-
): Promise<ServiceActionListView['actions'][number]
|
|
247
|
-
const
|
|
259
|
+
): Promise<Array<ServiceActionListView['actions'][number]>> => {
|
|
260
|
+
const actionList = await nextclawClient.serviceApps.listServiceActions({
|
|
248
261
|
bridgeSessionToken: session.token,
|
|
249
262
|
});
|
|
250
|
-
|
|
263
|
+
const target = actionList.actions.find(
|
|
251
264
|
(action: ServiceActionListView['actions'][number]) => action.id === actionId,
|
|
252
265
|
);
|
|
266
|
+
if (target && target.grantState !== 'not-granted') {
|
|
267
|
+
return [target];
|
|
268
|
+
}
|
|
269
|
+
const ungrantedActions = actionList.actions.filter((action) =>
|
|
270
|
+
action.grantState === 'not-granted'
|
|
271
|
+
);
|
|
272
|
+
const orderedActions = [
|
|
273
|
+
...ungrantedActions.filter((action) => action.id === actionId),
|
|
274
|
+
...ungrantedActions.filter((action) => action.id !== actionId),
|
|
275
|
+
];
|
|
276
|
+
if (orderedActions.length > 0) {
|
|
277
|
+
return orderedActions;
|
|
278
|
+
}
|
|
279
|
+
return [{
|
|
280
|
+
appId: target?.appId ?? actionId.split('.')[0] ?? actionId,
|
|
281
|
+
description: target?.description,
|
|
282
|
+
id: actionId,
|
|
283
|
+
name: target?.name ?? actionId,
|
|
284
|
+
risk: target?.risk ?? 'dangerous',
|
|
285
|
+
title: target?.title,
|
|
286
|
+
}];
|
|
253
287
|
};
|
|
254
288
|
|
|
255
289
|
private createInputPreview = (input: Record<string, unknown> | undefined): string | undefined => {
|
|
@@ -264,29 +298,11 @@ export class PanelAppBridgeManager {
|
|
|
264
298
|
}
|
|
265
299
|
};
|
|
266
300
|
|
|
267
|
-
private
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
let session = this.sessions.get(key);
|
|
273
|
-
if (!session) {
|
|
274
|
-
session = nextclawClient.panelApps.createBridgeSession({
|
|
275
|
-
panelAppId,
|
|
276
|
-
tabId: params.tab.id,
|
|
277
|
-
});
|
|
278
|
-
this.sessions.set(key, session);
|
|
279
|
-
}
|
|
280
|
-
return await session;
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
private readPanelAppId = (url: string): string => {
|
|
284
|
-
const parsed = new URL(url, window.location.origin);
|
|
285
|
-
const match = /^\/api\/panel-apps\/([^/]+)\/content$/.exec(parsed.pathname);
|
|
286
|
-
if (!match?.[1]) {
|
|
287
|
-
throw new Error('Current tab is not a Panel App.');
|
|
288
|
-
}
|
|
289
|
-
return decodeURIComponent(match[1]);
|
|
301
|
+
private getBridgeContext = (request: PanelAppBridgeRequest): PanelAppBridgeContext => {
|
|
302
|
+
return {
|
|
303
|
+
appId: request.appId,
|
|
304
|
+
token: request.runtimeToken,
|
|
305
|
+
};
|
|
290
306
|
};
|
|
291
307
|
|
|
292
308
|
private requireActionId = (request: PanelAppBridgeRequest): string => {
|
|
@@ -353,6 +369,8 @@ export class PanelAppBridgeManager {
|
|
|
353
369
|
return (
|
|
354
370
|
candidate.type === 'nextclaw:panel-app-service-actions:request' &&
|
|
355
371
|
typeof candidate.requestId === 'string' &&
|
|
372
|
+
typeof candidate.runtimeToken === 'string' &&
|
|
373
|
+
typeof candidate.appId === 'string' &&
|
|
356
374
|
(candidate.method === 'invoke' ||
|
|
357
375
|
candidate.method === 'agent.send' ||
|
|
358
376
|
candidate.method === 'agent.generateObject' ||
|
|
@@ -7,13 +7,17 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
|
|
|
7
7
|
const updatedAt = overrides.updatedAt ?? createdAt;
|
|
8
8
|
return {
|
|
9
9
|
id: overrides.id ?? overrides.fileName ?? 'demo.panel.html',
|
|
10
|
+
appId: overrides.appId ?? 'demo',
|
|
10
11
|
fileName: overrides.fileName ?? 'demo.panel.html',
|
|
12
|
+
kind: overrides.kind ?? 'single-file',
|
|
11
13
|
title: overrides.title ?? 'Demo',
|
|
12
14
|
contentPath: overrides.contentPath ?? '/api/panel-apps/demo/content',
|
|
13
15
|
createdAt,
|
|
14
16
|
updatedAt,
|
|
15
17
|
sizeBytes: overrides.sizeBytes ?? 12,
|
|
16
18
|
favorite: overrides.favorite ?? false,
|
|
19
|
+
clientDeclared: overrides.clientDeclared ?? false,
|
|
20
|
+
clientGranted: overrides.clientGranted ?? false,
|
|
17
21
|
openCount: overrides.openCount ?? 0,
|
|
18
22
|
...overrides,
|
|
19
23
|
};
|