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.
Files changed (41) hide show
  1. overcode/__init__.py +1 -1
  2. overcode/cli.py +7 -2
  3. overcode/implementations.py +74 -8
  4. overcode/monitor_daemon.py +60 -65
  5. overcode/monitor_daemon_core.py +261 -0
  6. overcode/monitor_daemon_state.py +7 -0
  7. overcode/session_manager.py +1 -0
  8. overcode/settings.py +22 -0
  9. overcode/supervisor_daemon.py +48 -47
  10. overcode/supervisor_daemon_core.py +210 -0
  11. overcode/testing/__init__.py +6 -0
  12. overcode/testing/renderer.py +268 -0
  13. overcode/testing/tmux_driver.py +223 -0
  14. overcode/testing/tui_eye.py +185 -0
  15. overcode/testing/tui_eye_skill.md +187 -0
  16. overcode/tmux_manager.py +17 -3
  17. overcode/tui.py +196 -2462
  18. overcode/tui_actions/__init__.py +20 -0
  19. overcode/tui_actions/daemon.py +201 -0
  20. overcode/tui_actions/input.py +128 -0
  21. overcode/tui_actions/navigation.py +117 -0
  22. overcode/tui_actions/session.py +428 -0
  23. overcode/tui_actions/view.py +357 -0
  24. overcode/tui_helpers.py +41 -9
  25. overcode/tui_logic.py +347 -0
  26. overcode/tui_render.py +414 -0
  27. overcode/tui_widgets/__init__.py +24 -0
  28. overcode/tui_widgets/command_bar.py +399 -0
  29. overcode/tui_widgets/daemon_panel.py +153 -0
  30. overcode/tui_widgets/daemon_status_bar.py +245 -0
  31. overcode/tui_widgets/help_overlay.py +71 -0
  32. overcode/tui_widgets/preview_pane.py +69 -0
  33. overcode/tui_widgets/session_summary.py +514 -0
  34. overcode/tui_widgets/status_timeline.py +253 -0
  35. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/METADATA +3 -1
  36. overcode-0.1.4.dist-info/RECORD +68 -0
  37. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
  38. overcode-0.1.3.dist-info/RECORD +0 -45
  39. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/WHEEL +0 -0
  40. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
  41. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,399 @@
