Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions .github/fork-features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2037,11 +2037,14 @@ features:

- id: pull-request-association-continuity
title: Keep worktrees associated with the correct pull request
status: review-needed
status: maintained
prs: [79, 125]
invariants:
- Repository and pull request identity follow the branch tracking remote or selected GitHub repository without matching a same-named branch from another fork.
- Pull request association survives hosted branch pruning.
- Repository identity uses the checkout's `gh repo set-default` choice (`remote.<name>.gh-resolved`, including an `owner/repo` value), else `upstream`, then `origin`, then the first remote, and never the remote the current branch tracks.
- Pull request association does not match a same-named branch from another fork.
- Pull request association survives hosted branch pruning by recovering the head branch from the branch's remote and merge configuration.
- A cached repository identity resolves without running git.
rationale: Upstream's repository identity follows `upstream`, then `origin`, and ignores `gh repo set-default`, while `gh` and upstream's own GitHub API calls honour it. The fork prefers that default first, so the app and agents running `gh` in the checkout pick the same repository. Identity no longer follows the branch's tracking remote, which disagreed with `gh` and ran git on every lookup. Unlike `gh`, a remote named `github` is not ranked before `origin`.
implementation_paths:
- apps/server/src/git/GitManager.ts
- apps/server/src/project/RepositoryIdentityResolver.ts
Expand All @@ -2057,9 +2060,11 @@ features:
- https://github.com/pingdotgg/t3code/pull/13200
- https://github.com/pingdotgg/t3code/pull/13689
- https://github.com/pingdotgg/t3code/pull/14291
- https://github.com/pingdotgg/t3code/pull/16321
- https://github.com/pingdotgg/t3code/pull/16322
- https://github.com/pingdotgg/t3code/pull/16353
- https://github.com/pingdotgg/t3code/pull/9125
retire_when: Upstream keeps repository-scoped pull request association correct across multiple remotes and branch pruning.
retire_when: Upstream's repository identity honours `gh repo set-default` before `upstream` and `origin`, and upstream keeps pull request association correct across forks and branch pruning.

