fix(app): restore file tree layout
Restore the established tree rails while retaining file actions. Keep Escape local to generic editable fields so cancelling a file draft cannot interrupt an agent.
This commit is contained in:
@@ -553,8 +553,19 @@ test("changes diff switches between flat and tree file lists", async ({ page })
|
|||||||
"none",
|
"none",
|
||||||
);
|
);
|
||||||
await expect(page.getByTestId("diff-file-0")).toBeVisible();
|
await expect(page.getByTestId("diff-file-0")).toBeVisible();
|
||||||
await expect(page.getByTestId("diff-folder-src-toggle").locator("svg")).toHaveCount(2);
|
await expect(page.getByTestId("diff-folder-src-toggle").locator("svg")).toHaveCount(1);
|
||||||
await expect(page.getByTestId("diff-file-0-toggle").locator("svg")).toHaveCount(1);
|
await expect(page.getByTestId("diff-file-0-toggle").locator("svg")).toHaveCount(1);
|
||||||
|
const folderToggleBounds = await page.getByTestId("diff-folder-src-toggle").boundingBox();
|
||||||
|
const folderChevronBounds = await page
|
||||||
|
.getByTestId("diff-folder-src-toggle")
|
||||||
|
.locator("svg")
|
||||||
|
.boundingBox();
|
||||||
|
expect(folderToggleBounds).not.toBeNull();
|
||||||
|
expect(folderChevronBounds).not.toBeNull();
|
||||||
|
expect(folderChevronBounds!.y + folderChevronBounds!.height / 2).toBeCloseTo(
|
||||||
|
folderToggleBounds!.y + folderToggleBounds!.height / 2,
|
||||||
|
0,
|
||||||
|
);
|
||||||
const folderLabelBounds = await page
|
const folderLabelBounds = await page
|
||||||
.getByTestId("diff-folder-src")
|
.getByTestId("diff-folder-src")
|
||||||
.getByText("src", { exact: true })
|
.getByText("src", { exact: true })
|
||||||
@@ -565,7 +576,7 @@ test("changes diff switches between flat and tree file lists", async ({ page })
|
|||||||
.boundingBox();
|
.boundingBox();
|
||||||
expect(folderLabelBounds).not.toBeNull();
|
expect(folderLabelBounds).not.toBeNull();
|
||||||
expect(fileLabelBounds).not.toBeNull();
|
expect(fileLabelBounds).not.toBeNull();
|
||||||
expect(fileLabelBounds!.x - folderLabelBounds!.x).toBeCloseTo(14, 0);
|
expect(fileLabelBounds!.x - folderLabelBounds!.x).toBeCloseTo(16, 0);
|
||||||
|
|
||||||
const folderToggle = page.getByTestId("diff-folder-src-toggle");
|
const folderToggle = page.getByTestId("diff-folder-src-toggle");
|
||||||
await folderToggle.click();
|
await folderToggle.click();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||||
import { hasGithubAuth, createTempGithubRepo } from "../support/helpers/github-fixtures";
|
import { hasGithubAuth, createTempGithubRepo } from "../support/helpers/github-fixtures";
|
||||||
import { getServerId } from "../support/helpers/server-id";
|
import { getServerId } from "../support/helpers/server-id";
|
||||||
|
import { openFileExplorer } from "../support/helpers/file-explorer";
|
||||||
|
|
||||||
const MINIMAL_PNG = Buffer.from(
|
const MINIMAL_PNG = Buffer.from(
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||||
@@ -294,6 +295,28 @@ test.describe("Composer attachments", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Escape cancels file creation without interrupting the running agent", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
const agent = await startRunningMockAgent(page, {
|
||||||
|
prefix: "file-create-escape-",
|
||||||
|
model: "one-minute-stream",
|
||||||
|
prompt: "Stay running while a file draft is cancelled.",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await openFileExplorer(page);
|
||||||
|
await page.getByTestId("files-new-file").click();
|
||||||
|
const nameInput = page.getByTestId("file-explorer-name-input");
|
||||||
|
await expect(nameInput).toBeVisible();
|
||||||
|
|
||||||
|
await nameInput.press("Escape");
|
||||||
|
|
||||||
|
await expect(nameInput).toBeHidden();
|
||||||
|
await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible();
|
||||||
|
} finally {
|
||||||
|
await agent.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("composer is locked while new workspace agent is being created", async ({ page }) => {
|
test("composer is locked while new workspace agent is being created", async ({ page }) => {
|
||||||
test.setTimeout(120_000);
|
test.setTimeout(120_000);
|
||||||
const serverId = getServerId();
|
const serverId = getServerId();
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ test("creates, renames, copies, and deletes entries through the file explorer",
|
|||||||
|
|
||||||
await page.getByTestId("files-new-folder").click();
|
await page.getByTestId("files-new-folder").click();
|
||||||
await expect(nameInput).toBeVisible();
|
await expect(nameInput).toBeVisible();
|
||||||
|
const draftRow = nameInput.locator("xpath=..");
|
||||||
|
const placeholderColor = await nameInput.evaluate(
|
||||||
|
(input) => getComputedStyle(input, "::placeholder").color,
|
||||||
|
);
|
||||||
|
const extraMutedChevronColor = await draftRow
|
||||||
|
.locator("svg")
|
||||||
|
.evaluate((icon) => getComputedStyle(icon).stroke);
|
||||||
|
expect(placeholderColor).toBe(extraMutedChevronColor);
|
||||||
await nameInput.press("Tab");
|
await nameInput.press("Tab");
|
||||||
await expect(nameInput).toBeHidden();
|
await expect(nameInput).toBeHidden();
|
||||||
|
|
||||||
@@ -189,18 +197,38 @@ test("creates, renames, copies, and deletes entries through the file explorer",
|
|||||||
const fileRow = entry("created.txt").locator(
|
const fileRow = entry("created.txt").locator(
|
||||||
"xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]",
|
"xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]",
|
||||||
);
|
);
|
||||||
await expect(folderRow.locator("svg")).toHaveCount(2);
|
await expect(folderRow.locator("svg")).toHaveCount(1);
|
||||||
await expect(fileRow.locator("svg")).toHaveCount(1);
|
await expect(fileRow.locator("svg")).toHaveCount(1);
|
||||||
const gitRow = entry(".git").locator(
|
const gitRow = entry(".git").locator(
|
||||||
"xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]",
|
"xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]",
|
||||||
);
|
);
|
||||||
const collapsedChevronBounds = await gitRow.locator("svg").first().boundingBox();
|
const collapsedChevronBounds = await gitRow.locator("svg").first().boundingBox();
|
||||||
const expandedChevronBounds = await folderRow.locator("svg").first().boundingBox();
|
const expandedChevronBounds = await folderRow.locator("svg").first().boundingBox();
|
||||||
|
const collapsedChevronSlotBounds = await gitRow
|
||||||
|
.locator("svg")
|
||||||
|
.first()
|
||||||
|
.locator("xpath=../..")
|
||||||
|
.boundingBox();
|
||||||
|
const expandedChevronSlotBounds = await folderRow
|
||||||
|
.locator("svg")
|
||||||
|
.first()
|
||||||
|
.locator("xpath=../..")
|
||||||
|
.boundingBox();
|
||||||
expect(collapsedChevronBounds).not.toBeNull();
|
expect(collapsedChevronBounds).not.toBeNull();
|
||||||
expect(expandedChevronBounds).not.toBeNull();
|
expect(expandedChevronBounds).not.toBeNull();
|
||||||
|
expect(collapsedChevronSlotBounds).not.toBeNull();
|
||||||
|
expect(expandedChevronSlotBounds).not.toBeNull();
|
||||||
const collapsedChevronCenter = collapsedChevronBounds!.x + collapsedChevronBounds!.width / 2;
|
const collapsedChevronCenter = collapsedChevronBounds!.x + collapsedChevronBounds!.width / 2;
|
||||||
const expandedChevronCenter = expandedChevronBounds!.x + expandedChevronBounds!.width / 2;
|
const expandedChevronCenter = expandedChevronBounds!.x + expandedChevronBounds!.width / 2;
|
||||||
expect(collapsedChevronCenter).toBeCloseTo(expandedChevronCenter, 0);
|
expect(collapsedChevronCenter).toBeCloseTo(expandedChevronCenter, 0);
|
||||||
|
expect(collapsedChevronCenter).toBeCloseTo(
|
||||||
|
collapsedChevronSlotBounds!.x + collapsedChevronSlotBounds!.width / 2,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(expandedChevronCenter).toBeCloseTo(
|
||||||
|
expandedChevronSlotBounds!.x + expandedChevronSlotBounds!.width / 2,
|
||||||
|
0,
|
||||||
|
);
|
||||||
const folderLabelBounds = await entry("folder").boundingBox();
|
const folderLabelBounds = await entry("folder").boundingBox();
|
||||||
const fileLabelBounds = await entry("created.txt").boundingBox();
|
const fileLabelBounds = await entry("created.txt").boundingBox();
|
||||||
expect(folderLabelBounds).not.toBeNull();
|
expect(folderLabelBounds).not.toBeNull();
|
||||||
|
|||||||
@@ -27,15 +27,7 @@ import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"
|
|||||||
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import {
|
import { ChevronDown, Eye, EyeOff, FilePlus, FolderPlus, RotateCw } from "lucide-react-native";
|
||||||
ChevronDown,
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
FilePlus,
|
|
||||||
Folder,
|
|
||||||
FolderPlus,
|
|
||||||
RotateCw,
|
|
||||||
} from "lucide-react-native";
|
|
||||||
import { MaterialFileIcon } from "@/components/material-file-icon";
|
import { MaterialFileIcon } from "@/components/material-file-icon";
|
||||||
import {
|
import {
|
||||||
TreeChevron,
|
TreeChevron,
|
||||||
@@ -89,7 +81,6 @@ const SORT_OPTIONS: { value: SortOption }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedFolder = withUnistyles(Folder);
|
|
||||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
});
|
});
|
||||||
@@ -213,12 +204,9 @@ function EntryNameInputRow({
|
|||||||
<View style={[styles.entryRow, { paddingLeft: treeRowPaddingLeft(depth) }]}>
|
<View style={[styles.entryRow, { paddingLeft: treeRowPaddingLeft(depth) }]}>
|
||||||
<TreeIndentGuides depth={depth} />
|
<TreeIndentGuides depth={depth} />
|
||||||
<View style={styles.entryInfo}>
|
<View style={styles.entryInfo}>
|
||||||
<View style={styles.entryChevron}>
|
|
||||||
{kind === "directory" ? <TreeChevron expanded={false} /> : null}
|
|
||||||
</View>
|
|
||||||
<View style={styles.entryIcon}>
|
<View style={styles.entryIcon}>
|
||||||
{kind === "directory" ? (
|
{kind === "directory" ? (
|
||||||
<ThemedFolder size={WORKSPACE_TREE_ICON_SIZE} uniProps={foregroundMutedColorMapping} />
|
<TreeChevron expanded={false} />
|
||||||
) : (
|
) : (
|
||||||
<MaterialFileIcon fileName={name || "untitled"} size={WORKSPACE_TREE_ICON_SIZE} />
|
<MaterialFileIcon fileName={name || "untitled"} size={WORKSPACE_TREE_ICON_SIZE} />
|
||||||
)}
|
)}
|
||||||
@@ -237,6 +225,7 @@ function EntryNameInputRow({
|
|||||||
}
|
}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
|
placeholderTextColor={styles.draftPlaceholder.color}
|
||||||
style={styles.draftInput}
|
style={styles.draftInput}
|
||||||
selectTextOnFocus={Boolean(initialName)}
|
selectTextOnFocus={Boolean(initialName)}
|
||||||
testID="file-explorer-name-input"
|
testID="file-explorer-name-input"
|
||||||
@@ -381,15 +370,9 @@ function TreeRowItem({
|
|||||||
>
|
>
|
||||||
<TreeIndentGuides depth={depth} />
|
<TreeIndentGuides depth={depth} />
|
||||||
<View ref={dragSourceRef} style={styles.entryInfo}>
|
<View ref={dragSourceRef} style={styles.entryInfo}>
|
||||||
<View style={styles.entryChevron}>
|
|
||||||
{isDirectory ? <DirectoryChevronIcon loading={loading} expanded={isExpanded} /> : null}
|
|
||||||
</View>
|
|
||||||
<View style={styles.entryIcon}>
|
<View style={styles.entryIcon}>
|
||||||
{isDirectory ? (
|
{isDirectory ? (
|
||||||
<ThemedFolder
|
<DirectoryChevronIcon loading={loading} expanded={isExpanded} />
|
||||||
size={WORKSPACE_TREE_ICON_SIZE}
|
|
||||||
uniProps={foregroundMutedColorMapping}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<MaterialFileIcon fileName={entry.name} size={WORKSPACE_TREE_ICON_SIZE} />
|
<MaterialFileIcon fileName={entry.name} size={WORKSPACE_TREE_ICON_SIZE} />
|
||||||
)}
|
)}
|
||||||
@@ -1772,13 +1755,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: WORKSPACE_TREE_ICON_LABEL_GAP,
|
gap: WORKSPACE_TREE_ICON_LABEL_GAP,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
entryChevron: {
|
|
||||||
width: WORKSPACE_TREE_ICON_SIZE,
|
|
||||||
height: WORKSPACE_TREE_ICON_SIZE,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
entryIcon: {
|
entryIcon: {
|
||||||
width: WORKSPACE_TREE_ICON_SIZE,
|
width: WORKSPACE_TREE_ICON_SIZE,
|
||||||
height: WORKSPACE_TREE_ICON_SIZE,
|
height: WORKSPACE_TREE_ICON_SIZE,
|
||||||
@@ -1799,6 +1775,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingVertical: 0,
|
paddingVertical: 0,
|
||||||
paddingHorizontal: 0,
|
paddingHorizontal: 0,
|
||||||
},
|
},
|
||||||
|
draftPlaceholder: {
|
||||||
|
color: theme.colors.foregroundExtraMuted,
|
||||||
|
},
|
||||||
contextMetaBlock: {
|
contextMetaBlock: {
|
||||||
paddingVertical: theme.spacing[1],
|
paddingVertical: theme.spacing[1],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,22 +10,22 @@ import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
|||||||
// paths) render different data, but their ROWS should look identical — same
|
// paths) render different data, but their ROWS should look identical — same
|
||||||
// indentation, guide lines, and chevron. Keep those here so the two trees can't
|
// indentation, guide lines, and chevron. Keep those here so the two trees can't
|
||||||
// drift apart.
|
// drift apart.
|
||||||
export const TREE_INDENT_PER_LEVEL = 14;
|
export const TREE_INDENT_PER_LEVEL = 16;
|
||||||
export const WORKSPACE_FILE_ROW_VERTICAL_PADDING = SPACING[1];
|
export const WORKSPACE_FILE_ROW_VERTICAL_PADDING = SPACING[1.5];
|
||||||
export const WORKSPACE_TREE_ICON_SIZE = 14;
|
export const WORKSPACE_TREE_ICON_SIZE = 16;
|
||||||
export const WORKSPACE_TREE_LOADING_ICON_SIZE = 12;
|
export const WORKSPACE_TREE_LOADING_ICON_SIZE = 14;
|
||||||
export const WORKSPACE_TREE_ICON_LABEL_GAP = SPACING[1];
|
export const WORKSPACE_TREE_ICON_LABEL_GAP = SPACING[2];
|
||||||
/**
|
/**
|
||||||
* Trailing glyph rail shared with the explorer X and Changes options chevron.
|
* Trailing glyph rail shared with the explorer X and Changes options chevron.
|
||||||
* The extra 2px is optical: text ink ends inside its layout box, while the
|
* The extra 2px is optical: text ink ends inside its layout box, while the
|
||||||
* header icons' strokes extend to theirs.
|
* header icons' strokes extend to theirs.
|
||||||
*/
|
*/
|
||||||
export const WORKSPACE_FILE_ROW_TRAILING_PADDING = SPACING[3] + 2;
|
export const WORKSPACE_FILE_ROW_TRAILING_PADDING = SPACING[4] + 2;
|
||||||
|
|
||||||
/** Left padding for a tree row at `depth`. Shared by folder rows and file headers
|
/** Left padding for a tree row at `depth`. Shared by folder rows and file headers
|
||||||
* in the Changes tree so their indentation can't drift apart. */
|
* in the Changes tree so their indentation can't drift apart. */
|
||||||
export function treeRowPaddingLeft(depth: number): number {
|
export function treeRowPaddingLeft(depth: number): number {
|
||||||
return SPACING[2] + depth * TREE_INDENT_PER_LEVEL;
|
return SPACING[3] + depth * TREE_INDENT_PER_LEVEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
const foregroundExtraMutedIconColorMapping = (theme: Theme) => ({
|
const foregroundExtraMutedIconColorMapping = (theme: Theme) => ({
|
||||||
@@ -46,7 +46,7 @@ export function TreeIndentGuides({ depth }: { depth: number }) {
|
|||||||
key: index,
|
key: index,
|
||||||
style: [
|
style: [
|
||||||
styles.indentGuide,
|
styles.indentGuide,
|
||||||
inlineUnistylesStyle({ left: SPACING[2] + index * TREE_INDENT_PER_LEVEL + 3 }),
|
inlineUnistylesStyle({ left: SPACING[3] + index * TREE_INDENT_PER_LEVEL + 4 }),
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
[depth],
|
[depth],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { View, Text, type LayoutChangeEvent, type PressableStateCallbackType } from "react-native";
|
import { View, Text, type LayoutChangeEvent, type PressableStateCallbackType } from "react-native";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { Folder } from "lucide-react-native";
|
|
||||||
import { DiffStat } from "@/components/diff-stat";
|
import { DiffStat } from "@/components/diff-stat";
|
||||||
import {
|
import {
|
||||||
TreeChevron,
|
TreeChevron,
|
||||||
@@ -18,9 +17,6 @@ import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
|
|||||||
import { FileActionsContextMenuContent } from "@/components/file-actions-menu";
|
import { FileActionsContextMenuContent } from "@/components/file-actions-menu";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
const ThemedFolder = withUnistyles(Folder);
|
|
||||||
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
|
||||||
|
|
||||||
interface DiffFolderRowProps {
|
interface DiffFolderRowProps {
|
||||||
/** full uncompressed directory path — the collapse identity */
|
/** full uncompressed directory path — the collapse identity */
|
||||||
dirPath: string;
|
dirPath: string;
|
||||||
@@ -145,15 +141,9 @@ export function DiffFolderRow({
|
|||||||
testID={testID ? `${testID}-toggle` : undefined}
|
testID={testID ? `${testID}-toggle` : undefined}
|
||||||
>
|
>
|
||||||
<View style={leftStyle}>
|
<View style={leftStyle}>
|
||||||
<View style={styles.chevronOpticalOffset}>
|
<View style={styles.chevronSlot}>
|
||||||
<TreeChevron expanded={!collapsed} />
|
<TreeChevron expanded={!collapsed} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.folderIcon}>
|
|
||||||
<ThemedFolder
|
|
||||||
size={WORKSPACE_TREE_ICON_SIZE}
|
|
||||||
uniProps={foregroundMutedColorMapping}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.folderName} numberOfLines={1}>
|
<Text style={styles.folderName} numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -204,13 +194,7 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
chevronOpticalOffset: {
|
chevronSlot: {
|
||||||
width: WORKSPACE_TREE_ICON_SIZE,
|
|
||||||
height: WORKSPACE_TREE_ICON_SIZE,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
folderIcon: {
|
|
||||||
width: WORKSPACE_TREE_ICON_SIZE,
|
width: WORKSPACE_TREE_ICON_SIZE,
|
||||||
height: WORKSPACE_TREE_ICON_SIZE,
|
height: WORKSPACE_TREE_ICON_SIZE,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -1161,7 +1161,6 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
|||||||
ref={dragSourceRef}
|
ref={dragSourceRef}
|
||||||
style={showDir ? styles.fileHeaderLeft : [styles.fileHeaderLeft, styles.fileHeaderLeftTree]}
|
style={showDir ? styles.fileHeaderLeft : [styles.fileHeaderLeft, styles.fileHeaderLeftTree]}
|
||||||
>
|
>
|
||||||
{showDir ? null : <View style={styles.fileChevronSpacer} />}
|
|
||||||
{showDir ? null : (
|
{showDir ? null : (
|
||||||
<View style={styles.fileIcon}>
|
<View style={styles.fileIcon}>
|
||||||
<MaterialFileIcon fileName={fileName} size={WORKSPACE_TREE_ICON_SIZE} />
|
<MaterialFileIcon fileName={fileName} size={WORKSPACE_TREE_ICON_SIZE} />
|
||||||
@@ -3465,11 +3464,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[1],
|
gap: theme.spacing[1],
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
fileChevronSpacer: {
|
|
||||||
width: WORKSPACE_TREE_ICON_SIZE,
|
|
||||||
height: WORKSPACE_TREE_ICON_SIZE,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
fileIcon: {
|
fileIcon: {
|
||||||
width: WORKSPACE_TREE_ICON_SIZE,
|
width: WORKSPACE_TREE_ICON_SIZE,
|
||||||
height: WORKSPACE_TREE_ICON_SIZE,
|
height: WORKSPACE_TREE_ICON_SIZE,
|
||||||
|
|||||||
@@ -404,6 +404,13 @@ describe("keyboard-shortcuts", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("leaves Escape to an editable field", () => {
|
||||||
|
expectNoShortcutResolution({
|
||||||
|
event: { key: "Escape", code: "Escape" },
|
||||||
|
context: { focusScope: "editable" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const nonMatchingCases: NonMatchingShortcutCase[] = [
|
const nonMatchingCases: NonMatchingShortcutCase[] = [
|
||||||
{
|
{
|
||||||
name: "does not keep old Mod+Alt+N binding",
|
name: "does not keep old Mod+Alt+N binding",
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ interface ShortcutWhen {
|
|||||||
terminal?: false;
|
terminal?: false;
|
||||||
/** false = disabled when command center is open */
|
/** false = disabled when command center is open */
|
||||||
commandCenter?: false;
|
commandCenter?: false;
|
||||||
/** Exact focus scope match */
|
/** Allowed focus scope or scopes */
|
||||||
focusScope?: KeyboardFocusScope;
|
focusScope?: KeyboardFocusScope | readonly KeyboardFocusScope[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShortcutPayloadDef =
|
type ShortcutPayloadDef =
|
||||||
@@ -1042,7 +1042,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
id: "agent-interrupt",
|
id: "agent-interrupt",
|
||||||
action: "agent.interrupt",
|
action: "agent.interrupt",
|
||||||
combo: "Escape",
|
combo: "Escape",
|
||||||
when: { commandCenter: false, terminal: false },
|
when: { commandCenter: false, focusScope: ["message-input", "other"] },
|
||||||
preventDefault: false,
|
preventDefault: false,
|
||||||
stopPropagation: false,
|
stopPropagation: false,
|
||||||
help: {
|
help: {
|
||||||
@@ -1215,7 +1215,14 @@ export function matchesKeyboardShortcutContext(
|
|||||||
}
|
}
|
||||||
if (when.terminal === false && context.focusScope === "terminal") return false;
|
if (when.terminal === false && context.focusScope === "terminal") return false;
|
||||||
if (when.commandCenter === false && context.commandCenterOpen) return false;
|
if (when.commandCenter === false && context.commandCenterOpen) return false;
|
||||||
if (when.focusScope !== undefined && context.focusScope !== when.focusScope) return false;
|
if (
|
||||||
|
when.focusScope !== undefined &&
|
||||||
|
!(typeof when.focusScope === "string"
|
||||||
|
? context.focusScope === when.focusScope
|
||||||
|
: when.focusScope.includes(context.focusScope))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user