Skip to content

feat(tui): add a global/project context switcher for per-project configs and installs#502

Draft
CorticalCode wants to merge 3 commits into
sirmalloc:mainfrom
CorticalCode:feat/project-mode-switcher
Draft

feat(tui): add a global/project context switcher for per-project configs and installs#502
CorticalCode wants to merge 3 commits into
sirmalloc:mainfrom
CorticalCode:feat/project-mode-switcher

Conversation

@CorticalCode

Copy link
Copy Markdown
Contributor

What

A global/project context switcher, as designed in #351. Global mode is today's behavior, unchanged. ctrl+p in the TUI switches to Project mode: the config lives in .claude/ccstatusline.json (committable, travels with the repo), and the install writes the statusLine and managed hooks to .claude/settings.local.json (personal) with --config on the command.

This closes the loop on #276, the original request: an install in settings.local.json survives company tooling that rewrites settings.json.

How it behaves

  • Launching the TUI where a project config exists starts in Project mode; a header line always shows the active mode and the resolved config path.
  • First switch into a project with no config offers to copy the global config or start with defaults. Seeding is in-memory only; the file is created on explicit save, so exiting without saving leaves no residue.
  • Switching with unsaved edits prompts save/discard/cancel, routed through the existing invalid-config save guard, and ctrl+p respects the pinned-version-mismatch lock the same way ctrl+s does.
  • Install dialogs show the real target file, and a project install saves the config first so the installed --config never references a missing file. Uninstall touches only the active mode's file and leaves .claude/ccstatusline.json in place.
  • An explicit --config on the command line behaves exactly as today and disables switching. Piped rendering and --hook are untouched: they never auto-detect, verified against live renders.

Design decisions (flagged for review)

  1. Project root is the literal cwd at TUI launch, per the design upthread in How should per-project install work? — follow-up to #309 #351 and matching feat(settings): Add support for local/project settings #58. Launching in a subdirectory binds project mode there; the mitigation is that the header always shows the resolved path. The alternative (walking up to a git root) was rejected as diverging from Claude Code's own launch-dir model.
  2. The installed --config uses an absolute path. The statusline command's working directory is not documented (Custom statusLine command resets shell CWD between Bash tool calls anthropics/claude-code#31471 suggests a fresh shell context), so a relative path is not guaranteed to resolve. Nothing portable is lost: settings.local.json is machine-local by definition.
  3. The config travels with the repo; the install stays personal. A teammate who clones gets .claude/ccstatusline.json but installs ccstatusline themselves. This follows the design upthread; the alternative (statusLine in committed .claude/settings.json) was rejected for surprise-install risk.
  4. Hook double-fire is accepted and documented. With a global and a project install both present, the statusLine renders once (local wins), but hooks merge across settings files and the two command strings differ, so both fire. The --hook handler never loads any config; its cache writes are keyed only by home dir and session id, so the duplicate fire is one redundant spawn per event, never a conflict.
  5. Project-scope hook sync never touches the global settings file, whose hooks legitimately serve other projects. This deliberately diverges from the cross-file cleanup in 25595a4, which predates two scopes being simultaneously valid.
  6. Explicit --config disables the switcher. You asked for a specific file; the TUI does not second-guess it.
  7. The switch is bidirectional (extends the one-way flow in feat(settings): Add support for local/project settings #58), with the indicator making the active mode unambiguous.
  8. Keybinding is ctrl+p, main menu only, no new menu rows.

Caveats and known limitations

  • Claude Code's managed (MDM) settings outrank every settings file — no statusline configuration can win that one. Allow claude.local.settings file to configure ccstatusline #276's company-tooling scenario (external tools rewriting settings.json) is what this fixes.
  • Dependency on current Claude Code precedence: statusLine honors normal settings precedence as of Claude Code v2.1.207. Anthropic has been narrowing project-scope settings (auto-mode settings left settings.local.json in v2.1.198; plugin configs left project settings in v2.1.207). If that trend ever reaches statusLine, project-mode installs stop taking effect.
  • installation metadata lands in the committable .claude/ccstatusline.json (a consequence of the config-path lockstep with the existing metadata system). The copy-from-global seed strips it, but a project-mode install records it. Machine-specific churn in a shareable file; open to suggestions if you'd rather it be suppressed in project scope.
  • saveClaudeSettings remains a plain (non-atomic) write, pre-existing behavior now also applied to settings.local.json. Symlinked targets are written through, not replaced. Note for any future atomic-write change there: it must carry symlink resolution or it will clobber user symlinks. (The ccstatusline config files themselves already get atomic, symlink-aware writes.) Happy to follow up with a shared atomic-write helper (temp file + rename with symlink resolution, reusing the pattern the ccstatusline config writes already use) as a separate PR.
  • Uninstall preserves .claude/ccstatusline.json contents but not byte-formatting — the pre-existing metadata writer re-serializes it with schema defaults. Functionally intact, not byte-identical.
  • TUI preview divergence for ThinkingEffort: in project mode the preview reads the project's settings.local.json while the real piped render reads the global file. Cosmetic.
  • The new TUI flows are covered by helper-level unit tests plus a scripted 12-scenario interactive walkthrough, not component-level render tests — the codebase has no ink test harness. Happy to propose one (e.g. ink-testing-library) as a separate follow-up if you want that infrastructure.

Verification

1680 tests (60+ new: target routing, scope resolution, key preservation for sandbox/permissions in settings.local.json, hook colocation, a mid-flight scope-change race pin, switcher planning), lint clean, each of the three commits green independently. Zero-behavior-change for piped/--hook/--config paths verified against live renders. A 12-item interactive walkthrough covered the full switcher flow, install/uninstall round-trips, symlinked config files and directories, and relaunch auto-detection.

Feedback wanted specifically

  • The literal-cwd project root (decision 1) and absolute --config (decision 2) are the two places this deviates from or interprets the How should per-project install work? — follow-up to #309 #351 sketch — confirm or redirect.
  • Whether project-mode installs should suppress installation metadata in the committable config (caveat 3).
  • The ctrl+p binding and the resolved-path header format, since they're pure UX taste.
  • Whether an ink component-test harness is welcome as a follow-up PR.

Credits

Builds on @jackall3n's #58 (project-config detection, copy-global seeding, active-path indicator) and the hook routing from 25595a4, re-derived on current main, per the split discussed in #351.

…targetPath

All load/save/backup, install-state readers, install/uninstall, and widget
hook sync accept an explicit target file, defaulting to today's user-global
settings.json. installStatusLine resolves its target once so a statusLine
and its hooks always land in the same file. Re-derives the targetPath
hook-routing approach from sirmalloc's 25595a4 onto current main.
Groundwork for per-project installs (sirmalloc#351).
…ets by it

Implements the context-switcher model sirmalloc proposed in sirmalloc#351: a scope
singleton mirrors the initConfigPath idiom; project scope points the config
at <root>/.claude/ccstatusline.json and installs into
<root>/.claude/settings.local.json; an explicit --config (custom scope) and
the piped/--hook paths behave exactly as before. The TUI branch of main()
auto-detects an existing project config. Load-error messages now name the
actual config file, hook sync no longer materializes an empty settings file
on plain saves, and the refresh-interval writer resolves its target once so
an in-flight scope switch cannot write across scopes.
ctrl+p flips between global and project mode from the main menu, with a
header indicator showing the resolved project config path. Entering a
project with no config offers to copy the global config or start with
defaults, seeding in memory only; the file is created on explicit save.
Switching with unsaved edits prompts save/discard/cancel, routed through
the invalid-config save guard. Install dialogs show the scope-aware target
and save the project config before installing so the installed --config
never dangles. The project-config detection, copy-global seeding, and
active-config indicator build on jackall3n's sirmalloc#58. Implements the switcher
sirmalloc designed in sirmalloc#351; closes the settings.local.json request from
sirmalloc#276.
@CorticalCode CorticalCode changed the title feat: add a global/project mode switcher for per-project installs feat(tui): add a global/project context switcher for per-project configs and installs Jul 13, 2026
@CorticalCode

Copy link
Copy Markdown
Contributor Author

Screenshots of the switcher states, captured from the TUI:

Global mode. The only change existing users see is one header line:

CCStatusline Configuration | v2.2.23

Mode: Global · ctrl+p to switch
╭──────────────────────────────────────────────────────────────────╮
│ > Preview  (ctrl+s to save configuration at any time)            │
╰──────────────────────────────────────────────────────────────────╯
  Model: Claude | Ctx: 18.6k | ⎇ main |...

Main Menu

▶  📝 Edit Lines
   🎨 Edit Colors
   ⚡ Powerline Setup
   ...

First switch into a project with no config (ctrl+p):

No project config found in this directory

▶  📋 Copy current global config
   ✨ Start with defaults
   ❌ Cancel

Project mode after seeding. The header shows the resolved config path; Save & Exit appears because the seed is in memory only, and nothing exists on disk until you save:

Mode: Project (~/projects/my-app/.claude/ccstatusline.json) · ctrl+p to switch
...
   📦 Install to Claude Code

   💾 Save & Exit
   ❌ Exit without saving

Switching away with unsaved edits (ctrl+p again):

Mode: Project (~/projects/my-app/.claude/ccstatusline.json) · ctrl+p to switch
...
You have unsaved changes

▶  💾 Save and switch
   🗑️  Discard and switch
   ❌ Cancel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant