feat: enhance workspace management and history tracking in application state
This commit is contained in:
@@ -313,6 +313,7 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> {
|
||||
);
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = state.session_dir.clone();
|
||||
ctx.workspaces = state.workspace_roots.clone();
|
||||
let probe_result = probe_build_test(
|
||||
&state.workspace_roots,
|
||||
state.settings.verify_command.as_deref(),
|
||||
|
||||
@@ -608,6 +608,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
format!("Starting workflow: {}…", &script.chars().take(40).collect::<String>()),
|
||||
));
|
||||
|
||||
let session_dir = state.session_dir.clone();
|
||||
let workspace_roots = state.workspace_roots.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -654,7 +657,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
});
|
||||
|
||||
let args: HashMap<String, String> = HashMap::new();
|
||||
let result = crate::app::workflow::engine::run_workflow_tracked(&wf, &args, Some(live));
|
||||
let result = crate::app::workflow::engine::run_workflow_tracked(
|
||||
&wf, &args, Some(live), &session_dir, &workspace_roots,
|
||||
);
|
||||
|
||||
let (kind, message) = match result {
|
||||
Ok(summary) => ("workflow_done".to_string(), summary),
|
||||
|
||||
+15
-5
@@ -72,6 +72,7 @@ pub struct InputState {
|
||||
pub autocomplete_candidates: Vec<String>,
|
||||
pub autocomplete_idx: usize,
|
||||
pub autocomplete_visible: bool,
|
||||
pub history_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
const COMMANDS: &[&str] = &[
|
||||
@@ -110,6 +111,7 @@ impl InputState {
|
||||
autocomplete_candidates: Vec::new(),
|
||||
autocomplete_idx: 0,
|
||||
autocomplete_visible: false,
|
||||
history_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,14 +221,22 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit the current buffer: push it to history, clear the buffer,
|
||||
/// and return the submitted text.
|
||||
///
|
||||
/// Return: the text that was in the buffer before clearing.
|
||||
pub fn submit(&mut self) -> String {
|
||||
let result = self.buffer.clone();
|
||||
if !result.is_empty() {
|
||||
self.history.push(result.clone());
|
||||
if self.history.last() != Some(&result) {
|
||||
self.history.push(result.clone());
|
||||
if let Some(ref path) = self.history_file {
|
||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
{
|
||||
use std::io::Write;
|
||||
let _ = writeln!(file, "{}", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.history_idx = None;
|
||||
}
|
||||
self.buffer.clear();
|
||||
|
||||
+29
-1
@@ -104,7 +104,7 @@ impl AppStateRest {
|
||||
tracing::warn!("[state] session_dir has no file_name component, using empty session_id");
|
||||
String::new()
|
||||
});
|
||||
let state = AppStateRest {
|
||||
let mut state = AppStateRest {
|
||||
|
||||
settings,
|
||||
app_config,
|
||||
@@ -133,6 +133,34 @@ impl AppStateRest {
|
||||
quit: false,
|
||||
};
|
||||
|
||||
// Load project-specific history
|
||||
let base_dir = state.memory_dir.parent().unwrap_or(&state.memory_dir);
|
||||
if let Some(root) = state.workspace_roots.first() {
|
||||
if let Ok(abs_root) = std::fs::canonicalize(root) {
|
||||
use sha2::Digest;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(abs_root.to_string_lossy().as_bytes());
|
||||
let hash_hex = format!("{:x}", hasher.finalize());
|
||||
let folder_name = abs_root.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "root".to_string());
|
||||
let history_filename = format!("{}-{}.txt", folder_name, &hash_hex[..8]);
|
||||
let history_dir = base_dir.join("history");
|
||||
let _ = std::fs::create_dir_all(&history_dir);
|
||||
let history_file = history_dir.join(history_filename);
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(&history_file) {
|
||||
let history: Vec<String> = content
|
||||
.lines()
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
state.input.history = history;
|
||||
}
|
||||
state.input.history_file = Some(history_file);
|
||||
}
|
||||
}
|
||||
|
||||
// Fire-and-forget background LSP provisioning.
|
||||
//
|
||||
// Flow: spawn OS thread -> provision_all() probes/installs every
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct SubagentContext {
|
||||
pub allowed_tools: Vec<String>,
|
||||
pub max_steps: usize,
|
||||
pub session_dir: PathBuf,
|
||||
pub workspaces: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Build a `SubagentContext` from an `AgentDefinition`.
|
||||
@@ -23,8 +24,8 @@ pub struct SubagentContext {
|
||||
/// fall back to an empty list (i.e. "all tools allowed") for other roles.
|
||||
/// `max_steps` is read from the definition, defaulting to 25 if absent.
|
||||
///
|
||||
/// Return: a context with empty `system_prompt` and `session_dir`,
|
||||
/// resolved `max_steps`, and the resolved allowed-tool list.
|
||||
/// Return: a context with empty `system_prompt`, empty `workspaces`,
|
||||
/// empty `session_dir`, resolved `max_steps`, and the resolved allowed-tool list.
|
||||
pub fn build_subagent_context(def: AgentDefinition) -> SubagentContext {
|
||||
let allowed_tools = def.allowed_tools.clone().unwrap_or_else(|| {
|
||||
if def.role == "reviewer" {
|
||||
@@ -39,5 +40,6 @@ pub fn build_subagent_context(def: AgentDefinition) -> SubagentContext {
|
||||
allowed_tools,
|
||||
max_steps,
|
||||
session_dir: PathBuf::new(),
|
||||
workspaces: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ pub fn run_subagent(ctx: SubagentContext, tx: mpsc::Sender<SubagentEvent>) -> an
|
||||
|
||||
let tool_ctx = crate::tool::ToolCtx::builder()
|
||||
.session_dir(ctx.session_dir.clone())
|
||||
.workspaces(ctx.workspaces.clone())
|
||||
.origin(crate::app::state::types::Origin::SubAgent)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ fn spawn_single_agent(
|
||||
prompt: &str,
|
||||
findings_snapshot: Vec<String>,
|
||||
live: Option<&LiveStateFn>,
|
||||
session_dir: &std::path::Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
) -> anyhow::Result<String> {
|
||||
use crate::app::subagent::context::build_subagent_context;
|
||||
use crate::app::subagent::engine::run_subagent;
|
||||
@@ -104,6 +106,8 @@ fn spawn_single_agent(
|
||||
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string())
|
||||
.with_max_steps(50);
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = session_dir.to_path_buf();
|
||||
ctx.workspaces = workspaces.to_vec();
|
||||
|
||||
let findings_section = if findings_snapshot.is_empty() {
|
||||
String::new()
|
||||
@@ -182,6 +186,8 @@ pub fn execute_primitive(
|
||||
concurrency_cap: usize,
|
||||
continue_on_error: bool,
|
||||
live: Option<&LiveStateFn>,
|
||||
session_dir: &std::path::Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
match primitive {
|
||||
ScriptPrimitive::Agent(prompt) => {
|
||||
@@ -189,7 +195,7 @@ pub fn execute_primitive(
|
||||
let findings_snapshot = FINDINGS.lock().map(|f| f.clone()).unwrap_or_default();
|
||||
let agent_id = uuid::Uuid::new_v4().to_string();
|
||||
let agent_name = resolved.chars().take(40).collect::<String>();
|
||||
match spawn_single_agent(&agent_id, &agent_name, &resolved, findings_snapshot, live) {
|
||||
match spawn_single_agent(&agent_id, &agent_name, &resolved, findings_snapshot, live, session_dir, workspaces) {
|
||||
Ok(text) => Ok(vec![text]),
|
||||
Err(e) => {
|
||||
if continue_on_error {
|
||||
@@ -219,12 +225,16 @@ pub fn execute_primitive(
|
||||
let results = Arc::clone(&results);
|
||||
let cap = concurrency_cap;
|
||||
let live_clone = live.cloned();
|
||||
let session_dir = session_dir.to_path_buf();
|
||||
let workspaces = workspaces.to_vec();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _permit = sem.acquire();
|
||||
let result = execute_primitive(
|
||||
&script, &args, cap, continue_on_error,
|
||||
live_clone.as_ref(),
|
||||
&session_dir,
|
||||
&workspaces,
|
||||
);
|
||||
if let Ok(mut locked) = results.lock() {
|
||||
locked.push((idx, result));
|
||||
@@ -258,7 +268,7 @@ pub fn execute_primitive(
|
||||
// the global FINDINGS mutex.
|
||||
let mut all = Vec::new();
|
||||
for (idx, script) in scripts.iter().enumerate() {
|
||||
match execute_primitive(script, args, concurrency_cap, continue_on_error, live) {
|
||||
match execute_primitive(script, args, concurrency_cap, continue_on_error, live, session_dir, workspaces) {
|
||||
Ok(outputs) => all.extend(outputs),
|
||||
Err(e) => {
|
||||
if continue_on_error {
|
||||
@@ -273,7 +283,7 @@ pub fn execute_primitive(
|
||||
}
|
||||
|
||||
ScriptPrimitive::Phase { name: _name, script } => {
|
||||
execute_primitive(script, args, concurrency_cap, continue_on_error, live)
|
||||
execute_primitive(script, args, concurrency_cap, continue_on_error, live, session_dir, workspaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,8 +292,13 @@ pub fn execute_primitive(
|
||||
/// summary string. Uses no live-state callback.
|
||||
///
|
||||
/// Return: a human-readable summary string.
|
||||
pub fn run_workflow(script: &WorkflowScript, args: &HashMap<String, String>) -> anyhow::Result<String> {
|
||||
run_workflow_tracked(script, args, None)
|
||||
pub fn run_workflow(
|
||||
script: &WorkflowScript,
|
||||
args: &HashMap<String, String>,
|
||||
session_dir: &std::path::Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
) -> anyhow::Result<String> {
|
||||
run_workflow_tracked(script, args, None, session_dir, workspaces)
|
||||
}
|
||||
|
||||
/// Run a `WorkflowScript` with real-time live-state callbacks so the TUI
|
||||
@@ -297,6 +312,8 @@ pub fn run_workflow_tracked(
|
||||
script: &WorkflowScript,
|
||||
args: &HashMap<String, String>,
|
||||
live: Option<LiveStateFn>,
|
||||
session_dir: &std::path::Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
) -> anyhow::Result<String> {
|
||||
if let Ok(mut findings) = FINDINGS.lock() {
|
||||
findings.clear();
|
||||
@@ -312,6 +329,7 @@ pub fn run_workflow_tracked(
|
||||
let results = execute_primitive(
|
||||
&script.script, args, concurrency_cap,
|
||||
script.options.continue_on_error, live_ref,
|
||||
session_dir, workspaces,
|
||||
)?;
|
||||
|
||||
let summary = if results.is_empty() {
|
||||
|
||||
@@ -47,6 +47,8 @@ pub enum ClientRequest {
|
||||
Submit(String),
|
||||
Resize(u16, u16),
|
||||
Close,
|
||||
ScrollUp,
|
||||
ScrollDown,
|
||||
}
|
||||
|
||||
/// Flattened chat message sent from daemon to client for transcript display.
|
||||
|
||||
+22
-5
@@ -11,6 +11,7 @@ use std::sync::Mutex;
|
||||
use anyhow::Result;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use crossterm::event::{EnableMouseCapture, DisableMouseCapture};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
@@ -116,7 +117,7 @@ fn run_single_process() -> Result<()> {
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
@@ -124,7 +125,7 @@ fn run_single_process() -> Result<()> {
|
||||
let run_result = run_loop(&mut state, &mut terminal);
|
||||
|
||||
let mut restore_stdout = io::stdout();
|
||||
let _ = execute!(restore_stdout, LeaveAlternateScreen);
|
||||
let _ = execute!(restore_stdout, LeaveAlternateScreen, DisableMouseCapture);
|
||||
let _ = disable_raw_mode();
|
||||
|
||||
if let Err(e) = run_result {
|
||||
@@ -413,6 +414,14 @@ fn run_daemon() -> Result<()> {
|
||||
apply_action(&mut state, Action::Resize(w, h));
|
||||
apply_action(&mut state, Action::Tick);
|
||||
}
|
||||
ClientRequest::ScrollUp => {
|
||||
apply_action(&mut state, Action::ScrollUp);
|
||||
apply_action(&mut state, Action::Tick);
|
||||
}
|
||||
ClientRequest::ScrollDown => {
|
||||
apply_action(&mut state, Action::ScrollDown);
|
||||
apply_action(&mut state, Action::Tick);
|
||||
}
|
||||
ClientRequest::Close => {
|
||||
running = false;
|
||||
}
|
||||
@@ -449,7 +458,7 @@ fn run_daemon() -> Result<()> {
|
||||
/// through the daemon, since the daemon has no notion of "this client
|
||||
/// wants to leave" beyond the explicit `Close` request.
|
||||
fn run_attach(session_id: &str) -> Result<()> {
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind};
|
||||
use ipc::protocol::ClientRequest;
|
||||
|
||||
let store = model::store::Store::new();
|
||||
@@ -460,7 +469,7 @@ fn run_attach(session_id: &str) -> Result<()> {
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
@@ -512,6 +521,13 @@ fn run_attach(session_id: &str) -> Result<()> {
|
||||
Event::Resize(w, h) => {
|
||||
client.send(&ClientRequest::Resize(w, h))?;
|
||||
}
|
||||
Event::Mouse(mouse_event) => {
|
||||
if mouse_event.kind == MouseEventKind::ScrollUp {
|
||||
client.send(&ClientRequest::ScrollUp)?;
|
||||
} else if mouse_event.kind == MouseEventKind::ScrollDown {
|
||||
client.send(&ClientRequest::ScrollDown)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
@@ -544,7 +560,7 @@ fn run_attach(session_id: &str) -> Result<()> {
|
||||
})?;
|
||||
}
|
||||
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
|
||||
let _ = disable_raw_mode();
|
||||
|
||||
let _ = client_state.settings.save();
|
||||
@@ -569,6 +585,7 @@ fn run_loop(
|
||||
if let Err(ref _e) = result {
|
||||
let _ = terminal.clear();
|
||||
|
||||
let _ = execute!(io::stdout(), DisableMouseCapture);
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ impl Default for ToolCtxBuilder {
|
||||
impl ToolCtxBuilder {
|
||||
/// Set the session directory.
|
||||
pub fn session_dir(mut self, v: PathBuf) -> Self { self.session_dir = v; self }
|
||||
/// Set the workspaces.
|
||||
pub fn workspaces(mut self, v: Vec<PathBuf>) -> Self { self.workspaces = v; self }
|
||||
/// Set the origin (main process vs. daemon-attached).
|
||||
pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self }
|
||||
/// Set the lsp_manager.
|
||||
|
||||
@@ -110,6 +110,8 @@ impl Tool for SpawnAgents {
|
||||
max_concurrency,
|
||||
true,
|
||||
live.as_ref(),
|
||||
&_ctx.session_dir,
|
||||
&_ctx.workspaces,
|
||||
)?;
|
||||
format_results(results, "parallel")
|
||||
}
|
||||
@@ -193,6 +195,8 @@ impl Tool for SpawnPipeline {
|
||||
1,
|
||||
false,
|
||||
live.as_ref(),
|
||||
&_ctx.session_dir,
|
||||
&_ctx.workspaces,
|
||||
)?;
|
||||
format_results(results, "pipeline")
|
||||
}
|
||||
|
||||
@@ -76,7 +76,9 @@ impl Tool for WorkflowRun {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
crate::app::workflow::engine::run_workflow(&workflow_script, &workflow_args)
|
||||
crate::app::workflow::engine::run_workflow(
|
||||
&workflow_script, &workflow_args, &_ctx.session_dir, &_ctx.workspaces,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user