Skip to main content

TUI Customization and Theming

The TUI today is not a "black box where you can only change colors", but it is not yet a plugin-based UI platform either.

The most accurate way to understand it:

  • The usage layer already has a theme / config mechanism that ships as implemented
  • The Java layer still retains renderer / runtime / factory level extension points
  • But these extensions are still code-injection-style customization, not a frontend plugin marketplace

1. Start with the TUI assembly chain

TUI-related assembly currently follows roughly this chain:

CodeCommand -> CodingCliSessionRunner
-> TuiConfigManager.load(...)
-> TuiConfigManager.resolveTheme(...)
-> DefaultCodingCliTuiFactory.create(...)
-> TuiSessionView
-> AppendOnlyTuiRuntime 或 AnsiTuiRuntime

The most important points here:

  • theme, config, renderer, and runtime are assembled in layers
  • A single TUI object does not do everything itself

This is why you can change each of these separately:

  • Configuration
  • Theme
  • Renderer
  • Runtime behavior

2. Customization points you can use right now

The two layers most directly usable:

  • theme
  • tui.json

2.1 Theme override via CLI startup flags

--theme <name>

This value comes from CodeCommandOptionsParser, and is then read in by TuiConfigManager.load(overrideTheme).

A key detail:

  • --theme is only an in-memory override for this launch
  • It is not automatically persisted to tui.json

2.2 In-session /theme

The /theme command currently supports:

/theme
/theme <name>

When you run /theme <name>, CodingCliSessionRunner.applyTheme(...) calls:

  • TuiConfigManager.switchTheme(themeName)

And this method will:

  1. Validate whether the theme exists
  2. Read the current configuration
  3. Switch config.theme to the new theme
  4. Save to the workspace tui.json

So /theme is a persistent switch, not a one-time preview.


3. The theme lookup order is not arbitrary

The lookup order in TuiConfigManager.resolveTheme(name) is currently very clear:

  1. <workspace>/.ai4j/themes/<name>.json
  2. ~/.ai4j/themes/<name>.json
  3. Built-in resource /io/github/lnyocly/ai4j/tui/themes/<name>.json
  4. If still not found and it is not default, fall back to default
  5. As a final fallback, use the code-generated default theme

So the current theme priority is:

  • workspace custom
  • home custom
  • built-in
  • hardcoded fallback

This means a team can perfectly well:

  • Put a repo-specific theme in the repository

Without affecting other TUI usage scenarios in the user's home directory.


4. The config file has two layers, but no field-level merge

The currently supported config file locations are:

  • <workspace>/.ai4j/tui.json
  • ~/.ai4j/tui.json

Many people instinctively assume:

  • The home config provides defaults
  • The workspace config overrides only individual fields

But the current TuiConfigManager.merge(base, override) is not a field-level merge.

Its behavior is closer to:

  • If the workspace config exists, use the workspace config directly
  • Otherwise, fall back to the home config

So the current semantics are:

  • File-level override

Rather than:

  • Field-level cascade merge

This is critical, because it means you cannot assume:

  • showFooter=false is set in home
  • Only theme=ocean is written in workspace
  • showFooter=false will still be preserved in the end

The current implementation does not guarantee this kind of field-level inheritance.


5. What tui.json actually controls today

The core fields of TuiConfig currently include:

  • theme
  • denseMode
  • showTimestamps
  • showFooter
  • maxEvents
  • useAlternateScreen

Meanwhile TuiConfigManager.normalize(config) guarantees:

  • An empty theme falls back to default
  • When maxEvents <= 0, it falls back to 10

So this is not "pass any value through as-is" either; there are minimal normalization rules.


6. Built-in themes are just the starting point, not the whole story

The built-in theme names in TuiConfigManager currently are:

  • default
  • amber
  • ocean
  • matrix
  • github-dark
  • github-light

But listThemeNames() merges three sources:

  1. Built-in names
  2. Custom themes in the home directory
  3. Custom themes in the workspace directory

This is why /theme does not necessarily list only built-in themes.

It lists user-defined and repo-defined themes together.


7. Which fields in a theme file really matter

TuiTheme is filled with a large number of defaults by TuiConfigManager.normalize(theme, fallbackName).

The key fields currently include:

  • brand
  • accent
  • success
  • warning
  • danger
  • text
  • muted
  • panelBorder
  • panelTitle
  • badgeForeground
  • codeBackground
  • codeBorder
  • codeText
  • codeKeyword
  • codeString
  • codeComment
  • codeNumber

This shows that a theme is more than "primary color + secondary color".

It already covers:

  • transcript
  • panel
  • badge
  • code block
  • syntax highlight

If you only change one or two fields, the other fields will continue to use the normalized default values.


8. What DefaultCodingCliTuiFactory actually decides

This is currently the development-layer entry point most worth reading directly.

It does four things in create(...):

  1. Read TuiConfig
  2. Resolve TuiTheme
  3. Construct TuiSessionView as the renderer
  4. Select the runtime based on the terminal and useAlternateScreen

In other words, it does not just "produce a TUI object" — it decides:

  • Which configuration to use
  • Which theme to use
  • Which renderer to use
  • Which runtime backend to use

9. Why it forks between AppendOnlyTuiRuntime and AnsiTuiRuntime

The current runtime selection rules are:

  • If useAlternateScreen=false and the terminal is JlineTerminalIO
    • Use AppendOnlyTuiRuntime
  • Otherwise
    • Use AnsiTuiRuntime

This means useAlternateScreen is not a purely visual preference.

It directly affects the choice of runtime backend.

In engineering terms, you can understand it this way:

AppendOnlyTuiRuntime

Closer to:

  • Append-style terminal output
  • More friendly to JLINE terminals

AnsiTuiRuntime

Closer to:

  • A full screen refresh model with a renderer
  • A unified fallback path for alternate screen or non-JLINE terminals

So do not think of useAlternateScreen as "just changing how the terminal clears the screen".


10. What actually gets updated when /theme switches

CodingCliSessionRunner.applyTheme(...) currently:

  1. Switches via TuiConfigManager.switchTheme(...)
  2. Re-runs resolveTheme(...)
  3. If it is a JlineShellTerminalIO, updates the shell terminal's theme styler
  4. If there is currently a TUI renderer, also calls tuiRenderer.updateTheme(config, theme)
  5. Refreshes the current session's output prompt

This shows that a theme switch does not "take effect the next time the TUI is opened".

It immediately affects:

  • The shell transcript style
  • The theme used by the renderer
  • Subsequent TUI rendering

11. What each deeper extension point is suited for

Changing color scheme, brand style, code highlight colors

Prefer to change:

  • TuiTheme

Prefer to change:

  • TuiConfig

Changing layout, status bar structure, message panel rendering

Prefer to change:

  • TuiRenderer
  • The current default implementation is TuiSessionView

Changing screen refresh mode, alternate screen strategy, runtime interaction shell

Prefer to change:

  • TuiRuntime
  • Or the higher-level CodingCliTuiFactory

Do not mix changes across these four layers; otherwise it becomes very hard to tell whether a given behavior comes from configuration, theme, renderer, or runtime.


12. What the current boundary is

The extension boundary of the current TUI can be summarized in one sentence:

  • Configurable out of the box, but not yet a plugin ecosystem

That is:

  • theme and tui.json are already first-class usage-layer capabilities
  • CodingCliTuiFactory, TuiRenderer, and TuiRuntime are development-layer extension points
  • There is not yet a system where "a user downloads a UI plugin package and can hot-swap it"

So if you want to do deep customization, the current expectation should still be:

  • Wire in a custom implementation at the Java layer

13. The five most common pitfalls

13.1 Assuming home and workspace tui.json do a field-level merge

They do not; when the workspace file exists it is closer to a full overwrite.

13.2 Assuming --theme is automatically persisted

It is only a startup-time override; it is not saved.

13.3 Assuming useAlternateScreen is just a visual switch

It directly changes the runtime backend selection.

13.4 Changing only the theme but expecting the layout to change too

Layout belongs to the renderer layer, not the theme layer.

13.5 Changing only the renderer while ignoring terminal backend differences

The interaction models of AppendOnlyTuiRuntime and AnsiTuiRuntime are not identical.


14. The key takeaways from this page

  • TUI customization today is split into four layers: config, theme, renderer, runtime
  • Theme lookup order is workspace > home > built-in > default fallback
  • tui.json today is closer to a file-level override, not a field-level merge
  • --theme is a one-time override; only /theme persists back to the workspace config
  • useAlternateScreen affects the runtime backend, not just the visual mode

15. Further reading

  1. CLI / TUI Usage Guide
  2. Command Reference
  3. Runtime Architecture