Extract provider resolution logic into two new modules:
- provider-catalog.ts: vendor presets, default model catalogs, protocol definitions
- provider-resolver.ts: unified resolution for all consumers (Claude SDK, AI SDK, model listing)
DB migration:
- Add protocol, headers_json, env_overrides_json, role_models_json columns to api_providers
- Create provider_models table for per-provider model catalog
Consumer refactoring:
- claude-client.ts: replace inline env-building with toClaudeCodeEnv()
- text-generator.ts: replace inline config with toAiSdkConfig()
- conversation-engine.ts, onboarding, checkin, plan routes: use shared resolveProvider()
- /api/providers/models: read from catalog + provider_models table
UI improvements:
- Merge confusing "Custom API (Anthropic-compatible)" into "Anthropic Third-party API"
- Add 3-field model name mapping (sonnet/opus/haiku) in advanced options
- ProviderForm.tsx: support new fields (protocol, env_overrides, role_models)
Bug fixes:
- Protect auth credentials from being deleted by legacy extra_env empty-string overrides
- Fix env var names: ANTHROPIC_DEFAULT_SONNET → ANTHROPIC_DEFAULT_SONNET_MODEL (SDK convention)
Tests: 229/229 passed. Verified with aiberm (no mapping) and pipellm (with mapping).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: `new Date().toISOString().slice(0, 10)` returns UTC date,
causing date-based features to be off by up to ±1 day for non-UTC users.
Check-in / assistant workspace:
- Add `getLocalDateString()` utility, replace all UTC date comparisons
- Schema v2→v3 migration: normalize lastCheckInDate only when it matches
UTC today (ambiguous window); leave past dates untouched so check-in
triggers correctly after upgrade
- Runtime compat in `needsDailyCheckIn`: accept UTC today as fallback
for edge cases the migration cannot catch
- `needsDailyCheckIn(state, now?)` accepts optional time for testability
Usage stats:
- Replace single-offset SQL bucketing (`DATE(datetime(..., '+N minutes'))`)
with JS-side per-row aggregation using `getLocalDateString(new Date(utc))`
- Date methods account for historical DST offset at each message's timestamp
- `getTokenUsageStats(days, now?)` accepts optional time to pin window start
- Window boundary via `localDayStartAsUTC()` is already DST-aware
Tests (new file: timezone-boundaries.test.ts, 22 tests):
- getLocalDateString at UTC+9/UTC-5/UTC boundaries with fixed timestamps
- needsDailyCheckIn: compat suppression, clearly-past trigger, local match
- v2→v3 migration: targeted normalization, past-date preservation, null safe
- localDayStartAsUTC: UTC+8/UTC-5 midnight boundaries
- Usage stats bucketing: UTC+9 cross-day, summary window, DST spring-forward
- All usage stats tests use pinned `now` — no time-bomb from fixed test data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
The SQLite database connection was never explicitly closed when the
process exits. In WAL mode, this can leave behind -wal and -shm files
and risk data loss if the WAL hasn't been checkpointed.
Changes:
- Add closeDb() export for explicit database closure
- Register process shutdown handlers (exit, SIGTERM, SIGINT, SIGHUP)
that call closeDb() before the process terminates
- Idempotent: multiple calls to closeDb() are safe
- After closeDb(), getDb() transparently re-opens the database
- Add 6 unit tests covering close, idempotency, reopen, data
persistence, and WAL cleanup (all passing)
Slash commands:
- Fix skills not appearing (removed non-existent `enabled` filter)
- Add badge/capsule UI for prompt-type commands (/compact, /review, etc.)
- SDK-native commands sent directly; CLI-only commands expanded to prompts
- /clear now resets SDK session and clears database messages
- /cost shows actual aggregated token usage statistics
- Keyboard: Backspace/Escape to remove badge, arrow keys auto-scroll popover
- Add /memory command for editing CLAUDE.md
Multi-provider management:
- CRUD API for API providers with SQLite storage
- ProviderManager UI with quick presets (GLM, Kimi, MiniMax, etc.)
- Dynamic model labels based on active provider
- Environment variable injection with conflict resolution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously the database path depended on app.getPath('userData') which
varies with app name changes (CodePilot vs codepilot vs Claude GUI).
Now uses a fixed ~/.codepilot/ directory for both dev and production.
Includes automatic migration: on first launch, if the DB doesn't exist
at the new path, it checks old locations (Application Support variants
and the old dev-mode data/ folder) and copies the database over.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full-featured desktop application built with Next.js + Electron:
- Streaming chat with Claude Code via Agent SDK
- Session management with SQLite persistence
- Project file tree and file preview
- MCP server and custom skills management
- Permission controls with Code/Plan/Ask modes
- Dark/light theme, token tracking
- macOS DMG packaging with electron-builder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>