Skip to content

Commit 18f7442

Browse files
committed
🤖 fix: Coder runtime improvements and agent discovery race condition
- Fix race condition where agents.list/get tried SSH before workspace init completed by adding waitForInit() calls in RPC handlers - Replace native <select> with Radix UI Select for consistent styling on Linux - Use coder ssh --wait=yes as single ready primitive instead of coder start - Add polling for stopping/canceling workspace states before connecting - Fix duplicate React keys when templates share names across orgs - Show org name in template dropdown only when disambiguation needed - Remove dead startWorkspace/startWorkspaceAndWait methods from CoderService --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high` • Cost: `$290.61`_
1 parent 0344fe7 commit 18f7442

File tree

9 files changed

+618
-337
lines changed

9 files changed

+618
-337
lines changed

src/browser/components/ChatInput/CoderControls.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { CoderWorkspaceConfig } from "@/common/types/runtime";
1313
import { cn } from "@/common/lib/utils";
1414
import { Loader2 } from "lucide-react";
1515
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
16+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
1617

1718
export interface CoderControlsProps {
1819
/** Whether to use Coder workspace (checkbox state) */
@@ -174,7 +175,7 @@ export function CoderControls(props: CoderControlsProps) {
174175
? undefined
175176
: presets.length === 1
176177
? presets[0]?.name
177-
: (coderConfig?.preset ?? defaultPresetName);
178+
: (coderConfig?.preset ?? defaultPresetName ?? presets[0]?.name);
178179

179180
return (
180181
<div className="flex flex-col gap-1.5" data-testid="coder-controls">
@@ -245,42 +246,61 @@ export function CoderControls(props: CoderControlsProps) {
245246
{loadingTemplates ? (
246247
<Loader2 className="text-muted h-4 w-4 animate-spin" />
247248
) : (
248-
<select
249+
<Select
249250
value={coderConfig?.template ?? ""}
250-
onChange={(e) => handleTemplateChange(e.target.value)}
251+
onValueChange={handleTemplateChange}
251252
disabled={disabled || templates.length === 0}
252-
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"
253-
data-testid="coder-template-select"
254253
>
255-
{templates.length === 0 && <option value="">No templates</option>}
256-
{templates.map((t) => (
257-
<option key={t.name} value={t.name}>
258-
{t.displayName || t.name}
259-
</option>
260-
))}
261-
</select>
254+
<SelectTrigger
255+
className="h-7 w-[180px] text-xs"
256+
data-testid="coder-template-select"
257+
>
258+
<SelectValue placeholder="No templates" />
259+
</SelectTrigger>
260+
<SelectContent>
261+
{templates.map((t) => {
262+
// Show org name only if there are duplicate template names
263+
const hasDuplicate = templates.some(
264+
(other) =>
265+
other.name === t.name && other.organizationName !== t.organizationName
266+
);
267+
return (
268+
<SelectItem key={`${t.organizationName}/${t.name}`} value={t.name}>
269+
{t.displayName || t.name}
270+
{hasDuplicate && (
271+
<span className="text-muted ml-1">({t.organizationName})</span>
272+
)}
273+
</SelectItem>
274+
);
275+
})}
276+
</SelectContent>
277+
</Select>
262278
)}
263279
</div>
264280
<div className="flex h-7 items-center gap-2">
265281
<label className="text-muted-foreground w-16 text-xs">Preset</label>
266282
{loadingPresets ? (
267283
<Loader2 className="text-muted h-4 w-4 animate-spin" />
268284
) : (
269-
<select
285+
<Select
270286
value={effectivePreset ?? ""}
271-
onChange={(e) => handlePresetChange(e.target.value)}
287+
onValueChange={handlePresetChange}
272288
disabled={disabled || presets.length === 0}
273-
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"
274-
data-testid="coder-preset-select"
275289
>
276-
{presets.length === 0 && <option value="">No presets</option>}
277-
{presets.length > 0 && <option value="">Select preset...</option>}
278-
{presets.map((p) => (
279-
<option key={p.id} value={p.name}>
280-
{p.name}
281-
</option>
282-
))}
283-
</select>
290+
<SelectTrigger
291+
className="h-7 w-[180px] text-xs"
292+
data-testid="coder-preset-select"
293+
>
294+
<SelectValue placeholder="No presets" />
295+
</SelectTrigger>
296+
<SelectContent>
297+
{presets.map((p) => (
298+
<SelectItem key={p.id} value={p.name}>
299+
{p.name}
300+
</SelectItem>
301+
))}
302+
</SelectContent>
303+
</Select>
284304
)}
285305
</div>
286306
</div>
@@ -293,21 +313,31 @@ export function CoderControls(props: CoderControlsProps) {
293313
{loadingWorkspaces ? (
294314
<Loader2 className="text-muted h-4 w-4 animate-spin" />
295315
) : (
296-
<select
316+
<Select
297317
value={coderConfig?.workspaceName ?? ""}
298-
onChange={(e) => handleExistingWorkspaceChange(e.target.value)}
318+
onValueChange={handleExistingWorkspaceChange}
299319
disabled={disabled || existingWorkspaces.length === 0}
300-
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"
301-
data-testid="coder-workspace-select"
302320
>
303-
{existingWorkspaces.length === 0 && <option value="">No workspaces found</option>}
304-
{existingWorkspaces.length > 0 && <option value="">Select workspace...</option>}
305-
{existingWorkspaces.map((w) => (
306-
<option key={w.name} value={w.name}>
307-
{w.name} ({w.templateName}) • {w.status}
308-
</option>
309-
))}
310-
</select>
321+
<SelectTrigger
322+
className="h-7 w-[180px] text-xs"
323+
data-testid="coder-workspace-select"
324+
>
325+
<SelectValue
326+
placeholder={
327+
existingWorkspaces.length === 0
328+
? "No workspaces found"
329+
: "Select workspace..."
330+
}
331+
/>
332+
</SelectTrigger>
333+
<SelectContent>
334+
{existingWorkspaces.map((w) => (
335+
<SelectItem key={w.name} value={w.name}>
336+
{w.name} ({w.templateName}) • {w.status}
337+
</SelectItem>
338+
))}
339+
</SelectContent>
340+
</Select>
311341
)}
312342
</div>
313343
)}

src/browser/components/ChatInput/CreationControls.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import React, { useCallback, useEffect } from "react";
22
import { RUNTIME_MODE, type RuntimeMode, type ParsedRuntime } from "@/common/types/runtime";
33
import type { RuntimeAvailabilityMap } from "./useCreationWorkspace";
44
import { Select } from "../Select";
5+
import {
6+
Select as RadixSelect,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
} from "../ui/select";
512
import { Loader2, Wand2 } from "lucide-react";
613
import { cn } from "@/common/lib/utils";
714
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
@@ -144,26 +151,28 @@ function SectionPicker(props: SectionPickerProps) {
144151
opacity: selectedSection ? 1 : 0.4,
145152
}}
146153
/>
147-
<label htmlFor="workspace-section" className="text-muted-foreground shrink-0 text-xs">
148-
Section
149-
</label>
150-
<select
151-
id="workspace-section"
154+
<label className="text-muted-foreground shrink-0 text-xs">Section</label>
155+
<RadixSelect
152156
value={selectedSectionId ?? ""}
153-
onChange={(e) => onSectionChange(e.target.value || null)}
157+
onValueChange={onSectionChange}
154158
disabled={disabled}
155-
className={cn(
156-
"bg-transparent text-sm font-medium focus:outline-none cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
157-
selectedSection ? "text-foreground" : "text-muted"
158-
)}
159159
>
160-
<option value="">None</option>
161-
{sections.map((section) => (
162-
<option key={section.id} value={section.id}>
163-
{section.name}
164-
</option>
165-
))}
166-
</select>
160+
<SelectTrigger
161+
className={cn(
162+
"h-auto border-0 bg-transparent px-0 py-0 text-sm font-medium shadow-none focus:ring-0",
163+
selectedSection ? "text-foreground" : "text-muted"
164+
)}
165+
>
166+
<SelectValue placeholder="Select..." />
167+
</SelectTrigger>
168+
<SelectContent>
169+
{sections.map((section) => (
170+
<SelectItem key={section.id} value={section.id}>
171+
{section.name}
172+
</SelectItem>
173+
))}
174+
</SelectContent>
175+
</RadixSelect>
167176
</div>
168177
);
169178
}

src/browser/components/RuntimeBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export function RuntimeBadge({
114114
</span>
115115
</TooltipTrigger>
116116
<TooltipContent align="end">
117-
<div>Coder: {coderWorkspaceName ?? runtimeConfig.host}</div>
117+
<div>Coder Workspace: {coderWorkspaceName ?? runtimeConfig.host}</div>
118118
{branchName && <BranchWithLabel branchName={branchName} />}
119119
{workspacePath && <PathWithCopy path={workspacePath} />}
120120
</TooltipContent>

src/node/orpc/router.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,10 @@ export const router = (authToken?: string) => {
449449
.input(schemas.agents.list.input)
450450
.output(schemas.agents.list.output)
451451
.handler(async ({ context, input }) => {
452+
// Wait for workspace init before agent discovery (SSH may not be ready yet)
453+
if (input.workspaceId) {
454+
await context.aiService.waitForInit(input.workspaceId);
455+
}
452456
const { runtime, discoveryPath } = await resolveAgentDiscoveryContext(context, input);
453457
const descriptors = await discoverAgentDefinitions(runtime, discoveryPath);
454458

@@ -483,6 +487,10 @@ export const router = (authToken?: string) => {
483487
.input(schemas.agents.get.input)
484488
.output(schemas.agents.get.output)
485489
.handler(async ({ context, input }) => {
490+
// Wait for workspace init before agent discovery (SSH may not be ready yet)
491+
if (input.workspaceId) {
492+
await context.aiService.waitForInit(input.workspaceId);
493+
}
486494
const { runtime, discoveryPath } = await resolveAgentDiscoveryContext(context, input);
487495
return readAgentDefinition(runtime, discoveryPath, input.agentId);
488496
}),

0 commit comments

Comments
 (0)