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:
@@ -1,26 +0,0 @@
|
||||
// Medium-saturation tones chosen so a white letter stays legible on top in both
|
||||
// light and dark themes. Soft and colorful, but not pale pastels.
|
||||
const PROJECT_ICON_COLORS = [
|
||||
"#8b5cf6", // violet
|
||||
"#0ea5e9", // sky
|
||||
"#10b981", // emerald
|
||||
"#f97316", // orange
|
||||
"#ec4899", // pink
|
||||
"#6366f1", // indigo
|
||||
"#14b8a6", // teal
|
||||
"#ef4444", // red
|
||||
"#eab308", // amber
|
||||
"#3b82f6", // blue
|
||||
];
|
||||
|
||||
function hashProjectKey(projectKey: string): number {
|
||||
let hash = 0;
|
||||
for (const character of projectKey) {
|
||||
hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function deriveProjectIconColor(projectKey: string): string {
|
||||
return PROJECT_ICON_COLORS[hashProjectKey(projectKey) % PROJECT_ICON_COLORS.length];
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
import { getProjectStatusBadgeContent } from "./project-status-badge-content";
|
||||
|
||||
describe("getProjectStatusBadgeContent", () => {
|
||||
it("shows the loader while running", () => {
|
||||
expect(getProjectStatusBadgeContent("running")).toEqual({ kind: "loader" });
|
||||
it("shows a running dot while working", () => {
|
||||
expect(getProjectStatusBadgeContent("running")).toEqual({ kind: "dot", bucket: "running" });
|
||||
});
|
||||
|
||||
it("shows the alert when input is needed", () => {
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||
|
||||
export type ProjectStatusBadgeDotBucket = "failed" | "attention" | "running";
|
||||
|
||||
export type ProjectStatusBadgeContent =
|
||||
| { kind: "loader" }
|
||||
| { kind: "alert" }
|
||||
| { kind: "dot"; bucket: "failed" | "attention" };
|
||||
| { kind: "dot"; bucket: ProjectStatusBadgeDotBucket };
|
||||
|
||||
/**
|
||||
* What the collapsed-project status badge should render for a project's aggregate bucket,
|
||||
* or null when no badge should show at all. Kept as plain data (no React) so it's testable
|
||||
* without JSDOM or component mounting — see docs/testing.md's two test categories.
|
||||
* What the project status badge should render for a project's aggregate bucket, or null when
|
||||
* no badge should show at all. Kept as plain data (no React) so it's testable without JSDOM
|
||||
* or component mounting — see docs/testing.md's two test categories.
|
||||
*
|
||||
* The "dot" variant is narrowed to failed/attention only: running renders the loader and
|
||||
* needs_input renders the alert, so those two buckets can never reach a dot.
|
||||
* Running is a dot like failed and attention rather than its own glyph. The badge is 14pt;
|
||||
* anything with internal detail at that size loses to a solid disc, so the buckets separate
|
||||
* by color and — for running alone — by pulsing. Only needs_input earns a glyph, because
|
||||
* "someone must act" has to survive being the one amber state next to running.
|
||||
*/
|
||||
export function getProjectStatusBadgeContent(
|
||||
statusBucket: SidebarStateBucket | null,
|
||||
): ProjectStatusBadgeContent | null {
|
||||
if (shouldRenderSyncedStatusLoader({ bucket: statusBucket })) {
|
||||
return { kind: "loader" };
|
||||
}
|
||||
if (statusBucket === "needs_input") {
|
||||
return { kind: "alert" };
|
||||
}
|
||||
if (statusBucket === "failed" || statusBucket === "attention") {
|
||||
if (statusBucket === "failed" || statusBucket === "attention" || statusBucket === "running") {
|
||||
return { kind: "dot", bucket: statusBucket };
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -8,6 +8,9 @@ export function getStatusDotColor(input: {
|
||||
}): string | null {
|
||||
const { theme, bucket, showDoneAsInactive = false } = input;
|
||||
|
||||
// Working and needs_input share amber: both mean "this agent has your turn in it". They
|
||||
// never collide in practice because needs_input always draws the alert glyph, never a bare
|
||||
// dot — see getProjectStatusBadgeContent.
|
||||
if (bucket === "needs_input") {
|
||||
return theme.colors.palette.amber[500];
|
||||
}
|
||||
@@ -15,7 +18,7 @@ export function getStatusDotColor(input: {
|
||||
return theme.colors.palette.red[500];
|
||||
}
|
||||
if (bucket === "running") {
|
||||
return theme.colors.palette.blue[500];
|
||||
return theme.colors.palette.amber[500];
|
||||
}
|
||||
if (bucket === "attention") {
|
||||
return theme.colors.palette.green[500];
|
||||
|
||||
Reference in New Issue
Block a user