fix(app): emphasize needs-input status indicators
Use a filled amber alert for needs-input while keeping running and review dots visually consistent between workspace rows and tabs.
This commit is contained in:
@@ -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).
|
// 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.
|
// 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
|
// The filled alert occupies the full badge shell so needs-input remains more prominent than
|
||||||
// against the 6pt dot — the two states read as the same-diameter disc. 8 is the closest even
|
// the passive status dots.
|
||||||
// size to the exact match (6 / 0.83 = 7.2); 6 would undershoot the dot rather than meet it.
|
|
||||||
const STATUS_BADGE_DOT_SIZE = 6;
|
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
|
// Matches the workspace title's lineHeight (sidebar-workspace-row-content's
|
||||||
// workspaceBranchText) so the icon centers on the title rather than floating above it.
|
// workspaceBranchText) so the icon centers on the title rather than floating above it.
|
||||||
const LEADING_SLOT_HEIGHT = 20;
|
const LEADING_SLOT_HEIGHT = 20;
|
||||||
@@ -42,7 +41,8 @@ const foregroundMutedColorMapping = (theme: Theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
});
|
});
|
||||||
const needsInputColorMapping = (theme: Theme) => ({
|
const needsInputColorMapping = (theme: Theme) => ({
|
||||||
color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
color: theme.colors.surface0,
|
||||||
|
fill: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import { useAppSettings } from "@/hooks/use-settings";
|
|||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||||
import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
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 { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||||
import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sidebar-workspace-title";
|
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 foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
const needsInputColorMapping = (theme: Theme) => ({
|
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);
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||||
@@ -239,7 +244,7 @@ function WorkspaceStatusIndicator({
|
|||||||
if (bucket === "needs_input") {
|
if (bucket === "needs_input") {
|
||||||
return (
|
return (
|
||||||
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-needs_input">
|
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-needs_input">
|
||||||
<ThemedCircleAlert size={14} uniProps={needsInputColorMapping} />
|
<ThemedCircleAlert size={STATUS_INDICATOR_ALERT_SIZE} uniProps={needsInputColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -568,14 +573,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
standaloneStatusDot: {
|
standaloneStatusDot: {
|
||||||
width: 8,
|
width: STATUS_INDICATOR_DOT_SIZE,
|
||||||
height: 8,
|
height: STATUS_INDICATOR_DOT_SIZE,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
backgroundColor: getStatusDotColor({ theme, bucket: "attention" }) ?? undefined,
|
||||||
},
|
},
|
||||||
standaloneRunningDot: {
|
standaloneRunningDot: {
|
||||||
width: 8,
|
width: STATUS_INDICATOR_DOT_SIZE,
|
||||||
height: 8,
|
height: STATUS_INDICATOR_DOT_SIZE,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: getStatusDotColor({ theme, bucket: "running" }) ?? undefined,
|
backgroundColor: getStatusDotColor({ theme, bucket: "running" }) ?? undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useCallback, useMemo, type ReactElement, type ReactNode } from "react";
|
import { useCallback, useMemo, type ReactElement, type ReactNode } from "react";
|
||||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
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 { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { SyncedLoader } from "@/components/synced-loader";
|
|
||||||
import { ensurePanelsRegistered } from "@/panels/register-panels";
|
import { ensurePanelsRegistered } from "@/panels/register-panels";
|
||||||
import { getPanelRegistration } from "@/panels/panel-registry";
|
import { getPanelRegistration } from "@/panels/panel-registry";
|
||||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||||
import { getStatusDotColor, isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
import { getStatusDotColor } from "@/utils/status-dot-color";
|
||||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
import {
|
||||||
|
STATUS_INDICATOR_ALERT_SIZE,
|
||||||
|
STATUS_INDICATOR_DOT_SIZE,
|
||||||
|
} from "@/utils/status-indicator-geometry";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import { usePanelInstanceAttributes } from "@/panels/panel-instance-attributes";
|
import { usePanelInstanceAttributes } from "@/panels/panel-instance-attributes";
|
||||||
|
|
||||||
@@ -26,10 +28,8 @@ export interface WorkspaceTabPresentation {
|
|||||||
statusBucket: SidebarStateBucket | null;
|
statusBucket: SidebarStateBucket | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_STATUS_DOT_SIZE = 7;
|
|
||||||
const EMPHASIZED_STATUS_DOT_SIZE = 9;
|
|
||||||
const DEFAULT_STATUS_DOT_OFFSET = -2;
|
const DEFAULT_STATUS_DOT_OFFSET = -2;
|
||||||
const EMPHASIZED_STATUS_DOT_OFFSET = -3;
|
const STATUS_ALERT_OFFSET = -3;
|
||||||
|
|
||||||
interface WorkspaceTabPresentationResolverProps {
|
interface WorkspaceTabPresentationResolverProps {
|
||||||
tab: WorkspaceTabDescriptor;
|
tab: WorkspaceTabDescriptor;
|
||||||
@@ -115,7 +115,12 @@ interface WorkspaceTabIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemedCheckIcon = withUnistyles(Check);
|
const ThemedCheckIcon = withUnistyles(Check);
|
||||||
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
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({
|
export function WorkspaceTabIcon({
|
||||||
presentation,
|
presentation,
|
||||||
@@ -126,20 +131,10 @@ export function WorkspaceTabIcon({
|
|||||||
const iconColor = active ? styles.iconActive.color : styles.iconInactive.color;
|
const iconColor = active ? styles.iconActive.color : styles.iconInactive.color;
|
||||||
const bucket = presentation.statusBucket;
|
const bucket = presentation.statusBucket;
|
||||||
let statusDotColor: string | undefined;
|
let statusDotColor: string | undefined;
|
||||||
if (bucket === "needs_input") statusDotColor = styles.statusDotNeedsInput.color;
|
if (bucket === "failed") statusDotColor = styles.statusDotFailed.color;
|
||||||
else if (bucket === "failed") statusDotColor = styles.statusDotFailed.color;
|
|
||||||
else if (bucket === "running") statusDotColor = styles.statusDotRunning.color;
|
else if (bucket === "running") statusDotColor = styles.statusDotRunning.color;
|
||||||
else if (bucket === "attention") statusDotColor = styles.statusDotAttention.color;
|
else if (bucket === "attention") statusDotColor = styles.statusDotAttention.color;
|
||||||
const statusDotSize = isEmphasizedStatusDotBucket(presentation.statusBucket)
|
const showNeedsInputAlert = bucket === "needs_input";
|
||||||
? 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 Icon = presentation.icon;
|
const Icon = presentation.icon;
|
||||||
const agentIconWrapperStyle = useMemo(
|
const agentIconWrapperStyle = useMemo(
|
||||||
() => [styles.agentIconWrapper, { width: size, height: size }],
|
() => [styles.agentIconWrapper, { width: size, height: size }],
|
||||||
@@ -151,31 +146,20 @@ export function WorkspaceTabIcon({
|
|||||||
{
|
{
|
||||||
backgroundColor: statusDotColor,
|
backgroundColor: statusDotColor,
|
||||||
borderColor: statusDotBorderColor ?? styles.statusDotBorderDefault.borderColor,
|
borderColor: statusDotBorderColor ?? styles.statusDotBorderDefault.borderColor,
|
||||||
width: statusDotSize,
|
|
||||||
height: statusDotSize,
|
|
||||||
right: statusDotOffset,
|
|
||||||
bottom: statusDotOffset,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[statusDotColor, statusDotBorderColor, statusDotSize, statusDotOffset],
|
[statusDotColor, statusDotBorderColor],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldShowLoader) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={agentIconWrapperStyle}
|
|
||||||
accessibilityRole="progressbar"
|
|
||||||
accessibilityLabel="Agent running"
|
|
||||||
>
|
|
||||||
<SyncedLoader size={size - 1} color={styles.syncedLoader.color} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={agentIconWrapperStyle}>
|
<View style={agentIconWrapperStyle}>
|
||||||
<Icon size={size} color={iconColor} />
|
<Icon size={size} color={iconColor} />
|
||||||
{statusDotColor ? <View style={statusDotStyle} /> : null}
|
{statusDotColor ? <View style={statusDotStyle} /> : null}
|
||||||
|
{showNeedsInputAlert ? (
|
||||||
|
<View style={styles.statusAlertOverlay}>
|
||||||
|
<ThemedCircleAlert size={STATUS_INDICATOR_ALERT_SIZE} uniProps={needsInputAlertMapping} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,16 +230,18 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: DEFAULT_STATUS_DOT_OFFSET,
|
right: DEFAULT_STATUS_DOT_OFFSET,
|
||||||
bottom: DEFAULT_STATUS_DOT_OFFSET,
|
bottom: DEFAULT_STATUS_DOT_OFFSET,
|
||||||
width: DEFAULT_STATUS_DOT_SIZE,
|
width: STATUS_INDICATOR_DOT_SIZE,
|
||||||
height: DEFAULT_STATUS_DOT_SIZE,
|
height: STATUS_INDICATOR_DOT_SIZE,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
statusDotBorderDefault: {
|
statusDotBorderDefault: {
|
||||||
borderColor: theme.colors.surface0,
|
borderColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
statusDotNeedsInput: {
|
statusAlertOverlay: {
|
||||||
color: getStatusDotColor({ theme, bucket: "needs_input" }) ?? undefined,
|
position: "absolute",
|
||||||
|
right: STATUS_ALERT_OFFSET,
|
||||||
|
bottom: STATUS_ALERT_OFFSET,
|
||||||
},
|
},
|
||||||
statusDotFailed: {
|
statusDotFailed: {
|
||||||
color: getStatusDotColor({ theme, bucket: "failed" }) ?? undefined,
|
color: getStatusDotColor({ theme, bucket: "failed" }) ?? undefined,
|
||||||
@@ -272,9 +258,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
iconInactive: {
|
iconInactive: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
syncedLoader: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
},
|
|
||||||
optionRow: {
|
optionRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const STATUS_INDICATOR_DOT_SIZE = 8;
|
||||||
|
export const STATUS_INDICATOR_ALERT_SIZE = 14;
|
||||||
Reference in New Issue
Block a user