feat: add workspace and tab navigation keyboard shortcuts
This commit is contained in:
@@ -92,7 +92,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
actionsReservedWidth: Math.max(0, tabsActionsWidth),
|
||||
rowPaddingHorizontal: theme.spacing[2],
|
||||
tabGap: theme.spacing[1],
|
||||
maxTabWidth: 150,
|
||||
maxTabWidth: 200,
|
||||
tabIconWidth: 14,
|
||||
tabHorizontalPadding: theme.spacing[3],
|
||||
estimatedCharWidth: 7,
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
useWorkspaceTabsStore,
|
||||
} from "@/stores/workspace-tabs-store";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import {
|
||||
buildWorkspaceOpenIntentParam,
|
||||
type WorkspaceOpenIntent,
|
||||
@@ -408,6 +409,12 @@ function WorkspaceScreenContent({
|
||||
const closeWorkspaceTab = useWorkspaceTabsStore((state) => state.closeTab);
|
||||
const promoteDraftToAgent = useWorkspaceTabsStore((state) => state.promoteDraftToAgent);
|
||||
const reorderWorkspaceTabs = useWorkspaceTabsStore((state) => state.reorderTabs);
|
||||
const workspaceTabActionRequest = useKeyboardShortcutsStore(
|
||||
(state) => state.workspaceTabActionRequest
|
||||
);
|
||||
const clearWorkspaceTabActionRequest = useKeyboardShortcutsStore(
|
||||
(state) => state.clearWorkspaceTabActionRequest
|
||||
);
|
||||
const consumedOpenIntentsRef = useRef(new Set<string>());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -943,6 +950,62 @@ function WorkspaceScreenContent({
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceTabActionRequest) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
workspaceTabActionRequest.serverId !== normalizedServerId ||
|
||||
workspaceTabActionRequest.workspaceId !== normalizedWorkspaceId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceTabActionRequest.kind === "new") {
|
||||
handleCreateDraftTab();
|
||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
||||
return;
|
||||
}
|
||||
if (workspaceTabActionRequest.kind === "close-current") {
|
||||
if (activeTabId) {
|
||||
void handleCloseTabById(activeTabId);
|
||||
}
|
||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
||||
return;
|
||||
}
|
||||
if (workspaceTabActionRequest.kind === "navigate-index") {
|
||||
const next = tabs[workspaceTabActionRequest.index - 1] ?? null;
|
||||
if (next?.tabId) {
|
||||
navigateToTabId(next.tabId);
|
||||
}
|
||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
||||
return;
|
||||
}
|
||||
if (workspaceTabActionRequest.kind === "navigate-relative") {
|
||||
if (tabs.length > 0) {
|
||||
const currentIndex = tabs.findIndex((tab) => tab.tabId === activeTabId);
|
||||
const fromIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
const nextIndex =
|
||||
(fromIndex + workspaceTabActionRequest.delta + tabs.length) % tabs.length;
|
||||
const next = tabs[nextIndex] ?? null;
|
||||
if (next?.tabId) {
|
||||
navigateToTabId(next.tabId);
|
||||
}
|
||||
}
|
||||
clearWorkspaceTabActionRequest(workspaceTabActionRequest.id);
|
||||
}
|
||||
}, [
|
||||
activeTabId,
|
||||
clearWorkspaceTabActionRequest,
|
||||
handleCloseTabById,
|
||||
handleCreateDraftTab,
|
||||
navigateToTabId,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
tabs,
|
||||
workspaceTabActionRequest,
|
||||
]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (
|
||||
shouldRenderMissingWorkspaceDescriptor({
|
||||
|
||||
@@ -6,7 +6,7 @@ const metrics = {
|
||||
actionsReservedWidth: 120,
|
||||
rowPaddingHorizontal: 8,
|
||||
tabGap: 4,
|
||||
maxTabWidth: 150,
|
||||
maxTabWidth: 200,
|
||||
tabIconWidth: 14,
|
||||
tabHorizontalPadding: 12,
|
||||
estimatedCharWidth: 7,
|
||||
@@ -14,7 +14,7 @@ const metrics = {
|
||||
};
|
||||
|
||||
describe("computeWorkspaceTabLayout", () => {
|
||||
it("caps tab width to the fixed maximum when enough space is available", () => {
|
||||
it("keeps fit-content tab widths when there is extra horizontal space", () => {
|
||||
const result = computeWorkspaceTabLayout({
|
||||
viewportWidth: 1200,
|
||||
tabLabelLengths: [8, 10, 7],
|
||||
@@ -25,7 +25,7 @@ describe("computeWorkspaceTabLayout", () => {
|
||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||
expect(result.items).toHaveLength(3);
|
||||
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
||||
expect(Math.max(...result.items.map((item) => item.width))).toBeLessThanOrEqual(150);
|
||||
expect(result.items.map((item) => item.width)).toEqual([126, 140, 119]);
|
||||
});
|
||||
|
||||
it("shrinks tab widths proportionally before hiding labels", () => {
|
||||
@@ -37,7 +37,7 @@ describe("computeWorkspaceTabLayout", () => {
|
||||
|
||||
expect(result.closeButtonPolicy).toBe("all");
|
||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||
expect(result.items.map((item) => item.width)).toEqual([137, 132, 108]);
|
||||
expect(result.items.map((item) => item.width)).toEqual([151, 121, 103]);
|
||||
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -67,10 +67,11 @@ export function computeWorkspaceTabLayout(
|
||||
const availableTabsWidth = Math.max(0, availableWidth - rowOverhead);
|
||||
const iconOnlyTabWidth =
|
||||
input.metrics.tabIconWidth + input.metrics.tabHorizontalPadding * 2 + input.metrics.closeButtonWidth;
|
||||
const labelSafetyPadding = 10;
|
||||
const estimateLabelWidth = (labelLength: number) => labelLength * input.metrics.estimatedCharWidth;
|
||||
const desiredTabWidths = input.tabLabelLengths.map((rawLength) => {
|
||||
const labelLength = Math.max(rawLength, 1);
|
||||
const estimatedWidth = iconOnlyTabWidth + estimateLabelWidth(labelLength);
|
||||
const estimatedWidth = iconOnlyTabWidth + estimateLabelWidth(labelLength) + labelSafetyPadding;
|
||||
return clamp(estimatedWidth, iconOnlyTabWidth, input.metrics.maxTabWidth);
|
||||
});
|
||||
|
||||
@@ -80,6 +81,7 @@ export function computeWorkspaceTabLayout(
|
||||
|
||||
let resolvedWidths: number[];
|
||||
if (desiredTotalTabsWidth <= availableTabsWidth) {
|
||||
// Fit-content baseline: do not stretch tabs when there is spare space.
|
||||
resolvedWidths = desiredTabWidths;
|
||||
} else if (availableTabsWidth <= iconOnlyTotalTabsWidth) {
|
||||
resolvedWidths = new Array(tabCount).fill(iconOnlyTabWidth);
|
||||
|
||||
Reference in New Issue
Block a user