- id: pull-request-chip-context-menu
title: Give pull request chips the chat link menu
Expand Down Expand Up @@ -2775,15 +2780,17 @@ features:
prs: [123]
invariants:
- Cloning a GitHub fork offers a default repository choice, keeps the fork as `origin`, and adds and fetches the parent as `upstream` without changing non-fork clone behavior, whether the clone blocks or runs in the background.
- T3 Code and the GitHub CLI share the selected default through `remote.<name>.gh-resolved`, and web and mobile allow that choice to be inspected, changed, or cleared after cloning.
- Branch tracking remotes continue to take precedence for repository identity, while the default repository resolves otherwise ambiguous untracked branches.
- Adding an existing folder on web, desktop, or mobile asks for the default repository only when its remotes point at more than one distinct GitHub repository and no default is set, offers each repository once, and lets the user decide later without writing a default.
- T3 Code and the GitHub CLI share the selected default through `remote.<name>.gh-resolved`, web and mobile allow that choice to be inspected, changed, or cleared after adding the project, and a change applies to the project's repository identity without waiting for the identity cache.
rationale: Upstream has no default-repository choice and does not add a fork's parent remote. The fork asks when cloning a fork and when adding a checkout whose remotes name several GitHub repositories, and stores the answer where `gh repo set-default` does, so the app and agents running `gh` in the checkout pick the same repository. Two remotes for one repository, non-GitHub remotes, and an existing default have nothing to ask.
implementation_paths:
- apps/mobile/src/features/projects/AddProjectScreen.tsx
- apps/mobile/src/features/threads/git/GitDefaultRepositorySheet.tsx
- apps/server/src/project/ProjectCloneTracker.ts
- apps/server/src/project/RepositoryIdentityResolver.ts
- apps/server/src/sourceControl/GitHubCli.ts
- apps/server/src/sourceControl/SourceControlRepositoryService.ts
- apps/server/src/ws.ts
- apps/web/src/components/CommandPalette.tsx
- apps/web/src/components/settings/ProjectSettingsPanel.tsx
- packages/client-runtime/src/operations/projects.ts
Expand All @@ -2796,6 +2803,7 @@ features:
- apps/server/src/project/RepositoryIdentityResolver.ts
- apps/server/src/sourceControl/GitHubCli.ts
- apps/server/src/sourceControl/SourceControlRepositoryService.ts
- apps/server/src/ws.ts
- apps/web/src/components/CommandPalette.tsx
- apps/web/src/components/settings/ProjectSettingsPanel.tsx
- packages/client-runtime/src/operations/projects.ts
Expand All @@ -2805,16 +2813,20 @@ features:
tests:
- apps/server/src/project/ProjectCloneTracker.test.ts
- apps/server/src/project/RepositoryIdentityResolver.test.ts
- apps/server/src/server.test.ts
- apps/server/src/sourceControl/GitHubCli.test.ts
- apps/server/src/sourceControl/SourceControlRepositoryService.test.ts
- packages/client-runtime/src/operations/projects.test.ts
- packages/shared/src/git.test.ts
upstream:
status: tracking
tracking:
- https://github.com/pingdotgg/t3code/pull/14291
- https://github.com/pingdotgg/t3code/pull/14673
- https://github.com/pingdotgg/t3code/pull/16321
- https://github.com/pingdotgg/t3code/pull/16322
- https://github.com/pingdotgg/t3code/pull/16353
retire_when: Upstream configures fork and parent remotes with an equivalent cross-client default-repository choice that remains consistent with GitHub CLI resolution and branch tracking.
retire_when: Upstream configures fork and parent remotes and offers an equivalent cross-client default-repository choice, on clone and when adding a checkout with several GitHub repositories, that stays consistent with GitHub CLI resolution.

- id: stable-workspace-ports
title: Assign stable development ports per workspace
Expand Down
4 changes: 4 additions & 0 deletions .github/upstream-tracked-prs.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
"pr": 13933,
"reason": "When a Claude background task or subagent finishes while the agent is idle, the agent wakes and keeps working, but the thread briefly reads as done: the completion sound and notification fire then, and again when the woken turn settles. This keeps the thread working through that handoff (fixes #13625). #14073 fixes the same alert inside the Claude adapter instead; take whichever upstream merges."
},
{
"pr": 14291,
"reason": "Makes upstream's repository identity honour gh repo set-default, which the fork's resolver has done since fork #123, including `owner/repo` values. When it merges, take upstream's version and keep the fork's rule that the branch's tracking remote does not count (ledger pull-request-association-continuity)."
},
{
"pr": 14445,
"reason": "Imported early from its unmerged commits: Claude Haiku 4.5 shows thinking On by default, matching Claude Code, and Claude text generation sends a thinking setting only when one was chosen. When it merges, the queue takes it and must reconcile with that import; the fork's manifest copy was reapplied with its own updatedAt, so expect a conflict there only."
Expand Down
227 changes: 188 additions & 39 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SettingsScreen } from "../settings/components/SettingsScreen";
import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
import { MaterialButton } from "../../components/MaterialButton";
import {
addProjectDefaultRepositoryChoices,
addProjectRemoteSourceLabel,
addProjectRemoteSourcePathHint,
addProjectRemoteSourceProvider,
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
resolveEnvironmentMachineKind,
SourceControlRepositoryError,
type SourceControlCloneDefaultRepository,
type SourceControlDefaultRepositoryRemote,
} from "@t3tools/contracts";
import {
CommonActions,
Expand Down Expand Up @@ -306,6 +308,52 @@ function SelectedCheckmark(props: { readonly selected: boolean }) {
);
}

