methodproof 0.7.35__tar.gz → 0.7.37__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 (86) hide show
  1. {methodproof-0.7.35 → methodproof-0.7.37}/CHANGELOG.md +20 -0
  2. {methodproof-0.7.35 → methodproof-0.7.37}/PKG-INFO +6 -4
  3. {methodproof-0.7.35 → methodproof-0.7.37}/README.md +5 -3
  4. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/base.py +0 -9
  5. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/cli.py +78 -11
  6. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/config.py +4 -14
  7. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/claude_code.py +6 -0
  8. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/claude_code.sh +54 -3
  9. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/start.py +82 -16
  10. {methodproof-0.7.35 → methodproof-0.7.37}/pyproject.toml +1 -1
  11. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_start.py +5 -2
  12. {methodproof-0.7.35 → methodproof-0.7.37}/.github/workflows/ci.yml +0 -0
  13. {methodproof-0.7.35 → methodproof-0.7.37}/.gitignore +0 -0
  14. {methodproof-0.7.35 → methodproof-0.7.37}/LICENSE +0 -0
  15. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/__init__.py +0 -0
  16. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/__main__.py +0 -0
  17. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/_daemon.py +0 -0
  18. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/__init__.py +0 -0
  19. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/music.py +0 -0
  20. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/terminal.py +0 -0
  21. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/agents/watcher.py +0 -0
  22. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/analysis.py +0 -0
  23. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/binding.py +0 -0
  24. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/bip39.py +0 -0
  25. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/bridge.py +0 -0
  26. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/crypto.py +0 -0
  27. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/e2e.py +0 -0
  28. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/graph.py +0 -0
  29. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hook.py +0 -0
  30. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/__init__.py +0 -0
  31. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/cline_hook.sh +0 -0
  32. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/codex_hook.sh +0 -0
  33. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/gemini_hook.sh +0 -0
  34. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/install.py +0 -0
  35. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/kiro_hook.sh +0 -0
  36. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/mcp_register.py +0 -0
  37. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/openclaw/HOOK.md +0 -0
  38. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/openclaw/handler.ts +0 -0
  39. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/openclaw_install.py +0 -0
  40. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/opencode_plugin.js +0 -0
  41. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/hooks/wrappers.py +0 -0
  42. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/integrity.py +0 -0
  43. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/kdf.py +0 -0
  44. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/keychain.py +0 -0
  45. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/live.py +0 -0
  46. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/lock.py +0 -0
  47. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/mcp.py +0 -0
  48. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/migrate_db.py +0 -0
  49. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/proxy.py +0 -0
  50. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/proxy_daemon.py +0 -0
  51. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/repos.py +0 -0
  52. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/skills/methodproof/SKILL.md +0 -0
  53. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/store.py +0 -0
  54. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/sync.py +0 -0
  55. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/__init__.py +0 -0
  56. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/consent.py +0 -0
  57. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/init.py +0 -0
  58. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/log.py +0 -0
  59. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/login_success.py +0 -0
  60. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/review.py +0 -0
  61. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/status.py +0 -0
  62. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/tui/theme.py +0 -0
  63. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/viewer.py +0 -0
  64. {methodproof-0.7.35 → methodproof-0.7.37}/methodproof/wordlist.py +0 -0
  65. {methodproof-0.7.35 → methodproof-0.7.37}/test_windows_compat.py +0 -0
  66. {methodproof-0.7.35 → methodproof-0.7.37}/tests/__init__.py +0 -0
  67. {methodproof-0.7.35 → methodproof-0.7.37}/tests/conftest.py +0 -0
  68. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_analysis.py +0 -0
  69. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_auth.py +0 -0
  70. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_config.py +0 -0
  71. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_helpers.py +0 -0
  72. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_session.py +0 -0
  73. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_share.py +0 -0
  74. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_cli_update.py +0 -0
  75. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_e2e_integration.py +0 -0
  76. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_graph.py +0 -0
  77. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_hooks.py +0 -0
  78. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_live.py +0 -0
  79. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_openclaw_hooks.py +0 -0
  80. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_profiles.py +0 -0
  81. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_security.py +0 -0
  82. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_store.py +0 -0
  83. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_sync.py +0 -0
  84. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_viewer.py +0 -0
  85. {methodproof-0.7.35 → methodproof-0.7.37}/tests/test_wrappers.py +0 -0
  86. {methodproof-0.7.35 → methodproof-0.7.37}/uv.lock +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.37] — 2026-04-12
