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
+67 -12
View File
@@ -14,6 +14,7 @@ import {
type HostConnection,
type HostProfile,
} from "@/types/host-connection";
import { defaultHostAppearance, type HostBadgeDisplay, type HostColor } from "@/hosts/appearance";
import {
buildDaemonWebSocketUrl,
buildRelayWebSocketUrl,
@@ -1347,6 +1348,7 @@ export class HostRuntimeStore {
private hostListVersion = 0;
private hostRegistryLoaded = false;
private hosts: HostProfile[] = [];
private hostAppearanceMutationTail: Promise<void> = Promise.resolve();
private hostRegistryStatus: HostRegistryStatus = "loading";
private deps: HostRuntimeControllerDeps;
private lastConnectionStatusByServer = new Map<string, HostRuntimeConnectionStatus>();
@@ -1461,7 +1463,9 @@ export class HostRuntimeStore {
this.hostRegistryStatus = "ready";
this.emitHostList();
if (shouldPersistHosts) {
void this.persistHosts();
void this.persistHosts().catch((error) =>
console.error("[HostRuntime] Failed to persist host registry", error),
);
}
}
}
@@ -1626,7 +1630,9 @@ export class HostRuntimeStore {
);
this.emitHostList();
this.emit(newServerId);
void this.persistHosts();
void this.persistHosts().catch((error) =>
console.error("[HostRuntime] Failed to persist host registry", error),
);
}
async upsertDirectConnection(input: {
@@ -1664,6 +1670,7 @@ export class HostRuntimeStore {
const probeHost: HostProfile = {
serverId: "",
label: input.label ?? input.connection.id,
appearance: defaultHostAppearance(),
lifecycle: {},
connections: [input.connection],
preferredConnectionId: input.connection.id,
@@ -1782,14 +1789,59 @@ export class HostRuntimeStore {
});
}
async renameHost(serverId: string, label: string): Promise<void> {
const next = this.hosts.map((h) =>
h.serverId === serverId ? { ...h, label, updatedAt: new Date().toISOString() } : h,
private async updateHost(
serverId: string,
apply: (host: HostProfile) => HostProfile,
): Promise<void> {
const updatedAt = new Date().toISOString();
const next = this.hosts.map((host) =>
host.serverId === serverId ? { ...apply(host), updatedAt } : host,
);
this.setHostsAndSync(next);
await this.persistHosts();
}
async renameHost(serverId: string, label: string): Promise<void> {
await this.updateHost(serverId, (host) => ({ ...host, label }));
}
async setHostColor(serverId: string, color: HostColor): Promise<void> {
await this.updateHostAppearance(serverId, (host) => ({
...host,
appearance: { ...host.appearance, color },
}));
}
async setHostBadgeDisplay(serverId: string, badgeDisplay: HostBadgeDisplay): Promise<void> {
await this.updateHostAppearance(serverId, (host) => ({
...host,
appearance: { ...host.appearance, badgeDisplay },
}));
}
private updateHostAppearance(
serverId: string,
apply: (host: HostProfile) => HostProfile,
): Promise<void> {
const update = this.hostAppearanceMutationTail.then(() =>
this.applyHostAppearance(serverId, apply),
);
this.hostAppearanceMutationTail = update.catch(() => undefined);
return update;
}
private async applyHostAppearance(
serverId: string,
apply: (host: HostProfile) => HostProfile,
): Promise<void> {
const updatedAt = new Date().toISOString();
const next = this.hosts.map((host) =>
host.serverId === serverId ? { ...apply(host), updatedAt } : host,
);
await this.persistHosts(next);
this.setHostsAndSync(next);
}
async removeHost(serverId: string): Promise<void> {
const remaining = this.hosts.filter((daemon) => daemon.serverId !== serverId);
this.setHostsAndSync(remaining);
@@ -1848,7 +1900,9 @@ export class HostRuntimeStore {
])
: undefined,
});
void this.persistHosts();
void this.persistHosts().catch((error) =>
console.error("[HostRuntime] Failed to persist host registry", error),
);
return next.find((daemon) => daemon.serverId === input.serverId) as HostProfile;
}
@@ -1866,12 +1920,8 @@ export class HostRuntimeStore {
this.emitHostList();
}
private async persistHosts(): Promise<void> {
try {
await this.storage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(this.hosts));
} catch (error) {
console.error("[HostRuntime] Failed to persist host registry", error);
}
private async persistHosts(hosts = this.hosts): Promise<void> {
await this.storage.setItem(REGISTRY_STORAGE_KEY, JSON.stringify(hosts));
}
private emitHostList(): void {
@@ -2421,6 +2471,8 @@ export interface HostMutations {
label?: string,
) => Promise<HostProfile>;
renameHost: (serverId: string, label: string) => Promise<void>;
setHostColor: (serverId: string, color: HostColor) => Promise<void>;
setHostBadgeDisplay: (serverId: string, badgeDisplay: HostBadgeDisplay) => Promise<void>;
removeHost: (serverId: string) => Promise<void>;
removeConnection: (serverId: string, connectionId: string) => Promise<void>;
}
@@ -2435,6 +2487,9 @@ export function useHostMutations(): HostMutations {
upsertConnectionFromOffer: (offer, label) => store.upsertConnectionFromOffer(offer, label),
upsertConnectionFromOfferUrl: (url, label) => store.upsertConnectionFromOfferUrl(url, label),
renameHost: (serverId, label) => store.renameHost(serverId, label),
setHostColor: (serverId, color) => store.setHostColor(serverId, color),
setHostBadgeDisplay: (serverId, badgeDisplay) =>
store.setHostBadgeDisplay(serverId, badgeDisplay),
removeHost: (serverId) => store.removeHost(serverId),
removeConnection: (serverId, connectionId) => store.removeConnection(serverId, connectionId),
}),