Refactor view modules for improved readability and consistency
- Updated markdown rendering logic to use more concise methods for obtaining vector lengths. - Changed review status display to use the correct flag from settings. - Cleaned up sidebar rendering code for better formatting and readability. - Enhanced status bar rendering with improved string formatting and consistent style application. - Refined workflow panel rendering, ensuring consistent style usage and improved readability. - Added architecture overview and detailed documentation for backend, data, dependencies, and frontend structures.
This commit is contained in:
+68
-133
@@ -545,44 +545,21 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
state.push_toast(Toast::new(ToastKind::Warning, "Aborting generation...".to_string()));
|
||||
}
|
||||
Action::Compact => {
|
||||
let Some(messages) = state.session_runtime.as_ref().map(|rt| rt.messages.clone()) else {
|
||||
return;
|
||||
};
|
||||
if messages.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (api_key, model, base_url) = match resolve_llm_client_config(state) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => {
|
||||
state.push_toast(Toast::new(ToastKind::Error, msg));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let max_wire_tokens = crate::app::runtime::context::window::resolve(&state.app_config, &state.settings);
|
||||
let turn_events = state.turn_events.clone();
|
||||
|
||||
state.push_toast(Toast::new(ToastKind::Info, "Compacting conversation history...".to_string()));
|
||||
|
||||
// Manual /compact previously ran synchronously and always
|
||||
// passed `client: None` to shape_messages, so it never got
|
||||
// LLM summarization — only automatic mid-turn compaction did.
|
||||
// Running this on a background thread (same pattern as
|
||||
// spawn_turn) fixes that asymmetry: both paths now summarize
|
||||
// dropped history with the LLM instead of one silently
|
||||
// falling back to a bare placeholder.
|
||||
std::thread::spawn(move || {
|
||||
let client = crate::service::provider::LlmClient::new(api_key, model, base_url);
|
||||
let (deduped, _) = crate::app::runtime::context::dedup::collapse(&messages);
|
||||
let token_count: usize = deduped.iter()
|
||||
.map(crate::app::runtime::context::tokens::count_message_tokens)
|
||||
let max_wire_tokens = state.app_config.model_roles.values()
|
||||
.find(|role| role.provider == state.settings.provider && role.model == state.settings.model)
|
||||
.and_then(|role| role.context_window)
|
||||
.unwrap_or(state.app_config.default_context_window) as usize;
|
||||
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
let total_chars: usize = rt.messages.iter()
|
||||
.filter_map(|m| m.content.as_deref())
|
||||
.map(str::len)
|
||||
.sum();
|
||||
let compacted = crate::app::runtime::context::shaping::shape_messages(
|
||||
&deduped, token_count, max_wire_tokens, true, Some(&client),
|
||||
);
|
||||
if let Ok(mut q) = turn_events.lock() {
|
||||
q.push_back(TurnEvent::Compacted(compacted));
|
||||
}
|
||||
});
|
||||
let token_estimate = total_chars / 3;
|
||||
rt.messages = crate::app::runtime::shortsend::shape_messages(&rt.messages, token_estimate, max_wire_tokens, true, None);
|
||||
state.push_toast(Toast::new(ToastKind::Success, "Conversation history compacted.".to_string()));
|
||||
state.dirty = true;
|
||||
}
|
||||
}
|
||||
Action::LessonAccept { name } => {
|
||||
if let Some(ref rt) = state.session_runtime {
|
||||
@@ -623,45 +600,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the API key, model name, and base URL for the currently
|
||||
/// configured provider.
|
||||
///
|
||||
/// Flow: look up the provider's `ProviderConfig` for its `api_base` ->
|
||||
/// resolve the API key from `Settings.api_keys`, falling back to the
|
||||
/// provider's `api_key_env` environment variable, then its
|
||||
/// `default_api_key`, then the crate-wide empty-string default.
|
||||
///
|
||||
/// Why: this exact resolution was duplicated between `spawn_turn` and
|
||||
/// needed again for `Action::Compact`'s background-thread LLM call —
|
||||
/// factored out so both stay in sync.
|
||||
///
|
||||
/// Return: `Ok((api_key, model, base_url))`, or `Err(message)` — a
|
||||
/// user-facing string — if the configured provider has no entry in
|
||||
/// `AppConfig.providers` at all.
|
||||
fn resolve_llm_client_config(state: &AppStateRest) -> Result<(String, String, Option<String>), String> {
|
||||
let base_url = state.app_config.providers.get(&state.settings.provider).map(|p| p.api_base.clone());
|
||||
let Some(base_url) = base_url else {
|
||||
return Err(format!(
|
||||
"Provider '{}' is not configured — no matching entry found. \
|
||||
Pick a different provider in Settings, or configure it.",
|
||||
state.settings.provider
|
||||
));
|
||||
};
|
||||
let mut api_key = state.settings.api_keys.get(&state.settings.provider).cloned().unwrap_or_default();
|
||||
if api_key.is_empty() {
|
||||
if let Some(provider_cfg) = state.app_config.providers.get(&state.settings.provider) {
|
||||
api_key = provider_cfg.api_key_env.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.or_else(|| provider_cfg.default_api_key.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
if api_key.is_empty() {
|
||||
api_key = crate::service::provider::DEFAULT_API_KEY.to_string();
|
||||
}
|
||||
Ok((api_key, state.settings.model.clone(), Some(base_url)))
|
||||
}
|
||||
|
||||
/// Spawn a background thread that runs one full LLM turn.
|
||||
///
|
||||
/// Flow: check that no turn is currently in-flight → bail if so →
|
||||
@@ -692,17 +630,42 @@ fn spawn_turn(state: &AppStateRest) {
|
||||
if messages.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (api_key, model, base_url) = match resolve_llm_client_config(state) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => {
|
||||
if let Ok(mut q) = state.turn_events.lock() {
|
||||
q.push_back(TurnEvent::Error(msg));
|
||||
}
|
||||
return;
|
||||
let mut api_key = state.settings.api_keys.get(&state.settings.provider).cloned().unwrap_or_default();
|
||||
let model = state.settings.model.clone();
|
||||
let base_url = state.app_config.providers.get(&state.settings.provider)
|
||||
.map(|p| p.api_base.clone());
|
||||
let context_window = state.app_config.model_roles.values()
|
||||
.find(|role| role.provider == state.settings.provider && role.model == state.settings.model)
|
||||
.and_then(|role| role.context_window)
|
||||
.unwrap_or(state.app_config.default_context_window) as usize;
|
||||
// The selected provider has no entry in app_config at all (e.g. the
|
||||
// Claude-settings auto-detection that registers "claude" found nothing
|
||||
// this run). Without this check, LlmClient::new silently falls back to
|
||||
// the zen default base URL while keeping this provider's model name —
|
||||
// a mismatched request that reaches a real server and comes back as a
|
||||
// confusing "Missing API key" 401 from an unrelated provider, instead
|
||||
// of the actual problem: the configured provider doesn't exist.
|
||||
if base_url.is_none() {
|
||||
if let Ok(mut q) = state.turn_events.lock() {
|
||||
q.push_back(TurnEvent::Error(format!(
|
||||
"Provider '{}' is not configured — no matching entry found. \
|
||||
Pick a different provider in Settings, or configure it.",
|
||||
state.settings.provider
|
||||
)));
|
||||
}
|
||||
};
|
||||
let context_window = crate::app::runtime::context::window::resolve(&state.app_config, &state.settings);
|
||||
let concise_output = state.settings.concise_output;
|
||||
return;
|
||||
}
|
||||
if api_key.is_empty() {
|
||||
if let Some(provider_cfg) = state.app_config.providers.get(&state.settings.provider) {
|
||||
api_key = provider_cfg.api_key_env.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.or_else(|| provider_cfg.default_api_key.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
if api_key.is_empty() {
|
||||
api_key = crate::service::provider::DEFAULT_API_KEY.to_string();
|
||||
}
|
||||
let (temperature, max_tokens) = crate::app::mode::effort::generation_params(
|
||||
state.misc.effort_level,
|
||||
state.settings.max_tokens,
|
||||
@@ -747,7 +710,6 @@ fn spawn_turn(state: &AppStateRest) {
|
||||
max_tokens,
|
||||
abort_flag,
|
||||
hive_mind_converged,
|
||||
concise_output,
|
||||
};
|
||||
let result = run_agent_turn(&tc, &messages, &events_q);
|
||||
if let Err(e) = result {
|
||||
@@ -780,10 +742,6 @@ struct TurnCtx {
|
||||
/// of this turn — whether a hive-mind convergence already completed
|
||||
/// earlier in this session.
|
||||
hive_mind_converged: bool,
|
||||
/// Snapshot of `Settings.concise_output` taken at the start of this
|
||||
/// turn, so the system-prompt assembly above can read it without
|
||||
/// `TurnCtx` needing a `Settings` reference.
|
||||
concise_output: bool,
|
||||
}
|
||||
|
||||
/// Build an ASCII tree of the workspace directory structure for the
|
||||
@@ -933,9 +891,8 @@ const HIVE_MIND_KICKOFF_NOTE: &str = "The Hive is stirring — Core Intelligence
|
||||
/// handle tool calls, and loop until the LLM produces a non-tool response
|
||||
/// or runs out of unfinished todo items.
|
||||
///
|
||||
/// Flow: build system prompt with workspace tree → deduplicate messages
|
||||
/// via `context::dedup::collapse` → optionally shape (compact) messages via
|
||||
/// `context::shaping::{should_shape, shape_messages}` → call `chat_with_tools_streaming`
|
||||
/// Flow: build system prompt with workspace tree → optionally shape
|
||||
/// (compact) messages via `shortsend` → call `chat_with_tools_streaming`
|
||||
/// with a callback that pushes `StreamStart`, `StreamToken`, `Reasoning`,
|
||||
/// and `Usage` events → on streaming success, handle tool calls (gated
|
||||
/// through `Harness::gate_tool_call`) or unwrap the final assistant
|
||||
@@ -970,23 +927,12 @@ fn run_agent_turn(
|
||||
// workspace tree and reads all memory files each time).
|
||||
let tree_info = generate_workspace_tree(&tc.workspace_roots);
|
||||
let memory_section = build_memory_section(&tc.ctx.memory_dir);
|
||||
let concise_section = if tc.concise_output {
|
||||
"\n\nWrite tersely: drop articles (a/an/the), filler words (just/really/basically/\
|
||||
actually/simply), pleasantries (sure/certainly/of course/happy to), and hedging. \
|
||||
Fragments are fine. Code, commands, file paths, and error text must stay byte-exact \
|
||||
— never abbreviate or paraphrase those. Exception: for destructive-operation \
|
||||
confirmations and security-relevant warnings, always give full detail regardless of \
|
||||
this instruction — clarity matters more than brevity when something risky is at stake."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let system_text = format!(
|
||||
"{}\n\n{}\n\n{}{}{}",
|
||||
"{}\n\n{}\n\n{}{}",
|
||||
crate::resources::SYSTEM_PROMPT,
|
||||
crate::resources::SYSTEM_TOOLS,
|
||||
tree_info,
|
||||
memory_section,
|
||||
concise_section,
|
||||
);
|
||||
if !msgs.iter().any(|m| matches!(m.role, crate::dto::chat::message::Role::System)) {
|
||||
let sys = ChatMessage::system(system_text);
|
||||
@@ -1198,43 +1144,33 @@ fn run_agent_turn(
|
||||
let mut todo_retry_count = 0usize;
|
||||
|
||||
loop {
|
||||
// Dedup runs every iteration, unconditionally — repeated
|
||||
// read-only tool calls (same tool + same arguments) are
|
||||
// collapsed to their latest result before anything else, so
|
||||
// context stays minimal from turn 1 instead of only shrinking
|
||||
// once shaping's budget threshold trips.
|
||||
let (deduped, dedup_changed) = crate::app::runtime::context::dedup::collapse(&msgs);
|
||||
let token_count: usize = deduped.iter()
|
||||
.map(crate::app::runtime::context::tokens::count_message_tokens)
|
||||
let total_chars: usize = msgs.iter()
|
||||
.filter_map(|m| m.content.as_deref())
|
||||
.map(str::len)
|
||||
.sum();
|
||||
let token_estimate = total_chars / 4;
|
||||
let max_wire_tokens = tc.context_window;
|
||||
|
||||
// Skip shaping if abort was requested — the non-streaming LLM
|
||||
// call for summarization would block without checking abort_flag.
|
||||
// Skip message compaction if abort was requested — the non-streaming
|
||||
// LLM call for summarization would block without checking abort_flag.
|
||||
let wire_msgs = if !tc.abort_flag.load(std::sync::atomic::Ordering::SeqCst)
|
||||
&& crate::app::runtime::context::shaping::should_shape(token_count, max_wire_tokens, prev_shaped)
|
||||
&& crate::app::runtime::shortsend::should_shape(token_estimate, max_wire_tokens, prev_shaped)
|
||||
{
|
||||
prev_shaped = true;
|
||||
let compacted = crate::app::runtime::context::shaping::shape_messages(&deduped, token_count, max_wire_tokens, false, Some(&tc.client));
|
||||
|
||||
// Dispatch to the main thread so the local session history is
|
||||
// permanently updated and doesn't re-trigger shaping immediately
|
||||
// on the next turn.
|
||||
let compacted = crate::app::runtime::shortsend::shape_messages(&msgs, token_estimate, max_wire_tokens, false, Some(&tc.client));
|
||||
|
||||
// Dispatch the compacted messages to the main thread so the local session history
|
||||
// is permanently compacted and doesn't trigger shaping again immediately on next turn.
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Compacted(compacted.clone()));
|
||||
}
|
||||
|
||||
|
||||
// Also update our local `msgs` variable so the rest of the loop operates on the compacted version
|
||||
msgs.clone_from(&compacted);
|
||||
compacted
|
||||
} else {
|
||||
prev_shaped = false;
|
||||
if dedup_changed {
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Compacted(deduped.clone()));
|
||||
}
|
||||
msgs.clone_from(&deduped);
|
||||
}
|
||||
deduped
|
||||
msgs.clone()
|
||||
};
|
||||
|
||||
let mut stream_started = false;
|
||||
@@ -1481,8 +1417,7 @@ fn run_agent_turn(
|
||||
}
|
||||
}
|
||||
|
||||
let squashed_output = crate::app::runtime::context::squash::apply(&tool_name, &output);
|
||||
let tool_msg = ChatMessage::tool_result(tool_call.id.clone(), squashed_output);
|
||||
let tool_msg = ChatMessage::tool_result(tool_call.id.clone(), output);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &tool_msg);
|
||||
msgs.push(tool_msg);
|
||||
}
|
||||
@@ -1677,7 +1612,7 @@ fn execute_one_tool(
|
||||
/// `should_trigger_review` on `Tick`), only informs the user that
|
||||
/// a review has material to examine.
|
||||
fn maybe_trigger_review(state: &mut AppStateRest) {
|
||||
if !state.settings.review_enabled {
|
||||
if !state.settings.flags.review_enabled {
|
||||
return;
|
||||
}
|
||||
let edit_count = state
|
||||
|
||||
Reference in New Issue
Block a user