4
+
5
+ ### Changed
6
+ - **Full tool metadata capture** — shell hook now stores complete `tool_input` and `tool_response` objects alongside display previews. Previously only extracted preview strings; raw data was discarded. Enables upstream graph analysis, causal edge building, and artifact tracking from tool events.
7
+ - **Enriched shell hook** — `PreToolUse` captures `tool_input_preview` per tool type (Bash→command, Read/Edit/Write→file_path, Grep→pattern+path, Glob→pattern, Agent→description). `PostToolUse` captures `result_preview` (Bash→stdout/stderr, Read→line count, Grep→file+line count, Glob→file count) plus `tool_input_preview` so results show what was done.
8
+ - **Journal gating removed** — all captured metadata is now persisted regardless of journal mode. No more field stripping in `emit()`. `JOURNAL_CONTENT_FIELDS` retained as a reference for TUI enrichment display.
9
+ - **`tool_result` TUI shows input + result** — e.g. `Grep ✓ def main /src 3 files, 15 lines` instead of just `Grep ✓`.
10
+
11
+ ## [0.7.36] — 2026-04-12
12
+
13
+ ### Added
14
+ - **`mp connect [session_id]`** — attach TUI to an active recording session. Defaults to the current active session. `mp start` with an active session now auto-connects instead of erroring.
15
+ - **Source tracking in TUI** — events display which AI tool session generated them (e.g. `claude`, `codex`). Session bar shows source name and conversation number (`claude #2`). Tracks across multiple AI sessions within one `mp start`.
16
+ - **End session from TUI (`x`)** — confirmation prompt (`y`/`n`) to stop the session and finalize from within the TUI. `q` now exits the TUI without stopping the daemon — reconnect later with `mp connect`.
17
+
18
+ ### Fixed
19
+ - **Tree indentation invisible** — `gold_ember` (`#3d3118`) was near-invisible on dark background; switched to `gold_aged` (`#9a7b3a`).
20
+ - **Orphan tool_call events showed no tree** — `tool_call` after `agent_turn_end`/`agent_complete` fell through to flat rendering. Now implicitly opens a new chain.
21
+ - **`tool_name` field empty in TUI** — hook events store the tool name in `tool` not `tool_name`; formatters now check both fields.
22
+
3
23
  ## [0.7.35] — 2026-04-12
4
24
 
5
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.35
3
+ Version: 0.7.37
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
@@ -88,9 +88,10 @@ methodproof view # explore your session in the browser
88
88
  | `enter` | Event detail | Modal overlay with all metadata fields |
89
89
  | `m` | Quiet mode | Hide dim events (music, environment, MCP, context compaction) |
90
90
  | `p` | Pause | Pause/resume event polling |
91
- | `q` | Stop | Stop the session |
91
+ | `q` | Exit TUI | Detach daemon keeps recording. Reconnect with `mp connect` |
92
+ | `x` | End session | Stop recording with confirmation prompt |
92
93
 
93
- Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all".
94
+ Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all". Source tracking shows which AI tool session (`claude #1`, `codex #2`) generated each event.
94
95
 
95
96
  ## Security Architecture
96
97
 
@@ -178,8 +179,9 @@ flowchart TB
178
179
  | Command | What it does |
179
180
  |---------|-------------|
180
181
  | `init` | Interactive consent selector, install hooks, create data directory |
181
- | `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording |
182
+ | `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording (auto-connects if session already active) |
182
183
  | `stop` | Stop recording, build process graph |
184
+ | `connect [session_id]` | Attach TUI to an active session (defaults to current) |
183
185
  | `view [session_id]` | Open session graph in browser |
184
186
  | `log` | List sessions with sync status, visibility, tags |
185
187
  | `login [--api-url URL]` | Authenticate with the platform |
@@ -71,9 +71,10 @@ methodproof view # explore your session in the browser
71
71
  | `enter` | Event detail | Modal overlay with all metadata fields |
72
72
  | `m` | Quiet mode | Hide dim events (music, environment, MCP, context compaction) |
73
73
  | `p` | Pause | Pause/resume event polling |
74
- | `q` | Stop | Stop the session |
74
+ | `q` | Exit TUI | Detach daemon keeps recording. Reconnect with `mp connect` |
75
+ | `x` | End session | Stop recording with confirmation prompt |
75
76
 
76
- Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all".
77
+ Active modes display as badges in the session bar: `J` (journal), `⏸` (scroll locked), `Q` (quiet), `T` (tree collapsed), filter name when not "all". Source tracking shows which AI tool session (`claude #1`, `codex #2`) generated each event.
77
78
 
78
79
  ## Security Architecture
79
80
 
@@ -161,8 +162,9 @@ flowchart TB
161
162
  | Command | What it does |
162
163
  |---------|-------------|
163
164
  | `init` | Interactive consent selector, install hooks, create data directory |
164
- | `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording |
165
+ | `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording (auto-connects if session already active) |
165
166
  | `stop` | Stop recording, build process graph |
