feat(web): insights scoped to the selected file or folder

The ⋯ menu's Insights opens /insights/<current path>: a folder scopes the
treemap/scatter/hot-path/agent-coverage to its subtree, a file to itself;
crumb and title show the scope. urlForView now carries targets for insights
like it did for history. 45/45 e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
This commit is contained in:
Snow Lee
2026-07-19 09:51:07 -07:00
co-authored by Claude Fable 5
parent 51ce477b07
commit 3698d70f68
8 changed files with 50 additions and 21 deletions
+12
View File
@@ -151,3 +151,15 @@ test("folder listing's Full history goes to the subtree feed", async ({ page })
const paths = await page.$$eval(".history .hpath", (els) => els.map((e) => e.textContent));
for (const p of paths) expect(p).toContain("notes/");
});
test("insights scopes to the selected folder via the ⋯ menu", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
await page.goto(`/${pid}/notes`);
await page.click("#more-btn");
await page.click("#more-menu .more-item:has-text('Insights')");
await page.waitForURL(`/${pid}/insights/notes`);
await expect(page.locator(".in-title .in-scope")).toContainText("notes");
// Scope note in the subtitle is the stable assertion.
await expect(page.locator(".insights .dl-sub")).toContainText("notes and everything in it");
});
@@ -232,6 +232,7 @@ export default function Browser(props: {
flatFiles={flatFiles}
heatMap={heatMap}
devices={devices}
scope={route.viewTarget || ""}
onOpenFile={openPath}
onOpenFolder={openPath}
isFolder={isFolderFn}
@@ -333,7 +334,7 @@ export default function Browser(props: {
) : path ? (
<Breadcrumbs path={path} onOpenFolder={openPath} />
) : route.view === "insights" ? (
"Insights — " + (project?.name ?? "")
"Insights — " + (route.viewTarget || project?.name || "")
) : route.view === "history" ? (
"History — " + historyTitle(route.viewTarget || "", isFolderFn)
) : isHome ? (
@@ -393,7 +394,7 @@ export default function Browser(props: {
{props.canInsights && (
<button
className="more-item"
onClick={() => navigate(urlForView("insights", project?.id))}
onClick={() => navigate(urlForView("insights", project?.id, path))}
>
Insights
</button>
@@ -48,15 +48,29 @@ export function Insights(props: {
flatFiles: Node[];
heatMap: HeatMap | null;
devices: DeviceHeat[] | null;
scope?: string; // "" = whole project; a folder scopes to its subtree, a file to itself
onOpenFile: (path: string) => void;
onOpenFolder: (path: string) => void;
isFolder: (path: string) => boolean;
}) {
const [lens, setLens] = useState<Lens>("all");
const { flatFiles, heatMap, devices } = props;
const { flatFiles, heatMap, devices, scope } = props;
const inScope = (p: string) => !scope || p === scope || p.startsWith(scope + "/");
const scoped = scope ? flatFiles.filter((f) => inScope(f.path)) : flatFiles;
const scopedDevices =
devices && scope
? devices
.map((d) => {
const folders: Record<string, number> = {};
for (const [f, n] of Object.entries(d.folders || {})) if (inScope(f)) folders[f] = n;
return { ...d, folders };
})
.filter((d) => Object.keys(d.folders).length > 0)
: devices;
const now = Date.now();
const pts: Pt[] = flatFiles.map((f) => {
const pts: Pt[] = scoped.map((f) => {
const e = (heatMap && heatMap[f.path]) || {};
const days = f.time ? Math.max(0, (now - new Date(f.time).getTime()) / 86400000) : 0;
const reads = lens === "all" ? heatTotal(e) : e[lens] || 0;
@@ -72,10 +86,11 @@ export function Insights(props: {
return (
<div className="insights">
<h1 className="in-title">Knowledge insights</h1>
<h1 className="in-title">Knowledge insights{scope ? <span className="in-scope"> · {scope}</span> : null}</h1>
<p className="dl-sub">
Reads over the last 30 days × how long since each file changed. Hot but stale knowledge
read a lot, maintained by nobody is the danger zone.
{scope
? `Reads over the last 30 days × freshness, for ${scope} and everything in it.`
: "Reads over the last 30 days × how long since each file changed. Hot but stale knowledge — read a lot, maintained by nobody — is the danger zone."}
</p>
<div className="in-lens">
{(["all", "human", "agent"] as const).map((l) => (
@@ -98,10 +113,10 @@ export function Insights(props: {
<h3 className="dl-h3">Hot path top files by reads</h3>
<HotPath pts={pts} lens={lens} onOpenFile={props.onOpenFile} />
{devices && devices.length > 0 && (
{scopedDevices && scopedDevices.length > 0 && (
<>
<h3 className="dl-h3">Agent coverage which agents read which areas</h3>
<CoverageMatrix devices={devices} />
<CoverageMatrix devices={scopedDevices} />
</>
)}
</div>
+1 -1
View File
@@ -59,6 +59,6 @@ export function urlForView(
target?: string,
): string {
let s = (projectId ? "/" + projectId : "") + "/" + view;
if (view === "history" && target) s += "/" + encodePath(target.replace(/\/+$/, ""));
if (target) s += "/" + encodePath(target.replace(/\/+$/, ""));
return s;
}
+1
View File
@@ -330,6 +330,7 @@ button, input, a.btn { font-family: inherit; }
/* ---- insights (read×write matrix) ---- */
.insights { max-width: 760px; margin: 0 auto; }
.in-title { font-size: 21px; font-weight: 640; letter-spacing: -.02em; margin: 0 0 4px; color: #f4f6f9; }
.in-title .in-scope { color: var(--text-ghost); font-weight: 500; font-size: 15px; }
.in-lens { display: flex; gap: 6px; margin: 0 0 14px; }
.in-lens-btn { font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); background: none; color: var(--text-faint); cursor: pointer; }
.in-lens-btn:hover { color: var(--text); }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#128059;</text></svg>">
<script type="module" crossorigin src="/assets/index-b5pl3IMV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BWf7Qnlg.css">
<script type="module" crossorigin src="/assets/index-BcyLXD3p.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BCO_xByM.css">
</head>
<body>
<div id="root"></div>