diff --git a/SYNC.md b/SYNC.md index a28060d7093..5f3f4409f6c 100644 --- a/SYNC.md +++ b/SYNC.md @@ -1,242 +1,54 @@ # Personal fork sync -**Agent trigger:** if the user says to follow this file and rebuild (e.g. “follow SYNC.md and rebuild the app”), execute **Agent checklist** end-to-end without asking for confirmation on the listed git force-with-lease pushes. +When asked to sync or rebuild the personal fork, run this checklist end-to-end. -Keep `main` as a clean mirror of upstream. Run from `personal`, which stacks every open PR authored by the authenticated GitHub user against `pingdotgg/t3code` on top of upstream. +## Sync -`SYNC.md` lives only on `personal`. The `personal-sync` tag must always point at **one** commit whose sole change is this file on top of `upstream/main` (never a merge tip). Rebuilds cherry-pick that commit. +- `origin/main` mirrors `upstream/main`; `personal` is the long-lived fork branch. +- Read [docs/personal-fork-changes.md](docs/personal-fork-changes.md) before resolving conflicts. Keep + every listed customization, its centralized flag and default, and upstream behavior when disabled. +- If upstream replaces or makes a customization obsolete, update the inventory instead of silently + dropping it. +- Never reset, rebase, or develop features directly on `personal`. -## Agent checklist - -Do these in order: - -1. Ensure remotes exist (`origin` = fork, `upstream` = `pingdotgg/t3code`). Fetch both. -2. Sync fork `main` to upstream (if `main` is checked out in another worktree, push without checking it out): - ```bash - git push origin upstream/main:main --force-with-lease - # or: - # git checkout main && git reset --hard upstream/main && git push origin main --force-with-lease - ``` -3. Rebuild `personal` from upstream + the single SYNC.md commit: - ```bash - git checkout personal - git reset --hard upstream/main - git cherry-pick personal-sync - ``` -4. If you must edit this file, do it **now**, before merging PRs, and keep it as that same single commit: - ```bash - # edit SYNC.md, then: - git add SYNC.md - git commit --amend --no-edit - git tag -f personal-sync HEAD - git push origin personal-sync --force - ``` - Never add extra `SYNC.md` commits on top of merges. -5. Query all open PRs authored by the authenticated GitHub user against upstream `main`, then fetch and merge each PR head. Sorting by PR number keeps the merge order stable (oldest first): - ```bash - for pr in $(gh pr list \ - --repo pingdotgg/t3code \ - --author @me \ - --base main \ - --state open \ - --limit 100 \ - --json number \ - --jq 'sort_by(.number)[] | .number'); do - git fetch upstream "+pull/$pr/head:refs/remotes/upstream/pr/$pr" - git merge --no-edit "upstream/pr/$pr" - done - ``` - Resolve conflicts and continue through every PR. Prefer fixing conflicts on the feature branch when practical. Do not maintain a static branch list in this file; the upstream PR query is authoritative for each rebuild. -6. Push: - ```bash - git push origin personal --force-with-lease - ``` -7. Build the Mac desktop app (unsigned local DMG) for this machine’s arch: - ```bash - # Apple Silicon - npm run dist:desktop:dmg:arm64 - # Intel - # npm run dist:desktop:dmg:x64 - ``` - Then install and launch: - ```bash - DMG=$(ls -t release/T3-Code-*-arm64.dmg | head -1) # or *-x64.dmg on Intel - hdiutil attach "$DMG" -nobrowse - VOL=$(ls -d /Volumes/T3\ Code* | head -1) - rm -rf "/Applications/T3 Code (Alpha).app" - cp -R "$VOL/T3 Code (Alpha).app" /Applications/ - xattr -cr "/Applications/T3 Code (Alpha).app" - hdiutil detach "$VOL" - # Quit Nightly first if running — Alpha and Nightly share com.t3tools.t3code - osascript -e 'quit app "T3 Code (Nightly)"' 2>/dev/null || true - pkill -f "T3 Code \\(Nightly\\)" 2>/dev/null || true - sleep 1 - open -a "T3 Code (Alpha)" - ``` - Do **not** start `npm run dev` unless the user asks. In the final reply, include the DMG path and confirm the app was launched. First Gatekeeper prompt: right-click → **Open**. -8. Build, install, and launch a self-contained Release app on Jake's physical iPhone. This path is - device-only (`arm64`), does not build a simulator app, and does not need Metro after launch. - - Use these machine-specific values: - - ```bash - export IOS_BUNDLE_ID=com.jakeleventhal.t3code - export IOS_TEAM_ID=BNKA7GN2H2 - export IOS_XCODE_DEVICE_ID=00008150-0011254C3C47801C - export IOS_CORE_DEVICE_ID=FD013F85-B776-57BD-BCD8-EAF72AEA30F0 - export IOS_DERIVED_DATA="$PWD/release/ios-device/DerivedData" - ``` - - The iPhone must be connected, unlocked, trusted, and have Developer Mode enabled. Use the installed - Xcode beta and CocoaPods; do not use Expo's device picker with Xcode 27 because its `devicectl` JSON - parser does not recognize Xcode 27's output yet. - - Current upstream has a Personal Team config regression. Until upstream resolves it, temporarily edit - `apps/mobile/app.config.ts` in the working tree so the Personal Team bundle ID is used consistently - and capabilities that require the T3 Tools team are omitted: - - ```diff - const variant = VARIANT_CONFIG[APP_VARIANT]; - +const iosBundleIdentifier = - + isIosPersonalTeamBuild && personalTeamBundleIdentifier - + ? personalTeamBundleIdentifier - + : variant.iosBundleIdentifier; - - - bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`, - - groupIdentifier: `group.${variant.iosBundleIdentifier}`, - + bundleIdentifier: `${iosBundleIdentifier}.widgets`, - + groupIdentifier: `group.${iosBundleIdentifier}`, - - - bundleIdentifier: variant.iosBundleIdentifier, - + bundleIdentifier: iosBundleIdentifier, - - appleTeamId: "ARK85ZXQ4Z", - - associatedDomains: [ - - `applinks:${variant.relyingParty}`, - - `webcredentials:${variant.relyingParty}`, - - ], - + appleTeamId: isIosPersonalTeamBuild ? undefined : "ARK85ZXQ4Z", - + associatedDomains: isIosPersonalTeamBuild - + ? undefined - + : [`applinks:${variant.relyingParty}`, `webcredentials:${variant.relyingParty}`], - ``` - - Verify the resolved config before generating the native project: - - ```bash - T3CODE_IOS_PERSONAL_TEAM=1 \ - T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID="$IOS_BUNDLE_ID" \ - APP_VARIANT=production EXPO_NO_DOTENV=1 \ - pnpm --filter @t3tools/mobile exec expo config --type public --json | \ - jq -e ' - .ios.bundleIdentifier == env.IOS_BUNDLE_ID and - (.ios.appleTeamId == null) and - ((.ios.associatedDomains // []) | length == 0) - ' - ``` - - Xcode 27 cannot compile the Clerk iOS SDK 1.2.9 pinned by `@clerk/expo` 3.7.2. Patch only the - installed podspec to require Clerk iOS 1.3.1; this is a no-op after `@clerk/expo` is upgraded: - - ```bash - CLERK_PODSPEC=$(cd apps/mobile && node -p \ - "require('node:path').join(require('node:path').dirname(require.resolve('@clerk/expo/package.json')), 'ios/ClerkExpo.podspec')") - ruby -e ' - path = ARGV.fetch(0) - source = File.read(path) - source = source.sub("clerk_ios_version = '\''1.2.9'\''", "clerk_ios_version = '\''1.3.1'\''") - abort "Clerk iOS 1.3.1 was not selected in #{path}" unless source.include?("clerk_ios_version = '\''1.3.1'\''") - File.write(path, source) - ' "$CLERK_PODSPEC" - ``` - - Generate the native iOS project, then build only the connected device destination. Keep DerivedData - outside `apps/mobile/ios` so `expo prebuild --clean` does not discard the cold-build cache: - - ```bash - T3CODE_IOS_PERSONAL_TEAM=1 \ - T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID="$IOS_BUNDLE_ID" \ - APP_VARIANT=production EXPO_NO_GIT_STATUS=1 EXPO_NO_DOTENV=1 \ - pnpm --filter @t3tools/mobile exec expo prebuild --clean --platform ios - - T3CODE_IOS_PERSONAL_TEAM=1 \ - T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID="$IOS_BUNDLE_ID" \ - APP_VARIANT=production EXPO_NO_DOTENV=1 \ - xcodebuild -quiet \ - -workspace apps/mobile/ios/T3Code.xcworkspace \ - -scheme T3Code \ - -configuration Release \ - -destination "platform=iOS,id=$IOS_XCODE_DEVICE_ID" \ - -derivedDataPath "$IOS_DERIVED_DATA" \ - -allowProvisioningUpdates \ - -allowProvisioningDeviceRegistration \ - DEVELOPMENT_TEAM="$IOS_TEAM_ID" \ - CODE_SIGN_STYLE=Automatic \ - IPHONEOS_DEPLOYMENT_TARGET=18.0 \ - build - ``` - - Verify the device artifact, install it, and launch it: - - ```bash - IOS_APP="$IOS_DERIVED_DATA/Build/Products/Release-iphoneos/T3Code.app" - test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$IOS_APP/Info.plist")" = "$IOS_BUNDLE_ID" - file "$IOS_APP/T3Code" | grep -q 'arm64' - codesign --verify --deep --strict --verbose=2 "$IOS_APP" - xcrun devicectl device install app --device "$IOS_CORE_DEVICE_ID" "$IOS_APP" - xcrun devicectl device process launch \ - --device "$IOS_CORE_DEVICE_ID" \ - --terminate-existing \ - "$IOS_BUNDLE_ID" - xcrun devicectl device info processes --device "$IOS_CORE_DEVICE_ID" | grep -q '/T3Code.app/T3Code' - ``` - - If launch reports that the device is locked, unlock it and rerun only the launch and process checks. - If iOS reports an untrusted developer, trust Jake's developer profile under **Settings → General → - VPN & Device Management**, then launch again. - - Restore the temporary tracked config edit after the install so `personal` remains clean: - - ```bash - git restore apps/mobile/app.config.ts - test -z "$(git status --porcelain)" - ``` - - In the final reply, include the `.app` path and confirm installation and launch on Jake's iPhone. - -Do not develop features on `personal`. Do not force-push unrelated branches. - -## Remotes +Start with a clean working tree, then: ```bash -# one-time, if missing -git remote add upstream git@github.com:pingdotgg/t3code.git +git fetch origin git fetch upstream +git push origin upstream/main:main --force-with-lease + +git checkout personal +git merge --no-edit upstream/main + +vp check +vp run typecheck +# Also run `vp run lint:mobile` if native mobile code changed. +git push origin personal ``` -| Branch / ref | Role | -| --------------- | ------------------------------------------------------------ | -| `main` | Tracks `upstream/main` only | -| `t3code/...` | Individual PRs into upstream | -| `personal` | Runnable build = upstream + one `SYNC.md` commit + PR merges | -| `personal-sync` | Tag = **exactly one** commit (upstream + `SYNC.md` only) | +Resolve merge conflicts in favor of the documented fork behavior, then rerun the checks. Test both +flag states for affected customizations. -## Recreate the SYNC.md commit (rare) +## Rebuild the macOS app -If `personal-sync` is missing or polluted with merge history: +Only when requested; do not start a development server. ```bash -git fetch upstream -git checkout --detach upstream/main -# ensure SYNC.md is the desired contents, then: -git add SYNC.md -git commit -m "SYNC.md" -git tag -f personal-sync HEAD -git push origin personal-sync --force -git checkout personal +set -euo pipefail +ARCH=$([ "$(uname -m)" = arm64 ] && echo arm64 || echo x64) +bun run "dist:desktop:dmg:$ARCH" + +DMG=$(ls -t release/T3-Code-*-$ARCH.dmg | head -1) +VOL=$(hdiutil attach "$DMG" -nobrowse | sed -n 's|^.* \(/Volumes/.*\)$|\1|p' | tail -1) +SOURCE_APP="$VOL/T3 Code (Fork).app" +test -d "$SOURCE_APP" + +rm -rf "/Applications/T3 Code (Fork).app" +ditto "$SOURCE_APP" "/Applications/T3 Code (Fork).app" +xattr -cr "/Applications/T3 Code (Fork).app" +hdiutil detach "$VOL" +open -a "T3 Code (Fork)" ``` -## Notes - -- Prefer **merge** over rebase on `personal` — easier conflict resolution when PR branches diverge. -- If two PRs conflict with each other, fix on the feature branch (or temporarily on `personal`), never on `main`. -- Alpha and Nightly share `com.t3tools.t3code`; only one can run at a time. +Report the DMG path and whether the app launched. diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 66e1cbe6a18..fcc0f50e8cb 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -21,6 +21,7 @@ import { getVisibleThreadsForProject, groupSidebarThreadsByWorktree, hasUnseenCompletion, + formatSidebarThreadDisplayTitle, isContextMenuPointerDown, isTrailingDoubleClick, orderItemsByPreferredIds, @@ -35,6 +36,8 @@ import { resolveThreadRowClassName, resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown, + resolveSidebarWorktreeLabelMode, + shouldShowSidebarEmptyThreadState, sortProjectsForSidebar, THREAD_JUMP_HINT_SHOW_DELAY_MS, } from "./Sidebar.logic"; @@ -983,16 +986,97 @@ describe("resolveThreadRowClassName", () => { describe("resolveProjectTitleClassName", () => { it("keeps ordinary project titles subtle", () => { - expect(resolveProjectTitleClassName(false)).toContain("text-foreground/80"); + const className = resolveProjectTitleClassName(false); + expect(className).toContain("native-sidebar-project-title-idle"); + expect(className).toContain("text-foreground/80"); }); it("fully emphasizes projects with unseen completed work", () => { const className = resolveProjectTitleClassName(true); + expect(className).toContain("native-sidebar-project-title-unseen"); expect(className).toContain("text-foreground"); expect(className).not.toContain("text-foreground/80"); }); }); +describe("resolveSidebarWorktreeLabelMode", () => { + it("inlines populated non-main worktrees in the native sidebar", () => { + expect( + resolveSidebarWorktreeLabelMode({ + enableNativeMacSidebar: true, + isMainCheckout: false, + threadGroupingMode: "worktree", + threadCount: 1, + }), + ).toBe("inline"); + }); + + it("hides the redundant main checkout label in the native sidebar", () => { + expect( + resolveSidebarWorktreeLabelMode({ + enableNativeMacSidebar: true, + isMainCheckout: true, + threadGroupingMode: "worktree", + threadCount: 0, + }), + ).toBe("hidden"); + }); + + it("preserves upstream grouping when the native sidebar is disabled", () => { + expect( + resolveSidebarWorktreeLabelMode({ + enableNativeMacSidebar: false, + isMainCheckout: true, + threadGroupingMode: "worktree", + threadCount: 1, + }), + ).toBe("header"); + }); + + it("keeps empty non-main worktrees as standalone rows", () => { + expect( + resolveSidebarWorktreeLabelMode({ + enableNativeMacSidebar: true, + isMainCheckout: false, + threadGroupingMode: "worktree", + threadCount: 0, + }), + ).toBe("header"); + }); +}); + +describe("shouldShowSidebarEmptyThreadState", () => { + it("hides the generic empty message in the native sidebar", () => { + expect( + shouldShowSidebarEmptyThreadState({ + enableNativeMacSidebar: true, + showEmptyThreadState: true, + }), + ).toBe(false); + }); + + it("preserves the upstream empty message when the native sidebar is disabled", () => { + expect( + shouldShowSidebarEmptyThreadState({ + enableNativeMacSidebar: false, + showEmptyThreadState: true, + }), + ).toBe(true); + }); +}); + +describe("formatSidebarThreadDisplayTitle", () => { + it("combines the worktree and thread names", () => { + expect(formatSidebarThreadDisplayTitle("art-15915", "Use historic conversions")).toBe( + "art-15915: Use historic conversions", + ); + }); + + it("keeps standalone thread titles unchanged", () => { + expect(formatSidebarThreadDisplayTitle(null, "General chat")).toBe("General chat"); + }); +}); + describe("resolveProjectStatusIndicator", () => { it("returns null when no threads have a notable status", () => { expect(resolveProjectStatusIndicator([null, null])).toBeNull(); diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index adc5c5eb4ff..5254ba582bf 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -503,10 +503,37 @@ export function resolveThreadRowClassName(input: { export function resolveProjectTitleClassName(hasUnseenCompletion: boolean): string { return cn( "native-sidebar-project-title truncate text-[13px] font-medium", - hasUnseenCompletion ? "text-foreground" : "text-foreground/80", + hasUnseenCompletion + ? "native-sidebar-project-title-unseen text-foreground" + : "native-sidebar-project-title-idle text-foreground/80", ); } +export function resolveSidebarWorktreeLabelMode(input: { + enableNativeMacSidebar: boolean; + isMainCheckout: boolean; + threadGroupingMode: "separate" | "worktree"; + threadCount: number; +}): "header" | "hidden" | "inline" { + if (!input.enableNativeMacSidebar || input.threadGroupingMode !== "worktree") return "header"; + if (input.isMainCheckout) return "hidden"; + return input.threadCount > 0 ? "inline" : "header"; +} + +export function shouldShowSidebarEmptyThreadState(input: { + enableNativeMacSidebar: boolean; + showEmptyThreadState: boolean; +}): boolean { + return input.showEmptyThreadState && !input.enableNativeMacSidebar; +} + +export function formatSidebarThreadDisplayTitle( + worktreeLabel: string | null, + threadTitle: string, +): string { + return worktreeLabel ? `${worktreeLabel}: ${threadTitle}` : threadTitle; +} + export function resolveThreadStatusPill(input: { thread: ThreadStatusInput; }): ThreadStatusPill | null { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index ec023d90f78..8bf3c13994f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -56,6 +56,7 @@ import { CloudIcon, ContainerIcon, FolderPlusIcon, + GitBranchIcon, Globe2Icon, LoaderIcon, SearchIcon, @@ -153,6 +154,7 @@ import { ProjectFavicon } from "./ProjectFavicon"; import { openDiscoveredPort } from "./preview/openDiscoveredPort"; import { getSidebarThreadIdsToPrewarm, + formatSidebarThreadDisplayTitle, isContextMenuPointerDown, isTrailingDoubleClick, orderItemsByPreferredIds, @@ -165,6 +167,8 @@ import { resolveThreadRowClassName, resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown, + resolveSidebarWorktreeLabelMode, + shouldShowSidebarEmptyThreadState, sortProjectsForSidebar, type ThreadStatusPill, useThreadJumpHintVisibility, @@ -343,6 +347,8 @@ function buildThreadJumpLabelMap(input: { interface SidebarThreadRowProps { thread: SidebarThreadSummary; nestedUnderWorktree: boolean; + worktreeLabel: string | null; + onWorktreeContextMenu?: (event: React.MouseEvent) => void; projectCwd: string | null; orderedProjectThreadKeys: readonly string[]; isActive: boolean; @@ -405,6 +411,8 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr openPrLink, thread, nestedUnderWorktree, + worktreeLabel, + onWorktreeContextMenu, } = props; const threadRef = scopeThreadRef(thread.environmentId, thread.id); const threadKey = scopedThreadKey(threadRef); @@ -504,6 +512,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr : !isThreadRunning ? "pointer-events-none transition-opacity duration-150 max-sm:pr-6 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0" : "pointer-events-none"; + const threadDisplayTitle = formatSidebarThreadDisplayTitle(worktreeLabel, thread.title); const clearConfirmingArchive = useCallback(() => { setConfirmingArchiveThreadKey((current) => (current === threadKey ? null : current)); }, [setConfirmingArchiveThreadKey, threadKey]); @@ -726,6 +735,12 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr className={`native-sidebar-thread-content flex min-w-0 flex-1 items-center gap-1.5 text-left ${nestedUnderWorktree ? "pl-3" : ""}`} data-nested-under-worktree={nestedUnderWorktree} > + {worktreeLabel ? ( +