Skip to content

feat(source-control): wire up fork remotes on clone - #123

Merged
incognitojam merged 1 commit into
mainfrom
t3code/configure-fork-upstream-remote
Aug 13, 2026
Merged

feat(source-control): wire up fork remotes on clone#123
incognitojam merged 1 commit into
mainfrom
t3code/configure-fork-upstream-remote

Conversation

@incognitojam

Copy link
Copy Markdown
Collaborator

Note

Cloning a GitHub fork now wires up the parent as upstream and pins which repository gh targets, with the choice offered during the clone and editable afterwards.

Cloning a fork left the parent repository unreachable — no upstream remote — and, worse, gh resolves a two-remote checkout to the fork's parent, so a pull request created from the workspace could land on someone else's project without ever saying so.

A fork clone now adds upstream, fetches it the way gh repo clone leaves it, and pins a default repository by writing the same remote.<name>.gh-resolved config gh repo set-default uses, so T3 Code and the GitHub CLI always agree. Because two remotes mean two possible targets, the clone flow asks which one; the cloned fork leads, so the pin agrees with the remote a branch on the fork tracks, and the parent stays one keystroke away for contributing upstream.

Choosing during the clone

Before After
Lookup goes straight to the destination step A step asking which repository is the default

The destination step then states the relationship instead of repeating the repository name as a URL:

Destination step showing "forked from CircuitLord/BigWalkVRInstaller"

Changing it later

Before After
Checkout settings without a default repository row Checkout settings with a Default repository row

Mobile gets the same choice at clone time, plus a Default repository row in the thread git sheet so the phone is not a one-way door:

The fork chooser on iOS with owner avatars

Notes

  • gh-resolved is not always base: with a single remote, gh repo set-default writes an owner/repo. That value is parsed and shown rather than silently overwritten, and the parse is now shared with RepositoryIdentityResolver so the two cannot drift.
  • The pin is written with --replace-all, since gh adds resolutions and a plain write refuses to overwrite a multi-value key (exit 5).
  • Only forks send provider/repository on clone, so every other clone costs no extra gh repo view and behaves exactly as before against an older server.
  • Identity still follows the branch's tracked remote first, per fix(server): follow selected pull request remote #79; the pin only decides when a branch has no upstream yet.

Verification

  • vp test run apps/server/src/sourceControl apps/server/src/project packages/shared/src/git.test.ts packages/client-runtime — 797 tests passed, including new cases for the owner/repo pin, the unset path, dotted remote names, and a fetch failure leaving the clone intact
  • Targeted typecheck, lint, and formatting across server, contracts, shared, client-runtime, web, and mobile
  • Web: clone flow and Settings driven by hand and captured above
  • iOS Simulator: fork chooser verified on device; the git sheet row is typechecked but unexercised, since it needs a thread with real git state
  • Two independent agent reviews; findings folded in

Written by an agent (Claude Code, claude-opus-5).

Cloning a GitHub fork left the parent repository unreachable: no
`upstream` remote, and `gh` silently resolved a two-remote checkout to
the parent, so pull requests created from the workspace could target the
wrong repository.

Clone a fork with its parent wired up as `upstream`, fetched the way
`gh repo clone` leaves it, and pin a default repository in the same git
config `gh repo set-default` writes. A new step in the Add Project clone
flow chooses which repository that is, and the choice is editable
afterwards in Settings for web and desktop, or the thread git sheet on
mobile. The cloned fork is offered first, so the pin agrees with the
remote a branch on the fork tracks; picking the parent stays one
keystroke away for contributing upstream.

Verification:
- `vp test run apps/server/src/sourceControl apps/server/src/project
  packages/shared/src/git.test.ts packages/client-runtime` - 1455 tests
  passed
- Targeted typecheck, lint, and formatting for the touched packages
- Web: clone flow and Settings driven by hand against a fork
- iOS Simulator: fork chooser verified on device; the git sheet row is
  typechecked but unexercised
- Two independent agent reviews; findings folded in, notably the
  `gh-resolved` value parse and the multi-value `git config` write

