feat: open Changes as a workspace tab (#2298)
Open the complete working comparison in a desktop workspace tab while preserving inline sidebar diffs when the tab is closed. Working and commit comparisons share one diff panel, and workspace layout is the sole owner of live tab persistence and identity. Refs #1520
This commit is contained in:
@@ -78,7 +78,7 @@ import type { MessagePayload } from "@/composer/types";
|
||||
import type { AgentAttachment, ForgeSearchItem } from "@getpaseo/protocol/messages";
|
||||
import type { CreatePaseoWorktreeInput } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||
import type { WorkspaceDraftTabSetup, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceDraftTabSetup, WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||
import { isEmptyWorkspaceSubmission, runCreateEmptyWorkspace } from "./new-workspace-empty";
|
||||
import {
|
||||
getWorkspaceNamingAttachments,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type { WorkspaceLayout } from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
import { selectVisibleAgentIds } from "./visible-agent-ids";
|
||||
|
||||
test("selects only the active agent tab in every visible pane", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { collectAllPanes, type WorkspaceLayout } from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
import { deriveWorkspacePaneState } from "./workspace-pane-state";
|
||||
|
||||
export function selectVisibleAgentIds(input: {
|
||||
|
||||
@@ -444,7 +444,7 @@ interface WorkspaceDesktopTabsRowProps {
|
||||
|
||||
function getFallbackTabLabel(
|
||||
tab: WorkspaceTabDescriptor,
|
||||
labels: { newAgent: string; setup: string; terminal: string; agent: string },
|
||||
labels: { newAgent: string; setup: string; terminal: string; agent: string; changes: string },
|
||||
): string {
|
||||
if (tab.target.kind === "draft") {
|
||||
return labels.newAgent;
|
||||
@@ -458,6 +458,9 @@ function getFallbackTabLabel(
|
||||
if (tab.target.kind === "file") {
|
||||
return tab.target.path.split("/").findLast(Boolean) ?? tab.target.path;
|
||||
}
|
||||
if (tab.target.kind === "working_diff") {
|
||||
return labels.changes;
|
||||
}
|
||||
return labels.agent;
|
||||
}
|
||||
|
||||
@@ -836,6 +839,7 @@ export function WorkspaceDesktopTabsRow({
|
||||
setup: t("workspace.tabs.fallback.setup"),
|
||||
terminal: t("workspace.tabs.fallback.terminal"),
|
||||
agent: t("workspace.tabs.fallback.agent"),
|
||||
changes: t("panels.diff.changesLabel"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
createWorkspaceFileTabTarget,
|
||||
normalizeWorkspaceFileLocation,
|
||||
} from "@/workspace/file-open";
|
||||
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||
|
||||
interface OpenWorkspaceFileFromExplorerInput {
|
||||
filePath: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
resolveSideFileOpenPlacement,
|
||||
} from "@/screens/workspace/workspace-pane-state";
|
||||
import type { WorkspaceLayout } from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
|
||||
function createTab(tabId: string, target: WorkspaceTab["target"]): WorkspaceTab {
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type SplitPane,
|
||||
type WorkspaceLayout,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTab, WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
import {
|
||||
buildDeterministicWorkspaceTabId,
|
||||
|
||||
@@ -74,14 +74,17 @@ import { selectIsFileExplorerOpen, usePanelStore } from "@/stores/panel-store";
|
||||
import { type ExplorerCheckoutContext } from "@/stores/explorer-checkout-context";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
collectAllTabs,
|
||||
getFocusedBrowserId,
|
||||
type WorkspaceLayout,
|
||||
useWorkspaceLayoutStore,
|
||||
useWorkspaceLayoutStoreHydrated,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
type WorkspaceTab,
|
||||
type WorkspaceTabTarget,
|
||||
} from "@/workspace-tabs/model";
|
||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
@@ -345,6 +348,7 @@ function getFallbackTabOptionLabel(
|
||||
terminal: string;
|
||||
browser: string;
|
||||
agent: string;
|
||||
changes: string;
|
||||
},
|
||||
): string {
|
||||
if (tab.target.kind === "draft") {
|
||||
@@ -362,6 +366,9 @@ function getFallbackTabOptionLabel(
|
||||
if (tab.target.kind === "file") {
|
||||
return tab.target.path.split("/").findLast(Boolean) ?? tab.target.path;
|
||||
}
|
||||
if (tab.target.kind === "working_diff") {
|
||||
return labels.changes;
|
||||
}
|
||||
if (tab.target.kind === "commit_diff") {
|
||||
return tab.target.sha.slice(0, 7);
|
||||
}
|
||||
@@ -376,6 +383,7 @@ function getFallbackTabOptionDescription(
|
||||
agent: string;
|
||||
terminal: string;
|
||||
browser: string;
|
||||
changes: string;
|
||||
},
|
||||
): string {
|
||||
if (tab.target.kind === "draft") {
|
||||
@@ -399,6 +407,9 @@ function getFallbackTabOptionDescription(
|
||||
if (tab.target.kind === "commit_diff") {
|
||||
return tab.target.sha.slice(0, 7);
|
||||
}
|
||||
if (tab.target.kind === "working_diff") {
|
||||
return labels.changes;
|
||||
}
|
||||
return tab.target.path;
|
||||
}
|
||||
|
||||
@@ -671,6 +682,7 @@ function MobileWorkspaceTabOption({
|
||||
terminal: t("workspace.tabs.fallback.terminal"),
|
||||
browser: t("workspace.tabs.fallback.browser"),
|
||||
agent: t("workspace.tabs.fallback.agent"),
|
||||
changes: t("panels.diff.changesLabel"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
@@ -2514,6 +2526,7 @@ function WorkspaceScreenContent({
|
||||
terminal: t("workspace.tabs.fallback.terminal"),
|
||||
browser: t("workspace.tabs.fallback.browser"),
|
||||
agent: t("workspace.tabs.fallback.agent"),
|
||||
changes: t("panels.diff.changesLabel"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
|
||||
import {
|
||||
buildWorkspaceDesktopTabActions,
|
||||
buildWorkspaceTabMenuEntries,
|
||||
} from "@/screens/workspace/workspace-tab-menu";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
|
||||
function createAgentTab(): WorkspaceTabDescriptor {
|
||||
@@ -253,6 +256,37 @@ describe("buildWorkspaceTabMenuEntries", () => {
|
||||
expect(onCopyFilePath).toHaveBeenCalledWith("/some/path.ts");
|
||||
});
|
||||
|
||||
it("uses a Changes close id for the working diff tab", () => {
|
||||
const actions = buildWorkspaceDesktopTabActions({
|
||||
tab: {
|
||||
key: "working_diff_abc",
|
||||
tabId: "working_diff_abc",
|
||||
kind: "working_diff",
|
||||
target: {
|
||||
kind: "working_diff",
|
||||
focusPath: "src/example.ts",
|
||||
focusRequestId: 1,
|
||||
},
|
||||
},
|
||||
index: 0,
|
||||
tabCount: 1,
|
||||
onCopyResumeCommand: vi.fn(),
|
||||
onCopyAgentId: vi.fn(),
|
||||
onCopyFilePath: vi.fn(),
|
||||
onReloadAgent: vi.fn(),
|
||||
onRenameTab: vi.fn(),
|
||||
onCloseTab: vi.fn(),
|
||||
onCloseTabsToLeft: vi.fn(),
|
||||
onCloseTabsToRight: vi.fn(),
|
||||
onCloseOtherTabs: vi.fn(),
|
||||
});
|
||||
|
||||
expect(actions.closeButtonTestId).toMatch(/^workspace-working-diff-close-/);
|
||||
expect(actions.menuEntries).not.toContainEqual(
|
||||
expect.objectContaining({ kind: "item", key: "copy-file-path" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the same rename entry shape for agent and terminal tabs", () => {
|
||||
const terminalTab: WorkspaceTabDescriptor = {
|
||||
key: "terminal_abc",
|
||||
|
||||
@@ -144,6 +144,9 @@ function getCloseButtonTestId(tab: WorkspaceTabDescriptor): string {
|
||||
if (tab.target.kind === "commit_diff") {
|
||||
return `workspace-commit-diff-close-${encodeFilePathForPathSegment(tab.target.sha)}`;
|
||||
}
|
||||
if (tab.target.kind === "working_diff") {
|
||||
return `workspace-working-diff-close-${encodeFilePathForPathSegment(buildDeterministicWorkspaceTabId(tab.target))}`;
|
||||
}
|
||||
return `workspace-file-close-${encodeFilePathForPathSegment(tab.target.path)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { deriveWorkspaceTabModel } from "@/screens/workspace/workspace-tab-model";
|
||||
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
|
||||
describe("deriveWorkspaceTabModel", () => {
|
||||
it("keeps normalized tabs in stored order and preserves targets", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTab, WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||
import {
|
||||
deriveWorkspacePaneState,
|
||||
type WorkspaceDerivedTab,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import type { WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||
|
||||
export interface WorkspaceTabDescriptor {
|
||||
key: string;
|
||||
|
||||
Reference in New Issue
Block a user