Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
71501c2
Revamp agents panel profiles
klopez4212 Jun 19, 2026
facace3
Delete persona-backed agents from profile
klopez4212 Jun 19, 2026
74fd4a5
Address agents panel review feedback
klopez4212 Jun 19, 2026
8e27a1d
Update agent start smoke expectation
klopez4212 Jun 19, 2026
3305f87
Address profile persona review feedback
klopez4212 Jun 19, 2026
9c35cf2
Sync desktop smoke tests with agent cards
klopez4212 Jun 19, 2026
5d0c428
Update persona env vars e2e entry point
klopez4212 Jun 19, 2026
4fe66cf
Address remaining agent profile review feedback
klopez4212 Jun 19, 2026
430ecb9
Show persona avatars on agent cards
klopez4212 Jun 19, 2026
cb37bca
Keep secondary persona agents reachable
klopez4212 Jun 19, 2026
6148347
Use live agent profile avatars
klopez4212 Jun 19, 2026
372201c
Preserve persona model without runtime
klopez4212 Jun 19, 2026
650400a
Left align agents panel content
klopez4212 Jun 19, 2026
bee5314
Sync persona profile edits to agents
klopez4212 Jun 20, 2026
9107401
Preserve imported persona provider
klopez4212 Jun 20, 2026
17622ce
Restore persona env var editing
klopez4212 Jun 20, 2026
844e6ca
Preserve cleared agent avatars
klopez4212 Jun 20, 2026
b38729a
Keep managed agent types under size limit
klopez4212 Jun 20, 2026
37c1012
Sync persona runtime edits to agents
klopez4212 Jun 21, 2026
1f406ef
Preserve provider in batch persona imports
klopez4212 Jun 21, 2026
b25d074
Preserve imported persona model on provider select
klopez4212 Jun 21, 2026
690781f
Fix profile agent delete and subview routes
klopez4212 Jun 22, 2026
47eefb1
Preserve imported avatar URL refs
klopez4212 Jun 22, 2026
c421b2a
Reset reused agent respond-to defaults
klopez4212 Jun 22, 2026
c25fa65
Fix agent panel desktop e2e expectations
klopez4212 Jun 22, 2026
7bd453b
Address remaining agent profile review feedback
klopez4212 Jun 22, 2026
71efb2b
Address agent import review feedback
klopez4212 Jun 22, 2026
941257a
Address profile panel review feedback
klopez4212 Jun 22, 2026
c5cdd4c
Address profile panel PR feedback
klopez4212 Jun 22, 2026
9c740c0
Fix agent avatars in cards and teams
klopez4212 Jun 22, 2026
8d28ee1
Prevent runtime logos from replacing persona avatars
klopez4212 Jun 22, 2026
49aa2c3
Restore built-in Fizz avatar
klopez4212 Jun 22, 2026
0673fd6
Update agents page loading skeletons
klopez4212 Jun 22, 2026
a666f00
Make create identity cards transparent by default
klopez4212 Jun 22, 2026
05f136f
Honor persona agent overrides and provider edits
klopez4212 Jun 22, 2026
b965437
Stabilize welcome onboarding e2e assertion
klopez4212 Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit",
"check:file-sizes": "node ./scripts/check-file-sizes.mjs",
"check:px-text": "node ./scripts/check-px-text.mjs",
"sync:goose-avatars": "node ./scripts/sync-goose-avatars.mjs",
"lint": "biome lint .",
"check": "biome check . && pnpm check:file-sizes && pnpm check:px-text",
"format": "biome format --write .",
Expand Down Expand Up @@ -37,6 +38,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-focus-scope": "^1.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.3.1",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
Expand Down
175 changes: 175 additions & 0 deletions desktop/scripts/sync-goose-avatars.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const ARTIFACTORY_BASE =
"https://global.block-artifacts.com/artifactory/goose-internal/avatars";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't have block internal stuff in our OSS PRs.

const LATEST_URL = `${ARTIFACTORY_BASE}/latest.json`;

const scriptDir = dirname(fileURLToPath(import.meta.url));
const desktopRoot = dirname(scriptDir);
const outputRoot = join(desktopRoot, "src/shared/assets/goose-avatars");
const catalogPath = join(outputRoot, "catalog.json");

const FORMATS = ["webm", "hevc"];

function variantOutputPath(asset, format) {
const extension = format === "hevc" ? "mp4" : "webm";
return join(
outputRoot,
format,
asset.collectionId,
`${asset.id}.${extension}`,
);
}

function posterOutputPath(asset) {
return join(outputRoot, "posters", asset.collectionId, `${asset.id}.png`);
}

async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
return response.json();
}

async function fileExistsWithSize(path, byteSize) {
try {
const info = await stat(path);
return info.size === byteSize;
} catch {
return false;
}
}

async function downloadFile(url, path, byteSize) {
if (await fileExistsWithSize(path, byteSize)) {
return "skipped";
}

const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.status}`);
}

const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength !== byteSize) {
throw new Error(
`Downloaded ${url} with ${bytes.byteLength} bytes, expected ${byteSize}.`,
);
}

await mkdir(dirname(path), { recursive: true });
await writeFile(path, bytes);
return "downloaded";
}

async function runFfmpeg(args) {
await new Promise((resolve, reject) => {
const child = spawn("ffmpeg", args, {
stdio: ["ignore", "ignore", "pipe"],
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(
`ffmpeg exited with ${code}: ${stderr.trim() || "no stderr"}`,
),
);
}
});
});
}

async function ensurePoster(asset) {
const posterPath = posterOutputPath(asset);
try {
await stat(posterPath);
return "skipped";
} catch {
// Generate it below.
}

await mkdir(dirname(posterPath), { recursive: true });
await runFfmpeg([
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
variantOutputPath(asset, "webm"),
"-frames:v",
"1",
posterPath,
]);
return "generated";
}

async function main() {
const latest = await fetchJson(LATEST_URL);
const manifest = await fetchJson(
`${ARTIFACTORY_BASE}/${latest.manifestPath}`,
);
const versionRoot = `${ARTIFACTORY_BASE}/${manifest.catalogVersion}`;

await mkdir(outputRoot, { recursive: true });
await writeFile(catalogPath, `${JSON.stringify(manifest, null, 2)}\n`);

const totals = {
downloaded: 0,
skipped: 0,
postersGenerated: 0,
postersSkipped: 0,
};

for (const [index, asset] of manifest.assets.entries()) {
for (const format of FORMATS) {
const variant = asset.variants[format];
const sourceUrl = `${versionRoot}/${variant.path}`;
const result = await downloadFile(
sourceUrl,
variantOutputPath(asset, format),
variant.byteSize,
);
totals[result] += 1;
}

const posterResult = await ensurePoster(asset);
if (posterResult === "generated") {
totals.postersGenerated += 1;
} else {
totals.postersSkipped += 1;
}

const completed = index + 1;
if (completed % 5 === 0 || completed === manifest.assets.length) {
console.log(
`Synced ${completed}/${manifest.assets.length} Goose avatars...`,
);
}
}

const catalogBytes = await readFile(catalogPath, "utf8");
JSON.parse(catalogBytes);

console.log(
`Done. Downloaded ${totals.downloaded}, skipped ${totals.skipped}, generated ${totals.postersGenerated} posters, skipped ${totals.postersSkipped} posters.`,
);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
55 changes: 47 additions & 8 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,40 @@ use crate::{
known_acp_runtime, load_managed_agents, load_personas, managed_agent_avatar_url,
missing_command_message, normalize_agent_args, resolve_command,
resolve_effective_prompt_model_provider, save_managed_agents, sync_managed_agent_processes,
try_regenerate_nest, AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest,
UpdateManagedAgentResponse,
try_regenerate_nest, AgentModelInfo, AgentModelsResponse, ManagedAgentRecord,
UpdateManagedAgentRequest, UpdateManagedAgentResponse,
},
relay::{relay_ws_url_with_override, sync_managed_agent_profile},
util::now_iso,
};

fn trim_optional(value: Option<String>) -> Option<String> {
value
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}

fn is_persona_runtime_avatar(record: &ManagedAgentRecord, avatar_url: &str) -> bool {
record.persona_id.is_some()
&& managed_agent_avatar_url(&record.agent_command)
.as_deref()
.is_some_and(|runtime_avatar_url| runtime_avatar_url == avatar_url.trim())
}

fn profile_sync_avatar_url(record: &ManagedAgentRecord) -> Option<String> {
record
.avatar_url
.clone()
.filter(|avatar_url| !is_persona_runtime_avatar(record, avatar_url))
.or_else(|| {
if record.persona_id.is_none() {
managed_agent_avatar_url(&record.agent_command)
} else {
None
}
})
}

/// Query available models from an agent via `buzz-acp models --json`.
///
/// Spawns a short-lived subprocess (no relay connection needed). The subprocess
Expand Down Expand Up @@ -139,7 +166,8 @@ pub async fn get_agent_models(
///
/// 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.
/// Name and avatar changes are synced to the relay immediately via a kind:0
/// re-publish.
#[tauri::command]
pub async fn update_managed_agent(
input: UpdateManagedAgentRequest,
Expand All @@ -162,13 +190,23 @@ pub async fn update_managed_agent(
let record = find_managed_agent_mut(&mut records, &input.pubkey)?;

let mut name_changed = false;
let mut avatar_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;
}
}
if let Some(avatar_update) = input.avatar_url {
let normalized = trim_optional(avatar_update);
let avatar_url_cleared = normalized.is_none();
if normalized != record.avatar_url || avatar_url_cleared != record.avatar_url_cleared {
record.avatar_url = normalized;
Comment thread
klopez4212 marked this conversation as resolved.
record.avatar_url_cleared = avatar_url_cleared;
avatar_changed = true;
}
}
if let Some(model_update) = input.model {
record.model = model_update;
}
Expand Down Expand Up @@ -245,15 +283,16 @@ pub async fn update_managed_agent(
.find(|r| r.pubkey == input.pubkey)
.ok_or_else(|| format!("agent {} not found", input.pubkey))?;

let sync_params = if name_changed {
let sync_params = if name_changed || avatar_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 display_name = record.name.clone();
let avatar_url = record
.avatar_url
.clone()
.or_else(|| managed_agent_avatar_url(&record.agent_command));
let avatar_url = if avatar_changed || record.avatar_url_cleared {
record.avatar_url.clone()
} else {
profile_sync_avatar_url(record)
};
let auth_tag = record.auth_tag.clone();
Some((agent_keys, relay_url, display_name, avatar_url, auth_tag))
} else {
Expand Down
Loading