Skip to content

feat(tui): add multi-tab system core (manager + persistence)#2864

Merged
Hmbown merged 3 commits into
Hmbown:codex/v0.9.0-stewardshipfrom
ljm3790865:feat/tab-core-narrow
Jun 6, 2026
Merged

feat(tui): add multi-tab system core (manager + persistence)#2864
Hmbown merged 3 commits into
Hmbown:codex/v0.9.0-stewardshipfrom
ljm3790865:feat/tab-core-narrow

Conversation

@ljm3790865
Copy link
Copy Markdown

@ljm3790865 ljm3790865 commented Jun 6, 2026

Summary

Sister PR to #2753, scoped to the narrow tab-core/persistence slice Hmbown asked for in the v0.9 stewardship review. Adds the tab module under crates/tui/src/tui/ and a one-line module registration in tui/mod.rs. Nothing else in the host changes here — the switcher / picker / meeting UI pass and the host wiring (App::tab_manager, keyboard shortcuts, mouse menu, tab-bar layout in ui.rs) live on #2753 and land in a follow-up PR.

This PR is intentionally an additive slice against codex/v0.9.0-stewardship:

  • new crates/tui/src/tui/tab/ directory, no overlapping commits
  • one-line pub mod tab; added to crates/tui/src/tui/mod.rs (alphabetical)
  • one pre-existing crates/tui/src/tools/shell.rs redundant cast fix (unrelated but required to keep the local CI matrix green on the current clippy -D warnings)

The conflicts Hmbown called out in #2753 (app.rs, ui.rs, views/mod.rs, mouse_ui.rs) are deliberately not on this branch, so a rebase on top of codex/v0.9.0-stewardship is a fast-forward with no merge resolution.

What's in the slice

File What it adds
tab/mod.rs Module entry, pub use TabManager, type re-exports, base types (TabId, TabMetadata, TabType, Priority, TabStatus)
tab/manager.rs TabManager with monotonic next_tab_id, max 9 tabs by default, snapshot/restore round-trip, group assignment, cross-tab event/links, persistence integration
tab/persistence.rs JSON file in the user's data dir, PersistedTabState / PersistedTab / PersistedDelegation / PersistedGroup with schema-version header, atomic save, bounded file size, corruption-tolerant load
tab/delegator.rs TaskDelegator — bounded pending queue with MAX_COMPLETED_RESULTS auto-prune; take_pending_for_tab marks InProgress in place and returns a clone so subsequent start_task / complete / fail_task / cancel_task can still find the task (the previous swap_remove would have dropped it on the first call)
tab/meeting.rs MeetingManager — participants, messages by type (Regular / Question / Answer / Proposal / Agreement / Objection / Summary), decisions
tab/cross_tab.rs CrossTabEvent (TaskDelegation / ReviewRequest / MeetingInvite / ContextSync / ResultReturn) and SharedContext
tab/group.rs TabGroup / TabGroupManager / GroupColor (Red/Orange/Yellow/Green/Cyan/Blue/Magenta/Gray)
tab/mention.rs @Tab<N>, @N, @tab<n> (case-insensitive) parser, with resolve_tab_mention for 1-indexed tab lookups
tab/benches.rs Roundtrip / save-load benchmark
tab/key_e2e.rs End-to-end save→load with keymap

Lint posture

  • #![allow(dead_code, unused_imports)] on tab/mod.rs because the collab/UI pass is not on this branch; the public surface is intentionally exposed for the follow-up wiring in feat(tui): multi-tab system with cross-tab collaboration #2753. Each #[allow] is co-located with a one-line WHY comment so a future PR can drop the allow as the collab/UI pass lands.
  • The pre-existing tools/shell.rs redundant cast (child.as_raw_handle() as *mut c_void) is fixed in passing because rust 1.95 promotes it to clippy::unnecessary_cast. Without the fix, cargo clippy --workspace --all-features --locked -- -D warnings (the CI command) would fail on this branch before it ever builds the new code.

Local CI matrix

Same flags as .github/workflows/ci.yml, run on a Windows runner:

step result
cargo fmt --all -- --check pass
cargo clippy --workspace --all-features --locked -- -D warnings pass, 0 errors, 0 warnings
cargo test --bin codewhale-tui tab:: 63/63 pass
cargo test --bin codewhale-tui delegator:: 7/7 pass
cargo test --bin codewhale-tui meeting:: 1/1 pass
git diff --exit-code -- Cargo.lock clean

The GitHub Actions run will need the standard fork-PR approval gate; once approved, the same matrix will execute on ubuntu-latest, macos-latest, and windows-latest. The 9-tab test suite and persistence round-trip cover the parts of the slice that the steward can break from the host side.

Test plan

  • cargo test --bin codewhale-tui tab:: — 63 unit tests cover TabManager invariants (max-tabs, monotonic id, snapshot/restore of delegations and groups, post-restore id uniqueness), DelegationTask lifecycle, MeetingManager state machine, @-mention parsing including case-insensitivity, persistence round-trip, schema-version warning, atomic save, oversized-file rejection, keymap end-to-end save/load
  • Manual: create several tabs, restart, confirm tabs and groups survive
  • Manual: @Tab2 parsing in agent messages (the parse function is unit-tested; the in-flight wiring is in feat(tui): multi-tab system with cross-tab collaboration #2753)

Follow-up

A second PR on top of this one will add the tab/tab_bar.rs renderer and the host wiring (App::tab_manager, ui.rs keyboard/mouse integration, views/tab_switcher.rs, views/tab_picker.rs, views/meeting_view.rs, views/mod.rs::ModalKind::Meeting). That work is staged on #2753 to keep this slice narrow per Hmbown's plan.

🤖 Generated with Claude Code

Greptile Summary

Adds the tab module under crates/tui/src/tui/ — a self-contained multi-tab core (manager, persistence, delegation, groups, meetings, @-mention parser) intentionally staged without host wiring, plus a one-line Clippy cast fix in tools/shell.rs.

  • TabManager manages up to 9 tabs with monotonic ID assignment, snapshot/restore that correctly advances all counters past restored IDs, and delegation validated against live tab existence.
  • TaskDelegator uses a VecDeque bounded at 256 for completed results, take_pending_for_tab marks in-place (no swap_remove on take) so lifecycle calls (complete/fail_task/cancel_task) can still locate the task by ID.
  • Persistence writes atomically via temp-file rename with a 1 MB size guard, schema-version logging, and corruption-tolerant load; close_tab does not cancel delegations referencing the removed tab, so those tasks survive into the next snapshot and are blindly restored even when the referenced tab no longer exists.

Confidence Score: 3/5

Merging now introduces a persistence defect: closing a tab leaves its in-flight delegations in the queue, they get written to disk, and are fully restored on the next startup even though the referenced tab is gone.

The additive slice is well-structured and the ID-collision, naming, and mention-ordering issues called out in previous rounds have all been addressed. The remaining gap is close_tab: it cleans up group membership but leaves delegator.pending_tasks holding tasks whose from_tab/to_tab point at the now-deleted tab. snapshot() includes those tasks via all_pending(), and restore_from_snapshot pushes them back without validating that the referenced tabs exist — so orphaned delegations accumulate across restarts. Because the persistence round-trip tests do not close tabs before saving, the defect is not caught by the current test suite.

manager.rs (close_tab and restore_from_snapshot) is the focus — the delegation cleanup gap lives there. persistence.rs is secondary (temp-file cleanup on rename failure).

Important Files Changed

Filename Overview
crates/tui/src/tui/tab/manager.rs Core TabManager with tab lifecycle, switching, group and delegation APIs; orphaned delegations are not cancelled when a tab is closed, surviving into snapshots and restores.
crates/tui/src/tui/tab/delegator.rs TaskDelegator with bounded completed-results queue (VecDeque, 256 cap), priority-sorted take, and correct post-restore ID advancement via advance_next_id_past_existing_tasks.
crates/tui/src/tui/tab/persistence.rs Atomic save (write temp + rename), 1 MB size guard, schema-version logging; temp file can be left behind on a failed rename.
crates/tui/src/tui/tab/meeting.rs MeetingManager with typed message kinds and decision tracking; history vec is unbounded unlike the delegation completed-results queue.
crates/tui/src/tui/tab/mention.rs Byte-level @tab<n>/@<n> parser with overflow-safe parse_usize; resolve_tab_mention uses caller-supplied positional order rather than sorting by ID (previous concern addressed).
crates/tui/src/tui/tab/group.rs TabGroupManager with monotonic counter-based IDs and post-restore advancement; TabGroup::new() still uses timestamp IDs (flagged previously), but that constructor is not called by the manager.
crates/tui/src/tui/tab/cross_tab.rs CrossTabEvent enum and SharedContext data types; all dead_code, no functional logic wired yet.
crates/tui/src/tui/tab/mod.rs Module entry with base types (TabId, TabType, TabMetadata, Priority, TabStatus) and re-export of TabManager; intentional dead_code allow with WHY comments for each module.
crates/tui/src/tools/shell.rs One-line clippy fix: removes unnecessary as *mut core::ffi::c_void cast from child.as_raw_handle() for Windows job object assignment.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[create_tab] -->|monotonic next_tab_id| B[TabState in tabs vec]
    B --> C{delegate_task}
    C -->|both tabs exist| D[TaskDelegator.pending_tasks\nstatus=Pending]
    C -->|unknown tab| E[return None]
    D --> F[take_next_delegation\nmarks InProgress in-place]
    F --> G{complete / fail / cancel}
    G -->|complete| H[swap_remove → DelegationResult\nin completed_results VecDeque\nbounded at 256]
    G -->|fail / cancel| H

    B --> I[close_tab]
    I -->|unassign from group| J[TabGroupManager]
    I -->|⚠️ does NOT cancel| D

    B --> K[snapshot]
    K -->|all_pending incl. orphans| L[PersistedTabState JSON]
    L -->|atomic write via .tmp rename| M[tabs.json on disk]
    M --> N[restore_from_snapshot]
    N -->|no tab-ID validation| D
Loading

Comments Outside Diff (2)

  1. crates/tui/src/tui/tab/delegator.rs, line 596-603 (link)

    P1 next_id reset on restore causes task-ID collision

    TaskDelegator::new() sets next_id = 1. When restore_from_snapshot creates a fresh TaskDelegator and pushes back pending tasks (which may have IDs like "delegation_42"), the counter stays at 1. After the user creates 41 more delegations in the same session, the 42nd will get ID "delegation_42" — colliding with the restored pending task. start_task, complete, fail_task, and cancel_task all find tasks by string ID via iter().find(|t| t.task_id == task_id), so the collision silently mutates or completes the wrong task. The snapshot should persist next_id (or compute it as max(restored_task_numeric_suffix) + 1 on restore) so the counter never re-issues an already-used ID.

    Fix in Codex Fix in Claude Code Fix in Cursor

  2. crates/tui/src/tui/tab/mention.rs, line 3127-3137 (link)

    P1 resolve_tab_mention sorts by ID value, not by visual/display order

    The function sorts the caller-supplied tab IDs numerically, then treats index tab_number - 1 of the sorted slice as the target. But TabManager orders tabs by insertion (the tabs: Vec<TabState> vector), so the user sees @Tab1 as the leftmost tab in the bar, @Tab2 as the second, and so on — regardless of their raw TabId values. After a session restore, IDs are preserved from before but insertion order may differ; in any case the sorted-ID order does not match what the user sees.

    Concretely: if the user has three tabs with IDs [100, 50, 200] in that visual order, resolve_tab_mention(1, [100, 50, 200]) returns 50 (the smallest ID, Tab B), but the user intended 100 (the leftmost visible tab, Tab A). The function should instead accept IDs already in display order and use positional lookup without sorting.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (4): Last reviewed commit: "fix(tui): advance tab restore counters" | Re-trigger Greptile

Sister PR to Hmbown#2753, scoped to the narrow tab-core/persistence slice
Hmbown asked for in the v0.9 stewardship review. Adds the `tab`
module under `crates/tui/src/tui/` and a one-line module registration
in `tui/mod.rs`. Nothing else in the host changes here — the
switcher / picker / meeting UI pass and the host wiring
(`App::tab_manager`, keyboard shortcuts, mouse menu, tab-bar layout
in `ui.rs`) live on Hmbown#2753 and land in a follow-up PR.

Scope:

* `tab::TabManager` with monotonic `next_tab_id`, max 9 tabs by
  default, snapshot/restore round-trip, group assignment,
  cross-tab event/links, and persistence integration
* `tab::delegator::TaskDelegator` — bounded pending queue with
  `MAX_COMPLETED_RESULTS` auto-prune; `take_pending_for_tab` marks
  InProgress in place and returns a clone so subsequent
  `start_task` / `complete` / `fail_task` / `cancel_task` can still
  find the task (the previous `swap_remove` would have dropped it
  on the first call)
* `tab::meeting::MeetingManager` — participants, messages by
  type (Regular / Question / Answer / Proposal / Agreement /
  Objection / Summary), decisions
* `tab::cross_tab` — `CrossTabEvent` (TaskDelegation / ReviewRequest
  / MeetingInvite / ContextSync / ResultReturn) and `SharedContext`
* `tab::group` — `TabGroup` / `TabGroupManager` /
  `GroupColor` (Red/Orange/Yellow/Green/Cyan/Blue/Magenta/Gray)
* `tab::mention` — `@Tab<N>`, `@N`, `@tab<n>` (case-insensitive)
  parser, with `resolve_tab_mention` for 1-indexed tab lookups
* `tab::persistence` — JSON file in the user's data dir,
  `PersistedTabState` / `PersistedTab` / `PersistedDelegation` /
  `PersistedGroup` with schema-version header, atomic save,
  bounded file size, corruption-tolerant load
* `tab::benches` and `tab::key_e2e` regression suites (roundtrip,
  save/load, end-to-end save→load with keymap)

Lint posture:

* `#![allow(dead_code, unused_imports)]` on `tab/mod.rs` because
  the collab/UI pass is not on this branch; the public surface is
  intentionally exposed for the follow-up wiring in Hmbown#2753
* One pre-existing `tools/shell.rs` fix piggy-backed: drop the
  redundant `as *mut c_void` cast on `child.as_raw_handle()` (the
  return type is already `*mut c_void`). Pre-existing on
  `codex/v0.9.0-stewardship`; promoted to `clippy::unnecessary_cast`
  by rust 1.95

Local CI matrix on this branch (Windows runner, same flags as
`.github/workflows/ci.yml`):

* `cargo fmt --all -- --check` — pass
* `cargo clippy --workspace --all-features --locked -- -D warnings`
  — pass, 0 errors
* `cargo test --bin codewhale-tui tab::` — 63/63 pass
* `git diff --exit-code -- Cargo.lock` — clean

GitHub Actions will run on the standard fork-PR approval gate; the
`on.pull_request` branch filter on this repo matches
`codex/v0.9.0-stewardship` so the same matrix will run on
ubuntu-latest, macos-latest, and windows-latest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 6, 2026

Thanks @ljm3790865 for taking the time to contribute.

This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive multi-tab system for the CodeWhale TUI, implementing core tab management, task delegation, multi-agent meetings, tab grouping, @-mention parsing, and state persistence. Feedback on these changes highlights several critical areas for improvement: cleaning up orphaned delegation tasks and active meetings when a tab is closed to prevent state leaks; returning an error instead of silently returning a default state when the persistence file is oversized; preserving the visual order of tabs in resolve_tab_mention by removing the sorting step; persisting the status of delegations and cross_tab_links across restarts; and adding defensive validation checks for tab IDs in delegate_task and start_meeting.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +306 to +316
/// Close a tab by index
pub fn close_tab(&mut self, index: usize) -> bool {
if index >= self.tabs.len() {
return false;
}

// Unassign from any group before removing
let tab_id = self.tabs[index].metadata.id;
self.groups.unassign_tab(tab_id);

self.tabs.remove(index);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When a tab is closed via close_tab, its associated delegation tasks in self.delegator.pending_tasks and its participation in active meetings in self.meeting_manager are not cleaned up. This leads to orphaned references and state leaks, as these tasks and meetings can never be completed or updated once the tab is gone. Consider cleaning up these references on tab closure (e.g., by retaining only tasks where from_tab and to_tab are still active, and removing the closed tab from active meetings).

Comment on lines +124 to +132
if metadata.len() > MAX_FILE_SIZE {
tracing::warn!(
size = metadata.len(),
max = MAX_FILE_SIZE,
path = %path.display(),
"Tab state file too large, ignoring"
);
return Ok(PersistedTabState::default());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning Ok(PersistedTabState::default()) when the tab state file exceeds MAX_FILE_SIZE is dangerous. If the file is oversized (e.g., due to corruption or unexpected growth), silently ignoring it and returning an empty state means that the next save operation will overwrite the file, permanently destroying the user's previous tabs. It is much safer to return an io::Error (similar to how JSON parsing errors are handled) so the application can detect the failure and avoid silently wiping the user's data.

    if metadata.len() > MAX_FILE_SIZE {
        tracing::error!(
            size = metadata.len(),
            max = MAX_FILE_SIZE,
            path = %path.display(),
            "Tab state file too large"
        );
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("Tab state file size {} exceeds maximum allowed size {}", metadata.len(), MAX_FILE_SIZE),
        ));
    }

Comment on lines +154 to +164
pub fn resolve_tab_mention<'a, I>(tab_number: usize, tab_ids: I) -> Option<u64>
where
I: IntoIterator<Item = &'a u64>,
{
let mut sorted: Vec<u64> = tab_ids.into_iter().copied().collect();
sorted.sort_unstable();
if tab_number == 0 || tab_number > sorted.len() {
return None;
}
Some(sorted[tab_number - 1])
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Sorting the tab_ids in resolve_tab_mention is a semantic bug. Tab mentions (like @Tab2 or @2) should correspond to the visual order of the tabs in the tab bar (i.e., their index in self.tabs), which is the order passed by the caller. By sorting the IDs, you destroy the caller's intended order and map mentions to arbitrary ID values instead of their visual positions. Removing the sorting step preserves the caller's order and ensures correct mapping. Note that the corresponding test test_resolve_tab_mention should also be updated to reflect the unsorted visual order.

pub fn resolve_tab_mention<'a, I>(tab_number: usize, tab_ids: I) -> Option<u64>
where
    I: IntoIterator<Item = &'a u64>,
{
    let ids: Vec<u64> = tab_ids.into_iter().copied().collect();
    if tab_number == 0 || tab_number > ids.len() {
        return None;
    }
    Some(ids[tab_number - 1])
}

Comment on lines +53 to +64
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedDelegation {
pub task_id: String,
pub from_tab: u64,
pub to_tab: u64,
pub description: String,
pub priority: Priority,
pub created_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub result: Option<String>,
pub was_successful: Option<bool>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PersistedDelegation struct lacks a status field, which causes all active delegations to be restored as Pending (via DelegationStatus::Pending in restore_from_snapshot), losing their InProgress state. Additionally, the fields completed_at, result, and was_successful are always serialized as None because completed delegations are not persisted. Consider adding a status field to preserve the active state of delegations across restarts, and cleaning up unused fields if completed delegations are not intended to be persisted.

Comment on lines +178 to +184
pub fn snapshot(&self) -> super::persistence::PersistedTabState {
use super::persistence::PersistedTab;
let tabs: Vec<PersistedTab> = self
.tabs
.iter()
.map(|t| super::persistence::from_metadata(&t.metadata))
.collect();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cross_tab_links map is completely omitted from the snapshot and restore_from_snapshot process. This means any established collaboration links between tabs will be silently lost when the session is persisted and restored. Consider adding cross_tab_links to the persisted state schema to preserve the collaboration topology across restarts.

Comment on lines +467 to +477
pub fn delegate_task(
&mut self,
from: TabId,
to: TabId,
description: String,
priority: Priority,
) -> Option<String> {
self.delegator
.create_delegation(from, to, description, priority)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The delegate_task method does not validate whether the from and to tab IDs actually exist in self.tabs. Creating delegations with invalid or non-existent tab IDs can lead to orphaned tasks and inconsistent state. Adding defensive checks to ensure both tabs are active before creating the delegation prevents this.

Suggested change
pub fn delegate_task(
&mut self,
from: TabId,
to: TabId,
description: String,
priority: Priority,
) -> Option<String> {
self.delegator
.create_delegation(from, to, description, priority)
}
pub fn delegate_task(
&mut self,
from: TabId,
to: TabId,
description: String,
priority: Priority,
) -> Option<String> {
let has_from = self.tabs.iter().any(|t| t.metadata.id == from);
let has_to = self.tabs.iter().any(|t| t.metadata.id == to);
if !has_from || !has_to {
return None;
}
self.delegator
.create_delegation(from, to, description, priority)
}

Comment thread crates/tui/src/tui/tab/manager.rs Outdated
Comment on lines +509 to +512
/// Start a meeting
pub fn start_meeting(&mut self, topic: String, participants: Vec<TabId>) -> Option<String> {
self.meeting_manager.start_meeting(topic, participants)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The start_meeting method does not validate whether the participant tab IDs actually exist in self.tabs. Starting meetings with invalid or non-existent tab IDs can lead to inconsistent state in MeetingManager. Adding defensive checks to ensure all participants are active tabs before starting the meeting prevents this.

    pub fn start_meeting(&mut self, topic: String, participants: Vec<TabId>) -> Option<String> {
        for p in &participants {
            if !self.tabs.iter().any(|t| t.metadata.id == *p) {
                return None;
            }
        }
        self.meeting_manager.start_meeting(topic, participants)
    }

Comment thread crates/tui/src/tui/tab/group.rs
Comment thread crates/tui/src/tui/tab/manager.rs Outdated
Six fixes for the bot review comments landed on PR Hmbown#2864 head
649d399. See phase2-playbook.md §7 for the triage rationale.

* persistence.rs: oversized state file now surfaces an InvalidData
  error instead of silently returning a default. The old behaviour
  would let the next save overwrite the oversized file and destroy
  the user's data. Test updated to expect the error.

* persistence.rs: PersistedDelegation gains a `status` field so
  in-flight `InProgress` delegations aren't silently demoted to
  `Pending` on restart. The snapshot now writes the live status
  and restore_from_snapshot honours it. Adds a regression test.

* mention.rs: resolve_tab_mention no longer sorts its input — tab
  mentions (@tab2) must map to the visual order in the tab bar,
  not to an arbitrary ID sort. Test updated.

* manager.rs: `pending_tasks` renamed to `completed_delegations`
  because the getter returns completed DelegationResults, not
  in-flight tasks. Docstring points to the in-flight getter
  `pending_delegations` to avoid the same confusion recurring.

* manager.rs: delegate_task and start_meeting now validate that
  the from/to tab IDs (or all participant IDs) currently exist
  in the manager. Returns `None` on any unknown ID, preventing
  orphaned tasks / meetings with stale tab references. Two new
  regression tests cover both methods.

Local CI matrix (Windows runner, flags matching ci.yml):
- cargo fmt --all -- --check: exit 0
- cargo clippy --workspace --all-features --locked -- -D warnings: exit 0
- cargo test --workspace --all-features --locked: 4282 pass, 6 fail
  (the 6 failures are pre-existing on the baseline; not caused
  by this PR)
- git diff --exit-code -- Cargo.lock: exit 0

The three deferred threads (#1 close_tab cleanup, Hmbown#5 cross_tab_links
snapshot, Hmbown#8 TabGroup::new collision risk) are explicitly out of
scope here; they belong to the follow-up collab/UI PR per the
narrow-harvest promise to Hmbown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ljm3790865
Copy link
Copy Markdown
Author

@gemini-code-assist 7 bot threads addressed in 7fcd7d7. Summary per thread:

  • PRRT_kwDOQ9AYz86HjtoH (close_tab leak) — deferred to follow-up collab/UI PR (feat(tui): multi-tab system with cross-tab collaboration #2753). close_tab cleanup is part of the collab-surface design.
  • PRRT_kwDOQ9AYz86HjtoI (oversized file data loss) — fixed. persistence.rs:147 now returns io::Error(InvalidData). Test updated to assert the error.
  • PRRT_kwDOQ9AYz86HjtoK (sort in resolve_tab_mention) — fixed. mention.rs:168 preserves caller order. Test updated.
  • PRRT_kwDOQ9AYz86HjtoL (PersistedDelegation status) — fixed. Added status field; snapshot writes it, restore_from_snapshot honours it. New regression test preserves InProgress across restart.
  • PRRT_kwDOQ9AYz86HjtoM (cross_tab_links not snapshotted) — deferred. cross_tab_links belongs to the collab design; follow-up PR will own the snapshot shape.
  • PRRT_kwDOQ9AYz86HjtoN (delegate_task validation) — fixed. Returns None when from/to tab doesn't exist. New regression test.
  • PRRT_kwDOQ9AYz86HjtoR (start_meeting validation) — fixed. Returns None when any participant doesn't exist. New regression test.

@greptile-apps:

  • PRRT_kwDOQ9AYz86HjuGj (TabGroup::new collision) — deferred. group.rs is in the WIP collab surface; follow-up PR will fix the ID scheme.
  • PRRT_kwDOQ9AYz86HjuGz (pending_tasks misnamed) — fixed in 7fcd7d7, renamed to completed_delegations with docstring.

Local CI: cargo fmt / clippy -D warnings / cargo test all clean (4282 pass, 6 fail — all pre-existing on baseline).

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Thanks for the update, @ljm3790865. The progress on these threads is clear, and the renaming of completed_delegations and the fix for resolve_tab_mention ordering are great improvements for the stability of this slice.

Regarding the remaining items:

  • next_id collision in delegator.rs: I noticed this wasn't explicitly addressed in your summary. Since TaskDelegator::new() resets next_id to 1 on every restore, you will eventually hit ID collisions with pending tasks that were restored from the snapshot. Even if this is a narrow slice, this is a logic bug that could lead to silent data corruption (mutating the wrong task). I recommend adding a next_id field to PersistedTabState or calculating it on restore as max(restored_task_id) + 1 to ensure uniqueness.
  • Deferrals: The decision to defer cross_tab_links and the TabGroup::new ID scheme collision to the follow-up PR is reasonable, given the current scope of this slice. Just ensure these are tracked in your follow-up planning so they aren't forgotten.

Overall, the architecture for the tab system is looking solid. Once the next_id collision is addressed, this should be in great shape for the follow-up UI wiring.

Comment thread crates/tui/src/tui/tab/manager.rs
ljm3790865 pushed a commit to ljm3790865/CodeWhale-multi that referenced this pull request Jun 6, 2026
Six fixes for the bot review comments landed on PR Hmbown#2864 head
649d399. See phase2-playbook.md §7 for the triage rationale.

* persistence.rs: oversized state file now surfaces an InvalidData
  error instead of silently returning a default. The old behaviour
  would let the next save overwrite the oversized file and destroy
  the user's data. Test updated to expect the error.

* persistence.rs: PersistedDelegation gains a `status` field so
  in-flight `InProgress` delegations aren't silently demoted to
  `Pending` on restart. The snapshot now writes the live status
  and restore_from_snapshot honours it. Adds a regression test.

* mention.rs: resolve_tab_mention no longer sorts its input — tab
  mentions (@tab2) must map to the visual order in the tab bar,
  not to an arbitrary ID sort. Test updated.

* manager.rs: `pending_tasks` renamed to `completed_delegations`
  because the getter returns completed DelegationResults, not
  in-flight tasks. Docstring points to the in-flight getter
  `pending_delegations` to avoid the same confusion recurring.

* manager.rs: delegate_task and start_meeting now validate that
  the from/to tab IDs (or all participant IDs) currently exist
  in the manager. Returns `None` on any unknown ID, preventing
  orphaned tasks / meetings with stale tab references. Two new
  regression tests cover both methods.

Local CI matrix (Windows runner, flags matching ci.yml):
- cargo fmt --all -- --check: exit 0
- cargo clippy --workspace --all-features --locked -- -D warnings: exit 0
- cargo test --workspace --all-features --locked: 4282 pass, 6 fail
  (the 6 failures are pre-existing on the baseline; not caused
  by this PR)
- git diff --exit-code -- Cargo.lock: exit 0

The three deferred threads (#1 close_tab cleanup, Hmbown#5 cross_tab_links
snapshot, Hmbown#8 TabGroup::new collision risk) are explicitly out of
scope here; they belong to the follow-up collab/UI PR per the
narrow-harvest promise to Hmbown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Hmbown Hmbown merged commit 0b96e89 into Hmbown:codex/v0.9.0-stewardship Jun 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants