overcode 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
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.
- overcode/__init__.py +1 -1
- overcode/cli.py +154 -51
- overcode/config.py +66 -0
- overcode/daemon_claude_skill.md +36 -33
- overcode/history_reader.py +69 -8
- overcode/implementations.py +178 -87
- overcode/monitor_daemon.py +87 -97
- overcode/monitor_daemon_core.py +261 -0
- overcode/monitor_daemon_state.py +24 -15
- overcode/pid_utils.py +17 -3
- overcode/session_manager.py +54 -0
- overcode/settings.py +34 -0
- overcode/status_constants.py +1 -1
- overcode/status_detector.py +8 -2
- overcode/status_patterns.py +19 -0
- overcode/summarizer_client.py +72 -27
- overcode/summarizer_component.py +87 -107
- overcode/supervisor_daemon.py +55 -38
- overcode/supervisor_daemon_core.py +210 -0
- overcode/testing/__init__.py +6 -0
- overcode/testing/renderer.py +268 -0
- overcode/testing/tmux_driver.py +223 -0
- overcode/testing/tui_eye.py +185 -0
- overcode/testing/tui_eye_skill.md +187 -0
- overcode/tmux_manager.py +117 -93
- overcode/tui.py +399 -1969
- overcode/tui_actions/__init__.py +20 -0
- overcode/tui_actions/daemon.py +201 -0
- overcode/tui_actions/input.py +128 -0
- overcode/tui_actions/navigation.py +117 -0
- overcode/tui_actions/session.py +428 -0
- overcode/tui_actions/view.py +357 -0
- overcode/tui_helpers.py +42 -9
- overcode/tui_logic.py +347 -0
- overcode/tui_render.py +414 -0
- overcode/tui_widgets/__init__.py +24 -0
- overcode/tui_widgets/command_bar.py +399 -0
- overcode/tui_widgets/daemon_panel.py +153 -0
- overcode/tui_widgets/daemon_status_bar.py +245 -0
- overcode/tui_widgets/help_overlay.py +71 -0
- overcode/tui_widgets/preview_pane.py +69 -0
- overcode/tui_widgets/session_summary.py +514 -0
- overcode/tui_widgets/status_timeline.py +253 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/METADATA +4 -1
- overcode-0.1.4.dist-info/RECORD +68 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/WHEEL +1 -1
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
- overcode-0.1.2.dist-info/RECORD +0 -45
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
overcode/tui_render.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pure render functions for TUI components.
|
|
3
|
+
|
|
4
|
+
These functions are extracted from TUI widgets to enable unit testing
|
|
5
|
+
without requiring the full Textual framework.
|
|
6
|
+
|
|
7
|
+
All functions are pure - they take data as input and return Rich Text objects.
|
|
8
|
+
No side effects, no external dependencies.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Optional, List, Dict, Tuple
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from .tui_helpers import (
|
|
16
|
+
format_interval,
|
|
17
|
+
format_duration,
|
|
18
|
+
format_tokens,
|
|
19
|
+
format_cost,
|
|
20
|
+
format_line_count,
|
|
21
|
+
get_status_symbol,
|
|
22
|
+
get_daemon_status_style,
|
|
23
|
+
calculate_uptime,
|
|
24
|
+
get_current_state_times,
|
|
25
|
+
)
|
|
26
|
+
from .settings import DAEMON_VERSION
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_daemon_monitor_section(
|
|
30
|
+
monitor_state, # MonitorDaemonState or None
|
|
31
|
+
is_stale: bool,
|
|
32
|
+
) -> Text:
|
|
33
|
+
"""Render the Monitor Daemon section of the status bar.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
monitor_state: Monitor daemon state object or None
|
|
37
|
+
is_stale: Whether the state is considered stale
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Rich Text for the monitor section
|
|
41
|
+
"""
|
|
42
|
+
content = Text()
|
|
43
|
+
content.append("Monitor: ", style="bold")
|
|
44
|
+
|
|
45
|
+
monitor_running = monitor_state is not None and not is_stale
|
|
46
|
+
|
|
47
|
+
if monitor_running:
|
|
48
|
+
symbol, style = get_daemon_status_style(monitor_state.status)
|
|
49
|
+
content.append(f"{symbol} ", style=style)
|
|
50
|
+
content.append(f"#{monitor_state.loop_count}", style="cyan")
|
|
51
|
+
content.append(f" @{format_interval(monitor_state.current_interval)}", style="dim")
|
|
52
|
+
|
|
53
|
+
# Version mismatch warning
|
|
54
|
+
if monitor_state.daemon_version != DAEMON_VERSION:
|
|
55
|
+
content.append(
|
|
56
|
+
f" ⚠v{monitor_state.daemon_version}→{DAEMON_VERSION}",
|
|
57
|
+
style="bold yellow"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
content.append("○ ", style="red")
|
|
61
|
+
content.append("stopped", style="red")
|
|
62
|
+
|
|
63
|
+
return content
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render_supervisor_section(
|
|
67
|
+
supervisor_running: bool,
|
|
68
|
+
monitor_state, # MonitorDaemonState or None
|
|
69
|
+
is_monitor_running: bool,
|
|
70
|
+
) -> Text:
|
|
71
|
+
"""Render the Supervisor Daemon section of the status bar.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
supervisor_running: Whether supervisor daemon is running
|
|
75
|
+
monitor_state: Monitor daemon state (for supervisor claude info)
|
|
76
|
+
is_monitor_running: Whether monitor daemon is running
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Rich Text for the supervisor section
|
|
80
|
+
"""
|
|
81
|
+
content = Text()
|
|
82
|
+
content.append("Supervisor: ", style="bold")
|
|
83
|
+
|
|
84
|
+
if supervisor_running:
|
|
85
|
+
content.append("● ", style="green")
|
|
86
|
+
|
|
87
|
+
if is_monitor_running and monitor_state and monitor_state.supervisor_claude_running:
|
|
88
|
+
# Calculate current run duration
|
|
89
|
+
run_duration = ""
|
|
90
|
+
if monitor_state.supervisor_claude_started_at:
|
|
91
|
+
try:
|
|
92
|
+
started = datetime.fromisoformat(monitor_state.supervisor_claude_started_at)
|
|
93
|
+
elapsed = (datetime.now() - started).total_seconds()
|
|
94
|
+
run_duration = format_duration(elapsed)
|
|
95
|
+
except (ValueError, TypeError):
|
|
96
|
+
run_duration = "?"
|
|
97
|
+
content.append(f"🤖 RUNNING {run_duration}", style="bold yellow")
|
|
98
|
+
|
|
99
|
+
elif is_monitor_running and monitor_state and monitor_state.total_supervisions > 0:
|
|
100
|
+
content.append(f"sup:{monitor_state.total_supervisions}", style="magenta")
|
|
101
|
+
if monitor_state.supervisor_tokens > 0:
|
|
102
|
+
content.append(f" {format_tokens(monitor_state.supervisor_tokens)}", style="blue")
|
|
103
|
+
if monitor_state.supervisor_claude_total_run_seconds > 0:
|
|
104
|
+
total_run = format_duration(monitor_state.supervisor_claude_total_run_seconds)
|
|
105
|
+
content.append(f" ⏱{total_run}", style="dim")
|
|
106
|
+
else:
|
|
107
|
+
content.append("ready", style="green")
|
|
108
|
+
else:
|
|
109
|
+
content.append("○ ", style="red")
|
|
110
|
+
content.append("stopped", style="red")
|
|
111
|
+
|
|
112
|
+
return content
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_ai_summarizer_section(
|
|
116
|
+
summarizer_available: bool,
|
|
117
|
+
summarizer_enabled: bool,
|
|
118
|
+
summarizer_calls: int,
|
|
119
|
+
) -> Text:
|
|
120
|
+
"""Render the AI Summarizer section of the status bar.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
summarizer_available: Whether API key is available
|
|
124
|
+
summarizer_enabled: Whether summarizer is enabled
|
|
125
|
+
summarizer_calls: Number of API calls made
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Rich Text for the AI section
|
|
129
|
+
"""
|
|
130
|
+
content = Text()
|
|
131
|
+
content.append("AI: ", style="bold")
|
|
132
|
+
|
|
133
|
+
if summarizer_available:
|
|
134
|
+
if summarizer_enabled:
|
|
135
|
+
content.append("● ", style="green")
|
|
136
|
+
if summarizer_calls > 0:
|
|
137
|
+
content.append(f"{summarizer_calls}", style="cyan")
|
|
138
|
+
else:
|
|
139
|
+
content.append("on", style="green")
|
|
140
|
+
else:
|
|
141
|
+
content.append("○ ", style="dim")
|
|
142
|
+
content.append("off", style="dim")
|
|
143
|
+
else:
|
|
144
|
+
content.append("○ ", style="red")
|
|
145
|
+
content.append("n/a", style="red dim")
|
|
146
|
+
|
|
147
|
+
return content
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def render_spin_stats(
|
|
151
|
+
sessions: List, # List of SessionDaemonState
|
|
152
|
+
asleep_session_ids: set,
|
|
153
|
+
show_cost: bool = False,
|
|
154
|
+
) -> Text:
|
|
155
|
+
"""Render spin rate statistics.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
sessions: List of session daemon states
|
|
159
|
+
asleep_session_ids: Set of session IDs that are asleep
|
|
160
|
+
show_cost: Show $ cost instead of token counts
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Rich Text for spin stats section
|
|
164
|
+
"""
|
|
165
|
+
content = Text()
|
|
166
|
+
|
|
167
|
+
# Filter out sleeping agents
|
|
168
|
+
active_sessions = [s for s in sessions if s.session_id not in asleep_session_ids]
|
|
169
|
+
sleeping_count = len(sessions) - len(active_sessions)
|
|
170
|
+
|
|
171
|
+
total_agents = len(active_sessions)
|
|
172
|
+
green_now = sum(1 for s in active_sessions if s.current_status == "running")
|
|
173
|
+
|
|
174
|
+
# Calculate mean spin rate
|
|
175
|
+
mean_spin = 0.0
|
|
176
|
+
for s in active_sessions:
|
|
177
|
+
total_time = s.green_time_seconds + s.non_green_time_seconds
|
|
178
|
+
if total_time > 0:
|
|
179
|
+
mean_spin += s.green_time_seconds / total_time
|
|
180
|
+
|
|
181
|
+
content.append("Spin: ", style="bold")
|
|
182
|
+
content.append(f"{green_now}", style="bold green" if green_now > 0 else "dim")
|
|
183
|
+
content.append(f"/{total_agents}", style="dim")
|
|
184
|
+
|
|
185
|
+
if sleeping_count > 0:
|
|
186
|
+
content.append(f" 💤{sleeping_count}", style="dim")
|
|
187
|
+
|
|
188
|
+
if mean_spin > 0:
|
|
189
|
+
content.append(f" μ{mean_spin:.1f}x", style="cyan")
|
|
190
|
+
|
|
191
|
+
# Total tokens/cost (include sleeping agents)
|
|
192
|
+
if show_cost:
|
|
193
|
+
total_cost = sum(s.estimated_cost_usd for s in sessions)
|
|
194
|
+
if total_cost > 0:
|
|
195
|
+
content.append(f" {format_cost(total_cost)}", style="orange1")
|
|
196
|
+
else:
|
|
197
|
+
total_tokens = sum(s.input_tokens + s.output_tokens for s in sessions)
|
|
198
|
+
if total_tokens > 0:
|
|
199
|
+
content.append(f" Σ{format_tokens(total_tokens)}", style="orange1")
|
|
200
|
+
|
|
201
|
+
return content
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def render_presence_indicator(
|
|
205
|
+
presence_state: int,
|
|
206
|
+
idle_seconds: float,
|
|
207
|
+
) -> Text:
|
|
208
|
+
"""Render presence status indicator.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
presence_state: 1=locked, 2=inactive, 3=active
|
|
212
|
+
idle_seconds: Seconds since last activity
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Rich Text for presence indicator
|
|
216
|
+
"""
|
|
217
|
+
content = Text()
|
|
218
|
+
|
|
219
|
+
state_icons = {1: "🔒", 2: "💤", 3: "👤"}
|
|
220
|
+
state_colors = {1: "red", 2: "yellow", 3: "green"}
|
|
221
|
+
|
|
222
|
+
icon = state_icons.get(presence_state, "?")
|
|
223
|
+
color = state_colors.get(presence_state, "dim")
|
|
224
|
+
|
|
225
|
+
content.append(f"{icon}", style=color)
|
|
226
|
+
content.append(f" {int(idle_seconds)}s", style="dim")
|
|
227
|
+
|
|
228
|
+
return content
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def render_session_summary_line(
|
|
232
|
+
name: str,
|
|
233
|
+
detected_status: str,
|
|
234
|
+
expanded: bool,
|
|
235
|
+
summary_detail: str, # "low", "med", "full"
|
|
236
|
+
start_time: str,
|
|
237
|
+
repo_name: Optional[str],
|
|
238
|
+
branch: Optional[str],
|
|
239
|
+
green_time: float,
|
|
240
|
+
non_green_time: float,
|
|
241
|
+
permissiveness_mode: str,
|
|
242
|
+
state_since: Optional[str],
|
|
243
|
+
local_status_changed_at: Optional[datetime],
|
|
244
|
+
steers_count: int,
|
|
245
|
+
total_tokens: Optional[int],
|
|
246
|
+
current_context_tokens: Optional[int],
|
|
247
|
+
interaction_count: Optional[int],
|
|
248
|
+
median_work_time: float,
|
|
249
|
+
git_diff_stats: Optional[Tuple[int, int, int]],
|
|
250
|
+
is_unvisited_stalled: bool,
|
|
251
|
+
has_focus: bool,
|
|
252
|
+
is_list_mode: bool,
|
|
253
|
+
max_repo_info_width: int = 18,
|
|
254
|
+
show_cost: bool = False,
|
|
255
|
+
estimated_cost_usd: float = 0.0,
|
|
256
|
+
) -> Text:
|
|
257
|
+
"""Render a single session summary line.
|
|
258
|
+
|
|
259
|
+
This is a pure function that builds the Rich Text display for a session.
|
|
260
|
+
All data is passed as parameters, no external dependencies.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
name: Session name
|
|
264
|
+
detected_status: Current detected status string
|
|
265
|
+
expanded: Whether session is expanded
|
|
266
|
+
summary_detail: Detail level
|
|
267
|
+
start_time: ISO timestamp of session start
|
|
268
|
+
repo_name: Repository name
|
|
269
|
+
branch: Git branch
|
|
270
|
+
green_time: Total green (running) time in seconds
|
|
271
|
+
non_green_time: Total non-green time in seconds
|
|
272
|
+
permissiveness_mode: "normal", "permissive", or "bypass"
|
|
273
|
+
state_since: ISO timestamp of current state start
|
|
274
|
+
local_status_changed_at: Local datetime when status changed
|
|
275
|
+
steers_count: Number of robot supervisions
|
|
276
|
+
total_tokens: Total tokens used (or None)
|
|
277
|
+
current_context_tokens: Current context window usage (or None)
|
|
278
|
+
interaction_count: Total interactions (or None)
|
|
279
|
+
median_work_time: Median autonomous work time
|
|
280
|
+
git_diff_stats: Tuple of (files, insertions, deletions) or None
|
|
281
|
+
is_unvisited_stalled: Whether this is an unvisited stalled agent
|
|
282
|
+
has_focus: Whether this widget has focus
|
|
283
|
+
is_list_mode: Whether in list view mode
|
|
284
|
+
max_repo_info_width: Width for repo info column
|
|
285
|
+
show_cost: Show $ cost instead of token counts
|
|
286
|
+
estimated_cost_usd: Estimated cost in USD
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Rich Text object for the summary line
|
|
290
|
+
"""
|
|
291
|
+
bg = " on #0d2137"
|
|
292
|
+
|
|
293
|
+
# Expansion indicator
|
|
294
|
+
expand_icon = "▼" if expanded else "▶"
|
|
295
|
+
|
|
296
|
+
# Calculate values
|
|
297
|
+
uptime = calculate_uptime(start_time)
|
|
298
|
+
repo_info = f"{repo_name or 'n/a'}:{branch or 'n/a'}"
|
|
299
|
+
|
|
300
|
+
# Status indicator
|
|
301
|
+
status_symbol, base_color = get_status_symbol(detected_status)
|
|
302
|
+
status_color = f"bold {base_color}{bg}"
|
|
303
|
+
|
|
304
|
+
# Permissiveness emoji
|
|
305
|
+
perm_emojis = {"bypass": "🔥", "permissive": "🏃"}
|
|
306
|
+
perm_emoji = perm_emojis.get(permissiveness_mode, "👮")
|
|
307
|
+
|
|
308
|
+
content = Text()
|
|
309
|
+
|
|
310
|
+
# Name width based on detail level
|
|
311
|
+
name_widths = {"low": 24, "med": 20, "full": 16}
|
|
312
|
+
name_width = name_widths.get(summary_detail, 20)
|
|
313
|
+
display_name = name[:name_width].ljust(name_width)
|
|
314
|
+
|
|
315
|
+
# Status symbol
|
|
316
|
+
content.append(f"{status_symbol} ", style=status_color)
|
|
317
|
+
|
|
318
|
+
# Stalled indicator
|
|
319
|
+
if is_unvisited_stalled:
|
|
320
|
+
content.append("🔔", style=f"bold blink red{bg}")
|
|
321
|
+
else:
|
|
322
|
+
content.append(" ", style=f"dim{bg}")
|
|
323
|
+
|
|
324
|
+
# Time in current state
|
|
325
|
+
state_start = local_status_changed_at
|
|
326
|
+
if state_since:
|
|
327
|
+
try:
|
|
328
|
+
daemon_state_start = datetime.fromisoformat(state_since)
|
|
329
|
+
if state_start is None or daemon_state_start > state_start:
|
|
330
|
+
state_start = daemon_state_start
|
|
331
|
+
except (ValueError, TypeError):
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
if state_start:
|
|
335
|
+
elapsed = (datetime.now() - state_start).total_seconds()
|
|
336
|
+
content.append(f"{format_duration(elapsed):>5} ", style=status_color)
|
|
337
|
+
else:
|
|
338
|
+
content.append(" - ", style=f"dim{bg}")
|
|
339
|
+
|
|
340
|
+
# Focus/expand indicator
|
|
341
|
+
if is_list_mode:
|
|
342
|
+
if has_focus:
|
|
343
|
+
content.append("→ ", style=status_color)
|
|
344
|
+
else:
|
|
345
|
+
content.append(" ", style=status_color)
|
|
346
|
+
else:
|
|
347
|
+
content.append(f"{expand_icon} ", style=status_color)
|
|
348
|
+
|
|
349
|
+
content.append(f"{display_name}", style=f"bold cyan{bg}")
|
|
350
|
+
|
|
351
|
+
# Full detail: repo:branch
|
|
352
|
+
if summary_detail == "full":
|
|
353
|
+
content.append(f" {repo_info:<{max_repo_info_width}} ", style=f"bold dim{bg}")
|
|
354
|
+
|
|
355
|
+
# Med/Full: uptime, times
|
|
356
|
+
if summary_detail in ("med", "full"):
|
|
357
|
+
content.append(f" ↑{uptime:>5}", style=f"bold white{bg}")
|
|
358
|
+
content.append(f" ▶{format_duration(green_time):>5}", style=f"bold green{bg}")
|
|
359
|
+
content.append(f" ⏸{format_duration(non_green_time):>5}", style=f"bold red{bg}")
|
|
360
|
+
|
|
361
|
+
if summary_detail == "full":
|
|
362
|
+
total_time = green_time + non_green_time
|
|
363
|
+
pct = (green_time / total_time * 100) if total_time > 0 else 0
|
|
364
|
+
pct_style = f"bold green{bg}" if pct >= 50 else f"bold red{bg}"
|
|
365
|
+
content.append(f" {pct:>3.0f}%", style=pct_style)
|
|
366
|
+
|
|
367
|
+
# Token usage or cost
|
|
368
|
+
if total_tokens is not None:
|
|
369
|
+
if show_cost:
|
|
370
|
+
content.append(f" {format_cost(estimated_cost_usd):>7}", style=f"bold orange1{bg}")
|
|
371
|
+
else:
|
|
372
|
+
content.append(f" Σ{format_tokens(total_tokens):>6}", style=f"bold orange1{bg}")
|
|
373
|
+
if current_context_tokens and current_context_tokens > 0:
|
|
374
|
+
max_context = 200_000
|
|
375
|
+
ctx_pct = min(100, current_context_tokens / max_context * 100)
|
|
376
|
+
content.append(f" c@{ctx_pct:>3.0f}%", style=f"bold orange1{bg}")
|
|
377
|
+
else:
|
|
378
|
+
content.append(" c@ -%", style=f"dim orange1{bg}")
|
|
379
|
+
else:
|
|
380
|
+
content.append(" - c@ -%", style=f"dim orange1{bg}")
|
|
381
|
+
|
|
382
|
+
# Git diff stats
|
|
383
|
+
if git_diff_stats:
|
|
384
|
+
files, ins, dels = git_diff_stats
|
|
385
|
+
if summary_detail == "full":
|
|
386
|
+
content.append(f" Δ{files:>2}", style=f"bold magenta{bg}")
|
|
387
|
+
content.append(f" +{format_line_count(ins):>4}", style=f"bold green{bg}")
|
|
388
|
+
content.append(f" -{format_line_count(dels):>4}", style=f"bold red{bg}")
|
|
389
|
+
else:
|
|
390
|
+
style = f"bold magenta{bg}" if files > 0 else f"dim{bg}"
|
|
391
|
+
content.append(f" Δ{files:>2}", style=style)
|
|
392
|
+
else:
|
|
393
|
+
if summary_detail == "full":
|
|
394
|
+
content.append(" Δ- + - - ", style=f"dim{bg}")
|
|
395
|
+
else:
|
|
396
|
+
content.append(" Δ-", style=f"dim{bg}")
|
|
397
|
+
|
|
398
|
+
# Med/Full: median work time
|
|
399
|
+
if summary_detail in ("med", "full"):
|
|
400
|
+
work_str = format_duration(median_work_time) if median_work_time > 0 else "0s"
|
|
401
|
+
content.append(f" ⏱{work_str:>5}", style=f"bold blue{bg}")
|
|
402
|
+
|
|
403
|
+
# Permission mode, human/robot counts
|
|
404
|
+
content.append(f" {perm_emoji}", style=f"bold white{bg}")
|
|
405
|
+
|
|
406
|
+
if interaction_count is not None:
|
|
407
|
+
human_count = max(0, interaction_count - steers_count)
|
|
408
|
+
content.append(f" 👤{human_count:>3}", style=f"bold yellow{bg}")
|
|
409
|
+
else:
|
|
410
|
+
content.append(" 👤 -", style=f"dim yellow{bg}")
|
|
411
|
+
|
|
412
|
+
content.append(f" 🤖{steers_count:>3}", style=f"bold cyan{bg}")
|
|
413
|
+
|
|
414
|
+
return content
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TUI Widget components for Overcode.
|
|
3
|
+
|
|
4
|
+
This package contains the individual widget classes extracted from tui.py
|
|
5
|
+
for better maintainability and testability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .help_overlay import HelpOverlay
|
|
9
|
+
from .preview_pane import PreviewPane
|
|
10
|
+
from .daemon_panel import DaemonPanel
|
|
11
|
+
from .daemon_status_bar import DaemonStatusBar
|
|
12
|
+
from .status_timeline import StatusTimeline
|
|
13
|
+
from .session_summary import SessionSummary
|
|
14
|
+
from .command_bar import CommandBar
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"HelpOverlay",
|
|
18
|
+
"PreviewPane",
|
|
19
|
+
"DaemonPanel",
|
|
20
|
+
"DaemonStatusBar",
|
|
21
|
+
"StatusTimeline",
|
|
22
|
+
"SessionSummary",
|
|
23
|
+
"CommandBar",
|
|
24
|
+
]
|