overcode 0.1.3__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 +7 -2
- overcode/implementations.py +74 -8
- overcode/monitor_daemon.py +60 -65
- overcode/monitor_daemon_core.py +261 -0
- overcode/monitor_daemon_state.py +7 -0
- overcode/session_manager.py +1 -0
- overcode/settings.py +22 -0
- overcode/supervisor_daemon.py +48 -47
- 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 +17 -3
- overcode/tui.py +196 -2462
- 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 +41 -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.3.dist-info → overcode-0.1.4.dist-info}/METADATA +3 -1
- overcode-0.1.4.dist-info/RECORD +68 -0
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
- overcode-0.1.3.dist-info/RECORD +0 -45
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/WHEEL +0 -0
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session summary widget for TUI.
|
|
3
|
+
|
|
4
|
+
Displays expandable session summary with status, metrics, and pane content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
from textual.reactive import reactive
|
|
12
|
+
from textual import events
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from ..session_manager import Session
|
|
16
|
+
from ..status_detector import StatusDetector
|
|
17
|
+
from ..history_reader import get_session_stats, ClaudeSessionStats
|
|
18
|
+
from ..tui_helpers import (
|
|
19
|
+
format_duration,
|
|
20
|
+
format_tokens,
|
|
21
|
+
format_cost,
|
|
22
|
+
format_line_count,
|
|
23
|
+
calculate_uptime,
|
|
24
|
+
get_current_state_times,
|
|
25
|
+
get_status_symbol,
|
|
26
|
+
style_pane_line,
|
|
27
|
+
get_git_diff_stats,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_standing_instructions(instructions: str, max_len: int = 95) -> str:
|
|
32
|
+
"""Format standing instructions for display.
|
|
33
|
+
|
|
34
|
+
Shows "[DEFAULT]" if instructions match the configured default,
|
|
35
|
+
otherwise shows the truncated instructions.
|
|
36
|
+
"""
|
|
37
|
+
from ..config import get_default_standing_instructions
|
|
38
|
+
|
|
39
|
+
if not instructions:
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
default = get_default_standing_instructions()
|
|
43
|
+
if default and instructions.strip() == default.strip():
|
|
44
|
+
return "[DEFAULT]"
|
|
45
|
+
|
|
46
|
+
if len(instructions) > max_len:
|
|
47
|
+
return instructions[:max_len - 3] + "..."
|
|
48
|
+
return instructions
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SessionSummary(Static, can_focus=True):
|
|
52
|
+
"""Widget displaying expandable session summary"""
|
|
53
|
+
|
|
54
|
+
expanded: reactive[bool] = reactive(True) # Start expanded
|
|
55
|
+
detail_lines: reactive[int] = reactive(5) # Lines of output to show (5, 10, 20, 50)
|
|
56
|
+
summary_detail: reactive[str] = reactive("low") # low, med, full
|
|
57
|
+
summary_content_mode: reactive[str] = reactive("ai_short") # ai_short, ai_long, orders, annotation (#74)
|
|
58
|
+
|
|
59
|
+
def __init__(self, session: Session, status_detector: StatusDetector, *args, **kwargs):
|
|
60
|
+
super().__init__(*args, **kwargs)
|
|
61
|
+
self.session = session
|
|
62
|
+
self.status_detector = status_detector
|
|
63
|
+
# Initialize from session status (for terminated) or persisted state
|
|
64
|
+
if session.status == "terminated":
|
|
65
|
+
self.detected_status = "terminated"
|
|
66
|
+
self.current_activity = "(tmux window no longer exists)"
|
|
67
|
+
else:
|
|
68
|
+
self.detected_status = session.stats.current_state if session.stats.current_state else "running"
|
|
69
|
+
self.current_activity = "Initializing..."
|
|
70
|
+
# AI-generated summaries (from daemon's SummarizerComponent)
|
|
71
|
+
self.ai_summary_short: str = "" # Short: current activity (~50 chars)
|
|
72
|
+
self.ai_summary_context: str = "" # Context: wider context (~80 chars)
|
|
73
|
+
self.monochrome: bool = False # B&W mode for terminals with ANSI issues (#138)
|
|
74
|
+
self.show_cost: bool = False # Show $ cost instead of token counts
|
|
75
|
+
self.summarizer_enabled: bool = False # Track if summarizer is enabled
|
|
76
|
+
self.pane_content: List[str] = [] # Cached pane content
|
|
77
|
+
self.claude_stats: Optional[ClaudeSessionStats] = None # Token/interaction stats
|
|
78
|
+
self.git_diff_stats: Optional[tuple] = None # (files, insertions, deletions)
|
|
79
|
+
# Track if this is a stalled agent that hasn't been visited yet
|
|
80
|
+
self.is_unvisited_stalled: bool = False
|
|
81
|
+
# Track when status last changed (for immediate time-in-state updates)
|
|
82
|
+
self._status_changed_at: Optional[datetime] = None
|
|
83
|
+
self._last_known_status: str = self.detected_status
|
|
84
|
+
# Start with expanded class since expanded=True by default
|
|
85
|
+
self.add_class("expanded")
|
|
86
|
+
|
|
87
|
+
def on_click(self) -> None:
|
|
88
|
+
"""Toggle expanded state on click"""
|
|
89
|
+
self.expanded = not self.expanded
|
|
90
|
+
# Notify parent app to save state
|
|
91
|
+
self.post_message(self.ExpandedChanged(self.session.id, self.expanded))
|
|
92
|
+
# Mark as visited if this is an unvisited stalled agent
|
|
93
|
+
if self.is_unvisited_stalled:
|
|
94
|
+
self.post_message(self.StalledAgentVisited(self.session.id))
|
|
95
|
+
|
|
96
|
+
def on_focus(self) -> None:
|
|
97
|
+
"""Handle focus event - mark stalled agent as visited and update selection"""
|
|
98
|
+
if self.is_unvisited_stalled:
|
|
99
|
+
self.post_message(self.StalledAgentVisited(self.session.id))
|
|
100
|
+
# Notify app to update selection highlighting
|
|
101
|
+
self.post_message(self.SessionSelected(self.session.id))
|
|
102
|
+
|
|
103
|
+
class SessionSelected(events.Message):
|
|
104
|
+
"""Message sent when a session is selected/focused"""
|
|
105
|
+
def __init__(self, session_id: str):
|
|
106
|
+
super().__init__()
|
|
107
|
+
self.session_id = session_id
|
|
108
|
+
|
|
109
|
+
class ExpandedChanged(events.Message):
|
|
110
|
+
"""Message sent when expanded state changes"""
|
|
111
|
+
def __init__(self, session_id: str, expanded: bool):
|
|
112
|
+
super().__init__()
|
|
113
|
+
self.session_id = session_id
|
|
114
|
+
self.expanded = expanded
|
|
115
|
+
|
|
116
|
+
class StalledAgentVisited(events.Message):
|
|
117
|
+
"""Message sent when user visits a stalled agent (focus or click)"""
|
|
118
|
+
def __init__(self, session_id: str):
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.session_id = session_id
|
|
121
|
+
|
|
122
|
+
def watch_expanded(self, expanded: bool) -> None:
|
|
123
|
+
"""Called when expanded state changes"""
|
|
124
|
+
# Toggle CSS class for proper height
|
|
125
|
+
if expanded:
|
|
126
|
+
self.add_class("expanded")
|
|
127
|
+
else:
|
|
128
|
+
self.remove_class("expanded")
|
|
129
|
+
self.refresh(layout=True)
|
|
130
|
+
# Notify parent app to save state
|
|
131
|
+
self.post_message(self.ExpandedChanged(self.session.id, expanded))
|
|
132
|
+
|
|
133
|
+
def watch_detail_lines(self, detail_lines: int) -> None:
|
|
134
|
+
"""Called when detail_lines changes - force layout refresh"""
|
|
135
|
+
self.refresh(layout=True)
|
|
136
|
+
|
|
137
|
+
def update_status(self) -> None:
|
|
138
|
+
"""Update the detected status for this session.
|
|
139
|
+
|
|
140
|
+
NOTE: This is now VIEW-ONLY. Time tracking is handled by the Monitor Daemon.
|
|
141
|
+
We only detect status for display and capture pane content for the expanded view.
|
|
142
|
+
"""
|
|
143
|
+
# detect_status returns (status, activity, pane_content) - reuse content to avoid
|
|
144
|
+
# duplicate tmux subprocess calls (was 2 calls per widget, now just 1)
|
|
145
|
+
new_status, self.current_activity, content = self.status_detector.detect_status(self.session)
|
|
146
|
+
self.apply_status(new_status, self.current_activity, content)
|
|
147
|
+
|
|
148
|
+
def apply_status(self, status: str, activity: str, content: str) -> None:
|
|
149
|
+
"""Apply pre-fetched status data to this widget.
|
|
150
|
+
|
|
151
|
+
Used by parallel status updates to apply data fetched in background threads.
|
|
152
|
+
Note: This still fetches claude_stats synchronously - used for single widget updates.
|
|
153
|
+
"""
|
|
154
|
+
# Fetch claude stats (only for standalone update_status calls)
|
|
155
|
+
claude_stats = get_session_stats(self.session)
|
|
156
|
+
# Fetch git diff stats
|
|
157
|
+
git_diff = None
|
|
158
|
+
if self.session.start_directory:
|
|
159
|
+
git_diff = get_git_diff_stats(self.session.start_directory)
|
|
160
|
+
self.apply_status_no_refresh(status, activity, content, claude_stats, git_diff)
|
|
161
|
+
self.refresh()
|
|
162
|
+
|
|
163
|
+
def apply_status_no_refresh(self, status: str, activity: str, content: str, claude_stats: Optional[ClaudeSessionStats] = None, git_diff_stats: Optional[tuple] = None) -> None:
|
|
164
|
+
"""Apply pre-fetched status data without triggering refresh.
|
|
165
|
+
|
|
166
|
+
Used for batched updates where the caller will refresh once at the end.
|
|
167
|
+
All data including claude_stats should be pre-fetched in background thread.
|
|
168
|
+
"""
|
|
169
|
+
self.current_activity = activity
|
|
170
|
+
|
|
171
|
+
# Use pane content from detect_status (already fetched)
|
|
172
|
+
if content:
|
|
173
|
+
# Keep all lines including blanks for proper formatting, just strip trailing blanks
|
|
174
|
+
lines = content.rstrip().split('\n')
|
|
175
|
+
self.pane_content = lines[-50:] if lines else [] # Keep last 50 lines max
|
|
176
|
+
else:
|
|
177
|
+
self.pane_content = []
|
|
178
|
+
|
|
179
|
+
# Update detected status for display
|
|
180
|
+
# NOTE: Time tracking removed - Monitor Daemon is the single source of truth
|
|
181
|
+
# The session.stats values are read from what Monitor Daemon has persisted
|
|
182
|
+
# If session is asleep, keep the asleep status instead of the detected status
|
|
183
|
+
new_status = "asleep" if self.session.is_asleep else status
|
|
184
|
+
|
|
185
|
+
# Track status changes for immediate time-in-state reset (#73)
|
|
186
|
+
if new_status != self._last_known_status:
|
|
187
|
+
self._status_changed_at = datetime.now()
|
|
188
|
+
self._last_known_status = new_status
|
|
189
|
+
|
|
190
|
+
self.detected_status = new_status
|
|
191
|
+
|
|
192
|
+
# Use pre-fetched claude stats (no file I/O on main thread)
|
|
193
|
+
if claude_stats is not None:
|
|
194
|
+
self.claude_stats = claude_stats
|
|
195
|
+
|
|
196
|
+
# Use pre-fetched git diff stats
|
|
197
|
+
if git_diff_stats is not None:
|
|
198
|
+
self.git_diff_stats = git_diff_stats
|
|
199
|
+
|
|
200
|
+
def watch_summary_detail(self, summary_detail: str) -> None:
|
|
201
|
+
"""Called when summary_detail changes"""
|
|
202
|
+
self.refresh()
|
|
203
|
+
|
|
204
|
+
def watch_summary_content_mode(self, summary_content_mode: str) -> None:
|
|
205
|
+
"""Called when summary_content_mode changes (#74)"""
|
|
206
|
+
self.refresh()
|
|
207
|
+
|
|
208
|
+
def render(self) -> Text:
|
|
209
|
+
"""Render session summary (compact or expanded)"""
|
|
210
|
+
import shutil
|
|
211
|
+
s = self.session
|
|
212
|
+
stats = s.stats
|
|
213
|
+
term_width = shutil.get_terminal_size().columns
|
|
214
|
+
|
|
215
|
+
# Helper for monochrome styling - returns simplified style when monochrome enabled
|
|
216
|
+
def mono(colored: str, simple: str = "bold") -> str:
|
|
217
|
+
return simple if self.monochrome else colored
|
|
218
|
+
|
|
219
|
+
# Expansion indicator
|
|
220
|
+
expand_icon = "▼" if self.expanded else "▶"
|
|
221
|
+
|
|
222
|
+
# Calculate all values (only use what we need per level)
|
|
223
|
+
uptime = calculate_uptime(self.session.start_time)
|
|
224
|
+
repo_info = f"{s.repo_name or 'n/a'}:{s.branch or 'n/a'}"
|
|
225
|
+
green_time, non_green_time, sleep_time = get_current_state_times(
|
|
226
|
+
self.session.stats, is_asleep=self.session.is_asleep
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Get median work time from claude stats (or 0 if unavailable)
|
|
230
|
+
median_work = self.claude_stats.median_work_time if self.claude_stats else 0.0
|
|
231
|
+
|
|
232
|
+
# Status indicator - larger emoji circles based on detected status
|
|
233
|
+
# Blue background matching Textual header/footer style
|
|
234
|
+
# In monochrome mode, use no colors (just bold/dim for emphasis)
|
|
235
|
+
if self.monochrome:
|
|
236
|
+
bg = ""
|
|
237
|
+
status_symbol, _ = get_status_symbol(self.detected_status)
|
|
238
|
+
status_color = "bold"
|
|
239
|
+
else:
|
|
240
|
+
bg = " on #0d2137"
|
|
241
|
+
status_symbol, base_color = get_status_symbol(self.detected_status)
|
|
242
|
+
status_color = f"bold {base_color}{bg}"
|
|
243
|
+
|
|
244
|
+
# Permissiveness mode with emoji
|
|
245
|
+
if s.permissiveness_mode == "bypass":
|
|
246
|
+
perm_emoji = "🔥" # Fire - burning through all permissions
|
|
247
|
+
elif s.permissiveness_mode == "permissive":
|
|
248
|
+
perm_emoji = "🏃" # Running permissively
|
|
249
|
+
else:
|
|
250
|
+
perm_emoji = "👮" # Normal mode with permissions
|
|
251
|
+
|
|
252
|
+
content = Text()
|
|
253
|
+
|
|
254
|
+
# Determine name width based on detail level (more space in lower detail modes)
|
|
255
|
+
if self.summary_detail == "low":
|
|
256
|
+
name_width = 24
|
|
257
|
+
elif self.summary_detail == "med":
|
|
258
|
+
name_width = 20
|
|
259
|
+
else: # full
|
|
260
|
+
name_width = 16
|
|
261
|
+
|
|
262
|
+
# Truncate name if needed
|
|
263
|
+
display_name = s.name[:name_width].ljust(name_width)
|
|
264
|
+
|
|
265
|
+
# Always show: status symbol, time in state, expand icon, agent name
|
|
266
|
+
content.append(f"{status_symbol} ", style=status_color)
|
|
267
|
+
|
|
268
|
+
# Show 🔔 indicator for unvisited stalled agents (needs attention)
|
|
269
|
+
if self.is_unvisited_stalled:
|
|
270
|
+
content.append("🔔", style=mono(f"bold blink red{bg}", "bold"))
|
|
271
|
+
else:
|
|
272
|
+
content.append(" ", style=mono(f"dim{bg}", "dim")) # Maintain alignment
|
|
273
|
+
|
|
274
|
+
# Time in current state (directly after status light)
|
|
275
|
+
# Use locally tracked change time if more recent than daemon's state_since (#73)
|
|
276
|
+
state_start = None
|
|
277
|
+
if self._status_changed_at:
|
|
278
|
+
state_start = self._status_changed_at
|
|
279
|
+
if stats.state_since:
|
|
280
|
+
try:
|
|
281
|
+
daemon_state_start = datetime.fromisoformat(stats.state_since)
|
|
282
|
+
# Use whichever is more recent (our local detection or daemon's record)
|
|
283
|
+
if state_start is None or daemon_state_start > state_start:
|
|
284
|
+
state_start = daemon_state_start
|
|
285
|
+
except (ValueError, TypeError):
|
|
286
|
+
pass
|
|
287
|
+
if state_start:
|
|
288
|
+
elapsed = (datetime.now() - state_start).total_seconds()
|
|
289
|
+
content.append(f"{format_duration(elapsed):>5} ", style=status_color)
|
|
290
|
+
else:
|
|
291
|
+
content.append(" - ", style=mono(f"dim{bg}", "dim"))
|
|
292
|
+
|
|
293
|
+
# In list-mode, show focus indicator instead of expand icon
|
|
294
|
+
if "list-mode" in self.classes:
|
|
295
|
+
if self.has_focus:
|
|
296
|
+
content.append("→ ", style=status_color)
|
|
297
|
+
else:
|
|
298
|
+
content.append(" ", style=status_color)
|
|
299
|
+
else:
|
|
300
|
+
content.append(f"{expand_icon} ", style=status_color)
|
|
301
|
+
content.append(f"{display_name}", style=mono(f"bold cyan{bg}", "bold"))
|
|
302
|
+
|
|
303
|
+
# Full detail: add repo:branch (padded to longest across all sessions)
|
|
304
|
+
if self.summary_detail == "full":
|
|
305
|
+
repo_width = getattr(self.app, 'max_repo_info_width', 18)
|
|
306
|
+
content.append(f" {repo_info:<{repo_width}} ", style=mono(f"bold dim{bg}", "dim"))
|
|
307
|
+
|
|
308
|
+
# Med/Full detail: add uptime, running time, stalled time, sleep time
|
|
309
|
+
if self.summary_detail in ("med", "full"):
|
|
310
|
+
content.append(f" ↑{uptime:>5}", style=mono(f"bold white{bg}", "bold"))
|
|
311
|
+
content.append(f" ▶{format_duration(green_time):>5}", style=mono(f"bold green{bg}", "bold"))
|
|
312
|
+
content.append(f" ⏸{format_duration(non_green_time):>5}", style=mono(f"bold red{bg}", "dim"))
|
|
313
|
+
# Show sleep time (#141) - always show for alignment, dim when 0
|
|
314
|
+
# Build complete column string with explicit padding to ensure consistent width
|
|
315
|
+
# Use 8 total cells: space(1) + emoji(2) + value(5) = 8
|
|
316
|
+
sleep_str = format_duration(sleep_time) if sleep_time > 0 else "-"
|
|
317
|
+
sleep_col = f" 💤{sleep_str:>5}" # This should be 8 cells
|
|
318
|
+
sleep_style = mono(f"bold cyan{bg}", "bold") if sleep_time > 0 else mono(f"dim cyan{bg}", "dim")
|
|
319
|
+
content.append(sleep_col, style=sleep_style)
|
|
320
|
+
# Full detail: show percentage active (excludes sleep time from total)
|
|
321
|
+
if self.summary_detail == "full":
|
|
322
|
+
active_time = green_time + non_green_time
|
|
323
|
+
pct = (green_time / active_time * 100) if active_time > 0 else 0
|
|
324
|
+
content.append(f" {pct:>3.0f}%", style=mono(f"bold green{bg}" if pct >= 50 else f"bold red{bg}", "bold"))
|
|
325
|
+
|
|
326
|
+
# Always show: token usage or cost (from Claude Code)
|
|
327
|
+
# ALIGNMENT: context indicator is always 7 chars " c@NNN%" (or placeholder)
|
|
328
|
+
if self.claude_stats is not None:
|
|
329
|
+
if self.show_cost:
|
|
330
|
+
# Show estimated cost instead of tokens
|
|
331
|
+
cost = s.stats.estimated_cost_usd
|
|
332
|
+
content.append(f" {format_cost(cost):>7}", style=mono(f"bold orange1{bg}", "bold"))
|
|
333
|
+
else:
|
|
334
|
+
content.append(f" Σ{format_tokens(self.claude_stats.total_tokens):>6}", style=mono(f"bold orange1{bg}", "bold"))
|
|
335
|
+
# Show current context window usage as percentage (assuming 200K max)
|
|
336
|
+
if self.claude_stats.current_context_tokens > 0:
|
|
337
|
+
max_context = 200_000 # Claude models have 200K context window
|
|
338
|
+
ctx_pct = min(100, self.claude_stats.current_context_tokens / max_context * 100)
|
|
339
|
+
content.append(f" c@{ctx_pct:>3.0f}%", style=mono(f"bold orange1{bg}", "bold"))
|
|
340
|
+
else:
|
|
341
|
+
content.append(" c@ -%", style=mono(f"dim orange1{bg}", "dim"))
|
|
342
|
+
else:
|
|
343
|
+
content.append(" - c@ -%", style=mono(f"dim orange1{bg}", "dim"))
|
|
344
|
+
|
|
345
|
+
# Git diff stats (outstanding changes since last commit)
|
|
346
|
+
# ALIGNMENT: Use fixed widths - low/med: 4 chars "Δnn ", full: 16 chars "Δnn +nnnn -nnnn"
|
|
347
|
+
# Large line counts are shortened: 173242 -> "173K", 1234567 -> "1.2M"
|
|
348
|
+
if self.git_diff_stats:
|
|
349
|
+
files, ins, dels = self.git_diff_stats
|
|
350
|
+
if self.summary_detail == "full":
|
|
351
|
+
# Full: show files and lines with fixed widths
|
|
352
|
+
content.append(f" Δ{files:>2}", style=mono(f"bold magenta{bg}", "bold"))
|
|
353
|
+
content.append(f" +{format_line_count(ins):>4}", style=mono(f"bold green{bg}", "bold"))
|
|
354
|
+
content.append(f" -{format_line_count(dels):>4}", style=mono(f"bold red{bg}", "dim"))
|
|
355
|
+
else:
|
|
356
|
+
# Compact: just files changed (fixed 4 char width)
|
|
357
|
+
content.append(f" Δ{files:>2}", style=mono(f"bold magenta{bg}" if files > 0 else f"dim{bg}", "bold" if files > 0 else "dim"))
|
|
358
|
+
else:
|
|
359
|
+
# Placeholder matching width for alignment
|
|
360
|
+
if self.summary_detail == "full":
|
|
361
|
+
content.append(" Δ- + - - ", style=mono(f"dim{bg}", "dim"))
|
|
362
|
+
else:
|
|
363
|
+
content.append(" Δ-", style=mono(f"dim{bg}", "dim"))
|
|
364
|
+
|
|
365
|
+
# Med/Full detail: add median work time (p50 autonomous work duration)
|
|
366
|
+
if self.summary_detail in ("med", "full"):
|
|
367
|
+
work_str = format_duration(median_work) if median_work > 0 else "0s"
|
|
368
|
+
content.append(f" ⏱{work_str:>5}", style=mono(f"bold blue{bg}", "bold"))
|
|
369
|
+
|
|
370
|
+
# Always show: permission mode, human interactions, robot supervisions
|
|
371
|
+
content.append(f" {perm_emoji}", style=mono(f"bold white{bg}", "bold"))
|
|
372
|
+
# Human interaction count = total interactions - robot interventions
|
|
373
|
+
if self.claude_stats is not None:
|
|
374
|
+
human_count = max(0, self.claude_stats.interaction_count - stats.steers_count)
|
|
375
|
+
content.append(f" 👤{human_count:>3}", style=mono(f"bold yellow{bg}", "bold"))
|
|
376
|
+
else:
|
|
377
|
+
content.append(" 👤 -", style=mono(f"dim yellow{bg}", "dim"))
|
|
378
|
+
# Robot supervision count (from daemon steers) - 3 digit padding
|
|
379
|
+
content.append(f" 🤖{stats.steers_count:>3}", style=mono(f"bold cyan{bg}", "bold"))
|
|
380
|
+
|
|
381
|
+
# Standing orders indicator (after supervision count) - always show for alignment
|
|
382
|
+
if s.standing_instructions:
|
|
383
|
+
if s.standing_orders_complete:
|
|
384
|
+
content.append(" ✓", style=mono(f"bold green{bg}", "bold"))
|
|
385
|
+
elif s.standing_instructions_preset:
|
|
386
|
+
# Show preset name (truncated to fit)
|
|
387
|
+
preset_display = f" {s.standing_instructions_preset[:8]}"
|
|
388
|
+
content.append(preset_display, style=mono(f"bold cyan{bg}", "bold"))
|
|
389
|
+
else:
|
|
390
|
+
content.append(" 📋", style=mono(f"bold yellow{bg}", "bold"))
|
|
391
|
+
else:
|
|
392
|
+
content.append(" ➖", style=mono(f"bold dim{bg}", "dim")) # No instructions indicator
|
|
393
|
+
|
|
394
|
+
# Agent value indicator (#61)
|
|
395
|
+
# Full detail: show numeric value with money bag
|
|
396
|
+
# Short/med: show priority chevrons (⏫ high, ⏹ normal, ⏬ low)
|
|
397
|
+
if self.summary_detail == "full":
|
|
398
|
+
content.append(f" 💰{s.agent_value:>4}", style=mono(f"bold magenta{bg}", "bold"))
|
|
399
|
+
else:
|
|
400
|
+
# Priority icon based on value relative to default 1000
|
|
401
|
+
# Note: Rich measures ⏹️ as 2 cells but ⏫️/⏬️ as 3 cells, so we add
|
|
402
|
+
# a trailing space to ⏹️ for alignment
|
|
403
|
+
if s.agent_value > 1000:
|
|
404
|
+
content.append(" ⏫️", style=mono(f"bold red{bg}", "bold")) # High priority
|
|
405
|
+
elif s.agent_value < 1000:
|
|
406
|
+
content.append(" ⏬️", style=mono(f"bold blue{bg}", "bold")) # Low priority
|
|
407
|
+
else:
|
|
408
|
+
content.append(" ⏹️ ", style=mono(f"dim{bg}", "dim")) # Normal (extra space for alignment)
|
|
409
|
+
|
|
410
|
+
if not self.expanded:
|
|
411
|
+
# Compact view: show content based on summary_content_mode (#74)
|
|
412
|
+
content.append(" │ ", style=mono(f"bold dim{bg}", "dim"))
|
|
413
|
+
# Calculate remaining space for content
|
|
414
|
+
current_len = len(content.plain)
|
|
415
|
+
remaining = max(20, term_width - current_len - 2)
|
|
416
|
+
|
|
417
|
+
# Determine what to show based on mode
|
|
418
|
+
mode = self.summary_content_mode
|
|
419
|
+
|
|
420
|
+
if mode == "annotation":
|
|
421
|
+
# Show human annotation (✏️ icon)
|
|
422
|
+
if s.human_annotation:
|
|
423
|
+
content.append(f"✏️ {s.human_annotation[:remaining-3]}", style=mono(f"bold magenta{bg}", "bold"))
|
|
424
|
+
else:
|
|
425
|
+
content.append("✏️ (no annotation)", style=mono(f"dim italic{bg}", "dim"))
|
|
426
|
+
elif mode == "orders":
|
|
427
|
+
# Show standing orders (🎯 icon, ✓ if complete)
|
|
428
|
+
if s.standing_instructions:
|
|
429
|
+
if s.standing_orders_complete:
|
|
430
|
+
order_style = mono(f"bold green{bg}", "bold")
|
|
431
|
+
prefix = "🎯✓ "
|
|
432
|
+
elif s.standing_instructions_preset:
|
|
433
|
+
order_style = mono(f"bold cyan{bg}", "bold")
|
|
434
|
+
prefix = f"🎯 {s.standing_instructions_preset}: "
|
|
435
|
+
else:
|
|
436
|
+
order_style = mono(f"bold italic yellow{bg}", "bold")
|
|
437
|
+
prefix = "🎯 "
|
|
438
|
+
display_text = f"{prefix}{format_standing_instructions(s.standing_instructions, remaining - len(prefix))}"
|
|
439
|
+
content.append(display_text[:remaining], style=order_style)
|
|
440
|
+
else:
|
|
441
|
+
content.append("🎯 (no standing orders)", style=mono(f"dim italic{bg}", "dim"))
|
|
442
|
+
elif mode == "ai_long":
|
|
443
|
+
# ai_long: show context summary (📖 icon - wider context/goal from AI)
|
|
444
|
+
if self.ai_summary_context:
|
|
445
|
+
content.append(f"📖 {self.ai_summary_context[:remaining-3]}", style=mono(f"bold italic{bg}", "bold"))
|
|
446
|
+
elif not self.summarizer_enabled:
|
|
447
|
+
content.append("📖 (summarizer disabled - press 'a')", style=mono(f"dim italic{bg}", "dim"))
|
|
448
|
+
else:
|
|
449
|
+
content.append("📖 (awaiting context...)", style=mono(f"dim italic{bg}", "dim"))
|
|
450
|
+
else:
|
|
451
|
+
# ai_short: show short summary (💬 icon - current activity from AI)
|
|
452
|
+
if self.ai_summary_short:
|
|
453
|
+
content.append(f"💬 {self.ai_summary_short[:remaining-3]}", style=mono(f"bold italic{bg}", "bold"))
|
|
454
|
+
elif not self.summarizer_enabled:
|
|
455
|
+
content.append("💬 (summarizer disabled - press 'a')", style=mono(f"dim italic{bg}", "dim"))
|
|
456
|
+
else:
|
|
457
|
+
content.append("💬 (awaiting summary...)", style=mono(f"dim italic{bg}", "dim"))
|
|
458
|
+
|
|
459
|
+
# Pad to fill terminal width
|
|
460
|
+
current_len = len(content.plain)
|
|
461
|
+
if current_len < term_width:
|
|
462
|
+
content.append(" " * (term_width - current_len), style=mono(f"{bg}", ""))
|
|
463
|
+
return content
|
|
464
|
+
|
|
465
|
+
# Pad header line to full width before adding expanded content
|
|
466
|
+
current_len = len(content.plain)
|
|
467
|
+
if current_len < term_width:
|
|
468
|
+
content.append(" " * (term_width - current_len), style=mono(f"{bg}", ""))
|
|
469
|
+
|
|
470
|
+
# Expanded view: show standing instructions first if set
|
|
471
|
+
if s.standing_instructions:
|
|
472
|
+
content.append("\n")
|
|
473
|
+
content.append(" ")
|
|
474
|
+
display_instr = format_standing_instructions(s.standing_instructions)
|
|
475
|
+
if s.standing_orders_complete:
|
|
476
|
+
content.append("│ ", style=mono("bold green", "bold"))
|
|
477
|
+
content.append("✓ ", style=mono("bold green", "bold"))
|
|
478
|
+
content.append(display_instr, style=mono("green", ""))
|
|
479
|
+
elif s.standing_instructions_preset:
|
|
480
|
+
content.append("│ ", style=mono("cyan", "dim"))
|
|
481
|
+
content.append(f"{s.standing_instructions_preset}: ", style=mono("bold cyan", "bold"))
|
|
482
|
+
content.append(display_instr, style=mono("cyan", ""))
|
|
483
|
+
else:
|
|
484
|
+
content.append("│ ", style=mono("cyan", "dim"))
|
|
485
|
+
content.append("📋 ", style=mono("yellow", "bold"))
|
|
486
|
+
content.append(display_instr, style=mono("italic yellow", "italic"))
|
|
487
|
+
|
|
488
|
+
# Expanded view: show pane content based on detail_lines setting
|
|
489
|
+
lines_to_show = self.detail_lines
|
|
490
|
+
# Account for standing instructions line if present
|
|
491
|
+
if s.standing_instructions:
|
|
492
|
+
lines_to_show = max(1, lines_to_show - 1)
|
|
493
|
+
|
|
494
|
+
# Get the last N lines of pane content
|
|
495
|
+
pane_lines = self.pane_content[-lines_to_show:] if self.pane_content else []
|
|
496
|
+
|
|
497
|
+
# Show pane output lines
|
|
498
|
+
for line in pane_lines:
|
|
499
|
+
content.append("\n")
|
|
500
|
+
content.append(" ") # Indent
|
|
501
|
+
# Truncate long lines and style based on content
|
|
502
|
+
display_line = line[:100] + "..." if len(line) > 100 else line
|
|
503
|
+
prefix_style, content_style = style_pane_line(line)
|
|
504
|
+
content.append("│ ", style=prefix_style)
|
|
505
|
+
content.append(display_line, style=content_style)
|
|
506
|
+
|
|
507
|
+
# If no pane content and no standing instructions shown above, show placeholder
|
|
508
|
+
if not pane_lines and not s.standing_instructions:
|
|
509
|
+
content.append("\n")
|
|
510
|
+
content.append(" ") # Indent
|
|
511
|
+
content.append("│ ", style=mono("cyan", "dim"))
|
|
512
|
+
content.append("(no output)", style=mono("dim italic", "dim"))
|
|
513
|
+
|
|
514
|
+
return content
|