-
fix(test): stop viewer resources before removing CLI-test temp directories —
server-ports-busyandbranch-servernow tear downserver.jstimers/watchers before deleting their isolatedCCV_LOG_DIR; shared best-effort cleanup usesfs.promises.rmwith native retries for transientEBUSY/ENOTEMPTY/EPERMfailures. -
fix(migrate): 迁移完成后仍反复提示迁移 —
pendingOf()仅按文件大小比对判断是否需要迁移,忽略了wire-v2-convert-state.json中的status: 'done'标记。迁移完成后活跃的 v1 日志继续增长(双写),大小不匹配导致误判为待迁移。修复:pendingOf()检测到status === 'done'时直接返回无需迁移。 -
fix(ccswitch-import): running cc-switch →
database is locked— importing from cc-switch while it is open failed with导入失败(未检测到 cc-switch 或读取出错): query failed: database is lockedbecause the read-only connection's first query contends with cc-switch'sBEGIN EXCLUSIVEwrite lock (its real contention mode: a valid hot journal under an EXCLUSIVE transaction blocks even read-only readers, unlikeBEGIN IMMEDIATE). The prior malformed-journal fix only caughtSQLITE_BUSYon its own escalation path; on the main read pathBUSYescaped to the catch-all and surfaced as the opaquequery failed: database is locked. Fix inserver/lib/ccswitch-import.js: detectSQLITE_BUSYon every path (the read-only open and the providers query), retry once after a 200ms backoff (cc-switch's write transactions are short — transient locks usually clear), and if still held surface the friendlycc-switch db is locked (cc-switch may be running); retry shortlyinstead of the raw wrapper. Newtest/ccswitch-import.test.jscases use a child process holdingBEGIN EXCLUSIVEto deterministically reproduce both the held-lock (→ friendly message) and transient-lock (→ retry recovers providers) paths. -
fix(ccswitch-import): malformed leftover journal →
attempt to write a readonly database— importing from cc-switch failed with导入失败(未检测到 cc-switch 或读取出错): query failed: attempt to write a readonly databasewhen cc-switch had been killed mid-write and left a malformedcc-switch.db-journal(truncated/corrupt rollback journal) behind. SQLite must discard a malformed journal to open the DB, which is a write; the read-only connection (readOnly:true) refused it →SQLITE_READONLY. Root cause locked deterministically: a malformed journal trips recovery; the trigger is a torn/partial journal from an unclean crash. Fix inserver/lib/ccswitch-import.js: keep the default read-only open, but onSQLITE_READONLYescalate once to a read-write open guarded byPRAGMA query_only = ON(lets SQLite recover/discard the corrupt journal, blocks our own writes), then retry — mirroring what cc-switch itself does on its next normal launch.SQLITE_BUSYon the escalation path surfaces a clear "db is locked; retry shortly" message; all other errors still surface their real cause (query failed: …, e.g. "file is not a database"). Newtest/ccswitch-import.test.jscases plant a garbage-journalnext to a valid DB and assert recovery reads providers + cleans the journal, with a follow-up plain read-only read proving the DB is left clean. -
fix(test):
windows-npm-root-regression的「cc-viewer 自身目录是最后兜底候选」断言改为与resolve(repoRoot, '..')比对,不再要求路径以node_modules结尾——该后缀只在npm i -g布局下成立,git clone 检出(CI)下父目录是 workspace 目录,导致 CI 失败。
- fix(electron): add missing
src/utils/to electron-builder bundle —server/lib/v2/meta-rows.jsandserver/lib/v2/live-feed.jsimportclassifyRequestfromsrc/utils/requestType.js, butelectron-builder.ymldid not includesrc/utils/**/*in its files list. The packaged Electron app crashed at startup with "Cannot find module '.../src/utils/requestType.js'". Addedsrc/utils/**/*to the electron-builder files array (npm packaging was already correct viapackage.jsonfiles).
- fix(findcc): reorder Claude discovery priority — native binary beats PATH on Windows —
resolvePreferredClaudeSelectionnow checksresolveNativePath()before PATH-based and npm-based resolution. On Windows, PATH may surface a postinstall stub (no-extension shim /.cmd/.ps1) that causesERROR_BAD_EXE_FORMAT(193 / 216) when spawned directly; the platform binary bypasses that. Added 6 unit tests covering the full priority chain (configured → codefuse → native → path → npm) including a Windows-specific regression test for the ERROR_BAD_EXE_FORMAT scenario.- Priority order: configured → codefuse → native → path → npm (native moved from last to third)
-
ui(proxy-stats): retry stats UI redesign — Config|Stats merged into one tabbed modal — the hamburger
retry-configentry now opens aProxyStatsModalwith aSegmentedswitch (Config default); desktop and mobile no longer take separate paths. The Stats tab is a sectionedProxyStatsDashboardcharted by a new zero-dependency SVGBarChart(upstream-vs-downstream availability, retry histogram, retry-burden buckets). -
feat(proxy-stats):
aggregateRecordsgains a globalretryBurden5-bucket distribution (0 / 1-5 / 6-20 / 21-50 / >50) plus per-bucketretryCodeCounts/dominantFailStatus/dominantFailCounton byModel/byPath/byProfile, surfaced as a "Dominant Fail Code" column in each dimension table. -
ui(proxy-stats): Config tab uses a two-column
Strategy/Execution Parametersgrouped layout, falling back to one column on narrow viewports. -
fix(win): Windows 上
ccv误报「找不到 Claude Code cli.js」 (#137) ——getGlobalNodeModulesDir()用execFileSync('npm.cmd', …)取全局node_modules根目录,而 Node 修复 CVE-2024-27980 后(18.20.2/20.12.2/22.0.0)不带 shell 直接 spawn.cmd/.bat会同步抛 EINVAL,异常被 catch 成null,导致所有依赖全局根目录的 Claude 探测全部落空——即使包就装在报错信息打印的那个目录下。改走cmd.exe /d /s /c npm root -g(argv 为固定字面量,无插值),新增inferGlobalNodeModulesDir()免 npm 兜底(NPM_CONFIG_PREFIX/%APPDATA%\npm/ Program Files / nvm-windows / Volta / cc-viewer 自身所在目录),超时 2s→10s,并容忍 npm 告警前缀(取最后一行非空)。 -
fix(electron/win): 打包版 Electron 开 tab 报
spawn node ENOENT、tab 永久卡加载中 (#129) —— tab worker 的fork()以where node结果作execPath,失败时回落到裸字符串'node',而打包版 GUI 进程不继承 shell PATH,该回落无法解析。现回落到 Electron 自带二进制 +ELECTRON_RUN_AS_NODE=1(无需外部 Node;node-pty 预编译产物为 N-API,跨 ABI 稳定),where多行输出只取.exe,用真实 Node 时清除该变量;child.on('error')立即clearTimeout+ 置 error 态并广播(不再干等 30s ready-timeout),child.send()加 try/catch 经appendDiag记录,避免write EPIPE盖掉真实原因。
- ui(proxy): fuse retry config and stats into a unified split-page —
RetryConfigFormextracted from RetryConfigModal as inline component;UnifiedProxyRetryPagewith left config / right stats panels (independent scroll); recent records table filtered to errors only. SharedisProxyMode()utility eliminates duplicated proxy-detection logic. Proxy stats toolbar/sidebar buttons removed; unified page now reachable via hamburger menu. P1–P2 code-review fixes applied.
-
ui(chat):
[UltraPlan]tag above UltraPlan user bubbles — when a chat user message was submitted via UltraPlan (UltraPlanModal/buildLocalUltraplan) or CLI/ultraplan, a small blue tag now appears above the user bubble so these special inputs stand out. Detection lives in the user-input classifier: newisUltraplanText()insrc/utils/contentFilter.jsmatches the<system-reminder>[SCOPED INSTRUCTION]…marker (emitted only byultraplanTemplates.js) on the raw text beforestripSystemTagsremoves it — anchored inside a single reminder block (negative-lookahead on the close tag) to avoid tagging prose that merely mentions the phrase after an unrelated reminder.classifyUserContent()now returns a message-levelultraplanflag (additive key);ChatViewthreads it through both the array and string user-render branches (never onplan-prompt), andChatMessage.renderUserMessage()renders the tag via new.ultraplanTag/.ultraplanTagRowclasses (blue =var(--color-primary), no!important). Reuses the existingui.ultraplanlabel. Newtest/ultraplan-detection.test.jscovers the detector (codeExpert/researchExpert/custom/seedPlan-prefixed + prose-mention / after-closed-reminder negatives) and theclassifyUserContentflag (incl. non-first block via.some). -
ui(menu): distinct icons for the log/prompt/messaging entries — the hamburger「日志管理工具」now uses a notepad (
FileTextOutlined) and「查看用户 Prompt」a single speech bubble (MessageOutlined), replacing the near-identical import/export box-arrow glyphs. The「通讯软件接入」entry and the MessagingModal title use a new two-bubble dialogue icon (src/components/common/DialogueIcon.jsx, lucide messages-square,.anticon-wrappedcurrentColorstroke) so the two-party IM integration reads differently from the single-bubble prompt entry. -
ui(retry-config): hide the「代理重试配置」menu entry unless proxy mode is confirmed — the hamburger entry (and its pinned shortcut / Electron tab-bar pin, which all derive from the same menu descriptors) now only renders when a non-built-in proxy profile is active, or the built-in Default points at a non-official endpoint (reuses ProxyModal's
api.anthropic.comorigin test). On an official subscription — or before/api/proxy-profileshas loaded — the entry is hidden, since retry orchestration only targets proxy gateways.
Fix: which claude still shows the ccv wrapper after ccv --uninstall + npm uninstall -g cc-viewer (stale shell function → command not found: ccv)
-
Root cause: the rc-file cleanup in
removeShellHook()works — the marker block IS removed from disk — but every already-open shell keeps theclaude()wrapper function loaded in memory (a child process cannotunset -fin the parent shell). Oncenpm uninstall -g cc-viewerdeletes theccvbin, the stale function's tailccv run -- claude --ccv-internal "$@"hard-fails withcommand not found: ccv. The existingunset -f claudehint printed by--uninstallwas one dim line immediately buried by the louder "done" message. -
Fix — self-unsetting guard in both hook variants: the generated
claude()function now opens withcommand -v ccv >/dev/null 2>&1 || { unset -f claude; command claude "$@"; return; }— a stranded wrapper transparently degrades to the realclaudeAND removes itself from the live shell on first use, sowhich claudeis clean from then on. Placed after the--ccv-internalstrip branch; identical in native and npm variants (the npm variant's terminal action iscommand claudeanyway, so early-return just skips self-heal). Existing installs pick the guard up on their nextccv -loggervia the content-differs reinstall path. The guard runshash -rfirst so a stale command-hash entry in a long-lived shell cannot false-pass thecommand -vcheck on the first call after the binary is deleted (review finding — the false-pass would have reproduced the exact reported symptom once before self-correcting). -
Fix — uninstall tombstone stops self-heal resurrection: the npm-mode hook backgrounds
ccv -loggerwhen it detects a missing injection, which could silently re-install everything in the window betweenccv --uninstallandnpm uninstall -g.--uninstallnow writes<claude-config-dir>/cc-viewer/uninstalled.flag(config-dir anchor — LOG_DIR honors--log-dir/CCV_LOG_DIRoverrides and would desync); hook self-heal calls now pass--self-heal, which the-loggerhandler vetoes silently (before mode detection, so a backgrounded heal with claude fully uninstalled exits 0 with no error noise). Plainccv -loggerwithin a 10-minute grace period refuses with a--forcehint (closes the race for already-deployed hooks that still call plain-logger); an aged tombstone or--forceclears the flag and installs — re-enabling stays one command. Residual risk documented: old deployed hooks after the grace period could still reinstall until their hook text is rewritten. -
Fix —
removeShellHook()false success: it reportedremovedwhenever the START marker was present, even when the block regex didn't match (user-damaged END marker) and the rewrite changed nothing; per-file outcomes also collapsed into a singlelastResult. It now returns per-file results (removed/corrupt/error), leaves corrupted files untouched with an explicit warning (newcli.uninstall.corruptBlockkey), and--uninstallprints one line per cleaned rc file;cli.uninstall.hookFailnow carries the failing{file}. -
Clearer uninstall guidance:
cli.uninstall.reloadShellnow says to rununset -f claudein each terminal where claude will be used — or just open a new terminal (exec $SHELL);cli.uninstall.donemakes the ordering explicit (npm uninstall -g cc-viewercomes AFTERccv --uninstall). New/updated keys (reloadShell,done,corruptBlock,hookFail,cli.logger.justUninstalled) in all 18 locales. Known npm-mode hazard (unchanged, CLI guidance covers the ordering): removing the package beforeccv --uninstallleaves a dangling static import in claude's cli.js that onlyccv --uninstallcan remove. -
Coverage: CI-safe source invariants pin the guard in BOTH hook variants and
--self-healon both self-heal calls (test/cli.test.jstemplate-invariants suite); behavioral cases for corrupted-block no-rewrite/no-false-success, per-file reporting across multiple rc files, tombstone write, self-heal veto, grace-period refusal +--force, and aged-tombstone clearing (tombstone tests pinCLAUDE_CONFIG_DIRto a shared tmp dir — theNODE_TEST_CONTEXTguard otherwise gives each spawned child a different pid-scoped config dir, making the tests vacuous). Verified end-to-end on a real zsh: stale function withccvoff PATH runs the realclaude --versionand self-unsets (which claude→ binary). A second 5-perspective team review (architecture / shell+security / regression / test-audit / maintainability) found no P0/P1; its adopted P2 hardens the tombstone fall-through tests with a sanitized PATH + fake native claude under the pinned config dir, so they can never inject into a user-writable real global claude install. Backlog: atomic rc writes, CRLF residue in the hook regex, ZDOTDIR, fish shell,--uninstall --purgeof the data dir, isTTY heuristic for legacy self-heal detection, orphan-START downgrade inremoveShellHook(mixed valid+corrupt blocks still reportremovedand leave the corrupt remnant), stderr note when the tombstone write fails, tombstone file left behind after a full uninstall. -
feat(proxy): LLM proxy retry engine — borrowed from
llm-retry-proxy(Python) and re-implemented natively in Node. The proxy now retries overloaded upstream responses (502/503/504/529/429) for LLM API requests (/v1/messagesand friends, detected via the existingisAnthropicApiPath), with three configurable strategies: serial (sequential retry), race (concurrent hedged requests, first 200 wins, rest aborted), and stagger (interleved hedging).offis the default (backward-compatible — no retry). 429 honors the upstreamRetry-Afterheader (seconds or HTTP-date). Streaming responses only retry before the first byte — once a 200 stream starts flowing it is passed through untouched to avoid corrupting already-sent content. The response carries anX-Forward-Attemptsheader telling the client how many attempts were made.- No interceptor double-logging: the retry engine calls fetch with the existing
x-cc-viewer-traceheader so the interceptor records every attempt; model replacement (resolveProfileModel) is applied by the engine itself before the first attempt (interceptor re-runs it on trace requests as an idempotent no-op). Network-proxy dispatcher (getProxyDispatcher) is still threaded through sohttp_proxy/https_proxykeep working. - Config: env vars
CCV_PROXY_RETRY_MODE(off/serial/race/stagger),CCV_PROXY_RETRY_STATUS_CODES,CCV_PROXY_RETRY_INTERVAL_MS,CCV_PROXY_RETRY_INTERVAL_429_MS,CCV_PROXY_MAX_RETRIES(0=unlimited),CCV_PROXY_MAX_CONCURRENT, andCCV_PROXY_RETRY_DURATION_MS(total deadline safety net). Runtime hot-reload viaretry-config.json(watchFile 1.5s cross-process sync); UI editable via the new RetryConfigModal. - 24 new
ui.retryConfig.*+ 5server.proxyStats.*i18n keys × 18 locales. Tests:test/proxy-retry.test.js(parseRetryAfter/shouldRetryStatus/extractModel/applyModelReplacement/resolveRetryConfig + executeRequest off/serial/race/stagger with mocked fetch).
- No interceptor double-logging: the retry engine calls fetch with the existing
-
fix(proxy-retry): connectTimeoutMs=0 dropped the AbortController signal —
singleFetchoverwroteopts.signalwithtimeoutCtl?.signal; whenconnectTimeoutMs===0(UImin:0)timeoutCtlstayednull, so the race/stagger cancel signal never reachedfetchand aborted losers kept running (socket leak). Now falls back toopts.signalwhen no timeout controller exists. New race-mode test assertsfetchstill receives a real signal atconnectTimeoutMs:0. -
fix(proxy-retry): exhausted race/stagger returned a hardcoded 503 —
lastFailedResponse()always returned 503 regardless of the real last upstream status (e.g. 429), so the client and stats got a wrong failure code. race/stagger now thread the real last failed response and return its status;executeRequestreports the trueupstreamStatus. Existing "全部失败 → 放弃" 503 tests still pass; new 429-exhaustion test assertsfinalStatus===429+upstreamStatus===429. -
fix(proxy-retry): upstreamStatus was always equal to finalStatus — collapsing the "upstream availability" vs "downstream availability" split to the same number.
upstreamStatusnow reflects the real last upstream response status tracked through each mode (serial/race/stagger), so the dual-availability cards differ as designed. -
fix(proxy-retry): maxRetries=0 meant infinite retries with no total cap — a persistently-overloaded upstream could loop forever. Added a
maxRetryDurationMsdeadline safety net (default 4h, envCCV_PROXY_RETRY_DURATION_MS, UI-editable) checked in serial/race/stagger;maxRetries=0still retries until success but stops at the deadline, so unattended congested-service tasks don't hang forever. NewvalidateRetryFieldcase + deadline test (Date.now monkeypatch). -
fix(proxy-retry): stagger first wave was a race burst — the initial fill launched all
maxConcurrentrequests synchronously (the "按 interval 交错" block was empty). Replaced with a unified scheduler that staggers the first wave byretryIntervalMslike subsequent refills. New test asserts aninterval-delayedsetTimeoutis scheduled. -
fix(proxy-retry): consecutive429 was dead state — incremented in stagger but never read. Wired to adaptive backoff: consecutive 429s grow the stagger interval exponentially (
retryInterval429Ms * 2^min(consecutive429,6)), so a rate-limited upstream isn't hammered. Newcompute429BackoffMshelper + test. -
fix(proxy-retry): trace-vs-internal doc/code contradiction — the file header and
proxy.jscomment claimed the retry fetch usedx-cc-viewer-internal(passthrough, no logging, no model replacement), butsingleFetchactually setsx-cc-viewer-trace(records every attempt, re-runsresolveProfileModel— idempotent no-op). Corrected the comments to match the intentional behavior; also fixes the stale "x-cc-viewer-internal" claim in the 1.6.341 changelog note above. -
fix(ui): RetryConfigModal showed "saved" before the POST resolved —
handleSavenowawaitsonConfigChange;AppBase.handleRetryConfigChangereturns the POST promise (still optimistic + rollback +message.erroron failure). Success toast + modal close happen only after the server confirms; on rejection the modal stays open for retry. -
chore(test): registered
RetryConfigModal.jsxintest/modal-mask.test.js'sEXPECTED_CONSUMERS(it consumesBLUR_MASK_STYLElike the other six settings modals — deliberate, count six→seven). -
fix(proxy-retry): RetryConfigModal save button had no feedback —
handleSavecalledonConfigChangebut neither closed the modal nor showed a success toast, so users thought the button was dead. Now closes the modal + showsui.retryConfig.savedafter POST. Added the missinghandleRetryConfigChangehandler inAppBase(optimistic setState + rollback on failure) and wiredretryConfig/retryDefaults/onRetryConfigChangefromApp.jsx→AppHeader(the modal rendered but could not save because the prop chain was broken). 2 new i18n keys (ui.retryConfig.saved,ui.retryConfig.saveFail) × 18 locales. -
fix(proxy-retry): Hop-by-hop headers blocked LLM retries —
handleLlmApiRequestforwarded the inboundtransfer-encoding: chunked/content-lengthheaders to undici, but the body had already been buffered to a fullBuffer, so undici rejected every retry withinvalid transfer-encoding header(status 0, 502 to the client). Now stripstransfer-encoding/connection/content-length(case-insensitive) before handing options to the retry engine; covered by a new chunked-TE live-proxy test intest/proxy-server.test.js. -
feat(proxy): Per-day proxy retry statistics — every proxied LLM request records a detail line (
attempts/retries/upstream_status/final_status/duration_ms/retry_codes/succeeded/profile_id/profile_name) to a per-dayproxy_YYYY-MM-DD.jsonlshard underLOG_DIR/<project>/. Disable withCCV_PROXY_STATS=off.- Stats integration: the existing
stats-worker(Worker thread) scansproxy_*.jsonlalongside the session JSONL and merges an aggregate into<project>/<project>.jsonunder a new top-levelproxyStatsfield (STATS_VERSIONbumped to 12 forces a rebuild). Aggregate covers upstream-vs-downstream availability (dual-caliber: first-attempt success rate vs final success rate), P50/P95/P99/max/avg duration, current streak + worst failure streak, retry-count distribution, upstream error-code distribution, by-model/by-path/by-profile breakdowns, slowest/fastest request, and recent records — all as pure functions inserver/lib/proxy-stats.js. - HTTP API + UI:
GET /api/proxy-statsandPOST /api/refresh-proxy-stats(newserver/routes/proxy-stats.js). A new?view=proxy-statsquery-param route renders a dedicated<ProxyStatsPage>(lazy-loaded, Ant Design only — no ECharts) with overview cards, availability analysis, retry distribution, duration analysis, by-model/by-path/by-profile tables, and a recent-records table (15s auto-refresh). Entry point: a "Proxy Retry Stats" toggle button in the AppHeader. - 37 new
ui.proxyStats.*i18n keys × 18 locales. Tests:test/proxy-stats.test.js(buildRecord/percentile/computeStreak/aggregateRecords dual-availability/streak/byModel/byPath/byProfile/retryDistribution + incremental cache).
- Stats integration: the existing
-
perf(proxy-stats): aggregateProxyStats full-rescanned every proxy detail file per request — ignored the
existingparam. Added file-level incremental caching (mergeProxyFileCachepure helper inproxy-stats.js): unchangedproxy_YYYY-MM-DD.jsonlshards (matched by size+mtime) reuse cached records, only changed/new files re-parse. TheproxyStatsfield rides theSTATS_VERSION12 rebuild. 6 new cache tests. -
perf(proxy-stats): two P1 findings from the 6-role review adopted. (1)
notifyProxyStatshad no debounce — every proxied LLM request posted a worker message that re-aggregated the whole project (the session-log path has the log-watcher debounce, the proxy path had none; O(N²) over a busy day). Now a 2s trailing coalesce: the first notify schedules a flush, further notifies within the window only update the pending file; the timer isunref()ed so shutdown is never held open. (2) The per-file records cache was persisted asproxyStatsFilesinside<project>.json— every raw record duplicated on disk (unbounded growth), every stats update rewrote it and everyGET /api/proxy-statsre-parsed it. The cache now lives in worker memory only (_proxyCacheByDir); the JSON carries just the aggregatedproxyStats, and a worker restart simply re-parses the shards once. -
fix(ui): ProxyStatsPage poll failure wiped the whole panel (review P1) — the fetch
.catchdidsetData(null), so one transient network blip / 5xx during the 15s auto-refresh collapsed already-rendered stats to the empty state, and the error was silently swallowed (CLAUDE.mdreportSwallowedrule). Failures now keep the last rendered data and report viareportSwallowed('proxyStats.fetch', err); non-OK responses throw instead of masquerading as "no data". -
fix(proxy-stats): seven P2 findings from the 6-role review adopted. (1)
upstream_statuswas hard-wired tofinal_statusinhandleLlmApiRequest— theupstreamStatusthatexecuteRequestalready returns was never destructured, so the schema's dual-status distinction (real last upstream code vs code returned to the client on race/stagger fallbacks) was dead on arrival; now wired through with a?? finalStatusfallback. (2)appendRecorddid a synchronousmkdirSync+appendFileSyncon the proxy hot path before the first response byte; it now writes through the sharedAsyncWriteQueue(ordered, sync-fallback on process exit viaflushRecords) and caches directory creation per process. (3) Dependency inversion:proxy.jsno longer dynamicallyimport('./server.js')to reach the statsWorker (a future proxy-only process would have booted a second viewer via module side effects) —server.jsregisters its debounced notifier through the newsetProxyStatsListener/emitProxyStatsUpdateregistry inproxy-stats.js; no listener → no-op. (4)POST /api/refresh-proxy-statscoalesces concurrent calls: a module-level latch shares ONE scan + ONE worker listener among all in-flight requests (was: one 30s listener per request piling up, anyscan-all-donecross-resolving other requests' waits, and N floods = N full scans). (5) Retention: opt-inCCV_PROXY_STATS_RETAIN_DAYS=Nprunesproxy_YYYY-MM-DD.jsonlshards dated older than N days during worker scans (strict filename parse — unparsable names never deleted; OFF by default so nothing is ever removed without the user choosing it). (6)ProxyStatsPage.module.csswrote a bare.ant-table-wrapperinside a CSS Module — the class got hashed and the rule never matched (uneven table widths); now:global(.ant-table-wrapper). (7) The ProxyStats header toggle is now gated on_isProxyMode()(non-built-in profile active, or Default pointing at a non-official endpoint — same test as ProxyModal's Max warning) for both the web button and the Electron tab-bar model, and its icon switchedApiOutlined→LineChartOutlined(was colliding with plugin-management). Tests: +1 route case (concurrent-refresh coalescing: one scan/one listener/all waiters resolved/latch resets), +2 worker cases (retention prunes beyond-window shard & keeps within-window; no env → nothing deleted), +2 lib cases (appendRecord async write + auto-mkdir + flush ordering; notifier no-op/forward/error-containment). -
test(proxy-stats): wiring-layer coverage (review P1 — the pure aggregation layer was well-tested, the integration layer had zero): new
test/api-proxy-stats.test.js(9 cases: GET 404-no-project / null-no-file / 200-with-field / null-pre-v12 / 500-corrupt-JSON; POST starts-worker+scan-all / 200-on-done+listener-removed / ignores-unrelated-messages / 500-no-worker) andtest/stats-worker-proxy.test.js(5 worker-thread cases: cross-day shard merge + no-persisted-cache invariant, corrupt-line skip, append+re-aggregate through the memory cache in one worker, proxy-only project still writes stats, session-only project gets empty proxyStats). -
perf(proxy): per-request
import('./server.js')cached into a module-level promise (_notifyProxyStats); failures now log underCCV_DEBUGinstead of being silently swallowed (stats ingest is a diagnostic side effect). -
fix(ui): ProxyStatsPage column titles were hardcoded English — 'Path'/'Model'/'Method'/'Time'/'Status' bypassed
t(). Replaced with 5 newui.proxyStats.col*i18n keys × 18 locales (non-en users now see translated headers). -
fix(ui): ProxyStatsPage
goBackdid a full page reload — ignored theonBackprop already passed fromApp.jsx. Now callsonBack(in-app state toggle, no reload) when present, falling back towindow.location.search=''only for the standalone?view=proxy-statsentry. -
feat(proxy-stats): Per-profile stats —
buildRecordnow carriesprofile_id/profile_name(frominterceptor._activeProfile, fallbackdefault/Default),aggregateRecordsgroups a newbyProfilearray (requests/retries/upstream-vs-downstream availability/P95 + carriedprofile_name), andProxyStatsPagerenders abyProfiletable after the byPath table. End-to-end verified: a profile pointing at a mock upstream records the profile id/name on the detail line, retries fire (503→200,x-forward-attempts: 2), and the aggregate surfaces the profile inbyProfile. 1 new i18n key (ui.proxyStats.byProfile) × 18 locales; 2 newproxy-stats.test.jscases (buildRecord profile fields + byProfile aggregation). -
fix(proxy-retry): five P1 findings from a 7-role review adopted. (1)
raceModeno longer gates a round onPromise.all: the round resolves the moment ANY attempt succeeds and the still-pending losers are aborted right then — previously the "winner" waited for the slowest header arrival and the loserabort()calls fired only after every attempt had already settled (no-ops), so all N hedged requests ran to completion and billed every round. (2)singleFetchcomposes the external abort signal with the timeout viaAbortSignal.anyfor the WHOLE response lifetime — the old listener-bridge was torn down infinallythe instant fetch resolved (headers in), which made a loser's already-streaming body un-cancellable wheneverconnectTimeoutMs > 0(the default). (3) Client disconnects now propagate:handleLlmApiRequestarms ares 'close'AbortController threaded throughexecuteRequestinto every mode's loop conditions, in-flight attempts, and retry sleeps (sleepis abortable and waits are clamped to the total-duration deadline, so a huge upstreamRetry-Aftercan no longer pin an abandoned request) — previously an abandoned request kept hammering (and billing) the upstream for up tomaxRetryDurationMs(4h default). (4) modeoffis truly backward-compatible again: the legacy pass-through had no time-to-headers bound, but the engine imposed the 10sconnectTimeoutMson every LLM request even with retry disabled, aborting non-streaming completions that hold headers past 10s into a brand-new 502 — the timeout now applies only when a retry mode is active. (5)POST /api/retry-configisisLocal-gated (403, mirroringccswitchImportPost): retry config lands in a cross-process hot-reloaded file, so a LAN client could flip the host intorace×high-concurrency and multiply the HOST's paid upstream traffic machine-wide. Also folded in from the same review: a single-winner latch instaggerMode(two same-tick 200s could overwriteresolvedwith a body the first winner's cleanup had just aborted — truncated 200 to the client), failed-attempt bodies are drained viadiscardBodyin race/stagger (undrained bodies pinned upstream sockets exactly when retries are most active; serial already did this, now before the wait instead of after), and race-round waits deriveRetry-Afterfrom the last REAL failure instead ofresults[0](which could be a headerless network error). Coverage: newtest/proxy-retry-cancel.test.js(9 cases) with a deferred, signal-aware fetch mock — the original suite's instant/signal-blind mock made every one of these properties unobservable (deleting allabort()calls kept it green) — pinning winner-before-stragglers + loser aborts, double-win body discard, client-disconnect teardown across all three modes, off-mode signal-less single-attempt, and failure-body drain; newtest/api-retry-config.test.js(403 gate writes nothing, 400 shape, validated 0o600 write + live-binding refresh + SSEretry_configbroadcast); and an end-to-end live-proxy case (529×2 → 200 withX-Forward-Attempts: 3) intest/proxy-server.test.js. -
feat(proxy): Import providers from cc-switch — reads AI provider credentials (baseURL/authToken/model mappings) from the local cc-switch Tauri app's SQLite database and auto-generates cc-viewer proxy profiles. Cross-platform path detection probes
~/.cc-switch/cc-switch.dbfirst on every platform (cc-switch hardcodes this path viaget_app_config_dir()in itsconfig.rson mac/linux/windows; the Tauri identifier does not affect the DB path), with platform-specific Tauri app-data paths (~/Library/Application Support/cc-switch/,%APPDATA%\cc-switch\,~/.local/share/cc-switch/) kept only as low-priority legacy fallbacks so a stale leftover there can never shadow the real DB. Openscc-switch.dbin read-only mode (no SQLITE_BUSY lock when cc-switch is running), queries theproviderstable forapp_type='claude'rows, and mapssettings_config.env→ cc-viewer profile fields (ANTHROPIC_BASE_URL→baseURL,ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY→apiKey,ANTHROPIC_MODEL+ the three family-model fields, ignoring the_NAMEsuffixed variants;CLAUDE_CODE_EFFORT_LEVEL→effortso a user's effort toggle is not silently dropped on import). Codex providers are skipped (incompatible auth format). Imported profiles get accs_id prefix andsource: 'cc-switch'marker; amergeImportedProfilespure function updates existingccs_entries (credential refresh) and appends new ones while leaving user-createdproxy_profiles untouched — deleted-from-cc-switch entries are pruned. Newserver/lib/ccswitch-import.js(pure functions, fully unit-tested against the real db);GET /api/ccswitch-providers(preview, masked off-host) +POST /api/ccswitch-import(local-only merge + SSEproxy_profilebroadcast); a "从 cc-switch 导入" button in ProxyModal. 4 newui.proxy.ccswitch*i18n keys × 18 locales;test/ccswitch-import.test.js(25 cases incl. live-db integration + cross-platform path-priority injection).- fix(cc-switch-import): the import reported a misleading
providers table not foundwhenever the DB was unreadable. Thesqlite_masterexistence query's innercatch {}swallowed the real error (e.g.file is not a databasefor a corrupt/non-SQLitecc-switch.db) and collapsed every failure into the generic message, so the user could not tell a corrupt file from a genuine schema mismatch. The catch is removed — a throw now propagates to the existing outer catch, which surfacesquery failed: <real message>; the genuine table-missing case reportsproviders table not found in <resolved path>(naming the file so a stale leftover at a probed path is distinguishable from the real cc-switch DB). Tests: a non-SQLite file asserts the error is not the masked string and exposes the real cause; a valid-but-tableless DB asserts the distinctproviders table not found in <path>message. - fix(cc-switch-import): five P1 findings from a 6-role review adopted. (1) A corrupt/unparseable existing
profile.jsonno longer wipes user-created proxies: the import previously swallowed the parse error, merged into an empty base, and overwrote the file with onlyccs_entries — it now ABORTS withok:false(reportSwallowed('ccswitch-import.read-existing')), and a parse to a non-object shape aborts the same way; the on-disk file is left byte-identical. (2) The client no longer misreports 403/400 as success: those response shapes carry noimported/updatedcounters, so the olddata.imported === 0heuristic fell through to the green "Imported 0, updated 0" toast — success is now decided strictly onresp.ok && data.ok === true, and every server error path (403 gate, 400 exception) carriesok:false. (3)mergeImportedProfilesnow upholds the samemax-invariant asproxyProfilesPost: the built-in Default is seeded at the front when missing (a fresh install importing before ever saving a proxy used to write a list with no Default option), and existing id-less entries are preserved instead of silently dropped. (4) The Node floor mismatch is surfaced instead of silent:node:sqliteneeds Node ≥ 22.5 (--experimental-sqlite) / ≥ 23.4 unflagged while the project floor is 20.14 — the unavailable-runtime error now names the requirement and ProxyModal maps it to a dedicated localized message (ui.proxy.ccswitchNodeUnsupported× 18 locales) instead of the misleading "cc-switch not detected"; the degradation contract is pinned by a test via the new_setDatabaseSyncForTesthook (CI runs Node 24, so the sqlite suites are exercised there). (5) The two HTTP endpoints — previously untested — get route coverage intest/api-ccswitch-import.test.js(fixture SQLite db under an injected HOME, no real cc-switch needed): local-only 403 gate writes nothing,ok:falsecontract on 400, first-import max seeding +0o600+ credential-free SSErefreshbroadcast, re-import preserve/prune semantics, corrupt/non-objectprofile.jsonabort leaves the file untouched, and off-host GET masking never leaks the plaintext key. - ui(cc-switch-import): ProxyModal footer buttons are now side-by-side — "从 cc-switch 导入" on the left, "+ 添加代理" on the right (each half-width) instead of stacked full-width rows; the explanatory hint line under the import button is removed together with its now-unused
ui.proxy.ccswitchImportHinti18n key (×18 locales). - fix(cc-switch-import): a stale/empty leftover
cc-switch.dbat a platform-specific probe path could shadow the real DB.candidateDbPaths()probed~/Library/Application Support/cc-switch/,%APPDATA%\cc-switch\, and~/.local/share/cc-switch/before~/.cc-switch/cc-switch.db, but cc-switch never writes to those Tauri app-data paths — a leftover file there was matched first and yieldedproviders table not found.~/.cc-switch/cc-switch.dbis now the primary probe on all platforms; the platform paths are demoted to legacy fallbacks.candidateDbPaths()was refactored to accept an injectable{plat, home, env}and exported as_candidateDbPathsForTestso the win32/darwin priority ordering is exercised on any test host (previously zero coverage on non-linux branches); new cross-platform tests assert the primary path wins and a stale legacy file cannot shadow it, using a neutral home value (no hardcoded username).
- fix(cc-switch-import): the import reported a misleading
- Root cause (empirically pinned on claude 2.1.212): claude's argv parser is last-wins for duplicate
--settings— the earlier flag is discarded wholesale, with no merge or warning. cc-viewer prepends its injected--settings {"env":{"ANTHROPIC_BASE_URL":…}}before user launch args (both spawn paths:pty-manager.jsandccv run'scli.js), so any user--settingsin workspace launch args / CLI passthrough clobbered the injected proxy override (capture silently broke whenever the user's settings definedANTHROPIC_BASE_URL) and, on the IM-worker path, theCCV_IM_DENYpermissions.denyhardening. - Fix — single merged flag: new pure helper
server/lib/settings-merge.js(mergeSettingsIntoArgs) extracts the user's--settingsoccurrences and folds the last one into the injected settings object, emitting ONE--settings: injected keys win (envshallow-merged withANTHROPIC_BASE_URLforced;permissions.denyunioned, userallow/askkept verbatim), every other user key rides along untouched (no deep merge —hooks/statusLinestay user-owned). Values load like claude does: inline JSON or a settings file path resolved against the spawn cwd, BOM-stripped, strict JSON. - Claude argv-semantics parity, pinned by probe-server experiments: flags after
--are prompt text (scan stops there);--settings --printconsumes the option-like token as its value exactly like claude; a trailing valueless--settingsis left in place so claude surfaces its own "argument missing" error; an empty--settings=— which claude accepts silently yet still lets clobber earlier flags — is always stripped. - Merge runs before cc-viewer's own injected flags. On both spawn paths the merge scans the RAW user args first, then
--thinking-display/--system-prompt-fileargs are appended. Otherwise a trailing valueless user--settingswould consume an injected token as its value — silently dropping the injection and leaking a stray bareword to claude. - Declared behavior change (release note): a user whose
--settingsdeliberately defines its ownANTHROPIC_BASE_URLnow has it silently overridden by the injected proxy URL — capture must win by design, and a successfully-loaded file fires no warning, so this line is the explicit signal (review P2). - Failure policy (injection must never block spawn): an unloadable user value (unreadable file, invalid JSON, non-object) is dropped with a warning and claude launches with the injected settings only. The warning is localized via a new
cli.settingsMergeFailedkey (all 18 locales) on both surfaces: the embedded terminal viaemitSpawnNotice(post-spawn, so the boot-time buffer reset can't swallow it; suppressed on internal respawns; the accompanying Englishconsole.warnis composed from the same key viatFor(..., 'en', …), so there is no duplicated English sentence to drift) and stderr on theccv runpath, which merges for the claude binary only — other tools' own--settingsargs are never touched. - Failure surface is content-free and control-char-safe. A user-pointed settings file is read defensively:
statSyncrejects non-regular files (blocks a--settings /dev/zeroevent-loop hang) and a 1 MB size gate caps the read; all read/parse failures collapse to generic reasons that never echo file bytes or the resolved path (the warning lands in the client-readable terminal buffer, so a raw error would be a file-probing oracle). The reportedvalue/reasonhave C0 control chars stripped so a crafted--settingscannot inject ANSI escapes into the embedded xterm. - Coverage:
test/settings-merge.test.js(merge/priority/union semantics, claude-parity argv shapes, byte-identical passthrough for the no-user-flag path incl. the IM deny object, content-failure matrix, hostile non-string argv, and a security-hardening block: no file-content/path leak, non-regular-file refusal, size gate, ANSI stripping), end-to-endspawnClaudecases intest/pty-manager-gap.test.js(exactly one merged--settings; warning reaches the terminal buffer; trailing user--settingsdoes not eat the injected--thinking-display), and CLI-tierccv runcases intest/cli-modes.test.js(merge, unloadable-value warning on stderr, non-claude gate). - Two review rounds (five perspectives each). Round 1 adopted P1+P2: CLI run-path test coverage, localized terminal notice, deny-fallback/non-object-env branch pins, warning-glyph consistency. Round 2 (on the committed change) adopted the injection-order fix, the file-read info-leak/DoS/ANSI hardening, and de-duplicating the English warning. P3 backlog:
--settingsfollowed by a literal--consumed as value,console.warnrespawn duplication, centralizing flag emission in the helper, Windows cmdline length when inlining a large settings file.
- Root cause (crash-stack + code forensics): the migration worker itself streams v1 input and was never the leak. When
convertProjectfinishes, its promote loop renames every converted session intosessions/— the directory the main thread'sV2LiveFeedwatches. Each promoted dir kept its staging-era birthtime (after feed start), so the feed attached it as a brand-new live session and replayed its ENTIRE history from byte 0: full synthesis, an unconditionalJSON.parse(JSON.stringify(entry))round-trip per item (checkpoint items carry the whole accumulated conversation), and O(N²) reconstructor allocation churn — all inside the 5s safety-pollsetInterval/ 80ms debouncesetTimeout, matching the reportedRunTimers → JsonStringifyfatal stack exactly. A multi-GB project pinned ~its whole content in the main heap and died at the ~4GB default limit (#129's streaming reads don't apply: they guard oversized lines read from disk, not giant arrays assembled in memory). - Fix — convert output never live-replays:
_attachnow reads the session'smeta.json;origin:'convert'(stamped by the converter since S8) attaches seeked to EOF — journal, responses, and every pre-existing conv epoch cursor start at the current file size, so nothing is read, synthesized, cloned, or broadcast (the cold-load channel owns migrated history; a theoretical later append still flows since it lands past the seeked offsets). No per-dir fs.watch is armed for these dead dirs (a promote can bring hundreds at once — one fd each). An unreadablemeta.json(abnormal: it is written synchronously before the journal's first drain) never falls through to a broadcast replay: treated as convert output while a conversion is running (same-process heuristic — the converter worker runs in the server process), seeded suppressed otherwise (a suppressed seed still reads and synthesizes, but stays off the broadcast path and is single-session-bounded). - Idle cursor eviction: an attached session whose journal hasn't moved for 10 minutes is detached from the feed, releasing its synthesizer/reconstructor/
_v3PrevMainretained state (previously_sessionsnever evicted — post-migration the feed held ~the whole project forever). Re-attach self-heals via the existing mtime-bump scan; the leader's owntick()nudge now re-attaches a previously-seen dir with history SUPPRESSED (it used to replay the whole session through the broadcast path — the same OOM shape resurfacing through normal idle-then-resume usage). Two review findings folded in: a cursor with an OPEN request is never evicted (the journal is only touched at request start/completion, so a single >10min turn looked "idle" and eviction would have stranded its already-broadcast placeholder — the feed now tracks open req seqs off the journal lines it already parses), and the trade is documented: an evicted large session re-synthesizes once (suppressed, single-session-bounded) on its next resume. - SSE senders zero-client short-circuit: all four broadcast helpers (
sendToClients& co.) built theirJSON.stringifypayload before checking for consumers — with no tab open, every reconstructed full-messages entry was serialized for nothing. They now return immediately when no client is connected. - Coverage: promote-simulation test (staging write →
renameSyncintosessions/→ safety-poll discovery) pinning zero frames + EOF-seeked cursors + post-promote append delivery; meta-unreadable fallback (both convert-running and not); eviction +tick()re-attach no-replay with live increments after; a stringify-landmine pin on the zero-client short-circuit. Backlog: streaming verify (readSessionstill materializes one whole session), per-file eviction of completed sessions in the converter's sharedV2Writer, seek-to-EOF for the generic suppressed seed (needs baseline semantics work).
- Root cause: an in-terminal
/resumeswitches the running claude to a past conversation while the wiremetadata.user_id.session_idstays the SAME — the V2Writer's same-sid map hit kept routing the resumed conversation into the OLD session dir (grafted astail-mismatchsnapshots; 130 such fingerprints found in real logs), and the panel — keyed on_seqEpoch = v2:<dir identity>— never switched. No wire-level signal exists for the switch; launch-time-cadoption never fires mid-session. - Fix — SessionStart hook bridge + targeted re-bind: a fourth cc-viewer-managed hook (
SessionStart, all sources — the server gates) runs the newserver/lib/session-start-bridge.js(same loopback + internal-token + always-exit-0 + clean-stdout contract as turn-end-bridge; SessionStart stdout is claude's context-injection channel, so silence is load-bearing) →POST /api/session-start-notify→interceptor.markSessionStart(gatessource==='resume', drops cross-project cwd, extracts the transcript basename) →V2Writer.beginResumeSwitch({transcriptUuid, hookSid}). The pending switch is consumed by the next REAL main request (countTokens/heartbeat probes excluded) regardless of its sid — covering both the observed same-sid wire behavior and a fresh-sid one: the writer re-binds routing to the resumed conversation's RECORDED dir (resolved by transcript uuid, identity preserved via meta.sessionId → the log re-joins its original folder and_seqEpochcontinues that conversation) or, when never recorded, a fresh<ts>_<hookSid>dir. The old binding is dropped and its owner.lock released (that conversation ended with SessionEnd); the target's claim is acquired atomically first — a target held by another LIVE window falls back to a fresh dir (fork semantics, same conflict rule as-cadoption). Downstream needs nothing: writer tick → live-feed attach → epoch boundary → panel switch, the exact chain-cadoption already rides. - Degradation: hook absent/removed → today's behavior, no errors. Injected mode (claude importing the interceptor directly) has no
CCVIEWER_PORT, so ALL hook bridges — including the existing turn-end/ask/perm ones — already no-op there; the CLIccvpath is where this works. A resumed conversation never recorded by ccv (or whose v2 identity predates its transcript lineage) lands in a fresh dir — no worse than before; transcript-lineage mapping is backlog, as aresource:'clear'wiring and a SessionEnd bridge. - Coverage: new
test/session-start-bridge.test.js(spawn-as-child: silent no-op without port, payload/token forwarding, clean stdout, refusal/timeout always exit 0); newtest/v2-resume-switch.test.js(same-sid re-join with identity/claim transfer + old-claim release + continuity, never-recorded →<ts>_<hookSid>, live-foreign target → fresh dir, dead-owner recycle, fresh-sid variant, probe/heartbeat non-consumption, last-wins + resetSessions clear); ensure-hooks suites extended (SessionStart injection idempotence, third-party coexistence, stale purge, removeAll); route 403/400/200 + dispatch-throw containment intest/branch-routes-events.test.js; markSessionStart wiring (source gate / cwd guard / uuid extraction) intest/interceptor-continuation-sync.test.js.
- Root cause: every ccv window (one process = V2Writer + viewer server) writes into the shared
LOG_DIR/<project>/sessions/root, and three read/write paths had no notion of per-process ownership: the live feed follows EVERY session dir under the root (deliberate, for teammate/IM cross-process producers — but it also broadcast a parallel window's traffic, and the frontend auto-switches on the new_seqEpoch); the cold-load fallback (getLiveLogSource→latestMainSessionDir) serves the project-wide newest main session, which is frequently the other window's; and-cfolder adoption picks that same "latest main" dir — two writers appending to ONE journal, physical log interleave. - Fix — per-session live-owner claim (
owner.lock, pid-liveness validity): the writing process claims each session dir it creates/adopts with anowner.lock{pid, startedAt}sidecar (newserver/lib/v2/session-owner.js). The file is only a pointer — the claim's validity is "the owning process is alive" (process.kill(pid,0), kernel state), so a crashed window's claim expires with its pid: no heartbeat, no TTL, and a dir can never be locked forever (the user-required in-memory-marker semantics; same liveness model asim-lock.js). Claims are only written by MAIN interactive leaders — teammate writers, IM workers (CCV_IM_PLATFORM), and the offline converter (stagingsessionsDirName) stay unclaimed so every project viewer keeps following them, and history stays universally readable (a dead owner's dir is plain unowned data; list/replay/export surfaces are untouched). - The three consumers:
latestMainSessiongainedskipForeignLive(skips candidates claimed by another live pid) — the cold-load fallback and adoption both pass it; the live feed's_attachrefuses foreign-live dirs (checked BEFORE the discard gate so they never enter_discardGated, whose delete side-effect would un-suppress history later) and the safety tick re-checks ATTACHED dirs, detaching one that turns foreign-owned after the fact (an unowned dir adopted by another window mid-follow); adoption acquires the claim ATOMICALLY (wxcreate-exclusive, dead-lock recycling à laacquireImLock) BEFORE committing, so two simultaneousccv -claunches racing onto the same dead-owned dir get exactly one adopter — the loser (like any launch whose target is live-foreign) mints a fresh folder, which matches Claude Code's own continue/fork semantics (fresh wire session_id per continue; official isolation is--fork-session). - Error-direction asymmetry (deliberate): acquire treats an unreadable/half-written lock as HELD (worst case: a fresh dir — never two writers on one journal); the read predicate
isForeignLiveOwnedtreats it as UNOWNED (a torn crash residue must never permanently gate a dir out of live/cold-load). Claims release synchronously at the head ofclose()(ahead of the 2s-bounded queue-drain race) and onresetSessions()(workspace switch → the dormant dir becomes legitimately adoptable). Known accepted limits: pid reuse can briefly mark a crashed window's dir "in use" (self-heals when the recycled pid exits; nothing is lost — the dir just isn't auto-followed/adopted meanwhile); shared LOG_DIR across hosts or pid namespaces (network mounts, containers) can't be liveness-checked and keeps today's behavior; and the claim is a cooperative/advisory lock — isolation holds only when BOTH parallel windows run a version that honors owner.lock (an older ccv ignores it and keeps the pre-fix cross-read behavior). - Coverage: new
test/v2-session-owner.test.js(acquire arbitration incl. the two-process dead-lock race, identity-checked release, asymmetry pins, realkill(0)probe); session-select foreign-live skip / dead-owner selectable / only-candidate-foreign → intentional empty cold load; live-feed foreign gate, attached-then-adopted detach, owner-death re-attach with history suppressed (no_discardGatedflood); adoption refusal onto a live-foreign dir, dead-claim recycling with claim transfer + release-on-close, idempotent self-claim; V2Writer claim lifecycle (sync release ahead of a hung drain, teammate/IM/converter never claim).
- Root cause (forensics on real data): the empty ~2.6KB sessions with a "—" overview are minted by Claude Code's own quota probes (
max_tokens:1, a single'quota'user message, no system/tools) — each probe carries a throwaway session_id (fired at CLI launches and agent-team spawns), and the v2 store creates one dir per sid. Not residue of anything launching ccv; the current mechanism inherently produces them. Verified across the whole LOG_DIR: every main-less session (18/18 across two projects) was exactly such a single-probe dir; every real session has akind:'main'req (own sessions) ormeta.leader(teammates). - Read-side discard (the fix): new shared predicate
isDiscardableSession(server/lib/v2/session-select.js — reuses the bounded streaming journal scan; wide 8MB budget so idle-heartbeat noise can never push a real session's first main past it): discard ⟺ nometa.leaderAND nokind:'main'|'teammate'req. Applied at every surface so these dirs never participate in any logic: log-management list (the modal),listV2Sessionsrows (adiscardflag), IM latest-log pick, workspacesessionCount(drives the auto-cheuristic), the stats worker (safe against stale cached probe units via theSTATS_VERSIONbump — see below), the live feed's attach gate (single choke point in_attach; self-heals via tick retry / 5s safety poll once a dir gains its first main), and teammate-leader candidate selection. Write side is untouched (a probe sid can't be distinguished from a nascent real session at write time) — the predicate is read-only and self-healing. - Two collateral bugs fixed by the same gate: (1) probe dirs could win the leaderless-teammate attribution tie-break (
findTeammateSessionDirs) and steal a teammate's traffic from the real leader's folded stream; (2) a probe-only workspace countedlogCount>0and auto-continued (-c) into a conversation that doesn't exist. The IM 对话记录 modal likewise no longer opens on a probe session. - Direct addressing (
validateLogPath/v2:refs) deliberately still resolves hidden dirs — the soft-delete/recycle machinery depends on it. Disk residue (~2.6KB per probe) stays put; an opt-in auto-reap into the existing recycle folder is backlog. - Final 6-perspective team review (architect/regression/quality/tests/compat/security) adopted P1+P2: the streaming reader's per-call chunk buffer is now capped to the file size (a fixed zero-filled 8MB alloc per KB-scale journal was a ~2600x allocation regression on startup scans);
listV2Sessions' inline discard verdict now CONFIRMS through the canonical predicate so a Windows file lock (EBUSY/EPERM swallowed into zero lines) can never mark a real session discardable in the log list / IM pick; a dir once refused by the live-feed discard gate re-attaches with history UNSUPPRESSED (its first main turn was never broadcast — cross-process producers have no cold-load fallback for connected clients);readNewLinesgained amaxLineBytestest seam with an oversized-line pin, plus a wire-v3 teammate live-row pin (kind/conv/mainAgent journal truth). Backlog: consolidate the four chunked line readers' shared byte-scan core (log-stream.js still has the multibyte-tear + string-cap flaws), workflow-subsystem whole-file journal reads, kind fallback 'sub' vs 'misc' alignment. - Team-review findings folded in: an I/O error on an EXISTING journal now defaults to KEEP (only a truly main-less readable journal — or a missing one — discards; a transient lock must never hide a real session);
STATS_VERSIONbumped 10→11 so the discard check sits AFTER the stats worker's incremental cache-reuse branch (v11 caches are written exclusively by post-discard code — probe units can't resurface, and cache hits skip the journal scan entirely); the live feed's deadcursor.pendingstring state (superseded by the chunked reader's Buffer carry) removed.
- Live conversation rendering restored. Since Wire v3 went default-on (b420b8f), the live metadata rows (
v2_requests_delta) omitted theconv/evtfields that the cold rows carry — the client assembler'sbuildEntryisif (row.conv)-gated, so every live-assembled entry came back with EMPTY messages. Symptom: the streaming typewriter worked, then at stream end the completed (empty) entry atomically cleared the streaming overlay and merged nothing — the turn vanished from the Chat tab until a page refresh (cold rows carryconv, so reload always worked). The v3 wire suppresses the legacy full-entry broadcast, so there was no fallback channel. - Fix: the synthesizer item now carries journal-truth
conv/evt/kind(adapter_emit), and live-feed_rowFrommirrors the cold fold row:conv,evt, journal-truthkind(heartbeat/countTokens were mis-reported assublive), and kind-derivedmainAgent(kind==='main' && !leader) instead of body re-derivation — the latter mattered because a countTokens probe wears the full main-agent body shape and, onceconvmade its messages non-empty, would have been merged into the chat live-only. Side fixes for free: live entries now get_isCheckpointon snapshot events (post-/clear boundaries were previously missed live), and the cacheLoss anchor no longer tracks countTokens as a main turn. - Team-review finding folded in: the cold Pass B (
attachBodyFields) also re-derivedmainAgentfrom the blob-backfilled body — with the live side fixed, a main-shaped countTokens would have re-polluted the chat on reload. Cold now uses the same kind-derived value (item.isMain), keeping live and cold rows in agreement (and the cold cacheLoss anchor off countTokens). - Coverage: the live-feed suite previously never ran in wireV3 mode at all. New tests run the feed with
wireV3: true, replay its emittedv3_conv/v3_resp/v2_requests_deltaframes through the REAL client assembler and assert non-empty message parity with cold reconstruction, field-level live/cold row parity, and countTokensmainAgent:falsepins on BOTH the live row and the default cold Pass B (with a realistic >5-tool main body that genuinely trips the outlawed re-derivation).
ERR_STRING_TOO_LONGno longer crashes the server. Migrating a very large v1 log can produce a session file (conv epoch / journal / responses.jsonl) past Node's ~512MiB string cap; the whole-filereadFileSync(path, 'utf-8')in the v2 read path then threw on every startup scan (readJsonlTolerant→readSession→scanV2Descriptors), killing the stats worker AND crash-looping the main process on restart (reported on Windows / Node 24, but platform-independent).- New
server/lib/v2/jsonl-read.jsiterateJsonlLines: chunked reads with byte-level\nsplitting (multi-byte UTF-8 across chunk boundaries is never torn), one line at a time ever becomes a string. A single line at the string cap is skipped with areportSwallowednotice instead of throwing (spec §14 tolerance). All session JSONL readers now ride it:readJsonlTolerant(replay/adapter/meta-rows/stats-worker/user-prompt consumers),readResponsesRaw, the wire-v3 native cold read (whose full-file reads previously silently dropped oversized conv/responses files), and the writer's_maxSeqInseq seeding (which previously would silently reset to seq 0 on an unreadable giant journal — a fold-corruption hazard). - Per-session containment: any residual read error degrades that one session to "not rendered" (
iterateSessionItemscatchesreadSessionthrows; the stats worker skips the broken session and keeps counting the rest) — one bad session can no longer take down the scan, the worker, or the server. - Team-review finding folded in: the live feed's own cursor reader (
readNewLines) was a second copy of the same crash class — its first attach seeds from offset 0 with one whole-file decode, and the boot path attaches recently-active sessions, so an oversized journal would still have crash-looped startup. It now reads in bounded chunks with byte-level newline splitting (partial tail carried as Buffer fragments on the cursor; an oversized single line is skipped with a report). - Known limit (backlog): a session whose parsed content itself exceeds the process heap can still OOM — windowed replay for pathological single sessions remains future work.
- Network detail view no longer loses request properties under the v2 store. The v1→v2 storage upgrade decomposed the request body into dedicated stores (messages → conversation events, system/tools → blobs) and silently dropped every other top-level body field —
max_tokens,temperature,top_p,stop_sequences,tool_choice,thinking,output_config,stream, extrametadatakeys, etc. — while headers (cookies included) survived intact. The writer now inlines those residual fields whole on the journal req line asparams(the same pattern that kept headers alive), and the adapter spreads them back into the rebuilt v1-shape body.response.statusText(captured by the interceptor since v1, dropped by the v2 store) is likewise persisted on the responses line and restored. Applies to live recording AND the v1→v2 offline converter (same writer path), so migrating old v1 logs preserves everything; DetailPanel needs no frontend change (wire-v3 detail rides/api/v2-entry→ the same adapter). - Additive + backward-compatible: old journal lines without
paramsreconstruct exactly as before (noWIRE_FORMAT_VERSIONbump — existing sessions stay readable, but sessions recorded/converted before this change cannot be backfilled retroactively).body.metadata.user_idstill always comes frommeta.userIdRaw(client session-boundary equality;-cadopted folders mix real user_ids), with otherparams.metadatakeys merged in. - Declared behavior change (V1 parity restored, not a regression): quota-probe requests (
max_tokens: 1, single'quota'message) re-classify from SubAgent to Count:Quota in the request list, becauseisQuotaCheckcan seebody.max_tokensagain.
-
V3.S1 — on-demand single-entry endpoint
GET /api/v2-entry?file=v2:<project>/<session>&seq=N[&sid=<uuid>]: rebuilds one full v1-shape entry (blob-backfilled, plusprevMain— the preceding mainAgent entry the detail view's Body Diff/Context tab needs) from the v2 store on demand. Mid-session main deltas are promoted to full replayed state via a new per-member checkpoint mode in the window materializer (client-equivalent to what reconstruction produces on the legacy channel). Accepts both the session dir basename and the bare session UUID (resolved server-side; rename/-c-adoption races answer 404 and the client surfaces an error with a Retry action). Response is brotli-compressed under the axis-B negotiation. Registered unconditionally — inert until the flag lands with V3.S3. -
V3.S2 — request-list metadata rows channel: with
CCV_WIRE_V3=1,/eventsadditionally emits av2_requestsframe (journal-fold rows: seq/timestamp/url/status/duration/usage/model/typeTag/cacheLoss, ~1.3KB each vs full entries) beforeload_end, and the live feed broadcasts onev2_requests_deltarow per emitted item (with a correction re-send when the next request changes the previous row's Preflight/Plan classification).typeTagis computed server-side with the SAMEclassifyRequestthe client uses (therequestType→contentFilter→teammateDetectorchain now carries explicit.jsextensions so Node can import it — single source, no fork). Three deliberate, test-pinned divergences from the legacy list, all more correct: row membership is the journal fold (a superset including conv-gapped crash-orphans; their detail fetch 404s gracefully),mainAgentis kind-derived (same semantics the mainAgentRing pinned), and cacheLoss'sttlreason actually fires (the client computed the gap as string-minus-string NaN). Flag read once at startup intodeps.wireV3and broadcast to clients viaserver_config.wireV3. -
V3.S3 — flagged frontend: request list from rows + on-demand detail: when the server announces
wireV3(viaserver_config), the request list renders from metadata rows (one_listSource()seam feeds desktop and mobile; selection-coupled call sites all go through it), cache-loss dots read the server-computed row field, and selecting a row fetches the full entry + its previous mainAgent from/api/v2-entry(loading/error/retry states, in-flight abort on rapid switching, 404 surfaces an error with a Retry action on rename races). The IndexedDB entry cache gains aprotocolVersionrecord tag instead of a DB_VERSION bump — a bump would wipe every user's cache at upgrade time regardless of the flag, breaking the dark launch. Flag off: byte-for-byte today's behavior. -
V3.S4/S5 — native chat wire + client assembler (flagged): with
CCV_WIRE_V3=1and a v2 source, the legacy full-entry stream is REPLACED end-to-end — cold loads send raw conv lines (from the last snapshot at-or-before the window start) + responses lines for the window members; the live feed forwards each raw conv/responses line as it lands and suppresses the full-entry broadcast (kv-cache/context side events unchanged). The client's newv3Assemblerreplays these into v1-shape entries (full accumulated messages by shared reference, no tools/system) and feeds the EXISTING ingest pipeline — merge guards, ChatView, team modal, and tool-result maps keep their exact semantics, verified by an oracle test asserting field-level parity between assembled entries and the legacy client-reconstructed stream. Deep consumers (ChatView/AppHeader/Mobile) read the assembledstate.requestswhile the list keeps rendering metadata rows. This replaces the originally-designed excerpt frames + parallel chat pipeline (consumer census showed near-full fidelity would be required anyway). -
V3.S6 — Wire v3 is now the DEFAULT (
CCV_WIRE_V3=0is the escape hatch back to the legacy full-entry wire, kept for one release cycle). New spec:docs/refactor/WIRE_FORMAT_V3.md. Measured on a real 68MB/3830-request session (tail-1000 cold load): plaintext 176.8MB → 42.5MB (÷4.2 — the remote plain-http path browsers can't brotli on), client JSON parse ÷4.2, live channel per-turn now delta-sized instead of re-broadcasting the full history twice per turn (was 3.7GB/client/session). Under brotli both wires converge near the same entropy floor, so localhost byte totals are similar by design. Retained surfaces:/api/local-log(log viewer + IM modal), downloads,ccv verify, v1 legacy files — all keep v1-shape output;entry-slimstays (idle on the v3 path) since the assembler architecture keeps entries client-side. -
Loading UX for the v3 wire: the full-screen loading mask is GONE. Metadata rows arrive in the very first frame, so the request list and on-demand detail are interactive immediately; the chat area shows its own inline Spin (the placeholder ChatView already had) plus a real byte meter —
load_startcarries the exact total of the upcoming v3 frame bytes (payloads are pre-built server-side) and the client rendersreceived/total MB (n%)as frames stream in. The legacy wire / v1 files keep the count-up text in the same inline spot. -
Team code review (6 perspectives) — P0/P1/P2 findings fixed:
src/utils/added to the npmfilesarray (the server now imports the sharedclassifyRequestchain from there — a published package would have crashed on boot; CI cannot catch this); v3 client state (rows/assembler/live-dedup) now resets on EVERY baseline reset (workspace switch, full_reload, fresh cold frame) — previously a workspace switch left the old project's rows in the list with 404 details, and reconnecting tabs grew memory unboundedly; live v3 frames arriving during the chunked cold assembly are buffered and replayed after it (a live delta could advance the shared channel pointer mid-assembly and corrupt cold entries); incremental reconnects (?since=) now send a since-scoped delta window instead of re-transmitting the whole session (the mobile reconnect regression); the legacy/escape-hatch cold load shows a loading state instead of flashing the onboarding guide;readV2NativeColdyields to the event loop and the whole v3 cold read is single-flighted per window (reconnect storms coalesce); cold kv/context fallback depth restored to 3 mains (legacy scan-ring parity);server_configcarries a build stamp and stale tabs self-reload across server upgrades; teammate sessions: fixed a crash in the v3 cold read (findTeammateSessionDirsitems are{dir}objects) and rows now carry the v1teammatecontract (agentName string) — both caught by the new teammate fixture tests;handleToggleViewModerouted through the list-source seam; classification catches report viareportSwallowed; dead loading-overlay CSS removed. -
Main-thread freeze during v3 cold load fixed (the "silent loading" symptom): native frames are now split at ~512KB — a multi-MB single SSE event forced one giant synchronous
JSON.parsethat blocked paint and rAF, so neither the spinner nor the meter could render; small frames are macrotask boundaries the browser paints between. The client's window assembly is likewise chunked with main-thread yields (live entries arriving during the async window are gated into the existing pipeline buffer, drained at commit).
- SSE and streaming JSON responses are now brotli-compressed when the client offers
Accept-Encoding: br(every modern browser does):/events,/api/local-log(per-event flush — frames still arrive immediately) and/api/requests(whole-stream). Measured on a real 68MB/3830-request session: cold load 168.5MB → 8.1MB (20.8x), live channel 3.7GB → 46MB (80x) per client. Downloads and static assets are intentionally unchanged. - Implementation: new
server/lib/wire-compress.jsis the single encoding seam (brotli q9, 16MB window, flush coalesced per macrotask; whole-stream responses like/api/requestsskip per-event flush for a better ratio); ALL SSE writes — including log-watcher broadcasts, workspace reload, and the update badge — route through itssseWriteso no plaintext byte can leak into a compressed stream. Backpressure watches whichever stream applies the pressure (the encoder's input buffer on compressed paths, the socket otherwise —awaitWireDrain),br;q=0is honored as a refusal,Vary: Accept-Encodingis emitted on both negotiation outcomes, and the per-connection encoder is destroyed on close and on dead-client eviction. - Negotiation is br|identity only (no gzip tier: its 32KB window cannot dedup the repeated multi-KB tools/system blocks — measured 1.8-3x vs brotli's 20-80x). Clients that do not offer
br(curl, tests) get byte-identical plaintext, unchanged from before. - Escape hatches:
CCV_WIRE_COMPRESSION=offdisables negotiation;CCV_BROTLI_QUALITY=<1-11>overrides the quality (default 9).
1.7.0 is a major release. Its core is a breaking change of the log storage: the v1 single-file JSONL is replaced end-to-end by the v2 per-session folder format. Entries below are grouped by theme; per-commit detail lives in git history.
- What it is: every session now lives in its own folder
sessions/<yyyymmddhhmmss>_<uuid>/(journal metadata lines + incremental conversation events + responses + content-addressed blobs), replacing the single per-project.jsonlfile. v2 is the ONLY write format; every v1 write path is retired. - Why it pays off:
- Size: content-addressed dedup of repeated tools/system payloads + incremental conversation events shrink a migrated archive by ~80% in real-data runs.
- Memory/stability: reads are memory-bounded end-to-end (two-pass windowed synthesis + streaming) — loading a big session drops the server heap from ~1.5GB to ~0.2GB, eliminating the OOM crashes.
- Per-session capabilities: soft-delete (sessions move to a recycle folder, restorable — nothing is ever physically unlinked), per-session migration verify (a bad session is quarantined into
sessions-quarantine/instead of sinking the whole batch), lossless per-session ZIP download/upload, and timestamp-prefixed folder names that sort chronologically right in the file system. - Liveness: the live channel pushes incremental journal/conversation appends; cold load falls back to the newest renderable session, so startup/refresh no longer shows a blank panel.
- How to upgrade: on the first launch after upgrading, a migration prompt appears whenever the project still has unmigrated v1 logs — click "Migrate now". You can also trigger it from Log Management → "View legacy (v1) logs", or via the CLI:
ccv convert <project>/ccv convert --all. Conversion and verification show live progress; the task is resident server-side and resumes after interruption. v1 files are never deleted automatically — they stay on disk after migration, so the run can always be repeated. A-ccontinuation whose earlier half still lives in v1 re-prompts for migration. - Removed together with the storage switch (also breaking): the multi-instance concept (
--pid/CCV_INSTANCE_ID), the Merge Logs / Archive Logs features (including.jsonl.zipread support), the "only show current session" preference (now the only live-view mode), and the three experimental v2 switches in the logs modal. Session size is now the recursive folder size (the old journal-only figure undercounted ~12x).
- One-click v1→v2 migration with three entry points — logs-modal button, startup prompt,
ccv convertCLI — resumable across restarts. - Golden verify is per-session and non-blocking (one bad session no longer aborts the batch); the verify phase shows live progress "verifying data (x/n) · m entries scanned…" in all 18 languages.
- Logs modal gains a v2/v1 dual view: opens on the v2 session list; while legacy v1 files remain on disk, a "View legacy (v1) logs" link offers list/view/download/migrate/soft-delete.
ccv -cno longer mints a blank new session: a continuation launch adopts the previous main session's folder (Claude CLI 2.1.210 hands a fresh session_id on every -c);--fork-sessionand explicit-rintentionally keep their own session.- Cold-load activation gate (requires a COMPLETED main turn) + fallback picker excluding the in-flight current session remove the residual startup/refresh blank flash; restart-continuation epoch regression and live event-ordering false alarms fixed.
- Log-list overview is now the session's full user-prompt set, statically cached per session (
prompts.jsonlside file, including the suggestion-probe replace-tail case). - IM status chips poll only while the platform is configured: a never-configured platform sends one probe then goes silent (fixes the constant 4-platforms × 5s request stream on fresh installs); transient probe failures keep polling (self-heal); in-app config changes re-arm via an event.
- README refreshed in all languages;
docs/WIRE_FORMAT.mdmarked superseded by the v2 spec.
Condensed summary of versions 1.6.301 ~ 1.6.348 (2026-06-06 ~ 2026-07-13). Full per-version detail lives in git history.
- Model-entry injection matches the ACTIVE configuration (
spawn-model-resolver.js) instead of past usage; tiered boot fallback + four guard layers so no system-prompt-pipeline failure can crash or block a spawn;${...}template variables actually rendered at spawn; skipped-injection no longer logs a false diagnostic. - Test-isolation data barriers L1c/L1d in findcc.js: under a test context an explicit
CCV_LOG_DIR/CLAUDE_CONFIG_DIRis honored only when it points at a safe location — unit tests can no longer touch real user data. - IM: conversation-record drawer gains an inline Start button for dead workers (+ six-role review hardening); bridge status no longer shows a stale "Connected" after network loss.
- Chat: MainAgent identity no longer flashes to the generic avatar at a session boundary with carried-over history; mid-conversation system rows relabeled "Append System Prompt" (18 locales); concept docs added for Artifact/DesignSync/RemoteTrigger; search-highlight recolored to the inline-code scheme.
1.6.341 ~ 1.6.344 (2026-07-08 ~ 07-10) — search/replace across files, Edit System Prompt maturation, CLI 2.1.201 wire shapes
- Search across files (VS Code-style activity-bar view) and Replace across files with inline before/after previews; the replace batch yields to the event loop every 64 files; multi-role review hardening.
- Edit System Prompt: moved into the hamburger menu, shared blurred mask, dictionary presets wired in (builder relocated to
server/lib/create_system_prompt.js), a fifth kimi-k2.7-code global preset, per-model preset differentiation, parameter-docs popup localized into all 18 locales. - Adapted to Claude Code CLI 2.1.201 wire shapes (
mid-conversation-systembeta) that broke post-plan-approval rendering; AgentTeam/UltraPlan enabled by default at launch;readClaudeProjectModelnormalizes cwd (realpath/symlink/trailing-slash) before matching~/.claude.json.
1.6.336 ~ 1.6.340 (2026-07-04 ~ 07-06) — teammate avatars, ChatView decomposition, error-reporting convention
- All 17 teammate role avatars redesigned as colored historical-figure bust portraits with one-shot draw-in animation (+ a Marvel alternate set); animation loads only for recent rows on refresh.
- Swallowed-catch reporting convention established (
reportSwallowed(tag, err), greppable[ccv:<tag>], per-tag dedup cap); all 15!importantdeclarations removed from FileContentView; 12+ hardcoded UI strings localized (18 locales); four phantom devDependencies declared. - ChatView decomposition first tranche (4,144 → 3,699 lines, behavior-preserving, unit-tested) + the crash it briefly introduced fixed; log-rotation no longer loses teammates from the Conversation view; identity fallbacks self-heal after refresh; AskUserQuestion eternal-empty and hollow-popup cases fixed; UltraPlan rainbow shimmer on toolbar/chat-input buttons; unified
--font-mono/--font-uiapp-wide.
1.6.331 ~ 1.6.335 (2026-07-02 ~ 07-04) — model-specific system prompts, fast/CLI test tiers, session anchoring
- Model-specific System Prompts: tabbed Edit System Prompt modal, per-model
<NAME>_SYSTEM.md/<NAME>_APPEND_SYSTEM.mdin global + workspace scopes with precedence,GET/POST /api/expert/model-prompts; six hamburger feature modals get the blurred overlay mask. - Test tiers: default
npm run testbecomes the fast in-process unit tier (~20-38s, was ~198s); 24 CLI/server-integration files gated behindCCV_TEST_CLI=1; L7 guard blocks real claude-binary discovery under tests. - "Only current session" anchoring fixed (newest-activity pin, shared
isSessionBoundarypredicate across batch and live, no stale upper bound); AskUserQuestion streaming-assembly blank popup fixed; animated proxy-pipeline SVG embedded in all READMEs.
1.6.318 ~ 1.6.330 (2026-06-18 ~ 06-30) — IM deep work (AI cards, skills, personas), system-text editing debut, multi-instance isolation, terminal UX
- IM AI-card token streaming on DingTalk (flowStatus labels), Feishu (CardKit) and WeCom, behind an opt-in
aiCardswitch; built-inmanage-ccv-projectsskill injected per worker; persona preset per UI language (server/imPreset/<lang>.md); persona file moved fromCLAUDE.mdtoCC_APPEND_SYSTEM.mdinjected via--append-system-prompt-file(harder to bypass, auto-migrated). - System-text editing debut (Expert settings): write the workspace's
CC_SYSTEM.md(override) /CC_APPEND_SYSTEM.md(append), auto-injected as claude flags at launch; later widened with markdown preview. - Multi-instance isolation (
--pid, per-instance logs/session pin/instance registry — later removed wholesale in 1.7.0) and per-project preference forks for shared-server (LAN) use. - Relative-path dist build (one artifact serves root and
CCV_BASE_PATHsub-path reverse proxies); terminal Ctrl+C/V copy-paste on Win/Linux; in-band reset preserves scrollback;cc_is_subagentmain-agent exclusion; skill permanent-delete + duplicate badges; Context tab tools-diff highlighting; multi-source IP geo fallback; scrollback restored viaCLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1; teammate thinking no longer leaks into the main live overlay.
1.6.301 ~ 1.6.317 (2026-06-06 ~ 06-18) — Windows root-cause batch, terminal-garble eradication, Workflow visualization, context-bar alignment
- Windows batch: startup error 193 (.exe-only resolution), ConPTY output-flood coalescer (~1.9MB/s cap, DEC 2026-balanced), /plugins permanent page freeze root-caused to catastrophic regex backtracking (rewritten as a linear line parser), Ctrl+C three-layer exit defense, CJK IME offset, CI pinned/upgraded around the VS2026 image roll.
- Terminal garble eradicated in three passes: anchor-scan safe slicing (
ansi-safe-slice.js) shared by all three write paths, batch-boundary carry of half sequences, in-band reset replacingterminal.reset(), truncation-snapshot realign — plus an end-to-end pipeline oracle test (nine scenarios, zero-fragment invariant); WebGL renderer re-enabled on macOS desktop behind a longtask capability gate. - Workflow/UltraCode visualization suite: inline chat panel (phases + per-agent rows), list/Gantt timeline toggle, always-on live HUD above the input, activity-bar Workflow area, read-only
/api/workflow-journal, phase column parsed from scriptmeta.phases. - Context bar aligned with Claude Code
/contextraw-occupancy math (dropped the ÷0.835 mapping) and recognizes fable-5 (1M window); Context tab raw-JSON view; four-pointed-star quick-settings menus (permission/plan auto-approve, AgentTeam); sticky-bottom scroll opt-out during streaming;_seq/_seqEpochfour-layer defense against duplicated mainAgent renders; IM record popup updates live via fs.watch; image-upload in-flight guard; usage pill no longer stuck off; base-path sub-path routing fixed.
以下为 1.6.251 ~ 1.6.300 所有版本的功能/修复摘要,详细变更记录已归档至 git 历史。 Below is a condensed summary of versions 1.6.251 ~ 1.6.300. Full per-version detail lives in git history.
- Windows 性能根治批(1.6.296):interceptor 热路径 appendFileSync 改异步写入队列(消除每请求 50-300ms 事件循环阻塞);Atomics.wait 阻塞锁改异步文件锁;UV_THREADPOOL_SIZE=16;日志监听 watchFile 500ms 轮询迁移 fs.watch 事件驱动(同目录共享 watcher + 80ms 防抖 + 5s 安全网);日志读取路径全面 fs.promises 异步化;JSONL 分割阈值 300MB→150MB;CCV_SYNC_WRITES=1 回退开关
- 测试覆盖率 72%→96%(1.6.299):新增/补强约 100 个测试文件(server 路由 / interceptor / sdk-manager / cli / WS/PTY / src/utils 全量);test/_shims ESM loader 让 node:test 直接导入 Vite 风格前端模块;根治并行测试 flake(跨进程端口窗隔离、固定 sleep 改条件轮询);test 脚本统一 --test-timeout=120000 + --test-force-exit
- 测试隔离守卫体系(1.6.300):findcc.js LOG_DIR/configDir 双铁闸(测试态强制进程私有临时目录)、测试态拒绝真实 spawn IM worker / registry 请求,单测从机制上无法触碰真实 ~/.claude 与外网;新增静态扫描守卫(env 隔离 / spawn 注入 / fetch 还原纪律);24 个测试文件端口隔离改造;50+ branch-*.test.js 定向补强,全量 6402 测试 0 失败
- 启动期配置备份(1.6.300):preferences/profile/workspaces 每次启动自动备份到数据目录外(滚动保留 10 份)
- 移动端历史日志尾部优先加载(1.6.298):服务端 readTailEntries() 仅读文件末尾 2-8MB 跳过全文件扫描,旧条目按需分页
- 终端渲染稳定性(1.6.298):PTY 输出批次启用 DEC 2026 同步渲染消除中间帧闪烁;WebGL longtask 自动降级 DOM 渲染器(30s 内 3 次 >200ms,7 天后重试)
- Windows 冷启动 Chrome tab 崩溃修复(1.6.298-299):桌面端首屏 SSE 1000→400 条 + idle 超时 2s→5s + requestIdleCallback 延迟连接 + 重连指数退避(2s→32s)+ ?since= 增量重连
- IM 接入重构为「每平台一个独立常驻 ccv worker 进程」(1.6.293):工作目录 ~/.claude/cc-viewer/IM_/、绑 127.0.0.1、skip-permissions 全自动运行,主 ccv 不再注入当前会话(消除排队/上下文污染);reconcile 自动拉起、im.lock 防多处接入、PreToolUse 在 bypass 前硬拦截危险操作;端口段 7050 起与主池不重叠
- IM 多平台桥接(1.6.293):钉钉桥接重构为通用编排核心 im-bridge-core + 平台适配器,新增飞书/Lark(长连接)、企业微信(智能机器人)、Discord(Gateway)三平台;描述符驱动设置面板
- IM 体验完善(1.6.294-296):即时确认(飞书/Discord 可更新卡片)、排队位置告知、turn_end 去抖 10s→200ms + Stop hook/idle 轮询双保险;对话记录按发送者显示真实姓名+头像;每 IM 独立 CLAUDE.md「模型性格定义」编辑与 SKILL 管理;配置失焦自动保存 + 启动/停止按钮;连接状态徽标以真实进程状态为准
- Windows 桌面版体验批(1.6.294):自定义标题栏(logo/菜单/tabs 合一行,titleBarOverlay 保留原生按钮与 Snap Layouts);启动白屏修复、窗口状态持久化、右键菜单补齐、AppUserModelId 对齐;主进程防阻塞加固(diag 日志异步队列、打包版 console 静默、审批级联去抖);child_process 全量 windowsHide
- Web 桌面端长任务页面卡死根治(1.6.294):桌面端复用移动端渲染窗口裁剪(默认只渲染最近 400 条,更早按需展开)——原先全量渲染 DOM 致 reconcile/layout 随条目数线性增长打满主线程;SSE backpressure 容忍 5s→30s 消除误判剔除→重连重放风暴
- ConPTY 输出洪泛防护(1.6.294):前端写队列积压超 2MB 丢最旧并提示;服务端按 ws.bufferedAmount 停发 + data-resync 快照对齐 + 60s 死连接判定
- 「Plan 自动审批」首发(1.6.294):偏好新增下拉(关/3s/5s/10s/立即),CLI(PTY) 模式下计划倒计时自动批准、可取消转手动;原「自动审批」更名「权限自动审批」
- UI 杂项(1.6.293):汉堡菜单「钉住」生成常驻快捷方式;主题切换简化为太阳/月亮图标按钮;用户 Prompt 导航加时间列与 Session 分隔线;「仅展示当前会话」偏好;源码裸控制字节转义 + pretest 守卫;Electron iPad 设备预览模式(500px 收窄 + header 控件迁原生 tab bar)
- 「显示大小」三步演进:CSS zoom 预设选择器 50%-200% + Cmd/Ctrl +/- 快捷键(1.6.285)→ 根容器高度链修复留缝(1.6.287)→ 弃用 CSS zoom 改 Electron 原生 webFrame.setZoomFactor、纯浏览器交还原生缩放(1.6.288,规避 Chromium 128 CSS zoom 坐标空间分裂)
- 套餐用量 pill(1.6.283):footer 新增 Claude 订阅(OAuth)5 小时/周窗口使用率展示,数据取自 anthropic-ratelimit-unified-* 响应头,纯前端实现;hover 详情血条化;低调灰收敛配色
- 代理转发两项根治(1.6.283):Node 26 起 global dispatcher 不再共享——EnvHttpProxyAgent 显式作为 fetch dispatcher 传入;上游强制 accept-encoding: identity 根治网关剥 content-encoding 头致 CLI 收到压缩字节报 malformed response
- SDK 停止语义修复(1.6.286):「停止」真正 halt 在途待发(清服务端 _messageQueue + 客户端 _pendingFlushQueue);停止按钮按模式分流(SDK close query 保会话 / PTY focus-in 再 ESC 修失焦吞 ESC);点击乐观即时切非运行态
- 流式吸底帧率节流(1.6.286):StickyBottomController 平滑追随按 33ms(~30fps)门控与显示器刷新率解耦,流式期 layout/paint 负载降 ~4×;移动端 Virtuoso followOutput smooth→auto 消除每帧平滑滚动动画(1.6.283)
- UltraPlan 专家体系(1.6.283):「管理专家」弹窗统一管理内置+自定义(显隐开关/拖拽排序/落 preferences);「预设专家」ultraAgents/*.json 随包发布 + 编辑器「载入模版」;title/description JSON 协议层内联本地化
- 杂项:同请求多 thinking 块合并单折叠框(1.6.284);LSP/Workflow 概念帮助文档 18 语言(1.6.283);用户消息裸路径上传图片渲染(1.6.283);opus-4 家族/mythons 1M 上下文判定 + 血条自适应纠偏(200K 判定但输入越窗自动升 1M)(1.6.282);IM 弹窗 Chrome tab 风格(1.6.282);默认登录密码规则简化 AB1234 形态(1.6.282)
- 密码登录认证体系(1.6.276):远程未授权访问弹密码页(Set-Cookie SameSite=Strict),本机永远免密 admin;全局 + 项目级覆盖两层持久化(base64 混淆、0600、/api/preferences 剥离);CLI --usePassword 启动即开启;鉴权收敛纯函数 decideAuth() HTTP/WS 共用(补上 WS 缺失鉴权);登录限流 60s/20 次
- 钉钉 Stream 双向桥接首发(1.6.277):仅填 AppKey/AppSecret 无需公网;消息注入会话 + turn_end 读 transcript 回干净 markdown(分块+令牌桶限流);/stop 中断、ack+LRU 去重、忙时排队、staffId 白名单、审计日志;多角色 review 修复批(状态脱敏、CR 消毒、队列封顶、语言跟随 UI 配置)
- server.js 路由拆分(1.6.275):5467→1791 行,84 路由 if-chain 按功能域拆 14 个 server/routes/* 模块 + 有序首匹配 dispatcher,deps 单例注入,纯搬移零行为变更
- components 目录重组(1.6.274):扁平目录按功能域分 10 个子目录(chat/terminal/git/files/viewers/approval/settings/mobile/dashboard/common);AppHeader.module.css 1507 行按归属拆分;五个对话展示偏好单一真相源收口 SettingsContext;askFlowController 从 ChatView 抽离(4777→3990 行)
- 终端「刷新」按钮三步演进:高度抖动驱动 xterm 重建(1.6.277)→ 连击逐级加强 L1/L2/L3 + 60s 后台预防性触发(1.6.280)→ 改 xterm 官方 escape hatch(clearTextureAtlas/fit/WebglAddon reload,保 cols/rows/滚动位置)(1.6.281)
- 审批档位简化 +「免审批」(1.6.278):删 15/20/30/60s 保留 3/5/10;新增免审批(AUTO_APPROVE_INSTANT=-1)在请求到达处直接放行(hook 回 allow / pty 直选允许),含 prompt 签名时窗去重与 ws 断连回落
- 本机明文查看凭据(1.6.278):127.0.0.1 admin 可查看代理 API Key / 钉钉 AppSecret 明文,远程仍脱敏;IM 来源消息显示平台图标(⟦im:dingtalk⟧ 标记)
- UltraPlan 自定义专家编辑器(1.6.279-280):双栏布局(左使用文档右表单)、预填 system-reminder 外壳骨架、左栏可折叠、打开编辑器不再关闭背后面板(rc-trigger capture-mousedown 守卫)
- 杂项:Homebrew bump 改 reusable workflow 链式调用修漏 bump(1.6.279);「new」版本徽标 SSE 补推跨刷新持久(1.6.277);代理 content-length 剥离修模型覆盖 profile 卡死(1.6.277);血条 Popover 内打开明细 Modal 不再消失(1.6.277);项目中心密码管理模型(1.6.277);per-project 别名(1.6.272)
- ask 体系大修(1.6.263/270):GUI AskUserQuestion 实质无超时与 TUI 对齐(先 60min+倒计时,后彻底无超时);ask-store 持久化 + server 重启恢复 UI;ask-bridge 短轮询协议(POST 返 askId + GET 25s wait + 404 重建);web 端取消 + 输入框打字打断(SDK sentinel deny / Hook cancelled 分支);十余项 race/边界修复(first-write-wins、单 lock consume、pruneStale、晚到 ack 关 modal 等);注入 hook 加 24h timeout 防 Claude Code 10min 强制中断
- 语音包体系(1.6.264/268/271):4 类生命周期事件绑定音频(plan 审批/ask/超时预警/turn 结束),内置 Pixel Buddy chiptune + 用户上传(magic bytes/Range 206/深合并 reconcile);新增三国 sanguo 内置包(zh 新用户默认);turn-end Stop hook + 10s trailing debounce 双保险;「审批提示音」与「语音包」合并单开关
- 服务端代码收纳 server/(1.6.273):根目录只留 cli.js/findcc.js + 一行 re-export shim;注入改 bare specifier(cc-viewer/interceptor.js)与物理路径解耦 + LEGACY 升级路径;stale hooks 主动 purge;--uninstall 清理 managed hooks;node-pty pnpm hoist 修复;_paths.js 集中路径常量;ask-store 跨进程锁加 PID 校验(1.6.273)
- Windows 适配批 1/2(1.6.257-258,Issue #84):路径校验 + '/' 改 + sep、startsWith('/')→isAbsolute、CRLF split、保留名守卫、symlink 拒绝防 TOCTOU、renameSyncWithRetry、git restore per-file mutex、上传目录平台分支
- 日志压缩归档(1.6.267):.jsonl 批量压 .jsonl.zip,查看/下载/合并/统计透明支持(tmpdir 解压缓存 + Zip Slip 防护 + 启动清理);合并上限前后端统一 400MB
- 血条 auto 校准(1.6.273):启动期回落 ~/.claude.json lastModelUsage 推断偏好 model,解决 haiku init ping 错显 200K;/clear lock 多条解锁兜底(1.6.267/269)
- 文件/Git 面板(1.6.251/255/256):GitChanges 新增「本地未推送 commit」折叠区(哨兵字符 + hex 校验防注入);自动刷新机制重写(监听 tool_result 双阶段、覆盖 subAgent/teammate);文件夹折叠/目录展开/scroll 位置按项目持久化;electron 35→42 消 17 个 high CVE(1.6.256)
- 对话渲染(1.6.262/266):web_search 结果卡片化 + 分组容器;工具结果 base64/url 图片渲染(白名单+降级);SubAgent 末轮工具结果跨请求补偿;多 text 块 synthesis 合并 markdown
- 杂项:sessionMerge 反向锚点对齐根治复制翻车残余 + WIRE_FORMAT.md 单一真理源(1.6.253);doubled-history Plan C eager update 补竞态漏检(1.6.251);zstd accept-encoding 剥离(1.6.252);移动端三 Modal 改抽屉 + 清理 !important(1.6.261);图片查看器触控板 pinch 灵敏度修复(1.6.260);zsh 不 source ~/.zshrc 修复、symlink 目录展开(1.6.269);UltraPlan modal 拖拽手柄(1.6.273);内置 slash 命令本地化标签、雪山白主题深底用户气泡(1.6.270);侧栏 popover 视口贴顶(1.6.254)
以下为 1.6.200 ~ 1.6.250 所有版本的功能/修复摘要,详细变更记录已归档至 git 历史。 Below is a condensed summary of versions 1.6.200 ~ 1.6.250. Full per-version detail lives in git history.
1.6.240 ~ 1.6.250 (2026-05-05 ~ 2026-05-12) — Doubled-history 根治、Sticky Bottom 重构、Assistant 时间戳精准化、AskUserQuestion 多路复用、Mobile/iPad 全面对齐
- Doubled-history 根治三层防御:
- 上游(Plan C,interceptor.js)
_lastTailFp+_sameLenInPlaceReplace强制 checkpoint,根治 in-place last-msg replace 时 delta=[]'丢失"末位换内容"信息 - 客户端
applyInPlaceLastMsgReplace(prevSessions, entry, ...)helper:命中_inPlaceReplaceDetected:true信号时构造新 lastSession(前 N-1 引用复用 + 末位用新 entry 末位),applied=true 跳过 sessionMerge revert老 Layer 2 客户端短路(既无收益又有副作用,整段拆掉)
- 上游(Plan C,interceptor.js)
- 流式吸底 StickyBottomController 抽离:~330 行独立 utility 收敛全部 sticky 状态、引用计数 lock、双 rAF 缓动、ResizeObserver、scrollTop 写入;修 4 个设计缺陷 + 2 个 P0(NaN 死循环 + Set 而非 Array 防 O(n²) 内存泄漏);36 case 单测 + 集成测覆盖;ChatView -149 行净减
- Assistant 时间戳精准化:
_generatedTs新字段表"消息生成时刻"(保留_timestamp作 carrier ts),让 bubble 显示时间不再晚一拍;同时修双向映射 msg↔request(resolveBubbleProducerTshelper + tsItemMap + 蓝框 highlight 三点对齐);slimmed-iter 漏赋补丁 - AskUserQuestion 系列:
- 多并发改 Map 多路复用(pendingAskHooks Map + 50 cap + 5min/entry timer + id 寻址 ws answer),抄齐 perm-hook 形态
- schema 校验失败渲染层红色徽章
❌ 此提问被 CLI 拒绝+<details>展开原始<tool_use_error>InputValidationError…;提交失败 toast 升级为 antdModal.warning含技术码 options[].description可选化 + 抽askOptionDesc.jshelper 收 5 处内联
- PC 端血条迁出 AppHeader:终端开启时挂在 TerminalPanel 工具栏中段,终端关闭时挂在 ChatInputBar 底部;popover placement bottomLeft→topRight;ReactDOM.createPortal + slot ref 实现 DOM 外移 + 数据所有权留 AppHeader
- 校准下拉简化:7 个具体型号 →
auto/1M/200K三档;AUTO 按 lastMainAgent 自检测(含1m子串 +opus-4-7大小写不敏感);老用户 localStorage 显式迁移 - antd themeConfig 改 Object.freeze 常量:消除每次 render 字面量重建导致的 useToken 全局 cache miss;Performance trace 实测 antd R 函数 5344ms→260ms (−95%)、GC 6242ms→703ms (−89%)、长任务清零,根治"长时间卡死 + 滚动掉帧"
- Tool result 内存优化:通用化 Read intern pool 默认覆盖所有 tool_result(Bash/Grep/MCP/Task);raw payload
tool_resultcontent 也走 readResultPool 共享;实测 hitRate 97.6% / 估算回收 36-92MB raw payload 重复 - Skill import 上传:
/api/skills/import接口 + Mobile cache popover 抽屉「添加/管理」入口;前端三入口(文件夹 / .zip / SKILL.md),多层安全(Zip Slip / Symlink / Zip Bomb 双层 / Unicode NFKC / TOCTOU 修复) - CLAUDE.md 入口分区:cache popover "持久记忆"上方新增 CLAUDE.md 候选清单;
/api/claude-md路径白名单 + sha1 id + 512KB cap + isReadAllowed fd-based read(闭 TOCTOU) - 持久记忆 popover "刷新"按钮:主动拉
/api/project-memory+message.success/error反馈;seq 防 stale + 连点守卫 + workspace 切换复位 - macOS 粘贴图片上传 403 修复(PR #81):darwin 平台
/private/tmp/cc-viewer-uploads显式加 allowlist + mkdir 后 bumpWorkspacesVersion 刷 root 缓存 - 移动端偏好与 PC 对齐:「仅窗口失焦时通知」/「日志设置」分组 / 18 语言语言选择器;ProcessModal kill 确认 / ProxyModal 删除确认 / PluginModal 子 modal 全部受控 Modal 替代
Modal.confirm - Modal 抽独立组件:插件管理 / CCV 进程管理 / 代理热切换 →
{Plugin,Process,Proxy}Modal.jsxPC+mobile 共用,AppHeader.jsx 净减 ~390 行 - 流式 spinner Claude 官方 SVG:8 个 sprite-sheet(thinking/waiting/tickle/orbiting/writing/shimmer/entrance/exit)用 SMIL
<animate calcMode="discrete">逐帧滚;ImageViewer 同步支持 SMIL 动画 SVG(svgSanitize 集中 hook 防 event handler 注入) - AskUserQuestion 渲染层兜底:
isInputValidationError标记 + 抽AskValidationBadge.jsx;ask-bridge //api/ask-hook双端 normalize options[].description 缺失补 "" - AppHeader 入口压缩:抽
CachePopoverContent.jsx+MemoryDetailModal.jsx给 AppHeader 与 Mobile 共用;删 ~290 行模板代码 - 多视角 5-agent review 体系成熟:每个非平凡改动配 3-5 reviewer(需求/防御/架构/质量/性能-安全),P0 强制采纳 + P1 选择性采纳;累计本区间触发 10+ 轮 UltraReview
- 测试覆盖:1568 → 1739 用例累计;新增
applyInPlaceLastMsgReplace、sticky-bottom-controller、refresh-plan-approval-cache、memoryLinkParser、skills-import、svg-sanitize、api-claude-md、server-ask-hook-map等多个独立单测文件
1.6.220 ~ 1.6.239 (2026-05-01 ~ 2026-05-05) — 全局审批 Modal、ExitPlanMode V2、Homebrew 分发、sessionMerge 内容感知、Scratch 终端、MdxEditor、Terminal 性能
- Scratch 终端上线(1.6.219-220):多 tab + PTY 隔离 + 拖拽复序 + focus border 结构化预留;主/小 terminal 双双引入
.terminalHost/.scratchHost包装层根治 xterm 渲染溢出 - Electron tab 栏:60px 圆角矩形重设计 + 多 tab 打包黑屏修复 + TerminalPanel 高度溢出根治
- MdxEditor 体系成熟:
- 文件浏览器 markdown 改用 MDXEditor (GUI WYSIWYG)(1.6.213)
- 解析失败自动降级到旧 marked + popupContainer 10px strip 修复(1.6.217)
- light 白底 + 保存按钮高亮(1.6.214)
- inline code 去背景 / 内边距(1.6.216)
- 工具栏 tooltip 18 语全译(之前仅中文,~375 条新翻译;
<MDXEditor key={lang}>强制重挂载实现切语言即时生效)(1.6.230) - Ctrl+S/Cmd+S 保存快捷键(1.6.230)
- sessionMerge 内容感知合并(1.6.223):根治 Plan Mode 上下文压缩窗口下 ExitPlanMode plan 内容丢失;新算法以
newMessages[0]为锚点从末尾反向扫 + 多块连续 fp 等价校验(text/thinking 加固为length + first32 + last32),三分支收口到单一findReverseAnchor主路径;流式热路径零分配 - 全局审批 Modal + ExitPlanMode V2(1.6.224):文件型 plan + LR/messages 双卡去重 + 5 视角 Code Review 后采纳 P0/P1(bell 持久重开 / plan-file LAN token / null-byte 防御 / ownPending 信息流);适配 Claude Code 2.x ExitPlanMode V2 文件型 plan
- 卡片审批状态修复(1.6.229):ExitPlanMode/AskUserQuestion 答完不切状态——
_sessionItemCache失效仅看 msgsLen 导致 cached React Element 持有过期 prop;抽refreshPlanApprovalCache.js/refreshAskAnswerCache.jshelper 走 cloneElement zero-alloc 快路径 - Homebrew 分发渠道(1.6.225):tap repo
weiesky/homebrew-cc-viewer+ 自动bump-homebrew.ymlworkflow 开 PR;updater 检测 brew 安装自动跳过 npm 自更新;根治 nvm 用户切 Node 版本后 ccv "消失"问题 - Assistant 消息时间戳旁显示 [X.XK] 上下文 token 总量(1.6.222)+ UltraReview P1/P2 采纳
- AskUserQuestion 双行选项卡片(1.6.231):从单行
dot + label — desc重排为双行dot + flex-column(label, desc);1.8× 大图标 + preview 自适应(≤750px column-reverse)+ aria-radio role 键盘可达 + "Other" 输入框去蓝 - iPad 全局审批 Modal 接通(1.6.231):Mobile.jsx 包
<ApprovalModal>(仅isPad &&启用),ChatView 接onPendingAsk/onPendingPtyPlan/ownTabId/projectName4 个之前缺失的 prop;mobile inline 卡片路径零回归 - lastPendingPlanId 算法重写(1.6.228):从扫全量 messages 改为反向扫到最后一条非空 assistant message 只在该 message 内查 ExitPlanMode/AskUserQuestion;根治历史 plan/ask 永远 pending 误弹 modal 的 bug
- LAN 移动端 403 修复 + DNS rebinding 守护(1.6.227-228):默认 allowlist 改
[loopback] ∪ getAllLocalIps(),与"手机扫码编程"核心场景兼容;CCV_ALLOWED_HOSTS 显式设时完全沿用用户值 - QR Popover 一点就关修复(1.6.228):trigger
['hover','focus']→['click']+ 受控 open state + 内部 stopPropagation,移动端 tap 扫码稳定可用 - 单 ws 合并方案 D(1.6.226-227):两条
/ws/terminal长连接合并为单条;input-sequential-done跨发送方 race 修复;ask 提交回归(B' ChatView 重新接收data类型)修复 - 偏好开关 + IPC(1.6.226):偏好面板 3 个 Switch 点击无响应修复;「仅窗口失焦时通知」偏好接通 electron 通知逻辑(原 P2 未实现项)
- 统一文件访问策略(1.6.227):放开项目外文件读取 + DNS rebinding 守护双件
- Terminal 写缓冲 O(n²) 修复(1.6.232):抽
terminalWriteQueue.js(string[] queue + offset 指针 + 周期压缩)替代原slice(CHUNK_SIZE);UTF-16 surrogate pair 边界回退 + try/catch 回滚 + drain dispose;trace 实测_flushWrite794ms→<100ms、主线程 idle 从 0.5%→16% - cssVar 回退(1.6.232):实测对比 trace 显示 antd cssVar:true 是性能负优化——cssinjs +170%、flattenToken +1426%、GC +56%、dropped frames +64%;getter 注释明确警告未来不要再开
- 热路径 Tooltip 原生化(1.6.232):3 处高频列表(TeamSessionPanel gantt 钻石 / RequestList cache-loss dot)从 antd Tooltip 改原生
<span title="...">;冷路径保留 - 持久记忆 popover(1.6.232):解析
~/.claude/projects/<encoded>/memory/MEMORY.md入口 + 链接打开明细;/api/project-memory路径白名单 + 512KB cap + 跨 scheme 严格白名单(仅#anchor+ 单段.mdbasename) - header 血条 popover 抽组件 + 三端接通(1.6.233):抽
CachePopoverContent.jsx;iPad 走 click trigger Popover、手机走mobileCachePanelOverlayCSS 抽屉(transform: translateX(-100%)→0+ zoom 0.6);chip tooltip 在手机改全屏 Modal 解决 zoom:0.6 偏移 - memoryLinkParser 白名单(1.6.233):discriminated union
{open|allow|reject}+ 白名单(任何 scheme 一律 reject,仅放行#anchor与单段.mdbasename),44 case 单测 - load more history 兜底(1.6.231):
_oldestTs防御 guard 防before=null拼出 400 请求;失败 toast 反馈 + 18 语言 i18n - UltraPlan 模板瘦身(1.6.233):codeExpert 4 项瘦身(删二次 AskUserQuestion / 量化"spawn 2-3 review agents" / P0+P1 选择性采纳 / git diff 判空 /
git rev-parse --show-toplevel);researchExpert 加 AskUserQuestion Pre-requisite;同步 18 个concepts/<lang>/UltraPlan.md
1.6.200 ~ 1.6.219 (2026-04-23 ~ 2026-04-28) — Scratch 终端基建、Per-message 模型头像、Windows ESM 全适配、MDXEditor 引入、Proxy Profile 隔离、deepseek 1M、history 归档
- Scratch 终端首版(1.6.219):独立多 tab + PTY 隔离 + 拖拽复序 + focus border 结构化预留(为 1.6.220+ 完善铺路)
- 工具栏快捷按钮 paste 块(1.6.218):紧贴
\r拆分修复(窗口失焦也能立即提交,避免 tab 切换后 paste 残留) - MdxEditor 引入文件浏览器(1.6.213):
.md文件 GUI WYSIWYG 编辑替代之前的纯 markdown 预览;保留 Code 模式切换;移动端 zoom:0.6 适配 - MdxEditor 解析失败自动降级(1.6.217):marked fallback 兜底 + Force GUI Edit 锁解除 + 1-frame 红横幅闪烁抑制
- 代码浏览器字体收敛 12px(1.6.216)+ AskUserQuestion "Other" Enter 提交修复 + MdxEditor inline code 去背景内边距
- History 1.6.0 ~ 1.6.199 压缩归档(1.6.216):旧版本详细变更归档至 git 历史,本文件保留 1.6.200+ 详情 + 1.6.0
1.6.199 5 个时间段摘要分组(180199 / 160179 / 130159 / 100129 / 5099 / 1~49) - README 多语言重构(1.6.216):17 个 docs/README.*.md(zh/zh-TW/en/ko/ja/de/es/fr/it/da/pl/ru/ar/no/pt-BR/th/tr/uk)结构对齐主 README,截图统一
- /clear 触发 Header 血条乐观重置(1.6.214):触发 /clear 后 Header 上下文血条立即乐观重置到低位
- /clear 后首条 user 输入错位修复(1.6.212):ChatView 中错位渲染问题修复 + 数据统计入口从 Header 顶部 Tag 迁移到左侧 navSidebar
- Per-message 模型头像 1v1 严格匹配(1.6.211):消除历史消息被最新 model 污染——
getModelInfoMap memo +modelNameByReqIdxcarry-over +resolveModelInfo(ts)闭包,多模型会话头像精准 - 模型名解析改 response 优先(1.6.210):避免请求阶段 model 字段不准;新增 deepseek-v4 1M 上下文识别
- KV-Cache-Text 复制路径(1.6.209):用 on-model XML 形态 + formatter 抽到 lib/;SubAgent KV-Cache-Text 复用同一路径
- Windows 用户插件加载 ESM 修复(1.6.208):1.6.207 的 ESM 适配漏掉了用户插件路径
- Windows ESM 全量适配(1.6.207):动态
import()在 Windows 用pathToFileURL包绝对路径(防file://${path}模板拼接错误);PATH 分隔符:→ 平台感知;;多个require.resolve兼容 - PR #70 post-review hardening(1.6.206):plugin 加载路径多项防御强化
- 部署后陈旧 chunk 自愈(1.6.215):server cache + lazy reload 修复"点 .md 文件偶现 Failed to load module script / Failed to fetch dynamically imported module"
- Proxy Profile per-workspace 隔离(1.6.200):拆两层存储——
~/.claude/cc-viewer/profile.json仅 profiles 列表(全局共享,watchFile 跨进程 CRUD),<projectDir>/active-profile.json仅{activeId}独占 workspace;多 workspace 热切换互不覆盖 - AppHeader 主题切换 pill-style button(1.6.200):从 antd Switch 改为 56×30 原生 button +
role="switch"+aria-checked+ 太阳/月亮 SVG 切换;QR 码入口与 themeToggle / compactBtn 同高 30px 对齐 - CountryFlag 组件抽出(1.6.200):从 AppHeader 右侧 18px emoji 迁到 footer 左下;字号 13px +
AbortSignal.timeout(5000)防悬挂;ipinfo.io 失败隐藏 - a11y 键盘可达(1.6.200):QR + CountryFlag
<button>包裹 +:focus-visible轮廓;Popover trigger 改['hover','focus']Tab 聚焦即展开 - 热切换诊断日志去 apiKey 明文(1.6.200):
CCV_DEBUG_HOTSWITCH分支只输出authSet/xApiKeySet/matchedAuthKey/matchedXApiKeykey 名,不输出任何 key 片段(审计工具 sk- 模式不再误报) - Auth 替换纯函数抽取(1.6.200):抽
_replaceProxyAuthHeaders(内部,不 export),toLowerCase()匹配任意大小写authorization/x-api-key,两者都不存在时强制植入x-api-key - 测试覆盖:1100+ → 1568 用例累计(含
proxy-profile-isolation.test.js228 行 +proxy.test.js11 用例 +synthetic-classification.test.js9 用例)
以下为 1.6.0 ~ 1.6.199 所有版本的功能/修复摘要,详细变更记录已归档至 git 历史。 Below is a condensed summary of versions 1.6.0 ~ 1.6.199. Full per-version detail lives in git history.
- RequestList 新增
Synthetic类型识别 Claude Code 合成调用(Recap/Title/Compact/Topic/Summary 5 类白名单 +tagMuted弱化样式) - AppHeader 工具弹层接入「已载入 Skill」分组 + Skill 管理 Modal:CRUD 切换 user/project skill 启用态,4 色徽章 + 响应式 width,写入
~/.claude/skills/<project>/.claude/skills - FileExplorer 支持批量文件夹拖入保留目录结构(
webkitGetAsEntry递归 + 深度上限 32 + 1000 文件二次确认 + 并发 3 +wx独占写防 TOCTOU) - Team 会话面板状态收敛:
endReason四值 +team-runtime.jsfs 探测 +POST /api/team-status,消除永久⏱中间态 - UltraPlan:
+按钮迁出 header 改.variantRow,许愿机弹层补图片缩略点击放大 + × 二次确认 + hover 蓝框 + 22×22 触控 - 撤回
CLAUDE_CODE_NO_FLICKER=1默认注入(销毁 scrollback 副作用),保留CLAUDE_CODE_DISABLE_MOUSE=1保住文本选中 - 终端 Shift+Enter 换行改走
\x1b\r对齐 Claude Code 2.x 官方约定,配合preventDefault + stopPropagation关闭 textarea 默认 LF 路径 - 图片上传 2000px 防线修复:删除字节回退 + 去掉
RESIZABLE_TYPES白名单 + HEIC/AVIF/GIF/BMP 一律转 JPEG - xterm.js 6.0.0
requestModeTDZ 修复:vite.config.js切到terser+mangle: false(Vite 顶层 esbuild 不传 build minify 阶段) - iOS 权限面板坐标修复:用
visualViewport.height替代window.innerHeight(iOS Safari 忽略interactive-widget=resizes-content) - CustomUltraplanEditModal mobile 双 modal 堆叠修复:
zIndex={1200}+ 父 UltraPlan 自动关闭,编辑期间单 modal - 接收陈旧消息修复 + 测试增强:1024 → 1180 绿用例累计
1.6.160 ~ 1.6.179 (2026-04-15 ~ 2026-04-20) — SSE 流式打字机、claude --thinking-display 兼容、CLAUDE_CONFIG_DIR 全链路、麦克风语音、模型头像稳定
- SSE 实时打字机覆盖:MainAgent 流式 chunk 通过
/api/stream-chunkPOST → SSEstream-progress事件 → ChatView Last Response 位 inline▌cursor,rAF 合批 +React.startTransition - 流式渲染性能:增量 markdown
splitFrozenTail仅重渲尾段 +_mdCacheLRU + Vendor chunk split(vendor-codemirror/vendor-antd等 8 组),app chunk 3.2MB → 827KB - 发送按钮 spinner 主线程提升修复:拆 HTML div 显式像素尺寸 +
will-change: transform让 Blink 提升 compositor 层 claude --thinking-display反应式回滚:pty-manager.js维护_thinkingDisplayRejectedPaths: Set,crash 时按outputBuffer匹配未知 option 自动重试无 flag,替代版本号探测CLAUDE_CONFIG_DIR6 处真实运行时路径迁移(Electron theme watcher / findcc / ensure-hooks / preferences API / TerminalPanel agentTeam tooltip),新tc()i18n wrapper 注入{configDir}占位- ccv 启动 claude 默认带
--thinking-display summarized(Opus 4.7 thinking 默认关闭后兼容) - Custom UltraPlan Expert:用户自定义专家模板,CRUD +
+按钮 + 跨组件ccv-presets-changed同步 - ChatInputBar 麦克风语音输入:
webkitSpeechRecognitionBCP47 自动跟 UI 语言,IME-safe,HTTPS/secure context 检测,interimPreview绝对定位浮在 textarea 底部 - ChatView 头像稳定 3 重修复:
getModelInfoMap memo +modelNameByReqIdxcarry-over +resolveModelInfo(ts)闭包,多模型会话 per-message 头像准确 - iPad 模式响应式扩展(
?ipad=1):iOS Safari 走transform:scale非虚拟化路径绕开minimumLogicalFontSize9px 钳制 - Claude logo 流式 wave 动画 + 单色 logo 浅色主题
currentColor修复(GLM/Kimi/MiniMax) ccvClaude Code 2.x 兼容:扫描bin/claude.exe+ 平台 optional dep@anthropic-ai/claude-code-darwin-arm64,老 npm hook 自愈到 native hook- 多 repo Git 支持、iPad 拖拽上传、移动端文件浏览器三层体验补齐
- ToolApprovalPanel 锚定到输入条顶边(
position: absolute; bottom: 100%),手机端通过--chat-input-bar-heightCSS var 跟随 - 测试覆盖:964 → 998 绿用例累计
1.6.130 ~ 1.6.159 (2026-04-09 ~ 2026-04-15) — 多 Tab Electron、浅色主题、SDK 集成、自动审批、Workspace 模式、UltraPlan 体系
- Electron 多 Tab 架构:BaseWindow + WebContentsView,每 Tab 独立 fork() 子进程(proxy/server/PTY 隔离),Cmd+T/W/1-9 快捷键,常规启动/免审启动双按钮
- 浅色主题(雪山白)全套:
[data-theme]+ ~50 语义 token + 31 组件 CSS 变量化 + Antd ConfigProvider/CodeMirror/xterm 主题适配 - Agent SDK 集成:
lib/sdk-adapter.js/sdk-manager.js跑 Claude 不走 PTY,SDK plan/AskUserQuestion/canUseTool 走 WebSocket - 工具审批面板:Bash/Write/Edit/NotebookEdit 走 PreToolUse hook bridge → web UI 审批,多设备同步
*-resolved广播 + 队列+N queued徽章 - 自动审批倒计时:按模型族(Claude/OpenAI 3s、Gemini/DeepSeek/Qwen 5s、GLM/Kimi/MiniMax 10s),off/3/5/10/15/20/30/60s 可配
- Workspace 模式登录页 + Electron 多项目切换 + auto add
-c续会 - UltraPlan 体系完工:代码专家/调研专家 pill 切换,
+自定义专家,许愿机 modal/popover 双入口,文件/图片上传,<system-reminder>自动包裹 + scoped instruction 限制扩散 - Markdown 操作条:复制/导出 .md/保存为图片(html2canvas)/保存到项目,hover 触发 + 节流 + actionBar 移到气泡外右侧 column 布局
- 移动端革新:底部 hamburger 菜单 + 文件浏览器 overlay + Git Diff 全屏 + iPad pad-mode 两栏 + 上下文血条铺到手机
- Markdown action bar 收纳复制按钮进下载菜单(避免覆盖 + 132 行 i18n 新 key)
- 多 repo Git 探测(项目根 + 一级子目录)+ 图片预览 + 行数
+N -M徽章(含 untracked 文件) - 主题快切(雪山白/曜石黑)+ Claude Code
/themePTY 命令同步 + 终端自动 focus 反馈 - File Explorer 拖拽移动 + 系统拖入导入(
/api/import-file+ 自动展开 hover 500ms) - ImageLightbox:滚轮缩放 / 双击切换 fit / 拖拽 / iOS 安全区,对话/diff/markdown 多入口接入
- 自定义用户名/头像 CLI(
--user-name/--user-avatar,本地 png/jpg/gif/webp ≤2MB 或 http URL) - macOS 代码签名/公证(entitlements + notarize 脚本,超时降级为跳过保 CI 60min 内)
- Mermaid 渲染 + DOMPurify svg profile + 主题切换重渲
- 简化工具显示模式:默认折叠工具调用为紧凑 tag,Edit/Write/Agent/TaskCreate/EnterPlanMode/ExitPlanMode/AskUserQuestion 保留全展示,hover popover/click popover
- 终端 Shift+Enter 换行 + Ctrl+C 双击拦截 + bracketed paste 单块粘贴
- AskUserQuestion
PreToolUsehook bridge:/api/ask-hook长轮询 + WebSocket 路由,结构化答案绕开 PTY 模拟,超时 30s 自动恢复 - Tool 审批面板首版(Bash/Edit/Write/NotebookEdit):黄色虚线动画边框,键盘 Tab/Esc 友好,focus 自动恢复
- 移动端 SSE 增量加载:初始 200 条,按 100 条 batch 请求
/api/entries/page,session 级冷热分片(8 热 + IndexedDB 冷) - LRU cache 系列:
renderMarkdown1024 /highlight512 /renderAssistantText512,session 级增量buildAllItems - 流式 spinner / streaming border / loading pet pixel 动画
- iOS 移动版面板互换:聊天主、终端 overlay(Safari 兼容)
- 体感小修补:
mobileVirtuosoFooter 不重渲(context prop) / 超 240 条 → 0 → race /_processEntries4 pass 合并 /setStaterAF 节流(500/s → 60/s) - CSS 颜色 203 → 102(-49%):rgba/rgb/named 统一 hex,灰/蓝/红/绿/黄合并,inline style 抽到 module
- ToolApprovalPanel 进入聊天区域(
position: absolute相对messageListWrap),自动 focus Allow,Esc 拒绝 - Multi-device perm/plan/ask 广播
*-resolved+ ask-hook 跨设备同步 - 全局设置日志目录:runtime
setLogDir()+ preferences UI + GlobalSettings concept doc 18 语言 - WebFetch/WebSearch 加入
APPROVAL_TOOLS,git/npm guard 合并到 perm-bridge 消除 Bash matcher 冲突 - 终端 pending 文件 tag 条 + 多设备同步 + Enter 自动注入路径 + git checkout
??改git clean -fd - KV-Cache popover 重构 builtin/MCP 分组 + ConceptHelp 接入
- File Explorer 右键菜单 7 项(reveal/copy path/rename/delete/new file/new dir/open terminal)+ Git Changes 右键 hover actions
- ipinfo.io 国旗 + 5s timeout 失败隐藏;
/api/import-file从 OS 拖文件进项目目录
- Plan 审批 GUI(ExitPlanMode):内容预览 + Approve/Edit/Reject 按钮;危险操作(Bash/Edit/Write)琥珀色审批卡 Allow/Deny;权限拒绝红色
Denied徽章 - AskUserQuestion 多问支持:multi-select Other 通过 → + Enter 提交;isMultiQuestion 标记尾问;PTY ↑↓ delay strategy 让 inquirer 重渲
- AppBase 拆分 Mobile/PC entry:动态 import code splitting,
AppBase.jsx共享 +App.jsx/Mobile.jsx子类 - 文件浏览器:内联 rename(双击/F2)、可点击聊天文件路径跳转 + 自动展开目录树、文件/文件夹右键菜单、删除/
reveal in explorer/copy path/new file/new folder - markdown preview toggle for
.mdfiles + DOMPurify 全链路 - 多设备审批/计划/问答同步 + perm-bridge 白名单反转(只 Bash/Edit/Write/NotebookEdit 走审批)+ 32 单测
- Image Lightbox:PC 滚轮+拖拽+双击;移动端 pinch+拖拽+点击关闭;iOS safe-area
- Native teammate detection:
Agent工具子代理改名Teammate,hook context 自动提取名字 + 颜色哈希 - 流式状态 SSE 全链路(
stream-progress):聊天输入条 SVG 流光边框 + Virtuoso footer spinner + 5 层渐变 - 终端剪贴板图片粘贴 + Retina 降采样 + 多设备同步 image-upload-notify
- chat textarea image paste + 文件 chip 预览 + 延迟路径注入(send 时拼接而非贴入 textarea)
- iOS Safari 移动布局:
mobileCLIBodyflex 方向修复,键盘安全 - macOS 系统头像 fallback、文件资源管理器集成(
/api/reveal-file//api/open-terminal//api/create-dir//api/create-file//api/delete-file//api/rename-file//api/import-file) - TerminalPanel chat 镜像:
pendingImages双向同步,textarea 不污染、send 时注入 - /api/file-raw 路径穿越 + 符号链接保护(realpathSync containment)
- ChatView 增量重构:
buildToolResultMapWeakMap O(1) +buildAllItems单 pass +appendCacheLossMapappend-only + Last Response 独立 state(消除 middle-insertion reflow) _reqScanCache拆独立计数器,isTeammateWeakMap,extractTeammateNameper-request cache- Teammate 显示优化:
Teammate: name(model)格式 + 专属 team 图标 + per-name HSL 哈希着色 + 真实姓名从 SendMessagerouting.sender提取 - AskQuestionForm 抽组件 + multi-select 本地 state 隔离消除父级 re-render
ptyChunkBuilder.js纯函数生成 PTY 序列;writeToPtySequential()服务端写队列;input-sequentialWS 类型- Mermaid 图表渲染(lazy-loaded ~460KB)+ DOMPurify svg profile + 主题适配
- Proxy Hot-Switch:
fs.watchFile动态切换 API URL/Auth/Model 不重启 Claude Code,profile.json 0o600 - 大 JSONL 文件 OOM 修复:服务端不再 reconstruct delta,原始 SSE 推送,前端本地 reconstruct;分块 1MB 读
- 移动端 SSE 增量首版(
sincefilter + Map dedup)+ react-virtuoso 虚拟列表(24000 → 2000 DOM 节点) - 上下文血条:
readModelContextSize()解析[1m]后缀,watchContextWindowpolling 移除避免跨进程数据污染 - 国家国旗(ipinfo.io)+ drag-drop 文件上传
- SSE heartbeat 30s + 客户端 45s 自动重连(最多 10 次)
/api/local-log独立 SSE 流隔离 CLI mode + checkpoint 对齐分页- KV-Cache popover:仅展示
cache_control内容块、tools/system/messages 三段折叠、SubAgent KV-Cache-Text - File Explorer 内联 rename、点击文件路径跳转、自动展开目录、auto-refresh on Edit/Write 检测
- AskUserQuestion
ensureAskHookPreToolUse hook 自动注入~/.claude/settings.json,xterm Ctrl+C 双击拦截 i18n toast - TeamModal hook order violation 修复(早 return 移到 hooks 之后)
- 浅色样式诸多过渡:sticky bottom 按钮位移、虚线动画、xterm 主题, light theme palette 修补
以下为 1.6.0 之前所有版本的功能摘要,详细变更记录已归档。 Below is a condensed summary of all versions prior to 1.6.0.
- 上下文血条:「当前项目」tag 替换为 context usage 血条(绿/黄/红),statusLine wrapper 脚本捕获
used_percentage推送 SSE;getModelMaxTokens()模型上下文窗口映射;KV-Cache user prompt 点击跳转 +scrollend动画时机 (1.5.24/26/45) - AskUserQuestion 交互式:聊天面板内渲染 Radio/Checkbox + 提交按钮,支持单选/多选/Other 自定义输入/Markdown preview;已回答自动切换静态卡片;多问题串行 PTY 提交 (1.5.21/39/41/43)
- Plan approval UI:ExitPlanMode 卡片审批/拒绝/反馈按钮,内置默认选项 fallback 无需等 PTY 侦测 (1.5.37/39)
- CodeMirror 6 编辑器:FileContentView 从 highlight.js 迁移到 CodeMirror,支持编辑保存(Ctrl+S +
/api/file-content)、minimap、自定义 gutter;GitDiff 点击路径跳转对应行 (1.5.3/11/16/22) $EDITOR/$VISUAL拦截:Claude 编辑请求在 FileContentView 打开,保存关闭继续;服务端 editorSessions Map + WebSocket 广播 (1.5.14)- CCV 进程管理:列出 7008-7099 端口所有实例,PID/port/命令/启动时间展示,UI 停止闲置进程;
GET /api/ccv-processes+POST /kill带安全校验 (1.5.12) - CLI 透传改造:
ccv成为 claude drop-in 替换,参数直传;ccv -logger独立安装 hook;-v/-h/--version/--help绕过 hook;--d=--dangerously-skip-permissions;注入 Claude PID 到onNewEntry(1.5.19/23/25) - 移动端性能与体验:IndexedDB 本地缓存 + 7 天过期;消息列表分页 (末尾 240/300 + load more);SSE 增量加载 (
since/ccmetadata) ;User Prompt 查看器 + 导出;长 bash 自动折叠;stick-to-bottom 按钮 2x 尺寸;display 设置进 mobile menu (1.5.0/5/8/10) - iOS 专项:终端从 WebGL 降级 Canvas 解决严重卡顿;
visualViewport+ fixed positioning 修复键盘顶起导航栏;interactive-widget=resizes-contentviewport meta;scrollback iOS=200 / Android=1000 / Desktop=3000;虚拟按键栏 touchstart preventDefault + 按键后 blur,消除按键误触发虚拟键盘 (1.5.7/17) - Terminal 增强:文件上传按钮(PC 工具栏 + chat input)50MB 限制 + 唯一文件名;bracketed paste (
\x1b[200~) 阻止多行粘贴误触发 submit;ultrathink按钮;大写入分 32KB 跨帧避免主线程阻塞;outputBuffer ANSI 安全截断 (1.5.4/15/31/42) - Log 管理:下载/批量删除日志(
/api/download-log、/api/delete-logs);Log 列表 List→Table 可排序;JSONL 紧凑格式 + MAX_LOG_SIZE 200MB→150MB + 合并 API 300MB 上限;Preview 列 Popover(hover/click)带 stats-worker v6→v8 缓存失效 (1.5.1/5/18/37/40) - Git/File 联动:Claude 写操作后(Write/Edit/Bash/NotebookEdit)自动刷新 FileExplorer 和 GitChanges;Git U 状态绿标替换
??;侧边栏文件夹/Git 按钮改 toggle (1.5.22/27/29) - 插件 API:
httpsOptionshook (waterfall) 替换硬编码 HTTPS cert;serverStartedhook 新增url/ip/token;/api/local-url尊重实际协议;proxy-errors.js/proxy-env.js移入 lib/ (1.5.21/32) - 修复与回归:
watchLogFile()初始化lastSize修复重启重复广播;proxy-errors.js补进 npm files array;installShellHook内容比对替换过期 hook;SSE clients 数组 mutate-in-place 修复断连后失联;claude -v/-h正确透传;QR popover 自适应宽度;DiffView 固定 gutter + 背景全宽;ConceptHelp dark-theme 修复 (1.5.2/6/9/20/30/34) - 测试与覆盖率:覆盖率 line 68.98%→71.23%、branch 69.17%→72.81%;新增
test/git-diff / log-watcher / findcc / context-watcher / upload-api / proxy-errors / updater / stats-worker系列单测;npm run test:coverage脚本 (1.5.29/31)
- CLI 模式 (
ccv -c):内置 PTY 终端直接运行 Claude,支持 npm/nvm 安装路径自动检测 - 分屏布局:终端 + 对话双面板,可拖拽调整比例
- 文件浏览器:树形目录、文件内容预览、minimap、支持 dot files 和 gitignore 灰显
- Git 集成:变更文件列表、统一 diff 视图(双行号)、diff minimap
- 工作区管理:多工作区切换、SSE 状态同步
- 插件系统:动态加载/卸载、启用/禁用状态管理
- 自动更新器:版本检测与自动升级
- 终端优化:WebGL 渲染 + context loss 恢复、Unicode11 CJK 支持、WebLinks、scrollback 扩容、PTY 输出批量合并
- SSE 分块加载:大日志文件分 50 条 chunk 传输,带进度指示
- 安全:LAN 移动端 token 鉴权修复
- 卸载命令 (
ccv --uninstall):完整清理 hooks 和配置
- 移动端响应式:虚拟按键栏、触摸滚动惯性、固定列宽自适应字号
- 国际化 (i18n):支持 18 种语言(中/英/日/韩/法/德/西/葡/俄/阿/印/泰/越/土/意/荷/波/瑞典)
- 代理模式 (proxy):拦截 Claude API 流量并记录
- 设置面板:主题、语言、显示选项等可视化配置
- 对话模式增强:thinking block 折叠/展开、工具调用结果渲染优化
- 安全:访问 token 认证、CORS 配置
- Chat 模式:将原始 API 请求/响应重组为对话视图
- Markdown 渲染:代码高亮 (highlight.js)、表格、列表
- Thinking blocks:可折叠的模型思考过程展示
- 工具调用结果:结构化渲染 tool_use / tool_result
- 搜索功能:全文搜索对话内容
- 智能自动滚动:仅在用户位于底部时自动跟随
- Dashboard:请求统计、模型用量图表、token 消耗分析
- 缓存重建分析:按原因分类统计(TTL、system/tools/model 变更、消息截断/修改)
- Request/Response 详情查看器:原始请求体、响应体、流式组装
- 缓存重建分析:精确识别 system prompt / tools / model 变更原因
- Body Diff:JSON/Text 视图切换、复制按钮
- 双向模式同步:Chat ↔ Raw 模式跳转定位
- Claude Code 工具参考文档(22 个内置工具)
- 拦截并记录 Claude API 请求/响应