Skip to content

Commit 5ea7045

Browse files
authored
Fix: Add configurable line wrapping for chat (#2816)
* add a wrapmode in chat args * add a ut for the wrap arg
1 parent 46ddc72 commit 5ea7045

File tree

3 files changed

+116
-16
lines changed

3 files changed

+116
-16
lines changed

crates/chat-cli/src/cli/agent/mod.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,15 @@ impl Default for Agent {
184184
set.extend(default_approve);
185185
set
186186
},
187-
resources: vec!["file://AmazonQ.md", "file://AGENTS.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
188-
.into_iter()
189-
.map(Into::into)
190-
.collect::<Vec<_>>(),
187+
resources: vec![
188+
"file://AmazonQ.md",
189+
"file://AGENTS.md",
190+
"file://README.md",
191+
"file://.amazonq/rules/**/*.md",
192+
]
193+
.into_iter()
194+
.map(Into::into)
195+
.collect::<Vec<_>>(),
191196
hooks: Default::default(),
192197
tools_settings: Default::default(),
193198
use_legacy_mcp_json: true,
@@ -1298,14 +1303,14 @@ mod tests {
12981303
"name": "test-agent",
12991304
"model": "claude-sonnet-4"
13001305
}"#;
1301-
1306+
13021307
let agent: Agent = serde_json::from_str(agent_json).expect("Failed to deserialize agent with model");
13031308
assert_eq!(agent.model, Some("claude-sonnet-4".to_string()));
1304-
1309+
13051310
// Test default agent has no model
13061311
let default_agent = Agent::default();
13071312
assert_eq!(default_agent.model, None);
1308-
1313+
13091314
// Test serialization includes model field
13101315
let agent_with_model = Agent {
13111316
model: Some("test-model".to_string()),
@@ -1319,33 +1324,33 @@ mod tests {
13191324
fn test_agent_model_fallback_priority() {
13201325
// Test that agent model is checked and falls back correctly
13211326
let mut agents = Agents::default();
1322-
1327+
13231328
// Create agent with unavailable model
13241329
let agent_with_invalid_model = Agent {
13251330
name: "test-agent".to_string(),
13261331
model: Some("unavailable-model".to_string()),
13271332
..Default::default()
13281333
};
1329-
1334+
13301335
agents.agents.insert("test-agent".to_string(), agent_with_invalid_model);
13311336
agents.active_idx = "test-agent".to_string();
1332-
1337+
13331338
// Verify the agent has the model set
13341339
assert_eq!(
13351340
agents.get_active().and_then(|a| a.model.as_ref()),
13361341
Some(&"unavailable-model".to_string())
13371342
);
1338-
1343+
13391344
// Test agent without model
13401345
let agent_without_model = Agent {
13411346
name: "no-model-agent".to_string(),
13421347
model: None,
13431348
..Default::default()
13441349
};
1345-
1350+
13461351
agents.agents.insert("no-model-agent".to_string(), agent_without_model);
13471352
agents.active_idx = "no-model-agent".to_string();
1348-
1353+
13491354
assert_eq!(agents.get_active().and_then(|a| a.model.as_ref()), None);
13501355
}
13511356
}

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ use clap::{
3939
Args,
4040
CommandFactory,
4141
Parser,
42+
ValueEnum,
4243
};
4344
use cli::compact::CompactStrategy;
4445
use cli::model::{
46+
find_model,
4547
get_available_models,
4648
select_model,
47-
find_model,
4849
};
4950
pub use conversation::ConversationState;
5051
use conversation::TokenWarningLevel;
@@ -189,6 +190,16 @@ pub const EXTRA_HELP: &str = color_print::cstr! {"
189190
<black!>Change using: q settings chat.skimCommandKey x</black!>
190191
"};
191192

193+
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
194+
pub enum WrapMode {
195+
/// Always wrap at terminal width
196+
Always,
197+
/// Never wrap (raw output)
198+
Never,
199+
/// Auto-detect based on output target (default)
200+
Auto,
201+
}
202+
192203
#[derive(Debug, Clone, PartialEq, Eq, Default, Args)]
193204
pub struct ChatArgs {
194205
/// Resumes the previous conversation from this directory.
@@ -212,6 +223,9 @@ pub struct ChatArgs {
212223
pub no_interactive: bool,
213224
/// The first question to ask
214225
pub input: Option<String>,
226+
/// Control line wrapping behavior (default: auto-detect)
227+
#[arg(short = 'w', long, value_enum)]
228+
pub wrap: Option<WrapMode>,
215229
}
216230

217231
impl ChatArgs {
@@ -343,7 +357,9 @@ impl ChatArgs {
343357
// Fallback logic: try user's saved default, then system default
344358
let fallback_model_id = || {
345359
if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
346-
find_model(&models, &saved).map(|m| m.model_id.clone()).or(Some(default_model_opt.model_id.clone()))
360+
find_model(&models, &saved)
361+
.map(|m| m.model_id.clone())
362+
.or(Some(default_model_opt.model_id.clone()))
347363
} else {
348364
Some(default_model_opt.model_id.clone())
349365
}
@@ -412,6 +428,7 @@ impl ChatArgs {
412428
tool_config,
413429
!self.no_interactive,
414430
mcp_enabled,
431+
self.wrap,
415432
)
416433
.await?
417434
.spawn(os)
@@ -621,6 +638,7 @@ pub struct ChatSession {
621638
interactive: bool,
622639
inner: Option<ChatState>,
623640
ctrlc_rx: broadcast::Receiver<()>,
641+
wrap: Option<WrapMode>,
624642
}
625643

626644
impl ChatSession {
@@ -640,6 +658,7 @@ impl ChatSession {
640658
tool_config: HashMap<String, ToolSpec>,
641659
interactive: bool,
642660
mcp_enabled: bool,
661+
wrap: Option<WrapMode>,
643662
) -> Result<Self> {
644663
// Reload prior conversation
645664
let mut existing_conversation = false;
@@ -731,6 +750,7 @@ impl ChatSession {
731750
interactive,
732751
inner: Some(ChatState::default()),
733752
ctrlc_rx,
753+
wrap,
734754
})
735755
}
736756

@@ -2419,8 +2439,20 @@ impl ChatSession {
24192439
let mut buf = String::new();
24202440
let mut offset = 0;
24212441
let mut ended = false;
2442+
let terminal_width = match self.wrap {
2443+
Some(WrapMode::Never) => None,
2444+
Some(WrapMode::Always) => Some(self.terminal_width()),
2445+
Some(WrapMode::Auto) | None => {
2446+
if std::io::stdout().is_terminal() {
2447+
Some(self.terminal_width())
2448+
} else {
2449+
None
2450+
}
2451+
},
2452+
};
2453+
24222454
let mut state = ParseState::new(
2423-
Some(self.terminal_width()),
2455+
terminal_width,
24242456
os.database.settings.get_bool(Setting::ChatDisableMarkdownRendering),
24252457
);
24262458
let mut response_prefix_printed = false;
@@ -3340,6 +3372,7 @@ mod tests {
33403372
tool_config,
33413373
true,
33423374
false,
3375+
None,
33433376
)
33443377
.await
33453378
.unwrap()
@@ -3482,6 +3515,7 @@ mod tests {
34823515
tool_config,
34833516
true,
34843517
false,
3518+
None,
34853519
)
34863520
.await
34873521
.unwrap()
@@ -3579,6 +3613,7 @@ mod tests {
35793613
tool_config,
35803614
true,
35813615
false,
3616+
None,
35823617
)
35833618
.await
35843619
.unwrap()
@@ -3654,6 +3689,7 @@ mod tests {
36543689
tool_config,
36553690
true,
36563691
false,
3692+
None,
36573693
)
36583694
.await
36593695
.unwrap()
@@ -3705,6 +3741,7 @@ mod tests {
37053741
tool_config,
37063742
true,
37073743
false,
3744+
None,
37083745
)
37093746
.await
37103747
.unwrap()

crates/chat-cli/src/cli/mod.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ impl Cli {
331331

332332
#[cfg(test)]
333333
mod test {
334+
use chat::WrapMode::{
335+
Always,
336+
Auto,
337+
Never,
338+
};
339+
334340
use super::*;
335341
use crate::util::CHAT_BINARY_NAME;
336342
use crate::util::test::assert_parse;
@@ -370,6 +376,7 @@ mod test {
370376
trust_all_tools: false,
371377
trust_tools: None,
372378
no_interactive: false,
379+
wrap: None,
373380
})),
374381
verbose: 2,
375382
help_all: false,
@@ -409,6 +416,7 @@ mod test {
409416
trust_all_tools: false,
410417
trust_tools: None,
411418
no_interactive: false,
419+
wrap: None,
412420
})
413421
);
414422
}
@@ -425,6 +433,7 @@ mod test {
425433
trust_all_tools: false,
426434
trust_tools: None,
427435
no_interactive: false,
436+
wrap: None,
428437
})
429438
);
430439
}
@@ -441,6 +450,7 @@ mod test {
441450
trust_all_tools: true,
442451
trust_tools: None,
443452
no_interactive: false,
453+
wrap: None,
444454
})
445455
);
446456
}
@@ -457,6 +467,7 @@ mod test {
457467
trust_all_tools: false,
458468
trust_tools: None,
459469
no_interactive: true,
470+
wrap: None,
460471
})
461472
);
462473
assert_parse!(
@@ -469,6 +480,7 @@ mod test {
469480
trust_all_tools: false,
470481
trust_tools: None,
471482
no_interactive: true,
483+
wrap: None,
472484
})
473485
);
474486
}
@@ -485,6 +497,7 @@ mod test {
485497
trust_all_tools: true,
486498
trust_tools: None,
487499
no_interactive: false,
500+
wrap: None,
488501
})
489502
);
490503
}
@@ -501,6 +514,7 @@ mod test {
501514
trust_all_tools: false,
502515
trust_tools: Some(vec!["".to_string()]),
503516
no_interactive: false,
517+
wrap: None,
504518
})
505519
);
506520
}
@@ -517,6 +531,50 @@ mod test {
517531
trust_all_tools: false,
518532
trust_tools: Some(vec!["fs_read".to_string(), "fs_write".to_string()]),
519533
no_interactive: false,
534+
wrap: None,
535+
})
536+
);
537+
}
538+
539+
#[test]
540+
fn test_chat_with_different_wrap_modes() {
541+
assert_parse!(
542+
["chat", "-w", "never"],
543+
RootSubcommand::Chat(ChatArgs {
544+
resume: false,
545+
input: None,
546+
agent: None,
547+
model: None,
548+
trust_all_tools: false,
549+
trust_tools: None,
550+
no_interactive: false,
551+
wrap: Some(Never),
552+
})
553+
);
554+
assert_parse!(
555+
["chat", "--wrap", "always"],
556+
RootSubcommand::Chat(ChatArgs {
557+
resume: false,
558+
input: None,
559+
agent: None,
560+
model: None,
561+
trust_all_tools: false,
562+
trust_tools: None,
563+
no_interactive: false,
564+
wrap: Some(Always),
565+
})
566+
);
567+
assert_parse!(
568+
["chat", "--wrap", "auto"],
569+
RootSubcommand::Chat(ChatArgs {
570+
resume: false,
571+
input: None,
572+
agent: None,
573+
model: None,
574+
trust_all_tools: false,
575+
trust_tools: None,
576+
no_interactive: false,
577+
wrap: Some(Auto),
520578
})
521579
);
522580
}

0 commit comments

Comments
 (0)