Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d51bc58
fix(tools): show images from MCP and OpenCode outputs (#30389)
Yash-Singh1 Nov 4, 2026
7ccd02c
fix(mobile): smoother Settings and composer transitions on wide Andro…
juliusmarminge Nov 4, 2026
eb1db29
fix(mobile): the Android split slides as one strip, so sidebar and de…
juliusmarminge Nov 4, 2026
737afe2
fix(server): retry unstarted runs after recovery (#30560)
Yash-Singh1 Nov 4, 2026
b3011e9
perf(server): narrow agent awareness startup reads (#30561)
Yash-Singh1 Nov 4, 2026
059804d
fix(web): validate terminal links against filesystem metadata (#30526)
Yash-Singh1 Nov 4, 2026
647bd14
fix(server): avoid message ordering scans during recovery and settlem…
Yash-Singh1 Nov 4, 2026
567a105
fix(client-runtime): expire missing file metadata per thread (#30606)
Yash-Singh1 Nov 4, 2026
3368dd7
fix(web): align usage table padding and thread status rows
Yash-Singh1 Nov 4, 2026
e68e19d
fix(mobile): every Android content pane gets its rounded card from Sc…
juliusmarminge Nov 4, 2026
bb4afa5
chore(release): prepare v0.1.7
t3-code-bot[bot] Nov 4, 2026
8b44f42
fix(web): load selected diff metadata before file patches (#30567)
Yash-Singh1 Nov 4, 2026
7f782b4
test(evals): measure how agents get secrets only the user has (#30256)
gabrielelpidio Nov 4, 2026
9e85538
feat(server): agents ask for secrets through request_secret instead o…
gabrielelpidio Nov 4, 2026
dd5f98e
fix(relay): use released drizzle rc.5 and fail installs on unmet peer…
juliusmarminge Nov 4, 2026
f408a1d
Merge commit 'dd5f98e' into fix/spawn-host-upstream-dd5f98e
fireboltdude1357 Nov 4, 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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,18 @@ jobs:
env:
TEST_PACKAGES: ${{ needs.changes.outputs.test_packages }}
run: |
set -o pipefail
filters=(--filter '!t3' --filter '!@t3tools/monorepo' --filter '!@t3tools/web')
if [[ -n "$TEST_PACKAGES" ]]; then
filters=()
for package in $TEST_PACKAGES; do filters+=(--filter "$package"); done
fi
vp run --parallel --concurrency-limit 4 "${filters[@]}" test
# The runner's trace writer can crash on surrogate pairs in test output.
vp run --parallel --concurrency-limit 4 "${filters[@]}" test 2>&1 | python3 -u -c '
import sys
for line in sys.stdin:
sys.stdout.write(line.encode("ascii", "backslashreplace").decode("ascii"))
'
# The web suite is the largest outside the server. In the shared Test job it
# started last and ran alone for about 40s after everything else finished.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"type": "module",
"main": "dist-electron/boot.cjs",
Expand Down
10 changes: 8 additions & 2 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1042,9 +1042,15 @@ export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navig
return (
<Navigator
screenLayout={(props) => <GuardedScreenLayout {...props} />}
screenOptions={({ route }) =>
screenOptions={({ route, navigation }) =>
Platform.OS === "android"
? { animation: androidWorkspaceDestinationAnimation(route.name, usesSplitView) }
? {
animation: androidWorkspaceDestinationAnimation(
route,
navigation.getState().routes,
usesSplitView,
),
}
: {}
}
/>
Expand Down
31 changes: 28 additions & 3 deletions apps/mobile/src/components/ui/ScreenContent.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
import type { ReactNode } from "react";
import { Platform, View } from "react-native";
import { Platform, View, type LayoutChangeEvent } from "react-native";

const SURFACE_CLASS_NAMES = {
sheet: "bg-sheet-solid",
screen: "bg-screen",
canvas: "bg-thread-canvas",
drawer: "bg-drawer",
} as const;

/**
* The body below a screen's AppBar. On Android it is the rounded card every pane and sheet
* draws under its bar; a screen picks only the card's surface.
*/
export function ScreenContent({
children,
surface = "sheet",
insetHorizontal = false,
fitToContents = false,
onLayout,
}: {
readonly children: ReactNode;
readonly surface?: keyof typeof SURFACE_CLASS_NAMES;
readonly insetHorizontal?: boolean;
readonly fitToContents?: boolean;
readonly onLayout?: (event: LayoutChangeEvent) => void;
}) {
if (Platform.OS !== "android") return children;
if (Platform.OS !== "android") {
return onLayout ? (
<View className="flex-1" onLayout={onLayout}>
{children}
</View>
) : (
children
);
}

return (
<View className={fitToContents ? "shrink bg-header" : "flex-1 bg-header"}>
<View
className="overflow-hidden rounded-t-[28px] bg-sheet-solid"
className={`overflow-hidden rounded-t-[28px] ${SURFACE_CLASS_NAMES[surface]}`}
collapsable={false}
onLayout={onLayout}
style={{
flexShrink: 1,
flexGrow: fitToContents ? 0 : 1,
Expand Down
15 changes: 12 additions & 3 deletions apps/mobile/src/components/ui/SegmentedControl.shared.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useState } from "react";
import { I18nManager, Platform, Pressable, View } from "react-native";
import Animated, { useAnimatedStyle, useReducedMotion, withTiming } from "react-native-reanimated";
import Animated, {
useAnimatedStyle,
useDerivedValue,
useReducedMotion,
withTiming,
} from "react-native-reanimated";

import { segmentedControlColors } from "./segmentedControlColors";
import { AppText } from "./AppText";
Expand All @@ -18,10 +23,14 @@ export function SegmentedControl<Value extends string | number>(
const reduceMotion = useReducedMotion();
const selectedIndex = props.options.findIndex((option) => option.value === props.selected);
const segmentWidth = props.options.length ? Math.max(0, width - 6) / props.options.length : 0;
const selectionOffset = selectedIndex * segmentWidth * (I18nManager.isRTL ? -1 : 1);
const direction = I18nManager.isRTL ? -1 : 1;
// The index animates, not the offset, so a resize moves the selection without sliding it.
const selectedPosition = useDerivedValue(() =>
withTiming(selectedIndex, { duration: reduceMotion ? 0 : 160 }),
);
const selectionStyle = useAnimatedStyle(() => ({
width: segmentWidth,
transform: [{ translateX: withTiming(selectionOffset, { duration: reduceMotion ? 0 : 160 }) }],
transform: [{ translateX: selectedPosition.value * segmentWidth * direction }],
}));
const { trackColor, selectedColor, selectedTextColor, mutedColor } = segmentedControlColors(
colors,
Expand Down
241 changes: 122 additions & 119 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { usePaneRole } from "../../components/ui/Pane";
import { ScreenContent } from "../../components/ui/ScreenContent";
import { useActiveNewTask } from "../../state/active-new-task";
import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
import { useAndroidControlSizing } from "../../components/ui/useAndroidControlSizing";
Expand Down Expand Up @@ -672,45 +673,51 @@ export function HomeScreen(props: HomeScreenProps) {
if (!hasAnyThreads) {
return (
<View className={containerClassName}>
<View
className={cn(
"flex-1 items-center justify-center px-8",
contentBackground,
Platform.OS === "android" && "overflow-hidden rounded-t-[28px]",
)}
style={{
paddingBottom: Platform.OS === "ios" ? iosBottomClearance : Math.max(insets.bottom, 24),
paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,
paddingLeft: 32 + (contentSideInsets?.left ?? 0),
paddingRight: 32 + (contentSideInsets?.right ?? 0),
}}
>
<View className="w-full max-w-[430px]">
<EmptyState
title={emptyState.title}
detail={emptyState.detail}
actionLabel={!props.catalogState.hasReadyEnvironment ? "Add environment" : undefined}
onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}
action={
Platform.OS === "android" && !props.catalogState.hasReadyEnvironment ? (
<FloatingActionButton
label="Add environment"
icon="plus"
variant="extended"
tone="primary"
onPress={props.onAddConnection}
/>
) : undefined
}
variant="plain"
/>
{emptyState.loading ? (
<View className="mt-4 items-center">
<AppActivityIndicator colorClassName="accent-icon-muted" />
</View>
) : null}
<ScreenContent surface={sidebarSurface ? "drawer" : "screen"}>
<View
className={cn(
"flex-1 items-center justify-center px-8",
Platform.OS !== "android" && contentBackground,
)}
style={{
paddingBottom:
Platform.OS === "ios" ? iosBottomClearance : Math.max(insets.bottom, 24),
paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,
paddingLeft: 32 + (contentSideInsets?.left ?? 0),
paddingRight: 32 + (contentSideInsets?.right ?? 0),
}}
>
<View className="w-full max-w-[430px]">
<EmptyState
title={emptyState.title}
detail={emptyState.detail}
actionLabel={
!props.catalogState.hasReadyEnvironment ? "Add environment" : undefined
}
onAction={
!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined
}
action={
Platform.OS === "android" && !props.catalogState.hasReadyEnvironment ? (
<FloatingActionButton
label="Add environment"
icon="plus"
variant="extended"
tone="primary"
onPress={props.onAddConnection}
/>
) : undefined
}
variant="plain"
/>
{emptyState.loading ? (
<View className="mt-4 items-center">
<AppActivityIndicator colorClassName="accent-icon-muted" />
</View>
) : null}
</View>
</View>
</View>
</ScreenContent>
</View>
);
}
Expand Down Expand Up @@ -766,97 +773,93 @@ export function HomeScreen(props: HomeScreenProps) {

if (Platform.OS === "android" && threadListV2Items.length === 0) {
return (
<View className="flex-1 bg-header">
<ScreenContent surface="screen">
<View
className="flex-1 items-center justify-center overflow-hidden rounded-t-[28px] bg-screen px-4"
className="flex-1 items-center justify-center px-4"
style={{ paddingBottom: insets.bottom }}
>
{v2ListEmpty}
</View>
</View>
</ScreenContent>
);
}

return (
<View className={containerClassName}>
<View
className={
Platform.OS === "android"
? "flex-1 overflow-hidden rounded-t-[28px] bg-screen"
: cn("flex-1", contentBackground)
}
>
<SwipeableScrollGateProvider enabled={swipeEnabled} activation={swipeRowActivation}>
<GestureDetector gesture={homeScrollGesture}>
<AnimatedLegendList
itemLayoutAnimation={sidebarSurface ? THREAD_SIDEBAR_ROW_LAYOUT : undefined}
ref={listRef}
onLoad={() => activateVisibleRows(threadListV2Items)}
onTouchStart={(event) => trackListTouches(event, true)}
onTouchEnd={(event) => trackListTouches(event, false)}
onTouchCancel={(event) => trackListTouches(event, false)}
renderScrollComponent={Platform.OS === "ios" ? renderHomeScrollView : undefined}
data={threadListV2Items}
renderItem={renderV2Item}
keyExtractor={v2KeyExtractor}
getItemType={(item) => item.type}
itemsAreEqual={threadListV2ListItemsAreEqual}
estimatedItemSize={ESTIMATED_THREAD_LIST_V2_ROW_HEIGHT}
drawDistance={THREAD_LIST_V2_DRAW_DISTANCE}
recycleItems
extraData={v2ExtraData}
ListHeaderComponent={v2ListHeader}
ListFooterComponent={
settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (
<ThreadListV2ShowMoreRow
hiddenCount={threadListV2Layout.hiddenSettledCount}
{...(olderSettledShelf.loading
? { state: "loading" as const }
: olderSettledShelf.failed
? { state: "failed" as const }
: {})}
onPress={() => {
olderSettledShelf.retry();
showMoreSettled();
}}
/>
) : null
}
ListEmptyComponent={v2ListEmpty}
style={{ flex: 1 }}
automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}
// UIKit owns the bar's inset, so a minimizing bar and a resized column move
// the list in the same frame as the bar.
contentInsetAdjustmentBehavior="automatic"
contentInsetStartAdjustment={Platform.OS === "ios" ? insets.top : 0}
// RN scrollTo clamps against raw insets and ignores the automatic bar inset.
scrollToOverflowEnabled
initialScrollOffset={
initialScrollOffset > 0
? initialScrollOffset - (Platform.OS === "ios" ? insets.top : 0)
: undefined
}
showsVerticalScrollIndicator={false}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
{...scrollGateHandlers}
scrollEventThrottle={16}
contentContainerStyle={{
paddingLeft:
(contentSideInsets?.left ?? 0) + (sidebarSurface ? 8 : 0) + androidListGutter,
paddingRight:
(contentSideInsets?.right ?? 0) + (sidebarSurface ? 8 : 0) + androidListGutter,
paddingBottom:
Platform.OS === "ios"
? iosBottomClearance - (columnMetrics?.safeArea.bottom ?? insets.bottom)
: (workspace?.bottomClearance ??
Math.max(insets.bottom, 16) +
(Platform.OS === "android" ? fabClearance : 88)),
}}
/>
</GestureDetector>
</SwipeableScrollGateProvider>
</View>
<ScreenContent surface="screen">
<View className={cn("flex-1", Platform.OS !== "android" && contentBackground)}>
<SwipeableScrollGateProvider enabled={swipeEnabled} activation={swipeRowActivation}>
<GestureDetector gesture={homeScrollGesture}>
<AnimatedLegendList
itemLayoutAnimation={sidebarSurface ? THREAD_SIDEBAR_ROW_LAYOUT : undefined}
ref={listRef}
onLoad={() => activateVisibleRows(threadListV2Items)}
onTouchStart={(event) => trackListTouches(event, true)}
onTouchEnd={(event) => trackListTouches(event, false)}
onTouchCancel={(event) => trackListTouches(event, false)}
renderScrollComponent={Platform.OS === "ios" ? renderHomeScrollView : undefined}
data={threadListV2Items}
renderItem={renderV2Item}
keyExtractor={v2KeyExtractor}
getItemType={(item) => item.type}
itemsAreEqual={threadListV2ListItemsAreEqual}
estimatedItemSize={ESTIMATED_THREAD_LIST_V2_ROW_HEIGHT}
drawDistance={THREAD_LIST_V2_DRAW_DISTANCE}
recycleItems
extraData={v2ExtraData}
ListHeaderComponent={v2ListHeader}
ListFooterComponent={
settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (
<ThreadListV2ShowMoreRow
hiddenCount={threadListV2Layout.hiddenSettledCount}
{...(olderSettledShelf.loading
? { state: "loading" as const }
: olderSettledShelf.failed
? { state: "failed" as const }
: {})}
onPress={() => {
olderSettledShelf.retry();
showMoreSettled();
}}
/>
) : null
}
ListEmptyComponent={v2ListEmpty}
style={{ flex: 1 }}
automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}
// UIKit owns the bar's inset, so a minimizing bar and a resized column move
// the list in the same frame as the bar.
contentInsetAdjustmentBehavior="automatic"
contentInsetStartAdjustment={Platform.OS === "ios" ? insets.top : 0}
// RN scrollTo clamps against raw insets and ignores the automatic bar inset.
scrollToOverflowEnabled
initialScrollOffset={
initialScrollOffset > 0
? initialScrollOffset - (Platform.OS === "ios" ? insets.top : 0)
: undefined
}
showsVerticalScrollIndicator={false}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
{...scrollGateHandlers}
scrollEventThrottle={16}
contentContainerStyle={{
paddingLeft:
(contentSideInsets?.left ?? 0) + (sidebarSurface ? 8 : 0) + androidListGutter,
paddingRight:
(contentSideInsets?.right ?? 0) + (sidebarSurface ? 8 : 0) + androidListGutter,
paddingBottom:
Platform.OS === "ios"
? iosBottomClearance - (columnMetrics?.safeArea.bottom ?? insets.bottom)
: (workspace?.bottomClearance ??
Math.max(insets.bottom, 16) +
(Platform.OS === "android" ? fabClearance : 88)),
}}
/>
</GestureDetector>
</SwipeableScrollGateProvider>
</View>
</ScreenContent>
</View>
);
}
Loading
Loading