Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
309889d
Move agent management into profile sidebar
klopez4212 Jun 23, 2026
f4d1664
merge: bring main into PR #1200 (profile sidebar)
Jun 24, 2026
5833946
merge: bring latest main into PR #1200 (profile sidebar)
Jun 25, 2026
85a5438
fix(desktop): enable all agent profile ingress views
tellaho Jun 24, 2026
ba47f02
fix(desktop): handle model discovery setup states
tellaho Jun 24, 2026
93f3f42
feat(profile): surface agent info fields in summary
tellaho Jun 24, 2026
adbb278
feat(profile): combine agent configuration ingress
tellaho Jun 24, 2026
4227ee6
feat(profile): refine agent diagnostics log layout
tellaho Jun 24, 2026
0a57ea6
fix(profile): emphasize diagnostics errors
tellaho Jun 24, 2026
0a2e487
feat(profile): refine agent detail grouping
tellaho Jun 24, 2026
dbf13ed
feat(profile): merge owner and respond-to into one field
Jun 24, 2026
1bba0b3
feat(profile): move agent actions into settings menu
tellaho Jun 25, 2026
e550c1f
fix(profile): show avatar for fallback owner row
tellaho Jun 25, 2026
feee836
fix(profile): show agent details before ingresses
tellaho Jun 25, 2026
06d4953
feat(profile): add tabbed agent profile with drag-scroll tab bar
tellaho Jun 25, 2026
f48f16e
fix(profile): refine profile panel layout
tellaho Jun 25, 2026
8e06ac7
fix(profile): rename diagnostics pane to harness log
tellaho Jun 25, 2026
ef1a7b0
fix(profile): open agent instructions in focused view
tellaho Jun 25, 2026
3a73346
fix(profile): refine agent profile panel actions
tellaho Jun 25, 2026
85a961e
fix(profile): place instructions above status
tellaho Jun 25, 2026
2adbcf2
fix(profile): move action labels into tooltips
tellaho Jun 25, 2026
dc0973a
feat(profile): add history-backed profile routing
tellaho Jun 25, 2026
412857d
fix(profile): align memory empty state
tellaho Jun 25, 2026
98c4563
refactor(profile): componentize shared panel and identity UI primitives
tellaho Jun 25, 2026
7d5c24d
fix(profile): update profile e2e activity navigation
tellaho Jun 25, 2026
aa3e6fd
feat(sidebar): show agent working status on channels
tellaho Jun 25, 2026
4fd4472
test(profile): add profile sidebar screenshot spec
Jun 25, 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
1 change: 1 addition & 0 deletions desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

