Skip to content

Commit 4e73c24

Browse files
committed
Release v0.0.34
## What's New ### Features - **Simplified Plan Mode** — New compact plan card and improved sidebar display - **Always Expand To-Do List** — New option to keep to-do list expanded (Settings → Preferences) - **Unified Analytics** — Improved PostHog tracking for desktop and web ### 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 3aa79cc commit 4e73c24

Some content is hidden

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

59 files changed

+1363
-4676
lines changed

bun.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.33",
3+
"version": "0.0.34",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {

src/main/auth-manager.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,29 @@ export class AuthManager {
250250
// Update locally
251251
return this.store.updateUser({ name: updates.name ?? null })
252252
}
253+
254+
/**
255+
* Fetch user's subscription plan from web backend
256+
* Used for PostHog analytics enrichment
257+
*/
258+
async fetchUserPlan(): Promise<{ email: string; plan: string; status: string | null } | null> {
259+
const token = await this.getValidToken()
260+
if (!token) return null
261+
262+
try {
263+
const response = await fetch(`${this.getApiUrl()}/api/desktop/user/plan`, {
264+
headers: { "X-Desktop-Token": token },
265+
})
266+
267+
if (!response.ok) {
268+
console.error("[AuthManager] Failed to fetch user plan:", response.status)
269+
return null
270+
}
271+
272+
return response.json()
273+
} catch (error) {
274+
console.error("[AuthManager] Failed to fetch user plan:", error)
275+
return null
276+
}
277+
}
253278
}

