Surface workspace changes from agent panes (#3682)
* feat(app): refine workspace pane actions Prioritize Changes when available, keep Files useful before a selection, and reserve desktop diff clicks for selection so comments require the explicit affordance. Move the Changes shortcut to Shift+G to avoid consuming a likely copy variant. * feat(app): improve access to workspace changes
This commit is contained in:
+3
-1
@@ -83,7 +83,9 @@ Running and successful setup never seed tabs.
|
||||
persisted in client app settings as `openSupportingTabsInSidePanel`. Off means new
|
||||
supporting tabs land in the focused pane instead. Mobile ignores it. Explicit actions
|
||||
— the header toggle, a pane's `+` menu, the empty-state launcher — are never subject
|
||||
to it.
|
||||
to it. The Changes keyboard shortcut and the diff-stat pill above an agent composer are
|
||||
supporting opens: they follow the preference and focus an existing Changes tab wherever
|
||||
the user left it.
|
||||
|
||||
## The name
|
||||
|
||||
|
||||
@@ -996,6 +996,23 @@ test("canvas diff creates, edits, and deletes an inline review without DOM code
|
||||
await expect(page.locator('[data-testid^="diff-code-row-"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("autofocusing an inline review keeps the Changes tab focused", async ({ page }) => {
|
||||
const workspace = await createWorkspaceWithMountedTabDiff();
|
||||
await useUnwrappedDiffLines(page);
|
||||
await openWorkspaceChanges(page, workspace);
|
||||
|
||||
const changesTab = page.getByTestId("workspace-tab-working_diff").filter({ visible: true });
|
||||
const focusedBackground = await changesTab.evaluate(
|
||||
(element) => getComputedStyle(element).backgroundColor,
|
||||
);
|
||||
|
||||
await startReviewOnFirstChangedLine(page);
|
||||
await expect(page.getByTestId("inline-review-editor-input")).toBeFocused();
|
||||
await expect
|
||||
.poll(() => changesTab.evaluate((element) => getComputedStyle(element).backgroundColor))
|
||||
.toBe(focusedBackground);
|
||||
});
|
||||
|
||||
test("split canvas creates a review on the changed side and keeps it in that column", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1095,7 +1112,7 @@ test("canvas diff copies a dragged character selection without opening a review"
|
||||
await expect(page.getByTestId("inline-review-editor")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("the first click after a canvas selection only dismisses the selection", async ({ page }) => {
|
||||
test("clicking the canvas dismisses a selection without opening a review", async ({ page }) => {
|
||||
const workspace = await createWorkspaceWithExactSelectionDiff("ABCDEFGHIJ");
|
||||
await useUnwrappedDiffLines(page);
|
||||
await openSelectionWorkspaceChanges(page, workspace);
|
||||
@@ -1105,7 +1122,7 @@ test("the first click after a canvas selection only dismisses the selection", as
|
||||
await expect(page.getByTestId("inline-review-editor")).toHaveCount(0);
|
||||
|
||||
await clickFirstChangedLine(page);
|
||||
await expect(page.getByTestId("inline-review-editor")).toBeVisible();
|
||||
await expect(page.getByTestId("inline-review-editor")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("canvas diff replaces a selection with forward and backward drags", async ({
|
||||
@@ -1537,7 +1554,8 @@ async function startReviewOnFirstChangedLine(
|
||||
if (!bodyBounds) throw new Error("Expanded diff body has no bounds");
|
||||
const lineHeight = Math.round(fontSize * 1.5);
|
||||
const columnLeft = side === "right" ? bodyBounds.x + bodyBounds.width / 2 : bodyBounds.x;
|
||||
await page.mouse.click(columnLeft + 20, bodyBounds.y + lineHeight * 1.5);
|
||||
await page.mouse.move(columnLeft + 20, bodyBounds.y + lineHeight * 1.5);
|
||||
await page.getByRole("button", { name: "Add review comment" }).click();
|
||||
await expect(page.getByTestId("inline-review-editor")).toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||
|
||||
const APP_SETTINGS_KEY = "@paseo:app-settings";
|
||||
const CHANGES_SHORTCUT = `${process.platform === "darwin" ? "Meta" : "Control"}+Shift+G`;
|
||||
|
||||
function mainPane(page: Page) {
|
||||
return page.getByTestId("workspace-pane-main").filter({ visible: true });
|
||||
}
|
||||
|
||||
function sidePanel(page: Page) {
|
||||
return page.getByTestId("workspace-side-panel").filter({ visible: true });
|
||||
}
|
||||
|
||||
test("Changes shortcut follows the default Side panel placement", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "changes-shortcut-side-" });
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
|
||||
await page.keyboard.press(CHANGES_SHORTCUT);
|
||||
|
||||
await expect(sidePanel(page).getByTestId("workspace-tab-working_diff")).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(mainPane(page).getByTestId("workspace-tab-working_diff")).toHaveCount(0);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("Changes shortcut uses the focused pane when Side panel placement is off", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript((settingsKey) => {
|
||||
localStorage.setItem(settingsKey, JSON.stringify({ openSupportingTabsInSidePanel: false }));
|
||||
}, APP_SETTINGS_KEY);
|
||||
const workspace = await seedWorkspace({ repoPrefix: "changes-shortcut-pane-" });
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
|
||||
await page.keyboard.press(CHANGES_SHORTCUT);
|
||||
|
||||
await expect(mainPane(page).getByTestId("workspace-tab-working_diff")).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(sidePanel(page)).toHaveCount(0);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
|
||||
const APP_SETTINGS_KEY = "@paseo:app-settings";
|
||||
|
||||
function visibleMainPane(page: Page) {
|
||||
return page.getByTestId("workspace-pane-main").filter({ visible: true });
|
||||
}
|
||||
|
||||
async function seedChangedAgent(repoPrefix: string) {
|
||||
const workspace = await seedMockAgentWorkspace({
|
||||
repoPrefix,
|
||||
title: "Composer diff stat",
|
||||
repo: { withRemote: true },
|
||||
});
|
||||
try {
|
||||
await rm(path.join(workspace.cwd, "remote.git"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(workspace.cwd, "README.md"),
|
||||
"# Temp Repo\nexport const one = 1;\nexport const two = 2;\n",
|
||||
);
|
||||
await workspace.client.checkoutRefresh(workspace.cwd);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const workspaces = await workspace.client.fetchWorkspaces();
|
||||
return (
|
||||
workspaces.entries.find((entry) => entry.id === workspace.workspaceId)?.diffStat ?? null
|
||||
);
|
||||
})
|
||||
.toEqual({ additions: 2, deletions: 0 });
|
||||
return workspace;
|
||||
} catch (error) {
|
||||
await workspace.cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
test("composer diff stat opens Changes in the preferred Side panel", async ({ page }) => {
|
||||
const workspace = await seedChangedAgent("composer-diff-stat-side-");
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await openAgentRoute(page, {
|
||||
workspaceId: workspace.workspaceId,
|
||||
agentId: workspace.agentId,
|
||||
});
|
||||
|
||||
const pill = page.getByTestId("composer-diff-stat-pill");
|
||||
await expect(pill).toBeVisible({ timeout: 30_000 });
|
||||
await expect(pill).toContainText("+2");
|
||||
await expect(pill).toContainText("-0");
|
||||
await pill.click();
|
||||
|
||||
const sidePanel = page.getByTestId("workspace-side-panel").filter({ visible: true });
|
||||
await expect(sidePanel.getByTestId("workspace-tab-working_diff")).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(sidePanel.getByTestId("working-diff-panel")).toBeVisible({ timeout: 30_000 });
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("composer diff stat opens Changes in the focused pane when Side panel routing is off", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript((settingsKey) => {
|
||||
localStorage.setItem(settingsKey, JSON.stringify({ openSupportingTabsInSidePanel: false }));
|
||||
}, APP_SETTINGS_KEY);
|
||||
const workspace = await seedChangedAgent("composer-diff-stat-tab-");
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await openAgentRoute(page, {
|
||||
workspaceId: workspace.workspaceId,
|
||||
agentId: workspace.agentId,
|
||||
});
|
||||
|
||||
await page.getByTestId("composer-diff-stat-pill").click();
|
||||
|
||||
const mainPane = visibleMainPane(page);
|
||||
await expect(mainPane.getByTestId("workspace-tab-working_diff")).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(mainPane.getByTestId("working-diff-panel")).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("workspace-side-panel")).toHaveCount(0);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
@@ -9,6 +9,18 @@ import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-cli
|
||||
|
||||
let workspace: SeededWorkspace;
|
||||
|
||||
async function effectiveBackground(locator: import("@playwright/test").Locator): Promise<string> {
|
||||
return locator.evaluate((element) => {
|
||||
let current: Element | null = element;
|
||||
while (current) {
|
||||
const color = getComputedStyle(current).backgroundColor;
|
||||
if (color !== "rgba(0, 0, 0, 0)" && color !== "transparent") return color;
|
||||
current = current.parentElement;
|
||||
}
|
||||
return "transparent";
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
workspace = await seedWorkspace({
|
||||
repoPrefix: "file-tree-rail-toggle-",
|
||||
@@ -21,6 +33,21 @@ test.afterAll(async () => {
|
||||
});
|
||||
|
||||
test.describe("File panel tree rail", () => {
|
||||
test("the Files panel starts with an empty content pane beside the tree", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await openFileExplorer(page);
|
||||
|
||||
const rail = page.getByTestId("files-tree-rail").filter({ visible: true });
|
||||
await expect(rail).toBeVisible({ timeout: 30_000 });
|
||||
const content = rail.getByTestId("files-tree-rail-content");
|
||||
const tree = rail.getByTestId("files-tree-rail-tree");
|
||||
await expect(content).toHaveText("Choose a file");
|
||||
await expect(tree).toContainText("docs");
|
||||
expect(await effectiveBackground(tree.getByTestId("file-explorer-tree-scroll"))).toBe(
|
||||
await effectiveBackground(content),
|
||||
);
|
||||
});
|
||||
|
||||
test("the file toolbar toggle closes and reopens the tree", async ({ page }, testInfo) => {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await openFileExplorer(page);
|
||||
|
||||
@@ -156,7 +156,7 @@ test.describe("Tab creation", () => {
|
||||
const shortcutPrefix = process.platform === "darwin" ? /⇧⌘/ : /Ctrl.*Shift/;
|
||||
await expect(agent).toContainText(new RegExp(`${shortcutPrefix.source}.*A`));
|
||||
await expect(terminal).toContainText(new RegExp(`${shortcutPrefix.source}.*T`));
|
||||
await expect(changes).toContainText(new RegExp(`${shortcutPrefix.source}.*C`));
|
||||
await expect(changes).toContainText(new RegExp(`${shortcutPrefix.source}.*G`));
|
||||
await expect(files).toContainText(new RegExp(`${shortcutPrefix.source}.*E`));
|
||||
await expect(agent).toBeFocused();
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ function emptyState(scope: Locator): Locator {
|
||||
return scope.getByTestId("workspace-pane-empty-state");
|
||||
}
|
||||
|
||||
async function launcherButtonLabels(launcher: Locator): Promise<(string | null)[]> {
|
||||
return launcher
|
||||
.getByRole("button")
|
||||
.evaluateAll((buttons) => buttons.map((button) => button.getAttribute("aria-label")));
|
||||
}
|
||||
|
||||
function sidePanelToggle(page: Page): Locator {
|
||||
return page.getByTestId("workspace-explorer-toggle").first();
|
||||
}
|
||||
@@ -61,7 +67,15 @@ test.describe("Side panel", () => {
|
||||
await test.step("revealing the Side panel seeds nothing", async () => {
|
||||
await sidePanelToggle(page).click();
|
||||
await expect(sidePanel(page)).toBeVisible({ timeout: 30_000 });
|
||||
await expect(emptyState(sidePanel(page))).toBeVisible();
|
||||
const launcher = emptyState(sidePanel(page));
|
||||
await expect(launcher).toBeVisible();
|
||||
expect(await launcherButtonLabels(launcher)).toEqual([
|
||||
"Changes",
|
||||
"Files",
|
||||
"Agent",
|
||||
"Terminal",
|
||||
null,
|
||||
]);
|
||||
await expect(page.getByTestId("workspace-tab-working_diff")).toHaveCount(0);
|
||||
await capture(page, testInfo, "01-side-panel-reveals-empty");
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface MockAgentWorkspace {
|
||||
export interface MockAgentOptions {
|
||||
repoPrefix: string;
|
||||
title: string;
|
||||
repo?: Parameters<typeof seedWorkspace>[0]["repo"];
|
||||
port?: number;
|
||||
initialPrompt?: string;
|
||||
model?: string;
|
||||
@@ -29,7 +30,11 @@ export interface MockAgentOptions {
|
||||
export async function seedMockAgentWorkspace(
|
||||
options: MockAgentOptions,
|
||||
): Promise<MockAgentWorkspace> {
|
||||
const workspace = await seedWorkspace({ repoPrefix: options.repoPrefix, port: options.port });
|
||||
const workspace = await seedWorkspace({
|
||||
repoPrefix: options.repoPrefix,
|
||||
repo: options.repo,
|
||||
port: options.port,
|
||||
});
|
||||
try {
|
||||
const agent = await workspace.client.createAgent({
|
||||
provider: "mock",
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface SeedWorkspaceDescriptor {
|
||||
projectDisplayName: string;
|
||||
projectRootPath: string;
|
||||
workspaceDirectory: string;
|
||||
diffStat: { additions: number; deletions: number } | null;
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1662,7 +1662,6 @@ function getErrorRecoveryPath(state: AgentFileExplorerState | undefined): string
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surfaceSidebar,
|
||||
},
|
||||
desktopSplit: {
|
||||
flex: 1,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { seedSessionWorkspaces } from "@/test/seed-session";
|
||||
import { useWorkspaceHasDiffStat } from "./workspace-diff-stat";
|
||||
|
||||
const SERVER_ID = "diff-stat-pill";
|
||||
const WORKSPACE_ID = "workspace";
|
||||
|
||||
const workspace: WorkspaceDescriptor = {
|
||||
id: WORKSPACE_ID,
|
||||
projectId: "project",
|
||||
projectDisplayName: "Project",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "main",
|
||||
status: "done",
|
||||
statusEnteredAt: null,
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
function setDiffStat(diffStat: WorkspaceDescriptor["diffStat"]): void {
|
||||
useSessionStore.getState().setWorkspaces(SERVER_ID, (workspaces) => {
|
||||
const next = new Map(workspaces);
|
||||
next.set(WORKSPACE_ID, { ...workspace, diffStat });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
describe("useWorkspaceHasDiffStat", () => {
|
||||
beforeEach(() => {
|
||||
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
|
||||
seedSessionWorkspaces(SERVER_ID, new Map([[WORKSPACE_ID, workspace]]));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useSessionStore.getState().clearSession(SERVER_ID);
|
||||
});
|
||||
|
||||
it("does not re-render its owner when only the visible counts change", () => {
|
||||
let renderCount = 0;
|
||||
const { result } = renderHook(() => {
|
||||
renderCount += 1;
|
||||
return useWorkspaceHasDiffStat(SERVER_ID, WORKSPACE_ID);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
expect(renderCount).toBe(1);
|
||||
|
||||
act(() => setDiffStat({ additions: 2, deletions: 0 }));
|
||||
expect(result.current).toBe(true);
|
||||
expect(renderCount).toBe(2);
|
||||
|
||||
act(() => setDiffStat({ additions: 5, deletions: 3 }));
|
||||
expect(result.current).toBe(true);
|
||||
expect(renderCount).toBe(2);
|
||||
|
||||
act(() => setDiffStat(null));
|
||||
expect(result.current).toBe(false);
|
||||
expect(renderCount).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { memo, useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { Pressable } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DiffStat } from "@/components/diff-stat";
|
||||
import { composerPillStyles } from "@/composer/pill-styles";
|
||||
import { useVisibleWorkspaceDiffStat } from "@/composer/workspace-diff-stat";
|
||||
|
||||
interface ComposerDiffStatPillProps {
|
||||
additions: number;
|
||||
deletions: number;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export function ComposerDiffStatPill({ additions, deletions, onPress }: ComposerDiffStatPillProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const handleHoverIn = useCallback(() => setIsHovered(true), []);
|
||||
const handleHoverOut = useCallback(() => setIsHovered(false), []);
|
||||
const bodyStyle = useMemo(
|
||||
() => [composerPillStyles.body, isHovered && composerPillStyles.bodyActive],
|
||||
[isHovered],
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
testID="composer-diff-stat-pill"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("workspace.git.diff.openChangesTab")}
|
||||
onPress={onPress}
|
||||
onHoverIn={handleHoverIn}
|
||||
onHoverOut={handleHoverOut}
|
||||
style={bodyStyle}
|
||||
>
|
||||
<DiffStat additions={additions} deletions={deletions} />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
export const WorkspaceDiffStatPill = memo(function WorkspaceDiffStatPill({
|
||||
serverId,
|
||||
workspaceId,
|
||||
onPress,
|
||||
}: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
onPress: () => void;
|
||||
}): ReactElement | null {
|
||||
const diffStat = useVisibleWorkspaceDiffStat(serverId, workspaceId);
|
||||
if (!diffStat) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ComposerDiffStatPill
|
||||
additions={diffStat.additions}
|
||||
deletions={diffStat.deletions}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
||||
|
||||
type DiffStat = NonNullable<WorkspaceDescriptor["diffStat"]>;
|
||||
|
||||
export function useVisibleWorkspaceDiffStat(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
): DiffStat | null {
|
||||
const diffStat = useWorkspaceFields(serverId, workspaceId, (workspace) => workspace.diffStat);
|
||||
return hasDiffStatChanges(diffStat) ? diffStat : null;
|
||||
}
|
||||
|
||||
export function useWorkspaceHasDiffStat(serverId: string, workspaceId: string): boolean {
|
||||
return (
|
||||
useWorkspaceFields(serverId, workspaceId, (workspace) =>
|
||||
hasDiffStatChanges(workspace.diffStat),
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
function hasDiffStatChanges(diffStat: WorkspaceDescriptor["diffStat"]): diffStat is DiffStat {
|
||||
return Boolean(diffStat && (diffStat.additions > 0 || diffStat.deletions > 0));
|
||||
}
|
||||
@@ -495,7 +495,14 @@ export function DiffSurface(props: DiffSurfaceProps) {
|
||||
schedulePaint();
|
||||
return;
|
||||
}
|
||||
if (drag && !moved && hit?.kind === "cell" && hit.target && reviewActions) {
|
||||
if (
|
||||
event.pointerType === "touch" &&
|
||||
drag &&
|
||||
!moved &&
|
||||
hit?.kind === "cell" &&
|
||||
hit.target &&
|
||||
reviewActions
|
||||
) {
|
||||
selectionRef.current = null;
|
||||
reviewActions.onStartComment(hit.target);
|
||||
schedulePaint();
|
||||
|
||||
@@ -1729,6 +1729,7 @@ export const ar: TranslationResources = {
|
||||
label: "الملفات",
|
||||
subtitle: "ملفات مساحة العمل",
|
||||
tooltip: "تصفح ملفات مساحة العمل",
|
||||
chooseFile: "اختر ملفًا",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "طلب السحب",
|
||||
|
||||
@@ -1739,6 +1739,7 @@ export const en = {
|
||||
label: "Files",
|
||||
subtitle: "Workspace files",
|
||||
tooltip: "Browse workspace files",
|
||||
chooseFile: "Choose a file",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "Pull request",
|
||||
|
||||
@@ -1775,6 +1775,7 @@ export const es: TranslationResources = {
|
||||
label: "Archivos",
|
||||
subtitle: "Archivos del espacio de trabajo",
|
||||
tooltip: "Explorar archivos del espacio de trabajo",
|
||||
chooseFile: "Elige un archivo",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "Solicitud de extracción",
|
||||
|
||||
@@ -1779,6 +1779,7 @@ export const fr: TranslationResources = {
|
||||
label: "Fichiers",
|
||||
subtitle: "Fichiers de l’espace de travail",
|
||||
tooltip: "Parcourir les fichiers de l’espace de travail",
|
||||
chooseFile: "Choisissez un fichier",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "Demande de fusion",
|
||||
|
||||
@@ -1746,6 +1746,7 @@ export const ja: TranslationResources = {
|
||||
label: "ファイル",
|
||||
subtitle: "ワークスペースのファイル",
|
||||
tooltip: "ワークスペースのファイルを参照",
|
||||
chooseFile: "ファイルを選択",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "プルリクエスト",
|
||||
|
||||
@@ -1739,6 +1739,7 @@ export const ko: TranslationResources = {
|
||||
label: "파일",
|
||||
subtitle: "워크스페이스 파일",
|
||||
tooltip: "워크스페이스 파일 탐색",
|
||||
chooseFile: "파일 선택",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "풀 리퀘스트",
|
||||
|
||||
@@ -1761,6 +1761,7 @@ export const ptBR: TranslationResources = {
|
||||
label: "Arquivos",
|
||||
subtitle: "Arquivos do espaço de trabalho",
|
||||
tooltip: "Explorar arquivos do espaço de trabalho",
|
||||
chooseFile: "Escolha um arquivo",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "Pull request",
|
||||
|
||||
@@ -1760,6 +1760,7 @@ export const ru: TranslationResources = {
|
||||
label: "Файлы",
|
||||
subtitle: "Файлы рабочего пространства",
|
||||
tooltip: "Просмотр файлов рабочего пространства",
|
||||
chooseFile: "Выберите файл",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "PR",
|
||||
|
||||
@@ -1709,6 +1709,7 @@ export const zhCN: TranslationResources = {
|
||||
label: "文件",
|
||||
subtitle: "工作区文件",
|
||||
tooltip: "浏览工作区文件",
|
||||
chooseFile: "选择文件",
|
||||
},
|
||||
pullRequest: {
|
||||
label: "拉取请求",
|
||||
|
||||
@@ -1158,7 +1158,7 @@ describe("direct new-tab target shortcuts", () => {
|
||||
const targetCases = [
|
||||
["a", "KeyA", "workspace.tab.target.agent"],
|
||||
["b", "KeyB", "workspace.tab.target.browser"],
|
||||
["c", "KeyC", "workspace.tab.target.changes"],
|
||||
["g", "KeyG", "workspace.tab.target.changes"],
|
||||
["e", "KeyE", "workspace.tab.target.files"],
|
||||
] as const;
|
||||
|
||||
@@ -1192,9 +1192,9 @@ describe("direct new-tab target shortcuts", () => {
|
||||
|
||||
it("uses the existing override map for target matching and display", () => {
|
||||
const bindingId = "workspace-tab-target-agent-ctrl-shift-a-non-mac";
|
||||
const overrides = { [bindingId]: "Ctrl+Shift+G" };
|
||||
const overrides = { [bindingId]: "Ctrl+Shift+H" };
|
||||
const rebound = resolveShortcut({
|
||||
event: { key: "g", code: "KeyG", ctrlKey: true, shiftKey: true },
|
||||
event: { key: "h", code: "KeyH", ctrlKey: true, shiftKey: true },
|
||||
context: { ...desktopNonMac, focusScope: "other" },
|
||||
bindings: buildEffectiveBindings(overrides),
|
||||
});
|
||||
@@ -1208,6 +1208,6 @@ describe("direct new-tab target shortcuts", () => {
|
||||
expect(original.match).toBeNull();
|
||||
expect(
|
||||
resolveShortcutKeysForAction("workspace-tab-target-agent", overrides, desktopNonMac),
|
||||
).toEqual([["ctrl", "shift", "G"]]);
|
||||
).toEqual([["ctrl", "shift", "H"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,9 +454,10 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
// Keep the binding id stable so saved overrides from the former Cmd+Shift+C default survive.
|
||||
id: "workspace-tab-target-changes-cmd-shift-c-mac",
|
||||
action: "workspace.tab.target.changes",
|
||||
combo: "Cmd+Shift+C",
|
||||
combo: "Cmd+Shift+G",
|
||||
when: { mac: true, commandCenter: false },
|
||||
help: {
|
||||
id: "workspace-tab-target-changes",
|
||||
@@ -465,9 +466,10 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
// Keep the binding id stable so saved overrides from the former Ctrl+Shift+C default survive.
|
||||
id: "workspace-tab-target-changes-ctrl-shift-c-non-mac",
|
||||
action: "workspace.tab.target.changes",
|
||||
combo: "Ctrl+Shift+C",
|
||||
combo: "Ctrl+Shift+G",
|
||||
when: { mac: false, commandCenter: false, terminal: false },
|
||||
help: {
|
||||
id: "workspace-tab-target-changes",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { ArchivedAgentCallout } from "@/components/archived-agent-callout";
|
||||
import { FileDropZone } from "@/components/file-drop/file-drop-zone";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { Composer } from "@/composer";
|
||||
import { useWorkspaceHasDiffStat } from "@/composer/workspace-diff-stat";
|
||||
import { resolveComposerTrackTailClearance } from "@/composer/pill-styles";
|
||||
import { getActiveMessageSubmissions } from "@/composer/submission/model";
|
||||
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
||||
@@ -748,6 +749,7 @@ function AgentPanelBody({
|
||||
return (
|
||||
<ChatAgentContent
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
agentId={agentId}
|
||||
isPaneFocused={isPaneFocused}
|
||||
client={client}
|
||||
@@ -760,6 +762,7 @@ function AgentPanelBody({
|
||||
|
||||
function ChatAgentContent({
|
||||
serverId,
|
||||
workspaceId,
|
||||
agentId,
|
||||
isPaneFocused,
|
||||
client,
|
||||
@@ -768,6 +771,7 @@ function ChatAgentContent({
|
||||
onOpenWorkspaceFile,
|
||||
}: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
agentId?: string;
|
||||
isPaneFocused: boolean;
|
||||
client: ReturnType<typeof useHostRuntimeClient>;
|
||||
@@ -1164,6 +1168,7 @@ function ChatAgentContent({
|
||||
return (
|
||||
<ChatAgentReadyContent
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
agentId={agentId}
|
||||
isPaneFocused={isPaneFocused}
|
||||
isArchivingCurrentAgent={isArchivingCurrentAgent}
|
||||
@@ -1192,6 +1197,7 @@ function ChatAgentContent({
|
||||
|
||||
const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
serverId,
|
||||
workspaceId,
|
||||
agentId,
|
||||
isPaneFocused,
|
||||
isArchivingCurrentAgent,
|
||||
@@ -1216,6 +1222,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
onOpenWorkspaceFile,
|
||||
}: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
isPaneFocused: boolean;
|
||||
isArchivingCurrentAgent: boolean;
|
||||
@@ -1240,8 +1247,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isCompactFormFactor = useIsCompactFormFactor();
|
||||
const composerTrackTailClearance = resolveComposerTrackTailClearance(isCompactFormFactor);
|
||||
const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId });
|
||||
const tasks = useSessionStore((state): TodoEntry[] | undefined =>
|
||||
state.sessions[serverId]?.agentTasks.get(agentId),
|
||||
@@ -1252,13 +1257,11 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
rows: subagentRows,
|
||||
});
|
||||
const hasActiveComposer = !agentState.archivedAt && !isArchivingCurrentAgent;
|
||||
const showAgentTracks =
|
||||
hasActiveComposer &&
|
||||
hasAgentTracks({
|
||||
subagentRows,
|
||||
tasks,
|
||||
archiveFinishedStatus: archiveFinishedSubagents.status,
|
||||
});
|
||||
const hasVisibleAgentTracks = hasAgentTracks({
|
||||
subagentRows,
|
||||
tasks,
|
||||
archiveFinishedStatus: archiveFinishedSubagents.status,
|
||||
});
|
||||
const rawAgentInputDraft = useAgentInputDraft({
|
||||
draftKey: buildDraftStoreKey({
|
||||
serverId,
|
||||
@@ -1329,18 +1332,21 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
<AgentStreamSection
|
||||
streamViewRef={streamViewRef}
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
agentId={agentId}
|
||||
agent={effectiveAgent}
|
||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
||||
bottomOverlayTailClearance={showAgentTracks ? composerTrackTailClearance : 0}
|
||||
hasActiveComposer={hasActiveComposer}
|
||||
hasVisibleAgentTracks={hasVisibleAgentTracks}
|
||||
toast={toastApi}
|
||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||
/>
|
||||
</RenderProfile>
|
||||
{showAgentTracks ? (
|
||||
{hasActiveComposer ? (
|
||||
<AgentTracks
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
subagentRows={subagentRows}
|
||||
tasks={tasks}
|
||||
archiveFinishedStatus={archiveFinishedSubagents.status}
|
||||
@@ -1409,24 +1415,35 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
const AgentStreamSection = memo(function AgentStreamSection({
|
||||
streamViewRef,
|
||||
serverId,
|
||||
workspaceId,
|
||||
agentId,
|
||||
agent,
|
||||
routeBottomAnchorRequest,
|
||||
hasAppliedAuthoritativeHistory,
|
||||
bottomOverlayTailClearance,
|
||||
hasActiveComposer,
|
||||
hasVisibleAgentTracks,
|
||||
toast,
|
||||
onOpenWorkspaceFile,
|
||||
}: {
|
||||
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
agentId?: string;
|
||||
agent: AgentScreenAgent;
|
||||
routeBottomAnchorRequest: RouteBottomAnchorRequest;
|
||||
hasAppliedAuthoritativeHistory: boolean;
|
||||
bottomOverlayTailClearance: number;
|
||||
hasActiveComposer: boolean;
|
||||
hasVisibleAgentTracks: boolean;
|
||||
toast: ReturnType<typeof useToastHost>["api"];
|
||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||
}) {
|
||||
const isCompactFormFactor = useIsCompactFormFactor();
|
||||
const hasWorkspaceDiffStat = useWorkspaceHasDiffStat(serverId, workspaceId);
|
||||
const hasVisibleComposerTracks =
|
||||
hasActiveComposer && (hasVisibleAgentTracks || hasWorkspaceDiffStat);
|
||||
const bottomOverlayTailClearance = hasVisibleComposerTracks
|
||||
? resolveComposerTrackTailClearance(isCompactFormFactor)
|
||||
: 0;
|
||||
const streamItemsRaw = useSessionStore((state) =>
|
||||
agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { memo, useCallback, type ReactElement } from "react";
|
||||
import { WorkspaceDiffStatPill } from "@/composer/diff-stat-pill";
|
||||
import { useWorkspaceHasDiffStat } from "@/composer/workspace-diff-stat";
|
||||
import { AgentTaskList } from "@/composer/task-list";
|
||||
import { ComposerTrackBar } from "@/composer/tracks";
|
||||
import { supportsDesktopPaneSplits, useIsCompactFormFactor } from "@/constants/layout";
|
||||
@@ -18,26 +20,29 @@ import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
||||
import { openSupportingTab } from "@/workspace-tabs/side-panel";
|
||||
|
||||
/**
|
||||
* The pane's trackers — its subagents and its task list — as a row of pills above the composer.
|
||||
* The pane's ambient context — workspace changes, subagents, and tasks — as a row of pills above
|
||||
* the composer.
|
||||
*
|
||||
* The row shares the composer's keyboard transform and owns the space between itself and the
|
||||
* transcript. Its data remains agent state: a subagent row opens a tab and the task list reads
|
||||
* the agent's stream.
|
||||
* transcript. Each pill owns its action while tab placement stays behind the workspace boundary.
|
||||
*/
|
||||
export const AgentTracks = memo(function AgentTracks({
|
||||
serverId,
|
||||
workspaceId,
|
||||
subagentRows,
|
||||
tasks,
|
||||
archiveFinishedStatus,
|
||||
onArchiveFinished,
|
||||
}: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
subagentRows: SubagentRow[];
|
||||
tasks: TodoEntry[] | undefined;
|
||||
archiveFinishedStatus: ArchiveFinishedStatus;
|
||||
onArchiveFinished: () => void;
|
||||
}): ReactElement | null {
|
||||
const { workspaceId, tabId, openTab } = usePaneContext();
|
||||
const { tabId, openTab } = usePaneContext();
|
||||
const hasWorkspaceDiffStat = useWorkspaceHasDiffStat(serverId, workspaceId);
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const canSplit = supportsDesktopPaneSplits() && !isCompact;
|
||||
const openInSidePanelByDefault = useSettings(
|
||||
@@ -87,13 +92,25 @@ export const AgentTracks = memo(function AgentTracks({
|
||||
},
|
||||
[canSplit, isCompact, openInSidePanelByDefault, openTab, tabId, workspaceKey],
|
||||
);
|
||||
const handleOpenChanges = useCallback(() => {
|
||||
if (!workspaceKey) {
|
||||
return;
|
||||
}
|
||||
openSupportingTab({
|
||||
isCompact,
|
||||
workspaceKey,
|
||||
target: { kind: "working_diff" },
|
||||
openInSidePanelByDefault,
|
||||
});
|
||||
}, [isCompact, openInSidePanelByDefault, workspaceKey]);
|
||||
|
||||
if (!hasAgentTracks({ subagentRows, tasks, archiveFinishedStatus })) {
|
||||
if (!hasWorkspaceDiffStat && !hasAgentTracks({ subagentRows, tasks, archiveFinishedStatus })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposerTrackBar>
|
||||
<AgentTaskList tasks={tasks} />
|
||||
<SubagentsTrack
|
||||
rows={subagentRows}
|
||||
onOpenSubagent={handleOpenSubagent}
|
||||
@@ -103,7 +120,11 @@ export const AgentTracks = memo(function AgentTracks({
|
||||
archiveFinishedStatus={archiveFinishedStatus}
|
||||
onDetachSubagent={canDetachSubagents ? detachSubagent : undefined}
|
||||
/>
|
||||
<AgentTaskList tasks={tasks} />
|
||||
<WorkspaceDiffStatPill
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
onPress={handleOpenChanges}
|
||||
/>
|
||||
</ComposerTrackBar>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Text, View } from "react-native";
|
||||
import { FolderTree } from "lucide-react-native";
|
||||
import { withUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import invariant from "tiny-invariant";
|
||||
@@ -9,14 +9,11 @@ import { usePaneContext } from "@/panels/pane-context";
|
||||
import type { PanelRegistration } from "@/panels/panel-registry";
|
||||
import { useAddFileToChat } from "@/panels/use-add-file-to-chat";
|
||||
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
||||
import { TreeRail } from "@/components/tree-rail";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
|
||||
const ThemedFolderTree = withUnistyles(FolderTree);
|
||||
const CENTERED_PADDED_STYLE = {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 16,
|
||||
} as const;
|
||||
const FILE_TREE_WIDTH = 220;
|
||||
|
||||
function useFilesPanelDescriptor() {
|
||||
const { t } = useTranslation();
|
||||
@@ -35,6 +32,7 @@ function FilesPanel() {
|
||||
const { serverId, workspaceId, target, retargetCurrentTab } = usePaneContext();
|
||||
const workspaceRoot = useWorkspaceDirectory(serverId, workspaceId);
|
||||
const { addFile, canAddToChat } = useAddFileToChat({ serverId, workspaceId });
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
invariant(target.kind === "files", "FilesPanel requires files target");
|
||||
const onOpenFile = useCallback(
|
||||
(path: string) => retargetCurrentTab({ kind: "file", path }),
|
||||
@@ -42,19 +40,35 @@ function FilesPanel() {
|
||||
);
|
||||
if (!workspaceRoot) {
|
||||
return (
|
||||
<View style={CENTERED_PADDED_STYLE}>
|
||||
<View style={styles.centerState}>
|
||||
<Text>{t("panels.file.directoryMissing")}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (isCompact) {
|
||||
return (
|
||||
<FileExplorerPane
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onOpenFile={onOpenFile}
|
||||
onAddToChat={canAddToChat ? addFile : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FileExplorerPane
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onOpenFile={onOpenFile}
|
||||
onAddToChat={canAddToChat ? addFile : undefined}
|
||||
/>
|
||||
<TreeRail testID="files-tree-rail" width={FILE_TREE_WIDTH}>
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.emptyText}>{t("panels.files.chooseFile")}</Text>
|
||||
</View>
|
||||
<FileExplorerPane
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onOpenFile={onOpenFile}
|
||||
onAddToChat={canAddToChat ? addFile : undefined}
|
||||
/>
|
||||
</TreeRail>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,3 +78,16 @@ export const filesPanelRegistration: PanelRegistration<"files"> = {
|
||||
component: FilesPanel,
|
||||
useDescriptor: useFilesPanelDescriptor,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
centerState: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: theme.spacing[4],
|
||||
},
|
||||
emptyText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.base,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { useWorkspaceFocusRestoration } from "@/workspace/focus";
|
||||
import { useReviewDraftComments, useReviewDraftStore, type ReviewDraftComment } from "./store";
|
||||
import { buildReviewableDiffTargetKey, type ReviewableDiffTarget } from "@/utils/diff-layout";
|
||||
import {
|
||||
@@ -441,7 +440,6 @@ export function InlineReviewEditor({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<EditingTextInputHandle | null>(null);
|
||||
const focus = useWorkspaceFocusRestoration();
|
||||
const [body, setBody] = useState(initialBody);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const trimmedBody = body.trim();
|
||||
@@ -452,13 +450,11 @@ export function InlineReviewEditor({
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
focus.unfocus();
|
||||
setIsFocused(true);
|
||||
}, [focus]);
|
||||
}, []);
|
||||
const handleBlur = useCallback(() => {
|
||||
setIsFocused(false);
|
||||
focus.restore();
|
||||
}, [focus]);
|
||||
}, []);
|
||||
const handleSave = useCallback(() => onSave(trimmedBody), [onSave, trimmedBody]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -106,8 +106,10 @@ import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
||||
import { useCheckoutStatusQuery } from "@/git/use-status-query";
|
||||
import { TrailingActionScrim } from "@/components/ui/trailing-action-scrim";
|
||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import { useSettings } from "@/hooks/use-settings";
|
||||
import { buildWorkspaceKeyboardHandlerId } from "@/keyboard/handler-id";
|
||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||
import { openSupportingTab } from "@/workspace-tabs/side-panel";
|
||||
|
||||
const DROPDOWN_WIDTH = 220;
|
||||
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
|
||||
@@ -1169,6 +1171,10 @@ function ResolvedWorkspaceDesktopTabsRow({
|
||||
}: ResolvedWorkspaceDesktopTabsRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const openInSidePanelByDefault = useSettings(
|
||||
(settings) => settings.openSupportingTabsInSidePanel,
|
||||
);
|
||||
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
||||
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
||||
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
||||
@@ -1450,6 +1456,14 @@ function ResolvedWorkspaceDesktopTabsRow({
|
||||
[openNewTab, paneId, workspaceKey],
|
||||
);
|
||||
const handleOpenChanges = useCallback(() => openPanelTarget(CHANGES_TARGET), [openPanelTarget]);
|
||||
const handleOpenChangesShortcut = useCallback(() => {
|
||||
openSupportingTab({
|
||||
isCompact,
|
||||
workspaceKey,
|
||||
target: CHANGES_TARGET,
|
||||
openInSidePanelByDefault,
|
||||
});
|
||||
}, [isCompact, openInSidePanelByDefault, workspaceKey]);
|
||||
const handleOpenFiles = useCallback(() => openPanelTarget(FILES_TARGET), [openPanelTarget]);
|
||||
const handleOpenPullRequest = useCallback(
|
||||
() => openPanelTarget(PULL_REQUEST_TARGET),
|
||||
@@ -1484,7 +1498,7 @@ function ResolvedWorkspaceDesktopTabsRow({
|
||||
case "workspace.tab.target.changes":
|
||||
if (!isGit) return true;
|
||||
setNewTabMenuOpen(false);
|
||||
handleOpenChanges();
|
||||
handleOpenChangesShortcut();
|
||||
return true;
|
||||
case "workspace.tab.target.files":
|
||||
setNewTabMenuOpen(false);
|
||||
@@ -1497,7 +1511,7 @@ function ResolvedWorkspaceDesktopTabsRow({
|
||||
[
|
||||
handleCreateAgentTab,
|
||||
handleCreateBrowser,
|
||||
handleOpenChanges,
|
||||
handleOpenChangesShortcut,
|
||||
handleOpenFiles,
|
||||
isFocused,
|
||||
isGit,
|
||||
|
||||
@@ -122,13 +122,6 @@ export const WorkspacePaneEmptyState = memo(function WorkspacePaneEmptyState({
|
||||
<View style={styles.container} testID="workspace-pane-empty-state">
|
||||
<View style={styles.rail}>
|
||||
<View style={styles.stack}>
|
||||
<LauncherRow
|
||||
label={t("workspace.tabs.actions.files")}
|
||||
icon={FILES_ICON}
|
||||
onPress={openFiles}
|
||||
>
|
||||
<LauncherShortcut actionId="workspace-tab-target-files" />
|
||||
</LauncherRow>
|
||||
{showChanges ? (
|
||||
<LauncherRow
|
||||
label={t("workspace.tabs.actions.changes")}
|
||||
@@ -138,6 +131,13 @@ export const WorkspacePaneEmptyState = memo(function WorkspacePaneEmptyState({
|
||||
<LauncherShortcut actionId="workspace-tab-target-changes" />
|
||||
</LauncherRow>
|
||||
) : null}
|
||||
<LauncherRow
|
||||
label={t("workspace.tabs.actions.files")}
|
||||
icon={FILES_ICON}
|
||||
onPress={openFiles}
|
||||
>
|
||||
<LauncherShortcut actionId="workspace-tab-target-files" />
|
||||
</LauncherRow>
|
||||
{showPullRequest ? (
|
||||
<LauncherRow
|
||||
label={t("workspace.tabs.actions.pullRequest")}
|
||||
@@ -146,14 +146,14 @@ export const WorkspacePaneEmptyState = memo(function WorkspacePaneEmptyState({
|
||||
/>
|
||||
) : null}
|
||||
<LauncherRow
|
||||
label={t("workspace.tabs.actions.newAgent")}
|
||||
label={t("workspace.tabs.fallback.agent")}
|
||||
icon={AGENT_ICON}
|
||||
onPress={createAgent}
|
||||
>
|
||||
<LauncherShortcut actionId="workspace-tab-target-agent" />
|
||||
</LauncherRow>
|
||||
<LauncherRow
|
||||
label={t("workspace.tabs.actions.newTerminal")}
|
||||
label={t("workspace.tabs.fallback.terminal")}
|
||||
icon={TERMINAL_ICON}
|
||||
onPress={createTerminal}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user