feat(tui): multi-tab system with cross-tab collaboration#2753
feat(tui): multi-tab system with cross-tab collaboration#2753ljm3790865 wants to merge 8 commits into
Conversation
|
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 Please read |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive multi-tab and cross-tab collaboration system for the CodeWhale TUI, enabling up to nine concurrent agent sessions across Chat, Delegation, Review, and Meeting modes. Key additions include a priority-based task delegator, a meeting manager, smart @-mention parsing, and session persistence. The code review highlights several critical bugs and improvement opportunities, most notably a logic error in take_pending_for_tab that prematurely removes tasks and breaks subsequent status updates, and a bug in restore_from_snapshot that discards saved delegations on restart. Additionally, multiple potential panic vectors from subtraction overflows in terminal layout calculations and direct array indexing were identified, alongside suggestions to simplify redundant type conversions and make delegation descriptions dynamic.
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.
|
Thanks for the review — pushed Critical / P1 (delegator correctness)
P1 (state / persistence)
P1 (tab switcher filter)
High (panic vectors)
Medium (cleanup)
Verification
|
|
Thanks @ljm3790865. This is substantial work, but it is too large for the current v0.9 stabilization harvest. I’m keeping it open, and any future harvest should probably split out a narrow tab-core/persistence slice after the UTF-8 truncation and stub collaboration paths are resolved. |
|
Thanks @ljm3790865. I rechecked this against the current v0.9.0 stewardship branch. There is a lot of useful foundation here, especially the tab manager, persistence, delegation queue, and regression tests. I do not think we should merge the full PR into the v0.9.0 release train as-is: the visible collaboration paths are still partly WIP, the picker/switcher title truncation still needs a UTF-8-safe The best future path looks like a narrow tab-core/persistence harvest first, with your authorship credited, and then a separate UI pass once the switcher/picker/meeting flows are fully wired and CI is green. Keeping this open as the source branch for that work. Thanks again for the substantial contribution. |
Addresses Hmbown's v0.9 stewardship review of PR Hmbown#2753: * UTF-8 safe `chars().count()` + `chars().take(N).collect()` truncation in `views::tab_picker` and `views::tab_switcher` (byte slicing on a multi-byte char at the cut point would have panicked). * Mechanical clippy cleanups: `sort_by` -> `sort_by_key(Reverse)`, `iter_overeager_cloned` -> filter-then-cloned, `or_insert_with(...)` -> `or_default()`, `collapsible_if` (5 sites), `derivable_impls` (3 sites), `wrong_self_convention` on `CrossTabEvent::from_tab`. * Drop a pre-existing redundant pointer cast in `tools::shell.rs` (`child.as_raw_handle()` already returns `*mut c_void`). * `#![allow(dead_code)]` on the WIP collaboration surface (`tab/delegator`, `tab/meeting`, `tab/cross_tab`, `tab/group`, `tab/mention`, `tab/persistence`, `tab/manager`, `tab/mod`, `views/meeting_view`) and targeted `#[allow(dead_code)]` on `views::ModalKind::Meeting` and `App::new_for_test` for the same reason: the collab/UI pass is the WIP piece Hmbown asked to be stubbed, so these items are intentionally not yet consumed inside the binary crate. The narrow tab-core harvest lands in a separate PR; these allows keep the v0.9 harvest lint-clean. Local CI matrix (`cargo fmt --check`, `cargo clippy --workspace --all-features --locked -- -D warnings`, `cargo test --bin codewhale-tui`, `git diff --exit-code -- Cargo.lock`) is green. The six pre-existing test failures in `commands::skills`, `settings`, and `tools::shell` are unrelated to this commit and reproduce on the baseline `2269d656` without these changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Local artifact used to track PR review comment URLs while resolving the 24 Gemini/Greptile threads on Hmbown#2753. Now superseded by the resolved state of the PR conversation and should not be in the working tree history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| /// title matches the filter, scanning from index 0. Without the full | ||
| /// re-scan a filter that matches a tab *before* the current cursor | ||
| /// would leave the cursor on a non-matching tab, and pressing Enter | ||
| /// would then activate the wrong tab. | ||
| fn apply_filter(&mut self) { | ||
| if self.filter.is_empty() { | ||
| return; | ||
| } | ||
| let filter_lower = self.filter.to_lowercase(); | ||
| if let Some(pos) = self |
There was a problem hiding this comment.
Filter "no-match" still fires Enter on stale cursor
When the filter string matches no tabs, apply_filter leaves self.cursor unchanged (the if let Some(pos) branch is skipped). The render correctly shows the "No tabs" placeholder via filtered_tabs(), but KeyCode::Enter => TabSwitcherAction::Select(self.cursor) still emits the stale cursor index. The host handler in ui.rs then silently switches to whatever tab the cursor was on before the filter was typed — despite the user seeing "No tabs". Resetting self.cursor to a sentinel (or returning early in the Enter handler when filtered_tabs() is empty) would prevent the accidental switch.
|
Thanks @Hmbown — the v0.9 stewardship review is heard, and I've pushed two new commits on top of the bot-feedback fix to make this harvest CodeWhale-CI clean. What landed (
Local CI matrix (Windows runner, same flags as
The 6 failing tests are pre-existing on the baseline GitHub Actions on the head SHA is in Narrow harvest plan The narrow tab-core/persistence slice you suggested is queued as the next deliverable:
Happy to flip the order — open the narrow PR first and circle back to the UI pass — if that's a smoother review path for you. 🤖 Generated with Claude Code |
State: Waiting for upstream Hmbown#2570 to be resolved before this branch can be cleanly rebased and merged. Contents (20 files, +5,600 LOC): - crates/tui/src/tui/tab/ : 12 files (tab system core) - crates/tui/src/tui/views/tab_* : 3 files (switcher, picker, meeting) - docs/TROUBLESHOOTING.md : troubleshooting guide - PR_BODY_BILINGUAL.md : bilingual PR description - HMBOWN_ISSUE.md, ISSUE_DRAFT.md : issue templates Related: - Upstream bug: Hmbown#2570 - PR (blocked): #1 - PR comment: #1 (comment) When upstream is fixed: 1. git fetch upstream main 2. git rebase upstream/main (will apply cleanly now) 3. git push origin wip/multi-agent-tab-system --force-with-lease Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add the missing host-side wiring for the multi-tab system that
landed in the previous WIP commit (tab data structures exist but
were unreachable from the keyboard, mouse, and event loop).
Keyboard shortcuts (in ui.rs):
- Ctrl+` open tab switcher (when tabs exist)
- Ctrl+Shift+N create a new tab
- Ctrl+Shift+W close the active tab
- Ctrl+1..9 jump to tab N
- Ctrl+Tab cycle to next tab (only when no input menu is open)
- Ctrl+BackTab cycle to previous tab
Mouse context menu (in mouse_ui.rs):
- 4 cross-tab collaboration entries: Delegate / Review / Invite to
Meeting / Share Context (visible when 2+ tabs are open)
ViewEvent additions (in views/mod.rs):
- TabSwitch{index} emitted by TabSwitcherView
- CollabRequested{kind,to_tab} emitted by TabPickerView when the
user picks a target tab for a collaboration action
Cross-tab collaboration dispatch is centralised in one host arm
(CollabRequested) so the picker → action flow cannot loop back into
the picker the way the previous 4 distinct ContextMenuAction
variants would have.
Render:
- New 1-row tab bar sits between the header and the chat body,
drawn by tab::render_tab_bar (no-ops when no tabs are open)
- render_tab_bar hot path tightened: TabManager::iter() avoids
allocating a Vec per frame; tab_group() looked up once per tab
instead of twice
Helpers added to TabManager:
- iter() zero-alloc (index, &TabState) iterator
- create_default_chat_tab() encapsulates the "Tab N" naming and
Chat type so Ctrl+Shift+N and ViewEvent handlers don't duplicate
the boilerplate
All 70 existing tab unit tests + 58 views tests pass; 3953 tests
across the workspace pass (the 6 pre-existing failures are in
shell / settings / skills and unrelated to this change).
- delegator: keep terminal-state tasks out of `pending_tasks`
- `take_pending_for_tab` now marks the task in-progress in place and
returns a clone; it no longer `swap_remove`s, so subsequent
`start_task` / `complete` / `fail_task` / `cancel_task` calls can
still find the task
- `complete`, `fail_task`, `cancel_task` now `swap_remove` the task
from `pending_tasks` once it reaches a terminal state, so the
queue is bounded over a long session
- make `pending_tasks` `pub(crate)` so the persistence layer can
restore it from snapshot
- drop the duplicate `/// Complete a task` doc comment
- manager: counter-based TabId and snapshot restore
- `next_tab_id` monotonic counter replaces the wall-clock nanosecond
ID, eliminating the same-nanosecond collision that could make a
second tab unreachable by `TabId`
- `restore_from_snapshot` rebuilds `delegator.pending_tasks` from
`state.delegations` so cross-tab work survives a restart
- `next_tab_id` is bumped past any restored tab ID so newly created
tabs can never collide with restored ones
- `take_next_delegation` no longer double-calls `start_task`
(the lower-level `take_pending_for_tab` now handles the transition)
- add `meeting(id)` accessor
- cross_tab: safe lookup for ContextSync::from_tab
- use `tab_ids.first().copied().unwrap_or(TabId(0))` instead of
`tab_ids[0]` to avoid a panic on an empty participant list
- views: hard-coded arithmetic -> saturating_sub
- `tab_switcher` and `tab_picker` use `saturating_sub` for every
`area.width - N` / `area.height - N` so resize-to-zero never
panics
- `tab_switcher::apply_filter` now re-scans from index 0; previously
a filter that matched a tab before the cursor left the cursor on
a non-matching tab, and Enter selected the wrong tab
- `tab_bar` drops the redundant `(area.width as usize).try_into()
.unwrap_or(u16::MAX)` — `area.width` is already `u16`
- ui: meaningful delegation description
- the right-click "Delegate Task to…" action now seeds the task
description with the from/to tab IDs instead of a hard-coded
"Task from tab"
- meeting_view: actually look up the meeting by ID
- `render_meeting_summary` now queries `app.tab_manager.meeting(id)`
and renders the topic + participant/message/decision counts
instead of an empty "Meeting Summary" border that always queried
`TabId(0)`
- tests: add 2 regression tests
- `test_snapshot_restore_preserves_delegations`
- `test_create_tab_id_does_not_collide_after_restore`
cargo test -p codewhale-tui tab:: → 72/72 pass (was 70/70)
cargo test -p codewhale-tui tui::views:: → 59/59 pass
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses Hmbown's v0.9 stewardship review of PR Hmbown#2753: * UTF-8 safe `chars().count()` + `chars().take(N).collect()` truncation in `views::tab_picker` and `views::tab_switcher` (byte slicing on a multi-byte char at the cut point would have panicked). * Mechanical clippy cleanups: `sort_by` -> `sort_by_key(Reverse)`, `iter_overeager_cloned` -> filter-then-cloned, `or_insert_with(...)` -> `or_default()`, `collapsible_if` (5 sites), `derivable_impls` (3 sites), `wrong_self_convention` on `CrossTabEvent::from_tab`. * Drop a pre-existing redundant pointer cast in `tools::shell.rs` (`child.as_raw_handle()` already returns `*mut c_void`). * `#![allow(dead_code)]` on the WIP collaboration surface (`tab/delegator`, `tab/meeting`, `tab/cross_tab`, `tab/group`, `tab/mention`, `tab/persistence`, `tab/manager`, `tab/mod`, `views/meeting_view`) and targeted `#[allow(dead_code)]` on `views::ModalKind::Meeting` and `App::new_for_test` for the same reason: the collab/UI pass is the WIP piece Hmbown asked to be stubbed, so these items are intentionally not yet consumed inside the binary crate. The narrow tab-core harvest lands in a separate PR; these allows keep the v0.9 harvest lint-clean. Local CI matrix (`cargo fmt --check`, `cargo clippy --workspace --all-features --locked -- -D warnings`, `cargo test --bin codewhale-tui`, `git diff --exit-code -- Cargo.lock`) is green. The six pre-existing test failures in `commands::skills`, `settings`, and `tools::shell` are unrelated to this commit and reproduce on the baseline `2269d656` without these changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Local artifact used to track PR review comment URLs while resolving the 24 Gemini/Greptile threads on Hmbown#2753. Now superseded by the resolved state of the PR conversation and should not be in the working tree history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
e8e5283 to
7038ab3
Compare
|
@greptile-apps The P1 on tab_switcher.rs (PRRT_kwDOQ9AYz86Hjpm) is already resolved by 2269d65 (rebased to f523ade) — Also: I rebased v0850 onto the new stewardship head (5bd2f6a, feat: expose git status metadata for agent view) and cherry-picked the Phase 2 bot-review fix series (7fcd7d7) on top, so PR #2753 is now at 7038ab3 with all 6 Phase 2 fixes applied. PR #2864 (narrow) is at 7fcd7d7 with the same fixes. Local CI: fmt / clippy -D warnings / 4295 pass / 6 pre-existing fail (shell / settings / skills), lockfile clean. Stewardship moved +3 commits since I last looked (#2862 git status metadata, plus 2 docs/release notes). The new commit only touches runtime_api.rs, CHANGELOG, and 2 docs — none of which v0850's multi-tab diff overlaps, so the rebase was 0-conflict. Backup at |
|
Confirmed — the current Two residual items from the same area worth noting (not re-opening the P1, just flagging for completeness):
If these were intentionally deferred to a follow-up PR, that's fine — but wanted to confirm they're still open before you close the thread entirely. Happy to mark the P1 thread resolved if you confirm those two are tracked elsewhere. Tip: You can customize Greptile's behavior for this repo with |
Capture the working state of the multi-tab system harvest to upstream Hmbown/CodeWhale: * STATUS.md — current PR/branch state, commit SHAs, CI status, local CI matrix results, pre-tagged triage for the bot review threads * phase2-playbook.md — review triage decision tree, fix/defer rules for bot feedback, GraphQL batch-resolve tooling Both files are working-state docs, not part of the harvest diff itself; they exist so a future contributor (or future-me) can pick up the work without re-deriving the strategy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PR was opened against `main`, which is older than the v0.9 stewardship branch. The resulting 201-file diff included 187 commits of stewardship cleanup (ohos-clang / vscode extensions / npm / etc.) that have nothing to do with the multi-tab work. Switching the base to `codex/v0.9.0-stewardship` trims the diff to 25 files / 5,961 insertions, all of which are the multi-tab harvest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@Hmbown Quick housekeeping: switched this PR's base from The diff still includes the full WIP surface (delegator / meeting / cross_tab / group / mention / persistence / tab_bar / render_tests + the 3 view files) because that was the original scope. The narrow slice is being landed via #2864 first per your earlier guidance; this PR stays open as the source branch for the follow-up UI pass when you're ready for it. Also: rebased onto the new stewardship head (5bd2f6a, git status metadata) with 0 conflicts, and the 6 Phase 2 bot-review fixes (7fcd7d7 → 7038ab3) are applied on top. One remaining Greptile P1 thread on tab_switcher.rs:122 is stale (already addressed in 2269d65); flagged in the earlier reply on 4638863128. Local CI: fmt / clippy -D warnings / 4295 pass / 6 pre-existing fail / lockfile clean. |
| self.groups = super::group::TabGroupManager::new(); | ||
| self.next_tab_id = 1; | ||
|
|
||
| let mut max_seen_id: u64 = 0; | ||
| for p in &state.tabs { | ||
| let meta = super::persistence::to_metadata(p); | ||
| // Advance the monotonic counter past any restored ID so freshly | ||
| // created tabs can never collide with restored ones. | ||
| if meta.id.0 > max_seen_id { | ||
| max_seen_id = meta.id.0; | ||
| } | ||
| let tab = TabState { | ||
| metadata: meta, | ||
| status: TabStatus::Idle, | ||
| pending_tasks: Vec::new(), | ||
| }; | ||
| self.tabs.push(tab); | ||
| } | ||
| self.next_tab_id = max_seen_id + 1; | ||
|
|
||
| // Restore active delegations so cross-tab work survives restarts. | ||
| // We honour the persisted status (`InProgress` is preserved) so | ||
| // work-in-progress isn't silently demoted to `Pending` on restart. | ||
| for d in &state.delegations { | ||
| let task = super::delegator::DelegationTask { | ||
| task_id: d.task_id.clone(), | ||
| from_tab: TabId(d.from_tab), | ||
| to_tab: TabId(d.to_tab), | ||
| description: d.description.clone(), | ||
| priority: d.priority, | ||
| status: d.status, | ||
| created_at: d.created_at, | ||
| deadline: None, | ||
| completed_at: d.completed_at, | ||
| result: d.result.clone(), | ||
| }; | ||
| self.delegator.pending_tasks.push(task); | ||
| } | ||
|
|
||
| // Restore groups AFTER tabs so tab_ids can reference real tabs | ||
| for g in &state.groups { | ||
| let group = super::group::TabGroup { | ||
| id: g.id.clone(), | ||
| name: g.name.clone(), | ||
| color: g.color, | ||
| tab_ids: g.tab_ids.clone(), | ||
| created_at: g.created_at, | ||
| }; | ||
| self.groups.groups.insert(group.id.clone(), group); | ||
| for tab_id in &g.tab_ids { | ||
| self.groups.tab_to_group.insert(*tab_id, g.id.clone()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Group counter not advanced after restore — new groups overwrite persisted ones
restore_from_snapshot calls TabGroupManager::new() (which resets next_id to 1) and then inserts restored groups directly into self.groups.groups, bypassing generate_group_id. After a reload, the first call to create_group() generates "group_1", which silently overwrites the first restored group in the HashMap via HashMap::insert. The pattern is identical to the TabId collision that was fixed in this PR — the TabManager advances self.next_tab_id = max_seen_id + 1 after restoring tabs, but the same logic is missing for the group manager.
After loading a session with, say, three saved groups (group_1..3), creating a new group erases group_1's membership list and color; every tab assigned to that group loses its assignment silently.
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. 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 #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>
Summary
TabManagerwith per-tab chat history, session context, mentions, and groups; tabs are persisted across restarts and can be cycled withCtrl+`` (switcher),Ctrl+Tab/Ctrl+Shift+Tab, andCtrl+1..Ctrl+9`TaskDelegatorfor A→B task delegation with priority and deadlines, aMeetingManagerfor multi-agent meetings with decisions and summaries, and a typedCrossTabEventbus (delegation / review / meeting / context-sync / result-return)Delegate / Review / Meeting / Shareactions into the existing modal/event pipeline (singleCollabRequestedevent drives dispatch, no picker loop)ViewportState::validate_areasclamps zero-sized rects,handle_resizerejects sub-minimum dimensions, tab-bar split is guarded on body heightVerification
cargo test -p codewhale-tui tab::— 70/70 passcargo test -p codewhale-tui tui::views::— 58/58 passcargo check -p codewhale-tuigit diff --checkTest plan
Ctrl+Shift+N; switch withCtrl+1..9andCtrl+TabDelegate Task to…, pick another tab, and verify the delegation lands in the target tab and a result message returns to the sourceInvite to Meeting, send a few messages, end it, and confirm the summary is recordedNotes
f2ca56d3) and follow-up integration commit (2f515410) are squashed-rebased onto the currentmain(v0.8.53)PR_BODY_BILINGUAL.mdis a contributor-facing bilingual summary kept for reference; the canonical PR body is this oneGreptile Summary
This PR introduces a multi-tab system with cross-tab collaboration primitives (
TaskDelegator,MeetingManager, typedCrossTabEventbus), wires them into the existing modal/event pipeline, and hardens the TUI against terminal-resize crashes. Several issues flagged in the previous review round (TabId collision, unbounded pending_tasks queue, wrong tab selected on filter, hardcoded TabId(0) in meeting summary) are addressed in this revision.TabManagernow assigns IDs via a monotonicnext_tab_idcounter and advances it past restored IDs on reload;TaskDelegatoruses the same pattern and bounds completed results via aVecDeque<256>.TabGroupManageralso has anext_idcounter for new groups, butrestore_from_snapshotresets the manager with::new()(counter = 1) and inserts groups directly, so the firstcreate_group()after a reload generates"group_1"and silently overwrites the first restored group in theHashMap.TabSwitcherView.apply_filtercorrectly rescans from index 0, butBackspacedoes not callapply_filter, leaving the cursor stale after deletion.Confidence Score: 3/5
Safe to merge for the UI and delegation primitives; the group-counter gap means creating a new tab group after a restart overwrites a previously saved group's membership and color.
The rest of the tab core (TabId uniqueness, delegation queue bounding, meeting lifecycle, atomic persistence) is solid. The group ID counter is not advanced after snapshot restore, so calling create_group() post-reload silently stomps persisted group data in the HashMap — the same class of bug fixed for TabIds in this same PR, just missed for groups.
crates/tui/src/tui/tab/manager.rs — restore_from_snapshot needs to advance TabGroupManager.next_id past the max restored group number, mirroring the next_tab_id advance already present for tabs.
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant TUI as TUI (ui.rs) participant Picker as TabPickerView participant Manager as TabManager participant Delegator as TaskDelegator participant Meeting as MeetingManager User->>TUI: Right-click → Delegate/Review/Meeting/Share TUI->>Picker: open TabPickerView(action) User->>Picker: Select target tab + Enter Picker-->>TUI: "ViewEvent::CollabRequested { kind, to_tab }" alt "kind == Delegate" TUI->>Manager: delegate_task(from, to, desc, priority) Manager->>Delegator: create_delegation(...) Delegator-->>Manager: task_id Manager-->>TUI: Some(task_id) else "kind == Meeting" TUI->>Manager: start_meeting(topic, [from, to]) Manager->>Meeting: "start_meeting(topic, participants >= 2)" Meeting-->>Manager: meeting_id Manager-->>TUI: Some(meeting_id) end Note over TUI: Ctrl+backtick opens TabSwitcherView User->>TUI: "Ctrl+backtick -> type filter -> Enter" TUI-->>TUI: "ViewEvent::TabSwitch { index }" TUI->>Manager: switch_to(index)Reviews (4): Last reviewed commit: "docs(STATUS): note PR #2753 base switche..." | Re-trigger Greptile