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]
|
[tui]
|
||||||
alternate_screen = "auto" # auto | always | never
|
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
|
# Feature Flags
|
||||||
|
|||||||
@@ -3376,6 +3376,29 @@ mod terminal_mode_tests {
|
|||||||
assert!(!should_use_mouse_capture(&cli, &config, true));
|
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]
|
#[test]
|
||||||
fn mouse_capture_is_off_without_alternate_screen() {
|
fn mouse_capture_is_off_without_alternate_screen() {
|
||||||
let cli = parse_cli(&["deepseek", "--mouse-capture"]);
|
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
|
/// 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
|
/// [`Self::queued_draft`] so the next Enter re-queues it carrying its
|
||||||
/// original skill instruction. No-op if the composer already has typed
|
/// original skill instruction. No-op if the composer already has typed
|
||||||
/// content or a draft is already being edited — surfacing the affordance
|
/// 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() {
|
if app.task_panel.is_empty() {
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
"No tasks",
|
"No active tasks",
|
||||||
Style::default().fg(palette::TEXT_MUTED),
|
Style::default().fg(palette::TEXT_MUTED),
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
@@ -291,11 +291,19 @@ fn render_sidebar_tasks(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
.count();
|
.count();
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(
|
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(),
|
Style::default().fg(palette::DEEPSEEK_SKY).bold(),
|
||||||
),
|
),
|
||||||
Span::styled(
|
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),
|
Style::default().fg(palette::TEXT_MUTED),
|
||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
|
|||||||
+97
-103
@@ -47,7 +47,9 @@ use crate::session_manager::{
|
|||||||
OfflineQueueState, QueuedSessionMessage, SavedSession, SessionManager,
|
OfflineQueueState, QueuedSessionMessage, SavedSession, SessionManager,
|
||||||
create_saved_session_with_mode, update_session,
|
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::spec::RuntimeToolServices;
|
||||||
use crate::tools::subagent::SubAgentStatus;
|
use crate::tools::subagent::SubAgentStatus;
|
||||||
use crate::tui::command_palette::{
|
use crate::tui::command_palette::{
|
||||||
@@ -286,12 +288,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
|
|||||||
active_task_id: None,
|
active_task_id: None,
|
||||||
active_thread_id: None,
|
active_thread_id: None,
|
||||||
};
|
};
|
||||||
app.task_panel = task_manager
|
refresh_active_task_panel(&mut app, &task_manager).await;
|
||||||
.list_tasks(Some(10))
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(task_summary_to_panel_entry)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let engine_config = build_engine_config(&app, config);
|
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)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn run_event_loop(
|
async fn run_event_loop(
|
||||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
@@ -433,32 +457,7 @@ async fn run_event_loop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if last_task_refresh.elapsed() >= Duration::from_millis(2500) {
|
if last_task_refresh.elapsed() >= Duration::from_millis(2500) {
|
||||||
let tasks = task_manager.list_tasks(Some(10)).await;
|
refresh_active_task_panel(app, &task_manager).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;
|
|
||||||
last_task_refresh = Instant::now();
|
last_task_refresh = Instant::now();
|
||||||
app.needs_redraw = true;
|
app.needs_redraw = true;
|
||||||
}
|
}
|
||||||
@@ -656,29 +655,7 @@ async fn run_event_loop(
|
|||||||
| "task_shell_start"
|
| "task_shell_start"
|
||||||
| "exec_shell"
|
| "exec_shell"
|
||||||
) {
|
) {
|
||||||
let tasks = task_manager.list_tasks(Some(10)).await;
|
refresh_active_task_panel(app, &task_manager).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;
|
|
||||||
last_task_refresh = Instant::now();
|
last_task_refresh = Instant::now();
|
||||||
}
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -1953,18 +1930,8 @@ async fn run_event_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// #85: Alt+↑ pops the most-recent queued message back into the
|
KeyCode::Up if key.modifiers.contains(KeyModifiers::SUPER) => {
|
||||||
// composer for editing when the preview's affordance is visible
|
app.scroll_up(app.viewport.last_transcript_visible.max(3));
|
||||||
// (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::ALT) => {
|
KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||||
app.scroll_up(3);
|
app.scroll_up(3);
|
||||||
@@ -1999,6 +1966,23 @@ async fn run_event_loop(
|
|||||||
let _ = app.select_previous_composer_attachment();
|
let _ = app.select_previous_composer_attachment();
|
||||||
continue;
|
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) => {
|
KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||||
app.scroll_down(3);
|
app.scroll_down(3);
|
||||||
}
|
}
|
||||||
@@ -3574,20 +3558,11 @@ async fn apply_command_result(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.task_panel = task_manager
|
refresh_active_task_panel(app, task_manager).await;
|
||||||
.list_tasks(Some(10))
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(task_summary_to_panel_entry)
|
|
||||||
.collect();
|
|
||||||
}
|
}
|
||||||
AppAction::TaskList => {
|
AppAction::TaskList => {
|
||||||
let tasks = task_manager.list_tasks(Some(30)).await;
|
let tasks = task_manager.list_tasks(Some(30)).await;
|
||||||
app.task_panel = tasks
|
refresh_active_task_panel(app, task_manager).await;
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(task_summary_to_panel_entry)
|
|
||||||
.collect();
|
|
||||||
app.add_message(HistoryCell::System {
|
app.add_message(HistoryCell::System {
|
||||||
content: format_task_list(&tasks),
|
content: format_task_list(&tasks),
|
||||||
});
|
});
|
||||||
@@ -3613,12 +3588,7 @@ async fn apply_command_result(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.task_panel = task_manager
|
refresh_active_task_panel(app, task_manager).await;
|
||||||
.list_tasks(Some(10))
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(task_summary_to_panel_entry)
|
|
||||||
.collect();
|
|
||||||
}
|
}
|
||||||
AppAction::ShellJob(action) => {
|
AppAction::ShellJob(action) => {
|
||||||
handle_shell_job_action(app, action);
|
handle_shell_job_action(app, action);
|
||||||
@@ -6450,10 +6420,7 @@ fn selection_point_from_position(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn selection_has_content(app: &App) -> bool {
|
fn selection_has_content(app: &App) -> bool {
|
||||||
match app.viewport.transcript_selection.ordered_endpoints() {
|
selection_to_text(app).is_some_and(|text| !text.is_empty())
|
||||||
Some((start, end)) => start != end,
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_active_selection(app: &mut App) {
|
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 end_index = end.line_index.min(lines.len().saturating_sub(1));
|
||||||
let start_index = start.line_index.min(end_index);
|
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)]
|
#[allow(clippy::needless_range_loop)]
|
||||||
for line_index in start_index..=end_index {
|
for line_index in start_index..=end_index {
|
||||||
let line_text = line_to_plain(&lines[line_index]);
|
let Some(body_start) = llm_io_selection_body_start(app, line_index) else {
|
||||||
let slice = if start_index == end_index {
|
continue;
|
||||||
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
|
|
||||||
};
|
};
|
||||||
out.push_str(&slice);
|
let line_text = line_to_plain(&lines[line_index]);
|
||||||
if line_index != end_index {
|
let line_width = text_display_width(&line_text);
|
||||||
out.push('\n');
|
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 {
|
fn open_pager_for_selection(app: &mut App) -> bool {
|
||||||
|
|||||||
@@ -94,7 +94,57 @@ fn selection_to_text_handles_multiline_and_reversed_endpoints() {
|
|||||||
column: 6,
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
|||||||
|
|
||||||
const SEND_FLASH_DURATION: Duration = Duration::from_millis(500);
|
const SEND_FLASH_DURATION: Duration = Duration::from_millis(500);
|
||||||
const COMPOSER_PANEL_HEIGHT: u16 = 2;
|
const COMPOSER_PANEL_HEIGHT: u16 = 2;
|
||||||
|
const MESSAGE_SELECTION_BODY_START_COLUMN: usize = 2;
|
||||||
|
|
||||||
pub struct ChatWidget {
|
pub struct ChatWidget {
|
||||||
content_area: Rect,
|
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 {
|
if line_index < start.line_index || line_index > end.line_index {
|
||||||
continue;
|
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)
|
(start.column, end.column)
|
||||||
} else if line_index == start.line_index {
|
} else if line_index == start.line_index {
|
||||||
(start.column, usize::MAX)
|
(start.column, usize::MAX)
|
||||||
@@ -1312,6 +1316,11 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) {
|
|||||||
(0, usize::MAX)
|
(0, usize::MAX)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if col_end <= body_start {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
col_start = col_start.max(body_start);
|
||||||
|
|
||||||
if col_start == 0 && col_end == usize::MAX {
|
if col_start == 0 && col_end == usize::MAX {
|
||||||
for span in &mut line.spans {
|
for span in &mut line.spans {
|
||||||
span.style = span.style.patch(selection_style);
|
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(
|
fn apply_detail_target_highlight(
|
||||||
lines: &mut [Line<'static>],
|
lines: &mut [Line<'static>],
|
||||||
top: usize,
|
top: usize,
|
||||||
|
|||||||
@@ -26,16 +26,14 @@ use crate::tui::widgets::Renderable;
|
|||||||
const PREVIEW_LINE_LIMIT: usize = 3;
|
const PREVIEW_LINE_LIMIT: usize = 3;
|
||||||
|
|
||||||
/// Description of the keybinding the hint line at the bottom should advertise
|
/// Description of the keybinding the hint line at the bottom should advertise
|
||||||
/// for the "edit last queued message" action. The default `Alt+↑` matches
|
/// for the "edit last queued message" action.
|
||||||
/// the chord we already wire up; callers can override for terminals where
|
|
||||||
/// Alt+ chords are eaten by the shell.
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EditBinding {
|
pub struct EditBinding {
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditBinding {
|
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.
|
/// Widget showing pending input while a turn is in progress.
|
||||||
@@ -68,7 +66,7 @@ impl PendingInputPreview {
|
|||||||
pending_steers: Vec::new(),
|
pending_steers: Vec::new(),
|
||||||
rejected_steers: Vec::new(),
|
rejected_steers: Vec::new(),
|
||||||
queued_messages: Vec::new(),
|
queued_messages: Vec::new(),
|
||||||
edit_binding: EditBinding::ALT_UP,
|
edit_binding: EditBinding::UP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +387,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pending_steer_renders_without_esc_or_alt_up_hint() {
|
fn pending_steer_renders_without_queue_edit_hint() {
|
||||||
let mut preview = PendingInputPreview::new();
|
let mut preview = PendingInputPreview::new();
|
||||||
preview.pending_steers.push("Please continue.".to_string());
|
preview.pending_steers.push("Please continue.".to_string());
|
||||||
let rows = render_to_string(&preview, 80);
|
let rows = render_to_string(&preview, 80);
|
||||||
@@ -402,8 +400,8 @@ mod tests {
|
|||||||
"unexpected Esc hint: {rows:?}"
|
"unexpected Esc hint: {rows:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!rows.iter().any(|r| r.contains("Alt+↑")),
|
!rows.iter().any(|r| r.contains("edit last queued message")),
|
||||||
"unexpected Alt+↑ hint in pending-steer-only view: {rows:?}"
|
"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("steer")));
|
||||||
assert!(rows.iter().any(|r| r.contains("rejected")));
|
assert!(rows.iter().any(|r| r.contains("rejected")));
|
||||||
assert!(rows.iter().any(|r| r.contains("queued")));
|
assert!(rows.iter().any(|r| r.contains("queued")));
|
||||||
assert!(rows.iter().any(|r| r.contains("Alt+↑")));
|
assert!(rows.iter().any(|r| r.contains("↑")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ If you are upgrading from older releases:
|
|||||||
- `[capacity].deepseek_v4_flash_prior` (float, default `4.2`)
|
- `[capacity].deepseek_v4_flash_prior` (float, default `4.2`)
|
||||||
- `[capacity].fallback_default_prior` (float, default `3.8`)
|
- `[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.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`).
|
- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).
|
||||||
- `features.*` (optional): feature flag overrides (see below).
|
- `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
|
- `-c, --continue`: resume the most recent session
|
||||||
- `--max-subagents <N>`: clamp to `1..=20`
|
- `--max-subagents <N>`: clamp to `1..=20`
|
||||||
- `--no-alt-screen`: run inline without the alternate screen buffer
|
- `--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
|
- `--profile <NAME>`: select config profile
|
||||||
- `--config <PATH>`: config file path
|
- `--config <PATH>`: config file path
|
||||||
- `-v, --verbose`: verbose logging
|
- `-v, --verbose`: verbose logging
|
||||||
|
|||||||
Reference in New Issue
Block a user