harness-tui + engine: jobs pane, subtask drill-in, reminders (M4)
TUI: Ctrl+J (or /jobs) opens a jobs pane listing the current session's board — alias, agent, state, objective, files read — populated on load and kept live via JobUpdated events; Enter drills into a subtask's child session. Snapshot test added. Engine: optional orchestration reminders (off by default) injected as synthetic, non-persisted turn-start blocks and, after a file tool runs, an after-file-tool block on the following turn. Processor reports file-tool usage via StepOutcome. This completes M4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,10 @@ pub enum InputAction {
|
||||
SessionPickerUp,
|
||||
SessionPickerDown,
|
||||
SessionPickerSelect,
|
||||
OpenJobs,
|
||||
JobsUp,
|
||||
JobsDown,
|
||||
JobsDrillIn,
|
||||
}
|
||||
|
||||
/// Translate a crossterm event into an action and/or mutate `state` directly.
|
||||
@@ -49,10 +53,21 @@ fn handle_key(key: KeyEvent, state: &mut AppState) -> InputAction {
|
||||
match &state.modal {
|
||||
ModalState::Permission { .. } => handle_permission_key(key, state),
|
||||
ModalState::SessionPicker { .. } => handle_session_picker_key(key, state),
|
||||
ModalState::JobsPane { .. } => handle_jobs_pane_key(key, state),
|
||||
ModalState::None => handle_normal_key(key, state),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_jobs_pane_key(key: KeyEvent, _state: &mut AppState) -> InputAction {
|
||||
match key.code {
|
||||
KeyCode::Up => InputAction::JobsUp,
|
||||
KeyCode::Down => InputAction::JobsDown,
|
||||
KeyCode::Enter => InputAction::JobsDrillIn,
|
||||
KeyCode::Esc => InputAction::CloseModal,
|
||||
_ => InputAction::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_permission_key(key: KeyEvent, _state: &mut AppState) -> InputAction {
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
@@ -119,6 +134,7 @@ fn handle_normal_key(key: KeyEvent, state: &mut AppState) -> InputAction {
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') if ctrl => InputAction::LoadSessions,
|
||||
KeyCode::Char('j') if ctrl => InputAction::OpenJobs,
|
||||
KeyCode::Up => {
|
||||
let (row, _) = state.input.cursor();
|
||||
if row == 0 {
|
||||
@@ -158,6 +174,7 @@ fn parse_slash_command(text: &str) -> Option<InputAction> {
|
||||
"/model" if !rest.is_empty() => Some(InputAction::SetModel(rest)),
|
||||
"/agent" if !rest.is_empty() => Some(InputAction::SetAgent(rest)),
|
||||
"/sessions" => Some(InputAction::LoadSessions),
|
||||
"/jobs" => Some(InputAction::OpenJobs),
|
||||
"/quit" => Some(InputAction::Quit),
|
||||
_ => None,
|
||||
}
|
||||
@@ -235,6 +252,45 @@ pub async fn apply_action(action: InputAction, state: &mut AppState, engine: &En
|
||||
}
|
||||
}
|
||||
}
|
||||
InputAction::OpenJobs => {
|
||||
if let Some(session_id) = state.session_id.clone() {
|
||||
match engine.jobs(session_id).await {
|
||||
Ok(jobs) => {
|
||||
state.set_jobs(jobs);
|
||||
state.open_jobs_pane();
|
||||
}
|
||||
Err(e) => tracing::error!(error = %e, "failed to load jobs"),
|
||||
}
|
||||
}
|
||||
}
|
||||
InputAction::JobsUp => {
|
||||
if let ModalState::JobsPane { selected } = &mut state.modal {
|
||||
*selected = selected.saturating_sub(1);
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
InputAction::JobsDown => {
|
||||
let job_count = state.jobs.len();
|
||||
if let ModalState::JobsPane { selected } = &mut state.modal {
|
||||
if *selected + 1 < job_count {
|
||||
*selected += 1;
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
InputAction::JobsDrillIn => {
|
||||
if let Some(child) = state.selected_job_child() {
|
||||
match engine.get_session(child).await {
|
||||
Ok(Some(session)) => {
|
||||
if let Err(e) = modal::select_session(state, engine, session).await {
|
||||
tracing::error!(error = %e, "failed to open subtask session");
|
||||
}
|
||||
}
|
||||
Ok(None) => tracing::warn!("subtask session no longer exists"),
|
||||
Err(e) => tracing::error!(error = %e, "failed to load subtask session"),
|
||||
}
|
||||
}
|
||||
}
|
||||
InputAction::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user