Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5d4c202
fix(web,mobile): Limits bars show the even-pace mark as a tick in a g…
juliusmarminge Oct 20, 2026
7e289ae
test(web): pooled Limits segments narrower than 6px draw no pace mark…
juliusmarminge Oct 20, 2026
dac3437
fix(mobile): the composer stays one row until the draft wraps (#20812)
juliusmarminge Oct 20, 2026
860e6d2
fix(web): restore left-aligned markdown table cells (#22482)
t3-code[bot] Oct 20, 2026
0c85b0d
feat(scheduled-tasks): event filters for webhook deliveries (#22194)
juliusmarminge Oct 20, 2026
ec58572
fix(mobile): prevent update checker from crashing iOS at launch (#22488)
juliusmarminge Oct 20, 2026
2ebe280
feat(scheduled-tasks): replay dedupe and coalescing for webhook deliv…
juliusmarminge Oct 20, 2026
14b47ee
ci(release): Make Windows arm64 CI job 50% faster (#22428)
jahands Oct 20, 2026
4c48118
ci(release): Windows arm64 verify no longer runs the crashing ARM Pow…
t3dotgg Oct 20, 2026
ca140c4
refactor(source-control): GitCafe creates repositories and pull reque…
juliusmarminge Oct 20, 2026
5bcc90e
test(web): fix release browser test failures (#22508)
t3dotgg Oct 20, 2026
c8667fd
fix(mobile): connecting an environment no longer crashes the header (…
juliusmarminge Oct 20, 2026
4af3231
fix(mobile): folded Back clears the detail route before the next sele…
juliusmarminge Oct 20, 2026
fe27e8e
ci(release): release tests stop timing out and finish in about 5 minu…
t3dotgg Oct 20, 2026
5d27ecd
test(server): Claude steer test no longer hangs for 120s under CI loa…
t3dotgg Oct 20, 2026
f9dd259
test: remove the timing races behind recurring CI and release failure…
t3dotgg Oct 20, 2026
fcb20e4
feat(web): render Mermaid diagrams with pele (#21472)
maria-rcks Oct 20, 2026
7f58640
Merge upstream main fcb20e49aa (2026-10-20) into the fork
orbitingflea Oct 20, 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ jobs:

# 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.
# release.yml mirrors this test, test_web, and test_server split.
test_web:
name: Test Web
needs: changes
Expand Down
420 changes: 272 additions & 148 deletions .github/workflows/release-desktop.yml

Large diffs are not rendered by default.

64 changes: 56 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ jobs:
run: vp run typecheck

# Keep tests on their own runners, using the same package split and server
# shards as CI, so the release check job does not spend its budget on tests.
# shards as CI (test, test_web, test_server in ci.yml). Keep the two in sync.
test:
name: Release tests
needs: [preflight]
Expand Down Expand Up @@ -286,23 +286,67 @@ jobs:
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config build-essential

# The web package's browser project runs React tests in headless Chromium.
- name: Test
run: vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' --filter '!@t3tools/web' test

# Web browser tests time out when they share a runner with the other package
# suites: Chromium gets too little CPU and diff workers miss their 10s start budget.
test_web:
name: Release web tests
needs: [preflight]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false

- name: Setup dependencies
uses: ./.github/actions/setup-ci-deps

- name: Resolve Playwright version
id: playwright_version
run: echo "version=$(node -p "require('./apps/web/node_modules/playwright/package.json').version")" >> "$GITHUB_OUTPUT"

- name: Cache Chromium
id: chromium_cache
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}

- name: Install Chromium for web browser tests
run: node apps/web/node_modules/playwright/cli.js install --with-deps --only-shell chromium
if: steps.chromium_cache.outputs.cache-hit != 'true'
run: node apps/web/node_modules/playwright/cli.js install --only-shell chromium

- name: Install missing Chromium system dependencies
run: |
browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
test -n "$browser"
if ldd "$browser" | grep '=> not found'; then
node apps/web/node_modules/playwright/cli.js install-deps chromium
fi

- name: Test
run: vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' test
run: vp run --filter @t3tools/web test

# Server test files run one at a time, so more small runners beat fewer big ones.
test_server:
name: Release server tests ${{ matrix.shard }}
needs: [preflight]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
shard: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -659,6 +703,8 @@ jobs:
resource_key: win32-x64
cli_archive: true

# Windows arm64 runners have 4 vCPUs, and the packaging is mostly CPU-bound,
# so the build runs on Linux and only the native checks run on arm64.
desktop_win_arm64:
name: Desktop Windows arm64
needs: [preflight, relay_public_config, build_bundle]
Expand All @@ -674,7 +720,8 @@ jobs:
clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
relay_url: ${{ needs.relay_public_config.outputs.relay_url }}
label: Windows arm64
runner: windows-11-arm
runner: blacksmith-16vcpu-ubuntu-2404
verify_runner: windows-11-arm
platform: win
target: nsis
arch: arm64
Expand All @@ -690,6 +737,7 @@ jobs:
relay_public_config,
quality,
test,
test_web,
test_server,
desktop_mac_arm64,
desktop_mac_x64,
Expand All @@ -698,7 +746,7 @@ jobs:
desktop_win_x64,
desktop_win_arm64,
]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.test.result == 'success' && needs.test_server.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' }}
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.test.result == 'success' && needs.test_web.result == 'success' && needs.test_server.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView(
private var applyingNativeValue = false
private var desiredLineHeightPx = 0
private var lastContentHeight = 0
private var lastContentWidth = 0
private var contentInsetVertical = 0
private var tokensJson = "[]"
private var tokens: List<ComposerToken> = emptyList()
Expand Down Expand Up @@ -424,12 +425,19 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView(
}

private fun emitContentSizeIfNeeded() {
val height = editor.layout?.height ?: editor.measuredHeight
val contentHeight = height + contentInsetVertical * 2
if (contentHeight == lastContentHeight) return
// The laid-out text without insets. A one-line draft reports its natural width, so
// JS can tell whether it fits a narrower row than the one it is laid out in; past
// one line only the height matters. Trailing whitespace hangs past a line without
// wrapping it, so getLineMax leaves it out.
val layout = editor.layout
val contentHeight = layout?.height ?: editor.measuredHeight
val contentWidth = if (layout?.lineCount == 1) layout.getLineMax(0).toInt() else 0
if (contentHeight == lastContentHeight && contentWidth == lastContentWidth) return
lastContentHeight = contentHeight
lastContentWidth = contentWidth
val density = resources.displayMetrics.density
onComposerContentSizeChange(
mapOf("height" to contentHeight / resources.displayMetrics.density),
mapOf("width" to contentWidth / density, "height" to contentHeight / density),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,28 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
}

private func emitContentSizeIfNeeded() {
let nextSize = textView.contentSize
// The laid-out text without insets. A one-line draft reports its natural width, so
// JS can tell whether it fits a narrower row than the one it is laid out in; past
// one line only the height matters, so layout stops at the second line. The width
// comes from the line fragment because usedRect spans the container once the view
// scrolls. A trailing newline's empty line only exists as the extra fragment.
let layout = textView.layoutManager
let container = textView.textContainer
var lines = 0
var width: CGFloat = 0
var height: CGFloat = 0
let glyphs = NSRange(location: 0, length: layout.numberOfGlyphs)
layout.enumerateLineFragments(forGlyphRange: glyphs) { _, used, _, _, stop in
lines += 1
width = used.width
height = used.maxY
if lines > 1 { stop.pointee = true }
}
if lines < 2, layout.extraLineFragmentTextContainer === container {
lines += 1
height = max(height, layout.extraLineFragmentUsedRect.maxY)
}
let nextSize = CGSize(width: lines == 1 ? ceil(width) : 0, height: ceil(height))
guard abs(nextSize.width - lastContentSize.width) > 0.5 ||
abs(nextSize.height - lastContentSize.height) > 0.5 else {
return
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"react-native-pdf-jsi": "5.0.0",
"react-native-reanimated": "4.7.1",
"react-native-safe-area-context": "~5.10.1",
"react-native-screens": "file:deps/react-native-screens-5.0.0-t3.29.tgz",
"react-native-screens": "file:deps/react-native-screens-5.0.0-t3.30.tgz",
"react-native-shiki-engine": "^0.3.12",
"react-native-svg": "15.16.0",
"react-native-webview": "14.0.1",
Expand Down
123 changes: 76 additions & 47 deletions apps/mobile/src/components/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
type ViewStyle,
} from "react-native";

import Animated, { FadeIn, LinearTransition } from "react-native-reanimated";

import { cn } from "../lib/cn";
import { AppText as Text } from "./ui/AppText";
import { SymbolView } from "./ui/AppSymbol";
Expand All @@ -21,6 +23,10 @@ import { useAndroidControlSizing } from "./ui/useAndroidControlSizing";
const COMPOSER_TOOLBAR_GAP = 8;
const COMPOSER_TOOLBAR_FADE_WIDTH = 18;
const COMPOSER_TOOLBAR_SCROLL_EPSILON = 4;
const INLINE_CONTROL_LABEL_ENTERING = FadeIn.duration(160);
// Android runs no composer layout transitions, so the toolbar never trails the dock.
const INLINE_CONTROL_WIDTH_TRANSITION =
Platform.OS === "android" ? undefined : LinearTransition.duration(180);

export function ComposerInlineControl(props: {
readonly accessibilityHint?: string;
Expand All @@ -30,6 +36,8 @@ export function ComposerInlineControl(props: {
readonly icon?: ComponentProps<typeof SymbolView>["name"];
readonly renderIcon?: (size: number) => ReactNode;
readonly label: string;
/** Show only the icon and chevron; the label still names the control for accessibility. */
readonly iconOnly?: boolean;
readonly maxWidth?: ViewStyle["maxWidth"];
readonly onPress?: () => void;
readonly selected?: boolean;
Expand All @@ -38,56 +46,77 @@ export function ComposerInlineControl(props: {
readonly showChevron?: boolean;
}) {
const { scale, smallIconSize } = useAndroidControlSizing();
// The label fades in as the control widens, and leaves at once: an exiting view keeps
// drawing at its old place while the chevron moves into it. The clip keeps content laid out
// at the final width inside the frame while it eases there.
const animatesLabel = props.iconOnly !== undefined;
return (
<Pressable
accessibilityLabel={props.accessibilityLabel ?? props.label}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.static ? undefined : "button"}
accessibilityState={
props.static ? undefined : { disabled: props.disabled, selected: props.selected }
}
className="h-11 flex-row items-center gap-2 rounded-xl px-2 active:bg-subtle"
disabled={props.disabled || props.static}
onPress={props.onPress}
style={{ maxWidth: props.maxWidth ?? 190, opacity: props.disabled ? 0.45 : 1 }}
<Animated.View
layout={animatesLabel ? INLINE_CONTROL_WIDTH_TRANSITION : undefined}
style={{
maxWidth: props.maxWidth ?? 190,
overflow: animatesLabel ? "hidden" : undefined,
}}
>
{props.renderIcon ? (
<View
className="size-4 shrink-0 items-center justify-center"
style={Platform.OS === "android" ? { width: 14 * scale, height: 14 * scale } : undefined}
>
{props.renderIcon(smallIconSize)}
</View>
) : props.icon ? (
<SymbolView
name={props.icon}
size={smallIconSize}
tintColorClassName={
props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
}
type="monochrome"
/>
) : null}
<Text
className={cn(
"shrink text-sm font-t3-medium",
props.emphasized || props.selected ? "text-foreground" : "text-foreground-muted",
)}
numberOfLines={1}
<Pressable
accessibilityLabel={props.accessibilityLabel ?? props.label}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.static ? undefined : "button"}
accessibilityState={
props.static ? undefined : { disabled: props.disabled, selected: props.selected }
}
className="h-11 flex-row items-center gap-2 rounded-xl px-2 active:bg-subtle"
disabled={props.disabled || props.static}
onPress={props.onPress}
style={{ opacity: props.disabled ? 0.45 : 1 }}
>
{props.label}
</Text>
{props.showChevron === false ? null : (
<SymbolView
name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"}
size={Math.round(10 * scale)}
tintColorClassName={
props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
}
type="monochrome"
/>
)}
</Pressable>
{props.renderIcon ? (
<View
className="size-4 shrink-0 items-center justify-center"
style={
Platform.OS === "android" ? { width: 14 * scale, height: 14 * scale } : undefined
}
>
{props.renderIcon(smallIconSize)}
</View>
) : props.icon ? (
<SymbolView
name={props.icon}
size={smallIconSize}
tintColorClassName={
props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
}
type="monochrome"
/>
) : null}
{props.iconOnly ? null : (
<Animated.View
className="shrink"
entering={animatesLabel ? INLINE_CONTROL_LABEL_ENTERING : undefined}
>
<Text
className={cn(
"text-sm font-t3-medium",
props.emphasized || props.selected ? "text-foreground" : "text-foreground-muted",
)}
numberOfLines={1}
>
{props.label}
</Text>
</Animated.View>
)}
{props.showChevron === false ? null : (
<SymbolView
name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"}
size={Math.round(10 * scale)}
tintColorClassName={
props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
}
type="monochrome"
/>
)}
</Pressable>
</Animated.View>
);
}

Expand Down
Loading
Loading