diff --git a/desktop/scripts/check-px-text.mjs b/desktop/scripts/check-px-text.mjs index f0d4fd1f4..8182d18cd 100644 --- a/desktop/scripts/check-px-text.mjs +++ b/desktop/scripts/check-px-text.mjs @@ -23,7 +23,7 @@ const rules = [ // glyph is a fixed display size sized to its avatar box (not readable message // text), so it stays as the lone documented `text-[6rem]` literal. const overrides = new Set([ - "src/features/settings/ui/ProfileSettingsCard.tsx:572", + "src/features/settings/ui/ProfileSettingsCard.tsx:584", "src/features/onboarding/ui/AvatarStep.tsx:89", ]); diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index 95f58878b..008922d5c 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -50,7 +50,7 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[BuiltInPersona { "Orchard", "Buzz", ], model: None, - runtime: None, + runtime: Some("goose"), }]; const RETIRED_PERSONAS: &[(&str, &str)] = &[ diff --git a/desktop/src-tauri/src/managed_agents/personas/tests.rs b/desktop/src-tauri/src/managed_agents/personas/tests.rs index 5d5d5e6b4..eff83c737 100644 --- a/desktop/src-tauri/src/managed_agents/personas/tests.rs +++ b/desktop/src-tauri/src/managed_agents/personas/tests.rs @@ -34,6 +34,9 @@ fn merge_personas_adds_missing_built_ins() { assert_eq!(records.len(), BUILT_IN_PERSONAS.len()); assert!(records.iter().all(|record| record.is_builtin)); assert!(records.iter().all(|record| record.is_active)); + assert!(records + .iter() + .any(|record| record.id == "builtin:fizz" && record.runtime.as_deref() == Some("goose"))); let display_names: Vec<&str> = records .iter() .map(|record| record.display_name.as_str()) diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index ed4680978..6cada5b15 100644 --- a/desktop/src/features/agents/hooks.ts +++ b/desktop/src/features/agents/hooks.ts @@ -361,6 +361,17 @@ export function useStartManagedAgentMutation() { return useMutation({ mutationFn: (pubkey: string) => startManagedAgent(pubkey), + onSuccess: (updated) => { + queryClient.setQueryData( + managedAgentsQueryKey, + (current) => { + if (!current) return current; + return current.map((agent) => + agent.pubkey === updated.pubkey ? updated : agent, + ); + }, + ); + }, onSettled: () => { invalidateManagedAgentQueriesInBackground(queryClient); }, diff --git a/desktop/src/features/agents/ui/AgentIdentityCard.tsx b/desktop/src/features/agents/ui/AgentIdentityCard.tsx index 5c27d5451..128f310a1 100644 --- a/desktop/src/features/agents/ui/AgentIdentityCard.tsx +++ b/desktop/src/features/agents/ui/AgentIdentityCard.tsx @@ -7,27 +7,23 @@ import { IdentityInitialsAvatar } from "./IdentityInitialsAvatar"; type AgentIdentityCardProps = { actions?: ReactNode; ariaLabel: string; + avatar?: ReactNode; avatarUrl?: string | null; dataTestId: string; label: string; - errorLabel?: string | null; - modelControl?: ReactNode; - modelLabel: string; + modelLabel?: string | null; onClick: () => void; - status?: ReactNode; }; export function AgentIdentityCard({ actions, ariaLabel, + avatar, avatarUrl, dataTestId, - errorLabel, label, - modelControl, modelLabel, onClick, - status, }: AgentIdentityCardProps) { const trimmedAvatarUrl = avatarUrl?.trim() || null; @@ -40,50 +36,43 @@ export function AgentIdentityCard({ > + {actions ? (
{actions}
) : null} - {status ? ( -
- {status} -
- ) : null} - -
+
{label} - {modelControl ?? ( + {modelLabel ? ( {modelLabel} - )} - {errorLabel ? ( - - {errorLabel} - ) : null}
diff --git a/desktop/src/features/agents/ui/AgentRuntimeAvatarControl.tsx b/desktop/src/features/agents/ui/AgentRuntimeAvatarControl.tsx new file mode 100644 index 000000000..6f34ffba7 --- /dev/null +++ b/desktop/src/features/agents/ui/AgentRuntimeAvatarControl.tsx @@ -0,0 +1,193 @@ +import { CircleAlert, Play } from "lucide-react"; +import { useReducedMotion } from "motion/react"; + +import { PresenceDot } from "@/features/presence/ui/PresenceBadge"; +import { + type AvatarBadgeCurve, + MaskedAvatarBadgeFrame, + STATUS_DOT_MASK_CURVE, +} from "@/features/profile/ui/MaskedAvatarBadgeFrame"; +import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; +import { cn } from "@/shared/lib/cn"; +import { Spinner } from "@/shared/ui/spinner"; +import { IdentityInitialsAvatar } from "./IdentityInitialsAvatar"; + +type AgentRuntimeAvatarControlProps = { + activeTestId: string; + avatarUrl?: string | null; + errorLabel?: string | null; + errorTestId?: string; + isActive: boolean; + isStarting: boolean; + label: string; + startTestId: string; + onOpenError?: () => void; + onStart: () => void; +}; + +const TAILWIND_SPACING = { + "1": 4, + "2": 8, + "2.5": 10, + "6": 24, + "11": 44, + "24": 96, +} as const; + +const AGENT_AVATAR_SIZE = TAILWIND_SPACING["24"]; +const ACTION_BADGE_SIZE = TAILWIND_SPACING["11"]; +const ACTIVE_BADGE_SIZE = TAILWIND_SPACING["6"]; +const ACTION_BADGE_OFFSET = TAILWIND_SPACING["2.5"]; +const ACTIVE_BADGE_INSET = TAILWIND_SPACING["1"]; +const ACTIVE_DOT_CLASS_NAME = "h-4.5 w-4.5"; +const PROFILE_STATUS_CUTOUT_RATIO = 1.25; + +function getBadgeCenter(badgeSize: number, outwardOffset: number) { + return AGENT_AVATAR_SIZE + outwardOffset - badgeSize / 2; +} + +function getActionBadge(offset: number) { + return { + cutout: { + cx: getBadgeCenter(ACTION_BADGE_SIZE, offset), + cy: getBadgeCenter(ACTION_BADGE_SIZE, offset), + r: ACTION_BADGE_SIZE / 2, + }, + shell: { + bottom: -offset, + height: ACTION_BADGE_SIZE, + right: -offset, + width: ACTION_BADGE_SIZE, + }, + } as const; +} + +function getActiveBadge(inset: number) { + return { + cutout: { + cx: getBadgeCenter(ACTIVE_BADGE_SIZE, -inset), + cy: getBadgeCenter(ACTIVE_BADGE_SIZE, -inset), + r: (ACTIVE_BADGE_SIZE / 2) * PROFILE_STATUS_CUTOUT_RATIO, + }, + shell: { + bottom: inset, + height: ACTIVE_BADGE_SIZE, + right: inset, + width: ACTIVE_BADGE_SIZE, + }, + } as const; +} + +const ACTION_MASK_CURVE = { + avatarRoundingAngle: 0.16, + cutoutRoundingLength: ACTION_BADGE_SIZE * 0.18, + cutoutRoundingMinAngle: 0.34, + cutoutRoundingMaxAngle: 0.52, + handleDistanceRatio: 0.58, + handleLengthRatio: 0.26, +} satisfies AvatarBadgeCurve; + +const ACTION_BADGE = getActionBadge(ACTION_BADGE_OFFSET); +const ACTIVE_BADGE = getActiveBadge(ACTIVE_BADGE_INSET); + +const MASK_TRANSITION = { + duration: 0.22, + ease: [0.23, 1, 0.32, 1], +} as const; + +export function AgentRuntimeAvatarControl({ + activeTestId, + avatarUrl, + errorLabel, + errorTestId, + isActive, + isStarting, + label, + startTestId, + onOpenError, + onStart, +}: AgentRuntimeAvatarControlProps) { + const shouldReduceMotion = useReducedMotion(); + const trimmedAvatarUrl = avatarUrl?.trim() || null; + const actionLabel = isStarting ? `Starting ${label}` : `Start ${label}`; + const hasError = !isActive && !isStarting && Boolean(errorLabel); + const errorActionLabel = `${label} has a runtime error. Open runtime details.`; + const transition = shouldReduceMotion ? { duration: 0 } : MASK_TRANSITION; + const badge = isActive ? ACTIVE_BADGE : ACTION_BADGE; + + return ( + + {isActive ? ( + + + + ) : ( + + )} + + } + badgeBox={badge.shell} + className="h-24 w-24" + curve={isActive ? STATUS_DOT_MASK_CURVE : ACTION_MASK_CURVE} + cutout={badge.cutout} + maskTransition={transition} + size={AGENT_AVATAR_SIZE} + > + {trimmedAvatarUrl ? ( + + ) : ( + + )} + + ); +} diff --git a/desktop/src/features/agents/ui/AgentsScreen.tsx b/desktop/src/features/agents/ui/AgentsScreen.tsx index d52ac7923..361199c50 100644 --- a/desktop/src/features/agents/ui/AgentsScreen.tsx +++ b/desktop/src/features/agents/ui/AgentsScreen.tsx @@ -14,7 +14,10 @@ import { } from "@/features/profile/ui/UserProfilePanelUtils"; import { useIdentityQuery } from "@/shared/api/hooks"; import type { AgentPersona } from "@/shared/api/types"; -import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext"; +import { + type ProfilePanelOpenOptions, + ProfilePanelProvider, +} from "@/shared/context/ProfilePanelContext"; import { useHistorySearchState } from "@/shared/hooks/useHistorySearchState"; import { useThreadPanelWidth } from "@/shared/hooks/useThreadPanelWidth"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; @@ -64,11 +67,11 @@ export function AgentsScreen() { const { goChannel } = useAppNavigation(); const handleOpenProfilePanel = React.useCallback( - (pubkey: string) => { + (pubkey: string, options?: ProfilePanelOpenOptions) => { applyPatch({ profile: pubkey, profilePersona: null, - profileTab: null, + profileTab: options?.tab === "info" ? null : (options?.tab ?? null), profileView: null, }); }, diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 5f17957dc..acc07363a 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -59,15 +59,13 @@ export function AgentsView() { return ( <> -
-
-
+
+
+
{ void agents.handleBulkRemoveStopped(); }} @@ -87,12 +84,18 @@ export function AgentsView() { onCreateAgent={() => { agents.setIsCreateOpen(true); }} - onOpenAgentProfile={(pubkey) => { - openProfilePanel?.(pubkey); + onOpenAgentProfile={(pubkey, options) => { + openProfilePanel?.(pubkey, options); }} onOpenPersonaProfile={(persona) => { openPersonaProfilePanel?.(persona); }} + onStartAgent={(pubkey) => { + void agents.handleStart(pubkey); + }} + onStartPersona={(persona) => { + void agents.handleStartPersona(persona); + }} // Persona props canChooseCatalog={personas.catalogPersonas.length > 0} personas={personas.libraryPersonas} diff --git a/desktop/src/features/agents/ui/EditAgentDialog.tsx b/desktop/src/features/agents/ui/EditAgentDialog.tsx index 5d47f5b2b..ed6635218 100644 --- a/desktop/src/features/agents/ui/EditAgentDialog.tsx +++ b/desktop/src/features/agents/ui/EditAgentDialog.tsx @@ -5,10 +5,12 @@ import { useUpdateManagedAgentMutation, } from "@/features/agents/hooks"; import type { + AgentModelsResponse, ManagedAgent, RespondToMode, UpdateManagedAgentInput, } from "@/shared/api/types"; +import { getAgentModels } from "@/shared/api/tauri"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -17,6 +19,7 @@ import { DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; +import { Input } from "@/shared/ui/input"; import { CreateAgentBasicsFields, CreateAgentRuntimeFields, @@ -24,6 +27,9 @@ import { import { EnvVarsEditor, type EnvVarsValue } from "./EnvVarsEditor"; import { CreateAgentRespondToField } from "./RespondToField"; +const AUTO_MODEL_VALUE = "__auto_model__"; +const CUSTOM_MODEL_VALUE = "__custom_model__"; + export function EditAgentDialog({ agent, open, @@ -59,6 +65,11 @@ export function EditAgentDialog({ const [systemPrompt, setSystemPrompt] = React.useState( agent.systemPrompt ?? "", ); + const [model, setModel] = React.useState(agent.model ?? ""); + const [modelsData, setModelsData] = + React.useState(null); + const [modelsLoading, setModelsLoading] = React.useState(false); + const [modelsError, setModelsError] = React.useState(null); const [envVars, setEnvVars] = React.useState(agent.envVars); const personasQuery = usePersonasQuery(); const linkedPersona = React.useMemo( @@ -96,6 +107,7 @@ export function EditAgentDialog({ setTurnTimeoutSeconds(String(agent.turnTimeoutSeconds)); setParallelism(String(agent.parallelism)); setSystemPrompt(agent.systemPrompt ?? ""); + setModel(agent.model ?? ""); setEnvVars(agent.envVars); setRespondTo(agent.respondTo); setRespondToAllowlist(agent.respondToAllowlist); @@ -103,6 +115,40 @@ export function EditAgentDialog({ } }, [open, agent.pubkey]); + React.useEffect(() => { + if (!open) { + return; + } + + let cancelled = false; + setModelsData(null); + setModelsError(null); + setModelsLoading(true); + + getAgentModels(agent.pubkey) + .then((data) => { + if (!cancelled) { + setModelsData(data); + } + }) + .catch((error) => { + if (!cancelled) { + setModelsError( + error instanceof Error ? error.message : String(error), + ); + } + }) + .finally(() => { + if (!cancelled) { + setModelsLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [open, agent.pubkey]); + function handleOpenChange(next: boolean) { onOpenChange(next); } @@ -138,6 +184,7 @@ export function EditAgentDialog({ .split(",") .map((v) => v.trim()) .filter((v) => v.length > 0); + const normalizedModel = model.trim() || null; // Harness pin resolution. The backend treats an empty string as the // "inherit from persona" sentinel (clears the override) and any concrete @@ -188,6 +235,10 @@ export function EditAgentDialog({ (systemPrompt.trim() || null) !== agent.systemPrompt ? systemPrompt.trim() || null : undefined, + model: + normalizedModel !== (agent.model ?? null) + ? normalizedModel + : undefined, envVars: envVarsChanged(envVars, agent.envVars) ? envVars : undefined, respondTo: respondTo !== agent.respondTo ? respondTo : undefined, // The allowlist is preserved across mode toggles in local UI state @@ -237,6 +288,15 @@ export function EditAgentDialog({ onModeChange={setRespondTo} /> + + {linkedPersona ? (
+ ) + } + badgeBox={{ + bottom: 0, + height: 54, + right: 0, + width: 54, + }} + className="h-48 w-48" + clipTestId="profile-avatar-preview-clip" + cutout={{ cx: 165, cy: 165, r: 30 }} + size={192} > -
- {shouldShowAnimatedPreview ? null : emojiAvatarPreview ? ( +
- 0 && "buzz-avatar-squish", - )} - data-testid="profile-avatar-preview-emoji" - key={avatarSquishKey} - > - {emojiAvatarPreview.emoji} - -
- ) : ( - - )} -
- -
- -
+
+
diff --git a/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx b/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx index 759ef4775..885dd7e9a 100644 --- a/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx +++ b/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx @@ -4,6 +4,10 @@ import { getPresenceLabel } from "@/features/presence/lib/presence"; import { PresenceDot } from "@/features/presence/ui/PresenceBadge"; import { useSelfProfileCache } from "@/features/profile/hooks"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; +import { + MaskedAvatarBadgeFrame, + STATUS_DOT_MASK_CURVE, +} from "@/features/profile/ui/MaskedAvatarBadgeFrame"; import { ProfilePopover } from "@/features/profile/ui/ProfilePopover"; import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji"; import type { Workspace } from "@/features/workspaces/types"; @@ -102,22 +106,32 @@ export function SidebarProfileCard({ }} type="button" > - - + + + } + badgeBox={{ bottom: -2, height: 14, right: -2, width: 14 }} + className="h-8 w-8" + curve={STATUS_DOT_MASK_CURVE} + cutout={{ cx: 28, cy: 28, r: 7.5 }} + size={32} > - - + +
diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index 569a915c3..0b49eb447 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -364,9 +364,10 @@ export type CreateManagedAgentInput = { acpCommand?: string; agentCommand?: string; /** - * True when `agentCommand` is a runtime command the user deliberately picked - * for a linked persona. Lets the backend preserve real selections, including - * installed aliases, while still ignoring missing-runtime fallbacks. + * True when `agentCommand` is a runtime command the caller deliberately wants + * to preserve instead of inheriting the linked persona command. This covers + * deploy-dialog runtime selections and discovered or installed aliases for the + * same persona runtime id, while still ignoring missing-runtime fallbacks. */ harnessOverride?: boolean; agentArgs?: string[]; diff --git a/desktop/src/shared/context/ProfilePanelContext.tsx b/desktop/src/shared/context/ProfilePanelContext.tsx index eea5f354b..3f47364c5 100644 --- a/desktop/src/shared/context/ProfilePanelContext.tsx +++ b/desktop/src/shared/context/ProfilePanelContext.tsx @@ -2,8 +2,14 @@ import * as React from "react"; import type { AgentPersona } from "@/shared/api/types"; +export type ProfilePanelOpenOptions = { + tab?: "info" | "runtime" | "channels" | "memories"; +}; + type ProfilePanelContextValue = { - openProfilePanel: ((pubkey: string) => void) | null; + openProfilePanel: + | ((pubkey: string, options?: ProfilePanelOpenOptions) => void) + | null; openPersonaProfilePanel: ((persona: AgentPersona) => void) | null; }; @@ -18,7 +24,10 @@ export function ProfilePanelProvider({ onOpenPersonaProfilePanel, }: { children: React.ReactNode; - onOpenProfilePanel: (pubkey: string) => void; + onOpenProfilePanel: ( + pubkey: string, + options?: ProfilePanelOpenOptions, + ) => void; onOpenPersonaProfilePanel?: (persona: AgentPersona) => void; }) { const value = React.useMemo( diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 115b20222..b0389103b 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -44,6 +44,7 @@ type MockManagedAgentSeed = { channelNames?: string[]; channelIds?: string[]; backend?: RawManagedAgent["backend"]; + lastError?: string | null; respondTo?: RawManagedAgent["respond_to"]; respondToAllowlist?: string[]; }; @@ -451,6 +452,10 @@ type RawPersona = { display_name: string; avatar_url: string | null; system_prompt: string; + runtime?: string | null; + model?: string | null; + provider?: string | null; + name_pool?: string[]; is_builtin: boolean; is_active: boolean; env_vars?: Record; @@ -1030,7 +1035,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent { last_started_at: status === "running" ? now : null, last_stopped_at: status === "stopped" ? now : null, last_exit_code: null, - last_error: null, + last_error: seed.lastError ?? null, log_path: `/tmp/mock-agent-${seed.pubkey}.log`, start_on_app_launch: true, backend: seed.backend ?? { type: "local" }, @@ -1115,6 +1120,8 @@ function resetMockPersonas(config?: E2eConfig) { display_name: "Fizz", avatar_url: null, system_prompt: "You are Fizz.", + runtime: "goose", + model: null, is_builtin: true, is_active: activePersonaIds.has("builtin:fizz"), created_at: now, diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js index 1e55595b4..a1e17bd2b 100644 --- a/desktop/tailwind.config.js +++ b/desktop/tailwind.config.js @@ -16,6 +16,9 @@ export default { md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, + spacing: { + 4.5: "1.125rem", + }, fontFamily: { sans: [ '"Inter Variable"', diff --git a/desktop/tests/e2e/active-turn-resilience.spec.ts b/desktop/tests/e2e/active-turn-resilience.spec.ts index 47a2ab78f..f8e3c39ae 100644 --- a/desktop/tests/e2e/active-turn-resilience.spec.ts +++ b/desktop/tests/e2e/active-turn-resilience.spec.ts @@ -59,6 +59,16 @@ async function seedTurns( }, turns); } +async function openAgentProfile( + page: import("@playwright/test").Page, + pubkey: string, +) { + await page.getByTestId(`managed-agent-${pubkey}`).click(); + const panel = page.getByTestId("user-profile-panel"); + await expect(panel).toBeVisible({ timeout: 5_000 }); + return panel; +} + test.describe("active turn badge resilience", () => { test.use({ viewport: { width: 1280, height: 720 } }); @@ -107,19 +117,20 @@ test.describe("active turn badge resilience", () => { }, ]); - const paulRow = page.getByTestId(`managed-agent-${AGENT_PAUL}`); - const duncanRow = page.getByTestId(`managed-agent-${AGENT_DUNCAN}`); - await expect(paulRow).toContainText("Working", { timeout: 5_000 }); - await expect(duncanRow).toContainText("Working", { timeout: 5_000 }); + const paulPanel = await openAgentProfile(page, AGENT_PAUL); + await expect(paulPanel).toContainText("Working in #general", { + timeout: 5_000, + }); + await expect(paulPanel).toContainText("Working in #engineering"); // Simulate the all-at-once relay drop: no further frames, advance the clock // past both thresholds. This fires several real prune ticks; shouldPausePrune // sees every turn's lastActivityAt stuck at T0 (gap > 20s) and pauses the - // prune, so the badges survive. Under the pre-fix code every badge would be - // gone after the first tick past 25s. + // prune, so the active-turn-driven working badges survive. Under the + // pre-fix code every badge would be gone after the first tick past 25s. await page.clock.fastForward(FRAME_GAP_MS); - await expect(paulRow).toContainText("Working"); - await expect(duncanRow).toContainText("Working"); + await expect(paulPanel).toContainText("Working in #general"); + await expect(paulPanel).toContainText("Working in #engineering"); }); }); diff --git a/desktop/tests/e2e/mesh-compute.spec.ts b/desktop/tests/e2e/mesh-compute.spec.ts index fe655747e..9fce32f0a 100644 --- a/desktop/tests/e2e/mesh-compute.spec.ts +++ b/desktop/tests/e2e/mesh-compute.spec.ts @@ -331,7 +331,9 @@ test("saved relay-mesh agents restart via the backend serve-target preflight", a const row = page.getByTestId(`managed-agent-${pubkey}`); await expect(row).toContainText("Saved relay mesh agent"); - await expect(row).toContainText("running"); + await expect( + page.getByTestId(`agent-runtime-active-${pubkey}`), + ).toBeVisible(); await page.getByRole("button", { name: "Done" }).click(); await expect(page.getByRole("dialog", { name: "Agent created" })).toHaveCount( 0, @@ -341,7 +343,7 @@ test("saved relay-mesh agents restart via the backend serve-target preflight", a await expect .poll(async () => await commands(page)) .toContain("stop_managed_agent"); - await expect(row).toContainText("stopped"); + await expect(page.getByTestId(`agent-runtime-start-${pubkey}`)).toBeVisible(); // With a live serve target for the model, manual restart goes through: // the backend preflight re-resolves the target and the agent starts. @@ -349,10 +351,12 @@ test("saved relay-mesh agents restart via the backend serve-target preflight", a await expect .poll(async () => await commands(page)) .toContain("start_managed_agent"); - await expect(row).toContainText("running"); + await expect( + page.getByTestId(`agent-runtime-active-${pubkey}`), + ).toBeVisible(); await triggerManagedAgentPrimaryAction(page, pubkey); - await expect(row).toContainText("stopped"); + await expect(page.getByTestId(`agent-runtime-start-${pubkey}`)).toBeVisible(); // Without a live serve target, the backend preflight rejects the start // with an actionable error, surfaced as a toast; the agent stays stopped. @@ -364,7 +368,7 @@ test("saved relay-mesh agents restart via the backend serve-target preflight", a .locator("[data-sonner-toast]") .filter({ hasText: "no live serve target is available" }), ).toBeVisible(); - await expect(row).toContainText("stopped"); + await expect(page.getByTestId(`agent-runtime-start-${pubkey}`)).toBeVisible(); await expect( page.evaluate(async (agentPubkey) => { @@ -383,5 +387,5 @@ test("saved relay-mesh agents restart via the backend serve-target preflight", a } }, pubkey), ).resolves.toContain("no live serve target is available"); - await expect(row).toContainText("stopped"); + await expect(page.getByTestId(`agent-runtime-start-${pubkey}`)).toBeVisible(); }); diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index e2bdb4de4..0ff0e8a79 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -301,7 +301,7 @@ test("nests the avatar edit button in a clipped notch", async ({ page }) => { await expect(page.getByTestId("profile-avatar-preview-clip")).toHaveCSS( "clip-path", - /url/, + /polygon/, ); const editShell = page.getByTestId("profile-avatar-edit-shell"); await expect(editShell).toHaveCSS("height", "54px"); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index ec9dc3a2d..2d1cc2561 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -52,6 +52,7 @@ type MockManagedAgentSeed = { backend?: | { type: "local" } | { type: "provider"; id: string; config: Record }; + lastError?: string | null; respondTo?: "owner-only" | "allowlist" | "anyone"; respondToAllowlist?: string[]; };