Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
risk score (see `zeph_tools::risk_chain` module docs for the full rationale and the accepted,
bounded residual-evasion risk). Added `--init` wizard support and `--migrate-config` support
(step 106).
- `zeph-tui`: content-driven side-panel sizing (issue #6675). The four side-panel slots
(Skills, Memory, Resources, SubAgents) are now sized from their own content each frame via
a new integer max-min fair water-filling allocator (`layout::fit_panel_heights`) instead of
a flat equal-share `Fill(1)` split — a sparse panel no longer wastes space and a busy one
is no longer silently clipped. `AppLayout::compute` takes a new `PanelSizing` (per-slot
`PanelDemand::{Collapsed, Rows(u16), Greedy}`) in place of the old `[bool; 4]` collapse
mask; `App::panel_demands()` builds it each frame from each widget's new pure
`desired_height` plus the chrome (focused-panel header, resources' gauge/compaction badge,
the subagents equalizer). All four measured panels route their rendering through a new
shared `widgets::panel::render_lines`, which truncates to the granted height and replaces
the last visible row with a muted `+N more` indicator on overflow. New `[tui] panel_sizing`
config key (`"auto"` default, `"even"` approximates the pre-#6675 equal-share split — same
total per slot, remainder placement may differ from the old cassowary-based split),
runtime-togglable via `/panel_sizing [auto|even]` or the `app:panel-sizing` command palette
entry; `--init` prompts for it and `--migrate-config` adds an advisory comment for existing
configs (step 107).

- `zeph-tui`: inline, non-modal `@` mention picker (issues #6647, #6648), replacing the
old modal file picker. Typing `@` at word-start inserts the character and opens a popup
Expand Down
27 changes: 27 additions & 0 deletions book/src/advanced/tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Note: Enabling this increases network traffic between parent and sub-agents. Onl
| `Ctrl+F` | Open transcript search (case-insensitive substring search across message content and tool names) |
| `Tab` | Cycle side panel focus (includes SubAgents panel) |
| `a` | Focus the SubAgents panel |
| `Alt+1` / `Alt+2` / `Alt+3` / `Alt+4` | Toggle collapse for Skills / Memory / Resources / SubAgents (works in Normal and Insert mode) |

### Insert Mode

Expand Down Expand Up @@ -546,6 +547,32 @@ The TUI adapts to terminal width:
| >= 80 cols | Full layout: chat (70%) + side panels (30%) |
| < 80 cols | Side panels hidden, chat takes full width |

### Side Panel Sizing

Within the side column, the four panels (Skills, Memory, Resources, SubAgents) are sized
from their own content rather than split into equal shares — a sparse panel doesn't waste
space, and a busy one isn't silently clipped. Each panel's exact row count is computed fresh
every frame from what it's about to render, and every panel signals truncation the same way:
if its content doesn't fit the rows it was granted, the last visible row becomes a muted
`+N more` indicator instead of clipping silently.

A panel collapsed with `Alt+1`..`Alt+4` (see [Keybindings](#keybindings)) still gets exactly
one summary row regardless of content — collapsing is unaffected by this change. Leftover
space beyond what all panels need is left blank at the bottom of the column; it can't be
donated to the chat pane, which is a horizontal sibling that already spans the full height.

Set `[tui] panel_sizing` in `config.toml` to control the strategy:

```toml
[tui]
panel_sizing = "auto" # default: size each panel from its content
# panel_sizing = "even" # pre-content-sizing behavior: split the column equally
```

Toggle at runtime with `/panel_sizing [auto|even]` (bare `/panel_sizing` toggles between the
two) or the `app:panel-sizing` command palette entry. The runtime toggle is not persisted
back to `config.toml`.

## Live Metrics

The TUI dashboard displays real-time metrics collected from the agent loop via `tokio::sync::watch` channel. The render loop polls the watch receiver before every frame. Frames are only emitted when the dirty flag is set (an event was received since the last draw), so the display does not redraw during idle 250 ms ticks with no activity.
Expand Down
3 changes: 2 additions & 1 deletion crates/zeph-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ pub use ui::{
ACP_AUTH_CLIENT_ID_DEFAULT, ACP_AUTH_CLIENT_ID_LOCAL, AcpAuthClient, AcpAuthMethod, AcpConfig,
AcpLspConfig, AcpModelConfigConfig, AcpSubagentsConfig, AcpTemperaturePreset,
AcpTimeoutsConfig, AcpTransport, AdditionalDir, AdditionalDirError, ColorMode, DelightsConfig,
FleetConfig, Motion, SubagentPresetConfig, ThemeConfig, ToolDensity, TuiConfig,
FleetConfig, Motion, PanelSizingMode, SubagentPresetConfig, ThemeConfig, ToolDensity,
TuiConfig,
};
pub use ui::{DiagnosticSeverity, DiagnosticsConfig, HoverConfig, LspConfig};
pub use vigil::VigilConfig;
Expand Down
110 changes: 109 additions & 1 deletion crates/zeph-config/src/migrate/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ pub fn migrate_tui_delights(toml_src: &str) -> Result<MigrationResult, MigrateEr
///
/// # Errors
///
/// Returns `MigrateError::TomlParse` if the input is not valid TOML; infallible otherwise.
/// Never returns `Err`; the `Result` is required by the `Migration` trait signature this
/// function backs (`MigrateTuiMouse`, registered in `MIGRATIONS`). This is pure string/regex
/// text manipulation with no TOML parsing step, so there is no failure mode to report.
pub fn migrate_tui_mouse(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !section_header_present(toml_src, "tui") {
return Ok(MigrationResult {
Expand Down Expand Up @@ -156,6 +158,74 @@ pub fn migrate_tui_mouse(toml_src: &str) -> Result<MigrationResult, MigrateError
})
}

/// Step 106 — add `panel_sizing = "auto"` advisory comment under `[tui]` (#6675).
///
/// `auto` is already the field's `#[default]`, so an absent key behaves identically —
/// this is a documentation-only advisory, mirroring [`migrate_tui_mouse`].
///
/// # Errors
///
/// Never returns `Err`; the `Result` is required by the `Migration` trait signature this
/// function backs (`MigrateTuiPanelSizing`, registered in `MIGRATIONS`). This is pure
/// string/regex text manipulation with no TOML parsing step, so there is no failure mode to
/// report.
pub fn migrate_tui_panel_sizing(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !section_header_present(toml_src, "tui") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}

let already_present = toml_src.lines().any(|l| {
let t = l.trim().trim_start_matches('#').trim();
t.starts_with("panel_sizing")
});
if already_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}

let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};

if !TUI_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}

let insert = "# panel_sizing = \"auto\" # \"auto\" sizes panels from content, \"even\" splits equally (#6675)\n";
let output = TUI_HEADER_RE
.replacen(src, 1, |caps: &regex::Captures| {
format!("{}{insert}", &caps[0])
})
.into_owned();

let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui".to_owned()]
} else {
Vec::new()
},
})
}

