Commit Graph
83 Commits
Author SHA1 Message Date
7418andClaude Opus 4.6 281e7f9502 fix: skills/MCP/completion system overhaul — separate skill kinds, fix MCP remote servers, deduplicate completion processing
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>
2026-03-10 12:49:43 +08:00
7418andClaude Opus 4.6 789b453e92 fix: onboarding stability, invalid JSON from hooks, file tree race conditions
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>
2026-03-09 21:27:54 +08:00
7418andClaude Opus 4.6 f76e4747cd feat: unified provider resolver with protocol-based dispatch
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>
2026-03-09 20:30:35 +08:00
7418andClaude Opus 4.6 94528edac9 fix: use local timezone for date-based features (check-in, usage stats)
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>
2026-03-09 11:55:29 +08:00
7418andClaude Opus 4.6 4da188bfe5 feat: theme family system with 12 color palettes + NavRail redesign
Theme system:
- Two-layer architecture: mode (light/dark/system via next-themes) + theme family (color palette via CSS variables)
- 12 theme families: Default, GitHub, Rosé Pine, Tokyo Night, Everforest, Nord, Kanagawa, Night Owl, Poimandres, Vesper, Horizon, SynthWave '84
- Per-family code block highlighting for Prism, HLJS, and Shiki (no more generic oneLight/oneDark fallback)
- Anti-FOUC: synchronous script in <head> reads localStorage and sets data-theme-family before paint
- Theme JSON files in themes/ directory, auto-discovered by server-side loader
- OKLCH color space for all values; CSS specificity html[data-theme-family] beats :root

UI changes:
- NavRail: moved Settings to bottom (replacing theme toggle), removed light/dark toggle button
- Settings: merged General controls into one card with dividers; Appearance section with live Shiki code preview
- Replaced hardcoded blue-500/blue-600 with primary/primary-foreground across 19 components
- Appearance settings: pill mode selector (light/dark/system), theme family dropdown, live code preview

