From 913e57f95483431ebe5a5909d4fb099ca864de89 Mon Sep 17 00:00:00 2001 From: Test Date: Sun, 22 Mar 2026 08:12:32 +0100 Subject: [PATCH] feat(vscode): native sidebar tree view + embed mode for WebView VS Code extension now feels native: - Sidebar tree view in activity bar with Rivet icon - 11 navigation items: Stats, Artifacts, Validation, STPA, Graph, Documents, Matrix, Coverage, Source, Results, Help - Clicking a tree item opens the content in the WebView panel - ?embed=1 mode strips sidebar/context bar from serve output - embed_layout function: minimal HTML with HTMX, no navigation chrome - WebView shows just content; sidebar handles navigation Implements: FEAT-057 Refs: REQ-007 Co-Authored-By: Claude Opus 4.6 (1M context) --- rivet-cli/src/serve/layout.rs | 37 +++++++++++++++++++++++ rivet-cli/src/serve/mod.rs | 8 +++-- vscode-rivet/package.json | 17 +++++++++++ vscode-rivet/src/extension.ts | 57 ++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 3 deletions(-) diff --git a/rivet-cli/src/serve/layout.rs b/rivet-cli/src/serve/layout.rs index 7433bff..e37c878 100644 --- a/rivet-cli/src/serve/layout.rs +++ b/rivet-cli/src/serve/layout.rs @@ -263,3 +263,40 @@ document.addEventListener('DOMContentLoaded',function(){{mermaid.run({{querySele CSS = styles::CSS, )) } + +/// Embed layout — no sidebar, no context bar, just content with HTMX. +/// Used when the dashboard is embedded in VS Code WebView. +pub(crate) fn embed_layout(content: &str, _state: &AppState) -> Html { + let version = env!("CARGO_PKG_VERSION"); + Html(format!( + r##" + + + + +Rivet + + + + + + + +
+{content} +
+
Rivet v{version}
+ +"##, + FONTS_CSS = styles::FONTS_CSS, + CSS = styles::CSS, + )) +} diff --git a/rivet-cli/src/serve/mod.rs b/rivet-cli/src/serve/mod.rs index bc0cfab..de37cc3 100644 --- a/rivet-cli/src/serve/mod.rs +++ b/rivet-cli/src/serve/mod.rs @@ -595,15 +595,16 @@ async fn wrap_full_page( let query = req.uri().query().unwrap_or("").to_string(); let is_htmx = req.headers().contains_key("hx-request"); let is_print = query.contains("print=1"); + let is_embed = query.contains("embed=1"); let method = req.method().clone(); let response = next.run(req).await; // Only wrap GET requests to view routes (not assets or APIs) - // For "/" without print=1, the index handler already renders the full page. + // For "/" without print/embed, the index handler already renders the full page. if method == axum::http::Method::GET && !is_htmx - && (path != "/" || is_print) + && (path != "/" || is_print || is_embed) && !path.starts_with("/api/") && !path.starts_with("/assets/") && !path.starts_with("/wasm/") @@ -618,6 +619,9 @@ async fn wrap_full_page( if is_print { return layout::print_layout(&content, &app).into_response(); } + if is_embed { + return layout::embed_layout(&content, &app).into_response(); + } return layout::page_layout(&content, &app).into_response(); } diff --git a/vscode-rivet/package.json b/vscode-rivet/package.json index 7d49a27..45546bf 100644 --- a/vscode-rivet/package.json +++ b/vscode-rivet/package.json @@ -27,6 +27,23 @@ ], "main": "./out/extension", "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "rivet", + "title": "Rivet SDLC", + "icon": "icon.svg" + } + ] + }, + "views": { + "rivet": [ + { + "id": "rivetExplorer", + "name": "Explorer" + } + ] + }, "commands": [ { "command": "rivet.showDashboard", diff --git a/vscode-rivet/src/extension.ts b/vscode-rivet/src/extension.ts index fb449ee..22bdf1e 100644 --- a/vscode-rivet/src/extension.ts +++ b/vscode-rivet/src/extension.ts @@ -24,6 +24,14 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('rivet.showSTPA', () => showDashboard(context, '/stpa')), vscode.commands.registerCommand('rivet.validate', () => runValidate()), vscode.commands.registerCommand('rivet.addArtifact', () => addArtifact()), + vscode.commands.registerCommand('rivet.navigateTo', (urlPath: string) => showDashboard(context, urlPath)), + ); + + // --- Sidebar Tree View --- + const treeProvider = new RivetTreeProvider(); + vscode.window.registerTreeDataProvider('rivetExplorer', treeProvider); + context.subscriptions.push( + vscode.commands.registerCommand('rivet.refreshTree', () => treeProvider.refresh()), ); // --- Status Bar --- @@ -178,7 +186,9 @@ async function showDashboard(context: vscode.ExtensionContext, urlPath: string = } // Map localhost to a VS Code-accessible URI (works in WebViews) - const localUri = vscode.Uri.parse(`http://127.0.0.1:${dashboardPort}${urlPath}`); + // ?embed=1 strips the sidebar (VS Code tree view handles navigation) + const sep = urlPath.includes('?') ? '&' : '?'; + const localUri = vscode.Uri.parse(`http://127.0.0.1:${dashboardPort}${urlPath}${sep}embed=1`); const mappedUri = await vscode.env.asExternalUri(localUri); if (dashboardPanel) { @@ -288,3 +298,48 @@ async function addArtifact() { vscode.window.showErrorMessage(`Failed to add artifact: ${msg}`); } } + +// --- Sidebar Tree View --- + +class RivetTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: RivetTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: RivetTreeItem): RivetTreeItem[] { + if (element) return []; + + return [ + new RivetTreeItem('Stats', '/stats', 'dashboard'), + new RivetTreeItem('Artifacts', '/artifacts', 'symbol-class'), + new RivetTreeItem('Validation', '/validate', 'pass'), + new RivetTreeItem('STPA', '/stpa', 'shield'), + new RivetTreeItem('Graph', '/graph', 'type-hierarchy'), + new RivetTreeItem('Documents', '/documents', 'book'), + new RivetTreeItem('Matrix', '/matrix', 'table'), + new RivetTreeItem('Coverage', '/coverage', 'checklist'), + new RivetTreeItem('Source', '/source', 'code'), + new RivetTreeItem('Results', '/results', 'beaker'), + new RivetTreeItem('Help', '/help', 'question'), + ]; + } +} + +class RivetTreeItem extends vscode.TreeItem { + constructor(label: string, public readonly urlPath: string, icon: string) { + super(label, vscode.TreeItemCollapsibleState.None); + this.iconPath = new vscode.ThemeIcon(icon); + this.command = { + command: 'rivet.navigateTo', + title: label, + arguments: [urlPath], + }; + } +}