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" ;
1315import { invoke } from "@tauri-apps/api/core" ;
1416import { 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" ;
1630import { initDesktopCore } from "./core/desktop-init" ;
1731import { authStorage } from "./auth/auth-storage" ;
1832import { 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+
43124export 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