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;