Real SVG icons, and Kubernetes manifests identified by content - #46
Merged
Conversation
The tree drew its file types as `▸ ◆ ▤ ▫` and the editor tabs as coloured CSS squares. Four buckets for every file in a Kubernetes manifest repository, which is to say a manifest and a Helm chart and a CI workflow all looked identical. This is the first of #37's eight children; the others are #39-#45. Two icon sources, and they resolved differently on licensing. lucide-react (ISC) is a real dependency: zero runtime dependencies, a React peer only, and it tree-shakes — the bundle carries chevron-right and ellipsis-vertical and does not carry air-vent, zap or sailboat, which was checked against the built artifact rather than assumed. It draws in currentColor, which is what UI chrome needs: a close button has to go from muted to foreground on hover, and an <img> cannot do that. material-icon-theme (MIT) is vendored — seventeen SVGs copied verbatim into frontend/src/icons/material/ with the upstream LICENSE and a README recording version and provenance. It is NOT taken as a dependency, and that is a deliberate rejection rather than an oversight: it is a VS Code extension, we import none of its code, and depending on it drags chroma-js into the production npm graph, whose declared licence is the compound expression `(BSD-3-Clause AND Apache-2.0)`. Both halves are on the allowlist, but license-checker-rseidelsohn --onlyAllow matches declared strings, so that is a gate failure waiting to happen on behalf of a package nothing here calls. vscode-icons was rejected outright, as the ticket said it would be: CC BY-SA. Kustomize has no artwork in the set — `kusto` in there is Azure Data Explorer, a different product — so kustomization.yaml gets lucide's stacked layers. Worth flagging that DESIGN.md defers kustomize support to v1.x; an icon is not support, and the ticket asked for it. Both sources sit behind one module. FileIcon takes an IconKind, UiIcon takes an action name ("close", not "an X"), and the tree, both tab strips and the changes rows all call the same two. FILE_SVG is typed Record<Exclude<IconKind, "kustomize">, string>, so adding a bucket to the union without artwork behind it fails type-checking rather than rendering nothing. iconKind moves from four extension buckets to seventeen path-based ones, and the Kubernetes rule is a judgment call that should be reviewed as one. Content sniffing is what would actually identify a manifest, and the ticket rules it out for the right reason: apiVersion+kind cannot be read for every entry in a directory the moment it is expanded without turning one click into several hundred file reads. So: .github/workflows/** is Actions; kustomization.y(a)ml is Kustomize; Chart.yaml, Chart.lock, values* and anything under a templates/ segment are Helm; a dotfile or YAML sitting at the repository root is plain YAML, because that is where repository configuration lives and a Kubernetes wheel on codecov.yml would be a lie; and everything else below the root is a manifest, because that is what these repositories contain. docker-compose.yaml two directories down is the case this gets wrong, and it takes the generic icon back once sniffing exists. kindFromIcon was the trap in this change and is the reason it is not cosmetic. It maps IconKind to the bucket that picks the CodeMirror language. Splitting YAML five ways while leaving that function matching only "yaml" would have silently opened Chart.yaml, every manifest and every workflow as plain text with no highlighting and no folding. There is a YAML_ICONS set now and a comment saying what it is holding up. Reviewing the diff for ways to break it found one defect in it. The lookup tables were object literals, so BY_NAME["constructor"] answers "present" and returns Object — a Function handed back where an IconKind was promised, which TypeScript cannot catch because the index signature declares the value type. A file named `constructor` or `toString` would have rendered an icon with no artwork behind it. Both tables are Maps, and the test over the five Object.prototype names fails against the previous version. Two smaller decisions worth naming: - vite.config.ts sets assetsInlineLimit: 8192. helm.svg is 4143 bytes and the 4096 default emitted it as a separate asset the app fetches through the Wails asset server on first paint, for a 4 KiB string that belongs in the bundle. icons.test.ts gates that every vendored icon stays under whatever the limit is set to, reading both numbers off disk — the same approach style.test.ts uses, and for the same reason: neither fact is visible to a rendering test, since vitest's dev transform serves assets as URLs and inlining is a property of `vite build`. - FileTree.tsx crossed the 600-line cap, so CreateRow and InlineField moved to their own module. They are the part of that file that knows nothing about trees, only a depth and two callbacks. Shaving comments to fit a gate would have been the other option and is not one. Buttons that lost their "+" gained an aria-label, since "file" alone is a worse accessible name than "+ file" was. Each label contains its visible word (WCAG 2.5.3). Verified: make verify green on this diff. 647 frontend tests, 0 lint issues, licences clean with the new dependency, real Wails smoke build, no dead code. The GUI is verified this time rather than skipped — run against a real manifest repository under `wails dev`: nested .yml shows the Kubernetes wheel and root YAML does not, folders open and close, README.md opens in preview with the pencil offering edit, the dirty dot fills, and the Kubernetes tab keeps YAML highlighting, which is kindFromIcon confirmed end to end. Both themes checked at the 22px row height. One gap stated plainly: patch-coverage is a Go-only gate and this diff is entirely frontend, so the >=85% figure was not measured. frontend-test and frontend-lint are the gates that ran. Closes #38
The first commit shipped the icon set with a placeholder for the hardest question in it: which YAML files are Kubernetes manifests. It answered from the path — any YAML below the repository root that was not a chart, a kustomization or a workflow. That rule is wrong in a way a rule about icons should never be: it asserts something about a file it has not read. A docker-compose.yaml two directories down got a Kubernetes wheel, and the only reason `codecov.yml` did not was a second guess layered on the first about root-level YAML being configuration. Now the name decides only what a name can decide, and the content decides the rest. `iconKind` can no longer return "kubernetes" at all — there is a test that says so, aimed squarely at the rule this replaces. Its YAML branch keeps the two cases a path genuinely settles: `.github/workflows/**` is a workflow because that is where GitHub looks for one, and `Chart.yaml`, `values*` and `templates/` are Helm because Helm named them. Everything else is plain YAML, wherever it sits. The root-and-dotfile special case is gone with the rule it was propping up. The upgrade is lazy, and the shape of it is the point. A listing renders immediately with the icons names earn; then `useFileTree` reads the head of that directory's plain-YAML files and the manifests among them change icon. Nothing blocks on a file read to draw a directory, and nothing is read outside the directories actually open. internal/watch.ReadPrefixes is what it reads with: the first 2 KiB of each named file, batched per directory. It returns evidence rather than a verdict on purpose — deciding in Go would put a second copy of the apiVersion/kind rule beside the frontend's, and `looksLikeManifest` in tree.ts is then the single implementation, used by the tree against a prefix and by the editor tab strip against the whole file it already holds. That is what keeps a manifest's tab and its tree row from disagreeing about the same file. The rule is a column-0 `apiVersion:` and a column-0 `kind:`, both with values, anywhere in the head. Column 0 is what makes "top level" checkable without a YAML parser: a `kind:` under `spec:` is indented and a list item's never starts a line. Both keys are required, so a chart's values file carrying its own `kind:` is not promoted. Reading a prefix rather than a file means this can only be wrong in one direction — a manifest whose two keys both sit past 2 KiB keeps the plain icon — and understating is the right way for an icon to fail. Four things found by reviewing this for ways to break it, all fixed here: - ReadPrefixes opened before it stat'd. Opening a named pipe blocks until something writes to it, and a repository is allowed to contain one: the call would have hung holding the batch and the tree's icons with it. It Lstats first and takes regular files only, which also rejects symlinks — os.Root refuses to follow one *out* of the root but will follow one within it. - A UTF-8 BOM sits in front of the first key and defeats a column-0 match on the one line most likely to carry apiVersion. Stripped. - The batch cap (1024, so this binding cannot be turned into a repository walk) was reachable: a flat directory of two thousand manifests would have failed the call and kept the plain icon forever. The frontend batches at 256 rather than discovering the cap. - A project switch during an in-flight classification would have written one project's verdicts into another's tree, where paths are relative and `deploy.yaml` means different files. Checked against the current root before the state write. Two pins move, both by exactly one, both wanting a maintainer's opinion: - internal/watch 21 -> 22 exported. One name, ReadPrefixes. The other two identifiers it needed — the prefix length and the oversized-batch sentinel — are unexported, because nothing outside names them. ReadFile could not serve this: it answers "give me this file to edit" and enforces an editing size limit for it, which is the wrong contract for "tell me what these thirty files are". - maxAppMethods 23 -> 24. One binding. It passes the test that ceiling's own comment sets — a request with an answer and no throughput, not a stream wearing a binding's clothes. The alternative was folding heads into ListDirectory's reply, which would make every expansion pay for reads it usually does not need, which is the cost being avoided. Also in this diff: prefix_test.go joined the package's convention (`package watch`, like every other test file here) and reuses the `write` helper that was already there rather than the near-copy it arrived with. One consequence stated rather than hidden: the changes rows show YAML as YAML. They have no content behind them, and wiring them to the tree's classifications would make a changed file's icon depend on whether its directory happened to be expanded — a panel inconsistent with itself is worse than one uniformly generic. #40 replaces that panel entirely. Verified: make verify green. 671 frontend tests, Go coverage 93.0% app / 94.4% watch, patch floor clear, 0 lint issues, real Wails smoke build. Checked in the running app against a real manifest repository: seven .yml files under a manifests directory render as plain YAML and then upgrade to the Kubernetes wheel as their heads arrive, which is the async wiring this commit is mostly about and the part unit tests cannot show.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #46 +/- ##
==========================================
- Coverage 93.54% 93.43% -0.12%
==========================================
Files 31 32 +1
Lines 1518 1553 +35
==========================================
+ Hits 1420 1451 +31
- Misses 62 64 +2
- Partials 36 38 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #38. First of #37's eight children (#39–#45 follow).
The tree drew its file types as
▸ ◆ ▤ ▫and the editor tabs as coloured CSS squares — four buckets for every file in a Kubernetes manifest repository, which is to say a manifest, a Helm chart and a CI workflow all looked identical. This replaces them with a real SVG set, and decides what a YAML file is by reading it rather than by guessing from its directory.1. Two icon sources, resolved differently on licensing
lucide-react(ISC) — a real dependency. Zero runtime dependencies, a React peer only, and it tree-shakes: the built bundle containschevron-rightandellipsis-verticaland does not containair-vent,zaporsailboat, which was checked against the artifact rather than assumed. Its icons draw incurrentColor, which is what UI chrome needs — a close button has to go muted → foreground on hover, and an<img>cannot do that.material-icon-theme(MIT) — vendored, not depended on. Seventeen SVGs copied verbatim intofrontend/src/icons/material/with the upstream LICENSE and a README recording version and provenance.Not taking it as a dependency is a deliberate rejection. It is a VS Code extension, we import none of its code, and depending on it drags
chroma-jsinto the production npm graph — whose declared license is the compound expression(BSD-3-Clause AND Apache-2.0). Both halves are on the allowlist, butlicense-checker-rseidelsohn --onlyAllowmatches declared strings, so that is a gate failure waiting to happen on behalf of a package nothing here calls.vscode-iconswas rejected outright, as #38 said it would be: CC BY-SA.Kustomize has no artwork in the set (
kustoin there is Azure Data Explorer, a different product), sokustomization.yamluses lucide's stacked layers. Worth flagging that DESIGN.md defers kustomize support to v1.x — an icon is not support, and the ticket asked for it.Both sources sit behind one module.
FileIcontakes anIconKind,UiIcontakes an action name ("close", not "an X"), and the tree, both tab strips and the changes rows all call the same two.FILE_SVGis typedRecord<Exclude<IconKind, "kustomize">, string>, so adding a bucket to the union without artwork behind it fails type-checking rather than rendering nothing.2. Kubernetes manifests are identified by content
The first commit answered this from the path: any YAML below the repository root that was not a chart, kustomization or workflow. That was wrong in the way a rule about icons should never be — it asserts something about a file it has not read.
docker-compose.yamltwo directories down got a Kubernetes wheel, and the only reasoncodecov.ymldid not was a second guess layered on the first about root-level YAML being configuration.The second commit replaces it.
iconKindcan no longer return"kubernetes"at all, and there is a test aimed squarely at the rule it replaces. Names now settle only what names can settle:.github/workflows/**Chart.yaml,Chart.lock,values*, undertemplates/kustomization.y(a)ml.yaml/.ymlThe upgrade is lazy, and its shape is the point. A listing renders immediately with the icons names earn; then
useFileTreereads the head of that directory's plain-YAML files and the manifests among them change icon. Nothing blocks on a file read to draw a directory, and nothing is read outside the directories actually open.internal/watch.ReadPrefixesis the read: the first 2 KiB of each named file, batched per directory. It returns evidence rather than a verdict on purpose — deciding in Go would put a second copy of the apiVersion/kind rule beside the frontend's.looksLikeManifestintree.tsis the single implementation, used by the tree against a prefix and by the editor tab strip against the whole file it already holds, which is what keeps a manifest's tab and its tree row from disagreeing about the same file.The rule is a column-0
apiVersion:and a column-0kind:, both with values, anywhere in the head. Column 0 is what makes "top level" checkable without a YAML parser: akind:underspec:is indented and a list item's never starts a line. Both keys are required, so a chart's values file carrying its ownkind:is not promoted. Reading a prefix means this can only be wrong in one direction — a manifest whose two keys both sit past 2 KiB keeps the plain icon — and understating is the right way for an icon to fail.Defects found by adversarial review, fixed here
Object-literal lookup returned inherited members. The icon tables were object literals, so
BY_NAME["constructor"]answers "present" and returnsObject— aFunctionhanded back where anIconKindwas promised, which TypeScript cannot catch because the index signature declares the value type. A file namedconstructorortoStringwould have rendered an icon with no artwork behind it. Both tables areMaps; the test over the fiveObject.prototypenames fails against the previous version.ReadPrefixesopened before it stat'd. Opening a named pipe blocks until something writes to it, and a repository is allowed to contain one — the call would have hung holding the batch, and the tree's icons with it. ItLstats first and takes regular files only, which also rejects symlinks (os.Rootrefuses to follow one out of the root but will follow one within it).A UTF-8 BOM defeated the match. It sits in front of the first key, on the line most likely to carry
apiVersion. Stripped.The batch cap was reachable. 1024, so this binding cannot be turned into a repository walk — but a flat directory of two thousand manifests would have failed the call and kept the plain icon forever. The frontend batches at 256 rather than discovering the cap.
A project switch could cross-contaminate. An in-flight classification would have written one project's verdicts into another's tree, where paths are relative and
deploy.yamlmeans different files. Checked against the current root before the state write.Two pins raised, both by exactly one — these want your opinion
Per CLAUDE.md these need maintainer sign-off, so they are the thing to review first:
internal/watch21 → 22 exported. One name,ReadPrefixes. The other two identifiers it needed (prefix length, oversized-batch sentinel) are unexported because nothing outside names them.ReadFilecould not serve this: it answers "give me this file to edit" and enforces an editing size limit for it, which is the wrong contract for "tell me what these thirty files are".maxAppMethods23 → 24. One binding. It passes the test that ceiling's own comment sets — a request with an answer and no throughput, not a stream wearing a binding's clothes. The alternative was folding heads intoListDirectory's reply, which would make every expansion pay for reads it usually does not need, which is exactly the cost being avoided.Both are argued at the pin, not only here.
Smaller decisions worth naming
assetsInlineLimit: 8192invite.config.ts.helm.svgis 4143 bytes and the 4096 default emitted it as a separate asset fetched through the Wails asset server on first paint, for a 4 KiB string that belongs in the bundle.icons.test.tsgates that every vendored icon stays under whatever the limit is, reading both numbers off disk — the same approachstyle.test.tsuses, and for the same reason: neither fact is visible to a rendering test, since vitest's dev transform serves assets as URLs and inlining is a property ofvite build.kindFromIconwas the trap. It mapsIconKindto the bucket that picks the CodeMirror language. Splitting YAML five ways while leaving it matching only"yaml"would have silently opened every manifest and workflow as plain text, no highlighting and no folding. There is aYAML_ICONSset now and a comment saying what it holds up.FileTree.tsxcrossed the 600-line cap, soCreateRow/InlineFieldmoved to their own module — the part of that file that knows nothing about trees. Shaving comments to fit a gate was the other option and is not one.+gained anaria-label, since "file" alone is a worse accessible name than "+ file" was. Each label contains its visible word (WCAG 2.5.3).Known consequence, stated rather than hidden
The changes rows show YAML as YAML. They have no content behind them, and wiring them to the tree's classifications would make a changed file's icon depend on whether its directory happened to be expanded — a panel inconsistent with itself is worse than one uniformly generic. #40 replaces that panel entirely.
Verification
make verifygreen, includingbindings-checkwith the regenerated bindings committed.internal/app93.0%,internal/watch94.4%; patch floor clearChecked in the running app against a real manifest repository, not only in jsdom:
.ymlfiles under a manifests directory render as plain YAML and then upgrade to the Kubernetes wheel as their heads arrive — the async wiring unit tests cannot showREADME.mdopens in preview with the pencil offering edit; the dirty dot fillskindFromIconconfirmed end to endOne gap:
patch-coverageis a Go-only gate, so the frontend half of this diff is covered byfrontend-testandfrontend-lintrather than by a measured patch percentage.