Cmd+Shift+P did nothing whenever the active workspace's sidebar row was not rendered. The `workspace.pin` handler was registered by the row itself, gated on `selected && canPin`, so collapsing the row's section unmounted the only handler for the action and the dispatcher found zero candidates. The keypress was swallowed with no toast and no error. Move the action to a single always-mounted handler keyed on the active route selection, following the existing `useGlobalNewWorkspaceAction` / `useActiveWorktreeNewAction` pattern, and delete the two per-row registrations. Besides the reported case this also fixes a collapsed status group, a collapsed Pinned section (so unpinning works too), and focus mode. The handler lives in a headless component rather than being called from the root layout, so subscribing to the active workspace's pin state does not re-render the whole app shell. Two supporting changes: - The controller now takes a narrow `PinnableWorkspace` instead of a full `SidebarWorkspaceEntry`, so a caller without a sidebar row can build one. Its in-flight guard moves to module scope, because the row menus and the shortcut hold separate controller instances and a per-instance guard would let a keypress and a menu click fire two concurrent, opposite RPCs. - The handler resolves the descriptor id via `useWorkspaceFields` rather than reusing the route id. The route carries an opaque workspace id that is not guaranteed to equal the descriptor id, which is why `selectWorkspace` resolves it through `resolveWorkspaceMapKeyByIdentity`. Both the RPC and the in-flight key need the descriptor id so that rows and this handler agree on one identity. `workspace.archive` has the same row-scoped defect and is deliberately left alone: it carries per-row state (`isArchiving`, optimistic hiding, the risky-worktree confirm) and needs its own change. Covered by a Playwright spec: the three collapse cases fail without this fix, plus one-RPC-per-press and a rejected-pin case that asserts the error toast and that the next press still succeeds. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
61 lines
2.2 KiB
TypeScript
61 lines
2.2 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { useToast } from "@/contexts/toast-context";
|
|
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
|
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
|
|
|
// Everything the pin toggle actually needs. Kept narrower than SidebarWorkspaceEntry so the
|
|
// global keyboard handler can build one from the active route selection without a sidebar row.
|
|
export type PinnableWorkspace = Pick<
|
|
SidebarWorkspaceEntry,
|
|
"serverId" | "workspaceId" | "workspaceKey" | "pinnedAt"
|
|
>;
|
|
|
|
export type ToggleSidebarWorkspacePin = (workspace: PinnableWorkspace) => void;
|
|
|
|
// Module scope, not a per-hook ref: the sidebar row menus and the global keyboard shortcut each
|
|
// hold their own controller instance, and a per-instance guard would let a keypress and a menu
|
|
// click fire two concurrent, opposite setWorkspacePinned calls for the same workspace.
|
|
const pendingWorkspaceKeys = new Set<string>();
|
|
|
|
export function useSidebarWorkspacePinController(): ToggleSidebarWorkspacePin {
|
|
const { t } = useTranslation();
|
|
const toast = useToast();
|
|
const mutation = useMutation({
|
|
mutationFn: async ({
|
|
workspace,
|
|
pinned,
|
|
}: {
|
|
workspace: PinnableWorkspace;
|
|
pinned: boolean;
|
|
}) => {
|
|
const client = getHostRuntimeStore().getClient(workspace.serverId);
|
|
if (!client) {
|
|
throw new Error(t("sidebar.workspace.toasts.hostDisconnected"));
|
|
}
|
|
await client.setWorkspacePinned(workspace.workspaceId, pinned);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(
|
|
error instanceof Error ? error.message : t("sidebar.workspace.toasts.hostDisconnected"),
|
|
);
|
|
},
|
|
onSettled: (_data, _error, { workspace }) => {
|
|
pendingWorkspaceKeys.delete(workspace.workspaceKey);
|
|
},
|
|
});
|
|
const mutate = mutation.mutate;
|
|
|
|
return useCallback(
|
|
(workspace: PinnableWorkspace) => {
|
|
if (pendingWorkspaceKeys.has(workspace.workspaceKey)) {
|
|
return;
|
|
}
|
|
pendingWorkspaceKeys.add(workspace.workspaceKey);
|
|
mutate({ workspace, pinned: workspace.pinnedAt == null });
|
|
},
|
|
[mutate],
|
|
);
|
|
}
|