Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a29a7cc
fix(mobile): blur glass fallbacks to prevent background text bleed (#…
juliusmarminge Sep 9, 2026
e16b8b0
feat(web): add provider model bulk toggle (#10947)
UtkarshUsername Sep 9, 2026
50f918c
fix(web): allow expanding duplicate tool call commands (#10981)
Yash-Singh1 Sep 9, 2026
75e4ceb
fix(mobile): prevent Android chat rows overlapping during sync (#10983)
SunkenInTime Sep 9, 2026
383cc40
fix(mobile): prevent text leaking through Android glass (#10998)
juliusmarminge Sep 9, 2026
afb8489
feat(pull-requests): link multiple pull requests to threads (#10839)
juliusmarminge Sep 9, 2026
f0401c6
feat(search): find threads by linked pull request (#10870)
juliusmarminge Sep 9, 2026
de37964
feat(prs): navigate, merge and rebase GitHub stacks (#10875)
juliusmarminge Sep 9, 2026
33242d0
fix(server): preserve recent PR reads across server restarts (#11007)
juliusmarminge Sep 9, 2026
8d8189e
feat(web): zoom and pan expanded images (#10869)
maria-rcks Sep 9, 2026
b7b3ef1
fix(ui): use available space for composer model names (#11002)
juliusmarminge Sep 9, 2026
addfb13
fix(web): restore pr list diff counts to the top right (#10609)
maria-rcks Sep 10, 2026
385cc0a
fix(web): show message copy buttons on touch devices (#11020)
maria-rcks Sep 10, 2026
d1eeb16
fix(web): middle-click pastes in the terminal on Linux (#11018)
maria-rcks Sep 10, 2026
0f602b3
fix(editors): open remote projects in Zed (#11022)
maria-rcks Sep 10, 2026
bb5e824
feat: add blue and orange diff color palette (#10671)
maria-rcks Sep 10, 2026
d29c56a
fix(server): resolve project identity before legacy pr relinks (#11045)
t3-code[bot] Sep 10, 2026
444fd8b
fix(mobile): keep Android markdown icons aligned with text (#11079)
SunkenInTime Sep 10, 2026
3836890
Revert "fix(mobile): keep Android markdown icons aligned with text" (…
juliusmarminge Sep 10, 2026
e784975
fix(ui): simplify multiple linked pull request badges (#11104)
maria-rcks Sep 10, 2026
0882431
fix(preview): return to pip when closing the right panel (#11102)
maria-rcks Sep 10, 2026
0527ddf
fix: quiet settled threads and simplify PR badges (#11101)
juliusmarminge Sep 10, 2026
f814983
fix(web): emphasize primary pull request actions (#11105)
juliusmarminge Sep 10, 2026
b4fd31d
merge: align official T3 through f814983c
yaacovcorcos Sep 10, 2026
dbeb26c
docs: record verified T3 alignment through f814983c
yaacovcorcos Sep 10, 2026
5a70ed5
docs: repair workspace layout reference
yaacovcorcos Sep 10, 2026
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
31 changes: 18 additions & 13 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,31 @@ revision remains literal ancestry of owned `main`; it is not merely a reviewed
or observed tip.

The current T3 alignment is recorded in
[`docs/internals/2026-09-09-upstream-sync-6c583620.md`](docs/internals/2026-09-09-upstream-sync-6c583620.md)
and in `upstream-state.json`. This local candidate preserves all 16 official commits after
`12391bd0d38eef6655b7a9f8945d0cb5febadc2b` through
`6c583620ff7ad3235b135af7107c0543467eecfa`. Merge
`72a99fadb7d42c34fd470d82827583b239335a46` retains owned main
`083993b13b42718873721fe90c92d8cd87fedd64` and the exact official target as its parents.
[`docs/internals/2026-09-10-upstream-sync-f814983c.md`](docs/internals/2026-09-10-upstream-sync-f814983c.md)
and in `upstream-state.json`. This local candidate preserves all 23 official commits after
`6c583620ff7ad3235b135af7107c0543467eecfa` through
`f814983c262b42bd79247bae377a709925c70d63`. Merge
`b4fd31d503250e89bdd103f111ff20a16395e70a` retains owned main
`7cefb603ef43fe0c4555826ceeb67b0e2338c668` and the exact official target as its parents.

Automated qualification passed; the owner will test the isolated candidate before PR delivery.
No push, PR, main merge, or publication is implied. The
[preceding alignment](docs/internals/2026-09-08-upstream-sync-349ce301-review.md) remains in
[preceding alignment](docs/internals/2026-09-09-upstream-sync-6c583620.md) remains in
ancestry. Later observed upstream tips do not move `integrationBase` by themselves.

## Receiving T3 updates

The local alignment through `6c583620` is documented in the
[2026-09-09 review](docs/internals/2026-09-09-upstream-sync-6c583620.md).
It adopts composer loading stability, shared setup wizards, terminal-link browser overrides,
lazy keyring loading, bounded event-replay memory, mobile thread arrangement, and Android/iOS
notification improvements. Scient provider lifecycle, fork behavior, identity, and cloud/mobile
publication holds remain. The receipt records qualification and remaining platform/manual gates.
The local alignment through `f814983c` is documented in the
[2026-09-10 review](docs/internals/2026-09-10-upstream-sync-f814983c.md).
It adopts multiple thread PR links, linked-thread search, GitHub stack actions, restart-safe PR
read caching, model bulk controls, image zoom/pan, preview-close PiP, diff palettes, and desktop/mobile
interaction fixes. Scient provider lifecycle, fork behavior, rich content, pending-save protection,
identity, and cloud/mobile publication holds remain.

Migration IDs are immutable local history, not an upstream synchronization counter. The new T3
multi-PR migration is registered as Scient `53`, after existing `51` and `52`; retired development
ID `50` is not reused. Never renumber recorded steps to match T3. The receipt records synthetic
upgrade/backfill tests and the remaining platform/manual gates.

The canonical procedure and stop conditions live in the
[T3 upstream alignment protocol](docs/internals/upstream-alignment-protocol.md). The short form is:
Expand Down
30 changes: 29 additions & 1 deletion apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,33 @@ describe("ElectronShell", () => {
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("opens Zed's ssh deep link", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal("zed://ssh/example.com/home/user/project");

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [["zed://ssh/example.com/home/user/project"]]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("does not open editor URLs that mix up link shapes", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const results = yield* Effect.all([
electronShell.openExternal("zed://extension/attacker"),
electronShell.openExternal("vscode://ssh/example.com/home/user/project"),
]);

assert.deepEqual(results, [false, false]);
assert.equal(openExternalMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("does not open remote editor URLs with userinfo", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);
Expand All @@ -123,9 +150,10 @@ describe("ElectronShell", () => {
electronShell.openExternal(
"vscode://:secret@vscode-remote/ssh-remote+example.com/home/user/project",
),
electronShell.openExternal("zed://ssh/user@example.com/home/user/project"),
]);

assert.deepEqual(results, [false, false]);
assert.deepEqual(results, [false, false, false]);
assert.equal(openExternalMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronShell.layer)),
);
Expand Down
16 changes: 11 additions & 5 deletions apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ const SYSTEM_SETTINGS_URLS: Record<SystemSettingsPane, string> = {
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
};

// Remote open-in-editor deep links (`vscode://vscode-remote/ssh-remote+…`)
// must reach the OS handler; every other non-web scheme stays blocked.
// Remote open-in-editor deep links (`vscode://vscode-remote/ssh-remote+…`,
// `zed://ssh/<host>/<path>`) must reach the OS handler; every other non-web
// scheme stays blocked.
const SAFE_WEB_PROTOCOLS = new Set(["http:", "https:"]);
const REMOTE_EDITOR_PROTOCOLS = new Set(
REMOTE_CAPABLE_EDITOR_IDS.flatMap((id) => {
Expand All @@ -34,13 +35,18 @@ const REMOTE_EDITOR_PROTOCOLS = new Set(
}),
);

// Zed's host sits in the first path segment, so it needs its own userinfo ban.
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.+$/;

const isRemoteEditorUrl = (url: URL) =>
REMOTE_EDITOR_PROTOCOLS.has(url.protocol) &&
url.username.length === 0 &&
url.password.length === 0 &&
url.host === "vscode-remote" &&
url.pathname.startsWith("/ssh-remote+") &&
url.pathname.length > "/ssh-remote+".length;
(url.protocol === "zed:"
? url.host === "ssh" && ZED_SSH_PATHNAME.test(url.pathname)
: url.host === "vscode-remote" &&
url.pathname.startsWith("/ssh-remote+") &&
url.pathname.length > "/ssh-remote+".length);

export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
if (typeof rawUrl !== "string") {
Expand Down
16 changes: 2 additions & 14 deletions apps/mobile/src/components/AndroidAnchoredMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu";
import { BlurView } from "expo-blur";
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { StyleProp, ViewStyle } from "react-native";
Expand All @@ -8,11 +7,11 @@ import { useKeyboardState } from "react-native-keyboard-controller";
import Animated, { FadeIn } from "react-native-reanimated";

import { appBlurTargetRef } from "../lib/appBlurTarget";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { cn } from "../lib/cn";
import { type AppSymbolName, SymbolView } from "./AppSymbol";
import { AppText as Text } from "./AppText";
import { OverlayPortal } from "./OverlayPortal";
import { GlassBackdrop } from "./GlassBackdrop";

const MENU_WIDTH = 250;
const SCREEN_MARGIN = 12;
Expand Down Expand Up @@ -79,8 +78,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
const anchorRef = useRef<View>(null);
const overlayRef = useRef<View>(null);

const { themeAppearance } = useAppearancePreferences();
const isDarkMode = themeAppearance === "dark";
const keyboardVisible = useKeyboardState((state) => state.isVisible);
const keyboardHeight = useKeyboardState((state) => state.height);
const close = useCallback(() => {
Expand Down Expand Up @@ -227,16 +224,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
: { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }),
}}
>
{/* Frosted backdrop: blur of the app content behind the menu,
washed with the translucent card tone so rows keep contrast. */}
<BlurView
blurMethod="dimezisBlurView"
blurTarget={appBlurTargetRef}
intensity={40}
tint={isDarkMode ? "dark" : "light"}
className="absolute inset-0"
/>
<View className="absolute inset-0 bg-card-translucent" />
<GlassBackdrop blurTarget={appBlurTargetRef} />
{/* keyboardShouldPersistTaps: the menu often opens over an
active editor; the first item tap must act, not just
dismiss the keyboard. */}
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/components/AppSymbol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import IconSearch from "@tabler/icons-react-native/IconSearch";
import IconServer from "@tabler/icons-react-native/IconServer";
import IconSettings from "@tabler/icons-react-native/IconSettings";
import IconSparkles from "@tabler/icons-react-native/IconSparkles";
import IconStack2 from "@tabler/icons-react-native/IconStack2";
import IconSun from "@tabler/icons-react-native/IconSun";
import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2";
import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease";
Expand All @@ -100,6 +101,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
"arrow.right.circle": IconArrowRightCircle,
"arrow.triangle.branch": IconGitBranch,
"arrow.triangle.pull": IconGitPullRequest,
"square.3.layers.3d": IconStack2,
"arrow.turn.left.up": IconArrowBackUp,
"arrow.up": IconArrowUp,
"arrow.up.circle": IconArrowUpCircle,
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function ComposerInlineControl(props: {
readonly icon?: ComponentProps<typeof SymbolView>["name"];
readonly iconNode?: ReactNode;
readonly label: string;
readonly maxWidth?: number;
readonly maxWidth?: ViewStyle["maxWidth"];
readonly onPress?: () => void;
readonly selected?: boolean;
readonly static?: boolean;
Expand Down
53 changes: 53 additions & 0 deletions apps/mobile/src/components/GlassBackdrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { BlurView } from "expo-blur";
import { useContext, type RefObject } from "react";
import { Platform, StyleSheet, View, type ColorValue } from "react-native";

import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { GlassBlurTargetContext } from "../lib/glassBlurTarget";
import { themeColorWithAlpha } from "../lib/mobileTheme";

/** Frosted backdrop for containers that clip their children to their shape. */
export function GlassBackdrop(props: {
readonly fallbackColor?: ColorValue;
readonly blurTarget?: RefObject<View | null>;
}) {
const { themeAppearance } = useAppearancePreferences();
const inheritedBlurTarget = useContext(GlassBlurTargetContext);
const target = props.blurTarget ?? inheritedBlurTarget;
const supportsBlur =
Platform.OS === "ios" ||
(Platform.OS === "android" && Platform.Version >= 31 && target !== undefined);
const colorStyle =
props.fallbackColor === undefined
? undefined
: { backgroundColor: themeColorWithAlpha(String(props.fallbackColor), 1) };

return (
<>
{/* Android samples a separate target. An opaque backing prevents any
transparent pixels in that sample from exposing the unblurred feed.
iOS samples its actual backdrop, so a backing there would hide it. */}
{Platform.OS === "android" ? (
<View pointerEvents="none" className="absolute inset-0 bg-card" style={colorStyle} />
) : null}
{supportsBlur ? (
<BlurView
pointerEvents="none"
blurTarget={target}
blurMethod="dimezisBlurViewSdk31Plus"
intensity={80}
tint={themeAppearance === "dark" ? "dark" : "default"}
style={StyleSheet.absoluteFill}
/>
) : null}
<View
pointerEvents="none"
className="absolute inset-0 bg-card"
style={[
colorStyle,
{ opacity: supportsBlur ? (themeAppearance === "dark" ? 0.25 : 0.55) : 1 },
]}
/>
</>
);
}
19 changes: 10 additions & 9 deletions apps/mobile/src/components/GlassSurface.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";
import type { ReactNode, Ref } from "react";
import type { ReactNode, Ref, RefObject } from "react";
import {
Platform,
useColorScheme,
View,
type ColorValue,
type StyleProp,
type ViewProps,
type ViewStyle,
} from "react-native";
import { withUniwind } from "uniwind";

import { cn } from "../lib/cn";
import { GlassBackdrop } from "./GlassBackdrop";

// Explicit mappings keep the native glassEffectStyle enum out of style-array conversion.
const ThemedGlassView = withUniwind(GlassView, {
Expand All @@ -26,8 +26,9 @@ interface GlassSurfaceProps extends ViewProps {
readonly tintColor?: ColorValue;
readonly tintColorClassName?: string;
readonly chrome?: "default" | "none";
/** Styling used only when native Liquid Glass is unavailable. */
readonly fallbackStyle?: StyleProp<ViewStyle>;
/** Base color for the frosted tint, or solid fill when blur is unavailable. */
readonly fallbackColor?: ColorValue;
readonly blurTarget?: RefObject<View | null>;
/** Uniwind styling used only when native Liquid Glass is unavailable. */
readonly fallbackClassName?: string;
}
Expand All @@ -39,7 +40,8 @@ export function GlassSurface({
chrome = "default",
tintColor,
tintColorClassName,
fallbackStyle,
fallbackColor,
blurTarget,
fallbackClassName,
className,
style,
Expand Down Expand Up @@ -95,14 +97,13 @@ export function GlassSurface({
{...props}
ref={ref}
className={cn(
chrome === "none"
? "border-0 border-transparent bg-transparent"
: "border border-border bg-glass-surface",
chrome === "none" ? "border-0 border-transparent" : "border border-border",
fallbackClassName,
className,
)}
style={[surfaceStyle, fallbackStyle, style]}
style={[surfaceStyle, style]}
>
<GlassBackdrop blurTarget={blurTarget} fallbackColor={fallbackColor} />
{children}
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function makeThread(
interactionMode: "default",
branch: null,
worktreePath: null,
pullRequests: [],
latestTurn: null,
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeListItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function makeThread(id: string, projectId: ProjectId): EnvironmentThreadShell {
interactionMode: "default",
branch: null,
worktreePath: null,
pullRequests: [],
latestTurn: null,
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function makeThread(
interactionMode: "default",
branch: null,
worktreePath: null,
pullRequests: [],
latestTurn: null,
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/home/homeThreadList.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
import {
buildProjectGroups,
derivePhysicalProjectKey,
Expand Down Expand Up @@ -318,6 +319,9 @@ export function buildHomeThreadGroups(input: {
: group.threads.filter(
(thread) =>
thread.title.toLocaleLowerCase().includes(query) ||
threadPullRequestSearchTerms(thread).some((term) =>
term.toLocaleLowerCase().includes(query),
) ||
input.matchedThreadKeys?.has(
threadSearchMatchKey({
environmentId: thread.environmentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function HardwareKeyboardCommandProvider({
? null
: resolveThreadReferenceCopyTarget({
threadId: activeThread?.id ?? activeThreadRef.threadId,
pullRequests: activeThread?.pullRequests,
linkedPullRequestUrl:
(activeThread?.linkedPullRequest ?? activeThread?.branchPullRequest)?.url ?? null,
}),
Expand Down
Loading
Loading