@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/assets/{api-DNLQPHfQ.js → api-m33O1Kb0.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-BlAFMOGV.js → appearance-settings-page-x4Vjnfmz.js} +1 -1
  4. package/dist/assets/{book-open-BryfPTTa.js → book-open-BHiasMCg.js} +1 -1
  5. package/dist/assets/{channels-list-page-Cfbgubyv.js → channels-list-page-DOxJZeXq.js} +1 -1
  6. package/dist/assets/{config-split-page-Dd8xG0Jk.js → config-split-page-DBnMEP1o.js} +1 -1
  7. package/dist/assets/{confirm-dialog-CZIWjnJ6.js → confirm-dialog-f5NKLh5I.js} +2 -2
  8. package/dist/assets/{createLucideIcon-BxwVe8ZD.js → createLucideIcon-CHALGA42.js} +1 -1
  9. package/dist/assets/{desktop-capabilities-page-C6YxsHeV.js → desktop-capabilities-page-DixayAe6.js} +1 -1
  10. package/dist/assets/{desktop-update-config-BVN4QPJn.js → desktop-update-config-ChhfeOHj.js} +1 -1
  11. package/dist/assets/{dist-Cd8Nm3BW.js → dist-_UMsWSY0.js} +1 -1
  12. package/dist/assets/{dist-BRloKr3S.js → dist-sD6V4nHX.js} +1 -1
  13. package/dist/assets/doc-browser-Bq7LJsQY.js +1 -0
  14. package/dist/assets/doc-browser-BuDgpY2e.js +1 -0
  15. package/dist/assets/{doc-browser-context-3JGufiMA.js → doc-browser-context-Capim3og.js} +1 -1
  16. package/dist/assets/{extensions-Dedd2oDI.js → extensions-Bdclr3Wr.js} +1 -1
  17. package/dist/assets/{form-actions-FWCEof3V.js → form-actions-CH0kWhEt.js} +1 -1
  18. package/dist/assets/index-BjxnLYE3.js +109 -0
  19. package/dist/assets/index-CBmwJjwE.css +1 -0
  20. package/dist/assets/loader-circle-CH_0iPLy.js +1 -0
  21. package/dist/assets/{mcp-marketplace-page-e9Rmfz-d.js → mcp-marketplace-page-BUsxLVB5.js} +2 -2
  22. package/dist/assets/mcp-marketplace-page-GPRV3fB0.js +1 -0
  23. package/dist/assets/{model-config-page-C173Ssil.js → model-config-page-CIevwqp4.js} +1 -1
  24. package/dist/assets/{navigation-link-DTPOvGGL.js → navigation-link-CAUiM3zS.js} +1 -1
  25. package/dist/assets/plus-B3obD55V.js +1 -0
  26. package/dist/assets/{privacy-settings-page-CRserBVF.js → privacy-settings-page-CNKfZsYH.js} +1 -1
  27. package/dist/assets/{providers-config-page-C8jUtnZb.js → providers-config-page-DAah2NIS.js} +1 -1
  28. package/dist/assets/{react-BAzO6LZS.js → react-Co6JiC1n.js} +1 -1
  29. package/dist/assets/{refresh-cw-BCj-yjde.js → refresh-cw-BG_LX3SS.js} +1 -1
  30. package/dist/assets/remote-DuscgfvR.js +1 -0
  31. package/dist/assets/{rotate-cw-BPnW4qUl.js → rotate-cw-Cz7I_m7O.js} +1 -1
  32. package/dist/assets/{runtime-config-page-aeQ1Dryz.js → runtime-config-page-Cl1RpmSC.js} +1 -1
  33. package/dist/assets/{save-KT0MumfJ.js → save-BiHnb8ps.js} +1 -1
  34. package/dist/assets/{search-Cv1Hx_nx.js → search-C47TyRg0.js} +1 -1
  35. package/dist/assets/{search-config-page-b283V-fo.js → search-config-page-BAIzd6gN.js} +1 -1
  36. package/dist/assets/{secrets-config-page-ss7x3BOo.js → secrets-config-page-Ddi-n43R.js} +1 -1
  37. package/dist/assets/security-config-C8MHnE1r.js +1 -0
  38. package/dist/assets/{select-BVgD4EpG.js → select-BzQKfW1e.js} +2 -2
  39. package/dist/assets/skeleton-BXFAE9SE.js +1 -0
  40. package/dist/assets/{tag-chip-DZ4ircUB.js → tag-chip-C3K814HZ.js} +1 -1
  41. package/dist/assets/x-C1r9IXet.js +1 -0
  42. package/dist/index.html +29 -21
  43. package/dist/themes/island/island-atmosphere.webp +0 -0
  44. package/index.html +8 -0
  45. package/package.json +4 -4
  46. package/public/themes/island/island-atmosphere.webp +0 -0
  47. package/src/app/components/layout/header.tsx +4 -1
  48. package/src/app/components/layout/page-layout.tsx +4 -1
  49. package/src/app/components/layout/sidebar.tsx +1 -0
  50. package/src/app/styles/island-theme.css +542 -0
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +1 -0
  52. package/src/features/chat/components/conversation/chat-conversation-header.tsx +5 -1
  53. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +4 -1
  54. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +10 -0
  55. package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
  56. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +2 -0
  57. package/src/features/chat/components/layout/chat-sidebar.tsx +1 -0
  58. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  59. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +1 -0
  60. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +1 -0
  61. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +9 -0
  62. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +1 -0
  63. package/src/features/service-apps/components/__tests__/service-apps-panel.test.tsx +35 -1
  64. package/src/features/service-apps/components/service-action-row.tsx +130 -0
  65. package/src/features/service-apps/components/service-app-list-item.tsx +11 -66
  66. package/src/features/service-apps/components/service-apps-panel.tsx +12 -0
  67. package/src/features/service-apps/hooks/use-service-apps.ts +20 -1
  68. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +2 -2
  69. package/src/features/side-dock/components/side-dock.tsx +1 -0
  70. package/src/index.css +1 -0
  71. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +8 -0
  72. package/src/platforms/desktop/components/desktop-app-shell.tsx +56 -2
  73. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -0
  74. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +3 -0
  75. package/src/shared/components/doc-browser/doc-browser.tsx +2 -0
  76. package/src/shared/components/ui/context-menu/context-menu.tsx +1 -0
  77. package/src/shared/components/ui/dialog.tsx +2 -0
  78. package/src/shared/components/ui/overlays/sheet.tsx +2 -0
  79. package/src/shared/components/ui/popover.tsx +1 -0
  80. package/src/shared/components/ui/select.tsx +1 -0
  81. package/src/shared/components/ui/tooltip.tsx +1 -0
  82. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  83. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +1 -0
  84. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  85. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +1 -0
  86. package/src/shared/lib/theme/index.test.ts +9 -0
  87. package/src/shared/lib/theme/index.ts +2 -0
  88. package/dist/assets/doc-browser-Bzgqmmiu.js +0 -1
  89. package/dist/assets/doc-browser-D0qI0Z_R.js +0 -1
  90. package/dist/assets/index-CXkHcESm.css +0 -1
  91. package/dist/assets/index-D25tRg1p.js +0 -109
  92. package/dist/assets/loader-circle-BoyCoFps.js +0 -1
  93. package/dist/assets/mcp-marketplace-page-Cn-9CDh3.js +0 -1
  94. package/dist/assets/plus-D1R4Vipq.js +0 -1
  95. package/dist/assets/remote-6BHWP5zi.js +0 -1
  96. package/dist/assets/security-config-HHz62oxH.js +0 -1
  97. package/dist/assets/skeleton-ChUmcyys.js +0 -1
  98. package/dist/assets/x-BWDRfVWL.js +0 -1