167
+ | `connect [session_id]` | Attach TUI to an active session (defaults to current) |
166
168
  | `view [session_id]` | Open session graph in browser |
167
169
  | `log` | List sessions with sync status, visibility, tags |
168
170
  | `login [--api-url URL]` | Authenticate with the platform |
@@ -140,15 +140,6 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
140
140
  if event_type == etype and not _capture.get(category, True):
141
141
  metadata.pop(field, None)
142
142
 
143
- # Journal mode gate — strip content fields when journal is OFF (default).
144
- # Structural equivalents (prompt_length, etc.) are always kept.
145
- # Journal ON = complete explicit record. Journal OFF = structural only.
146
- if not _journal_mode:
147
- from methodproof.config import JOURNAL_CONTENT_FIELDS
148
- for etype, field in JOURNAL_CONTENT_FIELDS:
149
- if event_type == etype and field in metadata:
150
- metadata.pop(field, None)
151
-
152
143
  entry = {
153
144
  "id": uuid.uuid4().hex,
154
145
  "session_id": _session_id,
@@ -1098,9 +1098,11 @@ def cmd_start(args: argparse.Namespace) -> None:
1098
1098
 
1099
1099
  if cfg.get("active_session"):
1100
1100
  if _is_daemon_alive():
1101
- print(f"Session active: {cfg['active_session'][:8]}")
1102
- print("Run `methodproof stop` first.")
1103
- sys.exit(1)
1101
+ sid = cfg["active_session"]
1102
+ print(f" Session {sid[:8]} already active. Connecting...")
1103
+ args.session_id = None # connect resolves from config
1104
+ cmd_connect(args)
1105
+ return
1104
1106
  stale_sid = cfg["active_session"]
1105
1107
  _log_step(f"Cleaning stale session {stale_sid[:8]}")
1106
1108
  store.complete_session(stale_sid)
@@ -1301,7 +1303,9 @@ def cmd_start(args: argparse.Namespace) -> None:
1301
1303
  if _resolve_ui(args, cfg):
1302
1304
  session = store.get_session(sid)
1303
1305
  from methodproof.tui.start import run as tui_start
1304
- tui_start(sid, session)
1306
+ result = tui_start(sid, session)
1307
+ if result == "end_session":
1308
+ cmd_stop(args)
1305
1309
  return
1306
1310
  print("Run `mp stop` to finish.")
1307
1311
  return
@@ -1444,6 +1448,33 @@ def cmd_stop(args: argparse.Namespace) -> None:
1444
1448
  _print_summary(session, stats, cfg)
1445
1449
 
1446
1450
 
1451
+ def cmd_connect(args: argparse.Namespace) -> None:
1452
+ """Attach TUI to an active recording session."""
1453
+ cfg = config.load()
1454
+ sid = getattr(args, "session_id", None)
1455
+ if sid:
1456
+ # Resolve prefix
1457
+ sessions = store.list_sessions()
1458
+ match = [s for s in sessions if s["id"].startswith(sid)]
1459
+ if not match:
1460
+ print(f"No session matching '{sid}'")
1461
+ sys.exit(1)
1462
+ sid = match[0]["id"]
1463
+ else:
1464
+ sid = cfg.get("active_session")
1465
+ if not sid:
1466
+ print("No active session. Run `mp start` first.")
1467
+ sys.exit(1)
1468
+ session = store.get_session(sid)
1469
+ if not session:
1470
+ print(f"Session {sid[:8]} not found.")
1471
+ sys.exit(1)
1472
+ from methodproof.tui.start import run as tui_start
1473
+ result = tui_start(sid, session)
1474
+ if result == "end_session":
1475
+ cmd_stop(args)
1476
+
1477
+
1447
1478
  def cmd_view(args: argparse.Namespace) -> None:
1448
1479
  session = _resolve_session(args.session_id)
1449
1480
  from methodproof.viewer import view
@@ -1782,18 +1813,51 @@ def cmd_push(args: argparse.Namespace) -> None:
1782
1813
  local = getattr(args, "local", False)
1783
1814
  cfg = config.load(local=local)
1784
1815
  if not cfg.get("token"):
1785
- target = "local API" if local else "platform"
1786
1816
  print(f"Run `methodproof login{' --api-url http://localhost:8000' if local else ''}` first.")
1787
1817
  sys.exit(1)
1788
1818
  from methodproof.sync import sync_research_consent
1789
1819
  sync_research_consent(cfg["token"], cfg["api_url"])
1790
1820
  cfg = config.load(local=local)
1791
- sid = args.session_id or _latest()
1792
- if not sid:
1793
- print("No sessions to push.")
1794
- sys.exit(1)
1795
- from methodproof.sync import push
1796
1821
  force = getattr(args, "force", False)
1822
+
1823
+ if args.session_id:
1824
+ _push_one(args.session_id, cfg, force=force)
1825
+ return
1826
+
1827
+ # No session_id — check for unsynced completed sessions
1828
+ unsynced = [s for s in store.list_sessions()
1829
+ if not s["synced"] and s.get("completed_at")]
1830
+ if not unsynced:
1831
+ # Fall back to latest session
1832
+ sid = _latest()
1833
+ if not sid:
1834
+ print("No sessions to push.")
1835
+ sys.exit(1)
1836
+ _push_one(sid, cfg, force=force)
1837
+ return
1838
+
1839
+ if len(unsynced) == 1:
1840
+ _push_one(unsynced[0]["id"], cfg, force=force)
1841
+ return
1842
+
1843
+ # Multiple unsynced — offer to push all
1844
+ print(f"Found {len(unsynced)} unsynced sessions:\n")
1845
+ for s in unsynced:
1846
+ events = len(store.get_events(s["id"]))
1847
+ date = s["created_at"][:10] if s.get("created_at") else "?"
1848
+ print(f" {s['id'][:8]} {date} {events} events")
1849
+ print()
1850
+ answer = input(f"Push all {len(unsynced)}? [Y/n] ").strip().lower()
1851
+ if answer in ("", "y", "yes"):
1852
+ for s in unsynced:
1853
+ _push_one(s["id"], cfg, force=force)
1854
+ print()
1855
+ else:
1856
+ print("Cancelled. Push individually with `mp push <session_id>`.")
1857
+
1858
+
1859
+ def _push_one(sid: str, cfg: dict, force: bool = False) -> None:
1860
+ from methodproof.sync import push
1797
1861
  remote_id = push(sid, cfg["token"], cfg["api_url"], force=force)
1798
1862
  app = _app_url(cfg["api_url"])
1799
1863
  print(f"Pushed {sid[:8]} → {cfg['api_url']} (private).")
@@ -2205,6 +2269,9 @@ def main() -> None:
2205
2269
  s.add_argument("--streaming", action="store_true", help="Blocking foreground — stream every captured event to stdout")
2206
2270
  _add_ui_flags(s)
2207
2271
  sub.add_parser("stop", help="Stop recording")
2272
+ cn = sub.add_parser("connect", help="Attach TUI to active session")
2273
+ cn.add_argument("session_id", nargs="?", help="Session ID prefix (defaults to active)")
2274
+ _add_ui_flags(cn)
2208
2275
  v = sub.add_parser("view", help="Inspect captured session data")
2209
2276
  v.add_argument("session_id", nargs="?")
2210
2277
  l_log = sub.add_parser("log", help="List sessions")
@@ -2287,7 +2354,7 @@ def main() -> None:
2287
2354
 
2288
2355
  args = p.parse_args()
2289
2356
  cmds = {
2290
- "init": cmd_init, "start": cmd_start, "stop": cmd_stop,
2357
+ "init": cmd_init, "start": cmd_start, "stop": cmd_stop, "connect": cmd_connect,
2291
2358
  "view": cmd_view, "log": cmd_log, "status": cmd_status,
2292
2359
  "login": cmd_login, "logout": cmd_logout, "accounts": cmd_accounts, "switch": cmd_switch,
2293
2360
  "push": cmd_push, "tag": cmd_tag, "publish": cmd_publish,
@@ -79,47 +79,37 @@ CAPTURE_DESCRIPTIONS: dict[str, str] = {
79
79
  "code_capture": "Full file diffs and git patches (Pro only, encrypted, private by default)",
80
80
  }
81
81
 
82
- # Content fields that Journal Mode unlocks. When journal_mode is OFF (default),
83
- # these fields are stripped only structural equivalents remain (lengths, counts, types).
84
- # When journal_mode is ON (Pro+), EVERYTHING is persisted and encrypted.
85
- # Journal = the complete, explicit record of the session.
82
+ # Journal content fields no longer gated. All captured metadata is persisted.
83
+ # This list is retained as a reference for the TUI journal enrichment layer,
84
+ # which uses it to identify fields worth displaying as secondary content lines.
86
85
  JOURNAL_CONTENT_FIELDS: list[tuple[str, str]] = [
87
- # AI prompts — full prompt text
88
86
  ("llm_prompt", "prompt_text"),
89
87
  ("agent_prompt", "prompt_preview"),
90
- # AI responses — full completion text
91
88
  ("llm_completion", "response_text"),
92
89
  ("agent_completion", "response_preview"),
93
90
  ("agent_tool_dispatch", "tool_input_preview"),
94
91
  ("agent_tool_result", "result_preview"),
95
92
  ("agent_skill_invoke", "skill_input_preview"),
96
- # Terminal — full command output (command itself is structural, not gated)
97
93
  ("terminal_cmd", "output_snippet"),
98
- # Code — full diffs and commit messages
99
94
  ("file_edit", "diff"),
100
95
  ("git_commit", "diff"),
101
96
  ("git_commit", "message"),
102
- # Web — full search queries, URLs, page titles
97
+ ("git_commit", "body"),
103
98
  ("web_search", "query"),
104
99
  ("web_search", "clicked_results"),
105
100
  ("web_visit", "url"),
106
101
  ("web_visit", "title"),
107
- # Browser — full search queries, URLs, copy content, AI chat input
108
102
  ("browser_search", "query"),
109
103
  ("browser_visit", "url"),
110
104
  ("browser_visit", "title"),
111
105
  ("browser_copy", "text_snippet"),
112
106
  ("browser_ai_chat", "detected_input"),
113
107
  ("browser_ai_chat", "url"),
114
- # Tasks — subject reveals intent
115
108
  ("task_created", "subject"),
116
- # Claude Code hooks — tool input/output and raw user prompt
117
109
  ("user_prompt", "prompt_text"),
118
110
  ("tool_call", "tool_input_preview"),
119
111
  ("tool_result", "result_preview"),
120
- # Agent final message and commit body reveal content
121
112
  ("agent_complete", "last_message_preview"),
122
- ("git_commit", "body"),
123
113
  ]
124
114
 
125
115
 
@@ -99,20 +99,26 @@ _META_EXTRACTORS = {
99
99
  },
100
100
  "PreToolUse": lambda d: {
101
101
  "tool": _TOOL, "tool_name": d.get("tool_name", "unknown"),
102
+ "tool_input": d.get("tool_input") or {},
102
103
  "tool_input_preview": _tool_input_preview(d),
103
104
  },
104
105
  "PostToolUse": lambda d: {
105
106
  "tool": _TOOL, "tool_name": d.get("tool_name", "unknown"), "success": True,
107
+ "tool_input": d.get("tool_input") or {},
108
+ "tool_response": d.get("tool_response") or {},
109
+ "tool_input_preview": _tool_input_preview(d),
106
110
  "result_preview": _extract_result_text(d.get("tool_response")),
107
111
  },
108
112
  "PostToolUseFailure": lambda d: {
109
113
  "tool": _TOOL, "tool_name": d.get("tool_name", "unknown"),
110
114
  "success": False, "is_interrupt": d.get("is_interrupt", False),
115
+ "tool_input": d.get("tool_input") or {},
111
116
  "error": str(d.get("error", ""))[:200],
112
117
  },
113
118
  "SubagentStart": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
114
119
  "SubagentStop": lambda d: {
115
120
  "tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", ""),
121
+ "last_assistant_message": d.get("last_assistant_message", ""),
116
122
  "last_message_preview": str(d.get("last_assistant_message", ""))[:200],
117
123
  },
118
124
  "TaskCreated": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", ""), "subject": d.get("task_subject", "")},
@@ -37,11 +37,62 @@ if command -v jq >/dev/null 2>&1; then
37
37
  ;;
38
38
  PreToolUse)
