diff --git a/packages/app/src/components/sidebar/project-leading-visual.tsx b/packages/app/src/components/sidebar/project-leading-visual.tsx index aeb0ad852..b2932117c 100644 --- a/packages/app/src/components/sidebar/project-leading-visual.tsx +++ b/packages/app/src/components/sidebar/project-leading-visual.tsx @@ -26,11 +26,10 @@ const STATUS_BADGE_OFFSET = -4; // odd size measured 1.5 device px right and down at 3x, ~3px of asymmetry between opposite gaps). // Even sizes divide the shell into whole pixels and land dead center with no correction. // -// Lucide's circle-alert paints ~83% of its nominal size, so an alert of 8 draws a ~6.6pt circle -// against the 6pt dot — the two states read as the same-diameter disc. 8 is the closest even -// size to the exact match (6 / 0.83 = 7.2); 6 would undershoot the dot rather than meet it. +// The filled alert occupies the full badge shell so needs-input remains more prominent than +// the passive status dots. const STATUS_BADGE_DOT_SIZE = 6; -const STATUS_BADGE_ALERT_SIZE = 8; +const STATUS_BADGE_ALERT_SIZE = 12; // Matches the workspace title's lineHeight (sidebar-workspace-row-content's // workspaceBranchText) so the icon centers on the title rather than floating above it. const LEADING_SLOT_HEIGHT = 20; @@ -42,7 +41,8 @@ const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); const needsInputColorMapping = (theme: Theme) => ({ - color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined, + color: theme.colors.surface0, + fill: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined, }); /** diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx index 5fa30440d..79c0e0269 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx @@ -20,6 +20,10 @@ import { useAppSettings } from "@/hooks/use-settings"; import type { Theme } from "@/styles/theme"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color"; +import { + STATUS_INDICATOR_ALERT_SIZE, + STATUS_INDICATOR_DOT_SIZE, +} from "@/utils/status-indicator-geometry"; import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sidebar-workspace-title"; @@ -35,7 +39,8 @@ const EMPHASIZED_STATUS_DOT_OFFSET = -1; const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const needsInputColorMapping = (theme: Theme) => ({ - color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined, + color: theme.colors.surface0, + fill: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined, }); const ThemedCircleAlert = withUnistyles(CircleAlert); @@ -239,7 +244,7 @@ function WorkspaceStatusIndicator({ if (bucket === "needs_input") { return ( - + ); } @@ -568,14 +573,14 @@ const styles = StyleSheet.create((theme) => ({ borderWidth: 1, }, standaloneStatusDot: { - width: 8, - height: 8, + width: STATUS_INDICATOR_DOT_SIZE, + height: STATUS_INDICATOR_DOT_SIZE, borderRadius: theme.borderRadius.full, backgroundColor: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined, }, standaloneRunningDot: { - width: 8, - height: 8, + width: STATUS_INDICATOR_DOT_SIZE, + height: STATUS_INDICATOR_DOT_SIZE, borderRadius: theme.borderRadius.full, backgroundColor: getStatusDotColor({ theme, bucket: "running" }) ?? undefined, }, diff --git a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx index c965a2f94..c369c3ec0 100644 --- a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx +++ b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx @@ -1,16 +1,18 @@ import { useCallback, useMemo, type ReactElement, type ReactNode } from "react"; import { Pressable, Text, View, type PressableStateCallbackType } from "react-native"; -import { Check } from "lucide-react-native"; +import { Check, CircleAlert } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; import invariant from "tiny-invariant"; -import { SyncedLoader } from "@/components/synced-loader"; import { ensurePanelsRegistered } from "@/panels/register-panels"; import { getPanelRegistration } from "@/panels/panel-registry"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; -import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color"; -import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; +import { getStatusDotColor } from "@/utils/status-dot-color"; +import { + STATUS_INDICATOR_ALERT_SIZE, + STATUS_INDICATOR_DOT_SIZE, +} from "@/utils/status-indicator-geometry"; import type { Theme } from "@/styles/theme"; import { usePanelInstanceAttributes } from "@/panels/panel-instance-attributes"; @@ -26,10 +28,8 @@ export interface WorkspaceTabPresentation { statusBucket: SidebarStateBucket | null; } -const DEFAULT_STATUS_DOT_SIZE = 7; -const EMPHASIZED_STATUS_DOT_SIZE = 9; const DEFAULT_STATUS_DOT_OFFSET = -2; -const EMPHASIZED_STATUS_DOT_OFFSET = -3; +const STATUS_ALERT_OFFSET = -3; interface WorkspaceTabPresentationResolverProps { tab: WorkspaceTabDescriptor; @@ -115,7 +115,12 @@ interface WorkspaceTabIconProps { } const ThemedCheckIcon = withUnistyles(Check); +const ThemedCircleAlert = withUnistyles(CircleAlert); const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); +const needsInputAlertMapping = (theme: Theme) => ({ + color: theme.colors.surface0, + fill: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined, +}); export function WorkspaceTabIcon({ presentation, @@ -126,20 +131,10 @@ export function WorkspaceTabIcon({ const iconColor = active ? styles.iconActive.color : styles.iconInactive.color; const bucket = presentation.statusBucket; let statusDotColor: string | undefined; - if (bucket === "needs_input") statusDotColor = styles.statusDotNeedsInput.color; - else if (bucket === "failed") statusDotColor = styles.statusDotFailed.color; + if (bucket === "failed") statusDotColor = styles.statusDotFailed.color; else if (bucket === "running") statusDotColor = styles.statusDotRunning.color; else if (bucket === "attention") statusDotColor = styles.statusDotAttention.color; - const statusDotSize = isEmphasizedStatusDotBucket(presentation.statusBucket) - ? EMPHASIZED_STATUS_DOT_SIZE - : DEFAULT_STATUS_DOT_SIZE; - const statusDotOffset = - statusDotSize === EMPHASIZED_STATUS_DOT_SIZE - ? EMPHASIZED_STATUS_DOT_OFFSET - : DEFAULT_STATUS_DOT_OFFSET; - const shouldShowLoader = shouldRenderSyncedStatusLoader({ - bucket: presentation.statusBucket, - }); + const showNeedsInputAlert = bucket === "needs_input"; const Icon = presentation.icon; const agentIconWrapperStyle = useMemo( () => [styles.agentIconWrapper, { width: size, height: size }], @@ -151,31 +146,20 @@ export function WorkspaceTabIcon({ { backgroundColor: statusDotColor, borderColor: statusDotBorderColor ?? styles.statusDotBorderDefault.borderColor, - width: statusDotSize, - height: statusDotSize, - right: statusDotOffset, - bottom: statusDotOffset, }, ], - [statusDotColor, statusDotBorderColor, statusDotSize, statusDotOffset], + [statusDotColor, statusDotBorderColor], ); - if (shouldShowLoader) { - return ( - - - - ); - } - return ( {statusDotColor ? : null} + {showNeedsInputAlert ? ( + + + + ) : null} ); } @@ -246,16 +230,18 @@ const styles = StyleSheet.create((theme) => ({ position: "absolute", right: DEFAULT_STATUS_DOT_OFFSET, bottom: DEFAULT_STATUS_DOT_OFFSET, - width: DEFAULT_STATUS_DOT_SIZE, - height: DEFAULT_STATUS_DOT_SIZE, + width: STATUS_INDICATOR_DOT_SIZE, + height: STATUS_INDICATOR_DOT_SIZE, borderRadius: theme.borderRadius.full, borderWidth: 1, }, statusDotBorderDefault: { borderColor: theme.colors.surface0, }, - statusDotNeedsInput: { - color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined, + statusAlertOverlay: { + position: "absolute", + right: STATUS_ALERT_OFFSET, + bottom: STATUS_ALERT_OFFSET, }, statusDotFailed: { color: getStatusDotColor({ theme, bucket: "failed" }) ?? undefined, @@ -272,9 +258,6 @@ const styles = StyleSheet.create((theme) => ({ iconInactive: { color: theme.colors.foregroundMuted, }, - syncedLoader: { - color: theme.colors.foreground, - }, optionRow: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/utils/status-indicator-geometry.ts b/packages/app/src/utils/status-indicator-geometry.ts new file mode 100644 index 000000000..87526f61f --- /dev/null +++ b/packages/app/src/utils/status-indicator-geometry.ts @@ -0,0 +1,2 @@ +export const STATUS_INDICATOR_DOT_SIZE = 8; +export const STATUS_INDICATOR_ALERT_SIZE = 14;