Skip to content

Commit 53cd740

Browse files
authored
Merge branch 'main' into open-primary-agent-for-workspace
2 parents befecc0 + 2d7dac8 commit 53cd740

18 files changed

+167
-162
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
- Update `/openDevContainer` to support all dev container features when hostPath
66
and configFile are provided.
7-
- Fix opening a workspace restores the VS Code session (files, tabs, context)
8-
of the selected agent.
7+
- Add `coder.disableUpdateNotifications` setting to disable workspace template
8+
update notifications.
9+
- Coder output panel enhancements: All log entries now include timestamps, and you
10+
can filter messages by log level in the panel.
911
- Consistently use the same session for each agent. Previously,
1012
depending on how you connected, it could be possible to get two
1113
different sessions for an agent. Existing connections may still

flake.lock

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
flake-utils.lib.eachDefaultSystem
88
(system:
99
let pkgs = nixpkgs.legacyPackages.${system};
10-
nodejs = pkgs.nodejs-18_x;
10+
nodejs = pkgs.nodejs;
1111
yarn' = pkgs.yarn.override { inherit nodejs; };
1212
in {
1313
devShells.default = pkgs.mkShell {

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@
109109
"markdownDescription": "Automatically log into the default URL when the extension is activated. coder.defaultUrl is preferred, otherwise the CODER_URL environment variable will be used. This setting has no effect if neither is set.",
110110
"type": "boolean",
111111
"default": false
112+
},
113+
"coder.disableUpdateNotifications": {
114+
"markdownDescription": "Disable notifications when workspace template updates are available.",
115+
"type": "boolean",
116+
"default": false
112117
}
113118
}
114119
},

