mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
Add terminal thread init command (#59374)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
## Summary - Adds an `agent.terminal_init_command` setting for Terminal Threads. - Runs the configured command automatically when creating a new Terminal Thread, while skipping restored terminal threads. - Documents the setting and exposes it in the AI settings page. Closes https://github.com/zed-industries/zed/issues/58697 Closes AI-337 Release Notes: - Added a setting to automatically run a command when opening a new terminal thread in the agent panel. --------- Co-authored-by: Anant Goel <anant@zed.dev>
This commit is contained in:
parent
3e8aae49b5
commit
fd5d42dd55
11 changed files with 360 additions and 11 deletions
|
|
@ -1205,6 +1205,13 @@
|
|||
//
|
||||
// Default: true
|
||||
"expand_terminal_card": true,
|
||||
// Command to automatically run when Zed creates a Terminal Thread shell in the agent panel.
|
||||
// The command is sent to the shell as if typed, so it is interpreted by your
|
||||
// configured shell (including on Windows and remote/WSL projects).
|
||||
// Set to "" to disable.
|
||||
//
|
||||
// Example: "terminal_init_command": "claude"
|
||||
"terminal_init_command": "",
|
||||
// How thinking blocks should be displayed by default in the agent panel.
|
||||
//
|
||||
// Default: auto
|
||||
|
|
|
|||
|
|
@ -597,6 +597,7 @@ mod tests {
|
|||
enable_feedback: false,
|
||||
expand_edit_card: true,
|
||||
expand_terminal_card: true,
|
||||
terminal_init_command: None,
|
||||
cancel_generation_on_terminal_stop: true,
|
||||
use_modifier_to_send: true,
|
||||
message_editor_min_lines: 1,
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ pub struct AgentSettings {
|
|||
pub enable_feedback: bool,
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
pub terminal_init_command: Option<String>,
|
||||
pub thinking_display: ThinkingBlockDisplay,
|
||||
pub cancel_generation_on_terminal_stop: bool,
|
||||
pub use_modifier_to_send: bool,
|
||||
|
|
@ -767,6 +768,9 @@ impl Settings for AgentSettings {
|
|||
enable_feedback: agent.enable_feedback.unwrap(),
|
||||
expand_edit_card: agent.expand_edit_card.unwrap(),
|
||||
expand_terminal_card: agent.expand_terminal_card.unwrap(),
|
||||
terminal_init_command: agent
|
||||
.terminal_init_command
|
||||
.filter(|command| !command.trim().is_empty()),
|
||||
thinking_display: agent.thinking_display.unwrap(),
|
||||
cancel_generation_on_terminal_stop: agent.cancel_generation_on_terminal_stop.unwrap(),
|
||||
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
|
||||
|
|
@ -978,6 +982,56 @@ mod tests {
|
|||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_terminal_init_command_filters_empty_without_trimming(cx: &mut gpui::App) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
project::DisableAiSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
let new_text = store
|
||||
.new_text_for_update("{}".to_string(), |settings| {
|
||||
settings.agent.get_or_insert_default().terminal_init_command =
|
||||
Some(" claude --resume ".to_string());
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
new_text.contains(r#""terminal_init_command": " claude --resume ""#),
|
||||
"updated settings JSON should include terminal_init_command, got {new_text}"
|
||||
);
|
||||
store.set_user_settings(&new_text, cx).unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
AgentSettings::get_global(cx)
|
||||
.terminal_init_command
|
||||
.as_deref(),
|
||||
Some(" claude --resume ")
|
||||
);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(r#"{ "agent": { "terminal_init_command": " " } }"#, cx)
|
||||
.unwrap();
|
||||
});
|
||||
assert!(
|
||||
AgentSettings::get_global(cx)
|
||||
.terminal_init_command
|
||||
.is_none()
|
||||
);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(r#"{ "agent": { "terminal_init_command": null } }"#, cx)
|
||||
.unwrap();
|
||||
});
|
||||
assert!(
|
||||
AgentSettings::get_global(cx)
|
||||
.terminal_init_command
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_permissions_parsing() {
|
||||
let json = json!({
|
||||
|
|
|
|||
|
|
@ -1943,6 +1943,7 @@ impl AgentPanel {
|
|||
None,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
source,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -1996,11 +1997,13 @@ impl AgentPanel {
|
|||
created_at: Option<DateTime<Utc>>,
|
||||
select: bool,
|
||||
focus: bool,
|
||||
run_init_command: bool,
|
||||
source: AgentThreadSource,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let terminal_working_directory = working_directory.clone();
|
||||
let init_command = Self::terminal_init_command(run_init_command, cx);
|
||||
let terminal_task = self.project.update(cx, |project, cx| {
|
||||
project.create_terminal_shell(working_directory, cx)
|
||||
});
|
||||
|
|
@ -2027,6 +2030,7 @@ impl AgentPanel {
|
|||
}
|
||||
};
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let terminal_for_init_command = terminal.clone();
|
||||
let terminal_view = cx.new(|cx| {
|
||||
TerminalView::new(terminal, workspace, workspace_id, project, window, cx)
|
||||
});
|
||||
|
|
@ -2043,12 +2047,38 @@ impl AgentPanel {
|
|||
window,
|
||||
cx,
|
||||
);
|
||||
Self::write_terminal_init_command(&terminal_for_init_command, init_command, cx);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn terminal_init_command(run_init_command: bool, cx: &App) -> Option<String> {
|
||||
run_init_command
|
||||
.then(|| AgentSettings::get_global(cx).terminal_init_command.clone())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn write_terminal_init_command(
|
||||
terminal: &Entity<terminal::Terminal>,
|
||||
init_command: Option<String>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(command) = init_command else {
|
||||
return;
|
||||
};
|
||||
let mut input = command.into_bytes();
|
||||
// CR, not "\r\n": "\r\n" puts PowerShell into continuation
|
||||
// mode (same convention as the activation-script writes in
|
||||
// `TerminalBuilder::new`).
|
||||
input.push(b'\x0d');
|
||||
// `write_init_command`, not `input`: the latter sets `keyboard_input_sent`,
|
||||
// which would auto-close the terminal (hiding the error) if the shell
|
||||
// fails to spawn after we've written the command.
|
||||
terminal.update(cx, |terminal, _| terminal.write_init_command(input));
|
||||
}
|
||||
|
||||
fn insert_terminal(
|
||||
&mut self,
|
||||
terminal_id: TerminalId,
|
||||
|
|
@ -2324,6 +2354,7 @@ impl AgentPanel {
|
|||
Some(metadata.created_at),
|
||||
true,
|
||||
focus,
|
||||
true,
|
||||
source,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -5149,6 +5180,7 @@ impl AgentPanel {
|
|||
None,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
source,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -5172,6 +5204,7 @@ impl AgentPanel {
|
|||
None,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
source,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -6686,6 +6719,7 @@ impl AgentPanel {
|
|||
None,
|
||||
focus,
|
||||
focus,
|
||||
true,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -6722,6 +6756,7 @@ impl AgentPanel {
|
|||
Some(metadata.created_at),
|
||||
true,
|
||||
focus,
|
||||
true,
|
||||
source,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -6738,10 +6773,12 @@ impl AgentPanel {
|
|||
created_at: Option<DateTime<Utc>>,
|
||||
select: bool,
|
||||
focus: bool,
|
||||
run_init_command: bool,
|
||||
source: AgentThreadSource,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let init_command = Self::terminal_init_command(run_init_command, cx);
|
||||
let settings = TerminalSettings::get_global(cx).clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let builder = terminal::TerminalBuilder::new_display_only(
|
||||
|
|
@ -6753,6 +6790,7 @@ impl AgentPanel {
|
|||
path_style,
|
||||
);
|
||||
let terminal = cx.new(|cx| builder.subscribe(cx));
|
||||
let terminal_for_init_command = terminal.clone();
|
||||
let terminal_view = cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
|
|
@ -6776,6 +6814,7 @@ impl AgentPanel {
|
|||
window,
|
||||
cx,
|
||||
);
|
||||
Self::write_terminal_init_command(&terminal_for_init_command, init_command, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -7373,6 +7412,150 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restored_terminal_runs_init_command_once(cx: &mut TestAppContext) {
|
||||
let (panel, mut cx) = setup_panel(cx).await;
|
||||
cx.update(|_, cx| {
|
||||
let mut settings = AgentSettings::get_global(cx).clone();
|
||||
settings.terminal_init_command = Some(" claude --resume ".to_string());
|
||||
AgentSettings::override_global(settings, cx);
|
||||
});
|
||||
|
||||
let metadata = TerminalThreadMetadata {
|
||||
terminal_id: TerminalId::new(),
|
||||
title: "Restored Terminal".into(),
|
||||
custom_title: None,
|
||||
created_at: Utc::now(),
|
||||
worktree_paths: WorktreePaths::from_folder_paths(&PathList::new(&[PathBuf::from(
|
||||
"/project",
|
||||
)])),
|
||||
remote_connection: None,
|
||||
working_directory: None,
|
||||
};
|
||||
let terminal_id = metadata.terminal_id;
|
||||
panel
|
||||
.update_in(&mut cx, |panel, window, cx| {
|
||||
panel.restore_test_terminal(
|
||||
metadata.clone(),
|
||||
true,
|
||||
AgentThreadSource::AgentPanel,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.expect("test terminal should be restored");
|
||||
cx.run_until_parked();
|
||||
|
||||
let terminal = panel.read_with(&cx, |panel, cx| {
|
||||
panel
|
||||
.terminals
|
||||
.get(&terminal_id)
|
||||
.expect("terminal should exist")
|
||||
.view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.clone()
|
||||
});
|
||||
let input_log = terminal.update(&mut cx, |terminal, _| terminal.take_input_log());
|
||||
assert_eq!(input_log, vec![b" claude --resume \r".to_vec()]);
|
||||
assert!(
|
||||
!terminal.read_with(&cx, |terminal, _| terminal.keyboard_input_sent()),
|
||||
"writing the init command must not mark the terminal as having received \
|
||||
user keyboard input, otherwise a shell that fails to spawn would be \
|
||||
auto-closed before the user can see the error"
|
||||
);
|
||||
|
||||
panel
|
||||
.update_in(&mut cx, |panel, window, cx| {
|
||||
panel.restore_test_terminal(
|
||||
metadata,
|
||||
true,
|
||||
AgentThreadSource::AgentPanel,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.expect("restoring an existing test terminal should succeed");
|
||||
cx.run_until_parked();
|
||||
|
||||
let input_log = terminal.update(&mut cx, |terminal, _| terminal.take_input_log());
|
||||
assert!(
|
||||
input_log.is_empty(),
|
||||
"activating an already-restored terminal should not re-run the init command, got {input_log:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Exercises the real `spawn_terminal` path with a genuine shell PTY (not the
|
||||
/// display-only test terminal, where `write_to_pty` is a no-op) to verify the
|
||||
/// init command is actually delivered to the shell and executed.
|
||||
#[cfg(unix)]
|
||||
#[gpui::test]
|
||||
async fn test_spawn_terminal_runs_init_command_in_real_shell(cx: &mut TestAppContext) {
|
||||
let (panel, mut cx) = setup_panel(cx).await;
|
||||
cx.executor().allow_parking();
|
||||
cx.update(|_, cx| {
|
||||
let mut settings = AgentSettings::get_global(cx).clone();
|
||||
// The output (`init_ran_42`) is distinct from the command text
|
||||
// (`init_ran_$((6*7))`), which the PTY also echoes back. Finding the
|
||||
// output therefore proves the shell actually executed the command
|
||||
// rather than merely echoing the keystrokes.
|
||||
settings.terminal_init_command = Some("echo init_ran_$((6*7))".to_string());
|
||||
AgentSettings::override_global(settings, cx);
|
||||
});
|
||||
|
||||
let terminal_id = TerminalId::new();
|
||||
panel.update_in(&mut cx, |panel, window, cx| {
|
||||
panel.spawn_terminal(
|
||||
terminal_id,
|
||||
// No working directory: the FakeFs project path doesn't exist on
|
||||
// the real filesystem the shell process runs against.
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// The shell spawns on a background thread and produces output
|
||||
// asynchronously, so poll (with a deadline) rather than using a fixed
|
||||
// sleep, matching the real-PTY test in `acp_thread`.
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
let terminal = loop {
|
||||
let terminal = panel.read_with(&cx, |panel, cx| {
|
||||
panel
|
||||
.terminals
|
||||
.get(&terminal_id)
|
||||
.map(|terminal| terminal.view.read(cx).terminal().clone())
|
||||
});
|
||||
if let Some(terminal) = &terminal
|
||||
&& terminal
|
||||
.read_with(&cx, |terminal, _| terminal.get_content())
|
||||
.contains("init_ran_42")
|
||||
{
|
||||
break terminal.clone();
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"init command output never appeared in the terminal"
|
||||
);
|
||||
cx.executor().timer(Duration::from_millis(50)).await;
|
||||
};
|
||||
|
||||
assert!(
|
||||
!terminal.read_with(&cx, |terminal, _| terminal.keyboard_input_sent()),
|
||||
"writing the init command must not mark the terminal as having received \
|
||||
user keyboard input"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restored_terminal_does_not_update_global_entry_kind(cx: &mut TestAppContext) {
|
||||
let (panel, mut cx) = setup_panel(cx).await;
|
||||
|
|
@ -8818,6 +9001,7 @@ mod tests {
|
|||
None,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -964,6 +964,7 @@ mod tests {
|
|||
enable_feedback: false,
|
||||
expand_edit_card: true,
|
||||
expand_terminal_card: true,
|
||||
terminal_init_command: None,
|
||||
cancel_generation_on_terminal_stop: true,
|
||||
use_modifier_to_send: true,
|
||||
message_editor_min_lines: 1,
|
||||
|
|
|
|||
|
|
@ -298,6 +298,13 @@ pub struct AgentSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
pub expand_terminal_card: Option<bool>,
|
||||
/// Command to automatically run when Zed creates a Terminal Thread shell in the agent panel.
|
||||
/// The command is sent to the shell as if typed, so it is interpreted by your
|
||||
/// configured shell (including on Windows and remote/WSL projects).
|
||||
/// An empty string disables this behavior.
|
||||
///
|
||||
/// Default: ""
|
||||
pub terminal_init_command: Option<String>,
|
||||
/// How thinking blocks should be displayed by default in the agent panel.
|
||||
///
|
||||
/// Default: automatic
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub struct SettingsInputField {
|
|||
display_confirm_button: bool,
|
||||
display_clear_button: bool,
|
||||
clear_on_confirm: bool,
|
||||
confirm_on_focus_out: bool,
|
||||
action_slot: Option<AnyElement>,
|
||||
color: Option<Color>,
|
||||
}
|
||||
|
|
@ -33,6 +34,7 @@ impl SettingsInputField {
|
|||
display_confirm_button: false,
|
||||
display_clear_button: false,
|
||||
clear_on_confirm: false,
|
||||
confirm_on_focus_out: false,
|
||||
action_slot: None,
|
||||
color: None,
|
||||
}
|
||||
|
|
@ -76,6 +78,11 @@ impl SettingsInputField {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn confirm_on_focus_out(mut self) -> Self {
|
||||
self.confirm_on_focus_out = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action_slot(mut self, action: impl IntoElement) -> Self {
|
||||
self.action_slot = Some(action.into_any_element());
|
||||
self
|
||||
|
|
@ -125,9 +132,10 @@ impl RenderOnce for SettingsInputField {
|
|||
}
|
||||
|
||||
if let Some(confirm) = confirm.take()
|
||||
&& !self.display_confirm_button
|
||||
&& !self.display_clear_button
|
||||
&& !self.clear_on_confirm
|
||||
&& (self.confirm_on_focus_out
|
||||
|| (!self.display_confirm_button
|
||||
&& !self.display_clear_button
|
||||
&& !self.clear_on_confirm))
|
||||
{
|
||||
cx.on_focus_out(
|
||||
&editor_focus_handle,
|
||||
|
|
@ -161,9 +169,10 @@ impl RenderOnce for SettingsInputField {
|
|||
}
|
||||
|
||||
if let Some(confirm) = confirm.take()
|
||||
&& !self.display_confirm_button
|
||||
&& !self.display_clear_button
|
||||
&& !self.clear_on_confirm
|
||||
&& (self.confirm_on_focus_out
|
||||
|| (!self.display_confirm_button
|
||||
&& !self.display_clear_button
|
||||
&& !self.clear_on_confirm))
|
||||
{
|
||||
cx.on_focus_out(
|
||||
&editor_focus_handle,
|
||||
|
|
|
|||
|
|
@ -8087,6 +8087,36 @@ fn ai_page(cx: &App) -> SettingsPage {
|
|||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Terminal Thread Init Command",
|
||||
description: "Command to automatically run when Zed creates a Terminal Thread shell in the agent panel. Runs in your configured shell.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("agent.terminal_init_command"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.agent
|
||||
.as_ref()?
|
||||
.terminal_init_command
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.terminal_init_command = value;
|
||||
},
|
||||
}),
|
||||
metadata: Some(Box::new(SettingsFieldMetadata {
|
||||
placeholder: Some("e.g. claude"),
|
||||
display_confirm_button: true,
|
||||
display_clear_button: true,
|
||||
confirm_on_focus_out: true,
|
||||
treat_missing_text_as_empty: true,
|
||||
..Default::default()
|
||||
})),
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Thinking Display",
|
||||
description: "How thinking blocks should be displayed by default. 'Auto' fully expands during streaming, then auto-collapses when done. 'Preview' auto-expands with a height constraint during streaming. 'Always Expanded' shows full content. 'Always Collapsed' keeps them collapsed.",
|
||||
|
|
|
|||
|
|
@ -416,6 +416,10 @@ impl Focusable for NonFocusableHandle {
|
|||
struct SettingsFieldMetadata {
|
||||
placeholder: Option<&'static str>,
|
||||
should_do_titlecase: Option<bool>,
|
||||
display_confirm_button: bool,
|
||||
display_clear_button: bool,
|
||||
confirm_on_focus_out: bool,
|
||||
treat_missing_text_as_empty: bool,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
|
|
@ -4713,17 +4717,37 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
|
|||
) -> AnyElement {
|
||||
let (_, initial_text) =
|
||||
SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
|
||||
let initial_text = initial_text.filter(|s| !s.as_ref().is_empty());
|
||||
let initial_text = if metadata.is_some_and(|metadata| metadata.treat_missing_text_as_empty) {
|
||||
Some(
|
||||
initial_text
|
||||
.map(|text| text.as_ref().to_string())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
initial_text
|
||||
.filter(|text| !text.as_ref().is_empty())
|
||||
.map(|text| text.as_ref().to_string())
|
||||
};
|
||||
|
||||
SettingsInputField::new()
|
||||
.tab_index(0)
|
||||
.when_some(initial_text, |editor, text| {
|
||||
editor.with_initial_text(text.as_ref().to_string())
|
||||
})
|
||||
.when_some(initial_text, |editor, text| editor.with_initial_text(text))
|
||||
.when_some(
|
||||
metadata.and_then(|metadata| metadata.placeholder),
|
||||
|editor, placeholder| editor.with_placeholder(placeholder),
|
||||
)
|
||||
.when(
|
||||
metadata.is_some_and(|metadata| metadata.display_confirm_button),
|
||||
|editor| editor.display_confirm_button(),
|
||||
)
|
||||
.when(
|
||||
metadata.is_some_and(|metadata| metadata.display_clear_button),
|
||||
|editor| editor.display_clear_button(),
|
||||
)
|
||||
.when(
|
||||
metadata.is_some_and(|metadata| metadata.confirm_on_focus_out),
|
||||
|editor| editor.confirm_on_focus_out(),
|
||||
)
|
||||
.on_confirm({
|
||||
move |new_text, window, cx| {
|
||||
update_settings_file(
|
||||
|
|
|
|||
|
|
@ -1860,10 +1860,21 @@ impl Terminal {
|
|||
}
|
||||
|
||||
pub fn input(&mut self, input: impl Into<Cow<'static, [u8]>>) {
|
||||
self.keyboard_input_sent = true;
|
||||
self.write_input(input);
|
||||
}
|
||||
|
||||
/// Write a programmatically-generated command to the PTY as if it had been
|
||||
/// typed, without marking the terminal as having received user keyboard
|
||||
/// input.
|
||||
pub fn write_init_command(&mut self, input: impl Into<Cow<'static, [u8]>>) {
|
||||
self.write_input(input);
|
||||
}
|
||||
|
||||
fn write_input(&mut self, input: impl Into<Cow<'static, [u8]>>) {
|
||||
self.events.push_back(InternalEvent::Scroll(Scroll::Bottom));
|
||||
self.events.push_back(InternalEvent::SetSelection(None));
|
||||
|
||||
self.keyboard_input_sent = true;
|
||||
let input = input.into();
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
self.input_log.push(input.to_vec());
|
||||
|
|
@ -1876,6 +1887,11 @@ impl Terminal {
|
|||
std::mem::take(&mut self.input_log)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn keyboard_input_sent(&self) -> bool {
|
||||
self.keyboard_input_sent
|
||||
}
|
||||
|
||||
pub fn toggle_vi_mode(&mut self) {
|
||||
self.events.push_back(InternalEvent::ToggleViMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,22 @@ Open the new-thread menu from the [Agent Panel](./agent-panel.md) using the agen
|
|||
|
||||
You can open as many Terminal Threads as you like. Each gets its own entry in the Threads Sidebar.
|
||||
|
||||
## Running a Command Automatically {#terminal-thread-init-command}
|
||||
|
||||
If you always run the same CLI in Terminal Threads, set the `agent.terminal_init_command` setting to have Zed run a command automatically whenever Zed creates a Terminal Thread shell:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"agent": {
|
||||
"terminal_init_command": "claude"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The command is sent to the shell as if you had typed it, so it is interpreted by your configured shell—including on Windows and in remote or WSL projects—and the terminal remains a regular interactive shell after the command exits. It runs when creating a new Terminal Thread and when recreating a saved Terminal Thread after reopening a project.
|
||||
|
||||
You can also configure this from the Settings UI under **AI**, via the "Terminal Thread Init Command" field.
|
||||
|
||||
## Terminal Thread Titles {#terminal-thread-titles}
|
||||
|
||||
The terminal title in the toolbar updates automatically to reflect the running shell or process. You can also set a custom name by clicking the title or the pencil icon that appears on hover.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue