Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
34df703
fix(server): resolve Claude SDK executable path on Windows npm instal…
nsxdavid Jul 20, 2026
45bf60a
Fix project action preview settings persistence (#3842)
keeperxy Jul 20, 2026
ff710c9
fix(desktop): allow clipboard writes in the preview browser (#3889)
carlosricojr Jul 20, 2026
8eb8a09
fix(web): handle sidebar shortcut before editors (#3921)
Bortlesboat Jul 20, 2026
3379cb1
fix(server): recognize Bedrock-backed Claude as authenticated (#3931)
PieterVanZyl-Dev Jul 20, 2026
4bd7b25
Fix incorrect pluralization of “entry” (#3933)
McMelonTV Jul 20, 2026
be210b2
feat(server): title background-task work-log rows with the task name …
t3dotgg Jul 20, 2026
055994c
fix: delegate OpenCode session titles to provider (#3720)
tris203 Jul 20, 2026
1fb47be
Archive selected threads from the context menu (#3895)
theduke Jul 20, 2026
8154092
fix(cli): support force removing projects (#3922)
Bortlesboat Jul 20, 2026
10bd35c
fix: allow sidebar to be shrunk when wider than viewport (#2456)
shoaib050326 Jul 20, 2026
d240943
fix(codex): show web search query and url in tool call details (#2093)
GuilhermeVieiraDev Jul 20, 2026
be0cd8d
Add Codex launch arguments setting (#2892)
jamesx0416 Jul 20, 2026
8d1edc4
[orchestration] Clear stale active turn when session becomes inactive…
Andrew-Forster Jul 20, 2026
1809642
Regenerate Codex reset credit protocol bindings (#4173)
juliusmarminge Jul 20, 2026
8c5acb3
fix(preview): preserve direct localhost navigation (#3939)
Chrrxs Jul 20, 2026
022d5af
Synchronize mobile threads with authoritative shell snapshots (#4163)
juliusmarminge Jul 20, 2026
d6ce25c
Gate iOS glass layout on native support (#4032)
juliusmarminge Jul 20, 2026
aa52d1f
fix(opencode): resume the OpenCode session on follow-ups instead of s…
vdmkotai Jul 20, 2026
e6078ed
fix(server): use CLI for OpenCode health check instead of spawning se…
UtkarshUsername Jul 20, 2026
f8eed09
fix(web): scope timeline minimap hover target to the side gutter (#3869)
xxashxx-svg Jul 20, 2026
a1220c1
[codex] show complete approval details (#4111)
maxwellyoung Jul 20, 2026
e519921
fix(web): paint text selection over composer chips (#4139)
yordis Jul 20, 2026
8ef7721
[codex] preserve custom model slugs (#4168)
maxwellyoung Jul 20, 2026
fdc38a7
fix(server): adapt fork providers to providerModelsFromSettings witho…
cursoragent Jul 25, 2026
b600a5c
fix(sync): format CursorSdkMappings and align timeline MVCP size test
cursoragent Jul 25, 2026
1cf751c
fix(sync): restore fork Cursor SDK + Claude provider after #4168
cursoragent Jul 25, 2026
3ec88c4
fix(sync): drop obsolete provider kind from custom model slug helper
cursoragent Jul 25, 2026
ea96008
fix(sync): drop provider kind from Claude providerModelsFromSettings …
cursoragent Jul 25, 2026
21a8fe4
fix(server): subscribe to domain events before forking live buffers
cursoragent Jul 25, 2026
04a9536
test(server): stub subscribeDomainEvents in shell buffer regression
cursoragent Jul 25, 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
51 changes: 51 additions & 0 deletions apps/desktop/src/preview/BrowserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { fromPartition, sessions } = vi.hoisted(() => ({
readonly clearStorageData: ReturnType<typeof vi.fn>;
readonly getUserAgent: ReturnType<typeof vi.fn>;
readonly setPermissionRequestHandler: ReturnType<typeof vi.fn>;
readonly setPermissionCheckHandler: ReturnType<typeof vi.fn>;
readonly setUserAgent: ReturnType<typeof vi.fn>;
}
>(),
Expand All @@ -40,6 +41,7 @@ describe("BrowserSession", () => {
clearStorageData: vi.fn(() => Promise.resolve()),
getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/0.0.27"),
setPermissionRequestHandler: vi.fn(),
setPermissionCheckHandler: vi.fn(),
setUserAgent: vi.fn(),
};
sessions.set(partition, browserSession);
Expand All @@ -61,6 +63,55 @@ describe("BrowserSession", () => {
}).pipe(Effect.provide(layer)),
);

it.effect("grants clipboard-sanitized-write through both the request and check handlers", () =>
Effect.gen(function* () {
const browserSessions = yield* BrowserSession.BrowserSession;
const partition = yield* browserSessions.getPartition("scope-a");
yield* browserSessions.getSession("scope-a");

const browserSession = sessions.get(partition);
assert.isDefined(browserSession);

const requestHandler = browserSession.setPermissionRequestHandler.mock.calls[0]?.[0];
const checkHandler = browserSession.setPermissionCheckHandler.mock.calls[0]?.[0];
assert.isFunction(requestHandler);
assert.isFunction(checkHandler);

const requestAllows = (permission: string): boolean => {
let granted: boolean | undefined;
requestHandler(null, permission, (value: boolean) => {
granted = value;
});
assert.isDefined(granted);
return granted;
};

for (const permission of [
"clipboard-read",
"clipboard-sanitized-write",
"notifications",
"geolocation",
]) {
assert.isTrue(requestAllows(permission), `request handler should allow ${permission}`);
assert.isTrue(
checkHandler(null, permission) as boolean,
`check handler should allow ${permission}`,
);
}

// `clipboard-write` is not a real Electron permission — the async write API
// uses `clipboard-sanitized-write` — so the stale name must not be granted,
// and unrelated permissions stay denied.
for (const permission of ["clipboard-write", "midi"]) {
assert.isFalse(requestAllows(permission), `request handler should deny ${permission}`);
assert.isFalse(
checkHandler(null, permission) as boolean,
`check handler should deny ${permission}`,
);
}
}).pipe(Effect.provide(layer)),
);

it.effect("preserves partition scope and the platform failure chain", () => {
const nativeCause = new Error("native digest failed");
const platformCause = PlatformError.systemError({
Expand Down
20 changes: 18 additions & 2 deletions apps/desktop/src/preview/BrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import * as SynchronizedRef from "effect/SynchronizedRef";

const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-";

// Permissions granted to preview web content. `clipboard-sanitized-write` is the
// Electron permission behind `navigator.clipboard.writeText()` — note it is NOT
// `clipboard-write`, which is not a valid Electron permission name. Async
// clipboard writes are gated by the permission *check* handler (not only the
// request handler), so both handlers must allow it; otherwise built-in "Copy"
// buttons — e.g. the Next.js / Vercel error overlay — fail with
// `Failed to execute 'writeText' on 'Clipboard': Write permission denied`.
const ALLOWED_PREVIEW_PERMISSIONS: ReadonlySet<string> = new Set([
"clipboard-read",
"clipboard-sanitized-write",
"notifications",
"geolocation",
]);

export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass<BrowserSessionPartitionDerivationError>()(
"BrowserSessionPartitionDerivationError",
{
Expand Down Expand Up @@ -120,9 +134,11 @@ export const make = Effect.gen(function* BrowserSessionMake() {
.replace(/\s*t3code\/[\d.]+/, "");
browserSession.setUserAgent(userAgent);
browserSession.setPermissionRequestHandler((_webContents, permission, callback) => {
const allowed = ["clipboard-read", "clipboard-write", "notifications", "geolocation"];
callback(allowed.includes(permission));
callback(ALLOWED_PREVIEW_PERMISSIONS.has(permission));
});
browserSession.setPermissionCheckHandler((_webContents, permission) =>
ALLOWED_PREVIEW_PERMISSIONS.has(permission),
);
const next = new Map(sessions);
next.set(partition, browserSession);
return [browserSession, next] as const;
Expand Down
22 changes: 13 additions & 9 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re
import { useResolveClassNames } from "uniwind";

import { AppText as Text } from "./components/AppText";
import { renderCompactBrandTitle } from "./components/CompactBrandTitle";
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent";
Expand Down Expand Up @@ -60,6 +59,7 @@ import {
EMPTY_INCOMING_SHARE_PRESENTATION_STATE,
transitionIncomingSharePresentation,
} from "./features/sharing/incoming-share-presentation";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";

Expand All @@ -81,19 +81,24 @@ type AppScreenOptions = NativeStackNavigationOptions & {
// Shared header presets. Screens only override genuinely dynamic values (titles,
// subtitles, toolbar items, search callbacks) via NativeStackScreenOptions.
//
// GLASS: transparent header over the screen's primary scroll view, with the iOS 26
// scroll-edge blur sampling the content (Home, Thread, Files tree, settings sheet).
// GLASS: transparent header over the screen's primary scroll view on supported
// iOS versions. Pre-glass iOS gets the same solid material as internal-scroll
// surfaces so content is laid out below the bar instead of underlapping it.
const GLASS_HEADER_OPTIONS: AppScreenOptions = {
headerBackButtonDisplayMode: "minimal",
headerBackTitle: "",
headerLargeTitle: false,
headerShadowVisible: false,
headerShown: true,
headerStyle: Platform.OS === "ios" ? { backgroundColor: "transparent" } : undefined,
headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED
? { backgroundColor: "transparent" }
: SHEET_BACKGROUND_COLOR !== undefined
? { backgroundColor: SHEET_BACKGROUND_COLOR as unknown as string }
: undefined,
headerTitleStyle: { fontSize: 18, fontWeight: "800" },
headerTransparent: Platform.OS === "ios",
scrollEdgeEffects: Platform.OS === "ios" ? HEADER_SCROLL_EDGE_EFFECTS : undefined,
unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined,
headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined,
unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
};

// SOLID: opaque sheet-colored header for surfaces whose content scrolls internally
Expand Down Expand Up @@ -384,8 +389,7 @@ export const RootStack = createNativeStackNavigator({
...GLASS_HEADER_OPTIONS,
contentStyle: { backgroundColor: "transparent" },
headerBackVisible: false,
headerTitle: renderCompactBrandTitle,
title: "T3 Code",
title: "Threads",
},
}),
Thread: createNativeStackScreen({
Expand Down
60 changes: 0 additions & 60 deletions apps/mobile/src/components/CompactBrandTitle.tsx

This file was deleted.

18 changes: 7 additions & 11 deletions apps/mobile/src/features/files/FileTreeBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import type { ProjectEntry } from "@t3tools/contracts";
import { SymbolView } from "../../components/AppSymbol";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ActivityIndicator,
FlatList,
Platform,
Pressable,
RefreshControl,
View,
} from "react-native";
import { ActivityIndicator, FlatList, Pressable, RefreshControl, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { AppText as Text } from "../../components/AppText";
import { PierreEntryIcon } from "../../components/PierreEntryIcon";
import { cn } from "../../lib/cn";
import { useThemeColor } from "../../lib/useThemeColor";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import {
buildFileTree,
defaultExpandedTreePaths,
Expand Down Expand Up @@ -129,7 +123,7 @@ export function FileTreeBrowser(props: {
const insets = useSafeAreaInsets();
// Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the
// observed adjustedContentInset bottom (~102) seen in the native trace.
const headerInset = Platform.OS === "ios" ? insets.top + 44 : 0;
const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 44 : 0;
const iconColor = String(useThemeColor("--color-icon-muted"));
const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;
const controlledSelectedPathRef = useRef(controlledSelectedPath);
Expand Down Expand Up @@ -249,9 +243,11 @@ export function FileTreeBrowser(props: {
className="flex-1"
data={visibleNodes}
keyExtractor={(item) => item.node.path}
contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
scrollIndicatorInsets={
Platform.OS === "ios" ? { top: headerInset, left: 0, right: 0, bottom: 0 } : undefined
NATIVE_LIQUID_GLASS_SUPPORTED
? { top: headerInset, left: 0, right: 0, bottom: 0 }
: undefined
}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
Expand Down
5 changes: 1 addition & 4 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useNavigation } from "@react-navigation/native";
import { useMemo, useState } from "react";

import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { renderCompactBrandTitle } from "../../components/CompactBrandTitle";
import { useProjects, useThreadShells } from "../../state/entities";
import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
import { useWorkspaceState } from "../../state/workspace";
Expand Down Expand Up @@ -87,9 +86,7 @@ export function HomeRouteScreen() {
>
<>
{/* Restore the compact title in case the split branch blanked it. */}
<NativeStackScreenOptions
options={{ title: "T3 Code", headerTitle: renderCompactBrandTitle }}
/>
<NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} />
<HomeHeader
environments={environments}
searchQuery={searchQuery}
Expand Down
18 changes: 14 additions & 4 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState";
import type { WorkspaceState } from "../../state/workspaceModel";
import type { SavedRemoteConnection } from "../../lib/connection";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import type { PendingNewTask } from "../../state/use-pending-new-tasks";
import {
Expand Down Expand Up @@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) {
className="flex-1 items-center justify-center bg-screen px-8"
style={{
paddingBottom: Math.max(insets.bottom, 24),
paddingTop: Platform.OS === "ios" ? insets.top + 72 : 0,
paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve safe-area insets on iOS without Liquid Glass.

When NATIVE_LIQUID_GLASS_SUPPORTED is false on an iOS device, Line 425 still suppresses HomeTopContentSpacer, but these changes also disable automatic content inset adjustment and remove empty-state top padding. Older or unsupported iOS devices can therefore render content beneath the native header. Keep the existing iOS inset behavior for the solid-header fallback, or add an equivalent fallback spacer/inset path.

Also applies to: 473-474

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/features/home/HomeScreen.tsx` at line 392, Update the
HomeScreen safe-area handling around the paddingTop calculation and
HomeTopContentSpacer at the referenced inset/empty-state paths so iOS devices
without native Liquid Glass retain the existing top inset beneath the solid
header. Preserve the Liquid Glass behavior while adding an equivalent fallback
spacer or content padding for unsupported iOS devices, including the empty-state
layout.

}}
>
<View className="w-full max-w-[430px]">
Expand All @@ -399,11 +400,20 @@ export function HomeScreen(props: HomeScreenProps) {
onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}
variant="plain"
/>
{emptyState.loading ? (
{emptyState.loading && !shouldShowConnectionStatus ? (
<View className="mt-4 items-center">
<ActivityIndicator color={accentColor} />
</View>
) : null}
{shouldShowConnectionStatus && Platform.OS === "ios" ? (
<View className="mt-4">
<WorkspaceConnectionStatus
state={props.catalogState}
onPress={props.onOpenEnvironments}
variant="sidebar"
/>
</View>
) : null}
</View>
{connectionStatus}
</View>
Expand Down Expand Up @@ -460,8 +470,8 @@ export function HomeScreen(props: HomeScreenProps) {
ListHeaderComponent={listHeader}
ListEmptyComponent={listEmpty}
style={{ flex: 1 }}
automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}
contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
showsVerticalScrollIndicator={false}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
Expand Down
17 changes: 17 additions & 0 deletions apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,21 @@ describe("workspace connection status", () => {
expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true);
expect(workspaceConnectionStatusLabel(state)).toBe("Could not reach Julius’s Mac mini");
});

it("shows shell catch-up while cached threads remain visible", () => {
const state = workspaceState({ hasPendingShellSnapshot: true });

expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true);
expect(workspaceConnectionStatusLabel(state)).toBe("Syncing threads...");
});

it("distinguishes initial shell loading from cached catch-up", () => {
const state = workspaceState({
hasLoadedShellSnapshot: false,
hasPendingShellSnapshot: true,
});

expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true);
expect(workspaceConnectionStatusLabel(state)).toBe("Loading threads...");
});
});
7 changes: 5 additions & 2 deletions apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function WorkspaceConnectionStatus(props: {
readonly variant?: "floating" | "sidebar";
}) {
const iconColor = useThemeColor("--color-icon-muted");
const isReconnecting = props.state.connectingEnvironments.length > 0;
const isSynchronizing =
props.state.networkStatus !== "offline" &&
props.state.connectionError === null &&
(props.state.connectingEnvironments.length > 0 || props.state.hasPendingShellSnapshot);
const variant = props.variant ?? "floating";

return (
Expand All @@ -37,7 +40,7 @@ export function WorkspaceConnectionStatus(props: {
: undefined
}
>
{isReconnecting ? (
{isSynchronizing ? (
<ActivityIndicator color={iconColor} size="small" />
) : (
<SymbolView name="wifi.slash" size={15} tintColor={iconColor} type="monochrome" />
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/home/workspace-connection-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function shouldShowWorkspaceConnectionStatus(state: WorkspaceState): bool
state.networkStatus === "offline" ||
state.connectionError !== null ||
state.hasConnectingEnvironment ||
state.hasPendingShellSnapshot ||
(state.hasLoadedShellSnapshot && !state.hasReadyEnvironment)
);
}
Expand All @@ -18,5 +19,8 @@ export function workspaceConnectionStatusLabel(state: WorkspaceState): string {
return `Reconnecting ${state.connectingEnvironments.length} environments`;
}
if (state.connectionError !== null) return state.connectionError;
if (state.hasPendingShellSnapshot) {
return state.hasLoadedShellSnapshot ? "Syncing threads..." : "Loading threads...";
}
return "Not connected";
}
Loading
Loading