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:
parent
255acbaf97
commit
ca03324838
67
docs/features/smart-ide-panel.md
Normal file
67
docs/features/smart-ide-panel.md
Normal 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).
|
||||
|
||||
@ -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();
|
||||
2278
patches/lapce/0002-feat-add-Smart-IDE-cockpit-panel.patch
Normal file
2278
patches/lapce/0002-feat-add-Smart-IDE-cockpit-panel.patch
Normal file
File diff suppressed because it is too large
Load Diff
30
patches/lapce/0003-fix-handle-notify-send-errors.patch
Normal file
30
patches/lapce/0003-fix-handle-notify-send-errors.patch
Normal 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}");
|
||||
+ }
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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: [],
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user