Slash command:
- Fix first-click bug: set textarea.value and cursor position before
calling handleInputChange so selectionStart reads correctly
- Move slash button to right of + (attach) button
- Simplify icon from CommandLineIcon to plain "/" text
Composer action bar:
- Move Agent toggle and permission selector to left side (was centered)
- Adjust spacing: reduce input padding, increase bottom margin
Permission selector:
- Add dropdown arrow indicator (ArrowDown01Icon)
- Change full_access color from orange to red (icon + text + background)
- Check res.ok before updating UI state (prevent false success)
- Make sessionId optional to support /chat new session page
- Log warnings on PATCH failure instead of silent catch
New chat page (/chat):
- Add ChatPermissionSelector with local state before session exists
- Pass permission_profile in session creation payload
- Backend createSession() now accepts and writes permission_profile
Context usage indicator:
- Reduce circle size from 22px to 16px
- Change used-portion color to dark gray (zinc-600/400)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Composer restructure:
- MessageInput internal footer now only contains: file button, model
selector, slash command button, and send button
- Removed mode toggle (Code/Plan) from composer area (logic preserved)
- New ChatComposerActionBar below input with three slots: left (Design
Agent), center (permission selector), right (context indicator)
New components:
- SlashCommandButton: inserts `/` at cursor, reuses existing slash
command detection and popover logic
- ChatPermissionSelector: session-level permission toggle (default /
full_access) with danger confirmation dialog
- ContextUsageIndicator: SVG donut chart showing context usage ratio
with HoverCard displaying model, tokens, cache stats
- ChatComposerActionBar: slot-based container for action bar items
Session permission (permission_profile):
- DB: new column on chat_sessions, default 'default'
- API: PATCH sessions supports updating permission_profile
- Chat API: bypassPermissions = global setting OR session full_access
- PermissionPrompt: auto-approves and hides when full_access
- Claude client: combines global and per-session bypass flags
Bridge integration:
- permission-broker: auto-approves permissions for full_access sessions
without sending IM notifications
- conversation-engine: passes bypassPermissions to SDK for full_access
- PATCH route: auto-approves pending bridge permissions on switch to
full_access
Other:
- ImageGenToggle relabeled to "Design Agent" / "设计 Agent"
- New model-context.ts with MODEL_CONTEXT_WINDOWS lookup
- Provider models API returns optional contextWindow field
- New useContextUsage hook for token usage calculation
- i18n: added composer.*, permission.*, context.* keys (en + zh)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Assistant Workspace V2:
- Markdown indexing engine (incremental manifest + chunks JSONL)
- Keyword retrieval with CJK bigram support and hotset boosting
- Directory taxonomy inference and evolution suggestions
- Workspace config management (.assistant/config.json)
- Organize API: capture, classify, move, archive, suggest-evolution
Workspace switching:
- Inspect API (GET /api/workspace/inspect) for pre-switch validation
- Two-phase switching with confirmation dialogs (5 workspace statuses)
- Atomic save: init/reset runs before setSetting(), rollback on failure
- Auto-navigate to new/reused session after switch
- Mismatch banner in ChatView (only for former assistant sessions)
- Debounced path validation with visual indicators
Security hardening:
- Completion fence (onboarding-complete/checkin-complete) only processes
when session.workingDirectory === assistant_workspace_path (frontend)
AND session.working_directory === workspacePath (backend double-check)
- assertContained() in workspace-organizer: rejects absolute paths, ~/,
../ traversal, and symlink escapes for captureNote/moveFile
- organize API validates all path params at entry (validateRelativePath)
- captureDefault sanitized to relative path on onboarding write
- R_OK + W_OK validation on GET settings, session creation, and PUT
Bug fixes:
- hookTriggeredSessionId now read before auto-trigger; allows retry if
session has no messages (previous trigger failed silently)
- workspace-switched event listener matches oldPath (not just != newPath)
so normal project chats never show the mismatch banner
- Daily check-in card hidden when onboarding not complete
- Refresh docs now calls generateRootDocs() + generateDirectoryDocs()
- Banner "Open" button reuses latest session (checkin mode) instead of
always creating new onboarding session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add "Assistant Workspace" feature that lets users designate a directory
(e.g. Obsidian vault) containing 4 Markdown files (soul.md, user.md,
memory.md, claude.md) to define AI personality, user profile, long-term
memory, and execution rules. These files are automatically injected into
the system prompt for assistant project sessions.
Core library (src/lib/assistant-workspace.ts):
- Workspace validation, initialization with template files
- File loading with 8000-char truncation, XML prompt assembly (40k limit)
- State management via .assistant/state.json
- Directory docs generation (README.ai.md + PATH.ai.md)
- Case-insensitive file name fallback (soul.md → SOUL.md)
System prompt injection (src/app/api/chat/route.ts):
- Scoped to sessions where working_directory === workspace path
- Workspace prompt prepended before session-level system prompt
- Onboarding/check-in instructions injected as <assistant-project-task>
- autoTrigger flag skips user message save and title update
Chat-based onboarding (10 questions) & daily check-in (3 questions):
- AI conducts conversation, outputs structured completion code blocks
- Frontend detects onboarding-complete/checkin-complete blocks
- Backend calls Claude API to generate file content from answers
- Fallback to raw answers if API call fails
Auto-trigger pipeline:
- Mount trigger (500ms delay) + focus fallback in MessageInput
- assistantTriggerFiredRef prevents duplicate triggers per mount
- hookTriggeredSessionId in state.json prevents cross-reload duplicates
- Daily check-in only triggers in the most recent workspace session
- hookTriggeredSessionId cleared via __clear__ sentinel after completion
API endpoints:
- GET/PUT /api/settings/workspace — path CRUD + file status
- POST /api/workspace/session — create (onboarding) or reuse (check-in)
- POST /api/workspace/onboarding — AI-generate soul.md + user.md
- POST /api/workspace/checkin — AI-update memory.md + user.md
- POST /api/workspace/hook-triggered — set/clear trigger guard
- GET /api/workspace/latest-session — latest session lookup
- POST /api/workspace/docs — refresh directory documentation
UI:
- Settings "Assistant" tab with workspace path, file status, controls
- Violet badge on workspace project group in ChatListPanel
- useAssistantWorkspace hook for state queries
Also includes:
- 102 passing tests (16 new regression tests for workspace logic)
- i18n keys for en.ts and zh.ts
- Handover documentation and execution plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add "View Plan" button with fullscreen modal to ExitPlanMode UI
When Claude writes a plan in plan mode, the plan content is written to a
file on disk but the ExitPlanMode UI only shows Approve/Reject buttons —
the user never sees the actual plan without manually finding the file.
Add a "View Plan" button that opens a fullscreen Dialog modal showing
the plan content rendered as markdown. The plan content is extracted from
the tool uses array (the Write tool's input for the plan file), so no
new API endpoints are needed.
Changes in StreamingMessage.tsx:
- extractPlanContent(): walks toolUses backwards to find the last Write
to a plans/*.md file and returns its content
- ExitPlanModeUI: accepts toolUses prop, renders "View Plan" button
(hidden if no plan content found), opens Radix Dialog modal with
MessageResponse markdown renderer
- Modal is 80vh tall, max-w-4xl, scrollable, with Close button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add "Do this instead" feedback textbox to plan approval UI
When rejecting a plan, users can now type feedback and click "Do this
instead" (or press Enter) to send a custom deny message to the SDK.
This matches the Claude Code CLI behavior where users can provide
alternative instructions when rejecting a plan.
Previously, the deny message was hardcoded to "User denied permission"
— the API already supported custom messages but the client never sent
them.
Changes:
- StreamingMessage.tsx: add feedback textbox + "Do this instead" button
to ExitPlanModeUI, wire onDenyWithMessage callback
- ChatView.tsx: pass denyMessage through handlePermissionResponse
- MessageList.tsx: update onPermissionResponse type signature
- stream-session-manager.ts: add denyMessage param to
respondToPermission(), use it in the deny decision body
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: fetch plan content from disk so View Plan always shows latest version
Previously extractPlanContent read the Write tool's input.content, which
showed stale content when Claude later edited the plan via the Edit tool.
Now extractPlanFilePath finds the plan file path (matching both Write and
Edit tools, with Windows backslash support), and the View Plan button
fetches the file from disk on each click via /api/files/preview.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The stream idle timer (330s) fires during long-running Claude Code tool
executions because the SDK's keep_alive heartbeat messages are silently
dropped. This causes a false "Stream idle timeout" error even though
Claude is still actively working.
Root cause: claude-client.ts only handles 6 SDK message types in its
switch statement and has no default case, so keep_alive messages from
the SDK are never forwarded to the frontend. The idle timer in
stream-session-manager.ts never gets reset by markActive().
Fix: thread keep_alive through the existing 3-layer event pipeline:
- claude-client.ts: forward keep_alive as SSE event (via default case)
- useSSEStream.ts: add onKeepAlive callback to SSECallbacks interface
- stream-session-manager.ts: call markActive() on keep_alive to reset
the idle timer
No visible UI change — keep_alive only resets the timer silently.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve paragraph breaks between text blocks in bridge responses
When Claude's response includes tool calls (Read, Edit, Bash, etc.),
the response text is split into multiple content blocks — one before
each tool call and one after. These blocks were joined with an empty
string (.join('')), causing all paragraphs to concatenate without
any spacing in the final Telegram message.
Changed .join('') to .join('\n\n') so paragraph breaks between text
blocks are preserved, matching how they appear in the desktop UI.
* fix: sanitize __NEXT_PRIVATE_* env vars from child processes
The bundled Next.js standalone server sets __NEXT_PRIVATE_STANDALONE_CONFIG
and __NEXT_PRIVATE_ORIGIN in process.env at runtime. These leak into every
child process spawned by the Electron main process (Claude Code CLI,
node version checks, package installs, etc.), which in turn pollutes
tmux/terminal sessions.
When other Next.js projects detect __NEXT_PRIVATE_STANDALONE_CONFIG in their
environment, they skip normal next.config loading and use CodePilot's baked-in
config instead — silently breaking builds and dev servers system-wide.
Fix: strip all __NEXT_PRIVATE_* variables when constructing env objects for
child processes. The standalone server.js already sets these internally, so
they don't need to be passed from the parent.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
Skills.sh Marketplace:
- Add marketplace browser with search, detail panel, and install/uninstall
- Search API proxying skills.sh with lock file integration for installed state
- Install/remove APIs using `npx skills` CLI with SSE streaming progress
- SKILL.md content fetching via GitHub Git Trees API with in-memory caching
- MarketplaceSkillCard, MarketplaceSkillDetail, MarketplaceBrowser components
- InstallProgressDialog with real-time SSE log output
- SkillsManager gains "My Skills" / "Marketplace" segmented control
- Type definitions: MarketplaceSkill, SkillLockFile, SkillLockEntry
Navigation restructure:
- Split Extensions page into standalone /skills and /mcp top-level pages
- NavRail: replace single Extensions icon with separate Skills (ZapIcon) and MCP (Plug01Icon) entries
- /extensions page now redirects to /skills or /mcp based on ?tab param
- /plugins redirects updated to point to new routes
- Remove redundant source badges from SkillListItem (section headers suffice)
Other:
- Add WeChat user group QR code to README.md and README_CN.md
- i18n: add 19 marketplace-related keys in en.ts and zh.ts
- Bump version to 0.18.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P1: tool_result dedup changed from "first wins" to "last wins"
- claude-client.ts: remove emittedToolResultIds Set, both PostToolUse and
user message handler emit freely. PostToolUse provides immediate UI feedback;
user handler's potentially more complete result arrives second.
- route.ts: collectStreamResponse replaces existing contentBlock entry when
duplicate tool_use_id arrives (last wins for DB persistence).
- ChatView.tsx: onToolResult replaces existing state entry by tool_use_id
instead of skipping, ensuring most complete result is displayed.
P2: SDK task ID collision risk eliminated
- db.ts: syncSdkTasks now uses full sessionId in task ID
(`sdk-{fullSessionId}-{index}`) instead of truncated 8-char prefix,
eliminating cross-session primary key collisions.
P2: Todo ordering stabilized
- db.ts: add sort_order column migration, syncSdkTasks writes array index
as sort_order. getTasksBySession sorts by sort_order ASC, created_at ASC
so tasks with identical timestamps maintain insertion order.
P3: Documentation field mapping corrected
- agent-tooling-todo-bridge.md: fix field mapping table — actual implementation
maps activeForm (not priority) to description column. Updated dedup strategy
docs from "three-layer skip" to "two-layer last-wins replace".
Lint: fix prefer-const for finalPrompt in claude-client.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Session runtime mutex:
- Add session_runtime_locks table with TTL-based locking via SQLite
single-writer guarantee (transaction: DELETE expired + INSERT, PK
conflict = already locked → 409 SESSION_BUSY)
- POST /api/chat acquires lock before processing, releases in
collectStreamResponse finally block and error catch
- Add runtime_status/runtime_updated_at/runtime_error columns to
chat_sessions for tracking session state
Permission request persistence:
- Add permission_requests table with full lifecycle tracking
(pending → allow/deny/timeout/aborted)
- Dual-write in permission-registry: in-memory Map for real-time
resolution + DB for audit and recovery
- claude-client canUseTool now persists to DB before SSE event
- permission/route.ts validates against DB before resolving:
404 (not found), 409 (already resolved), 410 (waiter gone)
Immutable sdk_cwd:
- Add sdk_cwd column, snapshot from working_directory at session
creation time, never changes afterward
- route.ts uses sdk_cwd for SDK cwd to prevent resume breakage
when user modifies working_directory
- Backfill existing sessions via migration
Startup recovery (migrateDb):
- Reset running/waiting_permission sessions to idle with error msg
- Clear all runtime locks (stale from previous process)
- Abort pending permission requests (no in-memory waiter exists)
All DB writes are try/catch guarded — failure degrades gracefully
without breaking the in-memory hot path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: assistant messages use temporary IDs (temp-assistant-{timestamp})
in the frontend, which don't match real DB message IDs. The PUT update by
message_id always found 0 rows, so results were never persisted.
Fix — fallback search by session + prompt:
- db.ts: updateMessageContent now returns changes count; new
updateMessageBySessionAndHint() finds the latest assistant message
containing image-gen-request with matching prompt text and updates it
- /api/chat/messages PUT: tries message_id first, falls back to
session_id + prompt_hint search; returns 404 if neither finds a match;
response includes updated_message_id and fallback_used for debugging
- ImageGenConfirmation: always sends session_id and prompt_hint in PUT
request, regardless of whether messageId is a real or temp ID
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: after image generation, the message content in DB stayed as
`image-gen-request` (the options form). On app restart, MessageItem re-parsed
this as a generation form instead of showing the completed result. The result
only lived in React state and localStorage.
Fix:
- db.ts: add updateMessageContent() to update a message's content by ID
- /api/chat/messages: add PUT handler for message content updates
- ImageGenConfirmation: accept messageId prop; on successful generation,
replace message content from image-gen-request to image-gen-result format
(with localPath references) via PUT API call
- MessageItem: pass message.id to ImageGenConfirmation
Now on restart, parseImageGenResult() matches the persisted image-gen-result
block and renders ImageGenCard directly with the saved image paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gallery UI overhaul:
- GalleryGrid: replace equal-size grid with CSS columns masonry layout (6 columns),
pure image cards without text/date info, hover gray ring highlight,
favorited items show red heart badge
- GalleryDetail: left-right layout dialog (70% image / 30% info),
nearly full-screen size with minimal margins, black background for image area,
absolute-positioned image container for correct sizing across all aspect ratios,
favorite toggle button replaces tag manager, removed outer border
- Gallery page: added "favorites only" filter button, replaced load-more button
with IntersectionObserver infinite scroll, removed tag system references
Favorites system:
- db.ts: added `favorited` column to media_generations with migration
- New API: PUT /api/media/[id]/favorite for toggle
- gallery/route.ts: support favoritesOnly=1 filter, return favorited field
- i18n: added favorites-related translations (en/zh)
Other:
- Default image model changed from Nano Banana Pro to Nano Banana 2
- providers/models/route.ts: skip media provider types in chat model selector
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Image Generation Refactor:
- MessageInput: add onImageGenerate callback, remove skill fetch dependency
- ChatView: implement handleImageGenerate that creates image-gen-request
message for ImageGenConfirmation UI, then user selects params and generates
- New API route POST /api/chat/messages for DB-only message persistence
- image-gen-completed event now only persists notice to DB, no longer triggers model
- Reference images passed via module-level Map (imageGenRefStore) to avoid
DB bloat from base64 data; user message DB content stores metadata only
Bug Fixes:
- Fix useCallback dependency missing imageGen causing stale closure reads
- Fix React Hooks order violation in MessageItem (early return before hooks)
- Fix localStorage cache collision across sessions (key now includes sessionId)
- Fix empty src attribute in FileAttachmentDisplay for DB-restored messages
- Add JSON parse fallback for malformed image-gen-request blocks
Gallery Reference Images:
- /api/media/generate saves reference images to disk, stores paths in metadata
- /api/media/gallery extracts referenceImages from metadata in mapRow
- GalleryDetail displays reference image thumbnails
- ImageGenCard supports referenceImages prop for chat display
UI Changes:
- ImageGenToggle: replace paint brush icon with text label (en: Image, zh: 生图)
- Model selector button: remove provider name display to save space
- ImageGenConfirmation: support referenceImagesData prop for base64 preview
i18n:
- Add imageGen.toggleLabel key (en: Image, zh: 生图)
Foundation for the image generation feature:
- Install @google/genai SDK (Google Gemini API client for Nano Banana Pro)
- Add media_generations table to store image generation metadata
(id, type, status, provider, model, prompt, aspect_ratio, image_size,
local_path, tags, session_id, message_id, etc.)
- Add media_tags table for tag-based filtering in gallery
- Add DB migration for existing databases
- Add indexes for created_at, session_id, status on media_generations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactors the provider/model selection system so users can switch providers
directly from the chat input model selector, instead of having to go to
Settings to activate/deactivate providers.
types/index.ts:
- Add ProviderModelGroup interface for grouped provider+model data
- Extend SendMessageRequest, ClaudeStreamOptions, ChatSession with provider_id
db.ts:
- Migration: add provider_id column to chat_sessions
- Migration: convert is_active=1 provider to default_provider_id setting
- New helpers: updateSessionProviderId, getDefaultProviderId, setDefaultProviderId
providers/models/route.ts:
- Rewrite to return { groups, default_provider_id } instead of flat model list
- Each configured provider becomes a group; env vars become "Environment" group
- Deduplicate models within a group (e.g. GLM sonnet/opus both → "GLM-4.7" once)
chat/route.ts:
- Accept provider_id from request body
- Resolution chain: request → session → default_provider_id → env vars
- provider_id='env' is sentinel for environment variable fallback
- Pass resolved provider object to streamClaude()
claude-client.ts:
- Use options.provider ?? getActiveProvider() so explicit provider takes priority
ChatView.tsx:
- Track currentProviderId state, pass provider_id in fetch body
- New handleProviderModelChange callback
MessageInput.tsx:
- New props: providerId, onProviderModelChange
- Replace dynamicModels/activeProviderName with providerGroups/defaultProviderId
- Model selector UI: grouped by provider with section headers, ✓ for active item
- Persist both model and provider ID to localStorage
chat/page.tsx, chat/[id]/page.tsx:
- Track and pass provider_id through the component chain
ProviderManager.tsx:
- Remove handleActivate/handleDeactivate, activatingId, Apply/Disable buttons
- Remove Active badge and green border styling
- Simplify env variable banner
- Add provider-changed event dispatch to save/delete handlers
providers/route.ts:
- GET response now includes default_provider_id field
i18n (en.ts, zh.ts):
- Add provider.default, provider.setDefault, provider.environment translations
Backward compatible: old sessions without provider_id default to '' which
triggers the fallback chain. is_active column preserved but no longer written.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New "Usage" tab in Settings showing token consumption analytics:
- **Data cards**: Total Tokens, Total Cost, Sessions, Cache Hit Rate
- **Bar chart**: Daily token usage with model/provider color grouping
using recharts v3, with custom tooltip and continuous date x-axis
- **Provider tracking**: Each session now records the active provider
name (e.g. "Kimi Coding Plan") so the chart distinguishes usage
across different providers, not just model names
Backend changes (db.ts):
- Added `provider_name` column to chat_sessions with auto-migration
- Added `getTokenUsageStats(days)` with summary + daily aggregation
- SQL uses `json_valid()` guard to prevent malformed JSON crashes
- Fixed off-by-one: 7D now shows exactly 7 calendar days, not 8
- Provider-qualified grouping: shows provider name when set,
plain model name when using direct Anthropic
API changes (route.ts):
- Persists effectiveModel and activeProvider name on every message
- Captures model from SDK init event for accurate tracking
- New GET /api/usage/stats endpoint
Frontend fixes:
- SettingsLayout: replaced useEffect+setState with useSyncExternalStore
to fix react-hooks/set-state-in-effect lint error
- UsageStatsSection: AbortController prevents request race on rapid
range switching; cache hit rate formula corrected to
cache_read/(cache_read+input); date gaps filled with zeros for
continuous x-axis
- Full i18n support (en + zh)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Resume pre-check and degradation (claude-client.ts):
- Check working_directory exists on disk before attempting resume;
if missing, skip resume, clear sdk_session_id, notify frontend
- On resume peek failure, clear sdk_session_id to prevent repeated
retries on the same broken session
- Change iterator wrap condition from sdkSessionId to shouldResume
so skipped resumes don't enter the peek path
2. Per-request independent permission timeout (permission-registry.ts):
- Replace lazy cleanup (cleanupExpired on next register) with a
dedicated setTimeout per permission request
- Auto-deny after 5 minutes with clearTimeout on resolve/abort
- Extract denyAndRemove helper for abort and timeout paths
3. Permission wait visibility (StreamingMessage.tsx):
- Always show StreamingStatusBar during streaming (remove
!pendingPermission guard)
- Display "Waiting for authorization: <tool>" with elapsed timer
when a permission request is pending, instead of hiding the bar
4. Stream idle timeout (ChatView.tsx):
- Add 330s idle timeout (slightly above 5-min permission timeout
to avoid races) using setInterval that checks lastEventTime
- Every SSE callback calls markActive() to reset the timer
- On idle timeout, abort the stream and show a readable error
- Clean up interval in both catch and finally blocks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "exited with code 1" error was hiding the real cause. Now:
- Logs full error object to console (message, stack, cause, stderr)
- Extracts stderr/cause from SDK error and includes in user-facing message
- Adds "Details:" section to the error when extra info is available
This will help diagnose why old sessions fail on some machines while
new sessions work fine with the same provider config.
- src/lib/claude-client.ts: enhanced catch block with console.error
logging and stderr/cause extraction for error messages
When a resumed SDK session crashes mid-stream (after outputting some
content), the sdk_session_id remains in the database. Every subsequent
message in that chat session attempts to resume the same broken SDK
session, causing repeated failures.
Root cause: the SDK process starts successfully with resume, outputs
some content, then crashes (exit code 1). The sdk_session_id was
already stored from a previous successful turn and never gets cleared
on error.
Fix: in the catch block of streamClaude(), when we were resuming a
session (sdkSessionId is set), clear the sdk_session_id in the
database. This ensures the next message starts a fresh SDK session
instead of repeatedly hitting the same broken resume.
Combined with the earlier peek-based fallback (which handles failures
at startup), this covers both failure modes:
1. Resume fails immediately → fallback retries without resume in the
same request
2. Resume fails mid-stream → clears sdk_session_id so the NEXT
request starts fresh
- src/lib/claude-client.ts: import updateSdkSessionId, add
sdk_session_id reset in catch block when sdkSessionId was set
When continuing an old chat session, CodePilot passes the stored
sdk_session_id to the SDK with resume=true. If the local SDK session
file is stale, corrupt, or incompatible (e.g. after a CLI version
update), the SDK process exits with code 1 and the user sees a
misleading "Invalid or missing API Key" error.
This fix wraps the initial query() call with a try-catch that detects
resume failures by peeking at the first async message. On failure, it
automatically retries without the resume option, starting a fresh SDK
session while preserving the chat history in CodePilot.
- src/lib/claude-client.ts: added resume-failure detection and
automatic fallback logic around the query() call
Streaming tool UI fix (ChatView.tsx):
- Add toolUsesRef/toolResultsRef to reliably track tool data across closures
- Serialize tool_use/tool_result into JSON content-blocks array when building
assistant message after stream ends, matching backend storage format
- MessageItem.parseToolBlocks() now renders tool UI from history messages
- Clean up refs in finally block and timeout auto-retry path
File tree UX (FileTree.tsx, file-tree.tsx):
- Remove folder path header from right panel, merge search + refresh into one row
- Remove file icon spacer (size-5) so file icons align flush with search box
- Change folder icon color from blue to muted-foreground
Chat list panel (ChatListPanel.tsx):
- Folder header: expanded uses FolderOpenIcon, collapsed uses Folder01Icon
- Add tooltip showing full working directory path on folder hover
- Double-click folder header opens in system file manager
Title bar folder name (chat/[id]/page.tsx):
- Add tooltip with full path on project name hover
- Click opens folder in Finder/Explorer via Electron IPC or API fallback
Open folder support:
- electron/main.ts: add shell:open-path IPC handler via shell.openPath
- electron/preload.ts: expose electronAPI.shell.openPath to renderer
- api/files/open/route.ts: fallback API route using open/explorer/xdg-open
Unify icon library — migrate app-level components from Lucide to Hugeicons:
- file-tree.tsx: folder, file, chevron, plus icons
- tool-actions-group.tsx: chevron expand icon
- MessageItem.tsx: copy, check, expand/collapse icons
- MessageInput.tsx: stop button (SquareIcon → StopIcon)
- FileCard.tsx: file icon
- ImageLightbox.tsx: left/right navigation arrows
- InstallWizard.tsx: status icons (check, x, minus, loader, circle, copy, download)
- Keep Lucide in ui/ and ai-elements/ base layers (shadcn standard)
Core fixes:
- Add settingSources to SDK options so CodePilot loads user/project settings
like the CLI (fixes WebFetch timeouts, tool permission issues)
- Delete CLAUDECODE env var to prevent nested session detection when
CodePilot runs alongside Claude Code CLI (fixes 'exited with code 1')
- Remove manual MCP config reading (SDK handles it via settingSources)
- Remove hardcoded timeout env vars (SDK loads from user settings)
UX improvements:
- Remember last selected model across sessions (localStorage)
- Same-project new chat skips directory picker
- File tree auto-refreshes on project switch and after each tool completes
- Left sidebar projects default collapsed (only most recent expanded)
- Right-side file tree defaults collapsed
- Fix file tree icon alignment (spacer size-4 → size-5)
- Dynamic model list from /api/providers/models endpoint
- Provider change dispatches event to refresh model selector
- Improved error messages with provider name and troubleshooting hints
- Fix context button file type matching for extension patterns
- Default tool timeout 120s → 300s
FileTree component was not passing baseDir to the file API, causing
it to fall back to restricting access to the home directory. On
Windows, projects on a different drive (e.g., D:\) were blocked
with 403.
Changes:
- FileTree now passes workingDirectory as baseDir parameter
- File API routes no longer require baseDir to be under homeDir
(allows cross-drive access on Windows)
- Still prevents using filesystem root as baseDir for safety
- Add isRootPath() utility for root path detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The chat route and claude-client both fell back to process.cwd() when
working_directory was empty, causing Claude to operate in the server's
directory instead of the user's project. Since session creation now
requires a valid working_directory, these fallbacks are unnecessary
and misleading. Fall back to os.homedir() as a safer last resort.
Also update CLAUDE.md: require explicit user confirmation before
publishing releases (no auto push+tag).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.10.7 skipped .cmd wrappers entirely, but the SDK's internal
fallback can't find the CLI either. Now we read the .cmd file,
extract the actual .js path (e.g. node_modules/@anthropic-ai/
claude-code/cli.js), resolve %~dp0 to the wrapper's directory,
and pass the resolved script path to the SDK.
Regex patterns are scoped to paths containing "claude" to avoid
matching unrelated .js files in the wrapper.
Bump version to 0.10.8.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two root causes of spawn EINVAL on Windows:
1. process.env spread can include undefined values. Windows
child_process.spawn requires all env values to be strings.
sanitizeEnv now builds a clean object, dropping non-string entries.
2. Claude CLI installed via npm creates .cmd wrappers that cannot be
spawned without shell:true. Detect .cmd/.bat extensions and fall
back to SDK's internal resolution instead.
Bump version to 0.10.7.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip null bytes and control characters from environment variable values
before passing them to child_process.spawn via the SDK. Third-party
Anthropic API providers could introduce invalid characters through
api_key, base_url, or extra_env, causing spawn EINVAL errors.
Bump version to 0.10.6.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: POST /api/chat/sessions now validates working_directory
exists (400 MISSING_DIRECTORY / INVALID_DIRECTORY)
- db.ts: remove process.cwd() fallback in createSession
- ChatListPanel: validate last-working-directory before use,
open FolderPicker when invalid; add SSR guard for localStorage
- NewChatPage: block sending without a project directory selected
Bump version to 0.10.5.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- platform.ts: only cache positive findClaudeBinary results so a fresh
CLI install is detected immediately on next check
- ChatView.tsx: use useRef as atomic lock to prevent double-fetch from
rapid clicks on "Load earlier messages"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>