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:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("react-native", () => ({
|
||||
Platform: { OS: "web" },
|
||||
View: ({ children, testID }: { children?: React.ReactNode; testID?: string }) => (
|
||||
<div data-testid={testID}>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("react-native-unistyles", () => ({
|
||||
StyleSheet: {
|
||||
create: (factory: unknown) =>
|
||||
typeof factory === "function"
|
||||
? (factory as (theme: Record<string, unknown>) => unknown)({
|
||||
spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24 },
|
||||
colors: {
|
||||
foregroundMuted: "#aaa",
|
||||
palette: { amber: { 500: "#f0b429", 700: "#b7791f" } },
|
||||
},
|
||||
})
|
||||
: factory,
|
||||
},
|
||||
useUnistyles: () => ({ rt: { breakpoint: "md" } }),
|
||||
withUnistyles: <T,>(component: T) => component,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/message", () => ({
|
||||
AssistantTurnFooter: () => null,
|
||||
LiveElapsed: () => <span data-testid="running-turn-timestamp" />,
|
||||
STREAM_METADATA_FONT_SIZE: 11,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/assistant-fork-menu", () => ({
|
||||
AssistantForkMenu: () => <button data-testid="running-turn-fork" type="button" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/synced-loader", () => ({
|
||||
SyncedLoader: () => <span data-testid="running-turn-loader" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/retained-panel", () => ({
|
||||
useRetainedPanelActive: () => true,
|
||||
}));
|
||||
|
||||
import { TurnFooter } from "./turn-footer";
|
||||
|
||||
const unusedRunningTurnStrategy = null as unknown as React.ComponentProps<
|
||||
typeof TurnFooter
|
||||
>["strategy"];
|
||||
|
||||
describe("TurnFooter", () => {
|
||||
let root: Root | null = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
|
||||
value: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
act(() => root?.unmount());
|
||||
}
|
||||
root = null;
|
||||
container?.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
it("places the running-turn fork between the loader and timestamp", () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root?.render(
|
||||
<TurnFooter
|
||||
isRunning
|
||||
inFlightTurnStartedAt={new Date("2026-08-01T10:00:00.000Z")}
|
||||
host={null}
|
||||
strategy={unusedRunningTurnStrategy}
|
||||
supportsTimelineCursor
|
||||
onForkInFlightTurn={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const footer = container.querySelector('[data-testid="turn-working-indicator"]');
|
||||
const controls = Array.from(footer?.querySelectorAll("[data-testid]") ?? []).map((node) =>
|
||||
node.getAttribute("data-testid"),
|
||||
);
|
||||
|
||||
expect(controls).toEqual([
|
||||
"running-turn-loader",
|
||||
"running-turn-fork",
|
||||
"running-turn-timestamp",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -130,6 +130,8 @@ const WorkingIndicator = memo(function WorkingIndicator({
|
||||
<View style={stylesheet.workingLoader}>
|
||||
<ThemedSyncedLoader size={14} uniProps={workingIndicatorColorMapping} />
|
||||
</View>
|
||||
{/* Match the completed-turn footer: actions precede timing metadata. */}
|
||||
{onForkInFlightTurn ? <AssistantForkMenu onFork={onForkInFlightTurn} /> : null}
|
||||
{inFlightTurnStartedAt ? (
|
||||
<LiveElapsed
|
||||
startedAt={inFlightTurnStartedAt}
|
||||
@@ -138,9 +140,6 @@ const WorkingIndicator = memo(function WorkingIndicator({
|
||||
testID="turn-working-elapsed"
|
||||
/>
|
||||
) : null}
|
||||
{/* Sits in the same 24px gapped row as the loader and elapsed timer, so
|
||||
fork is reachable without waiting for the run to finish. */}
|
||||
{onForkInFlightTurn ? <AssistantForkMenu onFork={onForkInFlightTurn} /> : null}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user