Skip to content

Commit 06a3653

Browse files
committed
fix: show all Coder workspaces in picker regardless of status
- Remove filterRunning parameter from listWorkspaces() - return all statuses - Show status in dropdown options (e.g., 'my-workspace (template) • stopped') - Change empty state text from 'No running workspaces' to 'No workspaces found' - Make coder controls container w-fit instead of full width ensureReady() already handles starting stopped workspaces, so users should be able to see and select them in the existing workspace picker. Addresses Codex review comment on PR #1617.
1 parent 40b0be4 commit 06a3653

File tree

3 files changed

+34
-56
lines changed

3 files changed

+34
-56
lines changed

src/browser/components/ChatInput/CoderControls.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export function CoderControls(props: CoderControlsProps) {
184184
{enabled && (
185185
<div
186186
className={cn(
187-
"flex rounded-md border",
187+
"flex w-fit rounded-md border",
188188
hasError ? "border-red-500" : "border-border-medium"
189189
)}
190190
data-testid="coder-controls-inner"
@@ -300,13 +300,11 @@ export function CoderControls(props: CoderControlsProps) {
300300
className="bg-bg-dark text-foreground border-border-medium focus:border-accent h-7 w-[180px] rounded-md border px-2 text-sm focus:outline-none disabled:opacity-50"
301301
data-testid="coder-workspace-select"
302302
>
303-
{existingWorkspaces.length === 0 && (
304-
<option value="">No running workspaces</option>
305-
)}
303+
{existingWorkspaces.length === 0 && <option value="">No workspaces found</option>}
306304
{existingWorkspaces.length > 0 && <option value="">Select workspace...</option>}
307305
{existingWorkspaces.map((w) => (
308306
<option key={w.name} value={w.name}>
309-
{w.name} ({w.templateName})
307+
{w.name} ({w.templateName}){w.status}
310308
</option>
311309
))}
312310
</select>

src/node/services/coderService.test.ts

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { EventEmitter } from "events";
22
import { Readable } from "stream";
3-
import { describe, it, expect, vi, beforeEach, afterEach } from "bun:test";
3+
import { describe, it, expect, vi, beforeEach, afterEach, spyOn } from "bun:test";
44
import { CoderService, compareVersions } from "./coderService";
5+
import * as childProcess from "child_process";
6+
7+
// eslint-disable-next-line @typescript-eslint/no-empty-function
8+
const noop = () => {};
59

610
function mockExecOk(stdout: string, stderr = ""): void {
711
mockExecAsync.mockReturnValue({
@@ -17,6 +21,12 @@ function mockExecError(error: Error): void {
1721
});
1822
}
1923

24+
/**
25+
* Mock spawn for streaming createWorkspace() tests.
26+
* Uses spyOn instead of vi.mock to avoid polluting other test files.
27+
*/
28+
let spawnSpy: ReturnType<typeof spyOn<typeof childProcess, "spawn">> | null = null;
29+
2030
function mockCoderCommandResult(options: {
2131
stdout?: string;
2232
stderr?: string;
@@ -26,7 +36,7 @@ function mockCoderCommandResult(options: {
2636
const stderr = Readable.from(options.stderr ? [Buffer.from(options.stderr)] : []);
2737
const events = new EventEmitter();
2838

29-
mockSpawn.mockReturnValue({
39+
spawnSpy?.mockReturnValue({
3040
stdout,
3141
stderr,
3242
exitCode: null,
@@ -39,19 +49,8 @@ function mockCoderCommandResult(options: {
3949
// Emit close after handlers are attached.
4050
setTimeout(() => events.emit("close", options.exitCode), 0);
4151
}
42-
// eslint-disable-next-line @typescript-eslint/no-empty-function
43-
const noop = () => {};
44-
45-
// Mock execAsync
4652

47-
// Mock spawn for streaming createWorkspace()
48-
void vi.mock("child_process", () => ({
49-
spawn: vi.fn(),
50-
}));
51-
52-
import { spawn } from "child_process";
53-
54-
const mockSpawn = spawn as ReturnType<typeof vi.fn>;
53+
// Mock execAsync (this is safe - it's our own module, not a Node.js built-in)
5554
void vi.mock("@/node/utils/disposableExec", () => ({
5655
execAsync: vi.fn(),
5756
}));
@@ -67,10 +66,14 @@ describe("CoderService", () => {
6766
beforeEach(() => {
6867
service = new CoderService();
6968
vi.clearAllMocks();
69+
// Set up spawn spy for tests that use mockCoderCommandResult
70+
spawnSpy = spyOn(childProcess, "spawn");
7071
});
7172

7273
afterEach(() => {
7374
service.clearCache();
75+
spawnSpy?.mockRestore();
76+
spawnSpy = null;
7477
});
7578

7679
describe("getCoderInfo", () => {
@@ -245,42 +248,24 @@ describe("CoderService", () => {
245248
});
246249

247250
describe("listWorkspaces", () => {
248-
it("returns only running workspaces by default", async () => {
251+
it("returns all workspaces regardless of status", async () => {
249252
mockExecAsync.mockReturnValue({
250253
result: Promise.resolve({
251254
stdout: JSON.stringify([
252255
{ name: "ws-1", template_name: "t1", latest_build: { status: "running" } },
253256
{ name: "ws-2", template_name: "t2", latest_build: { status: "stopped" } },
254-
{ name: "ws-3", template_name: "t3", latest_build: { status: "running" } },
257+
{ name: "ws-3", template_name: "t3", latest_build: { status: "starting" } },
255258
]),
256259
}),
257260
[Symbol.dispose]: noop,
258261
});
259262

260263
const workspaces = await service.listWorkspaces();
261264

262-
expect(workspaces).toEqual([
263-
{ name: "ws-1", templateName: "t1", status: "running" },
264-
{ name: "ws-3", templateName: "t3", status: "running" },
265-
]);
266-
});
267-
268-
it("returns all workspaces when filterRunning is false", async () => {
269-
mockExecAsync.mockReturnValue({
270-
result: Promise.resolve({
271-
stdout: JSON.stringify([
272-
{ name: "ws-1", template_name: "t1", latest_build: { status: "running" } },
273-
{ name: "ws-2", template_name: "t2", latest_build: { status: "stopped" } },
274-
]),
275-
}),
276-
[Symbol.dispose]: noop,
277-
});
278-
279-
const workspaces = await service.listWorkspaces(false);
280-
281265
expect(workspaces).toEqual([
282266
{ name: "ws-1", templateName: "t1", status: "running" },
283267
{ name: "ws-2", templateName: "t2", status: "stopped" },
268+
{ name: "ws-3", templateName: "t3", status: "starting" },
284269
]);
285270
});
286271

@@ -483,7 +468,7 @@ describe("CoderService", () => {
483468
const stderr = Readable.from([Buffer.from("err-1\n")]);
484469
const events = new EventEmitter();
485470

486-
mockSpawn.mockReturnValue({
471+
spawnSpy!.mockReturnValue({
487472
stdout,
488473
stderr,
489474
kill: vi.fn(),
@@ -498,7 +483,7 @@ describe("CoderService", () => {
498483
lines.push(line);
499484
}
500485

501-
expect(mockSpawn).toHaveBeenCalledWith(
486+
expect(spawnSpy).toHaveBeenCalledWith(
502487
"coder",
503488
["create", "my-workspace", "-t", "my-template", "--yes"],
504489
{ stdio: ["ignore", "pipe", "pipe"] }
@@ -517,7 +502,7 @@ describe("CoderService", () => {
517502
const stderr = Readable.from([]);
518503
const events = new EventEmitter();
519504

520-
mockSpawn.mockReturnValue({
505+
spawnSpy!.mockReturnValue({
521506
stdout,
522507
stderr,
523508
kill: vi.fn(),
@@ -530,7 +515,7 @@ describe("CoderService", () => {
530515
// drain
531516
}
532517

533-
expect(mockSpawn).toHaveBeenCalledWith(
518+
expect(spawnSpy).toHaveBeenCalledWith(
534519
"coder",
535520
["create", "ws", "-t", "tmpl", "--yes", "--preset", "preset"],
536521
{ stdio: ["ignore", "pipe", "pipe"] }
@@ -549,7 +534,7 @@ describe("CoderService", () => {
549534
const stderr = Readable.from([]);
550535
const events = new EventEmitter();
551536

552-
mockSpawn.mockReturnValue({
537+
spawnSpy!.mockReturnValue({
553538
stdout,
554539
stderr,
555540
kill: vi.fn(),
@@ -562,7 +547,7 @@ describe("CoderService", () => {
562547
// drain
563548
}
564549

565-
expect(mockSpawn).toHaveBeenCalledWith(
550+
expect(spawnSpy).toHaveBeenCalledWith(
566551
"coder",
567552
[
568553
"create",
@@ -587,7 +572,7 @@ describe("CoderService", () => {
587572
const stderr = Readable.from([]);
588573
const events = new EventEmitter();
589574

590-
mockSpawn.mockReturnValue({
575+
spawnSpy!.mockReturnValue({
591576
stdout,
592577
stderr,
593578
kill: vi.fn(),

src/node/services/coderService.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -523,9 +523,9 @@ export class CoderService {
523523
}
524524

525525
/**
526-
* List Coder workspaces. Only returns "running" workspaces by default.
526+
* List Coder workspaces (all statuses).
527527
*/
528-
async listWorkspaces(filterRunning = true): Promise<CoderWorkspace[]> {
528+
async listWorkspaces(): Promise<CoderWorkspace[]> {
529529
// Derive known statuses from schema to avoid duplication and prevent ORPC validation errors
530530
const KNOWN_STATUSES = new Set<string>(CoderWorkspaceStatusSchema.options);
531531

@@ -546,19 +546,14 @@ export class CoderService {
546546
};
547547
}>;
548548

549-
// Filter to known statuses first to avoid ORPC schema validation failures
550-
const mapped = workspaces
549+
// Filter to known statuses to avoid ORPC schema validation failures
550+
return workspaces
551551
.filter((w) => KNOWN_STATUSES.has(w.latest_build.status))
552552
.map((w) => ({
553553
name: w.name,
554554
templateName: w.template_name,
555555
status: w.latest_build.status as CoderWorkspaceStatus,
556556
}));
557-
558-
if (filterRunning) {
559-
return mapped.filter((w) => w.status === "running");
560-
}
561-
return mapped;
562557
} catch (error) {
563558
// Common user state: Coder CLI installed but not configured/logged in.
564559
// Don't spam error logs for UI list calls.

0 commit comments

Comments
 (0)