src/main/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AuthManager } from "./auth-manager"
77
import {
88
identify,
99
initAnalytics,
10+
setSubscriptionPlan,
1011
shutdown as shutdownAnalytics,
1112
trackAppOpened,
1213
trackAuthCompleted,
@@ -89,6 +90,16 @@ export async function handleAuthCode(code: string): Promise<void> {
8990
// Track successful authentication
9091
trackAuthCompleted(authData.user.id, authData.user.email)
9192

93+
// Fetch and set subscription plan for analytics
94+
try {
95+
const planData = await authManager.fetchUserPlan()
96+
if (planData) {
97+
setSubscriptionPlan(planData.plan)
98+
}
99+
} catch (e) {
100+
console.warn("[Auth] Failed to fetch user plan for analytics:", e)
101+
}
102+
92103
// Set desktop token cookie using persist:main partition
93104
const ses = session.fromPartition("persist:main")
94105
try {

src/main/lib/analytics.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
import { PostHog } from "posthog-node"
77
import { app } from "electron"
88

9-
// PostHog configuration from environment
10-
const POSTHOG_DESKTOP_KEY = import.meta.env.MAIN_VITE_POSTHOG_KEY
9+
// PostHog configuration - hardcoded key for opensource users, env var override for internal builds
10+
// This enables analytics for all users including those building from source
11+
const POSTHOG_DESKTOP_KEY = import.meta.env.MAIN_VITE_POSTHOG_KEY || "phc_wM7gbrJhOLTvynyhnhPkrVGDc5mKRSXsLGQHqM3T3vq"
1112
const POSTHOG_HOST = import.meta.env.MAIN_VITE_POSTHOG_HOST || "https://us.i.posthog.com"
1213

1314
let posthog: PostHog | null = null
1415
let currentUserId: string | null = null
1516
let userOptedOut = false // Synced from renderer
1617

18+
// Cached user properties for analytics enrichment
19+
let cachedSubscriptionPlan: string | null = null
20+
let cachedConnectionMethod: string | null = null
21+
1722
// Check if we're in development mode
1823
// Set FORCE_ANALYTICS=true to test analytics in development
1924
// Use a function to check lazily after app is ready
@@ -31,12 +36,15 @@ function isDev(): boolean {
3136
*/
3237
function getCommonProperties() {
3338
return {
34-
source: "desktop_main",
39+
source: "desktop", // Unified source for desktop vs web analytics
3540
app_version: app.getVersion(),
3641
platform: process.platform,
3742
arch: process.arch,
3843
electron_version: process.versions.electron,
3944
node_version: process.versions.node,
45+
// Analytics enrichment properties
46+
subscription_plan: cachedSubscriptionPlan,
47+
connection_method: cachedConnectionMethod,
4048
}
4149
}
4250

@@ -47,6 +55,21 @@ export function setOptOut(optedOut: boolean) {
4755
userOptedOut = optedOut
4856
}
4957

58+
/**
59+
* Set subscription plan (called after fetching from API)
60+
*/
61+
export function setSubscriptionPlan(plan: string) {
62+
cachedSubscriptionPlan = plan
63+
}
64+
65+
/**
66+
* Set connection method (called from renderer via IPC)
67+
* Values: "claude-subscription" | "api-key" | "custom-model"
68+
*/
69+
export function setConnectionMethod(method: string) {
70+
cachedConnectionMethod = method
71+
}
72+
5073
/**
5174
* Initialize PostHog for main process
5275
*/
@@ -135,6 +158,9 @@ export function getCurrentUserId(): string | null {
135158
*/
136159
export function reset() {
137160
currentUserId = null
161+
// Reset cached analytics properties
162+
cachedSubscriptionPlan = null
163+
cachedConnectionMethod = null
138164
// PostHog Node.js SDK doesn't have a reset method
139165
// Events will be sent as anonymous until next identify
140166
}
@@ -192,11 +218,13 @@ export function trackWorkspaceCreated(workspace: {
192218
id: string
193219
projectId: string
194220
useWorktree: boolean
221+
repository?: string
195222
}) {
196223
capture("workspace_created", {
197224
workspace_id: workspace.id,
198225
project_id: workspace.projectId,
199226
use_worktree: workspace.useWorktree,
227+
repository: workspace.repository,
200228
})
201229
}
202230

@@ -223,12 +251,12 @@ export function trackWorkspaceDeleted(workspaceId: string) {
223251
*/
224252
export function trackMessageSent(data: {
225253
workspaceId: string
226-
messageLength: number
254+
subChatId?: string
227255
mode: "plan" | "agent"
228256
}) {
229257
capture("message_sent", {
230258
workspace_id: data.workspaceId,
231-
message_length: data.messageLength,
259+
sub_chat_id: data.subChatId,
232260
mode: data.mode,
233261
})
234262
}
@@ -239,9 +267,41 @@ export function trackMessageSent(data: {
239267
export function trackPRCreated(data: {
240268
workspaceId: string
241269
prNumber: number
270+
repository?: string
271+
mode?: "worktree" | "local"
242272
}) {
243273
capture("pr_created", {
244274
workspace_id: data.workspaceId,
245275
pr_number: data.prNumber,
276+
repository: data.repository,
277+
mode: data.mode,
278+
})
279+
}
280+
281+
/**
282+
* Track commit created
283+
*/
284+
export function trackCommitCreated(data: {
285+
workspaceId: string
286+
filesChanged: number
287+
mode: "worktree" | "local"
288+
}) {
289+
capture("commit_created", {
290+
workspace_id: data.workspaceId,
291+
files_changed: data.filesChanged,
292+
mode: data.mode,
293+
})
294+
}
295+
296+
/**
297+
* Track sub-chat created
298+
*/
299+
export function trackSubChatCreated(data: {
300+
workspaceId: string
301+
subChatId: string
302+
}) {
303+
capture("sub_chat_created", {
304+
workspace_id: data.workspaceId,
305+
sub_chat_id: data.subChatId,
246306
})
247307
}

src/main/lib/claude/offline-handler.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@ export type OfflineCheckResult = {
2020
* Check if we should use Ollama as fallback
2121
* Priority:
2222
* 1. If customConfig provided → use it
23-
* 2. If OFFLINE → use Ollama (ignore auth token)
23+
* 2. If offline mode enabled AND no internet → use Ollama
2424
* 3. If online + auth → use Claude API
2525
*
2626
* @param customConfig - Custom config from user settings
2727
* @param claudeCodeToken - Claude Code auth token
2828
* @param selectedOllamaModel - User-selected Ollama model (optional)
29+
* @param offlineModeEnabled - Whether offline mode is enabled in settings
2930
*/
3031
export async function checkOfflineFallback(
3132
customConfig: CustomClaudeConfig | undefined,
3233
claudeCodeToken: string | null,
3334
selectedOllamaModel?: string | null,
35+
offlineModeEnabled: boolean = false,
3436
): Promise<OfflineCheckResult> {
3537
// If custom config is provided, use it (highest priority)
3638
if (customConfig) {
@@ -41,6 +43,15 @@ export async function checkOfflineFallback(
4143
}
4244
}
4345

46+
// If offline mode is disabled in settings, skip all Ollama checks
47+
// and just use Claude API (will fail with auth error if no token)
48+
if (!offlineModeEnabled) {
49+
return {
50+
config: undefined,
51+
isUsingOllama: false,
52+
}
53+
}
54+
4455
// Check internet FIRST - if offline, use Ollama regardless of auth
4556
console.log('[Offline] Checking internet connectivity...')
4657
const hasInternet = await checkInternetConnection()

src/main/lib/claude/transform.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,6 @@ export function createTransformer(options?: { emitSdkMessageUuid?: boolean; isUs
7575
}
7676

7777
return function* transform(msg: any): Generator<UIMessageChunk> {
78-
// Emit UUID early for rollback support (before abort can happen)
79-
// This ensures frontend has sdkMessageUuid even if streaming is interrupted
80-
if (emitSdkMessageUuid && msg.type === "assistant" && msg.uuid) {
81-
yield {
82-
type: "message-metadata",
83-
messageMetadata: { sdkMessageUuid: msg.uuid }
84-
}
85-
}
8678

8779
// Debug: log ALL message types to understand what SDK sends
8880
if (isUsingOllama) {

src/main/lib/mcp-auth.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { bringToFront } from './window';
2121
*/
2222
export async function fetchMcpTools(
2323
serverUrl: string,
24-
accessToken?: string
24+
headers?: Record<string, string>
2525
): Promise<string[]> {
2626
let client: Client | null = null;
2727
let transport: StreamableHTTPClientTransport | null = null;
@@ -33,10 +33,8 @@ export async function fetchMcpTools(
3333
});
3434

3535
const requestInit: RequestInit = {};
36-
if (accessToken) {
37-
requestInit.headers = {
38-
'Authorization': `Bearer ${accessToken}`,
39-
};
36+
if (headers && Object.keys(headers).length > 0) {
37+
requestInit.headers = { ...headers };
4038
}
4139

4240
transport = new StreamableHTTPClientTransport(new URL(serverUrl), {

src/main/lib/ollama/detector.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,21 @@ export async function checkOllamaStatus(): Promise<OllamaStatus> {
5050

5151
// If no exact match, try to find any qwen-coder, deepseek-coder, or codestral variant
5252
if (!recommendedModel) {
53-
recommendedModel = models.find(m =>
53+
recommendedModel = models.find((m: string) =>
5454
m.includes('qwen') && m.includes('coder') ||
5555
m.includes('deepseek') && m.includes('coder') ||
5656
m.includes('codestral')
5757
)
5858
}
5959

60-
console.log(`[Ollama] Available: ${models.length} models found`, models)
61-
6260
return {
6361
available: true,
6462
models,
6563
recommendedModel: recommendedModel || models[0], // Fallback to any model
6664
version: data.version,
6765
}
68-
} catch (error) {
69-
console.log('[Ollama] Not available:', error)
66+
} catch {
67+
// Ollama not available - no need to log, this is expected when offline mode is disabled
7068
return { available: false, models: [] }
7169
}
7270
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,19 @@ export const chatsRouter = router({
711711
}
712712

713713
// 5. Truncate messages to include up to and including the target message
714-
const truncatedMessages = messages.slice(0, targetIndex + 1)
714+
let truncatedMessages = messages.slice(0, targetIndex + 1)
715+
716+
// 5.5. Clear any old shouldResume flags, then set on the target message
717+
truncatedMessages = truncatedMessages.map((m: any, i: number) => {
718+
const { shouldResume, ...restMeta } = m.metadata || {}
719+
return {
720+
...m,
721+
metadata: {
722+
...restMeta,
723+
...(i === truncatedMessages.length - 1 && { shouldResume: true }),
724+
},
725+
}
726+
})
715727

716728
// 6. Update the sub-chat with truncated messages
717729
db.update(subChats)

0 commit comments

Comments
 (0)