Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 15 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 @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export {

// Chat API
export { ChatApi } from "./chat/api";

// Workspaces API
export { WorkspacesApi } from "./workspaces/api";
1 change: 1 addition & 0 deletions packages/shared/src/workspaces/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WorkspacesApi = {} as const;
22 changes: 22 additions & 0 deletions packages/workspaces/README.md
Original file line number Diff line number Diff line change
@@ -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
}
```
Comment thread
EhabY marked this conversation as resolved.

3. Reload VS Code

A new activity bar icon labeled **"Coder Remote (New)"** will appear when the setting is enabled.
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/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.workspacesPanelEnabled": false,
} as const;

type CoderContext = keyof typeof CONTEXT_DEFAULTS;
Expand Down
26 changes: 26 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 { WorkspacesPanelProvider } from "./webviews/workspaces/workspacesPanelProvider";
import {
WorkspaceProvider,
WorkspaceQuery,
Expand Down Expand Up @@ -265,6 +266,31 @@ async function doActivate(
),
);

const workspacesPanelEnabled = vscode.workspace
.getConfiguration("coder")
.get<boolean>("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,
Expand Down
102 changes: 102 additions & 0 deletions src/webviews/workspaces/workspacesPanelProvider.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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,
});
Comment thread
EhabY marked this conversation as resolved.
} 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();
}
}
Loading