Improve sidebar workspace readability and host identity (#2790)

* fix(app): align turn footer actions

Keep running-turn actions in the same position as completed-turn actions and use the shared small icon size for both copy and fork controls.

* feat(app): improve sidebar workspace identity

Make dense sidebar workspace rows easier to scan and let each device control how connected hosts are identified.

* fix(app): keep host badges readable

Use a compact display menu and keep identity color on the host icon so translated controls fit and badge text retains standard contrast.

* fix(app): announce selected sidebar badge mode

* fix(app): retain icon-only host names for screen readers

* fix(app): name hoisted workspace rows

* fix(app): retain host identity in workspace labels

* fix(app): retain workspace status in row labels

* fix(app): defer host appearance until identity resolves

* fix(app): serialize host appearance writes

* fix(app): serialize host registry writes

* fix(app): bound sidebar host badge labels

* test(app): match workspace status labels

* fix(app): consolidate sidebar identity behavior

* test(app): assert status grouping behavior

* fix(app): keep sidebar row actions accessible

* fix(app): stabilize optimistic message layout
This commit is contained in:
Mohamed Boudra
2026-08-02 16:58:20 +02:00
committed by GitHub
parent 553ac53643
commit ccb668b3b4
57 changed files with 3251 additions and 809 deletions
@@ -0,0 +1,91 @@
import { Text, View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Server } from "lucide-react-native";
import { HOST_COLORS, type HostColor } from "@/hosts/appearance";
import { identityColor } from "@/styles/identity-colors";
import type { Theme } from "@/styles/theme";
const ThemedServer = withUnistyles(Server);
const MAX_NAMED_BADGE_WIDTH = 96;
// The identity table is theme-independent, so its icon mapping is fixed for the life of the
// module. It must not be rebuilt per render: a fresh `uniProps` identity makes withUnistyles
// re-subscribe every pass.
function buildIconMappings(): Record<HostColor, (theme: Theme) => { color: string }> {
const byColor = {} as Record<HostColor, (theme: Theme) => { color: string }>;
for (const color of HOST_COLORS) {
byColor[color] =
color === "none"
? (theme: Theme) => ({ color: theme.colors.foregroundMuted })
: () => ({ color: identityColor(color) });
}
return byColor;
}
const ICON_MAPPINGS = buildIconMappings();
/**
* Which host a workspace lives on, as a pill after the title. Only rendered in multi-host
* setups — see selectHostBadges. It's a badge rather than a second line of text because the
* row has to stay scannable at one line per workspace.
*
*/
export function HostBadge({
serverId,
label,
color,
showLabel,
}: {
serverId: string;
label: string;
color: HostColor;
showLabel: boolean;
}) {
return (
<View
style={[hostBadgeStyles.badge, showLabel ? null : hostBadgeStyles.badgeIconOnly]}
testID={`sidebar-host-badge-${serverId}`}
>
<ThemedServer size={9} uniProps={ICON_MAPPINGS[color]} />
{showLabel ? (
<Text style={hostBadgeStyles.text} numberOfLines={1}>
{label}
</Text>
) : null}
</View>
);
}
const hostBadgeStyles = StyleSheet.create((theme) => ({
// Sized off the title's 20pt line box so the pill never grows the row. A named badge must
// leave room for the workspace title and trailing actions, even when a host has an arbitrary
// custom label. The full host name remains in the row's accessible label and hover card.
badge: {
flexDirection: "row",
alignItems: "center",
gap: 3,
height: 16,
paddingLeft: 4,
paddingRight: 6,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.surface2,
maxWidth: MAX_NAMED_BADGE_WIDTH,
minWidth: 0,
flexShrink: 1,
},
// Icon-only drops the label, so the asymmetric padding that made room for text leaves a
// lopsided pill. Symmetric padding recenters the icon.
badgeIconOnly: {
paddingLeft: 4,
paddingRight: 4,
flexShrink: 0,
},
text: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
lineHeight: 14,
color: theme.colors.foregroundMuted,
flexShrink: 1,
minWidth: 0,
},
}));