Merge pull request #2306 from idling11/feat/goal-to-hunt

Feat/Rename /goal → /hunt with HuntVerdict + trophy cards (#2092)
This commit is contained in:
Hunter Bown
2026-05-30 23:28:22 -07:00
committed by GitHub
7 changed files with 306 additions and 171 deletions
+236 -129
View File
@@ -1,61 +1,51 @@
//! /goal command — set a session objective with token budget and progress tracking.
//! /hunt command — declare a quarry with token budget and verdict tracking (#2092).
use crate::tui::app::{App, AppAction};
use std::io::Write;
use crate::tui::app::{App, AppAction, HuntVerdict};
use super::CommandResult;
/// Set or show the current goal
pub fn goal(app: &mut App, arg: Option<&str>) -> CommandResult {
/// Declare, show, or close a hunt
pub fn hunt(app: &mut App, arg: Option<&str>) -> CommandResult {
match arg {
Some("clear") | Some("reset") => {
app.goal.goal_objective = None;
app.goal.goal_token_budget = None;
app.goal.goal_started_at = None;
app.goal.goal_completed = false;
CommandResult::message("Goal cleared.")
}
Some("done") | Some("complete") => {
app.goal.goal_completed = true;
let elapsed = app
.goal
.goal_started_at
.map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
.unwrap_or_else(|| "unknown".to_string());
CommandResult::message(format!("Goal marked complete! Elapsed: {elapsed}"))
app.hunt.quarry = None;
app.hunt.token_budget = None;
app.hunt.started_at = None;
app.hunt.verdict = HuntVerdict::default();
CommandResult::message("Hunt cleared.")
}
Some("done") | Some("complete") | Some("hunted") => close_hunt(app, HuntVerdict::Hunted),
Some("wound") | Some("wounded") => close_hunt(app, HuntVerdict::Wounded),
Some("escape") | Some("escaped") => close_hunt(app, HuntVerdict::Escaped),
Some(text) if !text.is_empty() => {
// Parse optional budget: "/goal Implement login | budget: 50000"
let (objective, budget) = parse_goal_budget(text);
let objective = objective.trim().to_string();
let (objective, budget) = parse_hunt_budget(text);
if objective.is_empty() || objective.chars().all(|c| c == '|') {
return CommandResult::error("Usage: /goal <objective> [budget: N]");
return CommandResult::error("Usage: /hunt <quarry> [budget: N]");
}
app.goal.goal_objective = Some(objective.clone());
app.goal.goal_token_budget = budget;
app.goal.goal_started_at = Some(std::time::Instant::now());
app.goal.goal_completed = false;
app.hunt.quarry = Some(objective.clone());
app.hunt.token_budget = budget;
app.hunt.started_at = Some(std::time::Instant::now());
app.hunt.verdict = HuntVerdict::Hunting;
let budget_str = budget
.map(|b| format!(" (budget: {b} tokens)"))
.unwrap_or_default();
CommandResult::with_message_and_action(
format!("Goal set: \"{objective}\"{budget_str} — tracking progress."),
format!("Hunt set: \"{objective}\"{budget_str} — tracking progress."),
AppAction::SendMessage(objective),
)
}
_ => {
// Show current goal
if let Some(ref obj) = app.goal.goal_objective {
// #447: render long elapsed times as `2d 3h` rather
// than Rust's default Debug `Duration` (which produces
// `188415.234s` or similar for multi-day goals).
if let Some(ref obj) = app.hunt.quarry {
let elapsed = app
.goal
.goal_started_at
.hunt
.started_at
.map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
.unwrap_or_else(|| "unknown".to_string());
let budget_str = app
.goal
.goal_token_budget
.hunt
.token_budget
.map(|b| {
let used = app.session.total_conversation_tokens;
let pct = if b > 0 {
@@ -66,26 +56,61 @@ pub fn goal(app: &mut App, arg: Option<&str>) -> CommandResult {
format!(" | tokens: {used}/{b} ({pct:.0}%)")
})
.unwrap_or_default();
let status = if app.goal.goal_completed {
" [COMPLETED]"
} else {
""
let verdict_label = match app.hunt.verdict {
HuntVerdict::Hunting => "[HUNTING]",
HuntVerdict::Hunted => "[HUNTED]",
HuntVerdict::Wounded => "[WOUNDED]",
HuntVerdict::Escaped => "[ESCAPED]",
};
CommandResult::message(format!(
"Goal{status}: \"{obj}\" — elapsed: {elapsed}{budget_str}"
"Hunt {verdict_label}: \"{obj}\" — elapsed: {elapsed}{budget_str}"
))
} else {
CommandResult::message(
"No goal set. Use /goal <objective> [budget: N] to set one.\n\
/goal clear — remove the current goal.",
"No hunt set. Use /hunt <quarry> [budget: N] to declare one.\n\
/hunt hunted — mark complete\n\
/hunt wounded — mark interrupted (resumable)\n\
/hunt escaped — mark abandoned\n\
/hunt clear — remove the current hunt.",
)
}
}
}
}
/// Parse optional token budget from goal text: "Implement login | budget: 50000"
fn parse_goal_budget(text: &str) -> (String, Option<u32>) {
fn close_hunt(app: &mut App, verdict: HuntVerdict) -> CommandResult {
if app.hunt.quarry.as_deref().is_none_or(str::is_empty) {
return CommandResult::error("No hunt set. Use /hunt <quarry> [budget: N] first.");
}
let prev = app.hunt.verdict;
let should_write_trophy = prev != verdict || !matches!(verdict, HuntVerdict::Hunted);
if should_write_trophy {
if let Err(err) = write_trophy_card(app, verdict) {
return CommandResult::error(err);
}
}
app.hunt.verdict = verdict;
match verdict {
HuntVerdict::Hunted => {
let elapsed = app
.hunt
.started_at
.map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
.unwrap_or_else(|| "unknown".to_string());
CommandResult::message(format!("Hunt complete! Elapsed: {elapsed}"))
}
HuntVerdict::Wounded => {
CommandResult::message("Hunt wounded — progress saved, can be resumed.")
}
HuntVerdict::Escaped => CommandResult::message("Hunt escaped — quarry abandoned."),
HuntVerdict::Hunting => CommandResult::message("Hunt resumed."),
}
}
/// Parse text like "Implement login | budget: 50000" into (objective, budget).
fn parse_hunt_budget(text: &str) -> (String, Option<u32>) {
if let Some((obj, rest)) = text.split_once(" | budget:") {
let budget = rest
.split_whitespace()
@@ -103,18 +128,114 @@ fn parse_goal_budget(text: &str) -> (String, Option<u32>) {
}
}
/// Write a trophy card to `~/.codewhale/trophies/<date>-<time>-<slug>.md`
/// for the current hunt verdict (#2092).
fn write_trophy_card(app: &App, verdict: HuntVerdict) -> Result<std::path::PathBuf, String> {
let quarry = app
.hunt
.quarry
.as_deref()
.ok_or_else(|| "No hunt set. Use /hunt <quarry> [budget: N] first.".to_string())?;
// Collapse consecutive non-alphanumeric chars into a single '-'
let mut slug = String::new();
let mut last_dash = false;
for c in quarry.chars() {
if c.is_alphanumeric() {
slug.push(c.to_ascii_lowercase());
last_dash = false;
} else if !last_dash {
slug.push('-');
last_dash = true;
}
}
let slug = slug.trim_matches('-');
if slug.is_empty() {
return Err(
"Cannot write trophy card: hunt quarry has no filename-safe characters.".into(),
);
}
let now = chrono::Local::now();
let time = now.format("%H%M%S");
let date = now.format("%Y-%m-%d");
let date_str = date.to_string();
let now_str = now.to_string();
let dir = codewhale_config::resolve_state_dir("trophies")
.map_err(|err| format!("Could not resolve trophy directory: {err}"))?;
std::fs::create_dir_all(&dir)
.map_err(|err| format!("Could not create trophy directory {}: {err}", dir.display()))?;
// Include time in filename to avoid collisions on same-date hunts.
let filename = format!("{date}-{time}-{slug}.md");
let path = dir.join(&filename);
let elapsed = app
.hunt
.started_at
.as_ref()
.map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
.unwrap_or_else(|| "unknown".to_string());
let verdict_str = match verdict {
HuntVerdict::Hunting => "hunting",
HuntVerdict::Hunted => "hunted",
HuntVerdict::Wounded => "wounded",
HuntVerdict::Escaped => "escaped",
};
let tokens = app.session.total_conversation_tokens;
let budget_str = app
.hunt
.token_budget
.map(|b| format!("{b}"))
.unwrap_or_else(|| "".to_string());
let mut f = std::fs::File::create(&path)
.map_err(|err| format!("Could not create trophy card {}: {err}", path.display()))?;
write_trophy_card_contents(
&mut f,
TrophyCard {
quarry,
verdict: verdict_str,
date: &date_str,
elapsed: &elapsed,
tokens,
budget: &budget_str,
now: &now_str,
},
)
.map_err(|err| format!("Could not write trophy card {}: {err}", path.display()))?;
Ok(path)
}
struct TrophyCard<'a> {
quarry: &'a str,
verdict: &'a str,
date: &'a str,
elapsed: &'a str,
tokens: u32,
budget: &'a str,
now: &'a str,
}
fn write_trophy_card_contents(mut f: impl Write, card: TrophyCard<'_>) -> std::io::Result<()> {
writeln!(f, "# Trophy: {}", card.quarry)?;
writeln!(f)?;
writeln!(f, "- **Verdict**: {}", card.verdict)?;
writeln!(f, "- **Date**: {}", card.date)?;
writeln!(f, "- **Elapsed**: {}", card.elapsed)?;
writeln!(f, "- **Tokens used**: {}", card.tokens)?;
writeln!(f, "- **Token budget**: {}", card.budget)?;
writeln!(f)?;
writeln!(f, "_Generated by CodeWhale `/hunt` — {}_", card.now)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::tui::app::AppAction;
use crate::tui::app::{App, TuiOptions};
use std::path::PathBuf;
fn create_test_app() -> App {
let options = TuiOptions {
model: "deepseek-v4-flash".to_string(),
workspace: PathBuf::from("."),
let options = crate::tui::app::TuiOptions {
model: "deepseek-v4-pro".to_string(),
workspace: std::path::PathBuf::from("/tmp/test-workspace"),
config_path: None,
config_profile: None,
allow_shell: false,
@@ -122,29 +243,27 @@ mod tests {
use_mouse_capture: false,
use_bracketed_paste: true,
max_subagents: 1,
skills_dir: PathBuf::from("."),
memory_path: PathBuf::from("memory.md"),
notes_path: PathBuf::from("notes.txt"),
mcp_config_path: PathBuf::from("mcp.json"),
skills_dir: std::path::PathBuf::from("/tmp/test-skills"),
memory_path: std::path::PathBuf::from("memory.md"),
notes_path: std::path::PathBuf::from("notes.txt"),
mcp_config_path: std::path::PathBuf::from("mcp.json"),
use_memory: false,
start_in_agent_mode: true,
start_in_agent_mode: false,
skip_onboarding: true,
yolo: false,
resume_session_id: None,
initial_input: None,
resume_session_id: None,
yolo: false,
};
App::new(options, &Config::default())
let config = crate::config::Config::default();
App::new(options, &config)
}
#[test]
fn test_set_goal() {
fn test_set_hunt() {
let mut app = create_test_app();
let result = goal(&mut app, Some("Fix the login bug"));
assert!(result.message.unwrap().contains("Goal set"));
assert_eq!(
app.goal.goal_objective.as_deref(),
Some("Fix the login bug")
);
let result = hunt(&mut app, Some("Fix the login bug"));
assert!(result.message.unwrap().contains("Hunt set"));
assert_eq!(app.hunt.quarry.as_deref(), Some("Fix the login bug"));
assert!(matches!(
result.action,
Some(AppAction::SendMessage(msg)) if msg == "Fix the login bug"
@@ -152,105 +271,93 @@ mod tests {
}
#[test]
fn test_execute_goal_dispatched_as_sendmessage() {
fn test_hunt_without_argument_shows_state() {
let mut app = create_test_app();
let result = crate::commands::execute("/goal Implement login flow", &mut app);
assert!(
result
.message
.is_some_and(|message| message.contains("Goal set"))
);
assert!(matches!(
result.action,
Some(AppAction::SendMessage(content))
if content == *"Implement login flow"
));
}
#[test]
fn test_execute_goal_without_argument_shows_state() {
let mut app = create_test_app();
let result = crate::commands::execute("/goal", &mut app);
let result = hunt(&mut app, None);
assert!(result.action.is_none());
assert!(matches!(result.message.as_deref(), Some(value) if value.contains("No goal set")));
assert!(result.message.as_deref().unwrap().contains("No hunt set"));
}
#[test]
fn test_set_goal_with_budget() {
fn test_set_hunt_with_budget() {
let mut app = create_test_app();
let _ = goal(&mut app, Some("Refactor auth | budget: 50000"));
assert_eq!(app.goal.goal_objective.as_deref(), Some("Refactor auth"));
assert_eq!(app.goal.goal_token_budget, Some(50_000));
assert!(app.goal.goal_started_at.is_some());
let _ = hunt(&mut app, Some("Refactor auth | budget: 50000"));
assert_eq!(app.hunt.quarry.as_deref(), Some("Refactor auth"));
assert_eq!(app.hunt.token_budget, Some(50_000));
assert!(app.hunt.started_at.is_some());
}
#[test]
fn test_set_goal_rejects_budget_only_objective() {
fn test_set_hunt_rejects_budget_only_objective() {
let mut app = create_test_app();
app.goal.goal_objective = Some("existing objective".to_string());
app.goal.goal_token_budget = Some(10_000);
app.hunt.quarry = Some("existing objective".to_string());
app.hunt.token_budget = Some(10_000);
let result = crate::commands::execute("/goal budget: 50000", &mut app);
let result = hunt(&mut app, Some("budget: 50000"));
assert!(result.is_error);
assert!(result.action.is_none());
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains("Usage: /goal")
.contains("Usage: /hunt")
);
assert_eq!(
app.goal.goal_objective.as_deref(),
Some("existing objective")
);
assert_eq!(app.goal.goal_token_budget, Some(10_000));
let pipe_result = crate::commands::execute("/goal | budget: 50000", &mut app);
assert!(pipe_result.is_error);
assert!(pipe_result.action.is_none());
assert!(
pipe_result
.message
.as_deref()
.unwrap_or_default()
.contains("Usage: /goal")
);
assert_eq!(
app.goal.goal_objective.as_deref(),
Some("existing objective")
);
assert_eq!(app.goal.goal_token_budget, Some(10_000));
assert_eq!(app.hunt.quarry.as_deref(), Some("existing objective"));
assert_eq!(app.hunt.token_budget, Some(10_000));
}
#[test]
fn test_clear_goal() {
fn test_clear_hunt() {
let mut app = create_test_app();
app.goal.goal_objective = Some("test".to_string());
let _ = goal(&mut app, Some("clear"));
assert!(app.goal.goal_objective.is_none());
assert!(app.goal.goal_token_budget.is_none());
app.hunt.quarry = Some("test".to_string());
let _ = hunt(&mut app, Some("clear"));
assert!(app.hunt.quarry.is_none());
assert!(app.hunt.token_budget.is_none());
}
#[test]
fn test_show_goal_when_none() {
fn test_verdict_requires_existing_hunt() {
let mut app = create_test_app();
let result = goal(&mut app, None);
assert!(result.message.unwrap().contains("No goal set"));
let result = hunt(&mut app, Some("wounded"));
assert!(result.is_error);
assert_eq!(app.hunt.verdict, HuntVerdict::Hunting);
assert!(app.hunt.quarry.is_none());
}
#[test]
fn test_failed_trophy_write_does_not_mutate_verdict() {
let mut app = create_test_app();
app.hunt.quarry = Some("!!!".to_string());
app.hunt.verdict = HuntVerdict::Hunting;
let result = hunt(&mut app, Some("escaped"));
assert!(result.is_error);
assert_eq!(app.hunt.verdict, HuntVerdict::Hunting);
assert_eq!(app.hunt.quarry.as_deref(), Some("!!!"));
}
#[test]
fn test_show_hunt_when_none() {
let mut app = create_test_app();
let result = hunt(&mut app, None);
assert!(result.message.unwrap().contains("No hunt set"));
}
#[test]
fn test_parse_budget() {
assert_eq!(
parse_goal_budget("Do a thing | budget: 50000"),
parse_hunt_budget("Do a thing | budget: 50000"),
("Do a thing".to_string(), Some(50_000))
);
assert_eq!(
parse_goal_budget("Simple goal"),
parse_hunt_budget("Simple goal"),
("Simple goal".to_string(), None)
);
assert_eq!(
parse_goal_budget("Goal budget:1000"),
parse_hunt_budget("Goal budget:1000"),
("Goal".to_string(), Some(1000))
);
}
+12 -12
View File
@@ -463,9 +463,9 @@ pub const COMMANDS: &[CommandInfo] = &[
description_id: MessageId::CmdShareDescription,
},
CommandInfo {
name: "goal",
aliases: &["mubiao"],
usage: "/goal [objective] [budget: N]",
name: "hunt",
aliases: &["goal", "mubiao", "狩猎"],
usage: "/hunt [quarry] [budget: N]",
description_id: MessageId::CmdGoalDescription,
},
CommandInfo {
@@ -658,7 +658,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
"init" => init::init(app),
"lsp" => config::lsp_command(app, arg),
"share" => share::share(app, arg),
"goal" | "mubiao" => goal::goal(app, arg),
"goal" | "hunt" | "mubiao" | "狩猎" => goal::hunt(app, arg),
// Skills commands
"skills" | "jinengliebiao" => skills::list_skills(app, arg),
@@ -836,11 +836,11 @@ fn build_relay_instruction(app: &App, focus: Option<&str>) -> String {
if let Some(focus) = focus {
let _ = writeln!(out, "- Requested relay focus: {focus}");
}
if let Some(goal) = app.goal.goal_objective.as_deref() {
let _ = writeln!(out, "- Goal: {goal}");
if let Some(quarry) = app.hunt.quarry.as_deref() {
let _ = writeln!(out, "- Hunt quarry: {quarry}");
}
if let Some(budget) = app.goal.goal_token_budget {
let _ = writeln!(out, "- Goal token budget: {budget}");
if let Some(budget) = app.hunt.token_budget {
let _ = writeln!(out, "- Hunt token budget: {budget}");
}
if app.cycle_count > 0 {
let _ = writeln!(out, "- Cycle count: {}", app.cycle_count);
@@ -1175,8 +1175,8 @@ mod tests {
#[test]
fn relay_slash_command_routes_to_session_relay_instruction() {
let mut app = create_test_app();
app.goal.goal_objective = Some("Unify the work surface".to_string());
app.goal.goal_token_budget = Some(12_000);
app.hunt.quarry = Some("Unify the work surface".to_string());
app.hunt.token_budget = Some(12_000);
app.cycle_count = 2;
{
let mut todos = app.todos.try_lock().expect("todo lock");
@@ -1211,8 +1211,8 @@ mod tests {
assert!(message.contains("Write or update `.deepseek/handoff.md`"));
assert!(message.contains("# Session relay"));
assert!(message.contains("Requested relay focus: verify install"));
assert!(message.contains("Goal: Unify the work surface"));
assert!(message.contains("Goal token budget: 12000"));
assert!(message.contains("Hunt quarry: Unify the work surface"));
assert!(message.contains("Hunt token budget: 12000"));
assert!(message.contains("Cycle count: 2"));
assert!(message.contains("Work checklist (primary progress surface, 50% complete)"));
assert!(message.contains("#1 [completed] inspect workspace"));
+18 -10
View File
@@ -21,7 +21,7 @@
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::tui::app::{App, AppAction};
use crate::tui::app::{App, AppAction, HuntVerdict};
use super::CommandResult;
@@ -192,14 +192,16 @@ pub fn try_dispatch_user_command(app: &mut App, input: &str) -> Option<CommandRe
for (name, content) in &user_commands {
if name == command {
let (metadata, body) = parse_frontmatter(content);
app.goal.goal_objective = None;
app.goal.goal_started_at = None;
app.hunt.quarry = None;
app.hunt.started_at = None;
app.hunt.verdict = HuntVerdict::Hunting;
app.hunt.token_budget = None;
app.active_allowed_tools = None;
for (key, value) in &metadata {
match key.as_str() {
"description" => {
app.goal.goal_objective = Some(value.clone());
app.goal.goal_started_at = Some(std::time::Instant::now());
app.hunt.quarry = Some(value.clone());
app.hunt.started_at = Some(std::time::Instant::now());
}
"allowed-tools" => {
app.active_allowed_tools = Some(parse_allowed_tools(value));
@@ -603,13 +605,19 @@ mod tests {
let mut app = App::new(test_options(ws), &Config::default());
let _ = try_dispatch_user_command(&mut app, "/described").unwrap();
assert_eq!(app.goal.goal_objective.as_deref(), Some("Scan repos"));
assert!(app.goal.goal_started_at.is_some());
assert_eq!(app.hunt.quarry.as_deref(), Some("Scan repos"));
assert!(app.hunt.started_at.is_some());
assert_eq!(app.hunt.verdict, crate::tui::app::HuntVerdict::Hunting);
assert_eq!(app.hunt.token_budget, None);
assert_eq!(app.active_allowed_tools, Some(vec!["bash".to_string()]));
app.hunt.verdict = crate::tui::app::HuntVerdict::Escaped;
app.hunt.token_budget = Some(42);
let _ = try_dispatch_user_command(&mut app, "/plain").unwrap();
assert_eq!(app.goal.goal_objective, None);
assert_eq!(app.goal.goal_started_at, None);
assert_eq!(app.hunt.quarry, None);
assert_eq!(app.hunt.started_at, None);
assert_eq!(app.hunt.verdict, crate::tui::app::HuntVerdict::Hunting);
assert_eq!(app.hunt.token_budget, None);
assert_eq!(app.active_allowed_tools, None);
}
@@ -628,7 +636,7 @@ mod tests {
let mut app = App::new(test_options(ws.clone()), &Config::default());
let _ = try_dispatch_user_command(&mut app, "/git-scan").unwrap();
assert_eq!(
app.goal.goal_objective.as_deref(),
app.hunt.quarry.as_deref(),
Some("Scan nested git repositories")
);
let commands = load_user_commands(Some(&ws));
+2 -2
View File
@@ -1065,7 +1065,7 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
&& !goal_objective.trim().is_empty()
{
full_prompt = format!(
"{full_prompt}\n\n## Current Session Goal\n\n<session_goal>\n{}\n</session_goal>",
"{full_prompt}\n\n## Current Hunt\n\n<session_goal>\n{}\n</session_goal>",
goal_objective.trim()
);
}
@@ -2120,7 +2120,7 @@ mod tests {
};
assert!(!prompt.contains("<session_goal>"));
assert!(!prompt.contains("## Current Session Goal"));
assert!(!prompt.contains("## Current Hunt"));
}
#[test]
+25 -8
View File
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use ratatui::layout::Rect;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
@@ -1001,13 +1002,29 @@ impl Default for ViewportState {
}
}
/// Goal tracking state (#397).
/// Verdict for a hunt (#2092).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HuntVerdict {
Hunting,
Hunted,
Wounded,
Escaped,
}
impl Default for HuntVerdict {
fn default() -> Self {
Self::Hunting
}
}
/// Hunt tracking state (#2092 — was GoalState).
#[derive(Debug, Clone, Default)]
pub struct GoalState {
pub goal_objective: Option<String>,
pub goal_token_budget: Option<u32>,
pub goal_started_at: Option<Instant>,
pub goal_completed: bool,
pub struct HuntState {
pub quarry: Option<String>,
pub token_budget: Option<u32>,
pub started_at: Option<Instant>,
pub verdict: HuntVerdict,
}
/// Session cost and token telemetry state.
@@ -1105,7 +1122,7 @@ pub struct App {
/// Viewport sub-state (scroll, cache, selection).
pub viewport: ViewportState,
/// Goal sub-state.
pub goal: GoalState,
pub hunt: HuntState,
/// Session sub-state (cost, tokens, telemetry).
pub session: SessionState,
/// Active tool restriction from custom slash command frontmatter.
@@ -1861,7 +1878,7 @@ impl App {
selection_anchor: None,
},
viewport: ViewportState::default(),
goal: GoalState::default(),
hunt: HuntState::default(),
session: SessionState::default(),
active_allowed_tools: None,
history: Vec::new(),
+6 -4
View File
@@ -7,6 +7,8 @@
use std::fmt::Write;
use std::time::{Duration, Instant};
use crate::tui::app::HuntVerdict;
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
@@ -228,10 +230,10 @@ impl SidebarWorkSummary {
fn sidebar_work_summary(app: &App) -> SidebarWorkSummary {
let mut summary = SidebarWorkSummary {
goal_objective: app.goal.goal_objective.clone(),
goal_token_budget: app.goal.goal_token_budget,
goal_completed: app.goal.goal_completed,
goal_started_at: app.goal.goal_started_at,
goal_objective: app.hunt.quarry.clone(),
goal_token_budget: app.hunt.token_budget,
goal_completed: app.hunt.verdict == HuntVerdict::Hunted,
goal_started_at: app.hunt.started_at,
tokens_used: app.session.total_conversation_tokens,
cycle_count: app.cycle_count,
..SidebarWorkSummary::default()
+7 -6
View File
@@ -64,6 +64,7 @@ use crate::task_manager::{
};
use crate::tools::spec::{RuntimeToolServices, ToolResult};
use crate::tools::subagent::SubAgentStatus;
use crate::tui::app::HuntVerdict;
use crate::tui::auto_router;
use crate::tui::color_compat::ColorCompatBackend;
use crate::tui::command_palette::{
@@ -743,9 +744,9 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
todos: app.todos.clone(),
plan_state: app.plan_state.clone(),
goal_state: crate::tools::goal::new_shared_goal_state_from_host(
app.goal.goal_objective.clone(),
app.goal.goal_token_budget,
app.goal.goal_completed,
app.hunt.quarry.clone(),
app.hunt.token_budget,
app.hunt.verdict == HuntVerdict::Hunted,
),
max_spawn_depth: crate::tools::subagent::DEFAULT_MAX_SPAWN_DEPTH,
allowed_tools: app.active_allowed_tools.clone(),
@@ -769,7 +770,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
memory_path: config.memory_path(),
vision_config: config.vision_model_config(),
strict_tool_mode: config.strict_tool_mode.unwrap_or(false),
goal_objective: app.goal.goal_objective.clone(),
goal_objective: app.hunt.quarry.clone(),
locale_tag: app.ui_locale.tag().to_string(),
workshop: config.workshop.clone(),
search_provider: config.search_provider(),
@@ -4389,7 +4390,7 @@ async fn dispatch_user_message(
None,
prompts::PromptSessionContext {
user_memory_block: None,
goal_objective: app.goal.goal_objective.as_deref(),
goal_objective: app.hunt.quarry.as_deref(),
project_context_pack_enabled: config.project_context_pack_enabled(),
locale_tag: app.ui_locale.tag(),
translation_enabled: app.translation_enabled,
@@ -4482,7 +4483,7 @@ async fn dispatch_user_message(
content,
mode: app.mode,
model: effective_model,
goal_objective: app.goal.goal_objective.clone(),
goal_objective: app.hunt.quarry.clone(),
reasoning_effort: effective_reasoning_effort,
reasoning_effort_auto: auto_controls_reasoning,
auto_model: app.auto_model,