methodproof 0.8.0__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. methodproof-0.8.1/.code-review-graph/.gitignore +3 -0
  2. methodproof-0.8.1/.code-review-graph/graph.db +0 -0
  3. {methodproof-0.8.0 → methodproof-0.8.1}/CHANGELOG.md +19 -0
  4. {methodproof-0.8.0 → methodproof-0.8.1}/PKG-INFO +1 -1
  5. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/__init__.py +1 -1
  6. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/base.py +29 -0
  7. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/terminal.py +45 -5
  8. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/watcher.py +62 -19
  9. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/cli.py +0 -2
  10. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/claude_code.py +2 -3
  11. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/log.py +106 -12
  12. methodproof-0.8.1/methodproof/tui/theme.py +156 -0
  13. {methodproof-0.8.0 → methodproof-0.8.1}/pyproject.toml +1 -1
  14. methodproof-0.8.0/methodproof/tui/theme.py +0 -106
  15. {methodproof-0.8.0 → methodproof-0.8.1}/.github/workflows/ci.yml +0 -0
  16. {methodproof-0.8.0 → methodproof-0.8.1}/.gitignore +0 -0
  17. {methodproof-0.8.0 → methodproof-0.8.1}/LICENSE +0 -0
  18. {methodproof-0.8.0 → methodproof-0.8.1}/README.md +0 -0
  19. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/__main__.py +0 -0
  20. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/_daemon.py +0 -0
  21. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/__init__.py +0 -0
  22. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/music.py +0 -0
  23. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/analysis.py +0 -0
  24. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/binding.py +0 -0
  25. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/bip39.py +0 -0
  26. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/bridge.py +0 -0
  27. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/config.py +0 -0
  28. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/crypto.py +0 -0
  29. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/e2e.py +0 -0
  30. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/graph.py +0 -0
  31. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hook.py +0 -0
  32. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/__init__.py +0 -0
  33. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/claude_code.sh +0 -0
  34. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/cline_hook.sh +0 -0
  35. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/codex_hook.sh +0 -0
  36. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/gemini_hook.sh +0 -0
  37. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/install.py +0 -0
  38. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/kiro_hook.sh +0 -0
  39. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/mcp_register.py +0 -0
  40. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/openclaw/HOOK.md +0 -0
  41. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/openclaw/handler.ts +0 -0
  42. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/openclaw_install.py +0 -0
  43. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/opencode_plugin.js +0 -0
  44. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/wrappers.py +0 -0
  45. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/integrity.py +0 -0
  46. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/kdf.py +0 -0
  47. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/keychain.py +0 -0
  48. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/live.py +0 -0
  49. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/lock.py +0 -0
  50. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/mcp.py +0 -0
  51. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/migrate_db.py +0 -0
  52. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/proxy.py +0 -0
  53. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/proxy_daemon.py +0 -0
  54. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/repos.py +0 -0
  55. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/skills/methodproof/SKILL.md +0 -0
  56. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/store.py +0 -0
  57. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/sync.py +0 -0
  58. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/__init__.py +0 -0
  59. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/consent.py +0 -0
  60. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/init.py +0 -0
  61. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/login_success.py +0 -0
  62. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/review.py +0 -0
  63. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/start.py +0 -0
  64. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/status.py +0 -0
  65. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/viewer.py +0 -0
  66. {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/wordlist.py +0 -0
  67. {methodproof-0.8.0 → methodproof-0.8.1}/test_windows_compat.py +0 -0
  68. {methodproof-0.8.0 → methodproof-0.8.1}/tests/__init__.py +0 -0
  69. {methodproof-0.8.0 → methodproof-0.8.1}/tests/conftest.py +0 -0
  70. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_analysis.py +0 -0
  71. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_auth.py +0 -0
  72. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_config.py +0 -0
  73. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_helpers.py +0 -0
  74. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_session.py +0 -0
  75. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_share.py +0 -0
  76. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_start.py +0 -0
  77. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_update.py +0 -0
  78. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_e2e_integration.py +0 -0
  79. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_graph.py +0 -0
  80. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_hooks.py +0 -0
  81. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_live.py +0 -0
  82. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_openclaw_hooks.py +0 -0
  83. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_profiles.py +0 -0
  84. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_repos.py +0 -0
  85. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_security.py +0 -0
  86. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_store.py +0 -0
  87. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_sync.py +0 -0
  88. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_viewer.py +0 -0
  89. {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_wrappers.py +0 -0
  90. {methodproof-0.8.0 → methodproof-0.8.1}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ # Auto-generated by code-review-graph — do not commit database files.
2
+ # The graph.db contains absolute paths and code structure metadata.
3
+ *
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.1] — 2026-04-17
4
+
5
+ ### Added
6
+ - **File rename detection** — watcher `on_moved` handler emits `file_rename` atoms with `old_path`/`new_path` instead of producing two incorrect atoms (delete + create).
7
+ - **`git_branch_switch` event** — git poller reads `.git/HEAD` on each 2s cycle and emits `git_branch_switch` with `old_branch`/`new_branch` when the active branch changes.
8
+ - **`git_commit` branch name** — commits now carry a `branch` field read from `.git/HEAD`.
9
+ - **`git_commit` per-file change status** — `files_changed` is now accompanied by `file_statuses` (`{path: "A"|"M"|"D"|"R"}`) via `--name-status` instead of `--name-only`.
10
+ - **11 new test frameworks** — detection and result parsing for vitest, mocha, rspec, minitest, phpunit, unittest, dotnet test, swift test, gradle test, maven test, and ExUnit (total: 15).
11
+ - **`test_run` carries `cwd`** — forwarded from the shell hook JSONL instead of being discarded.
12
+ - **`is_journal_mode()` public API** — exposed on `agents.base` for agents making structural-vs-full content capture decisions.
13
+
14
+ ### Fixed
15
+ - **15 hook event types bypassed consent gates** — `claude_session_end`, `agent_turn_end`, `agent_turn_error`, `context_compact_start/end`, `permission_request/denied`, `mcp_elicitation/result`, `worktree_create/remove`, `cwd_changed`, `tool_failure`, plus new `file_rename` and `git_branch_switch` — all now properly gated in `_EVENT_GATES`.
16
+ - **Removed all arbitrary data truncation** — no more 50KB diff cap, 2000-line hunk cap, 2000-char commit body cap, 500-char terminal output cap, or 200-char error cap. The CLI captures faithfully; downstream consumers decide what to do with large payloads. Preview fields (derived summaries alongside raw data) are unchanged.
17
+
18
+ ### Changed
19
+ - **TUI inline detail screen** — `mp log` enter key opens a full-screen `DetailScreen` with event mix breakdown and per-type timeline instead of exiting to an external viewer.
20
+ - **SHOMEN/KINMYAKU theme toggle** — `MP_THEME=shomen` selects the light theme; default remains `kinmyaku` (dark). `Theme` dataclass adds `accent`, `cursor_bg`, `cursor_fg` semantic roles. DataTable cursor CSS for both themes.
21
+
3
22
  ## [0.8.0] — 2026-04-15
4
23
 
5
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: See how you code. Capture and visualize your engineering process.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.7.21"
3
+ __version__ = "0.8.1"
@@ -68,6 +68,24 @@ _EVENT_GATES: dict[str, str] = {
68
68
  "gemini_session_end": "ai_responses",
69
69
  "kiro_session_start": "ai_prompts",
70
70
  "kiro_session_end": "ai_responses",
71
+ # Hook lifecycle events (continued)
72
+ "claude_session_end": "ai_responses",
73
+ "agent_turn_end": "ai_responses",
74
+ "agent_turn_error": "ai_responses",
75
+ "context_compact_start": "ai_responses",
76
+ "context_compact_end": "ai_responses",
77
+ "permission_request": "ai_responses",
78
+ "permission_denied": "ai_responses",
79
+ "mcp_elicitation": "ai_responses",
80
+ "mcp_elicitation_result": "ai_responses",
81
+ "worktree_create": "ai_responses",
82
+ "worktree_remove": "ai_responses",
83
+ "cwd_changed": "ai_responses",
84
+ "tool_failure": "ai_responses",
85
+ # File system events
86
+ "file_rename": "file_changes",
87
+ # Git events
88
+ "git_branch_switch": "git_commits",
71
89
  }
72
90
 
73
91
  # Maps capture categories to (event_type, field) pairs for field-level gating.
@@ -142,6 +160,13 @@ def is_content_captured() -> bool:
142
160
  return bool(_capture.get("code_capture", False))
143
161
 
144
162
 
163
+ def is_journal_mode() -> bool:
164
+ """True when journal_mode is enabled — full content capture (prompts,
165
+ completions, diffs, terminal output). Agents use this to decide whether
166
+ to include content fields that are stripped in structural-only mode."""
167
+ return _journal_mode
168
+
169
+
145
170
  def emit(event_type: str, metadata: dict[str, Any]) -> None:
146
171
  if not _initialized:
147
172
  log("warning", "emit.before_init", type=event_type)
@@ -226,6 +251,10 @@ def _stream_event(entry: dict[str, Any]) -> None:
226
251
  detail = f'{meta.get("path", "?")} ({meta.get("size", 0)}B)'
227
252
  elif etype == "file_delete":
228
253
  detail = meta.get("path", "?")
254
+ elif etype == "file_rename":
255
+ detail = f'{meta.get("old_path", "?")} → {meta.get("new_path", "?")}'
256
+ elif etype == "git_branch_switch":
257
+ detail = f'{meta.get("old_branch", "?")} → {meta.get("new_branch", "?")}'
229
258
  elif etype == "terminal_cmd":
230
259
  detail = f'{meta.get("command", "?")[:60]} → exit {meta.get("exit_code", "?")}'
231
260
  elif etype == "test_run":
@@ -20,11 +20,27 @@ TEST_FRAMEWORKS = {
20
20
  "jest": re.compile(r"\bjest\b|npx jest|npm test"),
21
21
  "go_test": re.compile(r"\bgo test\b"),
22
22
  "cargo_test": re.compile(r"\bcargo test\b"),
23
+ "vitest": re.compile(r"\bvitest\b|npx vitest"),
24
+ "mocha": re.compile(r"\bmocha\b|npx mocha"),
25
+ "rspec": re.compile(r"\brspec\b|bundle exec rspec"),
26
+ "minitest": re.compile(r"\bruby\b.*test|rake test"),
27
+ "phpunit": re.compile(r"\bphpunit\b|vendor/bin/phpunit"),
28
+ "unittest": re.compile(r"python.*-m\s+unittest|python.*-m\s+nose"),
29
+ "dotnet_test": re.compile(r"\bdotnet test\b"),
30
+ "swift_test": re.compile(r"\bswift test\b"),
31
+ "gradle_test": re.compile(r"\bgradle\b.*\btest\b|gradlew.*\btest\b"),
32
+ "maven_test": re.compile(r"\bmvn\b.*\btest\b|mvnw.*\btest\b"),
33
+ "exunit": re.compile(r"\bmix test\b"),
23
34
  }
24
35
 
25
36
  _PYTEST_RE = re.compile(r"(\d+) passed(?:.*?(\d+) failed)?(?:.*?(\d+) skipped)?")
26
37
  _JEST_RE = re.compile(r"Tests:\s+(?:(\d+) failed,\s+)?(\d+) passed(?:,\s+(\d+) skipped)?")
27
38
  _CARGO_RE = re.compile(r"(\d+) passed.*?(\d+) failed")
39
+ _VITEST_RE = re.compile(r"Tests\s+(\d+) passed(?:\s*\|\s*(\d+) failed)?(?:\s*\|\s*(\d+) skipped)?")
40
+ _RSPEC_RE = re.compile(r"(\d+) examples?,\s*(\d+) failures?(?:,\s*(\d+) pending)?")
41
+ _PHPUNIT_RE = re.compile(r"OK \((\d+) tests?|Tests:\s*(\d+).*?Failures:\s*(\d+)")
42
+ _DOTNET_RE = re.compile(r"Passed!\s+-\s+Failed:\s+(\d+),\s+Passed:\s+(\d+),\s+Skipped:\s+(\d+)")
43
+ _EXUNIT_RE = re.compile(r"(\d+) tests?,\s*(\d+) failures?(?:,\s*(\d+) excluded)?")
28
44
 
29
45
 
30
46
  def _detect_test(command: str) -> str | None:
@@ -40,9 +56,12 @@ def _parse_test_results(output: str, framework: str, exit_code: int) -> tuple[in
40
56
  m = _PYTEST_RE.search(output)
41
57
  if m:
42
58
  return int(m.group(1)), int(m.group(2) or 0), int(m.group(3) or 0)
43
- elif framework == "jest":
44
- m = _JEST_RE.search(output)
59
+ elif framework in ("jest", "vitest"):
60
+ pat = _VITEST_RE if framework == "vitest" else _JEST_RE
61
+ m = pat.search(output)
45
62
  if m:
63
+ if framework == "vitest":
64
+ return int(m.group(1) or 0), int(m.group(2) or 0), int(m.group(3) or 0)
46
65
  return int(m.group(2) or 0), int(m.group(1) or 0), int(m.group(3) or 0)
47
66
  elif framework == "go_test":
48
67
  return output.count("--- PASS"), output.count("--- FAIL"), output.count("--- SKIP")
@@ -50,6 +69,24 @@ def _parse_test_results(output: str, framework: str, exit_code: int) -> tuple[in
50
69
  m = _CARGO_RE.search(output)
51
70
  if m:
52
71
  return int(m.group(1)), int(m.group(2)), 0
72
+ elif framework == "rspec":
73
+ m = _RSPEC_RE.search(output)
74
+ if m:
75
+ return int(m.group(1)) - int(m.group(2)), int(m.group(2)), int(m.group(3) or 0)
76
+ elif framework == "phpunit":
77
+ m = _PHPUNIT_RE.search(output)
78
+ if m:
79
+ if m.group(1):
80
+ return int(m.group(1)), 0, 0
81
+ return int(m.group(2) or 0) - int(m.group(3) or 0), int(m.group(3) or 0), 0
82
+ elif framework == "dotnet_test":
83
+ m = _DOTNET_RE.search(output)
84
+ if m:
85
+ return int(m.group(2)), int(m.group(1)), int(m.group(3))
86
+ elif framework == "exunit":
87
+ m = _EXUNIT_RE.search(output)
88
+ if m:
89
+ return int(m.group(1)) - int(m.group(2)), int(m.group(2)), int(m.group(3) or 0)
53
90
  if exit_code == 0:
54
91
  return 1, 0, 0
55
92
  return 0, 1, 0
@@ -91,7 +128,7 @@ def _process(line: str) -> None:
91
128
  return
92
129
  exit_code = entry.get("exit_code", 0)
93
130
  duration = entry.get("duration_ms", 0)
94
- output = entry.get("output", "")[:500]
131
+ output = entry.get("output", "")
95
132
  if SENSITIVE.search(output):
96
133
  output = "[redacted — contains sensitive content]"
97
134
  cwd = entry.get("cwd", "")
@@ -107,7 +144,10 @@ def _process(line: str) -> None:
107
144
  framework = _detect_test(command)
108
145
  if framework:
109
146
  passed, failed, skipped = _parse_test_results(output, framework, exit_code)
110
- base.emit("test_run", {
147
+ test_meta: dict = {
111
148
  "framework": framework, "passed": passed, "failed": failed,
112
149
  "skipped": skipped, "duration_ms": duration,
113
- })
150
+ }
151
+ if cwd:
152
+ test_meta["cwd"] = cwd
153
+ base.emit("test_run", test_meta)
@@ -45,9 +45,6 @@ IGNORE_PATTERNS = re.compile(
45
45
  )
46
46
 
47
47
 
48
- _MAX_DIFF_BYTES = 50_000
49
- _MAX_HUNK_LINES = 2_000
50
-
51
48
  _HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
52
49
  _DIFF_FILE_RE = re.compile(r"^diff --git a/(.+?) b/(.+?)$")
53
50
 
@@ -60,7 +57,6 @@ def _parse_hunks(diff_text: str, include_lines: bool) -> list[dict[str, object]]
60
57
  """
61
58
  hunks: list[dict[str, object]] = []
62
59
  current: dict[str, object] | None = None
63
- total_lines = 0
64
60
  for line in diff_text.splitlines():
65
61
  header = _HUNK_HEADER_RE.match(line)
66
62
  if header:
@@ -81,10 +77,7 @@ def _parse_hunks(diff_text: str, include_lines: bool) -> list[dict[str, object]]
81
77
  continue
82
78
  if line.startswith("+++") or line.startswith("---"):
83
79
  continue
84
- if total_lines >= _MAX_HUNK_LINES:
85
- continue
86
80
  current["lines"].append(line) # type: ignore[union-attr]
87
- total_lines += 1
88
81
  if current is not None:
89
82
  hunks.append(current)
90
83
  return hunks
@@ -122,7 +115,7 @@ def _git_diff_hunks(repo: str, path: str, include_lines: bool) -> list[dict[str,
122
115
  ["git", "-C", repo, "diff", "--unified=0", "--", path],
123
116
  capture_output=True, text=True, timeout=5,
124
117
  )
125
- return _parse_hunks(result.stdout[:_MAX_DIFF_BYTES], include_lines)
118
+ return _parse_hunks(result.stdout, include_lines)
126
119
  except Exception:
127
120
  return []
128
121
 
@@ -136,7 +129,7 @@ def _git_show_file_hunks(
136
129
  ["git", "-C", repo, "show", "--format=", "--unified=0", sha],
137
130
  capture_output=True, text=True, timeout=10,
138
131
  )
139
- return _parse_show_hunks(result.stdout[:_MAX_DIFF_BYTES * 4], include_lines)
132
+ return _parse_show_hunks(result.stdout, include_lines)
140
133
  except Exception:
141
134
  return {}
142
135
 
@@ -160,25 +153,25 @@ def _git_diff_stats(repo: str, path: str) -> tuple[int, int]:
160
153
 
161
154
 
162
155
  def _git_diff_content(repo: str, path: str) -> str:
163
- """Get full diff content for a file, capped at 50KB."""
156
+ """Get full diff content for a file."""
164
157
  try:
165
158
  result = subprocess.run(
166
159
  ["git", "-C", repo, "diff", "--", path],
167
160
  capture_output=True, text=True, timeout=5,
168
161
  )
169
- return result.stdout[:_MAX_DIFF_BYTES]
162
+ return result.stdout
170
163
  except Exception:
171
164
  return ""
172
165
 
173
166
 
174
167
  def _git_show_diff(repo: str, sha: str) -> str:
175
- """Get full diff for a commit, capped at 50KB."""
168
+ """Get full diff for a commit."""
176
169
  try:
177
170
  result = subprocess.run(
178
171
  ["git", "-C", repo, "show", "--format=", sha],
179
172
  capture_output=True, text=True, timeout=10,
180
173
  )
181
- return result.stdout[:_MAX_DIFF_BYTES]
174
+ return result.stdout
182
175
  except Exception:
183
176
  return ""
184
177
 
@@ -239,14 +232,44 @@ class _Handler(FileSystemEventHandler):
239
232
  lang = Path(event.src_path).suffix.lstrip(".")
240
233
  base.emit("file_delete", {"path": path, "language": lang})
241
234
 
235
+ def on_moved(self, event: FileSystemEvent) -> None:
236
+ if event.is_directory:
237
+ return
238
+ src = event.src_path
239
+ dest = getattr(event, "dest_path", "")
240
+ if IGNORE_PATTERNS.search(src) and IGNORE_PATTERNS.search(dest or src):
241
+ return
242
+ old_path = self._relpath(src)
243
+ new_path = self._relpath(dest) if dest else old_path
244
+ lang = Path(dest or src).suffix.lstrip(".")
245
+ old_hash = self._hashes.pop(old_path, None)
246
+ if old_hash:
247
+ self._hashes[new_path] = old_hash
248
+ base.emit("file_rename", {
249
+ "old_path": old_path, "new_path": new_path, "language": lang,
250
+ })
251
+
252
+
253
+ def _read_branch(head_file: Path) -> str:
254
+ """Read current branch from .git/HEAD. Returns '' for detached HEAD."""
255
+ try:
256
+ content = head_file.read_text().strip()
257
+ if content.startswith("ref: refs/heads/"):
258
+ return content[16:]
259
+ return ""
260
+ except OSError:
261
+ return ""
262
+
242
263
 
243
264
  def _poll_git(watch_dir: str, stop: threading.Event) -> None:
244
- """Poll .git/refs for new commits every 2 seconds."""
265
+ """Poll .git/refs for new commits and HEAD for branch switches."""
245
266
  git_dir = Path(watch_dir) / ".git"
246
267
  if not git_dir.exists():
247
268
  return
248
269
  seen: set[str] = set()
249
270
  refs = git_dir / "refs" / "heads"
271
+ head_file = git_dir / "HEAD"
272
+ last_branch = _read_branch(head_file)
250
273
  while not stop.is_set():
251
274
  try:
252
275
  for ref in refs.iterdir():
@@ -257,12 +280,17 @@ def _poll_git(watch_dir: str, stop: threading.Event) -> None:
257
280
  _log_commit(watch_dir, sha)
258
281
  except OSError:
259
282
  pass
283
+ current_branch = _read_branch(head_file)
284
+ if current_branch and current_branch != last_branch and last_branch:
285
+ base.emit("git_branch_switch", {
286
+ "old_branch": last_branch, "new_branch": current_branch,
287
+ })
288
+ last_branch = current_branch
260
289
  stop.wait(2)
261
290
 
262
291
 
263
292
  def _log_commit(watch_dir: str, sha: str) -> None:
264
293
  try:
265
- # Single git call: subject\x00author\x00email\x00iso_date\x00parent_hash\x00full_body
266
294
  fmt = subprocess.run(
267
295
  ["git", "-C", watch_dir, "log", "-1",
268
296
  "--format=%s%x00%an%x00%ae%x00%ai%x00%P%x00%B", sha],
@@ -275,12 +303,22 @@ def _log_commit(watch_dir: str, sha: str) -> None:
275
303
  committed_at = parts[3].strip() if len(parts) > 3 else ""
276
304
  parent_hash = parts[4].strip()[:7] if len(parts) > 4 else ""
277
305
  body = parts[5].strip() if len(parts) > 5 else ""
278
- files = subprocess.run(
279
- ["git", "-C", watch_dir, "diff-tree", "--no-commit-id", "-r", "--name-only", sha],
306
+ status_lines = subprocess.run(
307
+ ["git", "-C", watch_dir, "diff-tree", "--no-commit-id", "-r", "--name-status", sha],
280
308
  capture_output=True, text=True, timeout=5,
281
309
  ).stdout.strip().splitlines()
310
+ files: list[str] = []
311
+ file_statuses: dict[str, str] = {}
312
+ for sl in status_lines:
313
+ sl_parts = sl.split("\t", 1)
314
+ if len(sl_parts) == 2:
315
+ file_statuses[sl_parts[1]] = sl_parts[0]
316
+ files.append(sl_parts[1])
317
+ elif sl.strip():
318
+ files.append(sl.strip())
282
319
  except Exception:
283
- subject, author, author_email, committed_at, parent_hash, body, files = "", "", "", "", "", "", []
320
+ subject, author, author_email, committed_at, parent_hash, body = "", "", "", "", "", ""
321
+ files, file_statuses = [], {}
284
322
  meta: dict[str, object] = {
285
323
  "hash": sha[:7], "message": subject, "files_changed": files,
286
324
  "author": author, "author_email": author_email, "committed_at": committed_at,
@@ -288,7 +326,12 @@ def _log_commit(watch_dir: str, sha: str) -> None:
288
326
  if parent_hash:
289
327
  meta["parent_hash"] = parent_hash
290
328
  if body and body != subject:
291
- meta["body"] = body[:2000]
329
+ meta["body"] = body
330
+ if file_statuses:
331
+ meta["file_statuses"] = file_statuses
332
+ branch = _read_branch(Path(watch_dir) / ".git" / "HEAD")
333
+ if branch:
334
+ meta["branch"] = branch
292
335
  include_lines = base.is_content_captured()
293
336
  file_hunks = _git_show_file_hunks(watch_dir, sha, include_lines)
294
337
  if file_hunks:
@@ -1577,8 +1577,6 @@ def cmd_log(args: argparse.Namespace) -> None:
1577
1577
  fake = _ap.Namespace(session_id=sid, local=False)
1578
1578
  if action == "push":
1579
1579
  cmd_push(fake)
1580
- elif action == "view":
1581
- cmd_view(fake)
1582
1580
  return
1583
1581
  sessions = store.list_sessions()
1584
1582
  if not sessions:
@@ -113,20 +113,19 @@ _META_EXTRACTORS = {
113
113
  "tool": _TOOL, "tool_name": d.get("tool_name", "unknown"),
114
114
  "success": False, "is_interrupt": d.get("is_interrupt", False),
115
115
  "tool_input": d.get("tool_input") or {},
116
- "error": str(d.get("error", ""))[:200],
116
+ "error": d.get("error", ""),
117
117
  },
118
118
  "SubagentStart": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
119
119
  "SubagentStop": lambda d: {
120
120
  "tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", ""),
121
121
  "last_assistant_message": d.get("last_assistant_message", ""),
122
- "last_message_preview": str(d.get("last_assistant_message", ""))[:200],
123
122
  },
124
123
  "TaskCreated": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", ""), "subject": d.get("task_subject", "")},
125
124
  "TaskCompleted": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", "")},
126
125
  "SessionStart": lambda d: {"tool": _TOOL, "session_id": d.get("session_id", ""), "cwd": d.get("cwd", "")},
127
126
  "SessionEnd": lambda d: {"tool": _TOOL, "session_id": d.get("session_id", "")},
128
127
  "Stop": lambda d: {"tool": _TOOL},
129
- "StopFailure": lambda d: {"tool": _TOOL, "error": str(d.get("error", ""))[:200]},
128
+ "StopFailure": lambda d: {"tool": _TOOL, "error": d.get("error", "")},
130
129
  "CwdChanged": lambda d: {
131
130
  "tool": _TOOL, "cwd": d.get("cwd", ""),
132
131
  # NOTE: fires for both human `cd` and Claude tool use — caller is ambiguous
@@ -1,25 +1,28 @@
1
- """Textual TUI for mp log — session browser with preview pane."""
1
+ """Textual TUI for mp log — session browser with inline detail screen."""
2
2
  from __future__ import annotations
3
3
 
4
+ import json
4
5
  from datetime import datetime, UTC
5
6
 
6
7
  from textual.app import App, ComposeResult
7
8
  from textual.binding import Binding
8
- from textual.containers import Horizontal, Vertical
9
+ from textual.containers import Horizontal, Vertical, VerticalScroll
10
+ from textual.screen import Screen
9
11
  from textual.widgets import DataTable, Footer, Header, Static
10
12
 
11
13
  from methodproof import store
12
- from methodproof.tui.theme import BASE_CSS, BORDER, DIM, GOLD, GREEN, PURPLE, RED, TEXT
14
+ from methodproof.tui.theme import ACTIVE, BASE_CSS, BORDER, DIM, GOLD, GREEN, PURPLE, RED, TEXT
15
+ from methodproof.viewer import SENSITIVE_FIELDS, _event_summary, _offset
13
16
 
14
17
  _CSS = BASE_CSS + f"""
15
18
  #sessions-table {{
16
19
  width: 3fr;
17
- border-right: solid {BORDER};
20
+ border-right: solid {ACTIVE.border};
18
21
  }}
19
22
  #preview-pane {{
20
23
  width: 1fr;
21
24
  padding: 1 2;
22
- background: #0a0908;
25
+ background: {ACTIVE.sidebar_bg};
23
26
  }}
