mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add edit dialog for managed agents with relay profile sync (#277)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user