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
+100
View File
@@ -0,0 +1,100 @@
import { IDENTITY_COLOR_NAMES, type IdentityColorName } from "@/styles/identity-colors";
import type { HostProfile } from "@/types/host-connection";
export type HostColor = "none" | IdentityColorName;
export const HOST_COLORS: readonly HostColor[] = ["none", ...IDENTITY_COLOR_NAMES];
export type HostBadgeDisplay = "name" | "icon" | "hidden";
export const HOST_BADGE_DISPLAYS: readonly HostBadgeDisplay[] = ["name", "icon", "hidden"];
/**
* Per-device host presentation. `badgeDisplay` is null while the user has not chosen,
* because the default differs by host (local hides, remote shows) and local-ness is only
* knowable from a desktop-only async query — never at parse time.
*/
export interface HostAppearance {
color: HostColor;
badgeDisplay: HostBadgeDisplay | null;
}
export function defaultHostAppearance(): HostAppearance {
return { color: "none", badgeDisplay: null };
}
function isHostColor(value: unknown): value is HostColor {
return HOST_COLORS.some((color) => color === value);
}
function isHostBadgeDisplay(value: unknown): value is HostBadgeDisplay {
return HOST_BADGE_DISPLAYS.some((display) => display === value);
}
export function normalizeStoredHostAppearance(value: unknown): HostAppearance {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return defaultHostAppearance();
}
const record = value as Record<string, unknown>;
return {
color: isHostColor(record.color) ? record.color : "none",
badgeDisplay: isHostBadgeDisplay(record.badgeDisplay) ? record.badgeDisplay : null,
};
}
export function resolveHostBadgeDisplay(input: {
appearance: HostAppearance;
isLocalHost: boolean;
localHostResolutionPending?: boolean;
}): HostBadgeDisplay | null {
if (input.appearance.badgeDisplay) {
return input.appearance.badgeDisplay;
}
if (input.localHostResolutionPending) {
return null;
}
return input.isLocalHost ? "hidden" : "name";
}
export interface HostBadgeModel {
serverId: string;
label: string;
color: HostColor;
showLabel: boolean;
}
export type HostAppearanceSource = Pick<HostProfile, "serverId" | "label" | "appearance">;
/**
* The sidebar's whole host-badge decision, resolved once per host list. Rows look their
* badge up by serverId and render whatever they find; a host that should show no badge is
* simply absent from the map.
*/
export function selectHostBadges(input: {
hosts: readonly HostAppearanceSource[];
localServerId: string | null;
localHostResolutionPending?: boolean;
enabled: boolean;
}): ReadonlyMap<string, HostBadgeModel> {
const badges = new Map<string, HostBadgeModel>();
if (!input.enabled) {
return badges;
}
for (const host of input.hosts) {
const display = resolveHostBadgeDisplay({
appearance: host.appearance,
isLocalHost: host.serverId === input.localServerId,
localHostResolutionPending: input.localHostResolutionPending,
});
if (display === null || display === "hidden") {
continue;
}
badges.set(host.serverId, {
serverId: host.serverId,
label: host.label.trim() || host.serverId,
color: host.appearance.color,
showLabel: display === "name",
});
}
return badges;
}