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
@@ -176,6 +176,10 @@ function agentStreamEventKey(type: string): string {
return `agent-stream-event:${type}`;
}
function agentStreamItemKey(type: string): string {
return `agent-stream-item:${type}`;
}
function agentUpdateKey(agentId: string, status: string): string {
return `agent-update:${agentId}:${status}`;
}
@@ -526,6 +530,12 @@ export async function installDaemonWebSocketGate(page: Page) {
(message) => readAgentStreamEventType(message) === type,
);
},
holdNextAgentStreamItem(type: string): void {
pendingServerMessageHolds.set(
agentStreamItemKey(type),
(message) => readAgentStreamItemType(message) === type,
);
},
async waitForHeldServerMessage(type?: string): Promise<void> {
const key = type ? serverMessageKey(type) : null;
while (!heldServerMessages.some((message) => key === null || message.key === key)) {
@@ -574,6 +584,22 @@ export async function installDaemonWebSocketGate(page: Page) {
heldServerMessage.browser.send(follower);
}
},
async waitForHeldAgentStreamItem(type: string): Promise<void> {
const key = agentStreamItemKey(type);
while (!heldServerMessages.some((message) => message.key === key)) {
await new Promise<void>((resolve) => heldServerMessageWaiters.add(resolve));
}
},
releaseHeldAgentStreamItem(type: string): void {
const key = agentStreamItemKey(type);
const index = heldServerMessages.findIndex((message) => message.key === key);
const [heldServerMessage] = index >= 0 ? heldServerMessages.splice(index, 1) : [];
if (!heldServerMessage) throw new Error("No held agent stream item to release");
heldServerMessage.browser.send(heldServerMessage.message);
for (const follower of heldServerMessage.agentStreamFollowers) {
heldServerMessage.browser.send(follower);
}
},
requestTimelineTail(agentId: string): void {
if (!latestServer) throw new Error("No daemon WebSocket is connected");
latestServer.send(