/// Strip any existing `[memory.compression.predictor]` section from the config (#3251).
///
/// The compression predictor feature was removed. This migration cleans up both active
Expand Down Expand Up @@ -1202,6 +1272,44 @@ mod rate_limit_advisory_tests {
}
}

#[cfg(test)]
mod tui_panel_sizing_tests {
use super::*;

#[test]
fn migrate_tui_panel_sizing_adds_advisory_comment() {
let base = "[tui]\nmouse = false\n";
let result = migrate_tui_panel_sizing(base).unwrap();
assert_eq!(result.changed_count, 1);
assert!(result.output.contains("# panel_sizing = \"auto\""));
}

#[test]
fn migrate_tui_panel_sizing_idempotent() {
let base = "[tui]\nmouse = false\n";
let first = migrate_tui_panel_sizing(base).unwrap();
let second = migrate_tui_panel_sizing(&first.output).unwrap();
assert_eq!(second.changed_count, 0);
assert_eq!(second.output, first.output);
}

#[test]
fn migrate_tui_panel_sizing_noop_when_key_already_present() {
let base = "[tui]\npanel_sizing = \"even\"\n";
let result = migrate_tui_panel_sizing(base).unwrap();
assert_eq!(result.changed_count, 0);
assert_eq!(result.output, base);
}

#[test]
fn migrate_tui_panel_sizing_noop_when_no_tui_section() {
let base = "[agent]\nname = \"zeph\"\n";
let result = migrate_tui_panel_sizing(base).unwrap();
assert_eq!(result.changed_count, 0);
assert_eq!(result.output, base);
}
}

