feat(app): add running status ring and lighten status dots
Keep the animated ring phase-locked across hosts and derive its knockout surface from each host's actual interaction state. Align filled status circles at 6px while preserving the larger needs-input alert. Co-authored-by: Christoph Leiter <christoph@leiter.io>
This commit is contained in:
co-authored by
Christoph Leiter
parent
6ae6231d1f
commit
c0daf8e066
@@ -8,7 +8,10 @@ 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 type { SurfaceBackdrop } from "@/styles/surface-backdrop";
|
||||
import { getStatusDotColor } from "@/utils/status-dot-color";
|
||||
import { StatusRing } from "@/components/status-ring";
|
||||
import { getStatusRingOffset } from "@/components/status-ring/geometry";
|
||||
import {
|
||||
STATUS_INDICATOR_ALERT_SIZE,
|
||||
STATUS_INDICATOR_DOT_SIZE,
|
||||
@@ -112,6 +115,13 @@ interface WorkspaceTabIconProps {
|
||||
active?: boolean;
|
||||
size?: number;
|
||||
statusDotBorderColor?: string;
|
||||
/**
|
||||
* The surface this icon is sitting on, so the running ring can knock out of it. This icon is
|
||||
* shared by the desktop tab strip, the tab switcher trigger, the split drag chip and the
|
||||
* subagents track, which are on different surfaces and some of which change surface on hover —
|
||||
* it cannot work the colour out for itself.
|
||||
*/
|
||||
backdrop: SurfaceBackdrop;
|
||||
}
|
||||
|
||||
const ThemedCheckIcon = withUnistyles(Check);
|
||||
@@ -127,12 +137,13 @@ export function WorkspaceTabIcon({
|
||||
active = false,
|
||||
size = 14,
|
||||
statusDotBorderColor,
|
||||
backdrop,
|
||||
}: WorkspaceTabIconProps): ReactElement {
|
||||
const iconColor = active ? styles.iconActive.color : styles.iconInactive.color;
|
||||
const bucket = presentation.statusBucket;
|
||||
const isRunning = bucket === "running";
|
||||
let statusDotColor: string | undefined;
|
||||
if (bucket === "failed") statusDotColor = styles.statusDotFailed.color;
|
||||
else if (bucket === "running") statusDotColor = styles.statusDotRunning.color;
|
||||
else if (bucket === "attention") statusDotColor = styles.statusDotAttention.color;
|
||||
const showNeedsInputAlert = bucket === "needs_input";
|
||||
const Icon = presentation.icon;
|
||||
@@ -154,13 +165,16 @@ export function WorkspaceTabIcon({
|
||||
return (
|
||||
<View style={agentIconWrapperStyle}>
|
||||
<Icon size={size} color={iconColor} />
|
||||
{statusDotColor ? (
|
||||
{isRunning ? (
|
||||
<View
|
||||
style={statusDotStyle}
|
||||
accessibilityRole={bucket === "running" ? "progressbar" : undefined}
|
||||
accessibilityLabel={bucket === "running" ? "Agent running" : undefined}
|
||||
/>
|
||||
style={styles.statusRing}
|
||||
accessibilityRole="progressbar"
|
||||
accessibilityLabel="Agent running"
|
||||
>
|
||||
<StatusRing backdrop={backdrop} />
|
||||
</View>
|
||||
) : null}
|
||||
{statusDotColor ? <View style={statusDotStyle} /> : null}
|
||||
{showNeedsInputAlert ? (
|
||||
<View style={styles.statusAlertOverlay}>
|
||||
<ThemedCircleAlert size={STATUS_INDICATOR_ALERT_SIZE} uniProps={needsInputAlertMapping} />
|
||||
@@ -186,13 +200,18 @@ export function WorkspaceTabOptionRow({
|
||||
trailingAccessory,
|
||||
}: WorkspaceTabOptionRowProps): ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const pressableStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.optionMainPressable,
|
||||
(Boolean(hovered) || pressed || active) && styles.optionRowActive,
|
||||
],
|
||||
const isOptionActive = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) =>
|
||||
Boolean(hovered) || pressed || active,
|
||||
[active],
|
||||
);
|
||||
const pressableStyle = useCallback(
|
||||
(state: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.optionMainPressable,
|
||||
isOptionActive(state) && styles.optionRowActive,
|
||||
],
|
||||
[isOptionActive],
|
||||
);
|
||||
const optionRowStyle = useMemo(
|
||||
() => [styles.optionRow, active && styles.optionRowActive],
|
||||
[active],
|
||||
@@ -200,16 +219,27 @@ export function WorkspaceTabOptionRow({
|
||||
return (
|
||||
<View style={optionRowStyle}>
|
||||
<Pressable onPress={onPress} style={pressableStyle}>
|
||||
<View style={styles.optionLeadingSlot}>
|
||||
<WorkspaceTabIcon presentation={presentation} active={selected || active} />
|
||||
</View>
|
||||
<View style={styles.optionContent}>
|
||||
<Text numberOfLines={1} style={styles.optionLabel}>
|
||||
{presentation.titleState === "loading"
|
||||
? t("workspace.tabs.loading")
|
||||
: presentation.label}
|
||||
</Text>
|
||||
</View>
|
||||
{(state) => {
|
||||
const optionActive = isOptionActive(state);
|
||||
return (
|
||||
<>
|
||||
<View style={styles.optionLeadingSlot}>
|
||||
<WorkspaceTabIcon
|
||||
presentation={presentation}
|
||||
active={selected || active}
|
||||
backdrop={optionActive ? "surface1" : "surface0"}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.optionContent}>
|
||||
<Text numberOfLines={1} style={styles.optionLabel}>
|
||||
{presentation.titleState === "loading"
|
||||
? t("workspace.tabs.loading")
|
||||
: presentation.label}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Pressable>
|
||||
{presentation.modified ? (
|
||||
<View style={styles.optionModifiedDot} accessibilityLabel={t("workspace.tabs.modified")} />
|
||||
@@ -241,6 +271,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: 1,
|
||||
},
|
||||
statusRing: {
|
||||
position: "absolute",
|
||||
right: getStatusRingOffset(DEFAULT_STATUS_DOT_OFFSET, STATUS_INDICATOR_DOT_SIZE),
|
||||
bottom: getStatusRingOffset(DEFAULT_STATUS_DOT_OFFSET, STATUS_INDICATOR_DOT_SIZE),
|
||||
},
|
||||
statusDotBorderDefault: {
|
||||
borderColor: theme.colors.surface0,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user