/**
* The default-repository choice, modelled on `gh repo set-default`: which
* repository pull requests, issues, and releases target. Fork clones and
* existing checkouts with several GitHub repositories both ask it.
*/
function DefaultRepositorySection(props: { readonly children: ReactNode }) {
return (
<>
<SectionTitle>Default repository</SectionTitle>
<Text className="px-1 text-xs text-foreground-muted">
Where pull requests, issues, and releases go
</Text>
<ListSection>{props.children}</ListSection>
</>
);
}

function DefaultRepositoryRow(props: {
readonly nameWithOwner: string;
readonly remoteName: string;
readonly remoteUrl: string | null;
readonly provider: AddProjectRemoteProviderKind;
readonly isFirst?: boolean;
readonly disabled?: boolean;
readonly right?: ReactNode;
readonly onPress: () => void;
}) {
return (
<ListRow
isFirst={props.isFirst}
title={props.nameWithOwner}
subtitle={props.remoteName}
icon={
<RepositoryOwnerAvatar
nameWithOwner={props.nameWithOwner}
provider={props.provider}
remoteUrl={props.remoteUrl}
/>
}
disabled={props.disabled}
{...("right" in props ? { right: props.right } : {})}
onPress={props.onPress}
/>
);
}

function PrimaryActionButton(props: {
readonly label: string;
readonly disabled?: boolean;
Expand Down Expand Up @@ -979,10 +1027,35 @@ function FolderBrowser(props: {
export function AddProjectLocalFolderScreen(props: { readonly environmentId?: string | string[] }) {
const environment = useEnvironmentFromParam(props.environmentId);
const createProject = useCreateProject(environment);
const projects = useProjects();
const readDefaultRepository = useAtomQueryRunner(sourceControlEnvironment.defaultRepository, {
reportFailure: false,
reportDefect: false,
refresh: true,
});
const writeDefaultRepository = useAtomCommand(sourceControlEnvironment.setDefaultRepository, {
reportFailure: false,
});
const { isBrowseNavigating, navigateToBrowsePath, pathInput, setPathInput } =
useBrowsePathInput(environment);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// A checkout whose remotes name several GitHub repositories, and no default
// yet, stops here to choose one before it becomes a project.
const [defaultRepositoryPrompt, setDefaultRepositoryPrompt] = useState<{
readonly path: string;
readonly choices: ReadonlyArray<SourceControlDefaultRepositoryRemote>;
} | null>(null);

const addProject = useCallback(
async (path: string) => {
const result = await createProject(path);
if (result && AsyncResult.isFailure(result)) {
setError(errorMessage(Cause.squash(result.cause)));
}
},
[createProject],
);

const submitPath = useCallback(async () => {
if (!environment || isBrowseNavigating || isSubmitting) return;
Expand All @@ -998,12 +1071,104 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st
}

setIsSubmitting(true);
const result = await createProject(resolved.path);
if (result && AsyncResult.isFailure(result)) {
setError(errorMessage(Cause.squash(result.cause)));
const isExisting =
findExistingAddProject({
projects,
environmentId: environment.environmentId,
path: resolved.path,
}) !== null;
// A server without the default-repository RPC fails the read and skips the choice.
const defaultRepository = isExisting
? null
: await readDefaultRepository({
environmentId: environment.environmentId,
input: { cwd: resolved.path },
});
const choices =
defaultRepository !== null && AsyncResult.isSuccess(defaultRepository)
? addProjectDefaultRepositoryChoices(defaultRepository.value)
: [];
if (choices.length > 0) {
setDefaultRepositoryPrompt({ path: resolved.path, choices });
} else {
await addProject(resolved.path);
}
setIsSubmitting(false);
}, [createProject, environment, isBrowseNavigating, isSubmitting, pathInput]);
}, [
addProject,
environment,
isBrowseNavigating,
isSubmitting,
pathInput,
projects,
readDefaultRepository,
]);

/** Pins the chosen remote, or none for "Decide later", then adds the checkout. */
const chooseDefaultRepository = useCallback(
async (remoteName: string | null) => {
if (!environment || defaultRepositoryPrompt === null || isSubmitting) return;
setError(null);
setIsSubmitting(true);
if (remoteName !== null) {
const result = await writeDefaultRepository({
environmentId: environment.environmentId,
input: { cwd: defaultRepositoryPrompt.path, remoteName },
});
// The project is still worth adding; the choice stays available later.
if (AsyncResult.isFailure(result)) {
Alert.alert("Default repository not set", "You can choose it later.");
}
}
await addProject(defaultRepositoryPrompt.path);
setIsSubmitting(false);
},
[addProject, defaultRepositoryPrompt, environment, isSubmitting, writeDefaultRepository],
);

if (environment && defaultRepositoryPrompt !== null) {
return (
<AddProjectShell title="Local folder">
{error ? <ErrorBanner message={error} /> : null}
<View className="rounded-[24px] bg-card px-4 py-3">
<Text className="text-base font-t3-bold">
{inferProjectTitleFromPath(defaultRepositoryPrompt.path)}
</Text>
<Text className="mt-0.5 text-xs text-foreground-muted" numberOfLines={2}>
{defaultRepositoryPrompt.path}
</Text>
</View>
<DefaultRepositorySection>
{defaultRepositoryPrompt.choices.map((remote, index) => (
<DefaultRepositoryRow
key={remote.remoteName}
isFirst={index === 0}
nameWithOwner={remote.nameWithOwner ?? remote.url}
remoteName={remote.remoteName}
provider="github"
remoteUrl={remote.url}
disabled={isSubmitting}
onPress={() => void chooseDefaultRepository(remote.remoteName)}
/>
))}
<ListRow
title="Decide later"
subtitle="GitHub CLI decides"
icon={
<SymbolView
name="clock"
size={Platform.OS === "android" ? 24 : 17}
tintColorClassName="accent-icon-muted"
type="monochrome"
/>
}
disabled={isSubmitting}
onPress={() => void chooseDefaultRepository(null)}
/>
</DefaultRepositorySection>
</AddProjectShell>
);
}

return (
<AddProjectShell title="Local folder">
Expand Down Expand Up @@ -1194,41 +1359,25 @@ export function AddProjectDestinationScreen(props: {
</View>
) : null}
{parentRepository && repositoryTitle && provider ? (
<>
<SectionTitle>Default repository</SectionTitle>
<Text className="px-1 text-xs text-foreground-muted">
Where pull requests, issues, and releases go
</Text>
<ListSection>
<ListRow
isFirst
title={repositoryTitle}
subtitle="origin"
icon={
<RepositoryOwnerAvatar
nameWithOwner={repositoryTitle}
provider={provider}
remoteUrl={remoteUrl}
/>
}
right={<SelectedCheckmark selected={defaultRepository === "cloned"} />}
onPress={() => setDefaultRepository("cloned")}
/>
<ListRow
title={parentRepository}
subtitle="upstream"
icon={
<RepositoryOwnerAvatar
nameWithOwner={parentRepository}
provider={provider}
remoteUrl={remoteUrl}
/>
}
right={<SelectedCheckmark selected={defaultRepository === "parent"} />}
onPress={() => setDefaultRepository("parent")}
/>
</ListSection>
</>
<DefaultRepositorySection>
<DefaultRepositoryRow
isFirst
nameWithOwner={repositoryTitle}
remoteName="origin"
provider={provider}
remoteUrl={remoteUrl}
right={<SelectedCheckmark selected={defaultRepository === "cloned"} />}
onPress={() => setDefaultRepository("cloned")}
/>
<DefaultRepositoryRow
nameWithOwner={parentRepository}
remoteName="upstream"
provider={provider}
remoteUrl={remoteUrl}
right={<SelectedCheckmark selected={defaultRepository === "parent"} />}
onPress={() => setDefaultRepository("parent")}
/>
</DefaultRepositorySection>
) : null}
{environment ? (
<>
Expand Down
Loading
Loading