Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8045537
perf(orchestration): thread snapshots send each long ID once (#24643)
t3dotgg Oct 25, 2026
e666c01
fix(mobile): keep visible diff files highlighted (#24682)
juliusmarminge Oct 25, 2026
8fdaf34
fix(mobile): Settings split view keeps its selection and its header m…
juliusmarminge Oct 25, 2026
90c455f
fix(mobile): every Duo header blurs under the vertical bar, from one …
juliusmarminge Oct 25, 2026
b808f7f
feat(settings): follow-ups steer the running turn by default (#24718)
maria-rcks Oct 25, 2026
a0e0f8a
fix(mobile): Duo threads no longer open shifted left (#24732)
juliusmarminge Oct 25, 2026
c2845de
fix(mobile): PR links open in the app the same way they do on web (#2…
juliusmarminge Oct 25, 2026
e9d6125
feat(web): command palette search finds options inside submenus (#24709)
maria-rcks Oct 25, 2026
8c0670b
fix(mobile): one way to select a page, so split view stops pushing (#…
juliusmarminge Oct 25, 2026
c83342d
fix(mobile): Settings pages no longer flicker when switching beside t…
juliusmarminge Oct 25, 2026
c79ef8c
fix(server): use Codex native collaboration modes (#24737)
t3dotgg Oct 25, 2026
242d00c
fix(clients): threads no longer flash in the inbox before they settle…
t3dotgg Oct 25, 2026
906d1e9
fix(mobile): the iPhone compose icon matches the tab icons' size (#24…
juliusmarminge Oct 25, 2026
a43b003
feat(infra): share Vite task cache with Axiom savings telemetry (#24693)
juliusmarminge Oct 25, 2026
ebd29ca
fix(mobile): screen lists register with their header, enforced by lin…
juliusmarminge Oct 25, 2026
b3d382e
feat(mobile): search Settings, and keep its sidebar header clean (#24…
juliusmarminge Oct 25, 2026
8d3261e
fix(mobile): threads open with less waiting, and the Threads tab reme…
juliusmarminge Oct 25, 2026
15702a9
fix(mobile): reduce diff entry and source highlighting delays (#24770)
juliusmarminge Oct 25, 2026
17dc1af
perf(client): skip unchanged shell and config cache writes (#24266)
juliusmarminge Oct 25, 2026
3e20b65
fix(mobile): remove perpetual reconnect indicator animation (#24273)
juliusmarminge Oct 25, 2026
f694a19
feat(mobile): close the keyboard from the Duo's side bar (#24696)
t3dotgg Oct 25, 2026
cfdfc2a
fix(mobile): dispatch native toolbar actions to current handlers (#24…
juliusmarminge Oct 25, 2026
2da177c
fix(server): PR watch wakes say the PR stays linked and watched (#24781)
t3dotgg Oct 25, 2026
003b64d
feat(server): thread tools return a ready t3-thread link (#24784)
t3dotgg Oct 25, 2026
a96cf08
fix(server): running task results tell the agent to end its turn (#24…
t3dotgg Oct 25, 2026
a8603af
fix(server): mobile push no longer says "completed" between runs (#24…
t3dotgg Oct 25, 2026
e641761
fix(server): delegated children no longer see pull request watch tool…
t3dotgg Oct 25, 2026
5398be0
fix(mobile): search stays open after going back from a thread (#24748)
t3dotgg Oct 25, 2026
e0b1940
feat(server): request_secret can save a CLI token to a private file (…
t3dotgg Oct 25, 2026
639dc20
fix(server): threads settle right after a shell merge of an unlinked …
t3dotgg Oct 25, 2026
5d5b6e0
fix(server): worktree setup no longer hangs when a shell startup prom…
t3dotgg Oct 25, 2026
dca46e2
feat(server): link pull requests agents create from the shell (#24813)
t3dotgg Oct 25, 2026
c8231f9
fix(mobile): queue Steer button no longer opens the editor on a near …
t3dotgg Oct 25, 2026
056b445
fix(usage): Gemini 4 Argon usage from Antigravity now has a price (#2…
t3dotgg Oct 25, 2026
ee5c64d
Merge remote-tracking branch 'upstream/main' into yordis/chore-sync-u…
yordis Oct 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
22 changes: 22 additions & 0 deletions .github/actions/setup-ci-deps/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: Setup CI dependencies
description: Install Vite+ and the workspace dependencies from cache.
inputs:
task-cache-read-token:
description: Read-only credential for the private maintainer task cache
required: false
default: ""
runs:
using: composite
steps:
Expand Down Expand Up @@ -65,3 +70,20 @@ runs:
- name: Install dependencies
shell: bash
run: vp install

- name: Enable shared task cache
if: >-
github.repository == 'pingdotgg/t3code' &&
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
hashFiles('.task-cache.json') != '' &&
inputs.task-cache-read-token != ''
shell: bash
env:
T3_TASK_CACHE_READ_TOKEN: ${{ inputs.task-cache-read-token }}
run: |
node infra/task-cache/scripts/install-ci.ts
if [[ ( "$GITHUB_EVENT_NAME" == pull_request && "$GITHUB_BASE_REF" == main ) || ( "$GITHUB_EVENT_NAME" == push && "$GITHUB_REF" == refs/heads/main ) ]]; then
echo 'VP_REMOTE_CACHE=read-write' >> "$GITHUB_ENV"
else
echo 'VP_REMOTE_CACHE=read' >> "$GITHUB_ENV"
fi
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ jobs:
# file, docs included. The `Check` job at the end of this file passes only
# when every other job passes or was skipped by `changes`.
lint:
permissions:
contents: read
id-token: write
name: Lint
runs-on: ubuntu-24.04
timeout-minutes: 10
Expand All @@ -82,6 +85,8 @@ jobs:

- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

# electron's install.js (run by ensure:electron) reads this cache before it
# downloads from GitHub. The key changes only with the Electron version.
Expand All @@ -108,6 +113,9 @@ jobs:
run: vp check

build:
permissions:
contents: read
id-token: write
name: Build and Typecheck
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.build != 'false' || needs.changes.outputs.typecheck != 'false') }}
Expand Down Expand Up @@ -139,6 +147,8 @@ jobs:

- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

# electron's install.js (run by ensure:electron) reads this cache before it
# downloads from GitHub. The key changes only with the Electron version.
Expand Down Expand Up @@ -197,6 +207,9 @@ jobs:
# dependency layers. The concurrency limit stays at the default 4 so peak load
# per runner is unchanged.
test:
permissions:
contents: read
id-token: write
name: Test
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.test != 'false' }}
Expand Down Expand Up @@ -237,6 +250,8 @@ jobs:

- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

# electron's install.js (run by ensure:electron) reads this cache before it
# downloads from GitHub. The key changes only with the Electron version.
Expand Down Expand Up @@ -278,6 +293,9 @@ jobs:
# 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:
permissions:
contents: read
id-token: write
name: Test Web
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.test_web != 'false' }}
Expand All @@ -295,6 +313,8 @@ jobs:

- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

# The browser project runs React tests in headless Chromium. Same cache key as
# test_server, so both jobs share one downloaded browser.
Expand Down Expand Up @@ -333,6 +353,9 @@ jobs:
# The shards are the longest jobs, so they gate themselves instead of waiting
# ~15s for `changes`: a shard the change cannot affect stops after checkout.
test_server:
permissions:
contents: read
id-token: write
name: Test Server ${{ matrix.shard }}
runs-on: ubuntu-24.04
timeout-minutes: 10
Expand All @@ -359,6 +382,8 @@ jobs:
- name: Setup Vite+
if: steps.changes.outputs.test_server != 'false'
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

# No Electron setup here: `t3` (apps/server) has no Electron dependency
# and none of its tests touch the runtime. Only the non-server `test`
Expand Down Expand Up @@ -495,6 +520,9 @@ jobs:
run: vp run lint:mobile

release_smoke:
permissions:
contents: read
id-token: write
name: Release Smoke
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.release_smoke != 'false' }}
Expand All @@ -512,6 +540,8 @@ jobs:

- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

- name: Exercise release-only workflow steps
run: node scripts/release-smoke.ts
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ jobs:

- name: Setup dependencies
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}
env:
pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata

Expand Down Expand Up @@ -245,6 +247,8 @@ jobs:

- name: Setup dependencies
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron
Expand Down Expand Up @@ -275,6 +279,8 @@ jobs:

- name: Setup dependencies
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron
Expand Down Expand Up @@ -309,6 +315,8 @@ jobs:

- name: Setup dependencies
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

- name: Resolve Playwright version
id: playwright_version
Expand Down Expand Up @@ -359,6 +367,8 @@ jobs:

- name: Setup dependencies
uses: ./.github/actions/setup-ci-deps
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

# No Electron setup here: `t3` (apps/server) has no Electron dependency
# and none of its tests touch the runtime. Only the non-server `test`
Expand Down
3 changes: 3 additions & 0 deletions .task-cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"endpoint": "https://t3-task-cache.pinglabs.workers.dev/projects/t3code"
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public final class T3NativeControlsModule: Module {
Events("onMetricsChange")
}

View(T3ScrollEdgeBleedView.self) {
ViewName("ScrollEdgeBleed")
}

AsyncFunction("presentVideo") { (url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) in
try self.presentVideo(
url: url,
Expand Down
147 changes: 147 additions & 0 deletions apps/mobile/modules/t3-native-controls/ios/T3ScrollEdgeBleedView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import ExpoModulesCore
import UIKit

/// Wraps a screen's header scroll view. Where a column pads its content away from the iPhone Duo's
/// vertical bar, this widens the scroll view under the bar. UIKit draws
/// the header's scroll-edge effect inside the scroll view, so the blur reaches the bar like in the
/// system apps while the content stays in the safe area. Everything happens in UIKit layout, so
/// folds move the blur and the content together.
final class T3ScrollEdgeBleedView: ExpoView {
// Fabric recycles scroll views, so everything changed here is put back before this view leaves
// the window, which unmounting does before any recycling.
private var scrollView: UIScrollView?
private var container: UIView?
private var bleed: CGFloat = 0
/// Whether the scroll view currently carries this view's changes.
private var applied = false
/// React Native's clipping, saved while bleeding. React Native only rewrites it when the
/// `overflow` style changes, so it stays off while bleeding.
private var containerClipped: Bool?

required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
#if canImport(UIKit, _version: 9127.0.85)
if #available(iOS 27.1, *) {
registerForTraitChanges(UITraitCollection.systemTraitsAffectingVerticalBarEdge) { (view: T3ScrollEdgeBleedView, _: UITraitCollection) in
view.setNeedsLayout()
}
}
#endif
}

deinit {
MainActor.assumeIsolated { detach() }
}

override func didMoveToWindow() {
super.didMoveToWindow()
if window == nil { detach() } else { setNeedsLayout() }
}

override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
setNeedsLayout()
}

override func willRemoveSubview(_ subview: UIView) {
if let scrollView, scrollView.isDescendant(of: subview) { detach() }
super.willRemoveSubview(subview)
}

override func layoutSubviews() {
super.layoutSubviews()
guard window != nil, let found = findScrollView(), let parent = found.superview else { return }
if found !== scrollView {
detach()
scrollView = found
container = parent
}
bleed = verticalBarBleed()
apply()
}

/// How far the vertical bar's safe area reaches past this view's right edge, when this view sits
/// flush against the padding its column added for it. Nothing else is bled into: not a sidebar
/// column overlapping this one, not a notch, and not a bar on the left, where widening would
/// move the content and need a horizontal offset React Native keeps rewriting.
private func verticalBarBleed() -> CGFloat {
guard verticalBarIsOnTheRight() else { return 0 }
var ancestor = superview
while let view = ancestor {
let inset = view.safeAreaInsets.right
if inset > 0 {
let frame = convert(bounds, to: view)
return abs(view.bounds.maxX - inset - frame.maxX) < 0.5 ? inset : 0
}
ancestor = view.superview
}
return 0
}

private func verticalBarIsOnTheRight() -> Bool {
// The vertical-bar API ships in the iOS 27.1 SDK (UIKit 9127.0.85); see T3LayoutMetricsView.
#if canImport(UIKit, _version: 9127.0.85)
if #available(iOS 27.1, *) {
let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft
switch traitCollection.verticalBarEdge {
case .leading: return rtl
case .trailing: return !rtl
default: return false
}
}
#endif
return false
}

/// Widening to the right leaves the content where React Native laid it out, so only the frame,
/// the container's clipping and the scroll indicator change; contentInset and contentOffset are
/// never touched. Nothing at all is written unless this view is bleeding or undoing its bleed.
private func apply() {
guard let scrollView, let container else { return }
guard bleed > 0 || applied else { return }
// React Native sizes the scroll view's component view; the UIScrollView inside follows it by
// autoresizing, so the widened frame survives React Native's own layout updates.
let frame = container.bounds.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -bleed))
if scrollView.frame != frame { scrollView.frame = frame }
if bleed == 0 {
if let clipped = containerClipped {
containerClipped = nil
container.layer.masksToBounds = clipped
}
} else if container.layer.masksToBounds {
// The component view clips to its own bounds, which would cut the widened scroll view back.
// The scroll view itself still clips its content.
containerClipped = containerClipped ?? true
container.layer.masksToBounds = false
}
var indicators = scrollView.verticalScrollIndicatorInsets
if indicators.right != bleed {
indicators.right = bleed
scrollView.verticalScrollIndicatorInsets = indicators
}
applied = bleed > 0
}

private func detach() {
guard scrollView != nil else { return }
bleed = 0
apply()
scrollView = nil
container = nil
}

/// The scroll view sits a fixed few levels down (marker, React Native's scroll view component);
/// never walk into a scroll view's content.
private func findScrollView() -> UIScrollView? {
var level: [UIView] = subviews
for _ in 0..<4 {
var next: [UIView] = []
for view in level {
if let scrollView = view as? UIScrollView { return scrollView }
next.append(contentsOf: view.subviews)
}
level = next
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ final class T3SidebarTabBarView: ExpoView, UITabBarDelegate {
}
configuration.cornerStyle = .capsule
configuration.baseForegroundColor = .label
configuration.image = UIImage(systemName: "square.and.pencil")
configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(pointSize: 24)
configuration.image = tabIcon(UIImage(systemName: "square.and.pencil"))
composeButton.configuration = configuration
}

Expand Down
Loading
Loading