methodproof 0.7.32__tar.gz → 0.7.34__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 (88) hide show
  1. {methodproof-0.7.32 → methodproof-0.7.34}/CHANGELOG.md +15 -0
  2. {methodproof-0.7.32 → methodproof-0.7.34}/PKG-INFO +1 -1
  3. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/cli.py +3 -1
  4. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/store.py +8 -0
  5. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/sync.py +11 -3
  6. methodproof-0.7.34/methodproof/tui/start.py +436 -0
  7. methodproof-0.7.34/methodproof/tui/theme.py +106 -0
  8. {methodproof-0.7.32 → methodproof-0.7.34}/pyproject.toml +1 -1
  9. methodproof-0.7.32/methodproof/tui/start.py +0 -212
  10. methodproof-0.7.32/methodproof/tui/theme.py +0 -53
  11. {methodproof-0.7.32 → methodproof-0.7.34}/.github/workflows/ci.yml +0 -0
  12. {methodproof-0.7.32 → methodproof-0.7.34}/.gitignore +0 -0
  13. {methodproof-0.7.32 → methodproof-0.7.34}/LICENSE +0 -0
  14. {methodproof-0.7.32 → methodproof-0.7.34}/README.md +0 -0
  15. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/__init__.py +0 -0
  16. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/__main__.py +0 -0
  17. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/_daemon.py +0 -0
  18. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/__init__.py +0 -0
  19. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/base.py +0 -0
  20. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/music.py +0 -0
  21. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/terminal.py +0 -0
  22. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/watcher.py +0 -0
  23. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/analysis.py +0 -0
  24. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/binding.py +0 -0
  25. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/bip39.py +0 -0
  26. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/bridge.py +0 -0
  27. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/config.py +0 -0
  28. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/crypto.py +0 -0
  29. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/e2e.py +0 -0
  30. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/graph.py +0 -0
  31. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hook.py +0 -0
  32. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/__init__.py +0 -0
  33. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/claude_code.py +0 -0
  34. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/claude_code.sh +0 -0
  35. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/cline_hook.sh +0 -0
  36. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/codex_hook.sh +0 -0
  37. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/gemini_hook.sh +0 -0
  38. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/install.py +0 -0
  39. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/kiro_hook.sh +0 -0
  40. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/mcp_register.py +0 -0
  41. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/openclaw/HOOK.md +0 -0
  42. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/openclaw/handler.ts +0 -0
  43. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/openclaw_install.py +0 -0
  44. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/opencode_plugin.js +0 -0
  45. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/wrappers.py +0 -0
  46. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/integrity.py +0 -0
  47. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/kdf.py +0 -0
  48. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/keychain.py +0 -0
  49. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/live.py +0 -0
  50. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/lock.py +0 -0
  51. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/mcp.py +0 -0
  52. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/migrate_db.py +0 -0
  53. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/proxy.py +0 -0
  54. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/proxy_daemon.py +0 -0
  55. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/repos.py +0 -0
  56. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/skills/methodproof/SKILL.md +0 -0
  57. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/__init__.py +0 -0
  58. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/consent.py +0 -0
  59. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/init.py +0 -0
  60. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/log.py +0 -0
  61. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/login_success.py +0 -0
  62. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/review.py +0 -0
  63. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/status.py +0 -0
  64. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/viewer.py +0 -0
  65. {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/wordlist.py +0 -0
  66. {methodproof-0.7.32 → methodproof-0.7.34}/test_windows_compat.py +0 -0
  67. {methodproof-0.7.32 → methodproof-0.7.34}/tests/__init__.py +0 -0
  68. {methodproof-0.7.32 → methodproof-0.7.34}/tests/conftest.py +0 -0
  69. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_analysis.py +0 -0
  70. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_auth.py +0 -0
  71. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_config.py +0 -0
  72. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_helpers.py +0 -0
  73. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_session.py +0 -0
  74. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_share.py +0 -0
  75. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_start.py +0 -0
  76. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_update.py +0 -0
  77. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_e2e_integration.py +0 -0
  78. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_graph.py +0 -0
  79. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_hooks.py +0 -0
  80. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_live.py +0 -0
  81. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_openclaw_hooks.py +0 -0
  82. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_profiles.py +0 -0
  83. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_security.py +0 -0
  84. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_store.py +0 -0
  85. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_sync.py +0 -0
  86. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_viewer.py +0 -0
  87. {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_wrappers.py +0 -0
  88. {methodproof-0.7.32 → methodproof-0.7.34}/uv.lock +0 -0
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.34] — 2026-04-12
4
+
5
+ ### Fixed
6
+ - **`mp push` after mid-session push was permanently blocked** — pushing an active session marked it as synced. After stopping, additional events existed locally but could never be uploaded. Now: if a session was pushed while active and is now completed, `mp push` automatically re-pushes the full session. For already-completed sessions, `mp push --force` re-uploads all events.
7
+
8
+ ## [0.7.33] — 2026-04-12
9
+
10
+ ### Added
11
+ - **KINMYAKU theme palette**: `Palette` frozen dataclass replaces flat hex constants in `tui/theme.py`. Dark-adjusted colors from METHODPROOF.md spec — purple, green, red, dim all shifted for proper contrast on dark backgrounds. Five semantic roles (`ai_input`, `ai_output`, `human`, `verify`, `moment`) so TUI code reads intent, not hex. Backward-compatible re-exports — all existing imports unchanged.
12
+ - **Rich structural formatting for all 42 event types**: `mp start` feed now shows curated one-liners for every event type. `tool_call` → `Edit app/core/auth.py`, `tool_result` → `Edit ✓`, `agent_launch` → `explorer spawned`, etc. Previously only 8 types had formatters; the rest showed raw key dumps or nothing.
13
+ - **Causal chain tree indentation**: Prompt→tool_call→tool_result chains render with Unicode box-drawing characters (`├`, `│`, `└`) in gold-ember. Non-AI events (file edits, tests, commits) appear at root level. Mirrors the Neo4j graph structure during live capture.
14
+ - **Journal mode content enrichment**: When journal is ON, a second dim line appears below events carrying content fields — truncated prompt text, tool results, diffs. Quoted and indented with a `│` gutter. When journal is OFF, these lines don't exist.
15
+ - **Enriched session bar**: Now shows running event count and a gold `J` badge when journal mode is active.
16
+ - **Moment alert wiring**: `_show_moment()` (previously dead code) now fires when known moment types appear in the event stream.
17
+
3
18
  ## [0.7.32] — 2026-04-12
4
19
 
5
20
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.32
3
+ Version: 0.7.34
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
@@ -1793,7 +1793,8 @@ def cmd_push(args: argparse.Namespace) -> None:
1793
1793
  print("No sessions to push.")
1794
1794
  sys.exit(1)
1795
1795
  from methodproof.sync import push
1796
- remote_id = push(sid, cfg["token"], cfg["api_url"])
1796
+ force = getattr(args, "force", False)
1797
+ remote_id = push(sid, cfg["token"], cfg["api_url"], force=force)
1797
1798
  app = _app_url(cfg["api_url"])
1798
1799
  print(f"Pushed {sid[:8]} → {cfg['api_url']} (private).")
1799
1800
  print(f" View: {app}/personal/sessions/{remote_id}")
@@ -2222,6 +2223,7 @@ def main() -> None:
2222
2223
  sw.add_argument("account", nargs="?", help="Email or account ID prefix")
2223
2224
  pu = sub.add_parser("push", help="Upload privately to your account")
2224
2225
  pu.add_argument("session_id", nargs="?")
2226
+ pu.add_argument("--force", "-f", action="store_true", help="Re-upload all events (replaces previous push)")
2225
2227
  pu.add_argument("--local", action="store_true", help="Push to local dev API (localhost:8000)")
2226
2228
  tg = sub.add_parser("tag", help="Tag a session")
2227
2229
  tg.add_argument("session_id", help="Session ID (prefix ok)")
@@ -389,3 +389,11 @@ def mark_synced(session_id: str, remote_id: str) -> None:
389
389
  (remote_id, session_id),
390
390
  )
