feat(dashboard): open in workspace tab - #166
Open
andyshinn wants to merge 19 commits into
Open
Conversation
Adds claudeUsage.openLocation (sidebar|editor) and claudeUsage.collapseSidebarOnOpenInEditor, plus a claudeUsage.openInEditor command and sidebar title-bar button. Key design decision: the active surface is derived (panelExists ? "editor" : openLocation) rather than stored, which guarantees exactly one live dashboard instance by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven TDD tasks: extract webview-html, add pure routing predicates, sidebar placeholder mode, DashboardPanel singleton, host wiring + manifest, optional sidebar collapse, then docs and the v1.6.0 bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tton Both the sidebar and the forthcoming editor panel render identical HTML, so renderHtml/escapeHtml/makeNonce move to their own vscode-free module. The status pane's hardcoded 'Retry' -> claudeUsage.open button becomes a caller-supplied WebviewAction, which the editor panel needs to point its own retry at claudeUsage.openInEditor.
…tion Isolates the whole feature's decision table in a vscode-free module so it can be unit-tested. deriveActiveSurface() encodes the single-instance invariant as a derivation (panelExists ? editor : setting) rather than stored state. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
setPlaceholder() renders the 'dashboard is in an editor tab' pane; isVisible() gates sidebar collapse so we never close the Explorer by mistake.
Mirrors DashboardSidebar's API so the host can drive either surface uniformly. retainContextWhenHidden keeps dashboard state across tab switches; no serializer, so the tab closes on window reload by design.
reveal()/render() now no-op once the panel is disposed, matching DashboardSidebar's !this.view guard. dispose() is idempotent. Fixes a gap where a stale DashboardPanel reference held across an await (e.g. during server.start()) would throw if the user closed the tab in the meantime.
…mand Splits openDashboard() into ensureServer() plus two reveal paths, so starting the Python server is no longer welded to revealing the sidebar. The sidebar container reveal now lives only in showSidebar() — leaving it on the shared path would pop open the very panel the user is escaping. The active surface is derived (panelExists ? editor : setting), so exactly one dashboard instance is live in every combination of setting and tab state.
Q1: showEditorTab() re-derives the surface after ensureServer() settles instead of trusting a possibly-stale panel reference, so closing the tab mid-cold-start no longer strands the sidebar on a blank status. Q2: openDashboard() routes on the derived activeSurface() instead of the raw openLocation() setting, so claudeUsage.open (and both Retry actions that call it) no longer force the sidebar open when an editor tab already owns the dashboard — the exact bug this feature exists to avoid. Q3/Q4: broadcastError() clears serverUrl and both broadcast helpers fall back to the sidebar whenever no panel exists, instead of silently dropping status/ error updates when openLocation is "editor" but no tab has been created yet. The !spawnArgs failure now also raises a toast, matching the other two early returns. M1: doStartup()'s try/catch now covers resolveStablePort()/workspaceState too, so a port-probe rejection routes through broadcastError() and resolves to undefined like every other failure, instead of escaping as an unhandled rejection.
Declares the setting and wires it to the existing shouldCollapseSidebar() predicate, replacing maybeCollapseSidebar()'s Task-5 inlined condition. Guarded on the Claude Usage view actually being the visible container, since workbench.action.closeSidebar closes whatever is showing — without the guard, popping out with the Explorer open would close the Explorer. Also gates the collapse on how the tab was opened. showEditorTab() now takes a fromSidebarReveal flag: explicit invocations (the openInEditor command, its title-bar button, claudeUsage.open routing to the tab, and restart()) always collapse when enabled and visible, but the onSidebarShown() handoff only collapses when openLocation is "editor". Without that split, turning the setting on with openLocation: "sidebar" and a tab already open would make revealing the sidebar hand off to the tab and immediately collapse the sidebar again — leaving the "Show tab" placeholder permanently unreachable. shouldCollapseSidebar() carries the new fromSidebarReveal/openLocation gate so it stays unit-testable alongside the sibling open-target.ts decisions; test/open-target.test.ts covers the new handoff-vs-explicit cases. Also fixes rescan(): it used the same optional-chain-no-op pattern that broadcastStatus/broadcastError were fixed for in Task 5 (this.panel?.refresh() silently doing nothing when openLocation is "editor" but no tab has been created yet). Branches on this.panel truthiness instead, so claudeUsage.rescan falls back to refreshing the sidebar when it's the only surface that actually exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New user-visible feature, so a minor bump. scanner.VERSION, the CHANGELOG heading and package.json move together — tests/test_version.py enforces it. The heading stays TBD until the maintainer merges DEV to main. The collapse-setting wording in the CHANGELOG and README diverges from the task brief: the brief's text predates two review-driven changes that gate collapseSidebarOnOpenInEditor by call site (explicit pop-outs always collapse; the activity-bar handoff from an already-open tab only collapses when openLocation is "editor"), so the docs here describe that actual behavior instead.
onSidebarShown() branched on activeSurface(), which reports "editor" whenever a panel exists regardless of the openLocation setting. With the default openLocation: "sidebar" and the dashboard popped out to a tab, revealing the sidebar handed off to DashboardPanel.reveal() and yanked focus off whatever file the user was on. Branch on the setting (openLocation()) instead: only hand off to the tab when openLocation is "editor", matching the spec's state table. With openLocation: "sidebar" and a panel open, the icon click now falls through to renderSidebar(), which resolves to the "Show tab" placeholder instead of stealing focus. openDashboard() is untouched — an explicit "open the dashboard" request should still route via activeSurface() to whichever surface owns it.
The previous commit makes showEditorTab(true) reachable only when openLocation === "editor", so shouldCollapseSidebar's `if (fromSidebarReveal && openLocation !== "editor") return false` branch can never fire. Revert shouldCollapseSidebar to the plain 2-argument form (enabled && sidebarVisible), with a doc comment describing only the Explorer-safety visibility guard, and revert the test file's describe block to the 4 enabled x sidebarVisible cases — dropping the tests that only existed to cover the deleted dimension. This restores what the spec always said: the collapse fires on any path that opens the tab, gated only on our view being the visible container.
… README - Add the missing Commands-table row for "Claude Usage: Open Dashboard in Editor Tab". - Rewrite the collapseSidebarOnOpenInEditor row (README) and its matching CHANGELOG bullet: both self-contradicted by leading with "Only fires on explicit pop-outs (...)" and then describing a further trigger. State it plainly and completely instead — it collapses on any path that opens the editor tab, gated only on the Claude Usage view being the visible sidebar container. - Restore the "Claude Usage:" prefix on command titles in the settings-table row so they match package.json and the prose section below.
…comments - sidebar.ts: delete the unused ServerManager import/re-export and its comment describing wiring that no longer exists — extension.ts now goes through ensureServer() -> renderSidebar(), and nothing imports ServerManager from sidebar.ts (verified with grep). - sidebar.ts: drop refresh()'s redundant `if (!this.view) return` guard — render() already guards on the same condition, so the first check was unreachable. Matches editor-panel.ts's refresh(), which already relies solely on render()'s guard. - editor-panel.ts: correct DashboardPanel.get()'s doc comment. It does not "drive the derived active-surface rule" (extension.ts reads its own this.panel field); it's an accessor used only by tests.
There was no test that package.json's contributions match the code.
The title-bar pop-out button depends on the view/title menu entry's
"when": "view == claudeUsage.dashboard" matching
DashboardSidebar.viewId — a typo in either would silently remove the
button with zero test failures.
Covers: claudeUsage.openInEditor's command title/icon, the view/title
menu entry's command and when-clause (compared against the real
DashboardSidebar.viewId import, not a hardcoded string),
claudeUsage.openLocation's enum/default, and
claudeUsage.collapseSidebarOnOpenInEditor's type/default. No vscode
mock needed for the JSON.parse itself, but importing DashboardSidebar
from sidebar.ts requires the same vi.mock("vscode", ...) stub
test/sidebar.test.ts already uses.
onSidebarShown() runs ensureServer() itself now that it no longer delegates to showEditorTab() under openLocation: "sidebar". A panel can still exist in that state, so guarding only the sidebar dropped the URL when the panel's own startup had failed and this attempt succeeded, leaving the tab on a stale error pane over a running server. Re-derive the target instead. Also refreshes two README lines this feature made inaccurate: the Open Dashboard command routes by active surface, not always to the sidebar, and the Usage steps now mention the editor-tab option. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tab icon was a fixed-ink SVG handed to WebviewPanel.iconPath, which VS Code draws as a plain image and never recolors. The artwork is solid #000000, so it was invisible on dark themes. Only glyphs contributed through contributes.icons and referenced as a ThemeIcon get painted with the active theme's icon.foreground, so the icon now ships as a WOFF glyph. The source is the original vector logo, recovered from ad05701. It was replaced in 3d53f6d by a 167 KB SVG that is really two base64 PNGs in an SVG wrapper (zero path data), which is unusable for a font glyph and is also why the icon looks pixelated when opened in an editor. Glyph metrics are copied from VS Code's own codicon.ttf (300 upm, advance 300, ink fitted to a 282 box anchored at x=0) so the icon sits in the tab strip at the same size and label spacing as the built-in icons. Fitting the viewBox instead of the ink bbox is what made the first attempt render small with a visible left margin. Requires VS Code 1.110+ (microsoft/vscode#90616), up from 1.94. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit raised the floor to 1.110 so the themed tab icon would be
guaranteed, which was the wrong trade: it made the extension uninstallable on
1.94-1.109 to protect a 16px decoration.
Older builds degrade gracefully. Their setter is
$setIconPath(handle, URI.isUri(value) ? {light: value, dark: value} : value)
so a ThemeIcon is passed through untouched, does not throw, and the tab simply
renders without an icon (microsoft/vscode#282608). Everything else in the
extension works exactly as before.
The manifest test now asserts the floor stays AT OR BELOW 1.94 rather than at
or above 1.110, so the mistake cannot be reintroduced silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an editor-tab hosting option for the VS Code extension dashboard (in addition to the existing sidebar webview), with settings + commands that keep the “single live dashboard instance” invariant while reusing the same embedded localhost server and shared webview HTML.
Changes:
- Introduces a singleton
DashboardPanel(editor tab) and routing logic to open/focus either sidebar or tab based on derived state +claudeUsage.openLocation. - Extracts shared webview HTML rendering into
src/webview-html.tswith a parameterized status/placeholder action button (Retry / Show tab / Open tab). - Updates extension manifest/docs/tests, adds an icon font for a theme-colored tab icon, and bumps version to
1.6.0(incl. changelog +scanner.VERSION).
Reviewed changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vscode-extension/src/extension.ts | Refactors orchestration: derived active surface, shared server startup, new open-in-editor command, sidebar placeholder rendering, optional sidebar collapse logic |
| vscode-extension/src/editor-panel.ts | Adds singleton editor-tab webview panel host for the dashboard |
| vscode-extension/src/sidebar.ts | Removes embedded HTML helpers; adds placeholder/action support and visibility probing |
| vscode-extension/src/webview-html.ts | New shared HTML renderer + escaping + nonce generation + action button abstraction |
| vscode-extension/src/open-target.ts | Pure, vscode-free routing predicates and placeholder action constants |
| vscode-extension/package.json | Adds settings, new command + view/title menu entry, icon contribution, version bump |
| vscode-extension/README.md | Documents editor-tab option + new settings/commands; notes optional tab icon behavior on newer VS Code |
| vscode-extension/scripts/build-icon-font.py | Build script to generate the contributed WOFF icon font from a vector SVG |
| vscode-extension/resources/icon-vector.svg | Adds vector source used to generate the icon font |
| vscode-extension/test/webview-html.test.ts | New unit tests for shared HTML renderer + action button escaping |
| vscode-extension/test/open-target.test.ts | New unit tests for surface routing decision table and sidebar-collapse predicate |
| vscode-extension/test/editor-panel.test.ts | New unit tests for editor panel singleton behavior and rendering paths |
| vscode-extension/test/sidebar.test.ts | Updates sidebar tests for placeholder mode + visibility behavior |
| vscode-extension/test/manifest.test.ts | New tests asserting manifest wiring for commands/settings/icon resources |
| CHANGELOG.md | Adds v1.6.0 — TBD entry covering the feature set |
| scanner.py | Bumps VERSION to 1.6.0 to stay in parity with changelog + extension package |
| docs/superpowers/specs/2026-07-26-vscode-editor-tab-design.md | Design spec for the feature and invariants |
| docs/superpowers/plans/2026-07-26-vscode-editor-tab.md | Implementation plan and verification checklist |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+9
to
+12
| // Static JSON.parse of the manifest — no vscode API needed here, but the | ||
| // DashboardSidebar import below requires the stub above regardless. | ||
| const manifestPath = path.join(__dirname, "..", "package.json"); | ||
| const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); |
Comment on lines
+149
to
+153
| /** Idempotent: disposing an already-disposed panel is a safe no-op. */ | ||
| dispose(): void { | ||
| if (this.disposed) return; | ||
| this.panel.dispose(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds a new command and some settings around opening the dashboard in a workspace editor tab instead of the left sidebar navigation.
Screen.Recording.2026-07-28.at.8.32.07.PM.mov
New settings additions:
I also restored a real vector version of the SVG that was removed in 3d53f6d for some reason. If that was unintentional we can place the original SVG icon back to vector and remove my vector one. I needed the vector to create the WOFF font for the editor tab icon.
Closes #164