From 92b4f454ebb7e13c3b4955af1330184ad29436a4 Mon Sep 17 00:00:00 2001 From: Ashmit Biswas Date: Sun, 31 May 2026 07:40:52 +0530 Subject: [PATCH] chore: move VSCode extension to ashmitb95/canopy-dashboard The extension lives at https://github.com/ashmitb95/canopy-dashboard now. Git history for the extension's 26 commits was preserved via `git filter-repo --subdirectory-filter vscode-extension`. - Deletes vscode-extension/ (62 files, ~22.6k lines) - Drops the extension-build job from .github/workflows/ci.yml (the extension has its own CI on the new repo) - README: adds an inline pointer to the new repo next to the existing Marketplace link The extension and the CLI/MCP are at different release cadences and toolchains (TS+esbuild vs Python+hatchling). The split lets each ship on its own clock and unblocks the dashboard's perf rework (reading .canopy/state/*.json directly instead of round-tripping through MCP). --- .github/workflows/ci.yml | 35 - README.md | 2 +- vscode-extension/.gitignore | 5 - vscode-extension/.vscodeignore | 14 - vscode-extension/CHANGELOG.md | 128 - vscode-extension/LICENSE | 21 - vscode-extension/README.md | 82 - vscode-extension/esbuild.config.mjs | 71 - vscode-extension/jest.config.ts | 22 - vscode-extension/media/canopy-icon.png | Bin 18892 -> 0 bytes vscode-extension/media/canopy-icon.svg | 588 -- .../media/screenshots/dashboard-feature.png | Bin 487829 -> 0 bytes .../media/screenshots/dashboard-global.png | Bin 429711 -> 0 bytes vscode-extension/package-lock.json | 9054 ----------------- vscode-extension/package.json | 297 - vscode-extension/src/__mocks__/vscode.ts | 98 - vscode-extension/src/canopyCli.test.ts | 196 - vscode-extension/src/canopyCli.ts | 862 -- vscode-extension/src/canopyClient.ts | 704 -- vscode-extension/src/cliResolver.test.ts | 122 - vscode-extension/src/cliResolver.ts | 134 - .../src/commands/createFeature.ts | 161 - .../src/commands/createFeatureFromIssue.ts | 85 - .../src/commands/installBackend.ts | 218 - vscode-extension/src/commands/setupWizard.ts | 103 - vscode-extension/src/extension.ts | 828 -- vscode-extension/src/mcpResolver.ts | 130 - vscode-extension/src/stateReader.test.ts | 273 - vscode-extension/src/stateReader.ts | 287 - vscode-extension/src/statusBar.ts | 96 - vscode-extension/src/types.ts | 192 - .../src/views/GlobalDashboardPanel.ts | 1020 -- .../src/views/canopyTreeProvider.ts | 269 - vscode-extension/src/views/themeShim.ts | 113 - vscode-extension/src/watchers.ts | 79 - vscode-extension/src/webview/cockpitPanel.ts | 395 - .../src/webview/components/branchLedger.ts | 72 - .../src/webview/components/bridge.ts | 72 - .../src/webview/components/capReachedModal.ts | 112 - .../src/webview/components/focusTile.ts | 139 - .../src/webview/components/newFeatureForm.ts | 174 - .../src/webview/components/styles.ts | 691 -- .../src/webview/components/triageFeed.ts | 91 - .../src/webview/components/util.ts | 40 - .../src/webview/components/worktreeRow.ts | 71 - .../src/webview/dashboardPanel.ts | 904 -- .../webview/global-dashboard/Dashboard.tsx | 137 - .../webview/global-dashboard/FeatureView.tsx | 972 -- .../webview/global-dashboard/GlobalView.tsx | 649 -- .../webview/global-dashboard/Skeletons.tsx | 38 - .../src/webview/global-dashboard/diff.ts | 118 - .../src/webview/global-dashboard/index.tsx | 17 - .../src/webview/global-dashboard/protocol.ts | 129 - .../src/webview/global-dashboard/vscode.ts | 30 - .../src/webview/newFeaturePanel.ts | 322 - .../src/webview/shared/pastel.css | 801 -- vscode-extension/src/webview/themes/index.ts | 42 - .../src/webview/themes/minimal.ts | 62 - vscode-extension/src/webview/themes/navy.ts | 60 - vscode-extension/src/webview/themes/render.ts | 96 - vscode-extension/src/webview/themes/types.ts | 104 - vscode-extension/tsconfig.json | 20 - 62 files changed, 1 insertion(+), 22646 deletions(-) delete mode 100644 vscode-extension/.gitignore delete mode 100644 vscode-extension/.vscodeignore delete mode 100644 vscode-extension/CHANGELOG.md delete mode 100644 vscode-extension/LICENSE delete mode 100644 vscode-extension/README.md delete mode 100644 vscode-extension/esbuild.config.mjs delete mode 100644 vscode-extension/jest.config.ts delete mode 100644 vscode-extension/media/canopy-icon.png delete mode 100644 vscode-extension/media/canopy-icon.svg delete mode 100644 vscode-extension/media/screenshots/dashboard-feature.png delete mode 100644 vscode-extension/media/screenshots/dashboard-global.png delete mode 100644 vscode-extension/package-lock.json delete mode 100644 vscode-extension/package.json delete mode 100644 vscode-extension/src/__mocks__/vscode.ts delete mode 100644 vscode-extension/src/canopyCli.test.ts delete mode 100644 vscode-extension/src/canopyCli.ts delete mode 100644 vscode-extension/src/canopyClient.ts delete mode 100644 vscode-extension/src/cliResolver.test.ts delete mode 100644 vscode-extension/src/cliResolver.ts delete mode 100644 vscode-extension/src/commands/createFeature.ts delete mode 100644 vscode-extension/src/commands/createFeatureFromIssue.ts delete mode 100644 vscode-extension/src/commands/installBackend.ts delete mode 100644 vscode-extension/src/commands/setupWizard.ts delete mode 100644 vscode-extension/src/extension.ts delete mode 100644 vscode-extension/src/mcpResolver.ts delete mode 100644 vscode-extension/src/stateReader.test.ts delete mode 100644 vscode-extension/src/stateReader.ts delete mode 100644 vscode-extension/src/statusBar.ts delete mode 100644 vscode-extension/src/types.ts delete mode 100644 vscode-extension/src/views/GlobalDashboardPanel.ts delete mode 100644 vscode-extension/src/views/canopyTreeProvider.ts delete mode 100644 vscode-extension/src/views/themeShim.ts delete mode 100644 vscode-extension/src/watchers.ts delete mode 100644 vscode-extension/src/webview/cockpitPanel.ts delete mode 100644 vscode-extension/src/webview/components/branchLedger.ts delete mode 100644 vscode-extension/src/webview/components/bridge.ts delete mode 100644 vscode-extension/src/webview/components/capReachedModal.ts delete mode 100644 vscode-extension/src/webview/components/focusTile.ts delete mode 100644 vscode-extension/src/webview/components/newFeatureForm.ts delete mode 100644 vscode-extension/src/webview/components/styles.ts delete mode 100644 vscode-extension/src/webview/components/triageFeed.ts delete mode 100644 vscode-extension/src/webview/components/util.ts delete mode 100644 vscode-extension/src/webview/components/worktreeRow.ts delete mode 100644 vscode-extension/src/webview/dashboardPanel.ts delete mode 100644 vscode-extension/src/webview/global-dashboard/Dashboard.tsx delete mode 100644 vscode-extension/src/webview/global-dashboard/FeatureView.tsx delete mode 100644 vscode-extension/src/webview/global-dashboard/GlobalView.tsx delete mode 100644 vscode-extension/src/webview/global-dashboard/Skeletons.tsx delete mode 100644 vscode-extension/src/webview/global-dashboard/diff.ts delete mode 100644 vscode-extension/src/webview/global-dashboard/index.tsx delete mode 100644 vscode-extension/src/webview/global-dashboard/protocol.ts delete mode 100644 vscode-extension/src/webview/global-dashboard/vscode.ts delete mode 100644 vscode-extension/src/webview/newFeaturePanel.ts delete mode 100644 vscode-extension/src/webview/shared/pastel.css delete mode 100644 vscode-extension/src/webview/themes/index.ts delete mode 100644 vscode-extension/src/webview/themes/minimal.ts delete mode 100644 vscode-extension/src/webview/themes/navy.ts delete mode 100644 vscode-extension/src/webview/themes/render.ts delete mode 100644 vscode-extension/src/webview/themes/types.ts delete mode 100644 vscode-extension/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5699827..a1bc7fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,38 +39,3 @@ jobs: - name: Run pytest run: pytest tests/ -v --maxfail=5 - - extension-build: - name: VSCode extension build - runs-on: ubuntu-latest - defaults: - run: - working-directory: vscode-extension - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: npm - cache-dependency-path: vscode-extension/package-lock.json - - - name: Install dependencies - run: npm ci - - - name: Type-check - run: npx tsc --noEmit - - - name: Bundle with esbuild - run: npm run build - - - name: Package VSIX (sanity check) - run: npx vsce package --allow-missing-repository - - - name: Upload VSIX artifact - uses: actions/upload-artifact@v4 - with: - name: canopy-vsix - path: vscode-extension/*.vsix - if-no-files-found: error - retention-days: 14 diff --git a/README.md b/README.md index 0fe289e..e7b4dbd 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ canopy doctor # diagnose drift / staleness canopy state

