From f4681d77d4e834a6529c90dab3da8d5f54e6ac63 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 12 May 2026 14:07:09 +1000 Subject: [PATCH 1/5] feat: add experimental workspaces view --- package.json | 27 ++++- packages/workspaces/README.md | 27 +++++ packages/workspaces/package.json | 33 ++++++ packages/workspaces/src/App.tsx | 3 + packages/workspaces/src/css.d.ts | 1 + packages/workspaces/src/index.css | 1 + packages/workspaces/src/index.tsx | 26 +++++ packages/workspaces/storybook.preview.ts | 1 + packages/workspaces/tsconfig.json | 10 ++ packages/workspaces/vite.config.ts | 3 + pnpm-lock.yaml | 55 ++++++++++ src/core/commandManager.ts | 1 + src/core/contextManager.ts | 1 + src/extension.ts | 35 ++++++ .../workspaces/workspacesPanelProvider.ts | 100 ++++++++++++++++++ 15 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 packages/workspaces/README.md create mode 100644 packages/workspaces/package.json create mode 100644 packages/workspaces/src/App.tsx create mode 100644 packages/workspaces/src/css.d.ts create mode 100644 packages/workspaces/src/index.css create mode 100644 packages/workspaces/src/index.tsx create mode 100644 packages/workspaces/storybook.preview.ts create mode 100644 packages/workspaces/tsconfig.json create mode 100644 packages/workspaces/vite.config.ts create mode 100644 src/webviews/workspaces/workspacesPanelProvider.ts diff --git a/package.json b/package.json index 1066eb9e..60df2060 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "package:prerelease": "pnpm build:production && vsce package --pre-release --no-dependencies", "storybook": "storybook dev -p 6006 --config-dir .storybook", "storybook:build": "storybook build --config-dir .storybook", - "storybook:ci": "storybook build --test --config-dir .storybook", + "storybook:ci": "storybook build --test --config-dir .storybook", "test": "cross-env CI=true ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs", "test:extension": "cross-env ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project extension", "test:integration": "pnpm compile-tests:integration && node esbuild.mjs && vscode-test", @@ -149,6 +149,17 @@ "experimental" ] }, + "coder.experimental.workspacesPanel": { + "markdownDescription": "Enable the experimental Workspaces webview panel. This feature is in active development.", + "type": "boolean", + "default": false, + "tags": [ + "experimental" + ], + "scope": "application", + "ignoreSync": true, + "order": 999 + }, "coder.sshFlags": { "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). Prefer `#coder.proxyLogDirectory#` over `--log-dir`/`-l` for full functionality.", "type": "array", @@ -277,6 +288,11 @@ "id": "coderTasks", "title": "Coder Tasks", "icon": "media/tasks-logo.svg" + }, + { + "id": "coderExperimentalWorkspaces", + "title": "Coder Remote (New)", + "icon": "media/shorthand-logo.svg" } ], "secondarySidebar": [ @@ -326,6 +342,15 @@ "icon": "media/shorthand-logo.svg", "when": "coder.agentsEnabled" } + ], + "coderExperimentalWorkspaces": [ + { + "type": "webview", + "id": "coder.experimental.workspacesPanel", + "name": "Workspaces", + "icon": "media/logo-white.svg", + "when": "coder.authenticated && coder.experimental.webkitWorkspaces" + } ] }, "viewsWelcome": [ diff --git a/packages/workspaces/README.md b/packages/workspaces/README.md new file mode 100644 index 00000000..c899337a --- /dev/null +++ b/packages/workspaces/README.md @@ -0,0 +1,27 @@ +# Coder Experimental Workspaces Webview Panel + +This package contains the experimental Workspaces webview panel for the Coder VS Code extension. + +This feature is currently in development and hidden from the Settings UI. + +## Enabling the Feature + +The workspaces panel is controlled by the `coder.experimental.workspacesPanel` configuration setting. + +**This setting is hidden from the Settings UI** - it can only be enabled via settings.json: + +1. Open your VS Code settings.json (Cmd/Ctrl + Shift + P → "Preferences: Open User Settings (JSON)") +2. Add the following: + +```json +{ + "coder.experimental.workspacesPanel": true +} +``` + +3. Reload VS Code + +A new activity bar icon labeled **"Coder Remote (New)"** will appear in the activity bar when the setting is enabled. This creates a completely separate panel alongside the existing "Coder Remote" and "Coder Tasks" panels, allowing easy side-by-side comparison during development. + +> [!NOTE] +> The new view will only appear after you instantiate the Coder context (i.e clicking Tasks or Workspaces). diff --git a/packages/workspaces/package.json b/packages/workspaces/package.json new file mode 100644 index 00000000..d7a3b345 --- /dev/null +++ b/packages/workspaces/package.json @@ -0,0 +1,33 @@ +{ + "name": "@repo/workspaces", + "version": "1.0.0", + "description": "Coder Workspaces webview panel", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@repo/shared": "workspace:*", + "@repo/webview-shared": "workspace:*", + "@tanstack/react-query": "catalog:", + "@vscode-elements/react-elements": "catalog:", + "@vscode/codicons": "catalog:", + "date-fns": "catalog:", + "react": "catalog:", + "react-dom": "catalog:" + }, + "devDependencies": { + "@repo/mocks": "workspace:*", + "@repo/storybook-utils": "workspace:*", + "@rolldown/plugin-babel": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "babel-plugin-react-compiler": "catalog:", + "typescript": "catalog:", + "vite": "catalog:" + } +} diff --git a/packages/workspaces/src/App.tsx b/packages/workspaces/src/App.tsx new file mode 100644 index 00000000..abed2111 --- /dev/null +++ b/packages/workspaces/src/App.tsx @@ -0,0 +1,3 @@ +export default function App() { + return
TODO
; +} diff --git a/packages/workspaces/src/css.d.ts b/packages/workspaces/src/css.d.ts new file mode 100644 index 00000000..cbe652db --- /dev/null +++ b/packages/workspaces/src/css.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/packages/workspaces/src/index.css b/packages/workspaces/src/index.css new file mode 100644 index 00000000..8f414f58 --- /dev/null +++ b/packages/workspaces/src/index.css @@ -0,0 +1 @@ +/* TODO */ diff --git a/packages/workspaces/src/index.tsx b/packages/workspaces/src/index.tsx new file mode 100644 index 00000000..e6bb1159 --- /dev/null +++ b/packages/workspaces/src/index.tsx @@ -0,0 +1,26 @@ +import { ErrorBoundary } from "@repo/webview-shared/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import App from "./App"; +import "./index.css"; + +const queryClient = new QueryClient(); + +const root = document.getElementById("root"); +if (!root) { + throw new Error( + "Failed to find root element. The webview HTML must contain an element with id='root'.", + ); +} + +createRoot(root).render( + + + + + + + , +); diff --git a/packages/workspaces/storybook.preview.ts b/packages/workspaces/storybook.preview.ts new file mode 100644 index 00000000..c01b6f9c --- /dev/null +++ b/packages/workspaces/storybook.preview.ts @@ -0,0 +1 @@ +import "./src/index.css"; diff --git a/packages/workspaces/tsconfig.json b/packages/workspaces/tsconfig.json new file mode 100644 index 00000000..27059a98 --- /dev/null +++ b/packages/workspaces/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.packages.json", + "compilerOptions": { + "paths": { + "@repo/shared": ["../shared/src"], + "@repo/webview-shared": ["../webview-shared/src"] + } + }, + "include": ["src", "storybook.preview.ts"] +} diff --git a/packages/workspaces/vite.config.ts b/packages/workspaces/vite.config.ts new file mode 100644 index 00000000..ebbae452 --- /dev/null +++ b/packages/workspaces/vite.config.ts @@ -0,0 +1,3 @@ +import { createReactWebviewConfig } from "../webview-shared/createWebviewConfig"; + +export default createReactWebviewConfig("workspaces", __dirname); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a409aa6e..8a93e1e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -454,6 +454,61 @@ importers: specifier: 'catalog:' version: 6.0.3 + packages/workspaces: + dependencies: + '@repo/shared': + specifier: workspace:* + version: link:../shared + '@repo/webview-shared': + specifier: workspace:* + version: link:../webview-shared + '@tanstack/react-query': + specifier: 'catalog:' + version: 5.100.9(react@19.2.6) + '@vscode-elements/react-elements': + specifier: 'catalog:' + version: 2.4.0(@types/react@19.2.14)(@vscode/codicons@0.0.45)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@vscode/codicons': + specifier: 'catalog:' + version: 0.0.45 + date-fns: + specifier: 'catalog:' + version: 4.1.0 + react: + specifier: 'catalog:' + version: 19.2.6 + react-dom: + specifier: 'catalog:' + version: 19.2.6(react@19.2.6) + devDependencies: + '@repo/mocks': + specifier: workspace:* + version: link:../mocks + '@repo/storybook-utils': + specifier: workspace:* + version: link:../storybook-utils + '@rolldown/plugin-babel': + specifier: 'catalog:' + version: 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.18)(vite@8.0.11(@types/node@24.10.12)(esbuild@0.28.0)) + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.18)(vite@8.0.11(@types/node@24.10.12)(esbuild@0.28.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.11(@types/node@24.10.12)(esbuild@0.28.0)) + babel-plugin-react-compiler: + specifier: 'catalog:' + version: 1.0.0 + typescript: + specifier: 'catalog:' + version: 6.0.3 + vite: + specifier: 'catalog:' + version: 8.0.11(@types/node@24.10.12)(esbuild@0.28.0) + packages: '@abraham/reflection@0.13.0': diff --git a/src/core/commandManager.ts b/src/core/commandManager.ts index 241183c1..52d6d5eb 100644 --- a/src/core/commandManager.ts +++ b/src/core/commandManager.ts @@ -15,6 +15,7 @@ export const CODER_COMMAND_IDS = [ "coder.openFromSidebar", "coder.openAppStatus", "coder.workspace.update", + "coder.workspaces.refresh", "coder.createWorkspace", "coder.navigateToWorkspace", "coder.navigateToWorkspaceSettings", diff --git a/src/core/contextManager.ts b/src/core/contextManager.ts index 8facb2ea..61c9d3a8 100644 --- a/src/core/contextManager.ts +++ b/src/core/contextManager.ts @@ -7,6 +7,7 @@ const CONTEXT_DEFAULTS = { "coder.agentsEnabled": false, "coder.workspace.connected": false, "coder.workspace.updatable": false, + "coder.experimental.webkitWorkspaces": false, } as const; type CoderContext = keyof typeof CONTEXT_DEFAULTS; diff --git a/src/extension.ts b/src/extension.ts index cf310bce..0e045781 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,7 @@ import { registerUriHandler } from "./uri/uriHandler"; import { initVscodeProposed } from "./vscodeProposed"; import { ChatPanelProvider } from "./webviews/chat/chatPanelProvider"; import { TasksPanelProvider } from "./webviews/tasks/tasksPanelProvider"; +import { ExperimentalWorkspacesPanelProvider } from "./webviews/workspaces/workspacesPanelProvider"; import { WorkspaceProvider, WorkspaceQuery, @@ -265,6 +266,40 @@ async function doActivate( ), ); + // Register Experimental Workspaces webview panel (behind configuration setting) + const workspacesPanelEnabled = vscode.workspace + .getConfiguration("coder") + .get("experimental.workspacesPanel", false); + + // Set context first so the view visibility is correct + contextManager.set( + "coder.experimental.webkitWorkspaces", + workspacesPanelEnabled, + ); + + if (workspacesPanelEnabled) { + const workspacesPanelProvider = new ExperimentalWorkspacesPanelProvider( + ctx.extensionUri, + client, + output, + ); + commandManager.register("coder.workspaces.refresh", () => + workspacesPanelProvider.refresh(), + ); + ctx.subscriptions.push( + workspacesPanelProvider, + vscode.window.registerWebviewViewProvider( + ExperimentalWorkspacesPanelProvider.viewType, + workspacesPanelProvider, + { webviewOptions: { retainContextWhenHidden: true } }, + ), + // Refresh workspaces panel when deployment changes (login/logout/switch) + secretsManager.onDidChangeCurrentDeployment(() => + workspacesPanelProvider.refresh(), + ), + ); + } + ctx.subscriptions.push( registerUriHandler({ serviceContainer, diff --git a/src/webviews/workspaces/workspacesPanelProvider.ts b/src/webviews/workspaces/workspacesPanelProvider.ts new file mode 100644 index 00000000..4c9476d7 --- /dev/null +++ b/src/webviews/workspaces/workspacesPanelProvider.ts @@ -0,0 +1,100 @@ +import * as vscode from "vscode"; + +import { type CoderApi } from "../../api/coderApi"; +import { type Logger } from "../../logging/logger"; +import { + dispatchCommand, + dispatchRequest, + isIpcCommand, + isIpcRequest, + notifyWebview, +} from "../dispatch"; +import { getWebviewHtml } from "../html"; + +export class ExperimentalWorkspacesPanelProvider + implements vscode.WebviewViewProvider, vscode.Disposable +{ + public static readonly viewType = "coder.experimental.workspacesPanel"; + + private view?: vscode.WebviewView; + private disposables: vscode.Disposable[] = []; + + // Empty handlers for now - will be populated as we build out the API + private readonly requestHandlers = {}; + private readonly commandHandlers = {}; + + constructor( + private readonly extensionUri: vscode.Uri, + private readonly client: CoderApi, + private readonly logger: Logger, + ) {} + + public refresh(): void { + // TODO: Implement refresh logic + this.logger.debug("Workspaces panel refresh requested"); + } + + resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ): void { + this.view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this.extensionUri, "dist", "webviews"), + ], + }; + + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + + this.disposables.push( + webviewView.webview.onDidReceiveMessage((message: unknown) => { + this.handleMessage(message).catch((err: unknown) => { + this.logger.error("Unhandled error in message handler", err); + }); + }), + ); + + webviewView.webview.html = getWebviewHtml( + webviewView.webview, + this.extensionUri, + "workspaces", + "Coder Workspaces", + ); + + webviewView.onDidDispose(() => { + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + }); + } + + private async handleMessage(message: unknown): Promise { + const showErrorToUser = () => false; + if (isIpcRequest(message)) { + await dispatchRequest(message, this.requestHandlers, this.view?.webview, { + logger: this.logger, + showErrorToUser, + }); + } else if (isIpcCommand(message)) { + await dispatchCommand(message, this.commandHandlers, { + logger: this.logger, + showErrorToUser, + }); + } + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + } +} From d963f1de267534cc97c36bef73f3b83cfacb618f Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 12 May 2026 04:10:22 +0000 Subject: [PATCH 2/5] fix: remove unregistered coder.workspaces.refresh command --- src/core/commandManager.ts | 1 - src/extension.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/commandManager.ts b/src/core/commandManager.ts index 52d6d5eb..241183c1 100644 --- a/src/core/commandManager.ts +++ b/src/core/commandManager.ts @@ -15,7 +15,6 @@ export const CODER_COMMAND_IDS = [ "coder.openFromSidebar", "coder.openAppStatus", "coder.workspace.update", - "coder.workspaces.refresh", "coder.createWorkspace", "coder.navigateToWorkspace", "coder.navigateToWorkspaceSettings", diff --git a/src/extension.ts b/src/extension.ts index 0e045781..3580d0ee 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -283,9 +283,7 @@ async function doActivate( client, output, ); - commandManager.register("coder.workspaces.refresh", () => - workspacesPanelProvider.refresh(), - ); + ctx.subscriptions.push( workspacesPanelProvider, vscode.window.registerWebviewViewProvider( From b8984819bf9bbaa3b932b67e6a5203da5557fac9 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 12 May 2026 14:12:36 +1000 Subject: [PATCH 3/5] fix: remove unused import --- src/webviews/workspaces/workspacesPanelProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webviews/workspaces/workspacesPanelProvider.ts b/src/webviews/workspaces/workspacesPanelProvider.ts index 4c9476d7..0c6c12c1 100644 --- a/src/webviews/workspaces/workspacesPanelProvider.ts +++ b/src/webviews/workspaces/workspacesPanelProvider.ts @@ -7,7 +7,6 @@ import { dispatchRequest, isIpcCommand, isIpcRequest, - notifyWebview, } from "../dispatch"; import { getWebviewHtml } from "../html"; From 58075d54599ce8752968fa7048f5d6ecbc3f894e Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 12 May 2026 14:17:39 +1000 Subject: [PATCH 4/5] feat: implement basic api handler --- packages/shared/src/index.ts | 3 ++ packages/shared/src/workspaces/api.ts | 28 +++++++++++++++++++ .../workspaces/workspacesPanelProvider.ts | 13 +++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 packages/shared/src/workspaces/api.ts diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index b403822e..f9b553c0 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -19,3 +19,6 @@ export { // Chat API export { ChatApi } from "./chat/api"; + +// Workspaces API +export { WorkspacesApi } from "./workspaces/api"; diff --git a/packages/shared/src/workspaces/api.ts b/packages/shared/src/workspaces/api.ts new file mode 100644 index 00000000..cf321141 --- /dev/null +++ b/packages/shared/src/workspaces/api.ts @@ -0,0 +1,28 @@ +/** + * Workspaces API - Type-safe message definitions for the Workspaces webview. + * + * Usage: + * ```tsx + * const ipc = useIpc(); + * const workspaces = await ipc.request(WorkspacesApi.getWorkspaces); + * ipc.command(WorkspacesApi.openWorkspace, { workspaceId: "..." }); + * ``` + */ + +// import { +// defineCommand, +// defineNotification, +// defineRequest, +// } from "../ipc/protocol"; + +// TODO: Add workspace types as needed +// For now, this is an empty API to provide compile-time safety + +export const WorkspacesApi = { + // Requests will be added here as the feature is developed + // Example: getWorkspaces: defineRequest("getWorkspaces"), + // Commands will be added here as needed + // Example: openWorkspace: defineCommand<{ workspaceId: string }>("openWorkspace"), + // Notifications will be added here as needed + // Example: refresh: defineNotification("refresh"), +} as const; diff --git a/src/webviews/workspaces/workspacesPanelProvider.ts b/src/webviews/workspaces/workspacesPanelProvider.ts index 0c6c12c1..546fc7a4 100644 --- a/src/webviews/workspaces/workspacesPanelProvider.ts +++ b/src/webviews/workspaces/workspacesPanelProvider.ts @@ -1,5 +1,11 @@ import * as vscode from "vscode"; +import { + buildCommandHandlers, + buildRequestHandlers, + WorkspacesApi, +} from "@repo/shared"; + import { type CoderApi } from "../../api/coderApi"; import { type Logger } from "../../logging/logger"; import { @@ -18,9 +24,10 @@ export class ExperimentalWorkspacesPanelProvider private view?: vscode.WebviewView; private disposables: vscode.Disposable[] = []; - // Empty handlers for now - will be populated as we build out the API - private readonly requestHandlers = {}; - private readonly commandHandlers = {}; + // Use buildRequestHandlers/buildCommandHandlers for compile-time safety + // Empty for now, but will enforce exhaustiveness when WorkspacesApi is defined + private readonly requestHandlers = buildRequestHandlers(WorkspacesApi, {}); + private readonly commandHandlers = buildCommandHandlers(WorkspacesApi, {}); constructor( private readonly extensionUri: vscode.Uri, From 8485cdd931ca17c4afa9d95b40a366fb0ccc0abc Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 12 May 2026 15:22:23 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20address=20PR=20r?= =?UTF-8?q?eview=20feedback=20for=20experimental=20workspaces=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove verbose comments from WorkspacesApi (api.ts) - Remove setting definition from package.json (VS Code reads settings.json directly) - Rename ExperimentalWorkspacesPanelProvider → WorkspacesPanelProvider - Rename context key: coder.experimental.webkitWorkspaces → coder.workspacesPanelEnabled - Remove unused client: CoderApi constructor param - Use import type for Logger - Tighten localResourceRoots to dist/webviews/workspaces - Drop showErrorToUser (optional, matches chat panel pattern) - Extract disposeView() helper to deduplicate cleanup logic - Add else branch to log unexpected messages - Use shorthand-logo.svg icon for view container - Remove unnecessary comments from extension.ts registration - Clean up README (remove fragile implementation details) --- package.json | 17 ++------ packages/shared/src/workspaces/api.ts | 29 +------------ packages/workspaces/README.md | 13 ++---- src/core/contextManager.ts | 2 +- src/extension.ts | 15 ++----- .../workspaces/workspacesPanelProvider.ts | 42 +++++++++---------- 6 files changed, 32 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 60df2060..7e47ac1a 100644 --- a/package.json +++ b/package.json @@ -149,17 +149,6 @@ "experimental" ] }, - "coder.experimental.workspacesPanel": { - "markdownDescription": "Enable the experimental Workspaces webview panel. This feature is in active development.", - "type": "boolean", - "default": false, - "tags": [ - "experimental" - ], - "scope": "application", - "ignoreSync": true, - "order": 999 - }, "coder.sshFlags": { "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). Prefer `#coder.proxyLogDirectory#` over `--log-dir`/`-l` for full functionality.", "type": "array", @@ -346,10 +335,10 @@ "coderExperimentalWorkspaces": [ { "type": "webview", - "id": "coder.experimental.workspacesPanel", + "id": "coder.workspacesPanel", "name": "Workspaces", - "icon": "media/logo-white.svg", - "when": "coder.authenticated && coder.experimental.webkitWorkspaces" + "icon": "media/shorthand-logo.svg", + "when": "coder.authenticated && coder.workspacesPanelEnabled" } ] }, diff --git a/packages/shared/src/workspaces/api.ts b/packages/shared/src/workspaces/api.ts index cf321141..508b2e5b 100644 --- a/packages/shared/src/workspaces/api.ts +++ b/packages/shared/src/workspaces/api.ts @@ -1,28 +1 @@ -/** - * Workspaces API - Type-safe message definitions for the Workspaces webview. - * - * Usage: - * ```tsx - * const ipc = useIpc(); - * const workspaces = await ipc.request(WorkspacesApi.getWorkspaces); - * ipc.command(WorkspacesApi.openWorkspace, { workspaceId: "..." }); - * ``` - */ - -// import { -// defineCommand, -// defineNotification, -// defineRequest, -// } from "../ipc/protocol"; - -// TODO: Add workspace types as needed -// For now, this is an empty API to provide compile-time safety - -export const WorkspacesApi = { - // Requests will be added here as the feature is developed - // Example: getWorkspaces: defineRequest("getWorkspaces"), - // Commands will be added here as needed - // Example: openWorkspace: defineCommand<{ workspaceId: string }>("openWorkspace"), - // Notifications will be added here as needed - // Example: refresh: defineNotification("refresh"), -} as const; +export const WorkspacesApi = {} as const; diff --git a/packages/workspaces/README.md b/packages/workspaces/README.md index c899337a..f83b897a 100644 --- a/packages/workspaces/README.md +++ b/packages/workspaces/README.md @@ -1,14 +1,12 @@ -# Coder Experimental Workspaces Webview Panel +# Coder Workspaces Webview Panel -This package contains the experimental Workspaces webview panel for the Coder VS Code extension. - -This feature is currently in development and hidden from the Settings UI. +This package contains the Workspaces webview panel for the Coder VS Code extension. ## Enabling the Feature The workspaces panel is controlled by the `coder.experimental.workspacesPanel` configuration setting. -**This setting is hidden from the Settings UI** - it can only be enabled via settings.json: +To enable via settings.json: 1. Open your VS Code settings.json (Cmd/Ctrl + Shift + P → "Preferences: Open User Settings (JSON)") 2. Add the following: @@ -21,7 +19,4 @@ The workspaces panel is controlled by the `coder.experimental.workspacesPanel` c 3. Reload VS Code -A new activity bar icon labeled **"Coder Remote (New)"** will appear in the activity bar when the setting is enabled. This creates a completely separate panel alongside the existing "Coder Remote" and "Coder Tasks" panels, allowing easy side-by-side comparison during development. - -> [!NOTE] -> The new view will only appear after you instantiate the Coder context (i.e clicking Tasks or Workspaces). +A new activity bar icon labeled **"Coder Remote (New)"** will appear when the setting is enabled. diff --git a/src/core/contextManager.ts b/src/core/contextManager.ts index 61c9d3a8..179a1623 100644 --- a/src/core/contextManager.ts +++ b/src/core/contextManager.ts @@ -7,7 +7,7 @@ const CONTEXT_DEFAULTS = { "coder.agentsEnabled": false, "coder.workspace.connected": false, "coder.workspace.updatable": false, - "coder.experimental.webkitWorkspaces": false, + "coder.workspacesPanelEnabled": false, } as const; type CoderContext = keyof typeof CONTEXT_DEFAULTS; diff --git a/src/extension.ts b/src/extension.ts index 3580d0ee..59b8a093 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,7 +25,7 @@ import { registerUriHandler } from "./uri/uriHandler"; import { initVscodeProposed } from "./vscodeProposed"; import { ChatPanelProvider } from "./webviews/chat/chatPanelProvider"; import { TasksPanelProvider } from "./webviews/tasks/tasksPanelProvider"; -import { ExperimentalWorkspacesPanelProvider } from "./webviews/workspaces/workspacesPanelProvider"; +import { WorkspacesPanelProvider } from "./webviews/workspaces/workspacesPanelProvider"; import { WorkspaceProvider, WorkspaceQuery, @@ -266,32 +266,25 @@ async function doActivate( ), ); - // Register Experimental Workspaces webview panel (behind configuration setting) const workspacesPanelEnabled = vscode.workspace .getConfiguration("coder") .get("experimental.workspacesPanel", false); - // Set context first so the view visibility is correct - contextManager.set( - "coder.experimental.webkitWorkspaces", - workspacesPanelEnabled, - ); + contextManager.set("coder.workspacesPanelEnabled", workspacesPanelEnabled); if (workspacesPanelEnabled) { - const workspacesPanelProvider = new ExperimentalWorkspacesPanelProvider( + const workspacesPanelProvider = new WorkspacesPanelProvider( ctx.extensionUri, - client, output, ); ctx.subscriptions.push( workspacesPanelProvider, vscode.window.registerWebviewViewProvider( - ExperimentalWorkspacesPanelProvider.viewType, + WorkspacesPanelProvider.viewType, workspacesPanelProvider, { webviewOptions: { retainContextWhenHidden: true } }, ), - // Refresh workspaces panel when deployment changes (login/logout/switch) secretsManager.onDidChangeCurrentDeployment(() => workspacesPanelProvider.refresh(), ), diff --git a/src/webviews/workspaces/workspacesPanelProvider.ts b/src/webviews/workspaces/workspacesPanelProvider.ts index 546fc7a4..cac04d7c 100644 --- a/src/webviews/workspaces/workspacesPanelProvider.ts +++ b/src/webviews/workspaces/workspacesPanelProvider.ts @@ -6,8 +6,6 @@ import { WorkspacesApi, } from "@repo/shared"; -import { type CoderApi } from "../../api/coderApi"; -import { type Logger } from "../../logging/logger"; import { dispatchCommand, dispatchRequest, @@ -16,27 +14,25 @@ import { } from "../dispatch"; import { getWebviewHtml } from "../html"; -export class ExperimentalWorkspacesPanelProvider +import type { Logger } from "../../logging/logger"; + +export class WorkspacesPanelProvider implements vscode.WebviewViewProvider, vscode.Disposable { - public static readonly viewType = "coder.experimental.workspacesPanel"; + public static readonly viewType = "coder.workspacesPanel"; private view?: vscode.WebviewView; private disposables: vscode.Disposable[] = []; - // Use buildRequestHandlers/buildCommandHandlers for compile-time safety - // Empty for now, but will enforce exhaustiveness when WorkspacesApi is defined private readonly requestHandlers = buildRequestHandlers(WorkspacesApi, {}); private readonly commandHandlers = buildCommandHandlers(WorkspacesApi, {}); constructor( private readonly extensionUri: vscode.Uri, - private readonly client: CoderApi, private readonly logger: Logger, ) {} public refresh(): void { - // TODO: Implement refresh logic this.logger.debug("Workspaces panel refresh requested"); } @@ -50,14 +46,16 @@ export class ExperimentalWorkspacesPanelProvider webviewView.webview.options = { enableScripts: true, localResourceRoots: [ - vscode.Uri.joinPath(this.extensionUri, "dist", "webviews"), + vscode.Uri.joinPath( + this.extensionUri, + "dist", + "webviews", + "workspaces", + ), ], }; - for (const d of this.disposables) { - d.dispose(); - } - this.disposables = []; + this.disposeView(); this.disposables.push( webviewView.webview.onDidReceiveMessage((message: unknown) => { @@ -74,33 +72,31 @@ export class ExperimentalWorkspacesPanelProvider "Coder Workspaces", ); - webviewView.onDidDispose(() => { - for (const d of this.disposables) { - d.dispose(); - } - this.disposables = []; - }); + webviewView.onDidDispose(() => this.disposeView()); } private async handleMessage(message: unknown): Promise { - const showErrorToUser = () => false; if (isIpcRequest(message)) { await dispatchRequest(message, this.requestHandlers, this.view?.webview, { logger: this.logger, - showErrorToUser, }); } else if (isIpcCommand(message)) { await dispatchCommand(message, this.commandHandlers, { logger: this.logger, - showErrorToUser, }); + } else { + this.logger.warn("Unexpected webview message", message); } } - dispose(): void { + private disposeView(): void { for (const d of this.disposables) { d.dispose(); } this.disposables = []; } + + dispose(): void { + this.disposeView(); + } }