mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(tui/history): route diff and explore through domain events
This commit is contained in:
@@ -159,14 +159,14 @@ 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:** [~] `history_insert_with_key_global_tagged` now mirrors inserts into `HistoryState` to assign `HistoryId`s, but the `history_cells: Vec<Box<dyn HistoryCell>>` is still the primary source of truth and several helpers reconstruct `HistoryRecord` values from existing cells. Next steps: migrate the remaining rate-limit/system notice paths, then flip the ownership so `HistoryState` drives rendering and cells become a derived cache only.
|
||||
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:** [~] `HistoryState::apply_domain_event` now converts `HistoryDomainRecord` values into `HistoryRecord`s so background/system notices insert via domain events; remaining flows still emit bespoke mutations (e.g., patch success). Follow-up: add typed handlers per event family, eliminate `history_record_from_cell`, and collapse direct cell mutation once Step 3.5 is ready.
|
||||
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:** ✅ Wait tooling, diff inserts, and explore aggregations now flow through domain events; `history_cells` remains solely as the interim render cache until Step 3.5 removes the trait bridge.
|
||||
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:** ✅ Exec wait notes, diff pushes, and explore status updates now call into `HistoryState::apply_domain_event`; next wave is wiring streaming assistant/diff updates before the Step 4 renderer consolidation.
|
||||
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)
|
||||
- Patch apply success/failure currently mutates `PatchSummaryCell` in place; emit `HistoryEvent::Replace` once the replace arm is implemented so records stay authoritative.
|
||||
- Several helpers still rebuild cells inline (e.g., patch success) instead of emitting domain-specific events; consolidate around `HistoryState::apply_event` so the legacy `history_cells` vector can be retired.
|
||||
### Outstanding Gaps (2025-09-27)
|
||||
- Apply-patch failures still surface a minimal `PatchRecord` without stderr context; consider storing structured failure metadata (message, stdout/stderr digest) so the renderer can show richer feedback while remaining state-driven.
|
||||
- Several helpers still rebuild cells inline (e.g., merged exec summaries) instead of emitting domain-specific events; consolidate around `HistoryState::apply_event` so the legacy `history_cells` vector can be retired.
|
||||
- Track exec/assistant/diff layout caches as an intentional bridge; remove them only after the shared renderer cache in Step 6 ships to avoid frame-time regressions.
|
||||
|
||||
### Event Pipeline Consolidation Plan
|
||||
|
||||
+407
-231
@@ -212,6 +212,7 @@ use crate::history::state::PatchEventType as HistoryPatchEventType;
|
||||
use crate::history::state::{
|
||||
HistoryDomainEvent,
|
||||
HistoryDomainRecord,
|
||||
ExecWaitNote,
|
||||
AssistantMessageState,
|
||||
AssistantStreamDelta,
|
||||
AssistantStreamState,
|
||||
@@ -2257,101 +2258,38 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
)
|
||||
}) {
|
||||
if let Some(summary) = self.history_cells[idx]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::PatchSummaryCell>()
|
||||
if let Some(record) = self
|
||||
.history_cells
|
||||
.get(idx)
|
||||
.and_then(|existing| self.history_record_from_cell(existing.as_ref()))
|
||||
{
|
||||
let mut updated = history_cell::new_patch_event(
|
||||
PatchEventType::ApplySuccess,
|
||||
summary.record().changes.clone(),
|
||||
);
|
||||
updated.title = "Updated".to_string();
|
||||
updated.kind = history_cell::PatchKind::ApplySuccess;
|
||||
updated.record_mut().patch_type = HistoryPatchEventType::ApplySuccess;
|
||||
self.history_replace_at(idx, Box::new(updated));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(cell) = self.history_cells.get_mut(idx) {
|
||||
if let Some(plain) = cell
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::PlainHistoryCell>()
|
||||
{
|
||||
let state = plain.state_mut();
|
||||
if let Some(header) = state.header.as_mut() {
|
||||
header.label = "Updated".to_string();
|
||||
}
|
||||
if let Some(first_line) = state.lines.first_mut() {
|
||||
if first_line.spans.is_empty() {
|
||||
first_line.kind = crate::history::MessageLineKind::Paragraph;
|
||||
first_line.spans.push(crate::history::InlineSpan {
|
||||
text: "Updated".to_string(),
|
||||
tone: crate::history::TextTone::Success,
|
||||
emphasis: crate::history::TextEmphasis {
|
||||
bold: true,
|
||||
italic: false,
|
||||
dim: false,
|
||||
strike: false,
|
||||
underline: false,
|
||||
},
|
||||
entity: None,
|
||||
});
|
||||
} else {
|
||||
for span in &mut first_line.spans {
|
||||
span.tone = crate::history::TextTone::Success;
|
||||
span.emphasis.bold = true;
|
||||
span.emphasis.dim = false;
|
||||
}
|
||||
first_line.spans[0].text = "Updated".to_string();
|
||||
}
|
||||
}
|
||||
plain.set_kind(history_cell::HistoryCellType::Patch {
|
||||
kind: history_cell::PatchKind::ApplySuccess,
|
||||
if let HistoryRecord::Patch(mut patch_record) = record {
|
||||
patch_record.patch_type = HistoryPatchEventType::ApplySuccess;
|
||||
let record_index = self
|
||||
.record_index_for_cell(idx)
|
||||
.unwrap_or_else(|| self.record_index_for_position(idx));
|
||||
let mutation = self.history_state.apply_event(HistoryEvent::Replace {
|
||||
index: record_index,
|
||||
record: HistoryRecord::Patch(patch_record.clone()),
|
||||
});
|
||||
plain.invalidate_layout_cache();
|
||||
}
|
||||
}
|
||||
|
||||
let record = self.history_record_from_cell(self.history_cells[idx].as_ref());
|
||||
if let Some(record) = record {
|
||||
if let Some(record_index) = self.record_index_for_cell(idx) {
|
||||
if let HistoryMutation::Replaced { id, .. } = self
|
||||
.history_state
|
||||
.apply_event(HistoryEvent::Replace { index: record_index, record })
|
||||
{
|
||||
if let Some(cell) = self.history_cells.get_mut(idx) {
|
||||
if let Some(plain_cell) = cell
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::PlainHistoryCell>()
|
||||
{
|
||||
let state = plain_cell.state_mut();
|
||||
state.id = id;
|
||||
}
|
||||
}
|
||||
if let Some(id) = self.apply_mutation_to_cell_index(idx, mutation) {
|
||||
if idx < self.history_cell_ids.len() {
|
||||
self.history_cell_ids[idx] = Some(id);
|
||||
}
|
||||
self.maybe_hide_spinner();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"history-state mismatch: no persisted index for patch cell {}",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
// If nothing to update, fall through without inserting a duplicate cell.
|
||||
} else {
|
||||
let key = self.next_internal_key();
|
||||
let _ = self.history_insert_with_key_global(
|
||||
Box::new(history_cell::new_patch_apply_failure(ev.stderr)),
|
||||
key,
|
||||
);
|
||||
self.maybe_hide_spinner();
|
||||
return;
|
||||
}
|
||||
// After patch application completes, re-evaluate idle state
|
||||
|
||||
// failure path
|
||||
let failure_cell = history_cell::new_patch_apply_failure(ev.stderr);
|
||||
let key = self.next_internal_key();
|
||||
let _ = self.history_insert_plain_cell_with_key(failure_cell, key, "patch-failure");
|
||||
self.maybe_hide_spinner();
|
||||
}
|
||||
|
||||
@@ -4406,41 +4344,35 @@ impl ChatWidget<'_> {
|
||||
|
||||
let mut cell = cell;
|
||||
|
||||
let maybe_id = if let Some(domain_record) = record {
|
||||
let mutation = if let Some(domain_record) = record {
|
||||
let record_index = self.record_index_for_position(pos);
|
||||
match self.history_state.apply_domain_event(HistoryDomainEvent::Insert {
|
||||
index: record_index,
|
||||
record: domain_record,
|
||||
}) {
|
||||
HistoryMutation::Inserted { id, record, .. } => {
|
||||
if !self.hydrate_cell_from_record(&mut cell, &record) {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
}
|
||||
Some(id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
Some(
|
||||
self.history_state
|
||||
.apply_domain_event(HistoryDomainEvent::Insert {
|
||||
index: record_index,
|
||||
record: domain_record,
|
||||
}),
|
||||
)
|
||||
} else if let Some(record) = self.history_record_from_cell(cell.as_ref()) {
|
||||
let record_index = self.record_index_for_position(pos);
|
||||
match self
|
||||
.history_state
|
||||
.apply_event(HistoryEvent::Insert {
|
||||
index: record_index,
|
||||
record,
|
||||
})
|
||||
{
|
||||
HistoryMutation::Inserted { id, record, .. } => {
|
||||
if !self.hydrate_cell_from_record(&mut cell, &record) {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
}
|
||||
Some(id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
Some(
|
||||
self.history_state
|
||||
.apply_event(HistoryEvent::Insert {
|
||||
index: record_index,
|
||||
record,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut maybe_id = None;
|
||||
if let Some(mutation) = mutation {
|
||||
if let Some(id) = self.apply_mutation_to_cell(&mut cell, mutation) {
|
||||
maybe_id = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -4498,10 +4430,96 @@ impl ChatWidget<'_> {
|
||||
pos
|
||||
}
|
||||
|
||||
fn append_wait_pairs(target: &mut Vec<(String, bool)>, additions: &[(String, bool)]) {
|
||||
for (text, is_error) in additions {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if target
|
||||
.last()
|
||||
.map(|(existing, existing_err)| existing == trimmed && *existing_err == *is_error)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
target.push((trimmed.to_string(), *is_error));
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_pairs_from_exec_notes(notes: &[ExecWaitNote]) -> Vec<(String, bool)> {
|
||||
notes
|
||||
.iter()
|
||||
.map(|note| {
|
||||
(
|
||||
note.message.clone(),
|
||||
matches!(note.tone, TextTone::Error),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_exec_wait_state_with_pairs(
|
||||
&mut self,
|
||||
history_idx: usize,
|
||||
total_wait: Option<Duration>,
|
||||
wait_active: bool,
|
||||
notes: &[(String, bool)],
|
||||
) -> bool {
|
||||
if history_idx >= self.history_cells.len() {
|
||||
return false;
|
||||
}
|
||||
let record_idx = self
|
||||
.record_index_for_cell(history_idx)
|
||||
.unwrap_or_else(|| self.record_index_for_position(history_idx));
|
||||
let note_records: Vec<ExecWaitNote> = notes
|
||||
.iter()
|
||||
.filter_map(|(text, is_error)| {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ExecWaitNote {
|
||||
message: trimmed.to_string(),
|
||||
tone: if *is_error {
|
||||
TextTone::Error
|
||||
} else {
|
||||
TextTone::Info
|
||||
},
|
||||
timestamp: SystemTime::now(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mutation = self.history_state.apply_domain_event(HistoryDomainEvent::UpdateExecWait {
|
||||
index: record_idx,
|
||||
total_wait,
|
||||
wait_active,
|
||||
notes: note_records,
|
||||
});
|
||||
if let Some(id) = self.apply_mutation_to_cell_index(history_idx, mutation) {
|
||||
if history_idx < self.history_cell_ids.len() {
|
||||
self.history_cell_ids[history_idx] = Some(id);
|
||||
}
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn hydrate_cell_from_record(
|
||||
&self,
|
||||
cell: &mut Box<dyn HistoryCell>,
|
||||
record: &HistoryRecord,
|
||||
) -> bool {
|
||||
Self::hydrate_cell_from_record_inner(cell, record, &self.config)
|
||||
}
|
||||
|
||||
fn hydrate_cell_from_record_inner(
|
||||
cell: &mut Box<dyn HistoryCell>,
|
||||
record: &HistoryRecord,
|
||||
config: &Config,
|
||||
) -> bool {
|
||||
match record {
|
||||
HistoryRecord::PlainMessage(state) => {
|
||||
@@ -4554,7 +4572,7 @@ impl ChatWidget<'_> {
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::history_cell::StreamingContentCell>()
|
||||
{
|
||||
stream.update_from_state(state.clone(), &self.config);
|
||||
stream.update_from_state(state.clone(), config);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4567,12 +4585,180 @@ impl ChatWidget<'_> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
HistoryRecord::Patch(state) => {
|
||||
if let Some(patch) = cell
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::history_cell::PatchSummaryCell>()
|
||||
{
|
||||
*patch.record_mut() = state.clone();
|
||||
patch.invalidate_cache();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
HistoryRecord::Image(state) => {
|
||||
if let Some(image) = cell
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::history_cell::ImageOutputCell>()
|
||||
{
|
||||
*image.record_mut() = state.clone();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn build_cell_from_record(&self, record: &HistoryRecord) -> Option<Box<dyn HistoryCell>> {
|
||||
use crate::history_cell;
|
||||
|
||||
match record {
|
||||
HistoryRecord::PlainMessage(state) => Some(Box::new(
|
||||
history_cell::PlainHistoryCell::from_state(state.clone()),
|
||||
)),
|
||||
HistoryRecord::WaitStatus(state) => {
|
||||
Some(Box::new(history_cell::WaitStatusCell::from_state(state.clone())))
|
||||
}
|
||||
HistoryRecord::Loading(state) => {
|
||||
Some(Box::new(history_cell::LoadingCell::from_state(state.clone())))
|
||||
}
|
||||
HistoryRecord::RunningTool(state) => Some(Box::new(
|
||||
history_cell::RunningToolCallCell::from_state(state.clone()),
|
||||
)),
|
||||
HistoryRecord::ToolCall(state) => Some(Box::new(
|
||||
history_cell::ToolCallCell::from_state(state.clone()),
|
||||
)),
|
||||
HistoryRecord::PlanUpdate(state) => Some(Box::new(
|
||||
history_cell::PlanUpdateCell::from_state(state.clone()),
|
||||
)),
|
||||
HistoryRecord::UpgradeNotice(state) => Some(Box::new(
|
||||
history_cell::UpgradeNoticeCell::from_state(state.clone()),
|
||||
)),
|
||||
HistoryRecord::Reasoning(state) => Some(Box::new(
|
||||
history_cell::CollapsibleReasoningCell::from_state(state.clone()),
|
||||
)),
|
||||
HistoryRecord::Exec(state) => {
|
||||
Some(Box::new(history_cell::ExecCell::from_record(state.clone())))
|
||||
}
|
||||
HistoryRecord::AssistantStream(state) => Some(Box::new(
|
||||
history_cell::StreamingContentCell::from_state(state.clone(), &self.config),
|
||||
)),
|
||||
HistoryRecord::AssistantMessage(state) => Some(Box::new(
|
||||
history_cell::AssistantMarkdownCell::from_state(state.clone(), &self.config),
|
||||
)),
|
||||
HistoryRecord::Diff(state) => {
|
||||
Some(Box::new(history_cell::DiffCell::from_record(state.clone())))
|
||||
}
|
||||
HistoryRecord::Patch(state) => {
|
||||
Some(Box::new(history_cell::PatchSummaryCell::from_record(state.clone())))
|
||||
}
|
||||
HistoryRecord::Explore(state) => Some(Box::new(
|
||||
history_cell::ExploreAggregationCell::from_record(state.clone()),
|
||||
)),
|
||||
HistoryRecord::RateLimits(state) => Some(Box::new(
|
||||
history_cell::RateLimitsCell::from_record(state.clone()),
|
||||
)),
|
||||
HistoryRecord::BackgroundEvent(state) => {
|
||||
Some(Box::new(history_cell::BackgroundEventCell::new(state.clone())))
|
||||
}
|
||||
HistoryRecord::Image(state) => Some(Box::new(
|
||||
history_cell::ImageOutputCell::from_record(state.clone()),
|
||||
)),
|
||||
HistoryRecord::Notice(state) => Some(Box::new(
|
||||
history_cell::PlainHistoryCell::from_notice_record(state.clone()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_mutation_to_cell(
|
||||
&self,
|
||||
cell: &mut Box<dyn HistoryCell>,
|
||||
mutation: HistoryMutation,
|
||||
) -> Option<HistoryId> {
|
||||
match mutation {
|
||||
HistoryMutation::Inserted { id, record, .. }
|
||||
| HistoryMutation::Replaced { id, record, .. } => {
|
||||
if let Some(mut new_cell) = self.build_cell_from_record(&record) {
|
||||
self.assign_history_id(&mut new_cell, id);
|
||||
*cell = new_cell;
|
||||
} else if !self.hydrate_cell_from_record(cell, &record) {
|
||||
self.assign_history_id(cell, id);
|
||||
}
|
||||
Some(id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_mutation_to_cell_index(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
mutation: HistoryMutation,
|
||||
) -> Option<HistoryId> {
|
||||
if idx >= self.history_cells.len() {
|
||||
return None;
|
||||
}
|
||||
match mutation {
|
||||
HistoryMutation::Inserted { id, record, .. }
|
||||
| HistoryMutation::Replaced { id, record, .. } => {
|
||||
self.update_cell_from_record(id, record);
|
||||
Some(id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_index_for_history_id(&self, id: HistoryId) -> Option<usize> {
|
||||
if let Some(idx) = self
|
||||
.history_cell_ids
|
||||
.iter()
|
||||
.position(|maybe| maybe.map(|stored| stored == id).unwrap_or(false))
|
||||
{
|
||||
return Some(idx);
|
||||
}
|
||||
|
||||
self.history_cells.iter().enumerate().find_map(|(idx, cell)| {
|
||||
self.history_record_from_cell(cell.as_ref())
|
||||
.map(|record| record.id() == id)
|
||||
.filter(|matched| *matched)
|
||||
.map(|_| idx)
|
||||
})
|
||||
}
|
||||
|
||||
fn update_cell_from_record(&mut self, id: HistoryId, record: HistoryRecord) {
|
||||
if id == HistoryId::ZERO {
|
||||
tracing::debug!("skip update_cell_from_record: zero id");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(idx) = self.cell_index_for_history_id(id) {
|
||||
if let Some(mut rebuilt) = self.build_cell_from_record(&record) {
|
||||
Self::assign_history_id_inner(&mut rebuilt, id);
|
||||
self.history_cells[idx] = rebuilt;
|
||||
} else if let Some(cell_slot) = self.history_cells.get_mut(idx) {
|
||||
if !Self::hydrate_cell_from_record_inner(cell_slot, &record, &self.config) {
|
||||
Self::assign_history_id_inner(cell_slot, id);
|
||||
}
|
||||
}
|
||||
|
||||
if idx < self.history_cell_ids.len() {
|
||||
self.history_cell_ids[idx] = Some(id);
|
||||
}
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"history-state mismatch: unable to locate cell for id {:?}",
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assign_history_id(&self, cell: &mut Box<dyn HistoryCell>, id: HistoryId) {
|
||||
Self::assign_history_id_inner(cell, id);
|
||||
}
|
||||
|
||||
fn assign_history_id_inner(cell: &mut Box<dyn HistoryCell>, id: HistoryId) {
|
||||
if let Some(tool_call) = cell
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::history_cell::ToolCallCell>()
|
||||
@@ -4638,6 +4824,26 @@ impl ChatWidget<'_> {
|
||||
.downcast_mut::<crate::history_cell::RateLimitsCell>()
|
||||
{
|
||||
rate_limits.record_mut().id = id;
|
||||
} else 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4780,8 +4986,13 @@ impl ChatWidget<'_> {
|
||||
title.unwrap_or_default(),
|
||||
&diff_output,
|
||||
);
|
||||
let cell = history_cell::DiffCell::from_record(record);
|
||||
self.history_push(cell);
|
||||
let key = self.next_internal_key();
|
||||
let _ = self.history_insert_with_key_global_tagged(
|
||||
Box::new(history_cell::DiffCell::from_record(record.clone())),
|
||||
key,
|
||||
"diff",
|
||||
Some(HistoryDomainRecord::Diff(record)),
|
||||
);
|
||||
}
|
||||
/// Insert a background event near the top of the current request so it appears
|
||||
/// before imminent provider output (e.g. Exec begin).
|
||||
@@ -4860,21 +5071,7 @@ impl ChatWidget<'_> {
|
||||
record,
|
||||
});
|
||||
|
||||
if let Some(id) = match mutation {
|
||||
HistoryMutation::Replaced { id, record, .. } => {
|
||||
if !self.hydrate_cell_from_record(&mut cell, &record) {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
}
|
||||
Some(id)
|
||||
}
|
||||
HistoryMutation::Inserted { id, record, .. } => {
|
||||
if !self.hydrate_cell_from_record(&mut cell, &record) {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
}
|
||||
Some(id)
|
||||
}
|
||||
_ => None,
|
||||
} {
|
||||
if let Some(id) = self.apply_mutation_to_cell(&mut cell, mutation) {
|
||||
if idx < self.history_cell_ids.len() {
|
||||
self.history_cell_ids[idx] = Some(id);
|
||||
}
|
||||
@@ -4896,38 +5093,32 @@ impl ChatWidget<'_> {
|
||||
|
||||
match (record, self.record_index_for_cell(idx)) {
|
||||
(Some(record), Some(record_idx)) => {
|
||||
if let HistoryMutation::Replaced { id, record, .. } = self
|
||||
let mutation = self
|
||||
.history_state
|
||||
.apply_event(HistoryEvent::Replace {
|
||||
index: record_idx,
|
||||
record,
|
||||
})
|
||||
{
|
||||
if !self.hydrate_cell_from_record(&mut cell, &record) {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
}
|
||||
});
|
||||
if let Some(id) = self.apply_mutation_to_cell(&mut cell, mutation) {
|
||||
maybe_id = Some(id);
|
||||
}
|
||||
}
|
||||
(Some(record), None) => {
|
||||
let record_idx = self.record_index_for_position(idx);
|
||||
if let HistoryMutation::Inserted { id, record, .. } = self
|
||||
let mutation = self
|
||||
.history_state
|
||||
.apply_event(HistoryEvent::Insert {
|
||||
index: record_idx,
|
||||
record,
|
||||
})
|
||||
{
|
||||
if !self.hydrate_cell_from_record(&mut cell, &record) {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
}
|
||||
});
|
||||
if let Some(id) = self.apply_mutation_to_cell(&mut cell, mutation) {
|
||||
maybe_id = Some(id);
|
||||
}
|
||||
}
|
||||
(None, Some(record_idx)) => {
|
||||
let _ = self
|
||||
.history_state
|
||||
.apply_event(HistoryEvent::Remove { index: record_idx });
|
||||
.apply_domain_event(HistoryDomainEvent::Remove { index: record_idx });
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
@@ -4964,7 +5155,7 @@ impl ChatWidget<'_> {
|
||||
if let Some(record_idx) = self.record_index_for_cell(idx) {
|
||||
let _ = self
|
||||
.history_state
|
||||
.apply_event(HistoryEvent::Remove { index: record_idx });
|
||||
.apply_domain_event(HistoryDomainEvent::Remove { index: record_idx });
|
||||
}
|
||||
|
||||
self.history_cells.remove(idx);
|
||||
@@ -7011,18 +7202,12 @@ impl ChatWidget<'_> {
|
||||
},
|
||||
);
|
||||
if let HistoryMutation::Replaced {
|
||||
id,
|
||||
record: HistoryRecord::Exec(exec_record),
|
||||
..
|
||||
} = mutation
|
||||
{
|
||||
if idx < self.history_cells.len() {
|
||||
if let Some(exec) = self.history_cells[idx]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
{
|
||||
exec.sync_from_record(&exec_record);
|
||||
}
|
||||
}
|
||||
self.update_cell_from_record(id, HistoryRecord::Exec(exec_record));
|
||||
}
|
||||
}
|
||||
self.invalidate_height_cache();
|
||||
@@ -7208,22 +7393,17 @@ impl ChatWidget<'_> {
|
||||
.running_wait_tools
|
||||
.insert(ToolCallId(call_id.clone()), exec_call_id.clone());
|
||||
|
||||
let mut wait_update: Option<(usize, Option<Duration>, Vec<(String, bool)>)> = None;
|
||||
if let Some(running) = self.exec.running_commands.get_mut(&exec_call_id) {
|
||||
running.wait_active = true;
|
||||
running.wait_notes.clear();
|
||||
let history_index = running.history_index;
|
||||
if let Some(idx) = history_index {
|
||||
if idx < self.history_cells.len() {
|
||||
if let Some(exec_cell) = self.history_cells[idx]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
{
|
||||
exec_cell.set_waiting(true);
|
||||
exec_cell.clear_wait_notes();
|
||||
}
|
||||
}
|
||||
if let Some(idx) = running.history_index {
|
||||
wait_update = Some((idx, running.wait_total, running.wait_notes.clone()));
|
||||
}
|
||||
}
|
||||
if let Some((idx, total, notes)) = wait_update {
|
||||
let _ = self.update_exec_wait_state_with_pairs(idx, total, true, ¬es);
|
||||
}
|
||||
self.bottom_pane
|
||||
.update_status_text("waiting for command".to_string());
|
||||
self.invalidate_height_cache();
|
||||
@@ -7345,74 +7525,65 @@ impl ChatWidget<'_> {
|
||||
note_lines.push((note_text.to_string(), is_error_note));
|
||||
}
|
||||
}
|
||||
let mut history_index: Option<usize> = None;
|
||||
let mut history_idx: Option<usize> = None;
|
||||
let mut wait_total: Option<Duration> = None;
|
||||
let mut wait_notes_snapshot: Vec<(String, bool)> = Vec::new();
|
||||
if let Some(running) = self.exec.running_commands.get_mut(&exec_call_id) {
|
||||
let base = running.wait_total.unwrap_or_default();
|
||||
let total = base.saturating_add(duration);
|
||||
running.wait_total = Some(total);
|
||||
history_index = running.history_index;
|
||||
running.wait_active = wait_still_pending;
|
||||
for (text, is_error_note) in ¬e_lines {
|
||||
if running
|
||||
.wait_notes
|
||||
.last()
|
||||
.map(|(existing, existing_err)| existing == text && existing_err == is_error_note)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
Self::append_wait_pairs(&mut running.wait_notes, ¬e_lines);
|
||||
wait_notes_snapshot = running.wait_notes.clone();
|
||||
wait_total = running.wait_total;
|
||||
history_idx = running.history_index;
|
||||
} else {
|
||||
Self::append_wait_pairs(&mut wait_notes_snapshot, ¬e_lines);
|
||||
}
|
||||
|
||||
if history_idx.is_none() {
|
||||
if let Some((idx, _)) = self.history_cells.iter().enumerate().rev().find(|(_, cell)| {
|
||||
cell.as_any()
|
||||
.downcast_ref::<history_cell::ExecCell>()
|
||||
.is_some()
|
||||
}) {
|
||||
history_idx = Some(idx);
|
||||
if let Some(running) = self.exec.running_commands.get_mut(&exec_call_id) {
|
||||
running.history_index = Some(idx);
|
||||
}
|
||||
running.wait_notes.push((text.clone(), *is_error_note));
|
||||
}
|
||||
}
|
||||
|
||||
let mut updated = false;
|
||||
if let Some(idx) = history_index {
|
||||
if idx < self.history_cells.len() {
|
||||
if let Some(exec_cell) = self.history_cells[idx]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
{
|
||||
let total = exec_cell
|
||||
.wait_total()
|
||||
.unwrap_or_default()
|
||||
.saturating_add(duration);
|
||||
exec_cell.set_wait_total(Some(total));
|
||||
if wait_still_pending {
|
||||
exec_cell.set_waiting(true);
|
||||
} else {
|
||||
exec_cell.set_waiting(false);
|
||||
}
|
||||
for (text, is_error_note) in ¬e_lines {
|
||||
exec_cell.push_wait_note(text, *is_error_note);
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
if let Some(exec_cell) = self
|
||||
.history_cells
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find_map(|cell| {
|
||||
cell.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
})
|
||||
if let Some(idx) = history_idx {
|
||||
let record_idx = self
|
||||
.record_index_for_cell(idx)
|
||||
.unwrap_or_else(|| self.record_index_for_position(idx));
|
||||
if let Some(HistoryRecord::Exec(exec_record)) =
|
||||
self.history_state.get(record_idx).cloned()
|
||||
{
|
||||
let total = exec_cell
|
||||
.wait_total()
|
||||
.unwrap_or_default()
|
||||
.saturating_add(duration);
|
||||
exec_cell.set_wait_total(Some(total));
|
||||
if wait_still_pending {
|
||||
exec_cell.set_waiting(true);
|
||||
} else {
|
||||
exec_cell.set_waiting(false);
|
||||
if wait_total.is_none() {
|
||||
let base = exec_record.wait_total.unwrap_or_default();
|
||||
wait_total = Some(base.saturating_add(duration));
|
||||
}
|
||||
for (text, is_error_note) in ¬e_lines {
|
||||
exec_cell.push_wait_note(text, *is_error_note);
|
||||
if wait_notes_snapshot.is_empty() {
|
||||
wait_notes_snapshot =
|
||||
Self::wait_pairs_from_exec_notes(&exec_record.wait_notes);
|
||||
Self::append_wait_pairs(&mut wait_notes_snapshot, ¬e_lines);
|
||||
}
|
||||
} else {
|
||||
if wait_total.is_none() {
|
||||
wait_total = Some(duration);
|
||||
}
|
||||
if wait_notes_snapshot.is_empty() {
|
||||
Self::append_wait_pairs(&mut wait_notes_snapshot, ¬e_lines);
|
||||
}
|
||||
}
|
||||
let _ = self.update_exec_wait_state_with_pairs(
|
||||
idx,
|
||||
wait_total,
|
||||
wait_still_pending,
|
||||
&wait_notes_snapshot,
|
||||
);
|
||||
}
|
||||
|
||||
if success {
|
||||
@@ -11772,6 +11943,14 @@ impl ChatWidget<'_> {
|
||||
stream_id: &str,
|
||||
state: AssistantStreamState,
|
||||
) {
|
||||
if state.id != HistoryId::ZERO {
|
||||
self.update_cell_from_record(
|
||||
state.id,
|
||||
HistoryRecord::AssistantStream(state.clone()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(idx) = self.history_cells.iter().rposition(|c| {
|
||||
c.as_any()
|
||||
.downcast_ref::<history_cell::StreamingContentCell>()
|
||||
@@ -11838,21 +12017,18 @@ impl ChatWidget<'_> {
|
||||
},
|
||||
);
|
||||
|
||||
if let HistoryMutation::Inserted { index, record, .. }
|
||||
| HistoryMutation::Replaced { index, record, .. } = mutation
|
||||
{
|
||||
if let HistoryRecord::AssistantStream(state) = record {
|
||||
if index < self.history_cells.len() {
|
||||
if let Some(stream_cell) = self.history_cells[index]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::StreamingContentCell>()
|
||||
{
|
||||
stream_cell.update_from_state(state.clone(), &self.config);
|
||||
}
|
||||
} else {
|
||||
match mutation {
|
||||
HistoryMutation::Inserted { record, .. } => {
|
||||
if let HistoryRecord::AssistantStream(state) = record {
|
||||
self.refresh_streaming_cell_for_stream_id(stream_id, state);
|
||||
}
|
||||
}
|
||||
HistoryMutation::Replaced { id, record, .. } => {
|
||||
if matches!(record, HistoryRecord::AssistantStream(_)) {
|
||||
self.update_cell_from_record(id, record);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::ChatWidget;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::height_manager::HeightEvent;
|
||||
use crate::history::state::{
|
||||
ExecAction, ExecRecord, ExecStatus, HistoryDomainRecord,
|
||||
ExecAction, ExecRecord, ExecStatus, ExploreRecord, HistoryDomainRecord, HistoryId,
|
||||
};
|
||||
use crate::history_cell::CommandOutput;
|
||||
use crate::history_cell::{self, HistoryCell};
|
||||
@@ -51,6 +51,8 @@ fn exec_record_from_begin(ev: &ExecCommandBeginEvent) -> ExecRecord {
|
||||
stdout_chunks: Vec::new(),
|
||||
stderr_chunks: Vec::new(),
|
||||
exit_code: None,
|
||||
wait_total: None,
|
||||
wait_active: false,
|
||||
wait_notes: Vec::new(),
|
||||
started_at: std::time::SystemTime::now(),
|
||||
completed_at: None,
|
||||
@@ -95,6 +97,7 @@ pub(super) fn finalize_all_running_as_interrupted(chat: &mut ChatWidget<'_>) {
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.history_index, v.explore_entry))
|
||||
.collect();
|
||||
let mut agg_was_updated = false;
|
||||
for (call_id, maybe_idx, explore_entry) in &running {
|
||||
if let Some(idx) = maybe_idx {
|
||||
finalize_exec_cell_at(
|
||||
@@ -116,17 +119,20 @@ pub(super) fn finalize_all_running_as_interrupted(chat: &mut ChatWidget<'_>) {
|
||||
*entry_idx,
|
||||
history_cell::ExploreEntryStatus::Error { exit_code: None },
|
||||
);
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record);
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
cell.set_trailing(existing.is_trailing());
|
||||
chat.history_replace_at(*agg_idx, Box::new(cell));
|
||||
chat.history_replace_with_record(
|
||||
*agg_idx,
|
||||
Box::new(cell),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
chat.request_redraw();
|
||||
agg_was_updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
chat.canceled_exec_call_ids.insert(call_id.clone());
|
||||
}
|
||||
let agg_was_updated = running.iter().any(|(_, _, entry)| entry.is_some());
|
||||
chat.exec.running_commands.clear();
|
||||
if agg_was_updated {
|
||||
chat.exec.running_explore_agg_index = None;
|
||||
@@ -263,11 +269,14 @@ pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
|
||||
{
|
||||
let mut record = existing.record().clone();
|
||||
record.update_status(*entry_idx, history_cell::ExploreEntryStatus::Success);
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record);
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
cell.set_trailing(existing.is_trailing());
|
||||
chat.history_replace_at(*agg_idx, Box::new(cell));
|
||||
chat.history_replace_with_record(
|
||||
*agg_idx,
|
||||
Box::new(cell),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
chat.request_redraw();
|
||||
agg_was_updated = true;
|
||||
}
|
||||
}
|
||||
@@ -514,9 +523,15 @@ pub(super) fn handle_exec_begin_now(
|
||||
|
||||
if agg_idx.is_none() {
|
||||
let key = ChatWidget::order_key_from_order_meta(order);
|
||||
let idx = chat.history_insert_with_key_global(
|
||||
Box::new(history_cell::ExploreAggregationCell::new()),
|
||||
let record = ExploreRecord {
|
||||
id: HistoryId::ZERO,
|
||||
entries: Vec::new(),
|
||||
};
|
||||
let idx = chat.history_insert_with_key_global_tagged(
|
||||
Box::new(history_cell::ExploreAggregationCell::from_record(record.clone())),
|
||||
key,
|
||||
"explore",
|
||||
Some(HistoryDomainRecord::Explore(record)),
|
||||
);
|
||||
created_new = true;
|
||||
agg_idx = Some(idx);
|
||||
@@ -535,11 +550,14 @@ pub(super) fn handle_exec_begin_now(
|
||||
&chat.config.cwd,
|
||||
&ev.command,
|
||||
) {
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record);
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
cell.set_trailing(trailing);
|
||||
chat.history_replace_at(idx, Box::new(cell));
|
||||
chat.history_replace_with_record(
|
||||
idx,
|
||||
Box::new(cell),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
chat.request_redraw();
|
||||
chat.exec.running_explore_agg_index = Some(idx);
|
||||
chat.exec.running_commands.insert(
|
||||
super::ExecCallId(ev.call_id.clone()),
|
||||
@@ -682,11 +700,14 @@ pub(super) fn handle_exec_end_now(
|
||||
.map(|existing| (existing.record().clone(), existing.is_trailing()))
|
||||
}) {
|
||||
record.update_status(entry_idx, status.clone());
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record);
|
||||
let mut cell = history_cell::ExploreAggregationCell::from_record(record.clone());
|
||||
cell.set_trailing(trailing);
|
||||
chat.history_replace_at(agg_idx, Box::new(cell));
|
||||
chat.history_replace_with_record(
|
||||
agg_idx,
|
||||
Box::new(cell),
|
||||
HistoryDomainRecord::Explore(record),
|
||||
);
|
||||
chat.autoscroll_if_near_bottom();
|
||||
chat.request_redraw();
|
||||
}
|
||||
if !chat
|
||||
.exec
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::*;
|
||||
use crate::app_event::{AppEvent, BackgroundPlacement};
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::history::state::{ExecStatus, HistoryRecord, HistoryState};
|
||||
use crate::history::state::{ExecStatus, ExploreEntryStatus, HistoryId, HistoryRecord, HistoryState};
|
||||
use crate::slash_command::SlashCommand;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -11,6 +11,7 @@ use codex_core::config::ConfigToml;
|
||||
use codex_core::plan_tool::PlanItemArg;
|
||||
use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::plan_tool::UpdatePlanArgs;
|
||||
use codex_core::parse_command::ParsedCommand;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
use codex_core::protocol::AgentStatusUpdateEvent;
|
||||
@@ -20,6 +21,8 @@ use codex_core::protocol::AgentReasoningEvent;
|
||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::CustomToolCallBeginEvent;
|
||||
use codex_core::protocol::CustomToolCallEndEvent;
|
||||
use codex_core::protocol::ExecApprovalRequestEvent;
|
||||
use codex_core::protocol::ExecCommandBeginEvent;
|
||||
use codex_core::protocol::ExecCommandEndEvent;
|
||||
@@ -29,6 +32,7 @@ use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::RateLimitSnapshotEvent;
|
||||
use codex_core::protocol::PatchApplyBeginEvent;
|
||||
use codex_core::protocol::PatchApplyEndEvent;
|
||||
use codex_core::protocol::OrderMeta;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -43,8 +47,10 @@ use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use strip_ansi_escapes::strip as strip_ansi_bytes;
|
||||
use serde_json::json;
|
||||
|
||||
fn test_config() -> Config {
|
||||
// Use base defaults to avoid depending on host state.
|
||||
@@ -216,6 +222,191 @@ fn background_event_before_next_output_precedes_later_cells() {
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_tool_updates_exec_record_via_history_state() {
|
||||
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.handle_codex_event(Event {
|
||||
id: "req-1".into(),
|
||||
event_seq: 1,
|
||||
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: "call-1".into(),
|
||||
command: vec!["sleep".into(), "1".into()],
|
||||
cwd: cwd.clone(),
|
||||
parsed_cmd: Vec::new(),
|
||||
}),
|
||||
order: Some(order_meta(0)),
|
||||
});
|
||||
pump_app_events(&mut chat, &rx);
|
||||
let _ = drain_insert_history(&rx);
|
||||
|
||||
let wait_params = json!({
|
||||
"call_id": "call-1",
|
||||
"for": "sleep 1",
|
||||
});
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "req-1".into(),
|
||||
event_seq: 2,
|
||||
msg: EventMsg::CustomToolCallBegin(CustomToolCallBeginEvent {
|
||||
call_id: "wait-1".into(),
|
||||
tool_name: "wait".into(),
|
||||
parameters: Some(wait_params.clone()),
|
||||
}),
|
||||
order: Some(order_meta(1)),
|
||||
});
|
||||
pump_app_events(&mut chat, &rx);
|
||||
let _ = drain_insert_history(&rx);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "req-1".into(),
|
||||
event_seq: 3,
|
||||
msg: EventMsg::CustomToolCallEnd(CustomToolCallEndEvent {
|
||||
call_id: "wait-1".into(),
|
||||
tool_name: "wait".into(),
|
||||
parameters: Some(wait_params),
|
||||
duration: Duration::from_secs(2),
|
||||
result: Ok("Scaling services".to_string()),
|
||||
}),
|
||||
order: Some(order_meta(2)),
|
||||
});
|
||||
pump_app_events(&mut chat, &rx);
|
||||
let _ = drain_insert_history(&rx);
|
||||
|
||||
let exec_record = chat
|
||||
.history_state()
|
||||
.records
|
||||
.iter()
|
||||
.find_map(|rec| match rec {
|
||||
HistoryRecord::Exec(record) => Some(record.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("exec record present");
|
||||
assert_eq!(exec_record.wait_total, Some(Duration::from_secs(2)));
|
||||
assert!(!exec_record.wait_active);
|
||||
assert!(exec_record
|
||||
.wait_notes
|
||||
.iter()
|
||||
.any(|note| note.message.contains("Scaling services")));
|
||||
|
||||
let exec_cell = chat
|
||||
.history_cells
|
||||
.iter()
|
||||
.find_map(|cell| cell.as_any().downcast_ref::<history_cell::ExecCell>())
|
||||
.expect("exec cell present");
|
||||
assert_eq!(exec_cell.record.wait_total, exec_record.wait_total);
|
||||
assert_eq!(exec_cell.record.wait_notes.len(), exec_record.wait_notes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explore_updates_history_state_via_domain_events() {
|
||||
let (mut chat, rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
let cwd = chat.config.cwd.clone();
|
||||
let search_cmd = vec!["rg".into(), "foo".into()];
|
||||
let parsed = vec![ParsedCommand::Search {
|
||||
cmd: "rg foo".to_string(),
|
||||
query: Some("foo".to_string()),
|
||||
path: None,
|
||||
}];
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "req-1".into(),
|
||||
event_seq: 1,
|
||||
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: "call-1".into(),
|
||||
command: search_cmd.clone(),
|
||||
cwd: cwd.clone(),
|
||||
parsed_cmd: parsed.clone(),
|
||||
}),
|
||||
order: Some(order_meta(0)),
|
||||
});
|
||||
pump_app_events(&mut chat, &rx);
|
||||
let _ = drain_insert_history(&rx);
|
||||
|
||||
let explore_id = chat
|
||||
.history_state()
|
||||
.records
|
||||
.iter()
|
||||
.find_map(|rec| match rec {
|
||||
HistoryRecord::Explore(record) => Some(record.id),
|
||||
_ => None,
|
||||
})
|
||||
.expect("explore record inserted");
|
||||
assert_ne!(explore_id, HistoryId::ZERO);
|
||||
|
||||
let explore_state = chat
|
||||
.history_state()
|
||||
.records
|
||||
.iter()
|
||||
.find_map(|rec| match rec {
|
||||
HistoryRecord::Explore(record) if record.id == explore_id => Some(record.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(explore_state.entries.len(), 1);
|
||||
assert!(matches!(explore_state.entries[0].status, ExploreEntryStatus::Running));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "req-1".into(),
|
||||
event_seq: 2,
|
||||
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id: "call-1".into(),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
exit_code: 0,
|
||||
duration: Duration::from_millis(250),
|
||||
}),
|
||||
order: Some(order_meta(1)),
|
||||
});
|
||||
pump_app_events(&mut chat, &rx);
|
||||
let _ = drain_insert_history(&rx);
|
||||
|
||||
let explore_state = chat
|
||||
.history_state()
|
||||
.records
|
||||
.iter()
|
||||
.find_map(|rec| match rec {
|
||||
HistoryRecord::Explore(record) if record.id == explore_id => Some(record.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(explore_state.entries[0].status, ExploreEntryStatus::Success));
|
||||
|
||||
let explore_cell = chat
|
||||
.history_cells
|
||||
.iter()
|
||||
.find_map(|cell| cell.as_any().downcast_ref::<history_cell::ExploreAggregationCell>())
|
||||
.expect("explore cell present");
|
||||
assert_eq!(explore_cell.record().id, explore_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_inserts_history_record_through_domain_event() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
chat.history_push_diff(Some("README".to_string()), "@@ -0,0 +1 @@\n+hello world\n".to_string());
|
||||
|
||||
let diff_record = chat
|
||||
.history_state()
|
||||
.records
|
||||
.iter()
|
||||
.find_map(|rec| match rec {
|
||||
HistoryRecord::Diff(record) => Some(record.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("diff record present");
|
||||
assert_ne!(diff_record.id, HistoryId::ZERO);
|
||||
|
||||
let diff_cell = chat
|
||||
.history_cells
|
||||
.iter()
|
||||
.find_map(|cell| cell.as_any().downcast_ref::<history_cell::DiffCell>())
|
||||
.expect("diff cell present");
|
||||
assert_eq!(diff_cell.record().id, diff_record.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_overlay_loading_when_snapshot_missing() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
@@ -468,6 +659,14 @@ fn drain_insert_history(
|
||||
out
|
||||
}
|
||||
|
||||
fn order_meta(seq: u64) -> OrderMeta {
|
||||
OrderMeta {
|
||||
request_ordinal: 1,
|
||||
output_index: Some(0),
|
||||
sequence_number: Some(seq),
|
||||
}
|
||||
}
|
||||
|
||||
fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String {
|
||||
let mut s = String::new();
|
||||
for line in lines {
|
||||
|
||||
@@ -38,6 +38,9 @@ pub enum HistoryDomainEvent {
|
||||
index: usize,
|
||||
record: HistoryDomainRecord,
|
||||
},
|
||||
Remove {
|
||||
index: usize,
|
||||
},
|
||||
UpdateExecStream {
|
||||
index: usize,
|
||||
stdout_chunk: Option<ExecStreamChunk>,
|
||||
@@ -49,6 +52,12 @@ pub enum HistoryDomainEvent {
|
||||
delta: Option<AssistantStreamDelta>,
|
||||
metadata: Option<MessageMetadata>,
|
||||
},
|
||||
UpdateExecWait {
|
||||
index: usize,
|
||||
total_wait: Option<Duration>,
|
||||
wait_active: bool,
|
||||
notes: Vec<ExecWaitNote>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -60,6 +69,11 @@ pub enum HistoryDomainRecord {
|
||||
RateLimits(RateLimitsRecord),
|
||||
Exec(ExecRecord),
|
||||
AssistantStream(AssistantStreamState),
|
||||
Patch(PatchRecord),
|
||||
Image(ImageRecord),
|
||||
Diff(DiffRecord),
|
||||
Explore(ExploreRecord),
|
||||
Notice(NoticeRecord),
|
||||
}
|
||||
|
||||
impl From<PlainMessageState> for HistoryDomainRecord {
|
||||
@@ -104,6 +118,36 @@ impl From<AssistantStreamState> for HistoryDomainRecord {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatchRecord> for HistoryDomainRecord {
|
||||
fn from(state: PatchRecord) -> Self {
|
||||
HistoryDomainRecord::Patch(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImageRecord> for HistoryDomainRecord {
|
||||
fn from(state: ImageRecord) -> Self {
|
||||
HistoryDomainRecord::Image(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiffRecord> for HistoryDomainRecord {
|
||||
fn from(state: DiffRecord) -> Self {
|
||||
HistoryDomainRecord::Diff(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExploreRecord> for HistoryDomainRecord {
|
||||
fn from(state: ExploreRecord) -> Self {
|
||||
HistoryDomainRecord::Explore(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NoticeRecord> for HistoryDomainRecord {
|
||||
fn from(state: NoticeRecord) -> Self {
|
||||
HistoryDomainRecord::Notice(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryDomainRecord {
|
||||
fn into_history_record(self) -> HistoryRecord {
|
||||
match self {
|
||||
@@ -135,6 +179,26 @@ impl HistoryDomainRecord {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::AssistantStream(state)
|
||||
}
|
||||
HistoryDomainRecord::Patch(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::Patch(state)
|
||||
}
|
||||
HistoryDomainRecord::Image(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::Image(state)
|
||||
}
|
||||
HistoryDomainRecord::Diff(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::Diff(state)
|
||||
}
|
||||
HistoryDomainRecord::Explore(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::Explore(state)
|
||||
}
|
||||
HistoryDomainRecord::Notice(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::Notice(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,11 +207,23 @@ impl HistoryDomainRecord {
|
||||
pub struct PlainMessageState {
|
||||
pub id: HistoryId,
|
||||
pub role: PlainMessageRole,
|
||||
pub kind: PlainMessageKind,
|
||||
pub header: Option<MessageHeader>,
|
||||
pub lines: Vec<MessageLine>,
|
||||
pub metadata: Option<MessageMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PlainMessageKind {
|
||||
Plain,
|
||||
User,
|
||||
Assistant,
|
||||
Tool,
|
||||
Error,
|
||||
Background,
|
||||
Notice,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PlainMessageRole {
|
||||
System,
|
||||
@@ -403,6 +479,10 @@ pub struct ExecRecord {
|
||||
pub stdout_chunks: Vec<ExecStreamChunk>,
|
||||
pub stderr_chunks: Vec<ExecStreamChunk>,
|
||||
pub exit_code: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub wait_total: Option<Duration>,
|
||||
#[serde(default)]
|
||||
pub wait_active: bool,
|
||||
pub wait_notes: Vec<ExecWaitNote>,
|
||||
pub started_at: SystemTime,
|
||||
pub completed_at: Option<SystemTime>,
|
||||
@@ -769,6 +849,9 @@ impl HistoryState {
|
||||
let record = record.into_history_record();
|
||||
self.apply_event(HistoryEvent::Replace { index, record })
|
||||
}
|
||||
HistoryDomainEvent::Remove { index } => {
|
||||
self.apply_event(HistoryEvent::Remove { index })
|
||||
}
|
||||
HistoryDomainEvent::UpdateExecStream {
|
||||
index,
|
||||
stdout_chunk,
|
||||
@@ -846,6 +929,25 @@ impl HistoryState {
|
||||
record,
|
||||
})
|
||||
}
|
||||
HistoryDomainEvent::UpdateExecWait {
|
||||
index,
|
||||
total_wait,
|
||||
wait_active,
|
||||
notes,
|
||||
} => {
|
||||
if let Some(HistoryRecord::Exec(existing)) = self.records.get(index).cloned() {
|
||||
let mut updated = existing;
|
||||
updated.wait_total = total_wait;
|
||||
updated.wait_active = wait_active;
|
||||
updated.wait_notes = notes;
|
||||
self.apply_event(HistoryEvent::Replace {
|
||||
index,
|
||||
record: HistoryRecord::Exec(updated),
|
||||
})
|
||||
} else {
|
||||
HistoryMutation::Noop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,9 +366,9 @@ impl ExecCell {
|
||||
.completed_at
|
||||
.and_then(|done| done.duration_since(record.started_at).ok());
|
||||
let wait_state = ExecWaitState {
|
||||
total_wait: None,
|
||||
total_wait: record.wait_total,
|
||||
run_duration,
|
||||
waiting: matches!(record.status, ExecStatus::Running),
|
||||
waiting: record.wait_active,
|
||||
notes: wait_notes_from_record(&record.wait_notes),
|
||||
};
|
||||
let start_time = if matches!(record.status, ExecStatus::Running) {
|
||||
@@ -413,20 +413,32 @@ impl ExecCell {
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn set_waiting(&self, waiting: bool) {
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.waiting != waiting {
|
||||
state.waiting = waiting;
|
||||
drop(state);
|
||||
pub(crate) fn set_waiting(&mut self, waiting: bool) {
|
||||
let mut changed = false;
|
||||
{
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.waiting != waiting {
|
||||
state.waiting = waiting;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.record.wait_active = waiting;
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_wait_total(&self, total: Option<Duration>) {
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.total_wait != total {
|
||||
state.total_wait = total;
|
||||
drop(state);
|
||||
pub(crate) fn set_wait_total(&mut self, total: Option<Duration>) {
|
||||
let mut changed = false;
|
||||
{
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.total_wait != total {
|
||||
state.total_wait = total;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.record.wait_total = total;
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
}
|
||||
@@ -440,52 +452,6 @@ impl ExecCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wait_total(&self) -> Option<Duration> {
|
||||
self.wait_state.borrow().total_wait
|
||||
}
|
||||
|
||||
pub(crate) fn clear_wait_notes(&mut self) {
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.notes.is_empty() {
|
||||
return;
|
||||
}
|
||||
state.notes.clear();
|
||||
drop(state);
|
||||
self.record.wait_notes.clear();
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
|
||||
pub(crate) fn push_wait_note(&mut self, text: &str, is_error: bool) {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state
|
||||
.notes
|
||||
.last()
|
||||
.map(|note| note.text == trimmed && note.is_error == is_error)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
state.notes.push(ExecWaitNote {
|
||||
text: trimmed.to_string(),
|
||||
is_error,
|
||||
});
|
||||
drop(state);
|
||||
self.record.wait_notes.push(RecordExecWaitNote {
|
||||
message: trimmed.to_string(),
|
||||
tone: if is_error {
|
||||
TextTone::Error
|
||||
} else {
|
||||
TextTone::Info
|
||||
},
|
||||
timestamp: SystemTime::now(),
|
||||
});
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
|
||||
pub(crate) fn set_wait_notes(&mut self, notes: &[(String, bool)]) {
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
let mut changed = state.notes.len() != notes.len();
|
||||
@@ -796,6 +762,17 @@ impl ExecCell {
|
||||
self.parsed = record.parsed.clone();
|
||||
self.has_bold_command = command_has_bold_token(&self.command);
|
||||
|
||||
let run_duration = record
|
||||
.completed_at
|
||||
.and_then(|done| done.duration_since(record.started_at).ok());
|
||||
{
|
||||
let mut wait_state = self.wait_state.borrow_mut();
|
||||
wait_state.total_wait = record.wait_total;
|
||||
wait_state.waiting = record.wait_active;
|
||||
wait_state.run_duration = run_duration;
|
||||
wait_state.notes = wait_notes_from_record(&record.wait_notes);
|
||||
}
|
||||
|
||||
if matches!(record.status, ExecStatus::Running) {
|
||||
let stdout = chunks_to_string(&record.stdout_chunks);
|
||||
let stderr = chunks_to_string(&record.stderr_chunks);
|
||||
@@ -981,6 +958,8 @@ fn build_exec_record(
|
||||
stdout_chunks: chunk_from_text(&output.stdout),
|
||||
stderr_chunks: chunk_from_text(&output.stderr),
|
||||
exit_code: Some(exit_code),
|
||||
wait_total: None,
|
||||
wait_active: false,
|
||||
wait_notes: Vec::new(),
|
||||
started_at,
|
||||
completed_at: Some(SystemTime::now()),
|
||||
@@ -995,6 +974,8 @@ fn build_exec_record(
|
||||
stdout_chunks: Vec::new(),
|
||||
stderr_chunks: Vec::new(),
|
||||
exit_code: None,
|
||||
wait_total: None,
|
||||
wait_active: false,
|
||||
wait_notes: Vec::new(),
|
||||
started_at,
|
||||
completed_at: None,
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::history::state::{
|
||||
ExploreEntryStatus,
|
||||
ExploreRecord,
|
||||
ExploreSummary,
|
||||
HistoryId,
|
||||
};
|
||||
use codex_core::parse_command::ParsedCommand;
|
||||
use shlex::Shlex;
|
||||
@@ -17,16 +16,6 @@ pub(crate) struct ExploreAggregationCell {
|
||||
}
|
||||
|
||||
impl ExploreAggregationCell {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
record: ExploreRecord {
|
||||
id: HistoryId::ZERO,
|
||||
entries: Vec::new(),
|
||||
},
|
||||
is_trailing: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_record(record: ExploreRecord) -> Self {
|
||||
Self {
|
||||
record,
|
||||
|
||||
@@ -41,6 +41,15 @@ impl ImageOutputCell {
|
||||
Self { state, record }
|
||||
}
|
||||
|
||||
pub(crate) fn from_record(record: ImageRecord) -> Self {
|
||||
let state = ImageCellState {
|
||||
width: record.width as u32,
|
||||
height: record.height as u32,
|
||||
rgba: Vec::new(),
|
||||
};
|
||||
Self { state, record }
|
||||
}
|
||||
|
||||
pub(crate) fn record(&self) -> &ImageRecord {
|
||||
&self.record
|
||||
}
|
||||
|
||||
@@ -5842,18 +5842,6 @@ pub(crate) fn new_patch_event(
|
||||
event_type: PatchEventType,
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
) -> PatchSummaryCell {
|
||||
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 {
|
||||
@@ -5866,12 +5854,7 @@ pub(crate) fn new_patch_event(
|
||||
},
|
||||
changes,
|
||||
};
|
||||
PatchSummaryCell {
|
||||
title,
|
||||
kind,
|
||||
record,
|
||||
cached: std::cell::RefCell::new(None),
|
||||
}
|
||||
PatchSummaryCell::from_record(record)
|
||||
}
|
||||
|
||||
pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
|
||||
@@ -5927,6 +5910,31 @@ struct PatchLayoutCache {
|
||||
}
|
||||
|
||||
impl PatchSummaryCell {
|
||||
pub(crate) fn from_record(record: PatchRecord) -> Self {
|
||||
let kind = match record.patch_type {
|
||||
HistoryPatchEventType::ApprovalRequest => PatchKind::Proposed,
|
||||
HistoryPatchEventType::ApplyBegin { .. } => PatchKind::ApplyBegin,
|
||||
HistoryPatchEventType::ApplySuccess => PatchKind::ApplySuccess,
|
||||
HistoryPatchEventType::ApplyFailure => PatchKind::ApplyFailure,
|
||||
};
|
||||
let title = match record.patch_type {
|
||||
HistoryPatchEventType::ApprovalRequest => "proposed patch".to_string(),
|
||||
HistoryPatchEventType::ApplyBegin { .. } => "Updated".to_string(),
|
||||
HistoryPatchEventType::ApplySuccess => "Patch applied".to_string(),
|
||||
HistoryPatchEventType::ApplyFailure => "Patch failed".to_string(),
|
||||
};
|
||||
Self {
|
||||
title,
|
||||
kind,
|
||||
record,
|
||||
cached: std::cell::RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_cache(&self) {
|
||||
self.cached.borrow_mut().take();
|
||||
}
|
||||
|
||||
fn ui_event_type(&self) -> PatchEventType {
|
||||
match self.record.patch_type {
|
||||
HistoryPatchEventType::ApprovalRequest => PatchEventType::ApprovalRequest,
|
||||
|
||||
@@ -4,6 +4,8 @@ use crate::history::state::{
|
||||
HistoryId,
|
||||
MessageHeader,
|
||||
MessageLine,
|
||||
NoticeRecord,
|
||||
PlainMessageKind,
|
||||
PlainMessageRole,
|
||||
PlainMessageState,
|
||||
};
|
||||
@@ -29,7 +31,8 @@ pub(crate) struct PlainCellState {
|
||||
|
||||
impl PlainCellState {
|
||||
fn new(lines: Vec<Line<'static>>, kind: HistoryCellType) -> Self {
|
||||
let message = PlainMessageStateBuilder::from_lines(lines, kind);
|
||||
let mut message = PlainMessageStateBuilder::from_lines(lines, kind);
|
||||
message.kind = plain_message_kind_from_cell_kind(kind);
|
||||
Self { message, kind }
|
||||
}
|
||||
|
||||
@@ -59,6 +62,33 @@ impl PlainHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_state(state: PlainMessageState) -> Self {
|
||||
let kind = history_cell_kind_from_plain(state.kind);
|
||||
Self {
|
||||
state: PlainCellState {
|
||||
message: state,
|
||||
kind,
|
||||
},
|
||||
cached_layout: std::cell::RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_notice_record(record: NoticeRecord) -> Self {
|
||||
let header = record
|
||||
.title
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
.map(|label| MessageHeader { label, badge: None });
|
||||
let state = PlainMessageState {
|
||||
id: record.id,
|
||||
role: PlainMessageRole::System,
|
||||
kind: PlainMessageKind::Notice,
|
||||
header,
|
||||
lines: record.body,
|
||||
metadata: None,
|
||||
};
|
||||
Self::from_state(state)
|
||||
}
|
||||
|
||||
pub(crate) fn state(&self) -> &PlainMessageState {
|
||||
&self.state.message
|
||||
}
|
||||
@@ -68,13 +98,6 @@ impl PlainHistoryCell {
|
||||
&mut self.state.message
|
||||
}
|
||||
|
||||
pub(crate) fn set_kind(&mut self, kind: HistoryCellType) {
|
||||
if self.state.kind != kind {
|
||||
self.state.kind = kind;
|
||||
self.invalidate_layout_cache();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_layout_cache(&self) {
|
||||
self.cached_layout.borrow_mut().take();
|
||||
}
|
||||
@@ -307,6 +330,7 @@ impl PlainMessageStateBuilder {
|
||||
PlainMessageState {
|
||||
id: HistoryId::ZERO,
|
||||
role,
|
||||
kind: plain_message_kind_from_cell_kind(kind),
|
||||
header,
|
||||
lines: message_lines,
|
||||
metadata: None,
|
||||
@@ -326,6 +350,32 @@ fn plain_role_from_kind(kind: HistoryCellType) -> PlainMessageRole {
|
||||
}
|
||||
}
|
||||
|
||||
fn plain_message_kind_from_cell_kind(kind: HistoryCellType) -> PlainMessageKind {
|
||||
match kind {
|
||||
HistoryCellType::User => PlainMessageKind::User,
|
||||
HistoryCellType::Assistant => PlainMessageKind::Assistant,
|
||||
HistoryCellType::Tool { .. } => PlainMessageKind::Tool,
|
||||
HistoryCellType::Error => PlainMessageKind::Error,
|
||||
HistoryCellType::BackgroundEvent => PlainMessageKind::Background,
|
||||
HistoryCellType::Notice => PlainMessageKind::Notice,
|
||||
_ => PlainMessageKind::Plain,
|
||||
}
|
||||
}
|
||||
|
||||
fn history_cell_kind_from_plain(kind: PlainMessageKind) -> HistoryCellType {
|
||||
match kind {
|
||||
PlainMessageKind::User => HistoryCellType::User,
|
||||
PlainMessageKind::Assistant => HistoryCellType::Assistant,
|
||||
PlainMessageKind::Tool => HistoryCellType::Tool {
|
||||
status: super::ToolCellStatus::Success,
|
||||
},
|
||||
PlainMessageKind::Error => HistoryCellType::Error,
|
||||
PlainMessageKind::Background => HistoryCellType::BackgroundEvent,
|
||||
PlainMessageKind::Notice => HistoryCellType::Notice,
|
||||
PlainMessageKind::Plain => HistoryCellType::Plain,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_hide_header(kind: HistoryCellType) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
|
||||
@@ -69,6 +69,39 @@ impl CollapsibleReasoningCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_state(state: ReasoningState) -> Self {
|
||||
let ReasoningState {
|
||||
id,
|
||||
sections,
|
||||
effort: _,
|
||||
in_progress,
|
||||
} = state;
|
||||
|
||||
let theme = crate::theme::current_theme();
|
||||
let rendered_lines = sections_to_ratatui_lines(§ions, &theme);
|
||||
let entries = rendered_lines
|
||||
.into_iter()
|
||||
.map(|line| ReasoningLineEntry {
|
||||
semantic: SemanticLine::from_line(line.clone()),
|
||||
raw: line,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cell_state = CollapsibleReasoningState {
|
||||
lines: entries,
|
||||
sections,
|
||||
in_progress,
|
||||
hide_when_collapsed: false,
|
||||
id: None,
|
||||
history_id: id,
|
||||
};
|
||||
|
||||
Self {
|
||||
state: RefCell::new(cell_state),
|
||||
collapsed: Cell::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn matches_id(&self, candidate: &str) -> bool {
|
||||
self.state
|
||||
.borrow()
|
||||
|
||||
Reference in New Issue
Block a user