Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ca267bc
Revamp agents panel profiles
klopez4212 Jun 19, 2026
531a242
Delete persona-backed agents from profile
klopez4212 Jun 19, 2026
2066c00
Address agents panel review feedback
klopez4212 Jun 19, 2026
3bc59ec
Update agent start smoke expectation
klopez4212 Jun 19, 2026
9f6539e
Address profile persona review feedback
klopez4212 Jun 19, 2026
ec0d366
Sync desktop smoke tests with agent cards
klopez4212 Jun 19, 2026
656e9fb
Update persona env vars e2e entry point
klopez4212 Jun 19, 2026
b952a4b
Address remaining agent profile review feedback
klopez4212 Jun 19, 2026
d0d3a3e
Show persona avatars on agent cards
klopez4212 Jun 19, 2026
2f4e08a
Keep secondary persona agents reachable
klopez4212 Jun 19, 2026
f920e4f
Use live agent profile avatars
klopez4212 Jun 19, 2026
a825124
Preserve persona model without runtime
klopez4212 Jun 19, 2026
0e0740b
Left align agents panel content
klopez4212 Jun 19, 2026
92a639d
Restore agents page gutter
klopez4212 Jun 19, 2026
0b9acb8
Sync persona profile edits to agents
klopez4212 Jun 20, 2026
c2a79d7
Merge remote-tracking branch 'origin/main' into kennylopez-agents-panel
klopez4212 Jun 20, 2026
b046d17
Preserve imported persona provider
klopez4212 Jun 20, 2026
458e266
Restore persona env var editing
klopez4212 Jun 20, 2026
0066e99
Preserve cleared agent avatars
klopez4212 Jun 20, 2026
1834451
Keep managed agent types under size limit
klopez4212 Jun 20, 2026
5c9b795
Sync persona runtime edits to agents
klopez4212 Jun 21, 2026
c7fd8fc
Preserve provider in batch persona imports
klopez4212 Jun 21, 2026
b733c33
Preserve imported persona model on provider select
klopez4212 Jun 21, 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";
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);
});
33 changes: 27 additions & 6 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ use crate::{
util::now_iso,
};

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

/// 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 +145,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 +169,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 +262,19 @@ 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 {
record
.avatar_url
.clone()
.or_else(|| managed_agent_avatar_url(&record.agent_command))
};
let auth_tag = record.auth_tag.clone();
Some((agent_keys, relay_url, display_name, avatar_url, auth_tag))
} else {
Expand Down
Loading
Loading