Skip to content

Commit e68157b

Browse files
authored
Improve onboarding UX and several bugs & glitches (#115)
1 parent eef590c commit e68157b

32 files changed

Lines changed: 2424 additions & 366 deletions

crates/code_assistant/src/acp/ui.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,9 @@ impl UserInterface for ACPUserUI {
821821
UiEvent::PersistUiState => {
822822
// GPUI-specific, not applicable to ACP UI
823823
}
824+
UiEvent::ConfigChanged => {
825+
// Config file changes not relevant in ACP mode
826+
}
824827
}
825828
Ok(())
826829
}

crates/code_assistant/src/agent/runner.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,10 +1255,10 @@ impl Agent {
12551255
fn read_guidance_files(&self) -> Vec<(String, String)> {
12561256
let mut guidance_files = Vec::new();
12571257

1258-
if let Some(config_dir) = dirs::home_dir().map(|home| home.join(".config/code-assistant")) {
1259-
if let Some((_, content)) = Self::read_guidance_from_dir(&config_dir, &["AGENTS.md"]) {
1260-
guidance_files.push(("~/.config/code-assistant/AGENTS.md".to_string(), content));
1261-
}
1258+
let config_dir = crate::config_dir::config_dir();
1259+
if let Some((_, content)) = Self::read_guidance_from_dir(&config_dir, &["AGENTS.md"]) {
1260+
let label = format!("{}/AGENTS.md", config_dir.display());
1261+
guidance_files.push((label, content));
12621262
}
12631263

12641264
// Determine search root from effective_project_path (worktree or init_path),

crates/code_assistant/src/app/gpui.rs

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,13 @@ pub fn run(config: AgentRunConfig) -> Result<()> {
1818
let (backend_event_rx, backend_response_tx) = gui.setup_backend_communication();
1919

2020
// Setup dynamic types for MultiSessionManager
21-
let root_path = config.path.canonicalize()?;
2221
let persistence = crate::persistence::FileSessionPersistence::new();
2322

23+
// In GPUI mode, don't use the current directory as default session path.
24+
// Sessions are project-based and get their path from the sidebar/projects.json.
2425
let session_config_template = SessionConfig {
25-
init_path: Some(root_path.clone()),
26-
initial_project: root_path
27-
.file_name()
28-
.and_then(|name| name.to_str())
29-
.unwrap_or("unknown")
30-
.to_string(),
26+
init_path: None,
27+
initial_project: String::new(),
3128
tool_syntax: config.tool_syntax,
3229
use_diff_blocks: config.use_diff_format,
3330
sandbox_policy: config.sandbox_policy.clone(),
@@ -156,41 +153,10 @@ pub fn run(config: AgentRunConfig) -> Result<()> {
156153
}
157154
}
158155
} else {
159-
info!("No existing sessions found - creating a new session automatically");
160-
161-
// Create a new session automatically
162-
let new_session_id = {
163-
let mut manager = multi_session_manager.lock().await;
164-
manager
165-
.create_session_with_config(None, None, Some(base_model_config.clone()))
166-
.unwrap_or_else(|e| {
167-
error!("Failed to create new session: {}", e);
168-
// Return a fallback session ID if creation fails
169-
"fallback".to_string()
170-
})
171-
};
172-
173-
if new_session_id != "fallback" {
174-
debug!("Created new session: {}", new_session_id);
175-
176-
// Connect to the newly created session
177-
let ui_events = {
178-
let mut manager = multi_session_manager.lock().await;
179-
manager
180-
.set_active_session(new_session_id.clone())
181-
.await
182-
.unwrap_or_else(|e| {
183-
error!("Failed to set active session: {}", e);
184-
Vec::new()
185-
})
186-
};
187-
188-
for event in ui_events {
189-
if let Err(e) = gui_for_thread.send_event(event).await {
190-
error!("Failed to send UI event: {}", e);
191-
}
192-
}
193-
}
156+
info!("No existing sessions found - showing empty state (no session view)");
157+
// In GPUI mode, don't auto-create a session. The user can
158+
// create one from the sidebar. The MessagesView will render
159+
// the "no session" hint since no session is connected.
194160
}
195161
}
196162

crates/code_assistant/src/cli.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub struct Args {
7171
#[command(subcommand)]
7272
pub mode: Option<Mode>,
7373

74+
/// Override configuration directory (where models.json, providers.json, etc. live)
75+
#[arg(long)]
76+
pub config_dir: Option<PathBuf>,
77+
7478
/// Path to the code directory to analyze
7579
#[arg(long, default_value = ".")]
7680
pub path: PathBuf,
@@ -138,6 +142,11 @@ impl Args {
138142
}
139143

140144
/// Resolve a model name, ensuring it exists in the configuration and providing a fallback.
145+
///
146+
/// Resolution order:
147+
/// 1. Explicit `--model` CLI argument
148+
/// 2. Persisted `default_model` from ui-settings.json
149+
/// 3. First model alphabetically from models.json
141150
pub fn resolve_model_name(model: Option<String>) -> anyhow::Result<String> {
142151
let config = ConfigurationSystem::load()?;
143152

@@ -170,14 +179,15 @@ impl Args {
170179
);
171180
}
172181

173-
// Look for common model names as defaults
174-
let preferred_defaults = ["Claude Sonnet 4.5", "GPT-5", "Claude Opus 4", "GPT-4.1"];
175-
for default in &preferred_defaults {
176-
if models.iter().any(|entry| entry == default) {
177-
return Ok(default.to_string());
182+
// Check persisted default model from settings
183+
let settings = crate::ui::gpui::settings::UiSettings::load();
184+
if let Some(ref default_model) = settings.default_model {
185+
if config.get_model(default_model).is_some() {
186+
return Ok(default_model.clone());
178187
}
179188
}
180189

190+
// Fallback: first model alphabetically
181191
models.sort();
182192
Ok(models
183193
.first()

crates/code_assistant/src/config.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ use std::sync::Arc;
88

99
/// Get the path to the configuration file
1010
pub fn get_config_path() -> Result<PathBuf> {
11-
let home =
12-
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
13-
let config_dir = home.join(".config").join("code-assistant");
11+
let config_dir = crate::config_dir::config_dir();
1412
std::fs::create_dir_all(&config_dir)?; // Ensure directory exists
1513
Ok(config_dir.join("projects.json"))
1614
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Central configuration directory resolution.
2+
//!
3+
//! All config files (models.json, providers.json, tools.json, projects.json,
4+
//! ui-settings.json) live in a single directory determined by the following
5+
//! priority:
6+
//!
7+
//! 1. `--config-dir` CLI argument (sets `CODE_ASSISTANT_CONFIG_DIR` env var)
8+
//! 2. `CODE_ASSISTANT_CONFIG_DIR` environment variable
9+
//! 3. `$XDG_CONFIG_HOME/code-assistant`
10+
//! 4. `~/.config/code-assistant`
11+
12+
use std::path::PathBuf;
13+
14+
/// Returns the canonical configuration directory.
15+
///
16+
/// This is the single source of truth for where config files live.
17+
pub fn config_dir() -> PathBuf {
18+
if let Ok(custom_dir) = std::env::var("CODE_ASSISTANT_CONFIG_DIR") {
19+
return PathBuf::from(custom_dir);
20+
}
21+
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
22+
return PathBuf::from(xdg_config).join("code-assistant");
23+
}
24+
if let Some(home_dir) = dirs::home_dir() {
25+
return home_dir.join(".config").join("code-assistant");
26+
}
27+
// Last resort fallback
28+
PathBuf::from("code-assistant")
29+
}
30+
31+
/// Apply the `--config-dir` override by setting the environment variable.
32+
///
33+
/// Must be called early in main, before any config loading happens.
34+
/// The env var is picked up by all config resolution code (including the `llm` crate).
35+
pub fn apply_override(path: &PathBuf) {
36+
std::env::set_var("CODE_ASSISTANT_CONFIG_DIR", path);
37+
}

crates/code_assistant/src/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod app;
44
mod cli;
55
mod codex_commands;
66
mod config;
7+
mod config_dir;
78
mod logging;
89
mod mcp;
910
mod permissions;
@@ -25,6 +26,11 @@ use anyhow::Result;
2526
async fn main() -> Result<()> {
2627
let args = Args::parse();
2728

29+
// Apply config-dir override before anything loads config files
30+
if let Some(ref dir) = args.config_dir {
31+
config_dir::apply_override(dir);
32+
}
33+
2834
// Handle list commands first
2935
if args.handle_list_commands()? {
3036
return Ok(());
@@ -87,7 +93,13 @@ async fn main() -> Result<()> {
8793
anyhow::bail!("Path '{}' is not a directory", args.path.display());
8894
}
8995

90-
let model_name = args.get_model_name()?;
96+
// In GUI mode, allow starting without a valid model config
97+
// (the settings screen will guide the user through setup).
98+
let model_name = if args.ui {
99+
args.get_model_name().unwrap_or_default()
100+
} else {
101+
args.get_model_name()?
102+
};
91103
let sandbox_policy = args.sandbox_policy();
92104

93105
let config = app::AgentRunConfig {

crates/code_assistant/src/session/manager.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ impl SessionManager {
9595
SessionModelConfig::new(self.default_model_name.clone())
9696
}
9797

98+
/// Update the default model name used for newly created sessions.
99+
pub fn set_default_model_name(&mut self, model_name: String) {
100+
self.default_model_name = model_name;
101+
}
102+
98103
/// Create a new session with optional model config and return its ID
99104
pub fn create_session_with_config(
100105
&mut self,

crates/code_assistant/src/session/watcher.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const DEBOUNCE_DURATION: Duration = Duration::from_millis(300);
4949
/// Dropping this stops the watcher.
5050
pub struct SessionWatcher {
5151
_watcher: RecommendedWatcher,
52+
_config_watcher: Option<RecommendedWatcher>,
5253
}
5354

5455
/// Categorised dirty-file set, accumulated between debounce flushes.
@@ -97,11 +98,82 @@ impl SessionWatcher {
9798
rt.spawn(flush_loop(
9899
dirty,
99100
sessions_dir,
100-
event_tx,
101+
event_tx.clone(),
101102
current_session_id,
102103
));
103104

104-
Ok(Self { _watcher: watcher })
105+
// --- config file watcher ---
106+
let config_watcher = Self::start_config_watcher(event_tx, &rt);
107+
108+
Ok(Self {
109+
_watcher: watcher,
110+
_config_watcher: config_watcher,
111+
})
112+
}
113+
114+
/// Start a separate watcher for the config directory (providers.json, models.json).
115+
fn start_config_watcher(
116+
event_tx: async_channel::Sender<UiEvent>,
117+
rt: &tokio::runtime::Handle,
118+
) -> Option<RecommendedWatcher> {
119+
let config_path = llm::provider_config::ConfigurationSystem::providers_config_path();
120+
let config_dir = config_path.parent()?.to_path_buf();
121+
122+
if !config_dir.exists() {
123+
debug!("Config directory does not exist yet, skipping config watcher");
124+
return None;
125+
}
126+
127+
debug!("Starting config file watcher on {}", config_dir.display());
128+
129+
let config_dirty = Arc::new(Mutex::new(false));
130+
let config_dirty_for_callback = config_dirty.clone();
131+
132+
let mut watcher =
133+
match notify::recommended_watcher(move |res: Result<Event, notify::Error>| match res {
134+
Ok(event) => {
135+
for path in &event.paths {
136+
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
137+
if name == "providers.json" || name == "models.json" {
138+
trace!("Config watcher: {name} changed ({:?})", event.kind);
139+
*config_dirty_for_callback.lock().unwrap() = true;
140+
}
141+
}
142+
}
143+
}
144+
Err(e) => warn!("Config watcher error: {e}"),
145+
}) {
146+
Ok(w) => w,
147+
Err(e) => {
148+
warn!("Failed to create config watcher: {e}");
149+
return None;
150+
}
151+
};
152+
153+
if let Err(e) = watcher.watch(&config_dir, RecursiveMode::NonRecursive) {
154+
warn!("Failed to watch config directory: {e}");
155+
return None;
156+
}
157+
158+
// Debounce flush for config changes
159+
let event_tx = event_tx.clone();
160+
rt.spawn(async move {
161+
loop {
162+
tokio::time::sleep(DEBOUNCE_DURATION).await;
163+
let dirty = {
164+
let mut flag = config_dirty.lock().unwrap();
165+
let was_dirty = *flag;
166+
*flag = false;
167+
was_dirty
168+
};
169+
if dirty {
170+
debug!("Config watcher flush: emitting ConfigChanged");
171+
let _ = event_tx.try_send(UiEvent::ConfigChanged);
172+
}
173+
}
174+
});
175+
176+
Some(watcher)
105177
}
106178
}
107179

crates/code_assistant/src/tools/core/config.rs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,7 @@ impl ToolsConfig {
4242

4343
/// Get the path to the tools configuration file.
4444
pub fn config_path() -> Result<PathBuf> {
45-
// Use the same config directory as other code-assistant configs
46-
let config_dir = Self::config_directory()?;
47-
Ok(config_dir.join("tools.json"))
48-
}
49-
50-
/// Get the configuration directory.
51-
fn config_directory() -> Result<PathBuf> {
52-
// Check for custom config directory first
53-
if let Ok(custom_dir) = std::env::var("CODE_ASSISTANT_CONFIG_DIR") {
54-
return Ok(PathBuf::from(custom_dir));
55-
}
56-
57-
// Check XDG_CONFIG_HOME
58-
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
59-
return Ok(PathBuf::from(xdg_config).join("code-assistant"));
60-
}
61-
62-
// Fall back to ~/.config/code-assistant
63-
let home = dirs::home_dir()
64-
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
65-
Ok(home.join(".config").join("code-assistant"))
45+
Ok(crate::config_dir::config_dir().join("tools.json"))
6646
}
6747

6848
/// Substitute environment variables in configuration values.

0 commit comments

Comments
 (0)