fix(pairing): backport upstream mobile pairing and tailscale endpoint fixes - #8571
fix(pairing): backport upstream mobile pairing and tailscale endpoint fixes#8571enisze wants to merge 157 commits into
Conversation
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-project "Default agent" selector so a project can pin a specific provider instance (account) and model. New threads in that project start on that account, still overridable per thread. The selection persists to the project's existing defaultModelSelection, so no contract or server changes were needed. The field reads the value live from the projects atom rather than the dialog's captured snapshot, and keeps a removed/disabled instance visible as "(unavailable)" so a stale pin can be cleared instead of silently vanishing. Wired into both the V1 and V2 sidebar project dialogs, plus a collapsible Providers-settings guide explaining how to set up two accounts of the same provider and route each to a different project. Co-authored-by: Claude <noreply@anthropic.com>
…oject (#3) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-file "viewed" checkbox to the diff panel file headers so reviewers can track review progress. Toggling it marks the file viewed and collapses it (unchecking expands), mirroring the standard code review workflow. Viewed state is persisted to localStorage via a new diffViewedStore, scoped per thread + diff selection (same scope key used for collapse), and folded into the CodeView item version hash so headers re-render reliably when the flag changes. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Diff theme" dropdown to Settings → Appearance that lets users choose the syntax highlighting theme for code diffs from a curated set of themes bundled by @pierre/diffs (Pierre + Shiki collections). - New `diffTheme` client setting (contracts) with `pierre-dark` default, persisted and synced like other client settings and wired into the "Restore defaults" flow. - `resolveDiffThemeName` now maps dark mode to the selected theme and light mode to pierre-light; a `useDiffThemeName` hook makes every diff surface (DiffPanel, ChatMarkdown, worker pool, timeline, file preview) react live to the setting. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…trap thread.create (#6) Bootstrapping a draft is a create-if-absent operation: the client generates the threadId before the thread exists, so a duplicate send, a retry, or an already-promoted draft can target a threadId the server already has. Previously that hard-failed the whole turn with an "already exists and cannot be created twice" invariant, surfacing an error banner. Treat that specific invariant as a no-op in the bootstrap path and continue the turn against the existing thread. The message lands, the thread starts, and the draft route navigates the user into the already-created chat. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Persist project worktree branch prefixes - Scope GitHub CLI credentials to the selected account
Adds a "Merge PR" item to the git actions dropdown alongside Commit / Push / View PR, shown whenever there is an open pull request. Merging uses a merge commit and keeps the head branch. Wires the action through the existing source-control provider abstraction so it works across all providers: - GitHub: gh pr merge --merge - GitLab: glab mr merge --yes - Azure DevOps: az repos pr update --status completed --delete-source-branch false - Bitbucket: pull request merge API (merge_commit, close_source_branch false) Includes the new git.mergePullRequest RPC, GitManager/GitWorkflowService methods, WS handler, auth scope, client atom + hook, and UI wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(web): add merge PR action to git dropdown
Per-project GitHub account pinning covered `git push`/`fetch` and the integrated terminal, but not the `git` processes `gh` itself spawns: `gh pr create` pushing a branch with no upstream, and `gh pr checkout` fetching, still fell back to the machine credential helper and acted as whichever account it had cached. `GitHubCli.execute` now passes the full auth env instead of the `gh`-only one. Also hardens the env itself: the `GIT_CONFIG_*` pairs are appended after any `GIT_CONFIG_COUNT` already in the parent environment, so an inherited runtime git config is no longer silently overwritten by our pairs claiming indices 0 and 1. A failed or empty `gh auth token` now logs a warning rather than falling back to the ambient account in silence. Adds the tests this path was missing (env shape, inherited-config offset, gh child-process env, terminal spawn env), fixes a `worktreeBranchPrefix` fixture that broke typecheck, and updates the docs note that still claimed plain git pushes were unscoped. Verified against two real gh accounts: with the pin, git authenticates as the selected account while a different one is globally active; without it, git resolves a stale OS keychain entry. Claude Opus 5 (1M context) via Claude Code
Surfaces an in-thread affordance to open a fresh chat pointed at the
current thread's worktree, reusing the existing git worktree instead of
provisioning a new one. Mirrors the sidebar's "New thread on {branch}"
action.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rations (#11) Account switching was opt-in per call site, so the paths that mattered most never picked up the project's account: the background fetch that refreshes the branch list / ahead-behind counts, `git pull`, and several `fetch*` variants all ran with no account token and authenticated as whichever account `gh auth switch` last made active machine-wide. That forced a manual `gh auth switch` just to see the right branches. - Centralize network-git auth in `resolveNetworkGitEnv` and route every remote-touching git command through it (status fetch, pull, push, all fetch variants) so account selection can't be silently skipped. - Rewrite the account host's SSH remotes to HTTPS (url.insteadOf, scoped to the process) so the token applies even for `git@host:` remotes. - Surface an unusable selected account (expired/revoked token) in the GitHub account selector instead of silently falling back to the machine default; the per-account reason from `gh auth status` was already parsed but dropped before reaching the UI. Commit authorship is intentionally left on local git config. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
In wrap mode a long line spans several visual rows, but the virtualizer estimates every off-screen line at a single row. That under-counts total height, so the scrollbar bottoms out before the last file's trailing lines can be scrolled into view. Over-estimate the row height only in wrap mode; the library shrinks it back via measured deltas as items render, so the last file stays reachable without a permanent gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a tab row beneath the chat header listing every chat in the active thread's worktree, with the current one highlighted, click-to-switch, and a trailing button to start a new chat in the same worktree. Collapse the "Open in editor" and "Add action" header controls behind a single "…" overflow menu so the header keeps room for the project and thread identity. The controls render unchanged inside the popover, so their dropdowns and add/edit dialogs keep working. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merging a PR could fail with the opaque "GitHub CLI command failed.",
which was especially confusing with multiple GitHub accounts where the
account selected for a project isn't a collaborator on its repo.
Classify non-zero VCS exits into two new safe buckets — permission-denied
and merge-blocked — from stderr patterns, and map them to clear GitHub
errors ("the selected account lacks permission…", "GitHub wouldn't merge
this PR — conflicts, failing checks, branch protection, or a disallowed
merge method"). Raw stderr is still never retained, so no secret it might
contain can leak.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diff wrap cutoff fix, worktree chat tabs, and actionable gh merge errors
…t use Merge: - `gh pr merge` now queries the repo's allowed merge methods and picks a permitted one (merge > squash > rebase) instead of hardcoding `--merge`, which failed on squash-/rebase-only repositories. GitHub account selection: - Resolve a per-project account to a three-way outcome (resolved / ambient / unavailable) instead of silently falling back to the machine's active account. When a project has an account attached but its token can't be minted (not logged in to `gh`), `gh` and raw `git` network operations now refuse with an actionable "account not logged in" error rather than acting as the wrong user. Interactive terminals stay best-effort. - Add `ProjectionSnapshotQuery.listAccountRoutes()` mapping workspace roots and worktree paths (active AND archived threads) to their account, so a worktree belonging to an archived thread still resolves to the right account instead of falling back to ambient. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(server): adaptive PR merge method and reliable per-project account use
A merge fired right after a PR is opened/pushed loses a race: GitHub still reports the mergeable state as UNKNOWN and rejects the merge with a transient "not mergeable" / "base branch was modified" error, which the classifier can only surface as a hard merge-blocked failure. mergePullRequest now polls `gh pr view --json mergeable,mergeStateStatus` (bounded) until GitHub finishes computing mergeability before attempting the merge, and retries the merge itself a few times on the transient merge-blocked kind. Genuine conflicts, permission problems, and protection rules still fail immediately. Also corrected the merge-blocked message, which wrongly blamed the merge-commit method even though we now always pick an allowed method. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…state fix(server): stop merge button racing GitHub mergeability computation
Chats spawned into the same git worktree via the in-chat worktree tab strip no longer each claim their own sidebar row. A worktree now shows as a single row — the earliest-created chat — and its siblings are reachable only through the tab strip. Chats with no worktree are unchanged. Add collapseWorktreeSiblings to Sidebar.logic and apply it in both the v1 sidebar (per project) and the v2 sidebar (before active/snoozed/settled partitioning). When the active route is a collapsed sibling, its representative row highlights, pins when collapsed, gets pulled into view in the settled/snoozed shelves, and keyboard traversal steps relative to it. The v1 project status dot still reads every non-archived thread so a busy sibling is never hidden behind its row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(web): collapse same-worktree chats into one sidebar row
fix(web): keep the last diff file reachable with word wrap
- Add header actions to open branch diffs and insert review prompts - Make the review prompt configurable in source control settings
feat(web): add branch changes and review actions
- Open working tree changes from the chat header - Add changed-file summary controls to the diff panel - Support forced new drafts with an initial review prompt
feat(web): start reviews in a new chat
feat(web): close chats from worktree tabs
…n-reload fix(web): preserve file tree expansion state across reloads
pushCurrentBranch failed hard on a non-fast-forward rejection, leaving the user to fetch/rebase/push by hand. Now, when a push is rejected only because the remote branch has commits the local branch doesn't, the driver fetches that remote branch, rebases the local commits onto it, and retries the push once. A rebase that hits conflicts is aborted so the working tree is never left mid-rebase, and the conflict is surfaced with actionable detail. Auth/branch-protection failures still fail fast. Applies to all remote-targeted push paths (existing upstream, new upstream, and explicit remote). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(vcs): auto-rebase and retry a push when the remote moved ahead
Reopening a branch through a worktree thread failed with an opaque "git worktree add failed" whenever git refused the add. Handle the two cases the caller cannot fix by hand: - the branch is already checked out in another worktree — hand that worktree (and the threads rooted at it) back instead of failing, and explain clearly when the claim is the main repo checkout. - the derived directory is taken because a worktree kept its directory after its branch was switched — fall back to a sibling `<name>-2`. Prune-and-retry once for a stale worktree whose directory was removed behind git's back. All of this runs on the failure path only, so a successful add stays a single spawn. Resolve GitHub account routes through symlinks: a cwd and the stored project root routinely name one directory via different symlinks (/tmp vs /private/tmp, symlinked home or worktree dirs). On a literal miss, resolve both sides' real paths (cached) before falling back to the ambient account, and sharpen the token-failure warnings. Preserve the previous draft when opening a review chat beside the current one, so a fresh review draft stays attached to its worktree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h-threads feat(vcs): open the existing worktree that already holds a branch
Read a not-yet-sent composer draft's worktree/environment/project so the shared diff, git status, and branch preview load when the panel opens, instead of only after the first message promotes the draft to a server thread. Drafts with no worktree keep the empty per-chat panel, matching the pre-draft behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Review button (and "new chat in this worktree") targeted the thread's persisted `branch` field, which drifts from the worktree's actually checked-out branch after a checkout, PR checkout, or branch switch. The footer branch chip already prefers the live git ref, so the two diverged and a review could run against a stale reference. Resolve the new-chat branch the same way the footer does — live `gitStatus.refName`, falling back to the stored branch — for worktree-backed chats, so a review always follows the branch shown in the chat. Local chats keep the stored value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…out-submitting-chat feat(diff): show worktree changes for a draft + fix review to use the live branch
A worktree's collapsed sidebar row is positioned by its newest surviving chat — collapsing keeps the group at that chat's slot. Closing (archiving) the newest chat dropped it from the list, so the row fell back to an older sibling's timestamp and sank down the sidebar, even though closing a chat is itself a recent interaction. Record closing a chat as worktree activity in a persisted client store (`useUiStateStore.worktreeLastActivityAtByKey`, keyed by the same environment+worktree key the collapse uses) and fold that timestamp into the sidebar sort: each chat's effective sort time is the max of its own activity and its worktree's recorded activity. The row now keeps its place instead of sinking. Local (non-worktree) chats and callers that omit the map are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-close fix(sidebar): keep worktree row ordered after closing a chat
Sending made the new user message a LegendList anchoredEndSpace target, which reserved a viewport-tall blank tail below it and disabled maintainScrollAtEnd. Until a turn outgrew that reserve, the response sat at the top of the timeline with a large gap above the composer. Drop the anchored tail: the list now always maintains scroll at the end, so a streaming turn hugs the composer. This removes the anchor plumbing that existed only to position and re-settle that reserve, and replaces timelineScrollAnchoring with timelineScroll, whose overflow probe the follow effect already needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
listRefs already carries the remote in a ref's name (`origin/claude/x`) and repeats it in `remoteName`, but the workspace picker's branch suggestions concatenated both and the pull-request dialog matched refs against that same doubled shape. Selecting a branch that exists only on origin therefore sent `origin/origin/claude/x`, matched no ref, and silently created no worktree — the common case for an agent-pushed branch. Pass and display the ref name as listed, and resolve a reference through findBranchRefForReference, which accepts either the qualified or the plain branch name and prefers a local ref over a remote one. Add a shared stripRemoteRefPrefix that strips only a ref's own remote prefix, so a local `claude/x` keeps every segment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(chat): pin streaming responses to the composer; open remote-only branches as worktrees
…etwork The desktop resolved its LAN address once during startup and cached it in the exposure runtime state, so a machine that moved networks (or renewed its DHCP lease) kept handing pairing links and QR codes the address it had at launch. Phones then couldn't reach the backend even though it had been bound to every interface the whole time. Re-resolve the advertised host on every exposure-state and advertised-endpoint read. The bind host, loopback URL, and port stay untouched, so picking up a new address never needs a backend relaunch. A local-only run — including one that fell back for want of an address — still advertises nothing, since it only listens on loopback until the next relaunch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(desktop): keep the advertised LAN host in sync with the current network
(cherry picked from commit 1d694dc)
… environment (pingdotgg#7086) (cherry picked from commit 035058a)
…ndpoints (pingdotgg#7116) (cherry picked from commit d9c1732)
…ied (pingdotgg#6487) (cherry picked from commit 3bc4fdf)
`sidebarV2GroupByProject` landed in ClientSettings without reaching this fixture, so the suite failed on main and typecheck flagged the object as incomplete. Unrelated to the cherry-picks above, but it has to be green to verify them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
Opened by mistake against upstream — these four commits are already in upstream/main; this was meant as a backport PR on a fork. Closing, no review needed. Sorry for the noise. |
There was a problem hiding this comment.
Effect service conventions: a few issues in the new GitHub account/merge code.
Effect.catchTagis used in four new places; the convention isEffect.catchTags({ ... })even for a single tag.GitHubAccountNotLoggedInErrormanufactures anErrorpurely to fill a requiredcause.
The new ProjectWorktreeFileCopier and GitHubAccountResolver service modules otherwise follow the expected shape (inline Context.Service interface, make + layer, dependencies acquired via yield*, optional-service resolution at consumers).
Posted via Macroscope — Effect Service Conventions
| new GitHubAccountNotLoggedInError({ | ||
| command: "gh", | ||
| cwd: input.cwd, | ||
| host: resolution.account.host, | ||
| login: resolution.account.login, | ||
| cause: new Error("gh could not mint a token for the selected account"), |
There was a problem hiding this comment.
GitHubAccountNotLoggedInError is a pure domain refusal — nothing failed underneath, so this synthetic Error exists only to satisfy the required cause. Consider making cause optional on gitHubAccountFailureFields (cause: Schema.optional(Schema.Defect()), it is used only by this error) and omitting it here; the structural host/login/cwd fields already carry the full story.
Posted via Macroscope — Effect Service Conventions
| Effect.catchTag("OrchestrationCommandInvariantError", (error) => | ||
| isThreadAlreadyExistsInvariantError(error, command.threadId) | ||
| ? Effect.succeed(false) | ||
| : Effect.fail(error), | ||
| ), |
There was a problem hiding this comment.
Prefer Effect.catchTags over catchTag for a statically known tag.
| Effect.catchTag("OrchestrationCommandInvariantError", (error) => | |
| isThreadAlreadyExistsInvariantError(error, command.threadId) | |
| ? Effect.succeed(false) | |
| : Effect.fail(error), | |
| ), | |
| Effect.catchTags({ | |
| OrchestrationCommandInvariantError: (error) => | |
| isThreadAlreadyExistsInvariantError(error, command.threadId) | |
| ? Effect.succeed(false) | |
| : Effect.fail(error), | |
| }), |
Posted via Macroscope — Effect Service Conventions
| Effect.catchTag("GitHubProviderUnavailableError", (error) => | ||
| attemptsLeft <= 1 | ||
| ? Effect.fail(error) | ||
| : Effect.sleep(TRANSIENT_RETRY_INTERVAL).pipe( | ||
| Effect.flatMap(() => retryProviderUnavailable(effect, attemptsLeft - 1)), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
Convention is Effect.catchTags({ ... }) for statically known tags, including a single tag.
| Effect.catchTag("GitHubProviderUnavailableError", (error) => | |
| attemptsLeft <= 1 | |
| ? Effect.fail(error) | |
| : Effect.sleep(TRANSIENT_RETRY_INTERVAL).pipe( | |
| Effect.flatMap(() => retryProviderUnavailable(effect, attemptsLeft - 1)), | |
| ), | |
| ), | |
| Effect.catchTags({ | |
| GitHubProviderUnavailableError: (error) => | |
| attemptsLeft <= 1 | |
| ? Effect.fail(error) | |
| : Effect.sleep(TRANSIENT_RETRY_INTERVAL).pipe( | |
| Effect.flatMap(() => retryProviderUnavailable(effect, attemptsLeft - 1)), | |
| ), | |
| }), |
Posted via Macroscope — Effect Service Conventions
| Effect.catchTag("GitHubMergeBlockedError", (error) => | ||
| Effect.fail( | ||
| new GitHubMergeBlockedError({ | ||
| command: error.command, | ||
| cwd: error.cwd, | ||
| cause: error.cause, | ||
| mergeability, | ||
| }), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
Same here — prefer Effect.catchTags.
| Effect.catchTag("GitHubMergeBlockedError", (error) => | |
| Effect.fail( | |
| new GitHubMergeBlockedError({ | |
| command: error.command, | |
| cwd: error.cwd, | |
| cause: error.cause, | |
| mergeability, | |
| }), | |
| ), | |
| ), | |
| Effect.catchTags({ | |
| GitHubMergeBlockedError: (error) => | |
| Effect.fail( | |
| new GitHubMergeBlockedError({ | |
| command: error.command, | |
| cwd: error.cwd, | |
| cause: error.cause, | |
| mergeability, | |
| }), | |
| ), | |
| }), |
Posted via Macroscope — Effect Service Conventions
| Effect.catchTag("GitHubMergeBlockedError", (error) => | ||
| attemptsLeft <= 1 | ||
| ? Effect.fail(error) | ||
| : Effect.sleep(MERGE_RETRY_INTERVAL).pipe( | ||
| Effect.flatMap(() => attemptMerge(cwd, args, attemptsLeft - 1)), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
Same here — prefer Effect.catchTags.
| Effect.catchTag("GitHubMergeBlockedError", (error) => | |
| attemptsLeft <= 1 | |
| ? Effect.fail(error) | |
| : Effect.sleep(MERGE_RETRY_INTERVAL).pipe( | |
| Effect.flatMap(() => attemptMerge(cwd, args, attemptsLeft - 1)), | |
| ), | |
| ), | |
| Effect.catchTags({ | |
| GitHubMergeBlockedError: (error) => | |
| attemptsLeft <= 1 | |
| ? Effect.fail(error) | |
| : Effect.sleep(MERGE_RETRY_INTERVAL).pipe( | |
| Effect.flatMap(() => attemptMerge(cwd, args, attemptsLeft - 1)), | |
| ), | |
| }), |
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
UI Consistency: 3 issues found
Three issues in the changed web UI:
SidebarV2.tsxreimplements the new sharedProjectGitHubAccountFieldinstead of importing it, so the same per-project control renders with different labels, option text and sentinel values in the two sidebars.TasksDock.tsxterminal-tab close button is hover-only — nofocus-visiblereveal/ring and no coarse-pointer fallback, unlike the sibling tab strips (WorktreeThreadTabs,RightPanelTabs) added in the same PR.GitActionsControl.tsxleaves the Publish-repository flow unreachable after the git-actions menu was replaced by a single primary button; the dialog is still rendered but nothing can open it.
Nothing else flagged: diff theming is routed consistently through useDiffThemeName, tab strips reuse ScrollArea with the shared fade contract, and no CSS/theme files changed.
Posted via Macroscope — UI Consistency
| event.stopPropagation(); | ||
| closeTerminalTab(tab.terminalId); | ||
| }} | ||
| className="flex size-4 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 hover:bg-muted hover:text-foreground group-hover:opacity-100" |
There was a problem hiding this comment.
This close control is opacity-0 and only revealed by group-hover, with no focus-visible styling: keyboard-focusing it leaves an invisible, unringed target, and on a coarse pointer (no hover) it can't be revealed at all. The tab strip added alongside it in this PR (WorktreeThreadTabs) already handles both cases on its close button (focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring max-sm:opacity-100).
| className="flex size-4 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 hover:bg-muted hover:text-foreground group-hover:opacity-100" | |
| className="flex size-4 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring max-sm:opacity-100 group-hover:opacity-100" |
The adjacent “New terminal” + button (line 729) is also a raw button with no focus ring — either give it focus-visible:ring-2 focus-visible:ring-ring or render it as Button variant="ghost" size="icon-sm", which already carries the ring, cursor and disabled semantics.
Posted via Macroscope — UI Consistency
| </PopoverPopup> | ||
| </Popover> | ||
| ) : ( | ||
| <div className="flex shrink-0 items-center gap-2 text-xs"> |
There was a problem hiding this comment.
Replacing the Group + Menu block with this single primary button drops the only two entry points into the publish flow (the open_publish quick action and the “Publish repository…” menu item). setIsPublishDialogOpen(true) now only exists inside runQuickAction, which is itself no longer called, so <PublishRepositoryDialog> at the bottom of this component can never open — a repo with no remote is left with a “Create PR” button that will fail.
Same block also dropped the only surface for gitStatusError and the detached-HEAD warning, so git status failures are now silent in the header.
Could you either re-expose publish (a secondary button when canPublishRepository, or keep the overflow menu) or remove the now-dead code with it? Currently Group/GroupSeparator, Menu/MenuItem/MenuPopup/MenuTrigger, gitActionMenuItems, openDialogForMenuItem, getMenuActionDisabledReason, GitActionItemIcon, GitQuickActionIcon, quickAction/quickActionDisabledReason/runQuickAction and canPublishRepository are all unreferenced.
Posted via Macroscope — UI Consistency
| // query the settings panel uses (scoped to the member's environment) and only | ||
| // offers authenticated accounts. Selecting an option attaches that account to | ||
| // the project; "Use default account" clears it back to the machine default. | ||
| function ProjectGitHubAccountField({ |
There was a problem hiding this comment.
This PR adds a shared ProjectGitHubAccountField (apps/web/src/components/ProjectGitHubAccountField.tsx) and uses it from Sidebar.tsx, but SidebarV2 reimplements the same control locally with a different prop shape and drifting presentation:
- trigger label
login · hostvslogin (host) - placeholder
Default accountvsUse default account - active account marked with a
" (default)"text suffix vs a muteddefaultchip - different sentinel/value encoding (
__default__+\u0000join vs the shared module's own key format)
Same project, two sidebars, two different pickers. Consider importing the shared component here and folding this version's extras (the gh auth login empty state and the stale-token warning) into it, so both surfaces stay identical.
The inline Worktree branch prefix and Preview port fields below have the same problem against the new ProjectSettingsPanel, and they have already drifted: this dialog falls back to the global worktreeBranchPrefix setting for the placeholder/helper text, while ProjectSettingsPanel always shows the built-in WORKTREE_BRANCH_PREFIX, so the two surfaces advertise a different inherited default for the same project.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0763091. Configure here.
| } | ||
| case "document": { | ||
| return `${attachment.id}${inferDocumentExtension(attachment.name)}`; | ||
| } |
There was a problem hiding this comment.
Document attachments unresolvable by id
Medium Severity
Document attachments now persist as {id}.pdf / {id}.xlsx (and similar) via inferDocumentExtension, but resolveAttachmentPathById still only probes image extensions plus .bin. Asset URL creation and serving go through that id-only lookup, so a PDF or spreadsheet in chat cannot be found on disk after persist. Worktree copy still works because it uses the full attachment object; the chat asset path does not.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 0763091. Configure here.
| SELECT workspace_root AS "path", default_model_selection_json AS "modelSelection" | ||
| FROM projection_projects | ||
| WHERE deleted_at IS NULL | ||
| AND default_model_selection_json IS NOT NULL |
There was a problem hiding this comment.
🟡 Medium Layers/ProjectionSnapshotQuery.ts:395
getDefaultModelSelectionForCwd returns the parent project's model for a cwd inside a nested project whose default_model_selection_json is NULL, instead of returning None. Because the SQL query omits unset projects, the nested /repos/child route cannot shadow the /repos route; include unset project routes and select the longest match before converting a null selection to None.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts around line 395:
`getDefaultModelSelectionForCwd` returns the parent project's model for a `cwd` inside a nested project whose `default_model_selection_json` is `NULL`, instead of returning `None`. Because the SQL query omits unset projects, the nested `/repos/child` route cannot shadow the `/repos` route; include unset project routes and select the longest match before converting a null selection to `None`.
| rollup.some( | ||
| (check) => | ||
| check.status?.toUpperCase() !== "COMPLETED" || | ||
| !check.conclusion || | ||
| check.conclusion.toUpperCase() === "NEUTRAL", | ||
| ) |
There was a problem hiding this comment.
🟡 Medium sourceControl/gitHubPullRequests.ts:147
A completed check with conclusion STALE is reported as "passing" with failedCheckCount: 0, so a stale required check can make the PR appear healthy even though it has not succeeded and may block merging. Add STALE to the non-passing conclusion handling.
- check.status?.toUpperCase() !== "COMPLETED" ||
+ check.status?.toUpperCase() !== "COMPLETED" ||
!check.conclusion ||
- check.conclusion.toUpperCase() === "NEUTRAL",
+ check.conclusion.toUpperCase() === "NEUTRAL" ||
+ check.conclusion.toUpperCase() === "STALE",
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/gitHubPullRequests.ts around lines 147-152:
A completed check with conclusion `STALE` is reported as `"passing"` with `failedCheckCount: 0`, so a stale required check can make the PR appear healthy even though it has not succeeded and may block merging. Add `STALE` to the non-passing conclusion handling.
| if ( | ||
| rollup.some( | ||
| (check) => | ||
| check.status?.toUpperCase() !== "COMPLETED" || |
There was a problem hiding this comment.
🟡 Medium sourceControl/gitHubPullRequests.ts:149
Legacy StatusContext entries are always reported as pending, so pull requests whose checks use commit statuses can never be classified as passing or failing. These entries expose state (such as SUCCESS or FAILURE) rather than status/conclusion; include state in the schema and use it when counting failures and determining completion.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/gitHubPullRequests.ts around line 149:
Legacy `StatusContext` entries are always reported as `pending`, so pull requests whose checks use commit statuses can never be classified as `passing` or `failing`. These entries expose `state` (such as `SUCCESS` or `FAILURE`) rather than `status`/`conclusion`; include `state` in the schema and use it when counting failures and determining completion.
| snapshotQuery.listAccountRoutes().pipe( | ||
| // A projection read failure must not break the underlying git/gh command; | ||
| // fall back to the ambient account. | ||
| Effect.orElseSucceed((): ReadonlyArray<AccountRoute> => []), |
There was a problem hiding this comment.
🟠 High sourceControl/GitHubAccountResolver.ts:342
When listAccountRoutes() fails, findAccountForCwd returns no account, so resolveForCwd produces _tag: "ambient" and GitHubCli or remote git operations use the machine-global credentials. This bypasses the selected-account safety boundary during projection failures and can perform pushes, fetches, or PR creation as the wrong account. Do not convert this failure to an empty route list; propagate the failure or return a refusal outcome so callers fail closed.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/sourceControl/GitHubAccountResolver.ts around line 342:
When `listAccountRoutes()` fails, `findAccountForCwd` returns no account, so `resolveForCwd` produces `_tag: "ambient"` and `GitHubCli` or remote git operations use the machine-global credentials. This bypasses the selected-account safety boundary during projection failures and can perform pushes, fetches, or PR creation as the wrong account. Do not convert this failure to an empty route list; propagate the failure or return a refusal outcome so callers fail closed.
| } else { | ||
| // copyFile reads through to the target's contents, giving the worktree | ||
| // an independent file even when the source is a symlink. | ||
| yield* fileSystem.copyFile(source.value.absolutePath, destination.value.absolutePath); |
There was a problem hiding this comment.
🟠 High project/ProjectWorktreeFileCopier.ts:189
copyFile follows symlinks in the destination path, so a tracked path such as config pointing outside worktreePath lets config/local.json overwrite an external file. The lexical resolveRelativePathWithinRoot check does not detect this, breaking the stated containment guarantee; validate destination ancestors with symlink-aware filesystem operations or otherwise copy without following destination symlinks.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/project/ProjectWorktreeFileCopier.ts around line 189:
`copyFile` follows symlinks in the destination path, so a tracked path such as `config` pointing outside `worktreePath` lets `config/local.json` overwrite an external file. The lexical `resolveRelativePathWithinRoot` check does not detect this, breaking the stated containment guarantee; validate destination ancestors with symlink-aware filesystem operations or otherwise copy without following destination symlinks.
| normalized.includes("must have admin") || | ||
| normalized.includes("forbidden") || | ||
| normalized.includes("403") || |
There was a problem hiding this comment.
🟡 Medium vcs/VcsProcess.ts:108
Rate-limit responses containing only HTTP 403 are classified as permission-denied, so GitHubCli.fromVcsError raises GitHubPermissionError instead of indicating a temporary failure that should be retried. The generic normalized.includes("403") match causes this; remove the status-only match or detect rate-limit text before the permission rule.
normalized.includes("forbidden") ||
- normalized.includes("403") ||
normalized.includes("resource not accessible") ||🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/vcs/VcsProcess.ts around lines 108-110:
Rate-limit responses containing only HTTP `403` are classified as `permission-denied`, so `GitHubCli.fromVcsError` raises `GitHubPermissionError` instead of indicating a temporary failure that should be retried. The generic `normalized.includes("403")` match causes this; remove the status-only match or detect rate-limit text before the permission rule.
| threadId: input.threadId, | ||
| ...(normalizedInput ? { input: normalizedInput } : {}), | ||
| ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), | ||
| ...(providerInput ? { input: providerInput } : {}), |
There was a problem hiding this comment.
🟡 Medium Layers/ProviderCommandReactor.ts:760
A document-only turn is sent without input when writeWorktreeDocuments returns no paths, so ProviderService.sendTurn rejects the request even though document-copy failures are intended to be best-effort. Add fallback input text for this case.
- ...(providerInput ? { input: providerInput } : {}),
+ ...(providerInput
+ ? { input: providerInput }
+ : documentAttachments.length > 0 && providerAttachments.length === 0
+ ? { input: "The user attached documents, but they could not be made available in the worktree." }
+ : {}),🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 760:
A document-only turn is sent without `input` when `writeWorktreeDocuments` returns no paths, so `ProviderService.sendTurn` rejects the request even though document-copy failures are intended to be best-effort. Add fallback input text for this case.
| sizeBytes: bytes.byteLength, | ||
| } | ||
| : { | ||
| type: "document" as const, |
There was a problem hiding this comment.
🟠 High orchestration/Normalizer.ts:149
Document attachments persisted by normalizeDispatchCommand are never removed when a thread is deleted or a checkpoint is reverted, so their data remains in attachmentsDir and orphaned files accumulate. collectReferencedAttachmentRelativePaths skips every attachment whose type is not image; update that cleanup to include document attachments as well.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Normalizer.ts around line 149:
Document attachments persisted by `normalizeDispatchCommand` are never removed when a thread is deleted or a checkpoint is reverted, so their data remains in `attachmentsDir` and orphaned files accumulate. `collectReferencedAttachmentRelativePaths` skips every attachment whose `type` is not `image`; update that cleanup to include `document` attachments as well.
There was a problem hiding this comment.
🟡 Medium
t3code/apps/server/src/git/GitManager.ts
Line 1869 in 0763091
When an existing worktree is found for a cross-repository PR, preparePullRequestThread returns the synthetic localPullRequestBranch (for example, t3code/pr-83/feature/foo) even though the reused worktree is actually checked out on feature/foo. Consumers therefore receive and persist a branch name that does not exist; return the reused branch's actual name instead.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/git/GitManager.ts around line 1869:
When an existing worktree is found for a cross-repository PR, `preparePullRequestThread` returns the synthetic `localPullRequestBranch` (for example, `t3code/pr-83/feature/foo`) even though the reused worktree is actually checked out on `feature/foo`. Consumers therefore receive and persist a branch name that does not exist; return the reused branch's actual `name` instead.
| */ | ||
| export function isNonFastForwardRejection(error: GitCommandError): boolean { | ||
| const normalized = error.detail.toLowerCase(); | ||
| return normalized.includes("non-fast-forward") || normalized.includes("fetch first"); |
There was a problem hiding this comment.
🟠 High vcs/GitVcsDriverCore.ts:470
isNonFastForwardRejection classifies any push rejection containing fetch first as remote-ahead, so a server-side hook that emits that phrase for an unrelated policy failure triggers runPushWithAutoRebase to fetch and rewrite the user's local commits before retrying. Restrict this check to Git's actual non-fast-forward rejection status instead of matching arbitrary remote output.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/vcs/GitVcsDriverCore.ts around line 470:
`isNonFastForwardRejection` classifies any push rejection containing `fetch first` as remote-ahead, so a server-side hook that emits that phrase for an unrelated policy failure triggers `runPushWithAutoRebase` to fetch and rewrite the user's local commits before retrying. Restrict this check to Git's actual non-fast-forward rejection status instead of matching arbitrary remote output.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a large cross-cutting feature bundle, not just a mobile-pairing backport: it changes authentication and source-control side effects, persistence, orchestration, worktree/file handling, and major chat/sidebar workflows. Auth-sensitive changes and unresolved risks around credential routing, attachment handling, and worktree safety require human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |


Our fork is 653 commits behind
pingdotgg/t3code(152 ahead). While chasing a phone that couldn't pair with the desktop, I checked what upstream already fixed in this area and found four commits we simply never picked up. This backports them rather than reinventing them — a full 653-commit merge conflicts in 86 files / 300 hunks (includingMigrations.ts, the orchestration decider, andChatView.tsxat 45 hunks alone), so that stays a separate job.Backported (cherry-picked with
-x, original authors preserved)fix(mobile): default bare IP pairing to HTTP192.168.1.21:3773built an https URL against a plain-HTTP desktop, so the handshake failed and reported an opaque transport errorfix(mobile): stop a directly-saved backend from hiding its T3 Connect environmentfix(mobile): recover the QR pairing scanner when camera access is deniedfix(desktop): keep tailscale spawn defects from breaking advertised endpointstailscalespawn defect took down the whole advertised-endpoint list — which matters here, since Tailscale is the answer for pairing from cellularOne conflict, in
ConnectionsNewRouteScreen.tsx: upstream's hunk also importeduseReffor a commit we haven't taken. Resolved by keeping our hook set and taking onlyLinking, which #6487 actually needs.Also included: a one-line fixture fix (
sidebarV2GroupByProjectmissing fromDesktopClientSettings.test.ts). That test was already failing onmainand typecheck already flagged the object as incomplete — unrelated to the backports, but the suite had to be green to verify them.Verification
apps/mobile556,apps/desktop406 (was 405 + 1 pre-existing failure),packages/tailscale14,packages/shared323 — all pass.Stack.tsxandarchivedThreadList.test.ts. Both files are untouched by this branch — confirmed against the diff — so they predate it and are out of scope here.Relationship to the open PRs
#68 independently reimplemented #4990's scheme fix before I knew upstream had it. Upstream's version is canonical, so #68 should be trimmed to just its transport-error hint (which upstream lacks) and #69 rebased on top.
What this does not fix
Not the original report. The desktop serves
http://192.168.1.21:3773correctly, the firewall is off, Android cleartext HTTP is already enabled bywithAndroidCleartextTraffic.cjs, iOS declaresNSLocalNetworkUsageDescription— and with the correct address entered, no connection from the phone reaches this machine. That points at the network path between phone and Mac, not at app code.🤖 Generated with Claude Code
Note
Medium Risk
Touches persistence migrations, git/PR merge RPC authorization, and checkpoint cwd selection alongside user-facing pairing; most pairing changes are localized, but the bundled server and projection changes affect core orchestration paths.
Overview
Improves mobile–desktop pairing and connection UX by defaulting schemeless IP hosts to HTTP in
buildPairingUrl, documenting transport rules in the mobile test skill, and only treating relay-managed saves as occupying a T3 Connect slot so a direct backend does not hide the matching cloud environment. The QR flow now distinguishes revocable vs permanently denied camera permission and offers Open Settings when the user cannot be prompted again.On desktop,
getStateandgetAdvertisedEndpointsre-resolve the LAN address from current interfaces (without rebinding the backend), so pairing URLs and QR codes stay correct after DHCP or Wi‑Fi changes; tests cover roaming and local-only fallbacks.The same diff also carries a large project/orchestration slice: new projected project fields (GitHub account, worktree branch defaults/prefix, review model, preview port, worktree copy files) with DB migrations, cwd-based account and default-model routing, document attachments written into the worktree on turn start, checkpoint baselines aligned with the session-runtime repo for turn diffs, git merge PR support and richer PR status, worktree file copying on PR-thread setup, and mobile parity (thread list sorted by recent activity, project worktree defaults in git/task flows).
Reviewed by Cursor Bugbot for commit 0763091. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix mobile pairing URL scheme defaults and harden
tailscalespawn error handlingbuildPairingUrlin pairing.ts now defaults tohttpfor bare IP literals andhttpsfor bare hostnames, preserving explicit schemes when providedreadTailscaleStatusandrunTailscaleCommandin tailscale.ts wrapspawn(...)withEffect.catchDefectto convert synchronous spawn defects (e.g.ENOTDIR) into typedTailscaleCommandSpawnErrorfailures instead of letting them escape uncaughtadvertisedHostandendpointUrlfrom current network interfaces on eachgetStatecall, so LAN IP changes are reflected without relaunchparseGitHubAuthStatusin gitHubAuthStatus.ts was rewritten to parse human-readablegh auth statusoutput only; JSON parsing support is removed, which may affect older or custom gh wrappers that relied on JSON output📊 Macroscope summarized 0763091. 162 files reviewed, 74 issues evaluated, 54 issues filtered, 15 comments posted
🗂️ Filtered Issues
apps/server/src/attachmentStore.ts — 0 comments posted, 1 evaluated, 1 filtered
attachmentRelativePathnow stores a document under its original extension (for example,<id>.pdf), butresolveAttachmentPathByIdonly probes image extensions plus.bin. Asset access calls that ID resolver, so document attachment URLs consistently resolve as missing instead of serving the uploaded document. [ Cross-file consolidated ]apps/server/src/orchestration/Layers/ProviderCommandReactor.ts — 1 comment posted, 2 evaluated, 1 filtered
safeDocumentBaseNamepermits Windows reserved device basenames such asCON,NUL, andCOM1(and names ending in.). A document with such a valid user-provided name reacheswriteFileat line 672, where Windows cannot create an ordinary file with that name; the error is swallowed and the attachment is silently omitted from the model prompt. The helper's stated portable-filename contract is therefore not met. [ Out of scope (post-validation triage) ]apps/server/src/orchestration/Normalizer.ts — 1 comment posted, 2 evaluated, 1 filtered
collectThreadAttachmentRelativePathsonly addsimageattachments, so the prune step receives no document paths and removes every matching document file fromattachmentsDir; the persisted retained message still references the now-missing document. [ Already posted ]apps/server/src/sourceControl/GitHubSourceControlProvider.ts — 0 comments posted, 1 evaluated, 1 filtered
withReviewStatereports the value returned byreadPullRequestReviewStateas the total unresolved-thread count, but that lookup only fetchesreviewThreads(first:100)and does not paginate. A PR with more than 100 review threads and unresolved threads outside that first page will show an understated (potentially zero) unresolved-comment warning. [ Cross-file consolidated ]apps/server/src/sourceControl/gitHubPullRequests.ts — 2 comments posted, 4 evaluated, 2 filtered
classifyMergeabilityreturns"unknown"formergeStateStatus: "DRAFT"(and"BEHIND"), although GitHub reportsDRAFTspecifically as a blocked merge state. Consequently an attempt to merge a draft PR follows the generic failure path rather than reporting it as blocked, despite this helper being used to provide the concrete merge-block reason. [ Out of scope (post-validation triage) ]NEUTRALis classified aspendingat line 151. GitHub treats neutral completed checks as successful for dependent checks, and the GitHub CLI itself classifiesNEUTRALas passing, so this leaves successful neutral checks shown as indefinitely pending. [ Out of scope (post-validation triage) ]apps/server/src/terminal/Manager.ts — 0 comments posted, 1 evaluated, 1 filtered
resolveForCwdreturns_tag: "unavailable", but this branch returns{}. The spawned terminal consequently retains the ambientghcredentials and inherited git credential helpers, so anygit pushorghcommand can act as the wrong account rather than the project-selected account. The resolver explicitly distinguishes this state to prevent that fallback. [ Exceeded comment limit ]apps/server/src/vcs/GitVcsDriverCore.ts — 1 comment posted, 2 evaluated, 1 filtered
readUntrackedReviewDiffsapplies the new 10 MB limit independently to every untracked path, then retains every patch indiffsand concatenates them withjoin. A workspace containing many untracked files (for example, 200 files each just below 10 MB) can make a single preview allocate and return gigabytes of patch text, despite the stated 10 MB review budget, and can OOM the server process. [ Exceeded comment limit ]apps/server/src/vcs/VcsProcess.ts — 1 comment posted, 2 evaluated, 1 filtered
normalized.includes("at least")condition is not specific to a blocked merge and applies to everyVcsProcesscommand. Any unrelated CLI failure that says, for example, that a command "requires at least" one argument is surfaced asmerge-blocked; GitHub callers then present it as a PR mergeability problem rather than the actual command failure. Restrict this match to the platform's approval-review message. [ Out of scope (post-validation triage) ]apps/server/src/workspace/WorkspaceSearchIndex.ts — 0 comments posted, 2 evaluated, 2 filtered
".."as outsidecwd. A valid workspace directory such as..configcontaining.envproduces..config/.envfromNodePath.relative, so line 220 drops that in-workspace env file and the supplemental listing fails to surface it. Check an actual parent traversal segment (for examplerelativePath === ".." || relativePath.startsWith("../")) instead. [ Out of scope (post-validation triage) ]listinvokescollectEnvEntrieswithout any cap on discovered.env*files.ENV_WALK_MAX_DIRSonly bounds directories, so a workspace containing a single directory with a very large number of.env.*files causes the walk to retain every path (andreaddirto materialize every dirent) before the laterslice(0, 25_000). A repository/workspace can therefore make a list request consume unbounded memory and stall or terminate the server despite the advertised bounded walk. [ Exceeded comment limit ]apps/server/src/ws.ts — 0 comments posted, 3 evaluated, 3 filtered
buildContinuationTranscriptdoes not actually enforceCONTINUATION_TRANSCRIPT_MAX_CHARS. It budgets only the raw tail lines, then appends two"\n\n"separators and the"[… earlier conversation omitted …]"marker; moreover, if the first nonempty message alone is over the limit it is emitted intact. Thus long transcripts can still exceed the promised 24,000-character bound and send an oversized request to the continuation summarizer. [ Exceeded comment limit ]runCopyProjectFilesProgramonly invokescopyForThreadwhenbootstrap.runSetupScriptis true.runSetupScriptis optional and can explicitly be false whileprepareWorktreestill creates a fresh worktree, so configuredworktreeCopyFiles(such as.env.local) are silently omitted for those worktrees even though copying is independent of running a setup script. [ Exceeded comment limit ]prepareWorktreeeven whencreatedisfalse. A retried draft turn carries the samenewRefName;createWorktreeexplicitly treats an existing branch with-bas a failure, so a retry after the original worktree was created still fails rather than continuing the existing thread as this path intends. [ Exceeded comment limit ]apps/web/src/components/BranchToolbar.logic.ts — 0 comments posted, 1 evaluated, 1 filtered
newRefName(for exampleorigin/feature->feature). The ref list retains non-origin remotes even when they match a local branch, whilecreateWorktreeinvokesgit worktree add -bwhenevernewRefNameis present and explicitly cannot reuse an existing branch in that case. Clicking “Open this exact branch” for that remote therefore fails instead of reusing/opening the available local branch. [ Exceeded comment limit ]apps/web/src/components/ChatView.tsx — 0 comments posted, 7 evaluated, 7 filtered
worktreeSettledis false for a local-mode draft because it has neither a server thread nor aworktreePath. The terminal toggle still opens a session using the project root, but this drawer computescwdasnulland returnsnull, so users cannot see or interact with terminals in an unsent local draft. Gate only drafts that are actually awaiting a worktree, not every draft without one. [ Exceeded comment limit ]activeThreadKey, while terminal UI state is now keyed byworkspaceThreadRef. On a non-representative chat in a shared worktree, this mounts a drawer for the sibling key;PersistentThreadTerminalDrawerthen reads that sibling's default closed state, so the shared terminal drawer disappears when switching sibling chats. [ Exceeded comment limit ]addComposerFilesvalidates images and documents in separate calls against stale state. On a mixed drop/paste,addComposerImagespermits up to the image-only limit andaddComposerDocumentsreadscomposerImagesRefbefore those additions are committed, so users can attach up to 10 images plus 10 documents despite the documented sharedPROVIDER_SEND_TURN_MAX_ATTACHMENTScap. [ Exceeded comment limit ]toggleTerminalVisibilitystores the new terminal under the worktree representative (workspaceThreadRef) but opens it withthreadId: activeThreadId. When a non-representative sibling chat is active, later terminal queries are filtered toworkspaceThreadId, so the new session is not visible as part of the shared workspace and can be removed from the shared UI on reconciliation. [ Exceeded comment limit ]startConflictResolutionInNewChatcreates a forced draft withoutpreservePreviousDraft. If this project already has an unsent draft (for example, another worktree chat tab),useNewThreadHandlerreplaces the logical-project draft mapping and deletes that prior draft's composer state. Starting conflict resolution therefore silently destroys the user's unsent prompt and attachments. [ Exceeded comment limit ]createThreadrequest withctxSelectedModelSelection. With no configured provider this isNO_PROVIDER_MODEL_SELECTION, whose definition explicitly says it must never be persisted or dispatched. Thus the enabled “Create worktree” action sends a placeholder provider/model as real thread metadata instead of requiring a usable model selection. [ Exceeded comment limit ]createThread, but ifcreateThreadfails it only reports the error and returns. The newly-created worktree/branch is never deleted, leaving orphaned worktrees that users must manually clean up after a transient thread-creation failure. [ Exceeded comment limit ]apps/web/src/components/ProjectDefaultAgentField.tsx — 0 comments posted, 1 evaluated, 1 filtered
entriesonly filters onisProviderInstancePickerVisible, which accepts every enabled instance, including entries withisAvailable === falseor an unusable probe state. Selecting such an entry persists it as the project's default;useHandleNewThreadthen applies that project selection directly to each new draft, although the existing selection resolver explicitly excludes unavailable instances. Thus a provider unavailable in the current build can be selected as the project's agent and new chats/reviews are routed to an unusable provider instead of a selectable fallback. [ Exceeded comment limit ]apps/web/src/components/ProjectDefaultWorktreeBranchField.tsx — 0 comments posted, 1 evaluated, 1 filtered
branchNamesincludes the remote-tracking ref name (for exampleorigin/main) andonValueChangepersists it unchanged asdefaultWorktreeBranch. The VCS list explicitly returns a remote-only default asorigin/<branch>when no local equivalent exists, while this setting is also forwarded unchanged as the PR/MR base branch. Thus choosing the displayed repository default on such a clone makes PR creation targetorigin/main, which is not a branch on the hosting service and is rejected bygh pr create; the base must be normalized tomainbefore persistence/use as a change-request target. [ Exceeded comment limit ]apps/web/src/components/ProjectScriptsField.tsx — 0 comments posted, 4 evaluated, 3 filtered
persistcommits the project script update before attempting the separate keybinding mutation. IfupsertKeybindingthen fails, the dialog reports the save as failed even though the script is already stored; retrying an add appends a second script (with a different generated id), causing duplicate actions despite the reported failure. [ Exceeded comment limit ]upsertKeybindingwith that remoteenvironmentId. This contradicts the intended primary-server-only reconciliation and saves a shortcut into the wrong environment instead of leaving remote scripts without a shortcut. [ Exceeded comment limit ]persistupserts only the new rule and supplies noreplacetarget.upsertKeybindingRuleremoves only an identical key-and-command rule unlessreplaceis given, so the former shortcut remains registered and both shortcuts run the script. [ Exceeded comment limit ]apps/web/src/components/ProjectWorktreeCopyFilesField.tsx — 0 comments posted, 1 evaluated, 1 filtered
parseWorktreeCopyFilesdoes not enforce the contract's 512-character maximum for an individual path. Pasting a non-empty line longer than 512 characters returns it unchanged andonBlursends it throughonChange;ProjectWorktreeCopyFilesrejects that value, so the entire settings update fails rather than saving the other valid entries or showing a local validation error. [ Out of scope (post-validation triage) ]apps/web/src/components/Sidebar.logic.ts — 0 comments posted, 2 evaluated, 2 filtered
mergeWorktreeSiblingRunningStatusonly projects the session and attention flags, leavingsnoozedUntilon the earliest representative. Thus, if that representative is snoozed while another sibling in the same collapsed worktree is active/running, the subsequent sidebar partition classifies the merged row as snoozed and removes the active worktree from the inbox (conversely snoozing a non-representative has no visible effect). The collapsed row needs group-level snooze handling or snoozing must apply to the group. [ Exceeded comment limit ]sessionbelongs to a sibling, but the returned row keeps the representative'slatestTurn.WorkingDurationcallsresolveWorkingStartedAton that mixed row: when the representative's prior turn is complete, it falls back to the sibling session'supdatedAtinstead of the running sibling'sstartedAt/requestedAt. A collapsed worktree can therefore display an incorrect (typically too short) elapsed Working timer. [ Out of scope (post-validation triage) ]apps/web/src/components/SidebarV2.tsx — 0 comments posted, 1 evaluated, 1 filtered
accounts.length === 0branch renders only the "No GitHub accounts" message. It omits the "Use default account" option, so the user cannot clear the now-invalidmember.gitHubAccountfrom this settings UI despite this being the selection's recovery path. [ Exceeded comment limit ]apps/web/src/components/chat/ChangedFilesTree.tsx — 0 comments posted, 1 evaluated, 1 filtered
onKeyDownhandles Space/Enter after events bubble from the nestedCheckbox. Consequently, activating a focused viewed checkbox with the keyboard also callsonOpenFile(file.path)(and Space is prevented at the row), so keyboard users cannot mark a file viewed without navigating to that file. Stop keyboard propagation from the checkbox or have the row ignore events originating from interactive descendants. [ Exceeded comment limit ]apps/web/src/components/chat/ChatHeader.tsx — 0 comments posted, 1 evaluated, 1 filtered
gitCwdwithpreviewPortremoves the header'sGitActionsControlinput and its rendering. The only remainingGitActionsControlis passed asRightPanelTabsheader content, which is mounted only while the right panel is open. Consequently, when the panel is closed, users have no access to git commit/push/PR actions that were previously available in the chat header. [ Exceeded comment limit ]apps/web/src/components/chat/HeaderOverflowMenu.tsx — 0 comments posted, 1 evaluated, 1 filtered
OpenInPickerinsidePopoverPopupmeans it is unmounted whenever the overflow is closed (the Base UIPopover.PortaldefaultskeepMountedtofalse).OpenInPickerinstalls the globaleditor.openFavoritekeydown listener in its mount effect, so the favorite-editor shortcut no longer works during normal use until the user opens the overflow menu. [ Exceeded comment limit ]apps/web/src/components/files/FilePreviewPanel.tsx — 0 comments posted, 1 evaluated, 1 filtered
EditableFileSurfaceno longer suppliesenableGutterUtility,enableLineSelection,onLineSelectionEnd, orrenderAnnotationto the editableFile. Consequently, selecting a source-file line in the file panel can no longer open aLocalCommentAnnotationor save a file review comment tocomposerDraftTarget; the prop is still passed into this surface but is now entirely unused. This removes the existing inline review-comment workflow for editable file previews. [ Exceeded comment limit ]apps/web/src/components/preview/addBrowserSurface.ts — 0 comments posted, 1 evaluated, 1 filtered
addBrowserSurfacenow routes throughopenBrowserPreviewInChat, but that helper determines the worktree solely withreadThreadShell. A newly created client-side draft that already targets an existing worktree has no server shell (the workspace resolver explicitly obtains its worktree from the draft store), so opening a browser tab from that draft falls back to the right panel instead of creating the chat-column content tab. This makes the new preview routing fail until the draft has been sent/promoted. [ Exceeded comment limit ]apps/web/src/components/preview/openBrowserPreviewInChat.ts — 0 comments posted, 2 evaluated, 2 filtered
openBrowserPreviewInChatderives the worktree exclusively fromreadThreadShell. A newly created client-side draft has no server shell, even when its composer draft targets a worktree, so opening a preview from that draft always takes the right-panel fallback rather than adding the preview to its worktree content-tab strip. This contradicts the draft worktree behavior used elsewhere and leaves the new preview inaccessible from the intended chat-column surface. [ Cross-file consolidated ]openBrowserPreviewInChatrecords the tab under the caller'sthreadRef, but then stores it in a worktree-shared content strip. For a non-representative sibling chat,ChatViewrenders that strip usingworkspaceThreadRefand readsactivePreviewState.sessionsfrom that representative, so the newly created session (owned by the sibling) is absent; the preview tab is blank and closing it also targets the wrong thread/session. [ Exceeded comment limit ]apps/web/src/components/settings/ProjectSettingsPanel.tsx — 0 comments posted, 2 evaluated, 1 filtered
nulland persisted, silently clearing the currently configured port. For example, a project set to 5173 loses that setting when the user enters70000(which users can type despitemax) and blurs; the existing settings handler instead rejects out-of-range values and preserves the current port. [ Exceeded comment limit ]apps/web/src/components/settings/ProviderUsageSection.tsx — 0 comments posted, 1 evaluated, 1 filtered
ago, butformatElapsedDurationLabelreturns the complete phrasejust nowfor a newly fetched (or clock-skewed future) timestamp. Fresh provider usage therefore renders asUpdated just now ago, which is visibly incorrect until enough time has elapsed to produce a numeric label. [ Out of scope (post-validation triage) ]apps/web/src/components/settings/SettingsUsagePill.tsx — 0 comments posted, 1 evaluated, 1 filtered
SettingsUsagePillalways readsprimaryServerProvidersAtom, but chat routes can target a non-primary environment (threads are explicitly scoped byenvironmentId). When a remote chat is active, itsmodelSelection.instanceIdis searched only in the primary server's providers, so the pill shows—(and refreshes the primary server) instead of that chat's usage. This contradicts the component's active-chat summary behavior for every remote environment. [ Exceeded comment limit ]apps/web/src/components/tasks/TasksDock.tsx — 0 comments posted, 1 evaluated, 1 filtered
maxBodyHeight()is only consulted when a stored height is read or during a drag. After the dock is sized at a large viewport, shrinking the browser window leavesbodyHeightunchanged and it is rendered as a fixed pixel height, so the task dock can exceed the new 70%-viewport cap and obscure the chat area until the user resizes the dock manually. [ Out of scope (post-validation triage) ]apps/web/src/hooks/useStaleArchivedWorktreeCleanup.ts — 0 comments posted, 1 evaluated, 1 filtered
datasnapshot captured before the confirmation dialog. If the user starts or unarchives a chat using one of the listed worktrees while that dialog is open, the loop never rechecksliveThreadsand sendsforce: truetoremoveWorktree; it can therefore delete the newly active worktree and its uncommitted files. [ Exceeded comment limit ]apps/web/src/projectScripts.ts — 0 comments posted, 3 evaluated, 3 filtered
appendProjectScriptcan append a duplicate script id after the suffix search is exhausted. With a 64-character normalized name and existing ids through-9999,nextProjectScriptIdfalls back to ```${baseId}-${Date.now()}.slice(0, 64)``, which is exactly the already-takenbaseId`; the newly appended script then collides with an existing one, breaking keyed rendering and script/keybinding selection. [ Already posted ]appendProjectScriptcan append a duplicate script id after the suffix search is exhausted. With a 24-character normalized name and existing ids through-9999,nextProjectScriptIdfalls back to ```${baseId}-${Date.now()}.slice(0, MAX_SCRIPT_ID_LENGTH)``, which truncates back to the already-takenbaseId`; the new script then collides with an existing script and its keyed UI/keybinding command. [ Already posted ]isTaskTerminalIdclassifies everytask-id as task-owned, butTasksDockonly initializesshellIdswithtask-shelland does not restore additionaltask-shell-Ntabs after it unmounts. Closing the right panel unmounts the dock while its extra shell sessions remain alive; on reopening, those sessions are no longer represented by the dock and this filter also removes them from the general terminal drawer, leaving them inaccessible. [ Exceeded comment limit ]apps/web/src/state/query.ts — 0 comments posted, 2 evaluated, 1 filtered
recoveredRefis scoped to the hook instance rather thanselectedAtom, and is never reset when the caller switches to a different query atom. After any interrupted query sets it totrue, a later parameter/environment change can produce a different interrupted query; its effect returns at line 46 without refreshing it, so that new query is also hidden and remains permanently pending. [ Exceeded comment limit ]apps/web/src/workspaceContentTabsStore.ts — 0 comments posted, 1 evaluated, 1 filtered
closeTabremoves the preview tab from the worktree strip before the caller's asynchronousclosePreviewSessioncompletes. If the preview-close RPC fails,closePreviewSessionexplicitly restores the preview session, but nothing restores the removed content tab; the still-live preview is therefore no longer accessible from the chat-column tab strip until the user opens it again. [ Exceeded comment limit ]packages/client-runtime/src/state/server.ts — 0 comments posted, 1 evaluated, 1 filtered
snapshotbranch replacesconfig.providersdirectly instead of applyingretainModelsAcrossTimeouts. EverysubscribeServerConfigsubscription begins with a snapshot, so after a reconnect/reload where the server's current provider probe is timed out (and therefore hasmodels: []), this overwrites the cached/live catalogue and the picker again loses all models—the precise timeout case the new helper is meant to preserve. [ Exceeded comment limit ]packages/contracts/src/orchestration.ts — 0 comments posted, 2 evaluated, 2 filtered
ChatDocumentAttachmentallows filenames such asreport.pdf, which are persisted using that.pdfextension. ExistingresolveAttachmentPathByIdonly searches image extensions and.bin, so the asset endpoint cannot resolve the document by its attachment ID; consequentlyuseAssetUrlsreturns no preview/download URL for normal document attachments. Add document extensions to ID lookup or persist documents as.bin. [ Exceeded comment limit ]UploadChatDocumentAttachmentpermits a 25 MiB document, but the enclosingClientThreadTurnStartCommand.message.attachmentsarray has no maximum-length check. The normalizer iterates every supplied attachment and writes each decoded document to disk before later provider validation; a client can therefore submit arbitrarily many valid 25 MiB document data URLs in one turn and exhaust server disk space (the declaredPROVIDER_SEND_TURN_MAX_ATTACHMENTSlimit is only enforced by some clients/provider input, not this ingress schema). [ Exceeded comment limit ]packages/shared/src/git.ts — 0 comments posted, 3 evaluated, 3 filtered
release/deadbeefthat is used to start a new thread is accepted here;maybeGenerateAndRenameWorktreeBranchForFirstTurnthen renames it on the first user message. This can unexpectedly rename an existing user branch and update the thread to the generated branch. [ Exceeded comment limit ]sanitizeWorktreeBranchPrefixleaves internal dots unchanged, so a valid user setting such asteam..wipreturnsteam..wipandbuildTemporaryWorktreeBranchNameproducesteam..wip/<token>. Git explicitly rejects refnames containing.., so attempts to create a new worktree with that prefix fail even though this helper promises a valid ref segment. [ Exceeded comment limit ].lock:sanitizeWorktreeBranchPrefix("team.lock")returnsteam.lock, producingteam.lock/<token>. Git rejects any ref component ending in.lock, so that accepted worktree-prefix setting makes all new worktree creation attempts fail. [ Already posted ]