144 changes: 124 additions & 20 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ use tauri::{AppHandle, State};
use crate::{
app_state::AppState,
managed_agents::{
build_managed_agent_summary, default_agent_workdir, find_managed_agent_mut,
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,
build_databricks_defaults, build_managed_agent_summary, default_agent_workdir,
find_managed_agent_mut, 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, runtime_metadata_env_vars, save_managed_agents,
sync_managed_agent_processes, try_regenerate_nest, AgentModelInfo, AgentModelsResponse,
UpdateManagedAgentRequest, UpdateManagedAgentResponse,
},
relay::{relay_ws_url_with_override, sync_managed_agent_profile},
util::now_iso,
Expand All @@ -27,7 +27,16 @@ pub async fn get_agent_models(
app: AppHandle,
state: State<'_, AppState>,
) -> Result<AgentModelsResponse, String> {
let (resolved_acp, agent_command, agent_args, persisted_model, merged_env) = {
let (
resolved_acp,
agent_command,
agent_args,
persisted_model,
runtime_default_env,
runtime_metadata_env,
databricks_defaults,
merged_env,
) = {
let _store_guard = state
.managed_agents_store_lock
.lock()
Expand Down Expand Up @@ -75,14 +84,51 @@ pub async fn get_agent_models(

// Resolve the effective model from the linked persona so the ModelPicker
// dropdown shows the current persona model as selected.
let (_prompt, effective_model, _provider) = resolve_effective_prompt_model_provider(
record.persona_id.as_deref(),
&personas,
record.system_prompt.clone(),
record.model.clone(),
);

(resolved, resolved_agent, args, effective_model, env)
let (_prompt, effective_model, effective_provider) =
resolve_effective_prompt_model_provider(
record.persona_id.as_deref(),
&personas,
record.system_prompt.clone(),
record.model.clone(),
);
let runtime = known_acp_runtime(&record.agent_command);
let runtime_default_env: Vec<(String, String)> = runtime
.map(|meta| {
meta.default_env
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect()
})
.unwrap_or_default();
let runtime_metadata_env: Vec<(String, String)> = runtime
.map(|meta| {
runtime_metadata_env_vars(
meta.model_env_var,
meta.provider_env_var,
meta.provider_locked,
effective_model.as_deref(),
effective_provider.as_deref(),
)
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
})
.unwrap_or_default();
let databricks_defaults: Vec<(String, String)> = build_databricks_defaults()
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect();

(
resolved,
resolved_agent,
args,
effective_model,
runtime_default_env,
runtime_metadata_env,
databricks_defaults,
env,
)
}; // store lock released — subprocess runs without holding the lock

// Clone the env map for redaction below — `merged_env` is moved
Expand All @@ -105,13 +151,17 @@ pub async fn get_agent_models(
.arg("--json")
.env("BUZZ_ACP_AGENT_COMMAND", &agent_command)
.env("BUZZ_ACP_AGENT_ARGS", agent_args.join(","));
if let Some(meta) = known_acp_runtime(&agent_command) {
for (key, value) in meta.default_env {
if std::env::var(key).is_err() {
cmd.env(key, value);
}
for (key, value) in &runtime_default_env {
if std::env::var(key).is_err() {
cmd.env(key, value);
}
}
for (key, value) in &runtime_metadata_env {
cmd.env(key, value);
}
for (key, value) in &databricks_defaults {
cmd.env(key, value);
}
// User env layering — written LAST so it overrides any Buzz-set env above.
for (k, v) in &merged_env {
cmd.env(k, v);
Expand All @@ -132,6 +182,12 @@ pub async fn get_agent_models(
// a failing child process echoed back.
let stderr_redacted =
crate::managed_agents::redact_env_values_in(stderr.as_ref(), &env_for_redaction);
if let Some(configuration_error) = model_configuration_error(&stderr_redacted) {
return Ok(unavailable_agent_models(
persisted_model,
configuration_error,
));
}
return Err(format!(
"buzz-acp models failed (exit {}): {stderr_redacted}",
output.status.code().unwrap_or(-1)
Expand Down Expand Up @@ -413,5 +469,53 @@ fn normalize_agent_models(
agent_default_model,
selected_model: persisted_model,
supports_switching,
configuration_error: None,
}
}

fn unavailable_agent_models(
persisted_model: Option<String>,
configuration_error: String,
) -> AgentModelsResponse {
AgentModelsResponse {
agent_name: "unknown".to_string(),
agent_version: "unknown".to_string(),
models: Vec::new(),
agent_default_model: None,
selected_model: persisted_model,
supports_switching: false,
configuration_error: Some(configuration_error),
}
}

fn model_configuration_error(stderr: &str) -> Option<String> {
let normalized = stderr.to_ascii_lowercase();

if normalized.contains("buzz_agent_provider required") {
return Some(
"This agent does not have an LLM provider configured. Set a provider and model on the persona or agent, then retry."
.to_string(),
);
}

if normalized.contains("anthropic_model required")
|| normalized.contains("openai_compat_model required")
|| normalized.contains("databricks_model required")
{
return Some(
"This agent does not have an LLM model configured. Set a model on the persona or agent, then retry."
.to_string(),
);
}

if normalized.contains("anthropic_api_key required")
|| normalized.contains("openai_compat_api_key required")
{
return Some(
"This agent is missing credentials for its configured LLM provider. Add the provider credentials, then retry."
.to_string(),
);
}

None
}
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1845,7 +1845,7 @@ fn child_rust_log_filter() -> String {

/// Databricks host/model baked in at compile time for internal builds. Empty
/// in OSS builds, where the `BUZZ_BUILD_DATABRICKS_*` env is unset.
fn build_databricks_defaults() -> Vec<(&'static str, &'static str)> {
pub(crate) fn build_databricks_defaults() -> Vec<(&'static str, &'static str)> {
let mut defaults = Vec::new();
if let Some(host) = option_env!("BUZZ_DESKTOP_BUILD_DATABRICKS_HOST") {
if !host.is_empty() {
Expand Down Expand Up @@ -1977,7 +1977,7 @@ pub fn stop_managed_agent_process(
/// switching need the initial bootstrap value. Provider injection is skipped
/// when `provider_locked` is true (e.g. Claude runtimes that only work with
/// Anthropic).
fn runtime_metadata_env_vars<'a>(
pub(crate) fn runtime_metadata_env_vars<'a>(
model_env_var: Option<&'a str>,
provider_env_var: Option<&'a str>,
provider_locked: bool,
Expand Down
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/managed_agents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@ pub struct AgentModelsResponse {
pub selected_model: Option<String>,
/// Whether this agent supports model switching.
pub supports_switching: bool,
/// Human-readable setup issue that prevents model discovery.
pub configuration_error: Option<String>,
}

/// A single model available from an agent.
Expand Down
29 changes: 29 additions & 0 deletions desktop/src/app/routes/agents.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";

import {
parseProfilePanelTab,
parseProfilePanelView,
type ProfilePanelTab,
type ProfilePanelView,
} from "@/features/profile/ui/UserProfilePanelUtils";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type AgentsRouteSearch = {
profile?: string;
profilePersona?: string;
profileTab?: ProfilePanelTab;
profileView?: ProfilePanelView;
};

function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}

function validateAgentsSearch(
search: Record<string, unknown>,
): AgentsRouteSearch {
return {
profile: nonEmptyString(search.profile),
profilePersona: nonEmptyString(search.profilePersona),
profileTab: parseProfilePanelTab(search.profileTab) ?? undefined,
profileView: parseProfilePanelView(search.profileView) ?? undefined,
};
}

const AgentsScreen = React.lazy(async () => {
const module = await import("@/features/agents/ui/AgentsScreen");
return { default: module.AgentsScreen };
});

export const Route = createFileRoute("/agents")({
validateSearch: validateAgentsSearch,
component: AgentsRouteComponent,
});

Expand Down
16 changes: 10 additions & 6 deletions desktop/src/app/routes/channels.$channelId.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";

import {
parseProfilePanelTab,
parseProfilePanelView,
type ProfilePanelTab,
type ProfilePanelView,
} from "@/features/profile/ui/UserProfilePanelUtils";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type ChannelRouteSearch = {
agentSession?: string;
messageId?: string;
profile?: string;
profileView?: "memories" | "channels";
profileTab?: ProfilePanelTab;
profileView?: ProfilePanelView;
thread?: string;
threadRootId?: string;
};
Expand All @@ -16,18 +23,15 @@ function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}

function profileViewValue(value: unknown): "memories" | "channels" | undefined {
return value === "memories" || value === "channels" ? value : undefined;
}

function validateChannelSearch(
search: Record<string, unknown>,
): ChannelRouteSearch {
return {
agentSession: nonEmptyString(search.agentSession),
messageId: nonEmptyString(search.messageId),
profile: nonEmptyString(search.profile),
profileView: profileViewValue(search.profileView),
profileTab: parseProfilePanelTab(search.profileTab) ?? undefined,
profileView: parseProfilePanelView(search.profileView) ?? undefined,
thread: nonEmptyString(search.thread),
threadRootId: nonEmptyString(search.threadRootId),
};
Expand Down
15 changes: 10 additions & 5 deletions desktop/src/app/routes/pulse.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";

import {
parseProfilePanelTab,
parseProfilePanelView,
type ProfilePanelTab,
type ProfilePanelView,
} from "@/features/profile/ui/UserProfilePanelUtils";
import { usePreviewFeatureWarning } from "@/shared/features";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

Expand All @@ -11,7 +17,8 @@ const PulseScreen = React.lazy(async () => {

type PulseRouteSearch = {
profile?: string;
profileView?: "memories" | "channels";
profileTab?: ProfilePanelTab;
profileView?: ProfilePanelView;
};

function validatePulseSearch(
Expand All @@ -22,10 +29,8 @@ function validatePulseSearch(
typeof search.profile === "string" && search.profile.length > 0
? search.profile
: undefined,
profileView:
search.profileView === "memories" || search.profileView === "channels"
? search.profileView
: undefined,
profileTab: parseProfilePanelTab(search.profileTab) ?? undefined,
profileView: parseProfilePanelView(search.profileView) ?? undefined,
};
}

Expand Down
14 changes: 9 additions & 5 deletions desktop/src/features/agent-memory/ui/MemorySection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { AlertTriangle, ChevronDown, RefreshCw } from "lucide-react";
import { AlertTriangle, Brain, ChevronDown, RefreshCw } from "lucide-react";

import { useAgentMemoryGraph } from "@/features/agent-memory/hooks";
import type { MemoryTreeNode } from "@/features/agent-memory/lib/buildMemoryGraph";
Expand Down Expand Up @@ -225,12 +225,16 @@ function MemoryGraphView({
const isEmpty = !rootedTree && orphans.length === 0;
if (isEmpty) {
return (
<p
className="text-sm italic text-muted-foreground"
<div
className="flex min-h-56 flex-col items-center justify-center px-6 py-10 text-center"
data-testid="agent-memory-empty"
>
This agent has no memories yet.
</p>
<Brain className="mx-auto h-4 w-4 text-muted-foreground" />
<p className="mt-3 text-sm font-medium">No memories yet</p>
<p className="mt-1 text-sm text-muted-foreground">
This agent has no memories yet.
</p>
</div>
);
}

Expand Down
Loading