Skip to content

Commit b4f9086

Browse files
committed
Release v0.0.39
## What's New ### Improvements & Fixes - **Notifications Fix** — Filter notifications by chatId to prevent spam - **Per-Workspace Sidebars** — Plan and terminal sidebars are now per-workspace - **Error Handling** — Improved Claude SDK error handling and categorization - **Thinking Tokens** — Reduced max thinking tokens to prevent exceeding limit - **Paste Handling** — Updated paste text handling improvements - **MCP Auth** — Authentication improvements for MCP
1 parent 60a8c93 commit b4f9086

20 files changed

Lines changed: 378 additions & 204 deletions

bun.lockb

46.2 KB
Binary file not shown.

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.38",
3+
"version": "0.0.39",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {

src/main/auth-manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AuthStore, AuthData, AuthUser } from "./auth-store"
22
import { app, BrowserWindow } from "electron"
3+
import { AUTH_SERVER_PORT } from "./constants"
34

45
// Get API URL - in packaged app always use production, in dev allow override
56
function getApiBaseUrl(): string {
@@ -210,10 +211,10 @@ export class AuthManager {
210211

211212
let authUrl = `${this.getApiUrl()}/auth/desktop?auto=true`
212213

213-
// In dev mode, use localhost callback (we run HTTP server on port 21321)
214+
// In dev mode, use localhost callback (we run HTTP server on AUTH_SERVER_PORT)
214215
// Also pass the protocol so web knows which deep link to use as fallback
215216
if (this.isDev) {
216-
authUrl += `&callback=${encodeURIComponent("http://localhost:21321/auth/callback")}`
217+
authUrl += `&callback=${encodeURIComponent(`http://localhost:${AUTH_SERVER_PORT}/auth/callback`)}`
217218
// Pass dev protocol so production web can use correct deep link if callback fails
218219
authUrl += `&protocol=twentyfirst-agents-dev`
219220
}

src/main/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Dev mode detection
2+
export const IS_DEV = !!process.env.ELECTRON_RENDERER_URL
3+
4+
// Auth server port - use different port in dev to allow running alongside production
5+
export const AUTH_SERVER_PORT = IS_DEV ? 21322 : 21321

src/main/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ import { cleanupGitWatchers } from "./lib/git/watcher"
3030
import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth"
3131
import { createMainWindow, getWindow } from "./windows/main"
3232

33-
// Dev mode detection
34-
const IS_DEV = !!process.env.ELECTRON_RENDERER_URL
33+
import { IS_DEV, AUTH_SERVER_PORT } from "./constants"
3534

3635
// Deep link protocol (must match package.json build.protocols.schemes)
3736
// Use different protocol in dev to avoid conflicts with production app
@@ -249,9 +248,9 @@ const FAVICON_SVG = `<svg width="32" height="32" viewBox="0 0 1024 1024" fill="n
249248
const FAVICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}`
250249

251250
// Start local HTTP server for auth callbacks
252-
// This catches http://localhost:21321/auth/callback?code=xxx and /mcp-oauth/callback
251+
// This catches http://localhost:{AUTH_SERVER_PORT}/auth/callback?code=xxx and /mcp-oauth/callback
253252
const server = createServer((req, res) => {
254-
const url = new URL(req.url || "", "http://localhost:21321")
253+
const url = new URL(req.url || "", `http://localhost:${AUTH_SERVER_PORT}`)
255254

256255
// Serve favicon
257256
if (url.pathname === "/favicon.ico" || url.pathname === "/favicon.svg") {
@@ -430,8 +429,8 @@ const server = createServer((req, res) => {
430429
}
431430
})
432431

433-
server.listen(21321, () => {
434-
console.log("[Auth Server] Listening on http://localhost:21321")
432+
server.listen(AUTH_SERVER_PORT, () => {
433+
console.log(`[Auth Server] Listening on http://localhost:${AUTH_SERVER_PORT}`)
435434
})
436435

437436
// Clean up stale lock files from crashed instances

src/main/lib/mcp-auth.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,14 @@ export async function fetchMcpToolsStdio(config: {
133133
}
134134
}
135135

136-
const IS_DEV = !!process.env.ELECTRON_RENDERER_URL;
136+
import { AUTH_SERVER_PORT, IS_DEV } from '../constants';
137+
137138
const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
138139

139140
function getMcpOAuthRedirectUri(): string {
140141
return IS_DEV
141-
? 'http://localhost:21321/mcp-oauth/callback'
142-
: 'http://127.0.0.1:21321/mcp-oauth/callback';
142+
? `http://localhost:${AUTH_SERVER_PORT}/mcp-oauth/callback`
143+
: `http://127.0.0.1:${AUTH_SERVER_PORT}/mcp-oauth/callback`;
143144
}
144145

145146
interface PendingOAuth {

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,8 +1249,10 @@ ${prompt}
12491249
// Check for error messages from SDK (error can be embedded in message payload!)
12501250
const msgAny = msg as any
12511251
if (msgAny.type === "error" || msgAny.error) {
1252-
const sdkError =
1253-
msgAny.error || msgAny.message || "Unknown SDK error"
1252+
// Extract detailed error text from message content if available
1253+
// This is where the actual error description lives (e.g., "API Error: Claude Code is unable to respond...")
1254+
const messageText = msgAny.message?.content?.[0]?.text
1255+
const sdkError = messageText || msgAny.error || msgAny.message || "Unknown SDK error"
12541256
lastError = new Error(sdkError)
12551257

12561258
// Detailed SDK error logging in main process
@@ -1271,11 +1273,14 @@ ${prompt}
12711273
console.error(`[CLAUDE SDK ERROR] ========================================`)
12721274

12731275
// Categorize SDK-level errors
1276+
// Use the raw error code (e.g., "invalid_request") for category matching
1277+
const rawErrorCode = msgAny.error || ""
12741278
let errorCategory = "SDK_ERROR"
1275-
let errorContext = "Claude SDK error"
1279+
// Default errorContext to the full error text (which may include detailed message)
1280+
let errorContext = sdkError
12761281

12771282
if (
1278-
sdkError === "authentication_failed" ||
1283+
rawErrorCode === "authentication_failed" ||
12791284
sdkError.includes("authentication")
12801285
) {
12811286
errorCategory = "AUTH_FAILED_SDK"
@@ -1288,23 +1293,31 @@ ${prompt}
12881293
errorCategory = "MCP_INVALID_TOKEN"
12891294
errorContext = "Invalid access token. Update MCP settings"
12901295
} else if (
1291-
sdkError === "invalid_api_key" ||
1296+
rawErrorCode === "invalid_api_key" ||
12921297
sdkError.includes("api_key")
12931298
) {
12941299
errorCategory = "INVALID_API_KEY_SDK"
12951300
errorContext = "Invalid API key in Claude Code CLI"
12961301
} else if (
1297-
sdkError === "rate_limit_exceeded" ||
1302+
rawErrorCode === "rate_limit_exceeded" ||
12981303
sdkError.includes("rate")
12991304
) {
13001305
errorCategory = "RATE_LIMIT_SDK"
13011306
errorContext = "Session limit reached"
13021307
} else if (
1303-
sdkError === "overloaded" ||
1308+
rawErrorCode === "overloaded" ||
13041309
sdkError.includes("overload")
13051310
) {
13061311
errorCategory = "OVERLOADED_SDK"
13071312
errorContext = "Claude is overloaded, try again later"
1313+
} else if (
1314+
rawErrorCode === "invalid_request" ||
1315+
sdkError.includes("Usage Policy") ||
1316+
sdkError.includes("violate")
1317+
) {
1318+
// Usage Policy violation - keep the full detailed error text
1319+
errorCategory = "USAGE_POLICY_VIOLATION"
1320+
// errorContext already contains the full message from sdkError
13081321
}
13091322

13101323
// Emit auth-error for authentication failures, regular error otherwise
@@ -1319,7 +1332,7 @@ ${prompt}
13191332
errorText: errorContext,
13201333
debugInfo: {
13211334
category: errorCategory,
1322-
sdkError: sdkError,
1335+
rawErrorCode,
13231336
sessionId: msgAny.session_id,
13241337
messageId: msgAny.message?.id,
13251338
},
@@ -1329,8 +1342,8 @@ ${prompt}
13291342
console.log(`[SD] M:END sub=${subId} reason=sdk_error cat=${errorCategory} n=${chunkCount}`)
13301343
console.error(`[SD] SDK Error details:`, {
13311344
errorCategory,
1332-
errorContext,
1333-
sdkError,
1345+
errorContext: errorContext.slice(0, 200), // Truncate for log readability
1346+
rawErrorCode,
13341347
sessionId: msgAny.session_id,
13351348
messageId: msgAny.message?.id,
13361349
fullMessage: JSON.stringify(msgAny, null, 2),

src/renderer/components/dialogs/agents-settings-dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ export function AgentsSettingsDialog({
536536
key={tab.id}
537537
tab={tab}
538538
isActive={activeTab === tab.id}
539-
onClick={() => handleTabClick(tab.id)}
539+
onClick={() => setActiveTab(tab.id)}
540540
/>
541541
))}
542542
</div>
@@ -551,7 +551,7 @@ export function AgentsSettingsDialog({
551551
key={tab.id}
552552
tab={tab}
553553
isActive={activeTab === tab.id}
554-
onClick={() => handleTabClick(tab.id)}
554+
onClick={() => setActiveTab(tab.id)}
555555
/>
556556
))}
557557
</div>
@@ -568,7 +568,7 @@ export function AgentsSettingsDialog({
568568
key={tab.id}
569569
tab={tab}
570570
isActive={activeTab === tab.id}
571-
onClick={() => handleTabClick(tab.id)}
571+
onClick={() => setActiveTab(tab.id)}
572572
/>
573573
))}
574574
</div>

src/renderer/components/mermaid-block.tsx

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ const MermaidBlockInner = memo(function MermaidBlockInner({
206206
const [isFullscreen, setIsFullscreen] = useState(false)
207207
const renderIdRef = useRef(0)
208208
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)
209-
const retryCountRef = useRef(0)
210209
// Track the last successfully rendered code to avoid re-rendering same content
211210
const lastRenderedCodeRef = useRef<string>("")
212211
const lastRenderedThemeRef = useRef<boolean | null>(null)
@@ -243,7 +242,6 @@ const MermaidBlockInner = memo(function MermaidBlockInner({
243242
setRenderState({ status: "success", svg })
244243
lastRenderedCodeRef.current = code
245244
lastRenderedThemeRef.current = isDark
246-
retryCountRef.current = 0
247245

248246
// Clean up any error artifacts mermaid left in DOM
249247
cleanupMermaidErrors()
@@ -256,26 +254,13 @@ const MermaidBlockInner = memo(function MermaidBlockInner({
256254
// Clean up error SVGs that mermaid adds to DOM
257255
cleanupMermaidErrors()
258256

259-
// Check if this is a parse/syntax error (incomplete diagram) or a DOM error (race condition)
257+
// Check if this is a parse/syntax error (incomplete diagram)
260258
const isParseError = message.toLowerCase().includes("parse error") ||
261259
message.toLowerCase().includes("syntax error") ||
262260
message.toLowerCase().includes("expecting") ||
263261
message.toLowerCase().includes("unexpected") ||
264262
message.toLowerCase().includes("no diagram type detected") ||
265-
message.toLowerCase().includes("lexical error") ||
266-
message.toLowerCase().includes("cannot read properties of null") ||
267-
message.toLowerCase().includes("firstchild")
268-
269-
// Check if it's a DOM race condition error - retry automatically
270-
const isDomError = message.toLowerCase().includes("cannot read properties of null") ||
271-
message.toLowerCase().includes("firstchild")
272-
273-
// DOM race condition errors are transient - just show parsing state
274-
// The next render attempt will likely succeed
275-
if (isDomError) {
276-
setRenderState({ status: "parsing" })
277-
return
278-
}
263+
message.toLowerCase().includes("lexical error")
279264

280265
if (isParseError && !lastRenderedCodeRef.current) {
281266
// Show "Creating diagram..." only if we haven't successfully rendered before
@@ -367,17 +352,6 @@ const MermaidBlockInner = memo(function MermaidBlockInner({
367352
}
368353
}, [])
369354

370-
// Auto-retry when in parsing state (may be due to DOM race condition)
371-
useEffect(() => {
372-
if (renderState.status === "parsing" && retryCountRef.current < 3) {
373-
const retryTimeout = setTimeout(() => {
374-
retryCountRef.current += 1
375-
renderDiagram()
376-
}, 300)
377-
return () => clearTimeout(retryTimeout)
378-
}
379-
}, [renderState.status, renderDiagram])
380-
381355
const handleCopy = useCallback(async () => {
382356
await navigator.clipboard.writeText(code)
383357
setCopied(true)
@@ -461,7 +435,10 @@ const MermaidBlockInner = memo(function MermaidBlockInner({
461435
)}
462436

463437
{(renderState.status === "loading" || renderState.status === "parsing") && (
464-
<span className="text-muted-foreground text-sm">Creating diagram...</span>
438+
<div className="flex items-center gap-2 text-muted-foreground text-sm">
439+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
440+
<span>Creating diagram...</span>
441+
</div>
465442
)}
466443

467444
{renderState.status === "success" && (

src/renderer/features/agents/atoms/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -600,8 +600,8 @@ export const pendingUserQuestionsAtom = atom<Map<string, PendingUserQuestion>>(n
600600
export type PendingUserQuestions = PendingUserQuestion
601601

602602
// Track sub-chats with pending plan approval (plan ready but not yet implemented)
603-
// Set<subChatId>
604-
export const pendingPlanApprovalsAtom = atom<Set<string>>(new Set())
603+
// Map<subChatId, parentChatId> - allows filtering by workspace
604+
export const pendingPlanApprovalsAtom = atom<Map<string, string>>(new Map())
605605

606606
// Pending "Build plan" trigger - set by ChatView sidebar, consumed by ChatViewInner
607607
// Contains subChatId to approve, null when no pending approval

0 commit comments

Comments
 (0)