TUI: collaboration mode UX + always submit UserTurn when enabled (#9461)

- Adds experimental collaboration modes UX in TUI: Plan / Pair
Programming / Execute.
- Gated behind `Feature::CollaborationModes`; existing behavior remains
unchanged when disabled.
- Selection UX:
- `Shift+Tab` cycles modes while idle (no task running, no modal/popup).
- `/collab` cycles; `/collab <plan|pair|pp|execute|exec>` sets
explicitly.
- Footer flash after changes + shortcut overlay shows `Shift+Tab` “to
change mode”.
  - `/status` shows “Collaboration mode”.
- Submission semantics:
- When enabled: every submit uses `Op::UserTurn` and always includes
`collaboration_mode: Some(...)` (default Pair Programming).
  - Removes the one-shot “pending collaboration mode” behavior.
- Implementation:
- New `tui/src/collaboration_modes.rs` (selection enum/cycle, `/collab`
parsing, resolve to `CollaborationMode`, footer flash line).
- Fallback: `resolve_mode_or_fallback` synthesizes a `CollaborationMode`
when presets are missing (uses current model + reasoning effort; no
`developer_instructions`) to avoid core falling back to `Custom`.
  - TODO: migrate TUI to use `Op::UserTurn`.
This commit is contained in:
Ahmed Ibrahim
2026-01-19 09:32:04 -08:00
committed by GitHub
parent 3788e2cc0f
commit bf430ad9fe
24 changed files with 1407 additions and 123 deletions

View File

@@ -152,6 +152,7 @@ async fn status_snapshot_includes_reasoning_details() {
None,
captured_at,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -203,6 +204,7 @@ async fn status_snapshot_includes_forked_from() {
None,
captured_at,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -260,6 +262,7 @@ async fn status_snapshot_includes_monthly_limit() {
None,
captured_at,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -305,6 +308,7 @@ async fn status_snapshot_shows_unlimited_credits() {
None,
captured_at,
&model_slug,
None,
);
let rendered = render_lines(&composite.display_lines(120));
assert!(
@@ -349,6 +353,7 @@ async fn status_snapshot_shows_positive_credits() {
None,
captured_at,
&model_slug,
None,
);
let rendered = render_lines(&composite.display_lines(120));
assert!(
@@ -393,6 +398,7 @@ async fn status_snapshot_hides_zero_credits() {
None,
captured_at,
&model_slug,
None,
);
let rendered = render_lines(&composite.display_lines(120));
assert!(
@@ -435,6 +441,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
None,
captured_at,
&model_slug,
None,
);
let rendered = render_lines(&composite.display_lines(120));
assert!(
@@ -477,6 +484,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
None,
now,
&model_slug,
None,
);
let rendered = render_lines(&composite.display_lines(120));
@@ -534,6 +542,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
None,
captured_at,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(70));
if cfg!(windows) {
@@ -580,6 +589,7 @@ async fn status_snapshot_shows_missing_limits_message() {
None,
now,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -644,6 +654,7 @@ async fn status_snapshot_includes_credits_and_limits() {
None,
captured_at,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -696,6 +707,7 @@ async fn status_snapshot_shows_empty_limits_message() {
None,
captured_at,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -757,6 +769,7 @@ async fn status_snapshot_shows_stale_limits_message() {
None,
now,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -822,6 +835,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
None,
now,
&model_slug,
None,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
@@ -877,6 +891,7 @@ async fn status_context_window_uses_last_usage() {
None,
now,
&model_slug,
None,
);
let rendered_lines = render_lines(&composite.display_lines(80));
let context_line = rendered_lines