From f01d12a4410de5db0da7a46b3beebcbf97a1ca30 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Wed, 8 Apr 2026 15:48:06 -0400 Subject: [PATCH 1/7] remove outdated action and document rest --- vsextension/ACTIONS.md | 105 +++++++++++++++++++++++++++++++++++ vsextension/README.md | 2 + vsextension/package.json | 4 -- vsextension/src/extension.ts | 33 ----------- 4 files changed, 107 insertions(+), 37 deletions(-) create mode 100644 vsextension/ACTIONS.md diff --git a/vsextension/ACTIONS.md b/vsextension/ACTIONS.md new file mode 100644 index 0000000..09a68fd --- /dev/null +++ b/vsextension/ACTIONS.md @@ -0,0 +1,105 @@ +# projspec VSCode Extension — User Actions + +This document describes every user action available in the `projspec` VSCode +extension. + +The extension activates automatically once VS Code finishes starting up +(`onStartupFinished`). + +--- + +## Command Palette Commands + +### `Show Project Library` (`projspec.showTree`) + +Opens the Project Library panel. + +**Steps:** +1. Runs `projspec library list --json-out` to load the project tree. +2. Runs `projspec info` once to load documentation for all spec/content/artifact types (cached for the lifetime of the window). +3. Opens the **"Project Library"** panel. + +--- + +### `Open Project` (`projspec.openProject`) + +Opens a project folder in a new VS Code window. +This command is invoked internally when a project node is clicked inside +the Project Library panel. + +- **Local projects** (`file://` URLs): opens the folder directly with + `vscode.openFolder`. +- **Remote GCS projects** (`gs://` URLs): shows an error — "Cannot open GCS + buckets directly. Clone the repository locally first." +- **Other URL schemes**: shows an error — "Unsupported project URL scheme: …" + +--- + +### `Show` (`projspec.showJson`) + +Serialises a project tree node to JSON and opens it as a read-only editor tab. +This command is invoked internally from the Project Library panel when a +spec, content, or artifact node is selected. + +**Steps:** +1. Receives a tree node item. +2. JSON-stringifies the node's raw data. +3. Opens a new unsaved document with JSON syntax highlighting in the editor. + +--- + +## Project Library Panel + +The Project Library panel (`projspec.showTree`) renders a custom HTML UI +inside a Webview. The following interactive elements are available. + +### Toolbar + +| Element | Action | +|---------|--------| +| **Scan** button | Scans the current workspace folder into the projspec library (`projspec scan --library `), then refreshes the tree. | +| **Create** button | Opens the [Create Project modal](#create-project-modal). | +| **Search input** | Live-filters the tree by project name or any visible child field. Click the **×** button or press **Escape** to clear. | +| **Expand All** button | Expands every node in the tree. | +| **Collapse All** button | Collapses every node in the tree. | + +### Tree Nodes +The top-level nodes are all Projects, with a name and a project URL. The name is the final portion of the URL. +Projects contain Specs, and both Specs and Projects contain Contents and Artifacts. In the tree view, +all Artifacts are show, but only Contents that are direct +children of a Project are shown. + +Nodes are colour-coded: + +- **Projects** — bold, folder colour +- **Contents** — teal (`#4ec9b0`) +- **Artifacts** — orange (`#ce9178`) +- **Specs** — function symbol colour + +| Element | Action | +|---------|--------| +| Click a **project** node | Opens the project folder in a new VS Code window (same behaviour as `projspec.openProject`). | +| Click a **spec / content / artifact** node | Fetches fresh project data (`projspec library list --json-out`), opens `projspec-details:/Project details.json` in the side column, and scrolls to the selected item. | +| **▶ / ▼ arrow** on any node | Toggles the visibility of that node's children. | +| **"Make" button** on an artifact node | Runs `projspec make ""` in a dedicated **projspec** terminal panel. | +| **"i" info button** on a spec / content / artifact node | Shows an inline popup with the item's doc string and, when available, a link to the upstream specification documentation. Press **Escape** or click elsewhere to dismiss. | + +### Create Project Modal + +Opened by the **Create** button in the toolbar. + +| Element | Action | +|---------|--------| +| **Type input with autocomplete** | Start typing a project spec type; suggestions appear below. Use **↑ / ↓** arrow keys or click to select a suggestion. | +| **Create** button | Runs `projspec create ` in the current workspace folder, then automatically scans the result into the library (`projspec scan --library `) and refreshes the tree. | +| **Cancel** button / **Escape** key | Dismisses the modal without creating anything. | + +--- + +## Virtual Document Providers + +The extension registers one read-only document scheme: + +| Scheme | URI example | Content | +|--------|-------------|---------| +| `projspec-details:` | `projspec-details:/Project details.json` | In-memory JSON, updated each time a spec/content/artifact node is selected in the Project Library panel | diff --git a/vsextension/README.md b/vsextension/README.md index d8d2a53..7d57e74 100644 --- a/vsextension/README.md +++ b/vsextension/README.md @@ -10,3 +10,5 @@ artifact. ![screenshot](./im.png) Like the qt-app, this is POC experimental only. + +The user actions available are documented in ACTIONS.md (in LLM-friendly text). diff --git a/vsextension/package.json b/vsextension/package.json index b5fdaf5..cb67a56 100644 --- a/vsextension/package.json +++ b/vsextension/package.json @@ -16,10 +16,6 @@ "main": "./out/extension.js", "contributes": { "commands": [ - { - "command": "projspec.scan", - "title": "Projspec scan" - }, { "command": "projspec.showTree", "title": "Show Project Library" diff --git a/vsextension/src/extension.ts b/vsextension/src/extension.ts index 5febd86..85ca05f 100644 --- a/vsextension/src/extension.ts +++ b/vsextension/src/extension.ts @@ -297,35 +297,7 @@ async function handleSelectItem(item: TreeNode) { } export function activate(context: vscode.ExtensionContext) { - // register a content provider for scheme - const myScheme = 'projspec'; - const myProvider = new class implements vscode.TextDocumentContentProvider { - - // emitter and its event - onDidChangeEmitter = new vscode.EventEmitter(); - onDidChange = this.onDidChangeEmitter.event; - - provideTextDocumentContent(uri: vscode.Uri): string { - const out = execSync("projspec --html-out " + uri.toString().substring(18), { stdio: 'pipe' }); - return uri.toString().substring(18) + out; - } - }; - context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(projectDetailsScheme, projectDetailsProvider)); - context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(myScheme, myProvider)); - - context.subscriptions.push(vscode.commands.registerCommand('projspec.scan', async () => { - if (vscode.workspace.workspaceFolders !== undefined) { - const folderPath = vscode.workspace.workspaceFolders[0].uri.fsPath; - const uri = vscode.Uri.file(folderPath); - let text = vscode.Uri.parse("projspec:" + uri); - - const panel = vscode.window.createWebviewPanel("projspec", folderPath, vscode.ViewColumn.One, {}); - panel.webview.html = "" + to_html(text.toString()) + ""; - console.log(folderPath); - } - else {return;}; - })); context.subscriptions.push(vscode.commands.registerCommand('projspec.showTree', async () => { const treeData = getExampleData(); @@ -1571,8 +1543,3 @@ function getInfoWebviewContent(title: string, infoData: string): string { // This method is called when your extension is deactivated export function deactivate() {} - -function to_html(path: String): String { - let out = execSync("projspec --html-out " + path.substring(18), { stdio: 'pipe' }); - return out.toString(); -} From 2bbacb1f6f841e4f90db50efdace77424c7b0657 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Wed, 8 Apr 2026 15:51:54 -0400 Subject: [PATCH 2/7] Cache project library data --- vsextension/src/extension.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/vsextension/src/extension.ts b/vsextension/src/extension.ts index 85ca05f..59c2ac5 100644 --- a/vsextension/src/extension.ts +++ b/vsextension/src/extension.ts @@ -16,6 +16,7 @@ interface TreeNode { } let cachedInfo: { specs: Record; content: Record; artifact: Record } | null = null; +let cachedLibraryData: Record | null = null; let projectDocument: vscode.TextDocument | undefined = undefined; let projectJsonContent: string = ""; @@ -157,12 +158,12 @@ function buildTreeNodes(projectUrl: string, project: any): TreeNode[] { function getExampleData(): TreeNode { try { const out = execSync("projspec library list --json-out", { stdio: 'pipe', encoding: 'utf-8' }); - const data = JSON.parse(out) as Record; + cachedLibraryData = JSON.parse(out) as Record; const children: TreeNode[] = []; // Data is a dict of project_url -> project_data - for (const [projectUrl, project] of Object.entries(data)) { + for (const [projectUrl, project] of Object.entries(cachedLibraryData)) { const projectChildren = buildTreeNodes(projectUrl, project); // Extract basename from project URL for display @@ -252,8 +253,11 @@ async function handleSelectItem(item: TreeNode) { } try { - const out = execSync("projspec library list --json-out", { stdio: 'pipe', encoding: 'utf-8' }); - const data = JSON.parse(out) as Record; + const data = cachedLibraryData; + if (!data) { + vscode.window.showErrorMessage('Project library data is not loaded yet.'); + return; + } const projectData = data[item.projectUrl]; if (!projectData) { From a825a844dbaac774199514f6cc93ed0fc73aff27 Mon Sep 17 00:00:00 2001 From: Martin Durant Date: Wed, 8 Apr 2026 15:57:23 -0400 Subject: [PATCH 3/7] To context menu --- vsextension/ACTIONS.md | 5 +- vsextension/src/extension.ts | 127 +++++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/vsextension/ACTIONS.md b/vsextension/ACTIONS.md index 09a68fd..396adf4 100644 --- a/vsextension/ACTIONS.md +++ b/vsextension/ACTIONS.md @@ -78,8 +78,9 @@ Nodes are colour-coded: | Element | Action | |---------|--------| -| Click a **project** node | Opens the project folder in a new VS Code window (same behaviour as `projspec.openProject`). | -| Click a **spec / content / artifact** node | Fetches fresh project data (`projspec library list --json-out`), opens `projspec-details:/Project details.json` in the side column, and scrolls to the selected item. | +| Click a **project** node | Selects the node (no other action). Right-click to open the context menu. | +| Right-click a **project** node | Opens a context menu with an **Open** option that opens the project folder in a new VS Code window. | +| Click a **spec / content / artifact** node | Opens `projspec-details:/Project details.json` in the side column and scrolls to the selected item. | | **▶ / ▼ arrow** on any node | Toggles the visibility of that node's children. | | **"Make" button** on an artifact node | Runs `projspec make ""` in a dedicated **projspec** terminal panel. | | **"i" info button** on a spec / content / artifact node | Shows an inline popup with the item's doc string and, when available, a link to the upstream specification documentation. Press **Escape** or click elsewhere to dismiss. | diff --git a/vsextension/src/extension.ts b/vsextension/src/extension.ts index 59c2ac5..de4d48a 100644 --- a/vsextension/src/extension.ts +++ b/vsextension/src/extension.ts @@ -205,25 +205,6 @@ async function handleOpenProject(item: TreeNode) { } } -async function handleSelectProject(item: TreeNode) { - if (!item || !item.infoData || item.infoData.trim() === '') { - return; - } - - const projectUrl = item.infoData; - - // Only handle file:// URLs - if (projectUrl.startsWith('file://')) { - const fsPath = projectUrl.replace('file://', ''); - const uri = vscode.Uri.file(fsPath); - await vscode.commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true }); - } else if (projectUrl.startsWith('gs://')) { - vscode.window.showErrorMessage('Cannot open GCS buckets directly. Clone the repository locally first.'); - } else { - vscode.window.showErrorMessage(`Unsupported project URL scheme: ${projectUrl}`); - } -} - function flattenDeep(data: any): any { if (data === null || typeof data !== 'object') { return data; @@ -343,13 +324,10 @@ export function activate(context: vscode.ExtensionContext) { } } break; - case 'openProject': - await handleOpenProject(message.item); - break; - case 'selectProject': - await handleSelectProject(message.item); - break; - case 'selectItem': + case 'openProject': + await handleOpenProject(message.item); + break; + case 'selectItem': await handleSelectItem(message.item); break; case 'createProject': @@ -954,6 +932,37 @@ function getTreeWebviewContent(treeData: TreeNode, specNames: string[] = [], scr .modal-button-secondary:hover { background: var(--vscode-button-secondaryHoverBackground); } + + /* Context menu styles */ + .context-menu { + position: fixed; + background: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + padding: 4px 0; + z-index: 3000; + display: none; + min-width: 140px; + } + + .context-menu.visible { + display: block; + } + + .context-menu-item { + padding: 6px 16px; + cursor: pointer; + color: var(--vscode-menu-foreground); + font-family: inherit; + font-size: var(--vscode-font-size); + white-space: nowrap; + } + + .context-menu-item:hover { + background: var(--vscode-menu-selectionBackground); + color: var(--vscode-menu-selectionForeground); + } @@ -986,6 +995,11 @@ function getTreeWebviewContent(treeData: TreeNode, specNames: string[] = [], scr + +
+
Open
+
+