fix(app): traverse workspaces beyond nine (#6586)

Relative navigation shared the list capped for digit shortcuts, so it wrapped at the ninth workspace. Keep all sidebar targets and cap only numbered badges. Carry forward #4128 with keyboard-routing and browser regressions.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Raisal P Wardana <IEatCodeDaily@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-14 13:14:08 +02:00
committed by GitHub
co-authored by Raisal P Wardana
parent 938c4e2632
commit cafa04d3fb
3 changed files with 123 additions and 9 deletions
@@ -0,0 +1,74 @@
import type { Page } from "@playwright/test";
import { test, expect } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import { seedWorkspace } from "../support/helpers/seed-client";
import { getServerId } from "../support/helpers/server-id";
import {
expectSidebarWorkspaceSelected,
switchWorkspaceViaSidebar,
} from "../support/helpers/workspace-ui";
async function readVisibleWorkspaceOrder(page: Page): Promise<string[]> {
const rows = page.getByRole("button", { name: /^Workspace \d{2}$/ });
await expect(rows).toHaveCount(12);
return rows.evaluateAll(
(elements, prefix) =>
elements.map((element) => element.getAttribute("data-testid")!.slice(prefix.length)),
`sidebar-workspace-row-${getServerId()}:`,
);
}
async function navigateWorkspace(
page: Page,
direction: "next" | "previous",
workspaceId: string,
): Promise<void> {
await page.keyboard.press(direction === "next" ? "Alt+]" : "Alt+[");
await expectSidebarWorkspaceSelected({ page, serverId: getServerId(), workspaceId });
await expect(page).toHaveURL(new RegExp(`/workspace/${workspaceId}$`));
}
test("previous and next traverse every visible workspace while digit shortcuts stop at nine", async ({
page,
}) => {
test.setTimeout(120_000);
const project = await seedWorkspace({
repoPrefix: "relative-navigation-",
title: "Workspace 01",
});
try {
for (let index = 2; index <= 12; index += 1) {
const created = await project.client.createWorkspace({
source: { kind: "directory", path: project.repoPath },
title: `Workspace ${String(index).padStart(2, "0")}`,
});
expect(created.error).toBeNull();
}
await page.setViewportSize({ width: 1440, height: 1100 });
await gotoAppShell(page);
const order = await readVisibleWorkspaceOrder(page);
await test.step("next crosses the ninth workspace and previous returns to it", async () => {
await switchWorkspaceViaSidebar({ page, serverId: getServerId(), workspaceId: order[8] });
await navigateWorkspace(page, "next", order[9]);
await navigateWorkspace(page, "previous", order[8]);
});
await test.step("wrapping uses the true first and last visible workspace", async () => {
await switchWorkspaceViaSidebar({ page, serverId: getServerId(), workspaceId: order[0] });
await navigateWorkspace(page, "previous", order[11]);
await navigateWorkspace(page, "next", order[0]);
});
await test.step("the ninth digit shortcut still selects the ninth workspace", async () => {
await page.keyboard.press("Alt+9");
await expectSidebarWorkspaceSelected({
page,
serverId: getServerId(),
workspaceId: order[8],
});
});
} finally {
await project.cleanup();
}
});
@@ -5,6 +5,7 @@ import type {
SidebarWorkspaceEntry, SidebarWorkspaceEntry,
} from "@/hooks/use-sidebar-workspaces-list"; } from "@/hooks/use-sidebar-workspaces-list";
import { buildStatusGroups } from "@/hooks/sidebar-status-view-model"; import { buildStatusGroups } from "@/hooks/sidebar-status-view-model";
import { routeKeyboardShortcut } from "@/keyboard/route-shortcut";
import { import {
buildSidebarShortcutModel, buildSidebarShortcutModel,
@@ -65,6 +66,46 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
} }
describe("buildSidebarShortcutModel", () => { describe("buildSidebarShortcutModel", () => {
it.each([
[9, 1, 10],
[10, -1, 9],
[12, 1, 1],
[1, -1, 12],
] as const)(
"navigates from workspace %i by %i to workspace %i across the full visible sidebar",
(current, delta, next) => {
const workspaces = Array.from({ length: 12 }, (_, index) =>
workspace({
serverId: "s",
workspaceId: `ws-${index + 1}`,
workspaceDirectory: `/repo/w${index + 1}`,
name: `w${index + 1}`,
}),
);
const model = buildSidebarShortcutModel({
projects: [project("p", workspaces)],
collapsedProjectKeys: new Set<string>(),
});
expect(
routeKeyboardShortcut(
{ action: "workspace.navigate.relative", payload: { delta } },
{
pathname: `/h/s/workspace/ws-${current}`,
isMobile: false,
sidebarShortcutTargets: model.shortcutTargets,
navigationActiveWorkspace: null,
commandCenterOpen: false,
shortcutsDialogOpen: false,
},
),
).toEqual({ kind: "navigate-workspace", serverId: "s", workspaceId: `ws-${next}` });
expect(model.shortcutIndexByWorkspaceKey.size).toBe(9);
expect(model.shortcutIndexByWorkspaceKey.get("s:ws-9")).toBe(9);
expect(model.shortcutIndexByWorkspaceKey.has("s:ws-10")).toBe(false);
},
);
it("builds shortcut targets in visual order and excludes collapsed projects", () => { it("builds shortcut targets in visual order and excludes collapsed projects", () => {
const projects = [ const projects = [
project("p1", [ project("p1", [
@@ -111,7 +152,7 @@ describe("buildSidebarShortcutModel", () => {
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-repo2-main")).toBeUndefined(); expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-repo2-main")).toBeUndefined();
}); });
it("limits shortcuts to 9", () => { it("keeps all traversal targets while numbering only the first 9", () => {
const workspaces = Array.from({ length: 20 }, (_, index) => const workspaces = Array.from({ length: 20 }, (_, index) =>
workspace({ workspace({
serverId: "s", serverId: "s",
@@ -127,9 +168,11 @@ describe("buildSidebarShortcutModel", () => {
collapsedProjectKeys: new Set<string>(), collapsedProjectKeys: new Set<string>(),
}); });
expect(model.shortcutTargets).toHaveLength(9); expect(model.shortcutTargets).toHaveLength(20);
expect(model.shortcutTargets[0]).toEqual({ serverId: "s", workspaceId: "ws-1" });
expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "ws-9" }); expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "ws-9" });
expect(model.shortcutTargets[9]).toEqual({ serverId: "s", workspaceId: "ws-10" });
expect(model.shortcutIndexByWorkspaceKey.get("s:ws-9")).toBe(9);
expect(model.shortcutIndexByWorkspaceKey.get("s:ws-10")).toBeUndefined();
}); });
it("excludes a collapsed project's workspaces regardless of project kind", () => { it("excludes a collapsed project's workspaces regardless of project kind", () => {
+3 -6
View File
@@ -70,13 +70,10 @@ export function buildSidebarShortcutSections(input: {
} }
for (const workspace of section.workspaces) { for (const workspace of section.workspaces) {
if (shortcutTargets.length >= maxShortcuts) {
break;
}
const shortcutNumber = shortcutTargets.length + 1;
shortcutTargets.push(createShortcutTarget(workspace)); shortcutTargets.push(createShortcutTarget(workspace));
shortcutIndexByWorkspaceKey.set(workspace.workspaceKey, shortcutNumber); if (shortcutTargets.length <= maxShortcuts) {
shortcutIndexByWorkspaceKey.set(workspace.workspaceKey, shortcutTargets.length);
}
} }
} }