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.
- {methodproof-0.7.32 → methodproof-0.7.34}/CHANGELOG.md +15 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/PKG-INFO +1 -1
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/cli.py +3 -1
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/store.py +8 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/sync.py +11 -3
- methodproof-0.7.34/methodproof/tui/start.py +436 -0
- methodproof-0.7.34/methodproof/tui/theme.py +106 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/pyproject.toml +1 -1
- methodproof-0.7.32/methodproof/tui/start.py +0 -212
- methodproof-0.7.32/methodproof/tui/theme.py +0 -53
- {methodproof-0.7.32 → methodproof-0.7.34}/.github/workflows/ci.yml +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/.gitignore +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/LICENSE +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/README.md +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/__init__.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/__main__.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/_daemon.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/base.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/music.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/analysis.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/binding.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/bip39.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/bridge.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/config.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/crypto.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/e2e.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/graph.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hook.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/claude_code.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/install.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/integrity.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/kdf.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/keychain.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/live.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/lock.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/mcp.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/migrate_db.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/proxy.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/repos.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/__init__.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/consent.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/init.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/log.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/login_success.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/review.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/tui/status.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/viewer.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/methodproof/wordlist.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/test_windows_compat.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/__init__.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/conftest.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_analysis.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_auth.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_config.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_helpers.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_session.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_share.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_start.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_cli_update.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_e2e_integration.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_graph.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_hooks.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_live.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_profiles.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_security.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_store.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_sync.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_viewer.py +0 -0
- {methodproof-0.7.32 → methodproof-0.7.34}/tests/test_wrappers.py +0 -0
- {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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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.
|
|
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
|
|
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
|