Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,354 changes: 1,262 additions & 92 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"lucide-react": "^1.14.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions spec/catalog-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This spec replaces a deliberately heavier earlier draft (5-phase harness, snapsh
- **One script per upstream source.** Independent, runnable solo, easy to delete or replace.
- **Stdlib only where possible.** Node's built-in `fetch`, `node:test`, and `--experimental-test-coverage` cover the work. No new deps unless a parser genuinely needs one.
- **The output is the contract.** Each script produces `catalog/<source>.json` with a flat array of records under a small envelope (`{ source, fetchedAt, count, <records> }`).
- **Prose fields are markdown.** User-facing description fields (`description`, `purpose`, etc.) are CommonMark — links, inline code, and emphasis are passed through verbatim from upstream and rendered as markdown by the inspector. Site-relative links (`[…](/en/…)`) resolve against the docs root in `source`. Sync scripts must not strip backticks, brackets, or HTML-decode these fields.
- **Idempotent.** Re-running on unchanged upstream produces a one-line diff (`fetchedAt` only). Records are sorted by a stable key.
- **Reshape on the way in, not on the way out.** Flatten nested schemas to dotted-key rows, drop fields the consumer doesn't use. The committed catalog should be ergonomic for the UI even if the upstream form isn't.
- **Provenance is part of the data.** `source` URL and `fetchedAt` timestamp ride with every catalog file.
Expand Down
4 changes: 3 additions & 1 deletion spec/inspector-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ Non-modal. The list and rail remain navigable while the drawer is open.
catalog, the env-var's `purpose` prose is shown instead of the generic
parent-`env` description that the settings-catalog walk-up returns —
the env-vars catalog is the more specific authority for what each var
does.
does. Rendered as markdown (see catalog-sync.md "Prose fields are
markdown") — links open in the system browser via the opener plugin,
with site-relative URLs resolved against the docs root.
- Effective-value block — value + winning layer badge, accent-coloured
- **Layer waterfall** (see Design primitives)
- Path notes for set layers (`./.claude/settings.json`) — clickable to
Expand Down
41 changes: 38 additions & 3 deletions src/components/inspector/KeyDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import ReactMarkdown from "react-markdown";
import { cn } from "@/lib/utils";
import {
findEnvVar,
Expand All @@ -7,6 +8,8 @@ import {
type CatalogEntry,
} from "@/lib/catalog";
import { formatValue } from "@/lib/format";
import { resolveDocsUrl } from "@/lib/markdown";
import { openExternalUrl } from "@/lib/openPath";
import { buildRows, type Row } from "@/lib/rows";
import { buildWaterfall } from "@/lib/waterfall";
import type { ArrayMergedElement } from "@/lib/flatten";
Expand Down Expand Up @@ -191,14 +194,46 @@ function DrawerHeader({
)}
</div>
{description && (
<p className="mt-3 max-w-prose text-[12.5px] leading-relaxed text-fg-2">
{description}
</p>
<div className="mt-3 max-w-prose text-[12.5px] leading-relaxed text-fg-2">
<InlineMarkdown source={description} />
</div>
)}
</div>
);
}

// react-markdown wraps top-level content in a <p>; everything else
// (links, code, em/strong) is opt-in via the `components` map. We only
// override the nodes that show up in catalog descriptions and route
// link clicks through the opener plugin so URLs open in the system
// browser instead of the WebView.
const MARKDOWN_COMPONENTS = {
p: ({ children }: { children?: React.ReactNode }) => (
<p className="m-0">{children}</p>
),
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
<a
href={href}
onClick={(e) => {
e.preventDefault();
if (href) void openExternalUrl(resolveDocsUrl(href));
}}
className="text-accent underline decoration-dotted underline-offset-2 hover:decoration-solid"
>
{children}
</a>
),
code: ({ children }: { children?: React.ReactNode }) => (
<code className="rounded-[2px] bg-bg-2 px-1 py-px font-mono text-[11.5px] text-fg-1">
{children}
</code>
),
};

function InlineMarkdown({ source }: { source: string }) {
return <ReactMarkdown components={MARKDOWN_COMPONENTS}>{source}</ReactMarkdown>;
}

function EffectiveBlock({
row,
formatted,
Expand Down
22 changes: 22 additions & 0 deletions src/lib/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, test } from "vitest";
import { resolveDocsUrl } from "./markdown";

describe("resolveDocsUrl", () => {
test("rewrites site-relative paths against the docs root", () => {
expect(resolveDocsUrl("/en/amazon-bedrock#service-tiers")).toBe(
"https://code.claude.com/docs/en/amazon-bedrock#service-tiers",
);
});

test("leaves absolute URLs untouched", () => {
expect(resolveDocsUrl("https://example.com/x")).toBe(
"https://example.com/x",
);
});

test("leaves protocol-less, non-rooted strings untouched", () => {
expect(resolveDocsUrl("mailto:foo@example.com")).toBe(
"mailto:foo@example.com",
);
});
});
10 changes: 10 additions & 0 deletions src/lib/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Catalog descriptions (env-vars.json especially) come straight from
// the upstream docs and embed `[text](/en/...)` site-relative links.
// They resolve against the docs root the env-vars sync script pulls
// from.
const DOCS_BASE = "https://code.claude.com/docs";

export function resolveDocsUrl(url: string): string {
if (url.startsWith("/")) return DOCS_BASE + url;
return url;
}
17 changes: 16 additions & 1 deletion src/lib/openPath.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { openPath as openWithSystem } from "@tauri-apps/plugin-opener";
import {
openPath as openWithSystem,
openUrl as openUrlWithSystem,
} from "@tauri-apps/plugin-opener";
import { reportError } from "./errorLog";

// Thin wrapper so callers don't import the plugin directly. The opener
Expand All @@ -20,3 +23,15 @@ export async function openInEditor(path: string): Promise<void> {
});
}
}

export async function openExternalUrl(url: string): Promise<void> {
try {
await openUrlWithSystem(url);
} catch (e) {
reportError({
message: `Couldn't open ${url}`,
detail: e,
source: "openExternalUrl",
});
}
}
Loading