Cargo.lock 🔗
@@ -405,6 +405,7 @@ dependencies = [
"strsim 0.11.1",
"strum",
"telemetry_events",
+ "terminal_view",
"theme",
"tiktoken-rs",
"toml 0.8.10",
Bennet Bo Fenner created
This adds a `term` slash command to the assistant which allows to inject
the latest terminal output into the context.
Release Notes:
- N/A
Cargo.lock | 1
crates/assistant/Cargo.toml | 1
crates/assistant/src/assistant.rs | 3
crates/assistant/src/slash_command.rs | 1
crates/assistant/src/slash_command/term_command.rs | 105 ++++++++++++++++
crates/terminal/src/terminal.rs | 25 +++
6 files changed, 135 insertions(+), 1 deletion(-)
@@ -405,6 +405,7 @@ dependencies = [
"strsim 0.11.1",
"strum",
"telemetry_events",
+ "terminal_view",
"theme",
"tiktoken-rs",
"toml 0.8.10",
@@ -55,6 +55,7 @@ smol.workspace = true
strsim = "0.11"
strum.workspace = true
telemetry_events.workspace = true
+terminal_view.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true
toml.workspace = true
@@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use slash_command::{
active_command, default_command, diagnostics_command, fetch_command, file_command, now_command,
- project_command, prompt_command, rustdoc_command, search_command, tabs_command,
+ project_command, prompt_command, rustdoc_command, search_command, tabs_command, term_command,
};
use std::{
fmt::{self, Display},
@@ -314,6 +314,7 @@ fn register_slash_commands(cx: &mut AppContext) {
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
+ slash_command_registry.register_command(term_command::TermSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, true);
slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
@@ -28,6 +28,7 @@ pub mod prompt_command;
pub mod rustdoc_command;
pub mod search_command;
pub mod tabs_command;
+pub mod term_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
@@ -0,0 +1,105 @@
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+
+use anyhow::Result;
+use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
+use gpui::{AppContext, Task, WeakView};
+use language::{CodeLabel, LspAdapterDelegate};
+use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
+use ui::prelude::*;
+use workspace::Workspace;
+
+use super::create_label_for_command;
+
+pub(crate) struct TermSlashCommand;
+
+const LINE_COUNT_ARG: &str = "--line-count";
+
+impl SlashCommand for TermSlashCommand {
+ fn name(&self) -> String {
+ "term".into()
+ }
+
+ fn label(&self, cx: &AppContext) -> CodeLabel {
+ create_label_for_command("term", &[LINE_COUNT_ARG], cx)
+ }
+
+ fn description(&self) -> String {
+ "insert terminal output".into()
+ }
+
+ fn menu_text(&self) -> String {
+ "Insert terminal output".into()
+ }
+
+ fn requires_argument(&self) -> bool {
+ false
+ }
+
+ fn complete_argument(
+ self: Arc<Self>,
+ _query: String,
+ _cancel: Arc<AtomicBool>,
+ _workspace: Option<WeakView<Workspace>>,
+ _cx: &mut AppContext,
+ ) -> Task<Result<Vec<String>>> {
+ Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()]))
+ }
+
+ fn run(
+ self: Arc<Self>,
+ argument: Option<&str>,
+ workspace: WeakView<Workspace>,
+ _delegate: Arc<dyn LspAdapterDelegate>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<SlashCommandOutput>> {
+ let Some(workspace) = workspace.upgrade() else {
+ return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
+ };
+ let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
+ return Task::ready(Err(anyhow::anyhow!("no terminal panel open")));
+ };
+ let Some(active_terminal) = terminal_panel
+ .read(cx)
+ .pane()
+ .read(cx)
+ .active_item()
+ .and_then(|t| t.downcast::<TerminalView>())
+ else {
+ return Task::ready(Err(anyhow::anyhow!("no active terminal")));
+ };
+
+ let line_count = argument.and_then(|a| parse_argument(a)).unwrap_or(20);
+
+ let lines = active_terminal
+ .read(cx)
+ .model()
+ .read(cx)
+ .last_n_non_empty_lines(line_count);
+
+ let mut text = String::new();
+ text.push_str("Terminal output:\n");
+ text.push_str(&lines.join("\n"));
+ let range = 0..text.len();
+
+ Task::ready(Ok(SlashCommandOutput {
+ text,
+ sections: vec![SlashCommandOutputSection {
+ range,
+ icon: IconName::Terminal,
+ label: "Terminal".into(),
+ }],
+ run_commands_in_text: false,
+ }))
+ }
+}
+
+fn parse_argument(argument: &str) -> Option<usize> {
+ let mut args = argument.split(' ');
+ if args.next() == Some(LINE_COUNT_ARG) {
+ if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
+ return Some(line_count);
+ }
+ }
+ None
+}
@@ -1083,6 +1083,31 @@ impl Terminal {
}
}
+ pub fn last_n_non_empty_lines(&self, n: usize) -> Vec<String> {
+ let term = self.term.clone();
+ let terminal = term.lock_unfair();
+
+ let mut lines = Vec::new();
+ let mut current_line = terminal.bottommost_line();
+ while lines.len() < n {
+ let mut line_buffer = String::new();
+ for cell in &terminal.grid()[current_line] {
+ line_buffer.push(cell.c);
+ }
+ let line = line_buffer.trim_end();
+ if !line.is_empty() {
+ lines.push(line.to_string());
+ }
+
+ if current_line == terminal.topmost_line() {
+ break;
+ }
+ current_line = Line(current_line.0 - 1);
+ }
+ lines.reverse();
+ lines
+ }
+
pub fn focus_in(&self) {
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
self.write_to_pty("\x1b[I".to_string());