Skip to content

Commit 80ca57a

Browse files
piwi3910claude
andcommitted
feat(ui): mount PluginRoot + PluginManagerScreen (DCC-C1) and AgentsPanel (DCC-C2)
- wrap AccountWindow render tree in <PluginRoot activePlugins={...}> so server-installed client-side plugin bundles are loaded/unloaded on the fly - add PluginManagerScreen behind a "Plugins" toggle button in the header; backed by buildPluginManagerAPI() which calls /api/plugins/* with bearer auth - configure tauri.conf.json CSP to allow github.com / raw.githubusercontent.com for plugin asset downloads and script evaluation - extend CoreAdapter constructor with serverUrl + authTokenFn params - add CoreAdapter.agents namespace: list, create, delete, mintToken, revokeToken, setPolicy — all calling /api/agents/* directly via fetch - mount AgentsPanel from @azrtydxb/ui behind an "Agents" toggle button, wired to CoreAdapter.agents callbacks and local agents/newTokenResult state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f041b62 commit 80ca57a

3 files changed

Lines changed: 332 additions & 23 deletions

File tree

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"app": {
1313
"windows": [],
1414
"security": {
15-
"csp": null
15+
"csp": "default-src 'self' tauri: https://github.com https://raw.githubusercontent.com; script-src 'self' 'unsafe-eval' https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' tauri: https: wss:;"
1616
}
1717
},
1818
"bundle": {

src/AccountWindow.tsx

Lines changed: 240 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,25 @@
88
* 4. Render AppShell from @azrtydxb/ui as the top-level shell.
99
* 5. Touch the account on mount (updates last_logged_in_at).
1010
* 6. Listen for Tauri menu-action events and route them.
11+
* 7. (DCC-C1) Wrap the tree in PluginRoot; provide PluginManagerScreen.
12+
* 8. (DCC-C2) Wire AgentsPanel to CoreAdapter.agents.
1113
*/
12-
import { useEffect, useRef, useState } from "react";
14+
import { useCallback, useEffect, useRef, useState } from "react";
1315
import { invoke } from "@tauri-apps/api/core";
1416
import { listen } from "@tauri-apps/api/event";
15-
import { AppShell, KrytonDataProvider } from "@azrtydxb/ui";
17+
import {
18+
AppShell,
19+
KrytonDataProvider,
20+
PluginRoot,
21+
PluginManagerScreen,
22+
AgentsPanel,
23+
} from "@azrtydxb/ui";
24+
import type {
25+
ActivePluginInfo,
26+
PluginManagerAPI,
27+
AgentInfo,
28+
NewTokenResult,
29+
} from "@azrtydxb/ui";
1630
import { initDesktopCore } from "./core/desktop-init";
1731
import { authStorage } from "./auth/auth-storage";
1832
import { CoreAdapter } from "./core/CoreAdapter";
@@ -40,12 +54,90 @@ type LoadState =
4054
// Component
4155
// ---------------------------------------------------------------------------
4256

57+
// ---------------------------------------------------------------------------
58+
// Plugin screen helper — builds a PluginManagerAPI backed by the server
59+
// ---------------------------------------------------------------------------
60+
61+
function buildPluginManagerAPI(serverUrl: string, authTokenFn: () => Promise<string | null>): PluginManagerAPI {
62+
const headers = async (): Promise<Record<string, string>> => {
63+
const tok = await authTokenFn();
64+
return tok ? { Authorization: `Bearer ${tok}` } : {};
65+
};
66+
67+
const apiFetch = async (path: string, opts?: RequestInit): Promise<Response> => {
68+
const h = await headers();
69+
return fetch(`${serverUrl}${path}`, { ...opts, headers: { ...h, ...(opts?.headers as Record<string, string> | undefined) } });
70+
};
71+
72+
return {
73+
getRegistry: async () => {
74+
const res = await apiFetch("/api/plugins/registry");
75+
if (!res.ok) return [];
76+
const body = await res.json() as { plugins: unknown[] };
77+
return (body.plugins ?? []) as ReturnType<PluginManagerAPI["getRegistry"]> extends Promise<infer T> ? T : never;
78+
},
79+
getAllPlugins: async () => {
80+
const res = await apiFetch("/api/plugins/installed");
81+
if (!res.ok) return [];
82+
const body = await res.json() as { plugins: unknown[] };
83+
return (body.plugins ?? []) as ReturnType<PluginManagerAPI["getAllPlugins"]> extends Promise<infer T> ? T : never;
84+
},
85+
checkPluginUpdates: async () => {
86+
const res = await apiFetch("/api/plugins/updates");
87+
if (!res.ok) return [];
88+
const body = await res.json() as { updates: unknown[] };
89+
return (body.updates ?? []) as ReturnType<PluginManagerAPI["checkPluginUpdates"]> extends Promise<infer T> ? T : never;
90+
},
91+
getSettings: async () => {
92+
const res = await apiFetch("/api/plugins/settings");
93+
if (!res.ok) return {};
94+
return res.json() as Promise<Record<string, string>>;
95+
},
96+
updateSetting: async (key: string, value: string) => {
97+
await apiFetch("/api/plugins/settings", {
98+
method: "PATCH",
99+
headers: { "Content-Type": "application/json" },
100+
body: JSON.stringify({ key, value }),
101+
});
102+
},
103+
installPlugin: async (id: string) => {
104+
await apiFetch(`/api/plugins/${id}/install`, { method: "POST" });
105+
},
106+
uninstallPlugin: async (id: string) => {
107+
await apiFetch(`/api/plugins/${id}`, { method: "DELETE" });
108+
},
109+
enablePlugin: async (id: string) => {
110+
await apiFetch(`/api/plugins/${id}/enable`, { method: "POST" });
111+
},
112+
disablePlugin: async (id: string) => {
113+
await apiFetch(`/api/plugins/${id}/disable`, { method: "POST" });
114+
},
115+
reloadPlugin: async (id: string) => {
116+
await apiFetch(`/api/plugins/${id}/reload`, { method: "POST" });
117+
},
118+
updatePlugin: async (id: string) => {
119+
await apiFetch(`/api/plugins/${id}/update`, { method: "POST" });
120+
},
121+
};
122+
}
123+
43124
export function AccountWindow({ accountId }: { accountId: string }) {
44125
const [state, setState] = useState<LoadState>({ status: "loading" });
45126
// Keep a stable ref so the menu-action handler can access the latest adapter
46127
// without re-subscribing every render.
47128
const adapterRef = useRef<CoreAdapter | null>(null);
48129

130+
// DCC-C1: active plugins fetched from server for PluginRoot
131+
const [activePlugins, setActivePlugins] = useState<ActivePluginInfo[]>([]);
132+
133+
// DCC-C1: which secondary screen is open (null = main content)
134+
type ActiveScreen = null | "plugins" | "agents";
135+
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(null);
136+
137+
// DCC-C2: agents state
138+
const [agents, setAgents] = useState<AgentInfo[]>([]);
139+
const [newTokenResult, setNewTokenResult] = useState<NewTokenResult | null>(null);
140+
49141
useEffect(() => {
50142
let cancelled = false;
51143
let coreInstance: Kryton | null = null;
@@ -88,8 +180,14 @@ export function AccountWindow({ accountId }: { accountId: string }) {
88180
if (firstTag?.userId) userId = firstTag.userId;
89181
}
90182

91-
// 5. Build the adapter.
92-
const adapter = new CoreAdapter(core, userId);
183+
// 5. Build the adapter (DCC-C2: pass serverUrl + authTokenFn for agent API).
184+
const adapter = new CoreAdapter(
185+
core,
186+
userId,
187+
/* userEmail */ "",
188+
account.server_url,
189+
() => authStorage.getToken(accountId),
190+
);
93191
adapterRef.current = adapter;
94192

95193
setState({ status: "ready", adapter, accountLabel: account.label, serverUrl: account.server_url });
@@ -183,6 +281,52 @@ export function AccountWindow({ accountId }: { accountId: string }) {
183281
};
184282
}, []);
185283

284+
// ---------------------------------------------------------------------------
285+
// DCC-C1: Fetch active plugins from server when ready
286+
// ---------------------------------------------------------------------------
287+
288+
useEffect(() => {
289+
if (state.status !== "ready") return;
290+
const { serverUrl } = state;
291+
292+
async function fetchActivePlugins() {
293+
try {
294+
const tok = await authStorage.getToken(accountId);
295+
const headers: Record<string, string> = tok ? { Authorization: `Bearer ${tok}` } : {};
296+
const res = await fetch(`${serverUrl}/api/plugins/active`, { headers });
297+
if (!res.ok) return;
298+
const body = await res.json() as { plugins: ActivePluginInfo[] };
299+
setActivePlugins(body.plugins ?? []);
300+
} catch (err: unknown) {
301+
console.warn("[AccountWindow] fetchActivePlugins failed:", err);
302+
}
303+
}
304+
305+
void fetchActivePlugins();
306+
// eslint-disable-next-line react-hooks/exhaustive-deps
307+
}, [state.status, accountId]);
308+
309+
// ---------------------------------------------------------------------------
310+
// DCC-C2: Fetch agents when the agents screen is opened
311+
// ---------------------------------------------------------------------------
312+
313+
const refreshAgents = useCallback(async () => {
314+
if (state.status !== "ready") return;
315+
try {
316+
const list = await state.adapter.agents.list();
317+
setAgents(list);
318+
} catch (err: unknown) {
319+
console.warn("[AccountWindow] agents.list failed:", err);
320+
}
321+
}, [state]);
322+
323+
useEffect(() => {
324+
if (activeScreen === "agents") {
325+
void refreshAgents();
326+
}
327+
// eslint-disable-next-line react-hooks/exhaustive-deps
328+
}, [activeScreen]);
329+
186330
// ---------------------------------------------------------------------------
187331
// Render
188332
// ---------------------------------------------------------------------------
@@ -204,22 +348,100 @@ export function AccountWindow({ accountId }: { accountId: string }) {
204348
);
205349
}
206350