Model: claude-opus-5; harness: Claude Code.
@incognitojam
incognitojam enabled auto-merge (squash) August 13, 2026 19:07
@incognitojam
incognitojam merged commit ea9fbb3 into main Aug 13, 2026
8 checks passed
@incognitojam
incognitojam deleted the t3code/configure-fork-upstream-remote branch August 13, 2026 19:10
incognitojam added a commit that referenced this pull request Aug 13, 2026
The destination and local-folder steps of Add Project had no navigation
title, so the sheet header showed their route names:
`AddProjectDestination` and `AddProjectLocal`. The repository step
already sets one, so this just applies the same pattern to its two
siblings.

Titles are now **Destination** and **Local Folder**.

Split out of #123, where the raw route name showed up in a screenshot.

### Verification

- `tsc --noEmit` for `apps/mobile`, targeted lint and formatting
- iOS Simulator: the destination sheet header reads "Destination" over
Fast Refresh

---
Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot pushed a commit that referenced this pull request Aug 13, 2026
> [!NOTE]
> Cloning a GitHub fork now wires up the parent as `upstream` and pins
which repository `gh` targets, with the choice offered during the clone
and editable afterwards.

Cloning a fork left the parent repository unreachable — no `upstream`
remote — and, worse, `gh` resolves a two-remote checkout to the fork's
parent, so a pull request created from the workspace could land on
someone else's project without ever saying so.

A fork clone now adds `upstream`, fetches it the way `gh repo clone`
leaves it, and pins a default repository by writing the same
`remote.<name>.gh-resolved` config `gh repo set-default` uses, so T3
Code and the GitHub CLI always agree. Because two remotes mean two
possible targets, the clone flow asks which one; the cloned fork leads,
so the pin agrees with the remote a branch on the fork tracks, and the
parent stays one keystroke away for contributing upstream.

### Choosing during the clone

| Before | After |
| --- | --- |
| ![Lookup goes straight to the destination
step](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-before.png?v=064d1d3f9)
| ![A step asking which repository is the
default](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-step.png?v=064d1d3f9)
|

The destination step then states the relationship instead of repeating
the repository name as a URL:

