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:
@@ -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),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user