351+
// Build stable PluginManagerAPI reference (recreated only when serverUrl changes).
352+
const pluginManagerAPI: PluginManagerAPI = buildPluginManagerAPI(
353+
state.serverUrl,
354+
() => authStorage.getToken(accountId),
355+
);
356+
207357
return (
208358
<KrytonDataProvider adapter={state.adapter}>
209-
<AppShell
210-
header={
211-
<div className="flex items-center gap-3 px-4 py-2 text-sm font-medium text-surface-700 dark:text-surface-300">
212-
<span className="text-primary-600 font-semibold">Kryton</span>
213-
<span className="text-surface-400">/</span>
214-
<span>{state.accountLabel}</span>
215-
</div>
216-
}
217-
>
218-
{/* Main content area — feature panels will be mounted here in later phases. */}
219-
<div className="flex h-full items-center justify-center text-surface-400 text-sm select-none">
220-
Ready — account window initialised.
221-
</div>
222-
</AppShell>
359+
{/* DCC-C1: PluginRoot loads/unloads active client-side plugin bundles */}
360+
<PluginRoot activePlugins={activePlugins}>
361+
<AppShell
362+
header={
363+
<div className="flex items-center gap-3 px-4 py-2 text-sm font-medium text-surface-700 dark:text-surface-300">
364+
<span className="text-primary-600 font-semibold">Kryton</span>
365+
<span className="text-surface-400">/</span>
366+
<span>{state.accountLabel}</span>
367+
{/* Secondary-screen nav buttons */}
368+
<div className="ml-auto flex items-center gap-2">
369+
<button
370+
type="button"
371+
onClick={() => setActiveScreen(activeScreen === "plugins" ? null : "plugins")}
372+
className="rounded px-2 py-1 text-xs font-medium hover:bg-surface-100 dark:hover:bg-surface-800 data-[active=true]:text-primary-600"
373+
data-active={activeScreen === "plugins"}
374+
aria-label="Plugin Manager"
375+
>
376+
Plugins
377+
</button>
378+
<button
379+
type="button"
380+
onClick={() => setActiveScreen(activeScreen === "agents" ? null : "agents")}
381+
className="rounded px-2 py-1 text-xs font-medium hover:bg-surface-100 dark:hover:bg-surface-800 data-[active=true]:text-primary-600"
382+
data-active={activeScreen === "agents"}
383+
aria-label="Agents"
384+
>
385+
Agents
386+
</button>
387+
</div>
388+
</div>
389+
}
390+
>
391+
{/* DCC-C1: Plugin Manager screen */}
392+
{activeScreen === "plugins" && (
393+
<PluginManagerScreen api={pluginManagerAPI} />
394+
)}
395+
396+
{/* DCC-C2: Agents screen */}
397+
{activeScreen === "agents" && (
398+
<AgentsPanel
399+
agents={agents}
400+
newTokenResult={newTokenResult}
401+
onDismissNewToken={() => setNewTokenResult(null)}
402+
onCreateAgent={(name, label, cedarPolicy) => {
403+
state.adapter.agents
404+
.create({ name, label, policyText: cedarPolicy })
405+
.then(() => refreshAgents())
406+
.catch((err: unknown) =>
407+
console.warn("[AccountWindow] agents.create failed:", err),
408+
);
409+
}}
410+
onDeleteAgent={(id) => {
411+
state.adapter.agents
412+
.delete(id)
413+
.then(() => refreshAgents())
414+
.catch((err: unknown) =>
415+
console.warn("[AccountWindow] agents.delete failed:", err),
416+
);
417+
}}
418+
onMintToken={(agentId) => {
419+
state.adapter.agents
420+
.mintToken(agentId)
421+
.then((result) => setNewTokenResult(result))
422+
.catch((err: unknown) =>
423+
console.warn("[AccountWindow] agents.mintToken failed:", err),
424+
);
425+
}}
426+
onRevokeToken={(agentId, tokenId) => {
427+
state.adapter.agents
428+
.revokeToken(agentId, tokenId)
429+
.then(() => refreshAgents())
430+
.catch((err: unknown) =>
431+
console.warn("[AccountWindow] agents.revokeToken failed:", err),
432+
);
433+
}}
434+
/>
435+
)}
436+
437+
{/* Main content area */}
438+
{activeScreen === null && (
439+
<div className="flex h-full items-center justify-center text-surface-400 text-sm select-none">
440+
Ready — account window initialised.
441+
</div>
442+
)}
443+
</AppShell>
444+
</PluginRoot>
223445
</KrytonDataProvider>
224446
);
225447
}

0 commit comments

Comments
 (0)