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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
from published versions since it shows up in the VS Code extension changelog
tab and is confusing to users. Add it back between releases if needed. -->

## Unreleased

### Changed

- **Coder: Speed Test Workspace** results now render in an interactive throughput chart with
hover tooltips, a summary header, and a real-time progress bar while the CLI runs. A View JSON
action exposes the raw output.

## [v1.14.4-pre](https://github.com/coder/vscode-coder/releases/tag/v1.14.4-pre) 2026-04-20

### Added
Expand Down
1 change: 0 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default defineConfig(
"**/*.d.ts",
"vitest.config.ts",
"**/vite.config*.ts",
"**/createWebviewConfig.ts",
".vscode-test/**",
"test/fixtures/scripts/**",
]),
Expand Down
39 changes: 39 additions & 0 deletions packages/shared/src/error/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** Convert any thrown value into an Error. Pass `serialize` (e.g. `util.inspect`
* in Node) for richer object formatting; the default `JSON.stringify` will
* throw on circular inputs and fall through to `defaultMsg`. */
export function toError(
value: unknown,
defaultMsg?: string,
serialize: (value: unknown) => string = JSON.stringify,
): Error {
if (value instanceof Error) {
return value;
}

if (typeof value === "string") {
return new Error(value);
}

if (
value !== null &&
typeof value === "object" &&
"message" in value &&
typeof value.message === "string"
) {
const error = new Error(value.message);
if ("name" in value && typeof value.name === "string") {
error.name = value.name;
}
return error;
}

if (value === null || value === undefined) {
return new Error(defaultMsg ?? "Unknown error");
}

try {
return new Error(serialize(value));
} catch {
return new Error(defaultMsg ?? "Non-serializable error object");
}
}
11 changes: 11 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
// IPC protocol types
export * from "./ipc/protocol";

// Error utilities
export { toError } from "./error/utils";

// Tasks types, utilities, and API
export * from "./tasks/types";
export * from "./tasks/utils";
export * from "./tasks/api";

// Speedtest API
export {
SpeedtestApi,
type SpeedtestData,
type SpeedtestInterval,
type SpeedtestResult,
} from "./speedtest/api";
22 changes: 15 additions & 7 deletions packages/shared/src/ipc/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,26 +166,34 @@ export function buildApiHook<
api: Api,
ipc: {
request: <P, R>(
def: { method: string; _types?: { params: P; response: R } },
def: RequestDef<P, R>,
...args: P extends void ? [] : [params: P]
) => Promise<R>;
command: <P>(
def: { method: string; _types?: { params: P } },
def: CommandDef<P>,
...args: P extends void ? [] : [params: P]
) => void;
onNotification: <D>(
def: { method: string; _types?: { data: D } },
def: NotificationDef<D>,
cb: (data: D) => void,
) => () => void;
},
): ApiHook<Api>;
export function buildApiHook(
api: Record<string, { kind: string; method: string }>,
api: Record<
string,
| RequestDef<unknown, unknown>
| CommandDef<unknown>
| NotificationDef<unknown>
>,
ipc: {
request: (def: { method: string }, params?: unknown) => Promise<unknown>;
command: (def: { method: string }, params?: unknown) => void;
request: (
def: RequestDef<unknown, unknown>,
params?: unknown,
) => Promise<unknown>;
command: (def: CommandDef<unknown>, params?: unknown) => void;
onNotification: (
def: { method: string },
def: NotificationDef<unknown>,
cb: (data: unknown) => void,
) => () => void;
},
Expand Down
24 changes: 24 additions & 0 deletions packages/shared/src/speedtest/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineCommand, defineNotification } from "../ipc/protocol";

export interface SpeedtestInterval {
start_time_seconds: number;
end_time_seconds: number;
throughput_mbits: number;
}

export interface SpeedtestResult {
overall: SpeedtestInterval;
intervals: SpeedtestInterval[];
}

export interface SpeedtestData {
workspaceName: string;
result: SpeedtestResult;
}

export const SpeedtestApi = {
/** Extension pushes parsed results to the webview */
data: defineNotification<SpeedtestData>("speedtest/data"),
/** Webview requests to open raw JSON in a text editor */
viewJson: defineCommand<void>("speedtest/viewJson"),
} as const;
21 changes: 21 additions & 0 deletions packages/speedtest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@repo/speedtest",
"version": "1.0.0",
"description": "Coder Speedtest visualization webview",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/shared": "workspace:*",
"@repo/webview-shared": "workspace:*"
},
"devDependencies": {
"@types/vscode-webview": "catalog:",
"typescript": "catalog:",
"vite": "catalog:"
}
}
Loading