391
391
  _db().commit()
392
+
393
+
394
+ def clear_sync(session_id: str) -> None:
395
+ _db().execute(
396
+ "UPDATE sessions SET synced = 0, remote_id = NULL WHERE id = ?",
397
+ (session_id,),
398
+ )
399
+ _db().commit()
@@ -84,14 +84,22 @@ def _request(
84
84
  raise SystemExit(f"API error {exc.code}: {detail}") from None
85
85
 
86
86
 
87
- def push(session_id: str, token: str, api_url: str) -> str:
87
+ def push(session_id: str, token: str, api_url: str, force: bool = False) -> str:
88
88
  """Upload a local session to the platform."""
89
89
  session = store.get_session(session_id)
90
90
  if not session:
91
91
  raise SystemExit(f"Session not found: {session_id}")
92
92
  if session["synced"]:
93
- print(f"Already synced: {session_id[:8]}")
94
- return session.get("remote_id", "")
93
+ if force:
94
+ print(f"Force re-push: {session_id[:8]}")
95
+ store.clear_sync(session_id)
96
+ elif not session.get("completed_at"):
97
+ print(f"Session was pushed while still active — re-pushing complete session.")
98
+ store.clear_sync(session_id)
99
+ else:
100
+ print(f"Already synced: {session_id[:8]}")
101
+ print(f" (Use `mp push --force` to re-upload all events)")
102
+ return session.get("remote_id", "")
95
103
 
96
104
  # Create remote session (include binding + device_id if available)
97
105
  print("Creating remote session...", end=" ", flush=True)
@@ -0,0 +1,436 @@
1
+ """Textual TUI for mp start — live session event feed.
2
+
3
+ Four display layers:
4
+ B — Rich structural formatting for all 42 event types
5
+ E — Causal chain tree indentation (prompt→tool→result)
6
+ A — Journal mode content enrichment (second dim line)
7
+ D — Enriched session bar (badges, event count)
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ from datetime import datetime, UTC
14
+
15
+ from textual.app import App, ComposeResult
16
+ from textual.binding import Binding
17
+ from textual.containers import Horizontal, Vertical
18
+ from textual.reactive import reactive
19
+ from textual.widgets import Footer, Header, RichLog, Static
20
+ from methodproof import config, store
21
+ from methodproof.agents.base import log
22
+ from methodproof.tui.theme import ACTIVE, BASE_CSS
23
+
24
+ # ── CSS ──────────────────────────────────────────────────────────────
25
+ _CSS = BASE_CSS + f"""
26
+ #session-bar {{
27
+ background: {ACTIVE.surface};
28
+ height: 1;
29
+ padding: 0 2;
30
+ color: {ACTIVE.dim};
31
+ }}
32
+ #feed {{
33
+ width: 3fr;
34
+ border-right: solid {ACTIVE.border};
35
+ padding: 0 1;
36
+ }}
37
+ #sidebar {{
38
+ width: 22;
39
+ padding: 1 2;
40
+ background: {ACTIVE.sidebar_bg};
41
+ }}
42
+ .sidebar-title {{
43
+ color: {ACTIVE.gold};
44
+ text-style: bold;
45
+ margin: 0 0 1 0;
46
+ }}
47
+ .stat-row {{
48
+ color: {ACTIVE.dim};
49
+ height: 1;
50
+ }}
51
+ #moment-alert {{
52
+ background: {ACTIVE.gold_ember};
53
+ border: solid {ACTIVE.gold_deep};
54
+ margin: 1 1;
55
+ padding: 0 1;
56
+ height: 3;
57
+ display: none;
58
+ }}
59
+ #moment-alert.visible {{
60
+ display: block;
61
+ }}
62
+ """
63
+
64
+ _POLL_INTERVAL = 0.5
65
+
66
+ # ── Layer B: Semantic color roles ──────��─────────────────────────────
67
+ _EVENT_ROLE: dict[str, str] = {
68
+ # AI input (purple)
69
+ "llm_prompt": "ai_input", "agent_prompt": "ai_input",
70
+ "user_prompt": "ai_input", "tool_call": "ai_input",
71
+ "agent_launch": "ai_input", "task_start": "ai_input",
72
+ "permission_request": "ai_input",
73
+ "agent_tool_dispatch": "ai_input", "agent_skill_invoke": "ai_input",
74
+ "claude_session_start": "ai_input", "codex_session_start": "ai_input",
75
+ "gemini_session_start": "ai_input", "kiro_session_start": "ai_input",
76
+ # AI output (gold)
77
+ "llm_completion": "ai_output", "agent_completion": "ai_output",
78
+ "tool_result": "ai_output", "tool_failure": "ai_output",
79
+ "agent_complete": "ai_output", "task_end": "ai_output",
80
+ "agent_tool_result": "ai_output",
81
+ "agent_turn_end": "ai_output", "agent_turn_error": "ai_output",
82
+ "claude_session_end": "ai_output", "codex_session_end": "ai_output",
83
+ "gemini_session_end": "ai_output", "kiro_session_end": "ai_output",
84
+ # Human structural (cream/ink)
85
+ "file_edit": "human", "file_create": "human", "file_delete": "human",
86
+ "terminal_cmd": "human", "git_commit": "human",
87
+ "cwd_changed": "human",
88
+ "worktree_create": "human", "worktree_remove": "human",
89
+ # Verification (green)
90
+ "test_run": "verify",
91
+ "browser_visit": "verify", "browser_search": "verify",
92
+ "web_search": "verify", "web_visit": "verify",
93
+ }
94
+ # Everything else falls to "dim"
95
+
96
+ _MOMENT_TYPES = {
97
+ "rapid_iteration", "test_driven", "git_discipline",
98
+ "focused_session", "breakthrough", "approach_pivot",
99
+ }
100
+
101
+
102
+ def _event_color(etype: str) -> str:
103
+ role = _EVENT_ROLE.get(etype, "dim")
104
+ return getattr(ACTIVE, role, ACTIVE.dim)
105
+
106
+
107
+ # ── Layer E: Causal chain tree tracker ───��───────────────────────────
108
+ class _TreeTracker:
109
+ """Track prompt→tool_call→tool_result causal chains."""
110
+
111
+ _OPENERS = {
112
+ "user_prompt", "agent_launch", "task_start",
113
+ "claude_session_start", "codex_session_start",
114
+ "gemini_session_start", "kiro_session_start",
115
+ }
116
+ _INNERS = {
117
+ "tool_call", "tool_result", "tool_failure",
118
+ "permission_request", "permission_denied",
119
+ "agent_tool_dispatch", "agent_tool_result", "agent_skill_invoke",
120
+ "context_compact_start", "context_compact_end",
121
+ "cwd_changed", "mcp_elicitation", "mcp_elicitation_result",
122
+ "worktree_create", "worktree_remove",
123
+ }
124
+ _CLOSERS = {
125
+ "agent_turn_end", "agent_turn_error", "agent_complete", "task_end",
126
+ "claude_session_end", "codex_session_end",
127
+ "gemini_session_end", "kiro_session_end",
128
+ }
129
+
130
+ def __init__(self) -> None:
131
+ self._in_chain = False
132
+
133
+ def feed(self, etype: str) -> str:
134
+ if etype in self._OPENERS:
135
+ self._in_chain = True
136
+ return " "
137
+ if self._in_chain:
138
+ if etype in self._CLOSERS:
139
+ self._in_chain = False
140
+ return "└ "
141
+ if etype in self._INNERS:
142
+ return "├ "
143
+ # Non-chain event while in chain — implicit close
144
+ self._in_chain = False
145
+ return " "
146
+ return " "
147
+
148
+
149
+ # ── Layer B: Structural metadata formatters ──────────────────────────
150
+ def _fmt_meta(ev: dict) -> str:
151
+ etype = ev.get("type", "")
152
+ meta = ev.get("metadata") or {}
153
+ if not isinstance(meta, dict):
154
+ meta = {}
155
+
156
+ # File events
157
+ if etype in ("file_edit", "file_create", "file_delete"):
158
+ path = meta.get("path") or meta.get("file_path", "")
159
+ added = meta.get("lines_added") or meta.get("line_delta", "")
160
+ removed = meta.get("lines_removed", "")
161
+ delta = f"+{added}" if added else ""
162
+ if removed:
163
+ delta += f" -{removed}"
164
+ return f"{path} {delta}".strip()
165
+
166
+ # Terminal
167
+ if etype == "terminal_cmd":
168
+ cmd = (meta.get("command") or "")[:50]
169
+ ec = meta.get("exit_code", 0)
170
+ return f"{cmd} {'✓' if ec == 0 else f'✗ exit {ec}'}"
171
+
172
+ # Git
173
+ if etype == "git_commit":
174
+ h = (meta.get("hash") or "")[:7]
175
+ msg = (meta.get("message") or "")[:40]
176
+ return f"{h} {msg}".strip()
177
+
178
+ # Tests
179
+ if etype == "test_run":
180
+ fw = meta.get("framework", "")
181
+ p, f = meta.get("passed", 0), meta.get("failed", 0)
182
+ result = f"{p}✓ {f}✗" if f else f"{p}✓"
183
+ return f"{fw} {result}".strip()
184
+
185
+ # LLM / agent prompts
186
+ if etype in ("llm_prompt", "agent_prompt"):
187
+ tokens = meta.get("prompt_tokens") or meta.get("input_length", "")
188
+ return f"{tokens} tokens" if tokens else ""
189
+ if etype in ("llm_completion", "agent_completion"):
190
+ tokens = meta.get("completion_tokens") or meta.get("output_length", "")
191
+ dur = ev.get("duration_ms", "")
192
+ return f"{tokens} tokens {dur}ms" if tokens else ""
193
+
194
+ # Hook lifecycle — user prompt
195
+ if etype == "user_prompt":
196
+ length = meta.get("prompt_length", "")
197
+ return f"{length} chars" if length else ""
198
+
199
+ # Hook lifecycle — tool call / result
200
+ if etype == "tool_call":
201
+ name = meta.get("tool_name", "")
202
+ preview = (meta.get("tool_input_preview") or "")[:60]
203
+ return f"{name} {preview}".strip()
204
+ if etype == "tool_result":
205
+ name = meta.get("tool_name", "")
206
+ ok = "✓" if meta.get("success", True) else "✗"
207
+ return f"{name} {ok}"
208
+ if etype == "tool_failure":
209
+ name = meta.get("tool_name", "")
210
+ err = (meta.get("error") or "")[:40]
211
+ return f"{name} ✗ {err}".strip()
212
+
213
+ # Hook lifecycle — agents
214
+ if etype == "agent_launch":
215
+ return f"{meta.get('agent_type', '')} spawned"
216
+ if etype == "agent_complete":
217
+ return f"{meta.get('agent_type', '')} done"
218
+
219
+ # Hook lifecycle — tasks
220
+ if etype == "task_start":
221
+ return f"task {(meta.get('task_id') or '')[:8]}"
222
+ if etype == "task_end":
223
+ return f"task {(meta.get('task_id') or '')[:8]} done"
224
+
225
+ # Hook lifecycle — permissions
226
+ if etype == "permission_request":
227
+ return f"{meta.get('tool_name', '')} awaiting"
228
+ if etype == "permission_denied":
229
+ return f"{meta.get('tool_name', '')} denied"
230
+
231
+ # Session lifecycle
232
+ if etype.endswith("_session_start"):
233
+ return "session opened"
234
+ if etype.endswith("_session_end"):
235
+ return "session ended"
236
+
237
+ # Context / system
238
+ if etype == "context_compact_start":
239
+ return "compacting..."
240
+ if etype == "context_compact_end":
241
+ return "compacted"
242
+ if etype in ("agent_turn_end", "agent_turn_error"):
243
+ err = (meta.get("error") or "")[:40]
244
+ return err if err else ""
245
+ if etype in ("cwd_changed",):
246
+ return meta.get("cwd", "")
247
+ if etype in ("worktree_create", "worktree_remove"):
248
+ return meta.get("worktree_path", "")
249
+
250
+ # Music
251
+ if etype == "music_playing":
252
+ artist = meta.get("artist", "")
253
+ track = meta.get("track", "")
254
+ return f"{artist} — {track}" if artist else track
255
+
256
+ # Browser
257
+ if etype.startswith("browser_") or etype.startswith("web_"):
258
+ return (meta.get("url") or meta.get("query") or "")[:50]
259
+
260
+ # Environment
261
+ if etype == "environment_profile":
262
+ return f"{meta.get('tool_count', '?')} tools"
263
+
264
+ # Fallback: first 3 metadata keys
265
+ keys = list(meta.keys())[:3]
266
+ return ", ".join(f"{k}={meta[k]}" for k in keys) if keys else ""
267
+
268
+
269
+ # ── Layer A: Journal content enrichment ──���───────────────────────────
270
+ def _journal_line(ev: dict, journal_mode: bool) -> str | None:
271
+ """Return truncated content preview when journal mode is ON."""
272
+ if not journal_mode:
273
+ return None
274
+ meta = ev.get("metadata") or {}
275
+ if not isinstance(meta, dict):
276
+ return None
277
+ etype = ev.get("type", "")
278
+ for jetype, field in config.JOURNAL_CONTENT_FIELDS:
279
+ if jetype == etype and field in meta:
280
+ content = str(meta[field]).replace("\n", " ")[:120]
281
+ return content if content else None
282
+ return None
283
+
284
+
285
+ # ── App ──────────────────────────────────��───────────────────────────
286
+ class StartApp(App[None]):
287
+ """Live session view — tails the active session's events."""
288
+
289
+ TITLE = "methodproof — mp start"
290
+ CSS = _CSS
291
+ BINDINGS = [
292
+ Binding("q", "stop_session", "stop"),
293
+ Binding("p", "pause", "pause"),
294
+ Binding("l", "toggle_live", "toggle live"),
295
+ Binding("escape", "quit", "quit"),
296
+ ]
297
+
298
+ _elapsed: reactive[int] = reactive(0)
299
+ _paused: reactive[bool] = reactive(False)
300
+ _event_count: int = 0
301
+ _last_seen_id: str = ""
302
+
303
+ def __init__(self, session_id: str, session: dict) -> None:
304
+ super().__init__()
305
+ self._session_id = session_id
306
+ self._session = session
307
+ self._stats: dict[str, int] = {}
308
+ self._start_time = session.get("created_at", time.time())
309
+ self._tree = _TreeTracker()
310
+ self._journal_mode = False
311
+
312
+ def compose(self) -> ComposeResult:
313
+ yield Header(show_clock=False)
314
+ yield Static("", id="session-bar", markup=True)
315
+ with Horizontal():
316
+ with Vertical(id="feed-col"):
317
+ yield RichLog(id="feed", highlight=True, markup=True, wrap=False)
318
+ yield Static("", id="moment-alert", markup=True)
319
+ with Vertical(id="sidebar"):
320
+ yield Static("Stats", classes="sidebar-title")
321
+ yield Static("", id="stats-content", markup=True)
322
+ yield Footer()
323
+
324
+ def on_mount(self) -> None:
325
+ import base64
326
+ cfg = config.load()
327
+ self._journal_mode = cfg.get("journal_mode", False)
328
+ token = cfg.get("token", "")
329
+ try:
330
+ payload = token.split(".")[1] + "=="
331
+ claims = json.loads(base64.urlsafe_b64decode(payload))
332
+ except Exception as exc:
333
+ log("warning", "tui.jwt_decode.failed", error=str(exc))
334
+ claims = {}
335
+ self._account_type = (claims.get("account_type") or "free").capitalize()
336
+ self._tick_timer()
337
+ self.set_interval(_POLL_INTERVAL, self._poll_events)
338
+ self.set_interval(1.0, self._tick_timer)
339
+
340
+ # ── Layer D: Session bar ─────────────────────────────────────
341
+ def _tick_timer(self) -> None:
342
+ if self._paused:
343
+ return
344
+ P = ACTIVE
345
+ elapsed = int(time.time() - self._start_time)
346
+ h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
347
+ sid = self._session_id[:8]
348
+ watch_dir = self._session.get("watch_dir", "?")
349
+ journal = f" [{P.gold}]J[/{P.gold}]" if self._journal_mode else ""
350
+ ev = f" {self._event_count} ev" if self._event_count else ""
351
+ self.query_one("#session-bar", Static).update(
352
+ f" session: [{P.gold}]{sid}[/{P.gold}] · {watch_dir}"
353
+ f" · [{P.green}]●[/{P.green}] {h:02d}:{m:02d}:{s:02d}"
354
+ f"{ev}{journal} · [{P.purple}]{self._account_type}[/{P.purple}]"
355
+ )
356
+
357
+ # ── Event poll: Layers B + E + A ��────────────────────────────
358
+ def _poll_events(self) -> None:
359
+ if self._paused:
360
+ return
361
+ try:
362
+ events = store.get_session_events(
363
+ self._session_id, after_id=self._last_seen_id,
364
+ )
365
+ except Exception as exc:
366
+ log("warning", "tui.poll_events.failed", error=str(exc))
367
+ return
368
+
369
+ P = ACTIVE
370
+ feed = self.query_one(RichLog)
371
+ for ev in events:
372
+ self._last_seen_id = ev.get("id", self._last_seen_id)
373
+ etype = ev.get("type", "event")
374
+ color = _event_color(etype)
375
+ ts = datetime.fromtimestamp(
376
+ ev.get("ts", time.time()), tz=UTC,
377
+ ).strftime("%H:%M:%S")
378
+ prefix = self._tree.feed(etype)
379
+ meta = _fmt_meta(ev)
380
+
381
+ # Layer B + E: structural line with tree prefix
382
+ feed.write(
383
+ f"[{P.dim}]{ts}[/{P.dim}] "
384
+ f"[{P.gold_ember}]{prefix}[/{P.gold_ember}]"
385
+ f"[{color}]{etype:<18}[/{color}] "
386
+ f"[{P.dim}]{meta}[/{P.dim}]"
387
+ )
388
+ # Layer A: journal content enrichment
389
+ jline = _journal_line(ev, self._journal_mode)
390
+ if jline:
391
+ feed.write(
392
+ f" [{P.gold_ember}]│[/{P.gold_ember}] "
393
+ f"[{P.dim}]\"{jline}\"[/{P.dim}]"
394
+ )
395
+
396
+ self._stats[etype] = self._stats.get(etype, 0) + 1
397
+ self._event_count += 1
398
+
399
+ # Moment detection
400
+ if etype in _MOMENT_TYPES:
401
+ m = ev.get("metadata") or {}
402
+ detail = m.get("detail", m.get("description", etype))
403
+ self._show_moment(etype, str(detail)[:60])
404
+
405
+ if events:
406
+ self._refresh_stats()
407
+
408
+ def _refresh_stats(self) -> None:
409
+ lines = []
410
+ for etype, count in sorted(self._stats.items(), key=lambda x: -x[1])[:10]:
411
+ color = _event_color(etype)
412
+ lines.append(
413
+ f"[{ACTIVE.dim}]{etype:<14}[/{ACTIVE.dim}] [{color}]{count}[/{color}]"
414
+ )
415
+ self.query_one("#stats-content", Static).update("\n".join(lines))
416
+
417
+ def _show_moment(self, mtype: str, detail: str) -> None:
418
+ P = ACTIVE
419
+ alert = self.query_one("#moment-alert", Static)
420
+ alert.update(f" [{P.moment}]⚡ {mtype}[/{P.moment}] [{P.dim}]{detail}[/{P.dim}]")
421
+ alert.add_class("visible")
422
+ self.set_timer(4.0, lambda: alert.remove_class("visible"))
423
+
424
+ def action_stop_session(self) -> None:
425
+ self.exit(None)
426
+
427
+ def action_pause(self) -> None:
428
+ self._paused = not self._paused
429
+
430
+ def action_toggle_live(self) -> None:
431
+ pass # handled by caller in cli.py
432
+
433
+
434
+ def run(session_id: str, session: dict) -> None:
435
+ """Launch the live session view for the given session."""
436
+ StartApp(session_id, session).run()
@@ -0,0 +1,106 @@
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
+ """
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.7.32"
3
+ version = "0.7.34"
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"]
@@ -1,212 +0,0 @@
1
- """Textual TUI for mp start — live session event feed."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import time
6
- from datetime import datetime, UTC
7
-
8
- from textual.app import App, ComposeResult
9
- from textual.binding import Binding
10
- from textual.containers import Horizontal, Vertical
11
- from textual.reactive import reactive
12
- from textual.widgets import Footer, Header, RichLog, Static
13
- from methodproof import config, store
14
- from methodproof.tui.theme import BASE_CSS, BORDER, DIM, GOLD, GREEN, PURPLE, RED, TEXT
15
-
16
- _CSS = BASE_CSS + f"""
17
- #session-bar {{
18
- background: #1c1a18;
19
- height: 1;
20
- padding: 0 2;
21
- color: {DIM};
22
- }}
23
- #feed {{
24
- width: 3fr;
25
- border-right: solid {BORDER};
26
- padding: 0 1;
27
- }}
28
- #sidebar {{
29
- width: 22;
30
- padding: 1 2;
31
- background: #0a0908;
32
- }}
33
- .sidebar-title {{
34
- color: {GOLD};
35
- text-style: bold;
36
- margin: 0 0 1 0;
37
- }}
38
- .stat-row {{
39
- color: {DIM};
40
- height: 1;
41
- }}
42
- #moment-alert {{
43
- background: #1a1408;
44
- border: solid #3a2e10;
45
- margin: 1 1;
46
- padding: 0 1;
47
- height: 3;
48
- display: none;
49
- }}
50
- #moment-alert.visible {{
51
- display: block;
52
- }}
53
- """
54
-
55
- _EVENT_COLORS = {
56
- "file_edit": GREEN, "file_create": GREEN, "file_delete": RED,
57
- "terminal_cmd": TEXT, "test_run": GOLD, "git_commit": GOLD,
58
- "llm_prompt": PURPLE, "llm_completion": PURPLE,
59
- "agent_prompt": PURPLE, "agent_completion": PURPLE,
60
- "browser_visit": DIM, "browser_search": DIM,
61
- "music_playing": DIM,
62
- }
63
-
64
- _POLL_INTERVAL = 0.5 # seconds between store polls
65
-
66
-
67
- class StartApp(App[None]):
68
- """Live session view — tails the active session's events."""
69
-
70
- TITLE = "methodproof — mp start"
71
- CSS = _CSS
72
- BINDINGS = [
73
- Binding("q", "stop_session", "stop"),
74
- Binding("p", "pause", "pause"),
75
- Binding("l", "toggle_live", "toggle live"),
76
- Binding("escape", "quit", "quit"),
77
- ]
78
-
79
- _elapsed: reactive[int] = reactive(0)
80
- _paused: reactive[bool] = reactive(False)
81
- _event_count: int = 0
82
- _last_seen_id: str = ""
83
-
84
- def __init__(self, session_id: str, session: dict) -> None:
85
- super().__init__()
86
- self._session_id = session_id
87
- self._session = session
88
- self._stats: dict[str, int] = {}
89
- self._start_time = session.get("created_at", time.time())
90
-
91
- def compose(self) -> ComposeResult:
92
- yield Header(show_clock=False)
93
- watch_dir = self._session.get("watch_dir", "?")
94
- sid = self._session_id[:8]
95
- yield Static(
96
- f" session: [{GOLD}]{sid}[/{GOLD}] · {watch_dir} · [{GREEN}]●[/{GREEN}] 00:00:00",
97
- id="session-bar",
98
- markup=True,
99
- )
100
- with Horizontal():
101
- with Vertical(id="feed-col"):
102
- yield RichLog(id="feed", highlight=True, markup=True, wrap=False)
103
- yield Static("", id="moment-alert", markup=True)
104
- with Vertical(id="sidebar"):
105
- yield Static("Stats", classes="sidebar-title")
106
- yield Static("", id="stats-content", markup=True)
107
- yield Footer()
108
-
109
- def on_mount(self) -> None:
110
- import base64
111
- cfg = config.load()
112
- token = cfg.get("token", "")
113
- try:
114
- payload = token.split(".")[1] + "=="
115
- claims = json.loads(base64.urlsafe_b64decode(payload))
116
- except Exception:
117
- claims = {}
118
- self._account_type = (claims.get("account_type") or "free").capitalize()
119
- self.set_interval(_POLL_INTERVAL, self._poll_events)
120
- self.set_interval(1.0, self._tick_timer)
121
-
122
- def _tick_timer(self) -> None:
123
- if self._paused:
124
- return
125
- elapsed = int(time.time() - self._start_time)
126
- h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
127
- sid = self._session_id[:8]
128
- watch_dir = self._session.get("watch_dir", "?")
129
- self.query_one("#session-bar", Static).update(
130
- f" session: [{GOLD}]{sid}[/{GOLD}] · {watch_dir} · [{GREEN}]●[/{GREEN}]"
131
- f" {h:02d}:{m:02d}:{s:02d} · [{PURPLE}]{self._account_type}[/{PURPLE}]"
132
- )
133
-
134
- def _poll_events(self) -> None:
135
- if self._paused:
136
- return
137
- try:
138
- events = store.get_session_events(self._session_id, after_id=self._last_seen_id)
139
- except Exception as exc:
140
- self.log.warning(f"poll_events failed: {exc}")
141
- return
142
-
143
- feed = self.query_one(RichLog)
144
- for ev in events:
145
- self._last_seen_id = ev.get("id", self._last_seen_id)
146
- etype = ev.get("type", "event")
147
- color = _EVENT_COLORS.get(etype, DIM)
148
- ts = datetime.fromtimestamp(ev.get("ts", time.time()), tz=UTC).strftime("%H:%M:%S")
149
- meta = _fmt_meta(ev)
150
- feed.write(
151
- f"[{DIM}]{ts}[/{DIM}] [{color}]{etype:<18}[/{color}] [{DIM}]{meta}[/{DIM}]"
152
- )
153
- self._stats[etype] = self._stats.get(etype, 0) + 1
154
- self._event_count += 1
155
-
156
- if events:
157
- self._refresh_stats()
158
-
159
- def _refresh_stats(self) -> None:
160
- lines = []
161
- for etype, count in sorted(self._stats.items(), key=lambda x: -x[1])[:10]:
162
- color = _EVENT_COLORS.get(etype, DIM)
163
- lines.append(f"[{DIM}]{etype:<14}[/{DIM}] [{color}]{count}[/{color}]")
164
- self.query_one("#stats-content", Static).update("\n".join(lines))
165
-
166
- def _show_moment(self, mtype: str, detail: str) -> None:
167
- alert = self.query_one("#moment-alert", Static)
168
- alert.update(f" [{GOLD}]⚡ {mtype}[/{GOLD}] [{DIM}]{detail}[/{DIM}]")
169
- alert.add_class("visible")
170
- self.set_timer(4.0, lambda: alert.remove_class("visible"))
171
-
172
- def action_stop_session(self) -> None:
173
- self.exit(None)
174
-
175
- def action_pause(self) -> None:
176
- self._paused = not self._paused
177
-
178
- def action_toggle_live(self) -> None:
179
- pass # handled by caller in cli.py
180
-
181
-
182
- def _fmt_meta(ev: dict) -> str:
183
- etype = ev.get("type", "")
184
- meta = ev.get("metadata") or {}
185
- if not isinstance(meta, dict):
186
- meta = {}
187
- if etype in ("file_edit", "file_create", "file_delete"):
188
- path = meta.get("path") or meta.get("file_path", "")
189
- delta = meta.get("line_delta") or meta.get("lines_added", "")
190
- return f"{path} {f'+{delta}' if delta else ''}".strip()
191
- if etype == "terminal_cmd":
192
- cmd = (meta.get("command") or "")[:40]
193
- ec = meta.get("exit_code", 0)
194
- return f"{cmd} {'✓' if ec == 0 else f'✗{ec}'}"
195
- if etype == "git_commit":
196
- return (meta.get("message") or "")[:40]
197
- if etype in ("llm_prompt", "agent_prompt"):
198
- tokens = meta.get("prompt_tokens") or meta.get("input_length", "")
199
- return f"{tokens} tokens" if tokens else ""
200
- if etype in ("llm_completion", "agent_completion"):
201
- tokens = meta.get("completion_tokens") or meta.get("output_length", "")
202
- dur = ev.get("duration_ms", "")
203
- return f"{tokens} tokens {dur}ms" if tokens else ""
204
- if etype == "test_run":
205
- p, f = meta.get("passed", 0), meta.get("failed", 0)
206
- return f"{p} passed {f} failed" if f else f"{p} passed"
207
- return ""
208
-
209
-
210
- def run(session_id: str, session: dict) -> None:
211
- """Launch the live session view for the given session."""
212
- StartApp(session_id, session).run()
@@ -1,53 +0,0 @@
1
- """Shared brand colors and base CSS for all MethodProof TUI screens."""
2
-
3
- # Brand palette
4
- GOLD = "#c9a84c"
5
- GREEN = "#109446"
6
- RED = "#d93326"
7
- PURPLE = "#803794"
8
- NAVY = "#192a56"
9
-
10
- # Surface palette
11
- BG = "#12110f"
12
- BAR = "#1c1a18"
13
- PANEL_BG = "#0f0d0b"
14
- BORDER = "#2e2c29"
15
- TEXT = "#e8e4de"
16
- DIM = "#6b6560"
17
-
18
- BASE_CSS = f"""
19
- Screen {{
20
- background: {BG};
21
- }}
22
- Header {{
23
- background: {BAR};
24
- color: {GOLD};
25
- text-style: bold;
26
- }}
27
- Footer {{
28
- background: {BAR};
29
- color: {DIM};
30
- }}
31
- .panel {{
32
- border: solid {BORDER};
33
- background: {PANEL_BG};
34
- margin: 0 0 1 0;
35
- padding: 1 2;
36
- }}
37
- .section-title {{
38
- color: {GOLD};
39
- text-style: bold;
40
- margin: 0 0 1 0;
41
- }}
42
- .row-label {{
43
- width: 24;
44
- color: {TEXT};
45
- }}
46
- .row-desc {{
47
- color: {DIM};
48
- }}
49
- Rule {{
50
- color: {BORDER};
51
- margin: 1 0;
52
- }}
53
- """
File without changes
File without changes
File without changes
File without changes