@@ -15,6 +15,7 @@ const PWA_SHELL_THEME_COLORS: Record<UiTheme, string> = {
15
15
  graphite: '#F8F8F7',
16
16
  night: '#101318',
17
17
  charcoal: '#1C1C1C',
18
+ island: '#FBF8ED',
18
19
  probe: '#FEFCF6',
19
20
  };
20
21
 
@@ -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: { actions: [] },
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: { surface: 'panel-app'; appId: string };
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: "Charcoal" }));
70
- expect(mocks.setTheme).toHaveBeenCalledWith("charcoal");
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
@@ -1,5 +1,6 @@
1
1
  @import "./app/styles/design-system.css";
2
2
  @import "./app/styles/charcoal-theme.css";
3
+ @import "./app/styles/island-theme.css";
3
4
 
4
5
  @tailwind base;
5
6
  @tailwind components;
@@ -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 } = settingsScroll;
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
  >
@@ -24,6 +24,7 @@ const PopoverContent = React.forwardRef<
24
24
  <PopoverPrimitive.Portal>
25
25
  <PopoverPrimitive.Content
26
26
  ref={ref}
27
+ data-theme-overlay="popover"
27
28
  sideOffset={sideOffset}
28
29
  align={align}
29
30
  collisionPadding={collisionPadding}
@@ -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',