New files:
- src/lib/theme/{types,loader,render-css,context,code-themes}.ts — core theme infrastructure
- src/components/layout/ThemeFamilyProvider.tsx — client context provider
- src/components/settings/AppearanceSection.tsx — appearance settings UI
- src/hooks/useAppTheme.ts — combined theme hook
- themes/*.json — 12 theme family definitions
- docs/handover/theme-system.md — handover documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:15:01 +08:00
7418andClaude Opus 4.6 1e0958f598 feat: QQ bridge channel + SDK crash resilience fixes
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>
2026-03-07 22:24:54 +08:00
7418andClaude Opus 4.6 c18776f4ca feat: integrate Claude Agent SDK capabilities — model/command discovery, effort/thinking, MCP runtime, file rewind, structured output
Systematically integrate high-value SDK capabilities while preserving the
multi-provider architecture:

SDK capability discovery (per-provider cache):
- agent-sdk-capabilities.ts: cache models, commands, account info, MCP status
  per provider via Map<providerId, cache> to prevent cross-provider pollution
- claude-client.ts: fire-and-forget captureCapabilities() after query() init
- providers/models: merge SDK models with static defaults
- skills: merge SDK slash commands with filesystem skills

Query options passthrough:
- thinking (adaptive/enabled/disabled), effort (low/medium/high/max),
  outputFormat, agents, enableFileCheckpointing
- /chat page.tsx: first-message path now includes effort/thinking (was missing)
- stream-session-manager: passes effort/thinking in POST body

Graceful interrupt:
- /api/chat/interrupt: conversation.interrupt() before abort fallback
- stream-session-manager: tries interrupt first, aborts after 2s timeout

MCP runtime management:
- /api/plugins/mcp/status: live status from active Query, auto-resolves
  providerId from session DB record (prevents wrong-provider cache writes)
- /api/plugins/mcp/reconnect + toggle: reconnect/enable/disable servers
- McpManager + McpServerList: runtime status indicators with action buttons

File checkpointing & rewind:
- enableFileCheckpointing=true for code mode by default
- rewind_point SSE: only emitted for prompt-level user messages
  (parent_tool_use_id===null) and skips autoTrigger turns
- /api/chat/rewind: dry-run preview + actual rewind via rewindFiles()
- MessageList: RewindButton with dry-run preview

Structured output:
- /api/chat/structured: reads SDKResultSuccess.structured_output as primary
  path, text fallback only when absent

Account info & thinking config:
- /api/sdk/account: cached account info display
- Settings: thinking_mode in ALLOWED_KEYS, thinking mode selector in UI
- Effort selector in MessageInput with lifted state pattern

New files: 11 route handlers + 2 lib modules + 1 test file
Modified files: 18 (types, client, manager, UI components, i18n)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:22:05 +08:00
7418andClaude Opus 4.6 6c02c135fe fix: composer UI polish and permission selector improvements
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>
2026-03-07 16:03:19 +08:00
7418andClaude Opus 4.6 111fa8c92e feat: refactor composer to two-layer layout, add session permission & context indicator
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>
2026-03-07 14:55:38 +08:00
jasonjaclyn2017 a53c60531f fix: prevent stale SDK session and tool-marker parroting after idle timeout (#184)
fix: prevent stale SDK session and tool-marker parroting after idle timeout
2026-03-07 10:26:05 +08:00
jasonjaclyn2017 03d2636d7f fix: decouple stream listeners from stream entry lifecycle (#183)
fix: decouple stream listeners from stream entry lifecycle
2026-03-07 10:25:59 +08:00
7418andClaude Opus 4.6 94fdd36d27 feat: assistant workspace V2 — indexing, retrieval, security hardening & workspace switching
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>
2026-03-07 00:37:02 +08:00
7418andClaude Opus 4.6 19aba25c8f feat: assistant workspace — persistent AI personality & memory via Markdown files (#165)
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>
2026-03-06 20:31:42 +08:00
jasonjaclyn2017andClaude Opus 4.6 a1bde18cd1 feat: add View Plan modal and feedback textbox to plan approval UI (#164)
* 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>
2026-03-05 11:00:02 +08:00
jasonjaclyn2017andClaude Opus 4.6 a906172807 fix: handle SDK keep_alive messages to prevent idle timeout during long operations (#163)
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>
2026-03-05 10:59:43 +08:00
315aac0f04 fix: sanitize __NEXT_PRIVATE_* env vars from child processes (#161)
* 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>
2026-03-05 10:59:39 +08:00
7418andClaude Opus 4.6 113e15e2f4 feat: Discord bridge adapter — 完整实现 Discord 作为第三个桥接渠道
新增文件:
- discord-adapter.ts: 基于 discord.js v14 的适配器,支持 DM/频道消息、
  按钮交互、流式预览(edit-based)、@提及过滤(兼容角色 mention)、
  图片附件下载、!→/ 命令归一化、消息去重、typing indicator
- markdown/discord.ts: Discord 2000 字符分段,代码块围栏自动平衡
- API routes: settings CRUD(Token 掩码)+ verify(Discord REST 验证)
- DiscordBridgeSection.tsx: 设置 UI(凭据、授权、服务器策略、配置指南含 ID 获取方法)
- discord-bridge.test.ts: 22 项单元测试(chunking/auth/command/HTML 转换)

修改文件:
- bridge-manager.ts: Discord 分段投递 + channel-aware stream config
  (Discord 默认 1500ms/40chars/1900max vs Telegram 700ms/20/3900)
- BridgeLayout/BridgeSection: Discord 渠道开关和侧栏入口
- bridge/settings/route.ts: 新增 14 个 Discord 设置键
- next.config.ts: serverExternalPackages 添加 discord.js/zlib-sync
- i18n: 中英文完整翻译(30+ keys)
- docs/handover/bridge-system.md: Discord 架构文档

依赖:discord.js ^14.18.0, zlib-sync ^0.1.9

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:32:46 +08:00
7418andClaude Opus 4.6 243724a765 feat: 飞书(Feishu/Lark)Bridge 适配器 + 发布 v0.25.0
新增飞书 IM 通道,通过 @larksuiteoapi/node-sdk WSClient 接收消息、REST Client 发送回复。

适配器核心 (feishu-adapter.ts):
- WSClient 长连接接收 im.message.receive_v1 事件
- 消息去重 (message_id LRU 1000)、授权校验、群聊策略 (open/allowlist/disabled)
- @提及检测 (bot open_id/user_id/union_id 匹配)
- 图片/文件/音频/视频/富文本(post) 下载 → base64 FileAttachment
- Typing 指示器 (Openclaw 方案: emoji reaction)
- /perm 文本命令处理权限审批

渲染策略 (markdown/feishu.ts):
- 代码块/表格 → schema 2.0 interactive card (markdown 元素)
- 纯文本 → post (md tag), 支持粗体/斜体/行内代码/链接
- 权限卡片 → schema 2.0 card + /perm 文本命令 (无按钮, 见下方决策)
- 每层降级: card → post → text

关键决策 — 权限交互无按钮:
- 飞书 card.action.trigger 回调需 HTTP webhook, WSClient 不支持
- schema 2.0 不支持 action 标签 (错误码 200861)
- schema 1.0 按钮可渲染但点击报 200340 (无 webhook 端点)
- Openclaw 用 http.createServer + Lark.adaptDefault 解决, CodePilot 桌面端无公网端点
- 最终方案: 格式化卡片 + /perm 文本命令, 用户复制发送即可审批

其他变更:
- permission-broker.ts: permissionRequestId 去重 (30s TTL)
- telegram-bot.ts: bridge 模式互斥 (globalThis 标志)
- bridge-manager.ts: 飞书渲染分发 (deliverResponse feishu 分支)
- UI: FeishuBridgeSection 设置页 + 侧边栏入口 + 通道开关
- i18n: 中英文飞书配置文案
- 交接文档: bridge-system.md 更新飞书架构、数据流、6 项设计决策

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 19:03:13 +08:00
7418andClaude Opus 4.6 b1a043a995 feat: Telegram 流式预览(sendMessageDraft)+ 出站渲染解耦 + 投递容错
Bridge 新增"边生成边显示"能力,利用 Telegram Bot API 9.5 的
sendMessageDraft 在私聊中以草稿形式流式展示 Claude 生成进度。
架构上抽象为通道级可选能力,便于后续扩展到其他 IM。

types.ts
- 新增 PreviewCapabilities、StreamingPreviewState 接口

channel-adapter.ts
- BaseChannelAdapter 新增三个可选方法:
  getPreviewCapabilities / sendPreview / endPreview
- sendPreview 返回 'sent'|'skip'|'degrade' 三态,
  区分瞬时失败(跳过)和永久失败(降级)

telegram-utils.ts
- 新增 sendMessageDraft() API 封装(plain text,截断到 4096)

telegram-adapter.ts
- 实现 getPreviewCapabilities: 开关检查 + 私聊/群聊检测 + 降级状态
- 实现 sendPreview: 调 sendMessageDraft,400/404 永久降级,429 仅跳过
- 实现 endPreview: 空操作
- stop() 清空 previewDegraded Set

conversation-engine.ts
- 新增 OnPartialText 回调类型
- processMessage / consumeStream 透传 onPartialText 参数
- 使用独立 previewText 变量(只累积、不因 tool_use 清零),
  避免预览文本在工具调用后回退/抖动

bridge-manager.ts
- 流式预览状态机:generateDraftId / getStreamConfig / flushPreview
  节流闭包(间隔+最小增量+trailing-edge timer),finally 清理
- flushPreview 仅在 'degrade' 时降级,'skip'/网络错误不降级
- 新增 deliverResponse() 按 channelType 分发渲染:
  Telegram → markdownToTelegramChunks + deliverRendered
  其他 IM → deliver() 纯文本(内部自动分块)
  解除 handleMessage 对 Telegram 渲染的硬编码依赖

delivery-layer.ts
- deliverRendered: chunk 失败后继续投递剩余 chunk(不中断),
  追发截断提示通知用户,返回 ok:false 标识不完整投递
- deliver: 保持原有 fail-fast 行为(短消息无需跳过)

route.ts (bridge/settings)
- BRIDGE_SETTING_KEYS 白名单追加 5 个流式配置项

docs/handover/bridge-system.md
- 更新目录结构、数据流、设计决策(#12 流式预览)、设置项表

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:20:47 +08:00
7418andClaude Opus 4.6 38e7c96daa feat: Telegram Bridge 出站消息 Markdown 渲染
新增三层 Markdown → Telegram HTML 渲染管线:

markdown/ir.ts — markdown-it 解析 Markdown 为中间表示 (MarkdownIR)
  - 支持 bold/italic/strikethrough/code/code_block/blockquote/links/lists/headings/tables/hr
  - 表格使用 code-block 模式渲染为 ASCII 表格(<pre> 保留对齐)
  - HTML 内联 <br> 标签转换为换行符

markdown/render.ts — 通用标记渲染器
  - boundary tracking + LIFO stack 处理嵌套样式

markdown/telegram.ts — Telegram 专用渲染器
  - 样式映射到 Telegram HTML 标签 (b/i/s/code/pre+code/blockquote/a)
  - 文件引用保护:README.md/main.go 等不被误识别为 URL
  - render-first 分片:按渲染后 HTML 长度分片,超限时按比例重分割

delivery-layer.ts — 新增 deliverRendered() 接收预渲染 chunks
  - HTML + plain text 双通道,解析失败自动降级纯文本
  - sendWithRetry 支持 plainFallback 参数

bridge-manager.ts — Claude 回复改用 markdownToTelegramChunks + deliverRendered
  - 命令响应和错误消息保持 escapeHtml + deliver

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:38:12 +08:00
7418andClaude Opus 4.6 69d33df5b6 fix: Bridge 图片消息在桌面端聊天记录中回显
conversation-engine 保存用户消息时,复用桌面端 chat route 的存储格式:
- 将 base64 图片写入 .codepilot-uploads/ 目录
- 用 <!--files:[{id,name,type,size,filePath}]--> 格式嵌入消息 content
- 桌面 UI 的 MessageItem.parseMessageFiles() 可直接解析并通过
  FileAttachmentDisplay + /api/uploads 渲染图片

将 getSession() 调用提前到 addMessage 之前,确保 workingDirectory 可用。
写入失败时降级为 [N image(s) attached] 文本指示。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 12:58:50 +08:00
7418andClaude Opus 4.6 887870c343 fix: Telegram 图片接收 — offset 安全、caption 兜底、拒绝通知
P1a: caption-only 消息丢失
- poll loop 文本提取统一为 m.text ?? m.caption ?? ''
- 修复 image_enabled=false 时带 caption 的图片消息被跳过的回归

P1b: offset 越序风险
- markUpdateProcessed 改为连续水位推进(contiguous walk),
  不再直接跳到 updateId+1,避免越过尚未 flush 的相册 ID
- flushMediaGroup 成功入队后预注册所有 buffered ID 到 recentUpdateIds,
  保证 ack 时水位能连续推过整个相册

P2: 图片拒绝通知
- telegram-media: 统一返回 MediaDownloadResult { attachment, rejected, rejectedMessage }
  reject code 覆盖 too_large | unsupported_type | download_failed
  所有超限检查点(file_size/content-length/buffer)均返回可读文案
- telegram-adapter: processSingleImageMessage/flushMediaGroup 收集拒绝结果后
  直接通过 this.send() 发送通知到 Telegram,禁止静默丢弃

P3: 清理 lint warning
- 移除未使用的 escapeHtml import、QueueEntry interface
- 移除 downloadFileById 未使用的 expectedSize 参数

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 12:53:55 +08:00
7418andClaude Opus 4.6 570d26de3a feat: Telegram Bridge 图片接收能力
新增 telegram-media.ts 模块,处理 Telegram 图片下载与 base64 转换:
- selectOptimalPhoto: 选最优尺寸(长边 ≥ 1568px 的最小版本)
- downloadPhoto/downloadDocumentImage: getFile API → 下载 → base64,3 次重试 + 指数退避
- document 类型图片超 20MB 预检拒绝,提示用户按照片发送
- kill switch: bridge_telegram_image_enabled 设置项

扩展 telegram-adapter.ts poll loop:
- TelegramUpdate 类型增加 caption/photo/document/media_group_id
- 单图消息直接下载入队
- 相册消息 500ms 防抖合并后批量下载,构建单条 InboundMessage
- offset 安全:相册使用 max(updateIds) 保证不丢消息

透传 attachments 至 Claude vision pipeline:
- types.ts: InboundMessage 新增 attachments 可选字段
- conversation-engine.ts: processMessage 新增 files 参数,传入 streamClaude
- bridge-manager.ts: 允许纯图片消息通过,透传 attachments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 12:37:13 +08:00
7418andClaude Opus 4.6 c9f1b62809 fix: 模型切换串台 + 发送消息后滚动不到底部
1. page.tsx — loadSession 加 cancelled 竞态守卫,快速切换会话时
   丢弃过期的 fetch 结果,防止旧会话的 model/provider 覆盖新会话
2. ChatView.tsx — 去掉 if(modelName)/if(providerId) 守卫,改为
   无条件同步 props,空值走 localStorage → default fallback 链
3. ChatListPanel.tsx + types + route.ts + db.ts — 三个创建会话入口
   (handleFolderSelect/handleNewChat/handleCreateSessionInProject)
   统一从 localStorage 读取并传递 model + provider_id,DB 层写入
4. MessageList.tsx — ScrollOnStream 增加 messageCount prop,
   消息数增加时立即 scrollToBottom,覆盖乐观插入和 assistant 落地

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 12:01:10 +08:00
7418andClaude Opus 4.6 ead5cfcf8b refactor: 精简 CLAUDE.md + 分层上下文管理
根 CLAUDE.md 从 80 行压缩到 15 行,详细规则拆分到按需加载位置:

- .claude/rules/development.md — 开发规则、测试要求、commit 规范
- .claude/rules/release.md — 发版流程、纪律、Release Notes 格式、构建说明
- docs/CLAUDE.md — docs 目录导航,要求检索前先读子目录 README
- src/lib/bridge/CLAUDE.md — bridge 模块关键约定(runAdapterLoop 时序、
  bridgeModeActive 互斥、offset 安全水位、adapter 扩展方式)

目的:减少每次会话的固定上下文消耗,非相关模块的文档不加载到上下文。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:59:00 +08:00
7418andClaude Opus 4.6 6ad821d3af feat: 多 IM 远程桥接系统 + Bridge 一级导航 + bug 修复
**Bridge 系统核心(src/lib/bridge/)**
- bridge-manager: 生命周期编排、adapter 事件循环、会话级并发控制
- channel-adapter: 抽象基类 + 注册式 adapter 工厂
- channel-router: IM 地址 → CodePilot session 自动映射
- conversation-engine: 服务端消费 streamClaude() SSE 流
- permission-broker: 权限请求转发到 IM 内联按钮
- delivery-layer: 出站分片、限流、重试退避、HTML 降级
- telegram-adapter: Telegram 长轮询 + offset 安全水位 + 幂等去重
- security/: 路径校验、危险输入检测、滑动窗口限流

**Telegram 通知 Bot(src/lib/telegram-bot.ts)**
- 独立通知模式,与 bridge 模式互斥
- bridgeModeActive 使用 globalThis 存储,避免 HMR 重置导致轮询冲突

**API 路由**
- /api/bridge: GET 状态查询 + POST start/stop/auto-start
- /api/bridge/channels: 活跃通道列表
- /api/bridge/settings: bridge 设置读写
- /api/settings/telegram: 凭据管理(含 allowed_users 白名单)
- /api/settings/telegram/verify: 连接验证 + chat ID 自动检测

**UI 重构:Bridge 提升为一级导航**
- NavRail 新增 Bridge 图标导航项
- /bridge 页面:BridgeLayout hash 路由,Bridge + Telegram 两个子 tab
- BridgeSection: 总开关、状态、启停、通道开关、适配器状态、默认设置
- TelegramBridgeSection: 凭据配置 + 白名单 + Setup Guide(6 步)
- Settings 移除 Telegram 和 Bridge tab,只保留 4 个核心 tab

**Bug 修复**
- bridge-manager: runAdapterLoop 在 state.running=true 之前被调用,
  while 条件同步求值为 false 导致消费者循环不启动。修复为先设 running
  再启动 loop
- telegram-bot: bridgeModeActive 改用 globalThis 防止 HMR 丢失状态
- settings/telegram route: TELEGRAM_KEYS 补充 telegram_bridge_allowed_users

**Electron**
- 窗口关闭时 bridge 活跃则最小化到系统托盘而非退出
- 启动时通过 auto-start API 恢复桥接
- preload 暴露 bridge 相关 IPC

**文档**
- docs/bridge-system.md: 完整架构文档
- docs/chat-sdk-integration-feasibility.md: Chat SDK 集成调研
- CLAUDE.md 添加 bridge 文档链接

chore: bump v0.21.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 00:18:19 +08:00
7418andClaude Opus 4.6 c4d03c46e6 feat: Skills.sh marketplace integration + navigation restructure
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>
2026-02-27 21:43:46 +08:00
7418andClaude Opus 4.6 b5233d0257 fix: Image Agent 模式下图片不回显、不调用生成工具的问题
三个连锁 Bug 的根因修复:

1. route.ts — 图片 base64 数据保留
   - 之前:文件存盘后一律清空 base64 data(包括图片)
   - 现在:只对非图片文件清空 data,图片始终保留 base64
   - 确保 SDK 收到有效的 image content block 用于 vision

2. types/index.ts — ClaudeStreamOptions 增加 imageAgentMode 标记
   - 新增 imageAgentMode?: boolean 字段
   - 由 route.ts 根据 systemPromptAppend 是否存在来设置

3. claude-client.ts — buildFinalPrompt 在 imageAgentMode 下跳过文件路径引用
   - imageAgentMode=true 时不添加 [User attached image: /path] 文本
   - 防止 Claude 看到路径后调用 analyze_image 等内置工具
   - Claude 仅通过 vision(base64 content blocks)看到图片
   - 遵循 IMAGE_AGENT_SYSTEM_PROMPT 输出 image-gen-request 块
   - 普通模式行为不变,仍保留路径引用供 skill/工具使用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:45:49 +08:00
7418andClaude Opus 4.6 54515e1cec feat: stream session manager + always show Claude Code built-in models, bump v0.17.4
Stream Session Manager — 切换会话不中断流:

- src/lib/stream-session-manager.ts(新建): 全局单例管理所有 SSE 流,
  使用 globalThis 模式(同 conversation-registry.ts)保证 HMR 不丢状态。
  管理 AbortController、idle timeout (330s)、tool timeout 自动重试、
  5 分钟 GC,所有 SSE 回调写入 snapshot 并 emit 事件给订阅者。
  导出 startStream / stopStream / subscribe / getSnapshot /
  respondToPermission / clearSnapshot 等 API。

- src/types/index.ts: 新增 ToolUseInfo、ToolResultInfo(从 ChatView 提取)、
  StreamPhase、SessionStreamSnapshot、StreamEvent、StreamEventListener 类型,
  作为管理器与 ChatView 之间的数据契约。

- src/components/chat/ChatView.tsx(重构): 删除 abortControllerRef 及
  unmount abort effect(此前切换会话的根因)、整个 fetch/SSE 逻辑约 280 行、
  accumulatedRef/toolUsesRef/toolResultsRef/beforeunload handler。
  改为 subscribe() + getSnapshot() 订阅模式,unmount 仅 unsubscribe
  不 abort,mount 时通过 getSnapshot 恢复进行中的流状态。
  sendMessage / stopStreaming / handlePermissionResponse 全部委托管理器。

- src/hooks/usePanel.ts: PanelContextValue 新增
  activeStreamingSessions: Set<string> 和
  pendingApprovalSessionIds: Set<string>,支持多会话并发流指示。

- src/components/layout/AppShell.tsx: 监听 stream-session-event 窗口事件
  更新 activeStreamingSessions / pendingApprovalSessionIds Set 状态;
  将 beforeunload handler 从 ChatView 移至此处(全局感知所有流)。

- src/components/layout/ChatListPanel.tsx: 侧栏流指示器改用
  activeStreamingSessions.has() / pendingApprovalSessionIds.has(),
  支持同时显示多个会话的流/审批状态。

模型选择器始终显示 Claude Code 内置模型:

- src/app/api/providers/models/route.ts: 移除 hasEnvKey 环境变量检测条件,
  始终显示 "Claude Code" provider 组(Sonnet 4.6 / Opus 4.6 / Haiku 4.5)。
  根因:Claude Code CLI 通过 `claude login` 存储凭证在 ~/.claude/ 下,
  SDK 子进程可以读取,但 Next.js 进程的 process.env 中没有对应变量,
  导致之前的 hasEnvKey 检测失败时模型选择器不显示内置模型。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:58:30 +08:00
7418andClaude Opus 4.6 0c379e5607 fix: address tool_result dedup, SDK task ID collision, and doc accuracy
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>
2026-02-27 13:22:10 +08:00
7418andClaude Opus 4.6 c13ff54638 feat: fix tool call chain and Tasks panel integration
Phase 1: RightPanel Files/Tasks tab toggle
- RightPanel.tsx: add Files/Tasks toggle buttons, conditionally render
  FileTree vs TaskList based on panelContent state
- en.ts/zh.ts: add panel.tasks translation

Phase 2: TodoWrite SDK mapping
- types/index.ts: add source field to TaskItem, add task_update SSE event type
- db.ts: add source column migration, syncSdkTasks() replace-all function
  that syncs SDK TodoWrite output to local tasks table (user tasks untouched)
- claude-client.ts: PostToolUse hook detects TodoWrite tool, emits task_update
  SSE with mapped todo items
- route.ts: collectStreamResponse handles task_update, calls syncSdkTasks()
- useSSEStream.ts: add onTaskUpdate callback and task_update case
- ChatView.tsx: onTaskUpdate dispatches tasks-updated CustomEvent
- TaskList.tsx: listens for tasks-updated event to auto-refresh
- tasks/route.ts: add PUT bulk sync endpoint for testing

Phase 3: tool_result three-layer deduplication
- claude-client.ts: emittedToolResultIds Set prevents PostToolUse and user
  message handler from emitting duplicate tool_result for same tool_use_id
- route.ts: seenToolResultIds Set in collectStreamResponse skips duplicate
  DB writes
- ChatView.tsx: onToolResult checks existing tool_use_id before state update

Phase 4: Agent SDK upgrade
- Upgrade @anthropic-ai/claude-agent-sdk from v0.2.33 to v0.2.62
  (29 versions, zero TypeScript errors, all integration points compatible)

Phase 5: Documentation
- docs/agent-tooling-todo-bridge.md: architecture overview, TodoWrite field
  mapping, status mapping, idempotency strategy, three-layer dedup, SSE event
  types, troubleshooting guide
- CLAUDE.md: add Architecture Docs section with reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:12:59 +08:00
7418andClaude Opus 4.6 2a7dbd45c2 feat: add context sharing and storage consistency (P0)
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>
2026-02-27 12:20:01 +08:00
7418andClaude Opus 4.6 40a78ea0fc fix: image gen result persistence with fallback for temp message IDs, bump v0.17.3
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>
2026-02-27 11:51:31 +08:00
7418andClaude Opus 4.6 20a5934cfc fix: persist image generation results to DB for reload survival, bump v0.17.2
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>
2026-02-27 11:32:00 +08:00
7418andClaude Opus 4.6 a5e2cedc12 feat: gallery masonry layout, favorites system, left-right detail dialog, bump v0.17.0
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>
2026-02-27 11:00:58 +08:00
7418andClaude Opus 4.6 5ac4eb5eb6 feat: add Nano Banana 2 model, revert default to Nano Banana Pro, bump v0.16.0
模型变更:
- 新增 Nano Banana 2 (gemini-3.1-flash-image-preview) 到模型列表
- 默认模型恢复为 Nano Banana Pro (gemini-3-pro-image-preview)
  原因:Nano Banana 2 刚发布,部分区域尚未完全上线,调用会超时
- 模型列表顺序:Nano Banana 2 > Nano Banana Pro > Nano Banana
- 更新 ProviderManager、image-generator.ts、media/generate route 的默认值

依赖更新:
- @ai-sdk/google 3.0.31 → 3.0.32

版本:
- package.json 版本号 0.15.1 → 0.16.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:05:04 +08:00
7418andClaude Opus 4.6 d5dd9cfe34 feat: unify reference image data structure, add Nano Banana 2 model
Reference Image 统一重构:
- 新增 ReferenceImage 类型 (src/types/index.ts),统一承载 base64 和 localPath 两种来源
- 重写 image-ref-store.ts:单一 Map<string, ReferenceImage[]> 替代原来的
  imageGenRefStore + lastGeneratedImagePaths 双通路
  - sessionStorage 持久化 last-generated 路径,解决页面刷新后数据丢失
  - buildReferenceImages() 作为唯一的 merge 入口,消除各组件手写拼接逻辑
- ImageGenConfirmation props 从两个 (referenceImagePaths + referenceImagesData)
  合并为单一 referenceImages?: ReferenceImage[]
  - 预览渲染改为单循环,根据 data/localPath 决定 img src,上传图和结果图同时显示
  - API 调用时再拆分回 base64 和路径两个字段,后端无需改动
- StreamingMessage / MessageItem 各自 5 行手动 merge 逻辑替换为一行
  buildReferenceImages() 调用
- ChatView 改用 setLastGeneratedImages() 和 transferPendingToMessage()
  替代直接操作 Map
- MessageInput 改用 setRefImages() / deleteRefImages() 替代直接操作 Map

新增 Gemini 3.1 Flash Image 模型:
- 新增 Nano Banana 2 (gemini-3.1-flash-image-preview) 作为默认图片生成模型
- 模型列表: Nano Banana 2 > Nano Banana Pro > Nano Banana
- 更新 ProviderManager、image-generator.ts、media/generate route 的默认值

其他已有改动一并提交:
- batch image generation 功能文件
- 依赖更新 (package.json / package-lock.json)
- i18n、db schema、AppShell、uploads route 等相关改动

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:55:24 +08:00
7418 00d3fef5a4 feat: refactor image generation to bypass model dependency, fix UI issues
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: 生图)
2026-02-26 21:07:48 +08:00
7418andClaude Opus 4.6 97dc9d3a8c chore: add @google/genai SDK and media_generations DB schema
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>
2026-02-26 12:18:26 +08:00
7418andClaude Opus 4.6 448738d4f3 feat: provider/model system refactor — switch provider from chat input
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>
2026-02-26 00:22:18 +08:00
7418andClaude Opus 4.6 52f5bb5f86 feat: add token usage statistics page in Settings
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>
2026-02-25 00:07:41 +08:00
7418andClaude Opus 4.6 edd55a9b0f fix: improve session resume resilience, permission timeout, and stream idle handling
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>
2026-02-24 21:39:09 +08:00
7418 7b9de0ab23 fix: add detailed error logging for SDK process failures
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
2026-02-24 18:10:39 +08:00
7418 f871087a84 fix: clear stale sdk_session_id on mid-stream crash to prevent repeated failures
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
2026-02-24 17:56:38 +08:00
7418 61e093094e fix: auto-fallback to fresh conversation when SDK session resume fails
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
2026-02-24 17:50:49 +08:00
7418 9e7ccfefaa feat: v0.12.0 — fix streaming tool UI persistence, file tree UX improvements, unify icon library
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)
2026-02-24 12:32:03 +08:00
7418 9c13f043d0 feat: v0.11.0 — SDK settingSources, nested session fix, dynamic models, UX improvements
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
2026-02-24 03:08:17 +08:00
7418andClaude Opus 4.6 c21d8e4e90 fix: remove last process.cwd() fallback in file upload path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:06:49 +08:00
7418andClaude Opus 4.6 d446da9717 fix: allow file tree access on Windows cross-drive project paths
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>
2026-02-13 17:33:06 +08:00
7418andClaude Opus 4.6 6aca8c3870 fix: remove process.cwd() fallback for working directory in chat route
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>
2026-02-13 17:28:53 +08:00