Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 32 additions & 1 deletion apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ const CLAUDE_EFFORT_OPTIONS = {
{ value: "max", label: "Max" },
{ value: "ultrathink", label: "Ultrathink" },
],
sonnet5: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High", isDefault: true },
{ value: "xhigh", label: "Extra High" },
{ value: "max", label: "Max" },
{ value: "ultrathink", label: "Ultrathink" },
],
opus45: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
Expand Down Expand Up @@ -184,6 +192,29 @@ const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
],
}),
},
{
slug: "claude-sonnet-5",
name: "Claude Sonnet 5",
isCustom: false,
capabilities: createModelCapabilities({
optionDescriptors: [
buildSelectOptionDescriptor({
id: "effort",
label: "Reasoning",
options: CLAUDE_EFFORT_OPTIONS.sonnet5,
promptInjectedValues: ["ultrathink"],
}),
buildSelectOptionDescriptor({
id: "contextWindow",
label: "Context Window",
options: [
{ value: "200k", label: "200k", isDefault: true },
{ value: "1m", label: "1M" },
],
}),
],
}),
},
{
slug: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
Expand Down Expand Up @@ -295,7 +326,7 @@ export function normalizeClaudeCliEffort(
if (effort === "ultracode") {
return "xhigh";
}
if (effort === "xhigh" && model !== "claude-opus-4-8") {
if (effort === "xhigh" && model !== "claude-opus-4-8" && model !== "claude-sonnet-5") {
return "max";
}
if (effort === "max" && model === "claude-sonnet-4-6") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { RelayClientDeviceRecord } from "@t3tools/contracts/relay";
import { describe, expect, it } from "vite-plus/test";

import {
mobileClientNotificationDetail,
mobileClientPlatformLabel,
mobileClientUpdatedAtLabel,
} from "./MobileClientsUserProfilePage.logic";

function device(overrides: Partial<RelayClientDeviceRecord> = {}): RelayClientDeviceRecord {
return {
deviceId: "device-1",
label: "Julius’s iPhone",
platform: "ios",
iosMajorVersion: 18,
appVersion: "1.2.3",
notifications: {
enabled: true,
notifyOnApproval: true,
notifyOnInput: false,
notifyOnCompletion: true,
notifyOnFailure: false,
},
liveActivities: { enabled: true },
updatedAt: "2026-06-21T12:00:00.000Z",
...overrides,
};
}

describe("mobile client presentation", () => {
it("describes the client platform and enabled notification events", () => {
const client = device();

expect(mobileClientPlatformLabel(client)).toBe("iOS 18 · T3 Code 1.2.3");
expect(mobileClientNotificationDetail(client)).toBe(
"Alerts enabled for approvals, completions.",
);
});

it("distinguishes disabled notifications from an empty event selection", () => {
expect(
mobileClientNotificationDetail(
device({ notifications: { ...device().notifications, enabled: false } }),
),
).toBe("Push notifications are disabled on this device.");
expect(
mobileClientNotificationDetail(
device({
notifications: {
enabled: true,
notifyOnApproval: false,
notifyOnInput: false,
notifyOnCompletion: false,
notifyOnFailure: false,
},
}),
),
).toBe("Push notifications are enabled, but no alert types are selected.");
});

it("handles missing app versions and invalid update timestamps", () => {
expect(mobileClientPlatformLabel(device({ appVersion: null }))).toBe("iOS 18");
expect(mobileClientUpdatedAtLabel("not-a-date")).toBe("Update time unavailable");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { RelayClientDeviceRecord } from "@t3tools/contracts/relay";

const mobileClientUpdatedAtFormatter = new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
});

const NOTIFICATION_PREFERENCES = [
["notifyOnApproval", "approvals"],
["notifyOnInput", "input requests"],
["notifyOnCompletion", "completions"],
["notifyOnFailure", "failures"],
] as const satisfies ReadonlyArray<
readonly [keyof RelayClientDeviceRecord["notifications"], string]
>;

export function mobileClientPlatformLabel(device: RelayClientDeviceRecord): string {
return `iOS ${device.iosMajorVersion}${device.appVersion ? ` · T3 Code ${device.appVersion}` : ""}`;
}

export function mobileClientNotificationDetail(device: RelayClientDeviceRecord): string {
if (!device.notifications.enabled) {
return "Push notifications are disabled on this device.";
}

const enabledPreferences = NOTIFICATION_PREFERENCES.flatMap(([preference, label]) =>
device.notifications[preference] ? [label] : [],
);
return enabledPreferences.length > 0
? `Alerts enabled for ${enabledPreferences.join(", ")}.`
: "Push notifications are enabled, but no alert types are selected.";
}

export function mobileClientUpdatedAtLabel(updatedAt: string): string {
const date = new Date(updatedAt);
return Number.isNaN(date.getTime())
? "Update time unavailable"
: `Updated ${mobileClientUpdatedAtFormatter.format(date)}`;
}
166 changes: 166 additions & 0 deletions apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { RelayClientDeviceRecord } from "@t3tools/contracts/relay";
import { RefreshCwIcon, SmartphoneIcon } from "lucide-react";

import { useManagedRelayDevices } from "../../cloud/managedRelayState";
import { cn } from "../../lib/utils";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
import { Skeleton } from "../ui/skeleton";
import {
mobileClientNotificationDetail,
mobileClientPlatformLabel,
mobileClientUpdatedAtLabel,
} from "./MobileClientsUserProfilePage.logic";

const MOBILE_CLIENT_SKELETON_ROWS = ["primary", "secondary"] as const;

function MobileClientStatusBadge({
enabled,
label,
}: {
readonly enabled: boolean;
readonly label: string;
}) {
return (
<Badge variant={enabled ? "success" : "outline"}>
{label}: {enabled ? "On" : "Off"}
</Badge>
);
}

function MobileClientRow({ device }: { readonly device: RelayClientDeviceRecord }) {
return (
<li className="rounded-xl border bg-card p-4 text-card-foreground shadow-sm/4">
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-muted/40 text-muted-foreground">
<SmartphoneIcon className="size-4" />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
<div className="min-w-0">
<h3 className="truncate text-sm font-semibold text-foreground">{device.label}</h3>
<p className="text-xs text-muted-foreground">{mobileClientPlatformLabel(device)}</p>
</div>
<p className="shrink-0 text-[11px] text-muted-foreground/75">
{mobileClientUpdatedAtLabel(device.updatedAt)}
</p>
</div>
<div className="mt-3 flex flex-wrap gap-1.5">
<MobileClientStatusBadge
enabled={device.notifications.enabled}
label="Push notifications"
/>
<MobileClientStatusBadge
enabled={device.liveActivities.enabled}
label="Live Activities"
/>
</div>
<p className="mt-2 text-xs leading-relaxed text-muted-foreground/80">
{mobileClientNotificationDetail(device)}
</p>
</div>
</div>
</li>
);
}

function MobileClientsSkeleton() {
return (
<div aria-label="Loading mobile clients" className="space-y-3" role="status">
{MOBILE_CLIENT_SKELETON_ROWS.map((row) => (
<div key={row} className="rounded-xl border p-4">
<div className="flex gap-3">
<Skeleton className="size-9 shrink-0 rounded-lg" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-36" />
<Skeleton className="h-3 w-28" />
<div className="flex gap-2 pt-1">
<Skeleton className="h-5 w-28" />
<Skeleton className="h-5 w-24" />
</div>
</div>
</div>
</div>
))}
</div>
);
}

function EmptyMobileClients() {
return (
<Empty className="min-h-72 rounded-xl border border-dashed bg-muted/15">
<EmptyMedia variant="icon">
<SmartphoneIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>No mobile clients</EmptyTitle>
<EmptyDescription>
Sign in to T3 Code on your iPhone to register it for push notifications and Live
Activities.
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}

export function MobileClientsUserProfilePage() {
const devicesState = useManagedRelayDevices();
const devices = devicesState.data ?? [];
const isInitialLoad =
!devicesState.accountId || (devicesState.data === null && !devicesState.error);
const hasErrorWithoutData = devicesState.error !== null && devicesState.data === null;

return (
<div className="flex min-h-[30rem] w-full flex-col bg-background text-foreground">
<header className="flex flex-col gap-4 border-b px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-base font-semibold tracking-[-0.01em]">Mobile clients</h2>
<p className="mt-1 text-sm text-muted-foreground">
Devices registered to receive T3 Connect activity from your environments.
</p>
</div>
<Button
size="sm"
variant="outline"
disabled={devicesState.isPending}
onClick={devicesState.refresh}
>
<RefreshCwIcon className={cn("size-3.5", devicesState.isPending && "animate-spin")} />
Refresh
</Button>
</header>

<div className="flex-1 p-6">
{devicesState.error ? (
<div
className="mb-4 flex flex-col gap-3 rounded-lg border border-destructive/25 bg-destructive/5 p-3 text-sm sm:flex-row sm:items-center sm:justify-between"
role="alert"
>
<div>
<p className="font-medium text-destructive-foreground">
Could not load mobile clients
</p>
<p className="mt-0.5 text-xs text-muted-foreground">{devicesState.error}</p>
</div>
<Button size="xs" variant="outline" onClick={devicesState.refresh}>
Try again
</Button>
</div>
) : null}

{isInitialLoad ? (
<MobileClientsSkeleton />
) : hasErrorWithoutData ? null : devices.length > 0 ? (
<ul className="space-y-3">
{devices.map((device) => (
<MobileClientRow key={device.deviceId} device={device} />
))}
</ul>
) : (
<EmptyMobileClients />
)}
</div>
</div>
);
}
13 changes: 12 additions & 1 deletion apps/web/src/components/settings/CloudSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { usePrimarySessionState } from "../../environments/primary";
import { webRuntime } from "../../lib/runtime";
import { cn } from "../../lib/utils";
import { DesktopClerkWaitlist } from "../clerk/DesktopClerkWaitlist";
import { MobileClientsUserProfilePage } from "../clerk/MobileClientsUserProfilePage";
import { Button } from "../ui/button";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
import { Skeleton } from "../ui/skeleton";
Expand Down Expand Up @@ -127,7 +128,17 @@ function CloudSettingsPanelInner() {
<SettingsRow
title="Cloud account"
description="Manage your private-beta T3 Cloud session."
control={<UserButton />}
control={
<UserButton>
<UserButton.UserProfilePage
label="Mobile clients"
labelIcon={<SmartphoneIcon className="size-4" />}
url="mobile-clients"
>
<MobileClientsUserProfilePage />
</UserButton.UserProfilePage>
</UserButton>
}
/>
</SettingsSection>
<SettingsSection title="Preferences">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/settings/ProviderModelsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
*/
const CUSTOM_MODEL_PLACEHOLDER_BY_KIND: Partial<Record<ProviderDriverKind, string>> = {
[ProviderDriverKind.make("codex")]: "gpt-6.7-codex-ultra-preview",
[ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5-0",
[ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5",
[ProviderDriverKind.make("cursor")]: "claude-sonnet-4-6",
[ProviderDriverKind.make("opencode")]: "openai/gpt-5",
};
Expand Down
7 changes: 5 additions & 2 deletions packages/contracts/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini";

export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
[CODEX_DRIVER_KIND]: DEFAULT_MODEL,
[CLAUDE_DRIVER_KIND]: "claude-sonnet-4-6",
[CLAUDE_DRIVER_KIND]: "claude-sonnet-5",
[CURSOR_DRIVER_KIND]: "auto",
[OPENCODE_DRIVER_KIND]: "openai/gpt-5",
[GROK_BUILD_DRIVER_KIND]: "grok-build",
Expand Down Expand Up @@ -175,7 +175,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
"opus-4.6": "claude-opus-4-6",
"claude-opus-4.6": "claude-opus-4-6",
"claude-opus-4-6-20251117": "claude-opus-4-6",
sonnet: "claude-sonnet-4-6",
sonnet: "claude-sonnet-5",
"sonnet-5": "claude-sonnet-5",
"claude-sonnet-5.0": "claude-sonnet-5",
"claude-sonnet-5-0": "claude-sonnet-5",
"sonnet-4.6": "claude-sonnet-4-6",
"claude-sonnet-4.6": "claude-sonnet-4-6",
"claude-sonnet-4-6-20251117": "claude-sonnet-4-6",
Expand Down
Loading
Loading