refactor: complete Rust 2024 let-chain migration
Migrate all remaining collapsible_if patterns to Rust 2024 let-chain syntax across the entire codebase. This modernizes conditional logic by replacing nested if statements with single-level expressions using the && operator with let patterns. Changes: - storage.rs: 2 let-chain conversions (database dir creation, legacy archiving) - session.rs: 3 let-chain conversions (empty content check, ledger dir creation, consent flow) - ollama.rs: 8 let-chain conversions (socket parsing, cloud validation, model caching, capabilities) - main.rs: 2 let-chain conversions (API key validation, provider enablement) - owlen-tui: ~50 let-chain conversions across app/mod.rs, chat_app.rs, ui.rs, highlight.rs, and state modules Test fixes: - prompt_server.rs: Add missing .await on async RemoteMcpClient::new_with_config - presets.rs, prompt_server.rs: Add missing rpc_timeout_secs field to McpServerConfig - file_write.rs: Update error assertion to accept new "escapes workspace boundary" message Verification: - cargo build --all: ✅ succeeds - cargo clippy --all -- -D clippy::collapsible_if: ✅ zero warnings - cargo test --all: ✅ 109+ tests pass Net result: -46 lines of code, improved readability and maintainability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -223,12 +223,11 @@ fn run_config_doctor() -> Result<()> {
|
|||||||
if needs_env_update {
|
if needs_env_update {
|
||||||
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
|
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
|
||||||
}
|
}
|
||||||
if let Some(ref value) = original_api_key_env {
|
if let Some(ref value) = original_api_key_env
|
||||||
if value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
|
&& (value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
|
||||||
|| value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV)
|
|| value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV))
|
||||||
{
|
{
|
||||||
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
|
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if cloud.api_key_env != original_api_key_env {
|
if cloud.api_key_env != original_api_key_env {
|
||||||
changes
|
changes
|
||||||
@@ -338,20 +337,19 @@ fn run_config_doctor() -> Result<()> {
|
|||||||
|
|
||||||
if ensure_default_enabled {
|
if ensure_default_enabled {
|
||||||
let default_id = config.general.default_provider.clone();
|
let default_id = config.general.default_provider.clone();
|
||||||
if let Some(default_cfg) = config.providers.get(&default_id) {
|
if let Some(default_cfg) = config.providers.get(&default_id) && !default_cfg.enabled {
|
||||||
if !default_cfg.enabled {
|
if let Some(new_default) = config
|
||||||
if let Some(new_default) = config
|
.providers
|
||||||
.providers
|
.iter()
|
||||||
.iter()
|
.filter(|(id, cfg)| cfg.enabled && *id != &default_id)
|
||||||
.filter(|(id, cfg)| cfg.enabled && *id != &default_id)
|
.map(|(id, _)| id.clone())
|
||||||
.map(|(id, _)| id.clone())
|
.min()
|
||||||
.min()
|
{
|
||||||
{
|
config.general.default_provider = new_default.clone();
|
||||||
config.general.default_provider = new_default.clone();
|
changes.push(format!(
|
||||||
changes.push(format!(
|
"default provider '{default_id}' was disabled; switched default to '{new_default}'"
|
||||||
"default provider '{default_id}' was disabled; switched default to '{new_default}'"
|
));
|
||||||
));
|
} else {
|
||||||
} else {
|
|
||||||
let entry =
|
let entry =
|
||||||
core_config::ensure_provider_config_mut(&mut config, "ollama_local");
|
core_config::ensure_provider_config_mut(&mut config, "ollama_local");
|
||||||
if !entry.enabled {
|
if !entry.enabled {
|
||||||
@@ -371,7 +369,6 @@ fn run_config_doctor() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match config.mcp.mode {
|
match config.mcp.mode {
|
||||||
McpMode::Legacy => {
|
McpMode::Legacy => {
|
||||||
|
|||||||
@@ -361,10 +361,8 @@ fn probe_default_local_daemon(timeout: Duration) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for target in LOCAL_PROBE_TARGETS {
|
for target in LOCAL_PROBE_TARGETS {
|
||||||
if let Ok(address) = target.parse::<SocketAddr>() {
|
if let Ok(address) = target.parse::<SocketAddr>() && TcpStream::connect_timeout(&address, timeout).is_ok() {
|
||||||
if TcpStream::connect_timeout(&address, timeout).is_ok() {
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -453,13 +451,11 @@ impl OllamaProvider {
|
|||||||
ProviderVariant::Cloud => OllamaMode::Cloud,
|
ProviderVariant::Cloud => OllamaMode::Cloud,
|
||||||
};
|
};
|
||||||
|
|
||||||
if matches!(variant, ProviderVariant::Cloud) {
|
if matches!(variant, ProviderVariant::Cloud) && !api_key_present {
|
||||||
if !api_key_present {
|
return Err(Error::Config(
|
||||||
return Err(Error::Config(
|
"Ollama Cloud API key not configured. Set providers.ollama_cloud.api_key or export OLLAMA_API_KEY (legacy: OLLAMA_CLOUD_API_KEY / OWLEN_OLLAMA_CLOUD_API_KEY)."
|
||||||
"Ollama Cloud API key not configured. Set providers.ollama_cloud.api_key or export OLLAMA_API_KEY (legacy: OLLAMA_CLOUD_API_KEY / OWLEN_OLLAMA_CLOUD_API_KEY)."
|
.into(),
|
||||||
.into(),
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_candidate = match mode {
|
let base_candidate = match mode {
|
||||||
@@ -642,10 +638,8 @@ impl OllamaProvider {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ts) = entry.fetched_at {
|
if let Some(ts) = entry.fetched_at && ts.elapsed() < self.model_cache_ttl {
|
||||||
if ts.elapsed() < self.model_cache_ttl {
|
return Some(entry.models.clone());
|
||||||
return Some(entry.models.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to last good models even if stale; UI will mark as degraded
|
// Fallback to last good models even if stale; UI will mark as degraded
|
||||||
@@ -903,10 +897,8 @@ impl OllamaProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if combined.is_empty() {
|
if combined.is_empty() && let Some(err) = errors.pop() {
|
||||||
if let Some(err) = errors.pop() {
|
return Err(err);
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.annotate_scope_status(&mut combined).await;
|
self.annotate_scope_status(&mut combined).await;
|
||||||
@@ -1312,10 +1304,8 @@ impl OllamaProvider {
|
|||||||
provider_meta.insert("model".into(), Value::String(model));
|
provider_meta.insert("model".into(), Value::String(model));
|
||||||
provider_meta.insert("created_at".into(), Value::String(created_at));
|
provider_meta.insert("created_at".into(), Value::String(created_at));
|
||||||
|
|
||||||
if let Some(ref final_block) = final_data {
|
if let Some(ref final_block) = final_data && let Ok(value) = serde_json::to_value(final_block) {
|
||||||
if let Ok(value) = serde_json::to_value(final_block) {
|
provider_meta.insert("final_data".into(), value);
|
||||||
provider_meta.insert("final_data".into(), value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message
|
message
|
||||||
@@ -1855,14 +1845,12 @@ fn model_supports_tools(
|
|||||||
capabilities: &[String],
|
capabilities: &[String],
|
||||||
detail: Option<&OllamaModelInfo>,
|
detail: Option<&OllamaModelInfo>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(info) = detail {
|
if let Some(info) = detail && info
|
||||||
if info
|
.capabilities
|
||||||
.capabilities
|
.iter()
|
||||||
.iter()
|
.any(|capability| capability_implies_tools(capability))
|
||||||
.any(|capability| capability_implies_tools(capability))
|
{
|
||||||
{
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if capabilities
|
if capabilities
|
||||||
@@ -2113,10 +2101,8 @@ fn normalize_base_url(
|
|||||||
return Err(format!("URL '{candidate}' cannot be used as a base URL"));
|
return Err(format!("URL '{candidate}' cannot be used as a base URL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode_hint == OllamaMode::Cloud && url.scheme() != "https" {
|
if mode_hint == OllamaMode::Cloud && url.scheme() != "https" && std::env::var("OWLEN_ALLOW_INSECURE_CLOUD").is_err() {
|
||||||
if std::env::var("OWLEN_ALLOW_INSECURE_CLOUD").is_err() {
|
return Err("Ollama Cloud requires https:// base URLs".to_string());
|
||||||
return Err("Ollama Cloud requires https:// base URLs".to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = url.path().trim_end_matches('/');
|
let path = url.path().trim_end_matches('/');
|
||||||
@@ -2130,13 +2116,9 @@ fn normalize_base_url(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode_hint == OllamaMode::Cloud {
|
if mode_hint == OllamaMode::Cloud && let Some(host) = url.host_str() && host.eq_ignore_ascii_case("api.ollama.com") {
|
||||||
if let Some(host) = url.host_str() {
|
url.set_host(Some("ollama.com"))
|
||||||
if host.eq_ignore_ascii_case("api.ollama.com") {
|
.map_err(|err| format!("Failed to normalise Ollama Cloud host: {err}"))?;
|
||||||
url.set_host(Some("ollama.com"))
|
|
||||||
.map_err(|err| format!("Failed to normalise Ollama Cloud host: {err}"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url.set_query(None);
|
url.set_query(None);
|
||||||
|
|||||||
@@ -101,10 +101,8 @@ fn build_transcript(messages: &[Message]) -> String {
|
|||||||
Role::Tool => "Tool",
|
Role::Tool => "Tool",
|
||||||
};
|
};
|
||||||
let snippet = sanitize_snippet(&message.content);
|
let snippet = sanitize_snippet(&message.content);
|
||||||
if snippet.is_empty() {
|
if snippet.is_empty() && message.attachments.is_empty() {
|
||||||
if message.attachments.is_empty() {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
transcript.push_str(&format!("{role}: {snippet}\n"));
|
transcript.push_str(&format!("{role}: {snippet}\n"));
|
||||||
if !message.attachments.is_empty() {
|
if !message.attachments.is_empty() {
|
||||||
@@ -1072,15 +1070,13 @@ impl SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn persist_usage_serialized(path: PathBuf, serialized: String) {
|
async fn persist_usage_serialized(path: PathBuf, serialized: String) {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() && let Err(err) = fs::create_dir_all(parent).await {
|
||||||
if let Err(err) = fs::create_dir_all(parent).await {
|
warn!(
|
||||||
warn!(
|
"Failed to create usage ledger directory {}: {}",
|
||||||
"Failed to create usage ledger directory {}: {}",
|
parent.display(),
|
||||||
parent.display(),
|
err
|
||||||
err
|
);
|
||||||
);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = fs::write(&path, serialized).await {
|
if let Err(err) = fs::write(&path, serialized).await {
|
||||||
@@ -2274,30 +2270,28 @@ impl SessionController {
|
|||||||
.pending_tool_requests
|
.pending_tool_requests
|
||||||
.values()
|
.values()
|
||||||
.any(|pending| pending.message_id == message_id)
|
.any(|pending| pending.message_id == message_id)
|
||||||
{
|
&& let Some((tool_name, data_types, endpoints)) =
|
||||||
if let Some((tool_name, data_types, endpoints)) =
|
|
||||||
self.check_tools_consent_needed(&calls).into_iter().next()
|
self.check_tools_consent_needed(&calls).into_iter().next()
|
||||||
{
|
{
|
||||||
let request_id = Uuid::new_v4();
|
let request_id = Uuid::new_v4();
|
||||||
let pending = PendingToolRequest {
|
let pending = PendingToolRequest {
|
||||||
message_id,
|
message_id,
|
||||||
tool_name: tool_name.clone(),
|
tool_name: tool_name.clone(),
|
||||||
data_types: data_types.clone(),
|
data_types: data_types.clone(),
|
||||||
endpoints: endpoints.clone(),
|
endpoints: endpoints.clone(),
|
||||||
tool_calls: calls.clone(),
|
tool_calls: calls.clone(),
|
||||||
};
|
};
|
||||||
self.pending_tool_requests.insert(request_id, pending);
|
self.pending_tool_requests.insert(request_id, pending);
|
||||||
|
|
||||||
if let Some(tx) = &self.event_tx {
|
if let Some(tx) = &self.event_tx {
|
||||||
let _ = tx.send(ControllerEvent::ToolRequested {
|
let _ = tx.send(ControllerEvent::ToolRequested {
|
||||||
request_id,
|
request_id,
|
||||||
message_id,
|
message_id,
|
||||||
tool_name,
|
tool_name,
|
||||||
data_types,
|
data_types,
|
||||||
endpoints,
|
endpoints,
|
||||||
tool_calls: calls.clone(),
|
tool_calls: calls.clone(),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,12 @@ impl StorageManager {
|
|||||||
|
|
||||||
/// Create a storage manager using the provided database path
|
/// Create a storage manager using the provided database path
|
||||||
pub async fn with_database_path(database_path: PathBuf) -> Result<Self> {
|
pub async fn with_database_path(database_path: PathBuf) -> Result<Self> {
|
||||||
if let Some(parent) = database_path.parent() {
|
if let Some(parent) = database_path.parent() && !parent.exists() {
|
||||||
if !parent.exists() {
|
std::fs::create_dir_all(parent).map_err(|e| {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| {
|
Error::Storage(format!(
|
||||||
Error::Storage(format!(
|
"Failed to create database directory {parent:?}: {e}"
|
||||||
"Failed to create database directory {parent:?}: {e}"
|
))
|
||||||
))
|
})?;
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = SqliteConnectOptions::from_str(&format!(
|
let options = SqliteConnectOptions::from_str(&format!(
|
||||||
@@ -434,13 +432,11 @@ impl StorageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if migrated > 0 {
|
if migrated > 0 && let Err(err) = archive_legacy_directory(&legacy_dir) {
|
||||||
if let Err(err) = archive_legacy_directory(&legacy_dir) {
|
println!(
|
||||||
println!(
|
"Warning: migrated sessions but failed to archive legacy directory: {}",
|
||||||
"Warning: migrated sessions but failed to archive legacy directory: {}",
|
err
|
||||||
err
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Migrated {} legacy sessions.", migrated);
|
println!("Migrated {} legacy sessions.", migrated);
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ async fn write_outside_root_is_rejected() {
|
|||||||
// The server returns a Network error with path traversal message
|
// The server returns a Network error with path traversal message
|
||||||
let err_str = format!("{err}");
|
let err_str = format!("{err}");
|
||||||
assert!(
|
assert!(
|
||||||
err_str.contains("path traversal") || err_str.contains("Path traversal"),
|
err_str.contains("path traversal")
|
||||||
|
|| err_str.contains("Path traversal")
|
||||||
|
|| err_str.contains("escapes workspace boundary"),
|
||||||
"Expected path traversal error, got: {}",
|
"Expected path traversal error, got: {}",
|
||||||
err_str
|
err_str
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fn prune_preset_removes_extra_entries() {
|
|||||||
transport: "stdio".into(),
|
transport: "stdio".into(),
|
||||||
env: Default::default(),
|
env: Default::default(),
|
||||||
oauth: None,
|
oauth: None,
|
||||||
|
rpc_timeout_secs: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let report = apply_preset(&mut config, PresetTier::Standard, true).expect("apply preset");
|
let report = apply_preset(&mut config, PresetTier::Standard, true).expect("apply preset");
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ async fn test_render_prompt_via_external_server() -> Result<()> {
|
|||||||
transport: "stdio".into(),
|
transport: "stdio".into(),
|
||||||
env: std::collections::HashMap::new(),
|
env: std::collections::HashMap::new(),
|
||||||
oauth: None,
|
oauth: None,
|
||||||
|
rpc_timeout_secs: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = match RemoteMcpClient::new_with_config(&config) {
|
let client = match RemoteMcpClient::new_with_config(&config).await {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|||||||
@@ -272,10 +272,10 @@ impl App {
|
|||||||
match event::poll(poll_interval) {
|
match event::poll(poll_interval) {
|
||||||
Ok(true) => match event::read() {
|
Ok(true) => match event::read() {
|
||||||
Ok(raw_event) => {
|
Ok(raw_event) => {
|
||||||
if let Some(ui_event) = events::from_crossterm_event(raw_event) {
|
if let Some(ui_event) = events::from_crossterm_event(raw_event)
|
||||||
if sender.send(AppEvent::Ui(ui_event)).is_err() {
|
&& sender.send(AppEvent::Ui(ui_event)).is_err()
|
||||||
break;
|
{
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
@@ -369,10 +369,10 @@ impl FrameRequester {
|
|||||||
.expect("frame sender poisoned")
|
.expect("frame sender poisoned")
|
||||||
.clone()
|
.clone()
|
||||||
};
|
};
|
||||||
if let Some(tx) = sender {
|
if let Some(tx) = sender
|
||||||
if tx.send(AppEvent::RedrawRequested).is_ok() {
|
&& tx.send(AppEvent::RedrawRequested).is_ok()
|
||||||
return;
|
{
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failed to dispatch; clear pending flag so future attempts can retry.
|
// Failed to dispatch; clear pending flag so future attempts can retry.
|
||||||
|
|||||||
@@ -346,60 +346,60 @@ impl LayoutSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn region_at(&self, column: u16, row: u16) -> Option<UiRegion> {
|
fn region_at(&self, column: u16, row: u16) -> Option<UiRegion> {
|
||||||
if let Some(rect) = self.model_info_panel {
|
if let Some(rect) = self.model_info_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::ModelInfo);
|
{
|
||||||
}
|
return Some(UiRegion::ModelInfo);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.header_panel {
|
if let Some(rect) = self.header_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Header);
|
{
|
||||||
}
|
return Some(UiRegion::Header);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.code_panel {
|
if let Some(rect) = self.code_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Code);
|
{
|
||||||
}
|
return Some(UiRegion::Code);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.file_panel {
|
if let Some(rect) = self.file_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::FileTree);
|
{
|
||||||
}
|
return Some(UiRegion::FileTree);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.input_panel {
|
if let Some(rect) = self.input_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Input);
|
{
|
||||||
}
|
return Some(UiRegion::Input);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.system_panel {
|
if let Some(rect) = self.system_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::System);
|
{
|
||||||
}
|
return Some(UiRegion::System);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.status_panel {
|
if let Some(rect) = self.status_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Status);
|
{
|
||||||
}
|
return Some(UiRegion::Status);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.actions_panel {
|
if let Some(rect) = self.actions_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Actions);
|
{
|
||||||
}
|
return Some(UiRegion::Actions);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.attachments_panel {
|
if let Some(rect) = self.attachments_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Attachments);
|
{
|
||||||
}
|
return Some(UiRegion::Attachments);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.thinking_panel {
|
if let Some(rect) = self.thinking_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Thinking);
|
{
|
||||||
}
|
return Some(UiRegion::Thinking);
|
||||||
}
|
}
|
||||||
if let Some(rect) = self.chat_panel {
|
if let Some(rect) = self.chat_panel
|
||||||
if Self::contains(rect, column, row) {
|
&& Self::contains(rect, column, row)
|
||||||
return Some(UiRegion::Chat);
|
{
|
||||||
}
|
return Some(UiRegion::Chat);
|
||||||
}
|
}
|
||||||
if Self::contains(self.content, column, row) {
|
if Self::contains(self.content, column, row) {
|
||||||
Some(UiRegion::Content)
|
Some(UiRegion::Content)
|
||||||
@@ -661,10 +661,10 @@ fn search_candidate(candidate: &str, query: &str) -> Option<((usize, usize), Hig
|
|||||||
return Some(((2, start_byte), HighlightMask::new(mask)));
|
return Some(((2, start_byte), HighlightMask::new(mask)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(subsequence_mask) = subsequence_highlight(&lower_graphemes, &query_graphemes) {
|
if let Some(subsequence_mask) = subsequence_highlight(&lower_graphemes, &query_graphemes)
|
||||||
if subsequence_mask.iter().any(|b| *b) {
|
&& subsequence_mask.iter().any(|b| *b)
|
||||||
return Some(((3, candidate.len()), HighlightMask::new(subsequence_mask)));
|
{
|
||||||
}
|
return Some(((3, candidate.len()), HighlightMask::new(subsequence_mask)));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
@@ -2377,10 +2377,9 @@ impl ChatApp {
|
|||||||
.model_info_panel
|
.model_info_panel
|
||||||
.current_model_name()
|
.current_model_name()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
|
&& let Some(updated) = self.model_details_cache.get(¤t).cloned()
|
||||||
{
|
{
|
||||||
if let Some(updated) = self.model_details_cache.get(¤t).cloned() {
|
self.model_info_panel.set_model_info(updated);
|
||||||
self.model_info_panel.set_model_info(updated);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let total = self.model_details_cache.len();
|
let total = self.model_details_cache.len();
|
||||||
self.status = format!("Cached model details for {} model(s)", total);
|
self.status = format!("Cached model details for {} model(s)", total);
|
||||||
@@ -2604,10 +2603,10 @@ impl ChatApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.guidance_settings = guidance_settings;
|
self.guidance_settings = guidance_settings;
|
||||||
if dirty {
|
if dirty
|
||||||
if let Err(err) = self.with_config(config::save_config) {
|
&& let Err(err) = self.with_config(config::save_config)
|
||||||
eprintln!("Warning: Failed to persist guidance settings: {err}");
|
{
|
||||||
}
|
eprintln!("Warning: Failed to persist guidance settings: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if completed {
|
if completed {
|
||||||
@@ -3366,16 +3365,16 @@ impl ChatApp {
|
|||||||
latest_summary = Some((entry.level, clipped));
|
latest_summary = Some((entry.level, clipped));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.debug_log.is_visible() {
|
if !self.debug_log.is_visible()
|
||||||
if let Some((level, message)) = latest_summary {
|
&& let Some((level, message)) = latest_summary
|
||||||
let level_label = match level {
|
{
|
||||||
Level::Error => "Error",
|
let level_label = match level {
|
||||||
Level::Warn => "Warning",
|
Level::Error => "Error",
|
||||||
_ => "Log",
|
Level::Warn => "Warning",
|
||||||
};
|
_ => "Log",
|
||||||
self.status = format!("{level_label}: {message} (F12 to open debug log)");
|
};
|
||||||
self.error = None;
|
self.status = format!("{level_label}: {message} (F12 to open debug log)");
|
||||||
}
|
self.error = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3553,20 +3552,20 @@ impl ChatApp {
|
|||||||
const CANDIDATES: [&str; 6] =
|
const CANDIDATES: [&str; 6] =
|
||||||
["rendered", "text", "content", "value", "message", "body"];
|
["rendered", "text", "content", "value", "message", "body"];
|
||||||
for key in CANDIDATES {
|
for key in CANDIDATES {
|
||||||
if let Some(Value::String(text)) = map.get(key) {
|
if let Some(Value::String(text)) = map.get(key)
|
||||||
if !text.trim().is_empty() {
|
&& !text.trim().is_empty()
|
||||||
return Some(text.clone());
|
{
|
||||||
}
|
return Some(text.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Value::Array(items)) = map.get("lines") {
|
if let Some(Value::Array(items)) = map.get("lines") {
|
||||||
let mut collected = Vec::new();
|
let mut collected = Vec::new();
|
||||||
for item in items {
|
for item in items {
|
||||||
if let Some(segment) = item.as_str() {
|
if let Some(segment) = item.as_str()
|
||||||
if !segment.trim().is_empty() {
|
&& !segment.trim().is_empty()
|
||||||
collected.push(segment.trim());
|
{
|
||||||
}
|
collected.push(segment.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !collected.is_empty() {
|
if !collected.is_empty() {
|
||||||
@@ -3579,12 +3578,11 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extract_mcp_error(value: &Value) -> Option<String> {
|
fn extract_mcp_error(value: &Value) -> Option<String> {
|
||||||
if let Value::Object(map) = value {
|
if let Value::Object(map) = value
|
||||||
if let Some(Value::String(message)) = map.get("error") {
|
&& let Some(Value::String(message)) = map.get("error")
|
||||||
if !message.trim().is_empty() {
|
&& !message.trim().is_empty()
|
||||||
return Some(message.clone());
|
{
|
||||||
}
|
return Some(message.clone());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -3836,18 +3834,16 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if attachment.is_image() {
|
if attachment.is_image() {
|
||||||
if let Some(data) = attachment.base64_data() {
|
if let Some(data) = attachment.base64_data()
|
||||||
if let Ok(bytes) = BASE64_STANDARD.decode(data) {
|
&& let Ok(bytes) = BASE64_STANDARD.decode(data)
|
||||||
if let Some(lines) = Self::preview_lines_for_image(&bytes) {
|
&& let Some(lines) = Self::preview_lines_for_image(&bytes)
|
||||||
return lines;
|
{
|
||||||
}
|
return lines;
|
||||||
}
|
} else if let Some(path) = attachment.source_path.as_ref()
|
||||||
} else if let Some(path) = attachment.source_path.as_ref() {
|
&& let Ok(bytes) = fs::read(path)
|
||||||
if let Ok(bytes) = fs::read(path) {
|
&& let Some(lines) = Self::preview_lines_for_image(&bytes)
|
||||||
if let Some(lines) = Self::preview_lines_for_image(&bytes) {
|
{
|
||||||
return lines;
|
return lines;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4346,21 +4342,19 @@ impl ChatApp {
|
|||||||
let content_hash =
|
let content_hash =
|
||||||
Self::message_content_hash(&role, &content, &tool_signature, &attachment_signature);
|
Self::message_content_hash(&role, &content, &tool_signature, &attachment_signature);
|
||||||
|
|
||||||
if !is_streaming {
|
if !is_streaming
|
||||||
if let Some(entry) = self.message_line_cache.get(&message_id) {
|
&& let Some(entry) = self.message_line_cache.get(&message_id)
|
||||||
if entry.wrap_width == card_width
|
&& entry.wrap_width == card_width
|
||||||
&& entry.role_label_mode == role_label_mode
|
&& entry.role_label_mode == role_label_mode
|
||||||
&& entry.syntax_highlighting == syntax_highlighting
|
&& entry.syntax_highlighting == syntax_highlighting
|
||||||
&& entry.render_markdown == render_markdown
|
&& entry.render_markdown == render_markdown
|
||||||
&& entry.theme_name == theme.name
|
&& entry.theme_name == theme.name
|
||||||
&& entry.show_timestamps == self.show_message_timestamps
|
&& entry.show_timestamps == self.show_message_timestamps
|
||||||
&& entry.metrics.body_width == body_width
|
&& entry.metrics.body_width == body_width
|
||||||
&& entry.metrics.card_width == card_width
|
&& entry.metrics.card_width == card_width
|
||||||
&& entry.content_hash == content_hash
|
&& entry.content_hash == content_hash
|
||||||
{
|
{
|
||||||
return entry.lines.clone();
|
return entry.lines.clone();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rendered: Vec<Line<'static>> = Vec::new();
|
let mut rendered: Vec<Line<'static>> = Vec::new();
|
||||||
@@ -8412,12 +8406,11 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
||||||
// Move selection forward by word
|
// Move selection forward by word
|
||||||
if let Some((row, col)) = self.visual_end {
|
if let Some((row, col)) = self.visual_end
|
||||||
if let Some(new_col) =
|
&& let Some(new_col) =
|
||||||
self.find_next_word_boundary(row, col)
|
self.find_next_word_boundary(row, col)
|
||||||
{
|
{
|
||||||
self.visual_end = Some((row, new_col));
|
self.visual_end = Some((row, new_col));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FocusedPanel::Files => {}
|
FocusedPanel::Files => {}
|
||||||
@@ -8432,12 +8425,11 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
||||||
// Move selection backward by word
|
// Move selection backward by word
|
||||||
if let Some((row, col)) = self.visual_end {
|
if let Some((row, col)) = self.visual_end
|
||||||
if let Some(new_col) =
|
&& let Some(new_col) =
|
||||||
self.find_prev_word_boundary(row, col)
|
self.find_prev_word_boundary(row, col)
|
||||||
{
|
{
|
||||||
self.visual_end = Some((row, new_col));
|
self.visual_end = Some((row, new_col));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FocusedPanel::Files => {}
|
FocusedPanel::Files => {}
|
||||||
@@ -8466,11 +8458,11 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
FocusedPanel::Chat | FocusedPanel::Thinking => {
|
||||||
// Move selection to end of line
|
// Move selection to end of line
|
||||||
if let Some((row, _)) = self.visual_end {
|
if let Some((row, _)) = self.visual_end
|
||||||
if let Some(line) = self.get_line_at_row(row) {
|
&& let Some(line) = self.get_line_at_row(row)
|
||||||
let line_len = line.chars().count();
|
{
|
||||||
self.visual_end = Some((row, line_len));
|
let line_len = line.chars().count();
|
||||||
}
|
self.visual_end = Some((row, line_len));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FocusedPanel::Files => {}
|
FocusedPanel::Files => {}
|
||||||
@@ -9424,15 +9416,15 @@ impl ChatApp {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.available_providers.is_empty() {
|
if self.available_providers.is_empty()
|
||||||
if let Err(err) = self.refresh_models().await {
|
&& let Err(err) = self.refresh_models().await
|
||||||
self.error = Some(format!(
|
{
|
||||||
"Failed to refresh providers: {}",
|
self.error = Some(format!(
|
||||||
err
|
"Failed to refresh providers: {}",
|
||||||
));
|
err
|
||||||
self.status =
|
));
|
||||||
"Unable to refresh providers".to_string();
|
self.status =
|
||||||
}
|
"Unable to refresh providers".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = provider_query;
|
let filter = provider_query;
|
||||||
@@ -10436,24 +10428,23 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
if let Some(item) = self.current_model_selector_item() {
|
if let Some(item) = self.current_model_selector_item()
|
||||||
if let ModelSelectorItemKind::Header {
|
&& let ModelSelectorItemKind::Header {
|
||||||
provider, expanded, ..
|
provider, expanded, ..
|
||||||
} = item.kind()
|
} = item.kind()
|
||||||
{
|
{
|
||||||
if *expanded {
|
if *expanded {
|
||||||
let provider_name = provider.clone();
|
let provider_name = provider.clone();
|
||||||
self.collapse_provider(&provider_name);
|
self.collapse_provider(&provider_name);
|
||||||
self.status =
|
self.status =
|
||||||
format!("Collapsed provider: {}", provider_name);
|
format!("Collapsed provider: {}", provider_name);
|
||||||
} else {
|
} else {
|
||||||
let provider_name = provider.clone();
|
let provider_name = provider.clone();
|
||||||
self.expand_provider(&provider_name, true);
|
self.expand_provider(&provider_name, true);
|
||||||
self.status =
|
self.status =
|
||||||
format!("Expanded provider: {}", provider_name);
|
format!("Expanded provider: {}", provider_name);
|
||||||
}
|
|
||||||
self.error = None;
|
|
||||||
}
|
}
|
||||||
|
self.error = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
@@ -10506,10 +10497,10 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char(ch) if ch.is_ascii_digit() => {
|
KeyCode::Char(ch) if ch.is_ascii_digit() => {
|
||||||
if let Some(idx) = ch.to_digit(10) {
|
if let Some(idx) = ch.to_digit(10)
|
||||||
if idx >= 1 && (idx as usize) <= HELP_TAB_COUNT {
|
&& idx >= 1 && (idx as usize) <= HELP_TAB_COUNT
|
||||||
self.help_tab_index = (idx - 1) as usize;
|
{
|
||||||
}
|
self.help_tab_index = (idx - 1) as usize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -10728,12 +10719,12 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiRegion::Attachments => {
|
UiRegion::Attachments => {
|
||||||
if let Some(rect) = self.last_layout.attachments_panel {
|
if let Some(rect) = self.last_layout.attachments_panel
|
||||||
if row > rect.y + 1 {
|
&& row > rect.y + 1
|
||||||
let list_index = row.saturating_sub(rect.y + 1) as usize;
|
{
|
||||||
if list_index < self.attachment_preview_entries.len() {
|
let list_index = row.saturating_sub(rect.y + 1) as usize;
|
||||||
self.attachment_preview_selection = list_index;
|
if list_index < self.attachment_preview_entries.len() {
|
||||||
}
|
self.attachment_preview_selection = list_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.shift_attachment_selection(0);
|
self.shift_attachment_selection(0);
|
||||||
@@ -10746,12 +10737,12 @@ impl ChatApp {
|
|||||||
UiRegion::Input => {
|
UiRegion::Input => {
|
||||||
self.focus_panel(FocusedPanel::Input);
|
self.focus_panel(FocusedPanel::Input);
|
||||||
self.set_input_mode(InputMode::Editing);
|
self.set_input_mode(InputMode::Editing);
|
||||||
if let Some(rect) = self.last_layout.input_panel {
|
if let Some(rect) = self.last_layout.input_panel
|
||||||
if let Some((line, column)) = self.input_cursor_from_point(rect, column, row) {
|
&& let Some((line, column)) = self.input_cursor_from_point(rect, column, row)
|
||||||
let line = line.min(u16::MAX as usize) as u16;
|
{
|
||||||
let column = column.min(u16::MAX as usize) as u16;
|
let line = line.min(u16::MAX as usize) as u16;
|
||||||
self.textarea.move_cursor(CursorMove::Jump(line, column));
|
let column = column.min(u16::MAX as usize) as u16;
|
||||||
}
|
self.textarea.move_cursor(CursorMove::Jump(line, column));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiRegion::ModelInfo => {
|
UiRegion::ModelInfo => {
|
||||||
@@ -11992,18 +11983,17 @@ impl ChatApp {
|
|||||||
status_entry.state,
|
status_entry.state,
|
||||||
));
|
));
|
||||||
|
|
||||||
if status_entry.state != ModelAvailabilityState::Available
|
if (status_entry.state != ModelAvailabilityState::Available
|
||||||
|| status_entry.is_stale
|
|| status_entry.is_stale
|
||||||
|| status_entry.message.is_some()
|
|| status_entry.message.is_some())
|
||||||
|
&& let Some(summary) = Self::scope_status_summary(&status_entry)
|
||||||
{
|
{
|
||||||
if let Some(summary) = Self::scope_status_summary(&status_entry) {
|
provider_block.push(ModelSelectorItem::empty(
|
||||||
provider_block.push(ModelSelectorItem::empty(
|
provider.clone(),
|
||||||
provider.clone(),
|
Some(summary),
|
||||||
Some(summary),
|
Some(status_entry.state),
|
||||||
Some(status_entry.state),
|
));
|
||||||
));
|
rendered_body = true;
|
||||||
rendered_body = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let scope_allowed = self.filter_scope_allows_models(&scope, &status_entry);
|
let scope_allowed = self.filter_scope_allows_models(&scope, &status_entry);
|
||||||
@@ -12113,15 +12103,14 @@ impl ChatApp {
|
|||||||
.map(|item| matches!(item.kind(), ModelSelectorItemKind::Model { .. }))
|
.map(|item| matches!(item.kind(), ModelSelectorItemKind::Model { .. }))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !current_is_model {
|
if !current_is_model
|
||||||
if let Some((idx, _)) = self
|
&& let Some((idx, _)) = self
|
||||||
.model_selector_items
|
.model_selector_items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, item)| matches!(item.kind(), ModelSelectorItemKind::Model { .. }))
|
.find(|(_, item)| matches!(item.kind(), ModelSelectorItemKind::Model { .. }))
|
||||||
{
|
{
|
||||||
self.set_selected_model_item(idx);
|
self.set_selected_model_item(idx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12136,14 +12125,14 @@ impl ChatApp {
|
|||||||
let mut best: Option<(usize, usize)> = None;
|
let mut best: Option<(usize, usize)> = None;
|
||||||
|
|
||||||
let mut consider = |candidate: Option<&str>, target: &mut Option<HighlightMask>| {
|
let mut consider = |candidate: Option<&str>, target: &mut Option<HighlightMask>| {
|
||||||
if let Some(text) = candidate {
|
if let Some(text) = candidate
|
||||||
if let Some((score, mask)) = search_candidate(text, query) {
|
&& let Some((score, mask)) = search_candidate(text, query)
|
||||||
let replace = best.is_none_or(|current| score < current);
|
{
|
||||||
if replace {
|
let replace = best.is_none_or(|current| score < current);
|
||||||
best = Some(score);
|
if replace {
|
||||||
}
|
best = Some(score);
|
||||||
*target = Some(mask);
|
|
||||||
}
|
}
|
||||||
|
*target = Some(mask);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12766,14 +12755,14 @@ impl ChatApp {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider.eq_ignore_ascii_case(&self.selected_provider) {
|
if provider.eq_ignore_ascii_case(&self.selected_provider)
|
||||||
if let Err(err) = self.refresh_models().await {
|
&& let Err(err) = self.refresh_models().await
|
||||||
self.error = Some(format!(
|
{
|
||||||
"Provider mode updated but refreshing models failed: {}",
|
self.error = Some(format!(
|
||||||
err
|
"Provider mode updated but refreshing models failed: {}",
|
||||||
));
|
err
|
||||||
return Err(err);
|
));
|
||||||
}
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.error = None;
|
self.error = None;
|
||||||
@@ -12868,34 +12857,29 @@ impl ChatApp {
|
|||||||
.api_key
|
.api_key
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|value| !value.trim().is_empty());
|
.filter(|value| !value.trim().is_empty());
|
||||||
if resolved_api_key.is_none() {
|
if resolved_api_key.is_none()
|
||||||
if let Some(existing) = existing_plain_api_key.as_ref() {
|
&& let Some(existing) = existing_plain_api_key.as_ref()
|
||||||
if !existing.trim().is_empty() {
|
&& !existing.trim().is_empty()
|
||||||
resolved_api_key = Some(existing.clone());
|
{
|
||||||
}
|
resolved_api_key = Some(existing.clone());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resolved_api_key.is_none() && credential_manager.is_some() {
|
if resolved_api_key.is_none() && credential_manager.is_some()
|
||||||
if let Some(manager) = credential_manager.clone() {
|
&& let Some(manager) = credential_manager.clone()
|
||||||
if let Some(credentials) = manager
|
&& let Some(credentials) = manager
|
||||||
.get_credentials(OLLAMA_CLOUD_CREDENTIAL_ID)
|
.get_credentials(OLLAMA_CLOUD_CREDENTIAL_ID)
|
||||||
.await
|
.await
|
||||||
.with_context(|| "Failed to load stored Ollama Cloud credentials")?
|
.with_context(|| "Failed to load stored Ollama Cloud credentials")?
|
||||||
{
|
&& !credentials.api_key.trim().is_empty()
|
||||||
if !credentials.api_key.trim().is_empty() {
|
{
|
||||||
resolved_api_key = Some(credentials.api_key);
|
resolved_api_key = Some(credentials.api_key);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resolved_api_key.is_none() {
|
if resolved_api_key.is_none()
|
||||||
if let Ok(env_key) = std::env::var("OLLAMA_API_KEY") {
|
&& let Ok(env_key) = std::env::var("OLLAMA_API_KEY")
|
||||||
if !env_key.trim().is_empty() {
|
&& !env_key.trim().is_empty()
|
||||||
resolved_api_key = Some(env_key);
|
{
|
||||||
}
|
resolved_api_key = Some(env_key);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resolved_api_key.is_none() {
|
if resolved_api_key.is_none() {
|
||||||
@@ -13091,11 +13075,11 @@ impl ChatApp {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(idx) = self.best_model_match_index(query) {
|
if let Some(idx) = self.best_model_match_index(query)
|
||||||
if let Some(model) = self.models.get(idx).cloned() {
|
&& let Some(model) = self.models.get(idx).cloned()
|
||||||
self.apply_model_selection(model).await?;
|
{
|
||||||
return Ok(());
|
self.apply_model_selection(model).await?;
|
||||||
}
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow!(format!(
|
Err(anyhow!(format!(
|
||||||
@@ -14183,17 +14167,16 @@ impl ChatApp {
|
|||||||
|
|
||||||
let mut summary = segments.next()?.to_string();
|
let mut summary = segments.next()?.to_string();
|
||||||
|
|
||||||
if summary.chars().count() < 120 {
|
if summary.chars().count() < 120
|
||||||
if let Some(next) = segments.next() {
|
&& let Some(next) = segments.next()
|
||||||
if !next.is_empty() {
|
&& !next.is_empty()
|
||||||
if !summary.ends_with('.') && !summary.ends_with('!') && !summary.ends_with('?')
|
{
|
||||||
{
|
if !summary.ends_with('.') && !summary.ends_with('!') && !summary.ends_with('?')
|
||||||
summary.push('.');
|
{
|
||||||
}
|
summary.push('.');
|
||||||
summary.push(' ');
|
|
||||||
summary.push_str(next);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
summary.push(' ');
|
||||||
|
summary.push_str(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
if summary.chars().count() > 160 {
|
if summary.chars().count() > 160 {
|
||||||
|
|||||||
@@ -17,20 +17,22 @@ static THEME: Lazy<Theme> = Lazy::new(|| {
|
|||||||
});
|
});
|
||||||
|
|
||||||
fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference {
|
fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference {
|
||||||
if let Some(path) = path_hint {
|
if let Some(path) = path_hint
|
||||||
if let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path) {
|
&& let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path)
|
||||||
return syntax;
|
{
|
||||||
}
|
return syntax;
|
||||||
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) {
|
}
|
||||||
if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext) {
|
if let Some(path) = path_hint
|
||||||
return syntax;
|
&& let Some(ext) = path.extension().and_then(|ext| ext.to_str())
|
||||||
}
|
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext)
|
||||||
}
|
{
|
||||||
if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
|
return syntax;
|
||||||
if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name) {
|
}
|
||||||
return syntax;
|
if let Some(path) = path_hint
|
||||||
}
|
&& let Some(name) = path.file_name().and_then(|name| name.to_str())
|
||||||
}
|
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name)
|
||||||
|
{
|
||||||
|
return syntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
SYNTAX_SET.find_syntax_plain_text()
|
SYNTAX_SET.find_syntax_plain_text()
|
||||||
|
|||||||
@@ -302,18 +302,17 @@ impl FileTreeState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rel) = diff_paths(path, &self.root) {
|
if let Some(rel) = diff_paths(path, &self.root)
|
||||||
if let Some(index) = self
|
&& let Some(index) = self
|
||||||
.nodes
|
.nodes
|
||||||
.iter()
|
.iter()
|
||||||
.position(|node| node.path == rel || node.path == path)
|
.position(|node| node.path == rel || node.path == path)
|
||||||
|
{
|
||||||
|
self.expand_to(index);
|
||||||
|
if let Some(cursor_pos) = self.visible.iter().position(|entry| entry.index == index)
|
||||||
{
|
{
|
||||||
self.expand_to(index);
|
self.cursor = cursor_pos;
|
||||||
if let Some(cursor_pos) = self.visible.iter().position(|entry| entry.index == index)
|
self.ensure_cursor_in_view();
|
||||||
{
|
|
||||||
self.cursor = cursor_pos;
|
|
||||||
self.ensure_cursor_in_view();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,10 +562,10 @@ fn build_nodes(
|
|||||||
node.is_expanded = node.should_default_expand();
|
node.is_expanded = node.should_default_expand();
|
||||||
|
|
||||||
let index = nodes.len();
|
let index = nodes.len();
|
||||||
if let Some(parent_idx) = parent {
|
if let Some(parent_idx) = parent
|
||||||
if let Some(parent_node) = nodes.get_mut(parent_idx) {
|
&& let Some(parent_node) = nodes.get_mut(parent_idx)
|
||||||
parent_node.children.push(index);
|
{
|
||||||
}
|
parent_node.children.push(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
index_by_path.insert(relative, index);
|
index_by_path.insert(relative, index);
|
||||||
|
|||||||
@@ -289,10 +289,10 @@ impl KeymapState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn expire_if_needed(&mut self, now: Instant) {
|
fn expire_if_needed(&mut self, now: Instant) {
|
||||||
if let Some(deadline) = self.deadline {
|
if let Some(deadline) = self.deadline
|
||||||
if now > deadline {
|
&& now > deadline
|
||||||
self.reset();
|
{
|
||||||
}
|
self.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,11 +719,11 @@ impl KeymapLoader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(path) = path {
|
if let Some(path) = path
|
||||||
if let Ok(text) = fs::read_to_string(&path) {
|
&& let Ok(text) = fs::read_to_string(&path)
|
||||||
self.default_path_content = Some(text);
|
{
|
||||||
self.active = KeymapProfile::Custom;
|
self.default_path_content = Some(text);
|
||||||
}
|
self.active = KeymapProfile::Custom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,14 +182,13 @@ impl RepoSearchState {
|
|||||||
if matches!(
|
if matches!(
|
||||||
self.rows[self.selected_row].kind,
|
self.rows[self.selected_row].kind,
|
||||||
RepoSearchRowKind::FileHeader
|
RepoSearchRowKind::FileHeader
|
||||||
) {
|
)
|
||||||
if let Some(idx) = self
|
&& let Some(idx) = self
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.position(|row| matches!(row.kind, RepoSearchRowKind::Match { .. }))
|
.position(|row| matches!(row.kind, RepoSearchRowKind::Match { .. }))
|
||||||
{
|
{
|
||||||
self.selected_row = idx;
|
self.selected_row = idx;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.ensure_selection_visible();
|
self.ensure_selection_visible();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1675,11 +1675,11 @@ fn collect_unsaved_relative_paths(app: &ChatApp, root: &Path) -> HashSet<PathBuf
|
|||||||
if !pane.is_dirty {
|
if !pane.is_dirty {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(abs) = pane.absolute_path() {
|
if let Some(abs) = pane.absolute_path()
|
||||||
if let Some(rel) = diff_paths(abs, root) {
|
&& let Some(rel) = diff_paths(abs, root)
|
||||||
set.insert(rel);
|
{
|
||||||
continue;
|
set.insert(rel);
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(display) = pane.display_path() {
|
if let Some(display) = pane.display_path() {
|
||||||
let display_path = PathBuf::from(display);
|
let display_path = PathBuf::from(display);
|
||||||
@@ -1694,10 +1694,10 @@ fn collect_unsaved_relative_paths(app: &ChatApp, root: &Path) -> HashSet<PathBuf
|
|||||||
fn build_breadcrumbs(repo_name: &str, path: &Path) -> String {
|
fn build_breadcrumbs(repo_name: &str, path: &Path) -> String {
|
||||||
let mut parts = vec![repo_name.to_string()];
|
let mut parts = vec![repo_name.to_string()];
|
||||||
for component in path.components() {
|
for component in path.components() {
|
||||||
if let Component::Normal(segment) = component {
|
if let Component::Normal(segment) = component
|
||||||
if !segment.is_empty() {
|
&& !segment.is_empty()
|
||||||
parts.push(segment.to_string_lossy().into_owned());
|
{
|
||||||
}
|
parts.push(segment.to_string_lossy().into_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts.join(" > ")
|
parts.join(" > ")
|
||||||
@@ -2243,12 +2243,12 @@ fn compute_cursor_metrics(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cursor_found {
|
if !cursor_found
|
||||||
if let Some(last_segment) = segments.last() {
|
&& let Some(last_segment) = segments.last()
|
||||||
cursor_visual_row = segment_base_row + segments.len().saturating_sub(1);
|
{
|
||||||
cursor_col_width = UnicodeWidthStr::width(last_segment.as_str());
|
cursor_visual_row = segment_base_row + segments.len().saturating_sub(1);
|
||||||
cursor_found = true;
|
cursor_col_width = UnicodeWidthStr::width(last_segment.as_str());
|
||||||
}
|
cursor_found = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2620,10 +2620,9 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
|
|
||||||
// Apply visual selection highlighting if in visual mode and Chat panel is focused
|
// Apply visual selection highlighting if in visual mode and Chat panel is focused
|
||||||
if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Chat)
|
if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Chat)
|
||||||
|
&& let Some(selection) = app.visual_selection()
|
||||||
{
|
{
|
||||||
if let Some(selection) = app.visual_selection() {
|
lines = apply_visual_selection(lines, Some(selection), &theme);
|
||||||
lines = apply_visual_selection(lines, Some(selection), &theme);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update AutoScroll state with accurate content length
|
// Update AutoScroll state with accurate content length
|
||||||
@@ -2780,10 +2779,9 @@ fn render_thinking(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
// Apply visual selection highlighting if in visual mode and Thinking panel is focused
|
// Apply visual selection highlighting if in visual mode and Thinking panel is focused
|
||||||
if matches!(app.mode(), InputMode::Visual)
|
if matches!(app.mode(), InputMode::Visual)
|
||||||
&& matches!(app.focused_panel(), FocusedPanel::Thinking)
|
&& matches!(app.focused_panel(), FocusedPanel::Thinking)
|
||||||
|
&& let Some(selection) = app.visual_selection()
|
||||||
{
|
{
|
||||||
if let Some(selection) = app.visual_selection() {
|
lines = apply_visual_selection(lines, Some(selection), &theme);
|
||||||
lines = apply_visual_selection(lines, Some(selection), &theme);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update AutoScroll state with accurate content length
|
// Update AutoScroll state with accurate content length
|
||||||
@@ -2928,26 +2926,26 @@ fn render_attachment_preview(frame: &mut Frame<'_>, area: Rect, app: &mut ChatAp
|
|||||||
lines.push(Line::from(spans));
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(entry) = entries.get(selected) {
|
if let Some(entry) = entries.get(selected)
|
||||||
if !entry.preview_lines.is_empty() {
|
&& !entry.preview_lines.is_empty()
|
||||||
|
{
|
||||||
|
lines.push(Line::from(vec![Span::styled(
|
||||||
|
"",
|
||||||
|
Style::default()
|
||||||
|
.fg(crate::color_convert::to_ratatui_color(&theme.placeholder))
|
||||||
|
.add_modifier(Modifier::DIM),
|
||||||
|
)]));
|
||||||
|
for preview in entry
|
||||||
|
.preview_lines
|
||||||
|
.iter()
|
||||||
|
.take(INLINE_ATTACHMENT_PREVIEW_LINES)
|
||||||
|
{
|
||||||
lines.push(Line::from(vec![Span::styled(
|
lines.push(Line::from(vec![Span::styled(
|
||||||
"",
|
preview.clone(),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(crate::color_convert::to_ratatui_color(&theme.placeholder))
|
.fg(crate::color_convert::to_ratatui_color(&theme.placeholder))
|
||||||
.add_modifier(Modifier::DIM),
|
.add_modifier(Modifier::DIM),
|
||||||
)]));
|
)]));
|
||||||
for preview in entry
|
|
||||||
.preview_lines
|
|
||||||
.iter()
|
|
||||||
.take(INLINE_ATTACHMENT_PREVIEW_LINES)
|
|
||||||
{
|
|
||||||
lines.push(Line::from(vec![Span::styled(
|
|
||||||
preview.clone(),
|
|
||||||
Style::default()
|
|
||||||
.fg(crate::color_convert::to_ratatui_color(&theme.placeholder))
|
|
||||||
.add_modifier(Modifier::DIM),
|
|
||||||
)]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5662,29 +5660,29 @@ fn binding_variants(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if direct.is_none() {
|
if direct.is_none()
|
||||||
if let Some(desc) = candidates.first() {
|
&& let Some(desc) = candidates.first()
|
||||||
let seq = format_sequence(&desc.sequence);
|
{
|
||||||
if !desc
|
let seq = format_sequence(&desc.sequence);
|
||||||
.sequence
|
if !desc
|
||||||
.first()
|
.sequence
|
||||||
.map(|token| token.eq_ignore_ascii_case(leader))
|
.first()
|
||||||
.unwrap_or(false)
|
.map(|token| token.eq_ignore_ascii_case(leader))
|
||||||
{
|
.unwrap_or(false)
|
||||||
direct = Some(seq.clone());
|
{
|
||||||
}
|
direct = Some(seq.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if leader_binding.is_none() {
|
if leader_binding.is_none()
|
||||||
if let Some(desc) = candidates.iter().find(|candidate| {
|
&& let Some(desc) = candidates.iter().find(|candidate| {
|
||||||
candidate
|
candidate
|
||||||
.sequence
|
.sequence
|
||||||
.first()
|
.first()
|
||||||
.map(|token| token.eq_ignore_ascii_case(leader))
|
.map(|token| token.eq_ignore_ascii_case(leader))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}) {
|
})
|
||||||
leader_binding = Some(format_sequence(&desc.sequence));
|
{
|
||||||
}
|
leader_binding = Some(format_sequence(&desc.sequence));
|
||||||
}
|
}
|
||||||
(direct, leader_binding)
|
(direct, leader_binding)
|
||||||
}
|
}
|
||||||
@@ -6166,21 +6164,21 @@ fn render_theme_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
|
|
||||||
if columns.len() >= 2 {
|
if columns.len() >= 2 {
|
||||||
let preview_area = columns[1];
|
let preview_area = columns[1];
|
||||||
if preview_area.width > 0 && preview_area.height > 0 {
|
if preview_area.width > 0 && preview_area.height > 0
|
||||||
if let Some(selected_name) = themes.get(selected_index) {
|
&& let Some(selected_name) = themes.get(selected_index)
|
||||||
let preview_theme = all_themes
|
{
|
||||||
.get(selected_name.as_str())
|
let preview_theme = all_themes
|
||||||
.cloned()
|
.get(selected_name.as_str())
|
||||||
.or_else(|| all_themes.get(current_theme_name).cloned())
|
.cloned()
|
||||||
.unwrap_or_else(|| theme.clone());
|
.or_else(|| all_themes.get(current_theme_name).cloned())
|
||||||
render_theme_preview(
|
.unwrap_or_else(|| theme.clone());
|
||||||
frame,
|
render_theme_preview(
|
||||||
preview_area,
|
frame,
|
||||||
&preview_theme,
|
preview_area,
|
||||||
preview_theme.name == theme.name,
|
&preview_theme,
|
||||||
app.layer_settings(),
|
preview_theme.name == theme.name,
|
||||||
);
|
app.layer_settings(),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user