-
-
-
-
- {preview.name}
+
+
+ Import Team
+
+ Preview the team from {fileName || "file"} before importing.
+
+
+
+
+ {preview ? (
+
+
+
+
+
+ {preview.name}
+
+ {preview.description ? (
+
+ {preview.description}
- {preview.description ? (
-
- {preview.description}
-
- ) : null}
-
-
- {personas.length}{" "}
- {personas.length === 1 ? "persona" : "personas"}
-
+ ) : null}
+
+ {personas.length}{" "}
+ {personas.length === 1 ? "persona" : "personas"}
+
+
-
-
Personas to import
-
- Each persona will be created, then grouped into a new team.
-
-
+
+
Personas to import
+
+ Each persona will be created, then grouped into a new team.
+
+
-
- {personas.map((persona, index) => (
-
-
-
-
- {persona.display_name}
-
-
- {promptPreview(persona.system_prompt)}
-
-
-
+
+ {personas.map((persona, index) => (
+
+
+
+
+ {persona.display_name}
+
+
+ {promptPreview(persona.system_prompt)}
+
- ))}
-
+
+
+ ))}
- ) : null}
-
- {errorMessage ? (
-
- {errorMessage}
-
- ) : null}
-
-
-
-
-
-
+
+ ) : null}
+
+ {errorMessage ? (
+
+ {errorMessage}
+
+ ) : null}
+
+
+
+
+
From c9d5ac99da1e2e53b2b03f78e2b794c26768a077 Mon Sep 17 00:00:00 2001
From: Wes
Date: Sun, 22 Mar 2026 09:25:43 -0700
Subject: [PATCH 05/11] fix(desktop): disable in-channel personas, add team
drag/drop import + drop zone hints
---
.../src/features/agents/ui/PersonaDialog.tsx | 12 ++++-
.../src/features/agents/ui/TeamsSection.tsx | 48 ++++++++++++++++---
.../ui/AddChannelBotPersonasSection.tsx | 2 +-
.../channels/ui/AddChannelBotTeamsSection.tsx | 6 ++-
4 files changed, 58 insertions(+), 10 deletions(-)
diff --git a/desktop/src/features/agents/ui/PersonaDialog.tsx b/desktop/src/features/agents/ui/PersonaDialog.tsx
index e67d7f36..d2c37fbb 100644
--- a/desktop/src/features/agents/ui/PersonaDialog.tsx
+++ b/desktop/src/features/agents/ui/PersonaDialog.tsx
@@ -1,5 +1,5 @@
import * as React from "react";
-import { Loader2 } from "lucide-react";
+import { Loader2, Upload } from "lucide-react";
import type { ParsePersonaFilesResult } from "@/shared/api/tauriPersonas";
import { parsePersonaFiles } from "@/shared/api/tauriPersonas";
@@ -268,6 +268,16 @@ export function PersonaDialog({
/>
+ {enableImportDrop ? (
+
+
+
+ Drag a .persona.json, .persona.png, or .zip onto this dialog
+ to import.
+
+
+ ) : null}
+
{error ? (
{error.message}
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index b85595d2..b1c6791f 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -66,28 +66,59 @@ export function TeamsSection({
onImportFile,
}: TeamsSectionProps) {
const fileInputRef = React.useRef(null);
+ const [isDragOver, setIsDragOver] = React.useState(false);
+
+ async function importFile(file: File) {
+ const buffer = await file.arrayBuffer();
+ const bytes = Array.from(new Uint8Array(buffer));
+ onImportFile(bytes, file.name);
+ }
+
+ async function handleDrop(e: React.DragEvent) {
+ e.preventDefault();
+ setIsDragOver(false);
+
+ const file = e.dataTransfer.files[0];
+ if (!file) return;
+
+ await importFile(file);
+ }
function handleFileChange(e: React.ChangeEvent) {
const file = e.target.files?.[0];
if (!file) return;
- void (async () => {
- const buffer = await file.arrayBuffer();
- const bytes = Array.from(new Uint8Array(buffer));
- onImportFile(bytes, file.name);
- })();
+ void importFile(file);
// Reset so the same file can be re-selected
e.target.value = "";
}
return (
-
+ // biome-ignore lint/a11y/noStaticElementInteractions: drop zone for .team.json import
+ setIsDragOver(false)}
+ onDragOver={(e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ }}
+ onDrop={(e: React.DragEvent) => void handleDrop(e)}
+ >
+ {isDragOver ? (
+
+
+ Drop .team.json to import
+
+
+ ) : null}
+
Teams
- Named groups of personas you can deploy to a channel together.
+ Named groups of personas you can deploy to a channel together. Drop
+ a .team.json file to import.
@@ -274,6 +305,9 @@ export function TeamsSection({
Create a team to group personas for quick deployment to channels.
+
+ Or drop a .team.json file here to import.
+
) : null}
diff --git a/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx b/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx
index 91a6cda5..da2d67f8 100644
--- a/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx
@@ -131,7 +131,7 @@ export function AddChannelBotPersonasSection({
onTogglePersona(persona.id)}
selected={isSelected}
diff --git a/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx b/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
index 1e07520d..f7a5857f 100644
--- a/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
@@ -105,7 +105,11 @@ export function AddChannelBotTeamsSection({
onToggleTeam(validIds)}
selected={allSelected}
From c2b661cad97d6ed000755c124caa90b060535fac Mon Sep 17 00:00:00 2001
From: Wes
Date: Sun, 22 Mar 2026 09:35:36 -0700
Subject: [PATCH 06/11] fix(desktop): cap persona grid at 4 columns and prevent
Built-in badge wrapping
---
desktop/src/features/agents/ui/PersonasSection.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/desktop/src/features/agents/ui/PersonasSection.tsx b/desktop/src/features/agents/ui/PersonasSection.tsx
index e9ce44f3..307e5af1 100644
--- a/desktop/src/features/agents/ui/PersonasSection.tsx
+++ b/desktop/src/features/agents/ui/PersonasSection.tsx
@@ -70,8 +70,8 @@ export function PersonasSection({
{isLoading ? (
-
- {["first", "second", "third", "fourth", "fifth"].map((key) => (
+
+ {["first", "second", "third", "fourth"].map((key) => (
0 ? (
-
+
{personas.map((persona) => {
const preview = promptPreview(persona.systemPrompt);
@@ -111,7 +111,7 @@ export function PersonasSection({
{persona.displayName}
{persona.isBuiltIn ? (
-
+
Built-in
) : null}
From fda37083759c0a9cb05d59a6518a8d2db0c71e9d Mon Sep 17 00:00:00 2001
From: Wes
Date: Sun, 22 Mar 2026 09:44:11 -0700
Subject: [PATCH 07/11] feat(desktop): add drag/drop and file picker import to
PersonasSection
Mirror the TeamsSection import pattern on PersonasSection:
- Drag/drop overlay for .persona.json, .persona.png, or .zip files
- Upload button (file picker) next to the Create button
- Drop zone hint text in subtitle and empty state
- handlePersonaImportFile in AgentsView routes single files to
PersonaDialog and multi-file archives to BatchImportDialog
Bump AgentsView.tsx file size limit from 740 to 790 to accommodate
the new import handler wiring.
---
desktop/scripts/check-file-sizes.mjs | 2 +-
desktop/src/features/agents/ui/AgentsView.tsx | 45 +++++++-
.../features/agents/ui/PersonasSection.tsx | 109 +++++++++++++++---
3 files changed, 138 insertions(+), 18 deletions(-)
diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs
index 719aa370..ed100dcc 100644
--- a/desktop/scripts/check-file-sizes.mjs
+++ b/desktop/scripts/check-file-sizes.mjs
@@ -43,7 +43,7 @@ const overrides = new Map([
["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions
["src-tauri/src/commands/agents.rs", 820], // remote agent lifecycle routing (local + provider branches) + scope enforcement
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
- ["src/features/agents/ui/AgentsView.tsx", 740], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona/team import
+ ["src/features/agents/ui/AgentsView.tsx", 790], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona/team import + persona drag/drop handler
["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes
["src/features/channels/ui/AddChannelBotDialog.tsx", 600], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement
]);
diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index bab45742..c5e86f06 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -21,7 +21,10 @@ import {
import { useChannelsQuery } from "@/features/channels/hooks";
import { usePresenceQuery } from "@/features/presence/hooks";
import { sendChannelMessage } from "@/shared/api/tauri";
-import type { ParsePersonaFilesResult } from "@/shared/api/tauriPersonas";
+import {
+ parsePersonaFiles,
+ type ParsePersonaFilesResult,
+} from "@/shared/api/tauriPersonas";
import type {
AgentPersona,
@@ -391,6 +394,43 @@ export function AgentsView() {
void relayAgentsQuery.refetch();
}
+ async function handlePersonaImportFile(
+ fileBytes: number[],
+ fileName: string,
+ ) {
+ setActionNoticeMessage(null);
+ setActionErrorMessage(null);
+ try {
+ const result = await parsePersonaFiles(fileBytes, fileName);
+ const isPng =
+ fileBytes.length >= 4 && fileBytes[0] === 0x89 && fileBytes[1] === 0x50;
+ const isJson = fileBytes.length > 0 && fileBytes[0] === 0x7b;
+ if ((isPng || isJson) && result.personas.length === 1) {
+ const p = result.personas[0];
+ setPersonaDialogState({
+ title: `Import ${p.displayName}`,
+ description: "Review and save this imported persona.",
+ enableImportDrop: false,
+ submitLabel: "Create persona",
+ initialValues: {
+ displayName: p.displayName,
+ avatarUrl: p.avatarDataUrl ?? "",
+ systemPrompt: p.systemPrompt,
+ },
+ });
+ } else if (result.personas.length > 0) {
+ setBatchImportResult(result);
+ setBatchImportFileName(fileName);
+ } else {
+ setActionErrorMessage("No valid personas found in file.");
+ }
+ } catch (err) {
+ setActionErrorMessage(
+ err instanceof Error ? err.message : "Failed to parse persona file.",
+ );
+ }
+ }
+
const isActionPending =
startMutation.isPending ||
stopMutation.isPending ||
@@ -473,6 +513,9 @@ export function AgentsView() {
},
});
}}
+ onImportFile={(fileBytes, fileName) => {
+ void handlePersonaImportFile(fileBytes, fileName);
+ }}
onExport={(persona) => {
exportPersonaJsonMutation.mutate(persona.id, {
onSuccess: (saved) => {
diff --git a/desktop/src/features/agents/ui/PersonasSection.tsx b/desktop/src/features/agents/ui/PersonasSection.tsx
index 307e5af1..32edc458 100644
--- a/desktop/src/features/agents/ui/PersonasSection.tsx
+++ b/desktop/src/features/agents/ui/PersonasSection.tsx
@@ -1,3 +1,4 @@
+import * as React from "react";
import {
CopyPlus,
Download,
@@ -6,6 +7,7 @@ import {
Pencil,
Plus,
Trash2,
+ Upload,
} from "lucide-react";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
@@ -31,6 +33,7 @@ type PersonasSectionProps = {
onEdit: (persona: AgentPersona) => void;
onExport: (persona: AgentPersona) => void;
onDelete: (persona: AgentPersona) => void;
+ onImportFile: (fileBytes: number[], fileName: string) => void;
};
export function PersonasSection({
@@ -43,30 +46,101 @@ export function PersonasSection({
onEdit,
onExport,
onDelete,
+ onImportFile,
}: PersonasSectionProps) {
+ const fileInputRef = React.useRef(null);
+ const [isDragOver, setIsDragOver] = React.useState(false);
+
+ async function importFile(file: File) {
+ const buffer = await file.arrayBuffer();
+ const bytes = Array.from(new Uint8Array(buffer));
+ onImportFile(bytes, file.name);
+ }
+
+ async function handleDrop(e: React.DragEvent) {
+ e.preventDefault();
+ setIsDragOver(false);
+
+ const file = e.dataTransfer.files[0];
+ if (!file) return;
+
+ await importFile(file);
+ }
+
+ function handleFileChange(e: React.ChangeEvent) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ void importFile(file);
+
+ // Reset so the same file can be re-selected
+ e.target.value = "";
+ }
+
return (
-
+ // biome-ignore lint/a11y/noStaticElementInteractions: drop zone for persona file import
+ setIsDragOver(false)}
+ onDragOver={(e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ }}
+ onDrop={(e: React.DragEvent) => void handleDrop(e)}
+ >
+ {isDragOver ? (
+
+
+ Drop .persona.json, .persona.png, or .zip to import
+
+
+ ) : null}
+
Personas
- Reusable agent templates for common roles and prompts.
+ Reusable agent templates for common roles and prompts. Drop a file
+ to import.
-
-
-
-
- Create persona
-
+
+
+
+
+
+
+ Import persona
+
+
+
+
+
+ Create persona
+
+
{isLoading ? (
@@ -198,6 +272,9 @@ export function PersonasSection({
Create one to save a role, prompt, and optional avatar for reuse.
+
+ Or drop a .persona.json, .persona.png, or .zip file here to import.
+
) : null}
From 37d70c56948bd2dd03295d6b47c247dd9d0cb462 Mon Sep 17 00:00:00 2001
From: Wes
Date: Sun, 22 Mar 2026 09:49:33 -0700
Subject: [PATCH 08/11] refactor(desktop): extract shared fileMagic constants
and useFileImportZone hook
---
desktop/src/features/agents/ui/AgentsView.tsx | 6 +-
.../src/features/agents/ui/PersonaDialog.tsx | 13 ++--
.../features/agents/ui/PersonasSection.tsx | 50 ++++------------
.../src/features/agents/ui/TeamsSection.tsx | 50 ++++------------
desktop/src/shared/hooks/useFileImportZone.ts | 60 +++++++++++++++++++
desktop/src/shared/lib/fileMagic.ts | 24 ++++++++
6 files changed, 112 insertions(+), 91 deletions(-)
create mode 100644 desktop/src/shared/hooks/useFileImportZone.ts
create mode 100644 desktop/src/shared/lib/fileMagic.ts
diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index c5e86f06..b58175eb 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -25,6 +25,7 @@ import {
parsePersonaFiles,
type ParsePersonaFilesResult,
} from "@/shared/api/tauriPersonas";
+import { isSingleItemFile } from "@/shared/lib/fileMagic";
import type {
AgentPersona,
@@ -402,10 +403,7 @@ export function AgentsView() {
setActionErrorMessage(null);
try {
const result = await parsePersonaFiles(fileBytes, fileName);
- const isPng =
- fileBytes.length >= 4 && fileBytes[0] === 0x89 && fileBytes[1] === 0x50;
- const isJson = fileBytes.length > 0 && fileBytes[0] === 0x7b;
- if ((isPng || isJson) && result.personas.length === 1) {
+ if (isSingleItemFile(fileBytes) && result.personas.length === 1) {
const p = result.personas[0];
setPersonaDialogState({
title: `Import ${p.displayName}`,
diff --git a/desktop/src/features/agents/ui/PersonaDialog.tsx b/desktop/src/features/agents/ui/PersonaDialog.tsx
index d2c37fbb..c6654286 100644
--- a/desktop/src/features/agents/ui/PersonaDialog.tsx
+++ b/desktop/src/features/agents/ui/PersonaDialog.tsx
@@ -15,17 +15,16 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
+import {
+ JSON_FIRST_BYTE,
+ PNG_MAGIC,
+ ZIP_MAGIC,
+ matchesMagic,
+} from "@/shared/lib/fileMagic";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB (ZIP ceiling)
-const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47];
-const ZIP_MAGIC = [0x50, 0x4b, 0x03, 0x04];
-const JSON_FIRST_BYTE = 0x7b; // '{'
-
-function matchesMagic(bytes: number[], magic: number[]) {
- return magic.every((b, i) => bytes[i] === b);
-}
type PersonaDialogProps = {
open: boolean;
diff --git a/desktop/src/features/agents/ui/PersonasSection.tsx b/desktop/src/features/agents/ui/PersonasSection.tsx
index 32edc458..7f2d6196 100644
--- a/desktop/src/features/agents/ui/PersonasSection.tsx
+++ b/desktop/src/features/agents/ui/PersonasSection.tsx
@@ -1,4 +1,3 @@
-import * as React from "react";
import {
CopyPlus,
Download,
@@ -12,6 +11,7 @@ import {
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import type { AgentPersona } from "@/shared/api/types";
+import { useFileImportZone } from "@/shared/hooks/useFileImportZone";
import { promptPreview } from "@/shared/lib/promptPreview";
import { Button } from "@/shared/ui/button";
import {
@@ -48,46 +48,16 @@ export function PersonasSection({
onDelete,
onImportFile,
}: PersonasSectionProps) {
- const fileInputRef = React.useRef(null);
- const [isDragOver, setIsDragOver] = React.useState(false);
-
- async function importFile(file: File) {
- const buffer = await file.arrayBuffer();
- const bytes = Array.from(new Uint8Array(buffer));
- onImportFile(bytes, file.name);
- }
-
- async function handleDrop(e: React.DragEvent) {
- e.preventDefault();
- setIsDragOver(false);
-
- const file = e.dataTransfer.files[0];
- if (!file) return;
-
- await importFile(file);
- }
-
- function handleFileChange(e: React.ChangeEvent) {
- const file = e.target.files?.[0];
- if (!file) return;
-
- void importFile(file);
-
- // Reset so the same file can be re-selected
- e.target.value = "";
- }
+ const {
+ fileInputRef,
+ isDragOver,
+ dropHandlers,
+ handleFileChange,
+ openFilePicker,
+ } = useFileImportZone({ onImportFile });
return (
- // biome-ignore lint/a11y/noStaticElementInteractions: drop zone for persona file import
- setIsDragOver(false)}
- onDragOver={(e: React.DragEvent) => {
- e.preventDefault();
- setIsDragOver(true);
- }}
- onDrop={(e: React.DragEvent) => void handleDrop(e)}
- >
+
{isDragOver ? (
@@ -116,7 +86,7 @@ export function PersonasSection({
);
})}
+
+
+ Import
+
) : null}
{!isLoading && personas.length === 0 ? (
-
+
No personas yet
@@ -245,7 +240,7 @@ export function PersonasSection({
Or drop a .persona.json, .persona.png, or .zip file here to import.
-
+
) : null}
{error ? (
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index a201e1ed..8a4f7cb5 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -87,47 +87,30 @@ export function TeamsSection({
Teams
- Named groups of personas you can deploy to a channel together. Drop
- a .team.json file to import.
+ Named groups of personas you can deploy to a channel together.
-
-
-
-
-
-
-
-
- Import team
-
-
-
-
-
-
-
- Create team
-
-
+
+
+
+
+
+
+
+ Create team
+
{isLoading ? (
@@ -266,11 +249,23 @@ export function TeamsSection({
);
})}
+
+
+ Import
+
) : null}
{!isLoading && teams.length === 0 ? (
-
+
No teams yet
Create a team to group personas for quick deployment to channels.
@@ -278,7 +273,7 @@ export function TeamsSection({
Or drop a .team.json file here to import.
-
+
) : null}
{error ? (
From bd490274cc69e028f58ac43ba2aad8424957589c Mon Sep 17 00:00:00 2001
From: Wes
Date: Sun, 22 Mar 2026 10:08:25 -0700
Subject: [PATCH 10/11] style(desktop): accent color on import cards +
breathing room on drop overlay border
---
desktop/src/features/agents/ui/PersonasSection.tsx | 6 +++---
desktop/src/features/agents/ui/TeamsSection.tsx | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/desktop/src/features/agents/ui/PersonasSection.tsx b/desktop/src/features/agents/ui/PersonasSection.tsx
index 7044a8ca..40c978ae 100644
--- a/desktop/src/features/agents/ui/PersonasSection.tsx
+++ b/desktop/src/features/agents/ui/PersonasSection.tsx
@@ -59,7 +59,7 @@ export function PersonasSection({
return (
{isDragOver ? (
-
+
Drop .persona.json, .persona.png, or .zip to import
@@ -215,7 +215,7 @@ export function PersonasSection({
);
})}
@@ -227,7 +227,7 @@ export function PersonasSection({
{!isLoading && personas.length === 0 ? (
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index 8a4f7cb5..29e03823 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -76,7 +76,7 @@ export function TeamsSection({
return (
{isDragOver ? (
-
+
Drop .team.json to import
@@ -250,7 +250,7 @@ export function TeamsSection({
);
})}
@@ -262,7 +262,7 @@ export function TeamsSection({
{!isLoading && teams.length === 0 ? (
From 36b64f4ed5c47a25fa92c073d7bfdfaaf98b9714 Mon Sep 17 00:00:00 2001
From: Wes
Date: Sun, 22 Mar 2026 10:21:39 -0700
Subject: [PATCH 11/11] refactor(desktop): remove redundant drag/drop from
PersonaDialog
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Import is now handled by PersonasSection's drag/drop zone and file picker.
PersonaDialog is now a pure create/edit form — no import logic, no file
parsing, no drag overlay.
Removed: enableImportDrop prop, onBatchImport prop, handleDrop function,
isParsing/isDragOver/importError state, MAX_FILE_SIZE constant, file magic
imports, Loader2/Upload icon imports, drag overlay JSX, parsing overlay JSX,
drop zone hint box.
PersonaDialog: 320 → 187 lines (−133)
AgentsView: 781 → 768 lines (−13)
File size limit: 790 → 775
---
desktop/scripts/check-file-sizes.mjs | 3 +-
desktop/src/features/agents/ui/AgentsView.tsx | 11 --
.../src/features/agents/ui/PersonaDialog.tsx | 141 +-----------------
.../features/agents/ui/PersonasSection.tsx | 6 +-
.../src/features/agents/ui/TeamsSection.tsx | 6 +-
desktop/src/shared/lib/fileMagic.ts | 10 +-
6 files changed, 14 insertions(+), 163 deletions(-)
diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs
index ed100dcc..baffe0ce 100644
--- a/desktop/scripts/check-file-sizes.mjs
+++ b/desktop/scripts/check-file-sizes.mjs
@@ -32,7 +32,6 @@ const rules = [
const overrides = new Map([
["src-tauri/src/managed_agents/persona_card.rs", 700], // PNG/ZIP persona card codec + 21 unit tests (~300 lines of tests)
["src/app/AppShell.tsx", 775],
- ["src/features/agents/ui/AgentsView.tsx", 625], // persona/team orchestration plus import/export wiring
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
["src/features/channels/ui/ChannelManagementSheet.tsx", 800],
["src/features/messages/ui/MessageComposer.tsx", 665], // media upload handlers (paste, drop, dialog) + channelId reset effect
@@ -43,7 +42,7 @@ const overrides = new Map([
["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions
["src-tauri/src/commands/agents.rs", 820], // remote agent lifecycle routing (local + provider branches) + scope enforcement
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
- ["src/features/agents/ui/AgentsView.tsx", 790], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona/team import + persona drag/drop handler
+ ["src/features/agents/ui/AgentsView.tsx", 775], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona/team import
["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes
["src/features/channels/ui/AddChannelBotDialog.tsx", 600], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement
]);
diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index b58175eb..1404b0d0 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -55,7 +55,6 @@ import { useTeamActions } from "./useTeamActions";
type PersonaDialogState = {
description: string;
- enableImportDrop: boolean;
initialValues: CreatePersonaInput | UpdatePersonaInput;
submitLabel: string;
title: string;
@@ -408,7 +407,6 @@ export function AgentsView() {
setPersonaDialogState({
title: `Import ${p.displayName}`,
description: "Review and save this imported persona.",
- enableImportDrop: false,
submitLabel: "Create persona",
initialValues: {
displayName: p.displayName,
@@ -468,7 +466,6 @@ export function AgentsView() {
title: "Create persona",
description:
"Save a reusable role, prompt, and optional avatar for future agent deployments.",
- enableImportDrop: true,
submitLabel: "Create persona",
initialValues: {
displayName: "",
@@ -485,7 +482,6 @@ export function AgentsView() {
title: `Duplicate ${persona.displayName}`,
description:
"Create a new persona by copying this template and adjusting it as needed.",
- enableImportDrop: false,
submitLabel: "Create persona",
initialValues: {
displayName: `${persona.displayName} copy`,
@@ -501,7 +497,6 @@ export function AgentsView() {
title: `Edit ${persona.displayName}`,
description:
"Update this saved persona. New deployments will use the updated values.",
- enableImportDrop: false,
submitLabel: "Save changes",
initialValues: {
id: persona.id,
@@ -659,7 +654,6 @@ export function AgentsView() {
/>
{
- setBatchImportResult(result);
- setBatchImportFileName(fileName);
- setPersonaDialogState(null);
- }}
onOpenChange={(open) => {
if (!open) {
setPersonaDialogState(null);
diff --git a/desktop/src/features/agents/ui/PersonaDialog.tsx b/desktop/src/features/agents/ui/PersonaDialog.tsx
index c6654286..cc55d32e 100644
--- a/desktop/src/features/agents/ui/PersonaDialog.tsx
+++ b/desktop/src/features/agents/ui/PersonaDialog.tsx
@@ -1,8 +1,5 @@
import * as React from "react";
-import { Loader2, Upload } from "lucide-react";
-import type { ParsePersonaFilesResult } from "@/shared/api/tauriPersonas";
-import { parsePersonaFiles } from "@/shared/api/tauriPersonas";
import type {
CreatePersonaInput,
UpdatePersonaInput,
@@ -15,17 +12,9 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
-import {
- JSON_FIRST_BYTE,
- PNG_MAGIC,
- ZIP_MAGIC,
- matchesMagic,
-} from "@/shared/lib/fileMagic";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
-const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB (ZIP ceiling)
-
type PersonaDialogProps = {
open: boolean;
title: string;
@@ -34,10 +23,8 @@ type PersonaDialogProps = {
initialValues: CreatePersonaInput | UpdatePersonaInput | null;
error: Error | null;
isPending: boolean;
- enableImportDrop?: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (input: CreatePersonaInput | UpdatePersonaInput) => Promise;
- onBatchImport?: (result: ParsePersonaFilesResult, fileName: string) => void;
};
export function PersonaDialog({
@@ -48,17 +35,12 @@ export function PersonaDialog({
initialValues,
error,
isPending,
- enableImportDrop,
onOpenChange,
onSubmit,
- onBatchImport,
}: PersonaDialogProps) {
const [displayName, setDisplayName] = React.useState("");
const [avatarUrl, setAvatarUrl] = React.useState("");
const [systemPrompt, setSystemPrompt] = React.useState("");
- const [isDragOver, setIsDragOver] = React.useState(false);
- const [isParsing, setIsParsing] = React.useState(false);
- const [importError, setImportError] = React.useState(null);
React.useEffect(() => {
if (!open || !initialValues) {
@@ -75,74 +57,11 @@ export function PersonaDialog({
setDisplayName("");
setAvatarUrl("");
setSystemPrompt("");
- setIsDragOver(false);
- setIsParsing(false);
- setImportError(null);
}
onOpenChange(next);
}
- async function handleDrop(e: React.DragEvent) {
- e.preventDefault();
- setIsDragOver(false);
-
- if (!enableImportDrop) {
- return;
- }
-
- const file = e.dataTransfer.files[0];
- if (!file) {
- return;
- }
-
- if (file.size > MAX_FILE_SIZE) {
- setImportError("File is too large (max 100 MB).");
- return;
- }
-
- setImportError(null);
- setIsParsing(true);
-
- try {
- const buffer = await file.arrayBuffer();
- const bytes = Array.from(new Uint8Array(buffer));
- const result = await parsePersonaFiles(bytes, file.name);
-
- const isPng = matchesMagic(bytes, PNG_MAGIC);
- const isZip = matchesMagic(bytes, ZIP_MAGIC);
- const isJson = bytes.length > 0 && bytes[0] === JSON_FIRST_BYTE;
-
- if ((isPng || isJson) && result.personas.length === 1) {
- const persona = result.personas[0];
- setDisplayName(persona.displayName);
- setSystemPrompt(persona.systemPrompt);
- setAvatarUrl(persona.avatarDataUrl ?? "");
- return;
- }
-
- if (isZip && result.personas.length > 0 && onBatchImport) {
- onBatchImport(result, file.name);
- return;
- }
-
- if (result.personas.length === 0) {
- setImportError("No valid personas found in file.");
- return;
- }
-
- setImportError(
- "Unsupported file format. Drop a .persona.json, .persona.png, or .zip.",
- );
- } catch (err) {
- setImportError(
- err instanceof Error ? err.message : "Failed to parse file.",
- );
- } finally {
- setIsParsing(false);
- }
- }
-
async function handleSubmit() {
if (!initialValues) {
return;
@@ -167,46 +86,11 @@ export function PersonaDialog({
return (