feat(tui/history): route core cells via apply_event

This commit is contained in:
James Peter
2025-09-27 07:30:16 +10:00
parent 7fd3a8ae85
commit e6e72d36aa
18 changed files with 725 additions and 243 deletions
+1
View File
@@ -1249,6 +1249,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"sha2",
"shlex",
"strip-ansi-escapes",
"strum 0.27.2",
+1
View File
@@ -64,6 +64,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
shlex = "1.3.0"
sha2 = { workspace = true }
tempfile = "3"
arboard = "3"
strum = "0.27.2"
+8 -2
View File
@@ -150,11 +150,17 @@ Status legend: ✅ complete (semantic deterministic state ready), ⏳ still need
## Step 3 – HistoryState Manager *(Pending)*
3.1 **HistoryRecord enum** – ✅ complete: `history/state.rs` defines `HistoryRecord`, per-cell state structs, and `HistoryState` scaffolding with ID management helpers.
3.2 **ChatWidget incremental adoption** – Introduce `HistoryState`/`HistoryRenderState` alongside the legacy `history_cells` vector, migrate low-risk cell types (plain, loading, wait status) to the new state, then remove the legacy vector once the path is stable. **Status:** assistant streaming/final answer flows now write into `HistoryState`; next migrate remaining cell constructors and have render adaptors consume the recorded state.
3.3 **Apply-event pipeline** – Implement `HistoryState::apply_event(&mut self, event: &EventMsg)` covering all core/TUI event types (exec lifecycle, tool updates, background notices, resume snapshots, undo) and route migrated cells through it.
3.2 **ChatWidget incremental adoption** – Introduce `HistoryState`/`HistoryRenderState` alongside the legacy `history_cells` vector, migrate low-risk cell types (plain, loading, wait status) to the new state, then remove the legacy vector once the path is stable. **Status:** assistant streaming/final answer flows now write into `HistoryState`; plain/background/wait/loading cells, running/completed tool calls, plan updates, upgrade notices, reasoning summaries, exec cells, diff summaries, patch summaries, and image previews now insert exclusively via `history_insert_with_key_global_tagged` + `HistoryState::apply_event`. Next steps: migrate explore aggregations, rate-limit notices, and remaining niche cells before deleting the legacy vector. **Update (2025-09-26):** migration will now happen only via the new `HistoryState::apply_event` entrypoint so every cell’s lifecycle is driven by events rather than bespoke insert/replace helpers.
3.3 **Apply-event pipeline** – Implement `HistoryState::apply_event(&mut self, event: &EventMsg)` covering all core/TUI event types (exec lifecycle, tool updates, background notices, resume snapshots, undo) and route migrated cells through it. **Status:** Insert events now assign `HistoryId`s for plain/background/loading/wait/tool/plan/upgrade/reasoning/exec/diff/assistant/patch/image cells and keep `history_cell_ids` in sync; replacement/removal paths still mirror the legacy vector and need to be ported. Pending: wire explore/rate-limit mutations through dedicated `HistoryEvent` variants, propagate patch success/failure updates via `HistoryEvent::Replace`, and delete the remaining direct `history_cells` manipulations. **Update (2025-09-26):** build `apply_event` first, then iterate cell-by-cell: for each event family (plain messages, loading indicators, wait status, etc.) add a handler in `apply_event`, update `ChatWidget` to publish only events, and adapt rendering to rebuild from the resulting `HistoryRecord`s. The legacy `history_cells` vector will temporarily mirror `HistoryState` via an ID→cell cache until Step 3.5 removes it entirely.
3.4 **Undo/resume hooks** – Expose `snapshot`, `restore`, and `truncate_after(id)` to support /undo and resume flows.
3.5 **ChatWidget full integration** – Delete `history_cells: Vec<Box<dyn HistoryCell>>`, wire all helper methods (`history_push`, `history_replace`, etc.) into `HistoryState`, and treat Step 4 as the follow-up for centralized rendering.
### Outstanding Gaps (2025-09-26)
- Explore aggregation cells still maintain bespoke `ExploreAggregationState`; convert into `ExploreRecord` instances so inserts/replays serialize cleanly.
- Rate-limit warnings and compact panels render outside the history pipeline; decide whether they should emit `HistoryRecord::RateLimits` snapshots or remain overlay-only.
- Patch apply success/failure currently mutates `PatchSummaryCell` in place; emit `HistoryEvent::Replace` once the replace arm is implemented so records stay authoritative.
- `HistoryState::apply_event` only drives inserts today; removal/replace helpers still operate directly on `history_cells` and must migrate before we can drop the legacy vector.
## Step 4 – Event Mapping & Rendering *(Pending)*
4.1 **Centralize handlers** – Route every mutation in `handle_*` (exec events, tool deltas, diff updates, background notices) through `HistoryState` so ordering/id management lives in one place.
4.2 **Stable IDs for streaming** – Store `HistoryId` + domain IDs (exec id, tool call id, stream id) to ensure in-place updates and dedupe.
+281 -33
View File
@@ -211,11 +211,14 @@ use crate::history_cell::HistoryCell;
use crate::history_cell::HistoryCellType;
use crate::history_cell::PatchEventType;
use crate::history_cell::PlainHistoryCell;
use crate::history::state::PatchEventType as HistoryPatchEventType;
use crate::history::state::{
AssistantMessageState,
AssistantStreamDelta,
AssistantStreamState,
HistoryEvent,
HistoryId,
HistoryMutation,
HistoryRecord,
HistoryState,
MessageMetadata,
@@ -428,6 +431,7 @@ pub(crate) struct ChatWidget<'a> {
login_add_view_state: Option<Weak<RefCell<LoginAddAccountState>>>,
active_exec_cell: Option<ExecCell>,
history_cells: Vec<Box<dyn HistoryCell>>, // Store all history in memory
history_cell_ids: Vec<Option<HistoryId>>,
history_render: HistoryRenderState,
history_state: HistoryState,
config: Config,
@@ -1210,7 +1214,7 @@ impl ChatWidget<'_> {
}
}
let key = self.system_order_key(placement, order);
let pos = self.history_insert_with_key_global_tagged(Box::new(cell), key, tag);
let pos = self.history_insert_with_key_global_tagged(Box::new(cell), key, tag, None);
if let Some(id) = id_for_replace {
self.system_cell_by_id.insert(id, pos);
}
@@ -1567,7 +1571,7 @@ impl ChatWidget<'_> {
use crate::history::state::ExecStatus;
use crate::history_cell::HistoryCellType;
use crate::history_cell::PatchKind;
use crate::history_cell::ToolStatus;
use crate::history_cell::ToolCellStatus;
match item.kind() {
HistoryCellType::Plain => "Plain".to_string(),
HistoryCellType::User => "User".to_string(),
@@ -1590,9 +1594,9 @@ impl ChatWidget<'_> {
}
HistoryCellType::Tool { status } => {
let s = match status {
ToolStatus::Running => "Running",
ToolStatus::Success => "Success",
ToolStatus::Failed => "Failed",
ToolCellStatus::Running => "Running",
ToolCellStatus::Success => "Success",
ToolCellStatus::Failed => "Failed",
};
format!("Tool:{}", s)
}
@@ -1738,9 +1742,10 @@ impl ChatWidget<'_> {
let mut lines = Vec::new();
crate::markdown::append_markdown(text, &mut lines, &self.config);
let key = self.next_internal_key();
let _ = self.history_insert_with_key_global(
Box::new(PlainHistoryCell::new(lines, HistoryCellType::Assistant)),
let _ = self.history_insert_plain_cell_with_key(
PlainHistoryCell::new(lines, HistoryCellType::Assistant),
key,
"epilogue",
);
}
}
@@ -1761,6 +1766,7 @@ impl ChatWidget<'_> {
Box::new(crate::history_cell::new_background_event(message)),
key,
"background",
None,
);
}
ResponseItem::Reasoning { summary, .. } => {
@@ -1824,6 +1830,7 @@ impl ChatWidget<'_> {
Box::new(crate::history_cell::new_background_event(message)),
key,
"background",
None,
);
}
_ => {
@@ -2033,9 +2040,32 @@ impl ChatWidget<'_> {
// invalidate the height cache since indices shift and our cache is
// keyed by (idx,width).
let before_len = self.history_cells.len();
self.history_cells.retain(|cell| !cell.should_remove());
if self.history_cells.len() != before_len {
self.invalidate_height_cache();
if before_len > 0 {
let old_cells = std::mem::take(&mut self.history_cells);
let old_ids = std::mem::take(&mut self.history_cell_ids);
debug_assert_eq!(
old_cells.len(),
old_ids.len(),
"history ids out of sync with cells"
);
let mut removed_any = false;
let mut kept_cells = Vec::with_capacity(old_cells.len());
let mut kept_ids = Vec::with_capacity(old_ids.len());
for (cell, id) in old_cells.into_iter().zip(old_ids.into_iter()) {
if cell.should_remove() {
removed_any = true;
continue;
}
kept_ids.push(id);
kept_cells.push(cell);
}
self.history_cells = kept_cells;
self.history_cell_ids = kept_ids;
if removed_any {
self.invalidate_height_cache();
}
} else if !self.history_cell_ids.is_empty() {
self.history_cell_ids.clear();
}
// Send a redraw event to trigger UI update
@@ -2174,6 +2204,7 @@ impl ChatWidget<'_> {
{
summary.title = "Updated".to_string();
summary.kind = history_cell::PatchKind::ApplySuccess;
summary.record_mut().patch_type = HistoryPatchEventType::ApplySuccess;
self.request_redraw();
return;
}
@@ -2699,6 +2730,7 @@ impl ChatWidget<'_> {
pending_upgrade_notice: None,
history_render: HistoryRenderState::new(),
history_state: HistoryState::new(),
history_cell_ids: Vec::new(),
height_manager: RefCell::new(HeightManager::new(
crate::height_manager::HeightManagerConfig::default(),
)),
@@ -2932,6 +2964,7 @@ impl ChatWidget<'_> {
pending_upgrade_notice: None,
history_render: HistoryRenderState::new(),
history_state: HistoryState::new(),
history_cell_ids: Vec::new(),
height_manager: RefCell::new(HeightManager::new(
crate::height_manager::HeightManagerConfig::default(),
)),
@@ -4224,7 +4257,7 @@ impl ChatWidget<'_> {
cell: Box<dyn HistoryCell>,
key: OrderKey,
) -> usize {
self.history_insert_with_key_global_tagged(cell, key, "untagged")
self.history_insert_with_key_global_tagged(cell, key, "untagged", None)
}
// Internal: same as above but with a short tag for debug overlays.
@@ -4233,6 +4266,7 @@ impl ChatWidget<'_> {
cell: Box<dyn HistoryCell>,
key: OrderKey,
tag: &'static str,
record: Option<HistoryRecord>,
) -> usize {
#[cfg(debug_assertions)]
{
@@ -4340,7 +4374,27 @@ impl ChatWidget<'_> {
None
};
let mut cell = cell;
let record = record.or_else(|| self.history_record_from_cell(cell.as_ref()));
let maybe_id = if let Some(record) = record {
match self
.history_state
.apply_event(HistoryEvent::Insert { index: pos, record })
{
HistoryMutation::Inserted { id, .. } => {
self.assign_history_id(&mut cell, id);
Some(id)
}
_ => None,
}
} else {
None
};
self.history_cells.insert(pos, cell);
self.history_cell_ids.insert(pos, maybe_id);
// In terminal mode, App mirrors history lines into the native buffer.
// Ensure order vector is also long enough for position after cell insert
if self.cell_order_seq.len() < pos {
@@ -4396,6 +4450,179 @@ impl ChatWidget<'_> {
pos
}
fn assign_history_id(&self, cell: &mut Box<dyn HistoryCell>, id: HistoryId) {
if let Some(plain) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::PlainHistoryCell>()
{
plain.state_mut().id = id;
} else if let Some(wait) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::WaitStatusCell>()
{
wait.state_mut().id = id;
} else if let Some(loading) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::LoadingCell>()
{
loading.state_mut().id = id;
} else if let Some(background) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::BackgroundEventCell>()
{
background.state_mut().id = id;
} else if let Some(tool_call) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::ToolCallCell>()
{
tool_call.state_mut().id = id;
} else if let Some(running_tool) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::RunningToolCallCell>()
{
running_tool.state_mut().id = id;
} else if let Some(plan) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::PlanUpdateCell>()
{
plan.state_mut().id = id;
} else if let Some(upgrade) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::UpgradeNoticeCell>()
{
upgrade.state_mut().id = id;
} else if let Some(reasoning) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::CollapsibleReasoningCell>()
{
reasoning.set_history_id(id);
} else if let Some(exec) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::ExecCell>()
{
exec.record.id = id;
} else if let Some(stream) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::StreamingContentCell>()
{
stream.state_mut().id = id;
} else if let Some(assistant) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::AssistantMarkdownCell>()
{
assistant.state_mut().id = id;
} else if let Some(diff) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::DiffCell>()
{
diff.record_mut().id = id;
} else if let Some(image) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::ImageOutputCell>()
{
image.record_mut().id = id;
} else if let Some(patch) = cell
.as_any_mut()
.downcast_mut::<crate::history_cell::PatchSummaryCell>()
{
patch.record_mut().id = id;
}
}
fn history_record_from_cell(&self, cell: &dyn HistoryCell) -> Option<HistoryRecord> {
if let Some(plain) = cell
.as_any()
.downcast_ref::<crate::history_cell::PlainHistoryCell>()
{
return Some(HistoryRecord::PlainMessage(plain.state().clone()));
}
if let Some(wait) = cell
.as_any()
.downcast_ref::<crate::history_cell::WaitStatusCell>()
{
return Some(HistoryRecord::WaitStatus(wait.state().clone()));
}
if let Some(loading) = cell
.as_any()
.downcast_ref::<crate::history_cell::LoadingCell>()
{
return Some(HistoryRecord::Loading(loading.state().clone()));
}
if let Some(background) = cell
.as_any()
.downcast_ref::<crate::history_cell::BackgroundEventCell>()
{
return Some(HistoryRecord::BackgroundEvent(background.state().clone()));
}
if let Some(tool_call) = cell
.as_any()
.downcast_ref::<crate::history_cell::ToolCallCell>()
{
return Some(HistoryRecord::ToolCall(tool_call.state().clone()));
}
if let Some(running_tool) = cell
.as_any()
.downcast_ref::<crate::history_cell::RunningToolCallCell>()
{
return Some(HistoryRecord::RunningTool(running_tool.state().clone()));
}
if let Some(plan) = cell
.as_any()
.downcast_ref::<crate::history_cell::PlanUpdateCell>()
{
return Some(HistoryRecord::PlanUpdate(plan.state().clone()));
}
if let Some(upgrade) = cell
.as_any()
.downcast_ref::<crate::history_cell::UpgradeNoticeCell>()
{
return Some(HistoryRecord::UpgradeNotice(upgrade.state().clone()));
}
if let Some(reasoning) = cell
.as_any()
.downcast_ref::<crate::history_cell::CollapsibleReasoningCell>()
{
return Some(HistoryRecord::Reasoning(reasoning.reasoning_state()));
}
if let Some(exec) = cell
.as_any()
.downcast_ref::<crate::history_cell::ExecCell>()
{
return Some(HistoryRecord::Exec(exec.record.clone()));
}
if let Some(stream) = cell
.as_any()
.downcast_ref::<crate::history_cell::StreamingContentCell>()
{
return Some(HistoryRecord::AssistantStream(stream.state().clone()));
}
if let Some(assistant) = cell
.as_any()
.downcast_ref::<crate::history_cell::AssistantMarkdownCell>()
{
return Some(HistoryRecord::AssistantMessage(assistant.state().clone()));
}
if let Some(diff) = cell
.as_any()
.downcast_ref::<crate::history_cell::DiffCell>()
{
return Some(HistoryRecord::Diff(diff.record().clone()));
}
if let Some(image) = cell
.as_any()
.downcast_ref::<crate::history_cell::ImageOutputCell>()
{
return Some(HistoryRecord::Image(image.record().clone()));
}
if let Some(patch) = cell
.as_any()
.downcast_ref::<crate::history_cell::PatchSummaryCell>()
{
return Some(HistoryRecord::Patch(patch.record().clone()));
}
None
}
/// Push a cell using a synthetic global order key at the bottom of the current request.
pub(crate) fn history_push(&mut self, cell: impl HistoryCell + 'static) {
#[cfg(debug_assertions)]
@@ -4406,18 +4633,29 @@ impl ChatWidget<'_> {
);
}
let key = self.next_internal_key();
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "epilogue");
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "epilogue", None);
}
fn history_insert_plain_cell_with_key(
&mut self,
cell: crate::history_cell::PlainHistoryCell,
key: OrderKey,
tag: &'static str,
) -> usize {
let record = HistoryRecord::PlainMessage(cell.state().clone());
self.history_insert_with_key_global_tagged(Box::new(cell), key, tag, Some(record))
}
fn history_push_plain_cell(&mut self, cell: crate::history_cell::PlainHistoryCell) {
let key = self.next_internal_key();
let _ = self.history_insert_plain_cell_with_key(cell, key, "epilogue");
}
fn history_push_diff(&mut self, title: Option<String>, diff_output: String) {
let mut record = history_cell::diff_record_from_string(
let record = history_cell::diff_record_from_string(
title.unwrap_or_default(),
&diff_output,
);
let id = self
.history_state
.push(HistoryRecord::Diff(record.clone()));
record.id = id;
let cell = history_cell::DiffCell::from_record(record);
self.history_push(cell);
}
@@ -4468,17 +4706,20 @@ impl ChatWidget<'_> {
/// Push a cell using a synthetic key at the TOP of the NEXT request.
fn history_push_top_next_req(&mut self, cell: impl HistoryCell + 'static) {
let key = self.next_req_key_top();
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "prelude");
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "prelude", None);
}
/// Push a user prompt so it appears right under banners and above model output for the next request.
fn history_push_prompt_next_req(&mut self, cell: impl HistoryCell + 'static) {
let key = self.next_req_key_prompt();
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "prompt");
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "prompt", None);
}
fn history_replace_at(&mut self, idx: usize, cell: Box<dyn HistoryCell>) {
if idx < self.history_cells.len() {
self.history_cells[idx] = cell;
if idx < self.history_cell_ids.len() {
self.history_cell_ids[idx] = None;
}
self.invalidate_height_cache();
self.request_redraw();
self.refresh_explore_trailing_flags();
@@ -4503,6 +4744,9 @@ impl ChatWidget<'_> {
fn history_remove_at(&mut self, idx: usize) {
if idx < self.history_cells.len() {
self.history_cells.remove(idx);
if idx < self.history_cell_ids.len() {
self.history_cell_ids.remove(idx);
}
if idx < self.cell_order_seq.len() {
self.cell_order_seq.remove(idx);
}
@@ -4762,7 +5006,7 @@ impl ChatWidget<'_> {
"command: {}",
original_text.trim()
)));
self.history_push(crate::history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(crate::history_cell::PlainHistoryCell::new(
ack,
crate::history_cell::HistoryCellType::Notice,
));
@@ -4817,7 +5061,7 @@ impl ChatWidget<'_> {
}
)));
lines.push(Line::from(format!("command: {}", original_text.trim())));
self.history_push(crate::history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(crate::history_cell::PlainHistoryCell::new(
lines,
crate::history_cell::HistoryCellType::Notice,
));
@@ -6874,9 +7118,10 @@ impl ChatWidget<'_> {
if let Some(idx) = resolved_idx {
self.history_replace_at(idx, Box::new(wait_cancelled_cell));
} else {
let _ = self.history_insert_with_key_global(
Box::new(wait_cancelled_cell),
let _ = self.history_insert_plain_cell_with_key(
wait_cancelled_cell,
ok,
"untagged",
);
}
@@ -7170,7 +7415,7 @@ impl ChatWidget<'_> {
for line in prompt_text.lines() {
lines.push(Line::from(line.to_string()));
}
self.history_push(history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(history_cell::PlainHistoryCell::new(
lines,
history_cell::HistoryCellType::Notice,
));
@@ -7543,7 +7788,7 @@ impl ChatWidget<'_> {
for l in text.lines() {
lines.push(ratatui::text::Line::from(l.to_string()))
}
self.history_push(crate::history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(crate::history_cell::PlainHistoryCell::new(
lines,
crate::history_cell::HistoryCellType::Notice,
));
@@ -7849,7 +8094,7 @@ impl ChatWidget<'_> {
}
}
self.history_push(crate::history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(crate::history_cell::PlainHistoryCell::new(
lines,
crate::history_cell::HistoryCellType::Notice,
));
@@ -10442,7 +10687,7 @@ impl ChatWidget<'_> {
}
// Insert new status near the top of this request window
let key = self.near_time_key(None);
let pos = self.history_insert_with_key_global_tagged(Box::new(cell), key, "background");
let pos = self.history_insert_with_key_global_tagged(Box::new(cell), key, "background", None);
self.access_status_idx = Some(pos);
}
@@ -10672,7 +10917,10 @@ impl ChatWidget<'_> {
pub(crate) fn undo_jump_back(&mut self) {
if let Some(mut st) = self.pending_jump_back.take() {
// Restore removed cells in original order
self.history_cells.extend(st.removed_cells.drain(..));
for cell in st.removed_cells.drain(..) {
self.history_cell_ids.push(None);
self.history_cells.push(cell);
}
// Clear composer (no reliable way to restore prior text)
self.insert_str("");
self.request_redraw();
@@ -11699,7 +11947,7 @@ impl ChatWidget<'_> {
}
// Add status message
self.history_push(history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(history_cell::PlainHistoryCell::new(
vec![Line::from("✅ Chrome launched with user profile")],
history_cell::HistoryCellType::BackgroundEvent,
));
@@ -12324,7 +12572,7 @@ impl ChatWidget<'_> {
}
// Add status message
self.history_push(history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(history_cell::PlainHistoryCell::new(
vec![Line::from(format!(
"✅ Chrome launched with temporary profile at {}",
profile_dir.display()
@@ -12433,7 +12681,7 @@ impl ChatWidget<'_> {
// Add status message
let status_msg = format!("🌐 Opening internal browser: {}", full_url);
self.history_push(history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(history_cell::PlainHistoryCell::new(
vec![Line::from(status_msg)],
history_cell::HistoryCellType::BackgroundEvent,
));
@@ -12831,7 +13079,7 @@ impl ChatWidget<'_> {
.lines()
.map(|line| Line::from(line.to_string()))
.collect();
self.history_push(history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(history_cell::PlainHistoryCell::new(
lines,
history_cell::HistoryCellType::BackgroundEvent,
));
@@ -13627,7 +13875,7 @@ impl ChatWidget<'_> {
.lines()
.map(|line| Line::from(line.to_string()))
.collect();
self.history_push(history_cell::PlainHistoryCell::new(
self.history_push_plain_cell(history_cell::PlainHistoryCell::new(
lines,
history_cell::HistoryCellType::BackgroundEvent,
));
+7 -2
View File
@@ -287,9 +287,12 @@ pub(super) fn create_diff_summary_with_width(
let mut header_spans: Vec<RtSpan<'static>> = Vec::new();
// Colorize title: success for apply events, keep primary for approval requests
let title_style = match event_type {
PatchEventType::ApplyBegin { .. } => Style::default()
PatchEventType::ApplyBegin { .. } | PatchEventType::ApplySuccess => Style::default()
.fg(crate::colors::success())
.add_modifier(Modifier::BOLD),
PatchEventType::ApplyFailure => Style::default()
.fg(crate::colors::error())
.add_modifier(Modifier::BOLD),
PatchEventType::ApprovalRequest => Style::default()
.fg(crate::colors::primary())
.add_modifier(Modifier::BOLD),
@@ -346,7 +349,9 @@ pub(super) fn create_diff_summary_with_width(
event_type,
PatchEventType::ApplyBegin {
auto_approved: true
} | PatchEventType::ApprovalRequest
}
| PatchEventType::ApplySuccess
| PatchEventType::ApprovalRequest
);
if show_details {
+73
View File
@@ -623,6 +623,37 @@ impl HistoryState {
self.next_id = self.next_id.saturating_add(1);
id
}
pub fn apply_event(&mut self, event: HistoryEvent) -> HistoryMutation {
match event {
HistoryEvent::Insert { index, record } => {
let id = self.next_history_id();
let record = record.with_id(id);
let idx = index.min(self.records.len());
self.records.insert(idx, record.clone());
HistoryMutation::Inserted { index: idx, id, record }
}
HistoryEvent::Replace { index, record } => {
if let Some(existing) = self.records.get(index) {
let id = existing.id();
let record = record.with_id(id);
self.records[index] = record.clone();
HistoryMutation::Replaced { index, id, record }
} else {
HistoryMutation::Noop
}
}
HistoryEvent::Remove { index } => {
if index < self.records.len() {
let record = self.records.remove(index);
let id = record.id();
HistoryMutation::Removed { index, id, record }
} else {
HistoryMutation::Noop
}
}
}
}
}
#[allow(dead_code)]
@@ -708,3 +739,45 @@ impl WithId for HistoryRecord {
}
}
}
impl HistoryRecord {
pub fn id(&self) -> HistoryId {
match self {
HistoryRecord::PlainMessage(state) => state.id,
HistoryRecord::WaitStatus(state) => state.id,
HistoryRecord::Loading(state) => state.id,
HistoryRecord::RunningTool(state) => state.id,
HistoryRecord::ToolCall(state) => state.id,
HistoryRecord::PlanUpdate(state) => state.id,
HistoryRecord::UpgradeNotice(state) => state.id,
HistoryRecord::Reasoning(state) => state.id,
HistoryRecord::Exec(state) => state.id,
HistoryRecord::AssistantStream(state) => state.id,
HistoryRecord::AssistantMessage(state) => state.id,
HistoryRecord::Diff(state) => state.id,
HistoryRecord::Image(state) => state.id,
HistoryRecord::Explore(state) => state.id,
HistoryRecord::RateLimits(state) => state.id,
HistoryRecord::Patch(state) => state.id,
HistoryRecord::BackgroundEvent(state) => state.id,
HistoryRecord::Notice(state) => state.id,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub enum HistoryEvent {
Insert { index: usize, record: HistoryRecord },
Replace { index: usize, record: HistoryRecord },
Remove { index: usize },
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub enum HistoryMutation {
Inserted { index: usize, id: HistoryId, record: HistoryRecord },
Replaced { index: usize, id: HistoryId, record: HistoryRecord },
Removed { index: usize, id: HistoryId, record: HistoryRecord },
Noop,
}
@@ -66,6 +66,10 @@ impl AssistantMarkdownCell {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut AssistantMessageState {
&mut self.state
}
fn rebuild(&mut self, cfg: &codex_core::config::Config) {
let mut out: Vec<Line<'static>> = Vec::new();
out.push(Line::from("codex"));
@@ -14,6 +14,14 @@ impl BackgroundEventCell {
Self { state }
}
pub(crate) fn state(&self) -> &BackgroundEventRecord {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut BackgroundEventRecord {
&mut self.state
}
fn lines(&self) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
let dim_style = Style::default().fg(crate::colors::text_dim());
+8
View File
@@ -15,6 +15,14 @@ impl DiffCell {
}
}
pub(crate) fn record(&self) -> &DiffRecord {
&self.record
}
pub(crate) fn record_mut(&mut self) -> &mut DiffRecord {
&mut self.record
}
pub(crate) fn rebuild_with_theme(&self) {
self.layout.borrow_mut().take();
}
+25 -3
View File
@@ -1,4 +1,6 @@
use super::*;
use crate::history::state::{HistoryId, ImageRecord};
use sha2::{Digest, Sha256};
#[derive(Clone)]
pub(crate) struct ImageCellState {
@@ -18,13 +20,33 @@ impl ImageCellState {
pub(crate) struct ImageOutputCell {
state: ImageCellState,
record: ImageRecord,
}
impl ImageOutputCell {
pub(crate) fn new(image: DynamicImage) -> Self {
Self {
state: ImageCellState::from_dynamic(image),
}
let state = ImageCellState::from_dynamic(image);
let width_u16 = state.width.min(u16::MAX as u32) as u16;
let height_u16 = state.height.min(u16::MAX as u32) as u16;
let sha = Sha256::digest(&state.rgba);
let sha_hex = format!("{:x}", sha);
let record = ImageRecord {
id: HistoryId::ZERO,
source_path: None,
alt_text: None,
width: width_u16,
height: height_u16,
sha256: Some(sha_hex),
};
Self { state, record }
}
pub(crate) fn record(&self) -> &ImageRecord {
&self.record
}
pub(crate) fn record_mut(&mut self) -> &mut ImageRecord {
&mut self.record
}
}
+19 -13
View File
@@ -1,26 +1,32 @@
use super::*;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct LoadingCellState {
pub message: String,
}
impl LoadingCellState {
pub(crate) fn new(message: String) -> Self {
Self { message }
}
}
use crate::history::state::{HistoryId, LoadingState};
pub(crate) struct LoadingCell {
state: LoadingCellState,
state: LoadingState,
}
impl LoadingCell {
pub(crate) fn new(message: String) -> Self {
Self {
state: LoadingCellState::new(message),
state: LoadingState {
id: HistoryId::ZERO,
message,
},
}
}
#[allow(dead_code)]
pub(crate) fn from_state(state: LoadingState) -> Self {
Self { state }
}
pub(crate) fn state(&self) -> &LoadingState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut LoadingState {
&mut self.state
}
}
impl HistoryCell for LoadingCell {
+149 -97
View File
@@ -14,10 +14,15 @@ use crate::history::state::{
ExecRecord,
ExecStatus,
HistoryId,
RunningToolState,
PatchEventType as HistoryPatchEventType,
PatchRecord,
PlanIcon,
PlanProgress,
PlanStep,
PlanUpdateState,
ToolCallState,
ToolStatus as HistoryToolStatus,
UpgradeNoticeState,
};
use crate::history::state::{ArgumentValue, ToolArgument, ToolResultPreview};
@@ -94,13 +99,7 @@ pub(crate) use explore::{ExploreAggregationCell, ExploreEntryStatus};
pub(crate) use image::ImageOutputCell;
pub(crate) use loading::LoadingCell;
pub(crate) use reasoning::CollapsibleReasoningCell;
pub(crate) use tool::{
RunningToolCallCell,
RunningToolCallState,
ToolCallCell,
ToolCallCellState,
ToolCallStatus,
};
pub(crate) use tool::{RunningToolCallCell, ToolCallCell};
pub(crate) use wait_status::WaitStatusCell;
pub(crate) use plan_update::PlanUpdateCell;
pub(crate) use plain::PlainHistoryCell;
@@ -120,6 +119,8 @@ pub(crate) struct CommandOutput {
pub(crate) enum PatchEventType {
ApprovalRequest,
ApplyBegin { auto_approved: bool },
ApplySuccess,
ApplyFailure,
}
// ==================== HistoryCellType ====================
@@ -132,7 +133,7 @@ pub(crate) enum HistoryCellType {
Reasoning,
Error,
Exec { kind: ExecKind, status: ExecStatus },
Tool { status: ToolStatus },
Tool { status: ToolCellStatus },
Patch { kind: PatchKind },
PlanUpdate,
BackgroundEvent,
@@ -168,12 +169,22 @@ pub(crate) fn action_enum_from_parsed(
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ToolStatus {
pub(crate) enum ToolCellStatus {
Running,
Success,
Failed,
}
impl From<HistoryToolStatus> for ToolCellStatus {
fn from(status: HistoryToolStatus) -> Self {
match status {
HistoryToolStatus::Running => ToolCellStatus::Running,
HistoryToolStatus::Success => ToolCellStatus::Success,
HistoryToolStatus::Failed => ToolCellStatus::Failed,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PatchKind {
Proposed,
@@ -302,9 +313,9 @@ pub(crate) trait HistoryCell {
HistoryCellType::Reasoning => None,
HistoryCellType::Error => Some("✖"),
HistoryCellType::Tool { status } => Some(match status {
ToolStatus::Running => "⚙",
ToolStatus::Success => "✔",
ToolStatus::Failed => "✖",
ToolCellStatus::Running => "⚙",
ToolCellStatus::Success => "✔",
ToolCellStatus::Failed => "✖",
}),
HistoryCellType::Exec { kind, status } => {
// Show ❯ only for Run executions; hide for read/search/list summaries
@@ -4332,17 +4343,18 @@ fn new_exec_command_generic(
pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> ToolCallCell {
let invocation_line = format_mcp_invocation(invocation);
let invocation_text = line_to_plain_text(&invocation_line);
let state = ToolCallCellState::new(
"Working".to_string(),
ToolCallStatus::Running,
None,
vec![ToolArgument {
let state = ToolCallState {
id: HistoryId::ZERO,
status: HistoryToolStatus::Running,
title: "Working".to_string(),
duration: None,
arguments: vec![ToolArgument {
name: "invocation".to_string(),
value: ArgumentValue::Text(invocation_text),
}],
None,
None,
);
result_preview: None,
error_message: None,
};
ToolCallCell::new(state)
}
@@ -4353,17 +4365,18 @@ pub(crate) fn new_active_custom_tool_call(tool_name: String, args: Option<String
} else {
format!("{}()", tool_name)
};
let state = ToolCallCellState::new(
"Working".to_string(),
ToolCallStatus::Running,
None,
vec![ToolArgument {
let state = ToolCallState {
id: HistoryId::ZERO,
status: HistoryToolStatus::Running,
title: "Working".to_string(),
duration: None,
arguments: vec![ToolArgument {
name: "invocation".to_string(),
value: ArgumentValue::Text(invocation_str),
}],
None,
None,
);
result_preview: None,
error_message: None,
};
ToolCallCell::new(state)
}
@@ -4447,14 +4460,16 @@ pub(crate) fn new_running_browser_tool_call(
arguments.append(&mut kv_args);
}
}
RunningToolCallCell::new(RunningToolCallState::new(
browser_running_title(&tool_name).to_string(),
SystemTime::now(),
let state = RunningToolState {
id: HistoryId::ZERO,
title: browser_running_title(&tool_name).to_string(),
started_at: SystemTime::now(),
arguments,
false,
false,
None,
))
wait_has_target: false,
wait_has_call_id: false,
wait_cap_ms: None,
};
RunningToolCallCell::new(state)
}
fn custom_tool_running_title(tool_name: &str) -> String {
@@ -4534,14 +4549,16 @@ pub(crate) fn new_running_custom_tool_call(
}
}
}
RunningToolCallCell::new(RunningToolCallState::new(
custom_tool_running_title(&tool_name),
SystemTime::now(),
let state = RunningToolState {
id: HistoryId::ZERO,
title: custom_tool_running_title(&tool_name),
started_at: SystemTime::now(),
arguments,
wait_has_target,
wait_has_call_id,
wait_cap_ms,
))
};
RunningToolCallCell::new(state)
}
/// Running web search call (native Responses web_search)
@@ -4553,31 +4570,35 @@ pub(crate) fn new_running_web_search(query: Option<String>) -> RunningToolCallCe
value: ArgumentValue::Text(q),
});
}
RunningToolCallCell::new(RunningToolCallState::new(
"Web Search...".to_string(),
SystemTime::now(),
let state = RunningToolState {
id: HistoryId::ZERO,
title: "Web Search...".to_string(),
started_at: SystemTime::now(),
arguments,
false,
false,
None,
))
wait_has_target: false,
wait_has_call_id: false,
wait_cap_ms: None,
};
RunningToolCallCell::new(state)
}
pub(crate) fn new_running_mcp_tool_call(invocation: McpInvocation) -> RunningToolCallCell {
// Represent as provider.tool(...) on one dim line beneath a generic running header with timer
let line = format_mcp_invocation(invocation);
let invocation_text = line_to_plain_text(&line);
RunningToolCallCell::new(RunningToolCallState::new(
"Working...".to_string(),
SystemTime::now(),
vec![ToolArgument {
let state = RunningToolState {
id: HistoryId::ZERO,
title: "Working...".to_string(),
started_at: SystemTime::now(),
arguments: vec![ToolArgument {
name: "invocation".to_string(),
value: ArgumentValue::Text(invocation_text),
}],
false,
false,
None,
))
wait_has_target: false,
wait_has_call_id: false,
wait_cap_ms: None,
};
RunningToolCallCell::new(state)
}
pub(crate) fn new_completed_custom_tool_call(
@@ -4596,9 +4617,9 @@ pub(crate) fn new_completed_custom_tool_call(
return new_completed_agent_tool_call(tool_name, args, duration, success, result);
}
let status = if success {
ToolCallStatus::Success
HistoryToolStatus::Success
} else {
ToolCallStatus::Failed
HistoryToolStatus::Failed
};
let status_title = if success { "Complete" } else { "Error" };
let invocation_str = if let Some(args) = args.clone() {
@@ -4643,14 +4664,15 @@ pub(crate) fn new_completed_custom_tool_call(
})
};
let state = ToolCallCellState::new(
status_title.to_string(),
let state = ToolCallState {
id: HistoryId::ZERO,
status,
Some(duration),
title: status_title.to_string(),
duration: Some(duration),
arguments,
result_preview,
None,
);
error_message: None,
};
ToolCallCell::new(state)
}
@@ -4730,9 +4752,9 @@ pub(crate) fn new_completed_web_fetch_tool_call(
pre_lines,
body_lines,
state: if success {
ToolCallStatus::Success
ToolCellStatus::Success
} else {
ToolCallStatus::Failed
ToolCellStatus::Failed
},
}
}
@@ -4817,7 +4839,7 @@ fn select_preview_from_plain_text(text: &str, head: usize, tail: usize) -> Vec<L
pub(crate) struct WebFetchToolCell {
pre_lines: Vec<Line<'static>>, // header/invocation
body_lines: Vec<Line<'static>>, // bordered, dim preview
state: ToolCallStatus,
state: ToolCellStatus,
}
impl HistoryCell for WebFetchToolCell {
@@ -4828,13 +4850,7 @@ impl HistoryCell for WebFetchToolCell {
self
}
fn kind(&self) -> HistoryCellType {
HistoryCellType::Tool {
status: match self.state {
ToolCallStatus::Running => ToolStatus::Running,
ToolCallStatus::Success => ToolStatus::Success,
ToolCallStatus::Failed => ToolStatus::Failed,
},
}
HistoryCellType::Tool { status: self.state }
}
fn display_lines(&self) -> Vec<Line<'static>> {
// Fallback textual representation used only for measurement outside custom render
@@ -5213,9 +5229,9 @@ fn new_completed_browser_tool_call(
result: String,
) -> ToolCallCell {
let status = if success {
ToolCallStatus::Success
HistoryToolStatus::Success
} else {
ToolCallStatus::Failed
HistoryToolStatus::Failed
};
let mut arguments: Vec<ToolArgument> = Vec::new();
if let Some(args_str) = args {
@@ -5253,14 +5269,15 @@ fn new_completed_browser_tool_call(
})
};
let state = ToolCallCellState::new(
browser_tool_title(&tool_name).to_string(),
let state = ToolCallState {
id: HistoryId::ZERO,
status,
Some(duration),
title: browser_tool_title(&tool_name).to_string(),
duration: Some(duration),
arguments,
result_preview,
None,
);
error_message: None,
};
ToolCallCell::new(state)
}
@@ -5304,9 +5321,9 @@ fn new_completed_agent_tool_call(
result: String,
) -> ToolCallCell {
let status = if success {
ToolCallStatus::Success
HistoryToolStatus::Success
} else {
ToolCallStatus::Failed
HistoryToolStatus::Failed
};
let mut arguments: Vec<ToolArgument> = Vec::new();
if let Some(args_str) = args {
@@ -5335,14 +5352,15 @@ fn new_completed_agent_tool_call(
})
};
let state = ToolCallCellState::new(
agent_tool_title(&tool_name),
let state = ToolCallState {
id: HistoryId::ZERO,
status,
Some(duration),
title: agent_tool_title(&tool_name),
duration: Some(duration),
arguments,
result_preview,
None,
);
error_message: None,
};
ToolCallCell::new(state)
}
@@ -5397,9 +5415,9 @@ pub(crate) fn new_completed_mcp_tool_call(
}
let status = if success {
ToolCallStatus::Success
HistoryToolStatus::Success
} else {
ToolCallStatus::Failed
HistoryToolStatus::Failed
};
let invocation_line = format_mcp_invocation(invocation);
@@ -5459,14 +5477,15 @@ pub(crate) fn new_completed_mcp_tool_call(
})
};
let state = ToolCallCellState::new(
if success { "Complete" } else { "Error" }.to_string(),
let state = ToolCallState {
id: HistoryId::ZERO,
status,
Some(duration),
title: if success { "Complete" } else { "Error" }.to_string(),
duration: Some(duration),
arguments,
result_preview,
error_message,
);
};
Box::new(ToolCallCell::new(state))
}
@@ -5826,16 +5845,31 @@ pub(crate) fn new_patch_event(
let title = match event_type {
PatchEventType::ApprovalRequest => "proposed patch".to_string(),
PatchEventType::ApplyBegin { .. } => "Updated".to_string(),
PatchEventType::ApplySuccess => "Patch applied".to_string(),
PatchEventType::ApplyFailure => "Patch failed".to_string(),
};
let kind = match event_type {
PatchEventType::ApprovalRequest => PatchKind::Proposed,
PatchEventType::ApplyBegin { .. } => PatchKind::ApplyBegin,
PatchEventType::ApplySuccess => PatchKind::ApplySuccess,
PatchEventType::ApplyFailure => PatchKind::ApplyFailure,
};
let record = PatchRecord {
id: HistoryId::ZERO,
patch_type: match event_type {
PatchEventType::ApprovalRequest => HistoryPatchEventType::ApprovalRequest,
PatchEventType::ApplyBegin { auto_approved } => {
HistoryPatchEventType::ApplyBegin { auto_approved }
}
PatchEventType::ApplySuccess => HistoryPatchEventType::ApplySuccess,
PatchEventType::ApplyFailure => HistoryPatchEventType::ApplyFailure,
},
changes,
};
PatchSummaryCell {
title,
changes,
event_type,
kind,
record,
cached: std::cell::RefCell::new(None),
}
}
@@ -5879,9 +5913,8 @@ pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
pub(crate) struct PatchSummaryCell {
pub(crate) title: String,
pub(crate) changes: HashMap<PathBuf, FileChange>,
pub(crate) event_type: PatchEventType,
pub(crate) kind: PatchKind,
pub(crate) record: PatchRecord,
// Cache width-specific rendered lines to avoid repeated filesystem reads
// and pre-wrapping work inside create_diff_summary_with_width.
cached: std::cell::RefCell<Option<PatchLayoutCache>>,
@@ -5894,6 +5927,25 @@ struct PatchLayoutCache {
}
impl PatchSummaryCell {
fn ui_event_type(&self) -> PatchEventType {
match self.record.patch_type {
HistoryPatchEventType::ApprovalRequest => PatchEventType::ApprovalRequest,
HistoryPatchEventType::ApplyBegin { auto_approved } => {
PatchEventType::ApplyBegin { auto_approved }
}
HistoryPatchEventType::ApplySuccess => PatchEventType::ApplySuccess,
HistoryPatchEventType::ApplyFailure => PatchEventType::ApplyFailure,
}
}
pub(crate) fn record(&self) -> &PatchRecord {
&self.record
}
pub(crate) fn record_mut(&mut self) -> &mut PatchRecord {
&mut self.record
}
fn ensure_lines(&self, width: u16) -> Vec<Line<'static>> {
if let Some(c) = self.cached.borrow().as_ref() {
if c.width == width {
@@ -5902,8 +5954,8 @@ impl PatchSummaryCell {
}
let lines: Vec<Line<'static>> = create_diff_summary_with_width(
&self.title,
&self.changes,
self.event_type,
&self.record.changes,
self.ui_event_type(),
Some(width as usize),
)
.into_iter()
+16 -1
View File
@@ -1,7 +1,7 @@
//! Rendering for structured plan update history cells built from `PlanUpdateState`.
use super::*;
use crate::history::state::{PlanIcon, PlanProgress, PlanStep, PlanUpdateState};
use crate::history::state::{HistoryId, PlanIcon, PlanProgress, PlanStep, PlanUpdateState};
use codex_core::plan_tool::StepStatus;
pub(crate) struct PlanUpdateCell {
@@ -10,6 +10,8 @@ pub(crate) struct PlanUpdateCell {
impl PlanUpdateCell {
pub(crate) fn new(state: PlanUpdateState) -> Self {
let mut state = state;
state.id = HistoryId::ZERO;
Self { state }
}
@@ -18,6 +20,19 @@ impl PlanUpdateCell {
progress.total > 0 && progress.completed >= progress.total
}
#[allow(dead_code)]
pub(crate) fn from_state(state: PlanUpdateState) -> Self {
Self { state }
}
pub(crate) fn state(&self) -> &PlanUpdateState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut PlanUpdateState {
&mut self.state
}
fn header_line(&self) -> Line<'static> {
let progress = &self.state.progress;
let is_complete = self.is_complete();
+24 -1
View File
@@ -4,7 +4,14 @@ use super::semantic::SemanticLine;
use super::text;
use super::*;
use crate::history::state::{
BulletMarker, InlineSpan, ReasoningBlock, ReasoningSection, TextEmphasis, TextTone,
BulletMarker,
HistoryId,
InlineSpan,
ReasoningBlock,
ReasoningSection,
ReasoningState,
TextEmphasis,
TextTone,
};
use crate::history_cell::assistant::detect_bullet_prefix;
use crate::render::line_utils;
@@ -19,6 +26,7 @@ pub(crate) struct CollapsibleReasoningState {
pub in_progress: bool,
pub hide_when_collapsed: bool,
pub id: Option<String>,
pub history_id: HistoryId,
}
#[derive(Clone, Debug, PartialEq)]
@@ -43,6 +51,7 @@ impl CollapsibleReasoningState {
in_progress: false,
hide_when_collapsed: false,
id,
history_id: HistoryId::ZERO,
}
}
}
@@ -114,6 +123,20 @@ impl CollapsibleReasoningCell {
let theme = crate::theme::current_theme();
debug_title_overlay(&sections_to_ratatui_lines(&state.sections, &theme))
}
pub(crate) fn set_history_id(&self, id: HistoryId) {
self.state.borrow_mut().history_id = id;
}
pub(crate) fn reasoning_state(&self) -> ReasoningState {
let state = self.state.borrow();
ReasoningState {
id: state.history_id,
sections: state.sections.clone(),
effort: None,
in_progress: state.in_progress,
}
}
}
impl HistoryCell for CollapsibleReasoningCell {
+8
View File
@@ -301,4 +301,12 @@ impl StreamingContentCell {
fn ensure_stream_layout(&self, width: u16) -> AssistantLayoutCache {
self.assistant.ensure_layout(width)
}
pub(crate) fn state(&self) -> &AssistantStreamState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut AssistantStreamState {
&mut self.state
}
}
+60 -90
View File
@@ -1,65 +1,50 @@
//! Tool call history cells driven by structured argument/result state.
use super::*;
use crate::history::state::{ArgumentValue, ToolArgument, ToolResultPreview};
use crate::history::state::{
ArgumentValue,
HistoryId,
RunningToolState,
ToolArgument,
ToolCallState,
ToolStatus as HistoryToolStatus,
};
use crate::text_formatting::format_json_compact;
use std::time::{Duration, Instant, SystemTime};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ToolCallStatus {
Running,
Success,
Failed,
}
#[derive(Clone, Debug)]
pub(crate) struct ToolCallCellState {
pub title: String,
pub status: ToolCallStatus,
pub duration: Option<Duration>,
pub arguments: Vec<ToolArgument>,
pub result_preview: Option<ToolResultPreview>,
pub error_message: Option<String>,
}
impl ToolCallCellState {
pub(crate) fn new(
title: String,
status: ToolCallStatus,
duration: Option<Duration>,
arguments: Vec<ToolArgument>,
result_preview: Option<ToolResultPreview>,
error_message: Option<String>,
) -> Self {
Self {
title,
status,
duration,
arguments,
result_preview,
error_message,
}
}
}
pub(crate) struct ToolCallCell {
state: ToolCallCellState,
state: ToolCallState,
}
impl ToolCallCell {
pub(crate) fn new(state: ToolCallCellState) -> Self {
pub(crate) fn new(state: ToolCallState) -> Self {
let mut state = state;
state.id = HistoryId::ZERO;
Self { state }
}
#[allow(dead_code)]
pub(crate) fn from_state(state: ToolCallState) -> Self {
Self { state }
}
pub(crate) fn state(&self) -> &ToolCallState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut ToolCallState {
&mut self.state
}
pub(crate) fn retint(&mut self, _old: &crate::theme::Theme, _new: &crate::theme::Theme) {}
fn header_line(&self) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut style = Style::default().add_modifier(Modifier::BOLD);
style = match self.state.status {
ToolCallStatus::Running => style.fg(crate::colors::info()),
ToolCallStatus::Success => style.fg(crate::colors::success()),
ToolCallStatus::Failed => style.fg(crate::colors::error()),
HistoryToolStatus::Running => style.fg(crate::colors::info()),
HistoryToolStatus::Success => style.fg(crate::colors::success()),
HistoryToolStatus::Failed => style.fg(crate::colors::error()),
};
spans.push(Span::styled(self.state.title.clone(), style));
if let Some(duration) = self.state.duration {
@@ -83,11 +68,7 @@ impl HistoryCell for ToolCallCell {
fn kind(&self) -> HistoryCellType {
HistoryCellType::Tool {
status: match self.state.status {
ToolCallStatus::Running => ToolStatus::Running,
ToolCallStatus::Success => ToolStatus::Success,
ToolCallStatus::Failed => ToolStatus::Failed,
},
status: super::ToolCellStatus::from(self.state.status),
}
}
@@ -129,49 +110,37 @@ impl HistoryCell for ToolCallCell {
}
}
#[derive(Clone, Debug)]
pub(crate) struct RunningToolCallState {
pub title: String,
pub started_at: SystemTime,
pub arguments: Vec<ToolArgument>,
pub wait_has_target: bool,
pub wait_has_call_id: bool,
pub wait_cap_ms: Option<u64>,
}
impl RunningToolCallState {
pub(crate) fn new(
title: String,
started_at: SystemTime,
arguments: Vec<ToolArgument>,
wait_has_target: bool,
wait_has_call_id: bool,
wait_cap_ms: Option<u64>,
) -> Self {
Self {
title,
started_at,
arguments,
wait_has_target,
wait_has_call_id,
wait_cap_ms,
}
}
}
pub(crate) struct RunningToolCallCell {
state: RunningToolCallState,
state: RunningToolState,
start_clock: Instant,
}
impl RunningToolCallCell {
pub(crate) fn new(state: RunningToolCallState) -> Self {
pub(crate) fn new(state: RunningToolState) -> Self {
let mut state = state;
state.id = HistoryId::ZERO;
Self {
state,
start_clock: Instant::now(),
}
}
#[allow(dead_code)]
pub(crate) fn from_state(state: RunningToolState) -> Self {
Self {
state,
start_clock: Instant::now(),
}
}
pub(crate) fn state(&self) -> &RunningToolState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut RunningToolState {
&mut self.state
}
fn strip_zero_seconds_suffix(mut duration: String) -> String {
if duration.ends_with(" 00s") {
duration.truncate(duration.len().saturating_sub(4));
@@ -203,22 +172,23 @@ impl RunningToolCallCell {
});
}
let status = if success {
ToolCallStatus::Success
HistoryToolStatus::Success
} else {
ToolCallStatus::Failed
HistoryToolStatus::Failed
};
let state = ToolCallCellState::new(
if success {
let state = ToolCallState {
id: HistoryId::ZERO,
status,
title: if success {
"Web Search".to_string()
} else {
"Web Search (failed)".to_string()
},
status,
Some(duration),
duration: Some(duration),
arguments,
None,
None,
);
result_preview: None,
error_message: None,
};
ToolCallCell::new(state)
}
@@ -240,7 +210,7 @@ impl HistoryCell for RunningToolCallCell {
fn kind(&self) -> HistoryCellType {
HistoryCellType::Tool {
status: ToolStatus::Running,
status: super::ToolCellStatus::Running,
}
}
+20 -1
View File
@@ -1,7 +1,7 @@
//! Upgrade notice cell built from `UpgradeNoticeState` metadata.
use super::*;
use crate::history::state::UpgradeNoticeState;
use crate::history::state::{HistoryId, UpgradeNoticeState};
const TARGET_WIDTH: u16 = 70;
@@ -13,6 +13,17 @@ pub(crate) struct UpgradeNoticeCell {
impl UpgradeNoticeCell {
pub(crate) fn new(state: UpgradeNoticeState) -> Self {
let mut state = state;
state.id = HistoryId::ZERO;
Self::with_state(state)
}
#[allow(dead_code)]
pub(crate) fn from_state(state: UpgradeNoticeState) -> Self {
Self::with_state(state)
}
fn with_state(state: UpgradeNoticeState) -> Self {
let primary = crate::colors::primary();
let backdrop = crate::colors::mix_toward(primary, crate::colors::background(), 0.95);
let border_style = Style::default().bg(backdrop).fg(primary);
@@ -23,6 +34,14 @@ impl UpgradeNoticeCell {
}
}
pub(crate) fn state(&self) -> &UpgradeNoticeState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut UpgradeNoticeState {
&mut self.state
}
fn text(&self) -> Text<'static> {
Text::from(self.message_lines())
}
@@ -15,6 +15,19 @@ impl WaitStatusCell {
}
pub(crate) fn retint(&mut self, _old: &crate::theme::Theme, _new: &crate::theme::Theme) {}
#[allow(dead_code)]
pub(crate) fn from_state(state: WaitStatusState) -> Self {
Self { state }
}
pub(crate) fn state(&self) -> &WaitStatusState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut WaitStatusState {
&mut self.state
}
}
impl HistoryCell for WaitStatusCell {