24
27
  .preview-meta {{
25
28
  color: {TEXT};
@@ -34,6 +37,12 @@ _CSS = BASE_CSS + f"""
34
37
  text-style: bold;
35
38
  margin: 1 0 0 0;
36
39
  }}
40
+ #detail-scroll {{
41
+ padding: 1 3;
42
+ }}
43
+ #detail-content {{
44
+ width: 100%;
45
+ }}
37
46
  """
38
47
 
39
48
  _DT_FMT = "%b %d %H:%M"
@@ -48,8 +57,89 @@ def _fmt_events(n: int) -> str:
48
57
  return f"{n/1000:.1f}k" if n >= 1000 else str(n)
49
58
 
50
59
 
60
+ class DetailScreen(Screen):
61
+ """Full-screen session detail view — esc returns to the list."""
62
+
63
+ BINDINGS = [
64
+ Binding("escape", "back", "back"),
65
+ Binding("q", "back", "back"),
66
+ ]
67
+
68
+ def __init__(self, session: dict) -> None:
69
+ super().__init__()
70
+ self._session = session
71
+
72
+ def compose(self) -> ComposeResult:
73
+ yield Header(show_clock=False)
74
+ with VerticalScroll(id="detail-scroll"):
75
+ yield Static(self._render(), id="detail-content")
76
+ yield Footer()
77
+
78
+ def action_back(self) -> None:
79
+ self.app.pop_screen()
80
+
81
+ def _render(self) -> str:
82
+ sess = self._session
83
+ sid = (sess.get("id") or "")[:8]
84
+ created = sess.get("created_at", 0)
85
+ completed = sess.get("completed_at")
86
+ dt_str = datetime.fromtimestamp(created, tz=UTC).strftime(_DT_FMT) if created else "—"
87
+ dur = _fmt_dur(int((completed or created) - created)) if completed else "in progress"
88
+ vis = sess.get("visibility", "private")
89
+ synced = "synced" if sess.get("synced") else "local"
90
+ tags = json.loads(sess.get("tags") or "[]")
91
+ repo = sess.get("repo_url") or "—"
92
+
93
+ events = store.get_events(sess["id"], decrypt=True)
94
+
95
+ lines = [
96
+ f"[bold {GOLD}]session {sid}[/bold {GOLD}]",
97
+ f"[{DIM}]{dt_str} · {dur} · {len(events)} events · {vis} · {synced}[/{DIM}]",
98
+ f"[{DIM}]repo: {repo}[/{DIM}]",
99
+ ]
100
+ if tags:
101
+ lines.append(f"[{DIM}]tags: {', '.join(tags)}[/{DIM}]")
102
+
103
+ if not events:
104
+ lines.append("")
105
+ lines.append(f"[{DIM}]no events captured[/{DIM}]")
106
+ return "\n".join(lines)
107
+
108
+ by_type: dict[str, list[dict]] = {}
109
+ for e in events:
110
+ by_type.setdefault(e["type"], []).append(e)
111
+
112
+ total = len(events)
113
+ lines.append("")
114
+ lines.append(f"[bold {GOLD}]Event mix[/bold {GOLD}]")
115
+ for etype, items in sorted(by_type.items(), key=lambda x: -len(x[1])):
116
+ pct = len(items) / total
117
+ bar_len = max(1, int(pct * 18))
118
+ bar = "█" * bar_len + "░" * (18 - bar_len)
119
+ lines.append(
120
+ f"[{DIM}]{etype:<20}[/{DIM}] "
121
+ f"[{TEXT}]{len(items):>5}[/{TEXT}] "
122
+ f"[{GREEN}]{bar}[/{GREEN}] "
123
+ f"[{DIM}]{int(pct * 100):>3}%[/{DIM}]"
124
+ )
125
+
126
+ start_ts = events[0]["timestamp"]
127
+ lines.append("")
128
+ lines.append(f"[bold {GOLD}]Timeline[/bold {GOLD}]")
129
+ for etype, items in sorted(by_type.items(), key=lambda x: -len(x[1])):
130
+ lines.append("")
131
+ lines.append(f"[{PURPLE}]{etype}[/{PURPLE}] [{DIM}]({len(items)})[/{DIM}]")
132
+ for e in items:
133
+ ts = _offset(e["timestamp"], start_ts)
134
+ meta = json.loads(e.get("metadata") or "{}")
135
+ summary = _event_summary(etype, meta) or ""
136
+ lines.append(f" [{DIM}]{ts}[/{DIM}] [{TEXT}]{summary}[/{TEXT}]")
137
+
138
+ return "\n".join(lines)
139
+
140
+
51
141
  class LogApp(App[None]):
52
- """Session browser — navigate with ↑↓, push with p, view with enter."""
142
+ """Session browser — ↑↓ navigate, enter details, esc back/quit, p push."""
53
143
 
54
144
  TITLE = "methodproof — mp log"
55
145
  CSS = _CSS
@@ -95,6 +185,7 @@ class LogApp(App[None]):
95
185
 
96
186
  if self._sessions:
97
187
  self._update_preview(0)
188
+ table.focus()
98
189
 
99
190
  def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
100
191
  self._update_preview(event.cursor_row)
@@ -119,6 +210,8 @@ class LogApp(App[None]):
119
210
  f"[{DIM}]{dur} · {ev} events[/{DIM}]",
120
211
  f"[{DIM}]{sid}[/{DIM}]",
121
212
  "",
213
+ f"[{DIM}]enter: details · p: push · esc: quit[/{DIM}]",
214
+ "",
122
215
  ]
123
216
 
124
217
  if moments:
@@ -138,16 +231,17 @@ class LogApp(App[None]):
138
231
  def action_push_session(self) -> None:
139
232
  table = self.query_one(DataTable)
140
233
  row = table.cursor_row
141
- if row < len(self._sessions):
142
- sid = self._sessions[row].get("id", "")
143
- self.exit(("push", sid))
234
+ if row is None or row >= len(self._sessions):
235
+ return
236
+ sid = self._sessions[row].get("id", "")
237
+ self.exit(("push", sid))
144
238
 
145
239
  def action_view_session(self) -> None:
146
240
  table = self.query_one(DataTable)
147
241
  row = table.cursor_row
148
- if row < len(self._sessions):
149
- sid = self._sessions[row].get("id", "")
150
- self.exit(("view", sid))
242
+ if row is None or row >= len(self._sessions):
243
+ return
244
+ self.push_screen(DetailScreen(self._sessions[row]))
151
245
 
152
246
 
153
247
  def run() -> tuple[str, str] | None:
@@ -0,0 +1,156 @@
1
+ """Theme palette for the MethodProof TUI.
2
+
3
+ Two themes: **SHOMEN** (light) and **KINMYAKU** (dark). One is selected at
4
+ process startup via the ``MP_THEME`` environment variable (``shomen`` or
5
+ ``kinmyaku``); default is ``kinmyaku`` because most dev terminals run dark.
6
+
7
+ Call sites should import ``ACTIVE`` and read attributes on it
8
+ (``ACTIVE.accent``, ``ACTIVE.ai_input``) — intent, not hex. The uppercase
9
+ re-exports (``GOLD``, ``DIM``, …) are kept for legacy Rich markup and
10
+ resolve to the selected theme at import time.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from dataclasses import dataclass
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Theme:
20
+ name: str
21
+ # Surfaces
22
+ bg: str
23
+ surface: str
24
+ sidebar_bg: str
25
+ panel_bg: str
26
+ border: str
27
+ # Text
28
+ text: str
29
+ dim: str
30
+ # Brand node colors (what) — five-color palette from brand/
31
+ gold: str
32
+ gold_aged: str
33
+ gold_deep: str
34
+ gold_ember: str
35
+ red: str
36
+ green: str
37
+ purple: str
38
+ purple_muted: str
39
+ # Semantic roles (why) — every call site that means something should
40
+ # use one of these, not the raw brand names above
41
+ accent: str # primary interactive accent (gold dark / vermillion light)
42
+ ai_input: str # prompts, agent dispatch, tool calls
43
+ ai_output: str # completions, agent replies, tool results
44
+ human: str # file edits, terminal, git — structural engineer work
45
+ verify: str # tests, browser research, web lookups
46
+ moment: str # flagged moments, pivots, breakthroughs
47
+ # Cursor / selection highlight (list rows, focused controls)
48
+ cursor_bg: str
49
+ cursor_fg: str
50
+
51
+
52
+ # ── KINMYAKU — 金脈, "gold vein" (dark) ─────────────────────────────────
53
+ KINMYAKU = Theme(
54
+ name="kinmyaku",
55
+ bg="#12110f", surface="#1c1a18", sidebar_bg="#0a0908",
56
+ panel_bg="#0f0d0b", border="#2e2c29",
57
+ text="#e8e4de", dim="#8b8171",
58
+ gold="#c9a84c", gold_aged="#9a7b3a",
59
+ gold_deep="#6b5528", gold_ember="#3d3118",
60
+ red="#e85445",
61
+ green="#40d98c",
62
+ purple="#9b59b6", purple_muted="#6b2f7d",
63
+ accent="#c9a84c",
64
+ ai_input="#9b59b6", ai_output="#c9a84c", human="#e8e4de",
65
+ verify="#40d98c", moment="#e85445",
66
+ cursor_bg="#c9a84c", cursor_fg="#12110f",
67
+ )
68
+
69
+ # ── SHOMEN — 正面, "the front" (light) ──────────────────────────────────
70
+ # Gold is darkened for contrast on white. Vermillion is the primary accent
71
+ # per METHODPROOF.md: "In light mode, vermillion and the brand node colors
72
+ # are the only saturated colors."
73
+ SHOMEN = Theme(
74
+ name="shomen",
75
+ bg="#ffffff", surface="#fafaf7", sidebar_bg="#f5f4ef",
76
+ panel_bg="#f5f4ef", border="#d0ccc2",
77
+ text="#0a0a0a", dim="#6b6862",
78
+ gold="#8a6f2a", gold_aged="#6b5528",
79
+ gold_deep="#3d3118", gold_ember="#e8dfc5",
80
+ red="#d93326",
81
+ green="#2d7a42",
82
+ purple="#803794", purple_muted="#b89ac2",
83
+ accent="#d93326",
84
+ ai_input="#803794", ai_output="#8a6f2a", human="#0a0a0a",
85
+ verify="#2d7a42", moment="#d93326",
86
+ cursor_bg="#d93326", cursor_fg="#ffffff",
87
+ )
88
+
89
+
90
+ _THEMES = {"shomen": SHOMEN, "kinmyaku": KINMYAKU}
91
+ ACTIVE: Theme = _THEMES.get(os.environ.get("MP_THEME", "kinmyaku").strip().lower(), KINMYAKU)
92
+
93
+
94
+ # Legacy uppercase re-exports — bound to the selected theme. Kept so Rich
95
+ # markup in existing files (`[{GOLD}]…[/{GOLD}]`) keeps rendering. New code
96
+ # should use `ACTIVE.accent` / `ACTIVE.ai_output` instead of these.
97
+ GOLD = ACTIVE.gold
98
+ GREEN = ACTIVE.green
99
+ RED = ACTIVE.red
100
+ PURPLE = ACTIVE.purple
101
+ BG = ACTIVE.bg
102
+ BORDER = ACTIVE.border
103
+ TEXT = ACTIVE.text
104
+ DIM = ACTIVE.dim
105
+
106
+
107
+ BASE_CSS = f"""
108
+ Screen {{
109
+ background: {ACTIVE.bg};
110
+ color: {ACTIVE.text};
111
+ }}
112
+ Header {{
113
+ background: {ACTIVE.surface};
114
+ color: {ACTIVE.accent};
115
+ text-style: bold;
116
+ }}
117
+ Footer {{
118
+ background: {ACTIVE.surface};
119
+ color: {ACTIVE.dim};
120
+ }}
121
+ .panel {{
122
+ border: solid {ACTIVE.border};
123
+ background: {ACTIVE.panel_bg};
124
+ margin: 0 0 1 0;
125
+ padding: 1 2;
126
+ }}
127
+ .section-title {{
128
+ color: {ACTIVE.accent};
129
+ text-style: bold;
130
+ margin: 0 0 1 0;
131
+ }}
132
+ .row-label {{
133
+ width: 24;
134
+ color: {ACTIVE.text};
135
+ }}
136
+ .row-desc {{
137
+ color: {ACTIVE.dim};
138
+ }}
139
+ Rule {{
140
+ color: {ACTIVE.border};
141
+ margin: 1 0;
142
+ }}
143
+ DataTable > .datatable--cursor {{
144
+ background: {ACTIVE.cursor_bg};
145
+ color: {ACTIVE.cursor_fg};
146
+ text-style: bold;
147
+ }}
148
+ DataTable:focus > .datatable--cursor {{
149
+ background: {ACTIVE.cursor_bg};
150
+ color: {ACTIVE.cursor_fg};
151
+ text-style: bold;
152
+ }}
153
+ DataTable > .datatable--hover {{
154
+ background: {ACTIVE.surface};
155
+ }}
156
+ """
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.8.0"
3
+ version = "0.8.1"
4
4
  description = "See how you code. Capture and visualize your engineering process."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=43.0", "keyring>=25.0", "textual>=0.59", "rich>=13.7", "sqlcipher3>=0.6"]
@@ -1,106 +0,0 @@
1
- """Theme palette for MethodProof TUI — KINMYAKU (dark) mode.
2
-
3
- Colors derived from THEMES/METHODPROOF.md spec. Semantic roles encode
4
- meaning (ai_input, ai_output, human, verify, moment) so TUI code reads
5
- intent, not hex values.
6
- """
7
- from dataclasses import dataclass
8
-
9
-
10
- @dataclass(frozen=True)
11
- class Palette:
12
- # Brand accents
13
- gold: str
14
- gold_aged: str
15
- gold_deep: str
16
- gold_ember: str
17
- green: str
18
- green_muted: str
19
- red: str
20
- purple: str
21
- purple_muted: str
22
- # Surfaces
23
- bg: str
24
- surface: str
25
- panel_bg: str
26
- border: str
27
- sidebar_bg: str
28
- deep_bg: str
29
- purple_bg: str
30
- # Text
31
- text: str
32
- dim: str
33
- # Semantic roles (aliases for intent-driven lookup)
34
- ai_input: str
35
- ai_output: str
36
- human: str
37
- verify: str
38
- moment: str
39
-
40
-
41
- KINMYAKU = Palette(
42
- gold="#c9a84c", gold_aged="#9a7b3a", gold_deep="#6b5528", gold_ember="#3d3118",
43
- green="#40d98c", green_muted="#2a6b45",
44
- red="#e85445",
45
- purple="#9b59b6", purple_muted="#6b2f7d",
46
- bg="#12110f", surface="#1c1a18", panel_bg="#0f0d0b", border="#2e2c29",
47
- sidebar_bg="#0a0908", deep_bg="#050403", purple_bg="#200a26",
48
- text="#e8e4de", dim="#8b8171",
49
- ai_input="#9b59b6", ai_output="#c9a84c", human="#e8e4de",
50
- verify="#40d98c", moment="#e85445",
51
- )
52
-
53
- ACTIVE = KINMYAKU
54
-
55
- # Backward-compatible re-exports — all existing imports keep working.
56
- GOLD = ACTIVE.gold
57
- GREEN = ACTIVE.green
58
- RED = ACTIVE.red
59
- PURPLE = ACTIVE.purple
60
- NAVY = "#192a56" # brand-only, not in palette
61
- BG = ACTIVE.bg
62
- BAR = ACTIVE.surface
63
- PANEL_BG = ACTIVE.panel_bg
64
- BORDER = ACTIVE.border
65
- TEXT = ACTIVE.text
66
- DIM = ACTIVE.dim
67
- SIDEBAR_BG = ACTIVE.sidebar_bg
68
- DEEP_BG = ACTIVE.deep_bg
69
- PURPLE_BG = ACTIVE.purple_bg
70
-
71
- BASE_CSS = f"""
72
- Screen {{
73
- background: {ACTIVE.bg};
74
- }}
75
- Header {{
76
- background: {ACTIVE.surface};
77
- color: {ACTIVE.gold};
78
- text-style: bold;
79
- }}
80
- Footer {{
81
- background: {ACTIVE.surface};
82
- color: {ACTIVE.dim};
83
- }}
84
- .panel {{
85
- border: solid {ACTIVE.border};
86
- background: {ACTIVE.panel_bg};
87
- margin: 0 0 1 0;
88
- padding: 1 2;
89
- }}
90
- .section-title {{
91
- color: {ACTIVE.gold};
92
- text-style: bold;
93
- margin: 0 0 1 0;
94
- }}
95
- .row-label {{
96
- width: 24;
97
- color: {ACTIVE.text};
98
- }}
99
- .row-desc {{
100
- color: {ACTIVE.dim};
101
- }}
102
- Rule {{
103
- color: {ACTIVE.border};
104
- margin: 1 0;
105
- }}
106
- """
File without changes
File without changes
File without changes
File without changes