@nextclaw/ui 0.21.0 → 0.22.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 +22 -0
- package/dist/assets/{api-DNLQPHfQ.js → api-m33O1Kb0.js} +3 -3
- package/dist/assets/{appearance-settings-page-BlAFMOGV.js → appearance-settings-page-x4Vjnfmz.js} +1 -1
- package/dist/assets/{book-open-BryfPTTa.js → book-open-BHiasMCg.js} +1 -1
- package/dist/assets/{channels-list-page-Cfbgubyv.js → channels-list-page-DOxJZeXq.js} +1 -1
- package/dist/assets/{config-split-page-Dd8xG0Jk.js → config-split-page-DBnMEP1o.js} +1 -1
- package/dist/assets/{confirm-dialog-CZIWjnJ6.js → confirm-dialog-f5NKLh5I.js} +2 -2
- package/dist/assets/{createLucideIcon-BxwVe8ZD.js → createLucideIcon-CHALGA42.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-C6YxsHeV.js → desktop-capabilities-page-DixayAe6.js} +1 -1
- package/dist/assets/{desktop-update-config-BVN4QPJn.js → desktop-update-config-ChhfeOHj.js} +1 -1
- package/dist/assets/{dist-Cd8Nm3BW.js → dist-_UMsWSY0.js} +1 -1
- package/dist/assets/{dist-BRloKr3S.js → dist-sD6V4nHX.js} +1 -1
- package/dist/assets/doc-browser-Bq7LJsQY.js +1 -0
- package/dist/assets/doc-browser-BuDgpY2e.js +1 -0
- package/dist/assets/{doc-browser-context-3JGufiMA.js → doc-browser-context-Capim3og.js} +1 -1
- package/dist/assets/{extensions-Dedd2oDI.js → extensions-Bdclr3Wr.js} +1 -1
- package/dist/assets/{form-actions-FWCEof3V.js → form-actions-CH0kWhEt.js} +1 -1
- package/dist/assets/index-BjxnLYE3.js +109 -0
- package/dist/assets/index-CBmwJjwE.css +1 -0
- package/dist/assets/loader-circle-CH_0iPLy.js +1 -0
- package/dist/assets/{mcp-marketplace-page-e9Rmfz-d.js → mcp-marketplace-page-BUsxLVB5.js} +2 -2
- package/dist/assets/mcp-marketplace-page-GPRV3fB0.js +1 -0
- package/dist/assets/{model-config-page-C173Ssil.js → model-config-page-CIevwqp4.js} +1 -1
- package/dist/assets/{navigation-link-DTPOvGGL.js → navigation-link-CAUiM3zS.js} +1 -1
- package/dist/assets/plus-B3obD55V.js +1 -0
- package/dist/assets/{privacy-settings-page-CRserBVF.js → privacy-settings-page-CNKfZsYH.js} +1 -1
- package/dist/assets/{providers-config-page-C8jUtnZb.js → providers-config-page-DAah2NIS.js} +1 -1
- package/dist/assets/{react-BAzO6LZS.js → react-Co6JiC1n.js} +1 -1
- package/dist/assets/{refresh-cw-BCj-yjde.js → refresh-cw-BG_LX3SS.js} +1 -1
- package/dist/assets/remote-DuscgfvR.js +1 -0
- package/dist/assets/{rotate-cw-BPnW4qUl.js → rotate-cw-Cz7I_m7O.js} +1 -1
- package/dist/assets/{runtime-config-page-aeQ1Dryz.js → runtime-config-page-Cl1RpmSC.js} +1 -1
- package/dist/assets/{save-KT0MumfJ.js → save-BiHnb8ps.js} +1 -1
- package/dist/assets/{search-Cv1Hx_nx.js → search-C47TyRg0.js} +1 -1
- package/dist/assets/{search-config-page-b283V-fo.js → search-config-page-BAIzd6gN.js} +1 -1
- package/dist/assets/{secrets-config-page-ss7x3BOo.js → secrets-config-page-Ddi-n43R.js} +1 -1
- package/dist/assets/security-config-C8MHnE1r.js +1 -0
- package/dist/assets/{select-BVgD4EpG.js → select-BzQKfW1e.js} +2 -2
- package/dist/assets/skeleton-BXFAE9SE.js +1 -0
- package/dist/assets/{tag-chip-DZ4ircUB.js → tag-chip-C3K814HZ.js} +1 -1
- package/dist/assets/x-C1r9IXet.js +1 -0
- package/dist/index.html +29 -21
- package/dist/themes/island/island-atmosphere.webp +0 -0
- package/index.html +8 -0
- package/package.json +4 -4
- package/public/themes/island/island-atmosphere.webp +0 -0
- package/src/app/components/layout/header.tsx +4 -1
- package/src/app/components/layout/page-layout.tsx +4 -1
- package/src/app/components/layout/sidebar.tsx +1 -0
- package/src/app/styles/island-theme.css +542 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +1 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +5 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +4 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +10 -0
- package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +2 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +1 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +1 -0
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +9 -0
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +1 -0
- package/src/features/service-apps/components/__tests__/service-apps-panel.test.tsx +35 -1
- package/src/features/service-apps/components/service-action-row.tsx +130 -0
- package/src/features/service-apps/components/service-app-list-item.tsx +11 -66
- package/src/features/service-apps/components/service-apps-panel.tsx +12 -0
- package/src/features/service-apps/hooks/use-service-apps.ts +20 -1
- package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +2 -2
- package/src/features/side-dock/components/side-dock.tsx +1 -0
- package/src/index.css +1 -0
- package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +8 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +56 -2
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +2 -0
- package/src/shared/components/ui/context-menu/context-menu.tsx +1 -0
- package/src/shared/components/ui/dialog.tsx +2 -0
- package/src/shared/components/ui/overlays/sheet.tsx +2 -0
- package/src/shared/components/ui/popover.tsx +1 -0
- package/src/shared/components/ui/select.tsx +1 -0
- package/src/shared/components/ui/tooltip.tsx +1 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +1 -0
- package/src/shared/lib/theme/index.test.ts +9 -0
- package/src/shared/lib/theme/index.ts +2 -0
- package/dist/assets/doc-browser-Bzgqmmiu.js +0 -1
- package/dist/assets/doc-browser-D0qI0Z_R.js +0 -1
- package/dist/assets/index-CXkHcESm.css +0 -1
- package/dist/assets/index-D25tRg1p.js +0 -109
- package/dist/assets/loader-circle-BoyCoFps.js +0 -1
- package/dist/assets/mcp-marketplace-page-Cn-9CDh3.js +0 -1
- package/dist/assets/plus-D1R4Vipq.js +0 -1
- package/dist/assets/remote-6BHWP5zi.js +0 -1
- package/dist/assets/security-config-HHz62oxH.js +0 -1
- package/dist/assets/skeleton-ChUmcyys.js +0 -1
- package/dist/assets/x-BWDRfVWL.js +0 -1
|
@@ -11,6 +11,7 @@ const refetchServiceApps = vi.fn();
|
|
|
11
11
|
const refetchAppData = vi.fn();
|
|
12
12
|
const restartServiceAppMutate = vi.fn();
|
|
13
13
|
const revokeServiceActionGrantMutate = vi.fn();
|
|
14
|
+
const grantAgentServiceActionsMutate = vi.fn();
|
|
14
15
|
const resetDeleteServiceApp = vi.fn();
|
|
15
16
|
|
|
16
17
|
vi.mock('@/features/app-data/hooks/use-app-data', () => ({
|
|
@@ -66,6 +67,10 @@ vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
|
66
67
|
isPending: false,
|
|
67
68
|
mutateAsync: discoverServiceAppActionsMutateAsync,
|
|
68
69
|
}),
|
|
70
|
+
useGrantAgentServiceActions: () => ({
|
|
71
|
+
isPending: false,
|
|
72
|
+
mutate: grantAgentServiceActionsMutate,
|
|
73
|
+
}),
|
|
69
74
|
useRestartServiceApp: () => ({
|
|
70
75
|
mutate: restartServiceAppMutate,
|
|
71
76
|
}),
|
|
@@ -79,7 +84,18 @@ vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
|
79
84
|
refetch: refetchServiceActionGrants,
|
|
80
85
|
}),
|
|
81
86
|
useServiceActions: () => ({
|
|
82
|
-
data: {
|
|
87
|
+
data: {
|
|
88
|
+
actions: [{
|
|
89
|
+
id: 'notes.read',
|
|
90
|
+
appId: 'notes',
|
|
91
|
+
name: 'read',
|
|
92
|
+
title: 'Read notes',
|
|
93
|
+
description: 'Read persisted notes.',
|
|
94
|
+
risk: 'read',
|
|
95
|
+
runtimeState: 'matched',
|
|
96
|
+
grantState: 'not-granted',
|
|
97
|
+
}],
|
|
98
|
+
},
|
|
83
99
|
isError: false,
|
|
84
100
|
isLoading: false,
|
|
85
101
|
refetch: refetchServiceActions,
|
|
@@ -122,6 +138,10 @@ vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
|
122
138
|
}),
|
|
123
139
|
}));
|
|
124
140
|
|
|
141
|
+
vi.mock('@/shared/hooks/use-agents', () => ({
|
|
142
|
+
useAgents: () => ({ data: { agents: [{ id: 'main', displayName: 'Main' }] } }),
|
|
143
|
+
}));
|
|
144
|
+
|
|
125
145
|
describe('ServiceAppsPanel', () => {
|
|
126
146
|
it('keeps service actions progressive and deletes through a confirm dialog', async () => {
|
|
127
147
|
const user = userEvent.setup();
|
|
@@ -171,4 +191,18 @@ describe('ServiceAppsPanel', () => {
|
|
|
171
191
|
|
|
172
192
|
expect(onManagePackage).toHaveBeenCalledWith('nextclaw.personal-organizer');
|
|
173
193
|
});
|
|
194
|
+
|
|
195
|
+
it('explicitly grants a service action to the selected agent', async () => {
|
|
196
|
+
const user = userEvent.setup();
|
|
197
|
+
render(<ServiceAppsPanel onManagePackage={vi.fn()} />);
|
|
198
|
+
|
|
199
|
+
await user.click(screen.getAllByRole('button', { name: /Actions/ })[0]!);
|
|
200
|
+
await user.click(screen.getByRole('button', { name: 'Grant to Agent' }));
|
|
201
|
+
await user.click(screen.getByRole('button', { name: 'Main' }));
|
|
202
|
+
|
|
203
|
+
expect(grantAgentServiceActionsMutate).toHaveBeenCalledWith({
|
|
204
|
+
actionIds: ['notes.read'],
|
|
205
|
+
agentId: 'main',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
174
208
|
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
AgentProfileView,
|
|
4
|
+
ServiceActionGrantView,
|
|
5
|
+
ServiceActionListView,
|
|
6
|
+
} from "@nextclaw/client-sdk";
|
|
7
|
+
import { Bot, ShieldCheck, ShieldPlus, Trash2, Wrench } from "lucide-react";
|
|
8
|
+
import {
|
|
9
|
+
Popover,
|
|
10
|
+
PopoverContent,
|
|
11
|
+
PopoverTrigger,
|
|
12
|
+
} from "@/shared/components/ui/popover";
|
|
13
|
+
import { t } from "@/shared/lib/i18n";
|
|
14
|
+
|
|
15
|
+
type ServiceActionView = ServiceActionListView["actions"][number];
|
|
16
|
+
|
|
17
|
+
export function ServiceActionRow({
|
|
18
|
+
action,
|
|
19
|
+
grants,
|
|
20
|
+
agents,
|
|
21
|
+
grantAgentPending,
|
|
22
|
+
onGrantAgent,
|
|
23
|
+
onRevoke,
|
|
24
|
+
}: {
|
|
25
|
+
action: ServiceActionView;
|
|
26
|
+
grants: ServiceActionGrantView[];
|
|
27
|
+
agents: AgentProfileView[];
|
|
28
|
+
grantAgentPending: boolean;
|
|
29
|
+
onGrantAgent: (actionId: string, agentId: string) => void;
|
|
30
|
+
onRevoke: (grant: ServiceActionGrantView) => void;
|
|
31
|
+
}) {
|
|
32
|
+
const [agentMenuOpen, setAgentMenuOpen] = useState(false);
|
|
33
|
+
const grantedAgentIds = new Set(
|
|
34
|
+
grants.flatMap((grant) =>
|
|
35
|
+
grant.caller.surface === "agent" ? [grant.caller.agentId] : [],
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
const availableAgents = agents.filter((agent) => !grantedAgentIds.has(agent.id));
|
|
39
|
+
return (
|
|
40
|
+
<div className="py-2">
|
|
41
|
+
<div className="flex items-center justify-between gap-2">
|
|
42
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
43
|
+
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-muted-foreground">
|
|
44
|
+
<Wrench className="h-3.5 w-3.5" />
|
|
45
|
+
</span>
|
|
46
|
+
<div className="min-w-0">
|
|
47
|
+
<div className="truncate text-xs font-medium text-foreground">
|
|
48
|
+
{action.title ?? action.name}
|
|
49
|
+
</div>
|
|
50
|
+
{action.description ? (
|
|
51
|
+
<div className="truncate text-[11px] text-muted-foreground">
|
|
52
|
+
{action.description}
|
|
53
|
+
</div>
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
58
|
+
{action.runtimeState ? (
|
|
59
|
+
<span className="text-[11px] text-muted-foreground">
|
|
60
|
+
{t(`serviceAppsRuntimeState_${action.runtimeState}`)}
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
<span className="text-[11px] text-muted-foreground">{action.risk}</span>
|
|
64
|
+
<Popover open={agentMenuOpen} onOpenChange={setAgentMenuOpen}>
|
|
65
|
+
<PopoverTrigger asChild>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
disabled={grantAgentPending || availableAgents.length === 0}
|
|
69
|
+
className="ml-1 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-40"
|
|
70
|
+
title={t("serviceAppsGrantToAgent")}
|
|
71
|
+
aria-label={t("serviceAppsGrantToAgent")}
|
|
72
|
+
>
|
|
73
|
+
<ShieldPlus className="h-3.5 w-3.5" />
|
|
74
|
+
</button>
|
|
75
|
+
</PopoverTrigger>
|
|
76
|
+
<PopoverContent align="end" className="w-56 rounded-xl p-1.5">
|
|
77
|
+
<div className="px-2 py-1.5 text-[11px] font-medium text-muted-foreground">
|
|
78
|
+
{t("serviceAppsGrantToAgent")}
|
|
79
|
+
</div>
|
|
80
|
+
{availableAgents.map((agent) => (
|
|
81
|
+
<button
|
|
82
|
+
key={agent.id}
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={() => {
|
|
85
|
+
onGrantAgent(action.id, agent.id);
|
|
86
|
+
setAgentMenuOpen(false);
|
|
87
|
+
}}
|
|
88
|
+
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
89
|
+
>
|
|
90
|
+
<Bot className="h-3.5 w-3.5 text-muted-foreground" />
|
|
91
|
+
<span className="min-w-0 truncate">
|
|
92
|
+
{agent.displayName?.trim() || agent.id}
|
|
93
|
+
</span>
|
|
94
|
+
</button>
|
|
95
|
+
))}
|
|
96
|
+
</PopoverContent>
|
|
97
|
+
</Popover>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{grants.map((grant) => (
|
|
101
|
+
<div
|
|
102
|
+
key={`${grant.caller.surface}:${getGrantCallerId(grant)}:${grant.actionId}`}
|
|
103
|
+
className="mt-1.5 flex items-center justify-between gap-2 pl-8"
|
|
104
|
+
>
|
|
105
|
+
<div className="flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
106
|
+
<ShieldCheck className="h-3 w-3 shrink-0 text-emerald-500" />
|
|
107
|
+
<span className="truncate">
|
|
108
|
+
{t("serviceAppsGrantedTo")} {getGrantCallerId(grant)}
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => onRevoke(grant)}
|
|
114
|
+
className="rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-rose-600 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
115
|
+
title={t("serviceAppsRevokeGrant")}
|
|
116
|
+
aria-label={t("serviceAppsRevokeGrant")}
|
|
117
|
+
>
|
|
118
|
+
<Trash2 className="h-3 w-3" />
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getGrantCallerId(grant: ServiceActionGrantView): string {
|
|
127
|
+
return grant.caller.surface === "panel-app"
|
|
128
|
+
? grant.caller.appId
|
|
129
|
+
: grant.caller.agentId;
|
|
130
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type {
|
|
3
|
+
AgentProfileView,
|
|
3
4
|
ServiceActionGrantView,
|
|
4
5
|
ServiceActionListView,
|
|
5
6
|
AppDataEntry,
|
|
@@ -16,13 +17,12 @@ import {
|
|
|
16
17
|
PauseCircle,
|
|
17
18
|
Radar,
|
|
18
19
|
Server,
|
|
19
|
-
ShieldCheck,
|
|
20
20
|
Trash2,
|
|
21
21
|
Unplug,
|
|
22
22
|
type LucideIcon,
|
|
23
|
-
Wrench,
|
|
24
23
|
} from "lucide-react";
|
|
25
24
|
import { ServiceAppDeleteDialog } from "@/features/service-apps/components/service-app-delete-dialog";
|
|
25
|
+
import { ServiceActionRow } from "@/features/service-apps/components/service-action-row";
|
|
26
26
|
import {
|
|
27
27
|
Popover,
|
|
28
28
|
PopoverContent,
|
|
@@ -45,6 +45,7 @@ export function ServiceAppListItem({
|
|
|
45
45
|
actions,
|
|
46
46
|
actionsOpen,
|
|
47
47
|
grants,
|
|
48
|
+
agents,
|
|
48
49
|
deletePending,
|
|
49
50
|
deleteError,
|
|
50
51
|
dataEntry,
|
|
@@ -57,11 +58,14 @@ export function ServiceAppListItem({
|
|
|
57
58
|
onResetDelete,
|
|
58
59
|
onRestart,
|
|
59
60
|
onRevoke,
|
|
61
|
+
onGrantAgent,
|
|
62
|
+
grantAgentPending,
|
|
60
63
|
}: {
|
|
61
64
|
app: ServiceAppRecordView;
|
|
62
65
|
actions: ServiceActionView[];
|
|
63
66
|
actionsOpen: boolean;
|
|
64
67
|
grants: ServiceActionGrantView[];
|
|
68
|
+
agents: AgentProfileView[];
|
|
65
69
|
deletePending: boolean;
|
|
66
70
|
deleteError: unknown;
|
|
67
71
|
dataEntry?: AppDataEntry;
|
|
@@ -74,6 +78,8 @@ export function ServiceAppListItem({
|
|
|
74
78
|
onResetDelete: () => void;
|
|
75
79
|
onRestart: (appId: string) => void;
|
|
76
80
|
onRevoke: (grant: ServiceActionGrantView) => void;
|
|
81
|
+
onGrantAgent: (actionId: string, agentId: string) => void;
|
|
82
|
+
grantAgentPending: boolean;
|
|
77
83
|
}) {
|
|
78
84
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
79
85
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
@@ -245,6 +251,9 @@ export function ServiceAppListItem({
|
|
|
245
251
|
grants={grants.filter(
|
|
246
252
|
(grant) => grant.actionId === action.id,
|
|
247
253
|
)}
|
|
254
|
+
agents={agents}
|
|
255
|
+
grantAgentPending={grantAgentPending}
|
|
256
|
+
onGrantAgent={onGrantAgent}
|
|
248
257
|
onRevoke={onRevoke}
|
|
249
258
|
/>
|
|
250
259
|
))}
|
|
@@ -433,67 +442,3 @@ function ServiceAppDiagnosticRow({
|
|
|
433
442
|
</>
|
|
434
443
|
);
|
|
435
444
|
}
|
|
436
|
-
|
|
437
|
-
function ServiceActionRow({
|
|
438
|
-
action,
|
|
439
|
-
grants,
|
|
440
|
-
onRevoke,
|
|
441
|
-
}: {
|
|
442
|
-
action: ServiceActionView;
|
|
443
|
-
grants: ServiceActionGrantView[];
|
|
444
|
-
onRevoke: (grant: ServiceActionGrantView) => void;
|
|
445
|
-
}) {
|
|
446
|
-
return (
|
|
447
|
-
<div className="py-2">
|
|
448
|
-
<div className="flex items-center justify-between gap-2">
|
|
449
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
450
|
-
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-muted-foreground">
|
|
451
|
-
<Wrench className="h-3.5 w-3.5" />
|
|
452
|
-
</span>
|
|
453
|
-
<div className="min-w-0">
|
|
454
|
-
<div className="truncate text-xs font-medium text-foreground">
|
|
455
|
-
{action.title ?? action.name}
|
|
456
|
-
</div>
|
|
457
|
-
{action.description ? (
|
|
458
|
-
<div className="truncate text-[11px] text-muted-foreground">
|
|
459
|
-
{action.description}
|
|
460
|
-
</div>
|
|
461
|
-
) : null}
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
465
|
-
{action.runtimeState ? (
|
|
466
|
-
<span className="text-[11px] text-muted-foreground">
|
|
467
|
-
{t(`serviceAppsRuntimeState_${action.runtimeState}`)}
|
|
468
|
-
</span>
|
|
469
|
-
) : null}
|
|
470
|
-
<span className="text-[11px] text-muted-foreground">
|
|
471
|
-
{action.risk}
|
|
472
|
-
</span>
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
|
-
{grants.map((grant) => (
|
|
476
|
-
<div
|
|
477
|
-
key={`${grant.caller.surface}:${grant.caller.appId}:${grant.actionId}`}
|
|
478
|
-
className="mt-1.5 flex items-center justify-between gap-2 pl-8"
|
|
479
|
-
>
|
|
480
|
-
<div className="flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
481
|
-
<ShieldCheck className="h-3 w-3 shrink-0 text-emerald-500" />
|
|
482
|
-
<span className="truncate">
|
|
483
|
-
{t("serviceAppsGrantedTo")} {grant.caller.appId}
|
|
484
|
-
</span>
|
|
485
|
-
</div>
|
|
486
|
-
<button
|
|
487
|
-
type="button"
|
|
488
|
-
onClick={() => onRevoke(grant)}
|
|
489
|
-
className="rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-rose-600 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
490
|
-
title={t("serviceAppsRevokeGrant")}
|
|
491
|
-
aria-label={t("serviceAppsRevokeGrant")}
|
|
492
|
-
>
|
|
493
|
-
<Trash2 className="h-3 w-3" />
|
|
494
|
-
</button>
|
|
495
|
-
</div>
|
|
496
|
-
))}
|
|
497
|
-
</div>
|
|
498
|
-
);
|
|
499
|
-
}
|
|
@@ -11,12 +11,14 @@ import { ServiceAppListItem } from "@/features/service-apps/components/service-a
|
|
|
11
11
|
import {
|
|
12
12
|
useDeleteServiceApp,
|
|
13
13
|
useDiscoverServiceAppActions,
|
|
14
|
+
useGrantAgentServiceActions,
|
|
14
15
|
useRestartServiceApp,
|
|
15
16
|
useRevokeServiceActionGrant,
|
|
16
17
|
useServiceActionGrants,
|
|
17
18
|
useServiceActions,
|
|
18
19
|
useServiceApps,
|
|
19
20
|
} from "@/features/service-apps/hooks/use-service-apps";
|
|
21
|
+
import { useAgents } from "@/shared/hooks/use-agents";
|
|
20
22
|
import {
|
|
21
23
|
Tooltip,
|
|
22
24
|
TooltipContent,
|
|
@@ -36,10 +38,12 @@ export function ServiceAppsPanel({
|
|
|
36
38
|
const appData = useAppData();
|
|
37
39
|
const serviceActions = useServiceActions();
|
|
38
40
|
const serviceActionGrants = useServiceActionGrants();
|
|
41
|
+
const agents = useAgents();
|
|
39
42
|
const deleteServiceApp = useDeleteServiceApp();
|
|
40
43
|
const restartServiceApp = useRestartServiceApp();
|
|
41
44
|
const discoverServiceAppActions = useDiscoverServiceAppActions();
|
|
42
45
|
const revokeServiceActionGrant = useRevokeServiceActionGrant();
|
|
46
|
+
const grantAgentServiceActions = useGrantAgentServiceActions();
|
|
43
47
|
const [discoveredActionsByApp, setDiscoveredActionsByApp] = useState<
|
|
44
48
|
Record<string, ServiceActionView[]>
|
|
45
49
|
>({});
|
|
@@ -163,6 +167,7 @@ export function ServiceAppsPanel({
|
|
|
163
167
|
}
|
|
164
168
|
actionsOpen={Boolean(expandedActionsByApp[app.id])}
|
|
165
169
|
grants={grants}
|
|
170
|
+
agents={agents.data?.agents ?? []}
|
|
166
171
|
deletePending={deleteServiceApp.isPending}
|
|
167
172
|
deleteError={deleteServiceApp.error}
|
|
168
173
|
dataEntry={workspaceDataEntries.find((entry) => entry.appId === app.id)}
|
|
@@ -188,6 +193,13 @@ export function ServiceAppsPanel({
|
|
|
188
193
|
caller: grant.caller,
|
|
189
194
|
})
|
|
190
195
|
}
|
|
196
|
+
onGrantAgent={(actionId, agentId) =>
|
|
197
|
+
void grantAgentServiceActions.mutate({
|
|
198
|
+
actionIds: [actionId],
|
|
199
|
+
agentId,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
grantAgentPending={grantAgentServiceActions.isPending}
|
|
191
203
|
/>
|
|
192
204
|
))}
|
|
193
205
|
</div>
|
|
@@ -71,7 +71,9 @@ export function useRevokeServiceActionGrant() {
|
|
|
71
71
|
return useMutation({
|
|
72
72
|
mutationFn: (params: {
|
|
73
73
|
actionId: string;
|
|
74
|
-
caller:
|
|
74
|
+
caller:
|
|
75
|
+
| { surface: 'panel-app'; appId: string }
|
|
76
|
+
| { surface: 'agent'; agentId: string };
|
|
75
77
|
}) => nextclawClient.serviceApps.revokeServiceActionGrant(params),
|
|
76
78
|
onSuccess: async () => {
|
|
77
79
|
await Promise.all([
|
|
@@ -81,3 +83,20 @@ export function useRevokeServiceActionGrant() {
|
|
|
81
83
|
},
|
|
82
84
|
});
|
|
83
85
|
}
|
|
86
|
+
|
|
87
|
+
export function useGrantAgentServiceActions() {
|
|
88
|
+
const queryClient = useQueryClient();
|
|
89
|
+
return useMutation({
|
|
90
|
+
mutationFn: (params: { agentId: string; actionIds: string[] }) =>
|
|
91
|
+
nextclawClient.serviceApps.grantAgentServiceActions(
|
|
92
|
+
params.agentId,
|
|
93
|
+
params.actionIds,
|
|
94
|
+
),
|
|
95
|
+
onSuccess: async () => {
|
|
96
|
+
await Promise.all([
|
|
97
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
98
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_ACTION_GRANTS_QUERY_KEY }),
|
|
99
|
+
]);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -66,8 +66,8 @@ describe("AppearanceSettingsPage", () => {
|
|
|
66
66
|
render(<AppearanceSettingsPage />);
|
|
67
67
|
|
|
68
68
|
fireEvent.click(screen.getByRole("combobox", { name: "Theme" }));
|
|
69
|
-
fireEvent.click(screen.getByRole("option", { name: "
|
|
70
|
-
expect(mocks.setTheme).toHaveBeenCalledWith("
|
|
69
|
+
fireEvent.click(screen.getByRole("option", { name: "Island" }));
|
|
70
|
+
expect(mocks.setTheme).toHaveBeenCalledWith("island");
|
|
71
71
|
|
|
72
72
|
fireEvent.click(screen.getByRole("combobox", { name: "Language" }));
|
|
73
73
|
fireEvent.click(screen.getByRole("option", { name: "中文" }));
|
|
@@ -230,6 +230,7 @@ export function SideDock({ manager }: SideDockProps) {
|
|
|
230
230
|
<TooltipProvider delayDuration={250}>
|
|
231
231
|
<aside
|
|
232
232
|
data-testid="side-dock"
|
|
233
|
+
data-theme-surface="utility-rail"
|
|
233
234
|
className="z-30 flex h-full w-14 shrink-0 flex-col items-center gap-1 border-l border-border/60 bg-background/95 px-2 py-3"
|
|
234
235
|
>
|
|
235
236
|
<div className="flex w-full flex-col items-center gap-1">
|
package/src/index.css
CHANGED
|
@@ -83,6 +83,7 @@ describe("DesktopAppShell", () => {
|
|
|
83
83
|
const controls = screen.getByTestId("desktop-window-controls");
|
|
84
84
|
|
|
85
85
|
expect(chrome).toBeTruthy();
|
|
86
|
+
expect(chrome.getAttribute("data-theme-surface")).toBe("header");
|
|
86
87
|
expect(
|
|
87
88
|
chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height"),
|
|
88
89
|
).toBe("40px");
|
|
@@ -149,6 +150,13 @@ describe("DesktopAppShell", () => {
|
|
|
149
150
|
|
|
150
151
|
expect(screen.queryByTestId("desktop-window-chrome")).toBeNull();
|
|
151
152
|
expect(screen.getByTestId("app-content")).toBeTruthy();
|
|
153
|
+
expect(
|
|
154
|
+
document.querySelector('[data-theme-decoration="island-paper"]'),
|
|
155
|
+
).toBeTruthy();
|
|
156
|
+
expect(
|
|
157
|
+
document.querySelector('[data-theme-decoration="island-palm"]'),
|
|
158
|
+
).toBeTruthy();
|
|
159
|
+
expect(document.querySelector("#island-wind-filter")).toBeTruthy();
|
|
152
160
|
});
|
|
153
161
|
|
|
154
162
|
it("keeps mobile bottom navigation visible inside Windows desktop hosts at narrow widths", () => {
|
|
@@ -59,10 +59,12 @@ export function DesktopAppShell({
|
|
|
59
59
|
const settingsScroll = useScrollRestoration<HTMLElement>({
|
|
60
60
|
restorationKey: isMainRoute ? null : `settings-page:${pathname}`,
|
|
61
61
|
});
|
|
62
|
-
const { onScroll: onSettingsScroll, scrollRef: settingsScrollRef } =
|
|
62
|
+
const { onScroll: onSettingsScroll, scrollRef: settingsScrollRef } =
|
|
63
|
+
settingsScroll;
|
|
63
64
|
|
|
64
65
|
return (
|
|
65
66
|
<div
|
|
67
|
+
data-theme-surface="app-shell"
|
|
66
68
|
className={cn(
|
|
67
69
|
"h-screen flex flex-col overflow-hidden bg-background font-sans text-foreground",
|
|
68
70
|
shouldUseWindowsChrome ? "rounded-[10px]" : null,
|
|
@@ -77,10 +79,61 @@ export function DesktopAppShell({
|
|
|
77
79
|
: undefined
|
|
78
80
|
}
|
|
79
81
|
>
|
|
82
|
+
<div aria-hidden="true" data-theme-decoration="island-paper" />
|
|
83
|
+
<svg aria-hidden="true" className="absolute h-0 w-0" focusable="false">
|
|
84
|
+
<defs>
|
|
85
|
+
<filter
|
|
86
|
+
id="island-wind-filter"
|
|
87
|
+
x="-8%"
|
|
88
|
+
y="-8%"
|
|
89
|
+
width="116%"
|
|
90
|
+
height="116%"
|
|
91
|
+
colorInterpolationFilters="sRGB"
|
|
92
|
+
>
|
|
93
|
+
<feTurbulence
|
|
94
|
+
type="turbulence"
|
|
95
|
+
baseFrequency="0.0025 0.011"
|
|
96
|
+
numOctaves="2"
|
|
97
|
+
seed="7"
|
|
98
|
+
result="island-wind-noise"
|
|
99
|
+
>
|
|
100
|
+
<animate
|
|
101
|
+
attributeName="baseFrequency"
|
|
102
|
+
dur="8.5s"
|
|
103
|
+
values="0.0025 0.011;0.0038 0.015;0.0028 0.009;0.0025 0.011"
|
|
104
|
+
keyTimes="0;0.38;0.72;1"
|
|
105
|
+
calcMode="spline"
|
|
106
|
+
keySplines="0.42 0 0.25 1;0.45 0 0.2 1;0.4 0 0.3 1"
|
|
107
|
+
repeatCount="indefinite"
|
|
108
|
+
/>
|
|
109
|
+
</feTurbulence>
|
|
110
|
+
<feDisplacementMap
|
|
111
|
+
in="SourceGraphic"
|
|
112
|
+
in2="island-wind-noise"
|
|
113
|
+
scale="5"
|
|
114
|
+
xChannelSelector="R"
|
|
115
|
+
yChannelSelector="G"
|
|
116
|
+
>
|
|
117
|
+
<animate
|
|
118
|
+
attributeName="scale"
|
|
119
|
+
dur="8.5s"
|
|
120
|
+
values="5;17;9;14;5"
|
|
121
|
+
keyTimes="0;0.34;0.58;0.78;1"
|
|
122
|
+
calcMode="spline"
|
|
123
|
+
keySplines="0.42 0 0.25 1;0.45 0 0.2 1;0.4 0 0.3 1;0.42 0 0.3 1"
|
|
124
|
+
repeatCount="indefinite"
|
|
125
|
+
/>
|
|
126
|
+
</feDisplacementMap>
|
|
127
|
+
</filter>
|
|
128
|
+
</defs>
|
|
129
|
+
</svg>
|
|
130
|
+
<div aria-hidden="true" data-theme-decoration="island-palm">
|
|
131
|
+
<span data-island-palm-part="frond-wind" />
|
|
132
|
+
</div>
|
|
80
133
|
{shouldUseWindowsChrome ? (
|
|
81
134
|
<DesktopWindowChrome sidebarCollapsed={isSidebarCollapsed} />
|
|
82
135
|
) : null}
|
|
83
|
-
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
136
|
+
<div className="relative z-[1] flex min-h-0 flex-1 overflow-hidden">
|
|
84
137
|
{!isMainRoute && <Sidebar />}
|
|
85
138
|
<div className="flex-1 flex min-w-0 overflow-hidden relative">
|
|
86
139
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
@@ -88,6 +141,7 @@ export function DesktopAppShell({
|
|
|
88
141
|
<div className="flex-1 h-full overflow-hidden">{children}</div>
|
|
89
142
|
) : (
|
|
90
143
|
<main
|
|
144
|
+
data-theme-surface="settings-workspace"
|
|
91
145
|
ref={settingsScrollRef}
|
|
92
146
|
onScroll={onSettingsScroll}
|
|
93
147
|
className="flex-1 overflow-auto p-8 pb-16 custom-scrollbar"
|
|
@@ -30,6 +30,7 @@ export function DesktopWindowChrome({
|
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
32
|
<header
|
|
33
|
+
data-theme-surface="header"
|
|
33
34
|
className="desktop-window-drag relative flex h-[var(--desktop-titlebar-height)] shrink-0 bg-secondary after:absolute after:bottom-0 after:left-[var(--desktop-sidebar-width)] after:right-0 after:border-b after:border-border/80"
|
|
34
35
|
data-testid="desktop-window-chrome"
|
|
35
36
|
>
|
|
@@ -188,6 +188,9 @@ describe("DocBrowser", () => {
|
|
|
188
188
|
render(<DocBrowser />);
|
|
189
189
|
|
|
190
190
|
expect(screen.getByTestId("doc-browser-panel").style.width).toBe("420px");
|
|
191
|
+
expect(
|
|
192
|
+
screen.getByTestId("doc-browser-panel").getAttribute("data-theme-surface"),
|
|
193
|
+
).toBe("doc-browser");
|
|
191
194
|
expect(screen.getByTestId("resizable-right-panel-handle")).toBeTruthy();
|
|
192
195
|
});
|
|
193
196
|
|
|
@@ -410,6 +410,7 @@ export function DocBrowser({
|
|
|
410
410
|
return (
|
|
411
411
|
<ResizableRightPanel
|
|
412
412
|
data-testid="doc-browser-panel"
|
|
413
|
+
data-theme-surface="doc-browser"
|
|
413
414
|
defaultWidth={dockedWidth}
|
|
414
415
|
width={dockedWidth}
|
|
415
416
|
minWidth={DOC_BROWSER_DOCKED_MIN_WIDTH}
|
|
@@ -424,6 +425,7 @@ export function DocBrowser({
|
|
|
424
425
|
return (
|
|
425
426
|
<div
|
|
426
427
|
data-testid="doc-browser-panel"
|
|
428
|
+
data-theme-surface="doc-browser"
|
|
427
429
|
className={cn(
|
|
428
430
|
'relative flex flex-col overflow-hidden bg-card text-card-foreground',
|
|
429
431
|
isFullscreen
|
|
@@ -108,6 +108,7 @@ function ContextMenuSurface({
|
|
|
108
108
|
<div
|
|
109
109
|
ref={menuRef}
|
|
110
110
|
role="menu"
|
|
111
|
+
data-theme-overlay="menu"
|
|
111
112
|
aria-label={label}
|
|
112
113
|
className="fixed min-w-52 max-w-72 overflow-hidden rounded-xl border border-border bg-popover p-1 text-popover-foreground shadow-[0_18px_48px_-20px_rgba(15,23,42,0.42)]"
|
|
113
114
|
style={{ left: position.x, top: position.y }}
|
|
@@ -18,6 +18,7 @@ const DialogOverlay = React.forwardRef<
|
|
|
18
18
|
>(({ className, ...props }, ref) => (
|
|
19
19
|
<DialogPrimitive.Overlay
|
|
20
20
|
ref={ref}
|
|
21
|
+
data-theme-overlay="modal-backdrop"
|
|
21
22
|
className={cn(
|
|
22
23
|
"fixed inset-0 z-[var(--z-modal-backdrop,10000)] bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
23
24
|
className
|
|
@@ -35,6 +36,7 @@ const DialogContent = React.forwardRef<
|
|
|
35
36
|
<DialogOverlay />
|
|
36
37
|
<DialogPrimitive.Content
|
|
37
38
|
ref={ref}
|
|
39
|
+
data-theme-overlay="dialog"
|
|
38
40
|
className={cn(
|
|
39
41
|
"fixed left-[50%] top-[50%] z-[var(--z-modal,10050)] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-xl duration-base data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
40
42
|
className
|
|
@@ -16,6 +16,7 @@ const SheetOverlay = React.forwardRef<
|
|
|
16
16
|
>(({ className, ...props }, ref) => (
|
|
17
17
|
<SheetPrimitive.Overlay
|
|
18
18
|
ref={ref}
|
|
19
|
+
data-theme-overlay="modal-backdrop"
|
|
19
20
|
className={cn(
|
|
20
21
|
"fixed inset-0 z-[var(--z-modal-backdrop,10000)] bg-black/35 backdrop-blur-[1px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
21
22
|
className,
|
|
@@ -59,6 +60,7 @@ const SheetContent = React.forwardRef<
|
|
|
59
60
|
<SheetOverlay />
|
|
60
61
|
<SheetPrimitive.Content
|
|
61
62
|
ref={ref}
|
|
63
|
+
data-theme-overlay="sheet"
|
|
62
64
|
className={cn(sheetVariants({ side }), className)}
|
|
63
65
|
{...props}
|
|
64
66
|
>
|
|
@@ -77,6 +77,7 @@ const SelectContent = React.forwardRef<
|
|
|
77
77
|
<SelectPrimitive.Portal>
|
|
78
78
|
<SelectPrimitive.Content
|
|
79
79
|
ref={ref}
|
|
80
|
+
data-theme-overlay="select"
|
|
80
81
|
className={cn(
|
|
81
82
|
"relative z-[var(--z-popover,10100)] flex max-h-96 min-w-[8rem] flex-col overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
82
83
|
position === "popper" &&
|
|
@@ -31,6 +31,7 @@ const TooltipContent = React.forwardRef<
|
|
|
31
31
|
<TooltipPrimitive.Portal>
|
|
32
32
|
<TooltipPrimitive.Content
|
|
33
33
|
ref={ref}
|
|
34
|
+
data-theme-overlay="tooltip"
|
|
34
35
|
sideOffset={sideOffset}
|
|
35
36
|
className={cn(
|
|
36
37
|
'z-[var(--z-tooltip,10150)] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|