Add edit dialog for managed agents with relay profile sync (#277)

This commit is contained in:
Will Pfleger
2026-04-13 18:15:43 +00:00
committed by GitHub
parent 4c6c897d11
commit ccecf7e362
9 changed files with 519 additions and 136 deletions
+3 -2
View File
@@ -47,14 +47,15 @@ const overrides = new Map([
["src-tauri/src/commands/agents.rs", 880], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field
["src-tauri/src/managed_agents/runtime.rs", 690], // KNOWN_AGENT_BINARIES const + process_belongs_to_us FFI (macOS proc_name + Linux /proc/comm) + terminate_process + start/stop/sync lifecycle + pack persona live-read
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
["src/features/agents/hooks.ts", 520], // agent query/mutation surface now includes built-in persona library activation
["src/features/agents/hooks.ts", 540], // agent query/mutation surface now includes built-in persona library activation + useUpdateManagedAgentMutation
["src/features/agents/ui/AgentsView.tsx", 880], // remote agent lifecycle controls + persona/team management + persona import-update dialog wiring + built-in catalog/library state orchestration
["src/features/agents/ui/ManagedAgentRow.tsx", 530], // EditAgentDialog integration + provider/local branching
["src/features/agents/ui/TeamDialog.tsx", 530], // team create/edit dialog with persona multi-select, import button, window drag detection, removal confirmation
["src/features/agents/ui/TeamImportUpdateDialog.tsx", 660], // team import diff preview with member matching/updating/adding/removing sections, LCS line counts, removal confirmation
["src/features/agents/ui/useTeamActions.ts", 510], // team CRUD + export + import + import-update orchestration with query invalidation
["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", 640], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement, provider warnings display
["src/shared/api/types.ts", 540], // persona provider/model fields + forum types + workflow type re-exports + ephemeral channel TTL fields + mcpToolsets + sourcePack
["src/shared/api/types.ts", 550], // persona provider/model fields + forum types + workflow type re-exports + ephemeral channel TTL fields + mcpToolsets + sourcePack + UpdateManagedAgentInput edit fields
]);
async function walkFiles(directory) {
+121 -44
View File
@@ -1,15 +1,18 @@
use std::collections::HashSet;
use nostr::Keys;
use tauri::{AppHandle, State};
use crate::{
app_state::AppState,
managed_agents::{
build_managed_agent_summary, default_agent_workdir, find_managed_agent_mut,
load_managed_agents, missing_command_message, normalize_agent_args, resolve_command,
save_managed_agents, sync_managed_agent_processes, AgentModelInfo, AgentModelsResponse,
ManagedAgentSummary, UpdateManagedAgentRequest,
load_managed_agents, managed_agent_avatar_url, missing_command_message,
normalize_agent_args, resolve_command, save_managed_agents, sync_managed_agent_processes,
AgentModelInfo, AgentModelsResponse, ManagedAgentSummary, UpdateManagedAgentRequest,
UpdateManagedAgentResponse,
},
relay::{relay_ws_url, sync_managed_agent_profile},
util::now_iso,
};
@@ -96,56 +99,130 @@ pub async fn get_agent_models(
/// Update mutable fields on an existing managed agent record.
///
/// Does NOT auto-restart the agent. The frontend should prompt the user
/// to restart for model changes to take effect.
/// Does NOT auto-restart the agent. Runtime config changes (system prompt,
/// parallelism, commands, toolsets) take effect on the next agent spawn.
/// Name changes are synced to the relay immediately via a kind:0 re-publish.
#[tauri::command]
pub fn update_managed_agent(
pub async fn update_managed_agent(
input: UpdateManagedAgentRequest,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<ManagedAgentSummary, String> {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let mut records = load_managed_agents(&app)?;
let mut runtimes = state
.managed_agent_processes
.lock()
.map_err(|e| e.to_string())?;
sync_managed_agent_processes(&mut records, &mut runtimes);
) -> Result<UpdateManagedAgentResponse, String> {
// Phase 1: local save (synchronous, under lock)
let (summary, sync_params) = {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let mut records = load_managed_agents(&app)?;
let mut runtimes = state
.managed_agent_processes
.lock()
.map_err(|e| e.to_string())?;
sync_managed_agent_processes(&mut records, &mut runtimes);
let record = find_managed_agent_mut(&mut records, &input.pubkey)?;
let record = find_managed_agent_mut(&mut records, &input.pubkey)?;
if let Some(name_update) = input.name {
let trimmed = name_update.trim().to_string();
if !trimmed.is_empty() {
record.name = trimmed;
let mut name_changed = false;
if let Some(name_update) = input.name {
let trimmed = name_update.trim().to_string();
if !trimmed.is_empty() && trimmed != record.name {
record.name = trimmed;
name_changed = true;
}
}
}
// Tri-state: None = don't touch, Some(None) = clear, Some(Some(v)) = set
if let Some(model_update) = input.model {
record.model = model_update;
}
if let Some(prompt_update) = input.system_prompt {
record.system_prompt = prompt_update;
}
if let Some(toolsets_update) = input.mcp_toolsets {
record.mcp_toolsets = toolsets_update
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
.map(str::to_string);
}
record.updated_at = now_iso();
if let Some(model_update) = input.model {
record.model = model_update;
}
if let Some(prompt_update) = input.system_prompt {
record.system_prompt = prompt_update;
}
if let Some(toolsets_update) = input.mcp_toolsets {
record.mcp_toolsets = toolsets_update
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
.map(str::to_string);
}
if let Some(parallelism) = input.parallelism {
record.parallelism = parallelism;
}
if let Some(turn_timeout_seconds) = input.turn_timeout_seconds {
record.turn_timeout_seconds = turn_timeout_seconds;
}
if let Some(relay_url) = input.relay_url {
let trimmed = relay_url.trim();
record.relay_url = if trimmed.is_empty() {
relay_ws_url()
} else {
trimmed.to_string()
};
}
if let Some(acp_command) = input.acp_command {
record.acp_command = acp_command;
}
if let Some(agent_command) = input.agent_command {
record.agent_command = agent_command;
}
if let Some(agent_args) = input.agent_args {
record.agent_args = agent_args;
}
if let Some(mcp_command) = input.mcp_command {
record.mcp_command = mcp_command;
}
record.updated_at = now_iso();
save_managed_agents(&app, &records)?;
save_managed_agents(&app, &records)?;
let record = records
.iter()
.find(|r| r.pubkey == input.pubkey)
.ok_or_else(|| format!("agent {} not found", input.pubkey))?;
build_managed_agent_summary(&app, record, &runtimes)
let record = records
.iter()
.find(|r| r.pubkey == input.pubkey)
.ok_or_else(|| format!("agent {} not found", input.pubkey))?;
let sync_params = if name_changed {
let agent_keys = Keys::parse(&record.private_key_nsec)
.map_err(|e| format!("failed to parse agent keys: {e}"))?;
let relay_url = record.relay_url.clone();
let api_token = record.api_token.clone();
let display_name = record.name.clone();
let avatar_url = managed_agent_avatar_url(&record.agent_command);
Some((agent_keys, relay_url, api_token, display_name, avatar_url))
} else {
None
};
let summary = build_managed_agent_summary(&app, record, &runtimes)?;
(summary, sync_params)
}; // lock dropped here
// Phase 2: relay profile sync (async, best-effort, outside lock)
let profile_sync_error =
if let Some((agent_keys, relay_url, api_token, display_name, avatar_url)) = sync_params {
match sync_managed_agent_profile(
&state,
&relay_url,
&agent_keys,
api_token.as_deref(),
&[],
&display_name,
avatar_url.as_deref(),
)
.await
{
Ok(()) => None,
Err(e) => {
eprintln!("sprout-desktop: relay profile sync failed after rename: {e}");
Some(e)
}
}
} else {
None
};
Ok(UpdateManagedAgentResponse {
agent: summary,
profile_sync_error,
})
}
// ── Model normalization ───────────────────────────────────────────────────────
@@ -296,6 +296,26 @@ pub struct UpdateManagedAgentRequest {
pub system_prompt: Option<Option<String>>,
#[serde(default)]
pub mcp_toolsets: Option<Option<String>>,
#[serde(default)]
pub parallelism: Option<u32>,
#[serde(default)]
pub turn_timeout_seconds: Option<u64>,
#[serde(default)]
pub relay_url: Option<String>,
#[serde(default)]
pub acp_command: Option<String>,
#[serde(default)]
pub agent_command: Option<String>,
#[serde(default)]
pub agent_args: Option<Vec<String>>,
#[serde(default)]
pub mcp_command: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct UpdateManagedAgentResponse {
pub agent: ManagedAgentSummary,
pub profile_sync_error: Option<String>,
}
/// Response from `get_agent_models` — normalized model info for the frontend.
+25
View File
@@ -19,6 +19,7 @@ import {
mintManagedAgentToken,
startManagedAgent,
stopManagedAgent,
updateManagedAgent,
} from "@/shared/api/tauri";
import {
createPersona,
@@ -43,6 +44,7 @@ import type {
CreateTeamInput,
ManagedAgent,
MintManagedAgentTokenInput,
UpdateManagedAgentInput,
UpdatePersonaInput,
UpdateTeamInput,
} from "@/shared/api/types";
@@ -195,6 +197,29 @@ export function useCreateManagedAgentMutation() {
});
}
export function useUpdateManagedAgentMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: UpdateManagedAgentInput) => updateManagedAgent(input),
onSuccess: (result) => {
queryClient.setQueryData<ManagedAgent[]>(
managedAgentsQueryKey,
(current) => {
if (!current) return current;
return current.map((agent) =>
agent.pubkey === result.agent.pubkey ? result.agent : agent,
);
},
);
},
onSettled: async () => {
await queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey });
await queryClient.invalidateQueries({ queryKey: relayAgentsQueryKey });
},
});
}
export function useCreatePersonaMutation() {
const queryClient = useQueryClient();
@@ -0,0 +1,219 @@
import * as React from "react";
import { useUpdateManagedAgentMutation } from "@/features/agents/hooks";
import type { ManagedAgent, UpdateManagedAgentInput } from "@/shared/api/types";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import {
CreateAgentBasicsFields,
CreateAgentRuntimeFields,
} from "./CreateAgentDialogSections";
export function EditAgentDialog({
agent,
open,
onOpenChange,
onUpdated,
}: {
agent: ManagedAgent;
open: boolean;
onOpenChange: (open: boolean) => void;
onUpdated?: (agent: ManagedAgent) => void;
}) {
const updateMutation = useUpdateManagedAgentMutation();
const [name, setName] = React.useState(agent.name);
const [relayUrl, setRelayUrl] = React.useState(agent.relayUrl);
const [acpCommand, setAcpCommand] = React.useState(agent.acpCommand);
const [agentCommand, setAgentCommand] = React.useState(agent.agentCommand);
const [agentArgs, setAgentArgs] = React.useState(agent.agentArgs.join(","));
const [mcpCommand, setMcpCommand] = React.useState(agent.mcpCommand);
const [mcpToolsets, setMcpToolsets] = React.useState(agent.mcpToolsets ?? "");
const [turnTimeoutSeconds, setTurnTimeoutSeconds] = React.useState(
String(agent.turnTimeoutSeconds),
);
const [parallelism, setParallelism] = React.useState(
String(agent.parallelism),
);
const [systemPrompt, setSystemPrompt] = React.useState(
agent.systemPrompt ?? "",
);
// Reset form state only when the dialog opens or when switching to a different
// agent. Omitting the full agent object and its array fields from deps prevents
// the effect from firing on every 5s background poll (arrays are never
// reference-equal across renders), which would wipe in-progress user edits.
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — including agent fields would re-fire on every 5s poll and wipe edits
React.useEffect(() => {
if (open) {
setName(agent.name);
setRelayUrl(agent.relayUrl);
setAcpCommand(agent.acpCommand);
setAgentCommand(agent.agentCommand);
setAgentArgs(agent.agentArgs.join(","));
setMcpCommand(agent.mcpCommand);
setMcpToolsets(agent.mcpToolsets ?? "");
setTurnTimeoutSeconds(String(agent.turnTimeoutSeconds));
setParallelism(String(agent.parallelism));
setSystemPrompt(agent.systemPrompt ?? "");
updateMutation.reset();
}
}, [open, agent.pubkey]);
function handleOpenChange(next: boolean) {
onOpenChange(next);
}
const parallelismValid =
parallelism.trim() === "" ||
!Number.isNaN(Number.parseInt(parallelism, 10));
const timeoutValid =
turnTimeoutSeconds.trim() === "" ||
!Number.isNaN(Number.parseInt(turnTimeoutSeconds, 10));
// Block clearing a previously-set command to empty — the backend has no
// "clear to None" path for Option<String> fields, so an empty string would
// cause a runtime failure.
const acpCommandValid = !(agent.acpCommand && acpCommand.trim() === "");
const mcpCommandValid = !(agent.mcpCommand && mcpCommand.trim() === "");
const canSubmit =
name.trim().length > 0 &&
parallelismValid &&
timeoutValid &&
acpCommandValid &&
mcpCommandValid &&
!updateMutation.isPending;
async function handleSubmit() {
try {
const parsedParallelism = Number.parseInt(parallelism, 10);
const parsedTimeout = Number.parseInt(turnTimeoutSeconds, 10);
const parsedArgs = agentArgs
.split(",")
.map((v) => v.trim())
.filter((v) => v.length > 0);
const input: UpdateManagedAgentInput = {
pubkey: agent.pubkey,
name: name.trim() !== agent.name ? name.trim() : undefined,
relayUrl:
relayUrl.trim() !== agent.relayUrl ? relayUrl.trim() : undefined,
acpCommand:
acpCommand.trim() !== agent.acpCommand
? acpCommand.trim()
: undefined,
agentCommand:
agentCommand.trim() !== agent.agentCommand
? agentCommand.trim()
: undefined,
agentArgs:
parsedArgs.join(",") !== agent.agentArgs.join(",")
? parsedArgs
: undefined,
mcpCommand:
mcpCommand.trim() !== agent.mcpCommand
? mcpCommand.trim()
: undefined,
mcpToolsets:
(mcpToolsets.trim() || null) !== agent.mcpToolsets
? mcpToolsets.trim() || null
: undefined,
turnTimeoutSeconds:
parsedTimeout > 0 && parsedTimeout !== agent.turnTimeoutSeconds
? parsedTimeout
: undefined,
parallelism:
parsedParallelism > 0 && parsedParallelism !== agent.parallelism
? parsedParallelism
: undefined,
// Use tri-state: send null to clear, value to set, omit if unchanged.
systemPrompt:
(systemPrompt.trim() || null) !== agent.systemPrompt
? systemPrompt.trim() || null
: undefined,
};
const result = await updateMutation.mutateAsync(input);
if (result.profileSyncError) {
console.warn("Relay profile sync failed:", result.profileSyncError);
}
handleOpenChange(false);
onUpdated?.(result.agent);
} catch {
// React Query stores the error; keep dialog open and render it inline.
}
}
return (
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogContent className="max-w-3xl overflow-hidden p-0">
<div className="flex max-h-[85vh] flex-col">
<DialogHeader className="shrink-0 border-b border-border/60 px-6 py-5 pr-14">
<DialogTitle>Edit agent</DialogTitle>
<DialogDescription>
Update configuration for{" "}
<span className="font-medium">{agent.name}</span>. Changes take
effect on the next start.
</DialogDescription>
</DialogHeader>
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
<CreateAgentBasicsFields name={name} onNameChange={setName} />
<CreateAgentRuntimeFields
acpCommand={acpCommand}
agentArgs={agentArgs}
agentCommand={agentCommand}
mcpCommand={mcpCommand}
mcpToolsets={mcpToolsets}
onAcpCommandChange={setAcpCommand}
onAgentArgsChange={setAgentArgs}
onAgentCommandChange={setAgentCommand}
onMcpCommandChange={setMcpCommand}
onMcpToolsetsChange={setMcpToolsets}
onParallelismChange={setParallelism}
onRelayUrlChange={setRelayUrl}
onSystemPromptChange={setSystemPrompt}
onTurnTimeoutChange={setTurnTimeoutSeconds}
parallelism={parallelism}
relayUrl={relayUrl}
selectedProviderId="custom"
systemPrompt={systemPrompt}
turnTimeoutSeconds={turnTimeoutSeconds}
/>
{updateMutation.error instanceof Error ? (
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{updateMutation.error.message}
</p>
) : null}
</div>
<div className="flex shrink-0 justify-end gap-2 border-t border-border/60 px-6 py-4">
<Button
onClick={() => handleOpenChange(false)}
size="sm"
type="button"
variant="outline"
>
Cancel
</Button>
<Button
disabled={!canSubmit}
onClick={() => void handleSubmit()}
size="sm"
type="button"
>
{updateMutation.isPending ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -1,3 +1,5 @@
import * as React from "react";
import {
ChevronDown,
ChevronRight,
@@ -5,6 +7,7 @@ import {
Ellipsis,
FileText,
KeyRound,
Pencil,
Play,
Power,
Square,
@@ -26,6 +29,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { EditAgentDialog } from "./EditAgentDialog";
import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel";
import { ModelPicker } from "./ModelPicker";
import { truncatePubkey } from "./agentUi";
@@ -313,111 +317,128 @@ function AgentActionsMenu({
onStop: (pubkey: string) => void;
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
}) {
const [editOpen, setEditOpen] = React.useState(false);
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
type="button"
<>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
type="button"
>
<Ellipsis className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
onCloseAutoFocus={(event) => event.preventDefault()}
>
<Ellipsis className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
onCloseAutoFocus={(event) => event.preventDefault()}
>
{agent.backend.type === "provider" ? (
<>
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStart(agent.pubkey)}
>
<Play className="h-4 w-4" />
{isActive ? "Redeploy" : "Deploy"}
</DropdownMenuItem>
{agent.backend.type === "provider" ? (
<>
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStart(agent.pubkey)}
>
<Play className="h-4 w-4" />
{isActive ? "Redeploy" : "Deploy"}
</DropdownMenuItem>
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStop(agent.pubkey)}
>
<Square className="h-4 w-4" />
Shutdown
</DropdownMenuItem>
</>
) : isActive ? (
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStop(agent.pubkey)}
>
<Square className="h-4 w-4" />
Shutdown
Stop
</DropdownMenuItem>
</>
) : isActive ? (
) : (
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStart(agent.pubkey)}
>
<Play className="h-4 w-4" />
Spawn
</DropdownMenuItem>
)}
{agent.backend.type !== "provider" ? (
<DropdownMenuItem onClick={() => setEditOpen(true)}>
<Pencil className="h-4 w-4" />
Edit
</DropdownMenuItem>
) : null}
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStop(agent.pubkey)}
onClick={() => onAddToChannel(agent)}
>
<Square className="h-4 w-4" />
Stop
<UserPlus className="h-4 w-4" />
Add to channel
</DropdownMenuItem>
) : (
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onStart(agent.pubkey)}
onClick={() => onMintToken(agent.pubkey, agent.name)}
>
<Play className="h-4 w-4" />
Spawn
<KeyRound className="h-4 w-4" />
Mint token
</DropdownMenuItem>
)}
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onAddToChannel(agent)}
>
<UserPlus className="h-4 w-4" />
Add to channel
</DropdownMenuItem>
<DropdownMenuItem
disabled={isActionPending}
onClick={() => onMintToken(agent.pubkey, agent.name)}
>
<KeyRound className="h-4 w-4" />
Mint token
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(agent.pubkey)}
>
<Clipboard className="h-4 w-4" />
Copy pubkey
</DropdownMenuItem>
{agent.backend.type === "local" ? (
<DropdownMenuItem onClick={() => onOpenLogs(agent.pubkey)}>
<FileText className="h-4 w-4" />
View logs
</DropdownMenuItem>
) : null}
{agent.backend.type === "local" ? (
<DropdownMenuItem
disabled={isActionPending}
onClick={() =>
onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
}
onClick={() => navigator.clipboard.writeText(agent.pubkey)}
>
<Power className="h-4 w-4" />
{agent.startOnAppLaunch
? "Disable auto-start"
: "Enable auto-start"}
<Clipboard className="h-4 w-4" />
Copy pubkey
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
{agent.backend.type === "local" ? (
<DropdownMenuItem onClick={() => onOpenLogs(agent.pubkey)}>
<FileText className="h-4 w-4" />
View logs
</DropdownMenuItem>
) : null}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={isActionPending}
onClick={() => onDelete(agent.pubkey)}
>
<Trash2 className="h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{agent.backend.type === "local" ? (
<DropdownMenuItem
disabled={isActionPending}
onClick={() =>
onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
}
>
<Power className="h-4 w-4" />
{agent.startOnAppLaunch
? "Disable auto-start"
: "Enable auto-start"}
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={isActionPending}
onClick={() => onDelete(agent.pubkey)}
>
<Trash2 className="h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<EditAgentDialog
agent={agent}
onOpenChange={setEditOpen}
open={editOpen}
/>
</>
);
}
+14 -5
View File
@@ -1046,13 +1046,22 @@ export async function getAgentModels(pubkey: string) {
return invokeTauri<AgentModelsResponse>("get_agent_models", { pubkey });
}
type RawUpdateManagedAgentResponse = {
agent: RawManagedAgent;
profile_sync_error: string | null;
};
export async function updateManagedAgent(
input: UpdateManagedAgentInput,
): Promise<ManagedAgent> {
const response = await invokeTauri<RawManagedAgent>("update_managed_agent", {
input,
});
return fromRawManagedAgent(response);
): Promise<{ agent: ManagedAgent; profileSyncError: string | null }> {
const response = await invokeTauri<RawUpdateManagedAgentResponse>(
"update_managed_agent",
{ input },
);
return {
agent: fromRawManagedAgent(response.agent),
profileSyncError: response.profile_sync_error,
};
}
// ── Backend provider discovery ────────────────────────────────────────────────
+7
View File
@@ -410,6 +410,13 @@ export type UpdateManagedAgentInput = {
model?: string | null;
systemPrompt?: string | null;
mcpToolsets?: string | null;
parallelism?: number;
turnTimeoutSeconds?: number;
relayUrl?: string;
acpCommand?: string;
agentCommand?: string;
agentArgs?: string[];
mcpCommand?: string;
};
export type AgentPersona = {
id: string;
+6 -2
View File
@@ -3506,11 +3506,15 @@ async function handleGetManagedAgentLog(args: {
async function handleUpdateManagedAgent(args: {
input: {
pubkey: string;
name?: string;
model?: string | null;
systemPrompt?: string | null;
};
}): Promise<RawManagedAgent> {
}): Promise<{ agent: RawManagedAgent; profile_sync_error: string | null }> {
const agent = getMockManagedAgent(args.input.pubkey);
if (args.input.name !== undefined) {
agent.name = args.input.name;
}
if (args.input.model !== undefined) {
agent.model = args.input.model;
}
@@ -3518,7 +3522,7 @@ async function handleUpdateManagedAgent(args: {
agent.system_prompt = args.input.systemPrompt;
}
agent.updated_at = new Date().toISOString();
return cloneManagedAgent(agent);
return { agent: cloneManagedAgent(agent), profile_sync_error: null };
}
async function handleSearchMessages(