Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
104 changes: 104 additions & 0 deletions desktop/src-tauri/src/managed_agents/codex_home.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Buzz-owned Codex state directory (`$NEST/.codex`).
//!
//! Codex defaults to `~/.codex` for sessions and history. Managed agents must
//! not write there — that mixes Buzz sessions into the user's personal Codex
//! sidebar / ChatGPT Remote list (#2660).

use std::fs;
use std::path::{Path, PathBuf};

use super::nest_dir;

/// Buzz-owned Codex state directory (`$NEST/.codex`).
pub fn buzz_codex_home() -> Option<PathBuf> {
nest_dir().map(|root| root.join(".codex"))
}

/// Ensure a Buzz-owned `CODEX_HOME` exists and is seeded with auth/config from
/// the user's personal `~/.codex` when missing.
///
/// Copies (does not symlink) `auth.json` and `config.toml` so Buzz can own
/// session/history files while still inheriting an existing login. Returns the
/// directory path suitable for the `CODEX_HOME` env var.
pub fn prepare_isolated_codex_home() -> Option<PathBuf> {
let home = buzz_codex_home()?;
fs::create_dir_all(&home).ok()?;
seed_codex_home_file(&home, "auth.json");
seed_codex_home_file(&home, "config.toml");
Some(home)
}

/// Set `CODEX_HOME` on a managed-agent spawn when the runtime is Codex.
pub fn apply_isolated_codex_home_env(
command: &mut std::process::Command,
runtime_id: Option<&str>,
) {
if runtime_id != Some("codex") {
return;
}
if let Some(codex_home) = prepare_isolated_codex_home() {
command.env("CODEX_HOME", codex_home);
}
}

fn seed_codex_home_file(codex_home: &Path, name: &str) {
let dest = codex_home.join(name);
if dest.exists() {
return;
}
let Some(user_codex) = dirs::home_dir().map(|h| h.join(".codex")) else {
return;
};
let src = user_codex.join(name);
if src.is_file() {
let _ = fs::copy(&src, &dest);
}
}

#[cfg(test)]
fn seed_codex_home_from(user_codex: &Path, nest_codex: &Path) {
let _ = fs::create_dir_all(nest_codex);
for name in ["auth.json", "config.toml"] {
let dest = nest_codex.join(name);
if dest.exists() {
continue;
}
let src = user_codex.join(name);
if src.is_file() {
let _ = fs::copy(&src, &dest);
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn seed_codex_home_copies_auth_and_config_once() {
let tmp = tempfile::tempdir().unwrap();
let user = tmp.path().join("user-codex");
let nest = tmp.path().join("nest-codex");
fs::create_dir_all(&user).unwrap();
fs::write(user.join("auth.json"), r#"{"tokens":1}"#).unwrap();
fs::write(user.join("config.toml"), "model = \"o3\"\n").unwrap();

seed_codex_home_from(&user, &nest);
assert_eq!(
fs::read_to_string(nest.join("auth.json")).unwrap(),
r#"{"tokens":1}"#
);
assert_eq!(
fs::read_to_string(nest.join("config.toml")).unwrap(),
"model = \"o3\"\n"
);

fs::write(nest.join("auth.json"), r#"{"tokens":"buzz"}"#).unwrap();
fs::write(user.join("auth.json"), r#"{"tokens":"user"}"#).unwrap();
seed_codex_home_from(&user, &nest);
assert_eq!(
fs::read_to_string(nest.join("auth.json")).unwrap(),
r#"{"tokens":"buzz"}"#
);
}
}
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) use agent_env::{
baked_build_env, build_buzz_agent_provider_defaults, discovery_env_with_baked_floor,
};
mod backend;
mod codex_home;
pub(crate) mod config_bridge;
mod discovery;
mod env_vars;
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,7 @@ pub fn spawn_agent_child(
if runtime_meta.is_some_and(|r| r.mcp_hooks) {
command.env("MCP_HOOK_SERVERS", "*");
}
super::codex_home::apply_isolated_codex_home_env(&mut command, runtime_meta.map(|r| r.id));

// ── Readiness check: set setup-payload if agent is not ready ─────────────
//
Expand Down
15 changes: 12 additions & 3 deletions desktop/src/features/communities/ui/CommunitySwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,17 @@ export function CommunitySwitcher({
<div aria-label="Communities" role="menu">
{communities.map((community) => (
<div
className="group flex min-h-9 items-center rounded-lg transition-colors hover:bg-muted/50 focus-within:bg-muted/50"
className={cn(
"group flex min-h-9 items-center rounded-lg transition-colors hover:bg-muted/50 focus-within:bg-muted/50",
activeCommunity?.id === community.id && "bg-muted/60",
)}
key={community.id}
>
<button
className="flex min-h-9 min-w-0 flex-1 items-center gap-2 py-2 pl-2 pr-1 text-left text-sm outline-hidden focus:outline-none"
className={cn(
"flex min-h-9 min-w-0 flex-1 items-center gap-2 py-2 pl-2 pr-1 text-left text-sm outline-hidden focus:outline-none",
activeCommunity?.id === community.id && "font-medium",
)}
onClick={() => {
onSwitchCommunity(community.id);
setDropdownOpen(false);
Expand Down Expand Up @@ -325,7 +331,10 @@ export function CommunitySwitcher({
{communities.map((community) => (
<DropdownMenuItem
key={community.id}
className="group flex items-center gap-2 pr-1"
className={cn(
"group flex items-center gap-2 pr-1",
activeCommunity?.id === community.id && "bg-accent font-medium",
)}
onSelect={() => {
onSwitchCommunity(community.id);
}}
Expand Down
11 changes: 10 additions & 1 deletion desktop/src/features/sidebar/ui/CommunityRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,20 @@ function CommunityButton({
{...dragAttributes}
{...dragListeners}
>
{/* Active pill on the icon edge — ring alone is easy to miss when
a community icon covers bg-primary (#2570). */}
{isActive ? (
<span
aria-hidden="true"
className="absolute left-0 top-1/2 z-10 h-5 w-1 -translate-y-1/2 rounded-full bg-primary"
data-testid={`community-rail-active-${community.id}`}
/>
) : null}
<span
className={cn(
"flex h-9 w-9 items-center justify-center overflow-hidden rounded-2xl text-xs font-semibold transition-all",
isActive
? "rounded-xl bg-primary text-primary-foreground"
? "rounded-xl bg-primary text-primary-foreground ring-2 ring-primary/35 ring-offset-2 ring-offset-sidebar"
: "bg-sidebar-accent/60 text-sidebar-foreground/80 hover:rounded-xl hover:bg-primary/80 hover:text-primary-foreground",
pending && "opacity-60",
)}
Expand Down