Repository navigation
[Feature]: Import a design system from a local codebase into DESIGN.md #484
Copy link
Copy link
Open
Description
Problem / pain point
- Every run follows the workspace
DESIGN.mdwhen there is one (apps/desktop/src/main/prompt-context.ts:426), but someone designing for an existing product has to write it by hand: the existing "Link design system repo" scan only hands the model a candidate list of unnamed values, and defers toDESIGN.md. Their colors, type, spacing and radii already live in the repo: a Tailwind config, CSS custom properties, or design-token JSON. - The extractors for exactly those sources exist with tests:
extractFromTailwindConfig,extractFromCssVarsandimportDtcgJsoninpackages/core/src/brand/. Nothing in the app calls them. The roadmap lists this as v0.3, "Codebase → design system". - Claude Design builds a design system from a codebase during onboarding, so this is one of the first things people coming from it look for.
Proposed solution
An import that needs no model call: pick a folder, Open CoDesign runs the existing extractors over it and writes the result into the workspace DESIGN.md, which every run already reads. Delivered as PRs under 400 lines each:
- Scan. Builds on the existing "Link design system repo" picker and walk in
apps/desktop/src/main/design-system.ts(read-only, skipsnode_modules,.gitand build output, capped file count and bytes), and adds discovery oftailwind.config.{js,cjs,mjs,ts}, CSS files with:root/[data-theme]custom properties or a Tailwind v4@themeblock, and DTCG token files (*.tokens.json,tokens.json). Files are parsed as text, as the extractors already do; nothing in the repo is executed or copied. The scan module loads on first use. - Map tokens to
DESIGN.md. A pure function in core turns the tokens into fieldsvalidateDesignMdaccepts, and reports what it could not map:- colors →
colors.rgb(),hsl()andoklch()(Tailwind v4's default) are converted to sRGB hex. Colors with transparency,var()references that do not resolve within the scan, and other formats are reported instead of guessed. - spacing →
spacing; radii →rounded(px, em or rem). - font families →
typographyentries (fontFamilyis required there); font sizes and line heights → the Typography section; shadows → Elevation & Depth. - When a token appears in several themes, the
:rootvalue is used and the others are reported.
- colors →
- Review and write. An "Import design system from a folder…" action shows the generated
DESIGN.md, which file each value came from, and what was not imported. Confirming createsDESIGN.md; if one exists, the imported token fields replace its token fields and its Markdown sections are kept. - Docs. A short "Import a design system" section in the website docs.
Alternatives considered
- Asking the agent to read the repo with its file tools: costs tokens on every run, depends on the model, and the repo is usually outside the workspace, so each read needs a permission prompt. The extractors are deterministic, free and offline.
- Evaluating
tailwind.config.jsto get the resolved theme: runs code from the repo. The extractors parse it as text on purpose. - Inferring component styles from React or Vue source: larger and model-dependent; it can build on this token import later.
- A separate tokens file:
DESIGN.mdis already the design-system file that users and the agent both edit and every run reads.
Scope
Generation capability
Hard-constraints check
- I have read the hard constraints in CLAUDE.md, and this proposal does not conflict with any of them. No new dependencies: it reuses the existing extractors, the
DESIGN.mdserializer uses theyamlpackage thatpackages/sharedalready depends on, and the color conversion is a small pure function. The scan loads on first use, reads only a folder the user picks, stores its result in the workspaceDESIGN.md, and makes no network or model calls.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
github-actions commentedon Oct 12, 2026
Thanks @wudilyy999 — this is an unusually well-specified feature request, and a good chunk of it is already backed by commitments written down in the repo. Some notes and a few questions before PR 1.
Where the proposal already agrees with the project
CLAUDE.md(Hard constraints, §5) names codebase scan explicitly as a heavy feature that must dynamic-import on first use, so "the scan module loads on first use" is a requirement here, not a nice-to-have.AGENTS.md(Hard constraints, §8) says brand values are data, not model memory, and points atDESIGN.md, user files, official CSS/SVG/screenshots, or brand URLs. A deterministic extractor run over the user's own repo is the strongest form of that.AGENTS.md(Hard constraints, §4) — every design has a workspace, and the workspace filesystem is the source of truth, which is exactly why writing the result into the workspaceDESIGN.mdfits the existing model.packages/coreis the generation-orchestration package, so a pure tokens →DESIGN.mdmapping function in core is the right home for step 2.CLAUDE.mdalso requires at least one Vitest test for a new feature, and bansconsole.*inapps/desktop/src/main/**andpackages/core/**(usegetLogger()in main, the injectedCoreLoggerin core). Worth building the PRs with that in mind from the start.What I could verify, and what I could not
I could not open
apps/desktop/src/main/prompt-context.tsorpackages/core/src/brand/from the repository context available to me, so please read the two path/line references in your proposal as unverified by me rather than confirmed. That is not a blocker — just noting it so nobody mistakes this comment for an endorsement of those line numbers.One concrete thing I did spot: in
pnpm-lock.yaml, thepackages/coreimporter declares@mariozechner/pi-agent-core,@mariozechner/pi-ai,@mariozechner/pi-coding-agent,@open-codesign/providers,@open-codesign/shared, and@sinclair/typebox— I don't seeyamlthere. If the scan path does need YAML parsing, either it is coming in transitively (which should be promoted to a direct declaration) or the "no new dependencies" claim needs another look. If the extractors only do text/JSON parsing, the claim stands and the point is moot.Design questions worth settling before the scan PR
DESIGN.mdland? Since the workspace is the source of truth and the run reads the workspaceDESIGN.md, does "Import design system from a folder…" require an already-open design, or does it create a new design/workspace first? That choice drives the whole IPC shape.DESIGN.md. "The imported token fields replace its token fields and its Markdown sections are kept" needs to be precise: field-level replace or whole-section replace, and what happens to a value the user hand-edited that sits inside an imported field. Silently overwriting hand-written values is the fastest way to lose trust in the feature; a visible per-field diff in step 3 seems like the safer default.oklch()→ sRGB hex. Tailwind v4's default palette is wide-gamut, so some values will be out of sRGB range. Clamping silently changes the color. Worth deciding up front whether out-of-gamut values are clamped-and-reported, skipped-and-reported, or render ascolor(srgb …)/hex fallback. A handful of fixed test vectors would keep the conversion honest.var()resolution. Depth, file count, and byte caps should be named constants with tests rather than inline numbers, and "var()references that do not resolve within the scan" should be defined as resolve-within-scanned-folder or report-only.Housekeeping for the PR series
The
CLAUDE.mdguidance applies directly: keep each PR marking all four checks in.github/PULL_REQUEST_TEMPLATE.md, add a Vitest test per piece (extractor wiring, color conversion,DESIGN.mdmapping, merge behavior), and keep the scan read-only with no repo code execution — the "parse as text, don't evaluate" decision in your alternatives section is the right call and worth stating explicitly in the PR description so reviewers don't ask for a resolved-theme shortcut.I won't comment on roadmap sequencing — the roadmap lives outside the public checkout, so I can't verify the v0.3 reference from here.
Need more info:
extractFromTailwindConfig,extractFromCssVars, andimportDtcgJsonexported frompackages/core's public entry, or only reachable via deep imports? The main process importing core internals would be a design decision worth naming.@themesubset do you plan to support (plain custom properties,--color-*/--spacing-*namespaces,@theme inline, references to other custom properties)?:rootalways the winner, or should the user pick the theme in the review step?Open-CoDesign Bot
wudilyy999 commentedon Oct 12, 2026
Thanks, this is a useful read. Two of your points change the plan; the rest are answers.
Corrections
yaml(2.9.0) is declared inpackages/shared, wheredesign-md.tsuses it, and not inpackages/core. The serializer that writesDESIGN.mdgoes next to that parser inpackages/shared. The tokens → fields mapping stays a pure function in core that returns a plain object, so core gets no new dependency. I have updated the hard-constraints line in the description.packages/core/src/brand/index.ts, andpackages/core/package.jsonexposes only..src/index.tsdoes not re-exportbrand. PR 1 adds that re-export, so desktop main imports the public entry and not core internals.Answers
readProjectContext(workspaceRoot)readsDESIGN.mdper design, so the action lives inside a design and writes<workspace>/DESIGN.md. It does not create a design.DESIGN.mdis added. A key whose value differs is listed as a conflict and unchecked by default, so a hand-edited value changes only when the user ticks it. The Markdown sections stay as they are.oklch(). Converted through OKLab → linear sRGB → gamma. Out-of-gamut channels are clamped to [0, 1], and the preview lists every clamped token with its original value, so no change happens silently. Fixed test vectors cover in-gamut, out-of-gamut and achromatic inputs.var(). Depth, file count and bytes are named constants with tests. Avar()is resolved when the custom property is defined in a scanned file (with a cycle guard) and reported otherwise.@themeblock,@theme inlineincluded, contributes its--*declarations, classified by property name or value into color, font size, font family, line height, radius, shadow and spacing. Values stay as written, so--color-ink: var(--color-brand)is avar()string until the mapper resolves it. v3 configs contribute thethemeandtheme.extendsections for colors, fontSize, fontFamily, spacing, borderRadius, boxShadow and lineHeight.Running the extractors on a v4 sample showed two gaps, both fixed in PR 2 with tests:
--font-sansand the other v4--font-*families are dropped (the v4 classifier matchesfont-familyonly), and a token does not record which block it came from, so a:rootand a[data-theme="dark"]value with the same name look identical. PR 2 adds an optional source-block field.:root(or the@themeblock) wins and the other[data-theme]values are listed as not imported. A theme picker is outside this series.PR 1 follows once this plan looks right to you.
github-actions commentedon Oct 12, 2026
Thanks — both corrections land, and the answers close out every question from the last round. What I could and could not verify from here, then what is worth carrying into the PRs.
On the two corrections
yaml/packages/shared: in thepnpm-lock.yamlI can see, thepackages/coreimporter declares@mariozechner/pi-agent-core,@mariozechner/pi-ai,@mariozechner/pi-coding-agent,@open-codesign/providers,@open-codesign/sharedand@sinclair/typebox— noyaml. So your correction is consistent with the lockfile, and putting the serializer besidedesign-md.tsin the package that already depends on it keeps core dependency-free. I could not open thepackages/sharedimporter ordesign-md.tsfrom the context available to me, so treat that half as your assertion rather than something I re-verified.packages/core/src/brand/index.ts, withpackages/core/package.jsonexposing only.: same caveat, I cannot open those files from here. Either way, adding the re-export in PR 1 so desktop main goes through the public entry is the right shape — deep imports intopackages/core/src/**are the thing to avoid.Worth carrying into the PRs
CLAUDE.mdhard constraint 5 names codebase scan explicitly as a feature that must dynamic-import on first use. A staticbrandre-export fromsrc/index.tsis fine if the extractors are small and side-effect-free, but the scan module (walk, limits, file discovery) must not be reachable from a static top-level import —await import(...)at the main-process call site. Keeping the re-export and the scan wiring in separate commits inside PR 1 makes that boundary reviewable.console.*is banned inapps/desktop/src/main/**and inpackages/core,packages/providers,packages/exportersandpackages/shared—getLogger()in main, the injectedCoreLoggerin core/providers/exporters. The report-only branches are exactly where a stray log creeps in: unmapped tokens, clampedoklch(), unresolvedvar(), and non-:rootthemes. Give them a structured result the renderer renders, not a log line.--font-*classification fix. I cannot see the current tests from here.DESIGN.mdis a workspace file the user and the agent both edit, and the preview and the confirm are separated in time. This repo already has a staged, conflict-checked pattern for workspace source writes (.changeset/deterministic-local-source-edits.mddescribes hash-bound, atomic single-file writes with conflict checks). Reusing that path, or at minimum failing loudly when the file changed since preview, fits the existing model better than a blind write..github/PULL_REQUEST_TEMPLATE.mdmarked.Accepted as answered
<workspace>/DESIGN.mdand does not create a design.oklch()via OKLab → linear sRGB → gamma, out-of-gamut channels clamped and every clamp listed in the preview, with fixed in-gamut / out-of-gamut / achromatic vectors. Report-not-silent is the right default; worth also keeping the original value string next to the clamped one so the preview can show exactly what changed.var()resolved only when the custom property is defined in a scanned file (cycle-guarded) and reported otherwise.theme/theme.extendcontribution as described, withvar()strings preserved until the mapper resolves them; no resolved-theme shortcut, so no repo code is evaluated.:root/@themewins, other[data-theme]values listed as not imported; no theme picker in this series.Need more info:
packages/coreorapps/desktop/src/main? Reading the picked folder and owning the native dialog is main-process work, and that choice decides whether the lazy-import requirement is even visible in the diff.brandre-export a staticexport * from './brand'insrc/index.ts, or a separate entry/subpath so the extractors are not pulled into every consumer of core's entry?PR 1 once those two are settled — this plan looks right to me.
Open-CoDesign Bot
wudilyy999 commentedon Oct 13, 2026
Thanks, that settles it. Answers to the two questions, and the rest is noted for the PRs.
apps/desktop/src/main, next to the IPC handler that owns the native directory dialog. Walking the picked folder is main-process work, and the handler loads the scan withawait import(...)on first use, so the lazy boundary is visible at one call site.packages/coregets only the pure parts: the extractors, the color conversion and the tokens → fields mapping.src/index.ts(the three extractor functions and the token types), notexport *. The extractors are side-effect-free text parsers, so a static re-export adds nothing at startup, and the scan module is not reachable from core's entry. PR 1 keeps the re-export and the scan wiring in separate commits.--font-*fix and keeps the source-block field optional. The confirm step fails loudly whenDESIGN.mdchanged since the preview, reusing the hash-bound workspace write if it fits, and the preview shows the original and the clamped value side by side.I will open PR 1 next.
github-actions commentedon Oct 13, 2026
Thanks — both answers land, and the plan reads as settled from here. Nothing in the last two questions changes the review shape, so this is a sign-off on the plan rather than another round.
Scan location (
apps/desktop/src/main)CLAUDE.mdhard constraint 5 names codebase scan explicitly among heavy features that must dynamic-import on first use, andAGENTS.mdhard constraint 6 carries the same rule for heavy features (scaffolds, skills, brand refs, image generation). Putting the walk behindawait import(...)at the single IPC call site is the reviewable version of that requirement.node_modules/.git/ build output. The "parse as text, don't evaluate" decision is the one reviewers are most likely to question, so it should be visible rather than inferred.Re-export
export *is right for the reason you gave: side-effect-free text parsers, nothing added at startup, and the scan module stays unreachable from core's entry.pnpm-lock.yamlthat thepackages/coreimporter declares@mariozechner/pi-agent-core,@mariozechner/pi-ai,@mariozechner/pi-coding-agent,@open-codesign/providers,@open-codesign/sharedand@sinclair/typebox— noyaml. So the serializer next todesign-md.tsinpackages/shared, with the pure tokens → fields mapping in core, keeps core dependency-free exactly as you described. I cannot re-openpackages/core/package.jsonorsrc/brand/index.tsfrom the context I have, so theexports-map half stays your assertion — not a blocker, since the re-export goes through the public entry either way.Carry into the PRs
console.*rule coverspackages/sharedalongsideapps/desktop/src/main/**andpackages/core/**(CLAUDE.md, Things to avoid). Since the serializer lands in shared, returning a structured result on the report-only paths — unmapped tokens, clampedoklch(), unresolvedvar(), non-:rootthemes — is what keeps both packages clean without a logging escape hatch..changeset/deterministic-local-source-edits.md:10describes atomic single-file workspace writes bound to exact source hashes with conflict checks. If that helper is reusable forDESIGN.md, reusing it beats introducing a second conflict mechanism; if it is too preview-bound to reuse, failing loudly when the file changed since preview is still the right minimum..github/PULL_REQUEST_TEMPLATE.md:21is thepnpm lint && pnpm typecheck && pnpm testbox, andCLAUDE.mdconstraint 6 requires all four PRINCIPLES §5b checks marked. Note that rootpnpm testrunsnode --test .github/scripts/deepseek-common.test.mjsbeforeturbo run test(rootpackage.json), so a local run covers the script tests too.package.jsonscriptsdocs:dev,docs:buildanddocs:previewall target theopen-codesign-websitepackage, so the "Import a design system" page belongs there.Nothing further from me on the design — good luck with PR 1.
Open-CoDesign Bot
wudilyy999 commentedon Oct 13, 2026
One correction before PR 1. The app already has a "Link design system repo" action: the Add menu →
codesign:pick-design-system-directory→scanDesignSysteminapps/desktop/src/main/design-system.ts. It walks the picked folder, regex-collects unnamed colors (up to 24), fonts, spacing, radii and shadows (up to 16 each) from the 12 highest-scored files, stores that snapshot in the app config, andformatDesignSystemadds it to every run as a "candidate design-system scan" that defers toDESIGN.md. My description did not mention it, and "has to write it by hand" overstated the gap.What this series adds on top: the values arrive as named, typed tokens from the existing extractors instead of unnamed value lists, and they land in
<workspace>/DESIGN.md, the artifact the current scan already defers to, where the user can review and edit them.That changes PR 1. There is no new walker and no new picker: PR 1 reuses the existing walk and directory dialog, adds discovery of the token sources (Tailwind config, CSS with
:rootor@theme, DTCG JSON) next to it, and re-exports the extractors. Token extraction is loaded withawait import(...)in the handler. The existing snapshot and its prompt block keep their behavior, and whether the snapshot should eventually give way toDESIGN.mdis a call for you; this series does not touch it. PRs 2 to 4 are unchanged.github-actions commentedon Oct 13, 2026
Thanks — that is a materially better description of the current state, and the revised PR 1 follows from it. A few notes on what the correction changes, then two questions.
What the correction changes
scanDesignSystemwalk and thecodesign:pick-design-system-directorydialog, and add only (a) token-source discovery alongside the existing file list — Tailwind config, CSS with:root/@theme, DTCG JSON — (b) the named re-exports inpackages/core's entry, and (c) theawait import(...)at the handler. The commit split that made the lazy boundary reviewable still applies, it just has less to split.<workspace>/DESIGN.mdas a design-scoped artifact. They can look like one flow in the Add menu, so naming both targets explicitly in the IPC and PR description keeps reviewers from assuming the import overwrites the snapshot (or vice versa).formatDesignSystemalready defers toDESIGN.md, imported tokens take effect through the prompt block that exists today — no change toformatDesignSystemis needed, and none is proposed. Keeping the snapshot's behavior untouched, as you describe, is the right default for this series.packages/coreentry via named exports, and (b) token-source discovery selecting the right files out of the file list the existing walk already produces. Walk limits are the existing scan's concern, not PR 1's.On the snapshot vs.
DESIGN.mdquestionThat call belongs to the maintainers, not to this comment — but the smallest diff is to keep both for now, since they are scoped differently (app-level candidate values vs. per-design design system). Retiring the snapshot later is a separate change with its own issue and its own compatibility story.
One edit worth making in the issue body
The body still reads "has to write it by hand," which this correction shows overstates the gap. A one-line amendment acknowledging "Link design system repo" and what it already does would keep future readers from re-deriving the same correction from the comments. Same for the scope framing: the proposal is adding named, typed tokens into the artifact the existing scan already defers to, not introducing design-system awareness that does not exist.
Need more info:
<workspace>/DESIGN.md— so which process owns the confirm step follows from this.I can't open
apps/desktop/src/main/design-system.tsfrom the context available to me, so I'm taking the specifics of the existing action as your read of the code rather than something I independently verified. The point about the hash-bound staged write path still holds for the confirm step in PR 3.Open-CoDesign Bot
test(desktop): cover token files that are not well formed
wudilyy999 commentedon Oct 13, 2026
Thanks. Answers to the two questions, and PR 1 is open as #486.
<workspace>/DESIGN.mdfor the open design. The main process owns the confirm step. The renderer shows the preview, and main writes only ifDESIGN.mdis unchanged since the preview, reusing the hash-bound workspace write if it fits and failing loudly otherwise.DEFAULT_TOKEN_SCAN_LIMITS: depth 6, 10,000 files looked at, 256,000 bytes per file. The two scans do not share a budget.One difference from the plan I described earlier, which #486 also states: the walk in #486 is new rather than the existing one. The existing walk stops after 160 candidate files in directory order and has no tests, so on a larger repo it can stop before it reaches the token files. The two scans share the ignore list.
github-actions commentedon Oct 13, 2026
Thanks — both answers land. One of them also moves the review surface, so most of this is about that.
Entry point and IPC — accepted
A second Add-menu entry with its own channel, leaving
codesign:pick-design-system-directory/scanDesignSystemto keep writing the app-config snapshot, keeps the two targets separable: app-scoped candidate values vs design-scoped<workspace>/DESIGN.md. Main owning the confirm step is the right side for it, and the write contract you describe — preview in the renderer, main writes only ifDESIGN.mdis unchanged since the preview, hash-bound write reused where it fits, loud failure otherwise — is the right minimum. Worth stating in the PR description that the hash is captured at preview and rechecked at write, sinceCLAUDE.mdbans synchronous I/O in the main process and the recheck has to sit on the async path..changeset/deterministic-local-source-edits.md:10is the pattern I was pointing at: "Bind edits to exact source hashes and preview revisions ... stage atomic single-file writes with conflict checks. Surface saved-but-refresh-failed warnings without rolling back later external changes." IfDESIGN.mdcan go through that helper, reuse beats standing up a second conflict mechanism; if the helper is too preview-bound, say so explicitly in the PR.Limits — accepted
New named constants with their own tests, no shared budget with the snapshot caps, is exactly what the question asked for.
DEFAULT_TOKEN_SCAN_LIMITS(depth 6, 10,000 files, 256,000 bytes per file) reads as fixed policy for this series — say so outright so nobody later assumes the values are user-configurable.One test-shape note: the byte cap is per file and the file cap is across the walk, so pin them independently — a nested fixture for depth, a candidate set larger than the file cap, and one oversized file for the byte cap. A single test that trips whichever limit comes first will not tell them apart.
The new walk — this is the change
If the existing walk really stops at 160 candidate files in directory order and has no tests, a new walk is a defensible call: a token import that silently never reaches
tailwind.config.tson a large repo is a bad first impression. But it reverses the agreed PR 1 shape ("reuse the existing walk"), so three things follow:scanDesignSystemonto the new walk later would remove both the 160-file cap and the duplication, but that is a separate change with its own compatibility story — a follow-up issue rather than a PR in this series.CLAUDE.mdhard constraint 5 names codebase scan explicitly among features that must dynamic-import on first use, andAGENTS.mdhard constraint 6 repeats the rule for heavy features. The new walk, and the constants it enforces, must not be reachable from a static top-level import in main —await import(...)at the handler, with the limits living inside the lazily-loaded module so nothing drags the walk into the app-start graph. The diff will not show that on its own, so it belongs in the PR description.packages/corere-exports reachable from the public entry.Still open from earlier
The issue body still reads "has to write it by hand." Your own correction shows that overstates the gap, since
codesign:pick-design-system-directory→scanDesignSystemalready walks the repo andformatDesignSystemalready defers toDESIGN.md. A one-line amendment keeps future readers from re-deriving that from the comment thread.What I could and could not verify
pnpm-lock.yaml: thepackages/coreimporter declares@mariozechner/pi-agent-core,@mariozechner/pi-ai,@mariozechner/pi-coding-agent,@open-codesign/providers,@open-codesign/sharedand@sinclair/typebox— noyaml. So the serializer besidedesign-md.tsinpackages/shared, with a plain-object mapper in core, keeps core dependency-free as described.package.json:testrunspnpm test:scripts && turbo run test, andtest:scriptsisnode --test .github/scripts/deepseek-common.test.mjs, so a localpnpm testcovers the script tests too..github/PULL_REQUEST_TEMPLATE.md:21is thepnpm lint && pnpm typecheck && pnpm testbox.apps/desktop/src/main/design-system.ts(the 160-file cap, the "no tests" claim, the channel names), PR feat(desktop): find design-token sources in a picked folder #486's diff, and theDEFAULT_TOKEN_SCAN_LIMITSvalues. I am treating those as your read of the code, not as things I re-checked.Need more info:
apps/desktop/src/main/design-system.tsbesidescanDesignSystem, or a separate module — and is it only reachable behindawait import(...)at the handler?DEFAULT_TOKEN_SCAN_LIMITSfixed for this series, or expected to become configurable?scanDesignSystemonto the new walk and retiring the 160-candidate cap) be filed as a follow-up issue so this series stays additive?Open-CoDesign Bot
feat(desktop): find design-token sources in a picked folder (#486)
fix(core): accept the CSS spellings of a length DESIGN.md does not take
refactor(shared): derive the DESIGN.md allowed fields and the seriali…
fix(core): export UnresolvedReason next to UnsupportedReason