Workspace and project explorer
VS Code tree views for Workspace.proj discovery, member navigation, and focused Project.proj context.
Document layout: Feature capability layoutconforms
## Purpose## Requirements
Workspace and project explorer Specification
Purpose
VS Code tree views for Workspace.proj discovery, member navigation, and focused Project.proj context.
Requirements
Requirement: LSP graph: Decision [D-TOOL-VSC-0001]
The Beskid standard SHALL enforce the following migrated contract section. Accepted ADR decisions are binding; uppercase requirement keywords retain their BCP-14 meaning.
Graph via LSP executeCommand.
Stable ID: BSP-REQ-3922DA5525FC
Legacy source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/adr/0001-lsp-backed-graph/content.md
Source SHA-256: 0500b30e8213bd7ee0faba09224d7444bf8f989b085fcc2a27ced743887e233d
Scenario: Conformance exercises Decision
- GIVEN an implementation claims conformance with this capability
- WHEN behavior governed by this contract section is exercised
- THEN every MUST, SHALL, REQUIRED, prohibition, and accepted decision in the section is satisfied
Requirement: Focus without restart: Decision [D-TOOL-VSC-0002]
The Beskid standard SHALL enforce the following migrated contract section. Accepted ADR decisions are binding; uppercase requirement keywords retain their BCP-14 meaning.
didChangeConfiguration for focus; no LSP restart.
Stable ID: BSP-REQ-D96A3F00CFAA
Legacy source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/adr/0002-focus-without-lsp-restart/content.md
Source SHA-256: a62fe67d1043355b36fa9b648571da025b20e9a11bf00331d2715be8ff89326e
Scenario: Conformance exercises Decision
- GIVEN an implementation claims conformance with this capability
- WHEN behavior governed by this contract section is exercised
- THEN every MUST, SHALL, REQUIRED, prohibition, and accepted decision in the section is satisfied
Informative Source Provenance
The records below preserve migration history and are not normative except where text was extracted into a requirement above.
Source Record: Workspace and project explorer
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/content.md
SHA-256: a1c896abb3c19e61d73a4dafd99446ec01e6e653444079fb831e624520a57cee
Migrated source text
<SpecSection title="What this feature specifies" id="what-this-feature-specifies">
Tree views and focus coordination for Beskid workspaces and the active `Project.proj`, backed by LSP executeCommands.
</SpecSection>
<SpecSection title="Implementation anchors" id="implementation-anchors">
- `beskid_vscode/src/workspace/WorkspaceTreeProvider.ts`, `ProjectTreeProvider.ts`, `FocusCoordinator.ts`
- `beskid_vscode/src/workspace/lspProjectApi.ts`
- `compiler/crates/beskid_lsp/src/workspace_scan.rs` — workspace resolution and project graph data
</SpecSection>
<SpecSection title="Contract statement" id="contract-statement">
The **workspace and project explorer** exposes two Beskid activity-bar tree views. **Workspaces** lists every `Workspace.proj` under open VS Code folders and their declared members. **Project** shows the **focused** `Project.proj` (targets, dependencies, source folders) using LSP graph data—never duplicate manifest parsing in TypeScript.
</SpecSection>
<SpecSection title="Inputs and outputs" id="inputs-and-outputs">
| Input | Source |
| --- | --- |
| VS Code workspace folders | Editor |
| Focused project URI | `beskid.project.autoSelectFromEditor`, tree selection, or `beskid.selectProject` |
| LSP executeCommands | `beskid.listWorkspaces`, `beskid.getWorkspaceSummary`, `beskid.getGraph`, `beskid.getProjectDependencies` |
| Output | Consumer |
| --- | --- |
| Focus change | LSP `focusedProjectUri` via init options + `workspace/didChangeConfiguration` |
| Tree labels / commands | VS Code UI |
</SpecSection>
<SpecSection title="State model" id="state-model">
- **Focused project** — canonical `file://` URI of a `Project.proj`; persisted in workspace state as `beskid.focusedProjectUri` (legacy `beskid.selectedProjectUri` alias for one release).
- **Workspace cache** — refreshed on `beskid.refreshWorkspace` and file watchers for `*.proj`, `Project.lock`, `workspace.package.json`.
</SpecSection>
<SpecSection title="Algorithms and flow" id="algorithms-and-flow">
1. Extension activates → start LSP with `initializationOptions.focusedProjectUri`.
2. **Workspaces** view calls `beskid.listWorkspaces` → render members; member click runs `beskid.focusProject`.
3. **Project** view reads focused URI → `beskid.getProjectDependencies` / `beskid.getGraph` metadata for children; full graph exploration uses the Graph Explorer panel.
4. Optional **auto-select**: active editor path → nearest `Project.proj` when `beskid.project.autoSelectFromEditor` is true.
5. Focus change updates configuration **without** full LSP restart unless server binary settings change.
</SpecSection>
<SpecSection title="Decisions" id="decisions">
No open decisions. **`D-TOOL-VSC-0001`** (LSP-backed graph), **`0002`** (focus without LSP restart)—see **`adr/`** and the **ADRs** tab.
</SpecSection>
## Decisions
<!-- spec:generate:adr-index -->
No open decisions. Closed choices are normative ADRs under **`adr/`** (`D-TOOL-VSC-0001` … `D-TOOL-VSC-0002`); use the reader **ADRs** tab for expandable detail.
<!-- /spec:generate:adr-index -->
## Articles
<!-- spec:generate:article-index -->
- [Contracts and edge cases](./articles/contracts-and-edge-cases/)
- [Decisions record (legacy index)](./articles/decisions-record/)
- [Design model](./articles/design-model/)
- [Examples](./articles/examples/)
- [Flow and algorithm](./articles/flow-and-algorithm/)
<!-- /spec:generate:article-index -->
Source Record: LSP graph
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/adr/0001-lsp-backed-graph/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/adr/0001-lsp-backed-graph/content.md
SHA-256: 0500b30e8213bd7ee0faba09224d7444bf8f989b085fcc2a27ced743887e233d
Migrated source text
## Context
Parser drift.
## Decision
Graph via LSP executeCommand.
## Consequences
Thin wrappers.
## Verification anchors
beskid_lsp handlers.
Source Record: Focus without restart
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/adr/0002-focus-without-lsp-restart/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/adr/0002-focus-without-lsp-restart/content.md
SHA-256: a62fe67d1043355b36fa9b648571da025b20e9a11bf00331d2715be8ff89326e
Migrated source text
## Context
UX on focus.
## Decision
didChangeConfiguration for focus; no LSP restart.
## Consequences
Fast focus switch.
## Verification anchors
Session invalidation.
Source Record: Contracts and edge cases
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/contracts-and-edge-cases/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/contracts-and-edge-cases/content.md
SHA-256: d976ce782dd99a3aeb1ae5614ed1d8b24bf13847ef5694d3b6bdf5512b6c82ce
Migrated source text
## Purpose and scope
Normative **LSP `workspace/executeCommand`** payloads and **`focusedProjectUri`** configuration. All URIs **must** be `file://` URIs normalized per LSP URI rules.
## focusedProjectUri
| Rule ID | Rule |
| --- | --- |
| F-01 | On language client start, extension **must** send `initializationOptions.focusedProjectUri` when a project is focused. |
| F-02 | LSP **must** accept deprecated `initializationOptions.selectedProjectUri` as an alias when `focusedProjectUri` is absent. |
| F-03 | On focus change without client restart, extension **must** notify via `workspace/didChangeConfiguration` with settings object `{ "beskid": { "focusedProjectUri": "<uri>" } }` **or** the `beskid.project` configuration section equivalent documented in extension settings. |
| F-04 | When `focusedProjectUri` is set, LSP **should** prefer that manifest for workspace scan ordering and `cached_compilation_context` selection; when unset, behavior **must** match pre-explorer semantics (all roots). |
| F-05 | Extension **must not** restart the language client solely because `focusedProjectUri` changed. |
## Command transport
All commands below use LSP method **`workspace/executeCommand`** with:
- `command`: string identifier
- `arguments`: JSON array; first element **must** be the args object when args are required
Errors **must** use LSP error codes; clients **must** surface `message` in a notification.
## `beskid.refreshWorkspace`
| Field | Value |
| --- | --- |
| **Purpose** | Trigger workspace rescan and diagnostic refresh (existing contract). |
| **Arguments** | `[]` (empty) |
| **Result** | `null` |
Extension **must** invoke after manifest/lock watcher debounce and from command `beskid.refreshWorkspace`.
## `beskid.listWorkspaces`
Discover every **`Workspace.proj`** under current LSP workspace roots (respecting scan skip dirs).
**Arguments** (optional object):
```json
{
"roots": ["file:///path/to/folder"]
}
```
When `roots` is omitted, LSP **must** use initialized workspace folders.
**Result:**
```json
{
"workspaces": [
{
"uri": "file:///…/Workspace.proj",
"name": "corelib",
"members": [
{
"id": "collections",
"uri": "file:///…/packages/collections/Project.proj",
"name": "collections"
}
]
}
]
}
```
| Field | Required | Type | Meaning |
| --- | --- | --- | --- |
| `workspaces` | yes | array | One entry per discovered workspace manifest |
| `workspaces[].uri` | yes | string | `Workspace.proj` file URI |
| `workspaces[].name` | yes | string | Workspace name from manifest |
| `workspaces[].members` | yes | array | Parsed workspace members |
| `members[].id` | yes | string | Member id from `Workspace.proj` |
| `members[].uri` | yes | string | Member `Project.proj` URI |
| `members[].name` | no | string | Display name; defaults to `id` when omitted |
## `beskid.getWorkspaceSummary`
**Arguments:**
```json
{
"workspaceUri": "file:///…/Workspace.proj"
}
```
**Result:**
```json
{
"workspaceUri": "file:///…/Workspace.proj",
"name": "corelib",
"members": [
{
"id": "collections",
"uri": "file:///…/Project.proj",
"name": "collections",
"projectName": "collections"
}
],
"registries": [
{
"alias": "default",
"url": "https://registry.example/"
}
]
}
```
| Field | Required | Meaning |
| --- | --- | --- |
| `registries` | yes | Effective registry URLs from workspace resolution rules (see compiler workspace contracts); used by package panel for default registry base |
| `members[].projectName` | no | Parsed `project.name` when available |
## `beskid.getGraph` (tree navigation)
The Projects tree **must** call `beskid.getGraph` with `kind=projectDeps` and read `metadata.nodes` for Targets and Dependencies sections. See [Graph visualization contracts](/platform-spec/tooling/graph-visualization/contracts-and-edge-cases) for the full payload.
| `metadata.nodes[].kind` | Tree section |
| --- | --- |
| `root` | Targets |
| `path`, `git`, `registry` | Dependencies |
| *(warnings with code `unresolved`)* | Dependencies (unresolved) |
## `beskid.getProjectDependencies`
Merged view of manifest declarations and lock lines for one project.
**Arguments:**
```json
{
"projectUri": "file:///…/Project.proj"
}
```
**Result:**
```json
{
"projectUri": "file:///…/Project.proj",
"declared": [
{
"name": "corelib",
"version": null,
"source": "registry",
"registry": "default",
"descriptor": "corelib"
}
],
"locked": [
{
"name": "corelib",
"resolvedVersion": "1.2.0",
"registry": "default",
"checksum": "sha256:…",
"materializedRoot": "file:///…/.beskid/packages/corelib/1.2.0"
}
],
"unresolved": []
}
```
| Array | Meaning |
| --- | --- |
| `declared` | Entries from `Project.proj` dependency tables |
| `locked` | Entries from `Project.lock` when present; empty when lock missing |
| `unresolved` | Declared deps that could not be resolved or locked |
`locked[].resolvedVersion` **must** match lockfile semantics in [workspace and lock contracts](/platform-spec/tooling/manifests-and-lockfiles/workspace-and-lock-contracts/).
## Explorer-specific rules
| ID | Rule |
| --- | --- |
| E-W01 | `beskid.listWorkspaces` **must** complete in O(roots × files) with scan skip dirs; **must not** build full compile graphs. |
| E-W02 | `beskid.getGraph` (`projectDeps`) **must** route through `beskid_queries::graph_mermaid`; cycles **must** surface as warnings without infinite tree expansion. |
| E-W03 | When `projectUri` is outside workspace roots, LSP **must** return error `InvalidParams` with a clear message. |
| E-W04 | Extension command `beskid.focusProject` **must** set `focusedProjectUri`; `beskid.clearFocus` **must** clear it and notify LSP. |
## Related topics
- [Extension surface contracts](../extension-surface/contracts-and-edge-cases/) — view IDs and cross-feature settings
- [Snapshot and refresh](/platform-spec/tooling/lsp/snapshot-and-refresh-contract/) — interaction with `beskid.refreshWorkspace`
Source Record: Decisions record (legacy index)
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/decisions-record/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/decisions-record/content.md
SHA-256: 23f323c6ba045f5408f8eea1f25dc0834d1f38db824a6244f20b77e0fc732881
Migrated source text
## Purpose
Legacy URL. Normative text is in **`adr/`** and the hub **ADRs** tab.
| adrId | Title |
| --- | --- |
| D-TOOL-VSC-0001 | [LSP-backed graph](./adr/0001-lsp-backed-graph/) |
| D-TOOL-VSC-0002 | [Focus without LSP restart](./adr/0002-focus-without-lsp-restart/) |
Source Record: Design model
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/design-model/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/design-model/content.md
SHA-256: db3d4a5abebeb1dafc0c166fb517690c2aa4383818966ca9924b3929ef6ec72a
Migrated source text
## Purpose and scope
This article defines the **data and UI model** for **Workspaces** (`beskidWorkspaceView`) and **Project** (`beskidProjectView`) tree views, **`FocusedProjectState`**, and how they connect to LSP payloads.
## Focused project state
| Field | Type | Meaning |
| --- | --- | --- |
| `focusedProjectUri` | `string` (file URI) | Canonical `Project.proj` URI driving outline, packages “This project”, and LSP ordering |
| `workspaceUri` | `string` \| null | Parent `Workspace.proj` URI when the focused project is a workspace member |
| `memberId` | `string` \| null | Workspace member id from `Workspace.proj` when applicable |
The extension **must** persist `focusedProjectUri` in `ExtensionContext.workspaceState` under key `beskid.focusedProjectUri` and **must** push the same value to the LSP on change (see [contracts](./contracts-and-edge-cases/#focused-project-uri)).
## Workspaces tree shape
```arch
flowchart TB
subgraph workspacesView [beskidWorkspaceView]
W1[Workspace root]
W1 --> M1[Member: Project.proj]
W1 --> M2[Member: ...]
W1 --> WP[workspace.package.json hint]
end
```
| Node kind | Children source | Default action on click |
| --- | --- | --- |
| **Workspace root** | `beskid.listWorkspaces` → one entry per `Workspace.proj` | Expand/collapse |
| **Member** | `members[]` on workspace entry | Set focus to member `uri`; refresh project/outline/packages views |
| **Registry hint** | Static label when `workspace.package.json` exists beside `Workspace.proj` | Open file in editor |
Icons: workspace root **`root-folder`**; member **`folder-library`**; registry hint **`package`**.
## Project tree shape
Root **must** be the focused `Project.proj` node. Children **must** be built from `beskid.getGraph` (`kind=projectDeps`) `metadata.nodes` and **must** include:
| Child section | Source | Notes |
| --- | --- | --- |
| **Targets** | `metadata.nodes[]` where `kind === "root"` | One node for the root project |
| **Dependencies** | `metadata.nodes[]` where `kind` is `path`, `git`, or `registry` | Opens manifest URI when present; warning icon when `unresolved` |
| **Source folders** | Workspace file discovery under project root | Opens folder on click |
| **Unresolved** | `warnings[]` with code `unresolved` plus nodes with `unresolved: true` | Warning icon; label from warning message or node label |
## Multi-root labeling
When `vscode.workspace.workspaceFolders.length > 1`, any label that could collide across folders **must** use the format:
`{workspaceFolderName} / {localLabel}`
where `workspaceFolderName` is the VS Code folder name, not the on-disk path.
## Auto-select from editor
When setting `beskid.project.autoSelectFromEditor` is true (default **true**), the extension **must** on `onDidChangeActiveTextEditor`:
1. Map the editor file path to a containing directory.
2. Walk upward to find `Project.proj` (same discovery rules as CLI/LSP `discover_project_file`).
3. If the project is a workspace member, set `workspaceUri` and `memberId` from `beskid.getWorkspaceSummary` when needed.
4. Update `focusedProjectUri` without user confirmation unless the new project differs from focus and `beskid.project.confirmAutoFocus` is true (optional setting; default **false**).
## File watchers
The extension **must** register watchers (debounced, ≥300ms) for:
- `**/*.{bd,proj}`
- `**/Project.lock`
- `**/workspace.package.json`
On fire: debounced `beskid.refreshWorkspace`, then refresh both tree providers.
## Canonical references
- Parent hub: [Workspace and project explorer](../)
- LSP JSON contracts: [Contracts and edge cases](./contracts-and-edge-cases/)
- Manifest semantics: [Workspace and lock contracts](/platform-spec/tooling/manifests-and-lockfiles/workspace-and-lock-contracts/)
Source Record: Examples
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/examples/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/examples/content.md
SHA-256: ee376b21a6c1663d7885bdd16ec36e40ea7b7a3290249328dfc4136d281ae7de
Migrated source text
## Purpose and scope
Representative explorer behavior for the **corelib** workspace and a **single-app** folder opened without a workspace manifest.
## Corelib workspace
**Setup:** Open folder `compiler/corelib/` containing `Workspace.proj` named `corelib` with multiple members under `packages/`.
**Expected Workspaces view:**
- One workspace root labeled `corelib` (or `corelib / corelib` when multi-root).
- Member children for each `Workspace.proj` entry (for example `collections`, `query`, `system`).
- Optional node `workspace.package.json` when present at workspace root.
**User action:** Click member `collections`.
**Expected:**
- `focusedProjectUri` → `file://…/packages/collections/Project.proj`
- Project view root shows `collections` targets and dependencies from `beskid.getGraph` metadata
- `beskid.getProjectDependencies` returns locked versions after `beskid lock` has been run in that workspace
**Smoke checklist:**
1. All members listed match `Workspace.proj` without manual refresh.
2. Editing `Project.lock` triggers debounced refresh; locked versions update in Project tree tooltips.
3. `beskid.refreshWorkspace` from palette completes without LSP restart.
## Single-application project
**Setup:** Open folder containing only `Project.proj` (no parent `Workspace.proj`), e.g. a console app template output.
**Expected Workspaces view:**
- Empty state: “No Beskid workspaces found” (or equivalent localized string).
- User **may** still focus via Project picker or auto-select.
**Expected Project view:**
- Root = the lone `Project.proj`
- Dependencies from `getGraph` metadata; unresolved registry deps show warning icon until `beskid fetch`
**Auto-select:** Opening `Src/Main.bd` sets focus to the containing `Project.proj` when `beskid.project.autoSelectFromEditor` is true.
## Multi-root VS Code workspace
**Setup:** VS Code workspace with folders `corelib/` and `my-app/` side by side.
**Expected labeling:**
- `corelib / collections` vs `my-app / my-app` member labels to disambiguate.
**Focus:** Switching editors between folders updates focus per auto-select rules without mixing dependency graphs.
## Related topics
- [Extension surface examples](../extension-surface/examples/)
- [Workspace and lock contracts](/platform-spec/tooling/manifests-and-lockfiles/workspace-and-lock-contracts/)
Source Record: Flow and algorithm
Authority: informative provenance
Legacy path: /platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/flow-and-algorithm/
Source: site/spec-content/platform-spec/tooling/vscode-extension/workspace-project-explorer/articles/flow-and-algorithm/content.md
SHA-256: 6b07701690d98914fd48137a23b02e885521e2c4691386c2905797c324896235
Migrated source text
## Purpose and scope
End-to-end sequences from extension activation through tree population and focus synchronization.
## Activation and LSP start
```arch
sequenceDiagram
participant VS as VS Code
participant Ext as Extension
participant LSP as beskid_lsp
VS->>Ext: activate
Ext->>Ext: read workspaceState.focusedProjectUri
Ext->>LSP: start LanguageClient (initializationOptions.focusedProjectUri)
LSP-->>Ext: initialized
Ext->>LSP: executeCommand beskid.listWorkspaces
LSP-->>Ext: workspaces[]
Ext->>Ext: populate WorkspaceTreeProvider
alt focusedProjectUri set
Ext->>LSP: executeCommand beskid.getGraph (projectDeps)
LSP-->>Ext: GraphPayload metadata.nodes
Ext->>Ext: populate ProjectTreeProvider
end
```
1. Extension **must** start the language client before populating trees (commands are server-backed).
2. If no focus is stored, extension **may** defer Project view population until auto-select or user picks a member.
## Member selection flow
1. User activates a **member** node in `beskidWorkspaceView`.
2. Extension sets `focusedProjectUri = member.uri`.
3. Extension persists workspace state and sends configuration notification to LSP (no restart).
4. Extension refreshes project tree sections, outline, and package panel (parallel `getGraph` + `getProjectDependencies`).
5. Status bar **may** show project name briefly (non-blocking).
## Auto-select from editor
1. `onDidChangeActiveTextEditor` fires with document URI.
2. If `beskid.project.autoSelectFromEditor` is false, stop.
3. Resolve `Project.proj` for editor path; if none, stop.
4. If resolved URI equals current focus, stop.
5. Set focus and run member selection steps 3–4.
## Debounced workspace refresh
1. File watcher fires (manifest, lock, `.bd`, or `workspace.package.json`).
2. Extension debounces (≥300ms coalescing).
3. `executeCommand('beskid.refreshWorkspace')`.
4. Re-fetch `beskid.listWorkspaces`; if focus still valid, re-fetch graph and dependencies.
5. Fire `onDidChangeTreeData` on both providers.
## Reveal in tree
| Command | Behavior |
| --- | --- |
| `beskid.revealInWorkspaceTree` | Expand workspace containing focus; reveal member or workspace root |
| `beskid.revealInProjectTree` | Reveal focused `Project.proj` root and scroll to dependency/target node when `arg` node id provided |
Both **must** be available from editor title/context when a Beskid file is active.
## Verification notes
- Flow tests **should** mock LSP executeCommand and assert ordering: `listWorkspaces` before `getGraph` on cold start.
- Changing `focusedProjectUri` **must not** appear in logs as a full language-client restart.
## Related topics
- [Design model](./design-model/)
- [Examples](./examples/)