Repository: coder/code-server (TypeScript) — Feature

Issue:
[Feat]: Promote coder/coder in Get Started screen
Our [new project](https://github.com/coder/coder) is a natural extension of code-server and relevant to those attempting to set up code-server for their teams.

Let's add a loud callout to the Welcome Screen that says something to the effect of "Setting up code-server for a team? Check out coder/coder".


Discussion:
To get to this, use Command Palette > Help: Get Started

Here's what it looks like: 

<img width="1712" alt="image" src="https://user-images.githubusercontent.com/3806031/197252800-ab8981cd-c54e-43bf-b9ba-e9425a871283.png">


Resolving patch:
diff --git a/docs/FAQ.md b/docs/FAQ.md
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -32,6 +32,7 @@
 - [Does code-server have any security login validation?](#does-code-server-have-any-security-login-validation)
 - [Are there community projects involving code-server?](#are-there-community-projects-involving-code-server)
 - [How do I change the port?](#how-do-i-change-the-port)
+- [How do I hide the coder/coder promotion?](#how-do-i-hide-the-codercoder-promotion)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 <!-- prettier-ignore-end -->
@@ -418,3 +419,7 @@ There are two ways to change the port on which code-server runs:
 
 1. with an environment variable e.g. `PORT=3000 code-server`
 2. using the flag `--bind-addr` e.g. `code-server --bind-addr localhost:3000`
+
+## How do I hide the coder/coder promotion?
+
+You can pass the flag `--disable-getting-started-override` to `code-server`.
diff --git a/patches/getting-started.diff b/patches/getting-started.diff
new file mode 100644
--- /dev/null
+++ b/patches/getting-started.diff
@@ -0,0 +1,178 @@
+Modify Help: Getting Started
+
+This modifies some text on the Getting Started page and adds text about using
+code-server on a team.
+
+It is enabled by default but can be overriden using the cli flag
+`--disable-getting-started-override`.
+
+Index: code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts
++++ code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts
+@@ -62,7 +62,7 @@ import { GettingStartedIndexList } from 
+ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
+ import { KeyCode } from 'vs/base/common/keyCodes';
+ import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils';
+-import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys';
++import { IsEnabledCoderGettingStarted, WorkbenchStateContext } from 'vs/workbench/common/contextkeys';
+ import { OpenFolderViaWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions';
+ import { OpenRecentAction } from 'vs/workbench/browser/actions/windowActions';
+ import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
+@@ -753,11 +753,24 @@ export class GettingStartedPage extends 
+ 			onShowOnStartupChanged();
+ 		}));
+ 
+-		const header = $('.header', {},
++		let header = $('.header', {},
+ 			$('h1.product-name.caption', {}, this.productService.nameLong),
+ 			$('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved"))
+ 		);
+ 
++		if (this.contextService.contextMatchesRules(IsEnabledCoderGettingStarted)) {
++			header = $('.header', {},
++			   $('h1.product-name.caption', {}, this.productService.nameLong),
++			   $('p.subtitle.description.coder', {},
++				   "Using code-server on a team?",
++			   ),
++			   $('p.subtitle.description.coder-coder', {},
++				   "Check out: ",
++				   $('a', { href: "https://github.com/coder/coder" }, "coder/coder")
++			   ),
++		   );
++	   }
++
+ 
+ 		const leftColumn = $('.categories-column.categories-column-left', {},);
+ 		const rightColumn = $('.categories-column.categories-column-right', {},);
+Index: code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css
++++ code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css
+@@ -60,6 +60,15 @@
+ 	display: block;
+ }
+ 
++.monaco-workbench .part.editor > .content .gettingStartedContainer .coder {
++	margin-bottom: 0.2em;
++}
++
++.monaco-workbench .part.editor>.content .gettingStartedContainer .coder-coder {
++	font-size: 1em;
++	margin-top: 0.2em;
++}
++
+ .monaco-workbench.hc-black .part.editor>.content .gettingStartedContainer .subtitle,
+ .monaco-workbench.hc-light .part.editor>.content .gettingStartedContainer .subtitle {
+ 	font-weight: 200;
+Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts
++++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
+@@ -276,6 +276,11 @@ export interface IWorkbenchConstructionO
+ 	 */
+ 	readonly isEnabledFileDownloads?: boolean
+ 
++	/**
++	 * Whether to use Coder's custom Getting Started text.
++	 */
++	readonly isEnabledCoderGettingStarted?: boolean
++
+ 	//#endregion
+ 
+ 
+Index: code-server/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts
++++ code-server/lib/vscode/src/vs/workbench/services/environment/browser/environmentService.ts
+@@ -36,6 +36,11 @@ export interface IBrowserWorkbenchEnviro
+ 	 * Enable downloading files via menu actions.
+ 	 */
+ 	readonly isEnabledFileDownloads?: boolean;
++
++	/**
++	 * Enable Coder's custom getting started text.
++	 */
++	readonly isEnabledCoderGettingStarted?: boolean;
+ }
+ 
+ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvironmentService {
+@@ -74,6 +79,13 @@ export class BrowserWorkbenchEnvironment
+ 		return this.options.isEnabledFileDownloads;
+ 	}
+ 
++	get isEnabledCoderGettingStarted(): boolean {
++		if (typeof this.options.isEnabledCoderGettingStarted === "undefined") {
++			throw new Error('isEnabledCoderGettingStarted was not provided to the browser');
++		}
++		return this.options.isEnabledCoderGettingStarted;
++	}
++
+ 	@memoize
+ 	get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); }
+ 
+Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
++++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
+@@ -16,6 +16,7 @@ export const serverOptions: OptionDescri
+ 	'auth': { type: 'string' },
+ 	'disable-file-downloads': { type: 'boolean' },
+ 	'locale': { type: 'string' },
++	'disable-getting-started-override': { type: 'boolean' },
+ 
+ 	/* ----- server setup ----- */
+ 
+@@ -98,6 +99,7 @@ export interface ServerParsedArgs {
+ 	'auth'?: string
+ 	'disable-file-downloads'?: boolean;
+ 	'locale'?: string
++	'disable-getting-started-override'?: boolean;
+ 
+ 	/* ----- server setup ----- */
+ 
+Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts
++++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts
+@@ -308,6 +308,7 @@ export class WebClientServer {
+ 			webviewEndpoint: vscodeBase + this._staticRoute + '/out/vs/workbench/contrib/webview/browser/pre',
+ 			userDataPath: this._environmentService.userDataPath,
+ 			isEnabledFileDownloads: !this._environmentService.args['disable-file-downloads'],
++			isEnabledCoderGettingStarted: !this._environmentService.args['disable-getting-started-override'],
+ 			_wrapWebWorkerExtHostInIframe,
+ 			developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined, logLevel: this._logService.getLevel() },
+ 			settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined,
+Index: code-server/lib/vscode/src/vs/workbench/browser/contextkeys.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/workbench/browser/contextkeys.ts
++++ code-server/lib/vscode/src/vs/workbench/browser/contextkeys.ts
+@@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/ev
+ import { Disposable } from 'vs/base/common/lifecycle';
+ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
+ import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys';
+-import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, IsEnabledFileDownloads } from 'vs/workbench/common/contextkeys';
++import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, IsEnabledFileDownloads, IsEnabledCoderGettingStarted } from 'vs/workbench/common/contextkeys';
+ import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
+ import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom';
+ import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
+@@ -204,6 +204,7 @@ export class WorkbenchContextKeysHandler
+ 
+ 		// code-server
+ 		IsEnabledFileDownloads.bindTo(this.contextKeyService).set(this.environmentService.isEnabledFileDownloads ?? true)
++		IsEnabledCoderGettingStarted.bindTo(this.contextKeyService).set(this.environmentService.isEnabledCoderGettingStarted ?? true)
+ 
+ 		this.registerListeners();
+ 	}
+Index: code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
+===================================================================
+--- code-server.orig/lib/vscode/src/vs/workbench/common/contextkeys.ts
++++ code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
+@@ -33,6 +33,7 @@ export const IsFullscreenContext = new R
+ export const HasWebFileSystemAccess = new RawContextKey<boolean>('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access)
+ 
+ export const IsEnabledFileDownloads = new RawContextKey<boolean>('isEnabledFileDownloads', true, true);
++export const IsEnabledCoderGettingStarted = new RawContextKey<boolean>('isEnabledCoderGettingStarted', true, true);
+ 
+ //#endregion
+ 
diff --git a/patches/series b/patches/series
--- a/patches/series
+++ b/patches/series
@@ -19,3 +19,4 @@ telemetry.diff
 display-language.diff
 cli-window-open.diff
 exec-argv.diff
+getting-started.diff
diff --git a/src/node/cli.ts b/src/node/cli.ts
--- a/src/node/cli.ts
+++ b/src/node/cli.ts
@@ -51,6 +51,7 @@ export interface UserProvidedCodeArgs {
   "disable-update-check"?: boolean
   "disable-file-downloads"?: boolean
   "disable-workspace-trust"?: boolean
+  "disable-getting-started-override"?: boolean
 }
 
 /**
@@ -170,6 +171,10 @@ export const options: Options<Required<UserProvidedArgs>> = {
     type: "boolean",
     description: "Disable Workspace Trust feature. This switch only affects the current session.",
   },
+  "disable-getting-started-override": {
+    type: "boolean",
+    description: "Disable the coder/coder override in the Help: Getting Started page.",
+  },
   // --enable can be used to enable experimental features. These features
   // provide no guarantees.
   enable: { type: "string[]" },
@@ -563,6 +568,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
     args["disable-file-downloads"] = true
   }
 
+  if (process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE?.match(/^(1|true)$/)) {
+    args["disable-getting-started-override"] = true
+  }
+
   const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
   if (process.env.HASHED_PASSWORD) {
     args["hashed-password"] = process.env.HASHED_PASSWORD


Test patch:
diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts
--- a/test/unit/node/cli.test.ts
+++ b/test/unit/node/cli.test.ts
@@ -43,6 +43,7 @@ describe("parser", () => {
     delete process.env.LOG_LEVEL
     delete process.env.PASSWORD
     delete process.env.CS_DISABLE_FILE_DOWNLOADS
+    delete process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE
     console.log = jest.fn()
   })
 
@@ -97,6 +98,8 @@ describe("parser", () => {
 
           "--disable-file-downloads",
 
+          "--disable-getting-started-override",
+
           ["--host", "0.0.0.0"],
           "4",
           "--",
@@ -114,6 +117,7 @@ describe("parser", () => {
         value: path.resolve("path/to/cert"),
       },
       "disable-file-downloads": true,
+      "disable-getting-started-override": true,
       enable: ["feature1", "feature2"],
       help: true,
       host: "0.0.0.0",
@@ -378,6 +382,30 @@ describe("parser", () => {
     })
   })
 
+  it("should use env var CS_DISABLE_GETTING_STARTED_OVERRIDE", async () => {
+    process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE = "1"
+    const args = parse([])
+    expect(args).toEqual({})
+
+    const defaultArgs = await setDefaults(args)
+    expect(defaultArgs).toEqual({
+      ...defaults,
+      "disable-getting-started-override": true,
+    })
+  })
+
+  it("should use env var CS_DISABLE_GETTING_STARTED_OVERRIDE set to true", async () => {
+    process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE = "true"
+    const args = parse([])
+    expect(args).toEqual({})
+
+    const defaultArgs = await setDefaults(args)
+    expect(defaultArgs).toEqual({
+      ...defaults,
+      "disable-getting-started-override": true,
+    })
+  })
+
   it("should error if password passed in", () => {
     expect(() => parse(["--password", "supersecret123"])).toThrowError(
       "--password can only be set in the config file or passed in via $PASSWORD",