fix(app): spell out a result's path when its label cannot

A row shows the file name and its nearest parent, which is not always enough:
packages/app/src/utils/index.ts and packages/server/src/utils/index.ts render
identically. Pointing at a row, or selecting it, now replaces the elided
parent with every directory above the file, so a pointer user reads the exact
workspace-relative path without the preview getting a header back. At rest the
column stays scannable. The accessible name was already exact and is unchanged.

The app's Tooltip was tried first and cannot serve this surface: it portals
into the shared overlay root, which is pinned to the browser/overlay plane at
z-index 1, while the Command Center renders through React Native Web's Modal
at z-index 9999. The tooltip mounts, measures and reports itself visible, but
elementFromPoint at its own coordinates returns the row behind it, so a reader
never sees it. Every other desktop overlay in the app portals into the overlay
root instead of using RN's Modal, which is what docs/floating-panels.md
prescribes; moving the Command Center onto it would fix this for every scope
and is a larger change than one row affordance.
This commit is contained in:
Mohamed Boudra
2026-09-10 14:17:25 +02:00
parent 770648327f
commit c33c1d0855
5 changed files with 112 additions and 23 deletions
@@ -165,3 +165,18 @@ test("opening the same occurrence again navigates the pane it is already showing
}) => {
await reopenSameOccurrenceAfterMovingAway(page, await withWorkspace());
});
import { revealExactPathOnHover } from "../support/helpers/workspace-content-search";
test("pointing at a result reveals its exact path when the row label cannot", async ({
page,
withWorkspace,
}, testInfo) => {
const { panel, rows } = await revealExactPathOnHover(page, await withWorkspace());
await rows.last().hover();
await panel.screenshot({ path: testInfo.outputPath("hover-exact-path.png") });
// Pointing must not disturb the keyboard selection or the Enter contract.
await page.keyboard.press("Enter");
await expect(panel).toBeHidden();
await expect(page.getByTestId("file-source-editor")).toContainText("SHARED_UTIL");
});
@@ -241,3 +241,38 @@ async function openReopenTarget(page: Page) {
await page.keyboard.press("Enter");
await expect(panel).toBeHidden();
}
/**
* Two files can share a base name and their nearest parent, so the resting row label cannot name
* them. Pointing at a row, or selecting it, spells the exact workspace-relative path out.
*/
export async function revealExactPathOnHover(page: Page, workspace: CreatedWorkspace) {
for (const relative of [
"packages/app/src/utils/index.ts",
"packages/server/src/utils/index.ts",
]) {
const target = path.join(workspace.repoPath, relative);
await mkdir(path.dirname(target), { recursive: true });
await writeFile(target, `export const shared = "SHARED_UTIL";\n`);
}
await workspace.navigateTo();
await page.keyboard.press("Meta+Shift+F");
await page
.getByRole("textbox", { name: "Search saved file contents...", exact: true })
.fill("SHARED_UTIL");
const panel = page.getByTestId("command-center-panel");
const rows = panel.getByRole("button", { name: /index\.ts:1:\d+/ });
await expect(rows).toHaveCount(2);
// The accessible name always carries the exact path.
await expect(rows.first()).toHaveAccessibleName(/^packages\/app\/src\/utils\/index\.ts:1:24 /);
await expect(rows.last()).toHaveAccessibleName(/^packages\/server\/src\/utils\/index\.ts:1:24 /);
// The second row is at rest and shows only its nearest parent, which both files share.
await expect(rows.last()).toContainText("…/utils/index.ts:1:24");
await rows.last().hover();
await expect(rows.last()).toContainText("packages/server/src/utils/index.ts:1:24");
await rows.first().hover();
await expect(rows.first()).toContainText("packages/app/src/utils/index.ts:1:24");
return { panel, rows };
}
@@ -232,32 +232,44 @@ function ResultRow({
);
const line = useMemo(() => describeMatchLine(item), [item]);
const file = useMemo(() => describeResultPath(item.path), [item.path]);
const location = `${item.path}:${item.line}:${item.columnStart}`;
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={`${item.path}:${item.line}:${item.columnStart} ${item.snippet}`}
accessibilityLabel={`${location} ${item.snippet}`}
accessibilityState={accessibilityState}
aria-pressed={isWeb ? selected : undefined}
onPress={onPress}
style={style}
>
<Text style={styles.snippet} numberOfLines={1}>
{line.before}
<Text style={styles.match}>{line.match}</Text>
{line.after}
</Text>
{/* The file name and its coordinates are pinned; only the parent context may shrink, so a
deep path loses its middle rather than the identity the reader is scanning for. */}
<View style={styles.location}>
{file.directory ? (
<Text style={styles.locationDirectory} numberOfLines={1}>
{file.directory}
</Text>
) : null}
<Text style={styles.locationFile} numberOfLines={1}>
{file.name}:{item.line}:{item.columnStart}
</Text>
</View>
{({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => {
// Pointing at a row, or selecting it, spells every parent out; at rest the row shows only
// the nearest one so the column stays scannable.
const parents = hovered || selected ? file.head : file.directory;
return (
<>
<Text style={styles.snippet} numberOfLines={1}>
{line.before}
<Text style={styles.match}>{line.match}</Text>
{line.after}
</Text>
{/* The name and coordinates are pinned; only the parent context may shrink, so a deep
path loses its middle rather than the identity the reader is scanning for. Two files
can share a name and their nearest parent, so pointing at a row spells the rest of
the path out. */}
<View style={styles.location}>
{parents ? (
<Text style={styles.locationDirectory} numberOfLines={1}>
{parents}
</Text>
) : null}
<Text style={styles.locationFile} numberOfLines={1}>
{file.name}:{item.line}:{item.columnStart}
</Text>
</View>
</>
);
}}
</Pressable>
);
}
@@ -472,6 +484,11 @@ const styles = StyleSheet.create((theme) => ({
},
muted: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted },
error: { color: theme.colors.statusDanger },
tooltipPath: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontFamily: theme.fontFamily.mono,
},
empty: {
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[6],
@@ -2,13 +2,24 @@ import { describe, expect, it } from "vitest";
import { describeResultPath } from "./result-path";
describe("describeResultPath", () => {
it("spells out every parent for the row being pointed at", () => {
// Two files can share a name and their nearest parent; only the head tells them apart.
expect(describeResultPath("packages/app/src/utils/index.ts").head).toBe(
"packages/app/src/utils/",
);
expect(describeResultPath("packages/server/src/utils/index.ts").head).toBe(
"packages/server/src/utils/",
);
});
it("keeps a root file whole", () => {
expect(describeResultPath("README.md")).toEqual({ directory: "", name: "README.md" });
expect(describeResultPath("README.md")).toEqual({ directory: "", head: "", name: "README.md" });
});
it("keeps a shallow path whole", () => {
expect(describeResultPath("docs/architecture.md")).toEqual({
directory: "docs/",
head: "docs/",
name: "architecture.md",
});
});
@@ -17,14 +28,19 @@ describe("describeResultPath", () => {
// These two rows are both "index.tsx"; the parents are the only thing telling them apart.
expect(
describeResultPath("packages/app/src/command-center/workspace-content-search/index.tsx"),
).toEqual({ directory: "…/workspace-content-search/", name: "index.tsx" });
).toEqual({
directory: "…/workspace-content-search/",
head: "packages/app/src/command-center/workspace-content-search/",
name: "index.tsx",
});
expect(describeResultPath("packages/app/src/file-pane/source/index.tsx")).toEqual({
directory: "…/source/",
head: "packages/app/src/file-pane/source/",
name: "index.tsx",
});
});
it("keeps a literal backslash in a file name out of the directory", () => {
expect(describeResultPath("a\\b.txt")).toEqual({ directory: "", name: "a\\b.txt" });
expect(describeResultPath("a\\b.txt")).toEqual({ directory: "", head: "", name: "a\\b.txt" });
});
});
@@ -9,6 +9,8 @@
export interface ResultPathParts {
/** Directory context immediately above the file, already elided at the head when deeper. */
directory: string;
/** Every directory above the file, for the row the reader is pointing at or has selected. */
head: string;
/** File name, which is never truncated away. */
name: string;
}
@@ -18,8 +20,12 @@ const PARENTS_KEPT = 1;
export function describeResultPath(path: string): ResultPathParts {
const segments = path.split("/");
const name = segments.pop() ?? path;
if (segments.length === 0) return { directory: "", name };
if (segments.length === 0) return { directory: "", head: "", name };
const kept = segments.slice(-PARENTS_KEPT);
const elided = segments.length > kept.length;
return { directory: `${elided ? "…/" : ""}${kept.join("/")}/`, name };
return {
directory: `${elided ? "…/" : ""}${kept.join("/")}/`,
head: `${segments.join("/")}/`,
name,
};
}