mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(config-bridge): resolve persona values at call site and replace badges with provenance sentences
Phase 1: Resolve all three persona fields (prompt, model, provider) at
the get_agent_config_surface call site before calling the reader. Inject
resolved values into the record where absent, then post-process the
surface to re-tag injected fields from BuzzExplicit to PersonaDefault.
Reader stays untouched (pure tier-merge function).
Phase 2: Add AgentConfigPanel to ProfileSummaryView in the profile
pop-out, gated on isBot && isOwner && managedAgent defined.
Phase 3: Remove SourcesFooter and colored OriginBadge pills. Replace
with gray inline provenance sentences below each value ("Set in Buzz",
"Inherited from persona", "From environment variable", etc). No action
clauses.
Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
This commit is contained in:
co-authored by
Will Pfleger
parent
fba912fc48
commit
940608f50c
@@ -6,13 +6,15 @@ use crate::{
|
||||
config_bridge::{
|
||||
reader::read_config_surface,
|
||||
types::{
|
||||
AcpConfigOptionEntry, AcpConfigOptionValue, AcpModelEntry, ConfigWriteMechanism,
|
||||
RuntimeConfigSurface, SessionConfigCache, WriteConfigFieldRequest,
|
||||
WriteConfigResult, WriteConfigTarget,
|
||||
AcpConfigOptionEntry, AcpConfigOptionValue, AcpModelEntry, ConfigOrigin,
|
||||
ConfigWriteMechanism, RuntimeConfigSurface, SessionConfigCache,
|
||||
WriteConfigFieldRequest, WriteConfigResult, WriteConfigTarget,
|
||||
},
|
||||
writer::plan_config_write,
|
||||
},
|
||||
known_acp_runtime, load_managed_agents, save_managed_agents, sync_managed_agent_processes,
|
||||
known_acp_runtime, load_managed_agents, load_personas,
|
||||
resolve_effective_prompt_model_provider, save_managed_agents,
|
||||
sync_managed_agent_processes,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,13 +22,15 @@ use crate::{
|
||||
///
|
||||
/// Returns normalized + advanced config from all available tiers.
|
||||
/// Pre-spawn agents show config file values with ACP tiers marked as pending.
|
||||
/// Persona-sourced values are resolved here (call-site) and re-tagged as
|
||||
/// `PersonaDefault` after the reader produces the surface.
|
||||
#[tauri::command]
|
||||
pub async fn get_agent_config_surface(
|
||||
pubkey: String,
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<RuntimeConfigSurface, String> {
|
||||
let record = {
|
||||
let mut record = {
|
||||
let _store_guard = state
|
||||
.managed_agents_store_lock
|
||||
.lock()
|
||||
@@ -45,14 +49,74 @@ pub async fn get_agent_config_surface(
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?
|
||||
};
|
||||
|
||||
let runtime_meta = known_acp_runtime(&record.agent_command);
|
||||
let session_cache = state.get_session_cache(&pubkey);
|
||||
// Resolve persona values at the call site (not inside the reader).
|
||||
// Track which fields were absent on the record so we can re-tag them
|
||||
// as PersonaDefault after the reader produces the surface.
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
let had_prompt = record.system_prompt.is_some()
|
||||
|| record.env_vars.contains_key("BUZZ_ACP_SYSTEM_PROMPT");
|
||||
let had_model = record.model.is_some();
|
||||
|
||||
Ok(read_config_surface(
|
||||
&record,
|
||||
runtime_meta,
|
||||
session_cache.as_ref(),
|
||||
))
|
||||
let runtime_meta = known_acp_runtime(&record.agent_command);
|
||||
let provider_env_key = runtime_meta
|
||||
.and_then(|m| m.provider_env_var)
|
||||
.unwrap_or("");
|
||||
let had_provider = record.env_vars.contains_key(provider_env_key);
|
||||
|
||||
let (persona_prompt, persona_model, persona_provider) =
|
||||
resolve_effective_prompt_model_provider(
|
||||
record.persona_id.as_deref(),
|
||||
&personas,
|
||||
record.system_prompt.clone(),
|
||||
record.model.clone(),
|
||||
);
|
||||
|
||||
// Inject resolved persona values into the record where absent.
|
||||
if !had_prompt {
|
||||
if let Some(ref p) = persona_prompt {
|
||||
record
|
||||
.env_vars
|
||||
.insert("BUZZ_ACP_SYSTEM_PROMPT".to_string(), p.clone());
|
||||
}
|
||||
}
|
||||
if !had_model {
|
||||
record.model = persona_model;
|
||||
}
|
||||
if !had_provider && !provider_env_key.is_empty() {
|
||||
if let Some(ref prov) = persona_provider {
|
||||
record
|
||||
.env_vars
|
||||
.insert(provider_env_key.to_string(), prov.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let session_cache = state.get_session_cache(&pubkey);
|
||||
let mut surface = read_config_surface(&record, runtime_meta, session_cache.as_ref());
|
||||
|
||||
// Re-tag persona-sourced fields from BuzzExplicit to PersonaDefault.
|
||||
if !had_prompt {
|
||||
if let Some(ref mut field) = surface.normalized.system_prompt {
|
||||
if field.origin == ConfigOrigin::BuzzExplicit {
|
||||
field.origin = ConfigOrigin::PersonaDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !had_model {
|
||||
if let Some(ref mut field) = surface.normalized.model {
|
||||
if field.origin == ConfigOrigin::BuzzExplicit {
|
||||
field.origin = ConfigOrigin::PersonaDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !had_provider {
|
||||
if let Some(ref mut field) = surface.normalized.provider {
|
||||
if field.origin == ConfigOrigin::BuzzExplicit {
|
||||
field.origin = ConfigOrigin::PersonaDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(surface)
|
||||
}
|
||||
|
||||
/// Write a config field value for a managed agent.
|
||||
@@ -60,6 +124,10 @@ pub async fn get_agent_config_surface(
|
||||
/// Plans the write mechanism based on the current config surface, then
|
||||
/// executes: either updating the record (for env var respawn) or returning
|
||||
/// the mechanism for the frontend to send via observer control (for ACP writes).
|
||||
///
|
||||
// TODO: When inline editing lands, this function needs the same persona
|
||||
// injection as get_agent_config_surface — without it, plan_config_write
|
||||
// won't see persona-sourced fields and will return "field not available".
|
||||
#[tauri::command]
|
||||
pub async fn write_agent_config_field(
|
||||
request: WriteConfigFieldRequest,
|
||||
|
||||
@@ -579,4 +579,121 @@ mod tests {
|
||||
// the actual file in a unit test, just verify the override fields are populated
|
||||
// when we manually construct the scenario via build_model_field.
|
||||
}
|
||||
|
||||
// ── Persona resolution integration tests ────────────────────────────
|
||||
//
|
||||
// These simulate the call-site pattern in agent_config.rs:
|
||||
// 1. Inject persona-resolved values into the record (as if absent)
|
||||
// 2. Call read_config_surface (reader tags them BuzzExplicit)
|
||||
// 3. Re-tag injected fields to PersonaDefault
|
||||
//
|
||||
// This exercises the same logic path as get_agent_config_surface without
|
||||
// requiring Tauri AppHandle/State infrastructure.
|
||||
|
||||
#[test]
|
||||
fn persona_model_injection_produces_persona_default_origin() {
|
||||
let mut record = test_record();
|
||||
// Simulate: record has no model, persona provides one.
|
||||
// The call-site injects it before calling the reader.
|
||||
record.model = Some("persona-model".to_string());
|
||||
let runtime = test_runtime();
|
||||
|
||||
let mut surface = read_config_surface(&record, Some(runtime), None);
|
||||
|
||||
// Reader sees injected model as BuzzExplicit.
|
||||
let model = surface.normalized.model.as_ref().unwrap();
|
||||
assert_eq!(model.value.as_deref(), Some("persona-model"));
|
||||
assert_eq!(model.origin, ConfigOrigin::BuzzExplicit);
|
||||
|
||||
// Call-site re-tags (simulating had_model == false).
|
||||
if let Some(ref mut field) = surface.normalized.model {
|
||||
if field.origin == ConfigOrigin::BuzzExplicit {
|
||||
field.origin = ConfigOrigin::PersonaDefault;
|
||||
}
|
||||
}
|
||||
|
||||
let model = surface.normalized.model.unwrap();
|
||||
assert_eq!(model.value.as_deref(), Some("persona-model"));
|
||||
assert_eq!(model.origin, ConfigOrigin::PersonaDefault);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persona_provider_injection_produces_persona_default_origin() {
|
||||
let mut record = test_record();
|
||||
// Simulate: record has no provider env var, persona provides one.
|
||||
// The call-site injects it as GOOSE_PROVIDER before calling the reader.
|
||||
record
|
||||
.env_vars
|
||||
.insert("GOOSE_PROVIDER".to_string(), "anthropic".to_string());
|
||||
let runtime = test_runtime();
|
||||
|
||||
let mut surface = read_config_surface(&record, Some(runtime), None);
|
||||
|
||||
// Reader sees injected provider as BuzzExplicit.
|
||||
let provider = surface.normalized.provider.as_ref().unwrap();
|
||||
assert_eq!(provider.value.as_deref(), Some("anthropic"));
|
||||
assert_eq!(provider.origin, ConfigOrigin::BuzzExplicit);
|
||||
|
||||
// Call-site re-tags (simulating had_provider == false).
|
||||
if let Some(ref mut field) = surface.normalized.provider {
|
||||
if field.origin == ConfigOrigin::BuzzExplicit {
|
||||
field.origin = ConfigOrigin::PersonaDefault;
|
||||
}
|
||||
}
|
||||
|
||||
let provider = surface.normalized.provider.unwrap();
|
||||
assert_eq!(provider.value.as_deref(), Some("anthropic"));
|
||||
assert_eq!(provider.origin, ConfigOrigin::PersonaDefault);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persona_system_prompt_injection_produces_persona_default_origin() {
|
||||
let mut record = test_record();
|
||||
// Simulate: record has no system_prompt, persona provides one via env var.
|
||||
// The call-site injects it as BUZZ_ACP_SYSTEM_PROMPT before calling the reader.
|
||||
record.env_vars.insert(
|
||||
"BUZZ_ACP_SYSTEM_PROMPT".to_string(),
|
||||
"You are a helpful assistant.".to_string(),
|
||||
);
|
||||
let runtime = test_runtime();
|
||||
|
||||
let mut surface = read_config_surface(&record, Some(runtime), None);
|
||||
|
||||
// Reader sees injected prompt as BuzzExplicit.
|
||||
let prompt = surface.normalized.system_prompt.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
prompt.value.as_deref(),
|
||||
Some("You are a helpful assistant.")
|
||||
);
|
||||
assert_eq!(prompt.origin, ConfigOrigin::BuzzExplicit);
|
||||
|
||||
// Call-site re-tags (simulating had_prompt == false).
|
||||
if let Some(ref mut field) = surface.normalized.system_prompt {
|
||||
if field.origin == ConfigOrigin::BuzzExplicit {
|
||||
field.origin = ConfigOrigin::PersonaDefault;
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = surface.normalized.system_prompt.unwrap();
|
||||
assert_eq!(
|
||||
prompt.value.as_deref(),
|
||||
Some("You are a helpful assistant.")
|
||||
);
|
||||
assert_eq!(prompt.origin, ConfigOrigin::PersonaDefault);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_record_model_not_retagged_when_already_present() {
|
||||
let mut record = test_record();
|
||||
// Record already has its own model — persona resolution should NOT re-tag.
|
||||
record.model = Some("explicit-model".to_string());
|
||||
let runtime = test_runtime();
|
||||
|
||||
let surface = read_config_surface(&record, Some(runtime), None);
|
||||
|
||||
// had_model == true, so no re-tagging occurs. Origin stays BuzzExplicit.
|
||||
let model = surface.normalized.model.unwrap();
|
||||
assert_eq!(model.value.as_deref(), Some("explicit-model"));
|
||||
assert_eq!(model.origin, ConfigOrigin::BuzzExplicit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ pub enum ConfigOrigin {
|
||||
/// Read from harness config file on disk (tier 2b, lowest precedence).
|
||||
ConfigFile,
|
||||
/// Value inherited from persona defaults.
|
||||
/// Forward slot — not yet populated by any reader. Will be wired when
|
||||
/// persona pack config resolution is added to `read_config_surface`.
|
||||
/// Populated by the `get_agent_config_surface` call site: persona values are
|
||||
/// resolved before calling the reader, then the surface is post-processed to
|
||||
/// re-tag injected fields from `BuzzExplicit` to `PersonaDefault`.
|
||||
PersonaDefault,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { ChevronDown, ChevronRight, Info } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
|
||||
import { useAgentConfigSurface } from "../hooks";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -7,9 +7,9 @@ import { Spinner } from "@/shared/ui/spinner";
|
||||
import type {
|
||||
ConfigField,
|
||||
ConfigOrigin,
|
||||
ConfigWriteMechanism,
|
||||
NormalizedConfig,
|
||||
NormalizedField,
|
||||
ConfigSourceReport,
|
||||
} from "@/shared/api/types";
|
||||
|
||||
type Props = {
|
||||
@@ -17,68 +17,34 @@ type Props = {
|
||||
isRunning: boolean;
|
||||
};
|
||||
|
||||
// ── Origin badge ─────────────────────────────────────────────────────────────
|
||||
// ── Provenance sentence ──────────────────────────────────────────────────────
|
||||
|
||||
function originLabel(
|
||||
function provenanceSentence(
|
||||
origin: ConfigOrigin,
|
||||
writeVia: ConfigWriteMechanism,
|
||||
configFilePath: string | null,
|
||||
): string {
|
||||
switch (origin) {
|
||||
case "buzzExplicit":
|
||||
return "Buzz";
|
||||
case "acpConfigOption":
|
||||
return "ACP";
|
||||
case "acpNativeRead":
|
||||
return "ACP";
|
||||
case "envVar":
|
||||
return "Env";
|
||||
case "configFile": {
|
||||
if (configFilePath) {
|
||||
const parts = configFilePath.split(/[/\\]/);
|
||||
return parts[parts.length - 1] ?? configFilePath;
|
||||
return "Set in Buzz";
|
||||
case "personaDefault":
|
||||
return "Inherited from persona";
|
||||
case "envVar": {
|
||||
if (writeVia.type === "respawnWithEnvVar") {
|
||||
return `From environment variable (${writeVia.envKey})`;
|
||||
}
|
||||
return "Config";
|
||||
return "From environment variable";
|
||||
}
|
||||
case "personaDefault":
|
||||
return "Persona";
|
||||
}
|
||||
}
|
||||
|
||||
function originColorClass(origin: ConfigOrigin): string {
|
||||
switch (origin) {
|
||||
case "buzzExplicit":
|
||||
return "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300";
|
||||
case "configFile":
|
||||
return configFilePath
|
||||
? `From config file (${configFilePath})`
|
||||
: "From config file";
|
||||
case "acpConfigOption":
|
||||
case "acpNativeRead":
|
||||
return "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300";
|
||||
case "configFile":
|
||||
case "personaDefault":
|
||||
return "bg-muted text-muted-foreground";
|
||||
case "envVar":
|
||||
return "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300";
|
||||
return "From ACP session";
|
||||
}
|
||||
}
|
||||
|
||||
function OriginBadge({
|
||||
origin,
|
||||
configFilePath,
|
||||
}: {
|
||||
origin: ConfigOrigin;
|
||||
configFilePath: string | null;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block max-w-[120px] truncate rounded px-1.5 py-0.5 text-[10px] font-medium leading-none",
|
||||
originColorClass(origin),
|
||||
)}
|
||||
title={originLabel(origin, configFilePath)}
|
||||
>
|
||||
{originLabel(origin, configFilePath)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Normalized row ────────────────────────────────────────────────────────────
|
||||
|
||||
const NORMALIZED_LABELS: Record<keyof NormalizedConfig, string> = {
|
||||
@@ -107,19 +73,13 @@ function NormalizedRow({
|
||||
field.origin === "acpNativeRead" || field.origin === "acpConfigOption";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1 text-sm">
|
||||
<span className="w-36 shrink-0 text-xs text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{/* Value area: effective value + strikethrough overridden value */}
|
||||
<span className="min-w-0 flex-1 truncate font-medium">
|
||||
<div className="py-2">
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="mt-0.5 truncate text-sm font-medium">
|
||||
{isPreSpawn && isAcpOnly ? (
|
||||
<em className="font-normal text-muted-foreground not-italic text-xs">
|
||||
<span className="font-normal text-muted-foreground text-xs">
|
||||
Available after agent starts
|
||||
</em>
|
||||
) : isPreSpawn && field.origin === "configFile" && !field.value ? (
|
||||
<span className="text-xs text-muted-foreground/70">—</span>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{field.value ?? <span className="text-muted-foreground">—</span>}
|
||||
@@ -130,25 +90,11 @@ function NormalizedRow({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Badge area: effective badge + strikethrough overridden badge */}
|
||||
<span className="flex items-center gap-1 shrink-0">
|
||||
<OriginBadge origin={field.origin} configFilePath={configFilePath} />
|
||||
{field.overriddenOrigin && (
|
||||
<span className="opacity-50 line-through">
|
||||
<OriginBadge
|
||||
origin={field.overriddenOrigin}
|
||||
configFilePath={configFilePath}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{!field.isWritable && (
|
||||
<span title="Read-only — edit this field directly in the config file">
|
||||
<Info className="h-3 w-3 shrink-0 text-muted-foreground/50" />
|
||||
</span>
|
||||
</div>
|
||||
{field.value && (
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground/70">
|
||||
{provenanceSentence(field.origin, field.writeVia, configFilePath)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -164,53 +110,22 @@ function AdvancedRow({
|
||||
configFilePath: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1 text-sm">
|
||||
<span className="w-36 shrink-0 text-xs text-muted-foreground">
|
||||
{field.label}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium font-mono text-xs">
|
||||
<div className="py-2">
|
||||
<div className="text-xs text-muted-foreground">{field.label}</div>
|
||||
<div className="mt-0.5 truncate text-sm font-medium font-mono">
|
||||
{field.value ?? (
|
||||
<span className="not-italic text-muted-foreground">—</span>
|
||||
<span className="font-sans text-muted-foreground">—</span>
|
||||
)}
|
||||
</span>
|
||||
<OriginBadge origin={field.origin} configFilePath={configFilePath} />
|
||||
{!field.isWritable && (
|
||||
<span title="Read-only — edit this field directly in the config file">
|
||||
<Info className="h-3 w-3 shrink-0 text-muted-foreground/50" />
|
||||
</span>
|
||||
</div>
|
||||
{field.value && (
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground/70">
|
||||
{provenanceSentence(field.origin, field.writeVia, configFilePath)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sources footer ────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_ICON: Record<string, string> = {
|
||||
available: "✓",
|
||||
pending: "⏳",
|
||||
notApplicable: "—",
|
||||
};
|
||||
|
||||
function SourcesFooter({ sources }: { sources: ConfigSourceReport }) {
|
||||
const tiers = [
|
||||
{ label: "Config file", status: sources.configFile },
|
||||
{ label: "ACP native", status: sources.acpNative },
|
||||
{ label: "ACP config", status: sources.acpConfigOptions },
|
||||
{ label: "Env vars", status: sources.envVars },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<p className="mt-3 text-[10px] text-muted-foreground/70">
|
||||
{tiers.map((tier, i) => (
|
||||
<React.Fragment key={tier.label}>
|
||||
{i > 0 && <span className="mx-1.5">|</span>}
|
||||
{tier.label} <span>{STATUS_ICON[tier.status] ?? tier.status}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main component ────────────────────────────────────────────────────────────
|
||||
|
||||
export function AgentConfigPanel({ pubkey, isRunning: _isRunning }: Props) {
|
||||
@@ -302,8 +217,6 @@ export function AgentConfigPanel({ pubkey, isRunning: _isRunning }: Props) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SourcesFooter sources={sources} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MessageSquare,
|
||||
Pencil,
|
||||
Server,
|
||||
Settings,
|
||||
Terminal,
|
||||
UserMinus,
|
||||
UserPlus,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { MemorySection } from "@/features/agent-memory/ui/MemorySection";
|
||||
import { AgentConfigPanel } from "@/features/agents/ui/AgentConfigPanel";
|
||||
import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge";
|
||||
import { useActiveAgentTurns } from "@/features/agents/activeAgentTurnsStore";
|
||||
import { formatElapsed } from "@/features/agents/ui/agentSessionUtils";
|
||||
@@ -231,6 +233,23 @@ export function ProfileSummaryView({
|
||||
{metadataFields.length > 0 ? (
|
||||
<ProfileFieldGroup fields={metadataFields} />
|
||||
) : null}
|
||||
|
||||
{isBot && isOwner === true && managedAgent !== undefined ? (
|
||||
<section className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<Settings className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<h4 className="text-xs font-medium text-muted-foreground">
|
||||
Configuration
|
||||
</h4>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-muted/20 px-4 py-3">
|
||||
<AgentConfigPanel
|
||||
pubkey={managedAgent.pubkey}
|
||||
isRunning={managedAgent.status === "running"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user