100 lines
4.2 KiB
Python
Executable File
100 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""`/verbose`, TUI-side: flip Claude Code's *persisted* verbose view for the current project.
|
|
|
|
Run by hooks/local-commands.py from the UserPromptExpansion hook, i.e. when you type
|
|
`/verbose [on|off|status]` -- the hook executes this and blocks the skill expansion, so no
|
|
prompt ever reaches the model and the output below is shown to you directly. (On a host
|
|
without that hook, skills/verbose/SKILL.md falls back to having the model run it.)
|
|
|
|
Claude Code has no built-in runtime `/verbose`; the live toggle is a keypress (`ctrl+x v`
|
|
here, `ctrl+o` by default -- `app:toggleTranscript`). What can be scripted is the persisted
|
|
default: `viewMode` ("verbose" | "default" | "focus" -- this is what actually decides the view;
|
|
it also beats a sticky `/focus` choice for new sessions), plus `verbose` (full tool
|
|
input/output inline) and `showThinkingSummaries` for versions without viewMode. All accept any
|
|
settings scope, so this writes the project's
|
|
`.claude/settings.local.json` -- local to this machine, never committed -- instead of the
|
|
dots-tracked ~/.claude/settings.json, which would show a diff on every toggle. Project-local
|
|
settings win over user settings, so the user default stays `verbose: true` and a project can
|
|
be quietened individually.
|
|
|
|
usage: local.py [on|off|toggle|status] (no argument = toggle)
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
KEYS = ("verbose", "showThinkingSummaries")
|
|
VIEW = "viewMode" # "verbose" | "default" | "focus"; overrides KEYS and the sticky /focus choice
|
|
LIVE = "live view: ctrl+x v (or ctrl+o) toggles the transcript; ctrl+e inside it shows everything"
|
|
|
|
|
|
def project_root():
|
|
try:
|
|
r = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, timeout=2)
|
|
if r.returncode == 0 and r.stdout.strip():
|
|
return r.stdout.strip()
|
|
except Exception:
|
|
pass
|
|
return os.getcwd()
|
|
|
|
|
|
def load(path):
|
|
try:
|
|
with open(path) as fh:
|
|
return json.load(fh)
|
|
except FileNotFoundError:
|
|
return {}
|
|
except Exception as e: # a broken local file must not be silently overwritten
|
|
sys.exit(f"error: cannot parse {path}: {e}")
|
|
|
|
|
|
def main():
|
|
raw = " ".join(sys.argv[1:]).strip()
|
|
mode = raw.lower() or "toggle"
|
|
if mode not in ("on", "off", "toggle", "status"):
|
|
sys.exit("usage: /verbose [on|off|status]")
|
|
root = project_root()
|
|
local_path = os.path.join(root, ".claude", "settings.local.json")
|
|
user = load(os.path.expanduser("~/.claude/settings.json"))
|
|
local = load(local_path)
|
|
effective = {k: bool(local.get(k, user.get(k, False))) for k in KEYS}
|
|
view = local.get(VIEW, user.get(VIEW))
|
|
|
|
def state_of(view, eff):
|
|
if view == "verbose":
|
|
return "ON"
|
|
if view in ("default", "focus"):
|
|
return f"OFF ({view} forced by settings)"
|
|
if all(eff.values()):
|
|
return "ON via the verbose key (viewMode unset: a sticky /focus choice still wins)"
|
|
return "OFF" if not any(eff.values()) else "MIXED"
|
|
|
|
if mode != "status":
|
|
current_on = state_of(view, effective).startswith("ON")
|
|
on = {"on": True, "off": False}.get(mode, not current_on)
|
|
if on:
|
|
local[VIEW] = "verbose" # forces verbose for new sessions, beating a sticky /focus
|
|
else:
|
|
local.pop(VIEW, None) # release: the sticky /focus choice (or default) applies again
|
|
for k in KEYS:
|
|
local[k] = on
|
|
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
with open(local_path, "w") as fh:
|
|
json.dump(local, fh, indent=2)
|
|
fh.write("\n")
|
|
effective = {k: on for k in KEYS}
|
|
view = local.get(VIEW, user.get(VIEW))
|
|
print(f"verbose view: {'ON' if on else 'OFF'} for {root}")
|
|
print(f" written to {local_path} (viewMode applies to new sessions; the current one: ctrl+x v)")
|
|
else:
|
|
print(f"verbose view: {state_of(view, effective)} for {root}")
|
|
print(f" {VIEW}: {view or '-'} (project-local: {local.get(VIEW, '-')}, user: {user.get(VIEW, '-')})")
|
|
for k in KEYS:
|
|
print(f" {k}: {str(effective[k]).lower()} (project-local: {local.get(k, '-')}, user: {user.get(k, '-')})")
|
|
print(LIVE)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|