diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index ff50219caa..5786a01833 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -150,6 +150,9 @@ export function AgentsView() {
onEdit={teamActions.openEditDialog}
onExport={teamActions.handleExportTeam}
onImportFile={teamActions.handleImportFile}
+ onInstallFromDirectory={teamActions.handleInstallFromDirectory}
+ onSync={teamActions.handleSyncTeam}
+ onRevealInFinder={teamActions.handleRevealInFinder}
onAddToChannel={teamActions.setTeamToAddToChannel}
personas={personas.libraryPersonas}
teams={teamActions.teams}
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index 6ec719ec75..bcbcd7e104 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -2,7 +2,10 @@ import {
CopyPlus,
Download,
Ellipsis,
+ FolderOpen,
+ FolderSync,
Info,
+ Link,
Pencil,
Rocket,
Trash2,
@@ -41,6 +44,9 @@ type TeamsSectionProps = {
onDelete: (team: AgentTeam) => void;
onAddToChannel: (team: AgentTeam) => void;
onImportFile: (fileBytes: number[], fileName: string) => void;
+ onInstallFromDirectory: () => void;
+ onSync: (team: AgentTeam) => void;
+ onRevealInFinder: (team: AgentTeam) => void;
};
export function TeamsSection({
@@ -56,6 +62,9 @@ export function TeamsSection({
onDelete,
onAddToChannel,
onImportFile,
+ onInstallFromDirectory,
+ onSync,
+ onRevealInFinder,
}: TeamsSectionProps) {
const {
fileInputRef,
@@ -93,11 +102,21 @@ export function TeamsSection({
ref={fileInputRef}
type="file"
/>
-
+
+
+
+
{isLoading ? (
@@ -138,6 +157,25 @@ export function TeamsSection({
{team.name}
+ {team.isSymlink ? (
+
+
+
+
+
+
+
+
+ Linked from {team.symlinkTarget ?? team.sourceDir}
+
+
+
+ ) : null}
+ {team.version ? (
+
+ v{team.version}
+
+ ) : null}
{team.description ? (
@@ -221,6 +259,24 @@ export function TeamsSection({
Export
+ {team.sourceDir ? (
+ <>
+
+ onSync(team)}
+ >
+
+ Sync from directory
+
+ onRevealInFinder(team)}
+ >
+
+ Reveal in Finder
+
+ >
+ ) : null}
0 &&
+ `${result.personas_added.length} added`,
+ result.personas_updated.length > 0 &&
+ `${result.personas_updated.length} updated`,
+ result.personas_removed.length > 0 &&
+ `${result.personas_removed.length} removed`,
+ ].filter(Boolean);
+ const summary =
+ changes.length > 0 ? changes.join(", ") : "already up to date";
+ actions.setActionNoticeMessage(`Synced "${team.name}": ${summary}.`);
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: teamsQueryKey }),
+ queryClient.invalidateQueries({ queryKey: personasQueryKey }),
+ queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey }),
+ ]);
+ } catch (err) {
+ actions.setActionErrorMessage(
+ err instanceof Error ? err.message : "Failed to sync team directory.",
+ );
+ }
+ }
+
+ function handleRevealInFinder(team: AgentTeam) {
+ if (!team.sourceDir) return;
+ void revealItemInDir(team.sourceDir);
+ }
+
async function handleEditDialogImportUpdateFile(
teamId: string,
fileBytes: number[],
@@ -492,6 +553,9 @@ export function useTeamActions(
handleTeamDeployed,
handleExportTeam,
handleImportFile,
+ handleInstallFromDirectory,
+ handleSyncTeam,
+ handleRevealInFinder,
handleEditDialogImportUpdateFile,
handleTeamImportComplete,
handleTeamImportUpdateApply,
diff --git a/desktop/src/shared/api/tauriTeams.ts b/desktop/src/shared/api/tauriTeams.ts
index ddb2617d96..166078b99f 100644
--- a/desktop/src/shared/api/tauriTeams.ts
+++ b/desktop/src/shared/api/tauriTeams.ts
@@ -11,6 +11,10 @@ type RawTeam = {
description: string | null;
persona_ids: string[];
is_builtin?: boolean;
+ source_dir?: string | null;
+ is_symlink?: boolean;
+ symlink_target?: string | null;
+ version?: string | null;
created_at: string;
updated_at: string;
};
@@ -22,6 +26,10 @@ function fromRawTeam(team: RawTeam): AgentTeam {
description: team.description,
personaIds: team.persona_ids,
isBuiltin: team.is_builtin ?? false,
+ sourceDir: team.source_dir ?? null,
+ isSymlink: team.is_symlink ?? false,
+ symlinkTarget: team.symlink_target ?? null,
+ version: team.version ?? null,
createdAt: team.created_at,
updatedAt: team.updated_at,
};
@@ -83,3 +91,30 @@ export async function parseTeamFile(
fileName,
});
}
+
+export type SyncResult = {
+ personas_added: string[];
+ personas_removed: string[];
+ personas_updated: string[];
+ metadata_changed: boolean;
+};
+
+export async function pickTeamDirectory(): Promise {
+ return invokeTauri("pick_team_directory");
+}
+
+export async function installTeamFromDirectory(
+ path: string,
+ symlink?: boolean,
+): Promise {
+ return fromRawTeam(
+ await invokeTauri("install_team_from_directory", {
+ path,
+ symlink: symlink ?? false,
+ }),
+ );
+}
+
+export async function syncTeamDirectory(teamId: string): Promise {
+ return invokeTauri("sync_team_directory", { teamId });
+}
diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts
index d37367f21c..ea01f03100 100644
--- a/desktop/src/shared/api/types.ts
+++ b/desktop/src/shared/api/types.ts
@@ -501,6 +501,14 @@ export type AgentTeam = {
description: string | null;
personaIds: string[];
isBuiltin: boolean;
+ /** Absolute path to the team's backing directory (if directory-backed). */
+ sourceDir: string | null;
+ /** Whether sourceDir is a symlink to an external directory. */
+ isSymlink: boolean;
+ /** Resolved symlink target path (for display). Only set when isSymlink is true. */
+ symlinkTarget: string | null;
+ /** Version from the team's plugin.json manifest. */
+ version: string | null;
createdAt: string;
updatedAt: string;
};