feat: update Lapce patch series for Smart IDE panel

**Motivations:**
- Keep the Lapce fork changes replayable via patch files in the monorepo.
- Expose a minimal Smart IDE cockpit UX (agents, runs, services, connection) inside Lapce.

**Root causes:**
- N/A

**Correctifs:**
- ia-dev-gateway: parse YAML frontmatter (name/description) from agent markdown files.

**Evolutions:**
- Export new Lapce patch files for the Smart IDE panel + connection helpers.
- Add documentation for the Smart IDE panel.

**Pages affectées:**
- N/A
This commit is contained in:
Nicolas Cantu 2026-04-06 13:03:34 +02:00
parent 255acbaf97
commit ca03324838
7 changed files with 3056 additions and 5 deletions

View File

@ -1 +1 @@
0.0.7
0.0.8

View File

@ -0,0 +1,67 @@
# Smart IDE panel (Lapce)
The **Smart IDE** panel is a cockpit inside Lapce to:
- browse the `ia_dev` agent catalog,
- run a small set of operational agents (via `ia-dev-gateway`),
- follow run status + logs (SSE),
- open related web services (preview, AnythingLLM, local-office),
- check connectivity and generate an SSH tunnel command.
## Open the panel
- **Command palette**: `Smart IDE: Open Panel`
- **Pinned actions shortcuts**: `Smart IDE: Run Pinned Action 1..9` (bind your own keys)
The panel is registered as a Lapce **left-top** panel with the **lightbulb** icon.
## Tabs
### Agents
- **Refresh** fetches `GET /v1/agents` from `ia-dev-gateway`.
- **Run** starts a run (`POST /v1/runs`) when the agent is runnable.
- **Open** opens the agent Markdown definition under:
- `services/ia_dev/.smartIde/agents/<agentId>.md`
### Runs
- Shows recent runs started from the panel.
- Streams events from `GET /v1/runs/<runId>/events` and appends:
- stdout/stderr (script logs),
- lifecycle events (started/completed/failed).
### Services
Minimal “open in browser” hub:
- **Preview**: read from `projects/<id>/conf.json`:
- `smart_ide.preview_urls.<env>`
- **AnythingLLM**: `http://127.0.0.1:3001/` (requires tunnel mode `all`)
- **local-office**: `http://127.0.0.1:8000/` (requires tunnel mode `all`)
### Connection
- **Health checks**:
- `GET /health` on orchestrator, `ia-dev-gateway`, and tools-bridge.
- **SSH tunnel plan**:
- Runs `scripts/smart-ide-ssh-tunnel-plan.sh --json`
- Displays a copyable command (shell-escaped argv) and the hint message.
## Configuration keys
The panel reads settings from the Lapce config section `[smart-ide]`:
- `orchestrator_url`, `orchestrator_token`
- `ia_dev_gateway_url`, `ia_dev_gateway_token`
- `tools_bridge_url`, `tools_bridge_token`
- `project_id`, `env`
- `pinned_actions` (CSV)
Project context resolution (first match):
- settings (`project_id`, `env`)
- `projects/active-project.json` (gitignored)
Tokens must stay in user settings (never committed).

View File

@ -0,0 +1,613 @@
From a14d933b0b2c0016a9257912ee9bbfeea0ebb5ad Mon Sep 17 00:00:00 2001
From: Nicolas Cantu <nicolas.cantu@pm.me>
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<Doc> {
+ 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<String>| {
+ 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<String> {
+ 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<LapceDb> = 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<LapceWorkbenchCommand> {
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<String> {
+ 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<String> {
+ 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<String> {
+ 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> {
+ Client::builder()
+ .timeout(Duration::from_secs(20))
+ .build()
+ .context("Failed to build HTTP client")
+}
+
+pub fn orchestrator_execute(config: &LapceConfig, intent: &str) -> Result<Value> {
+ 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<Value> {
+ 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<Value> {
+ 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<String, String>| 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<String, String>| 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();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
From 198ee3928f5288c8c8d9ad7cb149616411066328 Mon Sep 17 00:00:00 2001
From: Nicolas Cantu <nicolas.cantu@pm.me>
Date: Mon, 6 Apr 2026 13:02:18 +0200
Subject: [PATCH 3/3] fix: handle notify-send errors
Log failures when spawning notify-send on unix to avoid unused-variable warnings and make errors visible.
diff --git a/lapce-app/src/app/logging.rs b/lapce-app/src/app/logging.rs
index bbc0586..335ddb1 100644
--- a/lapce-app/src/app/logging.rs
+++ b/lapce-app/src/app/logging.rs
@@ -137,7 +137,7 @@ pub(super) fn error_modal(title: &str, msg: &str) -> i32 {
#[cfg(unix)]
pub fn error_notification(title: &str, msg: &str) {
- let res = std::process::Command::new("notify-send")
+ if let Err(e) = std::process::Command::new("notify-send")
.args([
"-a",
"dev.lapce.lapce",
@@ -149,5 +149,8 @@ pub fn error_notification(title: &str, msg: &str) {
title,
msg,
])
- .spawn();
+ .spawn()
+ {
+ tracing::warn!("notify-send failed: {e}");
+ }
}

View File

@ -1,3 +1,6 @@
# List Lapce patch files (one per line).
# This file is consumed by: ./scripts/core-ide-apply-patches.sh
0001-feat-add-Smart-IDE-intents-palette-and-scratch-actio.patch
0002-feat-add-Smart-IDE-cockpit-panel.patch
0003-fix-handle-notify-send-errors.patch

View File

@ -62,22 +62,82 @@ const envLiteral = (v: unknown): "test" | "pprod" | "prod" | undefined => {
return undefined;
};
const listAgents = (): { id: string; name: string; summary: string; triggerCommands: string[] }[] => {
type AgentListItem = {
id: string;
name: string;
summary: string;
description?: string;
triggerCommands: string[];
};
const parseYamlFrontmatter = (markdown: string): Record<string, string> => {
const lines = markdown.split(/\r?\n/);
if (lines.length < 3 || lines[0].trim() !== "---") {
return {};
}
let end = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === "---") {
end = i;
break;
}
}
if (end < 0) {
return {};
}
const out: Record<string, string> = {};
for (let i = 1; i < end; i++) {
const raw = lines[i];
const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)\s*$/.exec(raw);
if (!m) {
continue;
}
const key = m[1];
let value = m[2] ?? "";
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
out[key] = value;
}
return out;
};
const listAgents = (): AgentListItem[] => {
const root = getIaDevRoot();
const dir = agentsDir(root);
if (!dirExists(dir)) {
return [];
}
const out: { id: string; name: string; summary: string; triggerCommands: string[] }[] = [];
const out: AgentListItem[] = [];
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
if (!ent.isFile() || !ent.name.endsWith(".md")) {
continue;
}
const id = ent.name.replace(/\.md$/i, "");
let name = id;
let description: string | undefined;
try {
const p = path.join(dir, ent.name);
const text = fs.readFileSync(p, "utf8");
const fm = parseYamlFrontmatter(text);
if (typeof fm.name === "string" && fm.name.trim().length > 0) {
name = fm.name.trim();
}
if (typeof fm.description === "string" && fm.description.trim().length > 0) {
description = fm.description.trim();
}
} catch {
// ignore per-file parse errors
}
const summary = description ?? `Agent definition ${ent.name}`;
out.push({
id,
name: id,
summary: `Agent definition ${ent.name}`,
name,
summary,
description,
triggerCommands: [],
});
}