Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -149,6 +149,17 @@
"experimental"
]
},
"coder.experimental.workspacesPanel": {
Comment thread
EhabY marked this conversation as resolved.
Outdated
"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",
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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",
Comment thread
EhabY marked this conversation as resolved.
Outdated
"when": "coder.authenticated && coder.experimental.webkitWorkspaces"
}
]
},
"viewsWelcome": [
Expand Down
27 changes: 27 additions & 0 deletions packages/workspaces/README.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
EhabY marked this conversation as resolved.
Outdated

## 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
}
```
Comment thread
EhabY marked this conversation as resolved.

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).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I know why this exists, I do a similar hack for the Coder Tasks where we register a dummy empty view that shows the login button if it's not authenticated but show the webview when it is. Maybe we should do something like this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted — didn't add the login view pattern in this pass since it's more of a follow-up concern, but it's a good idea. Could be a separate PR once we flesh out the panel.

🤖 Generated with Coder Agents

33 changes: 33 additions & 0 deletions packages/workspaces/package.json
Original file line number Diff line number Diff line change
@@ -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:"
}
}
3 changes: 3 additions & 0 deletions packages/workspaces/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function App() {
return <div>TODO</div>;
}
1 change: 1 addition & 0 deletions packages/workspaces/src/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";
1 change: 1 addition & 0 deletions packages/workspaces/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* TODO */
26 changes: 26 additions & 0 deletions packages/workspaces/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<App />
</ErrorBoundary>
</QueryClientProvider>
</StrictMode>,
);
1 change: 1 addition & 0 deletions packages/workspaces/storybook.preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./src/index.css";
10 changes: 10 additions & 0 deletions packages/workspaces/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
3 changes: 3 additions & 0 deletions packages/workspaces/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createReactWebviewConfig } from "../webview-shared/createWebviewConfig";

export default createReactWebviewConfig("workspaces", __dirname);
55 changes: 55 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/core/commandManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const CODER_COMMAND_IDS = [
"coder.openFromSidebar",
"coder.openAppStatus",
"coder.workspace.update",
"coder.workspaces.refresh",
Comment thread
jakehwll marked this conversation as resolved.
Outdated
"coder.createWorkspace",
"coder.navigateToWorkspace",
"coder.navigateToWorkspaceSettings",
Expand Down
1 change: 1 addition & 0 deletions src/core/contextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const CONTEXT_DEFAULTS = {
"coder.agentsEnabled": false,
"coder.workspace.connected": false,
"coder.workspace.updatable": false,
"coder.experimental.webkitWorkspaces": false,
Comment thread
EhabY marked this conversation as resolved.
Outdated
} as const;

type CoderContext = keyof typeof CONTEXT_DEFAULTS;
Expand Down
35 changes: 35 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -265,6 +266,40 @@ async function doActivate(
),
);

// Register Experimental Workspaces webview panel (behind configuration setting)
const workspacesPanelEnabled = vscode.workspace
.getConfiguration("coder")
.get<boolean>("experimental.workspacesPanel", false);

// Set context first so the view visibility is correct
contextManager.set(
"coder.experimental.webkitWorkspaces",
workspacesPanelEnabled,
);
Comment thread
EhabY marked this conversation as resolved.
Outdated

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,
Expand Down
100 changes: 100 additions & 0 deletions src/webviews/workspaces/workspacesPanelProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as vscode from "vscode";

import { type CoderApi } from "../../api/coderApi";
import { type Logger } from "../../logging/logger";
Comment thread
EhabY marked this conversation as resolved.
Outdated
import {
dispatchCommand,
dispatchRequest,
isIpcCommand,
isIpcRequest,
notifyWebview,

Check failure on line 10 in src/webviews/workspaces/workspacesPanelProvider.ts

View workflow job for this annotation

GitHub Actions / Lint

'notifyWebview' is defined but never used. Allowed unused vars must match /^_/u
} from "../dispatch";
import { getWebviewHtml } from "../html";

export class ExperimentalWorkspacesPanelProvider
implements vscode.WebviewViewProvider, vscode.Disposable
{
public static readonly viewType = "coder.experimental.workspacesPanel";
Comment thread
EhabY marked this conversation as resolved.
Outdated

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 = {};
Comment thread
jakehwll marked this conversation as resolved.
Outdated

constructor(
private readonly extensionUri: vscode.Uri,
private readonly client: CoderApi,
Comment thread
EhabY marked this conversation as resolved.
Outdated
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"),
Comment thread
EhabY marked this conversation as resolved.
Outdated
],
};

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 = [];
Comment thread
EhabY marked this conversation as resolved.
Outdated
});
}

private async handleMessage(message: unknown): Promise<void> {
const showErrorToUser = () => false;
Comment thread
EhabY marked this conversation as resolved.
Outdated
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,
});
Comment thread
EhabY marked this conversation as resolved.
}
}

dispose(): void {
for (const d of this.disposables) {
d.dispose();
}
this.disposables = [];
}
}
Loading