Skip to content

Commit b9fa4c5

Browse files
committed
Release v0.0.34
## What's New ### Features - **Simplified Plan Mode** — Plan now shows as a compact card with option to view full plan in sidebar - **Always Expand To-Do List** — New option to keep to-do list expanded (Settings → Preferences) ### Improvements & Fixes - Fixed Claude SDK spawn errors and handle expired sessions — thanks @nicholasoxford! - Fixed Windows compatibility for git worktree operations — thanks @nicholasoxford! - Only show traffic light spacer on macOS — thanks @nicholasoxford! - Improved plan collapsing and removed redundant "Build plan" button - Skip Ollama checks when offline mode is disabled - GitHub avatar loading placeholder in settings - Fixed custom headers in MCP auth
1 parent 4e73c24 commit b9fa4c5

8 files changed

Lines changed: 260 additions & 4 deletions

File tree

resources/cli/1code

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
# 1code CLI launcher
3+
# Opens 1Code app with the specified directory
4+
5+
# Resolve the directory argument (default to current directory)
6+
DIR="${1:-.}"
7+
8+
# Convert to absolute path
9+
if [[ "$DIR" != /* ]]; then
10+
DIR="$(cd "$DIR" 2>/dev/null && pwd)"
11+
fi
12+
13+
if [ -z "$DIR" ] || [ ! -d "$DIR" ]; then
14+
echo "Error: Invalid directory"
15+
exit 1
16+
fi
17+
18+
# Open 1Code app with the directory argument
19+
open -a "1Code" --args "$DIR"

src/main/index.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import {
1919
setupFocusUpdateCheck,
2020
} from "./lib/auto-updater"
2121
import { closeDatabase, initDatabase } from "./lib/db"
22+
import {
23+
getLaunchDirectory,
24+
isCliInstalled,
25+
installCli,
26+
uninstallCli,
27+
parseLaunchDirectory,
28+
} from "./lib/cli"
2229
import { cleanupGitWatchers } from "./lib/git/watcher"
2330
import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth"
2431
import { createMainWindow, getWindow } from "./windows/main"
@@ -582,6 +589,41 @@ if (gotTheLock) {
582589
},
583590
},
584591
{ type: "separator" },
592+
{
593+
label: isCliInstalled()
594+
? "Uninstall '1code' Command..."
595+
: "Install '1code' Command in PATH...",
596+
click: async () => {
597+
const { dialog } = await import("electron")
598+
if (isCliInstalled()) {
599+
const result = await uninstallCli()
600+
if (result.success) {
601+
dialog.showMessageBox({
602+
type: "info",
603+
message: "CLI command uninstalled",
604+
detail: "The '1code' command has been removed from your PATH.",
605+
})
606+
buildMenu()
607+
} else {
608+
dialog.showErrorBox("Uninstallation Failed", result.error || "Unknown error")
609+
}
610+
} else {
611+
const result = await installCli()
612+
if (result.success) {
613+
dialog.showMessageBox({
614+
type: "info",
615+
message: "CLI command installed",
616+
detail:
617+
"You can now use '1code .' in any terminal to open 1Code in that directory.",
618+
})
619+
buildMenu()
620+
} else {
621+
dialog.showErrorBox("Installation Failed", result.error || "Unknown error")
622+
}
623+
}
624+
},
625+
},
626+
{ type: "separator" },
585627
{ role: "services" },
586628
{ type: "separator" },
587629
{ role: "hide" },
@@ -748,6 +790,9 @@ if (gotTheLock) {
748790
}
749791
}, 3000)
750792

793+
// Handle directory argument from CLI (e.g., `1code /path/to/project`)
794+
parseLaunchDirectory()
795+
751796
// Handle deep link from app launch (Windows/Linux)
752797
const deepLinkUrl = process.argv.find((arg) =>
753798
arg.startsWith(`${PROTOCOL}://`),

src/main/lib/claude/env.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ let cachedShellEnv: Record<string, string> | null = null
1111
// Delimiter for parsing env output
1212
const DELIMITER = "_CLAUDE_ENV_DELIMITER_"
1313

14-
// Keys to strip (prevent auth interference)
14+
// Keys to strip (prevent interference from unrelated providers)
15+
// NOTE: We intentionally keep ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL
16+
// so users can use their existing Claude Code CLI configuration (API proxy, etc.)
17+
// Based on PR #29 by @sa4hnd
1518
const STRIPPED_ENV_KEYS = [
16-
"ANTHROPIC_API_KEY",
1719
"OPENAI_API_KEY",
1820
"CLAUDE_CODE_USE_BEDROCK",
1921
"CLAUDE_CODE_USE_VERTEX",

src/main/lib/cli.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* CLI command support for 1code
3+
* Allows users to open 1code from terminal with: 1code . or 1code /path/to/project
4+
*
5+
* Based on PR #16 by @caffeinum (Aleksey Bykhun)
6+
* https://github.com/21st-dev/1code/pull/16
7+
*/
8+
9+
import { app } from "electron"
10+
import { join } from "path"
11+
import { existsSync, lstatSync, readlinkSync } from "fs"
12+
13+
// Launch directory from CLI (e.g., `1code /path/to/project`)
14+
let launchDirectory: string | null = null
15+
16+
/**
17+
* Get the launch directory passed via CLI args (consumed once)
18+
*/
19+
export function getLaunchDirectory(): string | null {
20+
const dir = launchDirectory
21+
launchDirectory = null // consume once
22+
return dir
23+
}
24+
25+
/**
26+
* Parse CLI arguments to find a directory argument
27+
* Called on app startup to handle `1code .` or `1code /path/to/project`
28+
*/
29+
export function parseLaunchDirectory(): void {
30+
// Look for a directory argument in argv
31+
// Skip electron executable and script path
32+
const args = process.argv.slice(process.defaultApp ? 2 : 1)
33+
34+
for (const arg of args) {
35+
// Skip flags and protocol URLs
36+
if (arg.startsWith("-") || arg.includes("://")) continue
37+
38+
// Check if it's a valid directory
39+
if (existsSync(arg)) {
40+
try {
41+
const stat = lstatSync(arg)
42+
if (stat.isDirectory()) {
43+
console.log("[CLI] Launch directory:", arg)
44+
launchDirectory = arg
45+
return
46+
}
47+
} catch {
48+
// ignore
49+
}
50+
}
51+
}
52+
}
53+
54+
// CLI command installation paths
55+
const CLI_INSTALL_PATH = "/usr/local/bin/1code"
56+
57+
function getCliSourcePath(): string {
58+
if (app.isPackaged) {
59+
return join(process.resourcesPath, "cli", "1code")
60+
}
61+
return join(__dirname, "..", "..", "resources", "cli", "1code")
62+
}
63+
64+
/**
65+
* Check if the CLI command is installed
66+
*/
67+
export function isCliInstalled(): boolean {
68+
try {
69+
if (!existsSync(CLI_INSTALL_PATH)) return false
70+
const stat = lstatSync(CLI_INSTALL_PATH)
71+
if (!stat.isSymbolicLink()) return false
72+
const target = readlinkSync(CLI_INSTALL_PATH)
73+
return target === getCliSourcePath()
74+
} catch {
75+
return false
76+
}
77+
}
78+
79+
/**
80+
* Install the CLI command to /usr/local/bin/1code
81+
* Requires admin privileges on macOS
82+
*/
83+
export async function installCli(): Promise<{ success: boolean; error?: string }> {
84+
const { exec } = await import("child_process")
85+
const { promisify } = await import("util")
86+
const execAsync = promisify(exec)
87+
88+
const sourcePath = getCliSourcePath()
89+
90+
if (!existsSync(sourcePath)) {
91+
return { success: false, error: "CLI script not found in app bundle" }
92+
}
93+
94+
try {
95+
// Remove existing if present
96+
if (existsSync(CLI_INSTALL_PATH)) {
97+
await execAsync(
98+
`osascript -e 'do shell script "rm -f ${CLI_INSTALL_PATH}" with administrator privileges'`,
99+
)
100+
}
101+
102+
// Create symlink with admin privileges
103+
await execAsync(
104+
`osascript -e 'do shell script "ln -s \\"${sourcePath}\\" ${CLI_INSTALL_PATH}" with administrator privileges'`,
105+
)
106+
107+
console.log("[CLI] Installed 1code command to", CLI_INSTALL_PATH)
108+
return { success: true }
109+
} catch (error: unknown) {
110+
const errorMessage = error instanceof Error ? error.message : "Installation failed"
111+
console.error("[CLI] Failed to install:", error)
112+
return { success: false, error: errorMessage }
113+
}
114+
}
115+
116+
/**
117+
* Uninstall the CLI command from /usr/local/bin/1code
118+
* Requires admin privileges on macOS
119+
*/
120+
export async function uninstallCli(): Promise<{ success: boolean; error?: string }> {
121+
const { exec } = await import("child_process")
122+
const { promisify } = await import("util")
123+
const execAsync = promisify(exec)
124+
125+
try {
126+
if (existsSync(CLI_INSTALL_PATH)) {
127+
await execAsync(
128+
`osascript -e 'do shell script "rm -f ${CLI_INSTALL_PATH}" with administrator privileges'`,
129+
)
130+
}
131+
console.log("[CLI] Uninstalled 1code command")
132+
return { success: true }
133+
} catch (error: unknown) {
134+
const errorMessage = error instanceof Error ? error.message : "Uninstallation failed"
135+
console.error("[CLI] Failed to uninstall:", error)
136+
return { success: false, error: errorMessage }
137+
}
138+
}

src/main/lib/trpc/routers/claude-code.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { eq } from "drizzle-orm"
22
import { safeStorage, shell } from "electron"
33
import { z } from "zod"
44
import { getAuthManager } from "../../../index"
5+
import { getClaudeShellEnvironment } from "../../claude"
56
import { getExistingClaudeToken } from "../../claude-token"
67
import { getApiUrl } from "../../config"
78
import { claudeCodeCredentials, getDatabase } from "../../db"
@@ -63,6 +64,21 @@ function storeOAuthToken(oauthToken: string) {
6364
* Uses server only for sandbox creation, stores token locally
6465
*/
6566
export const claudeCodeRouter = router({
67+
/**
68+
* Check if user has existing CLI config (API key or proxy)
69+
* If true, user can skip OAuth onboarding
70+
* Based on PR #29 by @sa4hnd
71+
*/
72+
hasExistingCliConfig: publicProcedure.query(() => {
73+
const shellEnv = getClaudeShellEnvironment()
74+
const hasConfig = !!(shellEnv.ANTHROPIC_API_KEY || shellEnv.ANTHROPIC_BASE_URL)
75+
return {
76+
hasConfig,
77+
hasApiKey: !!shellEnv.ANTHROPIC_API_KEY,
78+
baseUrl: shellEnv.ANTHROPIC_BASE_URL || null,
79+
}
80+
}),
81+
6682
/**
6783
* Check if user has Claude Code connected (local check)
6884
*/

src/main/lib/trpc/routers/claude.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,10 +736,20 @@ export const claudeRouter = router({
736736
console.error(`[claude] Failed to setup isolated config dir:`, mkdirErr)
737737
}
738738

739-
// Build final env - only add OAuth token if we have one
739+
// Check if user has existing API key or proxy configured in their shell environment
740+
// If so, use that instead of OAuth (allows using custom API proxies)
741+
// Based on PR #29 by @sa4hnd
742+
const hasExistingApiConfig = !!(claudeEnv.ANTHROPIC_API_KEY || claudeEnv.ANTHROPIC_BASE_URL)
743+
744+
if (hasExistingApiConfig) {
745+
console.log(`[claude] Using existing CLI config - API_KEY: ${claudeEnv.ANTHROPIC_API_KEY ? "set" : "not set"}, BASE_URL: ${claudeEnv.ANTHROPIC_BASE_URL || "default"}`)
746+
}
747+
748+
// Build final env - only add OAuth token if we have one AND no existing API config
749+
// Existing CLI config takes precedence over OAuth
740750
const finalEnv = {
741751
...claudeEnv,
742-
...(claudeCodeToken && {
752+
...(claudeCodeToken && !hasExistingApiConfig && {
743753
CLAUDE_CODE_OAUTH_TOKEN: claudeCodeToken,
744754
}),
745755
// Re-enable CLAUDE_CONFIG_DIR now that we properly map MCP configs

src/main/lib/trpc/routers/projects.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ import { existsSync } from "node:fs"
1010
import { mkdir } from "node:fs/promises"
1111
import { getGitRemoteInfo } from "../../git"
1212
import { trackProjectOpened } from "../../analytics"
13+
import { getLaunchDirectory } from "../../cli"
1314

1415
const execAsync = promisify(exec)
1516

1617
export const projectsRouter = router({
18+
/**
19+
* Get launch directory from CLI args (consumed once)
20+
* Based on PR #16 by @caffeinum
21+
*/
22+
getLaunchDirectory: publicProcedure.query(() => {
23+
return getLaunchDirectory()
24+
}),
25+
1726
/**
1827
* List all projects
1928
*/

src/renderer/App.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,16 @@ function AppContent() {
4545
const anthropicOnboardingCompleted = useAtomValue(
4646
anthropicOnboardingCompletedAtom
4747
)
48+
const setAnthropicOnboardingCompleted = useSetAtom(anthropicOnboardingCompletedAtom)
4849
const apiKeyOnboardingCompleted = useAtomValue(apiKeyOnboardingCompletedAtom)
50+
const setApiKeyOnboardingCompleted = useSetAtom(apiKeyOnboardingCompletedAtom)
4951
const selectedProject = useAtomValue(selectedProjectAtom)
5052

53+
// Check if user has existing CLI config (API key or proxy)
54+
// Based on PR #29 by @sa4hnd
55+
const { data: cliConfig, isLoading: isLoadingCliConfig } =
56+
trpc.claudeCode.hasExistingCliConfig.useQuery()
57+
5158
// Migration: If user already completed Anthropic onboarding but has no billing method set,
5259
// automatically set it to "claude-subscription" (legacy users before billing method was added)
5360
useEffect(() => {
@@ -56,6 +63,16 @@ function AppContent() {
5663
}
5764
}, [billingMethod, anthropicOnboardingCompleted, setBillingMethod])
5865

66+
// Auto-skip onboarding if user has existing CLI config (API key or proxy)
67+
// This allows users with ANTHROPIC_API_KEY to use the app without OAuth
68+
useEffect(() => {
69+
if (cliConfig?.hasConfig && !billingMethod) {
70+
console.log("[App] Detected existing CLI config, auto-completing onboarding")
71+
setBillingMethod("api-key")
72+
setApiKeyOnboardingCompleted(true)
73+
}
74+
}, [cliConfig?.hasConfig, billingMethod, setBillingMethod, setApiKeyOnboardingCompleted])
75+
5976
// Fetch projects to validate selectedProject exists
6077
const { data: projects, isLoading: isLoadingProjects } =
6178
trpc.projects.list.useQuery()

0 commit comments

Comments
 (0)