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.
- methodproof-0.8.1/.code-review-graph/.gitignore +3 -0
- methodproof-0.8.1/.code-review-graph/graph.db +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/CHANGELOG.md +19 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/PKG-INFO +1 -1
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/__init__.py +1 -1
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/base.py +29 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/terminal.py +45 -5
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/watcher.py +62 -19
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/cli.py +0 -2
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/claude_code.py +2 -3
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/log.py +106 -12
- methodproof-0.8.1/methodproof/tui/theme.py +156 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/pyproject.toml +1 -1
- methodproof-0.8.0/methodproof/tui/theme.py +0 -106
- {methodproof-0.8.0 → methodproof-0.8.1}/.github/workflows/ci.yml +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/.gitignore +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/LICENSE +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/README.md +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/__main__.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/_daemon.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/agents/music.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/analysis.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/binding.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/bip39.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/bridge.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/config.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/crypto.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/e2e.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/graph.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hook.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/install.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/integrity.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/kdf.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/keychain.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/live.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/lock.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/mcp.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/migrate_db.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/proxy.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/repos.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/store.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/sync.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/__init__.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/consent.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/init.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/login_success.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/review.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/start.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/tui/status.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/viewer.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/methodproof/wordlist.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/test_windows_compat.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/__init__.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/conftest.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_analysis.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_auth.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_config.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_helpers.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_session.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_share.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_start.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_cli_update.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_e2e_integration.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_graph.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_hooks.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_live.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_profiles.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_repos.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_security.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_store.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_sync.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_viewer.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/tests/test_wrappers.py +0 -0
- {methodproof-0.8.0 → methodproof-0.8.1}/uv.lock +0 -0
|
Binary file
|
|
@@ -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
|
|
@@ -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
|
|
44
|
-
|
|
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", "")
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
279
|
-
["git", "-C", watch_dir, "diff-tree", "--no-commit-id", "-r", "--name-
|
|
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
|
|
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
|
|
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":
|
|
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":
|
|
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
|
|
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 {
|
|
20
|
+
border-right: solid {ACTIVE.border};
|
|
18
21
|
}}
|
|
19
22
|
#preview-pane {{
|
|
20
23
|
width: 1fr;
|
|
21
24
|
padding: 1 2;
|
|
22
|
-
background:
|
|
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
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|