Skip to content

Commit ad0c573

Browse files
committed
Release v0.0.54
## What's New - **Beta Release Channel**: Added Early Access opt-in toggle in Settings > Beta for users who want to receive beta releases - **Simplified Traffic Lights**: Always-native traffic lights on macOS, cleaned up file viewer headers - **Settings Cleanup**: Removed unused settings dialog, added async route redirect ## Downloads - [1Code-0.0.54-arm64.dmg](https://cdn.21st.dev/releases/desktop/1Code-0.0.54-arm64.dmg) — macOS Apple Silicon - [1Code-0.0.54.dmg](https://cdn.21st.dev/releases/desktop/1Code-0.0.54.dmg) — macOS Intel
1 parent 75fbcb6 commit ad0c573

62 files changed

Lines changed: 4175 additions & 1606 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lockb

8.83 KB
Binary file not shown.

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.53",
3+
"version": "0.0.54",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {
@@ -71,11 +71,12 @@
7171
"@xterm/addon-webgl": "^0.19.0",
7272
"ai": "^6.0.14",
7373
"async-mutex": "^0.5.0",
74-
"better-sqlite3": "^11.8.1",
74+
"better-sqlite3": "^12.6.2",
7575
"chokidar": "^5.0.0",
7676
"class-variance-authority": "^0.7.1",
7777
"clsx": "^2.1.1",
7878
"date-fns": "^3.6.0",
79+
"diff": "^8.0.3",
7980
"drizzle-orm": "^0.45.1",
8081
"electron-log": "^5.4.3",
8182
"electron-updater": "^6.7.3",
@@ -115,16 +116,17 @@
115116
"@electron-toolkit/preload": "^3.0.1",
116117
"@electron-toolkit/utils": "^4.0.0",
117118
"@types/better-sqlite3": "^7.6.13",
119+
"@types/diff": "^8.0.0",
118120
"@types/node": "^20.17.50",
119121
"@types/react": "^19.0.7",
120122
"@types/react-dom": "^19.0.3",
121123
"@vitejs/plugin-react": "^4.3.4",
122124
"@welldone-software/why-did-you-render": "^10.0.1",
123125
"autoprefixer": "^10.4.20",
124126
"drizzle-kit": "^0.31.8",
125-
"electron": "33.4.5",
127+
"electron": "~39.4.0",
126128
"electron-builder": "^25.1.8",
127-
"electron-rebuild": "^3.2.9",
129+
"@electron/rebuild": "^4.0.3",
128130
"electron-vite": "^3.0.0",
129131
"postcss": "^8.5.1",
130132
"tailwindcss": "^3.4.17",

scripts/generate-update-manifest.mjs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ import { fileURLToPath } from "url"
2424
const __filename = fileURLToPath(import.meta.url)
2525
const __dirname = dirname(__filename)
2626

27+
// Parse --channel argument (default: "latest")
28+
const channelArgIndex = process.argv.indexOf("--channel")
29+
const channel = channelArgIndex !== -1 && process.argv[channelArgIndex + 1]
30+
? process.argv[channelArgIndex + 1]
31+
: "latest"
32+
33+
if (channel !== "latest" && channel !== "beta") {
34+
console.error(`Invalid channel: "${channel}". Must be "latest" or "beta".`)
35+
process.exit(1)
36+
}
37+
2738
// Get version from package.json
2839
const packageJson = JSON.parse(
2940
readFileSync(join(__dirname, "../package.json"), "utf-8")
@@ -97,10 +108,11 @@ function generateManifest(arch) {
97108
}
98109

99110
// Manifest file names expected by electron-updater:
100-
// arm64: latest-mac.yml (primary)
101-
// x64: latest-mac-x64.yml
111+
// For stable (latest): latest-mac.yml / latest-mac-x64.yml
112+
// For beta: beta-mac.yml / beta-mac-x64.yml
113+
const prefix = channel === "beta" ? "beta" : "latest"
102114
const manifestFileName =
103-
arch === "arm64" ? "latest-mac.yml" : "latest-mac-x64.yml"
115+
arch === "arm64" ? `${prefix}-mac.yml` : `${prefix}-mac-x64.yml`
104116
const manifestPath = join(releaseDir, manifestFileName)
105117

106118
// Convert to YAML format (simple implementation)
@@ -167,6 +179,7 @@ console.log("=".repeat(50))
167179
console.log("Generating electron-updater manifests")
168180
console.log("=".repeat(50))
169181
console.log(`Version: ${version}`)
182+
console.log(`Channel: ${channel}`)
170183
console.log(`Release dir: ${releaseDir}`)
171184
console.log()
172185

@@ -182,15 +195,16 @@ if (!arm64Manifest && !x64Manifest) {
182195
console.log("=".repeat(50))
183196
console.log("Manifest generation complete!")
184197
console.log()
198+
const prefix = channel === "beta" ? "beta" : "latest"
185199
console.log("Next steps:")
186200
console.log("1. Upload the following files to cdn.21st.dev/releases/desktop/:")
187201
if (arm64Manifest) {
188-
console.log(` - latest-mac.yml`)
202+
console.log(` - ${prefix}-mac.yml`)
189203
console.log(` - Agents-${version}-arm64-mac.zip`)
190204
console.log(` - Agents-${version}-arm64.dmg (for manual download)`)
191205
}
192206
if (x64Manifest) {
193-
console.log(` - latest-mac-x64.yml`)
207+
console.log(` - ${prefix}-mac-x64.yml`)
194208
console.log(` - Agents-${version}-mac.zip`)
195209
console.log(` - Agents-${version}.dmg (for manual download)`)
196210
}

src/main/lib/auto-updater.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BrowserWindow, ipcMain, app } from "electron"
22
import log from "electron-log"
33
import { autoUpdater, type UpdateInfo, type ProgressInfo } from "electron-updater"
4+
import { readFileSync, writeFileSync, existsSync } from "fs"
5+
import { join } from "path"
46

57
/**
68
* IMPORTANT: Do NOT use lazy/dynamic imports for electron-updater!
@@ -30,6 +32,38 @@ const CDN_BASE = "https://cdn.21st.dev/releases/desktop"
3032
const MIN_CHECK_INTERVAL = 60 * 1000 // 1 minute
3133
let lastCheckTime = 0
3234

35+
// Update channel preference file
36+
const CHANNEL_PREF_FILE = "update-channel.json"
37+
38+
type UpdateChannel = "latest" | "beta"
39+
40+
function getChannelPrefPath(): string {
41+
return join(app.getPath("userData"), CHANNEL_PREF_FILE)
42+
}
43+
44+
function getSavedChannel(): UpdateChannel {
45+
try {
46+
const prefPath = getChannelPrefPath()
47+
if (existsSync(prefPath)) {
48+
const data = JSON.parse(readFileSync(prefPath, "utf-8"))
49+
if (data.channel === "beta" || data.channel === "latest") {
50+
return data.channel
51+
}
52+
}
53+
} catch {
54+
// Ignore read errors, fall back to default
55+
}
56+
return "latest"
57+
}
58+
59+
function saveChannel(channel: UpdateChannel): void {
60+
try {
61+
writeFileSync(getChannelPrefPath(), JSON.stringify({ channel }), "utf-8")
62+
} catch (error) {
63+
log.error("[AutoUpdater] Failed to save channel preference:", error)
64+
}
65+
}
66+
3367
let getAllWindows: (() => BrowserWindow[]) | null = null
3468

3569
/**
@@ -58,6 +92,11 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) {
5892
// Initialize config
5993
initAutoUpdaterConfig()
6094

95+
// Set update channel from saved preference
96+
const savedChannel = getSavedChannel()
97+
autoUpdater.channel = savedChannel
98+
log.info(`[AutoUpdater] Using update channel: ${savedChannel}`)
99+
61100
// Configure feed URL to point to R2 CDN
62101
// Note: We use a custom request headers to bypass CDN cache
63102
autoUpdater.setFeedURL({
@@ -202,6 +241,31 @@ function registerIpcHandlers() {
202241
currentVersion: app.getVersion(),
203242
}
204243
})
244+
245+
// Set update channel (latest = stable only, beta = stable + beta)
246+
ipcMain.handle("update:set-channel", async (_event, channel: string) => {
247+
if (channel !== "latest" && channel !== "beta") {
248+
log.warn(`[AutoUpdater] Invalid channel: ${channel}`)
249+
return false
250+
}
251+
log.info(`[AutoUpdater] Switching update channel to: ${channel}`)
252+
autoUpdater.channel = channel
253+
saveChannel(channel)
254+
// Check for updates immediately with new channel
255+
if (app.isPackaged) {
256+
try {
257+
await autoUpdater.checkForUpdates()
258+
} catch (error) {
259+
log.error("[AutoUpdater] Post-channel-switch check failed:", error)
260+
}
261+
}
262+
return true
263+
})
264+
265+
// Get current update channel
266+
ipcMain.handle("update:get-channel", () => {
267+
return getSavedChannel()
268+
})
205269
}
206270

207271
/**

src/main/lib/claude-config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,38 @@ export function updateMcpServerConfig(
179179
return config
180180
}
181181

182+
/**
183+
* Remove an MCP server from config
184+
* Use projectPath = GLOBAL_MCP_PATH (or null) for global MCP servers
185+
* Automatically resolves worktree paths to original project paths
186+
*/
187+
export function removeMcpServerConfig(
188+
config: ClaudeConfig,
189+
projectPath: string | null,
190+
serverName: string
191+
): ClaudeConfig {
192+
// Global MCP servers
193+
if (!projectPath || projectPath === GLOBAL_MCP_PATH) {
194+
if (config.mcpServers?.[serverName]) {
195+
delete config.mcpServers[serverName]
196+
}
197+
return config
198+
}
199+
// Project-specific MCP servers
200+
const resolvedPath = resolveProjectPathFromWorktree(projectPath) || projectPath
201+
if (config.projects?.[resolvedPath]?.mcpServers?.[serverName]) {
202+
delete config.projects[resolvedPath].mcpServers[serverName]
203+
// Clean up empty objects
204+
if (Object.keys(config.projects[resolvedPath].mcpServers).length === 0) {
205+
delete config.projects[resolvedPath].mcpServers
206+
}
207+
if (Object.keys(config.projects[resolvedPath]).length === 0) {
208+
delete config.projects[resolvedPath]
209+
}
210+
}
211+
return config
212+
}
213+
182214
/**
183215
* Resolve original project path from a worktree path.
184216
* Supports legacy (~/.21st/worktrees/{projectId}/{chatId}/) and

src/main/lib/claude/transform.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,25 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs
6060
if (currentToolCallId) {
6161
// Track this tool ID to avoid duplicates from assistant message
6262
emittedToolIds.add(currentToolCallId)
63-
63+
64+
let parsedInput = {}
65+
if (accumulatedToolInput) {
66+
try {
67+
parsedInput = JSON.parse(accumulatedToolInput)
68+
} catch (e) {
69+
// Stream may have been interrupted mid-JSON (e.g. network error, abort)
70+
// resulting in incomplete JSON like '{"prompt":"write co'
71+
console.error("[transform] Failed to parse tool input JSON:", (e as Error).message, "partial:", accumulatedToolInput.slice(0, 120))
72+
parsedInput = { _raw: accumulatedToolInput, _parseError: true }
73+
}
74+
}
75+
6476
// Emit complete tool call with accumulated input
6577
yield {
6678
type: "tool-input-available",
6779
toolCallId: currentToolCallId,
6880
toolName: currentToolName || "unknown",
69-
input: accumulatedToolInput ? JSON.parse(accumulatedToolInput) : {},
81+
input: parsedInput,
7082
}
7183
currentToolCallId = null
7284
currentToolName = null

src/main/lib/git/diff-parser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ export function splitUnifiedDiffByFile(diffText: string): ParsedDiffFile[] {
175175
let deletions = 0
176176

177177
for (const line of blockLines) {
178+
if (line.startsWith("diff --git ")) {
179+
// Fallback: parse paths from "diff --git a/path b/path"
180+
// Needed for binary files that don't have ---/+++ lines
181+
const match = line.match(/^diff --git a\/(.+) b\/(.+)$/)
182+
if (match) {
183+
if (!oldPath) oldPath = match[1]!
184+
if (!newPath) newPath = match[2]!
185+
}
186+
}
187+
178188
if (line.startsWith("Binary files ") && line.endsWith(" differ")) {
179189
isBinary = true
180190
}

src/main/lib/mcp-auth.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from './claude-config';
1212
import { getClaudeShellEnvironment } from './claude/env';
1313
import { CraftOAuth, fetchOAuthMetadata, getMcpBaseUrl, type OAuthMetadata, type OAuthTokens } from './oauth';
14+
import { discoverPluginMcpServers } from './plugins';
1415
import { bringToFront } from './window';
1516

1617

@@ -172,7 +173,26 @@ export async function startMcpOAuth(
172173
): Promise<{ success: boolean; error?: string }> {
173174
// 1. Read server config from ~/.claude.json
174175
const config = await readClaudeConfig();
175-
const serverConfig = getMcpServerConfig(config, projectPath, serverName);
176+
let serverConfig = getMcpServerConfig(config, projectPath, serverName);
177+
178+
// Fallback: check plugin MCP servers if not found in ~/.claude.json
179+
if (!serverConfig?.url) {
180+
const pluginMcpConfigs = await discoverPluginMcpServers();
181+
for (const pluginConfig of pluginMcpConfigs) {
182+
if (pluginConfig.mcpServers[serverName]) {
183+
serverConfig = pluginConfig.mcpServers[serverName];
184+
// Save plugin server config to ~/.claude.json so token storage works
185+
await updateClaudeConfigAtomic((cfg) => {
186+
return updateMcpServerConfig(cfg, GLOBAL_MCP_PATH, serverName, {
187+
url: serverConfig!.url,
188+
type: serverConfig!.url?.endsWith('/sse') ? 'sse' : 'http',
189+
authType: 'oauth',
190+
});
191+
});
192+
break;
193+
}
194+
}
195+
}
176196

177197
if (!serverConfig?.url) {
178198
return { success: false, error: `MCP server "${serverName}" URL not configured` };

0 commit comments

Comments
 (0)