Skip to content

feat(mirror): project files can stay on the client while agents run on the host - #6157

Closed
malekelkssas wants to merge 35 commits into
pingdotgg:mainfrom
Vilancer:feat/project-mirroring
Closed

malekelkssas wants to merge 35 commits into
pingdotgg:mainfrom
Vilancer:feat/project-mirroring

Conversation

@malekelkssas

@malekelkssas malekelkssas commented Aug 11, 2026 •

Copy link
Copy Markdown

What Changed

Adds project mirroring: in remote mode the provider CLI runs on the host, but project files can now stay on another environment (the origin). The host materializes a git-backed mirror of the origin's folder, agents run against the mirror at full local speed, and a sync protocol keeps both sides in agreement over the existing connection layer.

  • Contracts: ProjectOrigin on projects, mirror.* RPCs, mirror:sync scope, projectMirroring capability, t3.json mirror.include allowlist.
  • Server: GitSync snapshot/bundle/three-way-apply primitives, signed GET/PUT bundle route, MirrorService (host broker + sync state machine, restart-safe queued apply-backs), MirrorAgent (origin side), turn-start sync gate with an explicit stale-run override, post-checkpoint apply-back.
  • Web: capability-gated "Files live on another machine" creation flow and a mirror status chip with Sync now and conflict listing.
  • Docs: internals section in remote.md, user guide, glossary entries.

Why

Lets a project's real working copy stay on a laptop (uncommitted edits, untracked files, gitignored-but-allowlisted files like .env) while an always-on machine runs the agents, syncing automatically before and after every turn instead of requiring the files to live on the host.

UI Changes

Adds a "Files live on another machine" project creation option (shown only when both environments support mirroring) and a mirror status chip on the project header (Seeding/Syncing/Synced/Offline/Conflicts) with a manual "Sync now" action and conflict listing.

Test plan

Verified end-to-end with a two-device setup (host + origin): pairing, project seeding (dirty edits, untracked files, mirror.include entries), pre-turn push, post-turn apply-back, manual sync, origin-offline stale-run fallback with queued apply-back replay on reconnect, and conflict handling (user's local edit wins, agent's version stays reachable in git history). Also covers regression checks: gitignored files don't leak beyond the allowlist, non-git folders are rejected at setup, and non-mirrored projects behave unchanged.

Automated suites: pnpm vp test run src/mirror (GitSync + MirrorService end-to-end) and pnpm vp test run src/orchestration/Layers/ProviderCommandReactor.test.ts (turn gate).

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Add project mirroring to keep files on client, agents on host

  • Introduces a full mirroring system: host-side MirrorService orchestrates seed/sync/apply via a long-lived MirrorAgent connection, using git snapshots and HMAC-signed bundle transfers over new HTTP endpoints (http.ts, MirrorService.ts, MirrorAgent.ts, GitSync.ts)
  • Provider turns for mirrored projects are gated by MirrorService.ensureFresh at turn start; if the origin is offline, the turn is blocked with a 10-minute stale-run offer window allowing a resend of the same message to run against last-synced files (ProviderCommandReactor.ts)
  • After each turn, CheckpointReactor queues a mirror apply-back to push results to the origin working copy; project deletion triggers MirrorProjectDeletionReactor to revoke mirror links and peer sessions (CheckpointReactor.ts, MirrorProjectDeletionReactor.ts)
  • Adds WebSocket RPCs (mirrorAttach, mirrorConnect, mirrorRespond, subscribeMirrorStatus, etc.) with a new mirror:sync auth scope, client runtime atoms, and UI surfaces including MirrorStatusChip, sidebar mirror glyphs, a command-palette mirror-project flow, and settings panels for link management and include-ignored toggle (ws.ts, rpc.ts, mirror.ts, MirrorStatusChip.tsx, CommandPalette.tsx)
  • Adds DB migrations 041–044 for origin_json, mirror_include_ignored_files, mirror_sync_runtime, and mirror_links tables; Normalizer forces mirrored projects to use a server-managed directory under mirrorsDir with path-traversal validation (Migrations.ts, Normalizer.ts)
  • Risk: mirrored projects now depend on origin reachability at turn start; offline origins block turns. The ensureFresh gate in ProviderCommandReactor.make and the stale-run offer Map keyed by threadId are the primary places to check for regressions in turn dispatch. normalizeDispatchCommand resolves workspaceRoot to a mirror directory — verify projectId-derived paths cannot escape mirrorsDir. New RPC scope AuthMirrorSyncScope in RpcAuthorization.ts must be correctly mapped or peer connections will fail auth.

Macroscope summarized 58807c0.

Nardy11 and others added 13 commits August 9, 2026 17:18
…n the host

In remote mode the provider CLI runs on the host, but project files also
had to live there. This adds project mirroring: the host materializes a
git-backed mirror of a folder that lives on another environment (the
origin), agents run against the mirror at full local speed, and a sync
protocol keeps both sides in agreement over the existing connection layer.

- Contracts: ProjectOrigin on projects, mirror.* RPCs, mirror:sync scope,
  projectMirroring capability, t3.json mirror.include allowlist.
- Server: GitSync snapshot/bundle/three-way-apply primitives, signed
  GET/PUT bundle route, MirrorService (host broker + sync state machine,
  restart-safe queued apply-backs), MirrorAgent (origin side), turn-start
  sync gate with an explicit stale-run override, post-checkpoint apply-back.
- Web: capability-gated "Files live on another machine" creation flow and
  a mirror status chip with Sync now and conflict listing.
- Docs: internals section in remote.md, user guide, glossary entries.

Implemented by Claude Fable 5 via Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mirror.createPeerCredential required access:write, which only admin-scoped
(startup) pairings carry, so a normally paired client could not finish
mirror setup. The endpoint mints a token scoped to mirror:sync only, so
orchestration:operate - the scope that already permits running agents -
is the right gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirroring is built on git, but the folder itself didn't need to be a
repository already — a plain folder now gets one initialized in place on
attach instead of being refused outright, with GitSync's seed/apply
paths fixed to handle the resulting unborn-HEAD, no-branches state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Settings → General → Developer tools → React Grab overlay, letting
contributors hover T3 Code's own UI and copy component stack/source
locations for an agent. Gated so it can never reach a shipped build:
the loader script is injected only by vite dev (apply: "serve"), the
package stays a devDependency, and both the settings row and the hook
bail outside import.meta.env.DEV — enforced by reactGrabBoundary.test.ts.

The loader (reactGrabDevEntry.ts) has to run before react-dom evaluates
so React Grab's fiber instrumentation can see React mount, which a
same-module dynamic import can't guarantee; a separate injected
<script type="module"> ahead of main.tsx's own tag does. It also works
around react-grab's init({enabled:false}) returning a permanently inert
stub instead of a toggleable instance, and installs a live stylesheet
so turning the setting off fully hides the overlay instead of leaving
react-grab's own collapsed-toolbar affordance in the page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ving empty gitlink stubs

