feat(app): polish workspace tabs and changes
Size tabs from rendered labels and switch to horizontal overflow once minimum click targets are reached, keeping the add-tab action adjacent until it must remain fixed. Make Changes an always-expanded document with an optional desktop navigation tree, keep Git actions in their platform-owned headers, remove pinned and inline split shortcuts, and place review tabs beside Changes.
This commit is contained in:
@@ -304,7 +304,7 @@ test("changes context menus duplicate files and folders", async ({ page }) => {
|
|||||||
.poll(() => readFileIfPresent(path.join(workspace.repoPath, "src/use-mounted-tab-set copy.ts")))
|
.poll(() => readFileIfPresent(path.join(workspace.repoPath, "src/use-mounted-tab-set copy.ts")))
|
||||||
.toBe(AFTER);
|
.toBe(AFTER);
|
||||||
|
|
||||||
await page.getByTestId("changes-toggle-view-mode").click();
|
await page.getByTestId("changes-toggle-tree").click();
|
||||||
await changesTree(page).getByTestId("diff-folder-src-toggle").click({ button: "right" });
|
await changesTree(page).getByTestId("diff-folder-src-toggle").click({ button: "right" });
|
||||||
await page.getByTestId("diff-folder-src-duplicate").click();
|
await page.getByTestId("diff-folder-src-duplicate").click();
|
||||||
await expect
|
await expect
|
||||||
@@ -317,7 +317,7 @@ test("changes context menu recursively collapses descendant folders", async ({ p
|
|||||||
await useUnwrappedDiffLines(page);
|
await useUnwrappedDiffLines(page);
|
||||||
await openWorkspaceChanges(page, workspace);
|
await openWorkspaceChanges(page, workspace);
|
||||||
|
|
||||||
await page.getByTestId("changes-toggle-view-mode").click();
|
await page.getByTestId("changes-toggle-tree").click();
|
||||||
const tree = changesTree(page);
|
const tree = changesTree(page);
|
||||||
await expect(tree.getByTestId("diff-folder-src/zz-folder")).toBeVisible();
|
await expect(tree.getByTestId("diff-folder-src/zz-folder")).toBeVisible();
|
||||||
await expect(tree.getByTestId("diff-folder-src/zz-folder/nested")).toBeVisible();
|
await expect(tree.getByTestId("diff-folder-src/zz-folder/nested")).toBeVisible();
|
||||||
@@ -346,7 +346,7 @@ test("changes context menus expose folder revert and restore a file after confir
|
|||||||
await useUnwrappedDiffLines(page);
|
await useUnwrappedDiffLines(page);
|
||||||
await openWorkspaceChanges(page, workspace);
|
await openWorkspaceChanges(page, workspace);
|
||||||
|
|
||||||
await page.getByTestId("changes-toggle-view-mode").click();
|
await page.getByTestId("changes-toggle-tree").click();
|
||||||
const tree = changesTree(page);
|
const tree = changesTree(page);
|
||||||
await tree.getByTestId("diff-folder-src-toggle").click({ button: "right" });
|
await tree.getByTestId("diff-folder-src-toggle").click({ button: "right" });
|
||||||
const folderRevert = page.getByTestId("diff-folder-src-revert");
|
const folderRevert = page.getByTestId("diff-folder-src-revert");
|
||||||
@@ -357,7 +357,7 @@ test("changes context menus expose folder revert and restore a file after confir
|
|||||||
await expect(folderRevert.locator("svg")).toHaveCSS("stroke", revertLabelColor);
|
await expect(folderRevert.locator("svg")).toHaveCSS("stroke", revertLabelColor);
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
await tree.getByTestId("diff-file-0-toggle").click({ button: "right" });
|
await tree.getByTestId("diff-tree-file-0-toggle").click({ button: "right" });
|
||||||
const cancelledConfirmation = new Promise<string>((resolve) => {
|
const cancelledConfirmation = new Promise<string>((resolve) => {
|
||||||
page.once("dialog", async (dialog) => {
|
page.once("dialog", async (dialog) => {
|
||||||
const message = dialog.message();
|
const message = dialog.message();
|
||||||
@@ -365,14 +365,14 @@ test("changes context menus expose folder revert and restore a file after confir
|
|||||||
resolve(message);
|
resolve(message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await page.getByTestId("diff-file-0-revert").click();
|
await page.getByTestId("diff-tree-file-0-revert").click();
|
||||||
expect(await cancelledConfirmation).toContain("src/use-mounted-tab-set.ts");
|
expect(await cancelledConfirmation).toContain("src/use-mounted-tab-set.ts");
|
||||||
await expect(tree.getByTestId("diff-file-0")).toBeVisible();
|
await expect(tree.getByTestId("diff-tree-file-0")).toBeVisible();
|
||||||
await expect
|
await expect
|
||||||
.poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8"))
|
.poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8"))
|
||||||
.toBe(AFTER);
|
.toBe(AFTER);
|
||||||
|
|
||||||
await tree.getByTestId("diff-file-0-toggle").click({ button: "right" });
|
await tree.getByTestId("diff-tree-file-0-toggle").click({ button: "right" });
|
||||||
const confirmation = new Promise<string>((resolve) => {
|
const confirmation = new Promise<string>((resolve) => {
|
||||||
page.once("dialog", async (dialog) => {
|
page.once("dialog", async (dialog) => {
|
||||||
const message = dialog.message();
|
const message = dialog.message();
|
||||||
@@ -380,10 +380,10 @@ test("changes context menus expose folder revert and restore a file after confir
|
|||||||
resolve(message);
|
resolve(message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await page.getByTestId("diff-file-0-revert").click();
|
await page.getByTestId("diff-tree-file-0-revert").click();
|
||||||
expect(await confirmation).toContain("src/use-mounted-tab-set.ts");
|
expect(await confirmation).toContain("src/use-mounted-tab-set.ts");
|
||||||
|
|
||||||
await expect(tree.getByTestId("diff-file-0")).toHaveCount(0, { timeout: 30_000 });
|
await expect(tree.getByTestId("diff-tree-file-0")).toHaveCount(0, { timeout: 30_000 });
|
||||||
await expect
|
await expect
|
||||||
.poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8"))
|
.poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8"))
|
||||||
.toBe(BEFORE);
|
.toBe(BEFORE);
|
||||||
@@ -482,10 +482,11 @@ test("Changes keeps review navigation and controls inside its workspace tab", as
|
|||||||
await expect(visiblePanel.getByText("use-mounted-tab-set.ts", { exact: true })).toBeVisible();
|
await expect(visiblePanel.getByText("use-mounted-tab-set.ts", { exact: true })).toBeVisible();
|
||||||
await expect(visiblePanel).toContainText("zz-deleted.ts");
|
await expect(visiblePanel).toContainText("zz-deleted.ts");
|
||||||
await expect(visiblePanel.getByTestId("changes-open-tab")).toHaveCount(0);
|
await expect(visiblePanel.getByTestId("changes-open-tab")).toHaveCount(0);
|
||||||
|
await expect(visiblePanel.getByTestId("changes-primary-cta")).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("changes-primary-cta")).toHaveCount(1);
|
||||||
|
await expect(page.getByTestId("changes-primary-cta")).toContainText("Commit");
|
||||||
await expect(visiblePanel.getByTestId("diff-file-0-body")).toBeVisible();
|
await expect(visiblePanel.getByTestId("diff-file-0-body")).toBeVisible();
|
||||||
await visiblePanel.getByTestId("diff-file-0-toggle").click();
|
await visiblePanel.getByTestId("diff-file-0-toggle").click();
|
||||||
await expect(visiblePanel.getByTestId("diff-file-0-body")).toHaveCount(0);
|
|
||||||
await visiblePanel.getByTestId("diff-file-0-toggle").click();
|
|
||||||
await expect(visiblePanel.getByTestId("diff-file-0-body")).toBeVisible();
|
await expect(visiblePanel.getByTestId("diff-file-0-body")).toBeVisible();
|
||||||
const workingDiffLayoutToggle = visiblePanel.getByRole("button", {
|
const workingDiffLayoutToggle = visiblePanel.getByRole("button", {
|
||||||
name: "Switch to side-by-side diff",
|
name: "Switch to side-by-side diff",
|
||||||
@@ -501,10 +502,7 @@ test("Changes keeps review navigation and controls inside its workspace tab", as
|
|||||||
await visiblePanel.getByRole("button", { name: "Diff options" }).click();
|
await visiblePanel.getByRole("button", { name: "Diff options" }).click();
|
||||||
await expect(page.getByText("Scroll long lines", { exact: true })).toBeVisible();
|
await expect(page.getByText("Scroll long lines", { exact: true })).toBeVisible();
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
await page.getByRole("button", { name: "Expand all files" }).click();
|
await expect(page.getByRole("button", { name: /all files/i })).toHaveCount(0);
|
||||||
await expect(visiblePanel.getByTestId("diff-file-0-body")).toBeVisible();
|
|
||||||
await page.getByRole("button", { name: "Collapse all files" }).click();
|
|
||||||
await expect(visiblePanel.getByTestId(/^diff-file-\d+-body$/)).toHaveCount(0);
|
|
||||||
await expect(page.getByTestId(/^workspace-working-diff-close-/)).toHaveCount(1);
|
await expect(page.getByTestId(/^workspace-working-diff-close-/)).toHaveCount(1);
|
||||||
|
|
||||||
await writeFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), BEFORE);
|
await writeFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), BEFORE);
|
||||||
@@ -520,7 +518,9 @@ test("Changes keeps review navigation and controls inside its workspace tab", as
|
|||||||
await expect(visiblePanel.getByRole("img", { name: "Deleted" })).toBeVisible();
|
await expect(visiblePanel.getByRole("img", { name: "Deleted" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("changes diff switches between flat and tree file lists", async ({ page }) => {
|
test("desktop Changes toggles a navigation tree beside the expanded diff document", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
const workspace = await createWorkspaceWithMountedTabDiff();
|
const workspace = await createWorkspaceWithMountedTabDiff();
|
||||||
await useUnwrappedDiffLines(page);
|
await useUnwrappedDiffLines(page);
|
||||||
await openWorkspaceChanges(page, workspace);
|
await openWorkspaceChanges(page, workspace);
|
||||||
@@ -541,7 +541,7 @@ test("changes diff switches between flat and tree file lists", async ({ page })
|
|||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
await scrollToLowerUnwrappedDiffRows(page);
|
await scrollToLowerUnwrappedDiffRows(page);
|
||||||
await page.getByTestId("changes-toggle-view-mode").click();
|
await page.getByTestId("changes-toggle-tree").click();
|
||||||
const tree = changesTree(page);
|
const tree = changesTree(page);
|
||||||
const content = changesContent(page);
|
const content = changesContent(page);
|
||||||
await expect(tree.getByTestId("diff-folder-src")).toBeVisible();
|
await expect(tree.getByTestId("diff-folder-src")).toBeVisible();
|
||||||
@@ -549,9 +549,9 @@ test("changes diff switches between flat and tree file lists", async ({ page })
|
|||||||
"user-select",
|
"user-select",
|
||||||
"none",
|
"none",
|
||||||
);
|
);
|
||||||
await expect(tree.getByTestId("diff-file-0")).toBeVisible();
|
await expect(tree.getByTestId("diff-tree-file-0")).toBeVisible();
|
||||||
await expect(tree.getByTestId("diff-folder-src-toggle").locator("svg")).toHaveCount(1);
|
await expect(tree.getByTestId("diff-folder-src-toggle").locator("svg")).toHaveCount(1);
|
||||||
await expect(tree.getByTestId("diff-file-0-toggle").locator("svg")).toHaveCount(1);
|
await expect(tree.getByTestId("diff-tree-file-0-toggle").locator("svg")).toHaveCount(1);
|
||||||
const folderToggleBounds = await tree.getByTestId("diff-folder-src-toggle").boundingBox();
|
const folderToggleBounds = await tree.getByTestId("diff-folder-src-toggle").boundingBox();
|
||||||
const folderChevronBounds = await tree
|
const folderChevronBounds = await tree
|
||||||
.getByTestId("diff-folder-src-toggle")
|
.getByTestId("diff-folder-src-toggle")
|
||||||
@@ -568,7 +568,7 @@ test("changes diff switches between flat and tree file lists", async ({ page })
|
|||||||
.getByText("src", { exact: true })
|
.getByText("src", { exact: true })
|
||||||
.boundingBox();
|
.boundingBox();
|
||||||
const fileLabelBounds = await tree
|
const fileLabelBounds = await tree
|
||||||
.getByTestId("diff-file-0")
|
.getByTestId("diff-tree-file-0")
|
||||||
.getByText("use-mounted-tab-set.ts", { exact: true })
|
.getByText("use-mounted-tab-set.ts", { exact: true })
|
||||||
.boundingBox();
|
.boundingBox();
|
||||||
expect(folderLabelBounds).not.toBeNull();
|
expect(folderLabelBounds).not.toBeNull();
|
||||||
@@ -578,27 +578,31 @@ test("changes diff switches between flat and tree file lists", async ({ page })
|
|||||||
const folderToggle = tree.getByTestId("diff-folder-src-toggle");
|
const folderToggle = tree.getByTestId("diff-folder-src-toggle");
|
||||||
await folderToggle.click();
|
await folderToggle.click();
|
||||||
await expect(folderToggle).toHaveAttribute("aria-selected", "true");
|
await expect(folderToggle).toHaveAttribute("aria-selected", "true");
|
||||||
await expect(tree.getByTestId("diff-file-0")).toHaveCount(0);
|
await expect(tree.getByTestId("diff-tree-file-0")).toHaveCount(0);
|
||||||
await folderToggle.click();
|
await folderToggle.click();
|
||||||
await expect(folderToggle).toHaveAttribute("aria-selected", "true");
|
await expect(folderToggle).toHaveAttribute("aria-selected", "true");
|
||||||
await expect(tree.getByTestId("diff-file-0")).toBeVisible();
|
await expect(tree.getByTestId("diff-tree-file-0")).toBeVisible();
|
||||||
|
|
||||||
const fileToggle = tree.getByTestId("diff-file-0-toggle");
|
const fileToggle = tree.getByTestId("diff-tree-file-0-toggle");
|
||||||
await fileToggle.click({ button: "right" });
|
await fileToggle.click({ button: "right" });
|
||||||
await expect(fileToggle).toHaveAttribute("aria-selected", "true");
|
await expect(fileToggle).toHaveAttribute("aria-selected", "true");
|
||||||
await expect(folderToggle).toHaveAttribute("aria-selected", "false");
|
await expect(folderToggle).toHaveAttribute("aria-selected", "false");
|
||||||
await expect(page.getByTestId("diff-file-0-context-menu")).toBeVisible();
|
await expect(page.getByTestId("diff-tree-file-0-context-menu")).toBeVisible();
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Collapse all files" }).click();
|
await fileToggle.click();
|
||||||
await expect(content.getByTestId(/^diff-file-\d+-body$/)).toHaveCount(0);
|
|
||||||
await page.getByRole("button", { name: "Expand all files" }).click();
|
|
||||||
await expect(content.getByTestId("diff-file-0-body")).toBeVisible();
|
await expect(content.getByTestId("diff-file-0-body")).toBeVisible();
|
||||||
|
const contentBounds = await content.boundingBox();
|
||||||
|
const focusedFileBounds = await content.getByTestId("diff-file-0").boundingBox();
|
||||||
|
expect(contentBounds).not.toBeNull();
|
||||||
|
expect(focusedFileBounds).not.toBeNull();
|
||||||
|
expect(focusedFileBounds!.y).toBeGreaterThanOrEqual(contentBounds!.y);
|
||||||
|
expect(focusedFileBounds!.y).toBeLessThan(contentBounds!.y + 80);
|
||||||
|
|
||||||
await tree.getByTestId("diff-folder-src-toggle").click();
|
await tree.getByTestId("diff-folder-src-toggle").click();
|
||||||
await expect(tree.getByTestId("diff-file-0")).toHaveCount(0);
|
await expect(tree.getByTestId("diff-tree-file-0")).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByTestId("changes-toggle-view-mode").click();
|
await page.getByTestId("changes-toggle-tree").click();
|
||||||
await expectFlatFileList(page);
|
await expectFlatFileList(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -649,7 +653,7 @@ async function useUnwrappedDiffLines(page: Page): Promise<void> {
|
|||||||
preferencesKey,
|
preferencesKey,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
viewMode: "flat",
|
desktopTreeVisible: false,
|
||||||
wrapLines: false,
|
wrapLines: false,
|
||||||
hideWhitespace: false,
|
hideWhitespace: false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { test, expect } from "../support/fixtures";
|
|
||||||
import { gotoWorkspace } from "../support/helpers/launcher";
|
|
||||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
|
||||||
import { togglePinFromMenu, tabRowPin } from "../support/helpers/pins";
|
|
||||||
import { expectTerminalTabOpen } from "../support/helpers/workspace-tabs";
|
|
||||||
import type { PinnedTabTarget } from "../../src/workspace-pins/target";
|
|
||||||
|
|
||||||
const DRAFT_TARGET: PinnedTabTarget = { kind: "draft" };
|
|
||||||
const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" };
|
|
||||||
|
|
||||||
let workspace: SeededWorkspace;
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
workspace = await seedWorkspace({ repoPrefix: "workspace-pins-e2e-" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await workspace?.cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Pinned tab targets", () => {
|
|
||||||
test("pinning a target from the dropdown adds its quick-launch button to the tab row, unpinning removes it", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await gotoWorkspace(page, workspace.workspaceId);
|
|
||||||
|
|
||||||
await expect(tabRowPin(page, DRAFT_TARGET)).toHaveCount(0);
|
|
||||||
|
|
||||||
await togglePinFromMenu(page, DRAFT_TARGET);
|
|
||||||
await expect(tabRowPin(page, DRAFT_TARGET)).toBeVisible({ timeout: 10_000 });
|
|
||||||
|
|
||||||
await togglePinFromMenu(page, DRAFT_TARGET);
|
|
||||||
await expect(tabRowPin(page, DRAFT_TARGET)).toHaveCount(0, { timeout: 10_000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("clicking the pinned quick-launch button in the tab row opens a terminal tab", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
test.setTimeout(45_000);
|
|
||||||
await gotoWorkspace(page, workspace.workspaceId);
|
|
||||||
|
|
||||||
await expect(tabRowPin(page, TERMINAL_TARGET)).toBeVisible({ timeout: 10_000 });
|
|
||||||
await tabRowPin(page, TERMINAL_TARGET).click();
|
|
||||||
|
|
||||||
await expectTerminalTabOpen(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { expect, type Page } from "@playwright/test";
|
|
||||||
import type { PinnedTabTarget } from "../../../src/workspace-pins/target";
|
|
||||||
import { pinnedTargetKey } from "../../../src/workspace-pins/target";
|
|
||||||
|
|
||||||
// The new-tab dropdown menu items carry no stable web ARIA role for the inline
|
|
||||||
// pin toggle (it lives inside a reveal-on-hover slot), so the pins flow is
|
|
||||||
// addressed through the test ids the feature assigns per target key — the same
|
|
||||||
// escape-hatch convention the sidebar kebab and tab context menus use.
|
|
||||||
|
|
||||||
function pinToggle(page: Page, target: PinnedTabTarget) {
|
|
||||||
return page.getByTestId(`workspace-pin-toggle-${pinnedTargetKey(target)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function menuItemFor(page: Page, target: PinnedTabTarget) {
|
|
||||||
if (target.kind === "draft") {
|
|
||||||
return page.getByTestId("workspace-new-tab-menu-agent");
|
|
||||||
}
|
|
||||||
return page.getByTestId(`workspace-new-tab-menu-${target.kind}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tabRowPin(page: Page, target: PinnedTabTarget) {
|
|
||||||
return page.getByTestId(`workspace-pinned-target-${pinnedTargetKey(target)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openNewTabMenu(page: Page): Promise<void> {
|
|
||||||
const trigger = page
|
|
||||||
.getByTestId("workspace-new-tab-menu-trigger")
|
|
||||||
.filter({ visible: true })
|
|
||||||
.first();
|
|
||||||
await expect(trigger).toBeVisible({ timeout: 10_000 });
|
|
||||||
await trigger.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pin toggle is hidden behind reveal-on-hover (opacity + pointerEvents) on
|
|
||||||
// desktop web, so the menu item must be hovered before the toggle is clickable.
|
|
||||||
export async function togglePinFromMenu(page: Page, target: PinnedTabTarget): Promise<void> {
|
|
||||||
await openNewTabMenu(page);
|
|
||||||
const item = menuItemFor(page, target);
|
|
||||||
await expect(item).toBeVisible({ timeout: 10_000 });
|
|
||||||
await item.hover();
|
|
||||||
const toggle = pinToggle(page, target);
|
|
||||||
await expect(toggle).toBeVisible({ timeout: 10_000 });
|
|
||||||
await toggle.click();
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
}
|
|
||||||
@@ -94,7 +94,7 @@ export function BranchSwitcher({
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={t("branchSwitcher.currentBranch", { branchName: currentBranchName })}
|
accessibilityLabel={t("branchSwitcher.currentBranch", { branchName: currentBranchName })}
|
||||||
>
|
>
|
||||||
<ThemedGitBranch size={14} uniProps={foregroundMutedIconColorMapping} />
|
<ThemedGitBranch size={12} uniProps={foregroundMutedIconColorMapping} />
|
||||||
<Text style={styles.branchLabel} numberOfLines={1}>
|
<Text style={styles.branchLabel} numberOfLines={1}>
|
||||||
{currentBranchName}
|
{currentBranchName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -132,8 +132,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[1],
|
gap: theme.spacing[1],
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
paddingVertical: theme.spacing[1],
|
paddingVertical: theme.spacing[1],
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[1],
|
||||||
marginLeft: -theme.spacing[2],
|
|
||||||
borderRadius: theme.borderRadius.md,
|
borderRadius: theme.borderRadius.md,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
@@ -141,9 +140,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
backgroundColor: theme.colors.surface1,
|
backgroundColor: theme.colors.surface1,
|
||||||
},
|
},
|
||||||
branchLabel: {
|
branchLabel: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.xs,
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foregroundMuted,
|
||||||
fontWeight: theme.fontWeight.medium,
|
fontWeight: theme.fontWeight.normal,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { usePanelStore, selectIsFileExplorerOpen, type ExplorerTab } from "@/sto
|
|||||||
import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures";
|
import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures";
|
||||||
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
||||||
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
||||||
import { GitDiffPane } from "@/git/diff-pane";
|
import { ChangesSurface } from "@/git/diff-pane";
|
||||||
import { FileExplorerPane } from "./file-explorer-pane";
|
import { FileExplorerPane } from "./file-explorer-pane";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window";
|
import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window";
|
||||||
@@ -433,7 +433,7 @@ function ChangedFilesPane({
|
|||||||
>) {
|
>) {
|
||||||
const { addFile, canAddToChat } = useAddFileToChat({ serverId, workspaceId });
|
const { addFile, canAddToChat } = useAddFileToChat({ serverId, workspaceId });
|
||||||
return (
|
return (
|
||||||
<GitDiffPane
|
<ChangesSurface
|
||||||
host="explorer"
|
host="explorer"
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
|||||||
@@ -1007,7 +1007,7 @@ function SplitPaneView({
|
|||||||
buildPaneContentModel,
|
buildPaneContentModel,
|
||||||
onFocusPane,
|
onFocusPane,
|
||||||
onSplitPane: _onSplitPane,
|
onSplitPane: _onSplitPane,
|
||||||
onSplitPaneEmpty,
|
onSplitPaneEmpty: _onSplitPaneEmpty,
|
||||||
onReorderTabsInPane,
|
onReorderTabsInPane,
|
||||||
renderPaneEmptyState,
|
renderPaneEmptyState,
|
||||||
activeDragTabId,
|
activeDragTabId,
|
||||||
@@ -1114,15 +1114,6 @@ function SplitPaneView({
|
|||||||
},
|
},
|
||||||
[onReorderTabsInPane, paneId],
|
[onReorderTabsInPane, paneId],
|
||||||
);
|
);
|
||||||
const handleSplitRight = useCallback(
|
|
||||||
() => onSplitPaneEmpty({ targetPaneId: paneId, position: "right" }),
|
|
||||||
[onSplitPaneEmpty, paneId],
|
|
||||||
);
|
|
||||||
const handleSplitDown = useCallback(
|
|
||||||
() => onSplitPaneEmpty({ targetPaneId: paneId, position: "bottom" }),
|
|
||||||
[onSplitPaneEmpty, paneId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RenderProfile id={`SplitPaneView:${pane.id}`}>
|
<RenderProfile id={`SplitPaneView:${pane.id}`}>
|
||||||
<View ref={paneRef} collapsable={false} style={styles.pane}>
|
<View ref={paneRef} collapsable={false} style={styles.pane}>
|
||||||
@@ -1151,8 +1142,6 @@ function SplitPaneView({
|
|||||||
onCreateBrowserTab={onCreateBrowserTab}
|
onCreateBrowserTab={onCreateBrowserTab}
|
||||||
showCreateBrowserTab={showCreateBrowserTab}
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
onReorderTabs={handleReorderTabs}
|
onReorderTabs={handleReorderTabs}
|
||||||
onSplitRight={handleSplitRight}
|
|
||||||
onSplitDown={handleSplitDown}
|
|
||||||
externalDndContext
|
externalDndContext
|
||||||
activeDragTabId={activeDragTabId}
|
activeDragTabId={activeDragTabId}
|
||||||
tabDropPreviewIndex={
|
tabDropPreviewIndex={
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { SquareTerminal } from "lucide-react-native";
|
||||||
|
import { withUnistyles } from "react-native-unistyles";
|
||||||
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
|
|
||||||
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
|
||||||
|
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||||
|
|
||||||
|
function ProviderProfileIcon({
|
||||||
|
iconKey,
|
||||||
|
size,
|
||||||
|
color = "",
|
||||||
|
}: {
|
||||||
|
iconKey: string;
|
||||||
|
size: number;
|
||||||
|
color?: string;
|
||||||
|
}) {
|
||||||
|
const Icon = getProviderIcon(iconKey);
|
||||||
|
return <Icon size={size} color={color} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemedProviderProfileIcon = withUnistyles(ProviderProfileIcon);
|
||||||
|
|
||||||
|
export function TerminalProfileIcon({ iconKey }: { iconKey: string | undefined }): ReactElement {
|
||||||
|
if (!iconKey) {
|
||||||
|
return <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
|
||||||
|
}
|
||||||
|
return <ThemedProviderProfileIcon iconKey={iconKey} size={14} uniProps={mutedColorMapping} />;
|
||||||
|
}
|
||||||
+423
-569
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,18 @@
|
|||||||
import { GitActionsSplitButton } from "@/git/actions-split-button";
|
import { GitActionsSplitButton } from "@/git/actions-split-button";
|
||||||
import { useGitActions } from "@/git/use-actions";
|
|
||||||
import { GIT_ACTION_ICONS } from "@/git/action-icons";
|
import { GIT_ACTION_ICONS } from "@/git/action-icons";
|
||||||
|
import { useGitActions } from "@/git/use-actions";
|
||||||
|
|
||||||
interface WorkspaceActionsProps {
|
interface WorkspaceActionsProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
hideLabels?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceActions({ serverId, cwd, hideLabels }: WorkspaceActionsProps) {
|
export function WorkspaceActions({ serverId, cwd }: WorkspaceActionsProps) {
|
||||||
const { gitActions } = useGitActions({
|
const { gitActions } = useGitActions({
|
||||||
serverId,
|
serverId,
|
||||||
cwd,
|
cwd,
|
||||||
icons: GIT_ACTION_ICONS,
|
icons: GIT_ACTION_ICONS,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <GitActionsSplitButton gitActions={gitActions} hideLabels={hideLabels} />;
|
return <GitActionsSplitButton gitActions={gitActions} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe("loadChangesPreferencesFromStorage", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
viewMode: "flat",
|
desktopTreeVisible: false,
|
||||||
wrapLines: true,
|
wrapLines: true,
|
||||||
hideWhitespace: false,
|
hideWhitespace: false,
|
||||||
commitsCollapsed: true,
|
commitsCollapsed: true,
|
||||||
@@ -51,7 +51,7 @@ describe("loadChangesPreferencesFromStorage", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
layout: "split",
|
layout: "split",
|
||||||
viewMode: "tree",
|
desktopTreeVisible: true,
|
||||||
hideWhitespace: true,
|
hideWhitespace: true,
|
||||||
wrapLines: false,
|
wrapLines: false,
|
||||||
commitsCollapsed: true,
|
commitsCollapsed: true,
|
||||||
@@ -95,14 +95,14 @@ describe("saveChangesPreferences", () => {
|
|||||||
|
|
||||||
await saveChangesPreferences({
|
await saveChangesPreferences({
|
||||||
queryClient,
|
queryClient,
|
||||||
updates: { layout: "split", viewMode: "tree", hideWhitespace: true },
|
updates: { layout: "split", desktopTreeVisible: true, hideWhitespace: true },
|
||||||
storage,
|
storage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
...DEFAULT_CHANGES_PREFERENCES,
|
...DEFAULT_CHANGES_PREFERENCES,
|
||||||
layout: "split",
|
layout: "split",
|
||||||
viewMode: "tree",
|
desktopTreeVisible: true,
|
||||||
hideWhitespace: true,
|
hideWhitespace: true,
|
||||||
};
|
};
|
||||||
expect(queryClient.getQueryData(CHANGES_PREFERENCES_QUERY_KEY)).toEqual(expected);
|
expect(queryClient.getQueryData(CHANGES_PREFERENCES_QUERY_KEY)).toEqual(expected);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const CHANGES_PREFERENCES_QUERY_KEY = ["changes-preferences"];
|
|||||||
|
|
||||||
const changesPreferencesSchema = z.strictObject({
|
const changesPreferencesSchema = z.strictObject({
|
||||||
layout: z.enum(["unified", "split"]).optional(),
|
layout: z.enum(["unified", "split"]).optional(),
|
||||||
|
desktopTreeVisible: z.boolean().optional(),
|
||||||
|
// Read the former overloaded preference once so existing installations keep their desktop tree.
|
||||||
viewMode: z.enum(["flat", "tree"]).optional(),
|
viewMode: z.enum(["flat", "tree"]).optional(),
|
||||||
wrapLines: z.boolean().optional(),
|
wrapLines: z.boolean().optional(),
|
||||||
hideWhitespace: z.boolean().optional(),
|
hideWhitespace: z.boolean().optional(),
|
||||||
@@ -16,7 +18,7 @@ const changesPreferencesSchema = z.strictObject({
|
|||||||
|
|
||||||
export interface ChangesPreferences {
|
export interface ChangesPreferences {
|
||||||
layout: "unified" | "split";
|
layout: "unified" | "split";
|
||||||
viewMode: "flat" | "tree";
|
desktopTreeVisible: boolean;
|
||||||
wrapLines: boolean;
|
wrapLines: boolean;
|
||||||
hideWhitespace: boolean;
|
hideWhitespace: boolean;
|
||||||
commitsCollapsed: boolean;
|
commitsCollapsed: boolean;
|
||||||
@@ -24,7 +26,7 @@ export interface ChangesPreferences {
|
|||||||
|
|
||||||
export const DEFAULT_CHANGES_PREFERENCES: ChangesPreferences = {
|
export const DEFAULT_CHANGES_PREFERENCES: ChangesPreferences = {
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
viewMode: "flat",
|
desktopTreeVisible: false,
|
||||||
wrapLines: false,
|
wrapLines: false,
|
||||||
hideWhitespace: false,
|
hideWhitespace: false,
|
||||||
commitsCollapsed: true,
|
commitsCollapsed: true,
|
||||||
@@ -54,7 +56,12 @@ export async function loadChangesPreferencesFromStorage(
|
|||||||
changesPreferencesSchema,
|
changesPreferencesSchema,
|
||||||
);
|
);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
return { ...DEFAULT_CHANGES_PREFERENCES, ...stored };
|
const { viewMode, ...currentPreferences } = stored;
|
||||||
|
return {
|
||||||
|
...DEFAULT_CHANGES_PREFERENCES,
|
||||||
|
...currentPreferences,
|
||||||
|
desktopTreeVisible: stored.desktopTreeVisible ?? viewMode === "tree",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacyWrapLines = await loadLegacyWrapLinesPreference(storage);
|
const legacyWrapLines = await loadLegacyWrapLinesPreference(storage);
|
||||||
|
|||||||
@@ -600,8 +600,6 @@ export const ar: TranslationResources = {
|
|||||||
pullRequest: "طلب السحب",
|
pullRequest: "طلب السحب",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
editTerminalProfiles: "Edit profiles…",
|
editTerminalProfiles: "Edit profiles…",
|
||||||
pinTarget: "تثبيت",
|
|
||||||
unpinTarget: "إلغاء التثبيت",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "افتح المستكشف",
|
open: "افتح المستكشف",
|
||||||
@@ -855,14 +853,12 @@ export const ar: TranslationResources = {
|
|||||||
switchToUnified: "التبديل إلى الفرق الموحد",
|
switchToUnified: "التبديل إلى الفرق الموحد",
|
||||||
switchToSplit: "التبديل إلى الفرق جنبًا إلى جنب",
|
switchToSplit: "التبديل إلى الفرق جنبًا إلى جنب",
|
||||||
showTreeView: "إظهار شجرة المجلدات",
|
showTreeView: "إظهار شجرة المجلدات",
|
||||||
showFlatView: "إظهار قائمة الملفات المسطحة",
|
hideTreeView: "إخفاء شجرة المجلدات",
|
||||||
options: "خيارات الفرق",
|
options: "خيارات الفرق",
|
||||||
hideWhitespace: "إخفاء المسافة البيضاء",
|
hideWhitespace: "إخفاء المسافة البيضاء",
|
||||||
showWhitespace: "إظهار المسافة البيضاء",
|
showWhitespace: "إظهار المسافة البيضاء",
|
||||||
scrollLongLines: "قم بتمرير الخطوط الطويلة",
|
scrollLongLines: "قم بتمرير الخطوط الطويلة",
|
||||||
wrapLongLines: "لف الخطوط الطويلة",
|
wrapLongLines: "لف الخطوط الطويلة",
|
||||||
collapseAll: "طي كافة الملفات",
|
|
||||||
expandAll: "قم بتوسيع كافة الملفات",
|
|
||||||
collapseAllFolders: "طي كافة المجلدات",
|
collapseAllFolders: "طي كافة المجلدات",
|
||||||
expandAllFolders: "توسيع كافة المجلدات",
|
expandAllFolders: "توسيع كافة المجلدات",
|
||||||
refreshing: "منعش",
|
refreshing: "منعش",
|
||||||
|
|||||||
@@ -598,8 +598,6 @@ export const en = {
|
|||||||
pullRequest: "Pull request",
|
pullRequest: "Pull request",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
editTerminalProfiles: "Edit profiles…",
|
editTerminalProfiles: "Edit profiles…",
|
||||||
pinTarget: "Pin",
|
|
||||||
unpinTarget: "Unpin",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Open explorer",
|
open: "Open explorer",
|
||||||
@@ -864,14 +862,12 @@ export const en = {
|
|||||||
switchToUnified: "Switch to unified diff",
|
switchToUnified: "Switch to unified diff",
|
||||||
switchToSplit: "Switch to side-by-side diff",
|
switchToSplit: "Switch to side-by-side diff",
|
||||||
showTreeView: "Show folder tree",
|
showTreeView: "Show folder tree",
|
||||||
showFlatView: "Show flat file list",
|
hideTreeView: "Hide folder tree",
|
||||||
options: "Diff options",
|
options: "Diff options",
|
||||||
hideWhitespace: "Hide whitespace",
|
hideWhitespace: "Hide whitespace",
|
||||||
showWhitespace: "Show whitespace",
|
showWhitespace: "Show whitespace",
|
||||||
scrollLongLines: "Scroll long lines",
|
scrollLongLines: "Scroll long lines",
|
||||||
wrapLongLines: "Wrap long lines",
|
wrapLongLines: "Wrap long lines",
|
||||||
collapseAll: "Collapse all files",
|
|
||||||
expandAll: "Expand all files",
|
|
||||||
collapseAllFolders: "Collapse all folders",
|
collapseAllFolders: "Collapse all folders",
|
||||||
expandAllFolders: "Expand all folders",
|
expandAllFolders: "Expand all folders",
|
||||||
refreshing: "Refreshing",
|
refreshing: "Refreshing",
|
||||||
|
|||||||
@@ -605,8 +605,6 @@ export const es: TranslationResources = {
|
|||||||
pullRequest: "Solicitud de extracción",
|
pullRequest: "Solicitud de extracción",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
editTerminalProfiles: "Edit profiles…",
|
editTerminalProfiles: "Edit profiles…",
|
||||||
pinTarget: "Fijar",
|
|
||||||
unpinTarget: "Desfijar",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Explorador abierto",
|
open: "Explorador abierto",
|
||||||
@@ -886,14 +884,12 @@ export const es: TranslationResources = {
|
|||||||
switchToUnified: "Cambiar a diferencia unificada",
|
switchToUnified: "Cambiar a diferencia unificada",
|
||||||
switchToSplit: "Cambiar a diferencia lado a lado",
|
switchToSplit: "Cambiar a diferencia lado a lado",
|
||||||
showTreeView: "Mostrar árbol de carpetas",
|
showTreeView: "Mostrar árbol de carpetas",
|
||||||
showFlatView: "Mostrar lista plana de archivos",
|
hideTreeView: "Ocultar árbol de carpetas",
|
||||||
options: "Opciones de diferencia",
|
options: "Opciones de diferencia",
|
||||||
hideWhitespace: "Ocultar espacios en blanco",
|
hideWhitespace: "Ocultar espacios en blanco",
|
||||||
showWhitespace: "Mostrar espacios en blanco",
|
showWhitespace: "Mostrar espacios en blanco",
|
||||||
scrollLongLines: "Desplazarse por largas filas",
|
scrollLongLines: "Desplazarse por largas filas",
|
||||||
wrapLongLines: "Envolver largas filas",
|
wrapLongLines: "Envolver largas filas",
|
||||||
collapseAll: "Contraer todos los archivos",
|
|
||||||
expandAll: "Expandir todos los archivos",
|
|
||||||
collapseAllFolders: "Contraer todas las carpetas",
|
collapseAllFolders: "Contraer todas las carpetas",
|
||||||
expandAllFolders: "Expandir todas las carpetas",
|
expandAllFolders: "Expandir todas las carpetas",
|
||||||
refreshing: "Refrescante",
|
refreshing: "Refrescante",
|
||||||
|
|||||||
@@ -605,8 +605,6 @@ export const fr: TranslationResources = {
|
|||||||
pullRequest: "Demande de fusion",
|
pullRequest: "Demande de fusion",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
editTerminalProfiles: "Edit profiles…",
|
editTerminalProfiles: "Edit profiles…",
|
||||||
pinTarget: "Épingler",
|
|
||||||
unpinTarget: "Détacher",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Ouvrir l'explorateur",
|
open: "Ouvrir l'explorateur",
|
||||||
@@ -885,14 +883,12 @@ export const fr: TranslationResources = {
|
|||||||
switchToUnified: "Passer au diff unifié",
|
switchToUnified: "Passer au diff unifié",
|
||||||
switchToSplit: "Passer au diff côte à côte",
|
switchToSplit: "Passer au diff côte à côte",
|
||||||
showTreeView: "Afficher l'arborescence des dossiers",
|
showTreeView: "Afficher l'arborescence des dossiers",
|
||||||
showFlatView: "Afficher la liste de fichiers à plat",
|
hideTreeView: "Masquer l'arborescence des dossiers",
|
||||||
options: "Options du diff",
|
options: "Options du diff",
|
||||||
hideWhitespace: "Masquer les espaces",
|
hideWhitespace: "Masquer les espaces",
|
||||||
showWhitespace: "Afficher les espaces",
|
showWhitespace: "Afficher les espaces",
|
||||||
scrollLongLines: "Faire défiler les longues lignes",
|
scrollLongLines: "Faire défiler les longues lignes",
|
||||||
wrapLongLines: "Enroulez les longues lignes",
|
wrapLongLines: "Enroulez les longues lignes",
|
||||||
collapseAll: "Réduire tous les fichiers",
|
|
||||||
expandAll: "Développer tous les fichiers",
|
|
||||||
collapseAllFolders: "Réduire tous les dossiers",
|
collapseAllFolders: "Réduire tous les dossiers",
|
||||||
expandAllFolders: "Développer tous les dossiers",
|
expandAllFolders: "Développer tous les dossiers",
|
||||||
refreshing: "Rafraîchissant",
|
refreshing: "Rafraîchissant",
|
||||||
|
|||||||
@@ -605,8 +605,6 @@ export const ja: TranslationResources = {
|
|||||||
pullRequest: "プルリクエスト",
|
pullRequest: "プルリクエスト",
|
||||||
terminalProfilesMenu: "ターミナルプロファイル",
|
terminalProfilesMenu: "ターミナルプロファイル",
|
||||||
editTerminalProfiles: "プロファイルを編集…",
|
editTerminalProfiles: "プロファイルを編集…",
|
||||||
pinTarget: "ピン留め",
|
|
||||||
unpinTarget: "ピン留めを解除",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "エクスプローラーを開く",
|
open: "エクスプローラーを開く",
|
||||||
@@ -866,14 +864,12 @@ export const ja: TranslationResources = {
|
|||||||
switchToUnified: "ユニファイド差分に切り替え",
|
switchToUnified: "ユニファイド差分に切り替え",
|
||||||
switchToSplit: "左右比較差分に切り替え",
|
switchToSplit: "左右比較差分に切り替え",
|
||||||
showTreeView: "フォルダツリーを表示",
|
showTreeView: "フォルダツリーを表示",
|
||||||
showFlatView: "フラットなファイル一覧を表示",
|
hideTreeView: "フォルダツリーを非表示",
|
||||||
options: "差分オプション",
|
options: "差分オプション",
|
||||||
hideWhitespace: "空白を非表示",
|
hideWhitespace: "空白を非表示",
|
||||||
showWhitespace: "空白を表示",
|
showWhitespace: "空白を表示",
|
||||||
scrollLongLines: "長い行をスクロール",
|
scrollLongLines: "長い行をスクロール",
|
||||||
wrapLongLines: "長い行を折り返す",
|
wrapLongLines: "長い行を折り返す",
|
||||||
collapseAll: "すべて折りたたむ",
|
|
||||||
expandAll: "すべて展開",
|
|
||||||
collapseAllFolders: "すべてのフォルダを折りたたむ",
|
collapseAllFolders: "すべてのフォルダを折りたたむ",
|
||||||
expandAllFolders: "すべてのフォルダを展開",
|
expandAllFolders: "すべてのフォルダを展開",
|
||||||
refreshing: "更新中",
|
refreshing: "更新中",
|
||||||
|
|||||||
@@ -602,8 +602,6 @@ export const ko: TranslationResources = {
|
|||||||
pullRequest: "풀 리퀘스트",
|
pullRequest: "풀 리퀘스트",
|
||||||
terminalProfilesMenu: "터미널 프로필",
|
terminalProfilesMenu: "터미널 프로필",
|
||||||
editTerminalProfiles: "프로필 편집…",
|
editTerminalProfiles: "프로필 편집…",
|
||||||
pinTarget: "고정",
|
|
||||||
unpinTarget: "고정 해제",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "탐색기 열기",
|
open: "탐색기 열기",
|
||||||
@@ -862,14 +860,12 @@ export const ko: TranslationResources = {
|
|||||||
switchToUnified: "통합 diff로 전환",
|
switchToUnified: "통합 diff로 전환",
|
||||||
switchToSplit: "나란히 보기 diff로 전환",
|
switchToSplit: "나란히 보기 diff로 전환",
|
||||||
showTreeView: "폴더 트리 표시",
|
showTreeView: "폴더 트리 표시",
|
||||||
showFlatView: "플랫 파일 목록 표시",
|
hideTreeView: "폴더 트리 숨기기",
|
||||||
options: "Diff 옵션",
|
options: "Diff 옵션",
|
||||||
hideWhitespace: "공백 숨기기",
|
hideWhitespace: "공백 숨기기",
|
||||||
showWhitespace: "공백 표시",
|
showWhitespace: "공백 표시",
|
||||||
scrollLongLines: "긴 줄 스크롤",
|
scrollLongLines: "긴 줄 스크롤",
|
||||||
wrapLongLines: "긴 줄 줄바꿈",
|
wrapLongLines: "긴 줄 줄바꿈",
|
||||||
collapseAll: "모든 파일 접기",
|
|
||||||
expandAll: "모든 파일 펼치기",
|
|
||||||
collapseAllFolders: "모든 폴더 축소",
|
collapseAllFolders: "모든 폴더 축소",
|
||||||
expandAllFolders: "모든 폴더 확장",
|
expandAllFolders: "모든 폴더 확장",
|
||||||
refreshing: "새로고침 중",
|
refreshing: "새로고침 중",
|
||||||
|
|||||||
@@ -604,8 +604,6 @@ export const ptBR: TranslationResources = {
|
|||||||
pullRequest: "Pull request",
|
pullRequest: "Pull request",
|
||||||
terminalProfilesMenu: "Perfis de terminal",
|
terminalProfilesMenu: "Perfis de terminal",
|
||||||
editTerminalProfiles: "Editar perfis…",
|
editTerminalProfiles: "Editar perfis…",
|
||||||
pinTarget: "Fixar",
|
|
||||||
unpinTarget: "Desafixar",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Abrir explorador",
|
open: "Abrir explorador",
|
||||||
@@ -877,14 +875,12 @@ export const ptBR: TranslationResources = {
|
|||||||
switchToUnified: "Alternar para diff unificado",
|
switchToUnified: "Alternar para diff unificado",
|
||||||
switchToSplit: "Alternar para diff lado a lado",
|
switchToSplit: "Alternar para diff lado a lado",
|
||||||
showTreeView: "Mostrar árvore de pastas",
|
showTreeView: "Mostrar árvore de pastas",
|
||||||
showFlatView: "Mostrar lista plana de arquivos",
|
hideTreeView: "Ocultar árvore de pastas",
|
||||||
options: "Opções do diff",
|
options: "Opções do diff",
|
||||||
hideWhitespace: "Ocultar espaços em branco",
|
hideWhitespace: "Ocultar espaços em branco",
|
||||||
showWhitespace: "Mostrar espaços em branco",
|
showWhitespace: "Mostrar espaços em branco",
|
||||||
scrollLongLines: "Rolar linhas longas",
|
scrollLongLines: "Rolar linhas longas",
|
||||||
wrapLongLines: "Quebrar linhas longas",
|
wrapLongLines: "Quebrar linhas longas",
|
||||||
collapseAll: "Recolher todos os arquivos",
|
|
||||||
expandAll: "Expandir todos os arquivos",
|
|
||||||
collapseAllFolders: "Recolher todas as pastas",
|
collapseAllFolders: "Recolher todas as pastas",
|
||||||
expandAllFolders: "Expandir todas as pastas",
|
expandAllFolders: "Expandir todas as pastas",
|
||||||
refreshing: "Atualizando",
|
refreshing: "Atualizando",
|
||||||
|
|||||||
@@ -604,8 +604,6 @@ export const ru: TranslationResources = {
|
|||||||
pullRequest: "Запрос на слияние",
|
pullRequest: "Запрос на слияние",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
editTerminalProfiles: "Edit profiles…",
|
editTerminalProfiles: "Edit profiles…",
|
||||||
pinTarget: "Закрепить",
|
|
||||||
unpinTarget: "Открепить",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Открыть проводник",
|
open: "Открыть проводник",
|
||||||
@@ -877,14 +875,12 @@ export const ru: TranslationResources = {
|
|||||||
switchToUnified: "Переключить на объединенный diff",
|
switchToUnified: "Переключить на объединенный diff",
|
||||||
switchToSplit: "Переключить на diff рядом",
|
switchToSplit: "Переключить на diff рядом",
|
||||||
showTreeView: "Показать дерево папок",
|
showTreeView: "Показать дерево папок",
|
||||||
showFlatView: "Показать плоский список файлов",
|
hideTreeView: "Скрыть дерево папок",
|
||||||
options: "Параметры diff",
|
options: "Параметры diff",
|
||||||
hideWhitespace: "Скрыть пробелы",
|
hideWhitespace: "Скрыть пробелы",
|
||||||
showWhitespace: "Показать пробелы",
|
showWhitespace: "Показать пробелы",
|
||||||
scrollLongLines: "Прокручивать длинные строки",
|
scrollLongLines: "Прокручивать длинные строки",
|
||||||
wrapLongLines: "Перенос длинных строк",
|
wrapLongLines: "Перенос длинных строк",
|
||||||
collapseAll: "Свернуть все файлы",
|
|
||||||
expandAll: "Развернуть все файлы",
|
|
||||||
collapseAllFolders: "Свернуть все папки",
|
collapseAllFolders: "Свернуть все папки",
|
||||||
expandAllFolders: "Развернуть все папки",
|
expandAllFolders: "Развернуть все папки",
|
||||||
refreshing: "Освежающий",
|
refreshing: "Освежающий",
|
||||||
|
|||||||
@@ -600,8 +600,6 @@ export const zhCN: TranslationResources = {
|
|||||||
pullRequest: "拉取请求",
|
pullRequest: "拉取请求",
|
||||||
terminalProfilesMenu: "Terminal profiles",
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
editTerminalProfiles: "Edit profiles…",
|
editTerminalProfiles: "Edit profiles…",
|
||||||
pinTarget: "固定",
|
|
||||||
unpinTarget: "取消固定",
|
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "打开 explorer",
|
open: "打开 explorer",
|
||||||
@@ -847,14 +845,12 @@ export const zhCN: TranslationResources = {
|
|||||||
switchToUnified: "切换到统一差异视图",
|
switchToUnified: "切换到统一差异视图",
|
||||||
switchToSplit: "切换到并排差异视图",
|
switchToSplit: "切换到并排差异视图",
|
||||||
showTreeView: "显示文件夹树",
|
showTreeView: "显示文件夹树",
|
||||||
showFlatView: "显示平铺文件列表",
|
hideTreeView: "隐藏文件夹树",
|
||||||
options: "Diff 选项",
|
options: "Diff 选项",
|
||||||
hideWhitespace: "隐藏空白差异",
|
hideWhitespace: "隐藏空白差异",
|
||||||
showWhitespace: "显示空白差异",
|
showWhitespace: "显示空白差异",
|
||||||
scrollLongLines: "滚动长行",
|
scrollLongLines: "滚动长行",
|
||||||
wrapLongLines: "自动换行长行",
|
wrapLongLines: "自动换行长行",
|
||||||
collapseAll: "折叠所有文件",
|
|
||||||
expandAll: "展开所有文件",
|
|
||||||
collapseAllFolders: "折叠所有文件夹",
|
collapseAllFolders: "折叠所有文件夹",
|
||||||
expandAllFolders: "展开所有文件夹",
|
expandAllFolders: "展开所有文件夹",
|
||||||
refreshing: "正在刷新",
|
refreshing: "正在刷新",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ProfileIcon } from "@/workspace-pins/launch";
|
import { TerminalProfileIcon } from "@/components/terminal-profile-icon";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { ICON_SIZE } from "@/styles/theme";
|
import { ICON_SIZE } from "@/styles/theme";
|
||||||
import {
|
import {
|
||||||
@@ -54,7 +54,7 @@ function LaunchProfileMenuItem({
|
|||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const handleSelect = useCallback(() => onSelect(profile.id), [onSelect, profile.id]);
|
const handleSelect = useCallback(() => onSelect(profile.id), [onSelect, profile.id]);
|
||||||
const leading = useMemo(
|
const leading = useMemo(
|
||||||
() => <ProfileIcon iconKey={getTerminalProfileIcon(profile)} />,
|
() => <TerminalProfileIcon iconKey={getTerminalProfileIcon(profile)} />,
|
||||||
[profile],
|
[profile],
|
||||||
);
|
);
|
||||||
// What the profile runs before any prompt is typed, so the row says what it
|
// What the profile runs before any prompt is typed, so the row says what it
|
||||||
@@ -102,7 +102,7 @@ function TriggerIcon({
|
|||||||
return <ThemedMessageCircle size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
return <ThemedMessageCircle size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
}
|
}
|
||||||
if (profile) {
|
if (profile) {
|
||||||
return <ProfileIcon iconKey={getTerminalProfileIcon(profile)} />;
|
return <TerminalProfileIcon iconKey={getTerminalProfileIcon(profile)} />;
|
||||||
}
|
}
|
||||||
return <ThemedSquareTerminal size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
return <ThemedSquareTerminal size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import invariant from "tiny-invariant";
|
|||||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||||
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
import { DiffLayoutToggle, GitDiffPane, resolveDiffLayout, SharedDiffView } from "@/git/diff-pane";
|
import { ChangesSurface, DiffDocument, DiffLayoutToggle, resolveDiffLayout } from "@/git/diff-pane";
|
||||||
import { useCommitDiffFiles } from "@/git/use-diff-files";
|
import { useCommitDiffFiles } from "@/git/use-diff-files";
|
||||||
import { useChangesPreferences } from "@/hooks/use-changes-preferences";
|
import { useChangesPreferences } from "@/hooks/use-changes-preferences";
|
||||||
import { useAppSettings } from "@/hooks/use-settings";
|
import { useAppSettings } from "@/hooks/use-settings";
|
||||||
@@ -44,10 +44,6 @@ function useDiffPanelPreferences() {
|
|||||||
const toggleHideWhitespace = useCallback(() => {
|
const toggleHideWhitespace = useCallback(() => {
|
||||||
void updatePreferences({ hideWhitespace: !preferences.hideWhitespace });
|
void updatePreferences({ hideWhitespace: !preferences.hideWhitespace });
|
||||||
}, [preferences.hideWhitespace, updatePreferences]);
|
}, [preferences.hideWhitespace, updatePreferences]);
|
||||||
const toggleViewMode = useCallback(() => {
|
|
||||||
void updatePreferences({ viewMode: preferences.viewMode === "flat" ? "tree" : "flat" });
|
|
||||||
}, [preferences.viewMode, updatePreferences]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
preferences,
|
preferences,
|
||||||
isCompact,
|
isCompact,
|
||||||
@@ -56,7 +52,6 @@ function useDiffPanelPreferences() {
|
|||||||
toggleLayout,
|
toggleLayout,
|
||||||
toggleWrapLines,
|
toggleWrapLines,
|
||||||
toggleHideWhitespace,
|
toggleHideWhitespace,
|
||||||
toggleViewMode,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,12 +90,14 @@ function WorkingDiffPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="working-diff-panel">
|
<View style={styles.container} testID="working-diff-panel">
|
||||||
<GitDiffPane
|
<ChangesSurface
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
cwd={cwd}
|
cwd={cwd}
|
||||||
enabled={isActive}
|
enabled={isActive}
|
||||||
host="panel"
|
host="panel"
|
||||||
|
focusPath={target.focusPath}
|
||||||
|
focusRequestId={target.focusRequestId}
|
||||||
onOpenFile={handleOpenFile}
|
onOpenFile={handleOpenFile}
|
||||||
onAddToChat={canAddToChat ? addFile : undefined}
|
onAddToChat={canAddToChat ? addFile : undefined}
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +139,7 @@ function CommitDiffPanel() {
|
|||||||
body = <PanelState message={t("panels.diff.empty")} testID="commit-diff-empty" />;
|
body = <PanelState message={t("panels.diff.empty")} testID="commit-diff-empty" />;
|
||||||
} else {
|
} else {
|
||||||
body = (
|
body = (
|
||||||
<SharedDiffView
|
<DiffDocument
|
||||||
files={files}
|
files={files}
|
||||||
displayPreferences={panelPreferences.displayPreferences}
|
displayPreferences={panelPreferences.displayPreferences}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|||||||
@@ -2,23 +2,14 @@ import { useMemo } from "react";
|
|||||||
import { useWindowDimensions } from "react-native";
|
import { useWindowDimensions } from "react-native";
|
||||||
import {
|
import {
|
||||||
computeWorkspaceTabLayout,
|
computeWorkspaceTabLayout,
|
||||||
|
type WorkspaceTabLayoutMetrics,
|
||||||
type WorkspaceTabLayoutResult,
|
type WorkspaceTabLayoutResult,
|
||||||
} from "@/screens/workspace/workspace-tab-layout";
|
} from "@/screens/workspace/workspace-tab-layout";
|
||||||
|
|
||||||
interface UseWorkspaceTabLayoutInput {
|
interface UseWorkspaceTabLayoutInput {
|
||||||
tabLabelLengths: number[];
|
tabLabelWidths: number[];
|
||||||
viewportWidthOverride?: number | null;
|
viewportWidthOverride?: number | null;
|
||||||
metrics: {
|
metrics: WorkspaceTabLayoutMetrics;
|
||||||
rowHorizontalInset: number;
|
|
||||||
actionsReservedWidth: number;
|
|
||||||
rowPaddingHorizontal: number;
|
|
||||||
tabGap: number;
|
|
||||||
maxTabWidth: number;
|
|
||||||
tabIconWidth: number;
|
|
||||||
tabHorizontalPadding: number;
|
|
||||||
estimatedCharWidth: number;
|
|
||||||
closeButtonWidth: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseWorkspaceTabLayoutResult {
|
interface UseWorkspaceTabLayoutResult {
|
||||||
@@ -38,10 +29,10 @@ export function useWorkspaceTabLayout(
|
|||||||
() =>
|
() =>
|
||||||
computeWorkspaceTabLayout({
|
computeWorkspaceTabLayout({
|
||||||
viewportWidth: resolvedViewportWidth,
|
viewportWidth: resolvedViewportWidth,
|
||||||
tabLabelLengths: input.tabLabelLengths,
|
tabLabelWidths: input.tabLabelWidths,
|
||||||
metrics: input.metrics,
|
metrics: input.metrics,
|
||||||
}),
|
}),
|
||||||
[input.metrics, input.tabLabelLengths, resolvedViewportWidth],
|
[input.metrics, input.tabLabelWidths, resolvedViewportWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ import {
|
|||||||
CopyX,
|
CopyX,
|
||||||
ArrowLeftToLine,
|
ArrowLeftToLine,
|
||||||
ArrowRightToLine,
|
ArrowRightToLine,
|
||||||
Columns2,
|
|
||||||
Copy,
|
Copy,
|
||||||
Pencil,
|
Pencil,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
Rows2,
|
|
||||||
Globe,
|
Globe,
|
||||||
FileDiff,
|
FileDiff,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
@@ -87,11 +85,7 @@ import {
|
|||||||
} from "@getpaseo/protocol/terminal-profiles";
|
} from "@getpaseo/protocol/terminal-profiles";
|
||||||
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
||||||
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
||||||
import { ProfileIcon, usePinnedLaunchers } from "@/workspace-pins/launch";
|
import { TerminalProfileIcon } from "@/components/terminal-profile-icon";
|
||||||
import { runPinnedTabTarget, type TabTargetHandlers } from "@/workspace-pins/run";
|
|
||||||
import type { PinnedTabTarget } from "@/workspace-pins/target";
|
|
||||||
import { PinnedTargetsRow } from "@/workspace-pins/pinned-targets-row";
|
|
||||||
import { PinnableMenuItem } from "@/workspace-pins/pinnable-menu-item";
|
|
||||||
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||||
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
||||||
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
||||||
@@ -108,9 +102,13 @@ const TAB_CHIP_HORIZONTAL_PADDING = 8;
|
|||||||
const TAB_CHIP_GAP = 4;
|
const TAB_CHIP_GAP = 4;
|
||||||
const TAB_ROW_PADDING_HORIZONTAL = 4;
|
const TAB_ROW_PADDING_HORIZONTAL = 4;
|
||||||
const TAB_ICON_WIDTH = 14;
|
const TAB_ICON_WIDTH = 14;
|
||||||
|
const TAB_CONTENT_GAP = 4;
|
||||||
const TAB_DROP_INDICATOR_WIDTH = 4;
|
const TAB_DROP_INDICATOR_WIDTH = 4;
|
||||||
const TAB_MAX_WIDTH = 160;
|
const TAB_MIN_WIDTH = 96;
|
||||||
|
const TAB_MAX_WIDTH = 240;
|
||||||
const TAB_CLOSE_BUTTON_RESERVED_WIDTH = 0;
|
const TAB_CLOSE_BUTTON_RESERVED_WIDTH = 0;
|
||||||
|
const TAB_LABEL_FALLBACK_CHARACTER_WIDTH = 7;
|
||||||
|
const TAB_LABEL_LAYOUT_ALLOWANCE = 4;
|
||||||
|
|
||||||
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedX = withUnistyles(X);
|
const ThemedX = withUnistyles(X);
|
||||||
@@ -123,14 +121,13 @@ const ThemedPencil = withUnistyles(Pencil);
|
|||||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||||
const ThemedGlobe = withUnistyles(Globe);
|
const ThemedGlobe = withUnistyles(Globe);
|
||||||
const ThemedColumns2 = withUnistyles(Columns2);
|
|
||||||
const ThemedRows2 = withUnistyles(Rows2);
|
|
||||||
const ThemedPlus = withUnistyles(Plus);
|
const ThemedPlus = withUnistyles(Plus);
|
||||||
const ThemedFileDiff = withUnistyles(FileDiff);
|
const ThemedFileDiff = withUnistyles(FileDiff);
|
||||||
const ThemedFolderTree = withUnistyles(FolderTree);
|
const ThemedFolderTree = withUnistyles(FolderTree);
|
||||||
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
const extraMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundExtraMuted });
|
||||||
|
|
||||||
const AGENT_ICON = <ThemedSquarePen size={14} uniProps={mutedColorMapping} />;
|
const AGENT_ICON = <ThemedSquarePen size={14} uniProps={mutedColorMapping} />;
|
||||||
const TERMINAL_ICON = <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
|
const TERMINAL_ICON = <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
|
||||||
@@ -139,17 +136,10 @@ const CHANGES_ICON = <ThemedFileDiff size={14} uniProps={mutedColorMapping} />;
|
|||||||
const FILES_ICON = <ThemedFolderTree size={14} uniProps={mutedColorMapping} />;
|
const FILES_ICON = <ThemedFolderTree size={14} uniProps={mutedColorMapping} />;
|
||||||
const PULL_REQUEST_ICON = <ThemedGitPullRequest size={14} uniProps={mutedColorMapping} />;
|
const PULL_REQUEST_ICON = <ThemedGitPullRequest size={14} uniProps={mutedColorMapping} />;
|
||||||
|
|
||||||
const DRAFT_TARGET: PinnedTabTarget = { kind: "draft" };
|
|
||||||
const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" };
|
|
||||||
const BROWSER_TARGET: PinnedTabTarget = { kind: "browser" };
|
|
||||||
const CHANGES_TARGET = { kind: "working_diff" } as const;
|
const CHANGES_TARGET = { kind: "working_diff" } as const;
|
||||||
const FILES_TARGET = { kind: "files" } as const;
|
const FILES_TARGET = { kind: "files" } as const;
|
||||||
const PULL_REQUEST_TARGET = { kind: "pull_request" } as const;
|
const PULL_REQUEST_TARGET = { kind: "pull_request" } as const;
|
||||||
|
|
||||||
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
|
||||||
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
|
||||||
}
|
|
||||||
|
|
||||||
function inlineAddActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
function inlineAddActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
||||||
return [styles.inlineAddActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
return [styles.inlineAddActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
||||||
}
|
}
|
||||||
@@ -159,39 +149,48 @@ function updateMeasuredWidth(setWidth: Dispatch<SetStateAction<number>>, event:
|
|||||||
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileLeadingIcon({ iconKey }: { iconKey: string | undefined }) {
|
function TabLabelMeasurement({
|
||||||
|
tabKey,
|
||||||
|
label,
|
||||||
|
onMeasure,
|
||||||
|
}: {
|
||||||
|
tabKey: string;
|
||||||
|
label: string;
|
||||||
|
onMeasure: (tabKey: string, label: string, event: LayoutChangeEvent) => void;
|
||||||
|
}) {
|
||||||
|
const handleLayout = useCallback(
|
||||||
|
(event: LayoutChangeEvent) => onMeasure(tabKey, label, event),
|
||||||
|
[label, onMeasure, tabKey],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.terminalProfileIconWrapper}>
|
<Text
|
||||||
<ProfileIcon iconKey={iconKey} />
|
style={[styles.tabLabel, styles.tabLabelMeasurement]}
|
||||||
</View>
|
numberOfLines={1}
|
||||||
|
onLayout={handleLayout}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PinnableProfileMenuItemProps {
|
interface TerminalProfileMenuItemProps {
|
||||||
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
|
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onLaunch: (target: PinnedTabTarget) => void;
|
onLaunch: (profile: TerminalProfileInput) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PinnableProfileMenuItem({ profile, disabled, onLaunch }: PinnableProfileMenuItemProps) {
|
function TerminalProfileMenuItem({ profile, disabled, onLaunch }: TerminalProfileMenuItemProps) {
|
||||||
const target = useMemo<PinnedTabTarget>(
|
|
||||||
() => ({ kind: "profile", profileId: profile.id }),
|
|
||||||
[profile.id],
|
|
||||||
);
|
|
||||||
const leading = useMemo(
|
const leading = useMemo(
|
||||||
() => <ProfileLeadingIcon iconKey={getTerminalProfileIcon(profile)} />,
|
() => <TerminalProfileIcon iconKey={getTerminalProfileIcon(profile)} />,
|
||||||
[profile],
|
[profile],
|
||||||
);
|
);
|
||||||
const handleSelect = useCallback(() => onLaunch(target), [onLaunch, target]);
|
const handleSelect = useCallback(() => onLaunch(profile), [onLaunch, profile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PinnableMenuItem
|
<DropdownMenuItem leading={leading} disabled={disabled} onSelect={handleSelect}>
|
||||||
target={target}
|
{profile.name}
|
||||||
label={profile.name}
|
</DropdownMenuItem>
|
||||||
leading={leading}
|
|
||||||
disabled={disabled}
|
|
||||||
onSelect={handleSelect}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,58 +205,6 @@ interface TabTargetLauncherOptions {
|
|||||||
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The `+` menu and the pinned-targets row launch the same targets but sit in different parts of
|
|
||||||
* the row, so each owns its own subscription rather than re-rendering the whole row from a shared
|
|
||||||
* one. `useDaemonConfig` is a cached query, so the second caller is free.
|
|
||||||
*/
|
|
||||||
function useTabTargetLauncher({
|
|
||||||
normalizedServerId,
|
|
||||||
onCreateAgentTab,
|
|
||||||
onCreateTerminal,
|
|
||||||
onCreateBrowser,
|
|
||||||
onOpenChanges,
|
|
||||||
onOpenFiles,
|
|
||||||
onOpenPullRequest,
|
|
||||||
onCreateTerminalWithProfile,
|
|
||||||
}: TabTargetLauncherOptions) {
|
|
||||||
const { config } = useDaemonConfig(normalizedServerId);
|
|
||||||
const profiles = useMemo(
|
|
||||||
() => resolveTerminalProfiles(config?.terminalProfiles),
|
|
||||||
[config?.terminalProfiles],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlers = useMemo<TabTargetHandlers>(
|
|
||||||
() => ({
|
|
||||||
createDraft: onCreateAgentTab,
|
|
||||||
createTerminal: onCreateTerminal,
|
|
||||||
createBrowser: onCreateBrowser,
|
|
||||||
openChanges: onOpenChanges,
|
|
||||||
openFiles: onOpenFiles,
|
|
||||||
openPullRequest: onOpenPullRequest,
|
|
||||||
createTerminalWithProfile: onCreateTerminalWithProfile,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
onCreateAgentTab,
|
|
||||||
onCreateBrowser,
|
|
||||||
onCreateTerminal,
|
|
||||||
onCreateTerminalWithProfile,
|
|
||||||
onOpenChanges,
|
|
||||||
onOpenFiles,
|
|
||||||
onOpenPullRequest,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLaunch = useCallback(
|
|
||||||
(target: PinnedTabTarget) => {
|
|
||||||
runPinnedTabTarget(target, profiles, handlers);
|
|
||||||
},
|
|
||||||
[handlers, profiles],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { profiles, onLaunch };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkspaceNewTabButtonProps extends TabTargetLauncherOptions {
|
interface WorkspaceNewTabButtonProps extends TabTargetLauncherOptions {
|
||||||
shortcutKeys: ShortcutKey[][] | null;
|
shortcutKeys: ShortcutKey[][] | null;
|
||||||
onEditProfiles: () => void;
|
onEditProfiles: () => void;
|
||||||
@@ -286,16 +233,15 @@ function WorkspaceNewTabButton({
|
|||||||
onLayout,
|
onLayout,
|
||||||
}: WorkspaceNewTabButtonProps) {
|
}: WorkspaceNewTabButtonProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { profiles, onLaunch } = useTabTargetLauncher({
|
const { config } = useDaemonConfig(normalizedServerId);
|
||||||
normalizedServerId,
|
const profiles = useMemo(
|
||||||
onCreateAgentTab,
|
() => resolveTerminalProfiles(config?.terminalProfiles),
|
||||||
onCreateTerminal,
|
[config?.terminalProfiles],
|
||||||
onCreateBrowser,
|
);
|
||||||
onCreateTerminalWithProfile,
|
const shortcut = useMemo(
|
||||||
onOpenChanges,
|
() => (shortcutKeys ? <Shortcut chord={shortcutKeys} /> : undefined),
|
||||||
onOpenFiles,
|
[shortcutKeys],
|
||||||
onOpenPullRequest,
|
);
|
||||||
});
|
|
||||||
const tooltipText = t("workspace.tabs.actions.newTab");
|
const tooltipText = t("workspace.tabs.actions.newTab");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -309,7 +255,7 @@ function WorkspaceNewTabButton({
|
|||||||
accessibilityLabel={tooltipText}
|
accessibilityLabel={tooltipText}
|
||||||
style={inlineAddActionButtonStyle}
|
style={inlineAddActionButtonStyle}
|
||||||
>
|
>
|
||||||
<ThemedPlus size={14} uniProps={mutedColorMapping} />
|
<ThemedPlus size={14} uniProps={extraMutedColorMapping} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
<TooltipContent side="bottom" align="center" offset={8}>
|
||||||
@@ -317,64 +263,64 @@ function WorkspaceNewTabButton({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenuContent side="bottom" align="start" offset={4} minWidth={200}>
|
<DropdownMenuContent side="bottom" align="start" offset={4} minWidth={200}>
|
||||||
<PinnableMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-new-tab-menu-agent"
|
testID="workspace-new-tab-menu-agent"
|
||||||
target={DRAFT_TARGET}
|
|
||||||
label={t("workspace.tabs.actions.newAgent")}
|
|
||||||
leading={AGENT_ICON}
|
leading={AGENT_ICON}
|
||||||
shortcut={shortcutKeys}
|
trailing={shortcut}
|
||||||
onSelect={onCreateAgentTab}
|
onSelect={onCreateAgentTab}
|
||||||
/>
|
>
|
||||||
<PinnableMenuItem
|
{t("workspace.tabs.actions.newAgent")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
testID="workspace-new-tab-menu-terminal"
|
testID="workspace-new-tab-menu-terminal"
|
||||||
target={TERMINAL_TARGET}
|
|
||||||
label={t("workspace.tabs.actions.newTerminal")}
|
|
||||||
leading={TERMINAL_ICON}
|
leading={TERMINAL_ICON}
|
||||||
disabled={terminalDisabled}
|
disabled={terminalDisabled}
|
||||||
onSelect={terminalDisabled ? undefined : onCreateTerminal}
|
onSelect={terminalDisabled ? undefined : onCreateTerminal}
|
||||||
/>
|
>
|
||||||
|
{t("workspace.tabs.actions.newTerminal")}
|
||||||
|
</DropdownMenuItem>
|
||||||
{showCreateBrowserTab ? (
|
{showCreateBrowserTab ? (
|
||||||
<PinnableMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-new-tab-menu-browser"
|
testID="workspace-new-tab-menu-browser"
|
||||||
target={BROWSER_TARGET}
|
|
||||||
label={t("workspace.tabs.actions.newBrowser")}
|
|
||||||
leading={BROWSER_ICON}
|
leading={BROWSER_ICON}
|
||||||
onSelect={onCreateBrowser}
|
onSelect={onCreateBrowser}
|
||||||
/>
|
>
|
||||||
|
{t("workspace.tabs.actions.newBrowser")}
|
||||||
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
{isGit ? (
|
{isGit ? (
|
||||||
<PinnableMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-new-tab-menu-changes"
|
testID="workspace-new-tab-menu-changes"
|
||||||
target={CHANGES_TARGET}
|
|
||||||
label={t("workspace.tabs.actions.changes")}
|
|
||||||
leading={CHANGES_ICON}
|
leading={CHANGES_ICON}
|
||||||
onSelect={onOpenChanges}
|
onSelect={onOpenChanges}
|
||||||
/>
|
>
|
||||||
|
{t("workspace.tabs.actions.changes")}
|
||||||
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<PinnableMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-new-tab-menu-files"
|
testID="workspace-new-tab-menu-files"
|
||||||
target={FILES_TARGET}
|
|
||||||
label={t("workspace.tabs.actions.files")}
|
|
||||||
leading={FILES_ICON}
|
leading={FILES_ICON}
|
||||||
onSelect={onOpenFiles}
|
onSelect={onOpenFiles}
|
||||||
/>
|
>
|
||||||
|
{t("workspace.tabs.actions.files")}
|
||||||
|
</DropdownMenuItem>
|
||||||
{showPullRequest ? (
|
{showPullRequest ? (
|
||||||
<PinnableMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-new-tab-menu-pull-request"
|
testID="workspace-new-tab-menu-pull-request"
|
||||||
target={PULL_REQUEST_TARGET}
|
|
||||||
label={t("workspace.tabs.actions.pullRequest")}
|
|
||||||
leading={PULL_REQUEST_ICON}
|
leading={PULL_REQUEST_ICON}
|
||||||
onSelect={onOpenPullRequest}
|
onSelect={onOpenPullRequest}
|
||||||
/>
|
>
|
||||||
|
{t("workspace.tabs.actions.pullRequest")}
|
||||||
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
|
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
|
||||||
{profiles.map((profile) => (
|
{profiles.map((profile) => (
|
||||||
<PinnableProfileMenuItem
|
<TerminalProfileMenuItem
|
||||||
key={profile.id}
|
key={profile.id}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
disabled={terminalDisabled}
|
disabled={terminalDisabled}
|
||||||
onLaunch={onLaunch}
|
onLaunch={onCreateTerminalWithProfile}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -387,20 +333,6 @@ function WorkspaceNewTabButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspacePinnedTargets(
|
|
||||||
options: TabTargetLauncherOptions & { isGit: boolean; showPullRequest: boolean },
|
|
||||||
) {
|
|
||||||
const { onLaunch } = useTabTargetLauncher(options);
|
|
||||||
const launchers = usePinnedLaunchers({
|
|
||||||
serverId: options.normalizedServerId,
|
|
||||||
onLaunch,
|
|
||||||
isGit: options.isGit,
|
|
||||||
hasPullRequest: options.showPullRequest,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <PinnedTargetsRow launchers={launchers} testIdPrefix="workspace-pinned-target" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabContextMenuItem({
|
function TabContextMenuItem({
|
||||||
entry,
|
entry,
|
||||||
}: {
|
}: {
|
||||||
@@ -456,38 +388,8 @@ export interface WorkspaceDesktopTabRowItem {
|
|||||||
isClosingTab: boolean;
|
isClosingTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SplitActionButtonProps {
|
interface ResolvedWorkspaceDesktopTabRowItem extends WorkspaceDesktopTabRowItem {
|
||||||
onPress: () => void;
|
presentation: WorkspaceTabPresentation;
|
||||||
label: string;
|
|
||||||
shortcutKeys: ShortcutKey[][] | null;
|
|
||||||
icon: "split-right" | "split-down";
|
|
||||||
}
|
|
||||||
|
|
||||||
function SplitActionButton({ onPress, label, shortcutKeys, icon }: SplitActionButtonProps) {
|
|
||||||
return (
|
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger
|
|
||||||
onPress={onPress}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={label}
|
|
||||||
style={newTabActionButtonStyle}
|
|
||||||
>
|
|
||||||
{icon === "split-right" ? (
|
|
||||||
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
|
|
||||||
) : (
|
|
||||||
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
|
||||||
<View style={styles.newTabTooltipRow}>
|
|
||||||
<Text style={styles.newTabTooltipText}>{label}</Text>
|
|
||||||
{shortcutKeys ? (
|
|
||||||
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceDesktopTabsRowProps {
|
interface WorkspaceDesktopTabsRowProps {
|
||||||
@@ -515,16 +417,61 @@ interface WorkspaceDesktopTabsRowProps {
|
|||||||
disableCreateTerminal?: boolean;
|
disableCreateTerminal?: boolean;
|
||||||
isWaitingOnTerminalReadiness?: boolean;
|
isWaitingOnTerminalReadiness?: boolean;
|
||||||
onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void;
|
onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void;
|
||||||
onSplitRight: () => void;
|
|
||||||
onSplitDown: () => void;
|
|
||||||
externalDndContext?: boolean;
|
externalDndContext?: boolean;
|
||||||
activeDragTabId?: string | null;
|
activeDragTabId?: string | null;
|
||||||
tabDropPreviewIndex?: number | null;
|
tabDropPreviewIndex?: number | null;
|
||||||
showPaneSplitActions?: boolean;
|
|
||||||
focusModeEnabled: boolean;
|
focusModeEnabled: boolean;
|
||||||
onExitFocusMode: () => void;
|
onExitFocusMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResolvedWorkspaceDesktopTabsRowProps extends Omit<WorkspaceDesktopTabsRowProps, "tabs"> {
|
||||||
|
tabs: ResolvedWorkspaceDesktopTabRowItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkspaceDesktopTabRowsResolverProps {
|
||||||
|
tabs: WorkspaceDesktopTabRowItem[];
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
index: number;
|
||||||
|
resolvedTabs: ResolvedWorkspaceDesktopTabRowItem[];
|
||||||
|
children: (tabs: ResolvedWorkspaceDesktopTabRowItem[]) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_RESOLVED_TAB_ROWS: ResolvedWorkspaceDesktopTabRowItem[] = [];
|
||||||
|
|
||||||
|
function WorkspaceDesktopTabRowsResolver({
|
||||||
|
tabs,
|
||||||
|
serverId,
|
||||||
|
workspaceId,
|
||||||
|
index,
|
||||||
|
resolvedTabs,
|
||||||
|
children,
|
||||||
|
}: WorkspaceDesktopTabRowsResolverProps): React.ReactElement {
|
||||||
|
const item = tabs[index];
|
||||||
|
if (!item) {
|
||||||
|
return <>{children(resolvedTabs)}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkspaceTabPresentationResolver tab={item.tab} serverId={serverId} workspaceId={workspaceId}>
|
||||||
|
{(presentation) => {
|
||||||
|
const nextResolvedTabs = [...resolvedTabs, { ...item, presentation }];
|
||||||
|
return (
|
||||||
|
<WorkspaceDesktopTabRowsResolver
|
||||||
|
tabs={tabs}
|
||||||
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
index={index + 1}
|
||||||
|
resolvedTabs={nextResolvedTabs}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</WorkspaceDesktopTabRowsResolver>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</WorkspaceTabPresentationResolver>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getFallbackTabLabel(
|
function getFallbackTabLabel(
|
||||||
tab: WorkspaceTabDescriptor,
|
tab: WorkspaceTabDescriptor,
|
||||||
labels: {
|
labels: {
|
||||||
@@ -813,7 +760,7 @@ function TabChip({
|
|||||||
|
|
||||||
{showCloseButton ? (
|
{showCloseButton ? (
|
||||||
<View
|
<View
|
||||||
pointerEvents={showCloseControl ? "auto" : "none"}
|
pointerEvents={showCloseControl ? "box-none" : "none"}
|
||||||
style={[
|
style={[
|
||||||
styles.tabTrailingOverlay,
|
styles.tabTrailingOverlay,
|
||||||
showCloseControl ? styles.tabTrailingOverlayShown : styles.tabTrailingOverlayHidden,
|
showCloseControl ? styles.tabTrailingOverlayShown : styles.tabTrailingOverlayHidden,
|
||||||
@@ -865,7 +812,21 @@ function TabChip({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceDesktopTabsRow({
|
export function WorkspaceDesktopTabsRow(props: WorkspaceDesktopTabsRowProps) {
|
||||||
|
return (
|
||||||
|
<WorkspaceDesktopTabRowsResolver
|
||||||
|
tabs={props.tabs}
|
||||||
|
serverId={props.normalizedServerId}
|
||||||
|
workspaceId={props.normalizedWorkspaceId}
|
||||||
|
index={0}
|
||||||
|
resolvedTabs={EMPTY_RESOLVED_TAB_ROWS}
|
||||||
|
>
|
||||||
|
{(tabs) => <ResolvedWorkspaceDesktopTabsRow {...props} tabs={tabs} />}
|
||||||
|
</WorkspaceDesktopTabRowsResolver>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResolvedWorkspaceDesktopTabsRow({
|
||||||
paneId,
|
paneId,
|
||||||
isFocused = false,
|
isFocused = false,
|
||||||
tabs,
|
tabs,
|
||||||
@@ -890,25 +851,22 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
disableCreateTerminal = false,
|
disableCreateTerminal = false,
|
||||||
isWaitingOnTerminalReadiness = false,
|
isWaitingOnTerminalReadiness = false,
|
||||||
onReorderTabs,
|
onReorderTabs,
|
||||||
onSplitRight,
|
|
||||||
onSplitDown,
|
|
||||||
externalDndContext = false,
|
externalDndContext = false,
|
||||||
activeDragTabId = null,
|
activeDragTabId = null,
|
||||||
tabDropPreviewIndex = null,
|
tabDropPreviewIndex = null,
|
||||||
showPaneSplitActions = true,
|
|
||||||
focusModeEnabled,
|
focusModeEnabled,
|
||||||
onExitFocusMode,
|
onExitFocusMode,
|
||||||
}: WorkspaceDesktopTabsRowProps) {
|
}: ResolvedWorkspaceDesktopTabsRowProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
||||||
const focusModeKeys = useShortcutKeys("toggle-focus");
|
const focusModeKeys = useShortcutKeys("toggle-focus");
|
||||||
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
|
||||||
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
|
|
||||||
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
||||||
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
|
|
||||||
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
||||||
const [exitFocusModeWidth, setExitFocusModeWidth] = useState<number>(0);
|
const [exitFocusModeWidth, setExitFocusModeWidth] = useState<number>(0);
|
||||||
|
const [measuredTabLabels, setMeasuredTabLabels] = useState<
|
||||||
|
Record<string, { label: string; width: number }>
|
||||||
|
>({});
|
||||||
const workspaceRoot = useWorkspaceDirectory(normalizedServerId, normalizedWorkspaceId) ?? "";
|
const workspaceRoot = useWorkspaceDirectory(normalizedServerId, normalizedWorkspaceId) ?? "";
|
||||||
const checkoutStatus = useCheckoutStatusQuery({
|
const checkoutStatus = useCheckoutStatusQuery({
|
||||||
serverId: normalizedServerId,
|
serverId: normalizedServerId,
|
||||||
@@ -927,10 +885,6 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
updateMeasuredWidth(setTabsContainerWidth, event);
|
updateMeasuredWidth(setTabsContainerWidth, event);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTabsActionsLayout = useCallback((event: LayoutChangeEvent) => {
|
|
||||||
updateMeasuredWidth(setTabsActionsWidth, event);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleInlineAddButtonLayout = useCallback((event: LayoutChangeEvent) => {
|
const handleInlineAddButtonLayout = useCallback((event: LayoutChangeEvent) => {
|
||||||
updateMeasuredWidth(setInlineAddButtonWidth, event);
|
updateMeasuredWidth(setInlineAddButtonWidth, event);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -944,19 +898,19 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
rowHorizontalInset: 0,
|
rowHorizontalInset: 0,
|
||||||
actionsReservedWidth: Math.max(
|
actionsReservedWidth: Math.max(
|
||||||
0,
|
0,
|
||||||
tabsActionsWidth +
|
(inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH) +
|
||||||
(inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH) +
|
|
||||||
(focusModeEnabled ? exitFocusModeWidth : 0),
|
(focusModeEnabled ? exitFocusModeWidth : 0),
|
||||||
),
|
),
|
||||||
rowPaddingHorizontal: TAB_ROW_PADDING_HORIZONTAL,
|
rowPaddingHorizontal: TAB_ROW_PADDING_HORIZONTAL,
|
||||||
tabGap: TAB_CHIP_GAP,
|
tabGap: TAB_CHIP_GAP,
|
||||||
|
minTabWidth: TAB_MIN_WIDTH,
|
||||||
maxTabWidth: TAB_MAX_WIDTH,
|
maxTabWidth: TAB_MAX_WIDTH,
|
||||||
tabIconWidth: TAB_ICON_WIDTH,
|
tabIconWidth: TAB_ICON_WIDTH,
|
||||||
|
tabContentGap: TAB_CONTENT_GAP,
|
||||||
tabHorizontalPadding: TAB_CHIP_HORIZONTAL_PADDING,
|
tabHorizontalPadding: TAB_CHIP_HORIZONTAL_PADDING,
|
||||||
estimatedCharWidth: 7,
|
|
||||||
closeButtonWidth: TAB_CLOSE_BUTTON_RESERVED_WIDTH,
|
closeButtonWidth: TAB_CLOSE_BUTTON_RESERVED_WIDTH,
|
||||||
}),
|
}),
|
||||||
[exitFocusModeWidth, focusModeEnabled, inlineAddButtonWidth, tabsActionsWidth],
|
[exitFocusModeWidth, focusModeEnabled, inlineAddButtonWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fallbackTabLabels = useMemo(
|
const fallbackTabLabels = useMemo(
|
||||||
@@ -989,23 +943,49 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
}),
|
}),
|
||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
const tabLabelLengths = useMemo(
|
const tabLabels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
tabs.map((tab) => {
|
tabs.map((tab) => {
|
||||||
const label = getFallbackTabLabel(tab.tab, fallbackTabLabels);
|
const label =
|
||||||
return label.length;
|
tab.presentation.titleState === "loading"
|
||||||
|
? getFallbackTabLabel(tab.tab, fallbackTabLabels)
|
||||||
|
: tab.presentation.label;
|
||||||
|
return { key: tab.tab.key, label };
|
||||||
}),
|
}),
|
||||||
[fallbackTabLabels, tabs],
|
[fallbackTabLabels, tabs],
|
||||||
);
|
);
|
||||||
|
const tabLabelWidths = useMemo(
|
||||||
|
() =>
|
||||||
|
tabLabels.map(({ key, label }) => {
|
||||||
|
const measurement = measuredTabLabels[key];
|
||||||
|
return measurement?.label === label
|
||||||
|
? measurement.width + TAB_LABEL_LAYOUT_ALLOWANCE
|
||||||
|
: label.length * TAB_LABEL_FALLBACK_CHARACTER_WIDTH + TAB_LABEL_LAYOUT_ALLOWANCE;
|
||||||
|
}),
|
||||||
|
[measuredTabLabels, tabLabels],
|
||||||
|
);
|
||||||
|
const handleTabLabelLayout = useCallback(
|
||||||
|
(key: string, label: string, event: LayoutChangeEvent) => {
|
||||||
|
const width = Math.ceil(event.nativeEvent.layout.width);
|
||||||
|
setMeasuredTabLabels((current) => {
|
||||||
|
const existing = current[key];
|
||||||
|
if (existing?.label === label && existing.width === width) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return { ...current, [key]: { label, width } };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const { layout } = useWorkspaceTabLayout({
|
const { layout } = useWorkspaceTabLayout({
|
||||||
tabLabelLengths,
|
tabLabelWidths,
|
||||||
viewportWidthOverride: tabsContainerWidth > 0 ? tabsContainerWidth : null,
|
viewportWidthOverride: tabsContainerWidth > 0 ? tabsContainerWidth : null,
|
||||||
metrics: layoutMetrics,
|
metrics: layoutMetrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(nextTabs: WorkspaceDesktopTabRowItem[]) => {
|
(nextTabs: ResolvedWorkspaceDesktopTabRowItem[]) => {
|
||||||
onReorderTabs(nextTabs.map((tab) => tab.tab));
|
onReorderTabs(nextTabs.map((tab) => tab.tab));
|
||||||
},
|
},
|
||||||
[onReorderTabs],
|
[onReorderTabs],
|
||||||
@@ -1013,7 +993,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
|
|
||||||
const getTabDragData = useMemo(() => {
|
const getTabDragData = useMemo(() => {
|
||||||
if (!paneId) return undefined;
|
if (!paneId) return undefined;
|
||||||
return (tab: WorkspaceDesktopTabRowItem) => ({
|
return (tab: ResolvedWorkspaceDesktopTabRowItem) => ({
|
||||||
kind: "workspace-tab" as const,
|
kind: "workspace-tab" as const,
|
||||||
paneId,
|
paneId,
|
||||||
tabId: tab.tab.tabId,
|
tabId: tab.tab.tabId,
|
||||||
@@ -1070,7 +1050,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
index,
|
index,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
isActive,
|
isActive,
|
||||||
}: DraggableRenderItemInfo<WorkspaceDesktopTabRowItem>) => {
|
}: DraggableRenderItemInfo<ResolvedWorkspaceDesktopTabRowItem>) => {
|
||||||
const shouldShowCloseButton = layout.closeButtonPolicy === "all";
|
const shouldShowCloseButton = layout.closeButtonPolicy === "all";
|
||||||
const layoutItem = layout.items[index] ?? null;
|
const layoutItem = layout.items[index] ?? null;
|
||||||
const resolvedTabWidth = layoutItem?.width ?? 150;
|
const resolvedTabWidth = layoutItem?.width ?? 150;
|
||||||
@@ -1089,8 +1069,6 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
isDragging={isActive}
|
isDragging={isActive}
|
||||||
index={index}
|
index={index}
|
||||||
tabCount={tabs.length}
|
tabCount={tabs.length}
|
||||||
normalizedServerId={normalizedServerId}
|
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
onCopyTerminalId={onCopyTerminalId}
|
onCopyTerminalId={onCopyTerminalId}
|
||||||
@@ -1118,8 +1096,6 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
isFocused,
|
isFocused,
|
||||||
layout.closeButtonPolicy,
|
layout.closeButtonPolicy,
|
||||||
layout.items,
|
layout.items,
|
||||||
normalizedServerId,
|
|
||||||
normalizedWorkspaceId,
|
|
||||||
onCloseOtherTabs,
|
onCloseOtherTabs,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
@@ -1154,6 +1130,21 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
testID="workspace-tabs-row"
|
testID="workspace-tabs-row"
|
||||||
onLayout={handleTabsContainerLayout}
|
onLayout={handleTabsContainerLayout}
|
||||||
>
|
>
|
||||||
|
<View
|
||||||
|
style={styles.tabLabelMeasurements}
|
||||||
|
pointerEvents="none"
|
||||||
|
accessibilityElementsHidden
|
||||||
|
importantForAccessibility="no-hide-descendants"
|
||||||
|
>
|
||||||
|
{tabLabels.map(({ key, label }) => (
|
||||||
|
<TabLabelMeasurement
|
||||||
|
key={`${key}:${label}`}
|
||||||
|
tabKey={key}
|
||||||
|
label={label}
|
||||||
|
onMeasure={handleTabLabelLayout}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
{focusModeEnabled ? (
|
{focusModeEnabled ? (
|
||||||
<View style={styles.exitFocusModeSlot} onLayout={handleExitFocusModeLayout}>
|
<View style={styles.exitFocusModeSlot} onLayout={handleExitFocusModeLayout}>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
@@ -1198,26 +1189,29 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
getItemData={getTabDragData}
|
getItemData={getTabDragData}
|
||||||
renderItem={renderTab}
|
renderItem={renderTab}
|
||||||
/>
|
/>
|
||||||
|
{!layout.requiresHorizontalScrollFallback ? (
|
||||||
|
<WorkspaceNewTabButton
|
||||||
|
shortcutKeys={newTabKeys}
|
||||||
|
onCreateAgentTab={handleCreateAgentTab}
|
||||||
|
onCreateTerminal={handleCreateTerminal}
|
||||||
|
onCreateBrowser={handleCreateBrowser}
|
||||||
|
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
|
||||||
|
onOpenChanges={handleOpenChanges}
|
||||||
|
onOpenFiles={handleOpenFiles}
|
||||||
|
onOpenPullRequest={handleOpenPullRequest}
|
||||||
|
onEditProfiles={handleEditProfiles}
|
||||||
|
normalizedServerId={normalizedServerId}
|
||||||
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
|
terminalDisabled={terminalDisabled}
|
||||||
|
isGit={isGit}
|
||||||
|
showPullRequest={showPullRequest}
|
||||||
|
onLayout={handleInlineAddButtonLayout}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<WorkspaceNewTabButton
|
{layout.requiresHorizontalScrollFallback ? (
|
||||||
shortcutKeys={newTabKeys}
|
<WorkspaceNewTabButton
|
||||||
onCreateAgentTab={handleCreateAgentTab}
|
shortcutKeys={newTabKeys}
|
||||||
onCreateTerminal={handleCreateTerminal}
|
|
||||||
onCreateBrowser={handleCreateBrowser}
|
|
||||||
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
|
|
||||||
onOpenChanges={handleOpenChanges}
|
|
||||||
onOpenFiles={handleOpenFiles}
|
|
||||||
onOpenPullRequest={handleOpenPullRequest}
|
|
||||||
onEditProfiles={handleEditProfiles}
|
|
||||||
normalizedServerId={normalizedServerId}
|
|
||||||
showCreateBrowserTab={showCreateBrowserTab}
|
|
||||||
terminalDisabled={terminalDisabled}
|
|
||||||
isGit={isGit}
|
|
||||||
showPullRequest={showPullRequest}
|
|
||||||
onLayout={handleInlineAddButtonLayout}
|
|
||||||
/>
|
|
||||||
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
|
|
||||||
<WorkspacePinnedTargets
|
|
||||||
onCreateAgentTab={handleCreateAgentTab}
|
onCreateAgentTab={handleCreateAgentTab}
|
||||||
onCreateTerminal={handleCreateTerminal}
|
onCreateTerminal={handleCreateTerminal}
|
||||||
onCreateBrowser={handleCreateBrowser}
|
onCreateBrowser={handleCreateBrowser}
|
||||||
@@ -1225,27 +1219,15 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onOpenChanges={handleOpenChanges}
|
onOpenChanges={handleOpenChanges}
|
||||||
onOpenFiles={handleOpenFiles}
|
onOpenFiles={handleOpenFiles}
|
||||||
onOpenPullRequest={handleOpenPullRequest}
|
onOpenPullRequest={handleOpenPullRequest}
|
||||||
|
onEditProfiles={handleEditProfiles}
|
||||||
normalizedServerId={normalizedServerId}
|
normalizedServerId={normalizedServerId}
|
||||||
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
|
terminalDisabled={terminalDisabled}
|
||||||
isGit={isGit}
|
isGit={isGit}
|
||||||
showPullRequest={showPullRequest}
|
showPullRequest={showPullRequest}
|
||||||
|
onLayout={handleInlineAddButtonLayout}
|
||||||
/>
|
/>
|
||||||
{showPaneSplitActions ? (
|
) : null}
|
||||||
<>
|
|
||||||
<SplitActionButton
|
|
||||||
icon="split-right"
|
|
||||||
onPress={onSplitRight}
|
|
||||||
label={t("workspace.tabs.actions.splitRight")}
|
|
||||||
shortcutKeys={splitRightKeys}
|
|
||||||
/>
|
|
||||||
<SplitActionButton
|
|
||||||
icon="split-down"
|
|
||||||
onPress={onSplitDown}
|
|
||||||
label={t("workspace.tabs.actions.splitDown")}
|
|
||||||
shortcutKeys={splitDownKeys}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1257,8 +1239,6 @@ function ResolvedDesktopTabChip({
|
|||||||
isDragging,
|
isDragging,
|
||||||
index,
|
index,
|
||||||
tabCount,
|
tabCount,
|
||||||
normalizedServerId,
|
|
||||||
normalizedWorkspaceId,
|
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
onCopyTerminalId,
|
onCopyTerminalId,
|
||||||
@@ -1279,13 +1259,11 @@ function ResolvedDesktopTabChip({
|
|||||||
showDropIndicatorBefore,
|
showDropIndicatorBefore,
|
||||||
showDropIndicatorAfter,
|
showDropIndicatorAfter,
|
||||||
}: {
|
}: {
|
||||||
item: WorkspaceDesktopTabRowItem;
|
item: ResolvedWorkspaceDesktopTabRowItem;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
tabCount: number;
|
tabCount: number;
|
||||||
normalizedServerId: string;
|
|
||||||
normalizedWorkspaceId: string;
|
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
|
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
|
||||||
@@ -1307,6 +1285,7 @@ function ResolvedDesktopTabChip({
|
|||||||
showDropIndicatorAfter: boolean;
|
showDropIndicatorAfter: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const presentation = item.presentation;
|
||||||
const resolvedTab = useMemo(
|
const resolvedTab = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildWorkspaceDesktopTabActions({
|
buildWorkspaceDesktopTabActions({
|
||||||
@@ -1343,48 +1322,38 @@ function ResolvedDesktopTabChip({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const tooltipLabel =
|
||||||
<WorkspaceTabPresentationResolver
|
presentation.titleState === "loading"
|
||||||
tab={item.tab}
|
? t("workspace.tabs.loadingAgentTitle")
|
||||||
serverId={normalizedServerId}
|
: presentation.tooltip;
|
||||||
workspaceId={normalizedWorkspaceId}
|
|
||||||
>
|
|
||||||
{(presentation) => {
|
|
||||||
const tooltipLabel =
|
|
||||||
presentation.titleState === "loading"
|
|
||||||
? t("workspace.tabs.loadingAgentTitle")
|
|
||||||
: presentation.tooltip;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.tabSlot}>
|
<View style={styles.tabSlot}>
|
||||||
{showDropIndicatorBefore ? (
|
{showDropIndicatorBefore ? (
|
||||||
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} />
|
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} />
|
||||||
) : null}
|
) : null}
|
||||||
<TabChip
|
<TabChip
|
||||||
tab={item.tab}
|
tab={item.tab}
|
||||||
isActive={item.isActive}
|
isActive={item.isActive}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
isFocused={isFocused}
|
isFocused={isFocused}
|
||||||
resolvedTabWidth={resolvedTabWidth}
|
resolvedTabWidth={resolvedTabWidth}
|
||||||
showLabel={showLabel}
|
showLabel={showLabel}
|
||||||
showCloseButton={showCloseButton}
|
showCloseButton={showCloseButton}
|
||||||
isCloseHovered={item.isCloseHovered}
|
isCloseHovered={item.isCloseHovered}
|
||||||
isClosingTab={item.isClosingTab}
|
isClosingTab={item.isClosingTab}
|
||||||
presentation={presentation}
|
presentation={presentation}
|
||||||
tooltipLabel={tooltipLabel}
|
tooltipLabel={tooltipLabel}
|
||||||
resolvedTab={resolvedTab}
|
resolvedTab={resolvedTab}
|
||||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||||
onNavigateTab={onNavigateTab}
|
onNavigateTab={onNavigateTab}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
dragHandleProps={dragHandleProps}
|
dragHandleProps={dragHandleProps}
|
||||||
/>
|
/>
|
||||||
{showDropIndicatorAfter ? (
|
{showDropIndicatorAfter ? (
|
||||||
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} />
|
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} />
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
|
||||||
}}
|
|
||||||
</WorkspaceTabPresentationResolver>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1413,11 +1382,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: TAB_ROW_PADDING_HORIZONTAL,
|
paddingHorizontal: TAB_ROW_PADDING_HORIZONTAL,
|
||||||
},
|
},
|
||||||
tabsActions: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: theme.spacing[2],
|
|
||||||
},
|
|
||||||
exitFocusModeSlot: {
|
exitFocusModeSlot: {
|
||||||
alignSelf: "stretch",
|
alignSelf: "stretch",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -1498,6 +1462,17 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
},
|
},
|
||||||
|
tabLabelMeasurements: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
opacity: 0,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
|
tabLabelMeasurement: {
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
tabLabelSkeleton: {
|
tabLabelSkeleton: {
|
||||||
width: 96,
|
width: 96,
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
@@ -1522,6 +1497,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
tabTrailingOverlayShown: {
|
tabTrailingOverlayShown: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@@ -1553,13 +1529,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: theme.colors.foregroundMuted,
|
backgroundColor: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
newTabActionButton: {
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
inlineAddActionButton: {
|
inlineAddActionButton: {
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
@@ -1567,9 +1536,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
newTabActionButtonDisabled: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
newTabActionButtonHovered: {
|
newTabActionButtonHovered: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
@@ -1596,8 +1562,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
terminalProfileIconWrapper: {
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ import { findAdjacentPane } from "@/utils/split-navigation";
|
|||||||
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||||
import { getIsElectron, isNative, isWeb } from "@/constants/platform";
|
import { getIsElectron, isNative, isWeb } from "@/constants/platform";
|
||||||
import type { SurfaceBackdrop } from "@/styles/surface-backdrop";
|
import type { SurfaceBackdrop } from "@/styles/surface-backdrop";
|
||||||
import { useContainerWidthBelow } from "@/hooks/use-container-width";
|
|
||||||
import {
|
import {
|
||||||
buildHostRootRoute,
|
buildHostRootRoute,
|
||||||
buildSettingsHostRoute,
|
buildSettingsHostRoute,
|
||||||
@@ -530,8 +529,6 @@ function WorkspaceDocumentTitleEffect({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
function switcherTriggerStyle({ pressed }: { pressed?: boolean }) {
|
function switcherTriggerStyle({ pressed }: { pressed?: boolean }) {
|
||||||
return [styles.switcherTrigger, Boolean(pressed) && styles.switcherTriggerPressed];
|
return [styles.switcherTrigger, Boolean(pressed) && styles.switcherTriggerPressed];
|
||||||
}
|
}
|
||||||
@@ -2112,8 +2109,6 @@ function WorkspaceScreenContent({
|
|||||||
);
|
);
|
||||||
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
||||||
const { closingTabIds, closeTab } = useCloseTabs();
|
const { closingTabIds, closeTab } = useCloseTabs();
|
||||||
const { onLayout: onHeaderLayout, isBelow: showCompactButtonLabels } =
|
|
||||||
useContainerWidthBelow(700);
|
|
||||||
const closeWorkspaceTabWithCleanup = useCallback(
|
const closeWorkspaceTabWithCleanup = useCallback(
|
||||||
function closeWorkspaceTabWithCleanup(input: {
|
function closeWorkspaceTabWithCleanup(input: {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -3616,11 +3611,7 @@ function WorkspaceScreenContent({
|
|||||||
) : null}
|
) : null}
|
||||||
{!isMobile && workspaceDirectory ? (
|
{!isMobile && workspaceDirectory ? (
|
||||||
<>
|
<>
|
||||||
<WorkspaceActions
|
<WorkspaceActions serverId={normalizedServerId} cwd={workspaceDirectory} />
|
||||||
serverId={normalizedServerId}
|
|
||||||
cwd={workspaceDirectory}
|
|
||||||
hideLabels={showCompactButtonLabels}
|
|
||||||
/>
|
|
||||||
{isGitCheckout ? (
|
{isGitCheckout ? (
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -3727,7 +3718,6 @@ function WorkspaceScreenContent({
|
|||||||
handleScriptTerminalStarted,
|
handleScriptTerminalStarted,
|
||||||
handleViewScriptTerminal,
|
handleViewScriptTerminal,
|
||||||
handleOpenUrlInBrowserTab,
|
handleOpenUrlInBrowserTab,
|
||||||
showCompactButtonLabels,
|
|
||||||
isGitCheckout,
|
isGitCheckout,
|
||||||
handleToggleExplorer,
|
handleToggleExplorer,
|
||||||
isExplorerOpen,
|
isExplorerOpen,
|
||||||
@@ -3845,7 +3835,6 @@ function WorkspaceScreenContent({
|
|||||||
<View style={styles.centerColumn}>
|
<View style={styles.centerColumn}>
|
||||||
{showScreenHeader && (
|
{showScreenHeader && (
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
onRowLayout={onHeaderLayout}
|
|
||||||
left={
|
left={
|
||||||
<>
|
<>
|
||||||
<SidebarMenuToggle />
|
<SidebarMenuToggle />
|
||||||
@@ -3938,9 +3927,6 @@ function WorkspaceScreenContent({
|
|||||||
disableCreateTerminal={createTerminalMutation.isPending}
|
disableCreateTerminal={createTerminalMutation.isPending}
|
||||||
isWaitingOnTerminalReadiness={pendingTerminalCreateInput !== null}
|
isWaitingOnTerminalReadiness={pendingTerminalCreateInput !== null}
|
||||||
onReorderTabs={handleReorderTabsInFocusedPane}
|
onReorderTabs={handleReorderTabsInFocusedPane}
|
||||||
onSplitRight={noop}
|
|
||||||
onSplitDown={noop}
|
|
||||||
showPaneSplitActions={false}
|
|
||||||
focusModeEnabled={desktopFocusModeEnabled}
|
focusModeEnabled={desktopFocusModeEnabled}
|
||||||
onExitFocusMode={toggleFocusMode}
|
onExitFocusMode={toggleFocusMode}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ const metrics = {
|
|||||||
actionsReservedWidth: 120,
|
actionsReservedWidth: 120,
|
||||||
rowPaddingHorizontal: 8,
|
rowPaddingHorizontal: 8,
|
||||||
tabGap: 4,
|
tabGap: 4,
|
||||||
maxTabWidth: 160,
|
minTabWidth: 96,
|
||||||
|
maxTabWidth: 240,
|
||||||
tabIconWidth: 14,
|
tabIconWidth: 14,
|
||||||
|
tabContentGap: 4,
|
||||||
tabHorizontalPadding: 8,
|
tabHorizontalPadding: 8,
|
||||||
estimatedCharWidth: 7,
|
|
||||||
closeButtonWidth: 0,
|
closeButtonWidth: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("computeWorkspaceTabLayout", () => {
|
describe("computeWorkspaceTabLayout", () => {
|
||||||
it("caps equal-width tabs at the ideal width when there is extra horizontal space", () => {
|
it("keeps each tab at its natural content width when space is available", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 1200,
|
viewportWidth: 1200,
|
||||||
tabLabelLengths: [8, 10, 7],
|
tabLabelWidths: [56, 70, 49],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,26 +26,37 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||||
expect(result.items).toHaveLength(3);
|
expect(result.items).toHaveLength(3);
|
||||||
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
||||||
expect(result.items.map((item) => item.width)).toEqual([160, 160, 160]);
|
expect(result.items.map((item) => item.width)).toEqual([96, 104, 96]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shrinks equal-width tabs proportionally to fit the pane", () => {
|
it("sizes a single tab between the minimum and maximum from its content", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 520,
|
viewportWidth: 1200,
|
||||||
tabLabelLengths: [24, 12, 8],
|
tabLabelWidths: [105],
|
||||||
|
metrics,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||||
|
expect(result.items.map((item) => item.width)).toEqual([139]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shrinks natural widths proportionally without crossing the clickable minimum", () => {
|
||||||
|
const result = computeWorkspaceTabLayout({
|
||||||
|
viewportWidth: 460,
|
||||||
|
tabLabelWidths: [168, 84, 56],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.closeButtonPolicy).toBe("all");
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||||
expect(result.items.map((item) => item.width)).toEqual([125, 125, 125]);
|
expect(result.items.map((item) => item.width)).toEqual([119, 101, 96]);
|
||||||
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps evenly sized tabs at the cap when the split width exceeds it", () => {
|
it("caps long tabs at the maximum width", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 743,
|
viewportWidth: 1004,
|
||||||
tabLabelLengths: [8, 8, 8, 8],
|
tabLabelWidths: [280, 280, 280, 280],
|
||||||
metrics: {
|
metrics: {
|
||||||
...metrics,
|
...metrics,
|
||||||
actionsReservedWidth: 44,
|
actionsReservedWidth: 44,
|
||||||
@@ -55,39 +67,39 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
|
|
||||||
expect(result.closeButtonPolicy).toBe("all");
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||||
expect(result.items.map((item) => item.width)).toEqual([160, 160, 160, 160]);
|
expect(result.items.map((item) => item.width)).toEqual([240, 240, 240, 240]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("collapses to icon-only before allowing horizontal scroll fallback", () => {
|
it("keeps every tab at the clickable minimum at the exact fit boundary", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 268,
|
viewportWidth: 532,
|
||||||
tabLabelLengths: [14, 14, 14, 14],
|
tabLabelWidths: [98, 98, 98, 98],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.closeButtonPolicy).toBe("all");
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||||
expect(result.items.map((item) => item.width)).toEqual([30, 30, 30, 30]);
|
expect(result.items.map((item) => item.width)).toEqual([96, 96, 96, 96]);
|
||||||
expect(result.items.every((item) => !item.showLabel)).toBe(true);
|
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows horizontal scroll only when icon-only tabs still cannot fit", () => {
|
it("uses horizontal scroll rather than shrinking below the clickable minimum", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 267,
|
viewportWidth: 531,
|
||||||
tabLabelLengths: [14, 14, 14, 14],
|
tabLabelWidths: [98, 98, 98, 98],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.closeButtonPolicy).toBe("all");
|
expect(result.closeButtonPolicy).toBe("all");
|
||||||
expect(result.requiresHorizontalScrollFallback).toBe(true);
|
expect(result.requiresHorizontalScrollFallback).toBe(true);
|
||||||
expect(result.items.map((item) => item.width)).toEqual([30, 30, 30, 30]);
|
expect(result.items.map((item) => item.width)).toEqual([96, 96, 96, 96]);
|
||||||
expect(result.items.every((item) => !item.showLabel)).toBe(true);
|
expect(result.items.every((item) => item.showLabel)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty layout details when there are no tabs", () => {
|
it("returns empty layout details when there are no tabs", () => {
|
||||||
const result = computeWorkspaceTabLayout({
|
const result = computeWorkspaceTabLayout({
|
||||||
viewportWidth: 1200,
|
viewportWidth: 1200,
|
||||||
tabLabelLengths: [],
|
tabLabelWidths: [],
|
||||||
metrics,
|
metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,4 +107,14 @@ describe("computeWorkspaceTabLayout", () => {
|
|||||||
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
expect(result.requiresHorizontalScrollFallback).toBe(false);
|
||||||
expect(result.items).toEqual([]);
|
expect(result.items).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses rendered label width rather than character count", () => {
|
||||||
|
const result = computeWorkspaceTabLayout({
|
||||||
|
viewportWidth: 1200,
|
||||||
|
tabLabelWidths: [68, 104],
|
||||||
|
metrics,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.items.map((item) => item.width)).toEqual([102, 138]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
export type WorkspaceTabCloseButtonPolicy = "all";
|
export type WorkspaceTabCloseButtonPolicy = "all";
|
||||||
|
|
||||||
|
export interface WorkspaceTabLayoutMetrics {
|
||||||
|
rowHorizontalInset: number;
|
||||||
|
actionsReservedWidth: number;
|
||||||
|
rowPaddingHorizontal: number;
|
||||||
|
tabGap: number;
|
||||||
|
minTabWidth: number;
|
||||||
|
maxTabWidth: number;
|
||||||
|
tabIconWidth: number;
|
||||||
|
tabContentGap: number;
|
||||||
|
tabHorizontalPadding: number;
|
||||||
|
closeButtonWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkspaceTabLayoutInput {
|
export interface WorkspaceTabLayoutInput {
|
||||||
viewportWidth: number;
|
viewportWidth: number;
|
||||||
tabLabelLengths: number[];
|
tabLabelWidths: number[];
|
||||||
metrics: {
|
metrics: WorkspaceTabLayoutMetrics;
|
||||||
rowHorizontalInset: number;
|
|
||||||
actionsReservedWidth: number;
|
|
||||||
rowPaddingHorizontal: number;
|
|
||||||
tabGap: number;
|
|
||||||
maxTabWidth: number;
|
|
||||||
tabIconWidth: number;
|
|
||||||
tabHorizontalPadding: number;
|
|
||||||
estimatedCharWidth: number;
|
|
||||||
closeButtonWidth: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceTabLayoutItem {
|
export interface WorkspaceTabLayoutItem {
|
||||||
width: number;
|
width: number;
|
||||||
showLabel: boolean;
|
showLabel: boolean;
|
||||||
labelCharCap: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceTabLayoutResult {
|
export interface WorkspaceTabLayoutResult {
|
||||||
@@ -41,7 +43,7 @@ function clamp(value: number, min: number, max: number): number {
|
|||||||
export function computeWorkspaceTabLayout(
|
export function computeWorkspaceTabLayout(
|
||||||
input: WorkspaceTabLayoutInput,
|
input: WorkspaceTabLayoutInput,
|
||||||
): WorkspaceTabLayoutResult {
|
): WorkspaceTabLayoutResult {
|
||||||
const tabCount = input.tabLabelLengths.length;
|
const tabCount = input.tabLabelWidths.length;
|
||||||
if (tabCount === 0) {
|
if (tabCount === 0) {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
@@ -57,31 +59,39 @@ export function computeWorkspaceTabLayout(
|
|||||||
const rowOverhead =
|
const rowOverhead =
|
||||||
input.metrics.rowPaddingHorizontal * 2 + Math.max(tabCount - 1, 0) * input.metrics.tabGap;
|
input.metrics.rowPaddingHorizontal * 2 + Math.max(tabCount - 1, 0) * input.metrics.tabGap;
|
||||||
const availableTabsWidth = Math.max(0, availableWidth - rowOverhead);
|
const availableTabsWidth = Math.max(0, availableWidth - rowOverhead);
|
||||||
const iconOnlyTabWidth =
|
const tabChromeWidth =
|
||||||
input.metrics.tabIconWidth +
|
input.metrics.tabIconWidth +
|
||||||
|
input.metrics.tabContentGap +
|
||||||
input.metrics.tabHorizontalPadding * 2 +
|
input.metrics.tabHorizontalPadding * 2 +
|
||||||
input.metrics.closeButtonWidth;
|
input.metrics.closeButtonWidth;
|
||||||
const iconOnlyTotalTabsWidth = iconOnlyTabWidth * tabCount;
|
const naturalWidths = input.tabLabelWidths.map((labelWidth) =>
|
||||||
const requiresHorizontalScrollFallback = availableTabsWidth < iconOnlyTotalTabsWidth;
|
clamp(tabChromeWidth + labelWidth, input.metrics.minTabWidth, input.metrics.maxTabWidth),
|
||||||
const resolvedWidth = requiresHorizontalScrollFallback
|
);
|
||||||
? iconOnlyTabWidth
|
const naturalTotalWidth = naturalWidths.reduce((total, width) => total + width, 0);
|
||||||
: clamp(availableTabsWidth / tabCount, iconOnlyTabWidth, input.metrics.maxTabWidth);
|
const minimumTotalWidth = input.metrics.minTabWidth * tabCount;
|
||||||
const resolvedWidths = Array.from({ length: tabCount }, () => resolvedWidth);
|
const requiresHorizontalScrollFallback = availableTabsWidth < minimumTotalWidth;
|
||||||
|
|
||||||
|
let resolvedWidths = naturalWidths;
|
||||||
|
if (requiresHorizontalScrollFallback) {
|
||||||
|
resolvedWidths = Array.from({ length: tabCount }, () => input.metrics.minTabWidth);
|
||||||
|
} else if (naturalTotalWidth > availableTabsWidth) {
|
||||||
|
const widthToRemove = naturalTotalWidth - availableTabsWidth;
|
||||||
|
const shrinkCapacity = naturalTotalWidth - minimumTotalWidth;
|
||||||
|
const shrinkRatio = widthToRemove / shrinkCapacity;
|
||||||
|
resolvedWidths = naturalWidths.map(
|
||||||
|
(width) => width - (width - input.metrics.minTabWidth) * shrinkRatio,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const roundedWidths = resolvedWidths.map((width) =>
|
const roundedWidths = resolvedWidths.map((width) =>
|
||||||
Math.round(clamp(width, iconOnlyTabWidth, input.metrics.maxTabWidth)),
|
Math.round(clamp(width, input.metrics.minTabWidth, input.metrics.maxTabWidth)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: roundedWidths.map((width) => {
|
items: roundedWidths.map((width) => ({
|
||||||
const rawCharCap = Math.floor((width - iconOnlyTabWidth) / input.metrics.estimatedCharWidth);
|
width,
|
||||||
const labelCharCap = Math.max(0, rawCharCap);
|
showLabel: width > tabChromeWidth,
|
||||||
return {
|
})),
|
||||||
width,
|
|
||||||
showLabel: labelCharCap > 0,
|
|
||||||
labelCharCap,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
closeButtonPolicy: "all",
|
closeButtonPolicy: "all",
|
||||||
requiresHorizontalScrollFallback,
|
requiresHorizontalScrollFallback,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export interface PanelState {
|
|||||||
explorerTab: ExplorerTab;
|
explorerTab: ExplorerTab;
|
||||||
explorerTabByCheckout: Record<string, ExplorerTab>;
|
explorerTabByCheckout: Record<string, ExplorerTab>;
|
||||||
expandedPathsByWorkspace: Record<string, string[]>;
|
expandedPathsByWorkspace: Record<string, string[]>;
|
||||||
diffExpandedPathsByWorkspace: Record<string, string[]>;
|
|
||||||
// Changes-view folder tree. Inverted semantics vs the fields above:
|
// Changes-view folder tree. Inverted semantics vs the fields above:
|
||||||
// this stores COLLAPSED directory paths (empty = all folders expanded), keyed
|
// this stores COLLAPSED directory paths (empty = all folders expanded), keyed
|
||||||
// by full uncompressed dir path, so folders default to expanded and new
|
// by full uncompressed dir path, so folders default to expanded and new
|
||||||
@@ -111,7 +110,6 @@ export interface PanelState {
|
|||||||
setExplorerTab: (tab: ExplorerTab) => void;
|
setExplorerTab: (tab: ExplorerTab) => void;
|
||||||
setExplorerTabForCheckout: (params: ExplorerCheckoutContext & { tab: ExplorerTab }) => void;
|
setExplorerTabForCheckout: (params: ExplorerCheckoutContext & { tab: ExplorerTab }) => void;
|
||||||
setExpandedPathsForWorkspace: (workspaceKey: string, paths: ExpandedPathsUpdate) => void;
|
setExpandedPathsForWorkspace: (workspaceKey: string, paths: ExpandedPathsUpdate) => void;
|
||||||
setDiffExpandedPathsForWorkspace: (workspaceKey: string, paths: string[]) => void;
|
|
||||||
setDiffCollapsedFoldersForWorkspace: (workspaceKey: string, dirPaths: string[]) => void;
|
setDiffCollapsedFoldersForWorkspace: (workspaceKey: string, dirPaths: string[]) => void;
|
||||||
activateExplorerTabForCheckout: (checkout: ExplorerCheckoutContext) => void;
|
activateExplorerTabForCheckout: (checkout: ExplorerCheckoutContext) => void;
|
||||||
setSidebarWidth: (width: number) => void;
|
setSidebarWidth: (width: number) => void;
|
||||||
@@ -148,7 +146,6 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
explorerTab: "changes",
|
explorerTab: "changes",
|
||||||
explorerTabByCheckout: {},
|
explorerTabByCheckout: {},
|
||||||
expandedPathsByWorkspace: {},
|
expandedPathsByWorkspace: {},
|
||||||
diffExpandedPathsByWorkspace: {},
|
|
||||||
diffCollapsedFoldersByWorkspace: {},
|
diffCollapsedFoldersByWorkspace: {},
|
||||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||||
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||||
@@ -276,13 +273,6 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
setDiffExpandedPathsForWorkspace: (workspaceKey, paths) =>
|
|
||||||
set((state) => ({
|
|
||||||
diffExpandedPathsByWorkspace: {
|
|
||||||
...state.diffExpandedPathsByWorkspace,
|
|
||||||
[workspaceKey]: paths,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
setDiffCollapsedFoldersForWorkspace: (workspaceKey, dirPaths) =>
|
setDiffCollapsedFoldersForWorkspace: (workspaceKey, dirPaths) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
diffCollapsedFoldersByWorkspace: {
|
diffCollapsedFoldersByWorkspace: {
|
||||||
@@ -308,7 +298,7 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "panel-state",
|
name: "panel-state",
|
||||||
version: 13,
|
version: 14,
|
||||||
storage: createValidatedPersistStorage(AsyncStorage, PanelPersistedStateSchema),
|
storage: createValidatedPersistStorage(AsyncStorage, PanelPersistedStateSchema),
|
||||||
migrate: (persistedState, version) => migratePanelState(persistedState, version, { isWeb }),
|
migrate: (persistedState, version) => migratePanelState(persistedState, version, { isWeb }),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
@@ -316,7 +306,6 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
explorerTab: state.explorerTab,
|
explorerTab: state.explorerTab,
|
||||||
explorerTabByCheckout: state.explorerTabByCheckout,
|
explorerTabByCheckout: state.explorerTabByCheckout,
|
||||||
expandedPathsByWorkspace: state.expandedPathsByWorkspace,
|
expandedPathsByWorkspace: state.expandedPathsByWorkspace,
|
||||||
diffExpandedPathsByWorkspace: state.diffExpandedPathsByWorkspace,
|
|
||||||
diffCollapsedFoldersByWorkspace: state.diffCollapsedFoldersByWorkspace,
|
diffCollapsedFoldersByWorkspace: state.diffCollapsedFoldersByWorkspace,
|
||||||
sidebarWidth: state.sidebarWidth,
|
sidebarWidth: state.sidebarWidth,
|
||||||
explorerWidth: state.explorerWidth,
|
explorerWidth: state.explorerWidth,
|
||||||
|
|||||||
@@ -120,6 +120,14 @@ describe("panel-store migration", () => {
|
|||||||
expect(state.diffCollapsedFoldersByWorkspace).toEqual({ ws: ["src/app"] });
|
expect(state.diffCollapsedFoldersByWorkspace).toEqual({ ws: ["src/app"] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("drops the retired per-file diff expansion state", () => {
|
||||||
|
const state = migratePanelState({ diffExpandedPathsByWorkspace: { ws: ["src/app.ts"] } }, 13, {
|
||||||
|
isWeb: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.diffExpandedPathsByWorkspace).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("replaces the dead explorer split ratio with the shared tree rail width", () => {
|
it("replaces the dead explorer split ratio with the shared tree rail width", () => {
|
||||||
const state = migratePanelState({ explorerFilesSplitRatio: 0.5 }, 12, { isWeb: false });
|
const state = migratePanelState({ explorerFilesSplitRatio: 0.5 }, 12, { isWeb: false });
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export const PanelPersistedStateSchema = z.strictObject({
|
|||||||
explorerTab: ExplorerTabSchema.optional(),
|
explorerTab: ExplorerTabSchema.optional(),
|
||||||
explorerTabByCheckout: z.record(z.string(), ExplorerTabSchema).optional(),
|
explorerTabByCheckout: z.record(z.string(), ExplorerTabSchema).optional(),
|
||||||
expandedPathsByWorkspace: z.record(z.string(), z.array(z.string())).optional(),
|
expandedPathsByWorkspace: z.record(z.string(), z.array(z.string())).optional(),
|
||||||
|
// Accepted only so migration can discard the former per-file diff expansion state.
|
||||||
diffExpandedPathsByWorkspace: z.record(z.string(), z.array(z.string())).optional(),
|
diffExpandedPathsByWorkspace: z.record(z.string(), z.array(z.string())).optional(),
|
||||||
diffCollapsedFoldersByWorkspace: z.record(z.string(), z.array(z.string())).optional(),
|
diffCollapsedFoldersByWorkspace: z.record(z.string(), z.array(z.string())).optional(),
|
||||||
sidebarWidth: z.number().optional(),
|
sidebarWidth: z.number().optional(),
|
||||||
@@ -290,13 +291,7 @@ export function migratePanelState(
|
|||||||
) {
|
) {
|
||||||
state.expandedPathsByWorkspace = {};
|
state.expandedPathsByWorkspace = {};
|
||||||
}
|
}
|
||||||
if (
|
delete state.diffExpandedPathsByWorkspace;
|
||||||
version < 10 ||
|
|
||||||
typeof state.diffExpandedPathsByWorkspace !== "object" ||
|
|
||||||
!state.diffExpandedPathsByWorkspace
|
|
||||||
) {
|
|
||||||
state.diffExpandedPathsByWorkspace = {};
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
version < 12 ||
|
version < 12 ||
|
||||||
typeof state.diffCollapsedFoldersByWorkspace !== "object" ||
|
typeof state.diffCollapsedFoldersByWorkspace !== "object" ||
|
||||||
|
|||||||
@@ -423,6 +423,25 @@ describe("workspace-layout-store actions", () => {
|
|||||||
expect(findPaneContainingTab(layout.root, "pull_request")?.id).toBe(explorerPaneId);
|
expect(findPaneContainingTab(layout.root, "pull_request")?.id).toBe(explorerPaneId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("places an auto-added pull request in the pane containing Changes", () => {
|
||||||
|
const workspaceKey = createWorkspaceKey();
|
||||||
|
const store = workspaceLayoutStore.getState();
|
||||||
|
store.openTabFocused(workspaceKey, { kind: "working_diff" });
|
||||||
|
|
||||||
|
useWorkspaceLayoutIds("explorer");
|
||||||
|
const explorerPaneId = store.splitPaneEmpty(workspaceKey, {
|
||||||
|
targetPaneId: "main",
|
||||||
|
position: "right",
|
||||||
|
});
|
||||||
|
store.setExplorerPaneId(workspaceKey, explorerPaneId);
|
||||||
|
|
||||||
|
store.observePullRequest(workspaceKey, "url:https://example.test/pulls/1");
|
||||||
|
|
||||||
|
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||||
|
expect(findPaneContainingTab(layout.root, "working_diff")?.id).toBe("main");
|
||||||
|
expect(findPaneContainingTab(layout.root, "pull_request")?.id).toBe("main");
|
||||||
|
});
|
||||||
|
|
||||||
it("opens assistant files in an ensured explorer pane and reveals it on the next open", () => {
|
it("opens assistant files in an ensured explorer pane and reveals it on the next open", () => {
|
||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const store = workspaceLayoutStore.getState();
|
const store = workspaceLayoutStore.getState();
|
||||||
|
|||||||
@@ -521,10 +521,17 @@ export function createWorkspaceLayoutStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
||||||
|
const workingDiffTab = collectAllTabs(layout.root).find(
|
||||||
|
(tab) => tab.target.kind === "working_diff",
|
||||||
|
);
|
||||||
|
const workingDiffPane = workingDiffTab
|
||||||
|
? findPaneContainingTab(layout.root, workingDiffTab.tabId)
|
||||||
|
: null;
|
||||||
const explorerPaneId = state.explorerPaneIdByWorkspace[normalizedWorkspaceKey] ?? null;
|
const explorerPaneId = state.explorerPaneIdByWorkspace[normalizedWorkspaceKey] ?? null;
|
||||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||||
const placementLayout = explorerPane
|
const placementPane = workingDiffPane ?? explorerPane;
|
||||||
? (focusPaneInLayout({ layout, paneId: explorerPane.id }) ?? layout)
|
const placementLayout = placementPane
|
||||||
|
? (focusPaneInLayout({ layout, paneId: placementPane.id }) ?? layout)
|
||||||
: layout;
|
: layout;
|
||||||
const result = openTabInLayoutFocused({
|
const result = openTabInLayoutFocused({
|
||||||
layout: placementLayout,
|
layout: placementLayout,
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import { useMemo, type ReactElement } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
FileDiff,
|
|
||||||
FolderTree,
|
|
||||||
GitPullRequest,
|
|
||||||
Globe,
|
|
||||||
SquarePen,
|
|
||||||
SquareTerminal,
|
|
||||||
} from "lucide-react-native";
|
|
||||||
import { withUnistyles } from "react-native-unistyles";
|
|
||||||
import {
|
|
||||||
getTerminalProfileIcon,
|
|
||||||
resolveTerminalProfiles,
|
|
||||||
} from "@getpaseo/protocol/terminal-profiles";
|
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
|
||||||
import { getIsElectron } from "@/constants/platform";
|
|
||||||
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
|
||||||
import type { Theme } from "@/styles/theme";
|
|
||||||
import {
|
|
||||||
isPinnedTargetAvailable,
|
|
||||||
pinnedTargetKey,
|
|
||||||
type PinnedTabTarget,
|
|
||||||
} from "@/workspace-pins/target";
|
|
||||||
import { usePinnedTargetsStore } from "@/workspace-pins/store";
|
|
||||||
|
|
||||||
export interface ResolvedPin {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
icon: ReactElement;
|
|
||||||
onPress: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UsePinnedLaunchersInput {
|
|
||||||
serverId: string;
|
|
||||||
onLaunch: (target: PinnedTabTarget) => void;
|
|
||||||
isGit?: boolean;
|
|
||||||
hasPullRequest?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
||||||
|
|
||||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
|
||||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
|
||||||
const ThemedGlobe = withUnistyles(Globe);
|
|
||||||
const ThemedFileDiff = withUnistyles(FileDiff);
|
|
||||||
const ThemedFolderTree = withUnistyles(FolderTree);
|
|
||||||
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
|
||||||
|
|
||||||
function ProviderPinIcon({
|
|
||||||
iconKey,
|
|
||||||
size,
|
|
||||||
color = "",
|
|
||||||
}: {
|
|
||||||
iconKey: string;
|
|
||||||
size: number;
|
|
||||||
color?: string;
|
|
||||||
}) {
|
|
||||||
const Icon = getProviderIcon(iconKey);
|
|
||||||
return <Icon size={size} color={color} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemedProviderPinIcon = withUnistyles(ProviderPinIcon);
|
|
||||||
|
|
||||||
export function ProfileIcon({ iconKey }: { iconKey: string | undefined }): ReactElement {
|
|
||||||
if (!iconKey) {
|
|
||||||
return <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
|
|
||||||
}
|
|
||||||
return <ThemedProviderPinIcon iconKey={iconKey} size={14} uniProps={mutedColorMapping} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePinnedLaunchers({
|
|
||||||
serverId,
|
|
||||||
onLaunch,
|
|
||||||
isGit,
|
|
||||||
hasPullRequest,
|
|
||||||
}: UsePinnedLaunchersInput): ResolvedPin[] {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const pinned = usePinnedTargetsStore((state) => state.pinned);
|
|
||||||
const { config } = useDaemonConfig(serverId);
|
|
||||||
const profiles = useMemo(
|
|
||||||
() => resolveTerminalProfiles(config?.terminalProfiles),
|
|
||||||
[config?.terminalProfiles],
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
const resolved: ResolvedPin[] = [];
|
|
||||||
for (const target of pinned) {
|
|
||||||
if (
|
|
||||||
!isPinnedTargetAvailable(target, { isElectron: getIsElectron(), isGit, hasPullRequest })
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (target.kind === "draft") {
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: t("workspace.tabs.actions.newAgent"),
|
|
||||||
icon: <ThemedSquarePen size={14} uniProps={mutedColorMapping} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (target.kind === "terminal") {
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: t("workspace.tabs.actions.newTerminal"),
|
|
||||||
icon: <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (target.kind === "browser") {
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: t("workspace.tabs.actions.newBrowser"),
|
|
||||||
icon: <ThemedGlobe size={14} uniProps={mutedColorMapping} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (target.kind === "working_diff") {
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: t("workspace.tabs.actions.changes"),
|
|
||||||
icon: <ThemedFileDiff size={14} uniProps={mutedColorMapping} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (target.kind === "files") {
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: t("workspace.tabs.actions.files"),
|
|
||||||
icon: <ThemedFolderTree size={14} uniProps={mutedColorMapping} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (target.kind === "pull_request") {
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: t("workspace.tabs.actions.pullRequest"),
|
|
||||||
icon: <ThemedGitPullRequest size={14} uniProps={mutedColorMapping} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const profile = profiles.find((entry) => entry.id === target.profileId);
|
|
||||||
if (!profile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
resolved.push({
|
|
||||||
key: pinnedTargetKey(target),
|
|
||||||
label: profile.name,
|
|
||||||
icon: <ProfileIcon iconKey={getTerminalProfileIcon(profile)} />,
|
|
||||||
onPress: () => onLaunch(target),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}, [hasPullRequest, isGit, onLaunch, pinned, profiles, t]);
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
|
||||||
import { Pressable, View, type GestureResponderEvent } from "react-native";
|
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Pin, PinOff } from "lucide-react-native";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
|
||||||
import { isNative } from "@/constants/platform";
|
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
|
||||||
import type { Theme } from "@/styles/theme";
|
|
||||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
|
||||||
import { pinnedTargetKey, type PinnedTabTarget } from "@/workspace-pins/target";
|
|
||||||
import { usePinnedTargetsStore } from "@/workspace-pins/store";
|
|
||||||
|
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
||||||
|
|
||||||
const ThemedPin = withUnistyles(Pin);
|
|
||||||
const ThemedPinOff = withUnistyles(PinOff);
|
|
||||||
|
|
||||||
interface PinnableMenuItemProps {
|
|
||||||
target: PinnedTabTarget;
|
|
||||||
label: string;
|
|
||||||
leading: ReactElement;
|
|
||||||
disabled?: boolean;
|
|
||||||
onSelect?: () => void;
|
|
||||||
testID?: string;
|
|
||||||
/** Chord for the keyboard action that runs this target directly, shown beside the pin toggle. */
|
|
||||||
shortcut?: ShortcutKey[][] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PinnableMenuItem({
|
|
||||||
target,
|
|
||||||
label,
|
|
||||||
leading,
|
|
||||||
disabled,
|
|
||||||
onSelect,
|
|
||||||
testID,
|
|
||||||
shortcut,
|
|
||||||
}: PinnableMenuItemProps): ReactElement {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const isCompact = useIsCompactFormFactor();
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const isPinned = usePinnedTargetsStore((state) => state.isPinned(target));
|
|
||||||
const toggle = usePinnedTargetsStore((state) => state.toggle);
|
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(() => setIsHovered(true), []);
|
|
||||||
const handlePointerLeave = useCallback(() => setIsHovered(false), []);
|
|
||||||
|
|
||||||
const handleTogglePin = useCallback(
|
|
||||||
(event: GestureResponderEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
toggle(target);
|
|
||||||
},
|
|
||||||
[target, toggle],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showToggle = isHovered || isNative || isCompact;
|
|
||||||
|
|
||||||
const slotStyle = useMemo(
|
|
||||||
() => [styles.pinToggleSlot, showToggle ? styles.pinToggleShown : styles.pinToggleHidden],
|
|
||||||
[showToggle],
|
|
||||||
);
|
|
||||||
|
|
||||||
const trailing = useMemo(
|
|
||||||
() => (
|
|
||||||
<View style={styles.trailingRow}>
|
|
||||||
{shortcut ? <Shortcut chord={shortcut} /> : null}
|
|
||||||
<View style={slotStyle} pointerEvents={showToggle ? "auto" : "none"}>
|
|
||||||
<Pressable
|
|
||||||
onPress={handleTogglePin}
|
|
||||||
hitSlop={8}
|
|
||||||
style={styles.pinToggleButton}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={
|
|
||||||
isPinned
|
|
||||||
? t("workspace.tabs.actions.unpinTarget")
|
|
||||||
: t("workspace.tabs.actions.pinTarget")
|
|
||||||
}
|
|
||||||
testID={`workspace-pin-toggle-${pinnedTargetKey(target)}`}
|
|
||||||
>
|
|
||||||
{isPinned ? (
|
|
||||||
<ThemedPinOff size={14} uniProps={mutedColorMapping} />
|
|
||||||
) : (
|
|
||||||
<ThemedPin size={14} uniProps={mutedColorMapping} />
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
[handleTogglePin, isPinned, shortcut, showToggle, slotStyle, t, target],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave}>
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={testID}
|
|
||||||
leading={leading}
|
|
||||||
trailing={trailing}
|
|
||||||
disabled={disabled}
|
|
||||||
onSelect={onSelect}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
|
||||||
trailingRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
},
|
|
||||||
pinToggleSlot: {
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
pinToggleHidden: {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
pinToggleShown: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
pinToggleButton: {
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { ReactElement } from "react";
|
|
||||||
import { Text, View, type PressableStateCallbackType } from "react-native";
|
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import type { ResolvedPin } from "@/workspace-pins/launch";
|
|
||||||
|
|
||||||
function pinButtonStyle({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) {
|
|
||||||
return [styles.pinButton, (Boolean(hovered) || pressed) && styles.pinButtonHovered];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PinnedTargetsRowProps {
|
|
||||||
launchers: ResolvedPin[];
|
|
||||||
testIdPrefix: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PinnedTargetsRow({ launchers, testIdPrefix }: PinnedTargetsRowProps): ReactElement {
|
|
||||||
return (
|
|
||||||
<View style={styles.row}>
|
|
||||||
{launchers.map((launcher) => (
|
|
||||||
<Tooltip key={launcher.key} delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger
|
|
||||||
testID={`${testIdPrefix}-${launcher.key}`}
|
|
||||||
onPress={launcher.onPress}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={launcher.label}
|
|
||||||
style={pinButtonStyle}
|
|
||||||
>
|
|
||||||
{launcher.icon}
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
|
||||||
<Text style={styles.tooltipText}>{launcher.label}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
|
||||||
row: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
pinButton: {
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
pinButtonHovered: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
tooltipText: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { TerminalProfile } from "@getpaseo/protocol/messages";
|
|
||||||
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
|
||||||
import { runPinnedTabTarget, type TabTargetHandlers } from "./run";
|
|
||||||
|
|
||||||
const PROFILES: readonly TerminalProfile[] = [
|
|
||||||
{ id: "claude", name: "Claude Code", command: "claude" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface RecordedLaunch {
|
|
||||||
action: "draft" | "terminal" | "browser" | "changes" | "files" | "pull_request" | "profile";
|
|
||||||
profile?: TerminalProfileInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordingHandlers() {
|
|
||||||
const launches: RecordedLaunch[] = [];
|
|
||||||
const handlers: TabTargetHandlers = {
|
|
||||||
createDraft: () => launches.push({ action: "draft" }),
|
|
||||||
createTerminal: () => launches.push({ action: "terminal" }),
|
|
||||||
createBrowser: () => launches.push({ action: "browser" }),
|
|
||||||
openChanges: () => launches.push({ action: "changes" }),
|
|
||||||
openFiles: () => launches.push({ action: "files" }),
|
|
||||||
openPullRequest: () => launches.push({ action: "pull_request" }),
|
|
||||||
createTerminalWithProfile: (profile) => launches.push({ action: "profile", profile }),
|
|
||||||
};
|
|
||||||
return { launches, handlers };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("runPinnedTabTarget", () => {
|
|
||||||
it("creates a draft agent for the draft target", () => {
|
|
||||||
const { launches, handlers } = recordingHandlers();
|
|
||||||
runPinnedTabTarget({ kind: "draft" }, PROFILES, handlers);
|
|
||||||
expect(launches).toEqual([{ action: "draft" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a terminal for the terminal target", () => {
|
|
||||||
const { launches, handlers } = recordingHandlers();
|
|
||||||
runPinnedTabTarget({ kind: "terminal" }, PROFILES, handlers);
|
|
||||||
expect(launches).toEqual([{ action: "terminal" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a browser for the browser target", () => {
|
|
||||||
const { launches, handlers } = recordingHandlers();
|
|
||||||
runPinnedTabTarget({ kind: "browser" }, PROFILES, handlers);
|
|
||||||
expect(launches).toEqual([{ action: "browser" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches the resolved profile command for a known profile target", () => {
|
|
||||||
const { launches, handlers } = recordingHandlers();
|
|
||||||
runPinnedTabTarget({ kind: "profile", profileId: "claude" }, PROFILES, handlers);
|
|
||||||
expect(launches).toEqual([
|
|
||||||
{ action: "profile", profile: { name: "Claude Code", command: "claude", args: [] } },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches a prompt-taking profile bare, never with the sentinel as a literal argument", () => {
|
|
||||||
const { launches, handlers } = recordingHandlers();
|
|
||||||
runPinnedTabTarget(
|
|
||||||
{ kind: "profile", profileId: "codex" },
|
|
||||||
[{ id: "codex", name: "Codex", command: "codex", args: ["--yolo", "{{{prompt}}}"] }],
|
|
||||||
handlers,
|
|
||||||
);
|
|
||||||
// A pin has nowhere to type, so the prompt-only arg drops instead of
|
|
||||||
// reaching the CLI as the literal text `{{{prompt}}}`.
|
|
||||||
expect(launches).toEqual([
|
|
||||||
{ action: "profile", profile: { name: "Codex", command: "codex", args: ["--yolo"] } },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does nothing when the profile id is absent from the host", () => {
|
|
||||||
const { launches, handlers } = recordingHandlers();
|
|
||||||
runPinnedTabTarget({ kind: "profile", profileId: "missing" }, PROFILES, handlers);
|
|
||||||
expect(launches).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { resolveTerminalProfileLaunch } from "@getpaseo/protocol/terminal-profiles";
|
|
||||||
import type { TerminalProfile } from "@getpaseo/protocol/messages";
|
|
||||||
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
|
||||||
import type { PinnedTabTarget } from "@/workspace-pins/target";
|
|
||||||
|
|
||||||
export interface TabTargetHandlers {
|
|
||||||
createDraft: () => void;
|
|
||||||
createTerminal: () => void;
|
|
||||||
createBrowser: () => void;
|
|
||||||
openChanges: () => void;
|
|
||||||
openFiles: () => void;
|
|
||||||
openPullRequest: () => void;
|
|
||||||
createTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runPinnedTabTarget(
|
|
||||||
target: PinnedTabTarget,
|
|
||||||
profiles: readonly TerminalProfile[],
|
|
||||||
handlers: TabTargetHandlers,
|
|
||||||
): void {
|
|
||||||
if (target.kind === "draft") {
|
|
||||||
handlers.createDraft();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.kind === "terminal") {
|
|
||||||
handlers.createTerminal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.kind === "browser") {
|
|
||||||
handlers.createBrowser();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.kind === "working_diff") {
|
|
||||||
handlers.openChanges();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.kind === "files") {
|
|
||||||
handlers.openFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.kind === "pull_request") {
|
|
||||||
handlers.openPullRequest();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const profile = profiles.find((entry) => entry.id === target.profileId);
|
|
||||||
if (!profile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Nothing to type from a pin, so the prompt is empty and any sentinel arg
|
|
||||||
// drops out.
|
|
||||||
handlers.createTerminalWithProfile(resolveTerminalProfileLaunch(profile, ""));
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
vi.mock("@react-native-async-storage/async-storage", () => {
|
|
||||||
const storage = new Map<string, string>();
|
|
||||||
return {
|
|
||||||
default: {
|
|
||||||
getItem: vi.fn(async (key: string) => storage.get(key) ?? null),
|
|
||||||
setItem: vi.fn(async (key: string, value: string) => {
|
|
||||||
storage.set(key, value);
|
|
||||||
}),
|
|
||||||
removeItem: vi.fn(async (key: string) => {
|
|
||||||
storage.delete(key);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import { usePinnedTargetsStore } from "./store";
|
|
||||||
|
|
||||||
describe("pinned targets persistence", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await AsyncStorage.removeItem("pinned-tab-targets");
|
|
||||||
usePinnedTargetsStore.setState({ pinned: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("restores workspace review targets", async () => {
|
|
||||||
await AsyncStorage.setItem(
|
|
||||||
"pinned-tab-targets",
|
|
||||||
JSON.stringify({
|
|
||||||
state: {
|
|
||||||
pinned: [{ kind: "working_diff" }, { kind: "files" }, { kind: "pull_request" }],
|
|
||||||
},
|
|
||||||
version: 1,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await usePinnedTargetsStore.persist.rehydrate();
|
|
||||||
|
|
||||||
expect(usePinnedTargetsStore.getState().pinned).toEqual([
|
|
||||||
{ kind: "working_diff" },
|
|
||||||
{ kind: "files" },
|
|
||||||
{ kind: "pull_request" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { isTargetPinned, togglePinnedTarget, type PinnedTabTarget } from "@/workspace-pins/target";
|
|
||||||
import { createValidatedPersistStorage } from "@/storage/validated-persist-storage";
|
|
||||||
|
|
||||||
interface PinnedTargetsState {
|
|
||||||
pinned: PinnedTabTarget[];
|
|
||||||
toggle: (target: PinnedTabTarget) => void;
|
|
||||||
isPinned: (target: PinnedTabTarget) => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_PINNED_TARGETS: PinnedTabTarget[] = [{ kind: "terminal" }, { kind: "browser" }];
|
|
||||||
const PinnedTabTargetSchema = z.discriminatedUnion("kind", [
|
|
||||||
z.strictObject({ kind: z.literal("draft") }),
|
|
||||||
z.strictObject({ kind: z.literal("terminal") }),
|
|
||||||
z.strictObject({ kind: z.literal("browser") }),
|
|
||||||
z.strictObject({ kind: z.literal("working_diff") }),
|
|
||||||
z.strictObject({ kind: z.literal("files") }),
|
|
||||||
z.strictObject({ kind: z.literal("pull_request") }),
|
|
||||||
z.strictObject({ kind: z.literal("profile"), profileId: z.string() }),
|
|
||||||
]);
|
|
||||||
const PinnedTargetsPersistedStateSchema = z.strictObject({
|
|
||||||
pinned: z.array(PinnedTabTargetSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
function applyDefaultPinnedTargets(pinned: PinnedTabTarget[]): PinnedTabTarget[] {
|
|
||||||
const next = [...DEFAULT_PINNED_TARGETS];
|
|
||||||
for (const target of pinned) {
|
|
||||||
if (!isTargetPinned(next, target)) {
|
|
||||||
next.push(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePinnedTargetsStore = create<PinnedTargetsState>()(
|
|
||||||
persist<PinnedTargetsState, [], [], z.infer<typeof PinnedTargetsPersistedStateSchema>>(
|
|
||||||
(set, get) => ({
|
|
||||||
pinned: [],
|
|
||||||
toggle: (target) => set((state) => ({ pinned: togglePinnedTarget(state.pinned, target) })),
|
|
||||||
isPinned: (target) => isTargetPinned(get().pinned, target),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "pinned-tab-targets",
|
|
||||||
version: 1,
|
|
||||||
merge: (persistedState, currentState) => {
|
|
||||||
const result = PinnedTargetsPersistedStateSchema.safeParse(persistedState);
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
pinned: result.success ? result.data.pinned : applyDefaultPinnedTargets([]),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
storage: createValidatedPersistStorage(AsyncStorage, PinnedTargetsPersistedStateSchema),
|
|
||||||
partialize: (state) => ({ pinned: state.pinned }),
|
|
||||||
migrate: (persistedState, version) => {
|
|
||||||
const result = PinnedTargetsPersistedStateSchema.safeParse(persistedState);
|
|
||||||
const pinned = result.success ? result.data.pinned : [];
|
|
||||||
if (version === 0) {
|
|
||||||
return { pinned: applyDefaultPinnedTargets(pinned) };
|
|
||||||
}
|
|
||||||
return { pinned };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import {
|
|
||||||
isPinnedTargetAvailable,
|
|
||||||
isTargetPinned,
|
|
||||||
pinnedTargetKey,
|
|
||||||
togglePinnedTarget,
|
|
||||||
} from "./target";
|
|
||||||
|
|
||||||
describe("isPinnedTargetAvailable", () => {
|
|
||||||
it("only offers browser targets in Electron", () => {
|
|
||||||
const browser = { kind: "browser" } as const;
|
|
||||||
|
|
||||||
expect(isPinnedTargetAvailable(browser, { isElectron: true })).toBe(true);
|
|
||||||
expect(isPinnedTargetAvailable(browser, { isElectron: false })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("offers cross-platform targets outside Electron", () => {
|
|
||||||
const environment = { isElectron: false };
|
|
||||||
|
|
||||||
expect(isPinnedTargetAvailable({ kind: "draft" }, environment)).toBe(true);
|
|
||||||
expect(isPinnedTargetAvailable({ kind: "terminal" }, environment)).toBe(true);
|
|
||||||
expect(isPinnedTargetAvailable({ kind: "profile", profileId: "claude" }, environment)).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("pinnedTargetKey", () => {
|
|
||||||
it("uses the bare kind as the key for non-profile targets", () => {
|
|
||||||
expect(pinnedTargetKey({ kind: "draft" })).toBe("draft");
|
|
||||||
expect(pinnedTargetKey({ kind: "terminal" })).toBe("terminal");
|
|
||||||
expect(pinnedTargetKey({ kind: "browser" })).toBe("browser");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("namespaces a profile target by its profile id", () => {
|
|
||||||
expect(pinnedTargetKey({ kind: "profile", profileId: "claude" })).toBe("profile:claude");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gives two profiles with different ids different keys", () => {
|
|
||||||
const claude = pinnedTargetKey({ kind: "profile", profileId: "claude" });
|
|
||||||
const codex = pinnedTargetKey({ kind: "profile", profileId: "codex" });
|
|
||||||
expect(claude).not.toBe(codex);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("togglePinnedTarget / isTargetPinned", () => {
|
|
||||||
it("pins a target that is not yet pinned", () => {
|
|
||||||
const pinned = togglePinnedTarget([], { kind: "terminal" });
|
|
||||||
expect(isTargetPinned(pinned, { kind: "terminal" })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("unpins a target that is already pinned", () => {
|
|
||||||
const pinned = togglePinnedTarget([{ kind: "browser" }], { kind: "browser" });
|
|
||||||
expect(isTargetPinned(pinned, { kind: "browser" })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("treats profiles with different ids as independent pins", () => {
|
|
||||||
const pinned = togglePinnedTarget([], { kind: "profile", profileId: "claude" });
|
|
||||||
expect(isTargetPinned(pinned, { kind: "profile", profileId: "claude" })).toBe(true);
|
|
||||||
expect(isTargetPinned(pinned, { kind: "profile", profileId: "codex" })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("unpins only the matching profile id", () => {
|
|
||||||
let pinned = togglePinnedTarget([], { kind: "profile", profileId: "claude" });
|
|
||||||
pinned = togglePinnedTarget(pinned, { kind: "profile", profileId: "codex" });
|
|
||||||
pinned = togglePinnedTarget(pinned, { kind: "profile", profileId: "claude" });
|
|
||||||
|
|
||||||
expect(isTargetPinned(pinned, { kind: "profile", profileId: "claude" })).toBe(false);
|
|
||||||
expect(isTargetPinned(pinned, { kind: "profile", profileId: "codex" })).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export type PinnedTabTarget =
|
|
||||||
| { kind: "draft" }
|
|
||||||
| { kind: "terminal" }
|
|
||||||
| { kind: "browser" }
|
|
||||||
| { kind: "working_diff" }
|
|
||||||
| { kind: "files" }
|
|
||||||
| { kind: "pull_request" }
|
|
||||||
| { kind: "profile"; profileId: string };
|
|
||||||
|
|
||||||
export function isPinnedTargetAvailable(
|
|
||||||
target: PinnedTabTarget,
|
|
||||||
environment: { isElectron: boolean; isGit?: boolean; hasPullRequest?: boolean },
|
|
||||||
): boolean {
|
|
||||||
if (target.kind === "browser") {
|
|
||||||
return environment.isElectron;
|
|
||||||
}
|
|
||||||
if (target.kind === "working_diff") {
|
|
||||||
return environment.isGit !== false;
|
|
||||||
}
|
|
||||||
if (target.kind === "pull_request") {
|
|
||||||
return environment.hasPullRequest !== false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pinnedTargetKey(target: PinnedTabTarget): string {
|
|
||||||
if (target.kind === "profile") {
|
|
||||||
return `profile:${target.profileId}`;
|
|
||||||
}
|
|
||||||
return target.kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTargetPinned(
|
|
||||||
pinned: readonly PinnedTabTarget[],
|
|
||||||
target: PinnedTabTarget,
|
|
||||||
): boolean {
|
|
||||||
const key = pinnedTargetKey(target);
|
|
||||||
return pinned.some((entry) => pinnedTargetKey(entry) === key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function togglePinnedTarget(
|
|
||||||
pinned: readonly PinnedTabTarget[],
|
|
||||||
target: PinnedTabTarget,
|
|
||||||
): PinnedTabTarget[] {
|
|
||||||
const key = pinnedTargetKey(target);
|
|
||||||
const next = pinned.filter((entry) => pinnedTargetKey(entry) !== key);
|
|
||||||
if (next.length === pinned.length) {
|
|
||||||
next.push(target);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user