Skip to content

feat(sync): rebuild the Git sync experience and close its edge cases - #365

Merged
Grivn merged 10 commits into
mainfrom
claude/git-sync-ux
Oct 11, 2026
Merged

Grivn merged 10 commits into
mainfrom
claude/git-sync-ux

Conversation

@Grivn

@Grivn Grivn commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

摘要 / Summary

优化 Git 同步的操作体验与界面,并修复一批边界问题。界面按“连接仓库 → 立即同步 → 自动同步 → 备份历史 → 高级设置”的顺序重建为一张卡片:用 GitHub 账号(一键复制设备码并打开 GitHub,可搜索/新建私有仓库,选公开仓库前确认)或地址连接;立即同步一步完成合并与推送,检查远端先说明会发生什么且不写入;失败用页面语言说明原因并给出修正入口。Host 端修复:换到另一个仓库或远端分支被删后不再重新发布旧历史;并发推送被拒时自动合并重试一次,同一版本再次合并不再多出副本;高级设置里可以给这台电脑命名(默认用主机名),备份历史与检查远端用它标明版本来自哪台电脑;中途停止留下的锁可被接管;Git 2.20–2.27 可初始化镜像;本机无 Git 身份时使用通道身份;令牌不再跟随仓库到其他主机;状态探测 20 秒超时;失败原因以 details.reason 传给页面。

Rebuild the Git sync block around what a person does, and fix the Host edge cases found while running it. The block is now one card in the order it is set up — Repository, Sync, Automatic sync, Backup history, Advanced. A repository connects through a GitHub sign-in (one click copies the device code and opens GitHub; then search, pick or create a private repository, with a confirmation before a public one) or by an address checked before it is sent. Sync now merges and pushes in one step, Check remote says what a sync would change and writes nothing, and a failure is said in the page's language with the way to fix it. On the Host, switching to another repository or syncing after the remote branch was deleted no longer republishes earlier history; a push raced by another machine is merged and retried once, and merging the same version again adds no second copy; Advanced names this computer (its host name by default, or a custom name), which backup history and Check remote show; an abandoned lock no longer blocks every later sync; the mirror initializes on Git 2.20–2.27; a blank author on a machine without a Git identity uses the channel identity; a saved token no longer follows the repository to another host; the status probe gives up after 20 s; and failures reach the page with a details.reason.

关联 Issue 或背景 / Related Issue or Context

N/A. A maintainer-requested follow-up to the Git sync feature from #335 (released in v0.5.26): its operation, UI/UX consistency, flow and edge cases. The Host defects fixed here were found while running it and are each covered by a test that fails on main.

涉及区域 / Affected Areas

  • Host 激活、Headless 或 bundle / Host activation, Headless, or bundle
  • 运行时记忆 / Runtime Memory
  • 项目档案 / Project Documents
  • 记忆空间或 Provider / Memory Spaces or Providers
  • 子 Agent 或 Agent 工作流 / Subagent or Agent workflow
  • Web UI 或对话交互 / Web UI or conversation interaction
  • 设置、存储或安全 / Settings, storage, or security
  • CLI、RPC、命令或工具 / CLI, RPC, commands, or tools
  • 安装、更新或发布 / Installation, update, or release
  • 测试、构建或文档 / Tests, build, or documentation
  • 其他(请说明)/ Other (explain below)

PR 类型 / PR Type

  • 面向用户的功能或行为变更 / User-facing feature or behavior change
  • Bug 修复 / Bug fix
  • 增强或优化 / Enhancement or optimization
  • 兼容性适配 / Compatibility change
  • 维护或重构 / Maintenance or refactor
  • 测试或构建 / Tests or build

变更内容 / Changes

Host (src/host/git-sync.ts, pack.ts, auto-backup.ts, github-auth.ts, rpc.ts, protocol.ts, new sync-error.ts):

  • The mirror records the repository it was filled from (mnemon.origin in its Git config) and is rebuilt for another one; a branch the remote does not hold starts a new history. Before, the first push to an empty repository, or after the remote branch was deleted, published the earlier history there.
  • A push Git refuses because another machine published meanwhile folds the branch in again and is tried once more; what both attempts brought in is reported. Merging the same version again keeps one copy per diverging document or memory space (pack.ts, which ZIP import shares).
  • The sync lock records its owner (owner.json). A lock whose process is gone is taken over at once, one an older version left without a record after 15 minutes, any lock after an hour, each renamed aside before removal. One-command credential files a stopped Host left are removed.
  • git init no longer passes --initial-branch (Git 2.28), so the declared Git 2.20 floor holds. A blank author uses this computer's configured identity, else the channel identity, instead of failing or publishing one Git guessed.
  • A saved token is dropped when the repository moves to another host without a new token.
  • The status probe gives up after 20 s (MNEMON_SYNC_STATUS_TIMEOUT_MS) and names why a remote could not be read (issue: auth, not-found, network, timeout). A refused operation carries a fixed category in details.reason (MnemonSyncError).
  • status.lastRun (state/sync/last-run.json) keeps the last sync for the page. The preview counts payload files only; compaction sizes are bytes; a push reads the remote once per attempt.
  • The GitHub repository list reads up to five pages; a revoked grant is github-unauthorized. Automatic sync keeps the reason of its last failure.
  • This computer's name (machine-identity.ts): state/machine.json records labelSource. A host-name label follows the current host name, and a custom one stays as typed; configure takes machineLabel (a string, or null for the host name), checked before anything in the request is written, and status.machineLabel reports the source and the host name. The store re-reads the file on every call, so a rename reaches the pack manager's stamps at once.

Page (src/client/MnemonSyncSection.tsx, new MnemonSyncDialogs.tsx and sync-view.ts, locales.ts, MnemonSettingsCard.module.css):

  • One card in the order it is set up: Repository, Sync, Automatic sync, Backup history, Advanced.
  • Repository: GitHub account (one click copies the device code and opens GitHub; search, a private repository created from a typed name, a confirmation before a public one) or Repository address (checked as the Host checks it; a token field only for HTTPS, with Remove beside a saved token). Once connected, a state line says whether the repository answers and how it authenticates, names a failure with Set credentials where that is the fix, and marks an HTTPS repository without credentials before the first push fails. Disconnect is confirmed, deletes nothing and turns automatic sync off.
  • Sync now merges and pushes in one step and says what it brought in. Check remote compares each component and the runtime entries without writing, lists subjects written differently with a link to memory reconciliation, and offers Merge here only and Sync now; a failure started there is said there.
  • Automatic sync uses the DSH selector and says when it runs next or why it last failed. Backup history lists versions by time and computer with what each component held (latest 100). Advanced is a disclosure: this computer's name (Host name or Custom, with how history will show it), branch, directory and an explicit author choice (Default, This computer's Git, Custom).
  • Failures are said in the page's language; only what cannot be named keeps Git's words. The standing "运行规则" block on memory reconciliation is removed.

Docs: docs/{en,zh-CN}/guides/operations.md (Git repository sync, what the channel guarantees, backups and differences), the UI guides, README, configuration.md, storage-model.md; changeset dsh-mnemon: patch.

Tests: tests/git-sync.spec.ts, pack.spec.ts, sync-rpc.spec.ts, github-auth.spec.ts, machine-identity.spec.ts, new tests/client-sync.spec.tsx (30 cases), client-memory-enhancements.spec.tsx, the copy fingerprints; scripts/verify-sync-git.mjs checks the reasons and the computer's name through real DSH (64 checks).

最新代码确认 / Latest Codebase Confirmation

  • 我已基于最新 main 分支开发,或在提交前已 rebase 或合并最新 main。 / I developed from the latest main, or rebased or merged the latest main before submitting.

同步命令 / Sync command:

git fetch origin main
git rebase origin/main

Based on main 1a6ff881 (v0.5.27).

AI 编码披露 / AI Coding Disclosure

  • 完全 AI 编码:全部编程改动由 AI 产出,并由贡献者接受和审查。 / Fully AI-coded: AI produced all programming changes, which the contributor accepted and reviewed.
  • 部分 AI 辅助:AI 帮助编写或修改了部分内容。 / Partially AI-assisted: AI helped write or modify part of the change.
  • 未使用 AI 编码辅助。 / No AI coding assistance was used.

使用的 AI 模型 / AI model used: Claude Opus 5.5 (claude-opus-5-5)

使用的编码 Agent 工具 / Coding Agent tool used: Claude Code

仓库规范检查 / Repository Rules

  • 未修改 DSH 官方源码,未让 tsconfig 指向 DSH 源码 checkout,仅使用正式的 @deepseek-ai/* NPM 契约。 / I did not modify DSH source or point tsconfig at a DSH source checkout, and used only published @deepseek-ai/* NPM contracts.
  • Client 与 Host 边界仍以浏览器安全的 src/host/protocol.ts 为准,没有在两侧重复定义 wire DTO。 / The Client and Host boundary still uses browser-safe src/host/protocol.ts as the source for wire DTOs.
  • 持久化格式、RPC 权限、路径或凭据处理的变更包含兼容或拒绝路径、安全分析和相应测试。 / Changes to persistence formats, RPC authority, paths, or credentials include compatibility or rejection paths, security analysis, and tests.
  • 没有提交 token、密钥、私有记忆、未脱敏日志或生成的 lib/ 文件。 / I did not commit tokens, credentials, private memory, unredacted logs, or generated lib/ files.
  • 用户可见文案和长期文档已同步维护中文与英文版本,命令、配置键和路径保持一致。 / User-facing copy and long-lived documentation are synchronized in Chinese and English, with matching commands, configuration keys, and paths.
  • 会改变发布制品或其元数据的 PR 已添加 changeset;仅测试、CI 或站点文档变更可不添加。 / A PR that changes a published artifact or its metadata includes a changeset; test-only, CI-only, and site-documentation-only changes may omit one.
  • 新增和修改的代码、注释、文档、提交信息不含 emoji。 / New and modified code, comments, documentation, and commits contain no emoji.

Credentials: a saved token no longer follows the repository to another host (before, the next status probe and push handed it to the new host); the GitHub grant is still used only for github.com over HTTPS; MNEMON_SYNC_GIT_TOKEN still comes first; tokens are never echoed (tests/sync-rpc.spec.ts); one-command credential files left by a stopped Host are removed. details.reason is a fixed category name, never Git's text or a credential. The lock takeover and the mirror rebuild touch only state/sync. No RPC method or permission is added.

兼容性与数据安全 / Compatibility and Data Safety

  • DSH: checked on the pinned 0.1.7-rc.2, 0.2.0-rc.2 (npm latest and next) and 0.2.1-alpha.2; no DSH contract changes.
  • Configuration: no new keys. Disconnecting now also sets autoBackupMinutes to 0.
  • Machine identity: state/machine.json gains labelSource, which earlier versions ignore. A record without it whose label is still the host name keeps following the host name; any other label was set by hand and stays. A renamed computer's next sync records the new name in a version of its own; earlier versions keep theirs.
  • Storage: a mirror from an earlier version has no mnemon.origin and is rebuilt from the remote on the first sync after the upgrade. It only ever held a copy of the remote branch (each sync reset it to the fetched tip), so nothing is lost; the cost is one full fetch. state/sync/last-run.json is new and optional.
  • Rollback: an earlier version ignores mnemon.origin, last-run.json and the lock's owner.json; a lock left by this version is treated as before.
  • Pack merge: merging or importing the same version twice no longer adds a second copy of a diverging document or memory space; first merges are unchanged.
  • The repository's payload format, branch layout and commit contents are unchanged, so machines on v0.5.27 and on this change sync with each other.

本地验证 / Local Validation

执行的命令 / Commands run:

pnpm run verify
pnpm run verify:plugins
pnpm run e2e:sync
MNEMON_E2E_DSH=<DSH 0.2.0-rc.2>/lib/bin.js pnpm run e2e:sync
MNEMON_E2E_DSH=<DSH 0.2.1-alpha.2>/lib/bin.js pnpm run e2e:sync

结果摘要 / Result summary:

  • pnpm run verify on a clean checkout of 8a73ab09 (Node 24.19.0, pnpm 11.25.0, frozen lockfile, pinned DSH 0.1.7-rc.2): passed. 132 root test files passed and 2 skipped (1,880 tests passed, 6 skipped); Headless 37 tools with 8 Mnemon tools; package 59 files, 1,910,270 unpacked bytes.
  • pnpm run verify:plugins: 17 plugin repositories and 18 packed artifacts passed.
  • Git sync end to end (scripts/verify-sync-git.mjs, two DSH instances, a real bare repository): 64 of 64 checks on DSH 0.1.7-rc.2, 0.2.0-rc.2 and 0.2.1-alpha.2.
  • Against main: with main's Host sources under this branch's tests, 16 of 106 Host cases fail; all pass here. The list is in verification.json.
  • WebUI on DSH 0.2.0-rc.2 with two instances and one repository, in zh-CN and en-US, and at 390 px wide: the flows in the evidence below, no console errors, no horizontal overflow.
  • verify:docs with the evidence commit 65f3151a: passed.
  • Not run: a GitHub sign-in against a real account (the device flow, listing and creation are covered by tests/github-auth.spec.ts and page tests with a fake Host); Windows and Linux; the DSH desktop app.

用户可见变更证据 / Local Feature Evidence

证据 / Evidence: Git sync record · 中文 · verification.json

v0.5.27 This change
v0.5.27: operations above an unconfigured form Repository with GitHub account or address
v0.5.27 after a push After a sync: state, last sync and what it brought in
v0.5.27 Check remote: a Backups dialog stuck on reading Check remote: each component, counts, a subject written differently
v0.5.27 failure: Git's English error The reason in words with Set credentials

The record also shows the address check, backup history, advanced settings with this computer's name, the disconnect confirmation and the English page. The screens use a demonstration repository and a demonstration token; no real credential, private memory or local path appears.

🤖 Generated with Claude Code

Grivn and others added 10 commits October 11, 2026 15:46
…oned locks

- A mirror now names the repository it was filled from and is rebuilt for
  another one, and a branch the remote does not hold starts a new history.
  Before, switching to an empty repository, or pushing after the remote
  branch was deleted, published every earlier payload there again.
- A push Git refuses because another machine published meanwhile folds the
  branch in again and is tried once more.
- The sync lock records its owner, so a Host that stopped mid-operation no
  longer leaves every later sync refused; the one-command credential files
  such a Host left behind are removed.
- Remote failures carry a reason (credentials, not found, network, timeout,
  push rejected, empty branch, busy, no repository, Git missing, GitHub grant
  refused) through the RPC details, and the status probe gives up after 20 s.
- A blank commit author uses this machine's configured Git identity, else the
  channel's own instead of failing or publishing a guessed host name.
- git init no longer passes --initial-branch, which needs Git 2.28 while the
  channel declares 2.20.
- The preview no longer counts the re-stamped manifest as a changed file,
  compaction sizes are converted from KiB, the push reads the remote once
  instead of twice, and the last finished sync is kept on disk.
- The GitHub repository list reads up to five pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block now reads in the order it is set up, as preference rows in one
card like a Provider row's editor:

- Repository: connect through a GitHub sign-in (one click copies the device
  code and opens GitHub; then search, pick or create a private repository,
  with a confirmation before a public one) or by address, checked before it
  is sent, with a token field only for HTTPS. Once connected, the row shows
  the repository, whether it answers and how it authenticates, names a
  failure (credentials, not found, network, timeout) with a way to fix
  credentials, and offers Change and Disconnect (confirmed, deletes nothing).
- Sync: one primary "Sync now", which merges the repository and pushes, and
  "Check remote", one dialog comparing each component, conflicts and held
  deletions, with "Merge here only" and "Sync now". The preview bar, the
  second merge button and the history dialog that opened on Check remote
  are gone. Results and failures are said beside the row, in the page's
  language, with the last sync time.
- Automatic sync uses the DSH selector and says when it runs next or why it
  last failed; Backup history lists versions by time and computer.
- Advanced keeps branch, directory and an explicit commit author choice
  (default, this computer's Git, custom) behind a disclosure.

Memory reconciliation drops its standing three-paragraph rules; each
consequence is stated where it applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…guarantees

The operations guide, the UI guide, the README and the configuration and
storage references follow the new order (repository, sync, automatic sync,
backup history, advanced), name the guarantees behind it, and drop the
reconciliation rules block the page no longer shows. The end-to-end script
also checks that a refused operation reaches the page with its reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving the repository to another host forgot nothing, so the next status
probe and push handed a token saved for one host to another. The Host now
drops a saved token when the repository moves to another host and no new
token comes with the move; the address form stops offering the old token
for a different host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…timer on disconnect

- "Copy and open GitHub" passed `noopener` to window.open, which then always
  answered null, so the page claimed the browser had blocked a tab it had
  opened. The opener is now cut after opening, and the copy starts first.
- Check remote on a branch that holds no backup yet says so and still
  offers Sync now, the step that creates it.
- Disconnecting also turns automatic sync off, as the confirmation says,
  so connecting another repository later does not start syncing to it on
  the old timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host:
- give the status probe its 20-second limit; git() ignored timeoutMs
- count what both attempts of a retried push brought in
- keep one copy per diverging document or space when the same version
  is merged again, as a retried push does
- take over an ownerless lock after 15 minutes and rename it aside
  before removing it, so two Hosts cannot both take it
- keep an automatic sync failure's reason only while its error stands,
  and do not report a busy channel as a failure

Page:
- read the preview and the file diff one after the other, so the
  comparison never meets its own lock
- count remote-only and local-only entries without the held-back and
  removed ones the other lines already show
- show a sync started from the comparison failing inside the dialog,
  and a failed disconnect inside its confirmation
- offer Set credentials for a repository reported not found, and say
  "needs an access token" for HTTPS hosts other than github.com
- keep polling a GitHub device code through a transient error
- match an existing repository per owner before creating one
- say that the history lists the latest 100 versions
- accept an unchanged author saved by an earlier version, and name it
  by its email when it has no name
- move the token's Remove button out of its label, and read the state
  again after a removal or a cancelled change
- let the buttons return before the state line is read again

Docs: say when a sync fails before committing, that the environment
token still comes first, the lock takeover times and the 100-version
history limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Screens of v0.5.27 and this change on DSH 0.2.0-rc.2 with two instances
syncing through one repository, in Chinese and English; the Host edge
cases with the 16 tests that fail on main's Host sources; the review
follow-up; and the verify, verify:plugins and three-host end-to-end
results for 95bd46c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backup history and Check remote say which computer a version came from,
and that name was the host name an identity was minted with, frozen
there and changeable only by editing state/machine.json.

Advanced now holds This computer's name: Host name follows the
computer's current host name, and Custom keeps a typed name of up to
120 characters on one line. The name travels through the sync
configure call, is checked before anything in the same request is
written, and is reported in status with its source and the host name.

machine.json records labelSource. A record from an earlier version
whose label is still the host name keeps following it; any other label
was set by hand and stays. The store reads the file on every call, so a
rename reaches the pack manager's stamps at once, and the next sync
publishes the new name in a version of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The after screens show the name under Advanced and in the disclosure
summary, the record counts the new tests (30 page cases, the machine
identity cases) and the 64 end-to-end checks on three DSH releases, and
verify and verify:plugins are recorded for 8a73ab0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Grivn
Grivn merged commit 50c75d9 into main Oct 11, 2026
5 checks passed
@Grivn Grivn mentioned this pull request Oct 11, 2026
12 of 28 tasks
@Grivn
Grivn deleted the claude/git-sync-ux branch October 11, 2026 13:42
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.

1 participant