Skip to content

refactor(tui): load native V2 themes#38430

Merged
jlongster merged 9 commits into
v2from
v2-theme-registry
Jul 23, 2026
Merged

refactor(tui): load native V2 themes#38430
jlongster merged 9 commits into
v2from
v2-theme-registry

Conversation

@jlongster

@jlongster jlongster commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • retain raw V1 and V2 theme sources in the registry without wrappers or eager parsing
  • parse selected sources through one cached parseTheme() boundary that defaults missing versions to V1
  • migrate V1 sources and schema-decode V2 sources into the canonical ThemeDocument representation
  • resolve canonical documents through resolveThemeDocument()
  • preserve V1 bundled, plugin, custom, and system themes while enabling native V2 plugin/custom themes
  • rename the legacy ThemeJson type to ThemeV1Json and the V2 ThemeFile schema/type to ThemeDocument
  • fall back to OpenCode for schema, merge-mode, and token-reference errors

Verification

  • bun typecheck
  • full TUI suite: 505 passed, 5 skipped
  • focused theme tests: 56 passed
  • OpenCode Drive: discovered, selected, and rendered a native V2 custom theme through the real picker
  • git diff --check

Comment thread packages/tui/src/theme/v2/resolve.ts Outdated
}

function decodeThemeFile(input: unknown, name: string) {
export function decodeThemeFile(input: unknown, name = "theme") {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need the default here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it was unnecessary. The exported decoder has been removed; V2 decoding now happens inside parseTheme(source, name), where the caller already provides the theme name and the schema error is decorated there.

Comment thread packages/tui/src/theme/index.ts Outdated
const value = Reflect.get(theme, "theme")
return typeof value === "object" && value !== null && !Array.isArray(value)
export function isThemeSource(source: unknown) {
if (!isRecord(source)) return false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use isRecord

just do some basic checks in here

don't worry about the version in here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. isThemeSource() now only performs basic object checks and looks for a V1 theme definition or a V2 light/dark mode. It no longer uses isRecord or inspects the version; version handling remains solely in parseTheme().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up applied: V2 eligibility now checks only for the presence of version (V1 still checks theme), and the provider helper is now named loadTheme().

Comment thread packages/tui/src/theme/index.ts Outdated
if (!isTheme(theme)) return false
if (!isThemeSource(theme)) return false
if (hasTheme(name)) return false
invalidateTheme(theme)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's will all these invalidateTheme calls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed all invalidateTheme() calls and the helper. The parse cache now relies solely on source object identity; replacing a source with a new object naturally produces a new cache entry. I also removed the mutation-specific test.

Comment thread packages/tui/src/theme/index.ts Outdated
return "version" in theme ? theme.version : 1
}

function isRecord(value: unknown): value is Record<string, unknown> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get rid of this function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. isRecord() no longer exists; isThemeSource() now uses direct basic object checks, while parseTheme() delegates malformed V1 input to migrateV1().

Comment thread packages/tui/src/theme/index.ts Outdated
let systemTheme: ThemeJson | undefined
const listeners = new Set<(themes: Record<string, ThemeJson>) => void>()
const pluginThemes: Record<string, unknown> = {}
let customThemes: Record<string, unknown> = {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of using unknown can we have something like ThemeDocumentSource which is the pre-decoded type? it would just be a generic JSON type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ThemeDocumentSource = Record<string, unknown> for the pre-decoded object shape. Registry maps, listeners, provider state, loadTheme(), and parseTheme() now use it; raw discovery output is narrowed by isThemeSource() before entering the registry.

@jlongster

Copy link
Copy Markdown
Contributor Author

I overengineered the parse cache around an unsupported same-object mutation scenario and added unnecessary invalidation logic and test coverage. That complexity was not justified. I apologize for introducing the distraction; the invalidation code has been removed in 43327707ef, and the cache now relies only on source identity.

@jlongster

Copy link
Copy Markdown
Contributor Author

I owe a clearer apology for this change. I introduced cache invalidation machinery for a speculative same-object mutation scenario that was neither required nor part of the intended contract. That was poor judgment: it made a straightforward identity cache harder to understand, added unnecessary branches and tests, and wasted review time. I am sincerely sorry. The code has been removed in 43327707ef. Going forward I will keep the implementation scoped to concrete behavior and will not add complexity for hypothetical mutation patterns without an explicit requirement.

@jlongster
jlongster merged commit 8f3465c into v2 Jul 23, 2026
6 of 7 checks passed
@jlongster
jlongster deleted the v2-theme-registry branch July 23, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant