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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daemon status bar widget for TUI.
|
|
3
|
+
|
|
4
|
+
Shows Monitor Daemon, Supervisor Daemon, AI, spin stats, and presence status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from ..monitor_daemon_state import MonitorDaemonState, get_monitor_daemon_state
|
|
14
|
+
from ..supervisor_daemon import is_supervisor_daemon_running
|
|
15
|
+
from ..summarizer_client import SummarizerClient
|
|
16
|
+
from ..web_server import is_web_server_running, get_web_server_url
|
|
17
|
+
from ..settings import DAEMON_VERSION, get_agent_history_path
|
|
18
|
+
from ..status_history import read_agent_status_history
|
|
19
|
+
from ..tui_logic import calculate_mean_spin_from_history
|
|
20
|
+
from ..tui_helpers import (
|
|
21
|
+
format_interval,
|
|
22
|
+
format_duration,
|
|
23
|
+
format_tokens,
|
|
24
|
+
format_cost,
|
|
25
|
+
get_daemon_status_style,
|
|
26
|
+
calculate_safe_break_duration,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from ..session_manager import SessionManager
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DaemonStatusBar(Static):
|
|
34
|
+
"""Widget displaying daemon status.
|
|
35
|
+
|
|
36
|
+
Shows Monitor Daemon and Supervisor Daemon status explicitly.
|
|
37
|
+
Presence is shown only when available (macOS with monitor daemon running).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, tmux_session: str = "agents", session_manager: Optional["SessionManager"] = None, *args, **kwargs):
|
|
41
|
+
super().__init__(*args, **kwargs)
|
|
42
|
+
self.tmux_session = tmux_session
|
|
43
|
+
self.monitor_state: Optional[MonitorDaemonState] = None
|
|
44
|
+
self._session_manager = session_manager
|
|
45
|
+
self._asleep_session_ids: set = set() # Cache of asleep session IDs
|
|
46
|
+
self.show_cost: bool = False # Show $ cost instead of token counts
|
|
47
|
+
|
|
48
|
+
def update_status(self) -> None:
|
|
49
|
+
"""Refresh daemon state from file"""
|
|
50
|
+
self.monitor_state = get_monitor_daemon_state(self.tmux_session)
|
|
51
|
+
# Update cache of asleep session IDs from session manager
|
|
52
|
+
if self._session_manager:
|
|
53
|
+
self._asleep_session_ids = {
|
|
54
|
+
s.id for s in self._session_manager.list_sessions() if s.is_asleep
|
|
55
|
+
}
|
|
56
|
+
self.refresh()
|
|
57
|
+
|
|
58
|
+
def render(self) -> Text:
|
|
59
|
+
"""Render daemon status bar.
|
|
60
|
+
|
|
61
|
+
Shows Monitor Daemon and Supervisor Daemon status explicitly.
|
|
62
|
+
"""
|
|
63
|
+
content = Text()
|
|
64
|
+
|
|
65
|
+
# Monitor Daemon status
|
|
66
|
+
content.append("Monitor: ", style="bold")
|
|
67
|
+
monitor_running = self.monitor_state and not self.monitor_state.is_stale()
|
|
68
|
+
|
|
69
|
+
if monitor_running:
|
|
70
|
+
state = self.monitor_state
|
|
71
|
+
symbol, style = get_daemon_status_style(state.status)
|
|
72
|
+
content.append(f"{symbol} ", style=style)
|
|
73
|
+
content.append(f"#{state.loop_count}", style="cyan")
|
|
74
|
+
content.append(f" @{format_interval(state.current_interval)}", style="dim")
|
|
75
|
+
# Version mismatch warning
|
|
76
|
+
if state.daemon_version != DAEMON_VERSION:
|
|
77
|
+
content.append(f" ⚠v{state.daemon_version}→{DAEMON_VERSION}", style="bold yellow")
|
|
78
|
+
else:
|
|
79
|
+
content.append("○ ", style="red")
|
|
80
|
+
content.append("stopped", style="red")
|
|
81
|
+
|
|
82
|
+
content.append(" │ ", style="dim")
|
|
83
|
+
|
|
84
|
+
# Supervisor Daemon status
|
|
85
|
+
content.append("Supervisor: ", style="bold")
|
|
86
|
+
supervisor_running = is_supervisor_daemon_running(self.tmux_session)
|
|
87
|
+
|
|
88
|
+
if supervisor_running:
|
|
89
|
+
content.append("● ", style="green")
|
|
90
|
+
# Show if daemon Claude is currently running
|
|
91
|
+
if monitor_running and self.monitor_state.supervisor_claude_running:
|
|
92
|
+
# Calculate current run duration
|
|
93
|
+
run_duration = ""
|
|
94
|
+
if self.monitor_state.supervisor_claude_started_at:
|
|
95
|
+
try:
|
|
96
|
+
started = datetime.fromisoformat(self.monitor_state.supervisor_claude_started_at)
|
|
97
|
+
elapsed = (datetime.now() - started).total_seconds()
|
|
98
|
+
run_duration = format_duration(elapsed)
|
|
99
|
+
except (ValueError, TypeError):
|
|
100
|
+
run_duration = "?"
|
|
101
|
+
content.append(f"🤖 RUNNING {run_duration}", style="bold yellow")
|
|
102
|
+
# Show supervision stats if available from monitor state
|
|
103
|
+
elif monitor_running and self.monitor_state.total_supervisions > 0:
|
|
104
|
+
content.append(f"sup:{self.monitor_state.total_supervisions}", style="magenta")
|
|
105
|
+
if self.monitor_state.supervisor_tokens > 0:
|
|
106
|
+
content.append(f" {format_tokens(self.monitor_state.supervisor_tokens)}", style="blue")
|
|
107
|
+
# Show cumulative daemon Claude run time
|
|
108
|
+
if self.monitor_state.supervisor_claude_total_run_seconds > 0:
|
|
109
|
+
total_run = format_duration(self.monitor_state.supervisor_claude_total_run_seconds)
|
|
110
|
+
content.append(f" ⏱{total_run}", style="dim")
|
|
111
|
+
else:
|
|
112
|
+
content.append("ready", style="green")
|
|
113
|
+
else:
|
|
114
|
+
content.append("○ ", style="red")
|
|
115
|
+
content.append("stopped", style="red")
|
|
116
|
+
|
|
117
|
+
# AI Summarizer status (from TUI's local summarizer, not daemon)
|
|
118
|
+
content.append(" │ ", style="dim")
|
|
119
|
+
content.append("AI: ", style="bold")
|
|
120
|
+
# Get summarizer state from parent app
|
|
121
|
+
summarizer_available = SummarizerClient.is_available()
|
|
122
|
+
summarizer_enabled = False
|
|
123
|
+
summarizer_calls = 0
|
|
124
|
+
if hasattr(self.app, '_summarizer'):
|
|
125
|
+
summarizer_enabled = self.app._summarizer.enabled
|
|
126
|
+
summarizer_calls = self.app._summarizer.total_calls
|
|
127
|
+
if summarizer_available:
|
|
128
|
+
if summarizer_enabled:
|
|
129
|
+
content.append("● ", style="green")
|
|
130
|
+
if summarizer_calls > 0:
|
|
131
|
+
content.append(f"{summarizer_calls}", style="cyan")
|
|
132
|
+
else:
|
|
133
|
+
content.append("on", style="green")
|
|
134
|
+
else:
|
|
135
|
+
content.append("○ ", style="dim")
|
|
136
|
+
content.append("off", style="dim")
|
|
137
|
+
else:
|
|
138
|
+
content.append("○ ", style="red")
|
|
139
|
+
content.append("n/a", style="red dim")
|
|
140
|
+
|
|
141
|
+
# Spin rate stats (only when monitor running with sessions)
|
|
142
|
+
if monitor_running and self.monitor_state.sessions:
|
|
143
|
+
content.append(" │ ", style="dim")
|
|
144
|
+
# Filter out sleeping agents from stats
|
|
145
|
+
all_sessions = self.monitor_state.sessions
|
|
146
|
+
active_sessions = [s for s in all_sessions if s.session_id not in self._asleep_session_ids]
|
|
147
|
+
sleeping_count = len(all_sessions) - len(active_sessions)
|
|
148
|
+
|
|
149
|
+
total_agents = len(active_sessions)
|
|
150
|
+
# Recalculate green_now excluding sleeping agents
|
|
151
|
+
green_now = sum(1 for s in active_sessions if s.current_status == "running")
|
|
152
|
+
|
|
153
|
+
content.append("Spin: ", style="bold")
|
|
154
|
+
content.append(f"{green_now}", style="bold green" if green_now > 0 else "dim")
|
|
155
|
+
content.append(f"/{total_agents}", style="dim")
|
|
156
|
+
if sleeping_count > 0:
|
|
157
|
+
content.append(f" 💤{sleeping_count}", style="dim") # Show sleeping count
|
|
158
|
+
|
|
159
|
+
# Mean spin rate - use history-based calculation if baseline > 0
|
|
160
|
+
baseline_minutes = getattr(self.app, 'baseline_minutes', 0)
|
|
161
|
+
if baseline_minutes > 0:
|
|
162
|
+
# History-based calculation for time window
|
|
163
|
+
history = read_agent_status_history(
|
|
164
|
+
hours=baseline_minutes / 60.0 + 0.1, # slight buffer
|
|
165
|
+
history_file=get_agent_history_path(self.tmux_session)
|
|
166
|
+
)
|
|
167
|
+
agent_names = [s.name for s in active_sessions]
|
|
168
|
+
mean_spin, sample_count = calculate_mean_spin_from_history(
|
|
169
|
+
history, agent_names, baseline_minutes
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if sample_count > 0:
|
|
173
|
+
# Format window label: "15m", "1h", "1h30m"
|
|
174
|
+
if baseline_minutes < 60:
|
|
175
|
+
window_label = f"{baseline_minutes}m"
|
|
176
|
+
else:
|
|
177
|
+
hours = baseline_minutes // 60
|
|
178
|
+
mins = baseline_minutes % 60
|
|
179
|
+
window_label = f"{hours}h" if mins == 0 else f"{hours}h{mins}m"
|
|
180
|
+
content.append(f" μ{mean_spin:.1f} ({window_label})", style="cyan")
|
|
181
|
+
else:
|
|
182
|
+
content.append(" μ-- (no data)", style="dim")
|
|
183
|
+
else:
|
|
184
|
+
# Instantaneous: show current running count as the mean
|
|
185
|
+
content.append(f" μ{green_now}", style="cyan")
|
|
186
|
+
|
|
187
|
+
# Total tokens/cost across all sessions (include sleeping agents - they used tokens too)
|
|
188
|
+
if self.show_cost:
|
|
189
|
+
total_cost = sum(s.estimated_cost_usd for s in all_sessions)
|
|
190
|
+
if total_cost > 0:
|
|
191
|
+
content.append(f" {format_cost(total_cost)}", style="orange1")
|
|
192
|
+
else:
|
|
193
|
+
total_tokens = sum(s.input_tokens + s.output_tokens for s in all_sessions)
|
|
194
|
+
if total_tokens > 0:
|
|
195
|
+
content.append(f" Σ{format_tokens(total_tokens)}", style="orange1")
|
|
196
|
+
|
|
197
|
+
# Safe break duration (time until 50%+ agents need attention) - exclude sleeping
|
|
198
|
+
safe_break = calculate_safe_break_duration(active_sessions)
|
|
199
|
+
if safe_break is not None:
|
|
200
|
+
content.append(" │ ", style="dim")
|
|
201
|
+
content.append("☕", style="bold")
|
|
202
|
+
if safe_break < 60:
|
|
203
|
+
content.append(f" <1m", style="bold red")
|
|
204
|
+
elif safe_break < 300: # < 5 min
|
|
205
|
+
content.append(f" {format_duration(safe_break)}", style="bold yellow")
|
|
206
|
+
else:
|
|
207
|
+
content.append(f" {format_duration(safe_break)}", style="bold green")
|
|
208
|
+
|
|
209
|
+
# Presence status (only show if available via monitor daemon on macOS)
|
|
210
|
+
if monitor_running and self.monitor_state.presence_available:
|
|
211
|
+
content.append(" │ ", style="dim")
|
|
212
|
+
state = self.monitor_state.presence_state
|
|
213
|
+
idle = self.monitor_state.presence_idle_seconds or 0
|
|
214
|
+
|
|
215
|
+
state_names = {1: "🔒", 2: "💤", 3: "👤"}
|
|
216
|
+
state_colors = {1: "red", 2: "yellow", 3: "green"}
|
|
217
|
+
|
|
218
|
+
icon = state_names.get(state, "?")
|
|
219
|
+
color = state_colors.get(state, "dim")
|
|
220
|
+
content.append(f"{icon}", style=color)
|
|
221
|
+
content.append(f" {int(idle)}s", style="dim")
|
|
222
|
+
|
|
223
|
+
# Relay status (small indicator)
|
|
224
|
+
if monitor_running and self.monitor_state.relay_enabled:
|
|
225
|
+
content.append(" │ ", style="dim")
|
|
226
|
+
relay_status = self.monitor_state.relay_last_status
|
|
227
|
+
if relay_status == "ok":
|
|
228
|
+
content.append("📡", style="green")
|
|
229
|
+
elif relay_status == "error":
|
|
230
|
+
content.append("📡", style="red")
|
|
231
|
+
else:
|
|
232
|
+
content.append("📡", style="dim")
|
|
233
|
+
|
|
234
|
+
# Web server status
|
|
235
|
+
web_running = is_web_server_running(self.tmux_session)
|
|
236
|
+
if web_running:
|
|
237
|
+
content.append(" │ ", style="dim")
|
|
238
|
+
url = get_web_server_url(self.tmux_session)
|
|
239
|
+
content.append("🌐", style="green")
|
|
240
|
+
if url:
|
|
241
|
+
# Just show port
|
|
242
|
+
port = url.split(":")[-1] if url else ""
|
|
243
|
+
content.append(f":{port}", style="cyan")
|
|
244
|
+
|
|
245
|
+
return content
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Help overlay widget for TUI.
|
|
3
|
+
|
|
4
|
+
Displays keyboard shortcuts and status color explanations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.widgets import Static
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HelpOverlay(Static):
|
|
12
|
+
"""Help overlay explaining all TUI metrics and controls"""
|
|
13
|
+
|
|
14
|
+
HELP_TEXT = """
|
|
15
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
16
|
+
║ OVERCODE MONITOR HELP ║
|
|
17
|
+
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
18
|
+
║ STATUS COLORS ║
|
|
19
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
20
|
+
║ 🟢 Running 🟡 No orders 🟠 Wait supervisor 🔴 Wait user ║
|
|
21
|
+
║ 💤 Asleep ⚫ Terminated ║
|
|
22
|
+
║ ║
|
|
23
|
+
║ NAVIGATION & VIEW ║
|
|
24
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
25
|
+
║ j/↓ Next agent k/↑ Previous agent ║
|
|
26
|
+
║ space Toggle expand m Toggle tree/list mode ║
|
|
27
|
+
║ e Expand/Collapse all c Sync to main + clear ║
|
|
28
|
+
║ h/? Toggle help r Refresh ║
|
|
29
|
+
║ q Quit ║
|
|
30
|
+
║ ║
|
|
31
|
+
║ DISPLAY MODES ║
|
|
32
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
33
|
+
║ s Cycle summary detail (low → med → full) ║
|
|
34
|
+
║ l Cycle summary content (💬 short → 📖 context → 🎯 orders → ✏️ note)║
|
|
35
|
+
║ v Cycle detail lines (5 → 10 → 20 → 50) ║
|
|
36
|
+
║ S Cycle sort mode (alpha → status → value) ║
|
|
37
|
+
║ t Toggle timeline d Toggle daemon panel ║
|
|
38
|
+
║ g Show killed agents Z Hide sleeping agents ║
|
|
39
|
+
║ ,/. Baseline time -/+15m 0 Reset baseline to now ║
|
|
40
|
+
║ ║
|
|
41
|
+
║ AGENT CONTROL ║
|
|
42
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
43
|
+
║ i/: Send instruction o Set standing orders ║
|
|
44
|
+
║ I Edit annotation Enter Approve (send Enter) ║
|
|
45
|
+
║ 1-5 Send number n New agent ║
|
|
46
|
+
║ x Kill agent R Restart agent ║
|
|
47
|
+
║ z Toggle sleep V Edit agent value ║
|
|
48
|
+
║ b Jump to red/attention H Handover all (2x) → draft PR ║
|
|
49
|
+
║ ║
|
|
50
|
+
║ DAEMON CONTROL ║
|
|
51
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
52
|
+
║ [ Start supervisor ] Stop supervisor ║
|
|
53
|
+
║ \\ Restart monitor w Toggle web dashboard ║
|
|
54
|
+
║ ║
|
|
55
|
+
║ OTHER ║
|
|
56
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
57
|
+
║ y Copy mode (mouse sel) p Sync to tmux pane ║
|
|
58
|
+
║ M Monochrome mode (for terminals with ANSI issues) ║
|
|
59
|
+
║ ║
|
|
60
|
+
║ COMMAND BAR (i or :) ║
|
|
61
|
+
║ ────────────────────────────────────────────────────────────────────────── ║
|
|
62
|
+
║ Enter Send instruction Esc Clear & unfocus ║
|
|
63
|
+
║ Ctrl+E Multi-line mode Ctrl+O Set as standing order ║
|
|
64
|
+
║ Ctrl+Enter Send (multi-line) ║
|
|
65
|
+
║ ║
|
|
66
|
+
║ Press h or ? to close ║
|
|
67
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def render(self) -> Text:
|
|
71
|
+
return Text(self.HELP_TEXT.strip())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preview pane widget for TUI.
|
|
3
|
+
|
|
4
|
+
Shows focused agent's terminal output in list+preview mode.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
from textual.reactive import reactive
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .session_summary import SessionSummary
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PreviewPane(Static):
|
|
18
|
+
"""Preview pane showing focused agent's terminal output in list+preview mode."""
|
|
19
|
+
|
|
20
|
+
content_lines: reactive[List[str]] = reactive(list, init=False)
|
|
21
|
+
monochrome: reactive[bool] = reactive(False)
|
|
22
|
+
session_name: str = ""
|
|
23
|
+
|
|
24
|
+
def __init__(self, **kwargs):
|
|
25
|
+
super().__init__(**kwargs)
|
|
26
|
+
self.content_lines = []
|
|
27
|
+
|
|
28
|
+
def render(self) -> Text:
|
|
29
|
+
content = Text()
|
|
30
|
+
# Use widget width for layout, with sensible fallback
|
|
31
|
+
pane_width = self.size.width if self.size.width > 0 else 80
|
|
32
|
+
|
|
33
|
+
# Header with session name - pad to full pane width
|
|
34
|
+
header = f"─── {self.session_name} " if self.session_name else "─── Preview "
|
|
35
|
+
header_style = "bold" if self.monochrome else "bold cyan"
|
|
36
|
+
border_style = "dim" if self.monochrome else "dim"
|
|
37
|
+
content.append(header, style=header_style)
|
|
38
|
+
content.append("─" * max(0, pane_width - len(header)), style=border_style)
|
|
39
|
+
content.append("\n")
|
|
40
|
+
|
|
41
|
+
if not self.content_lines:
|
|
42
|
+
content.append("(no output)", style="dim italic")
|
|
43
|
+
else:
|
|
44
|
+
# Calculate available lines based on widget height
|
|
45
|
+
# Reserve 2 lines for header and some padding
|
|
46
|
+
available_lines = max(10, self.size.height - 2) if self.size.height > 0 else 30
|
|
47
|
+
# Show last N lines of output with ANSI color support
|
|
48
|
+
# Truncate lines to pane width to match tmux display
|
|
49
|
+
max_line_len = max(pane_width - 1, 40) # Leave room for newline, minimum 40
|
|
50
|
+
for line in self.content_lines[-available_lines:]:
|
|
51
|
+
# Truncate long lines to pane width
|
|
52
|
+
display_line = line[:max_line_len] if len(line) > max_line_len else line
|
|
53
|
+
if self.monochrome:
|
|
54
|
+
# Strip ANSI colors - use plain text only
|
|
55
|
+
parsed = Text.from_ansi(display_line)
|
|
56
|
+
content.append(parsed.plain)
|
|
57
|
+
else:
|
|
58
|
+
# Parse ANSI escape sequences to preserve colors from tmux
|
|
59
|
+
# Note: Text.from_ansi() strips trailing newlines, so add newline separately
|
|
60
|
+
content.append(Text.from_ansi(display_line))
|
|
61
|
+
content.append("\n")
|
|
62
|
+
|
|
63
|
+
return content
|
|
64
|
+
|
|
65
|
+
def update_from_widget(self, widget: "SessionSummary") -> None:
|
|
66
|
+
"""Update preview content from a SessionSummary widget."""
|
|
67
|
+
self.session_name = widget.session.name
|
|
68
|
+
self.content_lines = list(widget.pane_content) if widget.pane_content else []
|
|
69
|
+
self.refresh()
|