39
39
  TYPE="tool_call"
40
- META=$(echo "$INPUT" | jq -c '{tool: (.tool_name // "unknown"), tool_use_id: (.tool_use_id // "")}' 2>/dev/null || echo '{}')
40
+ META=$(echo "$INPUT" | jq -c '{
41
+ tool: (.tool_name // "unknown"),
42
+ tool_use_id: (.tool_use_id // ""),
43
+ tool_input: (.tool_input // {}),
44
+ tool_input_preview: (
45
+ (.tool_input // {}) as $ti |
46
+ (.tool_name // "unknown") as $tn |
47
+ (if $tn == "Bash" then ($ti.command // "")
48
+ elif $tn == "Read" then ($ti.file_path // "")
49
+ elif $tn == "Write" then ($ti.file_path // "")
50
+ elif $tn == "Edit" then ($ti.file_path // "")
51
+ elif $tn == "Grep" then (($ti.pattern // "") + " " + ($ti.path // ""))
52
+ elif $tn == "Glob" then ($ti.pattern // "")
53
+ elif $tn == "Agent" then ($ti.description // $ti.prompt // "" | .[0:200])
54
+ else ($ti.command // $ti.file_path // $ti.path // $ti.query // $ti.pattern // $ti.url // $ti.description // $ti.prompt // ($ti | tostring)) end
55
+ ) | tostring | .[0:200]
56
+ )
57
+ }' 2>/dev/null || echo '{}')
41
58
  ;;
42
59
  PostToolUse)
43
60
  TYPE="tool_result"
44
- META=$(echo "$INPUT" | jq -c '{tool: (.tool_name // "unknown"), tool_use_id: (.tool_use_id // "")}' 2>/dev/null || echo '{}')
61
+ META=$(echo "$INPUT" | jq -c '{
62
+ tool: (.tool_name // "unknown"),
63
+ tool_use_id: (.tool_use_id // ""),
64
+ success: true,
65
+ tool_input: (.tool_input // {}),
66
+ tool_response: (.tool_response // {}),
67
+ tool_input_preview: (
68
+ (.tool_input // {}) as $ti |
69
+ (.tool_name // "unknown") as $tn |
70
+ (if $tn == "Bash" then ($ti.command // "")
71
+ elif $tn == "Read" then ($ti.file_path // "")
72
+ elif $tn == "Write" then ($ti.file_path // "")
73
+ elif $tn == "Edit" then ($ti.file_path // "")
74
+ elif $tn == "Grep" then (($ti.pattern // "") + " " + ($ti.path // ""))
75
+ elif $tn == "Glob" then ($ti.pattern // "")
76
+ elif $tn == "Agent" then ($ti.description // $ti.prompt // "" | .[0:200])
77
+ else ($ti.command // $ti.file_path // $ti.path // $ti.query // $ti.pattern // $ti.url // $ti.description // $ti.prompt // ($ti | tostring)) end
78
+ ) | tostring | .[0:200]
79
+ ),
80
+ result_preview: (
81
+ (.tool_response // {}) as $tr |
82
+ (.tool_name // "unknown") as $tn |
83
+ (if $tn == "Bash" then
84
+ (if ($tr.stderr // "") != "" then ("stderr: " + $tr.stderr) else ($tr.stdout // "") end | .[0:200])
85
+ elif $tn == "Read" then
86
+ (($tr.file.numLines // $tr.numLines // 0) | tostring) + " lines"
87
+ elif $tn == "Grep" then
88
+ (($tr.numFiles // 0) | tostring) + " files, " + (($tr.numLines // 0) | tostring) + " lines"
89
+ elif $tn == "Glob" then
90
+ (($tr.numFiles // 0) | tostring) + " files"
91
+ elif ($tr | type) == "string" then ($tr | .[0:200])
92
+ else ($tr | tostring | .[0:200]) end
93
+ )
94
+ )
95
+ }' 2>/dev/null || echo '{}')
45
96
  ;;
46
97
  SubagentStart)
47
98
  TYPE="agent_launch"
@@ -65,7 +116,7 @@ if command -v jq >/dev/null 2>&1; then
65
116
  ;;
66
117
  PostToolUseFailure)
67
118
  TYPE="tool_failure"
68
- META=$(echo "$INPUT" | jq -c '{tool_name: (.tool_name // "unknown"), is_interrupt: (.is_interrupt // false), error: (.error // "" | .[0:200])}' 2>/dev/null || echo '{}')
119
+ META=$(echo "$INPUT" | jq -c '{tool: (.tool_name // "unknown"), is_interrupt: (.is_interrupt // false), success: false, error: (.error // "" | .[0:200])}' 2>/dev/null || echo '{}')
69
120
  ;;
70
121
  SessionEnd)
71
122
  TYPE="claude_session_end"
@@ -197,6 +197,11 @@ class _TreeTracker:
197
197
  return "├ ", True
198
198
  self._in_chain = False
199
199
  return " ", True
200
+ # Not in chain — tool_call starts one implicitly (orphan after closer)
201
+ if etype in self._INNERS:
202
+ self._in_chain = True
203
+ self._collapse_count = 0
204
+ return "├ ", True
200
205
  return " ", True
201
206
 
202
207
 
@@ -255,15 +260,17 @@ def _fmt_meta(ev: dict) -> str:
255
260
 
256
261
  # Hook lifecycle — tool call / result
257
262
  if etype == "tool_call":
258
- name = meta.get("tool_name", "")
263
+ name = meta.get("tool_name") or meta.get("tool", "")
259
264
  preview = (meta.get("tool_input_preview") or "")[:60]
260
265
  return f"{name} {preview}".strip()
261
266
  if etype == "tool_result":
262
- name = meta.get("tool_name", "")
267
+ name = meta.get("tool_name") or meta.get("tool", "")
263
268
  ok = "✓" if meta.get("success", True) else "✗"
264
- return f"{name} {ok}"
269
+ inp = (meta.get("tool_input_preview") or "")[:40]
270
+ result = (meta.get("result_preview") or "")[:40]
271
+ return f"{name} {ok} {inp} {result}".strip()
265
272
  if etype == "tool_failure":
266
- name = meta.get("tool_name", "")
273
+ name = meta.get("tool_name") or meta.get("tool", "")
267
274
  err = (meta.get("error") or "")[:40]
268
275
  return f"{name} ✗ {err}".strip()
269
276
 
@@ -392,14 +399,40 @@ class _DetailScreen(Screen[None]):
392
399
  self.app.pop_screen()
393
400
 
394
401
 
402
+ class _ConfirmEndScreen(Screen[bool]):
403
+ """Confirm session end."""
404
+
405
+ BINDINGS = [
406
+ Binding("y", "confirm", "yes"),
407
+ Binding("n", "cancel", "no"),
408
+ Binding("escape", "cancel", "cancel"),
409
+ ]
410
+
411
+ def compose(self) -> ComposeResult:
412
+ P = ACTIVE
413
+ yield Static(
414
+ f"\n [{P.red}]End this session?[/{P.red}]\n\n"
415
+ f" [{P.dim}]This will stop all capture agents and finalize the session.[/{P.dim}]\n\n"
416
+ f" [{P.gold}]y[/{P.gold}] [{P.dim}]confirm[/{P.dim}] "
417
+ f"[{P.gold}]n[/{P.gold}] [{P.dim}]cancel[/{P.dim}]",
418
+ )
419
+
420
+ def action_confirm(self) -> None:
421
+ self.dismiss(True)
422
+
423
+ def action_cancel(self) -> None:
424
+ self.dismiss(False)
425
+
426
+
395
427
  # ── App ──────────────────────────────────────────────────────────────
396
- class StartApp(App[None]):
428
+ class StartApp(App[str | None]):
397
429
  """Live session view — tails the active session's events."""
398
430
 
399
431
  TITLE = "methodproof — mp start"
400
432
  CSS = _CSS
401
433
  BINDINGS = [
402
- Binding("q", "stop_session", "stop"),
434
+ Binding("q", "exit_tui", "exit"),
435
+ Binding("x", "end_session", "end session"),
403
436
  Binding("p", "pause", "pause"),
404
437
  Binding("j", "toggle_journal", "journal"),
405
438
  Binding("f", "cycle_filter", "filter"),
@@ -438,6 +471,10 @@ class StartApp(App[None]):
438
471
  self._last_event_ts: float = 0.0
439
472
  self._recent_files: list[str] = []
440
473
  self._recent_tools: list[str] = []
474
+ # Source tracking: which AI tool session is active
475
+ self._active_source: str = "" # e.g. "claude", "codex", "gemini"
476
+ self._source_session_id: str = "" # short id of the AI session
477
+ self._source_count: int = 0 # how many AI sessions so far
441
478
 
442
479
  def compose(self) -> ComposeResult:
443
480
  yield Header(show_clock=False)
@@ -518,10 +555,13 @@ class StartApp(App[None]):
518
555
  badge_str = f" {badge_str}"
519
556
 
520
557
  ev = f" {self._event_count} ev" if self._event_count else ""
558
+ src = ""
559
+ if self._active_source:
560
+ src = f" · [{P.purple_muted}]{self._active_source} #{self._source_count}[/{P.purple_muted}]"
521
561
  self.query_one("#session-bar", Static).update(
522
562
  f" session: [{P.gold}]{sid}[/{P.gold}] · {watch_dir}"
523
563
  f" · [{P.green}]●[/{P.green}] {h:02d}:{m:02d}:{s:02d}"
524
- f"{ev}{badge_str} · [{P.purple}]{self._account_type}[/{P.purple}]"
564
+ f"{ev}{src}{badge_str} · [{P.purple}]{self._account_type}[/{P.purple}]"
525
565
  )
526
566
 
527
567
  # ── Event poll: Layers B + E + A ─────────────────────────────
@@ -544,6 +584,17 @@ class StartApp(App[None]):
544
584
  self._last_event = ev
545
585
  ev_ts = ev.get("ts", time.time())
546
586
 
587
+ # Source tracking: detect AI session boundaries
588
+ if etype.endswith("_session_start"):
589
+ self._active_source = etype.replace("_session_start", "")
590
+ ev_meta_src = ev.get("metadata") or {}
591
+ sid_field = ev_meta_src.get("session_id") or ev_meta_src.get("claude_session_id", "")
592
+ self._source_session_id = str(sid_field)[:8]
593
+ self._source_count += 1
594
+ elif etype.endswith("_session_end"):
595
+ self._active_source = ""
596
+ self._source_session_id = ""
597
+
547
598
  # Quiet mode: skip dim events
548
599
  if self._quiet_mode and etype in _DIM_TYPES:
549
600
  self._stats[etype] = self._stats.get(etype, 0) + 1
@@ -572,7 +623,7 @@ class StartApp(App[None]):
572
623
  self._recent_files.append(path)
573
624
  if len(self._recent_files) > 10:
574
625
  self._recent_files.pop(0)
575
- tool = ev_meta.get("tool_name")
626
+ tool = ev_meta.get("tool_name") or ev_meta.get("tool")
576
627
  if tool and etype in ("tool_call", "tool_result"):
577
628
  if tool not in self._recent_tools:
578
629
  self._recent_tools.append(tool)
@@ -585,16 +636,20 @@ class StartApp(App[None]):
585
636
  self._event_count += 1
586
637
  continue
587
638
 
639
+ # Source tag
640
+ src_tag = ""
641
+ if self._active_source:
642
+ src_tag = f"[{P.purple_muted}]{self._active_source}[/{P.purple_muted}] "
643
+
588
644
  # Search highlight
589
645
  line_plain = f"{etype} {meta}"
590
646
  if self._search_query and self._search_query.lower() not in line_plain.lower():
591
- # Still render but dimmed
592
647
  feed.write(f"[{P.dim}]{ts} {prefix}{etype:<18} {meta}[/{P.dim}]")
593
648
  else:
594
- # Layer B + E: structural line with tree prefix
595
649
  feed.write(
596
650
  f"[{P.dim}]{ts}[/{P.dim}] "
597
- f"[{P.gold_ember}]{prefix}[/{P.gold_ember}]"
651
+ f"{src_tag}"
652
+ f"[{P.gold_aged}]{prefix}[/{P.gold_aged}]"
598
653
  f"[{color}]{etype:<18}[/{color}] "
599
654
  f"[{P.dim}]{meta}[/{P.dim}]"
600
655
  )
@@ -602,7 +657,7 @@ class StartApp(App[None]):
602
657
  # Layer A: journal content enrichment
603
658
  for jline in _journal_lines(ev, self._journal_mode):
604
659
  feed.write(
605
- f" [{P.gold_ember}]│[/{P.gold_ember}] "
660
+ f" [{P.gold_aged}]│[/{P.gold_aged}] "
606
661
  f"[{P.dim}]{jline}[/{P.dim}]"
607
662
  )
608
663
 
@@ -655,9 +710,19 @@ class StartApp(App[None]):
655
710
  self.set_timer(4.0, lambda: alert.remove_class("visible"))
656
711
 
657
712
  # ── Actions ──────────────────────────────────────────────────
658
- def action_stop_session(self) -> None:
713
+ def action_exit_tui(self) -> None:
714
+ """Detach TUI — daemon keeps recording."""
659
715
  self.exit(None)
660
716
 
717
+ def action_end_session(self) -> None:
718
+ """End the session (with confirmation)."""
719
+ self.push_screen(_ConfirmEndScreen(), self._on_confirm_end)
720
+
721
+ def _on_confirm_end(self, confirmed: bool) -> None:
722
+ if not confirmed:
723
+ return
724
+ self.exit("end_session")
725
+
661
726
  def action_pause(self) -> None:
662
727
  self._paused = not self._paused
663
728
 
@@ -743,6 +808,7 @@ class StartApp(App[None]):
743
808
  self._tick_timer()
744
809
 
745
810
 
746
- def run(session_id: str, session: dict) -> None:
747
- """Launch the live session view for the given session."""
748
- StartApp(session_id, session).run()
811
+ def run(session_id: str, session: dict) -> str | None:
812
+ """Launch the live session view. Returns 'end_session' if user chose to end."""
813
+ app = StartApp(session_id, session)
814
+ return app.run()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.7.35"
3
+ version = "0.7.37"
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"]
@@ -38,14 +38,17 @@ def test_start_auth_fails(mock_auth, mock_hook, cli_args):
38
38
 
39
39
  @patch("methodproof.cli._is_daemon_alive", return_value=True)
40
40
  @patch("methodproof.hook.is_installed", return_value=True)
41
- def test_start_session_already_active(mock_hook, mock_alive, logged_in_cfg, cli_args, make_session):
41
+ def test_start_session_already_active(mock_hook, mock_alive, logged_in_cfg, cli_args, make_session, capsys):
42
42
  logged_in_cfg()
43
43
  sid, _ = make_session()
44
44
  cfg = config.load()
45
45
  cfg["active_session"] = sid
46
46
  config.save(cfg)
47
- with pytest.raises(SystemExit):
47
+ with patch("methodproof.cli.cmd_connect") as mock_connect:
48
48
  cli.cmd_start(cli_args())
49
+ mock_connect.assert_called_once()
50
+ out = capsys.readouterr().out
51
+ assert "already active" in out
49
52
 
50
53
 
51
54
  @patch("methodproof.cli._is_daemon_alive", return_value=False)
File without changes
File without changes
File without changes