src/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as vscode from "vscode";
1212
import * as ws from "ws";
1313
import { errToStr } from "./api-helper";
1414
import { CertificateError } from "./error";
15+
import { FeatureSet } from "./featureSet";
1516
import { getHeaderArgs } from "./headers";
1617
import { getProxyForUrl } from "./proxy";
1718
import { Storage } from "./storage";
@@ -105,7 +106,7 @@ export function makeCoderSdk(
105106
restClient.getAxiosInstance().interceptors.response.use(
106107
(r) => r,
107108
async (err) => {
108-
throw await CertificateError.maybeWrap(err, baseUrl, storage);
109+
throw await CertificateError.maybeWrap(err, baseUrl, storage.output);
109110
},
110111
);
111112

@@ -174,6 +175,7 @@ export async function startWorkspaceIfStoppedOrFailed(
174175
binPath: string,
175176
workspace: Workspace,
176177
writeEmitter: vscode.EventEmitter<string>,
178+
featureSet: FeatureSet,
177179
): Promise<Workspace> {
178180
// Before we start a workspace, we make an initial request to check it's not already started
179181
const updatedWorkspace = await restClient.getWorkspace(workspace.id);
@@ -191,6 +193,10 @@ export async function startWorkspaceIfStoppedOrFailed(
191193
"--yes",
192194
workspace.owner_name + "/" + workspace.name,
193195
];
196+
if (featureSet.buildReason) {
197+
startArgs.push(...["--reason", "vscode_connection"]);
198+
}
199+
194200
const startProcess = spawn(binPath, startArgs);
195201

196202
startProcess.stdout.on("data", (data: Buffer) => {

src/commands.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ export class Commands {
245245
} catch (err) {
246246
const message = getErrorMessage(err, "no response from the server");
247247
if (isAutologin) {
248-
this.storage.writeToCoderOutputChannel(
249-
`Failed to log in to Coder server: ${message}`,
248+
this.storage.output.warn(
249+
"Failed to log in to Coder server:",
250+
message,
250251
);
251252
} else {
252253
this.vscodeProposed.window.showErrorMessage(
@@ -670,14 +671,15 @@ export class Commands {
670671
if (!this.workspace || !this.workspaceRestClient) {
671672
return;
672673
}
673-
const action = await this.vscodeProposed.window.showInformationMessage(
674+
const action = await this.vscodeProposed.window.showWarningMessage(
674675
"Update Workspace",
675676
{
676677
useCustom: true,
677678
modal: true,
678-
detail: `Update ${this.workspace.owner_name}/${this.workspace.name} to the latest version?`,
679+
detail: `Update ${this.workspace.owner_name}/${this.workspace.name} to the latest version?\n\nUpdating will restart your workspace which stops any running processes and may result in the loss of unsaved work.`,
679680
},
680681
"Update",
682+
"Cancel",
681683
);
682684
if (action === "Update") {
683685
await this.workspaceRestClient.updateWorkspaceVersion(this.workspace);

src/error.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import https from "https";
44
import * as path from "path";
55
import { afterAll, beforeAll, it, expect, vi } from "vitest";
66
import { CertificateError, X509_ERR, X509_ERR_CODE } from "./error";
7+
import { Logger } from "./logger";
78

89
// Before each test we make a request to sanity check that we really get the
910
// error we are expecting, then we run it through CertificateError.
@@ -23,10 +24,16 @@ beforeAll(() => {
2324
});
2425
});
2526

26-
const logger = {
27-
writeToCoderOutputChannel(message: string) {
28-
throw new Error(message);
29-
},
27+
const throwingLog = (message: string) => {
28+
throw new Error(message);
29+
};
30+
31+
const logger: Logger = {
32+
trace: throwingLog,
33+
debug: throwingLog,
34+
info: throwingLog,
35+
warn: throwingLog,
36+
error: throwingLog,
3037
};
3138

3239
const disposers: (() => void)[] = [];

src/error.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors";
33
import * as forge from "node-forge";
44
import * as tls from "tls";
55
import * as vscode from "vscode";
6+
import { Logger } from "./logger";
67

78
// X509_ERR_CODE represents error codes as returned from BoringSSL/OpenSSL.
89
export enum X509_ERR_CODE {
@@ -21,10 +22,6 @@ export enum X509_ERR {
2122
UNTRUSTED_CHAIN = "Your Coder deployment's certificate chain does not appear to be trusted by this system. The root of the certificate chain must be added to this system's trust store. ",
2223
}
2324

24-
export interface Logger {
25-
writeToCoderOutputChannel(message: string): void;
26-
}
27-
2825
interface KeyUsage {
2926
keyCertSign: boolean;
3027
}
@@ -59,9 +56,7 @@ export class CertificateError extends Error {
5956
await CertificateError.determineVerifyErrorCause(address);
6057
return new CertificateError(err.message, cause);
6158
} catch (error) {
62-
logger.writeToCoderOutputChannel(
63-
`Failed to parse certificate from ${address}: ${error}`,
64-
);
59+
logger.warn(`Failed to parse certificate from ${address}`, error);
6560
break;
6661
}
6762
case X509_ERR_CODE.DEPTH_ZERO_SELF_SIGNED_CERT:

src/extension.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
4747
);
4848
}
4949

50-
const output = vscode.window.createOutputChannel("Coder");
50+
const output = vscode.window.createOutputChannel("Coder", { log: true });
5151
const storage = new Storage(
5252
output,
5353
ctx.globalState,
@@ -317,7 +317,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
317317
}
318318
} catch (ex) {
319319
if (ex instanceof CertificateError) {
320-
storage.writeToCoderOutputChannel(ex.x509Err || ex.message);
320+
storage.output.warn(ex.x509Err || ex.message);
321321
await ex.showModal("Failed to open workspace");
322322
} else if (isAxiosError(ex)) {
323323
const msg = getErrorMessage(ex, "None");
@@ -326,7 +326,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
326326
const method = ex.config?.method?.toUpperCase() || "request";
327327
const status = ex.response?.status || "None";
328328
const message = `API ${method} to '${urlString}' failed.\nStatus code: ${status}\nMessage: ${msg}\nDetail: ${detail}`;
329-
storage.writeToCoderOutputChannel(message);
329+
storage.output.warn(message);
330330
await vscodeProposed.window.showErrorMessage(
331331
"Failed to open workspace",
332332
{
@@ -337,7 +337,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
337337
);
338338
} else {
339339
const message = errToStr(ex, "No error message was provided");
340-
storage.writeToCoderOutputChannel(message);
340+
storage.output.warn(message);
341341
await vscodeProposed.window.showErrorMessage(
342342
"Failed to open workspace",
343343
{
@@ -356,14 +356,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
356356
// See if the plugin client is authenticated.
357357
const baseUrl = restClient.getAxiosInstance().defaults.baseURL;
358358
if (baseUrl) {
359-
storage.writeToCoderOutputChannel(
360-
`Logged in to ${baseUrl}; checking credentials`,
361-
);
359+
storage.output.info(`Logged in to ${baseUrl}; checking credentials`);
362360
restClient
363361
.getAuthenticatedUser()
364362
.then(async (user) => {
365363
if (user && user.roles) {
366-
storage.writeToCoderOutputChannel("Credentials are valid");
364+
storage.output.info("Credentials are valid");
367365
vscode.commands.executeCommand(
368366
"setContext",
369367
"coder.authenticated",
@@ -381,17 +379,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
381379
myWorkspacesProvider.fetchAndRefresh();
382380
allWorkspacesProvider.fetchAndRefresh();
383381
} else {
384-
storage.writeToCoderOutputChannel(
385-
`No error, but got unexpected response: ${user}`,
386-
);
382+
storage.output.warn("No error, but got unexpected response", user);
387383
}
388384
})
389385
.catch((error) => {
390386
// This should be a failure to make the request, like the header command
391387
// errored.
392-
storage.writeToCoderOutputChannel(
393-
`Failed to check user authentication: ${error.message}`,
394-
);
388+
storage.output.warn("Failed to check user authentication", error);
395389
vscode.window.showErrorMessage(
396390
`Failed to check user authentication: ${error.message}`,
397391
);
@@ -400,7 +394,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
400394
vscode.commands.executeCommand("setContext", "coder.loaded", true);
401395
});
402396
} else {
403-
storage.writeToCoderOutputChannel("Not currently logged in");
397+
storage.output.info("Not currently logged in");
404398
vscode.commands.executeCommand("setContext", "coder.loaded", true);
405399

406400
// Handle autologin, if not already logged in.

src/featureSet.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type FeatureSet = {
44
vscodessh: boolean;
55
proxyLogDirectory: boolean;
66
wildcardSSH: boolean;
7+
buildReason: boolean;
78
};
89

910
/**
@@ -29,5 +30,10 @@ export function featureSetForVersion(
2930
wildcardSSH:
3031
(version ? version.compare("2.19.0") : -1) >= 0 ||
3132
version?.prerelease[0] === "devel",
33+
34+
// The --reason flag was added to `coder start` in 2.25.0
35+
buildReason:
36+
(version?.compare("2.25.0") || 0) >= 0 ||
37+
version?.prerelease[0] === "devel",
3238
};
3339
}

0 commit comments

Comments
 (0)