Skip to content

feat(settings): 配置跨机器同步(本地导入导出 + WebDAV) - #747

Merged
xintaofei merged 10 commits into
xintaofei:mainfrom
galact-byte:feat/webdav-config-sync
Sep 17, 2026
Merged

xintaofei merged 10 commits into
xintaofei:mainfrom
galact-byte:feat/webdav-config-sync

Conversation

@galact-byte

Copy link
Copy Markdown
Contributor

关联 #633(第二项诉求)

背景

换一台机器就要重配一遍:模型服务商和 API 密钥、智能体设置、自定义智能体、快捷消息、任务模板。现有的备份是 GB 级加
密归档,含会话、上传文件、transcript,只能手动导出,不是"每隔几分钟自动跑一次"的单位。

改动

新增配置快照——只含六个配置域的 JSON,几十 KB:modelProviders / agentSettings / customAgents /
quickMessages / taskTemplates / preferences。可导出成单文件,也可推到用户自有的 WebDAV。

几条定死的语义:

  • 只自动上传,从不自动下载。 定时拉取等同于另一台机器静默覆盖本机设置。每次恢复都是显式动作:先取远端 manifest
    展示来源设备、时间、各域条目数,用户确认后才应用。
  • 按自然键 upsert,不删除本地多余行。 清表重插会重排自增主键,打断 agent_setting.model_provider_id 这类外键
    。代价是远端删除不向本地传播,已在 UI 文案里写明。
  • preferences 用允许清单而非排除清单portable_keys::PORTABLE_PREFERENCE_KEYS)。新增的 app_metadata key
    默认不参与同步,由白名单交集测试守护凭据不外流。同步自身的 WebDAV 凭据被排除在快照之外——否则两台机器会互相覆盖凭据,
    变成同步环。
  • 排除 work_task_settings:其 folder_id 外键挂在设备本地的项目记录上,跨机器会指向错误项目。只同步
    work_task_template
  • 快照明文。安全边界是用户自有的、已认证的 WebDAV 端点,API 密钥因此明文传输,这一点在设置页直接写给用户看。

实现分布:

  • commands/config_sync/domains(快照含什么)、portable_keys(哪些偏好可迁移)、snapshot(采集 / 校验 /
    应用 + 本地回滚副本)、local_io(单文件导入导出)、webdav_sync(设置、远端布局、上传下载)、auto_sync(周期哈
    希比对)。业务逻辑走 *_core 函数收普通引用,桌面命令、未来的 Axum handler、后台调度器共用同一份实现。37 个 Rust 单
    测。
  • network/webdav.rs — 最小 WebDAV 客户端(PROPFIND / MKCOL / GET / PUT)。ensure_dir 逐级 MKCOL:RFC 4918
    下中间集合缺失时 MKCOL 返回 409,一次性建多级在真实服务器上建不出来。WebdavErrorAppCommandError 的映射带
    i18n key,401 / 403 / 507 / 5xx / 传输失败各自有可读文案。
  • 远端布局 {远程目录}/v1/{配置档}/{config.json,manifest.json}先写 config 再写 manifest:WebDAV 没有多文件
    事务,上传中断必须让旧 manifest 指向一致的字节,绝不能应用半截配置。v1 这一级留给未来不兼容的协议并存。
  • 自动同步是周期性哈希比对(默认 5 分钟采集快照算 sha256,与上次成功上传的不同才上传),没有做"写入咽喉标脏 +
    防抖"——配置写入散在几十个命令里,没有单一咽喉。手动「立即同步」绕过哈希抑制,因为用户按它通常正是怀疑远端过期。
  • 密码编辑只有一个机制:留空 = 沿用已存的旧值,视图只回 hasPassword: bool,不引入第二真相源。「测试连接」收
    的是未保存的表单,凭据可以先验证再落库。
  • ConfigSyncSettings 设置页(设置 → 系统与网络 → 配置同步)四张卡片:同步范围(包含 / 不包含两栏对照)、本地备份
    、WebDAV 同步(坚果云 / Nextcloud / 群晖 / 自定义地址预设,仅填模板不做服务商分支)、自动同步。复用既有 Card /
    Button / Switch / AlertDialog,无自定义样式。16 个前端测试。
  • configSync.* 文案 10 个语言全量新增。

验证

