Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"expo-updates": "~57.0.19",
"expo-video": "~57.0.3",
"expo-web-browser": "~57.0.2",
"expo-widgets": "~57.0.15",
"expo-widgets": "57.0.15",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.3",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/widgets/AgentActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ vi.mock("@expo/ui/swift-ui", () => ({
}));

vi.mock("@expo/ui/swift-ui/modifiers", () => ({
activityBackgroundTint: (value: unknown) => value,
font: (value: unknown) => value,
foregroundStyle: (value: unknown) => value,
frame: (value: unknown) => value,
Expand Down
32 changes: 20 additions & 12 deletions apps/mobile/src/widgets/AgentActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HStack, Image, Spacer, Text, VStack, ZStack } from "@expo/ui/swift-ui";
import type { ComponentProps } from "react";
import {
activityBackgroundTint,
font,
foregroundStyle,
frame,
Expand Down Expand Up @@ -56,14 +57,13 @@ export function AgentActivity(
): LiveActivityLayout {
"widget";

// Use SwiftUI's semantic label colors rather than fixed hex keyed off the
// device color scheme. A Live Activity banner always renders over a dark
// system material regardless of the device's light/dark setting, so
// scheme-derived dark text read as unreadable dark-on-dark on the lock
// screen. Semantic colors adapt to whatever material the OS places them on:
// the dark LA banner and the (light or dark) home-screen widget alike.
const primaryForeground = "primary";
const secondaryForeground = "secondary";
// Hierarchical styles inherit the system's foreground treatment, including
// tinted and vibrant presentations, rather than resolving to a label color.
type Foreground = Parameters<typeof foregroundStyle>[0];
const primaryForeground = { type: "hierarchical", style: "primary" } as const;
const secondaryForeground = { type: "hierarchical", style: "secondary" } as const;
const monochrome =
environment.widgetRenderingMode === "accented" || environment.widgetRenderingMode === "vibrant";

// Status tints mirror the web sidebar's pills
// (apps/web/src/components/Sidebar.logic.ts resolveThreadStatusPill): amber
Expand All @@ -72,10 +72,13 @@ export function AgentActivity(
// Mac notification center) renders it on a light one — so pick the web
// palette's light (-600) or dark (-300) variant off the color scheme.
const isLightScheme = environment.colorScheme === "light";
const phaseTint = (phase: AgentActivityPhase | undefined): string => {
const phaseTint = (phase: AgentActivityPhase | undefined): Foreground => {
if (environment.isLuminanceReduced) {
return secondaryForeground;
}
if (monochrome) {
return primaryForeground;
}
switch (phase) {
case "waiting_for_approval":
return isLightScheme ? "#d97706" : "#fcd34d"; // amber-600 / amber-300
Expand Down Expand Up @@ -179,7 +182,7 @@ export function AgentActivity(

// SF Symbols, like the logo, ignore frame/foregroundStyle applied directly to
// the image; size + tint them through a container the resizable symbol fills.
const renderGlyph = (systemName: SFName, size: number, color: string) => (
const renderGlyph = (systemName: SFName, size: number, color: Foreground) => (
<HStack modifiers={[frame({ width: size, height: size }), foregroundStyle(color)]}>
<Image systemName={systemName} modifiers={[resizable()]} />
</HStack>
Expand Down Expand Up @@ -229,7 +232,7 @@ export function AgentActivity(
// frame the resizable image fills and tint it through the container's
// foreground style, which the template image inherits. The 3:2 frame matches
// the glyph's aspect ratio so it never distorts.
const renderLogo = (height: number, color: string) => (
const renderLogo = (height: number, color: Foreground) => (
<HStack modifiers={[frame({ width: height * 1.5, height }), foregroundStyle(color)]}>
<Image assetName="T3Mark" modifiers={[resizable()]} />
</HStack>
Expand All @@ -240,7 +243,12 @@ export function AgentActivity(
<VStack
alignment="leading"
spacing={6}
modifiers={deepLink ? [padding({ all: 14 }), widgetURL(deepLink)] : [padding({ all: 14 })]}
modifiers={[
padding({ all: 14 }),
// A clear tint reveals iOS 26's glass material; older hosts keep the standard surface.
activityBackgroundTint(environment.isLiquidGlassAvailable ? "clear" : null),
...(deepLink ? [widgetURL(deepLink)] : []),
]}
>
{/* Logo pinned to the leading edge; the status texts centered across the
full width (ZStack so the logo doesn't skew the centering). No footer —
Expand Down
114 changes: 86 additions & 28 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1584,18 +1584,21 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
useRightPanelStore.getState().open(threadRef, "pull-requests");
if (!props.isActive) onThreadActivate(threadRef);
}, [onThreadActivate, props.isActive, threadRef]);
const prBadge =
const renderPrBadge = (iconOnly: boolean, variant: "underline" | "badge" = "underline") =>
prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
<ThreadPullRequestBadgeControl
variant="underline"
variant={variant}
badge={prBadgeShape}
number={pr?.number ?? currentLinkedPr?.number}
url={pr?.url ?? currentLinkedPr?.url}
status={prStatus}
iconOnly={iconOnly}
onOpenStack={handlePrStackClick}
onOpenPullRequest={handlePrClick}
/>
) : null;
const hasPrBadge = prBadgeShape?.kind === "stack" || pr !== null || currentLinkedPr !== null;
const prBadge = renderPrBadge(false);
const terminalStatusIcon = terminalStatus ? (
<span
role="img"
Expand Down Expand Up @@ -1687,17 +1690,42 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
/>
}
>
{props.project ? (
<ProjectFavicon project={props.project} className="size-4 shrink-0" />
) : driverKind ? (
<ProviderInstanceIcon
driverKind={driverKind}
displayName={providerEntry?.displayName ?? modelInstanceId}
iconClassName="size-4"
/>
) : (
<SquarePenIcon aria-hidden className="size-4" />
)}
<span className="relative inline-flex size-4 shrink-0 items-center justify-center">
{props.project ? (
<ProjectFavicon project={props.project} className="size-4" />
) : driverKind ? (
<ProviderInstanceIcon
driverKind={driverKind}
displayName={providerEntry?.displayName ?? modelInstanceId}
iconClassName="size-4"
/>
) : (
<SquarePenIcon aria-hidden className="size-4" />
)}
{isRemote ? (
<Tooltip>
<TooltipTrigger
render={
<span
role="img"
aria-label={props.environmentLabel ?? "Remote environment"}
className="absolute -left-1 -bottom-1 inline-flex size-3 items-center justify-center rounded-full bg-sidebar text-sidebar-muted-foreground ring-1 ring-sidebar"
/>
}
>
<EnvironmentMachineIcon kind={props.environmentMachine} className="size-2.5" />
</TooltipTrigger>
<TooltipPopup side="right">
{props.environmentLabel ?? "Remote environment"}
</TooltipPopup>
</Tooltip>
) : null}
{hasPrBadge ? (
<span className="absolute -top-1 -right-1 inline-flex">
{renderPrBadge(true, "badge")}
</span>
) : null}
</span>
{topStatus ? (
<span
aria-hidden
Expand Down Expand Up @@ -1943,7 +1971,47 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
<div className="flex h-5 w-full min-w-0 items-center gap-1.5">
{draftIndicator}
{props.project ? (
<ProjectFavicon project={props.project} className="size-4 shrink-0" />
<span className="relative inline-flex size-4 shrink-0 items-center justify-center">
<ProjectFavicon project={props.project} className="size-4" />
{compactRows && isRemote ? (
<Tooltip>
<TooltipTrigger
render={
<span
role="img"
aria-label={props.environmentLabel ?? "Remote environment"}
className="absolute -right-1 -bottom-1 inline-flex size-3 items-center justify-center rounded-full bg-sidebar text-sidebar-muted-foreground ring-1 ring-sidebar"
/>
}
>
<EnvironmentMachineIcon
kind={props.environmentMachine}
className="size-2.5"
/>
</TooltipTrigger>
<TooltipPopup side="top">
{props.environmentLabel ?? "Remote environment"}
</TooltipPopup>
</Tooltip>
) : null}
</span>
) : compactRows && isRemote ? (
<Tooltip>
<TooltipTrigger
render={
<span
role="img"
aria-label={props.environmentLabel ?? "Remote environment"}
className="inline-flex size-4 shrink-0 items-center justify-center text-sidebar-muted-foreground/70"
/>
}
>
<EnvironmentMachineIcon kind={props.environmentMachine} className="size-3.5" />
</TooltipTrigger>
<TooltipPopup side="top">
{props.environmentLabel ?? "Remote environment"}
</TooltipPopup>
</Tooltip>
) : null}
{compactRows ? (
title
Expand All @@ -1962,18 +2030,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
{pinIndicator}
{compactRows ? (
<>
{isRemote ? (
<span
role="img"
aria-label={props.environmentLabel ?? "Remote environment"}
className="inline-flex shrink-0 text-sidebar-muted-foreground/70"
>
<EnvironmentMachineIcon
kind={props.environmentMachine}
className="size-3.5"
/>
</span>
) : null}
{terminalStatusIcon}
{topStatus && CompactStatusIcon ? (
isWokeStatus ? (
Expand Down Expand Up @@ -2001,7 +2057,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
</span>
)
) : null}
{prBadge}
{renderPrBadge(true)}
</>
) : null}
{/* The visible state owns this slot's width: status at rest,
Expand Down Expand Up @@ -2031,7 +2087,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
>
{compactRows ? (
status === "working" ? (
<WorkingDuration startedAt={resolveWorkingStartedAt(thread)} />
<span className={topStatus?.className}>
<WorkingDuration startedAt={resolveWorkingStartedAt(thread)} />
</span>
) : compactCompletedAt ? (
<SidebarCompletedTime completedAt={compactCompletedAt} />
) : (
Expand Down
16 changes: 12 additions & 4 deletions apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,17 @@ export function ThreadPullRequestBadgeControl({
number,
url,
status,
iconOnly = false,
onOpenStack,
onOpenPullRequest,
}: {
variant: "underline" | "ghost";
variant: "underline" | "ghost" | "badge";
badge: ThreadPullRequestBadge | null;
number?: number | undefined;
url?: string | undefined;
status: PrStatusIndicator | null;
/** Dense rows drop the number/layer count and keep only the state glyph. */
iconOnly?: boolean;
onOpenStack: () => void;
onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void;
}) {
Expand All @@ -168,7 +171,9 @@ export function ThreadPullRequestBadgeControl({
const className = cn(
variant === "ghost"
? buttonVariants({ variant: "ghost", size: "xs" })
: "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap border-b border-transparent hover:border-current focus-visible:outline-2 focus-visible:outline-ring",
: variant === "badge"
? "inline-flex size-3 shrink-0 cursor-pointer items-center justify-center rounded-full bg-sidebar ring-1 ring-sidebar outline-none focus-visible:ring-2 focus-visible:ring-ring"
: "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap border-b border-transparent hover:border-current focus-visible:outline-2 focus-visible:outline-ring",
"text-xs tabular-nums",
variant === "ghost" &&
"font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]",
Expand All @@ -178,8 +183,11 @@ export function ThreadPullRequestBadgeControl({
);
const content = (
<>
<ThreadPullRequestBadgeIcon icon={badge?.kind ?? "pull-request"} />
{isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number}
<ThreadPullRequestBadgeIcon
icon={badge?.kind ?? "pull-request"}
className={variant === "badge" ? "size-2.5" : undefined}
/>
{iconOnly ? null : isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number}
</>
);
return (
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4063,6 +4063,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
className={cn(
"flex flex-col rounded-md px-0.5 transition-colors",
isExpandedToolGroupEntry ? "py-0" : "py-0.5",
expanded && "mb-1",
canExpand &&
"cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70",
)}
Expand Down
Loading
Loading