diff --git a/package.json b/package.json
index 1066eb9e..7e47ac1a 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",
@@ -277,6 +277,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 +331,15 @@
"icon": "media/shorthand-logo.svg",
"when": "coder.agentsEnabled"
}
+ ],
+ "coderExperimentalWorkspaces": [
+ {
+ "type": "webview",
+ "id": "coder.workspacesPanel",
+ "name": "Workspaces",
+ "icon": "media/shorthand-logo.svg",
+ "when": "coder.authenticated && coder.workspacesPanelEnabled"
+ }
]
},
"viewsWelcome": [
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..508b2e5b
--- /dev/null
+++ b/packages/shared/src/workspaces/api.ts
@@ -0,0 +1 @@
+export const WorkspacesApi = {} as const;
diff --git a/packages/workspaces/README.md b/packages/workspaces/README.md
new file mode 100644
index 00000000..f83b897a
--- /dev/null
+++ b/packages/workspaces/README.md
@@ -0,0 +1,22 @@
+# Coder Workspaces Webview Panel
+
+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.
+
+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:
+
+```json
+{
+ "coder.experimental.workspacesPanel": true
+}
+```
+
+3. Reload VS Code
+
+A new activity bar icon labeled **"Coder Remote (New)"** will appear when the setting is enabled.
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/contextManager.ts b/src/core/contextManager.ts
index 8facb2ea..179a1623 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.workspacesPanelEnabled": false,
} as const;
type CoderContext = keyof typeof CONTEXT_DEFAULTS;
diff --git a/src/extension.ts b/src/extension.ts
index cf310bce..59b8a093 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 { WorkspacesPanelProvider } from "./webviews/workspaces/workspacesPanelProvider";
import {
WorkspaceProvider,
WorkspaceQuery,
@@ -265,6 +266,31 @@ async function doActivate(
),
);
+ const workspacesPanelEnabled = vscode.workspace
+ .getConfiguration("coder")
+ .get("experimental.workspacesPanel", false);
+
+ contextManager.set("coder.workspacesPanelEnabled", workspacesPanelEnabled);
+
+ if (workspacesPanelEnabled) {
+ const workspacesPanelProvider = new WorkspacesPanelProvider(
+ ctx.extensionUri,
+ output,
+ );
+
+ ctx.subscriptions.push(
+ workspacesPanelProvider,
+ vscode.window.registerWebviewViewProvider(
+ WorkspacesPanelProvider.viewType,
+ workspacesPanelProvider,
+ { webviewOptions: { retainContextWhenHidden: true } },
+ ),
+ 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..cac04d7c
--- /dev/null
+++ b/src/webviews/workspaces/workspacesPanelProvider.ts
@@ -0,0 +1,102 @@
+import * as vscode from "vscode";
+
+import {
+ buildCommandHandlers,
+ buildRequestHandlers,
+ WorkspacesApi,
+} from "@repo/shared";
+
+import {
+ dispatchCommand,
+ dispatchRequest,
+ isIpcCommand,
+ isIpcRequest,
+} from "../dispatch";
+import { getWebviewHtml } from "../html";
+
+import type { Logger } from "../../logging/logger";
+
+export class WorkspacesPanelProvider
+ implements vscode.WebviewViewProvider, vscode.Disposable
+{
+ public static readonly viewType = "coder.workspacesPanel";
+
+ private view?: vscode.WebviewView;
+ private disposables: vscode.Disposable[] = [];
+
+ private readonly requestHandlers = buildRequestHandlers(WorkspacesApi, {});
+ private readonly commandHandlers = buildCommandHandlers(WorkspacesApi, {});
+
+ constructor(
+ private readonly extensionUri: vscode.Uri,
+ private readonly logger: Logger,
+ ) {}
+
+ public refresh(): void {
+ 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",
+ "workspaces",
+ ),
+ ],
+ };
+
+ this.disposeView();
+
+ 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(() => this.disposeView());
+ }
+
+ private async handleMessage(message: unknown): Promise {
+ if (isIpcRequest(message)) {
+ await dispatchRequest(message, this.requestHandlers, this.view?.webview, {
+ logger: this.logger,
+ });
+ } else if (isIpcCommand(message)) {
+ await dispatchCommand(message, this.commandHandlers, {
+ logger: this.logger,
+ });
+ } else {
+ this.logger.warn("Unexpected webview message", message);
+ }
+ }
+
+ private disposeView(): void {
+ for (const d of this.disposables) {
+ d.dispose();
+ }
+ this.disposables = [];
+ }
+
+ dispose(): void {
+ this.disposeView();
+ }
+}