真机跑通坚果云(https://dav.jianguoyun.com/dav/):测试连接对尚不存在的目录 PROPFIND → 404 视为连接成功;上传
MKCOL /codeg → /codeg/v1 → /codeg/v1/default 各返 201,随后 PUT config.jsonPUT manifest.json;从远端恢复
GET manifest 预览后 GET manifest + config,校验和通过并应用。

  • cargo test --features test-utils → 3576 通过
  • cargo clippy --all-targets --features test-utils -- -D warnings → 通过;--no-default-features --bin codeg-server --lib → 通过;--bin codeg-mcp → 通过
  • vitest run → 6360 个测试通过(439 个文件)
  • eslint(改动文件)→ 0 error;next build → 成功

galact-byte and others added 10 commits September 15, 2026 15:06
Moving to a second machine meant retyping every model provider, agent
setting and task template by hand. Backup covers the whole database, so
it is the wrong tool: it carries conversations and uploads, and restoring
it overwrites the target machine.

This adds a configuration-only snapshot of six domains (model providers,
agent settings, custom agents, quick messages, task templates and the
portable app preferences) that can travel either as a JSON file or
through a WebDAV server the user already owns.

Applying a snapshot upserts by natural key and never deletes local rows,
so a machine that has extra entries keeps them, and auto-increment ids
stay put for the foreign keys that point at them. Automatic sync only
uploads: pulling from the server is always a deliberate action, so a
stale remote can never silently overwrite local configuration.

Uploads are driven by hashing the snapshot on a timer rather than
marking the database dirty, because configuration writes are spread
across dozens of commands. The snapshot is unencrypted, so the WebDAV
credentials themselves are excluded from it and the UI says as much.
…dential scope

The review of xintaofei#747 turned up two defects that make half the feature
unusable and one that leaks a credential:

- `ConfigImportPreview` promised `importable`/`blockedReason`, which the
  Rust command never sends. `disabled={!preview.importable}` therefore
  read `undefined` on every real file and the import confirm button was
  permanently disabled. There is no "previewed but not importable"
  state — `peek` runs the same parser and schema check the import does,
  so an unreadable file already comes back as a rejected promise with a
  `configSync.error.*` key. Drop the phantom fields (and the now-unused
  `importBlocked` string), and show the backend's recount instead of the
  file's self-reported `manifest.counts`.
- The master WebDAV switch only moved local state, and every other
  control including Save lives inside the block it hides — so sync could
  be turned on but never off. It now persists on click, like the proxy
  and launch-at-login switches in the same settings page.
- An empty password field meant "keep the stored one" unconditionally,
  so editing the URL alone was enough to send a saved app-password to
  another server. The password is now bound to the account it was typed
  for; changing host or user requires retyping it, and the field's hint
  stops offering to keep it.

Also: reset the upload hash baseline when the remote target changes
(otherwise retargeting left the new location empty until some unrelated
setting changed), skip auto-sync ticks until a server URL exists, read
the real host name on unix (`HOSTNAME` is never exported, so every
snapshot from a unix desktop was signed "unknown"), guard the forbidden
key list against the key the code actually writes, and tell the user a
restart is needed for the parts of a restore the running window caches.

`ConfigExportSummary` and `ConfigImportResult` were also declared with
fields the backend does not return; corrected to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clearing `last_uploaded_sha256` inside `save_settings_core` closed the
"retargeting leaves the new remote empty" hole only for a clean, serial
save. It is a second write, so it can fail silently (`save_state` only
warns), be interrupted between the two writes, or be undone by a
background upload that was already in flight against the OLD target and
writes its hash back afterwards. Each of those leaves the new
destination suppressed and permanently empty — the bug the reset was
there to fix.

Record the destination alongside the hash instead. Suppression now needs
both halves to match, so a changed target simply stops matching and
there is nothing to reset, nothing to interleave with, and no second
write to lose. A state row from before the field existed reads as `None`
and suppresses nothing, costing one redundant upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…encryption

The review left four design-level gaps open. This closes all four.

**It only ran on the desktop.** The commands were `#[tauri::command]`-only
and the panel returned `null` outside Tauri, so a feature whose entire
point is moving configuration between machines was unavailable on the
half of the product that runs on a server. Every entry point now has an
Axum handler over the same `_core` function, and `codeg-server` spawns
the auto-sync loop. File transfer goes by content rather than by path —
a snapshot is tens of KB, so `backup.rs`'s multipart upload, download
tickets and temp-file reaping would be machinery with nothing to carry.

**The rollback snapshot was written and then unreachable.** Every import
saved one and returned its `rollbackPath`, and nothing could list or
apply it. There are now list/apply commands behind an opaque id that is
validated against an alphabet before it is ever joined to a path, a
panel section that appears only when there is something to undo, and an
undo that saves its own rollback point first.

**The WebDAV password sat in plaintext in `app_metadata`** — and so in
every backup archive that database is packed into — while the snapshot
itself had a reserved `encryption` field that nothing ever set. Both
secrets moved to the store this codebase already uses for credentials
(OS keyring on desktop, the 0600 token file on a server), with a
migration that retries until the row is clean. Encryption is opt-in:
AES-256-GCM under an Argon2id key, in a JSON envelope so the remote file
keeps its `config.json` name and the import path can recognise it from
the value it already parses. The upload baseline is stamped with the
protection in force, so turning encryption on forces a re-upload instead
of leaving the plaintext copy sitting on the remote.

**`peek` decoded the manifest but not the payload**, so a hand-edited
file previewed as valid and then aborted mid-apply. Each domain now
carries a `validate` beside its `collect`/`apply`, called from
`parse_snapshot` — the one door every snapshot enters through — with a
test asserting validate and apply agree on every domain, and another
proving that agreement is not vacuous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**A forged manifest could switch encryption off.** `open_after_download`
treated the manifest as the authority on whether the payload was
wrapped, and the manifest is just a second file on the same share — so
the operator this feature encrypts *against* could replace both halves
with a plaintext snapshot of their choosing and a matching checksum.
Every check passed and attacker-chosen provider endpoints and API keys
landed in the local database while the switch read "encrypted". The
manifest is now believed when it says "encrypted" and not when it says
"plaintext"; the local switch decides that direction.

**An unreadable keyring erased the secrets.** `credentials::load` mapped
a failed read to `""`, and `""` means DELETE on the way back out, so a
denied keychain prompt plus any unrelated save — nudging the interval —
permanently destroyed the passphrase the remote copy is encrypted under.
Reads now distinguish absent from unreadable, and a save that cannot
read the store refuses instead of rewriting it.

**The legacy-password migration clobbered the row.** It serialized this
build's own struct over whatever was there, so a save landing in the
window lost its edits — pairing the OLD server URL with the NEW password
the keyring had just taken, the exact combination `merge_settings`
refuses to create — and a row written by a newer build lost the fields
this one cannot parse. It now removes a single key from the row as it
stands.

**The rollback list offered ids the resolver refused.** The lister took
any file stem; the resolver holds ids to an alphabet. One copy through a
file manager (`config-….json` → `config-… (1).json`) produced a Restore
button that answered "no longer on this machine" about a file sitting
right there. Both now share one definition.

**An import wrote its rollback point to the wrong directory.**
`save_rollback` called `rollback_dir()` while its callers took a
directory parameter, so an undo driven against one directory wrote and
pruned another — and the test suite was evicting snapshots from the
developer's real `~/.codeg`.

**The preview over-promised.** `count_entries` counted every key of the
`preferences` object, but the applier writes only allowlisted keys
holding strings, so the dialog said "3 preferences" and the import wrote
1. Counting is now a function on the domain table beside `apply`, held
to it by a test, the same shape `validate` already uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**A withdrawn passphrase was stored anyway.** Turning the encryption
switch back off hid the field without clearing its state, so the next
Save — about anything at all — submitted the passphrase the user had
just decided against, and `hasPassphrase` then claimed a protection they
never confirmed. The switch clears what was typed; the one already on
file survives, because it is what still decrypts the copy on the remote.

**The master switch behaved like a Save button.** It persists on click
by design (every other control lives inside the block it hides), but it
was persisting the whole live form — so typing a password and then
turning WebDAV off instead of saving wrote that password to the keyring,
cleared the field, and said nothing. It now sends `null` for both
secrets and leaves the fields alone.

**A dismissed file picker could deadlock the panel.** `pickLocalFile`
resolved on `change` and on `cancel` and otherwise never settled, while
the caller holds `busy` for the duration — so on any engine that does
not dispatch `cancel` (WebKitGTK, which a Linux desktop build runs)
dismissing the import dialog left every button in the section disabled
until the page was remounted. Regaining window focus with no file
attached now ends it too.

Also: the runtime split is tested where it lives. The panel's own tests
mock `@/lib/config-sync` wholesale, so their `desktop = false` could not
constrain which branch a real browser takes — it only guards against a
re-introduced `return null`. `src/lib/config-sync.test.ts` now pins the
branch itself: which command each runtime calls, that a browser never
reaches a `*_file` command, and that a dismissed picker settles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**The save guard could lock the feature.** Refusing to save whenever the
credential store would not open fixed the erasure, but it gated EVERY
save — so on a Linux desktop with no Secret Service running, or after a
denied macOS keychain prompt, the master switch and the interval could
not be changed either, permanently. The root cause was narrower than the
guard: the save wrote back secrets it had only just read. It now writes
only what the input actually decided — a new password, a new passphrase,
or the erasure an account change requires — so a save that carries no
credential touches the store at all, and `""` can no longer travel out
of a failed read as "delete this entry".

`same_account` moves into one function, because the two callers now both
act on it: `merge_settings` decides whether the password is kept, and
`save_settings_core` decides whether it is erased. Those must not differ.

**The file-picker fallback could eat a real selection.** Resolving on
window focus cannot tell "the dialog closed" from "the dialog is open
and the user alt-tabbed", so with a non-modal chooser it settled the
promise under someone who was still choosing — and the file they then
picked arrived on a dead promise and vanished silently. Guessing is the
wrong fix for a promise that may not settle; not gating the UI on it is.
`handlePickImport` no longer holds `busy` across the dialog, so an
unanswered picker costs a hidden input instead of the whole panel, and
`pickLocalFile` is back to `cancel` as its only dismissal signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Almost every holder is an async test that awaits a database while it is
held, and `clippy::await_holding_lock` is right to refuse that. A
`tokio::sync::Mutex` is the one that may cross an await — and it has no
poisoning either, so a failing test no longer risks cascading into the
rest of the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t name

`system-network-settings.test.tsx` captures the provider's live
`app_update_state` handler by overwriting a single module-level slot from
its `subscribe` double — for any event, because the double took `_event`
and dropped it.

That held only while the page had exactly one subscriber. This PR embeds
`<ConfigSyncSettings />` in that page, and the panel subscribes to
`config_sync_status` on mount; whichever landed last won the slot, and the
panel's did. `closes a stale rollback dialog when an upgrade becomes
staged` then pushed `ready_to_restart` into the config-sync status handler
instead of the update provider's. The update state never advanced, so
`canRollback` stayed true, the auto-close effect never fired, and the case
failed on a dialog that was still open — an assertion nowhere near the
cause.

The real `Transport.subscribe(event, handler)` routes per event, so both
handlers stay separate in production: the double was lying, not covering
for a product bug. Route by name in the double too, which also keeps the
next section added to this page from silently stealing the handle again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xintaofei

Copy link
Copy Markdown
Owner

codeg work task 195 is done — #747 (31 files, +4252/-237).

@xintaofei
xintaofei merged commit 7266cf8 into xintaofei:main Sep 17, 2026
7 checks passed
@galact-byte
galact-byte deleted the feat/webdav-config-sync branch September 17, 2026 09:32
xintaofei added a commit that referenced this pull request Sep 17, 2026
Your configuration can now travel between machines — model providers, agent settings, custom agents, quick messages and task templates, as a file or through your own WebDAV server, optionally encrypted.
A fast reply no longer locks up the interface: the streaming window now scales with the answer it re-renders, and a two-minute reply at full speed costs about a tenth of the rendering work it used to.
The composer learns two shortcuts — recall a sent prompt with the arrow keys, and click a queued message into the turn that is already running — while Gemini CLI, OpenCode, Cline and Kimi all get a deep adaptation pass.

## New

- **Configuration sync between machines** — a configuration-only snapshot of model providers (with their keys), agent settings, custom agents, quick messages, task templates and app preferences, carried by an exported file or by a WebDAV server you already own, on the desktop and in server mode alike; conversations and uploads stay out of it, applying a snapshot never deletes what the target machine has, and automatic sync only ever uploads. (#747, @galact-byte)
- **The uploaded snapshot can be encrypted with a passphrase** (AES-256-GCM), and both the WebDAV password and the passphrase live in the system keyring instead of the database.
- **Every import and restore leaves a way back** — the last ten configurations this machine had are kept, reachable from the file-actions row, and undoing one is itself undoable.
- **Recall a sent prompt with ArrowUp / ArrowDown** in the composer, the way a shell history works. (#744 requested by @evepupil, #756, @dawNotPoi)
- **Click a queued message to insert it into the running turn** — on any session with a live-feedback channel, instead of waiting for the turn to end. (#751, @leehom0123)
- **A work task can choose the branch it is created for** — the worktree starts at that branch's tip, the merge lands back on it, and a delivery opens its pull request against it, so a task no longer depends on what the project folder happens to be checked out on. (#760 requested by @wht300)
- **OpenCode has a "Behavior & compaction" settings card** — default agent, sharing, auto-update, filesystem snapshots, sub-agent depth, auto-compaction and tool-output limits, written to `opencode.json`, with any field left unset still following OpenCode's own defaults.
- **Cline's provider settings now split the two ways cline actually authenticates** — Cline Usage-Billing, ClinePass and ChatGPT get the `cline auth <id>` command to run instead of a key field, bring-your-own providers keep their key fields, and anything configured outside codeg keeps its own row.
- **Gemini sub-agent transcripts now show up** — a sub-agent's own conversation is discovered alongside the session that launched it.
- **OpenCode's `lsp` is now a permission of its own**, so you can decide it separately from the rest.

## Improved

- **Updated bundled agents:** Claude Code ACP 0.78.0 (Claude Agent SDK 0.3.270), Codex ACP 1.12.0 (Codex CLI 0.154.0), Gemini CLI 0.60.0, Kimi Code 2.0.0, Grok 1.0.34, Qoder 1.1.54.
- **A reply streaming at full speed no longer freezes the interface** — the flush window now grows with the answer being re-rendered and is timed per conversation, taking a two-minute reply at 300 tokens/s from 519M re-rendered characters down to 60M while one busy tab stops holding back the others. (#589 reported by @Lparksi, #716, @Adam-Dalloul)
- **Settings → System is now one "Data & sync" card** — configuration sync, backup and restore as three tabs, with the pre-restore snapshots sitting under the restore flow they undo instead of in a card of their own.
- **Kimi finds skills it was already loading** — codeg now reads all four of Kimi's skill roots, including the shared store pi and cline install into, and writes project skills at the repository root where Kimi actually looks.
- **A `kimi-k3` session's context gauge is correct** — history was measured against a 256K window on a model that has 1M.
- **Opening a historical conversation now shows the session coming up** — the composer names the agent being prepared and holds placeholders where the model and config chips will land, instead of looking dead for the several seconds the session takes to establish.
- **Gemini transcripts are read the way Gemini CLI 0.60 writes them** — sessions that showed no messages at all now open, and token usage and the context window (1M, 256K for Gemma) are counted correctly rather than reading about 5% high.
- **A Gemini `[MODE_UPDATE]` line no longer lands in the conversation as a message** — it switches the session's mode, which is what it was for.
- **Gemini's run, read, search, fetch and web-search calls now render as their own cards** instead of a bare title.
- **An OpenCode tool call keeps its real name live and after a reload** — `glob` was showing as grep, `lsp_diagnostics` as read and any MCP tool taking a `query` as web search, and a live read now keeps the line numbers the history view shows.
- **An OpenCode session still named "New session - …" now takes its title from the first message** — OpenCode's own rename fails silently when its title model is unreachable, which left the placeholder there forever.
- **A reopened OpenCode session shows what actually happened** — context-compaction dividers, sub-task summaries, assistant errors and aborts, and a question you declined no longer rendered as a red error card.
- **A Cline backup no longer carries `secrets.json` or your provider keys**, and its new SQLite session store is archived as a coherent database rather than as loose files that restore corrupt.
- **An agent's per-launch temp directory is created private to you**, and codeg refuses a scratch root it did not create itself, so no other local account can own the directory an agent unpacks into.

## Fixed

- **An agent that opens a unix socket starts again on macOS** — 0.30.8's per-launch temp isolation pushed the socket path past the kernel's 104-byte limit, so the agent died during startup and the session failed after a minute with a misleading "turn off MCP support" hint. (#754 reported by @Liang-HZ)
- **Delegating to another agent no longer fails silently** — on a long temp path the broker published a socket nothing on the system could dial, while the status indicator stayed green and every companion failed to reach it; the path is now refused loudly and falls back to a short one.
- **Cline conversations open with their messages again** — cline 3.x moved its transcripts to a new session store, so every conversation resolved to zero turns, and a finished live reply also lost its model, token usage and completion time.
- **Cline's provider dropdown in the composer is withheld when the launch pins the provider** — it could only ever fail with "Cannot change provider", and a pick saved from clicking it is no longer replayed on later sessions.
- **A ClinePass or ChatGPT sign-in is no longer billed as a plain Cline account** — a leftover key could short-circuit cline's auth gate without the sign-in ever taking effect.
- **AWS Bedrock and GCP Vertex leave Cline's key list** — they need more than a single API key and never worked from here; `cline auth bedrock` still sets them up, and codeg now leaves such an entry alone instead of retargeting it.
- **An OpenCode plugin loaded from a path is reported as loaded, not missing** — a `file:///…`, absolute or relative plugin came back "not installed" behind an Install button that could only fail, and preflight raised a failure that could never clear. (#745 reported by @LiDongYang743)
- **A Windows `file://C:\…` plugin path resolves** instead of being read as a host name, and a plugin under a directory containing `@` keeps its full name instead of being truncated or silently dropped.

Thanks to @galact-byte, @Adam-Dalloul, @dawNotPoi and @leehom0123 for contributing to this release, and to @Liang-HZ, @LiDongYang743, @evepupil, @Lparksi and @wht300 for the reports.

-----------------------------

# 发布版本 0.30.10

配置现在可以在多台机器之间同步了——模型供应商、智能体设置、自定义智能体、快捷消息和任务模板,可以导出为文件,也可以走你自己的 WebDAV 服务器,还能选择加密。
高速输出不再拖垮界面:流式刷新窗口会随着正在重绘的回复变宽,两分钟的满速回复渲染开销降到原来的约十分之一。
输入框新增两个用法——用方向键调回发送过的提示词、点一下排队消息直接插进正在进行的回合,同时 Gemini CLI、OpenCode、Cline、Kimi 都做了一轮深度适配。

## 新增

- **配置跨机器同步**——只包含配置的快照:模型供应商(含密钥)、智能体设置、自定义智能体、快捷消息、任务模板和应用偏好,可以导出成文件,也可以通过你自己的 WebDAV 服务器传输,桌面端与服务器模式都支持;会话和上传文件不在其中,导入时只增量合并、绝不删除目标机器上已有的条目,自动同步也只上传。(#747@galact-byte)
- **上传的快照可以用口令加密**(AES-256-GCM),WebDAV 密码和加密口令改存系统钥匙串,不再落在数据库里。
- **每次导入和恢复都留有退路**——保留这台机器最近十份配置,从文件操作行即可进入,撤销本身也可以再撤销。
- **输入框支持用 ↑/↓ 调回发送过的提示词**,和 shell 的历史记录一样。(#744@evepupil 提出,#756@dawNotPoi)
- **点击排队消息即可插入正在进行的回合**——只要该会话有实时反馈通道,不必等这一轮结束。(#751@leehom0123)
- **待办任务可以指定基于哪个分支创建**——工作树从该分支的最新提交开始,合并也回到该分支,交付时的 PR 同样以它为基线,不再取决于项目目录当前检出在哪。(#760@wht300 提出)
- **OpenCode 新增「行为与上下文压缩」设置卡片**——默认智能体、分享、自动更新、文件快照、子智能体层级、自动压缩和工具输出上限,保存后写入 `opencode.json`,留空的项仍沿用 OpenCode 自己的默认值。
- **Cline 供应商设置区分了它真正的两种认证方式**——Cline Usage-Billing、ClinePass、ChatGPT 改为给出 `cline auth <id>` 命令而不是密钥输入框,自带供应商保留密钥字段,在 codeg 之外配置的供应商单独成行。
- **Gemini 子智能体的会话记录现在能被识别**,会随发起它的会话一起出现。
- **OpenCode 的 `lsp` 成为独立的权限项**,可以单独决定是否放行。

## 改进

- **内置智能体版本更新:** Claude Code ACP 0.78.0(Claude Agent SDK 0.3.270)、Codex ACP 1.12.0(Codex CLI 0.154.0)、Gemini CLI 0.60.0、Kimi Code 2.0.0、Grok 1.0.34、Qoder 1.1.54。
- **满速输出的回复不会再让界面卡死**——刷新窗口会随着正在重绘的回复变宽,并且按会话独立计时;按 300 tokens/s 回放,两分钟的回复重绘字符数从 5.19 亿降到 0.6 亿,某个繁忙标签页也不会再压住其它标签页的输出。(#589@Lparksi 反馈,#716@Adam-Dalloul)
- **设置 → 系统 合并为一张「数据与同步」卡片**——配置同步、备份、恢复三个标签页,恢复前的快照也移到它所对应的恢复流程下方,不再单独占一张卡片。
- **Kimi 能找到它本来就在加载的技能**——codeg 现在读取 Kimi 全部四个技能目录,包括 pi、cline 共用的那个技能库,项目技能也改写到 Kimi 真正扫描的仓库根目录。
- **`kimi-k3` 会话的上下文用量显示正确**——历史侧此前按 256K 窗口计算,而该模型是 1M。
- **打开历史会话时会显示会话正在准备**——输入框会说明正在准备哪个智能体,并在模型和配置胶囊的位置先放占位骨架,不再在建立会话的数秒里看起来像是失灵了。
- **Gemini 会话按 Gemini CLI 0.60 的写入方式解析**——此前完全显示为空的会话现在能正常打开,Token 用量和上下文窗口(1M,Gemma 为 256K)也计算正确,不再偏高约 5%。
- **Gemini 的 `[MODE_UPDATE]` 不再作为一条消息出现在对话里**,而是切换会话模式——这本来就是它的用途。
- **Gemini 的执行、读取、搜索、抓取和网页搜索调用现在有各自的卡片**,不再只是一行标题。
- **OpenCode 工具调用在实时和重新打开后保持同一身份**——此前 `glob` 显示成 grep、`lsp_diagnostics` 显示成读取、带 `query` 的 MCP 工具显示成网页搜索;实时读取现在也保留历史视图里的行号。
- **仍叫「New session - …」的 OpenCode 会话现在以首条消息作为标题**——OpenCode 自己的重命名在标题模型不可用时会静默失败,占位名就此一直留着。
- **重新打开的 OpenCode 会话能还原真实经过**——上下文压缩分隔、子任务摘要、助手侧的报错与中断都在,被你拒绝的提问也不再渲染成红色错误卡片。
- **Cline 的备份不再打包 `secrets.json` 和你的供应商密钥**,其新的 SQLite 会话库也会作为完整数据库归档,而不是按散文件打包导致恢复后损坏。
- **智能体每次启动的临时目录以私有权限创建**,且 codeg 会拒绝不是自己创建的临时根目录,避免本机其他账户持有智能体解压和执行文件的目录。

## 修复

- **在 macOS 上会创建 unix socket 的智能体可以正常启动了**——0.30.8 的按启动隔离临时目录把 socket 路径顶过了内核 104 字节上限,智能体在启动阶段就退出,会话挂起约一分钟后失败,并给出误导性的"关闭 MCP 支持"提示。(#754@Liang-HZ 反馈)
- **委派给其它智能体不会再静默失败**——临时目录路径过长时,broker 会发布一个系统上谁都连不上的 socket,状态指示灯却是绿的,所有伴生进程都连不过去;现在会明确拒绝并回退到短路径。
- **Cline 会话重新能看到消息内容**——cline 3.x 换了会话存储位置,导致所有会话都解析为零轮对话,刚刚完成的实时回复也拿不到模型、Token 用量和完成时间。
- **当启动已锁定供应商时,输入框里的 Cline 供应商下拉不再出现**——它只会报 "Cannot change provider",此前误点保存下来的选择也不会再被回放到后续会话。
- **ClinePass 或 ChatGPT 登录不会再被当成普通 Cline 账号计费**——残留的密钥会绕过 cline 的认证入口,让登录实际上没有生效。
- **AWS Bedrock 与 GCP Vertex 从 Cline 的密钥列表中移除**——它们需要的不止一个 API Key,在这里从未真正可用;仍可用 `cline auth bedrock` 配置,codeg 现在会原样保留这类条目而不是改写它。
- **以路径声明的 OpenCode 插件会显示为已从磁盘加载,而不是"未安装"**——`file:///…`、绝对路径和相对路径插件此前都显示为缺失,安装按钮必然失败,预检也会报一个永远无法消除的错误。(#745@LiDongYang743 反馈)
- **Windows 的 `file://C:\…` 插件路径可以正确解析**,不再被当成主机名;位于含 `@` 的目录下的插件名也不会再被截断或被去重悄悄丢掉。

感谢 @galact-byte@Adam-Dalloul@dawNotPoi@leehom0123 为本次发布做出的贡献,也感谢 @Liang-HZ@LiDongYang743@evepupil@Lparksi@wht300 的反馈。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants