Files
paseo/packages/app/src/hooks/use-global-workspace-pin-action.ts
T
bb3f5c5a2d fix: prevent Shift+Tab from changing a backgrounded agent's mode (#1848)
* fix: prevent Shift+Tab from changing a backgrounded agent's mode

The New Workspace mode-cycle shortcut (Shift+Tab) could reach a
still-mounted background agent's mode control and silently change that
running agent's execution mode — including into a permissive/bypass
mode — with no feedback in the New Workspace UI.

Root cause is in useKeyboardActionHandler: it re-registered its handler
on every render (a fresh inline `actions` array plus a changing `handle`
identity). The dispatcher runs the most-recently-registered matching
handler first, so a frequently re-rendering control climbed ahead of the
active one and consumed the key. The registered `handle`/`enabled` were
also captured per registration and read stale by the native keydown
listener.

Fix: register once per (handlerId, priority, actions); read `handle` and
`isActive` live from a ref; fold `enabled` into a fresh `isActive` so
the dispatcher re-checks it at dispatch time. Registration order stays
stable and callbacks stay current for all six consumers of the hook.

Add an e2e regression test that opens a live agent, opens New
Workspace, and presses Shift+Tab. It asserts the running agent's mode
is unchanged: its daemon-committed mode, plus no set_agent_mode_request
on the wire. It fails on the previous code (the mode was flipped to a
more permissive mode) and passes with this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): restore workspace pin shortcut build

The shortcut change landed with an import path removed by the workspace tabs reshape, breaking app typecheck and web builds. Import the canonical workspace tab model.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-24 14:08:25 +00:00

65 lines
2.8 KiB
TypeScript

import { useCallback } from "react";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import { useSidebarWorkspacePinController } from "@/hooks/use-sidebar-workspace-pin";
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
import { useHostFeature } from "@/runtime/host-features";
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
const WORKSPACE_PIN_ACTIONS: readonly KeyboardActionId[] = ["workspace.pin"];
// The pin shortcut used to live on the sidebar row, so it disappeared whenever the row was not
// rendered — a collapsed project or status group, a collapsed Pinned section, or focus mode.
// It belongs here instead: one registration keyed on the active route selection.
//
// "Active workspace" means the route selection, not a focused pane. Those are equivalent today
// (panes belong to the routed workspace, and /settings parses to no selection, which correctly
// disables the handler). Revisit this if panes ever span workspaces.
export function useGlobalWorkspacePinAction() {
const selection = useActiveWorkspaceSelection();
const serverId = selection?.serverId ?? null;
const routeWorkspaceId = selection?.workspaceId ?? null;
// Narrow projection so pin state changes don't re-render on every gitRuntime/diffStat tick.
// A null result means the workspace is gone, which `pinnedAt: null` alone could not express.
//
// `id` is projected rather than reusing the route id: the route carries an opaque workspace id
// that is not guaranteed to equal the descriptor id (that is why `selectWorkspace` resolves it
// through `resolveWorkspaceMapKeyByIdentity`). The RPC and the in-flight key both need the
// descriptor id, so that sidebar rows and this handler agree on one identity.
const fields = useWorkspaceFields(serverId, routeWorkspaceId, (workspace) => ({
id: workspace.id,
pinnedAt: workspace.pinnedAt ?? null,
}));
const canPin = useHostFeature(serverId, "workspacePinning");
const togglePin = useSidebarWorkspacePinController();
const handle = useCallback(() => {
if (!serverId || !fields || !canPin) {
return false;
}
const workspaceKey = buildWorkspaceTabPersistenceKey({
serverId,
workspaceId: fields.id,
});
if (!workspaceKey) {
return false;
}
togglePin({
serverId,
workspaceId: fields.id,
workspaceKey,
pinnedAt: fields.pinnedAt,
});
return true;
}, [canPin, fields, serverId, togglePin]);
useKeyboardActionHandler({
handlerId: "workspace-pin-global",
actions: WORKSPACE_PIN_ACTIONS,
enabled: serverId !== null && fields !== null && canPin,
priority: 0,
handle,
});
}