fix(web): restore compact coarse-pointer file tree rows (#3326)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FileTreeNode } from "@/lib/types/backend";
|
||||
import type { FileBrowserRow } from "./file-browser-hooks";
|
||||
import type * as FileContextMenu from "./file-context-menu";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./file-context-menu", () => ({
|
||||
FileContextMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
useFileDeleteAction: () => null,
|
||||
useFileRename: () => ({
|
||||
isRenaming: false,
|
||||
renameValue: "",
|
||||
setRenameValue: vi.fn(),
|
||||
handleStartRename: vi.fn(),
|
||||
handleConfirmRename: vi.fn(),
|
||||
handleRenameKeyDown: vi.fn(),
|
||||
}),
|
||||
TreeNodeName: ({ node }: { node: FileTreeNode }) => <span>{node.name}</span>,
|
||||
getGitStatusTextClass: () => "",
|
||||
}));
|
||||
vi.mock("./file-context-menu", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof FileContextMenu>();
|
||||
return {
|
||||
...actual,
|
||||
FileContextMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
useFileDeleteAction: () => null,
|
||||
useFileRename: () => ({
|
||||
isRenaming: false,
|
||||
renameValue: "",
|
||||
setRenameValue: vi.fn(),
|
||||
handleStartRename: vi.fn(),
|
||||
handleConfirmRename: vi.fn(),
|
||||
handleRenameKeyDown: vi.fn(),
|
||||
}),
|
||||
getGitStatusTextClass: () => "",
|
||||
};
|
||||
});
|
||||
|
||||
import { TreeNodeItem } from "./file-browser-parts";
|
||||
import { shouldShowFileTreeTouchActions, TreeNodeItem } from "./file-browser-parts";
|
||||
|
||||
const FILE_NODE: FileTreeNode = { name: "README.md", path: "README.md", is_dir: false, size: 0 };
|
||||
const DIRECTORY_NODE: FileTreeNode = { name: "src", path: "src", is_dir: true, size: 0 };
|
||||
@@ -115,3 +119,63 @@ describe("TreeNodeItem touch context action", () => {
|
||||
expect(onAddToChatContext).toHaveBeenCalledWith(DIRECTORY_NODE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FileBrowser touch action gate", () => {
|
||||
it("keeps the touch action available on a coarse-pointer desktop viewport", () => {
|
||||
expect(shouldShowFileTreeTouchActions(false, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the touch action available on mobile and hidden on fine-pointer desktop", () => {
|
||||
expect(shouldShowFileTreeTouchActions(true, true)).toBe(true);
|
||||
expect(shouldShowFileTreeTouchActions(false, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TreeNodeItem row spacing", () => {
|
||||
it("keeps the touch-action row single-line with an independent 44px hit area", () => {
|
||||
render(
|
||||
<TreeNodeItem
|
||||
row={ROW}
|
||||
activeFolderPath=""
|
||||
visibleLoadingPaths={new Set()}
|
||||
fileStatuses={new Map()}
|
||||
tree={null}
|
||||
onToggleExpand={vi.fn()}
|
||||
onOpenFile={vi.fn()}
|
||||
setTree={() => {}}
|
||||
showTouchActions
|
||||
onAddToChatContext={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const row = screen.getByTestId("file-tree-node");
|
||||
const name = screen.getByText(FILE_NODE.name);
|
||||
// @covers AC-UI-FILE-TREE-CHAT-CONTEXT-001.9
|
||||
expect(row.className).not.toMatch(/(?:^|\s)flex-wrap(?:\s|$)/);
|
||||
expect(row.className).toContain("min-h-11");
|
||||
expect(name.className).toContain("min-w-0");
|
||||
expect(name.className).toContain("truncate");
|
||||
});
|
||||
|
||||
it("keeps fine-pointer rows on the compact layout without touch-action spacing", () => {
|
||||
render(
|
||||
<TreeNodeItem
|
||||
row={ROW}
|
||||
activeFolderPath=""
|
||||
visibleLoadingPaths={new Set()}
|
||||
fileStatuses={new Map()}
|
||||
tree={null}
|
||||
onToggleExpand={vi.fn()}
|
||||
onOpenFile={vi.fn()}
|
||||
setTree={() => {}}
|
||||
showTouchActions={false}
|
||||
onAddToChatContext={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const row = screen.getByTestId("file-tree-node");
|
||||
expect(row.className).not.toContain("relative");
|
||||
expect(row.className).not.toContain("min-h-11");
|
||||
expect(row.className).not.toContain("pr-11");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,10 @@ export {
|
||||
|
||||
type GitFileStatus = FileInfo["status"] | undefined;
|
||||
|
||||
export function shouldShowFileTreeTouchActions(isMobile: boolean, isFinePointer: boolean) {
|
||||
return isMobile || !isFinePointer;
|
||||
}
|
||||
|
||||
type TreeNodeRowProps = {
|
||||
row: FileBrowserRow;
|
||||
activeFolderPath: string;
|
||||
@@ -186,7 +190,7 @@ export function FileTreeNodeTouchActions({
|
||||
data-testid="file-tree-node-actions"
|
||||
data-path={node.path}
|
||||
aria-label={t("chat:fileTreeActions")}
|
||||
className="ml-auto flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer"
|
||||
className="absolute top-1/2 right-0 z-10 flex min-h-11 min-w-11 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer"
|
||||
onPointerDown={stopRowInteraction}
|
||||
onMouseDown={stopRowInteraction}
|
||||
onClick={stopRowInteraction}
|
||||
@@ -274,7 +278,8 @@ export function TreeNodeItem(props: TreeNodeRowProps) {
|
||||
role="treeitem"
|
||||
className={cn(
|
||||
getTreeNodeRowClass(isActive, isActiveFolder, isSelected, props.isDragging, isDropTarget),
|
||||
props.showTouchActions && "flex-wrap",
|
||||
showTouchActions &&
|
||||
"relative min-h-11 pr-11 has-[[data-testid=file-delete-inline-confirmation]]:flex-wrap has-[[data-testid=file-delete-inline-confirmation]]:pr-2",
|
||||
)}
|
||||
style={{ paddingLeft: treeNodePaddingLeft(row.depth, node.is_dir) }}
|
||||
onClick={handleClick}
|
||||
@@ -374,6 +379,7 @@ export function SearchResultsList({
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-1 px-2 py-0.5 text-left text-sm cursor-pointer",
|
||||
"hover:bg-muted",
|
||||
showTouchActions && "relative min-h-11 pr-11",
|
||||
)}
|
||||
onClick={() => onOpenFile(path)}
|
||||
>
|
||||
@@ -385,7 +391,7 @@ export function SearchResultsList({
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate group-hover:text-foreground",
|
||||
"min-w-0 flex-1 truncate group-hover:text-foreground",
|
||||
getGitStatusTextClass(gitStatus) || "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { FileTreeNode } from "@/lib/types/backend";
|
||||
import type * as FileBrowserParts from "./file-browser-parts";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { responsive, node } = vi.hoisted(() => ({
|
||||
responsive: { isMobile: false, isFinePointer: true },
|
||||
node: { name: "README.md", path: "README.md", is_dir: false, size: 0 },
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-responsive-breakpoint", () => ({
|
||||
useResponsiveBreakpoint: () => responsive,
|
||||
}));
|
||||
vi.mock("@/hooks/domains/session/use-session", () => ({
|
||||
useSession: () => ({ session: null, isFailed: false, errorMessage: null }),
|
||||
}));
|
||||
vi.mock("@/hooks/domains/workspace/use-repository", () => ({ useRepository: () => null }));
|
||||
vi.mock("@/hooks/domains/session/use-session-git-status", () => ({
|
||||
useSessionGitStatus: () => null,
|
||||
}));
|
||||
vi.mock("@/components/state-provider", () => ({
|
||||
useAppStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
sessionWorktreesBySessionId: { itemsBySessionId: {} },
|
||||
workspaceFilesRefresh: { bySessionId: {} },
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/hooks/use-open-session-folder", () => ({
|
||||
useOpenSessionFolder: () => ({ open: vi.fn() }),
|
||||
}));
|
||||
vi.mock("@/hooks/use-copy-to-clipboard", () => ({
|
||||
useCopyToClipboard: () => ({ copied: false, copy: vi.fn() }),
|
||||
}));
|
||||
vi.mock("@/components/toast-provider", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }) }));
|
||||
vi.mock("@/hooks/use-multi-select", () => ({
|
||||
useMultiSelect: () => ({
|
||||
selectedPaths: new Set(),
|
||||
setSelectedPaths: vi.fn(),
|
||||
isSelected: () => false,
|
||||
handleClick: () => false,
|
||||
clearSelection: vi.fn(),
|
||||
selectAll: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/lib/state/context-files-store", () => ({
|
||||
useContextFilesStore: (selector: (state: { addFile: () => void }) => unknown) =>
|
||||
selector({ addFile: vi.fn() }),
|
||||
}));
|
||||
vi.mock("./file-browser-header", () => ({ FileBrowserHeader: () => null }));
|
||||
vi.mock("./file-browser-hooks", () => ({
|
||||
useFileBrowserSearch: () => ({}),
|
||||
useFileBrowserTree: () => ({
|
||||
tree: node,
|
||||
isLoadingTree: false,
|
||||
loadState: "loaded",
|
||||
loadError: null,
|
||||
visibleRows: [],
|
||||
visibleLoadingPaths: new Set(),
|
||||
expandedPaths: new Set(),
|
||||
setTree: vi.fn(),
|
||||
setExpandedPaths: vi.fn(),
|
||||
isLoading: false,
|
||||
loadTree: vi.fn(),
|
||||
}),
|
||||
useScrollPersistence: () => {},
|
||||
loadNodeChildren: vi.fn(),
|
||||
toggleFolderExpand: vi.fn(),
|
||||
fetchAndOpenFile: () => Promise.resolve(),
|
||||
}));
|
||||
vi.mock("./file-browser-path", () => ({
|
||||
getFileBrowserSessionWorkspacePath: () => null,
|
||||
resolveFileBrowserPaths: () => ({ fullPath: "/workspace", displayPath: "workspace" }),
|
||||
}));
|
||||
vi.mock("./file-tree-reveal", () => ({ useFileTreeReveal: () => {} }));
|
||||
vi.mock("./file-tree-editor-menu", () => ({
|
||||
FileTreeEditorProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}));
|
||||
vi.mock("./file-context-menu", () => ({ useFileDeleteAction: () => null }));
|
||||
vi.mock("./file-browser-parts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof FileBrowserParts>();
|
||||
return {
|
||||
...actual,
|
||||
FileBrowserContentArea: ({
|
||||
showTouchActions,
|
||||
onAddToChatContext,
|
||||
}: {
|
||||
showTouchActions: boolean;
|
||||
onAddToChatContext: (treeNode: FileTreeNode) => void;
|
||||
}) => (
|
||||
<actual.FileTreeNodeTouchActions
|
||||
node={node}
|
||||
showTouchActions={showTouchActions}
|
||||
onAddToChatContext={onAddToChatContext}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
import { FileBrowser } from "./file-browser";
|
||||
|
||||
function renderFileBrowser() {
|
||||
render(<FileBrowser sessionId="session-1" onOpenFile={vi.fn()} />);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
responsive.isMobile = false;
|
||||
responsive.isFinePointer = true;
|
||||
});
|
||||
|
||||
describe("FileBrowser responsive touch action wiring", () => {
|
||||
it("renders the 44px action for a coarse-pointer desktop workbench", () => {
|
||||
responsive.isFinePointer = false;
|
||||
renderFileBrowser();
|
||||
|
||||
const trigger = screen.getByTestId("file-tree-node-actions");
|
||||
expect(trigger.className).toContain("min-h-11");
|
||||
expect(trigger.className).toContain("min-w-11");
|
||||
});
|
||||
|
||||
it("hides the touch action only for fine-pointer desktop", () => {
|
||||
renderFileBrowser();
|
||||
|
||||
expect(screen.queryByTestId("file-tree-node-actions")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the touch action for mobile", () => {
|
||||
responsive.isMobile = true;
|
||||
renderFileBrowser();
|
||||
|
||||
expect(screen.getByTestId("file-tree-node-actions")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FileTreeNode } from "@/lib/types/backend";
|
||||
|
||||
@@ -42,14 +42,16 @@ const SEARCH_PATH = "src/components/chat-input.tsx";
|
||||
|
||||
function renderSearchResults({
|
||||
showTouchActions = false,
|
||||
searchResults = [SEARCH_PATH],
|
||||
onAddToChatContext = vi.fn(),
|
||||
}: {
|
||||
showTouchActions?: boolean;
|
||||
searchResults?: string[];
|
||||
onAddToChatContext?: (node: FileTreeNode) => void;
|
||||
} = {}) {
|
||||
const props: Parameters<typeof FileBrowserContentArea>[0] = {
|
||||
isSearchActive: true,
|
||||
searchResults: [SEARCH_PATH],
|
||||
searchResults,
|
||||
isSessionFailed: false,
|
||||
sessionError: null,
|
||||
loadState: "loaded",
|
||||
@@ -110,4 +112,24 @@ describe("file browser search result context actions", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("anchors every touch action to a multi-result row with reserved name space", () => {
|
||||
const searchResults = [
|
||||
"src/components/long-first-search-result.tsx",
|
||||
"src/components/long-second-search-result.tsx",
|
||||
];
|
||||
renderSearchResults({ showTouchActions: true, searchResults });
|
||||
|
||||
const rows = screen.getAllByTestId("file-search-result");
|
||||
const triggers = screen.getAllByTestId("file-tree-node-actions");
|
||||
expect(rows).toHaveLength(searchResults.length);
|
||||
expect(triggers).toHaveLength(searchResults.length);
|
||||
for (const row of rows) {
|
||||
expect(row.className).toContain("relative");
|
||||
expect(row.className).toContain("pr-11");
|
||||
expect(within(row).getByText(/long-.*-search-result/).parentElement?.className).toContain(
|
||||
"min-w-0",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,12 @@ import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { isEditableKeydownTarget } from "@/lib/keyboard/utils";
|
||||
import { useContextFilesStore } from "@/lib/state/context-files-store";
|
||||
import { FileBrowserHeader } from "./file-browser-header";
|
||||
import { insertNodeInTree, removeNodeFromTree, FileBrowserContentArea } from "./file-browser-parts";
|
||||
import {
|
||||
insertNodeInTree,
|
||||
removeNodeFromTree,
|
||||
FileBrowserContentArea,
|
||||
shouldShowFileTreeTouchActions,
|
||||
} from "./file-browser-parts";
|
||||
import {
|
||||
useFileBrowserSearch,
|
||||
useFileBrowserTree,
|
||||
@@ -477,7 +482,15 @@ function FileBrowserTreeContent({
|
||||
}) {
|
||||
const { search, isSessionFailed, sessionError, treeState, fileStatuses } = data;
|
||||
return (
|
||||
<ScrollArea className="flex-1" ref={scrollAreaRef}>
|
||||
<ScrollArea
|
||||
className="flex-1 min-w-0"
|
||||
ref={scrollAreaRef}
|
||||
viewportProps={{
|
||||
// Keep the file-tree content constrained to the viewport so row labels
|
||||
// can shrink and apply their own truncation rules.
|
||||
className: "[&>div]:!block [&>div]:!min-w-0 [&>div]:!w-full",
|
||||
}}
|
||||
>
|
||||
<FileBrowserContentArea
|
||||
isSearchActive={search.isSearchActive}
|
||||
searchResults={search.searchResults}
|
||||
@@ -535,7 +548,7 @@ export function FileBrowser({
|
||||
addSourcesDisabledReason,
|
||||
}: FileBrowserProps) {
|
||||
const { isMobile, isFinePointer } = useResponsiveBreakpoint();
|
||||
const showTouchActions = isMobile || !isFinePointer;
|
||||
const showTouchActions = shouldShowFileTreeTouchActions(isMobile, isFinePointer);
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { data, handlers, multiSelect, dnd, handleClickOutside } = useFileBrowserViewModel({
|
||||
|
||||
@@ -166,6 +166,7 @@ export const test = backendFixture.extend<
|
||||
{
|
||||
testPage: Page;
|
||||
tabletTestPage: Page;
|
||||
coarseDesktopTestPage: Page;
|
||||
prCapture: PrAssetCapture;
|
||||
/**
|
||||
* Auto fixture that resets integration mock state and any persisted
|
||||
@@ -417,6 +418,20 @@ export const test = backendFixture.extend<
|
||||
await context.close();
|
||||
},
|
||||
|
||||
coarseDesktopTestPage: async ({ browser, backend, testPage }, use) => {
|
||||
void testPage;
|
||||
const context = await browser.newContext({
|
||||
baseURL: backend.frontendUrl,
|
||||
viewport: { width: 1280, height: 900 },
|
||||
hasTouch: true,
|
||||
isMobile: false,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await setupPage(page, backend);
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
|
||||
// PR asset capture — gated behind CAPTURE_PR_ASSETS env var.
|
||||
// When enabled, provides screenshot/recording helpers for PR descriptions.
|
||||
// Destructure in tests that need it: { testPage, prCapture }
|
||||
|
||||
@@ -12,16 +12,26 @@ async function setupMobileContextTask(
|
||||
apiClient: ApiClient,
|
||||
seedData: SeedData,
|
||||
backend: BackendContext,
|
||||
viewport?: { width: number; height: number },
|
||||
) {
|
||||
if (viewport) await testPage.setViewportSize(viewport);
|
||||
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const directoryPath = `mobile-context-directory-${suffix}`;
|
||||
const directoryPath = `mobile-context-directory-${suffix}-with-a-long-name-that-must-truncate-in-the-file-tree-on-a-narrow-touch-viewport-with-an-extra-long-label-for-overflow-verification`;
|
||||
const filePath = `mobile-context-file-${suffix}.md`;
|
||||
const searchResultPrefix = `mobile-search-layout-${suffix}`;
|
||||
const searchResultPaths = [
|
||||
`${searchResultPrefix}-first-result.ts`,
|
||||
`${searchResultPrefix}-second-result.ts`,
|
||||
];
|
||||
const git = new GitHelper(
|
||||
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
||||
makeGitEnv(backend.tmpDir),
|
||||
);
|
||||
git.createFile(`${directoryPath}/nested.txt`, "mobile directory content\n");
|
||||
git.createFile(filePath, "mobile file content\n");
|
||||
searchResultPaths.forEach((searchResultPath) =>
|
||||
git.createFile(searchResultPath, "search result\n"),
|
||||
);
|
||||
git.stageAll();
|
||||
git.commit(`add mobile chat context fixture ${suffix}`);
|
||||
|
||||
@@ -40,7 +50,7 @@ async function setupMobileContextTask(
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await session.waitForChatIdle({ timeout: 45_000 });
|
||||
return { session, directoryPath, filePath };
|
||||
return { session, directoryPath, filePath, searchResultPrefix };
|
||||
}
|
||||
|
||||
test.describe("Mobile file tree chat context", () => {
|
||||
@@ -52,17 +62,37 @@ test.describe("Mobile file tree chat context", () => {
|
||||
prCapture,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
const { session, directoryPath, filePath } = await setupMobileContextTask(
|
||||
const { session, directoryPath, filePath, searchResultPrefix } = await setupMobileContextTask(
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
{ width: 393, height: 851 },
|
||||
);
|
||||
|
||||
await testPage.getByRole("button", { name: "Files" }).tap();
|
||||
const directoryNode = session.fileTreeNode(directoryPath);
|
||||
await expect(directoryNode).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const rowGeometry = await directoryNode.evaluate((node) => {
|
||||
const name = node.querySelector<HTMLElement>(":scope > span.min-w-0.flex-1");
|
||||
return {
|
||||
flexWrap: getComputedStyle(node).flexWrap,
|
||||
nameWhiteSpace: name ? getComputedStyle(name).whiteSpace : null,
|
||||
nameOverflow: name ? getComputedStyle(name).overflow : null,
|
||||
nameTextOverflow: name ? getComputedStyle(name).textOverflow : null,
|
||||
nameScrollWidth: name?.scrollWidth ?? null,
|
||||
nameClientWidth: name?.clientWidth ?? null,
|
||||
};
|
||||
});
|
||||
expect(rowGeometry.flexWrap).toBe("nowrap");
|
||||
expect(rowGeometry.nameWhiteSpace).toBe("nowrap");
|
||||
expect(rowGeometry.nameOverflow).toBe("hidden");
|
||||
expect(rowGeometry.nameTextOverflow).toBe("ellipsis");
|
||||
expect(rowGeometry.nameScrollWidth).not.toBeNull();
|
||||
expect(rowGeometry.nameClientWidth).not.toBeNull();
|
||||
expect(rowGeometry.nameScrollWidth).toBeGreaterThan(rowGeometry.nameClientWidth!);
|
||||
|
||||
const trigger = session.fileTreeNodeActions(directoryPath);
|
||||
await expect(trigger).toBeVisible();
|
||||
const triggerBox = await trigger.boundingBox();
|
||||
@@ -109,6 +139,63 @@ test.describe("Mobile file tree chat context", () => {
|
||||
await session.fileTreeTouchAddToChatContextItem().tap();
|
||||
await session.fileSearchInput().press("Escape");
|
||||
|
||||
await session.fileSearchButton().tap();
|
||||
await session.fileSearchInput().fill(searchResultPrefix);
|
||||
const searchRows = testPage.locator('[data-testid="file-search-result"]:visible');
|
||||
await expect(searchRows).toHaveCount(2);
|
||||
const searchGeometry = await searchRows.evaluateAll((rows) =>
|
||||
rows.map((row) => {
|
||||
const trigger = row.querySelector<HTMLElement>('[data-testid="file-tree-node-actions"]');
|
||||
const name = row.querySelector<HTMLElement>(":scope > span.min-w-0.flex-1");
|
||||
const rowBox = row.getBoundingClientRect();
|
||||
const triggerBox = trigger?.getBoundingClientRect();
|
||||
return {
|
||||
actionBottom: triggerBox?.bottom ?? null,
|
||||
actionCenterX: triggerBox ? triggerBox.x + triggerBox.width / 2 : null,
|
||||
actionCenterY: triggerBox ? triggerBox.y + triggerBox.height / 2 : null,
|
||||
actionTop: triggerBox?.top ?? null,
|
||||
path: row.getAttribute("data-path"),
|
||||
rowBottom: rowBox.bottom,
|
||||
rowLeft: rowBox.left,
|
||||
rowRight: rowBox.right,
|
||||
rowTop: rowBox.top,
|
||||
nameOverflow: name ? getComputedStyle(name).overflow : null,
|
||||
nameTextOverflow: name ? getComputedStyle(name).textOverflow : null,
|
||||
nameScrollWidth: name?.scrollWidth ?? null,
|
||||
nameClientWidth: name?.clientWidth ?? null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
for (const [index, geometry] of searchGeometry.entries()) {
|
||||
expect(geometry.actionCenterX).toBeGreaterThanOrEqual(geometry.rowLeft);
|
||||
expect(geometry.actionCenterX).toBeLessThanOrEqual(geometry.rowRight);
|
||||
expect(geometry.actionCenterY).toBeGreaterThanOrEqual(geometry.rowTop);
|
||||
expect(geometry.actionCenterY).toBeLessThanOrEqual(geometry.rowBottom);
|
||||
expect(geometry.actionTop).toBeGreaterThanOrEqual(geometry.rowTop);
|
||||
expect(geometry.actionBottom).toBeLessThanOrEqual(geometry.rowBottom);
|
||||
expect(geometry.path).toBeTruthy();
|
||||
expect(geometry.nameOverflow).toBe("hidden");
|
||||
expect(geometry.nameTextOverflow).toBe("ellipsis");
|
||||
expect(geometry.nameScrollWidth).not.toBeNull();
|
||||
expect(geometry.nameClientWidth).not.toBeNull();
|
||||
expect(geometry.nameScrollWidth).toBeGreaterThan(geometry.nameClientWidth!);
|
||||
if (index > 0) {
|
||||
expect(searchGeometry[index - 1].actionBottom).toBeLessThanOrEqual(geometry.actionTop);
|
||||
}
|
||||
}
|
||||
const firstSearchTrigger = searchRows.nth(0).getByTestId("file-tree-node-actions");
|
||||
const firstSearchTriggerBox = await firstSearchTrigger.boundingBox();
|
||||
expect(firstSearchTriggerBox).not.toBeNull();
|
||||
await firstSearchTrigger.tap({
|
||||
position: {
|
||||
x: firstSearchTriggerBox!.width / 2,
|
||||
y: firstSearchTriggerBox!.height - 1,
|
||||
},
|
||||
});
|
||||
await expect(session.fileTreeTouchMenu()).toBeVisible();
|
||||
await testPage.keyboard.press("Escape");
|
||||
await session.fileSearchInput().press("Escape");
|
||||
|
||||
await expect(testPage.getByTestId("mobile-file-viewer-panel")).toHaveCount(0);
|
||||
await testPage.getByRole("button", { name: "Chat" }).tap();
|
||||
await expect(session.chatContextFile(directoryPath)).toHaveCount(1);
|
||||
@@ -127,4 +214,58 @@ test.describe("Mobile file tree chat context", () => {
|
||||
await expect(session.chatContextFile(directoryPath)).toHaveCount(0);
|
||||
await expect(session.chatContextFile(filePath)).toHaveCount(0);
|
||||
});
|
||||
test("keeps coarse desktop touch actions in separate 44px rows", async ({
|
||||
coarseDesktopTestPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
const { session, directoryPath, filePath } = await setupMobileContextTask(
|
||||
coarseDesktopTestPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
);
|
||||
|
||||
const viewport = await coarseDesktopTestPage.evaluate(() => ({
|
||||
width: window.innerWidth,
|
||||
hasFinePointer: window.matchMedia("(pointer: fine)").matches,
|
||||
}));
|
||||
expect(viewport.width).toBeGreaterThanOrEqual(1024);
|
||||
expect(viewport.hasFinePointer).toBe(false);
|
||||
|
||||
await session.clickTab("Files");
|
||||
const rows = [session.fileTreeNode(directoryPath), session.fileTreeNode(filePath)];
|
||||
for (const row of rows) {
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await expect
|
||||
.poll(async () => (await row.boundingBox())?.height ?? 0, { timeout: 2_000 })
|
||||
.toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
|
||||
const triggers = [
|
||||
session.fileTreeNodeActions(directoryPath),
|
||||
session.fileTreeNodeActions(filePath),
|
||||
];
|
||||
await expect(triggers[0]).toBeVisible();
|
||||
await expect(triggers[1]).toBeVisible();
|
||||
const triggerBoxes = await Promise.all(triggers.map((trigger) => trigger.boundingBox()));
|
||||
expect(triggerBoxes[0]).not.toBeNull();
|
||||
expect(triggerBoxes[1]).not.toBeNull();
|
||||
expect(triggerBoxes[0]!.width).toBeGreaterThanOrEqual(44);
|
||||
expect(triggerBoxes[0]!.height).toBeGreaterThanOrEqual(44);
|
||||
expect(triggerBoxes[0]!.y + triggerBoxes[0]!.height).toBeLessThanOrEqual(triggerBoxes[1]!.y);
|
||||
|
||||
await triggers[0].tap({
|
||||
position: {
|
||||
x: triggerBoxes[0]!.width / 2,
|
||||
y: triggerBoxes[0]!.height - 1,
|
||||
},
|
||||
});
|
||||
await expect(session.fileTreeTouchAddToChatContextItem()).toBeVisible();
|
||||
await session.fileTreeTouchAddToChatContextItem().tap();
|
||||
await session.clickSessionChatTab();
|
||||
await expect(session.chatContextFile(directoryPath)).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
created: 2026-09-03
|
||||
status: complete
|
||||
requirements:
|
||||
- REQ-UI-FILE-TREE-CHAT-CONTEXT-001
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/file-tree-chat-context.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: File Tree Responsive Spacing
|
||||
|
||||
## Overview
|
||||
|
||||
Restore compact file-tree row spacing on coarse-pointer devices, including
|
||||
zoomed-out phones that select the desktop workbench. The existing visible touch
|
||||
action remains available for every coarse pointer; all compositions keep
|
||||
single-line rows.
|
||||
|
||||
The prior `FileBrowser` implementation enabled a 44px overflow trigger and row
|
||||
wrapping for every non-fine pointer, including wide desktop workbenches. The
|
||||
trigger remains required; its wrapping behavior caused the spacing in the
|
||||
report.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Keep the visible 44px action trigger on every coarse-pointer file tree, including desktop workbench.
|
||||
- Keep file-tree rows compact and single-line in every composition.
|
||||
- Add regression coverage for coarse-pointer desktop and mobile geometry.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Changing file-tree data loading, selection, drag/drop, or file operations.
|
||||
- Changing the context-menu action or chat-context state and message contracts.
|
||||
- Redesigning the mobile Files surface or changing panel persistence.
|
||||
|
||||
## Technical approach
|
||||
|
||||
Update `FileBrowser` in `apps/web/components/task/file-browser.tsx` to retain
|
||||
the existing coarse-pointer touch-action gate. Keep `TreeNodeItem` and
|
||||
`SearchResultsList` in `apps/web/components/task/file-browser-parts.tsx`
|
||||
non-wrapping. Responsive rows reserve a 44px vertical interaction slot for
|
||||
their absolutely positioned trigger, while labels reserve the horizontal action
|
||||
space and truncate.
|
||||
|
||||
Add focused regression assertions for `FileBrowser` responsive action wiring
|
||||
and multi-result search overlays. Extend
|
||||
`apps/web/e2e/tests/task/mobile-file-tree-chat-context.spec.ts` with computed
|
||||
non-wrapping/truncation geometry, two simultaneous mobile search results, and a
|
||||
1280px coarse-pointer context that verifies non-overlapping 44px action slots.
|
||||
|
||||
## Tests
|
||||
|
||||
- `AC-UI-FILE-TREE-CHAT-CONTEXT-001.7`: existing touch-action component and
|
||||
mobile E2E coverage continue to assert a visible trigger and 44px menu/action
|
||||
targets.
|
||||
- `AC-UI-FILE-TREE-CHAT-CONTEXT-001.9`: focused component coverage verifies
|
||||
coarse-pointer desktop action wiring and multi-result search row anchoring;
|
||||
E2E verifies non-overlapping 44px action slots on wide desktop and no-wrap,
|
||||
truncation, and per-row search actions on mobile.
|
||||
|
||||
## E2E tests
|
||||
|
||||
- `apps/web/e2e/tests/task/mobile-file-tree-chat-context.spec.ts` using the
|
||||
`mobile-chrome` project: verify long-name no-wrap/truncation and two
|
||||
row-anchored mobile search results with non-overlapping action slots, then
|
||||
create a 1280px coarse-pointer context that proves a boundary touch reaches
|
||||
the intended 44px action and can add it to chat context.
|
||||
- `apps/web/e2e/tests/task/file-tree-chat-context.spec.ts`: existing desktop
|
||||
right-click flow remains unchanged and covers the desktop action path.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Restore responsive file-tree row spacing](task-01-restore-responsive-file-tree-spacing.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
- Focused component coverage passed, including FileBrowser-level coarse,
|
||||
mobile, and fine-pointer rendering plus multi-result search-overlay
|
||||
anchoring.
|
||||
- Mobile Chromium E2E: `pnpm e2e:run --project mobile-chrome
|
||||
tests/task/mobile-file-tree-chat-context.spec.ts` passed both Pixel 5
|
||||
no-wrap/truncation with two independently anchored search actions and 1280px
|
||||
coarse-pointer compact-row/add-to-chat scenarios.
|
||||
- `make fmt` and `make lint` passed. `make typecheck` passed before the
|
||||
aggregate `make test` stage exposed unrelated ambient backend-suite failures.
|
||||
|
||||
## Risks
|
||||
|
||||
- A wide coarse-pointer device must retain the visible action rather than
|
||||
relying on desktop right-click interaction.
|
||||
- The responsive hook distinguishes viewport composition from pointer precision;
|
||||
tests must cover both so a zoomed-out phone preserves touch access without
|
||||
row wrapping.
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
---
|
||||
id: "01-restore-responsive-file-tree-spacing"
|
||||
title: "Restore responsive file-tree row spacing"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-UI-FILE-TREE-CHAT-CONTEXT-001
|
||||
acceptance_criteria:
|
||||
- AC-UI-FILE-TREE-CHAT-CONTEXT-001.7
|
||||
- AC-UI-FILE-TREE-CHAT-CONTEXT-001.9
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/file-tree-chat-context.md
|
||||
---
|
||||
|
||||
# Task 01: Restore responsive file-tree row spacing
|
||||
|
||||
## Summary
|
||||
|
||||
Ensure the Files panel keeps compact single-line rows on every responsive
|
||||
composition. Preserve the 44px touch action and responsive menu on
|
||||
coarse-pointer devices, including desktop-workbench and zoomed-out layouts.
|
||||
|
||||
## In scope
|
||||
|
||||
- Correct the responsive presentation gate in `FileBrowser`.
|
||||
- Remove row wrapping that inflates file-tree spacing.
|
||||
- Add focused component regression coverage and mobile Playwright geometry
|
||||
coverage without changing the existing context action flow.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- File-tree data, filesystem operations, context-file storage, or chat sending.
|
||||
- New responsive primitives, breakpoints, or desktop panel redesign.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Coarse-pointer file trees, including wide desktop workbench viewports, retain
|
||||
the visible touch action with its 44px minimum active hitbox.
|
||||
- Tree and search-result rows remain compact and non-wrapping; long names
|
||||
truncate within reserved action space, and responsive rows provide an
|
||||
exclusive 44px vertical action slot.
|
||||
- Existing desktop and mobile file-tree context actions continue to pass.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
(cd apps && pnpm --filter @kandev/web test -- --run components/task/file-browser-context-action.test.tsx components/task/file-browser-responsive.test.tsx components/task/file-browser-search-context-action.test.tsx)
|
||||
(cd apps/web && pnpm e2e:run --project mobile-chrome tests/task/mobile-file-tree-chat-context.spec.ts)
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/file-browser.tsx`
|
||||
- `apps/web/components/task/file-browser-parts.tsx`
|
||||
- `apps/web/components/task/file-browser-context-action.test.tsx`
|
||||
- `apps/web/components/task/file-browser-responsive.test.tsx`
|
||||
- `apps/web/components/task/file-browser-search-context-action.test.tsx`
|
||||
- `apps/web/e2e/fixtures/test-base.ts`
|
||||
- `apps/web/e2e/tests/task/mobile-file-tree-chat-context.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- A coarse-pointer desktop must retain the visible action in a 44px row slot so
|
||||
adjacent triggers cannot overlap.
|
||||
- Each search result must anchor its overlay independently, reserve its action
|
||||
space, and truncate its label.
|
||||
- Geometry assertions cover non-overlapping action slots and a boundary touch
|
||||
that opens the intended row action.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `AC-UI-FILE-TREE-CHAT-CONTEXT-001.7` and `AC-UI-FILE-TREE-CHAT-CONTEXT-001.9`.
|
||||
- `docs/specs/ui/system-design/file-tree-chat-context.md`.
|
||||
- Existing file-tree component and Playwright tests.
|
||||
|
||||
## Results
|
||||
|
||||
- Focused component coverage passed, including FileBrowser-level wide
|
||||
coarse-pointer and mobile action visibility, fine-pointer hiding, no-wrap
|
||||
tree rows, and independently anchored multi-result search actions.
|
||||
- Mobile Chromium E2E passed Pixel 5 long-name no-wrap/truncation with two
|
||||
clipped, row-anchored search actions and a 1280px coarse-pointer desktop
|
||||
context with compact short and long rows, a 44px trigger, and Add to chat
|
||||
context.
|
||||
- `make fmt` and `make lint` passed. The aggregate `make test` stage remains
|
||||
blocked by unrelated ambient backend-suite failures.
|
||||
@@ -151,12 +151,13 @@ UI owns responsive behavior; other systems own behavior/state.
|
||||
- [Compact Workflow Step Navigation](system-design/compact-workflow-step-navigation.md)
|
||||
- [Workflow column visibility (Part 1)](system-design/board-step-visibility-filter-01.md)
|
||||
- [Workflow column visibility (Part 2)](system-design/board-step-visibility-filter-02.md)
|
||||
- [Task PR Automation Controls System Design Part 1](system-design/ci-pr-automation-01.md)
|
||||
- [Task PR Automation Controls System Design Part 2](system-design/ci-pr-automation-02.md)
|
||||
- [Task PR Automation Controls System Design Part 3](system-design/ci-pr-automation-03.md)
|
||||
- [Task PR Automation Controls 1](system-design/ci-pr-automation-01.md)
|
||||
- [Task PR Automation Controls 2](system-design/ci-pr-automation-02.md)
|
||||
- [Task PR Automation Controls 3](system-design/ci-pr-automation-03.md)
|
||||
- [Merge Queue Recovery Controls](system-design/ci-pr-merge-queue-recovery-controls.md)
|
||||
- [Entity Reference Composer](system-design/entity-reference-composer.md)
|
||||
- [File Tree Keyboard Scope](system-design/file-tree-keyboard-scope.md)
|
||||
- [File Tree Chat Context](system-design/file-tree-chat-context.md)
|
||||
- [Kandev MCP Tool Results](system-design/kandev-mcp-tool-results.md)
|
||||
- [Mobile Task Chrome](system-design/mobile-task-chrome.md)
|
||||
- [Persistent status motion](system-design/persistent-status-motion.md)
|
||||
|
||||
@@ -25,8 +25,9 @@ Users inspecting a task's Files tab must currently return to the chat composer a
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.4:** File and directory context items are visually distinguishable before send. Directory items do not attempt to open a file preview when selected.
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.5:** File-tree context items are ephemeral: they survive a reload and a failed send, and they are removed after the next successful send unless the user pins them through an existing context control.
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.6:** Sending the message, directly or after a busy-session send is queued, includes each selected path in the existing hidden context block and `context_files` message metadata so the agent is instructed to inspect the path and the sent user message records it. Metadata retains optional directory identity while remaining compatible with older `{ path, name }` entries.
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.7:** On phone and coarse-pointer layouts, each eligible file-tree row exposes a visible, accessible action trigger. The trigger opens the existing responsive menu treatment with a touch target at least 44px high; long press or right-click is not required.
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.7:** On phone, tablet, and coarse-pointer desktop-workbench compositions, each eligible file-tree row exposes a visible, accessible action trigger. The trigger opens the existing responsive menu treatment with a touch target at least 44px high; long press or right-click is not required.
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.8:** All new visible labels and feedback use the existing localization catalogs.
|
||||
- **AC-UI-FILE-TREE-CHAT-CONTEXT-001.9:** File-tree rows shall retain compact single-line spacing in every responsive composition. A visible coarse-pointer action, including in the desktop workbench, shall not cause wrapping; long names shall truncate instead.
|
||||
|
||||
## Migrated source detail
|
||||
|
||||
@@ -42,7 +43,8 @@ Users inspecting a task's Files tab must currently return to the chat composer a
|
||||
- File and directory context items are visually distinguishable before send. Directory items do not attempt to open a file preview when selected.
|
||||
- File-tree context items are ephemeral: they survive a reload and a failed send, and they are removed after the next successful send unless the user pins them through an existing context control.
|
||||
- Sending the message, directly or after a busy-session send is queued, includes each selected path in the existing hidden context block and `context_files` message metadata so the agent is instructed to inspect the path and the sent user message records it. Metadata retains optional directory identity while remaining compatible with older `{ path, name }` entries.
|
||||
- On phone and coarse-pointer layouts, each eligible file-tree row exposes a visible, accessible action trigger. The trigger opens the existing responsive menu treatment with a touch target at least 44px high; long press or right-click is not required.
|
||||
- On phone, tablet, and coarse-pointer desktop-workbench compositions, each eligible file-tree row exposes a visible, accessible action trigger. The trigger opens the existing responsive menu treatment with a touch target at least 44px high; long press or right-click is not required.
|
||||
- File-tree rows retain compact single-line spacing in every responsive composition; visible coarse-pointer actions do not cause wrapping, and long names truncate instead.
|
||||
- All new visible labels and feedback use the existing localization catalogs.
|
||||
- Existing file opening, folder expansion, multi-selection, drag/drop, editor, rename, download, and delete behavior remains unchanged.
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
status: current
|
||||
system: ui
|
||||
requirements:
|
||||
- REQ-UI-FILE-TREE-CHAT-CONTEXT-001
|
||||
---
|
||||
|
||||
# File Tree Chat Context System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The UI system owns the reusable file-tree row presentation and responsive action
|
||||
surface used by task Files panels. This repair preserves the existing
|
||||
file-tree interaction contract while ensuring the coarse-pointer action trigger
|
||||
does not turn compact rows into multi-line rows. File data, context-file state,
|
||||
and message delivery remain owned by their existing task and chat contracts.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --- | --- |
|
||||
| `AC-UI-FILE-TREE-CHAT-CONTEXT-001.7` | [Responsive row geometry](#responsive-row-geometry), [Interaction preservation](#interaction-preservation) |
|
||||
| `AC-UI-FILE-TREE-CHAT-CONTEXT-001.9` | [Responsive row geometry](#responsive-row-geometry) |
|
||||
|
||||
The existing task and chat contracts own the other acceptance criteria for file
|
||||
context state and message delivery. This design only records the responsive row
|
||||
and action-surface repair.
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
`FileBrowser` in `apps/web/components/task/file-browser.tsx` determines whether
|
||||
touch actions are required from the responsive breakpoint and pointer model.
|
||||
`FileBrowserContentArea` and `TreeNodeItem` in
|
||||
`apps/web/components/task/file-browser-parts.tsx` pass that presentation state
|
||||
through the file-tree render path. `FileTreeNodeTouchActions` owns the visible
|
||||
coarse-pointer overflow trigger and its responsive menu; its trigger remains a
|
||||
secondary action and does not own row navigation.
|
||||
|
||||
## Responsive row geometry
|
||||
|
||||
A file-tree row remains a compact single flex line when its name and controls
|
||||
fit within the panel. Responsive rows that render a 44px touch trigger establish
|
||||
an exclusive 44px vertical interaction slot. The absolutely positioned trigger
|
||||
stays within that slot while the filename reserves its action space, shrinks,
|
||||
and truncates. This prevents adjacent action targets from overlapping without
|
||||
allowing row wrapping.
|
||||
|
||||
The responsive action remains rendered on phone and coarse-pointer layouts,
|
||||
including when the desktop task composition is selected on a phone. Fine-pointer
|
||||
desktop rows retain the existing compact geometry and context-menu interaction.
|
||||
|
||||
## Interaction preservation
|
||||
|
||||
The row's primary click continues to open files or expand directories. The touch
|
||||
action trigger stops row click, selection, keyboard, and pointer propagation
|
||||
before opening the existing dropdown menu. The menu continues to provide the
|
||||
same context action and touch-sized menu items. Search-result rows use the same
|
||||
trigger geometry and action component.
|
||||
|
||||
## Verification
|
||||
|
||||
The component regression test shall verify that a touch-enabled row keeps the
|
||||
non-wrapping row geometry and that a fine-pointer row does not receive the
|
||||
responsive action layout. The existing desktop and mobile file-tree chat
|
||||
context Playwright flows remain the integration proof that the trigger is
|
||||
reachable and the action still adds the selected file or directory to chat.
|
||||
Reference in New Issue
Block a user