Skills system:
- Replace `CommandBadge.isSkill: boolean` with `kind: SkillKind` (agent_skill | slash_command | sdk_command | codepilot_command)
- handleSubmit dispatches by kind: agent_skill retains skill name in prompt, slash/sdk commands pass through to SDK, codepilot commands expand via COMMAND_PROMPTS
- Popover groups commands into Commands / Slash Commands / Agent Skills
- API `/api/skills` returns `kind` field; `displayOverride` persisted through full chain (frontend → API → DB)
- Forward SDK `system:init` slash_commands/skills metadata to frontend via SSE
MCP management:
- Fix remote server (SSE/HTTP) creation: validate `url` instead of `command`
- Fix dual-config read/write asymmetry: PUT routes servers by `_source` to correct file (~/.claude.json vs ~/.claude/settings.json)
- DELETE checks both config files; POST checks both for name collision
- McpManager preserves `_source` tags on edit/rename; JSON editor only manages settings.json servers
Completion processing:
- Extract core logic into `onboarding-processor.ts` and `checkin-processor.ts` (shared by route handlers and server-side detection)
- Remove frontend `detectAssistantCompletion` from ChatView — server-side path in `finally` block is now the single source of truth
- Replace hard-coded `fetch("http://localhost:PORT/...")` with direct function imports
- Both processors are internally idempotent (check state before processing)
Other:
- `settingSources` always includes 'user' so SDK loads ~/.claude/skills/
- Add 24 unit tests for SkillKind dispatch logic
- Add onboarding-completion extraction tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Issue 1 — Onboarding re-triggering after completion:
- ChatView.tsx: fence regex now tolerates CRLF, trailing whitespace, leading
whitespace before closing fence; JSON.parse input trimmed
- ChatView.tsx: hookTriggeredSessionId only cleared when backend returns ok
- assistant-workspace.ts: saveState uses atomic write (tmp + rename)
Issue 2 — "CLI output was not valid JSON" during document reads:
- claude-client.ts: remove all SDK hooks (Notification + PostToolUse) to
eliminate hook_callback control_request transport that corrupts stdout
- claude-client.ts: TodoWrite sync moved to message stream — tool_use records
pending input, tool_result emits task_update only on success
- claude-client.ts: notifications derived from system/task_notification and
result/is_error messages; Telegram forwarding preserved via notifyGeneric
Issue 3 — File tree not updating on project switch:
- FileTree.tsx: AbortController cancels in-flight requests on directory change;
abort fires before empty-directory early return to prevent stale overwrites;
error state distinguishes empty dir from load failure; loading always reset
- page.tsx: clear workingDirectory immediately on session switch
- SplitColumn.tsx: clear workingDirectory when active column has no directory
Tests: 9 new tests for fence parsing, atomic write, corrupted state fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
P1 fixes:
- Replace "Extensions page" with actual MCP + Skills sidebar pages
- Update signing status: macOS is signed (not notarized), Win/Linux unsigned
- Fix Gatekeeper warning description to match signed-but-unnotarized state
P2 fixes:
- Correct Assistant Workspace file locations: soul.md/user.md/claude.md/memory.md
are at workspace root, .assistant/ is state-only subdirectory
- Translate "Why CodePilot" section to full Japanese (was English regression)
- Add provider auth path clarification (CLI/env optional vs custom required)
- Add specific page names in First Launch (Settings > Providers, Settings > Assistant, MCP, Skills)
Missing content:
- Add structured Documentation section with user guides and developer docs
- Add FAQ section covering: claude not found, no models after provider config,
dev vs electron:dev, workspace file locations, Bridge platform tokens
- Sync all additions across EN, CN, JA
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace 17-item flat feature list with "Why CodePilot" benefit paragraphs
- Add Quick Links nav bar, First Launch guide, and 3 grouped capability tables
- Collapse ~120-line installation troubleshooting into <details> blocks
- Remove Tech Stack and Project Structure (already in ARCHITECTURE.md)
- Move WeChat QR from hero to Community section
- Add Documentation section linking to ARCHITECTURE.md and docs/
- README_CN.md: full Chinese translation, identical structure
- README_JA.md: Japanese headings/tables, English kept for narrative sections
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
QQ Bridge Adapter (new):
- qq-api.ts: AppAccessToken, WebSocket Gateway, heartbeat, passive reply,
msg_seq auto-increment — pure QQ protocol layer
- qq-adapter.ts: BaseChannelAdapter impl with WebSocket lifecycle,
C2C_MESSAGE_CREATE handling, image download (base64), dedup, reconnect
- Settings UI (QqBridgeSection.tsx) + API routes (/api/settings/qq)
- i18n keys for en/zh, PLATFORM_LIMITS qq: 2000
replyToMessageId threading:
- bridge-manager.ts: thread msg.messageId through all deliver/command/
permission paths — required for QQ passive reply (msg_id mandatory)
- permission-broker.ts: accept and forward replyToMessageId
SDK crash resilience (bug fix):
- bridge-manager.ts: check hasError BEFORE saving sdkSessionId — the SDK
may emit a session_id before crashing, and saving that broken ID caused
ALL subsequent messages to fail by repeatedly resuming a corrupted session
- claude-client.ts: always clear sdk_session_id on crash, even for fresh
sessions (SDK can persist a new ID via status event before dying)
- conversation-engine.ts: attach filePath to file objects after disk write
so streamClaude() reuses on-disk copies (prevents duplicate writes,
matches desktop route behavior)
QQ permission handling (P1 fix):
- permission-broker.ts: for channels without inline button support (QQ),
render text-based /perm allow|deny|allow_session commands instead of
invisible inlineButtons that would deadlock the stream
QQ passive reply budget (P2 fix):
- bridge-manager.ts: QQ-specific delivery limits chunks to 3 max,
truncates with notice to avoid exhausting passive reply quota
- delivery-layer.ts: export chunkText for reuse
Error message improvement:
- claude-client.ts: when exit code 1 with image files, hint "Provider may
not support image/vision input" instead of generic auth/network message
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>
ChatView.tsx (P2): snapshot consumption now calls detectAssistantCompletion()
so assistant workspace onboarding/check-in state updates even when the
stream completes while ChatView is unmounted.
ChatView.tsx (P2): replace local temp-message append with a DB re-fetch
on snapshot consumption to avoid duplicate messages (backend already
persists the reply in route.ts).
page.tsx (P3): add updatedInput and denyMessage parameters to
handlePermissionResponse so AskUserQuestion answers and ExitPlanMode
custom deny messages are forwarded to the permission API on the
first-message page.
electron/main.ts: fix Windows startup timeout by forcing IPv4 (family:4)
in health check HTTP request and improving error diagnostics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ChatView: consume unconsumed stream snapshots on mount so output
appears after idle without needing a conversation switch
- PermissionPrompt: extract permission UI from StreamingMessage into a
fixed-position component at the bottom of ChatView, preventing
permission popups from scrolling off-screen during long responses
- StreamingMessage/MessageList: remove ~409 lines of permission UI code
now handled by PermissionPrompt
- AssistantWorkspaceSection: fix macOS Electron file selector by using
the correct IPC API (window.electronAPI.dialog.openFolder)
- AppShell: increase title bar drag region from h-5 to h-10 (#152)
- ChatView: add key={sessionId} on MessageInput to reset input state
when switching conversations (#176)
- UpdateDialog: add remark-gfm plugin and table component mappings for
proper GFM markdown rendering in release notes (#117)
- RightPanel: add border and top margin to collapsed panel toggle for
visibility (#116)
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>
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.
* 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>