Make help easier to find and cloned workspaces open reliably (#2045)

* feat(app): add sidebar help and support menu

Give users a single place to run diagnostics, find shortcuts, and reach the support channels. Share one global diagnostic sheet between Settings and the sidebar so both entry points collect the same report.

* fix(app): keep cloned workspace navigation atomic

A newly merged clone flow still used the previous navigation signature and opened its draft tab separately. Route the cloned workspace and its draft target through the unified navigation command so attention selection cannot replace the intended tab.

* test(app): harden sidebar help coverage

Accept both the Discord invite URL and its canonical redirect, assert placement through real element geometry, and remove an unnecessary diagnostic-host callback.

* test(app): cover support redirect variants

Keep the help-menu E2E stable across prerelease versions and the valid redirect forms returned by Discord and GitHub.
This commit is contained in:
Mohamed Boudra
2026-07-14 00:02:53 +08:00
committed by GitHub
parent f2ebac931c
commit 5e2a8b6633
18 changed files with 427 additions and 68 deletions
+8 -4
View File
@@ -6,6 +6,8 @@ import {
type EmptyProjectDescriptor as ProjectWithoutWorkspacesDescriptor,
type WorkspaceDescriptor,
} from "@/stores/session-store";
import { generateDraftId } from "@/stores/draft-keys";
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
type OpenProjectPayload = ProjectAddResponse["payload"];
@@ -57,8 +59,7 @@ interface WorkspaceOpenCallbacks {
isConnected: boolean;
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
openDraftTab: (workspaceKey: string) => string | null;
navigateToWorkspace: (serverId: string, workspaceId: string) => void;
navigateToWorkspace: (input: NavigateToWorkspaceInput) => string;
}
export interface OpenGithubRepoDirectlyInput extends WorkspaceOpenCallbacks {
@@ -122,8 +123,11 @@ function finishWorkspaceOpen(
input.mergeWorkspaces(normalizedServerId, [workspace]);
input.setHasHydratedWorkspaces(normalizedServerId, true);
input.openDraftTab(workspaceKey);
input.navigateToWorkspace(normalizedServerId, workspace.id);
input.navigateToWorkspace({
serverId: normalizedServerId,
workspaceId: workspace.id,
target: { kind: "draft", draftId: generateDraftId() },
});
return true;
}
+12 -30
View File
@@ -8,6 +8,7 @@ import type {
EmptyProjectDescriptor as ProjectWithoutWorkspacesDescriptor,
WorkspaceDescriptor,
} from "@/stores/session-store";
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
const SERVER_ID = "server-1";
const PROJECT_PATH = "/repo/project";
@@ -55,15 +56,6 @@ interface RecordedHydrated {
hydrated: boolean;
}
interface RecordedOpenDraftTab {
workspaceKey: string;
}
interface RecordedNavigate {
serverId: string;
workspaceId: string;
}
interface RecordedClone {
repo: string;
targetDirectory: string;
@@ -90,23 +82,13 @@ function createFakeSession() {
};
}
function createFakeWorkspaceLayout() {
const openedTabs: RecordedOpenDraftTab[] = [];
return {
openedTabs,
openDraftTab: (workspaceKey: string) => {
openedTabs.push({ workspaceKey });
return "tab-1";
},
};
}
function createFakeNavigator() {
const navigations: RecordedNavigate[] = [];
const navigations: NavigateToWorkspaceInput[] = [];
return {
navigations,
navigateToWorkspace: (serverId: string, workspaceId: string) => {
navigations.push({ serverId, workspaceId });
navigateToWorkspace: (input: NavigateToWorkspaceInput) => {
navigations.push(input);
return `/hosts/${input.serverId}/workspaces/${input.workspaceId}`;
},
};
}
@@ -226,7 +208,6 @@ describe("openProjectDirectly", () => {
describe("openGithubRepoDirectly", () => {
it("opens a cloned GitHub workspace and seeds a draft tab", async () => {
const session = createFakeSession();
const layout = createFakeWorkspaceLayout();
const navigator = createFakeNavigator();
const workspacePayload = buildWorkspacePayload();
const github = createFakeGithubCloneClient(workspacePayload);
@@ -240,7 +221,6 @@ describe("openGithubRepoDirectly", () => {
client: github,
mergeWorkspaces: session.mergeWorkspaces,
setHasHydratedWorkspaces: session.setHasHydratedWorkspaces,
openDraftTab: layout.openDraftTab,
navigateToWorkspace: navigator.navigateToWorkspace,
});
@@ -261,13 +241,17 @@ describe("openGithubRepoDirectly", () => {
workspaceDirectory: PROJECT_PATH,
});
expect(session.hydrated).toEqual([{ serverId: SERVER_ID, hydrated: true }]);
expect(layout.openedTabs).toEqual([{ workspaceKey: `${SERVER_ID}:1` }]);
expect(navigator.navigations).toEqual([{ serverId: SERVER_ID, workspaceId: "1" }]);
expect(navigator.navigations).toEqual([
{
serverId: SERVER_ID,
workspaceId: "1",
target: { kind: "draft", draftId: expect.any(String) },
},
]);
});
it("rejects a workspace without an identity before changing app state", async () => {
const session = createFakeSession();
const layout = createFakeWorkspaceLayout();
const navigator = createFakeNavigator();
const github = createFakeGithubCloneClient({ ...buildWorkspacePayload(), id: " " });
@@ -280,14 +264,12 @@ describe("openGithubRepoDirectly", () => {
client: github,
mergeWorkspaces: session.mergeWorkspaces,
setHasHydratedWorkspaces: session.setHasHydratedWorkspaces,
openDraftTab: layout.openDraftTab,
navigateToWorkspace: navigator.navigateToWorkspace,
});
expect(result).toBe(false);
expect(session.merges).toEqual([]);
expect(session.hydrated).toEqual([]);
expect(layout.openedTabs).toEqual([]);
expect(navigator.navigations).toEqual([]);
});
});
+1 -17
View File
@@ -1,8 +1,6 @@
import { useCallback } from "react";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
import { generateDraftId } from "@/stores/draft-keys";
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
import {
openGithubRepoDirectly,
@@ -61,12 +59,6 @@ export function useOpenGithubRepo(
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces);
const openDraftTab = useCallback((workspaceKey: string) => {
return useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, {
kind: "draft",
draftId: generateDraftId(),
});
}, []);
return useCallback(
async (repo: string, targetDirectory: string, cloneProtocol?: WorkspaceGithubCloneProtocol) => {
@@ -79,17 +71,9 @@ export function useOpenGithubRepo(
client,
mergeWorkspaces,
setHasHydratedWorkspaces,
openDraftTab,
navigateToWorkspace,
});
},
[
client,
isConnected,
mergeWorkspaces,
normalizedServerId,
openDraftTab,
setHasHydratedWorkspaces,
],
[client, isConnected, mergeWorkspaces, normalizedServerId, setHasHydratedWorkspaces],
);
}