1
+ """
2
+ Command bar widget for TUI.
3
+
4
+ Inline command bar for sending instructions to agents.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from textual.widgets import Static, Label, Input, TextArea
10
+ from textual.containers import Horizontal
11
+ from textual.reactive import reactive
12
+ from textual.app import ComposeResult
13
+ from textual.message import Message
14
+ from textual import events
15
+
16
+
17
+ class CommandBar(Static):
18
+ """Inline command bar for sending instructions to agents.
19
+
20
+ Supports single-line (Input) and multi-line (TextArea) modes.
21
+ Toggle with Ctrl+E. Send with Enter (single) or Ctrl+Enter (multi).
22
+ Use Ctrl+O to set as standing order instead of sending.
23
+
24
+ Modes:
25
+ - "send": Default mode for sending instructions to an agent
26
+ - "standing_orders": Mode for editing standing orders for an agent
27
+ - "new_agent_dir": First step of new agent creation - enter working directory
28
+ - "new_agent_name": Second step of new agent creation - enter agent name
29
+ - "new_agent_perms": Third step of new agent creation - choose permission mode
30
+
31
+ Key handling is done via on_key() since Input/TextArea consume most keys.
32
+ """
33
+
34
+ expanded = reactive(False) # Toggle single/multi-line mode
35
+ target_session: Optional[str] = None
36
+ mode: str = "send" # "send", "standing_orders", "new_agent_dir", "new_agent_name", or "new_agent_perms"
37
+ new_agent_dir: Optional[str] = None # Store directory between steps
38
+ new_agent_name: Optional[str] = None # Store name between steps
39
+
40
+ class SendRequested(Message):
41
+ """Message sent when user wants to send text to a session."""
42
+ def __init__(self, session_name: str, text: str):
43
+ super().__init__()
44
+ self.session_name = session_name
45
+ self.text = text
46
+
47
+ class StandingOrderRequested(Message):
48
+ """Message sent when user wants to set a standing order."""
49
+ def __init__(self, session_name: str, text: str):
50
+ super().__init__()
51
+ self.session_name = session_name
52
+ self.text = text
53
+
54
+ class NewAgentRequested(Message):
55
+ """Message sent when user wants to create a new agent."""
56
+ def __init__(self, agent_name: str, directory: Optional[str] = None, bypass_permissions: bool = False):
57
+ super().__init__()
58
+ self.agent_name = agent_name
59
+ self.directory = directory
60
+ self.bypass_permissions = bypass_permissions
61
+
62
+ class ValueUpdated(Message):
63
+ """Message sent when user updates agent value (#61)."""
64
+ def __init__(self, session_name: str, value: int):
65
+ super().__init__()
66
+ self.session_name = session_name
67
+ self.value = value
68
+
69
+ class AnnotationUpdated(Message):
70
+ """Message sent when user updates human annotation (#74)."""
71
+ def __init__(self, session_name: str, annotation: str):
72
+ super().__init__()
73
+ self.session_name = session_name
74
+ self.annotation = annotation
75
+
76
+ class ClearRequested(Message):
77
+ """Message sent when user clears the command bar."""
78
+ pass
79
+
80
+ def compose(self) -> ComposeResult:
81
+ """Create command bar widgets."""
82
+ with Horizontal(id="cmd-bar-container"):
83
+ yield Label("", id="target-label")
84
+ yield Input(id="cmd-input", placeholder="Type instruction (Enter to send)...", disabled=True)
85
+ yield TextArea(id="cmd-textarea", classes="hidden", disabled=True)
86
+ yield Label("[^E]", id="expand-hint")
87
+
88
+ def on_mount(self) -> None:
89
+ """Initialize command bar state."""
90
+ self._update_target_label()
91
+ # Ensure widgets start disabled to prevent auto-focus
92
+ self.query_one("#cmd-input", Input).disabled = True
93
+ self.query_one("#cmd-textarea", TextArea).disabled = True
94
+
95
+ def _update_target_label(self) -> None:
96
+ """Update the target session label based on mode."""
97
+ label = self.query_one("#target-label", Label)
98
+ input_widget = self.query_one("#cmd-input", Input)
99
+
100
+ if self.mode == "new_agent_dir":
101
+ label.update("[New Agent: Directory] ")
102
+ input_widget.placeholder = "Enter working directory path..."
103
+ elif self.mode == "new_agent_name":
104
+ label.update("[New Agent: Name] ")
105
+ input_widget.placeholder = "Enter agent name (or Enter to accept default)..."
106
+ elif self.mode == "new_agent_perms":
107
+ label.update("[New Agent: Permissions] ")
108
+ input_widget.placeholder = "Type 'bypass' for --dangerously-skip-permissions, or Enter for normal..."
109
+ elif self.mode == "standing_orders":
110
+ if self.target_session:
111
+ label.update(f"[{self.target_session} Standing Orders] ")
112
+ else:
113
+ label.update("[Standing Orders] ")
114
+ input_widget.placeholder = "Enter standing orders (or empty to clear)..."
115
+ elif self.mode == "value":
116
+ if self.target_session:
117
+ label.update(f"[{self.target_session} Value] ")
118
+ else:
119
+ label.update("[Value] ")
120
+ input_widget.placeholder = "Enter priority value (1000 = normal, higher = more important)..."
121
+ elif self.mode == "annotation":
122
+ if self.target_session:
123
+ label.update(f"[{self.target_session} Annotation] ")
124
+ else:
125
+ label.update("[Annotation] ")
126
+ input_widget.placeholder = "Enter human annotation (or empty to clear)..."
127
+ elif self.target_session:
128
+ label.update(f"[{self.target_session}] ")
129
+ input_widget.placeholder = "Type instruction (Enter to send)..."
130
+ else:
131
+ label.update("[no session] ")
132
+ input_widget.placeholder = "Type instruction (Enter to send)..."
133
+
134
+ def set_target(self, session_name: Optional[str]) -> None:
135
+ """Set the target session for commands."""
136
+ self.target_session = session_name
137
+ self.mode = "send" # Reset to send mode when target changes
138
+ self._update_target_label()
139
+
140
+ def set_mode(self, mode: str) -> None:
141
+ """Set the command bar mode ('send' or 'new_agent')."""
142
+ self.mode = mode
143
+ self._update_target_label()
144
+
145
+ def watch_expanded(self, expanded: bool) -> None:
146
+ """Toggle between single-line and multi-line mode."""
147
+ input_widget = self.query_one("#cmd-input", Input)
148
+ textarea = self.query_one("#cmd-textarea", TextArea)
149
+
150
+ if expanded:
151
+ # Switch to multi-line
152
+ input_widget.add_class("hidden")
153
+ input_widget.disabled = True
154
+ textarea.remove_class("hidden")
155
+ textarea.disabled = False
156
+ # Transfer content
157
+ textarea.text = input_widget.value
158
+ input_widget.value = ""
159
+ textarea.focus()
160
+ else:
161
+ # Switch to single-line
162
+ textarea.add_class("hidden")
163
+ textarea.disabled = True
164
+ input_widget.remove_class("hidden")
165
+ input_widget.disabled = False
166
+ # Transfer content (first line only for single-line)
167
+ if textarea.text:
168
+ first_line = textarea.text.split('\n')[0]
169
+ input_widget.value = first_line
170
+ textarea.text = ""
171
+ input_widget.focus()
172
+
173
+ def on_key(self, event: events.Key) -> None:
174
+ """Handle key events for command bar shortcuts."""
175
+ if event.key == "ctrl+e":
176
+ self.action_toggle_expand()
177
+ event.stop()
178
+ elif event.key == "ctrl+o":
179
+ self.action_set_standing_order()
180
+ event.stop()
181
+ elif event.key == "escape":
182
+ self.action_clear_and_unfocus()
183
+ event.stop()
184
+ elif event.key == "ctrl+enter" and self.expanded:
185
+ self.action_send_multiline()
186
+ event.stop()
187
+
188
+ def on_input_submitted(self, event: Input.Submitted) -> None:
189
+ """Handle Enter in single-line mode."""
190
+ if event.input.id == "cmd-input":
191
+ text = event.value.strip()
192
+
193
+ if self.mode == "new_agent_dir":
194
+ # Step 1: Directory entered, validate and move to name step
195
+ # Note: _handle_new_agent_dir sets input value to default name, don't clear it
196
+ self._handle_new_agent_dir(text if text else None)
197
+ return
198
+ elif self.mode == "new_agent_name":
199
+ # Step 2: Name entered (or default accepted), move to permissions step
200
+ # If empty, use the pre-filled default
201
+ name = text if text else event.input.value.strip()
202
+ if not name:
203
+ # Derive from directory as fallback
204
+ from pathlib import Path
205
+ name = Path(self.new_agent_dir).name if self.new_agent_dir else "agent"
206
+ self._handle_new_agent_name(name)
207
+ event.input.value = ""
208
+ return
209
+ elif self.mode == "new_agent_perms":
210
+ # Step 3: Permissions chosen, create agent
211
+ bypass = text.lower().strip() in ("bypass", "y", "yes", "!")
212
+ self._create_new_agent(self.new_agent_name, bypass)
213
+ event.input.value = ""
214
+ self.action_clear_and_unfocus()
215
+ return
216
+ elif self.mode == "standing_orders":
217
+ # Set standing orders (empty string clears them)
218
+ self._set_standing_order(text)
219
+ event.input.value = ""
220
+ self.action_clear_and_unfocus()
221
+ return
222
+ elif self.mode == "value":
223
+ # Set agent value (#61)
224
+ self._set_value(text)
225
+ event.input.value = ""
226
+ self.action_clear_and_unfocus()
227
+ return
228
+ elif self.mode == "annotation":
229
+ # Set human annotation (empty string clears it)
230
+ self._set_annotation(text)
231
+ event.input.value = ""
232
+ self.action_clear_and_unfocus()
233
+ return
234
+
235
+ # Default "send" mode
236
+ if not text:
237
+ return
238
+ self._send_message(text)
239
+ event.input.value = ""
240
+ self.action_clear_and_unfocus()
241
+
242
+ def _send_message(self, text: str) -> None:
243
+ """Send message to target session."""
244
+ if not self.target_session or not text.strip():
245
+ return
246
+ self.post_message(self.SendRequested(self.target_session, text.strip()))
247
+
248
+ def _handle_new_agent_dir(self, directory: Optional[str]) -> None:
249
+ """Handle directory input for new agent creation.
250
+
251
+ Validates directory and transitions to name input step.
252
+ """
253
+ from pathlib import Path
254
+
255
+ # Expand ~ and resolve path
256
+ if directory:
257
+ dir_path = Path(directory).expanduser().resolve()
258
+ if not dir_path.exists():
259
+ # Create the directory
260
+ try:
261
+ dir_path.mkdir(parents=True, exist_ok=True)
262
+ self.app.notify(f"Created directory: {dir_path}", severity="information")
263
+ except OSError as e:
264
+ self.app.notify(f"Failed to create directory: {e}", severity="error")
265
+ return
266
+ if not dir_path.is_dir():
267
+ self.app.notify(f"Not a directory: {dir_path}", severity="error")
268
+ return
269
+ self.new_agent_dir = str(dir_path)
270
+ else:
271
+ # Use current working directory if none specified
272
+ self.new_agent_dir = str(Path.cwd())
273
+
274
+ # Derive default agent name from directory basename (#131)
275
+ # If an agent with that name exists, increment (foo -> foo2 -> foo3)
276
+ base_name = Path(self.new_agent_dir).name
277
+ default_name = self._get_unique_agent_name(base_name)
278
+
279
+ # Transition to name step
280
+ self.mode = "new_agent_name"
281
+ self._update_target_label()
282
+
283
+ # Pre-fill the input with the default name
284
+ input_widget = self.query_one("#cmd-input", Input)
285
+ input_widget.value = default_name
286
+
287
+ def _get_unique_agent_name(self, base_name: str) -> str:
288
+ """Get a unique agent name by incrementing suffix if needed (#131).
289
+
290
+ Args:
291
+ base_name: The base name to start with (e.g., directory name)
292
+
293
+ Returns:
294
+ A unique name: base_name if available, else base_name2, base_name3, etc.
295
+ """
296
+ # Check if base name is available
297
+ if not self.app.session_manager.get_session_by_name(base_name):
298
+ return base_name
299
+
300
+ # Try incrementing suffix until we find an unused name
301
+ suffix = 2
302
+ while suffix < 100: # Reasonable limit
303
+ candidate = f"{base_name}{suffix}"
304
+ if not self.app.session_manager.get_session_by_name(candidate):
305
+ return candidate
306
+ suffix += 1
307
+
308
+ # Fallback (very unlikely to reach)
309
+ return f"{base_name}_{suffix}"
310
+
311
+ def _handle_new_agent_name(self, name: str) -> None:
312
+ """Handle name input for new agent creation.
313
+
314
+ Stores the name and transitions to permissions step.
315
+ """
316
+ self.new_agent_name = name
317
+
318
+ # Transition to permissions step
319
+ self.mode = "new_agent_perms"
320
+ self._update_target_label()
321
+
322
+ def _create_new_agent(self, name: str, bypass_permissions: bool = False) -> None:
323
+ """Create a new agent with the given name, directory, and permission mode."""
324
+ self.post_message(self.NewAgentRequested(name, self.new_agent_dir, bypass_permissions))
325
+ # Reset state
326
+ self.new_agent_dir = None
327
+ self.new_agent_name = None
328
+ self.mode = "send"
329
+ self._update_target_label()
330
+
331
+ def _set_standing_order(self, text: str) -> None:
332
+ """Set text as standing order (empty string clears orders)."""
333
+ if not self.target_session:
334
+ return
335
+ self.post_message(self.StandingOrderRequested(self.target_session, text.strip()))
336
+
337
+ def _set_value(self, text: str) -> None:
338
+ """Set agent value (#61)."""
339
+ if not self.target_session:
340
+ return
341
+ try:
342
+ value = int(text.strip()) if text.strip() else 1000
343
+ if value < 0 or value > 9999:
344
+ self.app.notify("Value must be between 0 and 9999", severity="error")
345
+ return
346
+ self.post_message(self.ValueUpdated(self.target_session, value))
347
+ except ValueError:
348
+ # Invalid input, notify user but don't crash
349
+ self.app.notify("Invalid value - please enter a number", severity="error")
350
+
351
+ def _set_annotation(self, text: str) -> None:
352
+ """Set human annotation (empty string clears it) (#74)."""
353
+ if not self.target_session:
354
+ return
355
+ self.post_message(self.AnnotationUpdated(self.target_session, text.strip()))
356
+
357
+ def action_toggle_expand(self) -> None:
358
+ """Toggle between single and multi-line mode."""
359
+ self.expanded = not self.expanded
360
+
361
+ def action_send_multiline(self) -> None:
362
+ """Send content from multi-line textarea."""
363
+ textarea = self.query_one("#cmd-textarea", TextArea)
364
+ self._send_message(textarea.text)
365
+ textarea.text = ""
366
+ self.action_clear_and_unfocus()
367
+
368
+ def action_set_standing_order(self) -> None:
369
+ """Set current content as standing order."""
370
+ if self.expanded:
371
+ textarea = self.query_one("#cmd-textarea", TextArea)
372
+ self._set_standing_order(textarea.text)
373
+ textarea.text = ""
374
+ else:
375
+ input_widget = self.query_one("#cmd-input", Input)
376
+ self._set_standing_order(input_widget.value)
377
+ input_widget.value = ""
378
+
379
+ def action_clear_and_unfocus(self) -> None:
380
+ """Clear input and unfocus command bar."""
381
+ if self.expanded:
382
+ textarea = self.query_one("#cmd-textarea", TextArea)
383
+ textarea.text = ""
384
+ else:
385
+ input_widget = self.query_one("#cmd-input", Input)
386
+ input_widget.value = ""
387
+ # Reset mode and state
388
+ self.mode = "send"
389
+ self.new_agent_dir = None
390
+ self.new_agent_name = None
391
+ self._update_target_label()
392
+ # Let parent handle unfocus
393
+ self.post_message(self.ClearRequested())
394
+
395
+ def focus_input(self) -> None:
396
+ """Focus the command bar input and enable it."""
397
+ input_widget = self.query_one("#cmd-input", Input)
398
+ input_widget.disabled = False
399
+ input_widget.focus()
@@ -0,0 +1,153 @@
1
+ """
2
+ Daemon panel widget for TUI.
3
+
4
+ Inline panel showing daemon status and log viewer.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Optional
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 ..settings import get_session_dir
15
+ from ..tui_helpers import (
16
+ format_interval,
17
+ format_ago,
18
+ get_daemon_status_style,
19
+ )
20
+
21
+
22
+ class DaemonPanel(Static):
23
+ """Inline daemon panel with status and log viewer (like timeline)"""
24
+
25
+ LOG_LINES_TO_SHOW = 8 # Number of log lines to display
26
+
27
+ def __init__(self, tmux_session: str = "agents", *args, **kwargs):
28
+ super().__init__(*args, **kwargs)
29
+ self.tmux_session = tmux_session
30
+ self.log_lines: list[str] = []
31
+ self.monitor_state: Optional[MonitorDaemonState] = None
32
+ self._log_file_pos = 0
33
+
34
+ def on_mount(self) -> None:
35
+ """Start log tailing when mounted"""
36
+ self.set_interval(1.0, self._refresh_logs)
37
+ self._refresh_logs()
38
+
39
+ def _refresh_logs(self) -> None:
40
+ """Refresh daemon status and logs"""
41
+ from pathlib import Path
42
+
43
+ # Only refresh if visible
44
+ if not self.display:
45
+ return
46
+
47
+ # Update daemon state from Monitor Daemon
48
+ self.monitor_state = get_monitor_daemon_state(self.tmux_session)
49
+
50
+ # Read log lines from session-specific monitor_daemon.log
51
+ session_dir = get_session_dir(self.tmux_session)
52
+ log_file = session_dir / "monitor_daemon.log"
53
+ if log_file.exists():
54
+ try:
55
+ with open(log_file, 'r') as f:
56
+ if not self.log_lines:
57
+ # First read: get last 100 lines of file
58
+ all_lines = f.readlines()
59
+ self.log_lines = [l.rstrip() for l in all_lines[-100:]]
60
+ self._log_file_pos = f.tell()
61
+ else:
62
+ # Subsequent reads: only get new content
63
+ f.seek(self._log_file_pos)
64
+ new_content = f.read()
65
+ self._log_file_pos = f.tell()
66
+
67
+ if new_content:
68
+ new_lines = new_content.strip().split('\n')
69
+ self.log_lines.extend(new_lines)
70
+ # Keep last 100 lines
71
+ self.log_lines = self.log_lines[-100:]
72
+ except (OSError, IOError, ValueError):
73
+ # Log file not available, read error, or seek error
74
+ pass
75
+
76
+ self.refresh()
77
+
78
+ def render(self) -> Text:
79
+ """Render daemon panel inline (similar to timeline style)"""
80
+ content = Text()
81
+
82
+ # Header with status - match DaemonStatusBar format exactly
83
+ content.append("🤖 Supervisor Daemon: ", style="bold")
84
+
85
+ # Check Monitor Daemon state
86
+ if self.monitor_state and not self.monitor_state.is_stale():
87
+ state = self.monitor_state
88
+ symbol, style = get_daemon_status_style(state.status)
89
+
90
+ content.append(f"{symbol} ", style=style)
91
+ content.append(f"{state.status}", style=style)
92
+
93
+ # State details
94
+ content.append(" │ ", style="dim")
95
+ content.append(f"#{state.loop_count}", style="cyan")
96
+ content.append(f" @{format_interval(state.current_interval)}", style="dim")
97
+ last_loop = datetime.fromisoformat(state.last_loop_time) if state.last_loop_time else None
98
+ content.append(f" ({format_ago(last_loop)})", style="dim")
99
+ if state.total_supervisions > 0:
100
+ content.append(f" sup:{state.total_supervisions}", style="magenta")
101
+ else:
102
+ # Monitor Daemon not running or stale
103
+ content.append("○ ", style="red")
104
+ content.append("stopped", style="red")
105
+ # Show last activity if available from stale state
106
+ if self.monitor_state and self.monitor_state.last_loop_time:
107
+ try:
108
+ last_time = datetime.fromisoformat(self.monitor_state.last_loop_time)
109
+ content.append(f" (last: {format_ago(last_time)})", style="dim")
110
+ except ValueError:
111
+ pass
112
+
113
+ # Controls hint
114
+ content.append(" │ ", style="dim")
115
+ content.append("[", style="bold green")
116
+ content.append(":sup ", style="dim")
117
+ content.append("]", style="bold red")
118
+ content.append(":sup ", style="dim")
119
+ content.append("\\", style="bold yellow")
120
+ content.append(":mon", style="dim")
121
+
122
+ content.append("\n")
123
+
124
+ # Log lines
125
+ display_lines = self.log_lines[-self.LOG_LINES_TO_SHOW:] if self.log_lines else []
126
+
127
+ if not display_lines:
128
+ content.append(" (no logs yet - daemon may not have run)", style="dim italic")
129
+ content.append("\n")
130
+ else:
131
+ for line in display_lines:
132
+ content.append(" ", style="")
133
+ # Truncate line
134
+ display_line = line[:120] if len(line) > 120 else line
135
+
136
+ # Color based on content
137
+ if "ERROR" in line or "error" in line:
138
+ style = "red"
139
+ elif "WARNING" in line or "warning" in line:
140
+ style = "yellow"
141
+ elif ">>>" in line:
142
+ style = "bold cyan"
143
+ elif "supervising" in line.lower() or "steering" in line.lower():
144
+ style = "magenta"
145
+ elif "Loop" in line:
146
+ style = "dim cyan"
147
+ else:
148
+ style = "dim"
149
+
150
+ content.append(display_line, style=style)
151
+ content.append("\n")
152
+
153
+ return content