Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1dc8cbe
feat(observability): write a server heap snapshot on SIGUSR2 (#13694)
t3dotgg Sep 26, 2026
574b180
perf(server): shutdown no longer rewrites every stopped session row (…
t3dotgg Sep 26, 2026
b6eefc9
perf(server): build the thread list snapshot without decoding it twic…
t3dotgg Sep 26, 2026
b2577d6
fix(client): slow servers finish loading the thread list instead of l…
t3dotgg Sep 26, 2026
6989856
perf(web): hidden terminal drawers no longer keep full thread history…
t3dotgg Sep 26, 2026
3b0a495
perf(server): per-thread settlement and PR checks no longer rebuild t…
t3dotgg Sep 26, 2026
ecd3237
fix(mobile): running threads open at the latest message (#13530)
AKolenda Sep 26, 2026
ee18e56
feat(observability): record event loop stalls in the server trace (#1…
t3dotgg Sep 26, 2026
999161e
perf(server): stop re-running git for every project each minute (#13689)
t3dotgg Sep 26, 2026
94c4216
fix(usage): hide the Cursor keychain prompt when Cursor isn't set up …
Gigioxx Sep 26, 2026
04c15f3
feat(web): add chat width setting for wide screens (#11594)
otavio Sep 26, 2026
4408bf5
fix(opencode): accept v2 serve ready line when spawning server (#13651)
shirishpothi Sep 26, 2026
1e844a3
fix(editors): stop treating the agy CLI as the Antigravity IDE (#7079)
ishaanko Sep 26, 2026
eeea71a
fix(web): make the empty workspace draggable on desktop (#13713)
otavio Sep 26, 2026
dd8332d
fix(server): installed editors no longer vanish when discovery is slo…
bfowler Sep 26, 2026
29a3266
fix(git): exclude SSH ports from provider URLs (#12537)
GaMeRaM Sep 26, 2026
5d707bb
fix(web): Mod+B bolds on non-Latin layouts (#13409)
ValeraZSD Sep 26, 2026
a21b42c
fix(server): prune expired replay-protection files from the secrets d…
t3dotgg Sep 26, 2026
8bc9b78
fix(web): terminal links drop a trailing colon (#13408)
ValeraZSD Sep 26, 2026
4923ff4
fix(server): bump node-pty to 1.2.0-beta.15 for linux-arm64 prebuild …
Ephraim-9 Sep 26, 2026
92f0af2
Show a focus ring on sidebar thread and draft rows (#13344)
ryanilano Sep 26, 2026
dab9561
fix(mobile): keep composer within folded screen after resume (#13310)
PixPMusic Sep 26, 2026
2679d27
fix(server): let OpenCode generate session titles (#13368)
macodev00 Sep 26, 2026
def34c2
fix(server): let Antigravity inspect unsupported files by path (#13339)
Bil0000 Sep 26, 2026
fe6388f
fix(mobile): link URLs with ports and single-label hosts (#13795)
Yash-Singh1 Sep 26, 2026
daafcc4
feat(web): add keyboard navigation for usage (#10158)
tris203 Sep 26, 2026
99efaea
perf(observability): stop writing empty spans on spawns, projected ev…
t3dotgg Sep 26, 2026
8b873ea
perf(server): opening Diagnostics no longer loads the whole trace rin…
t3dotgg Sep 26, 2026
6f97b0f
perf(clients): sort projects and settled threads without re-parsing d…
t3dotgg Sep 26, 2026
294dd12
fix(observability): the renderer trace proxy stops tracing itself (#1…
t3dotgg Sep 26, 2026
1d6f23b
perf(server): background sweeps only read threads that can still sett…
t3dotgg Sep 26, 2026
9151ea4
perf(clients): saving the thread list cache no longer freezes the UI …
t3dotgg Sep 26, 2026
8872666
perf(server): cut idle wakeups from the Connect relay and session rea…
t3dotgg Sep 26, 2026
81e0491
fix(mobile): keep trailing underscores and tildes in autolinked URLs …
Yash-Singh1 Sep 26, 2026
295d7cb
fix(web): queued messages send while their thread is not open (#13764)
t3dotgg Sep 26, 2026
95030dc
fix(server): background git status fetches no longer fill the disk wi…
t3dotgg Sep 26, 2026
75d63d6
fix(mobile): thread list shows the pull request icon instead of # (#1…
flamboh Sep 26, 2026
393d595
fix(accessibility): correct control announcements and sidebar travers…
blinding-pixels Sep 26, 2026
10bb59b
fix(usage): tolerate newer provider variants (#10076)
tris203 Sep 26, 2026
d6802b4
fix(usage): omit Cursor warning when no login is saved (#13820)
tris203 Sep 26, 2026
3dae78f
fix(usage): identify client version mismatches (#8208)
tris203 Sep 26, 2026
dd582de
fix(web): stop mistaking offline servers for updates (#13083)
tris203 Sep 26, 2026
d110f98
test(usage): assert contract mismatch details (#13861)
Yash-Singh1 Sep 26, 2026
ea7d46a
fix(build): validate Linux node-pty prebuilds in Windows artifacts (#…
Yash-Singh1 Sep 26, 2026
b32b079
Merge tag 'v0.0.43-nightly.20260926.2318' into custom-nightly
chamaquita-d3[bot] Sep 26, 2026
f2a1d6e
🧪 test(client-runtime): give the generated snapshot test room on slow…
chamaquita-d3[bot] Sep 26, 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
19 changes: 13 additions & 6 deletions apps/mobile/src/connection/environment-cache-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ConnectionPersistenceError,
EnvironmentCacheStore,
encodeShellSnapshotForCache,
} from "@t3tools/client-runtime/platform";
import {
type EnvironmentId,
Expand Down Expand Up @@ -56,7 +57,6 @@ const StoredVcsRefs = Schema.Struct({
const decodeStoredShellSnapshot = Schema.decodeUnknownEffect(
Schema.fromJsonString(StoredShellSnapshot),
);
const encodeStoredShellSnapshot = Schema.encodeEffect(Schema.fromJsonString(StoredShellSnapshot));
const decodeStoredThreadSnapshot = Schema.decodeUnknownEffect(
Schema.fromJsonString(StoredThreadSnapshot),
);
Expand Down Expand Up @@ -136,11 +136,18 @@ export const make = Effect.fn("MobileEnvironmentCacheStore.make")(function* () {
}).pipe(Effect.tap(() => Effect.promise(() => projectFaviconDatabaseCache.hydrate()))),
),
saveShell: Effect.fn("MobileEnvironmentCache.saveShell")(function* (environmentId, snapshot) {
const payload = yield* encodeStoredShellSnapshot({
schemaVersion: SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION,
environmentId,
snapshot,
}).pipe(Effect.mapError((cause) => persistenceError("save-shell", cause)));
const encodedSnapshot = yield* encodeShellSnapshotForCache(snapshot).pipe(
Effect.mapError((cause) => persistenceError("save-shell", cause)),
);
const payload = yield* Effect.try({
try: () =>
JSON.stringify({
schemaVersion: SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION,
environmentId,
snapshot: encodedSnapshot,
} satisfies typeof StoredShellSnapshot.Encoded),
catch: (cause) => persistenceError("save-shell", cause),
});
yield* database
.saveCache(environmentId, "shell", "snapshot", SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION, payload)
.pipe(Effect.mapError(mapDatabaseError("save-shell")));
Expand Down
42 changes: 42 additions & 0 deletions apps/mobile/src/features/connection/ConnectionFormField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vite-plus/test";

vi.mock("react-native", () => ({
View: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
vi.mock("../../components/AppText", () => ({
AppText: ({
accessibilityElementsHidden,
importantForAccessibility,
children,
}: {
accessibilityElementsHidden?: boolean;
importantForAccessibility?: string;
children: ReactNode;
}) => (
<span
aria-hidden={
accessibilityElementsHidden || importantForAccessibility === "no-hide-descendants"
}
>
{children}
</span>
),
AppTextInput: ({ accessibilityLabel }: { accessibilityLabel?: string }) => (
<input aria-label={accessibilityLabel} />
),
}));

import { ConnectionFormField } from "./ConnectionFormField";

describe("ConnectionFormField accessibility", () => {
it.each(["Host", "Pairing code"])("exposes %s once as the input name", (label) => {
const markup = renderToStaticMarkup(
<ConnectionFormField label={label} placeholder="Not an accessible name" />,
);

expect(markup).toContain(`<span aria-hidden="true">${label}</span>`);
expect(markup).toContain(`<input aria-label="${label}"/>`);
});
});
10 changes: 7 additions & 3 deletions apps/mobile/src/features/connection/ConnectionFormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { View } from "react-native";
import { AppText, AppTextInput, type AppTextInputProps } from "../../components/AppText";
import { cn } from "../../lib/cn";

type ConnectionFormFieldProps = Omit<AppTextInputProps, "className"> & {
type ConnectionFormFieldProps = Omit<AppTextInputProps, "accessibilityLabel" | "className"> & {
readonly label: string;
readonly className?: string;
};
Expand All @@ -12,12 +12,16 @@ type ConnectionFormFieldProps = Omit<AppTextInputProps, "className"> & {
export function ConnectionFormField({ label, className, ...inputProps }: ConnectionFormFieldProps) {
return (
<View collapsable={false} className={cn("gap-1.5", className)}>
<AppText className="text-2xs font-t3-bold tracking-[0.8px] uppercase text-foreground-muted">
<AppText
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
className="text-2xs font-t3-bold tracking-[0.8px] uppercase text-foreground-muted"
>
{label}
</AppText>
<AppTextInput
accessibilityLabel={label}
{...inputProps}
accessibilityLabel={label}
className="rounded-[14px] px-4 py-3.5"
/>
</View>
Expand Down
8 changes: 5 additions & 3 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -683,10 +683,12 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
const isSplitLayout = layoutVariant === "split";
const contentMaxWidth = isSplitLayout ? CHAT_CONTENT_MAX_WIDTH : undefined;
const workspaceContentWidth = useWorkspaceContentWidth();
// Clearing animated width can retain the unfolded width after Android resumes folded.
// Assign both layouts explicitly so the dock always follows its current parent.
const composerWidthStyle = useAnimatedStyle(() =>
isSplitLayout && workspaceContentWidth !== null
? { width: workspaceContentWidth.value, right: undefined }
: { width: undefined, right: 0 },
? { width: workspaceContentWidth.value }
: { width: "100%" },
);
const selectedInstanceId = props.selectedThread.modelSelection.instanceId;
useStreamingHaptics(props.selectedThread.id, props.selectedThreadFeed);
Expand Down Expand Up @@ -968,7 +970,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
<Animated.View
layout={COMPOSER_LAYOUT_TRANSITION}
pointerEvents="box-none"
style={[{ position: "absolute", bottom: 0, left: 0, right: 0 }, composerWidthStyle]}
style={[{ position: "absolute", bottom: 0, left: 0 }, composerWidthStyle]}
>
{/* No paddingTop here: the overlay's measured height becomes the
list's bottom inset, so any padding above the pill/composer
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2477,7 +2477,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
// content-inset override. Seed the fresh instance synchronously with the
// current overlay height before the scroll integration's next reaction;
// on Android the declarative contentInset floor covers this same window.
const listMountKey = `${feedThreadKey}:${presentedFeed.length === 0 ? "empty" : "filled"}`;
// The thinking row a running thread shows while its messages load is not
// content: the list must still remount, and so open at the end, when they
// arrive.
const listMountKey = `${feedThreadKey}:${presentedFeed.some((entry) => entry.type !== "thinking") ? "filled" : "empty"}`;
useLayoutEffect(() => {
const bottom = props.contentInsetEndAdjustment.value;
if (bottom > 0) {
Expand Down
30 changes: 14 additions & 16 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1028,27 +1028,25 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
)}
{pr ? (
<View className="flex-row items-center gap-1" accessibilityLabel={pr.accessibilityLabel}>
{pr.kind === "stack" || pr.others > 0 ? (
<SymbolView
name={pr.kind === "stack" ? "square.3.layers.3d" : "arrow.triangle.pull"}
size={12}
tintColorClassName={
pr.state === null || pr.isDraft
? rowAppearance.mutedIconTintClassName
: pr.state === "open"
? "accent-adaptive-emerald-600-400"
: pr.state === "closed"
? "accent-adaptive-rose-600-400"
: "accent-adaptive-violet-600-400"
}
/>
) : null}
<SymbolView
name={pr.kind === "stack" ? "square.3.layers.3d" : "arrow.triangle.pull"}
size={12}
tintColorClassName={
pr.state === null || pr.isDraft
? rowAppearance.mutedIconTintClassName
: pr.state === "open"
? "accent-adaptive-emerald-600-400"
: pr.state === "closed"
? "accent-adaptive-rose-600-400"
: "accent-adaptive-violet-600-400"
}
/>
<Text
accessibilityLabel={pr.accessibilityLabel}
className={cn("text-xs", pr.textClassName)}
style={{ fontFamily: MONO_FONT }}
>
{pr.kind === "stack" || pr.others > 0 ? pr.label : `#${pr.label}`}
{pr.label}
</Text>
</View>
) : null}
Expand Down
7 changes: 2 additions & 5 deletions apps/mobile/src/features/threads/threadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
sortActiveThreadsByOrderKey,
resolveSettledThreadTimestamp,
sortPinnedThreadsByOrderKey,
sortSettledThreads,
} from "@t3tools/client-runtime/state/thread-sort";
import type { EnvironmentId, ProjectId } from "@t3tools/contracts";

Expand Down Expand Up @@ -606,11 +607,7 @@ export function buildThreadListV2Items(input: {
: orderedSnoozed.filter(
(thread) => `${thread.environmentId}:${thread.id}` === selectedThreadKey,
);
const orderedSettled = [...settled].sort(
(left, right) =>
parseTimestampMs(resolveSettledThreadTimestamp(right) ?? "") -
parseTimestampMs(resolveSettledThreadTimestamp(left) ?? ""),
);
const orderedSettled = sortSettledThreads(settled);
const settledLimit = input.settledLimit ?? Number.POSITIVE_INFINITY;
const pagedSettled =
orderedSettled.length > settledLimit ? orderedSettled.slice(0, settledLimit) : orderedSettled;
Expand Down
12 changes: 9 additions & 3 deletions apps/mobile/src/features/usage/UsageRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
formatHourShort,
formatPercent,
formatTokens,
formatUsageContractMismatch,
formatUsd,
makeWindow,
} from "@t3tools/shared/usageFormat";
Expand Down Expand Up @@ -101,8 +102,8 @@ export function UsageRouteScreen() {
);
const isFocused = useIsFocused();
const limits = useRefreshLimits(selectedEnvironmentIds, isFocused && tab === "limits");
const cursorAccessEnvironments = selectedEnvironments.filter((environment) =>
environment.summary?.sources.some((source) => source.action === "enableCursorKeychain"),
const cursorAccessEnvironments = selectedEnvironments.filter(
(environment) => environment.needsCursorKeychainAccess,
);
const refreshAfterCursorEnable = () => {
void refresh();
Expand Down Expand Up @@ -753,7 +754,12 @@ function usageEnvironmentStatus(environment: EnvironmentUsageStatus): string {
environment.summary &&
!isCompatibleUsageContractVersion(environment.summary.contractVersion, USAGE_CONTRACT_VERSION)
) {
return "Older server · excluded from usage totals";
return formatUsageContractMismatch(environment.label, {
direction:
environment.summary.contractVersion < USAGE_CONTRACT_VERSION
? "serverBehind"
: "clientBehind",
});
}
if (!environment.isConnected)
return environment.summary ? "Disconnected · showing saved usage" : "Waiting for connection…";
Expand Down
10 changes: 8 additions & 2 deletions apps/mobile/src/state/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
type UsageSummary,
type UsageSummaryInput,
} from "@t3tools/contracts";
import { refreshUsage } from "@t3tools/client-runtime/state/usage";
import { needsCursorKeychainAccess, refreshUsage } from "@t3tools/client-runtime/state/usage";
import { mergeUsage, type EnvironmentUsage, type MergedUsage } from "@t3tools/shared/usageMerge";
import * as Option from "effect/Option";
import { AsyncResult, Atom } from "effect/unstable/reactivity";
Expand All @@ -33,6 +33,7 @@ export interface EnvironmentUsageStatus {
readonly isConnected: boolean;
readonly error: string | null;
readonly summary: UsageSummary | null;
readonly needsCursorKeychainAccess: boolean;
}

/**
Expand All @@ -50,13 +51,18 @@ const usageByWindowAtom = Atom.family((windowKey: string) =>
const statuses: EnvironmentUsageStatus[] = [];
for (const [environmentId, presentation] of presentations) {
const result = get(serverEnvironment.usageSummary({ environmentId, input }));
const summary = Option.getOrNull(AsyncResult.value(result));
statuses.push({
environmentId,
label: presentation.entry.target.label,
isPending: result.waiting,
isConnected: presentation.connection.phase === "connected",
error: result._tag === "Failure" ? "This environment could not report usage." : null,
summary: Option.getOrNull(AsyncResult.value(result)),
summary,
needsCursorKeychainAccess: needsCursorKeychainAccess(
summary,
get(serverEnvironment.providersValueAtom(environmentId)),
),
});
}
return statuses;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ export const makeOrchestrationIntegrationHarness = (
Layer.provideMerge(
Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, {
publishThread: () => Effect.void,
requestCatchUp: () => Effect.void,
start: () => Effect.void,
}),
),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"ai": "7.0.111",
"diff": "8.0.3",
"effect": "catalog:",
"node-pty": "^1.1.0",
"node-pty": "^1.2.0-beta.15",
"stream-chain": "^4.2.5",
"stream-json": "3.6.0",
"yaml": "catalog:",
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/auth/dpop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
} from "./EnvironmentAuth.ts";
import * as ServerSecretStore from "./ServerSecretStore.ts";

/** Secret store name prefix of DPoP replay markers. The server prunes expired ones. */
export const DPOP_REPLAY_MARKER_PREFIX = "dpop-proof-";

export const mapDpopFailureReason = (code: DpopVerificationFailureCodeType): DpopFailureReason => {
switch (code) {
case "time_window":
Expand Down Expand Up @@ -96,7 +99,7 @@ export const verifyRequestDpopProof = (input: {
);
yield* secretStore
.create(
`dpop-proof-${replayKey}`,
`${DPOP_REPLAY_MARKER_PREFIX}${replayKey}`,
new TextEncoder().encode(
[
`thumbprint=${result.thumbprint}`,
Expand Down
Loading
Loading