-The CLI and MCP server are thin wrappers over the same actions — `canopy state X` and `mcp__canopy__feature_state(feature='X')` return identical bytes. There's also a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=SingularityInc.canopy) reading the same state the agent reads. +The CLI and MCP server are thin wrappers over the same actions — `canopy state X` and `mcp__canopy__feature_state(feature='X')` return identical bytes. There's also a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=SingularityInc.canopy) (source at [`ashmitb95/canopy-dashboard`](https://github.com/ashmitb95/canopy-dashboard)) reading the same state the agent reads. Full CLI reference: [docs/commands.md](docs/commands.md). diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore deleted file mode 100644 index 261183f..0000000 --- a/vscode-extension/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -dist/ -out/ -*.vsix -.vscode-test/ diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore deleted file mode 100644 index 9c84562..0000000 --- a/vscode-extension/.vscodeignore +++ /dev/null @@ -1,14 +0,0 @@ -.vscode/** -.vscode-test/** -src/** -out/** -node_modules/** -canopy-*.vsix -jest.config.ts -.gitignore -.yarnrc -esbuild.config.mjs -tsconfig.json -**/*.map -**/*.ts -**/*.test.* diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md deleted file mode 100644 index edf0058..0000000 --- a/vscode-extension/CHANGELOG.md +++ /dev/null @@ -1,128 +0,0 @@ -# Change Log - -## 0.7.1 - -- README points at the new dashboard screenshots via absolute - raw.githubusercontent.com URLs so the marketplace listing renders - them. Stale pre-rebuild screenshots removed. - -## 0.7.0 - -Pastel dashboard rebuild + the action surface for the new backend commands (`ship` / `draft-replies` / `conflicts` / `worktree-bootstrap` / `pr-checks`). - -### Dashboard - -- **One panel, two modes.** New `Canopy: Open Dashboard` (and activity-bar tree title bar) opens a React webview that mode-shifts between **global** (canonical / warm / cold lanes + triage rail) and **feature** (issue body + per-repo cards + threads + diff stack + 4-section action drawer). Click any feature in global to drill in; "Back to Global" returns. Auto-opens on first activation per VS Code user. -- **Theme system.** `canopy.dashboard.theme` offers `minimal` (default — near-monochrome dark), `pastel` (soft blue-grey cream), `navy` (legacy). Live-updates on change — no reload. Tokens reused from `webview/themes/.ts`; `themeShim.ts` maps them onto the shared pastel CSS contract so swapping a theme is a `:root` override, not a CSS rewrite. -- **Progressive cache + per-section streaming.** Module-level `FEATURE_CACHE` + `GLOBAL_CACHE` survive panel disposal. Each fetch (`feature_state`, `feature_status`, `feature_diff`, `review_comments`, `bot_status`, `issueGet`) writes its slot and posts its own `patch` message — the slowest sibling no longer blocks the focus card. Re-opens are instant. File-watchers trigger silent revalidation in place; write actions wipe and refetch with skeleton flash. -- **Inline shape-of-data skeletons.** Topbar / breadcrumb / section heads / sidebar render real data immediately. Shimmers appear inline only where async data is in flight, sized + shaped to the slot they'll fill (issue body paragraphs inside `.issue-body`, branch-name shimmer in repo cards, thread-card skeletons grouped by repo, diff-block skeletons with monospace body lines). - -### Action surfaces - -- **Ship feature** — `canopy ship` capstone in the Commit & push rail. Push + open/update one PR per repo with cross-repo body links. -- **Draft replies for addressed threads** — `canopy draft-replies` quick-pick per template, clipboard-on-select. -- **Cross-feature conflicts** — `canopy conflicts` from the Checks rail with toast summary. -- **Bootstrap worktrees** — `canopy worktree-bootstrap` for env-files + install + `.code-workspace`. -- **Mark addressed** on bot threads — `canopy commit --address --amend` keeps the bot-resolution log in sync. -- **CI chips on repo cards** — passing / pending / failing from `feature_state.repos[*].pr.ci_status`. -- **CTA buttons in focus card** — `next_actions` from `feature_state` map to webview messages (preflight / commit / push / stash / open-feature / refresh). - -### Transport + sidebar - -- **CLI transport.** `canopyCli.ts` is the dashboard's data plane: direct subprocess to `canopy` with TTL cache, login-shell PATH resolution, and a `cliResolver` that finds the binary across pipx / brew / venv. Typed wrappers added for `ship`, `draftReplies`, `conflicts`, `worktreeBootstrap`, `prChecks`, `switchFeature`, `setConfig`. -- **Sidebar trimmed to three sections.** ACTIVE (canonical, expandable per-repo), LAUNCHERS (Open Dashboard, New Feature from issue, Open canopy.toml), ISSUES (provider inbox). The legacy FEATURES section moves into the dashboard. -- **Per-repo target branch.** Feature view's repo cards render `feature/` from canopy.toml's per-repo `target_branch` augment. -- **Preflight chip.** Repo cards in feature view show passed / stale / failed against `.canopy/state/preflight.json`. -- **Address-in-agent plumbing.** Review threads copy context to clipboard + open Claude Code (terminal fallback if extension missing). - -### Build - -- `.vscodeignore` drops `node_modules/` (esbuild bundles everything). Vsix size 4.5 MB → 460 KB. -- esbuild copies `pastel.css` to `dist/webview/` so it ships in the packaged extension. -- React 19 + react-dom added; tsconfig gets `jsx: "react-jsx"` + `lib: [..., "DOM"]`. - -## 0.4.0 - -- **Single sidebar tree.** The five separate views (Features, Worktrees, Changes, Review Readiness, Linear Issues) are collapsed into one unified `Canopy` tree with three sections: ACTIVE (canonical feature, expandable to per-repo rows with `↑N · M dirty`), FEATURES (other lanes with repo count + Linear ID), and LINEAR INBOX (todo issues, collapsed by default). -- Right-click menus and title-bar buttons (Cockpit, New Feature, Refresh, Reinit) all rebind to the new view. -- Bundle dropped ~7 KB from removing the per-domain providers. - -## 0.3.3 - -- **Progressive dashboard rendering with per-feature cache.** Dashboards used to leave the panel blank for several seconds while five backend calls completed serially, and switching features re-fetched everything every time. Now sections render section-by-section as data arrives, and per-feature caches (race-protected) make repeat opens instant. - -## 0.3.2 - -- **Self-contained vsix.** Bundles `@modelcontextprotocol/sdk` so a fresh install no longer throws `Cannot find module '@modelcontextprotocol/sdk/client/index.js'` at activation. Phase G's stub providers + diagnostic commands live inside `bootstrap()`, so the require-time failure was bricking the extension on first run. - -## 0.3.1 - -- **Workspace-scope cockpit panel** (`Canopy: Open Cockpit`). New theme-pluggable webview that summarizes all features, the canonical-slot model state, and triage feed. Coexists with the per-feature dashboard. -- **New-feature panel** (`Canopy: Spin up a new feature from Linear`). Linear issue picker → repo selector → slot chooser (canonical vs. worktree). Replaces the bare quick-pick. -- **State-file watchers.** `.canopy/state/{active_feature,heads,preflight}.json` changes drive auto-refresh, so a `canopy switch` from the CLI surfaces in the dashboard immediately. -- **Theme system** (`canopy.dashboard.theme`): `navy` (default — deep navy with signal accents) and `minimal` (near-monochrome). -- **Self-healing activation.** Diagnostic commands (`Show Log`, `Retry Connect`, `Install Backend`) are now registered before the MCP probe, so a missing `canopy-mcp` no longer leaves the user with "no data provider registered" + zero canopy commands in the palette. -- **Switch fix.** Right-click "Switch to Feature" was calling the deleted `feature_switch` MCP tool; now uses the canonical-slot `switch` action with proper blocker handling. -- BlockerError plumbing through real CTAs; cap-reached modal for worktree-budget overflow; worktree row + branch ledger + triage feed in cockpit. - -## 0.2.5 - -- **Linear Issues sidebar view.** Lists open Todo / In Progress issues from your Linear workspace; right-click → "Start Feature from Linear Issue" wires the Linear ID into the new feature. -- `canopy.createFeatureFromIssue` command and `linear-mcp-server` integration. -- Dashboard upgrades: richer per-repo state, GitHub PR context, status pills. -- MCP client gains multi-source merge for `feature_list` (explicit + worktree-discovered + workspace_status active features). - -## 0.1.9 - -- Trimmed `installBackend` command — relies on the resolver in 0.1.3 instead of duplicating discovery logic. - -## 0.1.7 - -- README split — top-level README is now a brief intro; long-form docs moved to `docs/architecture.md`, `docs/commands.md`, `docs/mcp.md`, `docs/workspace.md`. -- New `setupWizard` command flow for first-time `canopy init`. - -## 0.1.6 - -- **Fixed dashboard crash (`i.map is not a function`)** — list-returning MCP tools (`feature_list`, `log`, `linear_my_issues`, etc.) now come through as arrays again. FastMCP wraps non-dict returns in `{ "result": }` to satisfy the spec's object-only `structuredContent`; the client now unwraps that convention before handing the value to callers. -- Features view and Worktrees view light up together after a reinit. - -## 0.1.5 - -- **Fixed post-reinit crash** — the MCP client now reads `structuredContent` first (MCP 2025-06 spec) before falling back to text blocks. This prevents `{}` responses that caused *"Cannot read properties of undefined (reading 'length')"* after `Force Reinit Workspace`. -- Hardened every tree provider, the reinit toast, and the status bar against any missing or malformed fields from the MCP — each failure now logs a stack trace to the Canopy output channel instead of silently emptying the view. -- `refresh()` no longer throws synchronously when the status bar can't compute ahead/behind; errors are caught per-slice with traces. - -## 0.1.4 - -- **Force Reinit Workspace** — `…` menu on the Features view (or the command palette) re-runs Canopy's repo/worktree discovery and overwrites `canopy.toml`. Useful after adding/removing repos or worktrees outside Canopy. -- **Preview Reinit (dry run)** — opens the would-be new `canopy.toml` in an editor tab without writing. Runs through the same modal confirmation as the real reinit. -- Backed by a new `workspace_reinit` MCP tool (Canopy now exposes 30 tools). - -## 0.1.3 - -- **Features view now merges three data sources** — `features.json` (explicit features), `.canopy/worktrees/*` on disk (implicit worktrees), and `workspace_status.active_features` (multi-repo branches). Worktrees created outside `canopy feature create` (e.g. by an older Canopy or plain `git worktree add`) now appear in Features instead of being invisible. -- Resolver now scans `~/projects/*`, `~/src/*`, `~/code/*`, `~/Developer/*`, `~/dev/*`, `~/workspace/*` for any sibling checkout with a `.venv/bin/canopy-mcp`. Finds existing Canopy installs automatically — no more false *"can't start canopy-mcp"* when Canopy is already installed in a neighbouring project's venv. -- Last-ditch resolver fallback: asks system `python3` whether it can import `canopy`, and derives the `canopy-mcp` entry point from `sys.executable`. -- Also scans the extension's managed venv (`~/.canopy-vscode/venv/bin/canopy-mcp`) so post-install reconnects work without needing the configured setting. - -## 0.1.2 - -- **Install Backend command**: one-click installer creates a managed venv at `~/.canopy-vscode/venv`, installs `canopy` from PyPI / a local checkout / a git URL, and points the extension at the new `canopy-mcp`. Triggered from the sidebar's *Install Canopy for me* button or from the error toast. -- Retry Connect re-reads the setting so a fresh install takes effect immediately. -- New `canopy.pythonPath` setting to pin the python3 used by the installer. - -## 0.1.1 - -- Auto-resolve `canopy-mcp` via the user's login shell and common venv locations, so GUI-launched VSCode windows work without pre-setting PATH. -- Rewrote the sidebar welcome so it stops falsely saying "No Canopy workspace detected" when the real problem is a missing backend binary. -- Collapsed per-provider error toasts into a single up-front activation error with **Open Settings / Show Log** actions. -- New commands: `Canopy: Retry Connect`, `Canopy: Show Log`. - -## 0.1.0 — Initial release - -- Activity-bar entry with four sidebar sections: Features, Worktrees, Changes, Review Readiness -- Per-feature dashboard webview with branch state, Linear/GitHub status, recent commits, and overlap warnings -- "Create Feature" quick pick with Linear-issue autocomplete -- Status-bar items for active feature, repo count, and aggregate ahead/behind -- File watching on `.canopy/features.json` and worktree HEADs for live refresh -- All data flows through the existing `canopy-mcp` server over stdio diff --git a/vscode-extension/LICENSE b/vscode-extension/LICENSE deleted file mode 100644 index a2f670b..0000000 --- a/vscode-extension/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Ashmit Biswas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vscode-extension/README.md b/vscode-extension/README.md deleted file mode 100644 index e284269..0000000 --- a/vscode-extension/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Canopy — Multi-Repo Worktree Manager - -**A switch-first dashboard for multi-repo workspaces. One canonical feature in focus, the rest hibernating in worktrees, every PR / preflight / draft reply one click away.** - -Canopy coordinates real Git branches and worktrees across multiple repositories — no proprietary abstractions, no virtual branches. This extension is a VSCode-native surface on top of the [Canopy CLI](https://github.com/ashmitb95/canopy); the same JSON contract the CLI ships. What the dashboard shows is exactly what `canopy state` / `canopy triage` / `canopy feature status` would show in a terminal. - -![Global dashboard](https://raw.githubusercontent.com/ashmitb95/canopy/main/vscode-extension/media/screenshots/dashboard-global.png) - ---- - -## What it does - -### One panel, two modes — global and per-feature - -Global mode is the focus board. Three vertical lanes mirror Canopy's canonical-slot model: the **canonical** feature (what's currently in your main checkout), **warm** features (linked worktrees, instantly switchable), and **cold** features (branch-only, switching creates a worktree on demand). A right-hand triage rail surfaces the priority list across every PR — changes-requested, bot reviews, review-required, approved. - -![Feature dashboard](https://raw.githubusercontent.com/ashmitb95/canopy/main/vscode-extension/media/screenshots/dashboard-feature.png) - -Click any feature to drill in. The feature view stacks: linked Linear/GitHub issue body → per-repo cards with branch + target + dirty + PR + actionable threads + CI chips → temporally classified review threads grouped by repo (each with **Address in agent** / **Reply** / **Mark addressed** for bot threads) → unified-diff stack with click-to-open in VSCode's native diff viewer. The right rail is the action drawer: Priority list of actionable threads, Checks (preflight + cross-feature conflicts + worktree bootstrap), Commit & push (per-repo Stage / Commit / Push / Open PR + the **Ship feature** capstone + draft-replies generator), State (stash / pop / back to global), Open (IDE / issue / PRs). - -### `switch` is the wedge - -Clicking a warm feature's **Switch into main** evacuates the current canonical to a warm worktree (instant to switch back) and promotes the clicked feature into the main checkout. Cold features auto-create a worktree; if you're at cap, the LRU warm worktree gets evicted (the button label tells you which one). The "Raise cap" affordance is permanent in the Worktrees section header — no modals, no interruptions. - -### Progressive cache, every section streams - -Each fetch (`feature_state`, `feature_status`, `feature_diff`, `review_comments`, `bot_status`, issue body) writes to a module-level cache and posts its own UI patch as soon as it returns. Re-clicking a feature you've seen before is essentially instant — the cache survives panel disposal. File-watchers on `.canopy/state/*.json` and `.canopy/features.json` revalidate silently, sections updating in place without skeleton flash. Skeletons appear only inline where async data is genuinely missing, not as full-page silhouettes. - -### Sidebar trimmed to launchers + active + issues - -Three sections: **ACTIVE** (canonical feature, expandable per-repo), **LAUNCHERS** (Open Dashboard, New Feature from Issue, Open canopy.toml), **ISSUES** (Linear / GitHub Issues inbox). The dashboard owns the rest. - ---- - -## Install - -1. Install the extension from the VSCode Marketplace (or `code --install-extension canopy-x.y.z.vsix` for a local build). -2. Open a folder containing a `canopy.toml`. The first time you click the activity-bar canopy icon the dashboard auto-opens; this is per-user, not per-workspace. -3. If the sidebar offers **Install Canopy for me**, click it — the extension sets up a managed venv at `~/.canopy-vscode/venv` and installs the Canopy backend. Otherwise `pipx install canopy` from a terminal works. - -The extension shells out to two binaries: `canopy` (the CLI) for the dashboard, and `canopy-mcp` (the MCP server) for the legacy panels + status bar. Both are auto-discovered via login-shell PATH; absolute paths in settings override. - ---- - -## Settings - -| Setting | Default | What it does | -| --- | --- | --- | -| `canopy.cliPath` | `canopy` | Path to the `canopy` CLI used by the dashboard. Set absolute if auto-detection fails. | -| `canopy.canopyMcpPath` | `canopy-mcp` | Path to the `canopy-mcp` executable used by the sidebar + status bar. | -| `canopy.dashboard.theme` | `minimal` | `minimal` (near-monochrome dark, default), `pastel` (soft blue-grey cream surfaces), `navy` (legacy). Live-updates on change. | -| `canopy.refreshIntervalSeconds` | `30` | How often to poll Canopy for updated sidebar state. `0` disables periodic refresh. | -| `canopy.pythonPath` | *(empty)* | Optional Python 3.10+ binary used by *Install Backend*. Leave empty to auto-detect. | - ---- - -## Commands (palette via `Cmd-Shift-P`) - -| Command | What it does | -| --- | --- | -| `Canopy: Open Dashboard` | Opens the new pastel dashboard. The activity-bar tree's title bar has the same shortcut. | -| `Canopy: Switch to Feature` | Quick-pick feature → promote to canonical slot. | -| `Canopy: Run Preflight` | Stages all repos in the canonical feature + runs hooks (no commit). | -| `Canopy: Sync All Repos` | `git pull --rebase` per repo. | -| `Canopy: Spin up a new feature from Linear` | Picks an open Linear / GH issue → creates branches + worktrees. | -| `Canopy: Mark Feature Done` | Archives a feature: removes worktrees, deletes branches. | -| `Canopy: Open Feature Worktrees in New Window` | One VSCode window per repo worktree for the chosen feature. | -| `Canopy: Run Doctor` | 17-category diagnostic + `--fix` for auto-repairable. | -| `Canopy: Force Reinit Workspace` | Rescan repos + regenerate canopy.toml. | -| `Canopy: Connect Linear` | Drops a Linear MCP entry into `.canopy/mcps.json`. | - -The action drawer in feature view exposes: **Run preflight**, **Cross-feature conflicts**, **Bootstrap worktrees** (env files + `install_cmd` + `.code-workspace`), per-repo **Stage / Commit / Push / Open PR**, **Ship feature** (commits + push + opens/updates one PR per repo with cross-repo body links), **Draft replies for addressed threads**, **Stash / Pop**, **Back to global**. - ---- - -## Links - -- **[Canopy on GitHub](https://github.com/ashmitb95/canopy)** — CLI, MCP server, full architecture docs -- **[Changelog](CHANGELOG.md)** -- **[Report a bug](https://github.com/ashmitb95/canopy/issues)** - -MIT licensed. diff --git a/vscode-extension/esbuild.config.mjs b/vscode-extension/esbuild.config.mjs deleted file mode 100644 index 357daa9..0000000 --- a/vscode-extension/esbuild.config.mjs +++ /dev/null @@ -1,71 +0,0 @@ -import * as esbuild from "esbuild"; -import { copyFileSync, mkdirSync } from "node:fs"; - -const watch = process.argv.includes("--watch"); - -/** - * Copy assets that the bundle needs at runtime but esbuild doesn't - * inline. Today it's just `pastel.css` — the dashboard controller - * reads it off disk and inlines it into the panel's HTML so editing - * the CSS doesn't require a webview rebuild. The file MUST live under - * `dist/` because `.vscodeignore` excludes the whole `src/` tree from - * the packaged .vsix. - */ -function copyAssets() { - mkdirSync("dist/webview", { recursive: true }); - copyFileSync("src/webview/shared/pastel.css", "dist/webview/pastel.css"); -} - -/** - * Two builds, in one config: - * 1. extension.js — node, runs in the VS Code host. Imports `vscode`. - * 2. webview/global-dashboard.js — browser, runs inside the dashboard - * panel's webview. Bundles React + the pastel CSS. - * - * Webview bundles target `es2020` because the webview's ` - -`; - } -} - -// ── Module-level helpers ───────────────────────────────────────────────── - -function sameMode(a: Mode, b: Mode): boolean { - if (a.kind !== b.kind) return false; - if (a.kind === "feature" && b.kind === "feature") return a.name === b.name; - return true; -} - -function randomNonce(): string { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let out = ""; - for (let i = 0; i < 32; i++) { - out += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return out; -} - -/** - * Look up `[[repos]]` sections by `name` and return the `target_branch` - * field if set. Naive but sufficient — canopy.toml is small. Falls back - * to null which the caller maps to "main". - */ -function matchTargetBranchForRepo(toml: string, repoName: string): string | null { - // Find the section that has `name = ""` and look for - // `target_branch = "..."` within the same section body. - const sections = toml.split(/^\s*\[\[repos\]\]\s*$/m).slice(1); - for (const body of sections) { - const cleaned = body - .split("\n") - .map((line) => line.replace(/#.*$/, "").trimEnd()) - .join("\n"); - const nameMatch = /^\s*name\s*=\s*["']([^"']+)["']\s*$/m.exec(cleaned); - if (nameMatch?.[1] !== repoName) continue; - const targetMatch = /^\s*target_branch\s*=\s*["']([^"']+)["']\s*$/m.exec(cleaned); - if (targetMatch) return targetMatch[1]; - return null; - } - return null; -} diff --git a/vscode-extension/src/views/canopyTreeProvider.ts b/vscode-extension/src/views/canopyTreeProvider.ts deleted file mode 100644 index 9ee4c1c..0000000 --- a/vscode-extension/src/views/canopyTreeProvider.ts +++ /dev/null @@ -1,269 +0,0 @@ -import * as vscode from "vscode"; - -import { CanopyClient } from "../canopyClient"; -import { LinearIssue } from "../types"; - -type Kind = - | "section-active" - | "section-launchers" - | "section-issues" - | "active-feature" - | "active-repo" - | "launcher" - | "linear-issue" - | "empty"; - -export interface CanopyNode { - kind: Kind; - label: string; - description?: string; - tooltip?: string; - contextValue?: string; - iconId?: string; - iconColor?: vscode.ThemeColor; - command?: vscode.Command; - collapsibleState?: vscode.TreeItemCollapsibleState; - - featureName?: string; - repoName?: string; - worktreePath?: string; - linearIssue?: LinearIssue; -} - -export class CanopyTreeProvider implements vscode.TreeDataProvider { - private readonly _onDidChange = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChange.event; - - private linearCount = 0; - - constructor( - private readonly client: CanopyClient, - private readonly getActiveFeature: () => string | null, - ) {} - - refresh(): void { - this._onDidChange.fire(undefined); - } - - getTreeItem(node: CanopyNode): vscode.TreeItem { - const item = new vscode.TreeItem(node.label, node.collapsibleState); - if (node.description !== undefined) item.description = node.description; - if (node.tooltip !== undefined) item.tooltip = node.tooltip; - if (node.contextValue !== undefined) item.contextValue = node.contextValue; - if (node.command !== undefined) item.command = node.command; - if (node.iconId !== undefined) { - item.iconPath = node.iconColor - ? new vscode.ThemeIcon(node.iconId, node.iconColor) - : new vscode.ThemeIcon(node.iconId); - } - return item; - } - - async getChildren(parent?: CanopyNode): Promise { - if (!parent) { - return [ - { - kind: "section-active", - label: "ACTIVE", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - contextValue: "section", - }, - { - kind: "section-launchers", - label: "LAUNCHERS", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - contextValue: "section", - }, - { - kind: "section-issues", - label: "ISSUES", - description: this.linearCount ? `${this.linearCount} todos` : undefined, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - contextValue: "section", - }, - ]; - } - - switch (parent.kind) { - case "section-active": - return this.activeChildren(); - case "section-launchers": - return this.launcherChildren(); - case "section-issues": - return this.linearChildren(); - case "active-feature": - return this.activeRepoChildren(parent.featureName!); - default: - return []; - } - } - - private async activeChildren(): Promise { - const active = this.getActiveFeature(); - if (!active) { - return [ - { - kind: "empty", - label: "(no active feature)", - tooltip: "Use Canopy: Switch to Feature to set one", - collapsibleState: vscode.TreeItemCollapsibleState.None, - }, - ]; - } - let lane; - try { - lane = await this.client.featureStatus(active); - } catch (err) { - return [ - { - kind: "empty", - label: `error: ${(err as Error).message}`, - collapsibleState: vscode.TreeItemCollapsibleState.None, - }, - ]; - } - const dirty = Object.values(lane.repo_states).reduce( - (n, s) => n + (s.changed_file_count ?? 0), - 0, - ); - const ahead = Object.values(lane.repo_states).reduce( - (n, s) => n + (s.ahead ?? 0), - 0, - ); - const desc = - lane.linear_issue - ? `${lane.linear_issue} · ↑${ahead} · ${dirty} dirty` - : `↑${ahead} · ${dirty} dirty`; - return [ - { - kind: "active-feature", - label: active, - description: desc, - tooltip: lane.linear_title ?? undefined, - contextValue: "feature", - iconId: "circle-filled", - iconColor: new vscode.ThemeColor("charts.green"), - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - featureName: active, - command: { - command: "canopy.openGlobalDashboard", - title: "Open dashboard", - }, - }, - ]; - } - - private async activeRepoChildren(feature: string): Promise { - let lane; - try { - lane = await this.client.featureStatus(feature); - } catch { - return []; - } - return Object.entries(lane.repo_states).map(([repo, state]) => { - const ahead = state.ahead ?? 0; - const dirty = state.changed_file_count ?? 0; - const parts: string[] = []; - if (ahead) parts.push(`↑${ahead}`); - if (dirty) parts.push(`${dirty} dirty`); - const desc = parts.join(" · ") || "clean"; - return { - kind: "active-repo", - label: repo, - description: desc, - contextValue: "feature.repo", - iconId: "repo", - collapsibleState: vscode.TreeItemCollapsibleState.None, - featureName: feature, - repoName: repo, - worktreePath: state.worktree_path ?? undefined, - command: state.worktree_path - ? { - command: "vscode.openFolder", - title: "Open worktree", - arguments: [vscode.Uri.file(state.worktree_path), { forceNewWindow: false }], - } - : undefined, - }; - }); - } - - private launcherChildren(): CanopyNode[] { - return [ - { - kind: "launcher", - label: "Open Dashboard", - tooltip: "Open the pastel global dashboard", - iconId: "layout", - collapsibleState: vscode.TreeItemCollapsibleState.None, - command: { - command: "canopy.openGlobalDashboard", - title: "Open Dashboard", - }, - }, - { - kind: "launcher", - label: "New feature from issue", - tooltip: "Spin up a feature from a Linear / GitHub issue", - iconId: "add", - collapsibleState: vscode.TreeItemCollapsibleState.None, - command: { - command: "canopy.openNewFeature", - title: "New feature from issue", - }, - }, - { - kind: "launcher", - label: "Open canopy.toml", - tooltip: "Edit workspace settings", - iconId: "settings-gear", - collapsibleState: vscode.TreeItemCollapsibleState.None, - command: { - command: "canopy.openConfigFile", - title: "Open canopy.toml", - }, - }, - ]; - } - - private async linearChildren(): Promise { - let issues: LinearIssue[]; - try { - issues = await this.client.linearMyIssues(25); - } catch (err) { - return [ - { - kind: "empty", - label: `Linear unavailable: ${(err as Error).message}`, - collapsibleState: vscode.TreeItemCollapsibleState.None, - }, - ]; - } - const todos = issues.filter((i) => i.state.toLowerCase() === "todo"); - this.linearCount = todos.length; - if (!todos.length) { - return [ - { - kind: "empty", - label: "(inbox empty)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - }, - ]; - } - return todos.map((i) => ({ - kind: "linear-issue" as const, - label: i.identifier, - description: i.title, - tooltip: `${i.identifier} · ${i.state}\n${i.title}`, - contextValue: "linear.issue", - iconId: "issues", - collapsibleState: vscode.TreeItemCollapsibleState.None, - linearIssue: i, - command: { - command: "canopy.createFeatureFromIssue", - title: "Start feature from this issue", - arguments: [i], - }, - })); - } -} diff --git a/vscode-extension/src/views/themeShim.ts b/vscode-extension/src/views/themeShim.ts deleted file mode 100644 index a65bdec..0000000 --- a/vscode-extension/src/views/themeShim.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Theme shim — bridges the existing `webview/themes/.ts` token files - * (used by the legacy cockpit / per-feature dashboard) to the new pastel - * dashboard's CSS-variable contract. - * - * `pastel.css` is the single source of layout: every selector references - * variables like `--paper` / `--ink` / `--canonical`. Switching the - * theme means redefining those variables — not rewriting the selectors. - * This module renders a small `:root { ... }` override sourced from the - * matching `ThemeTokens`, so adding a third theme only means adding a - * new tokens file (no CSS duplication). - */ -import { type ThemeTokens, getTheme } from "../webview/themes"; - -/** - * Render the `:root` override that maps a `ThemeTokens` palette onto - * pastel.css's variable names. Returns an empty string when ``themeName`` - * is "pastel" (or unknown — pastel.css's own `:root` block is the - * source of truth there). - */ -export function renderThemeOverride(themeName: string): string { - if (themeName === "pastel") return ""; - const tokens = getTheme(themeName); - return _overrideFromTokens(tokens); -} - -function _overrideFromTokens(t: ThemeTokens): string { - const c = t.colors; - // The pastel selectors use a wider colour vocabulary (per-slot tints - // for the canonical/warm/cold/hot/info/bot families) than the - // minimal/navy tokens carry. We map each pastel variable to the - // closest available token; the *-soft / *-tint shades fall back to - // the same colour with reduced opacity (using the existing -soft - // tokens which are already 1f-suffixed in the legacy themes). - return ` -:root { - /* surfaces */ - --paper: ${c.bg}; - --surface: ${c.bgElev}; - --surface-2: ${c.bgElev2}; - --surface-3: ${c.bgElev3}; - - /* lines */ - --hairline: ${c.borderSoft}; - --rule: ${c.border}; - --rule-strong: ${c.border}; - - /* text */ - --ink: ${c.fg}; - --ink-soft: ${c.fgMuted}; - --ink-muted: ${c.fgMuted}; - --ink-dim: ${c.fgDim}; - - /* slot accents — colour as text only, fill via -soft (alpha) */ - --canonical: ${c.ok}; - --canonical-soft:${c.okSoft}; - --canonical-tint:${c.okSoft}; - - --warm: ${c.warn}; - --warm-soft: ${c.warnSoft}; - --warm-tint: ${c.warnSoft}; - - --cold: ${c.bot}; - --cold-soft: ${c.botSoft}; - --cold-tint: ${c.botSoft}; - - /* status accents */ - --hot: ${c.hot}; - --hot-soft: ${c.hotSoft}; - --hot-tint: ${c.hotSoft}; - - --info: ${c.accent}; - --info-soft: ${c.accentSoft}; - --info-tint: ${c.accentSoft}; - - --bot: ${c.bot}; - --bot-soft: ${c.botSoft}; - --bot-tint: ${c.botSoft}; - - /* shadows — flatten in dark themes; the pastel cream casts shadows, - monochrome dark surfaces don't need them */ - --shadow-sm: none; - --shadow-md: none; - --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4); - - /* diff colours — mapped to ok/hot families on a dark surface */ - --diff-add-bg: ${c.okSoft}; - --diff-add-fg: ${c.ok}; - --diff-del-bg: ${c.hotSoft}; - --diff-del-fg: ${c.hot}; - --diff-ctx-fg: ${c.fgMuted}; - --diff-hunk-bg: ${c.bgElev}; - --diff-hunk-fg: ${c.fgDim}; -} - -/* Override the gradient-y / shadow-heavy bits of pastel that read as - "bright cream paper" — keep dark themes flat. */ -.focus, -.standby-card { - background: transparent !important; -} -.layout, .layout.feature, .triage, .layout.feature .rail { - background: var(--paper) !important; -} -button.btn.primary, -button.action-btn.primary { - background: var(--ink); border-color: var(--ink); color: var(--paper); -} -button.btn.primary:hover, -button.action-btn.primary:hover { background: #fff; } -.crumb .leaf { background: transparent; border: 1px solid var(--canonical-soft); color: var(--canonical); } -`.trim(); -} diff --git a/vscode-extension/src/watchers.ts b/vscode-extension/src/watchers.ts deleted file mode 100644 index d85fb6b..0000000 --- a/vscode-extension/src/watchers.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as vscode from "vscode"; - -interface WatcherCallbacks { - onFeaturesChanged: () => void; - onWorktreeChanged: () => void; - /** - * Wave 2.9 state files — fired when the active-feature pointer - * changes (canopy switch), when the post-checkout hook records a - * head, or when preflight records a fresh result. Drives the - * cockpit's auto-refresh. - */ - onStateFilesChanged?: () => void; -} - -/** - * Debounced FileSystemWatchers covering canopy's persistent state: - * - * 1. `.canopy/features.json` (300ms) → tree views + cockpit - * 2. `.canopy/worktrees/**\/.git/{HEAD,index}` (500ms) → Changes view - * 3. `.canopy/state/{active_feature,heads,preflight}.json` (200ms) → cockpit - * - * State-file changes get the tightest debounce since the cockpit's - * "what's canonical" + "is preflight stale" displays are user-facing - * and we want them to react quickly to a `canopy switch` from the CLI. - */ -export function createWatchers( - workspaceRoot: vscode.WorkspaceFolder, - cb: WatcherCallbacks, -): vscode.Disposable { - const disposables: vscode.Disposable[] = []; - - const featuresPattern = new vscode.RelativePattern( - workspaceRoot, - ".canopy/features.json", - ); - const featuresWatcher = vscode.workspace.createFileSystemWatcher(featuresPattern); - const fireFeatures = debounce(cb.onFeaturesChanged, 300); - featuresWatcher.onDidCreate(fireFeatures); - featuresWatcher.onDidChange(fireFeatures); - featuresWatcher.onDidDelete(fireFeatures); - disposables.push(featuresWatcher); - - const worktreePattern = new vscode.RelativePattern( - workspaceRoot, - ".canopy/worktrees/**/.git/{HEAD,index}", - ); - const worktreeWatcher = vscode.workspace.createFileSystemWatcher(worktreePattern); - const fireWorktree = debounce(cb.onWorktreeChanged, 500); - worktreeWatcher.onDidCreate(fireWorktree); - worktreeWatcher.onDidChange(fireWorktree); - worktreeWatcher.onDidDelete(fireWorktree); - disposables.push(worktreeWatcher); - - if (cb.onStateFilesChanged) { - const statePattern = new vscode.RelativePattern( - workspaceRoot, - ".canopy/state/{active_feature,heads,preflight}.json", - ); - const stateWatcher = vscode.workspace.createFileSystemWatcher(statePattern); - const fireState = debounce(cb.onStateFilesChanged, 200); - stateWatcher.onDidCreate(fireState); - stateWatcher.onDidChange(fireState); - stateWatcher.onDidDelete(fireState); - disposables.push(stateWatcher); - } - - return vscode.Disposable.from(...disposables); -} - -function debounce void>(fn: T, ms: number): T { - let handle: NodeJS.Timeout | null = null; - return ((...args: unknown[]) => { - if (handle) clearTimeout(handle); - handle = setTimeout(() => { - handle = null; - fn(...args); - }, ms); - }) as T; -} diff --git a/vscode-extension/src/webview/cockpitPanel.ts b/vscode-extension/src/webview/cockpitPanel.ts deleted file mode 100644 index 6b6d2e8..0000000 --- a/vscode-extension/src/webview/cockpitPanel.ts +++ /dev/null @@ -1,395 +0,0 @@ -import * as vscode from "vscode"; - -import type { CanopyClient, FeatureStateResult, SwitchBlocker } from "../canopyClient"; -import { isSwitchBlocker } from "../canopyClient"; -import { getTheme, renderThemeCss, type ThemeName } from "./themes"; -import { componentCss } from "./components/styles"; -import { renderBridge } from "./components/bridge"; -import { renderFocusTile } from "./components/focusTile"; -import { renderWorktreeRow } from "./components/worktreeRow"; -import { renderBranchLedger } from "./components/branchLedger"; -import { renderTriageFeed } from "./components/triageFeed"; -import { renderCapReachedModal } from "./components/capReachedModal"; -import { escapeHtml } from "./components/util"; - -/** - * Workspace-scoped cockpit dashboard (Wave 7). - * - * One panel per workspace (singleton). Renders the canonical-slot model - * end-to-end: bridge bar → focus tile (canonical) → worktrees → branches - * → triage feed (rail). Phase B ships bridge + focus tile only; the rest - * land in Phase C. - * - * Differs from the per-feature DashboardPanel in three ways: - * 1. One panel for the whole workspace (not per feature). - * 2. Driven by feature_state(canonical) + triage(), not feature_status. - * 3. Theme-pluggable via canopy.dashboard.theme setting. - */ -export class CockpitPanel { - private static instance: CockpitPanel | null = null; - private readonly panel: vscode.WebviewPanel; - private disposed = false; - private themeName: ThemeName; - private themeListener: vscode.Disposable; - - static show(context: vscode.ExtensionContext, client: CanopyClient): void { - if (CockpitPanel.instance) { - CockpitPanel.instance.panel.reveal(vscode.ViewColumn.Active); - void CockpitPanel.instance.refresh(); - return; - } - CockpitPanel.instance = new CockpitPanel(context, client); - } - - static refreshIfOpen(): void { - if (CockpitPanel.instance) { - void CockpitPanel.instance.refresh(); - } - } - - private constructor( - context: vscode.ExtensionContext, - private readonly client: CanopyClient, - ) { - this.themeName = readThemeName(); - - this.panel = vscode.window.createWebviewPanel( - "canopy.cockpit", - "Canopy", - vscode.ViewColumn.Active, - { enableScripts: true, retainContextWhenHidden: true }, - ); - this.panel.iconPath = vscode.Uri.joinPath( - context.extensionUri, - "media", - "canopy-icon.svg", - ); - - this.panel.onDidDispose(() => { - this.disposed = true; - this.themeListener.dispose(); - CockpitPanel.instance = null; - }); - - // Re-render when the user changes the theme setting. - this.themeListener = vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration("canopy.dashboard.theme")) { - this.themeName = readThemeName(); - void this.refresh(); - } - }); - - this.panel.webview.onDidReceiveMessage((msg: WebviewMessage) => - this.handleMessage(msg), - ); - - void this.refresh(); - } - - private async refresh(): Promise { - if (this.disposed) return; - try { - const html = await this.renderHtml(); - this.panel.webview.html = html; - } catch (err) { - this.panel.webview.html = this.renderError((err as Error).message); - } - } - - // ── Rendering ──────────────────────────────────────────────────── - - private async renderHtml(): Promise { - const theme = getTheme(this.themeName); - const themeCss = renderThemeCss(theme); - - const status = await this.client.workspaceStatus(); - const workspaceLabel = abbreviatePath(status.root); - - // Phase B: only canonical feature is wired. Worktree count + cap - // come from triage when Phase C lands; for now we show 0 / cap or - // pull from features with worktree_paths. - const triage = await this.client.triage().catch(() => null); - const canonicalFeature = triage?.canonical_feature ?? null; - const worktreeFeatures = triage - ? triage.features.filter( - (f) => !f.is_canonical && f.physical_state === "warm", - ) - : []; - const branchFeatures = triage - ? triage.features.filter( - (f) => !f.is_canonical && f.physical_state === "cold", - ) - : []; - const worktreeCount = worktreeFeatures.length; - const worktreeCap = readWorktreeCap(); - const evictionCandidate = - worktreeCap > 0 && worktreeCount >= worktreeCap - ? worktreeFeatures[worktreeFeatures.length - 1]?.feature ?? null - : null; - - // Canonical feature state — drives the focus tile. - let featureState: FeatureStateResult | null = null; - let linearTitle: string | undefined; - let linearUrl: string | undefined; - if (canonicalFeature) { - try { - featureState = await this.client.featureState(canonicalFeature); - } catch { - featureState = null; - } - // Linear metadata sourced from the matching triage entry to avoid - // an extra MCP roundtrip. - const t = triage?.features.find((f) => f.feature === canonicalFeature); - linearTitle = t?.linear_title; - linearUrl = t?.linear_url; - } - - return ` - - - - -Canopy - - - - -${renderBridge({ - workspaceLabel, - canonicalFeature, - worktreeCount, - worktreeCap, - activeTheme: this.themeName, -})} - -
-
-
- Main ${canonicalFeature ? "in focus" : "(empty)"} - primary action sourced from feature_state.next_actions[0] -
- ${renderFocusTile({ state: featureState, linearTitle, linearUrl })} - -
- Worktrees ${worktreeCount} / ${worktreeCap || "∞"} - linked worktrees · click to switch into main -
- ${renderWorktreeRow({ features: worktreeFeatures })} - -
- Branches ${branchFeatures.length} - no worktree · switching creates one${ - evictionCandidate ? " (and may evict the LRU worktree)" : "" - } -
- ${renderBranchLedger({ features: branchFeatures, evictionCandidate })} -
- - -
- - - - - - -`; - } - - private renderError(message: string): string { - return ` -

Canopy dashboard failed to load

-
${escapeHtml(message)}
- `; - } - - // ── Message handling (browser → extension) ─────────────────────── - - private async handleMessage(msg: WebviewMessage): Promise { - switch (msg.type) { - case "setTheme": { - const theme = msg.theme; - if (theme === "navy" || theme === "minimal") { - await vscode.workspace - .getConfiguration() - .update( - "canopy.dashboard.theme", - theme, - vscode.ConfigurationTarget.Global, - ); - } - return; - } - case "invokeAction": - await this.dispatchAction(msg.action, msg.args ?? {}); - return; - case "refresh": - void this.refresh(); - return; - } - } - - private async dispatchAction( - action: string, - args: Record, - ): Promise { - try { - switch (action) { - case "switch": { - const result = await this.client.switchFeature({ - feature: args.feature as string, - releaseCurrent: args.release_current as boolean | undefined, - noEvict: args.no_evict as boolean | undefined, - evict: args.evict as string | undefined, - }); - if (isSwitchBlocker(result)) { - this.showCapReachedModal(result, args.feature as string); - return; - } - await this.refresh(); - return; - } - case "preflight": { - const r = await this.client.preflight(); - void vscode.window.showInformationMessage( - r.all_passed - ? "Canopy preflight: all repos passed" - : "Canopy preflight: failures — see Output", - ); - await this.refresh(); - return; - } - case "openInIde": - await vscode.commands.executeCommand( - "canopy.openInIde", - args.feature as string, - ); - return; - case "openCockpitForFeature": - // Phase E: opens the focused-feature drilldown panel. Until - // that lands, fall back to the existing per-feature dashboard - // so clicks still take the user somewhere useful. - await vscode.commands.executeCommand( - "canopy.openDashboard", - args.feature as string, - ); - return; - case "newFeature": - await vscode.commands.executeCommand("canopy.openNewFeature"); - return; - case "address_review_comments": - case "addressComments": - // Existing helper used by the per-feature dashboard for the - // "Start workflow with Claude" CTA. Reused here. - await vscode.commands.executeCommand( - "canopy.startWorkflowWithClaude", - args.feature as string, - ); - return; - case "viewComments": - // Open the per-feature dashboard which has the existing - // comments view — Phase E swaps for the action drawer. - await vscode.commands.executeCommand( - "canopy.openDashboard", - args.feature as string, - ); - return; - default: - void vscode.window.showInformationMessage( - `Canopy: ${action} not yet wired (args: ${JSON.stringify(args)})`, - ); - } - } catch (err) { - void vscode.window.showErrorMessage( - `Canopy: ${action} failed — ${(err as Error).message}`, - ); - } - } - - private showCapReachedModal(blocker: SwitchBlocker, originalTarget: string): void { - // Render the modal HTML extension-side (so escapeHtml runs in our - // process, not the webview), post it across, the webview script - // injects into