Skip to content

Commit 941257a

Browse files
committed
Address profile panel review feedback
1 parent 71efb2b commit 941257a

5 files changed

Lines changed: 200 additions & 0 deletions

File tree

desktop/src/features/profile/ui/UserProfilePanel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,12 @@ export function UserProfilePanel({
583583

584584
const handleConfirmDeletePersona = React.useCallback(
585585
async (personaToConfirm: AgentPersona) => {
586+
if (personaToConfirm.sourceTeam) {
587+
toast.error("This agent is managed by a team.");
588+
setPersonaToDelete(null);
589+
return;
590+
}
591+
586592
try {
587593
const deletedInstances =
588594
await deleteManagedAgentsForPersona(personaToConfirm);
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { validateLinkedAgentRuntimeEdit } from "./UserProfilePanelPersonaSubmit.ts";
5+
6+
function agent(overrides = {}) {
7+
return {
8+
pubkey: "deadbeef".repeat(8),
9+
name: "Fizz",
10+
personaId: "persona-1",
11+
relayUrl: "ws://localhost:3000",
12+
acpCommand: "buzz-acp",
13+
agentCommand: "goose",
14+
agentArgs: [],
15+
mcpCommand: "",
16+
turnTimeoutSeconds: 320,
17+
idleTimeoutSeconds: null,
18+
maxTurnDurationSeconds: null,
19+
parallelism: 1,
20+
systemPrompt: "Prompt",
21+
avatarUrl: null,
22+
model: null,
23+
mcpToolsets: null,
24+
envVars: {},
25+
status: "stopped",
26+
pid: null,
27+
createdAt: "2026-01-01T00:00:00Z",
28+
updatedAt: "2026-01-01T00:00:00Z",
29+
lastStartedAt: null,
30+
lastStoppedAt: null,
31+
lastExitCode: null,
32+
lastError: null,
33+
logPath: null,
34+
startOnAppLaunch: true,
35+
backend: { type: "local" },
36+
backendAgentId: null,
37+
respondTo: "owner-only",
38+
respondToAllowlist: [],
39+
...overrides,
40+
};
41+
}
42+
43+
function persona(overrides = {}) {
44+
return {
45+
id: "persona-1",
46+
displayName: "Fizz",
47+
avatarUrl: null,
48+
systemPrompt: "Prompt",
49+
runtime: "goose",
50+
model: null,
51+
provider: null,
52+
namePool: [],
53+
isBuiltIn: false,
54+
isActive: true,
55+
envVars: {},
56+
createdAt: "2026-01-01T00:00:00Z",
57+
updatedAt: "2026-01-01T00:00:00Z",
58+
...overrides,
59+
};
60+
}
61+
62+
function updateInput(overrides = {}) {
63+
return {
64+
id: "persona-1",
65+
displayName: "Fizz",
66+
avatarUrl: undefined,
67+
systemPrompt: "Prompt",
68+
runtime: "claude",
69+
model: undefined,
70+
provider: undefined,
71+
namePool: [],
72+
...overrides,
73+
};
74+
}
75+
76+
function runtime(overrides = {}) {
77+
return {
78+
id: "claude",
79+
label: "Claude Code",
80+
avatarUrl: "",
81+
availability: "available",
82+
command: "claude",
83+
binaryPath: "/usr/local/bin/claude",
84+
defaultArgs: [],
85+
mcpCommand: null,
86+
installHint: "",
87+
installInstructionsUrl: "",
88+
canAutoInstall: false,
89+
underlyingCliPath: null,
90+
...overrides,
91+
};
92+
}
93+
94+
test("validateLinkedAgentRuntimeEdit allows available runtime changes", () => {
95+
assert.equal(
96+
validateLinkedAgentRuntimeEdit({
97+
input: updateInput({ runtime: "claude" }),
98+
managedAgent: agent(),
99+
previousPersona: persona({ runtime: "goose" }),
100+
runtimes: [runtime()],
101+
}),
102+
null,
103+
);
104+
});
105+
106+
test("validateLinkedAgentRuntimeEdit rejects unavailable linked-agent runtime changes", () => {
107+
assert.equal(
108+
validateLinkedAgentRuntimeEdit({
109+
input: updateInput({ runtime: "claude" }),
110+
managedAgent: agent(),
111+
previousPersona: persona({ runtime: "goose" }),
112+
runtimes: [runtime({ availability: "cli_missing", command: null })],
113+
}),
114+
"Claude Code is not available. Install it before saving this linked agent.",
115+
);
116+
});
117+
118+
test("validateLinkedAgentRuntimeEdit allows unchanged or unlinked runtime preferences", () => {
119+
assert.equal(
120+
validateLinkedAgentRuntimeEdit({
121+
input: updateInput({ runtime: "goose" }),
122+
managedAgent: agent(),
123+
previousPersona: persona({ runtime: "goose" }),
124+
runtimes: [],
125+
}),
126+
null,
127+
);
128+
129+
assert.equal(
130+
validateLinkedAgentRuntimeEdit({
131+
input: updateInput({ runtime: "claude" }),
132+
managedAgent: undefined,
133+
previousPersona: persona({ runtime: "goose" }),
134+
runtimes: [],
135+
}),
136+
null,
137+
);
138+
});

desktop/src/features/profile/ui/UserProfilePanelPersonaSubmit.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,42 @@ type SubmitProfilePersonaDialogOptions = {
2727
updatePersona: (input: UpdatePersonaInput) => Promise<AgentPersona>;
2828
};
2929

30+
type ValidateLinkedAgentRuntimeEditOptions = {
31+
input: UpdatePersonaInput;
32+
managedAgent: ManagedAgent | undefined;
33+
previousPersona?: AgentPersona;
34+
runtimes?: readonly AcpRuntimeCatalogEntry[];
35+
};
36+
37+
function normalizeRuntimePreference(value: string | null | undefined): string {
38+
return value?.trim() ?? "";
39+
}
40+
41+
export function validateLinkedAgentRuntimeEdit({
42+
input,
43+
managedAgent,
44+
previousPersona,
45+
runtimes,
46+
}: ValidateLinkedAgentRuntimeEditOptions): string | null {
47+
if (!managedAgent || !previousPersona) {
48+
return null;
49+
}
50+
51+
const previousRuntime = normalizeRuntimePreference(previousPersona.runtime);
52+
const nextRuntime = normalizeRuntimePreference(input.runtime);
53+
if (previousRuntime === nextRuntime) {
54+
return null;
55+
}
56+
57+
const runtime = runtimes?.find((candidate) => candidate.id === nextRuntime);
58+
if (runtime?.availability === "available" && runtime.command) {
59+
return null;
60+
}
61+
62+
const runtimeLabel = runtime?.label ?? "This provider";
63+
return `${runtimeLabel} is not available. Install it before saving this linked agent.`;
64+
}
65+
3066
export async function submitProfilePersonaDialog({
3167
createManagedAgentForPersona,
3268
createPersona,
@@ -40,6 +76,17 @@ export async function submitProfilePersonaDialog({
4076
}: SubmitProfilePersonaDialogOptions) {
4177
try {
4278
if ("id" in input) {
79+
const runtimeEditError = validateLinkedAgentRuntimeEdit({
80+
input,
81+
managedAgent,
82+
previousPersona,
83+
runtimes,
84+
});
85+
if (runtimeEditError) {
86+
toast.error(runtimeEditError);
87+
return;
88+
}
89+
4390
const persona = await updatePersona(input);
4491
const agentUpdate = managedAgent
4592
? personaManagedAgentUpdate(managedAgent, persona, {

desktop/src/shared/api/tauriPersonas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type RawPersona = {
6262
name_pool?: string[];
6363
is_builtin: boolean;
6464
is_active?: boolean;
65+
source_team?: string | null;
6566
env_vars?: Record<string, string>;
6667
created_at: string;
6768
updated_at: string;
@@ -79,6 +80,7 @@ function fromRawPersona(persona: RawPersona): AgentPersona {
7980
namePool: persona.name_pool ?? [],
8081
isBuiltIn: persona.is_builtin,
8182
isActive: persona.is_active ?? true,
83+
sourceTeam: persona.source_team ?? null,
8284
envVars: persona.env_vars ?? {},
8385
createdAt: persona.created_at,
8486
updatedAt: persona.updated_at,

desktop/src/testing/e2eBridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ type RawPersona = {
437437
name_pool?: string[];
438438
is_builtin: boolean;
439439
is_active: boolean;
440+
source_team?: string | null;
440441
env_vars?: Record<string, string>;
441442
created_at: string;
442443
updated_at: string;
@@ -4785,6 +4786,7 @@ async function handleCreatePersona(args: {
47854786
name_pool: [...(args.input.namePool ?? [])],
47864787
is_builtin: false,
47874788
is_active: true,
4789+
source_team: null,
47884790
env_vars: { ...(args.input.envVars ?? {}) },
47894791
created_at: now,
47904792
updated_at: now,
@@ -4840,6 +4842,11 @@ async function handleDeletePersona(args: { id: string }): Promise<void> {
48404842
if (persona.is_builtin) {
48414843
throw new Error("Built-in personas cannot be deleted.");
48424844
}
4845+
if (persona.source_team) {
4846+
throw new Error(
4847+
`${persona.display_name} belongs to a team. Delete the team to remove all team personas together.`,
4848+
);
4849+
}
48434850
if (mockTeams.some((team) => team.persona_ids.includes(args.id))) {
48444851
throw new Error(
48454852
`${persona.display_name} is still referenced by a team. Remove it from those teams first.`,

0 commit comments

Comments
 (0)