Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
96c4bfa
feat(providers): check remote compatibility ranges (#13130)
juliusmarminge Sep 23, 2026
2eaff08
chore(lint): keep mobile theme escape-hatch allowlist honest (#13146)
juliusmarminge Sep 23, 2026
6975efd
fix(web): the pull request badge reads at the meta size again (#13175)
juliusmarminge Sep 23, 2026
b919961
fix(mobile): uniwind platform variants stay guarded on both platforms…
juliusmarminge Sep 23, 2026
21be723
refactor(mobile): git sheets use uniwind platform variants instead of…
juliusmarminge Sep 23, 2026
db898a3
refactor(mobile): remaining className platform ternaries become class…
juliusmarminge Sep 23, 2026
219c1d2
fix(web): align provider emails without clipping (#13174)
Derpedyea Sep 23, 2026
151324b
perf(mobile): recycle the default v2 home list and scope the snooze m…
juliusmarminge Sep 23, 2026
0c91f68
refactor(mobile): retire the legacy grouped thread list (#13183)
juliusmarminge Sep 23, 2026
5975ec7
fix(server): background PR checks spend less GitHub quota (#13189)
juliusmarminge Sep 23, 2026
18de6bb
fix(server): background PR sync reads summaries in batches (#13198)
juliusmarminge Sep 23, 2026
eafb4a9
fix(server): GitHub PR lookups stop probing owner-qualified heads (#1…
juliusmarminge Sep 23, 2026
aca3c87
chore(mobile): clear the legacy-list deletion fallout (#13203)
juliusmarminge Sep 23, 2026
29931dd
hatch/variant functions (#13191)
juliusmarminge Sep 23, 2026
266d70c
refactor(web): context chips render through one ContextChip component…
juliusmarminge Sep 23, 2026
361b047
refactor(web): ui components drop their secondary className props (#1…
juliusmarminge Sep 23, 2026
866fc90
refactor(web): menu triggers and items stop restyling ui/menu (#13205)
juliusmarminge Sep 23, 2026
2107939
refactor(web): field controls stop restyling Input, Select, Combobox …
juliusmarminge Sep 23, 2026
f465227
refactor(web): app code stops restyling sidebar, popover, table and m…
juliusmarminge Sep 23, 2026
71c9b69
refactor(web): Button consumers outside the composer stop restyling i…
juliusmarminge Sep 23, 2026
0e512db
refactor(web): composer controls own their look instead of restyling …
juliusmarminge Sep 23, 2026
10882be
chore(web): no-restyle fails lint, and the ceiling gate goes (#13210)
juliusmarminge Sep 23, 2026
23c7ab9
fix(mobile): recover from screen render errors (#13197)
juliusmarminge Sep 23, 2026
829af7b
feat(web): navigate back and forward with mod+[ and mod+] (#13212)
juliusmarminge Sep 23, 2026
b21c545
fix(web): sort title matches by recent activity (#13219)
Yash-Singh1 Sep 23, 2026
eb6c170
test(desktop): remove redundant keyring module-load test (#13220)
t3-code[bot] Sep 23, 2026
f5ef0dd
chore(mobile): bump app version to 1.3.1
t3-code[bot] Sep 23, 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
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ jobs:
- name: Check
run: vp check

- name: Check className overrides on components/ui do not grow
run: vp run lint:restyle-ceiling

- name: Typecheck
run: vpr typecheck

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Full glossary with file links: `docs/internals/glossary.md`
## Taste

- Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb.
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits, add a variant to the component, not classes at the call site. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` reports violations and CI caps their count.
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits and the look is a generic concept, add a variant to the component; a look that belongs to one feature stays in that feature's own component, not in `components/ui`. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` fails lint on violations.
- Inferred types over annotations. `any` is the enemy.
- Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior.
- Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ vi.mock("@napi-rs/keyring", () => {
throw new Error("Cannot find native binding");
});

it("loads browser import code without a keyring native binding", async () => {
await expect(import("./ChromiumKeys.ts")).resolves.toBeDefined();
});

it.effect("reports an unavailable keychain when the macOS binding cannot load", () =>
Effect.gen(function* () {
const { ChromiumKeyError, resolveChromiumKeys } = yield* Effect.promise(
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const config: ExpoConfig = {
slug: "t3-code",
platforms: ["ios", "android"],
scheme: variant.scheme,
version: "1.3.0",
version: "1.3.1",
runtimeVersion: {
// Development manifests resolve on every launch, so avoid fingerprint's
// expensive native-project calculation there. Preview and production stay
Expand Down
46 changes: 41 additions & 5 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
createNativeStackScreen,
type NativeStackNavigationOptions,
} from "@react-navigation/native-stack";
import { useEffect, useRef } from "react";
import { useEffect, useRef, type ReactNode } from "react";
import {
Platform,
Pressable,
Expand All @@ -23,6 +23,11 @@ import { useResolveClassNames } from "uniwind";

import { AppText as Text } from "./components/AppText";
import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
import {
RenderErrorBoundary,
RenderFailureView,
type RenderFailureProps,
} from "./components/RenderErrorBoundary";
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen";
Expand Down Expand Up @@ -777,10 +782,13 @@ const RootStackConfig = createNativeStackNavigator({
// The whole new-task flow (choose project → draft → add project) shares
// draft state via NewTaskFlowProvider. The expo-router era mounted it in
// app/new/_layout.tsx; this layout wrapper is the native-stack equivalent.
layout: ({ children }) => (
<NewTaskFlowProvider>
<View className="flex-1 bg-sheet-solid">{children}</View>
</NewTaskFlowProvider>
// A screen's layout replaces the navigator's screenLayout.
layout: ({ children, route }) => (
<GuardedScreenLayout route={route}>
<NewTaskFlowProvider>
<View className="flex-1 bg-sheet-solid">{children}</View>
</NewTaskFlowProvider>
</GuardedScreenLayout>
),
options: {
gestureEnabled: true,
Expand All @@ -794,13 +802,41 @@ const RootStackConfig = createNativeStackNavigator({
},
});

function GuardedScreenLayout(props: {
readonly children: ReactNode;
readonly route: { readonly name: string; readonly params?: object | undefined };
}) {
return (
<RenderErrorBoundary
resetKeys={[props.route.params]}
renderFallback={(fallback) => (
<ScreenRenderFallback {...fallback} routeName={props.route.name} />
)}
>
{props.children}
</RenderErrorBoundary>
);
}

function ScreenRenderFallback(props: RenderFailureProps & { readonly routeName: string }) {
const navigation = useNavigation();
const exit = navigation.canGoBack()
? { label: "Go back", onPress: () => navigation.goBack() }
: props.routeName === "Home"
? { label: "Open settings", onPress: () => navigation.navigate("SettingsSheet") }
: { label: "Return home", onPress: () => navigation.dispatch(StackActions.replace("Home")) };

return <RenderFailureView {...props} exit={exit} />;
}

export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) {
const { width, height } = useWindowDimensions();
const usesWorkspaceFlowScreens =
Platform.OS === "android" || deriveLayout({ width, height }).usesSplitView;

return (
<Navigator
screenLayout={GuardedScreenLayout}
screenOptions={({ route }) => {
if (route.name !== "SettingsSheet" && route.name !== "NewTaskSheet") {
return {};
Expand Down
127 changes: 127 additions & 0 deletions apps/mobile/src/components/RenderErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Component, type ReactNode } from "react";
import { ScrollView, View } from "react-native";

import { AppText as Text } from "./AppText";
import { MaterialButton } from "./MaterialButton";
import { copyTextWithHaptic } from "../lib/copyTextWithHaptic";

interface RenderErrorBoundaryProps {
readonly children: ReactNode;
readonly resetKeys?: ReadonlyArray<unknown>;
readonly renderFallback?: (props: RenderFailureProps) => ReactNode;
}

interface RenderErrorBoundaryState {
readonly failed: boolean;
readonly error: unknown;
readonly componentStack?: string;
readonly resetKeys?: ReadonlyArray<unknown>;
}

export interface RenderFailureProps {
readonly details: string;
readonly retry: () => void;
}

function errorDetails(error: unknown, componentStack?: string): string {
let description: string;
try {
description = error instanceof Error ? (error.stack ?? error.message) : String(error);
} catch {
description = "Unknown render error";
}
return componentStack ? `${description}\nComponent stack:\n${componentStack}` : description;
}

/** A failed subtree remounts on retry or when its identifying inputs change. */
export class RenderErrorBoundary extends Component<
RenderErrorBoundaryProps,
RenderErrorBoundaryState
> {
override state: RenderErrorBoundaryState = {
failed: false,
error: null,
resetKeys: this.props.resetKeys,
};

static getDerivedStateFromProps(
{ resetKeys }: RenderErrorBoundaryProps,
state: RenderErrorBoundaryState,
): Partial<RenderErrorBoundaryState> | null {
if (
resetKeys?.length !== state.resetKeys?.length ||
resetKeys?.some((key, index) => !Object.is(key, state.resetKeys?.[index]))
) {
return { failed: false, error: null, componentStack: undefined, resetKeys };
}
return null;
}

static getDerivedStateFromError(error: unknown): Partial<RenderErrorBoundaryState> {
return { failed: true, error };
}

override componentDidCatch(_error: unknown, info: { componentStack?: string }) {
this.setState({ componentStack: info.componentStack });
}

private readonly retry = () => {
this.setState({ failed: false, error: null, componentStack: undefined });
};

override render() {
if (!this.state.failed) return this.props.children;
const fallback =
this.props.renderFallback ??
((props: RenderFailureProps) => <RenderFailureView {...props} />);
return fallback({
details: errorDetails(this.state.error, this.state.componentStack),
retry: this.retry,
});
}
}

export function RenderFailureView(
props: RenderFailureProps & {
readonly title?: string;
readonly bottomInset?: number;
readonly exit?: { readonly label: string; readonly onPress: () => void };
},
) {
const title = props.title ?? "This screen couldn't be displayed";
return (
<ScrollView
className="flex-1 bg-screen"
contentContainerClassName="flex-grow items-center justify-center gap-5 px-6 py-8"
contentContainerStyle={
props.bottomInset ? { paddingBottom: 32 + props.bottomInset } : undefined
}
>
<Text accessibilityRole="header" className="text-center text-xl font-t3-bold">
{title}
</Text>
<Text className="text-center text-sm text-foreground-muted">
Try again. If it keeps happening, copy the details for a bug report.
</Text>
<Text selectable className="text-center font-mono text-xs text-danger-foreground">
{props.details.split("\n", 1)[0]?.slice(0, 300)}
</Text>
<View className="w-full max-w-xs gap-2">
<MaterialButton label="Try again" onPress={props.retry} tone="primary" fullWidth />
<MaterialButton
label="Copy details"
onPress={() => copyTextWithHaptic(props.details, { target: "error details" })}
fullWidth
/>
{props.exit ? (
<MaterialButton
label={props.exit.label}
onPress={props.exit.onPress}
tone="text"
fullWidth
/>
) : null}
</View>
</ScrollView>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export function ConnectionsNewRouteScreen({

{pairingConnectionError ? <ErrorBanner message={pairingConnectionError} /> : null}

<View className={Platform.OS === "android" ? "flex-row justify-end" : undefined}>
<View className="android:flex-row android:justify-end">
<ConnectionSheetButton
icon="plus"
label={isSubmitting ? "Pairing..." : "Add environment"}
Expand Down
9 changes: 1 addition & 8 deletions apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,6 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
const { fileInspector, layout, showAuxiliaryPane } = useAdaptiveWorkspaceLayout();
const [searchQuery, setSearchQuery] = useState("");
const { themeAppearance: highlightTheme } = useAppearancePreferences();
const headerColor = useUniwindTheme()["--color-header"];
const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
props.route.params,
);
Expand Down Expand Up @@ -554,13 +553,7 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
</>
);

return Platform.OS === "android" ? (
<View className="flex-1" style={{ backgroundColor: headerColor }}>
{content}
</View>
) : (
content
);
return Platform.OS === "android" ? <View className="flex-1 bg-header">{content}</View> : content;
}

export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function ThreadFileNavigatorPane(props: {
}
>
<View
className={Platform.OS === "android" ? "bg-header" : "border-b border-border"}
className="ios:border-b ios:border-border android:bg-header"
style={{ paddingTop: Platform.OS === "android" ? 0 : props.headerInset }}
>
{Platform.OS === "android" ? (
Expand Down
64 changes: 6 additions & 58 deletions apps/mobile/src/features/home/HomeHeader.android.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import type { MenuAction } from "@react-native-menu/menu";
import { useCallback, useMemo } from "react";
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
import { MaterialThreadListToolbar } from "./MaterialThreadListToolbar";
import {
hasCustomHomeListOptions,
PROJECT_SORT_OPTIONS,
THREAD_SORT_OPTIONS,
} from "./home-list-options";
import type { HomeHeaderProps } from "./HomeHeader.types";

export type { HomeHeaderEnvironment } from "./HomeHeader.types";
Expand All @@ -17,13 +11,11 @@ function checkedMenuState(checked: boolean) {
}

export function HomeHeader(props: HomeHeaderProps) {
// Thread List v2 lays the list out in fixed creation order, so the
// sort/group filter controls would be silently ignored — hide them and
// key the "customized" icon state off the environment filter alone.
const threadListV2Enabled = useThreadListV2Enabled();
const hasCustomListOptions = threadListV2Enabled
? props.selectedEnvironmentId !== null || props.selectedProjectKey !== null
: hasCustomHomeListOptions(props);
// The list uses a fixed creation order and ignores sort/group options, so
// the filter menu only carries the filters and the "customized" icon state
// keys off those alone.
const hasCustomListOptions =
props.selectedEnvironmentId !== null || props.selectedProjectKey !== null;
const menuActions = useMemo<MenuAction[]>(
() => [
{
Expand Down Expand Up @@ -62,38 +54,8 @@ export function HomeHeader(props: HomeHeaderProps) {
],
},
] satisfies MenuAction[])),
...(threadListV2Enabled
? []
: ([
{
id: "project-sort",
title: "Sort projects",
subactions: PROJECT_SORT_OPTIONS.map((option) => ({
id: `project-sort:${option.value}`,
title: option.label,
state: checkedMenuState(props.projectSortOrder === option.value),
})),
},
{
id: "thread-sort",
title: "Sort threads",
subactions: THREAD_SORT_OPTIONS.map((option) => ({
id: `thread-sort:${option.value}`,
title: option.label,
state: checkedMenuState(props.threadSortOrder === option.value),
})),
},
] satisfies MenuAction[])),
],
[
props.environments,
props.projectSortOrder,
props.projects,
props.selectedEnvironmentId,
props.selectedProjectKey,
props.threadSortOrder,
threadListV2Enabled,
],
[props.environments, props.projects, props.selectedEnvironmentId, props.selectedProjectKey],
);
const handleMenuAction = useCallback(
(event: { nativeEvent: { event: string } }) => {
Expand Down Expand Up @@ -126,20 +88,6 @@ export function HomeHeader(props: HomeHeaderProps) {
}
return;
}

const projectSort = PROJECT_SORT_OPTIONS.find(
(option) => id === `project-sort:${option.value}`,
);
if (projectSort) {
props.onProjectSortOrderChange(projectSort.value);
return;
}

const threadSort = THREAD_SORT_OPTIONS.find((option) => id === `thread-sort:${option.value}`);
if (threadSort) {
props.onThreadSortOrderChange(threadSort.value);
return;
}
},
[props],
);
Expand Down
Loading
Loading