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
36 changes: 36 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,42 @@ jobs:
retention-days: 30
if-no-files-found: error

build-windows:
name: Build Windows
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags

- uses: ./.github/actions/setup-mux

- name: Install GNU Make (for build)
run: choco install -y make

- name: Verify tools
shell: bash
run: |
make --version
bun --version
magick --version | head -1

- name: Build application
run: bun run build

# No code signing - releases use release.yml (triggered by tag publish).
- name: Package for Windows
run: make dist-win

- name: Upload Windows exe
uses: actions/upload-artifact@v4
with:
name: windows-exe
path: release/*.exe
retention-days: 30
if-no-files-found: error

build-vscode-extension:
name: Build VS Code Extension
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:

permissions:
contents: write # Required for electron-builder to upload release assets
id-token: write # Required for GCP workload identity authentication (Windows code signing)

env:
RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
Expand Down Expand Up @@ -168,7 +169,50 @@ jobs:
- name: Build application
run: bun run build

# Setup Java for jsign (EV code signing with GCP KMS)
- name: Setup Java
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
distribution: "zulu"
java-version: "11.0"

- name: Authenticate to Google Cloud
id: gcloud_auth
if: ${{ vars.GCP_WORKLOAD_ID_PROVIDER != '' }}
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
with:
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
token_format: "access_token"

- name: Setup code signing
shell: pwsh
run: |
if (-not $env:EV_SIGNING_CERT) {
Write-Host "⚠️ No Windows code signing certificate provided - building unsigned"
exit 0
}

# Save EV certificate to temp file
$certPath = Join-Path $env:TEMP "ev_cert.pem"
Set-Content -Path $certPath -Value $env:EV_SIGNING_CERT
Add-Content -Path $env:GITHUB_ENV -Value "EV_CERTIFICATE_PATH=$certPath"

# Download jsign
$jsignPath = Join-Path $env:TEMP "jsign-6.0.jar"
Invoke-WebRequest -Uri "https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar" -OutFile $jsignPath
Add-Content -Path $env:GITHUB_ENV -Value "JSIGN_PATH=$jsignPath"

Write-Host "✅ Windows EV code signing configured"
env:
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}

- name: Package and publish for Windows (.exe)
run: bun x electron-builder --win --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# EV signing environment variables (used by custom sign script if configured)
EV_KEYSTORE: ${{ vars.EV_KEYSTORE }}
EV_KEY: ${{ vars.EV_KEY }}
EV_TSA_URL: ${{ vars.EV_TSA_URL }}
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
1 change: 0 additions & 1 deletion docs/system-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,4 @@ You are in a git worktree at ${workspacePath}
}
```


{/* END SYSTEM_PROMPT_DOCS */}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@
"win": {
"target": "nsis",
"icon": "build/icon.png",
"artifactName": "${productName}-${version}-${arch}.${ext}"
"artifactName": "${productName}-${version}-${arch}.${ext}",
"sign": "scripts/sign-windows.js",
"signingHashAlgorithms": [
"sha256"
]
},
"npmRebuild": false,
"buildDependenciesFromSource": false
Expand Down
78 changes: 78 additions & 0 deletions scripts/sign-windows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Windows EV code signing script for electron-builder
* Uses jsign with GCP Cloud KMS for EV certificate signing
*
* Required environment variables:
* JSIGN_PATH - Path to jsign JAR file
* EV_KEYSTORE - GCP Cloud KMS keystore URL
* EV_KEY - Key alias in the keystore
* EV_CERTIFICATE_PATH - Path to the EV certificate PEM file
* EV_TSA_URL - Timestamp server URL
* GCLOUD_ACCESS_TOKEN - GCP access token for authentication
*/

const { execSync } = require("child_process");
const path = require("path");

/**
* @param {import("electron-builder").CustomWindowsSignTaskConfiguration} configuration
* @returns {Promise<void>}
*/
exports.default = async function sign(configuration) {
const filePath = configuration.path;

// Check if signing is configured
if (!process.env.JSIGN_PATH || !process.env.EV_KEYSTORE) {
console.log(
`⚠️ Windows code signing not configured - skipping signing for ${filePath}`
);
return;
}

// Validate required environment variables
const requiredVars = [
"JSIGN_PATH",
"EV_KEYSTORE",
"EV_KEY",
"EV_CERTIFICATE_PATH",
"EV_TSA_URL",
"GCLOUD_ACCESS_TOKEN",
];

for (const varName of requiredVars) {
if (!process.env[varName]) {
throw new Error(`Missing required environment variable: ${varName}`);
}
}

console.log(`Signing ${filePath} with EV certificate...`);

const jsignArgs = [
"-jar",
process.env.JSIGN_PATH,
"--storetype",
"GOOGLECLOUD",
"--storepass",
process.env.GCLOUD_ACCESS_TOKEN,
"--keystore",
process.env.EV_KEYSTORE,
"--alias",
process.env.EV_KEY,
"--certfile",
process.env.EV_CERTIFICATE_PATH,
"--tsmode",
"RFC3161",
"--tsaurl",
process.env.EV_TSA_URL,
filePath,
];

try {
execSync(`java ${jsignArgs.map((a) => `"${a}"`).join(" ")}`, {
stdio: "inherit",
});
console.log(`✅ Successfully signed ${filePath}`);
} catch (error) {
throw new Error(`Failed to sign ${filePath}: ${error.message}`);
}
};
171 changes: 89 additions & 82 deletions src/browser/components/ChatInput/CreationControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,59 +56,7 @@ export function CreationControls(props: CreationControlsProps) {

return (
<div className="flex flex-col gap-2">
{/* First row: Workspace name with magic wand toggle */}
<div className="flex items-center gap-2" data-component="WorkspaceNameGroup">
<label htmlFor="workspace-name" className="text-muted text-xs whitespace-nowrap">
Name:
</label>
<div className="relative max-w-xs flex-1">
<input
id="workspace-name"
type="text"
value={nameState.name}
onChange={handleNameChange}
onFocus={handleInputFocus}
placeholder={nameState.isGenerating ? "Generating..." : "workspace-name"}
disabled={props.disabled}
className={cn(
"bg-separator text-foreground border-border-medium focus:border-accent h-6 w-full rounded border px-2 pr-6 text-xs focus:outline-none disabled:opacity-50",
nameState.error && "border-red-500"
)}
/>
{/* Magic wand / loading indicator - vertically centered */}
<div className="absolute inset-y-0 right-0 flex items-center pr-1.5">
{nameState.isGenerating ? (
<Loader2 className="text-accent h-3.5 w-3.5 animate-spin" />
) : (
<TooltipWrapper inline>
<button
type="button"
onClick={handleWandClick}
disabled={props.disabled}
className="flex h-full items-center disabled:opacity-50"
aria-label={nameState.autoGenerate ? "Disable auto-naming" : "Enable auto-naming"}
>
<Wand2
className={cn(
"h-3.5 w-3.5 transition-colors",
nameState.autoGenerate
? "text-accent"
: "text-muted-foreground opacity-50 hover:opacity-75"
)}
/>
</button>
<Tooltip className="tooltip" align="center">
{nameState.autoGenerate ? "Auto-naming enabled" : "Click to enable auto-naming"}
</Tooltip>
</TooltipWrapper>
)}
</div>
</div>
{/* Error display - inline */}
{nameState.error && <span className="text-xs text-red-500">{nameState.error}</span>}
</div>

{/* Second row: Runtime, Branch, SSH */}
{/* First row: Runtime selector (left) + Workspace name (right), wraps on small screens */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Runtime Selector - icon-based with tooltips */}
<RuntimeIconSelector
Expand All @@ -119,39 +67,98 @@ export function CreationControls(props: CreationControlsProps) {
disabled={props.disabled}
/>

{/* Trunk Branch Selector - hidden for Local runtime */}
{showTrunkBranchSelector && (
<div
className="flex h-6 items-center gap-1"
data-component="TrunkBranchGroup"
data-tutorial="trunk-branch"
>
<label htmlFor="trunk-branch" className="text-muted text-xs">
From:
</label>
<Select
id="trunk-branch"
value={props.trunkBranch}
options={props.branches}
onChange={props.onTrunkBranchChange}
{/* Workspace name with magic wand toggle */}
<div className="flex items-center gap-2" data-component="WorkspaceNameGroup">
<label htmlFor="workspace-name" className="text-muted text-xs whitespace-nowrap">
Name:
</label>
<div className="relative w-48">
<input
id="workspace-name"
type="text"
value={nameState.name}
onChange={handleNameChange}
onFocus={handleInputFocus}
placeholder={nameState.isGenerating ? "Generating..." : "workspace-name"}
disabled={props.disabled}
className="h-6 max-w-[120px]"
className={cn(
"bg-separator text-foreground border-border-medium focus:border-accent h-6 w-full rounded border px-2 pr-6 text-xs focus:outline-none disabled:opacity-50",
nameState.error && "border-red-500"
)}
/>
{/* Magic wand / loading indicator - vertically centered */}
<div className="absolute inset-y-0 right-0 flex items-center pr-1.5">
{nameState.isGenerating ? (
<Loader2 className="text-accent h-3.5 w-3.5 animate-spin" />
) : (
<TooltipWrapper inline>
<button
type="button"
onClick={handleWandClick}
disabled={props.disabled}
className="flex h-full items-center disabled:opacity-50"
aria-label={
nameState.autoGenerate ? "Disable auto-naming" : "Enable auto-naming"
}
>
<Wand2
className={cn(
"h-3.5 w-3.5 transition-colors",
nameState.autoGenerate
? "text-accent"
: "text-muted-foreground opacity-50 hover:opacity-75"
)}
/>
</button>
<Tooltip className="tooltip" align="center">
{nameState.autoGenerate ? "Auto-naming enabled" : "Click to enable auto-naming"}
</Tooltip>
</TooltipWrapper>
)}
</div>
</div>
)}

{/* SSH Host Input - after From selector */}
{props.runtimeMode === RUNTIME_MODE.SSH && (
<input
type="text"
value={props.sshHost}
onChange={(e) => props.onSshHostChange(e.target.value)}
placeholder="user@host"
disabled={props.disabled}
className="bg-separator text-foreground border-border-medium focus:border-accent h-6 w-32 rounded border px-1 text-xs focus:outline-none disabled:opacity-50"
/>
)}
{/* Error display - inline */}
{nameState.error && <span className="text-xs text-red-500">{nameState.error}</span>}
</div>
</div>

{/* Second row: Branch, SSH - only shown when there's content */}
{(showTrunkBranchSelector || props.runtimeMode === RUNTIME_MODE.SSH) && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Trunk Branch Selector - hidden for Local runtime */}
{showTrunkBranchSelector && (
<div
className="flex h-6 items-center gap-1"
data-component="TrunkBranchGroup"
data-tutorial="trunk-branch"
>
<label htmlFor="trunk-branch" className="text-muted text-xs">
From:
</label>
<Select
id="trunk-branch"
value={props.trunkBranch}
options={props.branches}
onChange={props.onTrunkBranchChange}
disabled={props.disabled}
className="h-6 max-w-[120px]"
/>
</div>
)}

{/* SSH Host Input - after From selector */}
{props.runtimeMode === RUNTIME_MODE.SSH && (
<input
type="text"
value={props.sshHost}
onChange={(e) => props.onSshHostChange(e.target.value)}
placeholder="user@host"
disabled={props.disabled}
className="bg-separator text-foreground border-border-medium focus:border-accent h-6 w-32 rounded border px-1 text-xs focus:outline-none disabled:opacity-50"
/>
)}
</div>
)}
</div>
);
}