diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..3fc94becb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+lib/vscode
+node_modules
+dist
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..88ad451ec
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# vscode-cloud
+
+Run VS Code in the cloud.
+
+## Contributing
+
+### Getting the source
+
+```
+git clone https://github.com/codercom/vscode-cloud
+```
+
+### Installing dependencies
+
+```
+cd vscode-cloud
+yarn
+```
+
+### Run
+
+```
+yarn start
+```
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..1b9e170c3
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@coder/vscode-cloud",
+ "repository": "https://github.com/codercom/vscode-cloud",
+ "author": "Coder",
+ "license": "TBD",
+ "description": "VS Code in the cloud.",
+ "scripts": {
+ "vscode:clone": "mkdir -p ./lib && test -d ./lib/vscode || git clone https://github.com/Microsoft/vscode/ ./lib/vscode",
+ "vscode:install": "cd ./lib/vscode && git checkout tags/1.30.1 && yarn",
+ "vscode": "npm-run-all vscode:*",
+ "packages:install": "cd ./packages && yarn",
+ "postinstall": "npm-run-all --parallel vscode packages:install",
+ "start": "webpack-dev-server --config ./webpack.config.app.js",
+ "test": "cd ./packages && yarn test"
+ },
+ "devDependencies": {
+ "npm-run-all": "^4.1.5"
+ }
+}
diff --git a/packages/app/src/index.html b/packages/app/src/index.html
new file mode 100644
index 000000000..b9ab7b468
--- /dev/null
+++ b/packages/app/src/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ VS Code
+
+
+
+
+
+
+
+
diff --git a/packages/app/src/index.scss b/packages/app/src/index.scss
new file mode 100644
index 000000000..ae57f9859
--- /dev/null
+++ b/packages/app/src/index.scss
@@ -0,0 +1,144 @@
+html, body {
+ height: 100%;
+ margin: 0;
+ width: 100%;
+}
+
+#overlay {
+ background: rgba(0, 0, 0, 0.2);
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+#overlay {
+ align-items: center;
+ background-color: #252526;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ justify-content: center;
+ left: 0;
+ opacity: 1;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: 150ms opacity ease;
+ z-index: 2;
+}
+
+#overlay>.message {
+ color: white;
+ margin-top: 10px;
+ opacity: 0.5;
+}
+
+#overlay.error>.message {
+ color: white;
+ opacity: 0.3;
+}
+
+#overlay>.activitybar {
+ background-color: rgb(44, 44, 44);
+ bottom: 0;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 50px;
+}
+
+#overlay>.activitybar svg {
+ fill: white;
+ margin-left: 2px;
+ margin-top: 2px;
+ opacity: 0.3;
+}
+
+#overlay.error>#status {
+ opacity: 0;
+}
+
+#overlay>.statusbar {
+ background-color: rgb(0, 122, 204);
+ bottom: 0;
+ cursor: default;
+ height: 22px;
+ left: 0;
+ position: absolute;
+ right: 0;
+}
+
+#logo {
+ transform-style: preserve-3d;
+}
+
+#logo>svg {
+ fill: rgb(0, 122, 204);
+ opacity: 1;
+ width: 100px;
+}
+
+#status {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 5px;
+ box-shadow: 0px 2px 10px -2px rgba(0, 0, 0, 0.75);
+ color: white;
+ font-size: 0.9em;
+ margin-top: 15px;
+ min-width: 100px;
+ position: relative;
+ transition: 300ms opacity ease;
+}
+
+#status>#progress {
+ background: rgba(0, 0, 0, 0.2);
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ bottom: 0;
+ height: 3px;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+}
+
+@-moz-keyframes statusProgress {
+ 0% {
+ background-position: 0% 50%
+ }
+
+ 50% {
+ background-position: 100% 50%
+ }
+
+ 100% {
+ background-position: 0% 50%
+ }
+}
+
+@keyframes statusProgress {
+ 0% {
+ background-position: 0% 50%
+ }
+
+ 50% {
+ background-position: 100% 50%
+ }
+
+ 100% {
+ background-position: 0% 50%
+ }
+}
+
+#status>#progress>#fill {
+ animation: statusProgress 2s ease infinite;
+ background-size: 400% 400%;
+ background: linear-gradient(270deg, #007acc, #0016cc);
+ height: 100%;
+ transition: 500ms width ease;
+ width: 0%;
+}
diff --git a/packages/disposable/package.json b/packages/disposable/package.json
new file mode 100644
index 000000000..28c7886aa
--- /dev/null
+++ b/packages/disposable/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "@coder/disposable",
+ "main": "src/disposable.ts"
+}
diff --git a/packages/disposable/src/disposable.ts b/packages/disposable/src/disposable.ts
new file mode 100644
index 000000000..273a45adc
--- /dev/null
+++ b/packages/disposable/src/disposable.ts
@@ -0,0 +1,5 @@
+export interface IDisposable {
+
+ dispose(): void;
+
+}
diff --git a/packages/electron-browser/package.json b/packages/electron-browser/package.json
new file mode 100644
index 000000000..1ec31a661
--- /dev/null
+++ b/packages/electron-browser/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@coder/electron-browser",
+ "description": "A browser implementation of Electron's API.",
+ "main": "src/index.ts",
+ "devDependencies": {
+ "electron": "^4.0.1"
+ }
+}
diff --git a/packages/electron-browser/src/dialog.scss b/packages/electron-browser/src/dialog.scss
new file mode 100644
index 000000000..e804f5a18
--- /dev/null
+++ b/packages/electron-browser/src/dialog.scss
@@ -0,0 +1,68 @@
+.msgbox {
+ padding-top: 25px;
+ padding-left: 40px;
+ padding-right: 40px;
+ padding-bottom: 25px;
+ background: #242424;
+ -webkit-box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75);
+ -moz-box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75);
+ box-shadow: 0px 0px 10px -3px rgba(0,0,0,0.75);
+ border-radius: 3px;
+}
+
+.msgbox.input {
+ max-width: 500px;
+ width: 100%;
+}
+
+.msgbox > .input {
+ background: #141414;
+ border: none;
+ box-sizing: border-box;
+ margin-bottom: 25px;
+ padding: 10px;
+ width: 100%;
+}
+
+.msgbox > .msg {
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.msgbox > .detail {
+ font-size: 14px;
+ margin-top: 5px;
+}
+
+.msgbox > .errors {
+ margin-bottom: 25px;
+}
+
+.msgbox > .errors {
+ color: #f44747;
+}
+
+.msgbox > .button-wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.msgbox > .button-wrapper > button {
+ flex: 1;
+ border-radius: 2px;
+ padding: 10px;
+ color: white;
+ background: #3d3d3d;
+ border: 0px;
+ cursor: pointer;
+ opacity: 0.8;
+}
+
+.msgbox > .button-wrapper > button:hover {
+ opacity: 1;
+}
+
+.msgbox > .button-wrapper > button:not(:last-child) {
+ margin-right: 8px;
+}
diff --git a/packages/electron-browser/src/dialog.ts b/packages/electron-browser/src/dialog.ts
new file mode 100644
index 000000000..9563f05c0
--- /dev/null
+++ b/packages/electron-browser/src/dialog.ts
@@ -0,0 +1,194 @@
+import { IDisposable } from "@coder/disposable";
+import { Emitter } from "@coder/emitter";
+
+import "./dialog.scss";
+
+/**
+ * Dialog options.
+ */
+export interface IDialogOptions {
+ message?: string;
+ detail?: string;
+ buttons?: string[];
+ input?: {
+ value: string;
+ selection?: {
+ start: number;
+ end: number;
+ };
+ };
+}
+
+export interface IDialogAction {
+ buttonIndex?: number;
+ key?: IKey;
+}
+
+/**
+ * Pressed keys.
+ */
+export enum IKey {
+ Enter = "Enter",
+ Escape = "Escape",
+}
+
+export class Dialog {
+
+ private options: IDialogOptions;
+ private overlay: HTMLElement;
+ private cachedActiveElement: HTMLElement;
+ private input: HTMLInputElement;
+ private actionEmitter: Emitter;
+ private errors: HTMLElement;
+ private buttons: HTMLElement[];
+
+ public constructor(options: IDialogOptions) {
+ this.options = options;
+
+ this.actionEmitter = new Emitter();
+
+ const msgBox = document.createElement("div");
+ msgBox.classList.add("msgbox");
+
+ if (this.options.message) {
+ const messageDiv = document.createElement("div");
+ messageDiv.classList.add("msg");
+ messageDiv.innerText = this.options.message;
+ msgBox.appendChild(messageDiv);
+ }
+
+ if (this.options.detail) {
+ const detailDiv = document.createElement("div");
+ detailDiv.classList.add("detail");
+ detailDiv.innerText = this.options.detail;
+ msgBox.appendChild(detailDiv);
+ }
+
+ if (this.options.input) {
+ msgBox.classList.add("input");
+ this.input = document.createElement("input");
+ this.input.classList.add("input");
+ this.input.value = this.options.input.value;
+ this.input.addEventListener("keydown", (event) => {
+ if (event.key === IKey.Enter) {
+ event.preventDefault();
+ this.actionEmitter.emit({
+ buttonIndex: undefined,
+ key: IKey.Enter,
+ });
+ }
+ });
+ msgBox.appendChild(this.input);
+ }
+
+ this.errors = document.createElement("div");
+ this.errors.classList.add("errors");
+ msgBox.appendChild(this.errors);
+
+ if (this.options.buttons && this.options.buttons.length > 0) {
+ this.buttons = this.options.buttons.map((buttonText, buttonIndex) => {
+ const button = document.createElement("button");
+ button.innerText = buttonText;
+ button.addEventListener("click", () => {
+ this.actionEmitter.emit({
+ buttonIndex,
+ key: undefined,
+ });
+ });
+
+ return button;
+ });
+
+ const buttonWrapper = document.createElement("div");
+ buttonWrapper.classList.add("button-wrapper");
+ this.buttons.forEach((b) => buttonWrapper.appendChild(b));
+ msgBox.appendChild(buttonWrapper);
+ }
+
+
+ this.overlay = document.createElement("div");
+ this.overlay.style.cssText = `display: flex; align-items: center; justify-content: center; top: 0; left: 0; right: 0; bottom: 0; z-index: 15; position: absolute; background: rgba(0, 0, 0, 0.4); opacity: 0; transition: 300ms opacity ease;`;
+ this.overlay.appendChild(msgBox);
+
+ setTimeout(() => {
+ this.overlay.style.opacity = "1";
+ });
+ }
+
+ /**
+ * Register a function to be called when the user performs an action.
+ */
+ public onAction(callback: (action: IDialogAction) => void): IDisposable {
+ return this.actionEmitter.event(callback);
+ }
+
+ /**
+ * Input value if this dialog has an input.
+ */
+ public get inputValue(): string {
+ return this.input ? this.input.value : undefined;
+ }
+
+ /**
+ * Display or remove an error.
+ */
+ public set error(error: string) {
+ while (this.errors.lastChild) {
+ this.errors.removeChild(this.errors.lastChild);
+ }
+ if (error) {
+ const errorDiv = document.createElement("error");
+ errorDiv.innerText = error;
+ this.errors.appendChild(errorDiv);
+ }
+ }
+
+ /**
+ * Show the dialog.
+ */
+ public show(): void {
+ if (!this.cachedActiveElement) {
+ this.cachedActiveElement = document.activeElement as HTMLElement;
+ document.body.appendChild(this.overlay);
+ document.addEventListener("keydown", this.onKeydown);
+ if (this.input) {
+ this.input.focus();
+ if (this.options.input.selection) {
+ this.input.setSelectionRange(
+ this.options.input.selection.start,
+ this.options.input.selection.end
+ );
+ }
+ } else if (this.buttons) {
+ this.buttons[0].focus();
+ }
+ }
+ }
+
+ /**
+ * Remove the dialog and clean up.
+ */
+ public hide(): void {
+ if (this.cachedActiveElement) {
+ this.overlay.remove();
+ document.removeEventListener("keydown", this.onKeydown);
+ this.cachedActiveElement.focus();
+ this.cachedActiveElement = null;
+ }
+ }
+
+ /**
+ * Capture escape.
+ */
+ private onKeydown = (event: KeyboardEvent): void => {
+ if (event.key === "Escape") {
+ event.preventDefault();
+ event.stopPropagation();
+ this.actionEmitter.emit({
+ buttonIndex: undefined,
+ key: IKey.Escape,
+ });
+ }
+ }
+
+}
diff --git a/packages/electron-browser/src/electron.ts b/packages/electron-browser/src/electron.ts
new file mode 100644
index 000000000..27cded6b6
--- /dev/null
+++ b/packages/electron-browser/src/electron.ts
@@ -0,0 +1,352 @@
+import * as electron from "electron";
+import { EventEmitter } from "events";
+import * as fs from "fs";
+import { getFetchUrl } from "../src/coder/api";
+import { escapePath } from "../src/coder/common";
+import { wush } from "../src/coder/server";
+import { IKey, Dialog } from "./dialog";
+
+(global as any).getOpenUrls = () => {
+ return [];
+};
+
+const oldCreateElement = document.createElement;
+
+document.createElement = (tagName: string) => {
+ const createElement = (tagName: string) => {
+ return oldCreateElement.call(document, tagName);
+ };
+
+ if (tagName === "webview") {
+ const view = createElement("iframe") as HTMLIFrameElement;
+ view.style.border = "0px";
+ const frameID = Math.random().toString();
+ view.addEventListener("error", (event) => {
+ console.log("Got iframe error", event.error, event.message);
+ });
+ window.addEventListener("message", (event) => {
+ if (!event.data || !event.data.id) {
+ return;
+ }
+ if (event.data.id !== frameID) {
+ return;
+ }
+ const e = new CustomEvent("ipc-message");
+ (e as any).channel = event.data.channel;
+ (e as any).args = event.data.data;
+ view.dispatchEvent(e);
+ });
+ view.sandbox.add("allow-same-origin", "allow-scripts", "allow-popups", "allow-forms");
+ Object.defineProperty(view, "preload", {
+ set: (url: string) => {
+ view.onload = () => {
+ view.contentDocument.body.id = frameID;
+ view.contentDocument.body.parentElement.style.overflow = "hidden";
+ const script = document.createElement("script");
+ script.src = url;
+ view.contentDocument.head.appendChild(script);
+ };
+ },
+ });
+ (view as any).getWebContents = () => undefined;
+ (view as any).send = (channel: string, ...args) => {
+ if (args[0] && typeof args[0] === "object" && args[0].contents) {
+ args[0].contents = (args[0].contents as string).replace(/"(file:\/\/[^"]*)"/g, (m) => `"${getFetchUrl(m)}"`);
+ args[0].contents = (args[0].contents as string).replace(/"vscode-resource:([^"]*)"/g, (m) => `"${getFetchUrl(m)}"`);
+ }
+ view.contentWindow.postMessage({
+ channel,
+ data: args,
+ id: frameID,
+ }, "*");
+ };
+ return view;
+ }
+
+ return createElement(tagName);
+};
+
+const rendererToMainEmitter = new EventEmitter();
+const mainToRendererEmitter = new EventEmitter();
+
+module.exports = {
+ clipboard: {
+ has: () => {
+ return false;
+ },
+ writeText: (value: string) => {
+ // Taken from https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
+ const active = document.activeElement as HTMLElement;
+ const el = document.createElement('textarea'); // Create a