Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4c9c7d0
chore(mobile): bump app version to 2.2.0
t3-code-bot[bot] Oct 28, 2026
c677d5f
fix(web): scroll-to-end button no longer jumps up and down (#26375)
t3dotgg Oct 28, 2026
07f7f97
ci(release): releases stop depending on npm and nodejs.org when nothi…
t3dotgg Oct 28, 2026
001d16e
feat(web): boring sidebar hides projects with no active threads (#26387)
t3dotgg Oct 28, 2026
9f33a1c
feat(web): show T3 Connect tunnel usage and push full accounts to T3 …
t3dotgg Oct 28, 2026
40d335a
fix(web): settling a thread no longer scrolls the sidebar to the bott…
t3dotgg Oct 28, 2026
80fcb7f
fix(web): boring sidebar folders put your latest message on top (#26391)
t3dotgg Oct 28, 2026
2a73827
fix(server): threads settle right away when a watched PR merges (#26395)
t3dotgg Oct 28, 2026
d2429f7
fix(server): load long thread history in bounded pages (#26240)
Yash-Singh1 Oct 28, 2026
c3858bf
fix(web): snooze options sort soonest first, fit 2 digits, and link f…
t3dotgg Oct 28, 2026
48a2923
fix(web): file preview wrap buttons no longer turn off word wrap ever…
t3dotgg Oct 28, 2026
bd84b17
feat(web): simpler composer with grouped controls, effort words and n…
t3dotgg Oct 28, 2026
96a330c
fix(web): remote delegates show their harness icon, a parent row, and…
t3dotgg Oct 28, 2026
1abf368
fix(build): builds no longer fail when GitHub's CDN times out on lice…
t3dotgg Oct 28, 2026
0160c44
feat(server): agents list snoozed threads across all projects in one …
t3dotgg Oct 28, 2026
c280e64
fix(server): searching delegate models by provider name no longer sho…
t3dotgg Oct 28, 2026
eb751fa
fix(web): chat tables open at the right size instead of collapsed and…
t3dotgg Oct 28, 2026
6bc87b9
ci(release): desktop builds stop hitting the npm registry for staged …
t3dotgg Oct 28, 2026
5211acf
feat(web): boring sidebar can show folder threads on two lines (#26463)
t3dotgg Oct 28, 2026
dc8d3e0
Merge commit '5211acfe95a0c6d82a60e2524b5e46b47fe67a3e' into fix/bots…
fireboltdude1357 Oct 28, 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
20 changes: 20 additions & 0 deletions .github/actions/setup-ci-deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ runs:
INSTALL_ARGS: ${{ inputs.install-args }}
run: |
echo "path=$(vp pm cache dir)" >> "$GITHUB_OUTPUT"
metadata_dir="${pnpm_config_cache_dir:-$RUNNER_TEMP/pnpm-metadata}"
echo "pnpm_config_cache_dir=$metadata_dir" >> "$GITHUB_ENV"
echo "verified=$metadata_dir/lockfile-verified.jsonl" >> "$GITHUB_OUTPUT"
if [[ -z "$INSTALL_ARGS" ]]; then
echo "namespace=full" >> "$GITHUB_OUTPUT"
else
Expand All @@ -94,6 +97,16 @@ runs:
pnpm-${{ steps.pnpm_store.outputs.namespace }}-v1-${{ runner.os }}-${{ runner.arch }}-
pnpm-full-v1-${{ runner.os }}-${{ runner.arch }}-

# pnpm accepts an earlier pass for the same lockfile and policy at any age, so
# a cached record skips fetching registry metadata for every package.
- name: Restore lockfile verification
id: pnpm_verified
if: inputs.install == 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ steps.pnpm_store.outputs.verified }}
key: pnpm-verified-v1-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'package.json') }}

- name: Install dependencies
if: inputs.install == 'true'
shell: bash
Expand All @@ -102,6 +115,13 @@ runs:
# Unquoted so each filter is its own argument.
run: vp install $INSTALL_ARGS

- name: Save lockfile verification
if: inputs.install == 'true' && steps.pnpm_verified.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ steps.pnpm_store.outputs.verified }}
key: ${{ steps.pnpm_verified.outputs.cache-primary-key }}

# A filtered install that started from the current full store adds nothing
# worth a second copy of it.
- name: Save pnpm store
Expand Down
101 changes: 93 additions & 8 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,29 @@ jobs:
env:
pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata

# The CLI archive and the staged app install without a lockfile, so they
# ask the registry about every package and fetch platform packages the
# workspace store lacks. They share this registry metadata and store. A
# new week starts cold, which bounds how stale cached ranges can get.
- name: Resolve staged install cache
id: stage_pnpm
shell: bash
run: |
echo "dir=$RUNNER_TEMP/desktop-stage-pnpm" >> "$GITHUB_OUTPUT"
echo "prefix=desktop-stage-pnpm-v2-${{ inputs.platform }}-${{ inputs.arch }}-$(date -u +%G-%V)-" >> "$GITHUB_OUTPUT"

- name: Restore staged install cache
id: stage_pnpm_cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ steps.stage_pnpm.outputs.dir }}
key: ${{ steps.stage_pnpm.outputs.prefix }}${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: ${{ steps.stage_pnpm.outputs.prefix }}

- name: Mark staged install cache
shell: bash
run: mkdir -p "${{ steps.stage_pnpm.outputs.dir }}" && touch "$RUNNER_TEMP/desktop-stage-pnpm.marker"

- name: Cache resource monitor
id: resource_monitor_cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
Expand Down Expand Up @@ -254,21 +277,60 @@ jobs:
if [[ "${{ inputs.version }}" =~ -nightly\.[0-9]{8}\.[0-9]+$ ]]; then brand=nightly; fi
node scripts/apply-web-brand-assets.ts "$brand" apps/server/dist/client

- name: Resolve target Node.js for the CLI executable
id: target_node
if: inputs.cli_archive && inputs.platform == 'win'
shell: bash
run: echo "version=$(node -p 'require("./package.json").engines.node')" >> "$GITHUB_OUTPUT"

# The download only changes with engines.node, so a cached copy keeps
# releases off nodejs.org, which stalls on the runners some days.
- name: Restore target Node.js for the CLI executable
id: target_node_cache
if: inputs.cli_archive && inputs.platform == 'win'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ runner.temp }}/node-download
key: node-download-v${{ steps.target_node.outputs.version }}-win-${{ inputs.arch }}

# tsdown downloads the target Node with one fetch and no retry, and
# nodejs.org drops that download often enough to fail releases. Seed
# tsdown's cache with a retried, checksum-verified copy instead.
# tsdown's cache with a retried, checksum-verified copy instead. A transfer
# under 100 KB/s for 30s counts as failed, so a stall retries.
- name: Download target Node.js for the CLI executable
if: inputs.cli_archive && inputs.platform == 'win' && steps.target_node_cache.outputs.cache-hit != 'true'
shell: bash
env:
VERSION: ${{ steps.target_node.outputs.version }}
run: |
set -euo pipefail
name="node-v$VERSION-win-${{ inputs.arch }}"
dir="$RUNNER_TEMP/node-download"
mkdir -p "$dir"
fetch=(curl -fsSL --retry 5 --retry-all-errors --connect-timeout 10 --speed-limit 102400 --speed-time 30)
"${fetch[@]}" -o "$dir/$name.zip" "https://nodejs.org/dist/v$VERSION/$name.zip"
"${fetch[@]}" -o "$dir/SHASUMS256.txt" "https://nodejs.org/dist/v$VERSION/SHASUMS256.txt"
(cd "$dir" && grep " $name.zip\$" SHASUMS256.txt | sha256sum -c -)

# Saved right away so a later failure in this job still keeps the download.
- name: Save target Node.js for the CLI executable
if: inputs.cli_archive && inputs.platform == 'win' && steps.target_node_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ runner.temp }}/node-download
key: ${{ steps.target_node_cache.outputs.cache-primary-key }}

- name: Install target Node.js for the CLI executable
if: inputs.cli_archive && inputs.platform == 'win'
shell: bash
env:
VERSION: ${{ steps.target_node.outputs.version }}
run: |
set -euo pipefail
version="$(node -p 'require("./package.json").engines.node')"
name="node-v$version-win-${{ inputs.arch }}"
dest="${XDG_CACHE_HOME:-$HOME/.cache}/tsdown/node/v$version/win-${{ inputs.arch }}"
name="node-v$VERSION-win-${{ inputs.arch }}"
dir="$RUNNER_TEMP/node-download"
mkdir -p "$dir" "$dest"
curl -fsSL --retry 5 --retry-all-errors -o "$dir/$name.zip" "https://nodejs.org/dist/v$version/$name.zip"
curl -fsSL --retry 5 --retry-all-errors -o "$dir/SHASUMS256.txt" "https://nodejs.org/dist/v$version/SHASUMS256.txt"
dest="${XDG_CACHE_HOME:-$HOME/.cache}/tsdown/node/v$VERSION/win-${{ inputs.arch }}"
mkdir -p "$dest"
(cd "$dir" && grep " $name.zip\$" SHASUMS256.txt | sha256sum -c -)
unzip -p "$dir/$name.zip" "$name/node.exe" > "$dest/node.exe"

Expand Down Expand Up @@ -332,6 +394,9 @@ jobs:
if: inputs.cli_archive
shell: bash
env:
pnpm_config_cache_dir: ${{ steps.stage_pnpm.outputs.dir }}/cache
pnpm_config_store_dir: ${{ steps.stage_pnpm.outputs.dir }}/store
pnpm_config_prefer_offline: "true"
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
Expand Down Expand Up @@ -451,7 +516,10 @@ jobs:
- name: Build desktop artifact
shell: bash
env:
pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
# pnpm only goes to the registry for packages or versions the cache lacks.
pnpm_config_cache_dir: ${{ steps.stage_pnpm.outputs.dir }}/cache
pnpm_config_store_dir: ${{ steps.stage_pnpm.outputs.dir }}/store
pnpm_config_prefer_offline: "true"
T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: true
T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }}
CSC_LINK: ${{ secrets.CSC_LINK }}
Expand Down Expand Up @@ -536,6 +604,23 @@ jobs:

vp run dist:desktop:artifact "${args[@]}"

# Warm installs only touch the store index, so new content means a fetch.
- name: Check staged install cache for new content
id: stage_pnpm_changed
shell: bash
run: |
dir="${{ steps.stage_pnpm.outputs.dir }}"
new=$(find "$dir/cache/v12" "$dir/store/v11/files" -type f -newer "$RUNNER_TEMP/desktop-stage-pnpm.marker" 2>/dev/null | wc -l | tr -d ' ')
echo "Staged installs added $new cached files."
echo "changed=$([[ "$new" -gt 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"

- name: Save staged install cache
if: steps.stage_pnpm_changed.outputs.changed == 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ steps.stage_pnpm.outputs.dir }}
key: ${{ steps.stage_pnpm_cache.outputs.cache-primary-key }}

# A build can package fine and still not open (a missing module in the
# backend or the bundle). This launches the app from the shipped
# AppImage or DMG.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const clientSettings: ClientSettings = {
confirmThreadDelete: false,
confirmThreadUnpin: false,
contextWindowMeterEnabled: false,
composerCollapseOnScroll: true,
diffFilesCollapsed: true,
diffIgnoreWhitespace: true,
diffLayout: "stacked",
Expand Down Expand Up @@ -64,6 +63,7 @@ const clientSettings: ClientSettings = {
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
boringSidebarEnabled: false,
boringSidebarMultilineRows: false,
legacySidebarEnabled: false,
sidebarWorkingShelfEnabled: false,
loadBalancingEnabled: false,
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 @@ -226,7 +226,7 @@ const config: ExpoConfig = {
slug: "t3-code",
platforms: ["ios", "android"],
scheme: variant.scheme,
version: repoEnv.T3CODE_MOBILE_VERSION ?? "2.1.0",
version: repoEnv.T3CODE_MOBILE_VERSION ?? "2.2.0",
runtimeVersion: {
// Skip costly fingerprinting on dev launches; preview and production OTAs require matching native projects.
policy: runtimeVersionPolicy,
Expand Down
4 changes: 0 additions & 4 deletions apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ async function generateMobileThirdPartyLicenses() {
const manifest = await generateThirdPartyLicenseManifest({
configFile: path.join(workspaceRoot, "third-party-licenses.config.json"),
packageManifests: [{ bundle: "mobile", path: path.join(__dirname, "package.json") }],
allowMissingGeneratedNotices:
process.env.NODE_ENV !== "production" &&
process.env.EAS_BUILD !== "true" &&
process.env.T3CODE_LICENSES_STRICT !== "1",
});

await Promise.all([
Expand Down
9 changes: 4 additions & 5 deletions apps/mobile/src/features/threads/LinkRequestCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SymbolView, type AppSymbolName } from "../../components/ui/AppSymbol";
import { AppText as Text, AppTextInput as TextInput } from "../../components/ui/AppText";
import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
import { useLinkableMachines } from "../../state/peerLinks";
import { peerLinkEnvironment, useLinkableMachines } from "../../state/peerLinks";
import { mintPairingCodeOn } from "../../state/peerPairingCode";
import { serverEnvironment } from "../../state/server";
import { useEnvironmentScope, usePreparedConnection } from "../../state/session";
import { useAtomCommand } from "../../state/use-atom-command";
import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow";
Expand Down Expand Up @@ -107,13 +106,13 @@ function PendingLinkRequest(props: {
const [open, setOpen] = useState(false);
const [declining, setDeclining] = useState(false);
const [error, setError] = useState<string | null>(null);
const answer = useAtomCommand(serverEnvironment.answerLinkRequest, {
const answer = useAtomCommand(peerLinkEnvironment.answerRequest, {
label: "decline link request",
reportFailure: false,
reportDefect: false,
});
const canAnswer = useAtomValue(
serverEnvironment.answerLinkRequest.permissionAtom(props.environmentId),
peerLinkEnvironment.answerRequest.permissionAtom(props.environmentId),
);
const target = linkRequestTargetName(item);

Expand Down Expand Up @@ -244,7 +243,7 @@ function LinkRequestSheet(props: {
const [mintFailed, setMintFailed] = useState<ReadonlySet<EnvironmentId>>(new Set());
// Submit then a tap can both run before a re-render; this guard is synchronous.
const inFlight = useRef(false);
const answer = useAtomCommand(serverEnvironment.answerLinkRequest, {
const answer = useAtomCommand(peerLinkEnvironment.answerRequest, {
label: "answer link request",
// The failure cause holds the request, and with it the code; keep it out of the console.
reportFailure: false,
Expand Down
119 changes: 95 additions & 24 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,92 @@ function compactionLabel(label: string, title: string | undefined) {
: label;
}

function RunFoldButton({
entry,
environmentId,
threadId,
onToggle,
rowSizing,
iconColor,
}: {
entry: Extract<ThreadFeedEntry, { type: "run-fold" }>;
environmentId: EnvironmentId;
threadId: ThreadId;
onToggle: (runId: RunId) => void;
rowSizing: ReturnType<typeof deriveThreadWorkLogSizing>;
iconColor: ColorValue;
}) {
const loadWork = useAtomCommand(threadEnvironment.loadEarlierHistory, { reportFailure: false });
const anchorsKey = entry.deferredWorkItemIds?.join("\n") ?? "";
const [failed, setFailed] = useState(false);
const load = () => {
if (!anchorsKey) return;
void Promise.all(
anchorsKey
.split("\n")
.map((workAnchorId) => loadWork({ environmentId, input: { threadId, workAnchorId } })),
).then((results) =>
setFailed(
results.some((result) => result._tag === "Failure" || result.value._tag === "error"),
),
);
};
const loadOnExpand = useEffectEvent(load);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Restored expanded folds load their deferred server history on mount.
useEffect(() => {
if (entry.expanded && anchorsKey && environmentId && threadId) loadOnExpand();
}, [entry.expanded, anchorsKey, environmentId, threadId]);
const error = anchorsKey && failed ? "Couldn't load work. Tap to retry." : null;
const loading = entry.expanded && anchorsKey !== "" && !failed;
const toggle = () => {
setFailed(false);
if (error && entry.expanded) load();
else onToggle(entry.runId);
};

return (
<View className="min-w-0 flex-1">
<Pressable
accessibilityRole="button"
accessibilityState={{ expanded: entry.expanded, busy: loading }}
onPress={toggle}
hitSlop={4}
className="min-h-11 min-w-0 flex-1 flex-row items-center gap-2"
style={{ minHeight: Math.max(TURN_FOLD_HEIGHT - 3.5, rowSizing.estimatedRowHeight) }}
>
<Text
key={rowSizing.textSizeKey}
numberOfLines={1}
className="shrink font-t3-medium text-sm tabular-nums text-foreground-muted"
>
{entry.label}
{error ? ` · ${error}` : ""}
</Text>
<ThreadDisclosureChevron
expanded={entry.expanded}
collapsedDirection="right"
size={15}
tintColor={iconColor}
/>
</Pressable>
{loading ? (
<View
accessible={false}
importantForAccessibility="no-hide-descendants"
className="gap-2 px-1 pb-2"
>
{(["40%", "60%", "33%"] as const).map((width) => (
<View key={width} className="flex-row items-center gap-2">
<View className="size-3 rounded bg-subtle-strong" />
<View className="h-2.5 rounded-full bg-subtle-strong" style={{ width }} />
</View>
))}
</View>
) : null}
</View>
);
}

function renderFeedEntry(
info: { item: ThreadFeedRow; index: number },
props: Pick<
Expand Down Expand Up @@ -1279,30 +1365,15 @@ function renderFeedEntry(
if (entry.type === "run-fold") {
return (
<View className="mb-1 flex-row items-center border-b border-border-subtle px-2">
<Pressable
accessibilityRole="button"
accessibilityState={{ expanded: entry.expanded }}
onPress={() => props.onToggleTurnFold(entry.runId)}
hitSlop={4}
className="min-h-11 min-w-0 flex-1 flex-row items-center gap-2"
style={{
minHeight: Math.max(TURN_FOLD_HEIGHT - 3.5, props.workRowSizing.estimatedRowHeight),
}}
>
<Text
key={props.workRowSizing.textSizeKey}
numberOfLines={1}
className="shrink font-t3-medium text-sm tabular-nums text-foreground-muted"
>
{entry.label}
</Text>
<ThreadDisclosureChevron
expanded={entry.expanded}
collapsedDirection="right"
size={15}
tintColor={iconSubtleColor}
/>
</Pressable>
<RunFoldButton
key={entry.id}
entry={entry}
environmentId={props.environmentId}
threadId={props.threadId}
onToggle={props.onToggleTurnFold}
rowSizing={props.workRowSizing}
iconColor={iconSubtleColor}
/>
{entry.checkpointSummary ? (
<TurnDiffStat
checkpoint={entry.checkpointSummary}
Expand Down
Loading
Loading