/// Step 104 — add an `expandable_blockquote_min_lines` advisory comment to an existing
/// active `[telegram]` table (spec 007-3-telegram-rich-text, issue #6541).
///
Expand Down
10 changes: 6 additions & 4 deletions crates/zeph-config/src/migrate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub use features::{
migrate_orchestration_persistence, migrate_orchestration_whole_plan_verifier_timeout,
migrate_rate_limit_advisory, migrate_skill_trust_require_check, migrate_skills_registry,
migrate_telegram_expandable_blockquote_config, migrate_tui_delights, migrate_tui_mouse,
migrate_tui_theme_config, migrate_tui_theme_defaults,
migrate_tui_panel_sizing, migrate_tui_theme_config, migrate_tui_theme_defaults,
};
pub use infra::*;
pub use integrity::migrate_integrity_config;
Expand Down Expand Up @@ -640,9 +640,9 @@ use steps::{
MigrateSkillsRegistry, MigrateSttToProvider, MigrateSupervisorConfig,
MigrateTelegramExpandableBlockquoteConfig, MigrateTelemetryConfig,
MigrateToolsCompressionConfig, MigrateTraceMetadata, MigrateTuiDelights, MigrateTuiMouse,
MigrateTuiThemeConfig, MigrateTuiThemeDefaults, MigrateUtilityHighGainTools,
MigrateVigilConfig, MigrateWorktreeConfig, MigrateWorktreeGitTimeout,
MigrateWorktreeQuotaFields,
MigrateTuiPanelSizing, MigrateTuiThemeConfig, MigrateTuiThemeDefaults,
MigrateUtilityHighGainTools, MigrateVigilConfig, MigrateWorktreeConfig,
MigrateWorktreeGitTimeout, MigrateWorktreeQuotaFields,
};

/// Ordered registry of all sequential migration steps (steps 1–99).
Expand Down Expand Up @@ -867,6 +867,8 @@ pub static MIGRATIONS: std::sync::LazyLock<Vec<Box<dyn Migration + Send + Sync>>
// Step 106 — add risk_chain_window_turns advisory comment to [tools.shell]
// for RiskChainAccumulator's cross-turn multi-step chain detection (#6603)
Box::new(MigrateShellRiskChainWindowTurns),
// Step 107 — add panel_sizing = "auto" advisory comment under [tui] (#6675)
Box::new(MigrateTuiPanelSizing),
]
});

Expand Down
24 changes: 20 additions & 4 deletions crates/zeph-config/src/migrate/steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@
//! step 100 adds a commented `[integrity]` advisory block for vault-anchor
//! downgrade-resistance (issue #6449); step 101 adds a commented `[security.rate_limit]`
//! advisory block — purely documentary, the tool rate limiter already defaults to enabled
//! at the struct-default level (issue #6469).
//! at the struct-default level (issue #6469); step 106 adds a commented
//! `panel_sizing = "auto"` advisory under `[tui]` — purely documentary, `auto` is already
//! the field's struct default (issue #6675).
//!
//! Each struct is a zero-size type that delegates to the corresponding free function in
//! `super`. They exist solely to satisfy the object-safe [`super::Migration`] trait so the
Expand Down Expand Up @@ -127,9 +129,9 @@ use super::{
migrate_stt_to_provider, migrate_supervisor_config,
migrate_telegram_expandable_blockquote_config, migrate_telemetry_config,
migrate_tools_compression_config, migrate_trace_metadata, migrate_tui_delights,
migrate_tui_mouse, migrate_tui_theme_config, migrate_tui_theme_defaults,
migrate_utility_high_gain_tools, migrate_vigil_config, migrate_worktree_config,
migrate_worktree_git_timeout, migrate_worktree_quota_fields,
migrate_tui_mouse, migrate_tui_panel_sizing, migrate_tui_theme_config,
migrate_tui_theme_defaults, migrate_utility_high_gain_tools, migrate_vigil_config,
migrate_worktree_config, migrate_worktree_git_timeout, migrate_worktree_quota_fields,
};

// ── Wrapper structs for all 73 sequential migration steps ───────────────────────────────────────
Expand Down Expand Up @@ -1357,6 +1359,8 @@ impl Migration for MigrateAgentsMaxSpawnsPerSession {
}
}

/// Step 106 — add `risk_chain_window_turns` advisory comment to `[tools.shell]` for
/// `RiskChainAccumulator`'s cross-turn multi-step chain detection (issue #6603).
pub(super) struct MigrateShellRiskChainWindowTurns;
impl Migration for MigrateShellRiskChainWindowTurns {
fn name(&self) -> &'static str {
Expand All @@ -1367,3 +1371,15 @@ impl Migration for MigrateShellRiskChainWindowTurns {
migrate_shell_risk_chain_window_turns(toml_src)
}
}

/// Step 107 — add `panel_sizing = "auto"` advisory comment under `[tui]` (issue #6675).
pub(super) struct MigrateTuiPanelSizing;
impl Migration for MigrateTuiPanelSizing {
fn name(&self) -> &'static str {
"migrate_tui_panel_sizing"
}

fn apply(&self, toml_src: &str) -> Result<MigrationResult, MigrateError> {
migrate_tui_panel_sizing(toml_src)
}
}
26 changes: 23 additions & 3 deletions crates/zeph-config/src/migrate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use super::*;
fn migrations_registry_has_all_steps() {
assert_eq!(
MIGRATIONS.len(),
106,
"MIGRATIONS registry must contain all 106 sequential steps"
107,
"MIGRATIONS registry must contain all 107 sequential steps"
);
for m in MIGRATIONS.iter() {
assert!(
Expand Down Expand Up @@ -2124,7 +2124,26 @@ fn migrate_focus_auto_consolidate_noop_when_only_commented_section() {

#[test]
fn registry_has_fifty_entries() {
assert_eq!(MIGRATIONS.len(), 106);
assert_eq!(MIGRATIONS.len(), 107);
}

/// Mirrors the SC-003 wire-X-into-registry check below for `MigrateTuiPanelSizing` (#6675):
/// a function that exists but is never pushed into `MIGRATIONS` would pass its own isolated
/// unit tests in `migrate/features.rs` while doing nothing for a real `--migrate-config` run.
#[test]
fn full_registry_adds_panel_sizing_advisory_to_legacy_tui_config() {
let legacy = "[tui]\nmouse = false\n";
let mut current = legacy.to_owned();
for m in MIGRATIONS.iter() {
current = m
.apply(&current)
.expect("registry migration must not fail")
.output;
}
assert!(
current.contains("# panel_sizing = \"auto\""),
"MIGRATIONS must add the panel_sizing advisory comment, got: {current}"
);
}

/// SC-003 (issue #6545): the isolated `migrate_agents_max_spawns_per_session` tests in
Expand Down Expand Up @@ -2307,6 +2326,7 @@ fn registry_preserves_order_matches_dispatch() {
"migrate_telegram_expandable_blockquote_config",
"migrate_agents_max_spawns_per_session",
"migrate_shell_risk_chain_window_turns",
"migrate_tui_panel_sizing",
];
let actual: Vec<&str> = MIGRATIONS.iter().map(|m| m.name()).collect();
assert_eq!(actual, expected);
Expand Down
24 changes: 24 additions & 0 deletions crates/zeph-config/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,30 @@ pub struct TuiConfig {
/// the TUI. Text selection via Shift+drag still works. Default: `false`.
#[serde(default)]
pub mouse: bool,
/// Side-panel vertical sizing strategy (#6675).
///
/// `auto` (default) sizes each unpinned side panel from its own content; `even`
/// approximates the pre-#6675 behavior of splitting the column equally regardless of
/// content (same total per slot; exact per-slot remainder placement can differ from the
/// old cassowary-based split — see `zeph_tui::layout::PanelDemand`'s docs). Runtime-
/// togglable via `/panel_sizing [auto|even]`.
#[serde(default)]
pub panel_sizing: PanelSizingMode,
}

/// Side-panel vertical sizing strategy (see [`TuiConfig::panel_sizing`], #6675).
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PanelSizingMode {
/// Size each unpinned side panel from its own content (`desired_height`), via a
/// max-min fair water-filling allocator. Leftover space stays blank at the bottom of
/// the column. Default.
#[default]
Auto,
/// Approximates pre-#6675 behavior: unpinned panels split the column evenly, regardless
/// of content (same total height per slot; exact remainder placement can differ from
/// the old cassowary-based split).
Even,
}

/// Configuration for the TUI fleet panel (#3884).
Expand Down
Loading
Loading