![Destination step showing "forked from
CircuitLord/BigWalkVRInstaller"](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-destination.png?v=064d1d3f9)

### Changing it later

| Before | After |
| --- | --- |
| ![Checkout settings without a default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-before.png?v=064d1d3f9)
| ![Checkout settings with a Default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-after.png?v=064d1d3f9)
|

Mobile gets the same choice at clone time, plus a **Default repository**
row in the thread git sheet so the phone is not a one-way door:

<img
src="https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/mobile-after.png?v=064d1d3f9"
width="320" alt="The fork chooser on iOS with owner avatars">

### Notes

- `gh-resolved` is not always `base`: with a single remote, `gh repo
set-default` writes an `owner/repo`. That value is parsed and shown
rather than silently overwritten, and the parse is now shared with
`RepositoryIdentityResolver` so the two cannot drift.
- The pin is written with `--replace-all`, since `gh` *adds* resolutions
and a plain write refuses to overwrite a multi-value key (exit 5).
- Only forks send `provider`/`repository` on clone, so every other clone
costs no extra `gh repo view` and behaves exactly as before against an
older server.
- Identity still follows the branch's tracked remote first, per #79; the
pin only decides when a branch has no upstream yet.

### Verification

- `vp test run apps/server/src/sourceControl apps/server/src/project
packages/shared/src/git.test.ts packages/client-runtime` — 797 tests
passed, including new cases for the `owner/repo` pin, the unset path,
dotted remote names, and a fetch failure leaving the clone intact
- Targeted typecheck, lint, and formatting across server, contracts,
shared, client-runtime, web, and mobile
- Web: clone flow and Settings driven by hand and captured above
- iOS Simulator: fork chooser verified on device; the git sheet row is
typechecked but unexercised, since it needs a thread with real git state
- Two independent agent reviews; findings folded in

---
Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot pushed a commit that referenced this pull request Aug 13, 2026
The destination and local-folder steps of Add Project had no navigation
title, so the sheet header showed their route names:
`AddProjectDestination` and `AddProjectLocal`. The repository step
already sets one, so this just applies the same pattern to its two
siblings.

Titles are now **Destination** and **Local Folder**.

Split out of #123, where the raw route name showed up in a screenshot.

### Verification

- `tsc --noEmit` for `apps/mobile`, targeted lint and formatting
- iOS Simulator: the destination sheet header reads "Destination" over
Fast Refresh

---
Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot pushed a commit that referenced this pull request Aug 14, 2026
> [!NOTE]
> Cloning a GitHub fork now wires up the parent as `upstream` and pins
which repository `gh` targets, with the choice offered during the clone
and editable afterwards.

Cloning a fork left the parent repository unreachable — no `upstream`
remote — and, worse, `gh` resolves a two-remote checkout to the fork's
parent, so a pull request created from the workspace could land on
someone else's project without ever saying so.

A fork clone now adds `upstream`, fetches it the way `gh repo clone`
leaves it, and pins a default repository by writing the same
`remote.<name>.gh-resolved` config `gh repo set-default` uses, so T3
Code and the GitHub CLI always agree. Because two remotes mean two
possible targets, the clone flow asks which one; the cloned fork leads,
so the pin agrees with the remote a branch on the fork tracks, and the
parent stays one keystroke away for contributing upstream.

| Before | After |
| --- | --- |
| ![Lookup goes straight to the destination
step](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-before.png?v=064d1d3f9)
| ![A step asking which repository is the
default](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-step.png?v=064d1d3f9)
|

The destination step then states the relationship instead of repeating
the repository name as a URL:

![Destination step showing "forked from
CircuitLord/BigWalkVRInstaller"](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-destination.png?v=064d1d3f9)

| Before | After |
| --- | --- |
| ![Checkout settings without a default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-before.png?v=064d1d3f9)
| ![Checkout settings with a Default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-after.png?v=064d1d3f9)
|

Mobile gets the same choice at clone time, plus a **Default repository**
row in the thread git sheet so the phone is not a one-way door:

<img
src="https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/mobile-after.png?v=064d1d3f9"
width="320" alt="The fork chooser on iOS with owner avatars">

- `gh-resolved` is not always `base`: with a single remote, `gh repo
set-default` writes an `owner/repo`. That value is parsed and shown
rather than silently overwritten, and the parse is now shared with
`RepositoryIdentityResolver` so the two cannot drift.
- The pin is written with `--replace-all`, since `gh` *adds* resolutions
and a plain write refuses to overwrite a multi-value key (exit 5).
- Only forks send `provider`/`repository` on clone, so every other clone
costs no extra `gh repo view` and behaves exactly as before against an
older server.
- Identity still follows the branch's tracked remote first, per #79; the
pin only decides when a branch has no upstream yet.

- `vp test run apps/server/src/sourceControl apps/server/src/project
packages/shared/src/git.test.ts packages/client-runtime` — 797 tests
passed, including new cases for the `owner/repo` pin, the unset path,
dotted remote names, and a fetch failure leaving the clone intact
- Targeted typecheck, lint, and formatting across server, contracts,
shared, client-runtime, web, and mobile
- Web: clone flow and Settings driven by hand and captured above
- iOS Simulator: fork chooser verified on device; the git sheet row is
typechecked but unexercised, since it needs a thread with real git state
- Two independent agent reviews; findings folded in

---
Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot pushed a commit that referenced this pull request Aug 14, 2026
The destination and local-folder steps of Add Project had no navigation
title, so the sheet header showed their route names:
`AddProjectDestination` and `AddProjectLocal`. The repository step
already sets one, so this just applies the same pattern to its two
siblings.

Titles are now **Destination** and **Local Folder**.

Split out of #123, where the raw route name showed up in a screenshot.

### Verification

- `tsc --noEmit` for `apps/mobile`, targeted lint and formatting
- iOS Simulator: the destination sheet header reads "Destination" over
Fast Refresh

---
Written by an agent (Claude Code, claude-opus-5).
incognitojam added a commit that referenced this pull request Aug 14, 2026
`#123`, `GH-123` and `owner/repo#123` name an issue or a pull request, and a
bare URL to one is written back as that shorthand. Neither is GFM, so remark
left both as they were.

The link is addressed at `/issues/{n}` whichever kind it turns out to be: the
host redirects that to `/pull/{n}` for a pull request, so the link is right in
a browser before anything is known about it.

Inert without a repository to read a number against, which is what keeps `#2`
in a conversation the plain text it reads as. Nothing passes one yet.

The matching rules are GitHub's own, each checked against its renderer:
`foo#123` and `#123abc` are not references, `(#123)`, `#123.`, `#123-abc` and
`/#123` are, `#0` is not, and code spans and link labels are left alone.

The body is read twice — as a tree to linkify, as text to collect what to ask
about — so the two are held to an invariant rather than to each other: asking
covers everything rendered, since over-asking costs one entry in a batch while
under-asking would leave a rendered reference unanswered for good. A test
holds that containment over the bodies where they disagree.

The marker travels as a data attribute, so the sanitizer's allowlist is part
of the feature: one missing from it is stripped silently, and every test that
stops before the rehype half keeps passing while nothing renders at all.
incognitojam added a commit that referenced this pull request Aug 14, 2026
The pull request surfaces now read `#123`, `GH-123` and `owner/repo#123` in
every body they render — the description, comments, reviews and timeline —
and follow them the way the app follows anything else it recognises.

What each number turns out to be decides where it opens: a pull request in a
project on this machine opens as a tab beside the one being read, and an issue
or another organisation's repository opens in a browser, since there is no
issue surface here to open it in. Cmd/Ctrl+click is the browser either way. A
reference clicked before its answer arrives follows the link as written, which
the host redirects to whichever kind it is — the right page, in a browser.

A number the host has nothing under is underlined the way an unknown word is,
and still opens: the reader may have access in a browser they are signed into
differently. Only an answer marks a link this way. A request that failed on
the way, or one answered by a different host than the body was written
against, leaves every reference alone — a host having a bad minute cannot
repaint a body full of good references as mistakes. That mark is a rule rather
than a class, because `.chat-markdown a` sets `text-decoration: none` and
outranks any single class put on the element.

The thread a panel is mounted beside reaches the bodies it renders through one
context, rather than a prop threaded through four components with no other use
for it. Only reference clicks read it, so nothing else about those bodies
changes.
incognitojam added a commit that referenced this pull request Aug 14, 2026
`#123`, `GH-123` and `owner/repo#123` name an issue or a pull request, and a
bare URL to one is written back as that shorthand. Neither is GFM, so remark
left both as they were.

The link is addressed at `/issues/{n}` whichever kind it turns out to be: the
host redirects that to `/pull/{n}` for a pull request, so the link is right in
a browser before anything is known about it.

Inert without a repository to read a number against, which is what keeps `#2`
in a conversation the plain text it reads as. Nothing passes one yet.

The matching rules are GitHub's own, each checked against its renderer:
`foo#123` and `#123abc` are not references, `(#123)`, `#123.`, `#123-abc` and
`/#123` are, `#0` is not, and code spans and link labels are left alone.

The body is read twice — as a tree to linkify, as text to collect what to ask
about — so the two are held to an invariant rather than to each other: asking
covers everything rendered, since over-asking costs one entry in a batch while
under-asking would leave a rendered reference unanswered for good. A test
holds that containment over the bodies where they disagree.

The marker travels as a data attribute, so the sanitizer's allowlist is part
of the feature: one missing from it is stripped silently, and every test that
stops before the rehype half keeps passing while nothing renders at all.
incognitojam added a commit that referenced this pull request Aug 14, 2026
The pull request surfaces now read `#123`, `GH-123` and `owner/repo#123` in
every body they render — the description, comments, reviews and timeline —
and follow them the way the app follows anything else it recognises.

What each number turns out to be decides where it opens: a pull request in a
project on this machine opens as a tab beside the one being read, and an issue
or another organisation's repository opens in a browser, since there is no
issue surface here to open it in. Cmd/Ctrl+click is the browser either way. A
reference clicked before its answer arrives follows the link as written, which
the host redirects to whichever kind it is — the right page, in a browser.

A number the host has nothing under is underlined the way an unknown word is,
and still opens: the reader may have access in a browser they are signed into
differently. Only an answer marks a link this way. A request that failed on
the way, or one answered by a different host than the body was written
against, leaves every reference alone — a host having a bad minute cannot
repaint a body full of good references as mistakes. That mark is a rule rather
than a class, because `.chat-markdown a` sets `text-decoration: none` and
outranks any single class put on the element.

The thread a panel is mounted beside reaches the bodies it renders through one
context, rather than a prop threaded down through the tabs, none of which has
any other use for it. It is provided from outside the panel's own body, so the
panel reads exactly as it did before and only reference clicks are affected.
incognitojam added a commit that referenced this pull request Aug 14, 2026
`#123`, `GH-123` and `owner/repo#123` name an issue or a pull request, and a
bare URL to one is written back as that shorthand. Neither is GFM, so remark
left both as they were.

The link is addressed at `/issues/{n}` whichever kind it turns out to be: the
host redirects that to `/pull/{n}` for a pull request, so the link is right in
a browser before anything is known about it.

Inert without a repository to read a number against, which is what keeps `#2`
in a conversation the plain text it reads as. Nothing passes one yet.

The matching rules are GitHub's own, each checked against its renderer:
`foo#123` and `#123abc` are not references, `(#123)`, `#123.`, `#123-abc` and
`/#123` are, `#0` is not, and code spans and link labels are left alone.

The body is read twice — as a tree to linkify, as text to collect what to ask
about — so the two are held to an invariant rather than to each other: asking
covers everything rendered, since over-asking costs one entry in a batch while
under-asking would leave a rendered reference unanswered for good. A test
holds that containment over the bodies where they disagree.

The marker travels as a data attribute, so the sanitizer's allowlist is part
of the feature: one missing from it is stripped silently, and every test that
stops before the rehype half keeps passing while nothing renders at all.
incognitojam added a commit that referenced this pull request Aug 14, 2026
The pull request surfaces now read `#123`, `GH-123` and `owner/repo#123` in
every body they render — the description, comments, reviews and timeline —
and follow them the way the app follows anything else it recognises.

What each number turns out to be decides where it opens: a pull request in a
project on this machine opens as a tab beside the one being read, and an issue
or another organisation's repository opens in a browser, since there is no
issue surface here to open it in. Cmd/Ctrl+click is the browser either way. A
reference clicked before its answer arrives follows the link as written, which
the host redirects to whichever kind it is — the right page, in a browser.

A number the host has nothing under is underlined the way an unknown word is,
and still opens: the reader may have access in a browser they are signed into
differently. Only an answer marks a link this way. A request that failed on
the way, or one answered by a different host than the body was written
against, leaves every reference alone — a host having a bad minute cannot
repaint a body full of good references as mistakes. That mark is a rule rather
than a class, because `.chat-markdown a` sets `text-decoration: none` and
outranks any single class put on the element.

The thread a panel is mounted beside reaches the bodies it renders through one
context, rather than a prop threaded down through the tabs, none of which has
any other use for it. It is provided from outside the panel's own body, so the
panel reads exactly as it did before and only reference clicks are affected.
yngatech-nightly Bot pushed a commit that referenced this pull request Aug 14, 2026
> [!NOTE]
> Cloning a GitHub fork now wires up the parent as `upstream` and pins
which repository `gh` targets, with the choice offered during the clone
and editable afterwards.

Cloning a fork left the parent repository unreachable — no `upstream`
remote — and, worse, `gh` resolves a two-remote checkout to the fork's
parent, so a pull request created from the workspace could land on
someone else's project without ever saying so.

A fork clone now adds `upstream`, fetches it the way `gh repo clone`
leaves it, and pins a default repository by writing the same
`remote.<name>.gh-resolved` config `gh repo set-default` uses, so T3
Code and the GitHub CLI always agree. Because two remotes mean two
possible targets, the clone flow asks which one; the cloned fork leads,
so the pin agrees with the remote a branch on the fork tracks, and the
parent stays one keystroke away for contributing upstream.

| Before | After |
| --- | --- |
| ![Lookup goes straight to the destination
step](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-before.png?v=064d1d3f9)
| ![A step asking which repository is the
default](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-step.png?v=064d1d3f9)
|

The destination step then states the relationship instead of repeating
the repository name as a URL:

![Destination step showing "forked from
CircuitLord/BigWalkVRInstaller"](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-destination.png?v=064d1d3f9)

| Before | After |
| --- | --- |
| ![Checkout settings without a default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-before.png?v=064d1d3f9)
| ![Checkout settings with a Default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-after.png?v=064d1d3f9)
|

Mobile gets the same choice at clone time, plus a **Default repository**
row in the thread git sheet so the phone is not a one-way door:

<img
src="https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/mobile-after.png?v=064d1d3f9"
width="320" alt="The fork chooser on iOS with owner avatars">

- `gh-resolved` is not always `base`: with a single remote, `gh repo
set-default` writes an `owner/repo`. That value is parsed and shown
rather than silently overwritten, and the parse is now shared with
`RepositoryIdentityResolver` so the two cannot drift.
- The pin is written with `--replace-all`, since `gh` *adds* resolutions
and a plain write refuses to overwrite a multi-value key (exit 5).
- Only forks send `provider`/`repository` on clone, so every other clone
costs no extra `gh repo view` and behaves exactly as before against an
older server.
- Identity still follows the branch's tracked remote first, per #79; the
pin only decides when a branch has no upstream yet.

- `vp test run apps/server/src/sourceControl apps/server/src/project
packages/shared/src/git.test.ts packages/client-runtime` — 797 tests
passed, including new cases for the `owner/repo` pin, the unset path,
dotted remote names, and a fetch failure leaving the clone intact
- Targeted typecheck, lint, and formatting across server, contracts,
shared, client-runtime, web, and mobile
- Web: clone flow and Settings driven by hand and captured above
- iOS Simulator: fork chooser verified on device; the git sheet row is
typechecked but unexercised, since it needs a thread with real git state
- Two independent agent reviews; findings folded in

---
Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot pushed a commit that referenced this pull request Aug 14, 2026
The destination and local-folder steps of Add Project had no navigation
title, so the sheet header showed their route names:
`AddProjectDestination` and `AddProjectLocal`. The repository step
already sets one, so this just applies the same pattern to its two
siblings.

Titles are now **Destination** and **Local Folder**.

Split out of #123, where the raw route name showed up in a screenshot.

### Verification

- `tsc --noEmit` for `apps/mobile`, targeted lint and formatting
- iOS Simulator: the destination sheet header reads "Destination" over
Fast Refresh

---
Written by an agent (Claude Code, claude-opus-5).
incognitojam added a commit that referenced this pull request Aug 14, 2026
> [!NOTE]
> `#123`, `GH-123` and `owner/repo#123` in a pull request body are now
links, the way they are on
> GitHub. What each number turns out to be decides where it opens: a
pull request in a project on
> this machine opens as a tab beside the one being read, an issue opens
in a browser, and a number
> the host has nothing under is underlined in red and still opens.

GitHub's autolinked references are its own extension rather than GFM, so
remark left them as they
were: every `pingdotgg#6039` an agent or a reviewer wrote in a description, a
comment or a review stayed
plain text, and following one meant retyping it into a browser.

Reading them is the easy half. Knowing what one *is* takes asking the
host, and a body can name a
dozen — so they are asked about together, in one GraphQL document,
aliased by repository and by
number. `issueOrPullRequest` answers which of the two each number turned
out to be, which is what
decides where it opens; a cache keeps a panel of many bodies from
spawning a process per body for
numbers a neighbour just resolved.

The link is addressed at `/issues/{n}` before any of that comes back,
because the host redirects
that to `/pull/{n}` for a pull request — so it is already right in a
browser, and resolving only
ever improves it. A reference clicked before its answer arrives follows
it as written.

Everything else follows from one rule: **only an answer marks a link as
broken.** A request that
failed on the way — rate limited, logged out, offline — leaves every
reference exactly as it was,
and a null is read as nothing-there only where the host filed
`NOT_FOUND` against it, since
`FORBIDDEN` is SAML or an IP allowlist standing between the reader and
something they can very
likely open themselves. A host having a bad minute must not repaint a
body full of good references
as mistakes.

Inert unless a surface passes the repository to read numbers against, so
`#2` in a conversation —
where it is far more likely to be a step than an issue — stays the plain
text it reads as.

## Before / after

The description of #126, which cites the stale-stack guard from `#95`
and the window it was built
for (`#94`).

| Before | After |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/before.png"
alt="Reference numbers rendered as plain grey text" width="460"> | <img
src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/after.png"
alt="The same numbers rendered as links" width="460"> |

`#95` resolves to a pull request and opens as a tab here; `#94` resolves
to an issue and opens in a
browser, since there is no issue surface to open it in.

A reference the host has nothing under keeps its link colour and takes
the mark an unknown word
takes. No pull request in this repository happens to cite a number the
repository lacks, so the
state below was forced on a resolved reference to photograph it:

<img
src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/missing.png"
alt="A reference underlined with a red wavy line" width="460">

## Reading the commits

Three layers, each of which stands alone: the plugin that reads
references and renders nothing
differently on its own, the request that answers them and is called by
nobody, then the wiring that
turns both on.

## Verification

- `vp test run` for the touched suites: `sourceControl` (147),
`GitManager`, and the web
  `markdown-github-references`, `chat` and `pullRequest` suites (469).
- Targeted `tsgo --noEmit` for `apps/web`, `apps/server` and
`packages/contracts`, at each of the
  three commits rather than only at the tip.
- The matching rules are GitHub's own, each checked against its renderer
before being written down:
`foo#123` and `#123abc` are not references, `(#123)`, `#123.`,
`#123-abc` and `/#123` are, `#0`
  is not, and code spans and link labels are left alone.
- Read live against this repository in `test-t3-app`: references
linkified in a real description,
`#95` re-addressed itself to `/pull/95` and opened here, `#94` stayed
addressed for the browser.
That pass is also what found two faults nothing else did — a memoized
renderer holding the first
render's answers, and a mark cancelled by `.chat-markdown a {
text-decoration: none }`.

---
Written by an agent (T3 Code, claude-opus-5).
incognitojam added a commit that referenced this pull request Aug 15, 2026
> [!NOTE]
> Cloning a GitHub fork now wires up the parent as `upstream` and pins
which repository `gh` targets, with the choice offered during the clone
and editable afterwards.

Cloning a fork left the parent repository unreachable — no `upstream`
remote — and, worse, `gh` resolves a two-remote checkout to the fork's
parent, so a pull request created from the workspace could land on
someone else's project without ever saying so.

A fork clone now adds `upstream`, fetches it the way `gh repo clone`
leaves it, and pins a default repository by writing the same
`remote.<name>.gh-resolved` config `gh repo set-default` uses, so T3
Code and the GitHub CLI always agree. Because two remotes mean two
possible targets, the clone flow asks which one; the cloned fork leads,
so the pin agrees with the remote a branch on the fork tracks, and the
parent stays one keystroke away for contributing upstream.

| Before | After |
| --- | --- |
| ![Lookup goes straight to the destination
step](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-before.png?v=064d1d3f9)
| ![A step asking which repository is the
default](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-step.png?v=064d1d3f9)
|

The destination step then states the relationship instead of repeating
the repository name as a URL:

![Destination step showing "forked from
CircuitLord/BigWalkVRInstaller"](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/clone-after-destination.png?v=064d1d3f9)

| Before | After |
| --- | --- |
| ![Checkout settings without a default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-before.png?v=064d1d3f9)
| ![Checkout settings with a Default repository
row](https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/settings-after.png?v=064d1d3f9)
|

Mobile gets the same choice at clone time, plus a **Default repository**
row in the thread git sheet so the phone is not a one-way door:

<img
src="https://raw.githubusercontent.com/yngatech/t3code/pr-assets/configure-fork-upstream-remote/mobile-after.png?v=064d1d3f9"
width="320" alt="The fork chooser on iOS with owner avatars">

- `gh-resolved` is not always `base`: with a single remote, `gh repo
set-default` writes an `owner/repo`. That value is parsed and shown
rather than silently overwritten, and the parse is now shared with
`RepositoryIdentityResolver` so the two cannot drift.
- The pin is written with `--replace-all`, since `gh` *adds* resolutions
and a plain write refuses to overwrite a multi-value key (exit 5).
- Only forks send `provider`/`repository` on clone, so every other clone
costs no extra `gh repo view` and behaves exactly as before against an
older server.
- Identity still follows the branch's tracked remote first, per #79; the
pin only decides when a branch has no upstream yet.

- `vp test run apps/server/src/sourceControl apps/server/src/project
packages/shared/src/git.test.ts packages/client-runtime` — 797 tests
passed, including new cases for the `owner/repo` pin, the unset path,
dotted remote names, and a fetch failure leaving the clone intact
- Targeted typecheck, lint, and formatting across server, contracts,
shared, client-runtime, web, and mobile
- Web: clone flow and Settings driven by hand and captured above
- iOS Simulator: fork chooser verified on device; the git sheet row is
typechecked but unexercised, since it needs a thread with real git state
- Two independent agent reviews; findings folded in

---
Written by an agent (Claude Code, claude-opus-5).
incognitojam added a commit that referenced this pull request Aug 15, 2026
The destination and local-folder steps of Add Project had no navigation
title, so the sheet header showed their route names:
`AddProjectDestination` and `AddProjectLocal`. The repository step
already sets one, so this just applies the same pattern to its two
siblings.

Titles are now **Destination** and **Local Folder**.

Split out of #123, where the raw route name showed up in a screenshot.

### Verification

- `tsc --noEmit` for `apps/mobile`, targeted lint and formatting
- iOS Simulator: the destination sheet header reads "Destination" over
Fast Refresh

---
Written by an agent (Claude Code, claude-opus-5).
incognitojam added a commit that referenced this pull request Aug 15, 2026
> [!NOTE]
> `#123`, `GH-123` and `owner/repo#123` in a pull request body are now
links, the way they are on
> GitHub. What each number turns out to be decides where it opens: a
pull request in a project on
> this machine opens as a tab beside the one being read, an issue opens
in a browser, and a number
> the host has nothing under is underlined in red and still opens.

GitHub's autolinked references are its own extension rather than GFM, so
remark left them as they
were: every `pingdotgg#6039` an agent or a reviewer wrote in a description, a
comment or a review stayed
plain text, and following one meant retyping it into a browser.

Reading them is the easy half. Knowing what one *is* takes asking the
host, and a body can name a
dozen — so they are asked about together, in one GraphQL document,
aliased by repository and by
number. `issueOrPullRequest` answers which of the two each number turned
out to be, which is what
decides where it opens; a cache keeps a panel of many bodies from
spawning a process per body for
numbers a neighbour just resolved.

The link is addressed at `/issues/{n}` before any of that comes back,
because the host redirects
that to `/pull/{n}` for a pull request — so it is already right in a
browser, and resolving only
ever improves it. A reference clicked before its answer arrives follows
it as written.

Everything else follows from one rule: **only an answer marks a link as
broken.** A request that
failed on the way — rate limited, logged out, offline — leaves every
reference exactly as it was,
and a null is read as nothing-there only where the host filed
`NOT_FOUND` against it, since
`FORBIDDEN` is SAML or an IP allowlist standing between the reader and
something they can very
likely open themselves. A host having a bad minute must not repaint a
body full of good references
as mistakes.

Inert unless a surface passes the repository to read numbers against, so
`#2` in a conversation —
where it is far more likely to be a step than an issue — stays the plain
text it reads as.

The description of #126, which cites the stale-stack guard from `#95`
and the window it was built
for (`#94`).

| Before | After |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/before.png"
alt="Reference numbers rendered as plain grey text" width="460"> | <img
src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/after.png"
alt="The same numbers rendered as links" width="460"> |

`#95` resolves to a pull request and opens as a tab here; `#94` resolves
to an issue and opens in a
browser, since there is no issue surface to open it in.

A reference the host has nothing under keeps its link colour and takes
the mark an unknown word
takes. No pull request in this repository happens to cite a number the
repository lacks, so the
state below was forced on a resolved reference to photograph it:

<img
src="https://raw.githubusercontent.com/yngatech/t3code/assets/github-reference-links/missing.png"
alt="A reference underlined with a red wavy line" width="460">

Three layers, each of which stands alone: the plugin that reads
references and renders nothing
differently on its own, the request that answers them and is called by
nobody, then the wiring that
turns both on.

- `vp test run` for the touched suites: `sourceControl` (147),
`GitManager`, and the web
  `markdown-github-references`, `chat` and `pullRequest` suites (469).
- Targeted `tsgo --noEmit` for `apps/web`, `apps/server` and
`packages/contracts`, at each of the
  three commits rather than only at the tip.
- The matching rules are GitHub's own, each checked against its renderer
before being written down:
`foo#123` and `#123abc` are not references, `(#123)`, `#123.`,
`#123-abc` and `/#123` are, `#0`
  is not, and code spans and link labels are left alone.
- Read live against this repository in `test-t3-app`: references
linkified in a real description,
`#95` re-addressed itself to `/pull/95` and opened here, `#94` stayed
addressed for the browser.
That pass is also what found two faults nothing else did — a memoized
renderer holding the first
render's answers, and a mark cancelled by `.chat-markdown a {
text-decoration: none }`.

---
Written by an agent (T3 Code, claude-opus-5).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant