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