Gitlinks (mode 160000) were never followed by the mirror's bundle/read-tree
pipeline, so submodules and unregistered nested repos always mirrored as
empty directories. Detect gitlinks via `git ls-tree`, and mirror each one
through the same seed/sync/checkout primitives already used for the
superproject, whether or not the path is registered in `.gitmodules`. A
gitlink with no local copy anywhere on the origin is a per-path warning,
not a failed sync. Includes a fix so a plain resync also picks up gitlinks
that predate this feature (previously only newly-changed gitlinks were
retried), plus two git-behavior bugs found along the way: isRepository
misdetecting an uninitialized submodule path as already a repo, and
`git fetch` triggering unwanted submodule auto-recursion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Setup Worktree script depends on .env and infra/relay/.env, which
are gitignored and so are skipped by mirror sync by default.
Adds a "Sync gitignored env files" setting in project settings for
mirrored projects, storing a mirrorIncludeIgnoredFiles flag alongside
the existing defaultThreadEnvMode per-project override. The host sends
the resolved extra include patterns (.env, .env.local, .env.*.local)
to the origin on mirror.connect; both sides union them into their
sync-snapshot include paths so no t3.json edit is required.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The mirror.connect stream is long-lived, so a project's
mirrorIncludeIgnoredFiles toggle only reached the origin at the next
reconnect, not the next sync. Send a settings-updated event with the
freshly resolved extra include paths right before every directive, so
toggling the setting takes effect on the very next sync/apply-back
(root or submodule) without needing the connection to drop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`git add` is fatal on an unmatched pathspec and then adds NONE of its
arguments, so one missing entry (.env.local when only .env exists)
silently dropped every include path from the snapshot — gitignored env
files never synced. Add each include path in its own git add
invocation so a missing pattern only skips itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plain `.env` pathspecs only match at the repo root, but monorepos keep
their env files in per-app subfolders (apps/api/.env), so the toggle
synced nothing for exactly the projects that need it most. Switch the
patterns to `:(glob)**/...` pathspecs and exclude node_modules matches
from every include force-add, since dependency trees are never synced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A stale mirror link (host project deleted or detached) made the origin
agent retry mirror.connect every 15 seconds forever, flooding the log
with MirrorProjectNotMirroredError. That answer is definitive, so stop
the agent instead and log a single warning. The link row is kept; a
real re-attach restarts the agent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Deleting a mirrored project now sends link-revoked to a connected
  origin (previously the directive had no producer), so the origin
  drops its mirror_links row and token instead of leaving a stale
  leftover; the host's sync watermark is cleared too.
- New mirror.listLinks RPC plus a "Shared folders" section in
  Connections settings: see every folder a device shares to a host and
  remove leftovers via the existing mirror.detach.
- Seed/sync transfer progress: the bundle upload route counts received
  bytes (with Content-Length totals) into MirrorBundleTransfer, the
  status stream republishes once a second while a transfer is in
  flight, and the mirror chip shows "Seeding · 30%" with a progress
  bar in the popover.
- Mirror chip popover shows copyable origin and host mirror paths.
- Project settings shows a "Mirrored project" info row (origin
  machine, path, live state); the Danger-zone copy explains deletion
  also disconnects the origin.
- Sidebar thread rows show a mirror glyph on mirrored projects and the
  hover tooltip names the machine holding the files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c435e8c-7044-42af-bf7d-6f7b6a08d612

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 11, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review of the new apps/server/src/mirror/* services and the packages/contracts/src/mirror.ts error contracts. Service definition shape (Context.Service with inline interface, make, layer, Foo["Service"] annotations, subpath namespace imports) looks consistent with the conventions.

Findings, all in the error-modelling and dependency-acquisition rules:

  • MirrorSyncFailedError models failures as one unstructured detail string, derives message from it, and has no cause; every production wrap sets detail: cause.message / detail: String(cause), dropping the underlying error chain.
  • MirrorAgent's transfer wraps stringify the HTTP failure into detail, which carries the HMAC-signed single-use bundle URL into a caller-visible error.
  • MirrorService.syncFailed is a pass-through error constructor helper, and failPending fabricates projectId: "" as ProjectId when the real project id is in scope.
  • Effect.catchTag used instead of Effect.catchTags.
  • SubmoduleSync and mirrorInclude take service instances as parameters instead of acquiring them from the Effect environment, hiding those requirements from the types.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/mirror/MirrorAgent.ts Outdated
Comment thread apps/server/src/mirror/MirrorService.ts Outdated
Comment thread apps/server/src/mirror/mirrorInclude.ts
Comment thread packages/contracts/src/mirror.ts
Comment thread apps/server/src/mirror/SubmoduleSync.ts Outdated
Comment thread apps/server/src/mirror/MirrorService.ts Outdated
Comment thread apps/server/src/mirror/MirrorAgent.ts Outdated
Comment thread apps/server/src/mirror/MirrorService.ts
Comment thread apps/server/src/auth/RpcAuthorization.ts
Comment thread apps/server/src/orchestration/Layers/CheckpointReactor.ts Outdated
Comment thread apps/server/src/mirror/GitSync.ts Outdated
Comment thread apps/server/src/orchestration/Normalizer.ts Outdated
Comment thread apps/server/src/mirror/MirrorBundleTransfer.ts
Comment thread apps/server/src/mirror/GitSync.ts Outdated
Comment thread apps/server/src/mirror/MirrorAgent.ts
Comment thread apps/server/src/mirror/MirrorAgent.ts
Comment thread apps/server/src/mirror/MirrorService.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. Diff is too large for automated approval analysis, so this PR cannot be approved automatically.

Not approved because:

  • 2 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

# Conflicts:
#	apps/web/src/components/chat/ChatHeader.tsx
#	apps/web/src/components/settings/ProjectSettingsPanel.tsx
…ctness, and consistency

Fixes raised by Macroscope's review of pingdotgg#6157:

Security:
- Bind mirror.connect/mirror.respond to the caller's authenticated peer
  subject so a peer token minted for one project can't act as the origin
  for another (ws.ts, MirrorService.ts).
- Reject project ids that would let a mirrored project.create escape
  mirrorsDir via path traversal (Normalizer.ts).
- Reject submodule directive paths that escape the project root on the
  origin agent (MirrorAgent.ts).
- Stop leaking the signed single-use bundle URL into user-visible error
  detail on transport failures (MirrorAgent.ts).
- Check the bundle transfer token's direction before consuming it, so a
  wrong-method request can't burn the one legitimate use (MirrorBundleTransfer.ts, http.ts).
- Require the stale-run retry to resend the exact failed message, not just
  any message on the same thread within the offer window (ProviderCommandReactor.ts).

Correctness:
- Stage (snapshot + persist) a post-turn apply-back synchronously under the
  project lock before turn completion is signaled; only the network
  delivery to the origin is backgrounded. Fixes a race where a
  fast-following turn's ensureFresh could overwrite the mirror before the
  completed turn's edits were captured (CheckpointReactor.ts, MirrorService.ts).
- Fix reconnect teardown ordering so a superseded connection's in-flight
  requests are failed immediately instead of leaking until their timeout (MirrorService.ts).
- Remove a staged submodule bundle on every non-success exit, not just the
  success path, to stop bundle files accumulating in staging (MirrorService.ts).
- Parse git ls-tree/merge-tree output with -z so paths with special
  characters aren't mishandled during conflict resolution, and handle
  submodule gitlink entries (mode 160000) alongside blobs (GitSync.ts).
- Propagate branch-update failures during apply instead of ignoring them
  while still marking the apply successful (MirrorAgent.ts).
- Guard against a double Enter-key submission creating duplicate mirrored
  projects (CommandPalette.tsx).
- Return early on every detach failure, not just non-interrupted ones, so
  an interrupted failure doesn't show a false success toast (MirrorLinksSection.tsx).

Other:
- Align the bundle transfer token TTL with the 30-minute seed timeout.
- Use Effect.catchTags consistently for single-tag recovery.
- Remove a docs contradiction about non-git folders.
Comment thread apps/server/src/mirror/MirrorService.ts
Comment thread apps/server/src/mirror/GitSync.ts Outdated
Comment thread apps/web/src/components/settings/MirrorLinksSection.tsx
Comment thread apps/server/src/mirror/MirrorAgent.ts
Comment thread apps/web/src/components/settings/ProjectSettingsPanel.tsx Outdated
Comment thread apps/server/src/mirror/MirrorService.ts Outdated
Comment thread apps/server/src/mirror/MirrorBundleTransfer.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
Comment thread apps/server/src/ws.ts
Comment thread apps/web/src/components/CommandPalette.tsx
…ork-log warnings

The claude-agent-sdk build running here emits a command_lifecycle message
type ahead of what the pinned SDK's TypeScript types declare, so it fell
through the adapter's exhaustiveness guard into an opaque
"Claude SDK message 'command_lifecycle' — command_uuid: ... · state: ..."
runtime-warning row in the thread's work log. It's internal bookkeeping
with no T3 surface, so consume it deliberately like prompt_suggestion.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One new finding on error payload boundedness in GitSync.ts. The other convention issues I flagged in the previous run (unstructured MirrorSyncFailedError, the syncFailed/gitFailed construction helpers, GitSync["Service"]/FileSystem+Path injection in SubmoduleSync.ts and mirrorInclude.ts) are still open in this revision; I have not re-posted those threads.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/mirror/GitSync.ts Outdated
Fixes raised by Macroscope's follow-up review of pingdotgg#6157:

Security:
- Revoke any prior mirror-peer session for a project before issuing a new
  peer credential, so a stale token from a previous origin can't reconnect
  and displace the live connection (ws.ts).

Correctness:
- Fast-forward the checked-out branch via applyBranchUpdatesToCurrent in
  ingestBranchUpdates instead of just resetting the index — it was left on
  the old commit while the working tree had already moved (MirrorService.ts).
- Fail on staging-directory creation instead of swallowing it, so the
  bundle-transfer layer doesn't silently start in a broken state (MirrorBundleTransfer.ts).
- Delete the mirror link row at the point link-revoked is detected, not
  after the (now-failed) stream returns — the failure was short-circuiting
  past that check, so a revoked link retried with its dead token forever (MirrorAgent.ts).
- Show a failed-to-load state on shared folders instead of an
  indistinguishable empty state (MirrorLinksSection.tsx).
- Derive the deletion marker's OID width from a real oid in the same repo
  instead of assuming SHA-1, so SHA-256 repos don't break conflict
  resolution (GitSync.ts).
- Thread priorState through processGitlink's recursive call so nested
  submodules-of-submodules resolve their real prior oid instead of always
  re-seeding from scratch (MirrorService.ts).
- Base isMirrored on any checkout in the project group, not just the
  representative, so a mirrored non-representative checkout isn't hidden (ProjectSettingsPanel.tsx).
- Use monotonic time (not wall-clock) for the stale-run offer window so a
  clock correction can't extend or collapse it (ProviderCommandReactor.ts).
- Roll back the host-side project on any mirror setup failure after
  creation, so a broken origin folder doesn't leave an orphaned project
  behind on every retry (CommandPalette.tsx).

Documented as an accepted trade-off rather than fixed: the bundle-transfer
single-use token registry is in-memory only, so a restart within the TTL
window allows one replay of an already-consumed token.
…aw argv/stderr

args can carry a remote URL with embedded credentials (git remote add/set-url),
and this error's message flows into MirrorSyncFailedError.detail and from
there the RPC boundary, status, and logs. Keep args/stderr as structured
fields but derive message from bounded attributes (subcommand, root, exit
code, stderr length) only.
Comment thread apps/server/src/ws.ts Outdated
Comment thread apps/server/src/mirror/MirrorService.ts
Comment thread apps/server/src/mirror/MirrorService.ts
Comment thread apps/server/src/mirror/GitSync.ts
Comment thread apps/server/src/mirror/GitSync.ts
Comment thread apps/server/src/mirror/MirrorAgent.ts
Comment thread apps/web/src/components/CommandPalette.tsx
Comment thread apps/server/src/orchestration/Layers/ProviderCommandReactor.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One new finding on error modeling in the mirror service code; the findings from the previous runs on this PR (unstructured MirrorSyncFailedError.detail, the syncFailed construction helper in MirrorService.ts, GitSync["Service"] passed as a parameter in SubmoduleSync.ts, and readMirrorIncludePaths closing over FileSystem/Path instead of yielding them from the environment) are still open and are not re-posted here.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/mirror/MirrorAgent.ts Outdated
Keeps ProviderCommandReactor.test.ts within the no-manual-effect-runtime-in-tests
legacy baseline (70 occurrences), fixing the failing CI lint check on PR #1.
# Conflicts:
#	apps/web/src/AppRoot.tsx
#	apps/web/src/components/settings/settingsSearch.ts
#	packages/contracts/src/environment.ts
- Move mirror-link revocation off project delete into a
  MirrorProjectDeletionReactor triggered by the project.deleted domain
  event, so it covers every dispatch transport (WS, HTTP, offline CLI),
  not just the WebSocket handler. It now also revokes the mirror-peer
  auth session so a disconnected origin's credential can't outlive the
  project.
- Fix mirrorCreatePeerCredential to issue the replacement session before
  revoking the old one, so a failed issue doesn't strand the origin
  without any working credential.
- Gate the mirror freshness check before first-turn branch rename/title
  generation side effects, so they can't run against a stale mirror.
- Resolve mirror-add's explicit relative path against the origin browse
  cwd instead of null, and await the host-project rollback (surfacing a
  toast on failure) instead of firing it and forgetting.
- Bound GitSyncCommandError's serializable fields (subcommand,
  stderrLength) instead of carrying raw argv/stderr, which could embed
  remote credentials.
- Close MirrorAgent's manager scope via a finalizer, and add the missing
  gap/motion-reduce classes flagged on ChatHeader and MirrorStatusChip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two Effect service-convention findings on the new MirrorProjectDeletionReactor. Previously reported items that are unchanged (e.g. syncFailed, SubmoduleSync/mirrorInclude service-instance parameters, MirrorNotARepositoryError.detail) are not repeated here.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/orchestration/Layers/MirrorProjectDeletionReactor.ts Outdated
Comment thread apps/server/src/mirror/GitSync.ts Outdated
Comment thread apps/server/src/mirror/MirrorAgent.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: the new "Shared folders" settings section is not gated by the projectMirroring capability the way the rest of the mirroring UI is, so it surfaces (and can render a permanent error row) on environments whose server doesn't support mirroring.

The previously flagged breadcrumb gap and the reduced-motion guard on the transfer progress bar are both resolved.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
malekelkssas and others added 2 commits August 19, 2026 20:33
- Fix listRemotes' regex to allow remote URLs/paths containing spaces
  (e.g. a mirror workspace under "My Project"), which previously fell
  through unmatched and caused setRemotes to re-add an existing remote.
- Resolve symlinks before the submodule path containment check, so a
  symlink inside the working tree can't be used to point a submodule
  seed/apply/sync operation outside the project root.
- Thread MirrorService/EnvironmentAuth as Effect requirements in
  revokeMirrorLinkAndCredentials instead of passing service instances as
  plain parameters, and mock them via Layer.mock in the test instead of
  `as unknown as` casts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oring

MirrorLinksSection queried mirror.listLinks against every saved
environment, including ones running an older server without the RPC
method, which rendered a permanent "unavailable" row and broke the
version-skew contract CommandPalette already honors elsewhere. Filter
to environments that advertise projectMirroring before handing them to
the section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread apps/server/src/mirror/GitSync.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding in the new mirror UI. The previously raised point about MirrorLinksSection querying mirror.listLinks on environments that never advertised projectMirroring is still unaddressed, but I'm not re-posting it here.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MirrorStatusChip.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI consistency review of the changed web surfaces (mirror chip/settings rows, sidebar mirror glyph, command palette mirror flow, React Grab settings row). Earlier findings (breadcrumb gap, reduced-motion guard on the transfer bar, capability-gated MirrorLinksSection) are resolved on this head. One minor labeling nit left.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MirrorStatusChip.tsx Outdated
…gg#6157

- Retry mirror-link/credential revocation a few times with backoff
  before falling back to a logged best-effort skip, since it's consumed
  exactly once off a hot domain-event stream and a transient failure
  previously dropped the revocation for good.
- Treat merge-tree exit code 1 with no listed conflict paths as a
  failure instead of silently applying a tree that may still contain
  raw conflict markers.
- Give the two path-copy buttons in MirrorStatusChip's popover distinct
  aria-labels (origin vs. mirror) instead of both saying "Copy link".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread apps/server/src/mirror/GitSync.ts Outdated
Comment thread apps/server/src/mirror/GitSync.ts
malekelkssas and others added 2 commits August 20, 2026 15:39
CI's Check job flagged two no-manual-effect-runtime-in-tests lint
errors in the new reactor test. Switch to @effect/vitest's it.effect,
matching the convention every other Effect test file in this repo uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Sync

- ls-tree now runs with -r so a conflict path that's a directory on the
  preferred side (a file replaced by a folder of the same name) expands
  to every blob/gitlink beneath it, instead of being silently dropped by
  the blob/commit-only match and leaving merge-tree's synthetic subtree
  in the final result. Clears any bare-file entry at the conflict path
  first, since an index can't hold both a blob and nested entries there.
- ls-tree and update-index --index-info both run with -z, so a
  conflicted path containing a literal tab or newline round-trips
  correctly instead of being quoted/escaped or split across records.
- Added a test covering a file-vs-directory type conflict end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread apps/server/src/mirror/GitSync.ts Outdated
malekelkssas and others added 2 commits August 20, 2026 16:01
applyBranchUpdates used allowNonZeroExit on `git worktree list
--porcelain` and never checked for truncated output, so a failing or
truncated discovery silently degraded to "no other worktrees have this
branch checked out" instead of blocking the ref update — the opposite
of the guarantee this check exists to provide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
diffGitlinks/discoverAllGitlinks took GitSync as a plain parameter
instead of acquiring it from the environment, so the git dependency
was invisible in their returned effects' requirements. Callers already
hold a resolved instance, so they provide it locally instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread apps/server/src/mirror/SubmoduleSync.ts
Comment thread apps/server/src/mirror/MirrorService.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: the new "Shared folders" settings section renders its heading and intro paragraph with no body for users who have no mirror links, which is the default state for everyone. Everything else in the changed web UI (mirror status chip, sidebar glyph/tooltip, command-palette mirror flow, project mirroring row, developer-tools row) follows the existing primitives and settings-layout conventions, and the earlier findings on this PR (copy-button accessible names, breadcrumb gap, reduced-motion guard on the transfer bar, capability filtering for mirror links) are addressed.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/settings/MirrorLinksSection.tsx
…otARepositoryError

Both errors derive their message from a detail string that production
call sites built as `cause.message`, discarding the underlying error
entirely. Added an optional cause: Schema.Defect() field (the same
pattern already used elsewhere in packages/contracts) and threaded the
real cause through at every call site that had one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@malekelkssas

Copy link
Copy Markdown
Author

Follow-up on the "Effect Service Conventions" check's retained-findings summary (commit bbec5bd):

Fixed:

  • SubmoduleSync.ts: diffGitlinks/discoverAllGitlinks now acquire GitSync from the environment instead of taking it as a parameter.
  • MirrorAgent.ts / MirrorService.ts: every MirrorSyncFailedError/MirrorNotARepositoryError built from a caught error now preserves it via a new cause: Schema.optional(Schema.Defect()) field (same pattern as FilesystemBrowseError etc.), instead of only copying .message.

Deliberately not changed, with reasoning:

  • MirrorProjectDeletionReactor's Services/+Layers/ split — matches this directory's own 100%-consistent convention (every other reactor here does the same split); already covered in an earlier reply.
  • mirrorInclude.ts's readMirrorIncludePaths({ fileSystem, path }) closure — converting it to acquire FileSystem/Path from the environment is straightforward in isolation, but every caller's helper functions have explicit Effect.Effect<...>/Effect.fn.Return<...> return-type annotations pinning their requirements to never (or a narrow error-only union), so the change cascades into ~8 unrelated signatures across MirrorService.ts/MirrorAgent.ts with materially higher regression risk than the contained fixes above. Not something I want to push through without dedicated review.
  • MirrorService.ts's syncFailed factory — kept as-is; only widened it to accept an optional cause so it stops discarding the underlying error, which was the actual defect. Inlining it at each call site is a pure style preference with no behavior change.

Happy to take another pass at the mirrorInclude refactor as a focused follow-up if that's wanted.

Comment thread apps/server/src/mirror/MirrorAgent.ts Outdated
malekelkssas and others added 2 commits August 20, 2026 16:33
revokeLink deleted mirror_sync_runtime without holding the same
per-project lock every sync/apply-back core runs under, so an in-flight
operation's later saveRuntime call could recreate the row right after
it was cleared — letting a subsequent relink reuse stale state instead
of reseeding cleanly.

fix(web): show an explicit empty state for Shared folders

MirrorLinksSection rendered its heading and intro paragraph whenever
any environment advertised mirroring, even when every environment had
zero links, leaving a bodyless heading — the common case. Track
per-environment content status and render a "No shared folders" row
once every environment has reported none, matching the empty-state
convention used elsewhere on this page (AccessRows).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g path

agentStagingPath interpolated directive.syncId directly into a file
path; since it's host-controlled input (untrusted, per this module's
own trust boundary) with no character restriction, a compromised host
could set it to a traversal sequence and make this origin-side agent
write or delete files outside stagingDir. Percent-encode it first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/settings/MirrorLinksSection.tsx
Comment thread apps/server/src/mirror/MirrorAgent.ts Outdated
yield* hooks.afterMirrorChanged({ projectId, workspaceRoot: root });
// The mirror moved under any queued apply-back; recompute it so a
// stale snapshot can never regress the origin working copy.
const pending = runtime.pendingApplyJson;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical mirror/MirrorService.ts:1222

A following ensureFresh can overwrite host edits captured by queueApplyBack, permanently losing the queued apply-back. The pending state is checked only after applySnapshot runs with conflictPreference: "target", so freshness can win the project lock, replace the working tree, and then clear or restage the pending apply from the already-overwritten tree; deliver or merge the persisted pending snapshot before applying the new origin snapshot.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/mirror/MirrorService.ts around line 1222:

A following `ensureFresh` can overwrite host edits captured by `queueApplyBack`, permanently losing the queued apply-back. The pending state is checked only after `applySnapshot` runs with `conflictPreference: "target"`, so freshness can win the project lock, replace the working tree, and then clear or restage the pending apply from the already-overwritten tree; deliver or merge the persisted pending snapshot before applying the new origin snapshot.

for (const link of targetLinks) {
if (diffedPaths.has(link.path)) continue;
const fullPath = pathPrefix === "" ? link.path : `${pathPrefix}/${link.path}`;
if (priorState[fullPath] !== undefined) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High mirror/MirrorService.ts:931

Uncommitted edits inside an existing submodule are never mirrored when its superproject gitlink OID is unchanged, so manual and turn-start syncs silently miss those changes. The unchanged-gitlink pass skips every path present in priorState; remove that skip so known submodules are polled for submodule-sync-requested responses.

-        if (priorState[fullPath] !== undefined) continue;
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/mirror/MirrorService.ts around line 931:

Uncommitted edits inside an existing submodule are never mirrored when its superproject gitlink OID is unchanged, so manual and turn-start syncs silently miss those changes. The unchanged-gitlink pass skips every path present in `priorState`; remove that skip so known submodules are polled for `submodule-sync-requested` responses.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding on the new mirror settings section; everything else in the changed web UI (mirror status chip, sidebar glyph, command palette flow, project settings rows, dev-tools switch) follows the existing primitive and settings-layout conventions, and the issues raised in earlier runs (empty state, copy-button labels, capability filtering, breadcrumb gap, reduced-motion guard) are resolved at this head.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/settings/MirrorLinksSection.tsx
malekelkssas and others added 2 commits August 20, 2026 16:39
…nks warning

className="text-warning" on SettingsRow had no visible effect since
the row's own children (h3, description p) set their color explicitly.
Wrap the error text in a span instead, matching the pattern used by
ConnectionsSettings' WSL failure row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Aug 23, 2026

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Introduces client-host filesystem mirroring and changes project ownership across 85 files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants