Skip to content

ensureTuiPluginEntry creates shadowing tui.json when tui.jsonc has the real config #176

@HaleTom

Description

@HaleTom

Bug

ensureTuiPluginEntry() in tui-config.ts writes to tui.json even when the user's real TUI config is in tui.jsonc, creating a shadowing file.

Root cause

resolveTuiConfigPath() checks tui.jsonc first (correct), then tui.json. But on first run when neither file exists, it defaults to creating tui.json. Once tui.json exists, subsequent runs keep writing to it — even though the user may have created tui.jsonc with their real config (keybinds, comments, etc.).

Since OpenCode resolves .json before .jsonc, the tui.json with just {} shadows the user's tui.jsonc with real content.

Current code

function resolveTuiConfigPath(): string {
    const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
    const jsoncPath = join(configDir, "tui.jsonc");
    const jsonPath = join(configDir, "tui.json");

    if (existsSync(jsoncPath)) return jsoncPath;
    if (existsSync(jsonPath)) return jsonPath;
    return jsonPath; // default: create tui.json  ← BUG
}

Reproduction

  1. User has ~/.config/opencode/tui.jsonc with keybinds and comments
  2. magic-context runs for the first time (neither tui.json nor tui.jsonc exists yet)
  3. It creates ~/.config/opencode/tui.json with { "plugin": ["@cortexkit/opencode-magic-context@latest"] }
  4. User creates ~/.config/opencode/tui.jsonc with their real config
  5. On next startup, resolveTuiConfigPath() finds tui.jsonc first → reads from .jsonc
  6. But tui.json still shadows tui.jsonc in OpenCode's config resolution (.json takes precedence)

Actual user impact

My current files:

~/.config/opencode/tui.json   → { }          ← created by magic-context, shadows .jsonc
~/.config/opencode/tui.jsonc  → real config with keybinds, comments, etc.

Proposed fix

Two changes:

1. Default to .jsonc instead of .json

When neither file exists, create tui.jsonc (not tui.json). JSONC supports comments and is a superset of JSON, so it's strictly more user-friendly:

function resolveTuiConfigPath(): string {
    const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
    const jsoncPath = join(configDir, "tui.jsonc");
    const jsonPath = join(configDir, "tui.json");

    if (existsSync(jsoncPath)) return jsoncPath;
    if (existsSync(jsonPath)) return jsonPath;
    return jsoncPath; // default: create tui.jsonc (supports comments)
}

2. Prefer .jsonc over .json when both exist

When both tui.json and tui.jsonc exist, the current code prefers .jsonc (correct for reading). But ensureTuiPluginEntry() writes back to whatever path resolveTuiConfigPath() returns. If it returns .jsonc, the write is correct. If .jsonc doesn't exist but .json does, it returns .json — which is fine if .jsonc genuinely doesn't exist.

The remaining edge case: if tui.json exists (from a prior buggy run) but the user's real config is in tui.jsonc, the code already prefers .jsonc on read. But the stale tui.json still shadows it at the OpenCode level. A cleanup step could be added:

// In ensureTuiPluginEntry(), after writing to the resolved path:
// If we wrote to .jsonc and a stale .json exists with only magic-context's entry,
// remove it to avoid shadowing.
if (configPath.endsWith('.jsonc')) {
    const jsonSibling = configPath.replace(/\.jsonc$/, '.json');
    if (existsSync(jsonSibling)) {
        try {
            const sibling = parse(readFileSync(jsonSibling, 'utf-8')) as Record<string, unknown>;
            const siblingPlugins = Array.isArray(sibling.plugin) ? sibling.plugin : [];
            if (Object.keys(sibling).length <= 1 && siblingPlugins.every(isMagicContextEntry)) {
                unlinkSync(jsonSibling);
                log(`[magic-context] removed stale shadowing ${jsonSibling}`);
            }
        } catch { /* leave it alone if parse fails */ }
    }
}

Same pattern as context-mode issue

This is the same class of bug as mksglu/context-mode#849.json files shadowing .jsonc user configs. The fix pattern is consistent: prefer .jsonc for both default creation and write targets.

Affected file

src/shared/tui-config.tsresolveTuiConfigPath() and ensureTuiPluginEntry()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions