From a14d933b0b2c0016a9257912ee9bbfeea0ebb5ad Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Sun, 5 Apr 2026 23:11:33 +0200 Subject: [PATCH 1/3] feat: add Smart IDE intents palette and scratch actions - Add Smart IDE settings (orchestrator + ia-dev-gateway URLs/tokens) - Add '!'-prefixed palette kind to execute orchestrator intents - Add commands to fetch orchestrator timeline and list ia-dev agents into scratch docs diff --git a/lapce-app/src/app.rs b/lapce-app/src/app.rs index 0676963..6fcf580 100644 --- a/lapce-app/src/app.rs +++ b/lapce-app/src/app.rs @@ -2558,6 +2558,7 @@ fn palette_item( ) } PaletteItemContent::Line { .. } + | PaletteItemContent::SmartIdeIntent { .. } | PaletteItemContent::Workspace { .. } | PaletteItemContent::SshHost { .. } | PaletteItemContent::Language { .. } diff --git a/lapce-app/src/command.rs b/lapce-app/src/command.rs index c0cc3b8..25f9027 100644 --- a/lapce-app/src/command.rs +++ b/lapce-app/src/command.rs @@ -402,6 +402,10 @@ pub enum LapceWorkbenchCommand { #[strum(serialize = "palette.palette_help_and_file")] PaletteHelpAndFile, + #[strum(message = "Smart IDE: Run Intent")] + #[strum(serialize = "palette.smart_ide_intent")] + PaletteSmartIdeIntent, + #[strum(message = "Run and Debug Restart Current Running")] #[strum(serialize = "palette.run_and_debug_restart")] RunAndDebugRestart, @@ -410,6 +414,14 @@ pub enum LapceWorkbenchCommand { #[strum(serialize = "palette.run_and_debug_stop")] RunAndDebugStop, + #[strum(message = "Smart IDE: Open Timeline")] + #[strum(serialize = "smart_ide.timeline")] + SmartIdeTimeline, + + #[strum(message = "Smart IDE: List Agents")] + #[strum(serialize = "smart_ide.agents.list")] + SmartIdeListAgents, + #[strum(serialize = "source_control.checkout_reference")] CheckoutReference, diff --git a/lapce-app/src/lib.rs b/lapce-app/src/lib.rs index 43a55ce..ea3c659 100644 --- a/lapce-app/src/lib.rs +++ b/lapce-app/src/lib.rs @@ -33,6 +33,7 @@ pub mod settings; pub mod snippet; pub mod source_control; +pub mod smart_ide; pub mod status; pub mod terminal; pub mod text_area; diff --git a/lapce-app/src/main_split.rs b/lapce-app/src/main_split.rs index d698754..fae3b48 100644 --- a/lapce-app/src/main_split.rs +++ b/lapce-app/src/main_split.rs @@ -2429,6 +2429,46 @@ pub fn open_keymap(&self) { self.get_editor_tab_child(EditorTabChildSource::Keymap, false, false); } + pub fn open_named_scratch(&self, name: &str, content: String) -> Rc { + let name = name.to_string(); + let existing = + self.scratch_docs.with_untracked(|scratch_docs| scratch_docs.get(&name).cloned()); + + let doc = if let Some(doc) = existing { + doc + } else { + let doc_content = DocContent::Scratch { + id: BufferId::next(), + name: name.clone(), + }; + let doc = Rc::new(Doc::new_content( + self.scope, + doc_content, + self.editors, + self.common.clone(), + )); + self.scratch_docs.update(|scratch_docs| { + scratch_docs.insert(name.clone(), doc.clone()); + }); + doc + }; + + doc.reload(Rope::from(content), true); + doc.set_syntax(Syntax::init(&PathBuf::from(name.clone()))); + doc.trigger_syntax_change(None); + + self.get_editor_tab_child( + EditorTabChildSource::Editor { + path: PathBuf::from(name), + doc: doc.clone(), + }, + false, + false, + ); + + doc + } + pub fn new_file(&self) -> EditorTabChild { self.get_editor_tab_child(EditorTabChildSource::NewFileEditor, false, false) } diff --git a/lapce-app/src/palette.rs b/lapce-app/src/palette.rs index fb85013..699dfaa 100644 --- a/lapce-app/src/palette.rs +++ b/lapce-app/src/palette.rs @@ -11,7 +11,7 @@ time::Instant, }; -use anyhow::Result; +use anyhow::{Context, Result}; use floem::{ ext_event::{create_ext_action, create_signal_from_channel}, keyboard::Modifiers, @@ -344,6 +344,9 @@ pub fn run(&self, kind: PaletteKind) { /// Get the placeholder text to use in the palette input field. pub fn placeholder_text(&self) -> &'static str { match self.kind.get() { + PaletteKind::SmartIdeIntent => { + "Type an intent (e.g. chat.local, git.clone) or select one below" + } PaletteKind::SshHost => { "Type [user@]host or select a previously connected workspace below" } @@ -390,6 +393,9 @@ fn run_inner(&self, kind: PaletteKind) { PaletteKind::WorkspaceSymbol => { self.get_workspace_symbols(); } + PaletteKind::SmartIdeIntent => { + self.get_smart_ide_intents(); + } PaletteKind::SshHost => { self.get_ssh_hosts(); } @@ -598,6 +604,93 @@ fn get_commands(&self) { self.items.set(items); } + fn get_smart_ide_intents(&self) { + const INTENTS: &[(&str, &str)] = &[ + ("chat.local", "Chat (LLM/RAG via orchestrator)"), + ("code.complete", "Code completion (LLM via orchestrator)"), + ("rag.query", "RAG query (AnythingLLM via orchestrator)"), + ("git.clone", "Clone repository (repos-devtools)"), + ("search.regex", "Regex search (agent-regex-search-api)"), + ("extract.entities", "Entity extraction (langextract-api)"), + ("doc.office.upload", "Upload office document (local-office)"), + ("tools.registry", "List available tools (tools-bridge)"), + ("tools.carbonyl.plan", "Open Carbonyl preview plan (tools-bridge)"), + ("tools.pageindex.run", "Run PageIndex (tools-bridge)"), + ("tools.chandra.ocr", "Run OCR (tools-bridge)"), + ("agent.run", "Run an ia_dev agent (ia-dev-gateway)"), + ]; + + let items = INTENTS + .iter() + .map(|(intent, label)| PaletteItem { + content: PaletteItemContent::SmartIdeIntent { + intent: (*intent).to_string(), + }, + filter_text: format!("{intent} — {label}"), + score: 0, + indices: Vec::new(), + }) + .collect(); + + self.items.set(items); + } + + fn smart_ide_execute_intent(&self, intent: String) { + let intent = intent.trim().to_string(); + if intent.is_empty() { + self.common.internal_command.send(InternalCommand::ShowAlert { + title: "Smart IDE".to_string(), + msg: "Intent is empty.".to_string(), + buttons: Vec::new(), + }); + return; + } + + let config = self.common.config.get_untracked(); + if let Err(err) = crate::smart_ide::orchestrator_token(&config) { + self.common.internal_command.send(InternalCommand::ShowAlert { + title: "Smart IDE".to_string(), + msg: err.to_string(), + buttons: Vec::new(), + }); + return; + } + + self.main_split.open_named_scratch( + "SmartIDE-output.json", + format!("Running intent: {intent}\n"), + ); + + let main_split = self.main_split.clone(); + let send = create_ext_action(self.common.scope, move |result: Result| { + match result { + Ok(text) => { + main_split.open_named_scratch("SmartIDE-output.json", text); + } + Err(err) => { + main_split.open_named_scratch( + "SmartIDE-output.json", + format!("Error:\n{err}"), + ); + } + } + }); + + let intent_for_thread = intent.clone(); + std::thread::Builder::new() + .name("SmartIdeExecuteIntent".to_owned()) + .spawn(move || { + let run = || -> Result { + let resp = + crate::smart_ide::orchestrator_execute(&config, &intent_for_thread)?; + serde_json::to_string_pretty(&resp) + .context("Failed to format JSON response") + }; + send(run()); + }) + .ok(); + } + /// Initialize the palette with all the available workspaces, local and remote. fn get_workspaces(&self) { let db: Arc = use_context().unwrap(); @@ -1188,6 +1281,9 @@ fn select(&self) { PaletteItemContent::Command { cmd } => { self.common.lapce_command.send(cmd.clone()); } + PaletteItemContent::SmartIdeIntent { intent } => { + self.smart_ide_execute_intent(intent.clone()); + } PaletteItemContent::Workspace { workspace } => { self.common.window_common.window_command.send( WindowCommand::SetWorkspace { @@ -1342,6 +1438,9 @@ fn select(&self) { }, }, ); + } else if self.kind.get_untracked() == PaletteKind::SmartIdeIntent { + let input = self.input.with_untracked(|input| input.input.clone()); + self.smart_ide_execute_intent(input); } } @@ -1387,6 +1486,7 @@ fn preview(&self) { ); } PaletteItemContent::Command { .. } => {} + PaletteItemContent::SmartIdeIntent { .. } => {} PaletteItemContent::Workspace { .. } => {} PaletteItemContent::RunAndDebug { .. } => {} PaletteItemContent::SshHost { .. } => {} diff --git a/lapce-app/src/palette/item.rs b/lapce-app/src/palette/item.rs index 8f0cf04..f9e7833 100644 --- a/lapce-app/src/palette/item.rs +++ b/lapce-app/src/palette/item.rs @@ -24,6 +24,9 @@ pub enum PaletteItemContent { PaletteHelp { cmd: LapceWorkbenchCommand, }, + SmartIdeIntent { + intent: String, + }, File { path: PathBuf, full_path: PathBuf, diff --git a/lapce-app/src/palette/kind.rs b/lapce-app/src/palette/kind.rs index 1a1b894..6e9e56f 100644 --- a/lapce-app/src/palette/kind.rs +++ b/lapce-app/src/palette/kind.rs @@ -12,6 +12,7 @@ pub enum PaletteKind { Reference, DocumentSymbol, WorkspaceSymbol, + SmartIdeIntent, SshHost, #[cfg(windows)] WslHost, @@ -34,6 +35,7 @@ pub fn symbol(&self) -> &'static str { PaletteKind::Line => "/", PaletteKind::DocumentSymbol => "@", PaletteKind::WorkspaceSymbol => "#", + PaletteKind::SmartIdeIntent => "!", // PaletteKind::GlobalSearch => "?", PaletteKind::Workspace => ">", PaletteKind::Command => ":", @@ -61,6 +63,7 @@ pub fn from_input(input: &str) -> PaletteKind { _ if input.starts_with('/') => PaletteKind::Line, _ if input.starts_with('@') => PaletteKind::DocumentSymbol, _ if input.starts_with('#') => PaletteKind::WorkspaceSymbol, + _ if input.starts_with('!') => PaletteKind::SmartIdeIntent, _ if input.starts_with('>') => PaletteKind::Workspace, _ if input.starts_with(':') => PaletteKind::Command, _ if input.starts_with('<') => PaletteKind::TerminalProfile, @@ -79,6 +82,9 @@ pub fn command(self) -> Option { PaletteKind::WorkspaceSymbol => { Some(LapceWorkbenchCommand::PaletteWorkspaceSymbol) } + PaletteKind::SmartIdeIntent => { + Some(LapceWorkbenchCommand::PaletteSmartIdeIntent) + } PaletteKind::Workspace => Some(LapceWorkbenchCommand::PaletteWorkspace), PaletteKind::Command => Some(LapceWorkbenchCommand::PaletteCommand), PaletteKind::File => Some(LapceWorkbenchCommand::Palette), @@ -136,6 +142,7 @@ pub fn get_input<'a>(&self, input: &'a str) -> &'a str { | PaletteKind::Workspace | PaletteKind::DocumentSymbol | PaletteKind::WorkspaceSymbol + | PaletteKind::SmartIdeIntent | PaletteKind::Line | PaletteKind::TerminalProfile // | PaletteType::GlobalSearch diff --git a/lapce-app/src/smart_ide.rs b/lapce-app/src/smart_ide.rs new file mode 100644 index 0000000..44f6478 --- /dev/null +++ b/lapce-app/src/smart_ide.rs @@ -0,0 +1,154 @@ +use std::time::Duration; + +use anyhow::{Context, Result, anyhow}; +use reqwest::blocking::Client; +use serde_json::{Value, json}; + +use crate::config::LapceConfig; + +pub const CONFIG_SECTION: &str = "smart-ide"; + +pub const KEY_ORCHESTRATOR_URL: &str = "orchestrator_url"; +pub const KEY_ORCHESTRATOR_TOKEN: &str = "orchestrator_token"; + +pub const KEY_IA_DEV_GATEWAY_URL: &str = "ia_dev_gateway_url"; +pub const KEY_IA_DEV_GATEWAY_TOKEN: &str = "ia_dev_gateway_token"; + +fn plugin_string(config: &LapceConfig, key: &str) -> Option { + config + .plugins + .get(CONFIG_SECTION) + .and_then(|m| m.get(key)) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) +} + +fn normalize_base_url(raw: &str) -> String { + raw.trim_end_matches('/').to_string() +} + +pub fn orchestrator_base_url(config: &LapceConfig) -> String { + plugin_string(config, KEY_ORCHESTRATOR_URL) + .map(|s| normalize_base_url(&s)) + .unwrap_or_else(|| "http://127.0.0.1:37145".to_string()) +} + +pub fn orchestrator_token(config: &LapceConfig) -> Result { + plugin_string(config, KEY_ORCHESTRATOR_TOKEN).ok_or_else(|| { + anyhow!( + "Missing setting: [smart-ide].{KEY_ORCHESTRATOR_TOKEN}\n\ +\n\ +Add it to your settings file:\n\ +- global: settings.toml\n\ +- per-workspace: .lapce/settings.toml\n\ +\n\ +Example:\n\ +[smart-ide]\n\ +orchestrator_url = \"http://127.0.0.1:37145\"\n\ +orchestrator_token = \"...\"" + ) + }) +} + +pub fn ia_dev_gateway_base_url(config: &LapceConfig) -> String { + plugin_string(config, KEY_IA_DEV_GATEWAY_URL) + .map(|s| normalize_base_url(&s)) + .unwrap_or_else(|| "http://127.0.0.1:37144".to_string()) +} + +pub fn ia_dev_gateway_token(config: &LapceConfig) -> Result { + plugin_string(config, KEY_IA_DEV_GATEWAY_TOKEN).ok_or_else(|| { + anyhow!( + "Missing setting: [smart-ide].{KEY_IA_DEV_GATEWAY_TOKEN}\n\ +\n\ +Add it to your settings file.\n\ +\n\ +Example:\n\ +[smart-ide]\n\ +ia_dev_gateway_url = \"http://127.0.0.1:37144\"\n\ +ia_dev_gateway_token = \"...\"" + ) + }) +} + +fn http_client() -> Result { + Client::builder() + .timeout(Duration::from_secs(20)) + .build() + .context("Failed to build HTTP client") +} + +pub fn orchestrator_execute(config: &LapceConfig, intent: &str) -> Result { + let base_url = orchestrator_base_url(config); + let token = orchestrator_token(config)?; + + let url = format!("{base_url}/v1/execute"); + let body = json!({ + "intent": intent, + }); + + let client = http_client()?; + let resp = client + .post(url) + .bearer_auth(token) + .json(&body) + .send() + .context("Orchestrator request failed")?; + + let status = resp.status(); + let text = resp.text().context("Failed to read orchestrator response body")?; + if !status.is_success() { + return Err(anyhow!( + "Orchestrator error (HTTP {status}):\n{text}" + )); + } + + serde_json::from_str(&text).context("Failed to parse orchestrator JSON response") +} + +pub fn orchestrator_timeline(config: &LapceConfig) -> Result { + let base_url = orchestrator_base_url(config); + let token = orchestrator_token(config)?; + + let url = format!("{base_url}/v1/timeline"); + let client = http_client()?; + let resp = client + .get(url) + .bearer_auth(token) + .send() + .context("Orchestrator request failed")?; + + let status = resp.status(); + let text = resp.text().context("Failed to read orchestrator response body")?; + if !status.is_success() { + return Err(anyhow!( + "Orchestrator error (HTTP {status}):\n{text}" + )); + } + + serde_json::from_str(&text).context("Failed to parse orchestrator JSON response") +} + +pub fn ia_dev_list_agents(config: &LapceConfig) -> Result { + let base_url = ia_dev_gateway_base_url(config); + let token = ia_dev_gateway_token(config)?; + + let url = format!("{base_url}/v1/agents"); + let client = http_client()?; + let resp = client + .get(url) + .bearer_auth(token) + .send() + .context("IA-dev-gateway request failed")?; + + let status = resp.status(); + let text = resp.text().context("Failed to read ia-dev-gateway response body")?; + if !status.is_success() { + return Err(anyhow!( + "IA-dev-gateway error (HTTP {status}):\n{text}" + )); + } + + serde_json::from_str(&text).context("Failed to parse ia-dev-gateway JSON response") +} + diff --git a/lapce-app/src/window_tab.rs b/lapce-app/src/window_tab.rs index 41223a9..857a215 100644 --- a/lapce-app/src/window_tab.rs +++ b/lapce-app/src/window_tab.rs @@ -713,6 +713,100 @@ pub fn run_lapce_command(&self, cmd: LapceCommand) { } } + fn smart_ide_open_timeline(&self) { + let config = self.common.config.get_untracked(); + if let Err(err) = crate::smart_ide::orchestrator_token(&config) { + self.common.internal_command.send(InternalCommand::ShowAlert { + title: "Smart IDE".to_string(), + msg: err.to_string(), + buttons: Vec::new(), + }); + return; + } + + self.main_split.open_named_scratch( + "SmartIDE-timeline.json", + "Loading orchestrator timeline...\n".to_string(), + ); + + let main_split = self.main_split.clone(); + let send = create_ext_action( + self.common.scope, + move |result: std::result::Result| match result { + Ok(text) => { + main_split.open_named_scratch("SmartIDE-timeline.json", text); + } + Err(err) => { + main_split.open_named_scratch( + "SmartIDE-timeline.json", + format!("Error:\n{err}"), + ); + } + }, + ); + + std::thread::Builder::new() + .name("SmartIdeTimeline".to_owned()) + .spawn(move || { + let result = match crate::smart_ide::orchestrator_timeline(&config) { + Ok(v) => match serde_json::to_string_pretty(&v) { + Ok(s) => Ok(s), + Err(e) => Ok(format!("{v}\n\n(pretty print failed: {e})")), + }, + Err(e) => Err(e.to_string()), + }; + send(result); + }) + .ok(); + } + + fn smart_ide_list_agents(&self) { + let config = self.common.config.get_untracked(); + if let Err(err) = crate::smart_ide::ia_dev_gateway_token(&config) { + self.common.internal_command.send(InternalCommand::ShowAlert { + title: "Smart IDE".to_string(), + msg: err.to_string(), + buttons: Vec::new(), + }); + return; + } + + self.main_split.open_named_scratch( + "SmartIDE-agents.json", + "Loading ia-dev-gateway agents...\n".to_string(), + ); + + let main_split = self.main_split.clone(); + let send = create_ext_action( + self.common.scope, + move |result: std::result::Result| match result { + Ok(text) => { + main_split.open_named_scratch("SmartIDE-agents.json", text); + } + Err(err) => { + main_split.open_named_scratch( + "SmartIDE-agents.json", + format!("Error:\n{err}"), + ); + } + }, + ); + + std::thread::Builder::new() + .name("SmartIdeListAgents".to_owned()) + .spawn(move || { + let result = match crate::smart_ide::ia_dev_list_agents(&config) { + Ok(v) => match serde_json::to_string_pretty(&v) { + Ok(s) => Ok(s), + Err(e) => Ok(format!("{v}\n\n(pretty print failed: {e})")), + }, + Err(e) => Err(e.to_string()), + }; + send(result); + }) + .ok(); + } + pub fn run_workbench_command( &self, cmd: LapceWorkbenchCommand, @@ -1105,6 +1199,7 @@ pub fn run_workbench_command( // ==== Palette Commands ==== PaletteHelp => self.palette.run(PaletteKind::PaletteHelp), PaletteHelpAndFile => self.palette.run(PaletteKind::HelpAndFile), + PaletteSmartIdeIntent => self.palette.run(PaletteKind::SmartIdeIntent), PaletteLine => { self.palette.run(PaletteKind::Line); } @@ -1141,6 +1236,10 @@ pub fn run_workbench_command( } DiffFiles => self.palette.run(PaletteKind::DiffFiles), + // ==== Smart IDE ==== + SmartIdeTimeline => self.smart_ide_open_timeline(), + SmartIdeListAgents => self.smart_ide_list_agents(), + // ==== Running / Debugging ==== RunAndDebugRestart => { let active_term = self.terminal.debug.active_term.get_untracked();