fix(tui): tighten selection and live task panels
This commit is contained in:
+1
-1
@@ -165,7 +165,7 @@ max_subagents = 5 # optional (1-20)
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
[tui]
|
||||
alternate_screen = "auto" # auto | always | never
|
||||
mouse_capture = true # true keeps wheel scrolling inside the TUI; false allows terminal-native drag selection/copy
|
||||
mouse_capture = true # true copies only transcript user/assistant text; false uses raw terminal selection/copy
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Feature Flags
|
||||
|
||||
@@ -3376,6 +3376,29 @@ mod terminal_mode_tests {
|
||||
assert!(!should_use_mouse_capture(&cli, &config, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_capture_flag_enables_mouse_capture() {
|
||||
let cli = parse_cli(&["deepseek", "--mouse-capture"]);
|
||||
let config = Config::default();
|
||||
|
||||
assert!(should_use_mouse_capture(&cli, &config, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_can_enable_mouse_capture() {
|
||||
let cli = parse_cli(&["deepseek"]);
|
||||
let config = Config {
|
||||
tui: Some(crate::config::TuiConfig {
|
||||
alternate_screen: None,
|
||||
mouse_capture: Some(true),
|
||||
status_items: None,
|
||||
}),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
assert!(should_use_mouse_capture(&cli, &config, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_capture_is_off_without_alternate_screen() {
|
||||
let cli = parse_cli(&["deepseek", "--mouse-capture"]);
|
||||
|
||||
@@ -2727,7 +2727,7 @@ impl App {
|
||||
}
|
||||
|
||||
/// Pop the most-recently queued message back into the composer for editing
|
||||
/// (issue #85 — Alt+↑ affordance). The popped message is parked in
|
||||
/// (issue #85 — ↑ affordance). The popped message is parked in
|
||||
/// [`Self::queued_draft`] so the next Enter re-queues it carrying its
|
||||
/// original skill instruction. No-op if the composer already has typed
|
||||
/// content or a draft is already being edited — surfacing the affordance
|
||||
|
||||
@@ -280,7 +280,7 @@ fn render_sidebar_tasks(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
if app.task_panel.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
"No tasks",
|
||||
"No active tasks",
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
} else {
|
||||
@@ -291,11 +291,19 @@ fn render_sidebar_tasks(f: &mut Frame, area: Rect, app: &App) {
|
||||
.count();
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
format!("{running} running"),
|
||||
if running == app.task_panel.len() {
|
||||
format!("{running} running")
|
||||
} else {
|
||||
format!("{} active", app.task_panel.len())
|
||||
},
|
||||
Style::default().fg(palette::DEEPSEEK_SKY).bold(),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" / {}", app.task_panel.len()),
|
||||
if running == app.task_panel.len() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({running} running)")
|
||||
},
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
]));
|
||||
|
||||
+97
-103
@@ -47,7 +47,9 @@ use crate::session_manager::{
|
||||
OfflineQueueState, QueuedSessionMessage, SavedSession, SessionManager,
|
||||
create_saved_session_with_mode, update_session,
|
||||
};
|
||||
use crate::task_manager::{NewTaskRequest, SharedTaskManager, TaskManager, TaskManagerConfig};
|
||||
use crate::task_manager::{
|
||||
NewTaskRequest, SharedTaskManager, TaskManager, TaskManagerConfig, TaskStatus,
|
||||
};
|
||||
use crate::tools::spec::RuntimeToolServices;
|
||||
use crate::tools::subagent::SubAgentStatus;
|
||||
use crate::tui::command_palette::{
|
||||
@@ -286,12 +288,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
|
||||
active_task_id: None,
|
||||
active_thread_id: None,
|
||||
};
|
||||
app.task_panel = task_manager
|
||||
.list_tasks(Some(10))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(task_summary_to_panel_entry)
|
||||
.collect();
|
||||
refresh_active_task_panel(&mut app, &task_manager).await;
|
||||
|
||||
let engine_config = build_engine_config(&app, config);
|
||||
|
||||
@@ -399,6 +396,33 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_active_task_panel(app: &mut App, task_manager: &SharedTaskManager) {
|
||||
let tasks = task_manager.list_tasks(None).await;
|
||||
let mut entries: Vec<TaskPanelEntry> = tasks
|
||||
.into_iter()
|
||||
.filter(|task| matches!(task.status, TaskStatus::Queued | TaskStatus::Running))
|
||||
.map(task_summary_to_panel_entry)
|
||||
.collect();
|
||||
|
||||
if let Some(shell_mgr) = app.runtime_services.shell_manager.as_ref()
|
||||
&& let Ok(mut mgr) = shell_mgr.lock()
|
||||
{
|
||||
for job in mgr.list_jobs() {
|
||||
if !matches!(job.status, crate::tools::shell::ShellStatus::Running) {
|
||||
continue;
|
||||
}
|
||||
entries.push(TaskPanelEntry {
|
||||
id: job.id,
|
||||
status: "running".to_string(),
|
||||
prompt_summary: format!("shell: {}", job.command),
|
||||
duration_ms: Some(job.elapsed_ms),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
app.task_panel = entries;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn run_event_loop(
|
||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
@@ -433,32 +457,7 @@ async fn run_event_loop(
|
||||
}
|
||||
|
||||
if last_task_refresh.elapsed() >= Duration::from_millis(2500) {
|
||||
let tasks = task_manager.list_tasks(Some(10)).await;
|
||||
let mut entries: Vec<TaskPanelEntry> =
|
||||
tasks.into_iter().map(task_summary_to_panel_entry).collect();
|
||||
|
||||
// #373: merge live shell jobs into the Tasks panel.
|
||||
if let Some(shell_mgr) = app.runtime_services.shell_manager.as_ref()
|
||||
&& let Ok(mut mgr) = shell_mgr.lock()
|
||||
{
|
||||
for job in mgr.list_jobs() {
|
||||
let status = match job.status {
|
||||
crate::tools::shell::ShellStatus::Running => "running",
|
||||
crate::tools::shell::ShellStatus::Completed => "completed",
|
||||
crate::tools::shell::ShellStatus::Failed => "failed",
|
||||
crate::tools::shell::ShellStatus::Killed => "canceled",
|
||||
crate::tools::shell::ShellStatus::TimedOut => "failed",
|
||||
};
|
||||
entries.push(TaskPanelEntry {
|
||||
id: job.id,
|
||||
status: status.to_string(),
|
||||
prompt_summary: format!("shell: {}", job.command),
|
||||
duration_ms: Some(job.elapsed_ms),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
app.task_panel = entries;
|
||||
refresh_active_task_panel(app, &task_manager).await;
|
||||
last_task_refresh = Instant::now();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
@@ -656,29 +655,7 @@ async fn run_event_loop(
|
||||
| "task_shell_start"
|
||||
| "exec_shell"
|
||||
) {
|
||||
let tasks = task_manager.list_tasks(Some(10)).await;
|
||||
let mut entries: Vec<TaskPanelEntry> =
|
||||
tasks.into_iter().map(task_summary_to_panel_entry).collect();
|
||||
if let Some(shell_mgr) = app.runtime_services.shell_manager.as_ref()
|
||||
&& let Ok(mut mgr) = shell_mgr.lock()
|
||||
{
|
||||
for job in mgr.list_jobs() {
|
||||
let status = match job.status {
|
||||
crate::tools::shell::ShellStatus::Running => "running",
|
||||
crate::tools::shell::ShellStatus::Completed => "completed",
|
||||
crate::tools::shell::ShellStatus::Failed => "failed",
|
||||
crate::tools::shell::ShellStatus::Killed => "canceled",
|
||||
crate::tools::shell::ShellStatus::TimedOut => "failed",
|
||||
};
|
||||
entries.push(TaskPanelEntry {
|
||||
id: job.id,
|
||||
status: status.to_string(),
|
||||
prompt_summary: format!("shell: {}", job.command),
|
||||
duration_ms: Some(job.elapsed_ms),
|
||||
});
|
||||
}
|
||||
}
|
||||
app.task_panel = entries;
|
||||
refresh_active_task_panel(app, &task_manager).await;
|
||||
last_task_refresh = Instant::now();
|
||||
}
|
||||
if matches!(
|
||||
@@ -1953,18 +1930,8 @@ async fn run_event_loop(
|
||||
}
|
||||
}
|
||||
},
|
||||
// #85: Alt+↑ pops the most-recent queued message back into the
|
||||
// composer for editing when the preview's affordance is visible
|
||||
// (queue non-empty, composer idle). Splits the binding into two
|
||||
// arms so the legacy scroll fallback is unambiguous on the same
|
||||
// chord.
|
||||
KeyCode::Up
|
||||
if key.modifiers.contains(KeyModifiers::ALT)
|
||||
&& app.input.is_empty()
|
||||
&& app.queued_draft.is_none()
|
||||
&& !app.queued_messages.is_empty() =>
|
||||
{
|
||||
let _ = app.pop_last_queued_into_draft();
|
||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::SUPER) => {
|
||||
app.scroll_up(app.viewport.last_transcript_visible.max(3));
|
||||
}
|
||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
app.scroll_up(3);
|
||||
@@ -1999,6 +1966,23 @@ async fn run_event_loop(
|
||||
let _ = app.select_previous_composer_attachment();
|
||||
continue;
|
||||
}
|
||||
// #85: ↑ edits the most-recent queued message when the composer
|
||||
// is idle and the pending-input preview is showing queued work.
|
||||
KeyCode::Up
|
||||
if key.modifiers.is_empty()
|
||||
&& app.input.is_empty()
|
||||
&& app.cursor_position == 0
|
||||
&& app.queued_draft.is_none()
|
||||
&& !app.queued_messages.is_empty()
|
||||
&& !mention_menu_open
|
||||
&& !slash_menu_open
|
||||
&& app.selected_composer_attachment_index().is_none() =>
|
||||
{
|
||||
let _ = app.pop_last_queued_into_draft();
|
||||
}
|
||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::SUPER) => {
|
||||
app.scroll_down(app.viewport.last_transcript_visible.max(3));
|
||||
}
|
||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
app.scroll_down(3);
|
||||
}
|
||||
@@ -3574,20 +3558,11 @@ async fn apply_command_result(
|
||||
});
|
||||
}
|
||||
}
|
||||
app.task_panel = task_manager
|
||||
.list_tasks(Some(10))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(task_summary_to_panel_entry)
|
||||
.collect();
|
||||
refresh_active_task_panel(app, task_manager).await;
|
||||
}
|
||||
AppAction::TaskList => {
|
||||
let tasks = task_manager.list_tasks(Some(30)).await;
|
||||
app.task_panel = tasks
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(task_summary_to_panel_entry)
|
||||
.collect();
|
||||
refresh_active_task_panel(app, task_manager).await;
|
||||
app.add_message(HistoryCell::System {
|
||||
content: format_task_list(&tasks),
|
||||
});
|
||||
@@ -3613,12 +3588,7 @@ async fn apply_command_result(
|
||||
});
|
||||
}
|
||||
}
|
||||
app.task_panel = task_manager
|
||||
.list_tasks(Some(10))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(task_summary_to_panel_entry)
|
||||
.collect();
|
||||
refresh_active_task_panel(app, task_manager).await;
|
||||
}
|
||||
AppAction::ShellJob(action) => {
|
||||
handle_shell_job_action(app, action);
|
||||
@@ -6450,10 +6420,7 @@ fn selection_point_from_position(
|
||||
}
|
||||
|
||||
fn selection_has_content(app: &App) -> bool {
|
||||
match app.viewport.transcript_selection.ordered_endpoints() {
|
||||
Some((start, end)) => start != end,
|
||||
None => false,
|
||||
}
|
||||
selection_to_text(app).is_some_and(|text| !text.is_empty())
|
||||
}
|
||||
|
||||
fn copy_active_selection(app: &mut App) {
|
||||
@@ -6481,25 +6448,52 @@ fn selection_to_text(app: &App) -> Option<String> {
|
||||
let end_index = end.line_index.min(lines.len().saturating_sub(1));
|
||||
let start_index = start.line_index.min(end_index);
|
||||
|
||||
let mut out = String::new();
|
||||
let mut selected_lines = Vec::new();
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for line_index in start_index..=end_index {
|
||||
let line_text = line_to_plain(&lines[line_index]);
|
||||
let slice = if start_index == end_index {
|
||||
slice_text(&line_text, start.column, end.column)
|
||||
} else if line_index == start_index {
|
||||
slice_text(&line_text, start.column, text_display_width(&line_text))
|
||||
} else if line_index == end_index {
|
||||
slice_text(&line_text, 0, end.column)
|
||||
} else {
|
||||
line_text
|
||||
let Some(body_start) = llm_io_selection_body_start(app, line_index) else {
|
||||
continue;
|
||||
};
|
||||
out.push_str(&slice);
|
||||
if line_index != end_index {
|
||||
out.push('\n');
|
||||
let line_text = line_to_plain(&lines[line_index]);
|
||||
let line_width = text_display_width(&line_text);
|
||||
let (col_start, col_end) = if start_index == end_index {
|
||||
(start.column, end.column)
|
||||
} else if line_index == start_index {
|
||||
(start.column, line_width)
|
||||
} else if line_index == end_index {
|
||||
(0, end.column)
|
||||
} else {
|
||||
(0, line_width)
|
||||
};
|
||||
|
||||
if col_end <= body_start {
|
||||
continue;
|
||||
}
|
||||
let slice = slice_text(&line_text, col_start.max(body_start), col_end);
|
||||
selected_lines.push(slice);
|
||||
}
|
||||
Some(selected_lines.join("\n"))
|
||||
}
|
||||
|
||||
fn llm_io_selection_body_start(app: &App, line_index: usize) -> Option<usize> {
|
||||
const MESSAGE_BODY_START_COLUMN: usize = 2;
|
||||
|
||||
let (filtered_cell_index, _) = app
|
||||
.viewport
|
||||
.transcript_cache
|
||||
.line_meta()
|
||||
.get(line_index)?
|
||||
.cell_line()?;
|
||||
let cell_index = app
|
||||
.collapsed_cell_map
|
||||
.get(filtered_cell_index)
|
||||
.copied()
|
||||
.unwrap_or(filtered_cell_index);
|
||||
|
||||
match app.cell_at_virtual_index(cell_index)? {
|
||||
HistoryCell::User { .. } | HistoryCell::Assistant { .. } => Some(MESSAGE_BODY_START_COLUMN),
|
||||
_ => None,
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn open_pager_for_selection(app: &mut App) -> bool {
|
||||
|
||||
@@ -94,7 +94,57 @@ fn selection_to_text_handles_multiline_and_reversed_endpoints() {
|
||||
column: 6,
|
||||
});
|
||||
|
||||
assert_eq!(selection_to_text(&app).as_deref(), Some("a beta\n gam"));
|
||||
assert_eq!(selection_to_text(&app).as_deref(), Some("a beta\ngam"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_to_text_only_copies_user_and_assistant_bodies() {
|
||||
let mut app = create_test_app();
|
||||
app.history = vec![
|
||||
HistoryCell::System {
|
||||
content: "skip system".to_string(),
|
||||
},
|
||||
HistoryCell::User {
|
||||
content: "copy user".to_string(),
|
||||
},
|
||||
HistoryCell::Thinking {
|
||||
content: "skip thinking".to_string(),
|
||||
streaming: false,
|
||||
duration_secs: Some(1.0),
|
||||
},
|
||||
HistoryCell::Assistant {
|
||||
content: "copy assistant".to_string(),
|
||||
streaming: false,
|
||||
},
|
||||
];
|
||||
app.resync_history_revisions();
|
||||
app.viewport.transcript_cache.ensure(
|
||||
&app.history,
|
||||
&app.history_revisions,
|
||||
80,
|
||||
app.transcript_render_options(),
|
||||
);
|
||||
|
||||
app.viewport.transcript_selection.anchor = Some(TranscriptSelectionPoint {
|
||||
line_index: 0,
|
||||
column: 0,
|
||||
});
|
||||
app.viewport.transcript_selection.head = Some(TranscriptSelectionPoint {
|
||||
line_index: app
|
||||
.viewport
|
||||
.transcript_cache
|
||||
.total_lines()
|
||||
.saturating_sub(1),
|
||||
column: 80,
|
||||
});
|
||||
|
||||
let selected = selection_to_text(&app).expect("selection text");
|
||||
assert!(selected.contains("copy user"), "{selected:?}");
|
||||
assert!(selected.contains("copy assistant"), "{selected:?}");
|
||||
assert!(!selected.contains("skip system"), "{selected:?}");
|
||||
assert!(!selected.contains("skip thinking"), "{selected:?}");
|
||||
assert!(!selected.contains('▎'), "{selected:?}");
|
||||
assert!(!selected.contains('●'), "{selected:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -46,6 +46,7 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
const SEND_FLASH_DURATION: Duration = Duration::from_millis(500);
|
||||
const COMPOSER_PANEL_HEIGHT: u16 = 2;
|
||||
const MESSAGE_SELECTION_BODY_START_COLUMN: usize = 2;
|
||||
|
||||
pub struct ChatWidget {
|
||||
content_area: Rect,
|
||||
@@ -1301,8 +1302,11 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) {
|
||||
if line_index < start.line_index || line_index > end.line_index {
|
||||
continue;
|
||||
}
|
||||
let Some(body_start) = llm_io_selection_body_start(app, line_index) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (col_start, col_end) = if start.line_index == end.line_index {
|
||||
let (mut col_start, col_end) = if start.line_index == end.line_index {
|
||||
(start.column, end.column)
|
||||
} else if line_index == start.line_index {
|
||||
(start.column, usize::MAX)
|
||||
@@ -1312,6 +1316,11 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) {
|
||||
(0, usize::MAX)
|
||||
};
|
||||
|
||||
if col_end <= body_start {
|
||||
continue;
|
||||
}
|
||||
col_start = col_start.max(body_start);
|
||||
|
||||
if col_start == 0 && col_end == usize::MAX {
|
||||
for span in &mut line.spans {
|
||||
span.style = span.style.patch(selection_style);
|
||||
@@ -1323,6 +1332,27 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) {
|
||||
}
|
||||
}
|
||||
|
||||
fn llm_io_selection_body_start(app: &App, line_index: usize) -> Option<usize> {
|
||||
let (filtered_cell_index, _) = app
|
||||
.viewport
|
||||
.transcript_cache
|
||||
.line_meta()
|
||||
.get(line_index)?
|
||||
.cell_line()?;
|
||||
let cell_index = app
|
||||
.collapsed_cell_map
|
||||
.get(filtered_cell_index)
|
||||
.copied()
|
||||
.unwrap_or(filtered_cell_index);
|
||||
|
||||
match app.cell_at_virtual_index(cell_index)? {
|
||||
HistoryCell::User { .. } | HistoryCell::Assistant { .. } => {
|
||||
Some(MESSAGE_SELECTION_BODY_START_COLUMN)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_detail_target_highlight(
|
||||
lines: &mut [Line<'static>],
|
||||
top: usize,
|
||||
|
||||
@@ -26,16 +26,14 @@ use crate::tui::widgets::Renderable;
|
||||
const PREVIEW_LINE_LIMIT: usize = 3;
|
||||
|
||||
/// Description of the keybinding the hint line at the bottom should advertise
|
||||
/// for the "edit last queued message" action. The default `Alt+↑` matches
|
||||
/// the chord we already wire up; callers can override for terminals where
|
||||
/// Alt+ chords are eaten by the shell.
|
||||
/// for the "edit last queued message" action.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditBinding {
|
||||
pub label: &'static str,
|
||||
}
|
||||
|
||||
impl EditBinding {
|
||||
pub const ALT_UP: EditBinding = EditBinding { label: "Alt+↑" };
|
||||
pub const UP: EditBinding = EditBinding { label: "↑" };
|
||||
}
|
||||
|
||||
/// Widget showing pending input while a turn is in progress.
|
||||
@@ -68,7 +66,7 @@ impl PendingInputPreview {
|
||||
pending_steers: Vec::new(),
|
||||
rejected_steers: Vec::new(),
|
||||
queued_messages: Vec::new(),
|
||||
edit_binding: EditBinding::ALT_UP,
|
||||
edit_binding: EditBinding::UP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +387,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_steer_renders_without_esc_or_alt_up_hint() {
|
||||
fn pending_steer_renders_without_queue_edit_hint() {
|
||||
let mut preview = PendingInputPreview::new();
|
||||
preview.pending_steers.push("Please continue.".to_string());
|
||||
let rows = render_to_string(&preview, 80);
|
||||
@@ -402,8 +400,8 @@ mod tests {
|
||||
"unexpected Esc hint: {rows:?}"
|
||||
);
|
||||
assert!(
|
||||
!rows.iter().any(|r| r.contains("Alt+↑")),
|
||||
"unexpected Alt+↑ hint in pending-steer-only view: {rows:?}"
|
||||
!rows.iter().any(|r| r.contains("edit last queued message")),
|
||||
"unexpected edit hint in pending-steer-only view: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -422,7 +420,7 @@ mod tests {
|
||||
assert!(rows.iter().any(|r| r.contains("steer")));
|
||||
assert!(rows.iter().any(|r| r.contains("rejected")));
|
||||
assert!(rows.iter().any(|r| r.contains("queued")));
|
||||
assert!(rows.iter().any(|r| r.contains("Alt+↑")));
|
||||
assert!(rows.iter().any(|r| r.contains("↑")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -271,7 +271,7 @@ If you are upgrading from older releases:
|
||||
- `[capacity].deepseek_v4_flash_prior` (float, default `4.2`)
|
||||
- `[capacity].fallback_default_prior` (float, default `3.8`)
|
||||
- `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. `auto` disables the alternate screen in Zellij; `--no-alt-screen` forces inline mode. Set `never` or run with `--no-alt-screen` when you want real terminal scrollback.
|
||||
- `tui.mouse_capture` (bool, optional, default `true` when the alternate screen is active): enable internal mouse scrolling, transcript selection, and right-click context actions. Set this to `false` or run with `--no-mouse-capture` for terminal-native drag selection and highlight-to-copy.
|
||||
- `tui.mouse_capture` (bool, optional, default `true` when the alternate screen is active): enable internal mouse scrolling, transcript selection, and right-click context actions. TUI-owned drag selection copies only user/assistant transcript text. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection.
|
||||
- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).
|
||||
- `features.*` (optional): feature flag overrides (see below).
|
||||
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ Run `deepseek --help` for the canonical list. Common flags:
|
||||
- `-c, --continue`: resume the most recent session
|
||||
- `--max-subagents <N>`: clamp to `1..=20`
|
||||
- `--no-alt-screen`: run inline without the alternate screen buffer
|
||||
- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, and right-click context actions. Mouse capture is enabled by default when the alternate screen is active; use `--no-mouse-capture` when you need terminal-native drag selection.
|
||||
- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, and right-click context actions. Mouse capture is enabled by default so drag selection copies only user/assistant transcript text; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection.
|
||||
- `--profile <NAME>`: select config profile
|
||||
- `--config <PATH>`: config file path
|
||||
- `-v, --verbose`: verbose logging
|
||||
|
||||
Reference in New Issue
Block a user