Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8c2181a
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
39facd2
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
b83d721
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
32e066c
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
16883c4
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
fc16b1a
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
820fe9a
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
a5f3b32
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
b3131ef
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
be79c20
fix: adapt upstream #5073 to fork
arhxam Aug 4, 2026
76146b2
fix(web): truncate long project switcher names (#5348)
FllipEis Aug 5, 2026
ff5cb69
fix(mobile): avoid double dividers between thread sections (#5391)
shivamhwp Aug 5, 2026
6fb38a2
fix(web): keep the composer command menu anchored to the composer (#5…
StiensWout Aug 5, 2026
4c95d38
fix(web): restore terminal link hover styles (#5382)
StiensWout Aug 5, 2026
40cd33a
fix(ci): isolate releases from shared API rate limits (#5394)
t3dotgg Aug 5, 2026
a2cf5e6
fix(web): keep model picker shortcuts in sync (#5400)
t3dotgg Aug 5, 2026
c708fc5
fix(web): keep terminal font settings reliable (#5397)
StiensWout Aug 5, 2026
ff42556
Enrich terminal font previews (#5428)
juliusmarminge Aug 5, 2026
416b74b
fix(web): preserve terminal font size when splitting (#5444)
t3-code[bot] Aug 5, 2026
6674792
Prevent terminal loading flash (#5432)
juliusmarminge Aug 5, 2026
2296024
fix: reconnect faster after remote server updates (#5404)
t3dotgg Aug 6, 2026
471b31a
fix: keep RELEASE_APP token for GitHub Release publish
cursoragent Aug 6, 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
9 changes: 7 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,12 @@ jobs:
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
permissions:
contents: write
steps:
# Fork: keep publishing GitHub Releases with the Release App token rather
# than github.token from upstream #5394. This fork's RELEASE_APP_* secrets
# are the established publish path for assets and release notes.
- id: app_token
name: Mint release app token
uses: actions/create-github-app-token@v2
Expand Down Expand Up @@ -883,7 +888,7 @@ jobs:

- name: Publish release
if: needs.preflight.outputs.previous_tag != ''
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ needs.preflight.outputs.ref }}
Expand All @@ -905,7 +910,7 @@ jobs:

- name: Publish first release
if: needs.preflight.outputs.previous_tag == ''
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ needs.preflight.outputs.ref }}
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,11 @@ export function HomeScreen(props: HomeScreenProps) {
);

const renderV2Item = useCallback(
({ item }: { readonly item: ThreadListV2ListItem }) => {
({ item, index }: { readonly item: ThreadListV2ListItem; readonly index: number }) => {
const nextItem = threadListV2Items[index + 1];
const showTrailingDivider =
nextItem?.type === "v2-thread" ||
(nextItem?.type === "v2-pending" && !nextItem.showPendingDivider);
if (item.type === "v2-pending") {
const pendingScopeKey = scopedProjectKey(
item.pendingTask.message.environmentId,
Expand All @@ -716,6 +720,7 @@ export function HomeScreen(props: HomeScreenProps) {
: null
}
showPendingDivider={item.showPendingDivider}
showTrailingDivider={showTrailingDivider}
onSelectPendingTask={props.onSelectPendingTask}
onDeletePendingTask={props.onDeletePendingTask}
/>
Expand Down Expand Up @@ -748,6 +753,7 @@ export function HomeScreen(props: HomeScreenProps) {
pinned={item.item.pinned}
snoozePresetMinute={nowMinute}
snoozeWakeLabelText={item.snoozeWakeLabelText}
showTrailingDivider={showTrailingDivider}
project={
projectByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
}
Expand Down Expand Up @@ -818,6 +824,7 @@ export function HomeScreen(props: HomeScreenProps) {
serverConfigs,
settlementEnvironmentIds,
snoozeEnvironmentIds,
threadListV2Items,
threadSearchMatchByKey,
toggleSettledShelf,
toggleSnoozedShelf,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Platform, ScrollView, View, useColorScheme } from "react-native";
import {
Platform,
ScrollView,
type StyleProp,
type TextStyle,
View,
useColorScheme,
} from "react-native";

import { AppText as Text } from "../../../../components/AppText";
import {
Expand Down Expand Up @@ -54,14 +61,48 @@ export function TerminalAppearancePreview(props: { readonly fontSize: number })
fontSize: props.fontSize,
lineHeight,
} as const;
// AppText stamps the sans font on every node, so nested spans must
// re-apply the terminal font instead of relying on inheritance, exactly
// like the code preview's tokens below.
const span = (color: string, extra?: TextStyle): StyleProp<TextStyle> => [
lineStyle,
{ color, ...extra },
];

return (
<View className="p-4">
<Text style={[lineStyle, { color: theme.foreground }]}>$ npm run dev</Text>
<Text style={[lineStyle, { color: theme.palette[2] }]}>✓ Ready in 430ms</Text>
<Text style={[lineStyle, { color: theme.foreground }]}>
Local: http://localhost:3000{" "}
<Text style={[lineStyle, { color: theme.cursorForeground }]}>▏</Text>
<Text style={span(theme.foreground)}>
<Text style={span(theme.palette[2])}>→ </Text>
<Text style={span(theme.palette[6])}>t3code </Text>
<Text style={span(theme.palette[4])}>git:(</Text>
<Text style={span(theme.palette[1])}>main</Text>
<Text style={span(theme.palette[4])}>)</Text>
<Text style={span(theme.palette[3])}> ✗</Text>
<Text style={span(theme.foreground)}> vpr dev</Text>
</Text>
<Text style={span(theme.foreground)}>
<Text style={span(theme.palette[2])}>VITE v7.1.1</Text>
<Text style={span(theme.mutedForeground)}> ready in</Text>
<Text style={span(theme.foreground)}> 1.24s</Text>
</Text>
<Text style={span(theme.foreground)}>
<Text style={span(theme.palette[2])}>→ </Text>
<Text style={span(theme.mutedForeground)}>Local: </Text>
<Text style={span(theme.palette[6], { textDecorationLine: "underline" })}>
http://127.0.0.1:5173/
</Text>
</Text>
<Text style={span(theme.foreground)}>
<Text style={span(theme.palette[2])}>✓ 85 passed</Text>
<Text style={span(theme.palette[3])}> △ 2 warnings</Text>
<Text style={span(theme.palette[1])}> ✗ 0 failed</Text>
</Text>
<Text style={span(theme.foreground)}>
<Text style={span(theme.background, { backgroundColor: theme.palette[2] })}>
{" READY "}
</Text>
<Text style={span(theme.mutedForeground)}> watching for changes</Text>{" "}
<Text style={span(theme.cursorForeground)}>▏</Text>
</Text>
</View>
);
Expand Down
12 changes: 10 additions & 2 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
readonly pane?: "screen" | "sidebar";
/** Draws the "Pending" divider above the first queued row. */
readonly showPendingDivider: boolean;
/** Keeps row hairlines inside a section; section headers draw their own rule. */
readonly showTrailingDivider?: boolean;
readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void;
readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
}) {
Expand Down Expand Up @@ -291,7 +293,9 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
) : (
<View className="bg-screen">
<View className="px-5 py-2.5">{rowContent}</View>
<View className="ml-5 h-px bg-border-subtle" />
{props.showTrailingDivider !== false ? (
<View className="ml-5 h-px bg-border-subtle" />
) : null}
</View>
)}
</Pressable>
Expand Down Expand Up @@ -327,6 +331,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
into the drawer surface, selection filled with the accent color —
matching the v1 sidebar rows. */
readonly pane?: "screen" | "sidebar";
/** Keeps row hairlines inside a section; section headers draw their own rule. */
readonly showTrailingDivider?: boolean;
/** Highlights the thread open in the detail pane (iPad split view). The
compact Home list never sets it — phones navigate away on select. */
readonly selected?: boolean;
Expand Down Expand Up @@ -728,7 +734,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
actions reveal behind the row. */
<View className="bg-screen">
<View className="px-5 py-2.5">{cardContent}</View>
<View className="ml-5 h-px bg-border-subtle" />
{props.showTrailingDivider !== false ? (
<View className="ml-5 h-px bg-border-subtle" />
) : null}
</View>
)}
</Pressable>
Expand Down
9 changes: 7 additions & 2 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,13 @@ export const makeServerLayer = Layer.unwrap(
const server = yield* HttpServer.HttpServer;
const address = server.address;
if (typeof address === "string" || !("port" in address)) return;
yield* Effect.sleep("250 millis").pipe(
Effect.andThen(reconcileDesiredCloudLink(`http://127.0.0.1:${address.port}`)),
// No settling delay before the first attempt: routes are already
// serving by the time activation opens this gate (the startup
// sequence awaits routesReady), and the retry schedule below
// covers anything this sleep used to hedge against. Every
// millisecond here is dead time on the path to remote
// reachability after a restart.
yield* reconcileDesiredCloudLink(`http://127.0.0.1:${address.port}`).pipe(
Effect.retry({
while: (error) =>
error._tag !== "EnvironmentHttpBadRequestError" &&
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/appearanceFonts.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vite-plus/test";

import {
areFontAdvancesMonospace,
clampCodeFontSize,
clampInterfaceFontSize,
clampPromptFontSize,
Expand All @@ -9,8 +10,22 @@ import {
appearanceFontStack,
cssFontFamilies,
resolveDefaultFamilyLabel,
resolveTerminalFontPreference,
} from "./appearanceFonts";

describe("areFontAdvancesMonospace", () => {
it("accepts a fixed advance and rejects any proportional glyph", () => {
expect(areFontAdvancesMonospace([10, 10, 10, 10])).toBe(true);
expect(areFontAdvancesMonospace([10, 10, 7, 10])).toBe(false);
expect(areFontAdvancesMonospace([10, 10.02])).toBe(false);
});

it("fails open when canvas metrics are unavailable", () => {
expect(areFontAdvancesMonospace([])).toBe(true);
expect(areFontAdvancesMonospace([Number.NaN, Number.NaN])).toBe(true);
});
});

describe("cssFontFamilies", () => {
it("returns null for effectively empty input", () => {
expect(cssFontFamilies("")).toBeNull();
Expand Down Expand Up @@ -54,6 +69,34 @@ describe("appearanceFontStack", () => {
});
});

describe("resolveTerminalFontPreference", () => {
it("inherits the code font in simple mode", () => {
expect(
resolveTerminalFontPreference({ advanced: false, code: "Fira Code", terminal: "" }),
).toBe("Fira Code");
expect(
resolveTerminalFontPreference({
advanced: false,
code: "Fira Code",
terminal: "Berkeley Mono",
}),
).toBe("Fira Code");
});

it("keeps code and terminal fonts independent in advanced mode", () => {
expect(resolveTerminalFontPreference({ advanced: true, code: "Fira Code", terminal: "" })).toBe(
"",
);
expect(
resolveTerminalFontPreference({
advanced: true,
code: "Fira Code",
terminal: "Berkeley Mono",
}),
).toBe("Berkeley Mono");
});
});

describe("font size clamping", () => {
it("keeps sizes inside the ranges the UI can absorb", () => {
expect(clampInterfaceFontSize(16)).toBe(16);
Expand Down
44 changes: 39 additions & 5 deletions apps/web/src/appearanceFonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ export const DEFAULT_SANS_FONT_STACK =
export const DEFAULT_CODE_FONT_STACK =
'"SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace';

export const TYPOGRAPHY_ADVANCED_STORAGE_KEY = "t3code:typography-advanced";

/**
* Simple typography treats the terminal as another monospace surface. In
* Advanced mode an empty terminal preference means the terminal default,
* keeping later code-font changes isolated to code surfaces.
*/
export function resolveTerminalFontPreference(input: {
readonly advanced: boolean;
readonly code: string;
readonly terminal: string;
}): string {
if (input.advanced) return input.terminal;
return input.code;
}

function quoteFontFamilyName(name: string): string {
const bare = name.trim();
if (bare.length === 0) return "";
Expand Down Expand Up @@ -165,6 +181,22 @@ export function isFontFamilyAvailable(family: string): boolean {
}
}

const MONOSPACE_PROBE_VARIANTS = ["normal 400", "normal 700", "italic 400", "italic 700"] as const;
const MONOSPACE_PROBE_GLYPHS = ["i", "M", "W", "0", "@", "#", ".", " "] as const;
const MONOSPACE_ADVANCE_TOLERANCE = 0.01;

export function areFontAdvancesMonospace(advances: readonly number[]): boolean {
const reference = advances[0];
if (
reference === undefined ||
reference <= 0 ||
advances.some((advance) => !Number.isFinite(advance) || advance <= 0)
) {
return true;
}
return advances.every((advance) => Math.abs(advance - reference) < MONOSPACE_ADVANCE_TOLERANCE);
}

/**
* Whether a family renders every character on the same advance. Cell-grid
* surfaces (the terminal) require this: a proportional face draws its text
Expand All @@ -182,13 +214,15 @@ export function isMonospaceFamily(family: string): boolean {
fontProbeContext = document.createElement("canvas").getContext("2d");
}
if (fontProbeContext === null) return true;
const context = fontProbeContext;
// Fall back to a generic mono so an absent face measures as monospace and
// is left for the normal fallback chain to resolve.
fontProbeContext.font = `32px ${families}, monospace`;
const narrow = fontProbeContext.measureText("i").width;
const wide = fontProbeContext.measureText("M").width;
if (!Number.isFinite(narrow) || !Number.isFinite(wide) || wide === 0) return true;
return Math.abs(wide - narrow) < 0.5;
for (const variant of MONOSPACE_PROBE_VARIANTS) {
context.font = `${variant} 32px ${families}, monospace`;
const advances = MONOSPACE_PROBE_GLYPHS.map((glyph) => context.measureText(glyph).width);
if (!areFontAdvancesMonospace(advances)) return false;
}
return true;
} catch {
return true;
}
Expand Down
Loading
Loading