mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
chore(tui/history): introduce domain events and task briefs
This commit is contained in:
@@ -13,6 +13,8 @@ To do that we're working on refactoring the history cells so that state is separ
|
||||
- [~] **Introduce single `HistoryState` vector** – foundational types (`HistoryRecord`, `HistoryState`, `HistoryId`) now live in `history/state.rs`; still need to adopt them in `chatwidget.rs` and event handling.
|
||||
- [ ] **Centralize event → state mapping** – funnel all `handle_*` mutations through `HistoryState::apply_event` and a dedicated render adapter.
|
||||
- [ ] **Unlock serialization & perf goals** – once the state vector exists, wire serialization, resume snapshots, undo rewind, and cached layout per `(record_id, width)`.
|
||||
- [ ] **Document interim cache bridge** – keep legacy per-cell layout caches (exec/assistant/diff) in place until the Step 6 renderer cache is implemented, and track their removal behind the new caching system.
|
||||
- [ ] **Land HistoryDomainEvent layer** – introduce a dedicated domain-event enum + helpers so every TUI mutation flows through `HistoryState::apply_event` without reconstructing records from cells.
|
||||
|
||||
## Shared Rendering Inputs
|
||||
- History viewport width (drives wrapping and layout caches).
|
||||
@@ -99,27 +101,32 @@ Status legend: ✅ complete (semantic deterministic state ready), ⏳ still need
|
||||
### ⏳ Exec Commands – `ExecCell`
|
||||
- **Status:** [~] `ExecRecord` lives in `HistoryState`, but rendering still depends on the legacy `ExecCell` caches.
|
||||
- **Current:** command metadata, layout caches, and wait state remain coupled to the UI struct instead of the serialized record.
|
||||
- **Needed:** read/write `ExecRecord` via a dedicated `history_cell/exec.rs`, and move per-width layout caches into the renderer layer.
|
||||
- **Decision:** keep those caches energized until the Step 6 renderer cache is available; otherwise exec redraws regressed by >2× during testing.
|
||||
- **Needed:** read/write `ExecRecord` via a dedicated `history_cell/exec.rs`, and move per-width layout caches into the renderer layer once the shared cache exists.
|
||||
- **External settings:** theme, width, monotonic time for “running” durations.
|
||||
|
||||
### ⏳ Assistant Streaming – `StreamingContentCell`
|
||||
- **Status:** [~] streaming records capture IDs, markdown deltas, citations, and metadata in `AssistantStreamState`; the cell still owns a redundant `Vec<Line>` cache.
|
||||
- **Current:** renderer ignores the stored deltas and instead maintains wrapped lines per width.
|
||||
- **Decision:** hold this cache until the centralized renderer cache exists so streaming updates stay smooth.
|
||||
- **Needed:** rebuild previews from `AssistantStreamState`, drop the duplicated line cache, and finish piping citations/token usage through serialization.
|
||||
|
||||
### ⏳ Assistant Answers – `AssistantMarkdownCell`
|
||||
- **Status:** [~] finalized messages persist markdown/citations/token usage via `AssistantMessageState`, while the cell caches a wrapped copy for legacy rendering.
|
||||
- **Current:** raw markdown and per-width layout live in the UI layer, so `HistoryState` snapshots cannot rehydrate the answer alone.
|
||||
- **Decision:** keep the wrapped-line cache until Step 6 introduces the shared renderer cache to avoid flicker on theme/width changes.
|
||||
- **Needed:** render directly from `AssistantMessageState`, move layout caching to the renderer, and delete the redundant `lines` buffer.
|
||||
|
||||
### ⏳ Merged Exec Summary – `MergedExecCell`
|
||||
- **Status:** ⏳ still reuses rendered `ExecCell` line pairs instead of the structured `ExecRecord` output chunks.
|
||||
- **Current:** keeps preformatted preamble/output text, preventing reuse of serialized exec data.
|
||||
- **Decision:** rely on the existing `ExecCell` cache bridge until the new renderer cache is ready so merged exec performance stays stable.
|
||||
- **Needed:** rebuild merged summaries from `Vec<ExecRecord>` snapshots once the exec module is refactored.
|
||||
|
||||
### ⏳ Diffs – `DiffCell`
|
||||
- **Status:** [~] `DiffRecord` already stores hunks and `DiffLineKind`; the cell precomputes styled lines for legacy rendering.
|
||||
- **Current:** cached `Vec<Line<'static>>` duplicates the structured data and bypasses `HistoryState` for updates.
|
||||
- **Decision:** retain the RefCell layout cache until the shared renderer cache is available; otherwise large diffs stutter on scroll.
|
||||
- **Needed:** route diff events through `HistoryState`, render directly from `DiffRecord`, then remove the cached line buffer.
|
||||
|
||||
### ⏳ Explore Fetch / HTTP – `ExploreRecord`
|
||||
@@ -141,23 +148,33 @@ Status legend: ✅ complete (semantic deterministic state ready), ⏳ still need
|
||||
1.6 **Wait status & background notices** – ✅ wait tools use `WaitStatusState`; background notices render via `BackgroundEventRecord`.
|
||||
1.7 **Documentation** – ✅ inline docs now cover reasoning summaries/bullets; audit of constructors confirmed strongly typed state (2025-09-26).
|
||||
|
||||
## Step 2 – Exec / Streaming / Diff Family *(Pending)*
|
||||
2.1 **Exec state extraction & module split** – ✅ `ExecCell` now lives in `history_cell/exec.rs`, builds from `ExecRecord` (including wait notes/stream chunks), and keeps layout caches strictly in the renderer layer (2025-09-26).
|
||||
2.2 **Streaming assistant module** – Capture raw markdown deltas + metadata in `AssistantStreamState`, relocate the streaming renderer to `history_cell/stream.rs`, and support merge/upsert by stream id. **Status:** ✅ streaming renderer now rebuilds cells directly from `AssistantStreamState`, token usage metadata is upserted alongside deltas, and redundant layout caches were removed (2025-09-26).
|
||||
2.3 **Finalized assistant markdown module** – Use `AssistantMessageState` storing markdown + citations + token metadata. **Status:** ✅ assistant markdown cells now rebuild directly from `AssistantMessageState`, `InsertFinalAnswer` carries stream metadata (citations/token usage) into history state, and redundant raw caching has been removed in favor of state-driven rebuilds (2025-09-26).
|
||||
2.4 **Diff module breakout** – ✅ diff cells now rebuild from `DiffRecord` state with per-width layouts, ChatWidget persists diff snapshots in `HistoryState`, and the renderer applies line-kind styling with dedicated marker columns (2025-09-26).
|
||||
2.5 **Merged exec views** – ✅ merged exec cells now clone `ExecRecord` snapshots for each segment, rebuild layouts on demand (theme-aware caches), and ChatWidget merges/retints using the recorded exec state instead of legacy line buffers (2025-09-26).
|
||||
## Step 2 – Exec / Streaming / Diff Bridge *(In Progress)*
|
||||
2.1 **Exec state extraction & module split** – [~] `ExecCell` now lives in `history_cell/exec.rs` and consumes `ExecRecord`, but we are deliberately keeping the legacy per-width layout caches and wait-state buffers until the new renderer cache (Step 6) lands to avoid regressions.
|
||||
2.2 **Streaming assistant module** – [~] `AssistantStreamState` drives the streaming cell and carries token/citation metadata; `StreamingContentCell` still wraps `AssistantMarkdownCell`, so the internal markdown+layout caches remain as an intentional bridge until the shared renderer cache is ready.
|
||||
2.3 **Finalized assistant markdown module** – [~] `AssistantMessageState` is the source of truth, but `AssistantMarkdownCell` continues to own per-width layout caches for performance; plan to migrate them once Step 6 introduces the centralized cache.
|
||||
2.4 **Diff module breakout** – [~] diff cells read `DiffRecord` hunks, yet the `DiffCell` keeps a per-width layout cache; removal is blocked on the new caching layer.
|
||||
2.5 **Merged exec views** – [~] merged exec cells construct segments from `ExecRecord`, but each segment spins up an `ExecCell` to reuse the old caches; this is acceptable until the shared renderer cache replaces both layers.
|
||||
|
||||
## 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`; plain/background/wait/loading cells, running/completed tool calls, plan updates, upgrade notices, reasoning summaries, exec cells, diff summaries, patch summaries, image previews, **and explore aggregations** now insert exclusively via `history_insert_with_key_global_tagged` + `HistoryState::apply_event`. Next steps: migrate 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/explore/rate-limit cells and keep `history_cell_ids` in sync. Replacement and removal helpers also call `HistoryState::apply_event`; remaining follow-up: emit domain-specific `HistoryEvent::Replace` updates for patch/result metadata and delete the few direct `history_cells` mutations that still rebuild cells inline. **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.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.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.
|
||||
- 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
|
||||
- **Phase A – Inventory:** enumerate every direct mutation (`history_cells[idx] = ...`, `history_cells.remove`, `history_record_from_cell`) and map them to a target `HistoryEvent` variant. Track results in `chatwidget/events_audit.md` (new doc) so progress is visible.
|
||||
- **Phase B – API surface:** extend `HistoryState::apply_event` to accept domain enums (`HistoryDomainEvent`) which wrap core `EventMsg` payloads; expose helpers like `apply_exec_event` to keep match arms local to each module.
|
||||
- **Phase C – Migration waves:**
|
||||
1. Plain/background/loading/wait/tool/plan/upgrade (already idempotent) – replace inline constructors with `HistoryDomainEvent::InsertPlain` etc.
|
||||
2. Exec/stream/assistant – route lifecycle through domain events so streaming merges/final answers run purely on `HistoryState` data.
|
||||
3. Diff/patch/image/explore/rate-limit – remove bespoke mutation helpers and rely on event emissions from their source modules.
|
||||
- **Phase D – Flip ownership:** once all mutations go through domain events, replace `history_cells` with a derived cache built from `HistoryRecord` snapshots + the renderer cache. Provide a debug flag to assert no code path mutates `history_cells` directly.
|
||||
|
||||
## 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.
|
||||
@@ -171,6 +188,20 @@ Status legend: ✅ complete (semantic deterministic state ready), ⏳ still need
|
||||
- Persist history vector in session logs; implement `/undo` by restoring prior snapshot.
|
||||
|
||||
## Step 6 – Performance Improvements *(Blocked on Step 4)*
|
||||
- Memoize per-cell layouts keyed by `(HistoryId, width, theme_epoch)`.
|
||||
- Ship the shared renderer cache (memoized per-cell layouts keyed by `(HistoryId, width, theme_epoch)`) so the interim exec/assistant/diff caches can be removed.
|
||||
- Add instrumentation to measure render latency and scrolling cost pre/post refactor.
|
||||
- Benchmark resume/undo operations using new snapshots.
|
||||
|
||||
### Renderer Cache Design Sketch
|
||||
- **Responsibility:** centralize layout caching in `HistoryRenderState` so individual cells own only semantic state. Cache entries map `(HistoryId, width, theme_epoch, reason_visibility)` → `RenderedCell` (lines + per-row buffers + meta).
|
||||
- **Structure:**
|
||||
- `RenderedCell` wraps immutable `Vec<Line<'static>>` plus precomposed `Vec<Box<[BufferCell]>>` for fast draw.
|
||||
- `HistoryRenderState` keeps an `IndexMap<CacheKey, Arc<RenderedCell>>` with LRU eviction (target: 512 entries, tunable via config).
|
||||
- `CacheKey` includes `theme_epoch` (incremented on palette change) and any feature flags (e.g., reasoning collapsed) so stale entries drop naturally.
|
||||
- **Invalidation hooks:**
|
||||
- `HistoryState::apply_event` emits `HistoryRenderInvalidate` messages when the underlying record changes.
|
||||
- Width changes call `HistoryRenderState::handle_width_change(new_width)` to flush mismatched entries.
|
||||
- Theme swaps bump `theme_epoch`; toggling reasoning detail flips a `ReasoningVisibility` enum baked into the key.
|
||||
- **Warm path:** when rendering, look up `(id, width, theme_epoch, visibility)`. If present, reuse. If missing, invoke cell-specific `render_to_lines(record, settings)` helpers to produce lines, then store.
|
||||
- **Cold path optimizations:** reuse arena-allocated span buffers to reduce allocations; consider hashing markdown bodies to share across identical answers when history rewinds.
|
||||
- **Telemetry:** wrap cache hits/misses with counters exported via `/perf` to confirm the shared cache covers ≥90 % of paint operations before removing per-cell caches.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Plain/Background/Loading/Wait Migration
|
||||
|
||||
## Summary
|
||||
- Replace all plain/background/loading/wait cell insertions and replacements in
|
||||
`chatwidget.rs` with `HistoryDomainEvent` calls so `HistoryState` becomes the
|
||||
source of truth for these stable cell types.
|
||||
- Remove any remaining uses of `history_record_from_cell` for these families
|
||||
and ensure IDs are assigned via the new domain-event helpers.
|
||||
|
||||
## Prerequisites
|
||||
- `HistoryDomainEvent` infrastructure landed and exported from
|
||||
`history/state.rs`.
|
||||
- Background system notices already routed through
|
||||
`history_replace_with_record` (completed 2025-09-27).
|
||||
|
||||
## Scope
|
||||
- `history_insert_plain_cell_with_key`, `history_push_plain_cell`, wait tool
|
||||
completions, loading spinners, and generic system helpers (`push_system_cell`).
|
||||
- Update `assign_history_id` branches that become redundant once the domain
|
||||
events populate IDs.
|
||||
- Extend `events_audit.md` with status notes as paths are migrated.
|
||||
|
||||
## Deliverables
|
||||
- New domain-event constructors for `PlainMessageState`, `WaitStatusState`,
|
||||
`LoadingState`, and `BackgroundEventRecord`.
|
||||
- `chatwidget.rs` updated to call `history_state.apply_domain_event(...)`
|
||||
(exact API name per infrastructure work).
|
||||
- Unit or integration coverage that emits events and confirms `HistoryState`
|
||||
stores the expected records without referencing `history_cells` directly.
|
||||
- Documentation update in `events_audit.md` marking these categories as
|
||||
migrated.
|
||||
|
||||
## References
|
||||
- `codex-rs/tui/HISTORY_CELLS_PLAN.md` – Step 3, Event Pipeline Consolidation
|
||||
Plan (Phase C – wave 1).
|
||||
- `codex-rs/tui/src/chatwidget/events_audit.md` – mutation inventory.
|
||||
@@ -0,0 +1,35 @@
|
||||
# History Renderer Cache Implementation
|
||||
|
||||
## Summary
|
||||
- Implement the shared renderer cache described in Step 6 so per-cell layout
|
||||
caches (exec/assistant/diff) can be retired once all migrations are complete.
|
||||
- Provide APIs for cache lookup, invalidation, and telemetry that integrate with
|
||||
the `HistoryDomainEvent` pipeline.
|
||||
|
||||
## Prerequisites
|
||||
- Domain-event migrations for waves 1 & 2 complete (cells reconstruct from
|
||||
`HistoryState` without mutating `history_cells`).
|
||||
- `HistoryRenderState` ready to accept cache invalidation signals.
|
||||
|
||||
## Scope
|
||||
- Introduce `RenderedCell` structs, cache key types, and LRU storage inside
|
||||
`history_render.rs` (or a new module) based on the design sketch in
|
||||
`HISTORY_CELLS_PLAN.md`.
|
||||
- Expose invalidation hooks that respond to History mutations (insert/replace/
|
||||
remove) and viewport/theme changes.
|
||||
- Wire ChatWidget rendering paths to consult the shared cache before invoking
|
||||
cell-specific rendering logic.
|
||||
- Define instrumentation (counters or tracing) to measure hit/miss rates.
|
||||
- Plan removal of legacy per-cell caches once cache hit rates are validated.
|
||||
|
||||
## Deliverables
|
||||
- New caching module with unit tests covering key eviction, width/theme
|
||||
invalidation, and reasoning-visibility variants.
|
||||
- ChatWidget rendering updated to consume cached buffers for all migrated cell
|
||||
types.
|
||||
- Documentation update (HISTORY_CELLS_PLAN.md Step 6) noting completion and
|
||||
instructions for removing cell-local caches.
|
||||
|
||||
## References
|
||||
- `codex-rs/tui/HISTORY_CELLS_PLAN.md` – Step 6 renderer cache design sketch.
|
||||
- `codex-rs/tui/src/chatwidget/history_render.rs` – existing memoization hooks.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Streaming / Exec / Assistant Migration
|
||||
|
||||
## Summary
|
||||
- Move streaming deltas, finalized assistant messages, exec lifecycle updates,
|
||||
and merged exec summaries onto the `HistoryDomainEvent` pipeline.
|
||||
- Eliminate cell-side state mutations and redundant cached line buffers where
|
||||
practical, while preserving the interim cache bridge called out in Step 2.
|
||||
|
||||
## Prerequisites
|
||||
- Infrastructure: `HistoryDomainEvent` enums + helpers merged.
|
||||
- Wave 1 (plain/background/loading/wait) migrated so supporting utilities are
|
||||
battle-tested.
|
||||
|
||||
## Scope
|
||||
- Streaming handlers: `ensure_answer_stream_state`, tail updates, review-flow
|
||||
paths, and final message insertions (`chatwidget.rs:11380-11890`).
|
||||
- Exec lifecycle: active exec state, merged exec summaries, wait notes, and
|
||||
tool completion merges (`chatwidget.rs:6515-7330`).
|
||||
- Ensure `history_cell::ExecCell`, `StreamingContentCell`, and
|
||||
`AssistantMarkdownCell` rebuild purely from `HistoryState` data after events.
|
||||
- Update `assign_history_id` only where still required; prefer storing IDs via
|
||||
the domain events.
|
||||
|
||||
## Deliverables
|
||||
- Domain-event variants covering exec begin/end/stream chunks, assistant stream
|
||||
deltas, assistant final messages, and merged exec summaries.
|
||||
- ChatWidget handlers updated to emit domain events instead of mutating
|
||||
`history_cells` directly.
|
||||
- Regression tests (prefer snapshot-based tests around
|
||||
`chatwidget::tests::run_script`) validating that streaming/exec flows produce
|
||||
equivalent UI output after the migration.
|
||||
- Notes added to `events_audit.md` marking the executive/assistant entries as
|
||||
completed or updated with any residual technical debt.
|
||||
|
||||
## References
|
||||
- `codex-rs/tui/HISTORY_CELLS_PLAN.md` – Step 2 bridge & Step 3 Phase C wave 2.
|
||||
- `codex-rs/tui/src/chatwidget/events_audit.md` – exec/stream mutation list.
|
||||
@@ -209,6 +209,8 @@ use crate::history_cell::PatchEventType;
|
||||
use crate::history_cell::PlainHistoryCell;
|
||||
use crate::history::state::PatchEventType as HistoryPatchEventType;
|
||||
use crate::history::state::{
|
||||
HistoryDomainEvent,
|
||||
HistoryDomainRecord,
|
||||
AssistantMessageState,
|
||||
AssistantStreamDelta,
|
||||
AssistantStreamState,
|
||||
@@ -1255,20 +1257,25 @@ impl ChatWidget<'_> {
|
||||
/// If `id_for_replace` is provided and we have a prior index for it, replace in place.
|
||||
fn push_system_cell(
|
||||
&mut self,
|
||||
cell: impl HistoryCell + 'static,
|
||||
cell: Box<dyn HistoryCell>,
|
||||
placement: SystemPlacement,
|
||||
id_for_replace: Option<String>,
|
||||
order: Option<&codex_core::protocol::OrderMeta>,
|
||||
tag: &'static str,
|
||||
record: Option<HistoryDomainRecord>,
|
||||
) {
|
||||
if let Some(id) = id_for_replace.as_ref() {
|
||||
if let Some(&idx) = self.system_cell_by_id.get(id) {
|
||||
self.history_replace_at(idx, Box::new(cell));
|
||||
if let Some(record) = record.clone() {
|
||||
self.history_replace_with_record(idx, cell, record);
|
||||
} else {
|
||||
self.history_replace_at(idx, cell);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
let key = self.system_order_key(placement, order);
|
||||
let pos = self.history_insert_with_key_global_tagged(Box::new(cell), key, tag, None);
|
||||
let pos = self.history_insert_with_key_global_tagged(cell, key, tag, record);
|
||||
if let Some(id) = id_for_replace {
|
||||
self.system_cell_by_id.insert(id, pos);
|
||||
}
|
||||
@@ -4239,7 +4246,7 @@ impl ChatWidget<'_> {
|
||||
cell: Box<dyn HistoryCell>,
|
||||
key: OrderKey,
|
||||
tag: &'static str,
|
||||
record: Option<HistoryRecord>,
|
||||
record: Option<HistoryDomainRecord>,
|
||||
) -> usize {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -4349,8 +4356,19 @@ impl ChatWidget<'_> {
|
||||
|
||||
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 {
|
||||
let maybe_id = 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, .. } => {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
Some(id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} 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
|
||||
@@ -4640,7 +4658,7 @@ impl ChatWidget<'_> {
|
||||
key: OrderKey,
|
||||
tag: &'static str,
|
||||
) -> usize {
|
||||
let record = HistoryRecord::PlainMessage(cell.state().clone());
|
||||
let record = HistoryDomainRecord::Plain(cell.state().clone());
|
||||
self.history_insert_with_key_global_tagged(Box::new(cell), key, tag, Some(record))
|
||||
}
|
||||
|
||||
@@ -4681,12 +4699,15 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let cell = history_cell::new_background_event(message);
|
||||
let record = HistoryDomainRecord::BackgroundEvent(cell.state().clone());
|
||||
self.push_system_cell(
|
||||
history_cell::new_background_event(message),
|
||||
Box::new(cell),
|
||||
system_placement,
|
||||
None,
|
||||
None,
|
||||
"background",
|
||||
Some(record),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4712,6 +4733,42 @@ impl ChatWidget<'_> {
|
||||
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "prompt", None);
|
||||
}
|
||||
|
||||
fn history_replace_with_record(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
mut cell: Box<dyn HistoryCell>,
|
||||
record: HistoryDomainRecord,
|
||||
) {
|
||||
if idx >= self.history_cells.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let record_idx = self
|
||||
.record_index_for_cell(idx)
|
||||
.unwrap_or_else(|| self.record_index_for_position(idx));
|
||||
|
||||
let mutation = self.history_state.apply_domain_event(HistoryDomainEvent::Replace {
|
||||
index: record_idx,
|
||||
record,
|
||||
});
|
||||
|
||||
if let Some(id) = match mutation {
|
||||
HistoryMutation::Replaced { id, .. } => Some(id),
|
||||
HistoryMutation::Inserted { id, .. } => Some(id),
|
||||
_ => None,
|
||||
} {
|
||||
self.assign_history_id(&mut cell, id);
|
||||
if idx < self.history_cell_ids.len() {
|
||||
self.history_cell_ids[idx] = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
self.history_cells[idx] = cell;
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
self.refresh_explore_trailing_flags();
|
||||
}
|
||||
|
||||
fn history_replace_at(&mut self, idx: usize, mut cell: Box<dyn HistoryCell>) {
|
||||
if idx >= self.history_cells.len() {
|
||||
return;
|
||||
@@ -6671,7 +6728,7 @@ impl ChatWidget<'_> {
|
||||
Box::new(cell),
|
||||
key,
|
||||
"rate-limits",
|
||||
Some(HistoryRecord::RateLimits(record)),
|
||||
Some(HistoryDomainRecord::RateLimits(record)),
|
||||
);
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -7382,12 +7439,15 @@ impl ChatWidget<'_> {
|
||||
};
|
||||
let id_for_replace = Some(id.clone());
|
||||
let message_clone = message.clone();
|
||||
let cell = history_cell::new_background_event(message_clone);
|
||||
let record = HistoryDomainRecord::BackgroundEvent(cell.state().clone());
|
||||
self.push_system_cell(
|
||||
history_cell::new_background_event(message_clone),
|
||||
Box::new(cell),
|
||||
placement,
|
||||
id_for_replace,
|
||||
event.order.as_ref(),
|
||||
"background",
|
||||
Some(record),
|
||||
);
|
||||
// If we inserted during streaming, keep the reasoning ellipsis visible.
|
||||
self.restore_reasoning_in_progress_if_streaming();
|
||||
@@ -10321,12 +10381,14 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
let placement = self.ui_placement_for_now();
|
||||
let cell = history_cell::new_model_output(&self.config.model, self.config.model_reasoning_effort);
|
||||
self.push_system_cell(
|
||||
history_cell::new_model_output(&self.config.model, self.config.model_reasoning_effort),
|
||||
Box::new(cell),
|
||||
placement,
|
||||
Some("ui:model".to_string()),
|
||||
None,
|
||||
"system",
|
||||
None,
|
||||
);
|
||||
|
||||
self.request_redraw();
|
||||
@@ -10561,12 +10623,14 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Add status message to history (replaceable system notice)
|
||||
let placement = self.ui_placement_for_now();
|
||||
let cell = history_cell::new_reasoning_output(&new_effort);
|
||||
self.push_system_cell(
|
||||
history_cell::new_reasoning_output(&new_effort),
|
||||
Box::new(cell),
|
||||
placement,
|
||||
Some("ui:reasoning".to_string()),
|
||||
None,
|
||||
"system",
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10742,12 +10806,15 @@ impl ChatWidget<'_> {
|
||||
};
|
||||
let message = format!("Theme changed to {}", theme_name);
|
||||
let placement = self.ui_placement_for_now();
|
||||
let cell = history_cell::new_background_event(message);
|
||||
let record = HistoryDomainRecord::BackgroundEvent(cell.state().clone());
|
||||
self.push_system_cell(
|
||||
history_cell::new_background_event(message),
|
||||
Box::new(cell),
|
||||
placement,
|
||||
Some("ui:theme".to_string()),
|
||||
None,
|
||||
"background",
|
||||
Some(record),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10768,12 +10835,15 @@ impl ChatWidget<'_> {
|
||||
// Confirmation message (replaceable system notice)
|
||||
let message = format!("Spinner changed to {}", spinner_name);
|
||||
let placement = self.ui_placement_for_now();
|
||||
let cell = history_cell::new_background_event(message);
|
||||
let record = HistoryDomainRecord::BackgroundEvent(cell.state().clone());
|
||||
self.push_system_cell(
|
||||
history_cell::new_background_event(message),
|
||||
Box::new(cell),
|
||||
placement,
|
||||
Some("ui:spinner".to_string()),
|
||||
None,
|
||||
"background",
|
||||
Some(record),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# ChatWidget History Event Audit (2025-09-27)
|
||||
|
||||
This note summarizes the places where `ChatWidget` still mutates `history_cells`
|
||||
directly so we can migrate them onto the `HistoryState::apply_event` pipeline.
|
||||
Line numbers reference `codex-rs/tui/src/chatwidget.rs` as of 2025-09-27.
|
||||
|
||||
## Structural mutations
|
||||
|
||||
- **Global insert path** – `history_insert_with_key_global_tagged`
|
||||
(`chatwidget.rs:4638`) now accepts optional `HistoryDomainRecord` inputs. When
|
||||
present (e.g., background/system notices as of 2025-09-27) it routes through
|
||||
`HistoryState::apply_domain_event(Insert)` and assigns IDs before the cell is
|
||||
stored. Other callers still fall back to `history_record_from_cell`.
|
||||
- **Indexed replacement** – `history_replace_at` (`chatwidget.rs:4715`) rebuilds a
|
||||
record from the incoming cell, applies `HistoryEvent::Replace`, then writes
|
||||
`history_cells[idx] = cell`. Several call sites mutate the cell before it flows
|
||||
here (e.g., plan/patch updates).
|
||||
- **Removal path** – `history_remove_at` (`chatwidget.rs:4781`) mirrors
|
||||
`HistoryEvent::Remove` and then calls `history_cells.remove(idx)` alongside the
|
||||
auxiliary ID/order vectors.
|
||||
- **Patch apply success** – `handle_patch_apply_end_now`
|
||||
(`chatwidget.rs:2240-2338`) finds a prior patch cell, mutates its state in
|
||||
place (including manual text span edits), and calls `history_record_from_cell`
|
||||
+ `HistoryEvent::Replace`. This is a prime candidate for a dedicated
|
||||
`HistoryDomainEvent::PatchOutcome` handler.
|
||||
- **Streaming updates** – the streaming lifecycle functions repeatedly locate
|
||||
cells via `rposition` and mutate them directly:
|
||||
- tail update (`chatwidget.rs:11390-11436`)
|
||||
- indexed update (`chatwidget.rs:11417-11446`)
|
||||
- final message hydration (`chatwidget.rs:11782-11860`)
|
||||
- review-flow variants (`chatwidget.rs:11660-11747`)
|
||||
These still rely on cell-owned caches and in-place mutation.
|
||||
- **Exec lifecycle** – exec merge/update logic holds an active `ExecCell` in
|
||||
`active_exec_cell` and downcasts cells when updates arrive
|
||||
(`chatwidget.rs:7159-7184`, `7233-7242`).
|
||||
- **Background/system notices** – `push_system_cell` (`chatwidget.rs:1254`) now
|
||||
uses `HistoryDomainRecord::BackgroundEvent` for inserts/replacements, so these
|
||||
paths no longer reconstruct records from existing cells (migrated 2025-09-27).
|
||||
- **Reasoning merge** – reasoning stream handling mutates the stored
|
||||
`CollapsibleReasoningCell` state (`chatwidget.rs:1440-2058`,
|
||||
`11288-11336`).
|
||||
|
||||
## Non-structural traversal (stateful toggles)
|
||||
|
||||
The following loops walk `history_cells` to update cached state or trigger
|
||||
animations; they should eventually derive from `HistoryRecord` once the renderer
|
||||
cache lands, but they are less urgent than the structural mutations above.
|
||||
|
||||
- `clear_reasoning_in_progress` / `set_show_reasoning`
|
||||
(`chatwidget.rs:1991-2034`).
|
||||
- `on_composer_expanded`, `toggle_reasoning_visibility`, export helpers, etc.
|
||||
- Animation checks (`chatwidget.rs:3174`, `18153`, `18309`).
|
||||
|
||||
## Migration plan mapping
|
||||
|
||||
- **Phase A (inventory complete)** – this document captures all direct mutation
|
||||
sites that need event coverage.
|
||||
- **Phase B (API surface)** – introduce `HistoryDomainEvent` enums per category:
|
||||
`Plain`, `Tool`, `Exec`, `Streaming`, `Patch`, `Diff`, etc. Each enum variant
|
||||
carries the payload currently embedded in the cell mutation.
|
||||
- **Phase C (migration waves)** – replace call-site logic:
|
||||
1. Plain/background/loading/wait/tool/plan/upgrade – already flow through
|
||||
insert/replace; only need to move pre-insert mutation into domain event
|
||||
constructors.
|
||||
2. Exec/stream/assistant – emit domain events so state changes happen inside
|
||||
`HistoryState`, then render from the resulting record.
|
||||
3. Diff/patch/image/explore/rate-limit – replace bespoke `history_cells`
|
||||
mutations with domain events and remove the remaining uses of
|
||||
`history_record_from_cell`.
|
||||
- **Phase D (ownership flip)** – once all events route through
|
||||
`HistoryState::apply_event`, collapse `history_cells` into a derived cache
|
||||
(fed by the Step 6 renderer cache) and guard with debug asserts to prevent
|
||||
new direct mutations.
|
||||
@@ -28,6 +28,84 @@ pub enum HistoryRecord {
|
||||
Notice(NoticeRecord),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum HistoryDomainEvent {
|
||||
Insert {
|
||||
index: usize,
|
||||
record: HistoryDomainRecord,
|
||||
},
|
||||
Replace {
|
||||
index: usize,
|
||||
record: HistoryDomainRecord,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum HistoryDomainRecord {
|
||||
Plain(PlainMessageState),
|
||||
WaitStatus(WaitStatusState),
|
||||
Loading(LoadingState),
|
||||
BackgroundEvent(BackgroundEventRecord),
|
||||
RateLimits(RateLimitsRecord),
|
||||
}
|
||||
|
||||
impl From<PlainMessageState> for HistoryDomainRecord {
|
||||
fn from(state: PlainMessageState) -> Self {
|
||||
HistoryDomainRecord::Plain(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WaitStatusState> for HistoryDomainRecord {
|
||||
fn from(state: WaitStatusState) -> Self {
|
||||
HistoryDomainRecord::WaitStatus(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LoadingState> for HistoryDomainRecord {
|
||||
fn from(state: LoadingState) -> Self {
|
||||
HistoryDomainRecord::Loading(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackgroundEventRecord> for HistoryDomainRecord {
|
||||
fn from(state: BackgroundEventRecord) -> Self {
|
||||
HistoryDomainRecord::BackgroundEvent(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RateLimitsRecord> for HistoryDomainRecord {
|
||||
fn from(state: RateLimitsRecord) -> Self {
|
||||
HistoryDomainRecord::RateLimits(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryDomainRecord {
|
||||
fn into_history_record(self) -> HistoryRecord {
|
||||
match self {
|
||||
HistoryDomainRecord::Plain(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::PlainMessage(state)
|
||||
}
|
||||
HistoryDomainRecord::WaitStatus(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::WaitStatus(state)
|
||||
}
|
||||
HistoryDomainRecord::Loading(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::Loading(state)
|
||||
}
|
||||
HistoryDomainRecord::BackgroundEvent(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::BackgroundEvent(state)
|
||||
}
|
||||
HistoryDomainRecord::RateLimits(mut state) => {
|
||||
state.id = HistoryId::ZERO;
|
||||
HistoryRecord::RateLimits(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PlainMessageState {
|
||||
pub id: HistoryId,
|
||||
@@ -677,6 +755,19 @@ impl HistoryState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_domain_event(&mut self, event: HistoryDomainEvent) -> HistoryMutation {
|
||||
match event {
|
||||
HistoryDomainEvent::Insert { index, record } => {
|
||||
let record = record.into_history_record();
|
||||
self.apply_event(HistoryEvent::Insert { index, record })
|
||||
}
|
||||
HistoryDomainEvent::Replace { index, record } => {
|
||||
let record = record.into_history_record();
|
||||
self.apply_event(HistoryEvent::Replace { index, record })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
Reference in New Issue
Block a user