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
|
persisted in client app settings as `openSupportingTabsInSidePanel`. Off means new
|
||||||
supporting tabs land in the focused pane instead. Mobile ignores it. Explicit actions
|
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
|
— 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
|
## 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);
|
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 ({
|
test("split canvas creates a review on the changed side and keeps it in that column", async ({
|
||||||
page,
|
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);
|
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");
|
const workspace = await createWorkspaceWithExactSelectionDiff("ABCDEFGHIJ");
|
||||||
await useUnwrappedDiffLines(page);
|
await useUnwrappedDiffLines(page);
|
||||||
await openSelectionWorkspaceChanges(page, workspace);
|
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 expect(page.getByTestId("inline-review-editor")).toHaveCount(0);
|
||||||
|
|
||||||
await clickFirstChangedLine(page);
|
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 ({
|
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");
|
if (!bodyBounds) throw new Error("Expanded diff body has no bounds");
|
||||||
const lineHeight = Math.round(fontSize * 1.5);
|
const lineHeight = Math.round(fontSize * 1.5);
|
||||||
const columnLeft = side === "right" ? bodyBounds.x + bodyBounds.width / 2 : bodyBounds.x;
|
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();
|
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;
|
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 () => {
|
test.beforeAll(async () => {
|
||||||
workspace = await seedWorkspace({
|
workspace = await seedWorkspace({
|
||||||
repoPrefix: "file-tree-rail-toggle-",
|
repoPrefix: "file-tree-rail-toggle-",
|
||||||
@@ -21,6 +33,21 @@ test.afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("File panel tree rail", () => {
|
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) => {
|
test("the file toolbar toggle closes and reopens the tree", async ({ page }, testInfo) => {
|
||||||
await gotoWorkspace(page, workspace.workspaceId);
|
await gotoWorkspace(page, workspace.workspaceId);
|
||||||
await openFileExplorer(page);
|
await openFileExplorer(page);
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ test.describe("Tab creation", () => {
|
|||||||
const shortcutPrefix = process.platform === "darwin" ? /⇧⌘/ : /Ctrl.*Shift/;
|
const shortcutPrefix = process.platform === "darwin" ? /⇧⌘/ : /Ctrl.*Shift/;
|
||||||
await expect(agent).toContainText(new RegExp(`${shortcutPrefix.source}.*A`));
|
await expect(agent).toContainText(new RegExp(`${shortcutPrefix.source}.*A`));
|
||||||
await expect(terminal).toContainText(new RegExp(`${shortcutPrefix.source}.*T`));
|
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(files).toContainText(new RegExp(`${shortcutPrefix.source}.*E`));
|
||||||
await expect(agent).toBeFocused();
|
await expect(agent).toBeFocused();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ function emptyState(scope: Locator): Locator {
|
|||||||
return scope.getByTestId("workspace-pane-empty-state");
|
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 {
|
function sidePanelToggle(page: Page): Locator {
|
||||||
return page.getByTestId("workspace-explorer-toggle").first();
|
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 test.step("revealing the Side panel seeds nothing", async () => {
|
||||||
await sidePanelToggle(page).click();
|
await sidePanelToggle(page).click();
|
||||||
await expect(sidePanel(page)).toBeVisible({ timeout: 30_000 });
|
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 expect(page.getByTestId("workspace-tab-working_diff")).toHaveCount(0);
|
||||||
await capture(page, testInfo, "01-side-panel-reveals-empty");
|
await capture(page, testInfo, "01-side-panel-reveals-empty");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface MockAgentWorkspace {
|
|||||||
export interface MockAgentOptions {
|
export interface MockAgentOptions {
|
||||||
repoPrefix: string;
|
repoPrefix: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
repo?: Parameters<typeof seedWorkspace>[0]["repo"];
|
||||||
port?: number;
|
port?: number;
|
||||||
initialPrompt?: string;
|
initialPrompt?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -29,7 +30,11 @@ export interface MockAgentOptions {
|
|||||||
export async function seedMockAgentWorkspace(
|
export async function seedMockAgentWorkspace(
|
||||||
options: MockAgentOptions,
|
options: MockAgentOptions,
|
||||||
): Promise<MockAgentWorkspace> {
|
): 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 {
|
try {
|
||||||
const agent = await workspace.client.createAgent({
|
const agent = await workspace.client.createAgent({
|
||||||
provider: "mock",
|
provider: "mock",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface SeedWorkspaceDescriptor {
|
|||||||
projectDisplayName: string;
|
projectDisplayName: string;
|
||||||
projectRootPath: string;
|
projectRootPath: string;
|
||||||
workspaceDirectory: string;
|
workspaceDirectory: string;
|
||||||
|
diffStat: { additions: number; deletions: number } | null;
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1662,7 +1662,6 @@ function getErrorRecoveryPath(state: AgentFileExplorerState | undefined): string
|
|||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.surfaceSidebar,
|
|
||||||
},
|
},
|
||||||
desktopSplit: {
|
desktopSplit: {
|
||||||
flex: 1,
|
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();
|
schedulePaint();
|
||||||
return;
|
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;
|
selectionRef.current = null;
|
||||||
reviewActions.onStartComment(hit.target);
|
reviewActions.onStartComment(hit.target);
|
||||||
schedulePaint();
|
schedulePaint();
|
||||||
|
|||||||
@@ -1729,6 +1729,7 @@ export const ar: TranslationResources = {
|
|||||||
label: "الملفات",
|
label: "الملفات",
|
||||||
subtitle: "ملفات مساحة العمل",
|
subtitle: "ملفات مساحة العمل",
|
||||||
tooltip: "تصفح ملفات مساحة العمل",
|
tooltip: "تصفح ملفات مساحة العمل",
|
||||||
|
chooseFile: "اختر ملفًا",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "طلب السحب",
|
label: "طلب السحب",
|
||||||
|
|||||||
@@ -1739,6 +1739,7 @@ export const en = {
|
|||||||
label: "Files",
|
label: "Files",
|
||||||
subtitle: "Workspace files",
|
subtitle: "Workspace files",
|
||||||
tooltip: "Browse workspace files",
|
tooltip: "Browse workspace files",
|
||||||
|
chooseFile: "Choose a file",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "Pull request",
|
label: "Pull request",
|
||||||
|
|||||||
@@ -1775,6 +1775,7 @@ export const es: TranslationResources = {
|
|||||||
label: "Archivos",
|
label: "Archivos",
|
||||||
subtitle: "Archivos del espacio de trabajo",
|
subtitle: "Archivos del espacio de trabajo",
|
||||||
tooltip: "Explorar archivos del espacio de trabajo",
|
tooltip: "Explorar archivos del espacio de trabajo",
|
||||||
|
chooseFile: "Elige un archivo",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "Solicitud de extracción",
|
label: "Solicitud de extracción",
|
||||||
|
|||||||
@@ -1779,6 +1779,7 @@ export const fr: TranslationResources = {
|
|||||||
label: "Fichiers",
|
label: "Fichiers",
|
||||||
subtitle: "Fichiers de l’espace de travail",
|
subtitle: "Fichiers de l’espace de travail",
|
||||||
tooltip: "Parcourir les fichiers de l’espace de travail",
|
tooltip: "Parcourir les fichiers de l’espace de travail",
|
||||||
|
chooseFile: "Choisissez un fichier",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "Demande de fusion",
|
label: "Demande de fusion",
|
||||||
|
|||||||
@@ -1746,6 +1746,7 @@ export const ja: TranslationResources = {
|
|||||||
label: "ファイル",
|
label: "ファイル",
|
||||||
subtitle: "ワークスペースのファイル",
|
subtitle: "ワークスペースのファイル",
|
||||||
tooltip: "ワークスペースのファイルを参照",
|
tooltip: "ワークスペースのファイルを参照",
|
||||||
|
chooseFile: "ファイルを選択",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "プルリクエスト",
|
label: "プルリクエスト",
|
||||||
|
|||||||
@@ -1739,6 +1739,7 @@ export const ko: TranslationResources = {
|
|||||||
label: "파일",
|
label: "파일",
|
||||||
subtitle: "워크스페이스 파일",
|
subtitle: "워크스페이스 파일",
|
||||||
tooltip: "워크스페이스 파일 탐색",
|
tooltip: "워크스페이스 파일 탐색",
|
||||||
|
chooseFile: "파일 선택",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "풀 리퀘스트",
|
label: "풀 리퀘스트",
|
||||||
|
|||||||
@@ -1761,6 +1761,7 @@ export const ptBR: TranslationResources = {
|
|||||||
label: "Arquivos",
|
label: "Arquivos",
|
||||||
subtitle: "Arquivos do espaço de trabalho",
|
subtitle: "Arquivos do espaço de trabalho",
|
||||||
tooltip: "Explorar arquivos do espaço de trabalho",
|
tooltip: "Explorar arquivos do espaço de trabalho",
|
||||||
|
chooseFile: "Escolha um arquivo",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "Pull request",
|
label: "Pull request",
|
||||||
|
|||||||
@@ -1760,6 +1760,7 @@ export const ru: TranslationResources = {
|
|||||||
label: "Файлы",
|
label: "Файлы",
|
||||||
subtitle: "Файлы рабочего пространства",
|
subtitle: "Файлы рабочего пространства",
|
||||||
tooltip: "Просмотр файлов рабочего пространства",
|
tooltip: "Просмотр файлов рабочего пространства",
|
||||||
|
chooseFile: "Выберите файл",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "PR",
|
label: "PR",
|
||||||
|
|||||||
@@ -1709,6 +1709,7 @@ export const zhCN: TranslationResources = {
|
|||||||
label: "文件",
|
label: "文件",
|
||||||
subtitle: "工作区文件",
|
subtitle: "工作区文件",
|
||||||
tooltip: "浏览工作区文件",
|
tooltip: "浏览工作区文件",
|
||||||
|
chooseFile: "选择文件",
|
||||||
},
|
},
|
||||||
pullRequest: {
|
pullRequest: {
|
||||||
label: "拉取请求",
|
label: "拉取请求",
|
||||||
|
|||||||
@@ -1158,7 +1158,7 @@ describe("direct new-tab target shortcuts", () => {
|
|||||||
const targetCases = [
|
const targetCases = [
|
||||||
["a", "KeyA", "workspace.tab.target.agent"],
|
["a", "KeyA", "workspace.tab.target.agent"],
|
||||||
["b", "KeyB", "workspace.tab.target.browser"],
|
["b", "KeyB", "workspace.tab.target.browser"],
|
||||||
["c", "KeyC", "workspace.tab.target.changes"],
|
["g", "KeyG", "workspace.tab.target.changes"],
|
||||||
["e", "KeyE", "workspace.tab.target.files"],
|
["e", "KeyE", "workspace.tab.target.files"],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -1192,9 +1192,9 @@ describe("direct new-tab target shortcuts", () => {
|
|||||||
|
|
||||||
it("uses the existing override map for target matching and display", () => {
|
it("uses the existing override map for target matching and display", () => {
|
||||||
const bindingId = "workspace-tab-target-agent-ctrl-shift-a-non-mac";
|
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({
|
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" },
|
context: { ...desktopNonMac, focusScope: "other" },
|
||||||
bindings: buildEffectiveBindings(overrides),
|
bindings: buildEffectiveBindings(overrides),
|
||||||
});
|
});
|
||||||
@@ -1208,6 +1208,6 @@ describe("direct new-tab target shortcuts", () => {
|
|||||||
expect(original.match).toBeNull();
|
expect(original.match).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
resolveShortcutKeysForAction("workspace-tab-target-agent", overrides, desktopNonMac),
|
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",
|
id: "workspace-tab-target-changes-cmd-shift-c-mac",
|
||||||
action: "workspace.tab.target.changes",
|
action: "workspace.tab.target.changes",
|
||||||
combo: "Cmd+Shift+C",
|
combo: "Cmd+Shift+G",
|
||||||
when: { mac: true, commandCenter: false },
|
when: { mac: true, commandCenter: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-target-changes",
|
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",
|
id: "workspace-tab-target-changes-ctrl-shift-c-non-mac",
|
||||||
action: "workspace.tab.target.changes",
|
action: "workspace.tab.target.changes",
|
||||||
combo: "Ctrl+Shift+C",
|
combo: "Ctrl+Shift+G",
|
||||||
when: { mac: false, commandCenter: false, terminal: false },
|
when: { mac: false, commandCenter: false, terminal: false },
|
||||||
help: {
|
help: {
|
||||||
id: "workspace-tab-target-changes",
|
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 { FileDropZone } from "@/components/file-drop/file-drop-zone";
|
||||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||||
import { Composer } from "@/composer";
|
import { Composer } from "@/composer";
|
||||||
|
import { useWorkspaceHasDiffStat } from "@/composer/workspace-diff-stat";
|
||||||
import { resolveComposerTrackTailClearance } from "@/composer/pill-styles";
|
import { resolveComposerTrackTailClearance } from "@/composer/pill-styles";
|
||||||
import { getActiveMessageSubmissions } from "@/composer/submission/model";
|
import { getActiveMessageSubmissions } from "@/composer/submission/model";
|
||||||
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
||||||
@@ -748,6 +749,7 @@ function AgentPanelBody({
|
|||||||
return (
|
return (
|
||||||
<ChatAgentContent
|
<ChatAgentContent
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
isPaneFocused={isPaneFocused}
|
isPaneFocused={isPaneFocused}
|
||||||
client={client}
|
client={client}
|
||||||
@@ -760,6 +762,7 @@ function AgentPanelBody({
|
|||||||
|
|
||||||
function ChatAgentContent({
|
function ChatAgentContent({
|
||||||
serverId,
|
serverId,
|
||||||
|
workspaceId,
|
||||||
agentId,
|
agentId,
|
||||||
isPaneFocused,
|
isPaneFocused,
|
||||||
client,
|
client,
|
||||||
@@ -768,6 +771,7 @@ function ChatAgentContent({
|
|||||||
onOpenWorkspaceFile,
|
onOpenWorkspaceFile,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
isPaneFocused: boolean;
|
isPaneFocused: boolean;
|
||||||
client: ReturnType<typeof useHostRuntimeClient>;
|
client: ReturnType<typeof useHostRuntimeClient>;
|
||||||
@@ -1164,6 +1168,7 @@ function ChatAgentContent({
|
|||||||
return (
|
return (
|
||||||
<ChatAgentReadyContent
|
<ChatAgentReadyContent
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
isPaneFocused={isPaneFocused}
|
isPaneFocused={isPaneFocused}
|
||||||
isArchivingCurrentAgent={isArchivingCurrentAgent}
|
isArchivingCurrentAgent={isArchivingCurrentAgent}
|
||||||
@@ -1192,6 +1197,7 @@ function ChatAgentContent({
|
|||||||
|
|
||||||
const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||||
serverId,
|
serverId,
|
||||||
|
workspaceId,
|
||||||
agentId,
|
agentId,
|
||||||
isPaneFocused,
|
isPaneFocused,
|
||||||
isArchivingCurrentAgent,
|
isArchivingCurrentAgent,
|
||||||
@@ -1216,6 +1222,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
onOpenWorkspaceFile,
|
onOpenWorkspaceFile,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
isPaneFocused: boolean;
|
isPaneFocused: boolean;
|
||||||
isArchivingCurrentAgent: boolean;
|
isArchivingCurrentAgent: boolean;
|
||||||
@@ -1240,8 +1247,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isCompactFormFactor = useIsCompactFormFactor();
|
|
||||||
const composerTrackTailClearance = resolveComposerTrackTailClearance(isCompactFormFactor);
|
|
||||||
const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId });
|
const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId });
|
||||||
const tasks = useSessionStore((state): TodoEntry[] | undefined =>
|
const tasks = useSessionStore((state): TodoEntry[] | undefined =>
|
||||||
state.sessions[serverId]?.agentTasks.get(agentId),
|
state.sessions[serverId]?.agentTasks.get(agentId),
|
||||||
@@ -1252,13 +1257,11 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
rows: subagentRows,
|
rows: subagentRows,
|
||||||
});
|
});
|
||||||
const hasActiveComposer = !agentState.archivedAt && !isArchivingCurrentAgent;
|
const hasActiveComposer = !agentState.archivedAt && !isArchivingCurrentAgent;
|
||||||
const showAgentTracks =
|
const hasVisibleAgentTracks = hasAgentTracks({
|
||||||
hasActiveComposer &&
|
subagentRows,
|
||||||
hasAgentTracks({
|
tasks,
|
||||||
subagentRows,
|
archiveFinishedStatus: archiveFinishedSubagents.status,
|
||||||
tasks,
|
});
|
||||||
archiveFinishedStatus: archiveFinishedSubagents.status,
|
|
||||||
});
|
|
||||||
const rawAgentInputDraft = useAgentInputDraft({
|
const rawAgentInputDraft = useAgentInputDraft({
|
||||||
draftKey: buildDraftStoreKey({
|
draftKey: buildDraftStoreKey({
|
||||||
serverId,
|
serverId,
|
||||||
@@ -1329,18 +1332,21 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
<AgentStreamSection
|
<AgentStreamSection
|
||||||
streamViewRef={streamViewRef}
|
streamViewRef={streamViewRef}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
agent={effectiveAgent}
|
agent={effectiveAgent}
|
||||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||||
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
||||||
bottomOverlayTailClearance={showAgentTracks ? composerTrackTailClearance : 0}
|
hasActiveComposer={hasActiveComposer}
|
||||||
|
hasVisibleAgentTracks={hasVisibleAgentTracks}
|
||||||
toast={toastApi}
|
toast={toastApi}
|
||||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||||
/>
|
/>
|
||||||
</RenderProfile>
|
</RenderProfile>
|
||||||
{showAgentTracks ? (
|
{hasActiveComposer ? (
|
||||||
<AgentTracks
|
<AgentTracks
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
subagentRows={subagentRows}
|
subagentRows={subagentRows}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
archiveFinishedStatus={archiveFinishedSubagents.status}
|
archiveFinishedStatus={archiveFinishedSubagents.status}
|
||||||
@@ -1409,24 +1415,35 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
const AgentStreamSection = memo(function AgentStreamSection({
|
const AgentStreamSection = memo(function AgentStreamSection({
|
||||||
streamViewRef,
|
streamViewRef,
|
||||||
serverId,
|
serverId,
|
||||||
|
workspaceId,
|
||||||
agentId,
|
agentId,
|
||||||
agent,
|
agent,
|
||||||
routeBottomAnchorRequest,
|
routeBottomAnchorRequest,
|
||||||
hasAppliedAuthoritativeHistory,
|
hasAppliedAuthoritativeHistory,
|
||||||
bottomOverlayTailClearance,
|
hasActiveComposer,
|
||||||
|
hasVisibleAgentTracks,
|
||||||
toast,
|
toast,
|
||||||
onOpenWorkspaceFile,
|
onOpenWorkspaceFile,
|
||||||
}: {
|
}: {
|
||||||
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
agent: AgentScreenAgent;
|
agent: AgentScreenAgent;
|
||||||
routeBottomAnchorRequest: RouteBottomAnchorRequest;
|
routeBottomAnchorRequest: RouteBottomAnchorRequest;
|
||||||
hasAppliedAuthoritativeHistory: boolean;
|
hasAppliedAuthoritativeHistory: boolean;
|
||||||
bottomOverlayTailClearance: number;
|
hasActiveComposer: boolean;
|
||||||
|
hasVisibleAgentTracks: boolean;
|
||||||
toast: ReturnType<typeof useToastHost>["api"];
|
toast: ReturnType<typeof useToastHost>["api"];
|
||||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
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) =>
|
const streamItemsRaw = useSessionStore((state) =>
|
||||||
agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined,
|
agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { memo, useCallback, type ReactElement } from "react";
|
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 { AgentTaskList } from "@/composer/task-list";
|
||||||
import { ComposerTrackBar } from "@/composer/tracks";
|
import { ComposerTrackBar } from "@/composer/tracks";
|
||||||
import { supportsDesktopPaneSplits, useIsCompactFormFactor } from "@/constants/layout";
|
import { supportsDesktopPaneSplits, useIsCompactFormFactor } from "@/constants/layout";
|
||||||
@@ -18,26 +20,29 @@ import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
|||||||
import { openSupportingTab } from "@/workspace-tabs/side-panel";
|
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
|
* 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
|
* transcript. Each pill owns its action while tab placement stays behind the workspace boundary.
|
||||||
* the agent's stream.
|
|
||||||
*/
|
*/
|
||||||
export const AgentTracks = memo(function AgentTracks({
|
export const AgentTracks = memo(function AgentTracks({
|
||||||
serverId,
|
serverId,
|
||||||
|
workspaceId,
|
||||||
subagentRows,
|
subagentRows,
|
||||||
tasks,
|
tasks,
|
||||||
archiveFinishedStatus,
|
archiveFinishedStatus,
|
||||||
onArchiveFinished,
|
onArchiveFinished,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
subagentRows: SubagentRow[];
|
subagentRows: SubagentRow[];
|
||||||
tasks: TodoEntry[] | undefined;
|
tasks: TodoEntry[] | undefined;
|
||||||
archiveFinishedStatus: ArchiveFinishedStatus;
|
archiveFinishedStatus: ArchiveFinishedStatus;
|
||||||
onArchiveFinished: () => void;
|
onArchiveFinished: () => void;
|
||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
const { workspaceId, tabId, openTab } = usePaneContext();
|
const { tabId, openTab } = usePaneContext();
|
||||||
|
const hasWorkspaceDiffStat = useWorkspaceHasDiffStat(serverId, workspaceId);
|
||||||
const isCompact = useIsCompactFormFactor();
|
const isCompact = useIsCompactFormFactor();
|
||||||
const canSplit = supportsDesktopPaneSplits() && !isCompact;
|
const canSplit = supportsDesktopPaneSplits() && !isCompact;
|
||||||
const openInSidePanelByDefault = useSettings(
|
const openInSidePanelByDefault = useSettings(
|
||||||
@@ -87,13 +92,25 @@ export const AgentTracks = memo(function AgentTracks({
|
|||||||
},
|
},
|
||||||
[canSplit, isCompact, openInSidePanelByDefault, openTab, tabId, workspaceKey],
|
[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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerTrackBar>
|
<ComposerTrackBar>
|
||||||
|
<AgentTaskList tasks={tasks} />
|
||||||
<SubagentsTrack
|
<SubagentsTrack
|
||||||
rows={subagentRows}
|
rows={subagentRows}
|
||||||
onOpenSubagent={handleOpenSubagent}
|
onOpenSubagent={handleOpenSubagent}
|
||||||
@@ -103,7 +120,11 @@ export const AgentTracks = memo(function AgentTracks({
|
|||||||
archiveFinishedStatus={archiveFinishedStatus}
|
archiveFinishedStatus={archiveFinishedStatus}
|
||||||
onDetachSubagent={canDetachSubagents ? detachSubagent : undefined}
|
onDetachSubagent={canDetachSubagents ? detachSubagent : undefined}
|
||||||
/>
|
/>
|
||||||
<AgentTaskList tasks={tasks} />
|
<WorkspaceDiffStatPill
|
||||||
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
onPress={handleOpenChanges}
|
||||||
|
/>
|
||||||
</ComposerTrackBar>
|
</ComposerTrackBar>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
import { FolderTree } from "lucide-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 { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
@@ -9,14 +9,11 @@ import { usePaneContext } from "@/panels/pane-context";
|
|||||||
import type { PanelRegistration } from "@/panels/panel-registry";
|
import type { PanelRegistration } from "@/panels/panel-registry";
|
||||||
import { useAddFileToChat } from "@/panels/use-add-file-to-chat";
|
import { useAddFileToChat } from "@/panels/use-add-file-to-chat";
|
||||||
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
||||||
|
import { TreeRail } from "@/components/tree-rail";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
|
|
||||||
const ThemedFolderTree = withUnistyles(FolderTree);
|
const ThemedFolderTree = withUnistyles(FolderTree);
|
||||||
const CENTERED_PADDED_STYLE = {
|
const FILE_TREE_WIDTH = 220;
|
||||||
flex: 1,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: 16,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function useFilesPanelDescriptor() {
|
function useFilesPanelDescriptor() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -35,6 +32,7 @@ function FilesPanel() {
|
|||||||
const { serverId, workspaceId, target, retargetCurrentTab } = usePaneContext();
|
const { serverId, workspaceId, target, retargetCurrentTab } = usePaneContext();
|
||||||
const workspaceRoot = useWorkspaceDirectory(serverId, workspaceId);
|
const workspaceRoot = useWorkspaceDirectory(serverId, workspaceId);
|
||||||
const { addFile, canAddToChat } = useAddFileToChat({ serverId, workspaceId });
|
const { addFile, canAddToChat } = useAddFileToChat({ serverId, workspaceId });
|
||||||
|
const isCompact = useIsCompactFormFactor();
|
||||||
invariant(target.kind === "files", "FilesPanel requires files target");
|
invariant(target.kind === "files", "FilesPanel requires files target");
|
||||||
const onOpenFile = useCallback(
|
const onOpenFile = useCallback(
|
||||||
(path: string) => retargetCurrentTab({ kind: "file", path }),
|
(path: string) => retargetCurrentTab({ kind: "file", path }),
|
||||||
@@ -42,19 +40,35 @@ function FilesPanel() {
|
|||||||
);
|
);
|
||||||
if (!workspaceRoot) {
|
if (!workspaceRoot) {
|
||||||
return (
|
return (
|
||||||
<View style={CENTERED_PADDED_STYLE}>
|
<View style={styles.centerState}>
|
||||||
<Text>{t("panels.file.directoryMissing")}</Text>
|
<Text>{t("panels.file.directoryMissing")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isCompact) {
|
||||||
|
return (
|
||||||
|
<FileExplorerPane
|
||||||
|
serverId={serverId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
workspaceRoot={workspaceRoot}
|
||||||
|
onOpenFile={onOpenFile}
|
||||||
|
onAddToChat={canAddToChat ? addFile : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<FileExplorerPane
|
<TreeRail testID="files-tree-rail" width={FILE_TREE_WIDTH}>
|
||||||
serverId={serverId}
|
<View style={styles.centerState}>
|
||||||
workspaceId={workspaceId}
|
<Text style={styles.emptyText}>{t("panels.files.chooseFile")}</Text>
|
||||||
workspaceRoot={workspaceRoot}
|
</View>
|
||||||
onOpenFile={onOpenFile}
|
<FileExplorerPane
|
||||||
onAddToChat={canAddToChat ? addFile : undefined}
|
serverId={serverId}
|
||||||
/>
|
workspaceId={workspaceId}
|
||||||
|
workspaceRoot={workspaceRoot}
|
||||||
|
onOpenFile={onOpenFile}
|
||||||
|
onAddToChat={canAddToChat ? addFile : undefined}
|
||||||
|
/>
|
||||||
|
</TreeRail>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +78,16 @@ export const filesPanelRegistration: PanelRegistration<"files"> = {
|
|||||||
component: FilesPanel,
|
component: FilesPanel,
|
||||||
useDescriptor: useFilesPanelDescriptor,
|
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 { isWeb } from "@/constants/platform";
|
||||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import { useWorkspaceFocusRestoration } from "@/workspace/focus";
|
|
||||||
import { useReviewDraftComments, useReviewDraftStore, type ReviewDraftComment } from "./store";
|
import { useReviewDraftComments, useReviewDraftStore, type ReviewDraftComment } from "./store";
|
||||||
import { buildReviewableDiffTargetKey, type ReviewableDiffTarget } from "@/utils/diff-layout";
|
import { buildReviewableDiffTargetKey, type ReviewableDiffTarget } from "@/utils/diff-layout";
|
||||||
import {
|
import {
|
||||||
@@ -441,7 +440,6 @@ export function InlineReviewEditor({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputRef = useRef<EditingTextInputHandle | null>(null);
|
const inputRef = useRef<EditingTextInputHandle | null>(null);
|
||||||
const focus = useWorkspaceFocusRestoration();
|
|
||||||
const [body, setBody] = useState(initialBody);
|
const [body, setBody] = useState(initialBody);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const trimmedBody = body.trim();
|
const trimmedBody = body.trim();
|
||||||
@@ -452,13 +450,11 @@ export function InlineReviewEditor({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback(() => {
|
||||||
focus.unfocus();
|
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
}, [focus]);
|
}, []);
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
focus.restore();
|
}, []);
|
||||||
}, [focus]);
|
|
||||||
const handleSave = useCallback(() => onSave(trimmedBody), [onSave, trimmedBody]);
|
const handleSave = useCallback(() => onSave(trimmedBody), [onSave, trimmedBody]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -106,8 +106,10 @@ import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
|||||||
import { useCheckoutStatusQuery } from "@/git/use-status-query";
|
import { useCheckoutStatusQuery } from "@/git/use-status-query";
|
||||||
import { TrailingActionScrim } from "@/components/ui/trailing-action-scrim";
|
import { TrailingActionScrim } from "@/components/ui/trailing-action-scrim";
|
||||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
|
import { useSettings } from "@/hooks/use-settings";
|
||||||
import { buildWorkspaceKeyboardHandlerId } from "@/keyboard/handler-id";
|
import { buildWorkspaceKeyboardHandlerId } from "@/keyboard/handler-id";
|
||||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
|
import { openSupportingTab } from "@/workspace-tabs/side-panel";
|
||||||
|
|
||||||
const DROPDOWN_WIDTH = 220;
|
const DROPDOWN_WIDTH = 220;
|
||||||
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
|
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
|
||||||
@@ -1169,6 +1171,10 @@ function ResolvedWorkspaceDesktopTabsRow({
|
|||||||
}: ResolvedWorkspaceDesktopTabsRowProps) {
|
}: ResolvedWorkspaceDesktopTabsRowProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isCompact = useIsCompactFormFactor();
|
||||||
|
const openInSidePanelByDefault = useSettings(
|
||||||
|
(settings) => settings.openSupportingTabsInSidePanel,
|
||||||
|
);
|
||||||
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
||||||
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
||||||
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
|
||||||
@@ -1450,6 +1456,14 @@ function ResolvedWorkspaceDesktopTabsRow({
|
|||||||
[openNewTab, paneId, workspaceKey],
|
[openNewTab, paneId, workspaceKey],
|
||||||
);
|
);
|
||||||
const handleOpenChanges = useCallback(() => openPanelTarget(CHANGES_TARGET), [openPanelTarget]);
|
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 handleOpenFiles = useCallback(() => openPanelTarget(FILES_TARGET), [openPanelTarget]);
|
||||||
const handleOpenPullRequest = useCallback(
|
const handleOpenPullRequest = useCallback(
|
||||||
() => openPanelTarget(PULL_REQUEST_TARGET),
|
() => openPanelTarget(PULL_REQUEST_TARGET),
|
||||||
@@ -1484,7 +1498,7 @@ function ResolvedWorkspaceDesktopTabsRow({
|
|||||||
case "workspace.tab.target.changes":
|
case "workspace.tab.target.changes":
|
||||||
if (!isGit) return true;
|
if (!isGit) return true;
|
||||||
setNewTabMenuOpen(false);
|
setNewTabMenuOpen(false);
|
||||||
handleOpenChanges();
|
handleOpenChangesShortcut();
|
||||||
return true;
|
return true;
|
||||||
case "workspace.tab.target.files":
|
case "workspace.tab.target.files":
|
||||||
setNewTabMenuOpen(false);
|
setNewTabMenuOpen(false);
|
||||||
@@ -1497,7 +1511,7 @@ function ResolvedWorkspaceDesktopTabsRow({
|
|||||||
[
|
[
|
||||||
handleCreateAgentTab,
|
handleCreateAgentTab,
|
||||||
handleCreateBrowser,
|
handleCreateBrowser,
|
||||||
handleOpenChanges,
|
handleOpenChangesShortcut,
|
||||||
handleOpenFiles,
|
handleOpenFiles,
|
||||||
isFocused,
|
isFocused,
|
||||||
isGit,
|
isGit,
|
||||||
|
|||||||
@@ -122,13 +122,6 @@ export const WorkspacePaneEmptyState = memo(function WorkspacePaneEmptyState({
|
|||||||
<View style={styles.container} testID="workspace-pane-empty-state">
|
<View style={styles.container} testID="workspace-pane-empty-state">
|
||||||
<View style={styles.rail}>
|
<View style={styles.rail}>
|
||||||
<View style={styles.stack}>
|
<View style={styles.stack}>
|
||||||
<LauncherRow
|
|
||||||
label={t("workspace.tabs.actions.files")}
|
|
||||||
icon={FILES_ICON}
|
|
||||||
onPress={openFiles}
|
|
||||||
>
|
|
||||||
<LauncherShortcut actionId="workspace-tab-target-files" />
|
|
||||||
</LauncherRow>
|
|
||||||
{showChanges ? (
|
{showChanges ? (
|
||||||
<LauncherRow
|
<LauncherRow
|
||||||
label={t("workspace.tabs.actions.changes")}
|
label={t("workspace.tabs.actions.changes")}
|
||||||
@@ -138,6 +131,13 @@ export const WorkspacePaneEmptyState = memo(function WorkspacePaneEmptyState({
|
|||||||
<LauncherShortcut actionId="workspace-tab-target-changes" />
|
<LauncherShortcut actionId="workspace-tab-target-changes" />
|
||||||
</LauncherRow>
|
</LauncherRow>
|
||||||
) : null}
|
) : null}
|
||||||
|
<LauncherRow
|
||||||
|
label={t("workspace.tabs.actions.files")}
|
||||||
|
icon={FILES_ICON}
|
||||||
|
onPress={openFiles}
|
||||||
|
>
|
||||||
|
<LauncherShortcut actionId="workspace-tab-target-files" />
|
||||||
|
</LauncherRow>
|
||||||
{showPullRequest ? (
|
{showPullRequest ? (
|
||||||
<LauncherRow
|
<LauncherRow
|
||||||
label={t("workspace.tabs.actions.pullRequest")}
|
label={t("workspace.tabs.actions.pullRequest")}
|
||||||
@@ -146,14 +146,14 @@ export const WorkspacePaneEmptyState = memo(function WorkspacePaneEmptyState({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<LauncherRow
|
<LauncherRow
|
||||||
label={t("workspace.tabs.actions.newAgent")}
|
label={t("workspace.tabs.fallback.agent")}
|
||||||
icon={AGENT_ICON}
|
icon={AGENT_ICON}
|
||||||
onPress={createAgent}
|
onPress={createAgent}
|
||||||
>
|
>
|
||||||
<LauncherShortcut actionId="workspace-tab-target-agent" />
|
<LauncherShortcut actionId="workspace-tab-target-agent" />
|
||||||
</LauncherRow>
|
</LauncherRow>
|
||||||
<LauncherRow
|
<LauncherRow
|
||||||
label={t("workspace.tabs.actions.newTerminal")}
|
label={t("workspace.tabs.fallback.terminal")}
|
||||||
icon={TERMINAL_ICON}
|
icon={TERMINAL_ICON}
|
||||||
onPress={createTerminal}
|
onPress={createTerminal}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user