Skip to content

Commit 71efb2b

Browse files
committed
Address agent import review feedback
1 parent 7bd453b commit 71efb2b

7 files changed

Lines changed: 200 additions & 40 deletions

File tree

desktop/src/features/agents/ui/personaImportPlan.test.mjs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function createPersona(overrides = {}) {
1414
systemPrompt: "Be helpful.",
1515
runtime: null,
1616
model: null,
17+
provider: null,
1718
namePool: [],
1819
isBuiltIn: false,
1920
isActive: true,
@@ -31,6 +32,7 @@ function createPreview(overrides = {}) {
3132
avatarRef: null,
3233
runtime: null,
3334
model: null,
35+
provider: null,
3436
namePool: [],
3537
sourceFile: "alice.persona.json",
3638
...overrides,
@@ -118,6 +120,19 @@ test("buildPersonaImportPlan detects model change", () => {
118120
assert.equal(plan.fields[0]?.label, "Preferred model");
119121
});
120122

123+
test("buildPersonaImportPlan detects provider change", () => {
124+
const plan = buildPersonaImportPlan({
125+
persona: createPersona({ provider: "anthropic" }),
126+
preview: createPreview({ provider: "databricks" }),
127+
});
128+
129+
assert.equal(plan.fields.length, 1);
130+
assert.equal(plan.fields[0]?.field, "provider");
131+
assert.equal(plan.fields[0]?.label, "LLM provider");
132+
assert.equal(plan.fields[0]?.existingValue, "anthropic");
133+
assert.equal(plan.fields[0]?.importedValue, "databricks");
134+
});
135+
121136
test("buildPersonaImportPlan detects name pool change", () => {
122137
const plan = buildPersonaImportPlan({
123138
persona: createPersona({ namePool: ["Birch", "Compass"] }),
@@ -135,17 +150,24 @@ test("buildPersonaImportPlan detects multiple field changes", () => {
135150
displayName: "Alice",
136151
systemPrompt: "Old prompt",
137152
model: "gpt-4o",
153+
provider: "openai",
138154
}),
139155
preview: createPreview({
140156
displayName: "Alicia",
141157
systemPrompt: "New prompt",
142158
model: "claude-sonnet-4-20250514",
159+
provider: "anthropic",
143160
}),
144161
});
145162

146-
assert.equal(plan.fields.length, 3);
163+
assert.equal(plan.fields.length, 4);
147164
const fieldNames = plan.fields.map((f) => f.field);
148-
assert.deepEqual(fieldNames, ["displayName", "systemPrompt", "model"]);
165+
assert.deepEqual(fieldNames, [
166+
"displayName",
167+
"systemPrompt",
168+
"model",
169+
"provider",
170+
]);
149171
});
150172

151173
test("hasAnyPersonaImportChanges returns false for empty plan", () => {

desktop/src/features/agents/ui/personaImportPlan.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ export function buildPersonaImportPlan({
177177
});
178178
}
179179

180+
const existingProvider = normalizeOptionalText(persona.provider);
181+
const importedProvider = normalizeOptionalText(preview.provider);
182+
if (existingProvider !== importedProvider) {
183+
fields.push({
184+
field: "provider",
185+
label: "LLM provider",
186+
existingValue: existingProvider,
187+
importedValue: importedProvider,
188+
...singleLineChanges(existingProvider, importedProvider),
189+
});
190+
}
191+
180192
const existingNamePool = namePoolToString(persona.namePool);
181193
const importedNamePool = namePoolToString(preview.namePool);
182194
if (existingNamePool !== importedNamePool) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { buildPersonaImportUpdateInput } from "./personaImportUpdateInput.ts";
5+
6+
function createPersona(overrides = {}) {
7+
return {
8+
id: "persona-1",
9+
displayName: "Alice",
10+
avatarUrl: null,
11+
systemPrompt: "Be helpful.",
12+
runtime: "goose",
13+
model: "claude-sonnet-4",
14+
provider: "anthropic",
15+
namePool: [],
16+
isBuiltIn: false,
17+
isActive: true,
18+
envVars: {},
19+
createdAt: "2026-01-01T00:00:00Z",
20+
updatedAt: "2026-01-01T00:00:00Z",
21+
...overrides,
22+
};
23+
}
24+
25+
function createPreview(overrides = {}) {
26+
return {
27+
displayName: "Alice",
28+
systemPrompt: "Be helpful.",
29+
avatarDataUrl: null,
30+
avatarRef: null,
31+
runtime: "goose",
32+
model: "gpt-5",
33+
provider: "databricks",
34+
namePool: [],
35+
sourceFile: "alice.md",
36+
...overrides,
37+
};
38+
}
39+
40+
test("buildPersonaImportUpdateInput applies selected model and provider updates", () => {
41+
const input = buildPersonaImportUpdateInput({
42+
existing: createPersona(),
43+
preview: createPreview(),
44+
selectedFields: ["model", "provider"],
45+
});
46+
47+
assert.equal(input.model, "gpt-5");
48+
assert.equal(input.provider, "databricks");
49+
});
50+
51+
test("buildPersonaImportUpdateInput preserves provider when provider is not selected", () => {
52+
const input = buildPersonaImportUpdateInput({
53+
existing: createPersona(),
54+
preview: createPreview(),
55+
selectedFields: ["model"],
56+
});
57+
58+
assert.equal(input.model, "gpt-5");
59+
assert.equal(input.provider, "anthropic");
60+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { ParsedPersonaPreview } from "@/shared/api/tauriPersonas";
2+
import type { AgentPersona, UpdatePersonaInput } from "@/shared/api/types";
3+
import { resolveImportedPersonaAvatarUrl } from "@/shared/avatars/gooseAppAvatarRefs";
4+
5+
type BuildPersonaImportUpdateInputArgs = {
6+
existing: AgentPersona;
7+
preview: ParsedPersonaPreview;
8+
selectedFields: Iterable<string>;
9+
};
10+
11+
export function buildPersonaImportUpdateInput({
12+
existing,
13+
preview,
14+
selectedFields,
15+
}: BuildPersonaImportUpdateInputArgs): UpdatePersonaInput {
16+
const selectedFieldSet = new Set(selectedFields);
17+
const previewAvatarUrl = resolveImportedPersonaAvatarUrl(preview);
18+
19+
return {
20+
id: existing.id,
21+
displayName: selectedFieldSet.has("displayName")
22+
? preview.displayName
23+
: existing.displayName,
24+
systemPrompt: selectedFieldSet.has("systemPrompt")
25+
? preview.systemPrompt
26+
: existing.systemPrompt,
27+
avatarUrl: selectedFieldSet.has("avatarUrl")
28+
? (previewAvatarUrl ?? undefined)
29+
: (existing.avatarUrl ?? undefined),
30+
runtime: selectedFieldSet.has("runtime")
31+
? (preview.runtime ?? undefined)
32+
: (existing.runtime ?? undefined),
33+
model: selectedFieldSet.has("model")
34+
? (preview.model ?? undefined)
35+
: (existing.model ?? undefined),
36+
provider: selectedFieldSet.has("provider")
37+
? (preview.provider ?? undefined)
38+
: (existing.provider ?? undefined),
39+
namePool: selectedFieldSet.has("namePool")
40+
? preview.namePool.length > 0
41+
? preview.namePool
42+
: undefined
43+
: existing.namePool.length > 0
44+
? [...existing.namePool]
45+
: undefined,
46+
};
47+
}

desktop/src/features/agents/ui/usePersonaImportActions.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
updatePersona as updatePersonaApi,
88
type ParsedPersonaPreview,
99
} from "@/shared/api/tauriPersonas";
10-
import { resolveImportedPersonaAvatarUrl } from "@/shared/avatars/gooseAppAvatarRefs";
11-
import type { AgentPersona, UpdatePersonaInput } from "@/shared/api/types";
10+
import type { AgentPersona } from "@/shared/api/types";
1211
import { buildPersonaImportPlan } from "./personaImportPlan";
12+
import { buildPersonaImportUpdateInput } from "./personaImportUpdateInput";
1313
import {
1414
editPersonaDialogState,
1515
type PersonaDialogState,
@@ -106,42 +106,20 @@ export function usePersonaImportActions(
106106
preview: personaImportTargetPreview.preview,
107107
});
108108

109-
const selectedFieldSet = new Set(selectedFields);
110109
const preview = personaImportTargetPreview.preview;
111110
const existing = personaImportTarget;
112-
const previewAvatarUrl = resolveImportedPersonaAvatarUrl(preview);
113111

114112
try {
115-
const updateInput: UpdatePersonaInput = {
116-
id: existing.id,
117-
displayName: selectedFieldSet.has("displayName")
118-
? preview.displayName
119-
: existing.displayName,
120-
systemPrompt: selectedFieldSet.has("systemPrompt")
121-
? preview.systemPrompt
122-
: existing.systemPrompt,
123-
avatarUrl: selectedFieldSet.has("avatarUrl")
124-
? (previewAvatarUrl ?? undefined)
125-
: (existing.avatarUrl ?? undefined),
126-
runtime: selectedFieldSet.has("runtime")
127-
? (preview.runtime ?? undefined)
128-
: (existing.runtime ?? undefined),
129-
model: selectedFieldSet.has("model")
130-
? (preview.model ?? undefined)
131-
: (existing.model ?? undefined),
132-
namePool: selectedFieldSet.has("namePool")
133-
? preview.namePool.length > 0
134-
? preview.namePool
135-
: undefined
136-
: existing.namePool.length > 0
137-
? [...existing.namePool]
138-
: undefined,
139-
};
113+
const updateInput = buildPersonaImportUpdateInput({
114+
existing,
115+
preview,
116+
selectedFields,
117+
});
140118

141119
await updatePersonaApi(updateInput);
142120

143121
const updatedFieldCount = plan.fields.filter((field) =>
144-
selectedFieldSet.has(field.field),
122+
selectedFields.includes(field.field),
145123
).length;
146124

147125
feedback.setPersonaNoticeMessage(

desktop/src/shared/avatars/gooseAppAvatarRefs.test.mjs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ test("toGooseAppAvatarRef canonicalizes app-avatar refs", () => {
1313
);
1414
});
1515

16-
test("toGooseAppAvatarRef detects Goose avatar ids in paths", () => {
16+
test("toGooseAppAvatarRef ignores Goose-looking paths by default", () => {
17+
assert.equal(toGooseAppAvatarRef("./avatars/pollies_2.png"), null);
18+
});
19+
20+
test("toGooseAppAvatarRef detects Goose avatar ids in paths during import", () => {
1721
assert.equal(
18-
toGooseAppAvatarRef("./avatars/pollies_2.png"),
22+
toGooseAppAvatarRef("./avatars/pollies_2.png", {
23+
allowFilenameFallback: true,
24+
}),
1925
"app-avatar:pollies-2",
2026
);
2127
});
@@ -40,6 +46,16 @@ test("resolveImportedPersonaAvatarUrl preserves ordinary image URLs", () => {
4046
);
4147
});
4248

49+
test("resolveImportedPersonaAvatarUrl does not rewrite Goose-looking remote URLs", () => {
50+
assert.equal(
51+
resolveImportedPersonaAvatarUrl({
52+
avatarDataUrl: "https://cdn.example.com/avatars/pollies_2.png",
53+
avatarRef: null,
54+
}),
55+
"https://cdn.example.com/avatars/pollies_2.png",
56+
);
57+
});
58+
4359
test("resolveImportedPersonaAvatarUrl preserves URL avatar refs", () => {
4460
assert.equal(
4561
resolveImportedPersonaAvatarUrl({
@@ -49,3 +65,13 @@ test("resolveImportedPersonaAvatarUrl preserves URL avatar refs", () => {
4965
"https://example.com/persona-avatar.png",
5066
);
5167
});
68+
69+
test("resolveImportedPersonaAvatarUrl preserves Goose-looking URL avatar refs", () => {
70+
assert.equal(
71+
resolveImportedPersonaAvatarUrl({
72+
avatarDataUrl: null,
73+
avatarRef: " https://cdn.example.com/avatars/pollies_2.png ",
74+
}),
75+
"https://cdn.example.com/avatars/pollies_2.png",
76+
);
77+
});

desktop/src/shared/avatars/gooseAppAvatarRefs.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ export const GOOSE_APP_AVATAR_REF_PREFIX = "app-avatar:" as const;
33
const APP_AVATAR_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
44
const GOOSE_COLLECTION_ID_PATTERN = /^(fuzzies|gloopies|pollies)[-_](\d+)$/;
55

6+
type ParseGooseAppAvatarOptions = {
7+
allowFilenameFallback?: boolean;
8+
};
9+
610
function cleanAvatarCandidate(value: string): string {
711
const basename = value
812
.trim()
@@ -15,6 +19,7 @@ function cleanAvatarCandidate(value: string): string {
1519

1620
export function parseGooseAppAvatarId(
1721
value: string | null | undefined,
22+
options: ParseGooseAppAvatarOptions = {},
1823
): string | null {
1924
const trimmed = value?.trim();
2025
if (!trimmed) {
@@ -28,19 +33,22 @@ export function parseGooseAppAvatarId(
2833
return APP_AVATAR_ID_PATTERN.test(id) ? id : null;
2934
}
3035

31-
const candidate = cleanAvatarCandidate(trimmed);
32-
const collectionMatch = GOOSE_COLLECTION_ID_PATTERN.exec(candidate);
33-
if (collectionMatch) {
34-
return `${collectionMatch[1]}-${collectionMatch[2]}`;
36+
if (options.allowFilenameFallback) {
37+
const candidate = cleanAvatarCandidate(trimmed);
38+
const collectionMatch = GOOSE_COLLECTION_ID_PATTERN.exec(candidate);
39+
if (collectionMatch) {
40+
return `${collectionMatch[1]}-${collectionMatch[2]}`;
41+
}
3542
}
3643

3744
return null;
3845
}
3946

4047
export function toGooseAppAvatarRef(
4148
value: string | null | undefined,
49+
options: ParseGooseAppAvatarOptions = {},
4250
): string | null {
43-
const id = parseGooseAppAvatarId(value);
51+
const id = parseGooseAppAvatarId(value, options);
4452
return id ? `${GOOSE_APP_AVATAR_REF_PREFIX}${id}` : null;
4553
}
4654

@@ -59,8 +67,15 @@ export function resolveImportedPersonaAvatarUrl({
5967
avatarDataUrl?: string | null;
6068
avatarRef?: string | null;
6169
}): string | null {
70+
const trimmedAvatarRef = avatarRef?.trim();
71+
const avatarRefFileFallback =
72+
trimmedAvatarRef && !isPersistableAvatarUrl(trimmedAvatarRef)
73+
? toGooseAppAvatarRef(trimmedAvatarRef, { allowFilenameFallback: true })
74+
: null;
6275
const gooseRef =
63-
toGooseAppAvatarRef(avatarRef) ?? toGooseAppAvatarRef(avatarDataUrl);
76+
toGooseAppAvatarRef(avatarRef) ??
77+
avatarRefFileFallback ??
78+
toGooseAppAvatarRef(avatarDataUrl);
6479
if (gooseRef) {
6580
return gooseRef;
6681
}

0 commit comments

Comments
 (0)