Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions desktop/src/features/agents/ui/AgentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
66 changes: 61 additions & 5 deletions desktop/src/features/agents/ui/TeamsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import {
CopyPlus,
Download,
Ellipsis,
FolderOpen,
FolderSync,
Info,
Link,
Pencil,
Rocket,
Trash2,
Expand Down Expand Up @@ -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({
Expand All @@ -56,6 +62,9 @@ export function TeamsSection({
onDelete,
onAddToChannel,
onImportFile,
onInstallFromDirectory,
onSync,
onRevealInFinder,
}: TeamsSectionProps) {
const {
fileInputRef,
Expand Down Expand Up @@ -93,11 +102,21 @@ export function TeamsSection({
ref={fileInputRef}
type="file"
/>
<CreateNewButton
ariaLabel="Create team"
label="Team"
onClick={onCreate}
/>
<div className="flex items-center gap-2">
<button
className="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onClick={onInstallFromDirectory}
type="button"
>
<FolderOpen className="h-3.5 w-3.5" />
Install from directory
</button>
<CreateNewButton
ariaLabel="Create team"
label="Team"
onClick={onCreate}
/>
</div>
</div>

{isLoading ? (
Expand Down Expand Up @@ -138,6 +157,25 @@ export function TeamsSection({
<p className="truncate text-sm font-semibold tracking-tight">
{team.name}
</p>
{team.isSymlink ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
<Link className="h-3.5 w-3.5" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<p>
Linked from {team.symlinkTarget ?? team.sourceDir}
</p>
</TooltipContent>
</Tooltip>
) : null}
{team.version ? (
<span className="shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
v{team.version}
</span>
) : null}
{team.description ? (
<Tooltip>
<TooltipTrigger asChild>
Expand Down Expand Up @@ -221,6 +259,24 @@ export function TeamsSection({
<Download className="h-4 w-4" />
Export
</DropdownMenuItem>
{team.sourceDir ? (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={isPending}
onClick={() => onSync(team)}
>
<FolderSync className="h-4 w-4" />
Sync from directory
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onRevealInFinder(team)}
>
<FolderOpen className="h-4 w-4" />
Reveal in Finder
</DropdownMenuItem>
</>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
Expand Down
64 changes: 64 additions & 0 deletions desktop/src/features/agents/ui/useTeamActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ import {
type ParsedTeamPreview,
createTeam as createTeamApi,
exportTeamToJson,
installTeamFromDirectory,
parseTeamFile,
pickTeamDirectory,
syncTeamDirectory,
} from "@/shared/api/tauriTeams";
import {
createPersona,
deletePersona,
updatePersona,
} from "@/shared/api/tauriPersonas";
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import type {
AgentPersona,
AgentTeam,
Expand Down Expand Up @@ -207,6 +211,63 @@ export function useTeamActions(
}
}

async function handleInstallFromDirectory() {
actions.setActionNoticeMessage(null);
actions.setActionErrorMessage(null);
try {
const path = await pickTeamDirectory();
if (!path) return;
const team = await installTeamFromDirectory(path, true);
actions.setActionNoticeMessage(
`Installed team "${team.name}" from directory.`,
);
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 install team from directory.",
);
}
}

async function handleSyncTeam(team: AgentTeam) {
actions.setActionNoticeMessage(null);
actions.setActionErrorMessage(null);
try {
const result = await syncTeamDirectory(team.id);
const changes = [
result.personas_added.length > 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[],
Expand Down Expand Up @@ -492,6 +553,9 @@ export function useTeamActions(
handleTeamDeployed,
handleExportTeam,
handleImportFile,
handleInstallFromDirectory,
handleSyncTeam,
handleRevealInFinder,
handleEditDialogImportUpdateFile,
handleTeamImportComplete,
handleTeamImportUpdateApply,
Expand Down
35 changes: 35 additions & 0 deletions desktop/src/shared/api/tauriTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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,
};
Expand Down Expand Up @@ -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<string | null> {
return invokeTauri<string | null>("pick_team_directory");
}

export async function installTeamFromDirectory(
path: string,
symlink?: boolean,
): Promise<AgentTeam> {
return fromRawTeam(
await invokeTauri<RawTeam>("install_team_from_directory", {
path,
symlink: symlink ?? false,
}),
);
}

export async function syncTeamDirectory(teamId: string): Promise<SyncResult> {
return invokeTauri<SyncResult>("sync_team_directory", { teamId });
}
8 changes: 8 additions & 0 deletions desktop/src/shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
Loading