Add auto-accept prompts and Ctrl+Shift+V paste for terminal

- Add auto-accept feature to automatically handle Y/yes prompts from CLI tools
  like Claude Code, triggered via action menu with configurable duration
- Shows countdown toast (5s) with Skip/Accept buttons before confirming
- Tab badge displays remaining time (e.g., ⏱4:32)
- Detects prompts by reading pyte screen buffer (works with TUI apps)
- Uses marker-based parsing to avoid re-triggering on same prompt
- Add Ctrl+Shift+V paste support in terminal display
- Update CLAUDE.md with feature documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-24 21:50:18 -04:00
parent 10d7baff81
commit 43f7deb5a6
6 changed files with 578 additions and 4 deletions

View File

@ -57,8 +57,10 @@ src/development_hub/
| `WorkspaceManager` | workspace_manager.py | Manages splittable pane layout |
| `PaneWidget` | pane_widget.py | Tab container with action menu |
| `DraggableTabWidget` | draggable_tab_widget.py | Tab widget supporting cross-pane drag |
| `TerminalWidget` | terminal_widget.py | PTY terminal with search functionality |
| `TerminalWidget` | terminal_widget.py | PTY terminal with search and auto-accept |
| `TerminalDisplay` | terminal_display.py | Terminal rendering with scrollback buffer |
| `AutoAcceptToast` | terminal_widget.py | Countdown toast for auto-accept prompts |
| `AutoAcceptDialog` | dialogs.py | Duration selection for auto-accept |
| `DashboardDataStore` | views/dashboard/data_store.py | Data persistence with file watching |
| `UndoManager` | views/dashboard/undo_manager.py | Undo/redo action management |
@ -78,6 +80,7 @@ src/development_hub/
- **External File Watching**: Dashboard auto-reloads when files modified externally
- **New Project Dialog**: Integrates with Ramble for voice input
- **Progress Reports**: Export weekly progress summaries from daily standups
- **Auto-accept Prompts**: Automatically accept Y/yes prompts in terminals for CLI tools (Claude, Codex, etc.)
### Keyboard Shortcuts
@ -104,12 +107,29 @@ src/development_hub/
| Shortcut | Action |
|----------|--------|
| `Ctrl+Shift+F` | Open search bar |
| `Ctrl+Shift+V` | Paste from clipboard |
| `Shift+PageUp/Down` | Scroll through history |
| `Enter` (in search) | Next match |
| `Shift+Enter` (in search) | Previous match |
| `F3` / `Shift+F3` | Next/previous match |
| `Escape` | Close search bar |
### Auto-accept Prompts
The terminal includes an auto-accept feature for handling Y/yes confirmation prompts from CLI tools like Claude Code, Codex, etc. This is useful when running long-running AI tasks that may ask many permission questions.
**How to use:**
1. Click ☰ (action menu) → "Auto-accept prompts..."
2. Select a duration (5 min, 15 min, 30 min, 1 hour, or custom)
3. When a Y/yes prompt is detected, a toast appears with a 5-second countdown
4. Click "Skip" to ignore, "Accept" to confirm immediately, or wait for countdown
5. Tab badge shows remaining time (e.g., `⏱4:32`)
6. Click ☰ → "Stop auto-accept" to disable early
**Supported patterns:**
- Standard `[Y/n]`, `[y/N]`, `(y/n)` prompts
- Claude Code permission dialogs with ` 1. Yes` menu selection
#### Dashboard Shortcuts (when dashboard tab is active)
| Shortcut | Action |

View File

@ -1849,3 +1849,101 @@ class WeeklyReportDialog(QDialog):
"Copied",
"Report copied to clipboard!"
)
class AutoAcceptDialog(QDialog):
"""Dialog to configure auto-accept duration for terminal prompts."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Auto-accept Prompts")
self.setMinimumWidth(350)
self._duration_seconds = 0
self._setup_ui()
def _setup_ui(self):
"""Set up the dialog UI."""
layout = QVBoxLayout(self)
layout.setSpacing(16)
# Description
desc = QLabel(
"Automatically accept Y/yes prompts in this terminal.\n"
"Useful when running CLI tools that ask many confirmation questions."
)
desc.setWordWrap(True)
desc.setStyleSheet("color: #aaa;")
layout.addWidget(desc)
# Preset duration buttons
preset_label = QLabel("Duration:")
layout.addWidget(preset_label)
preset_layout = QHBoxLayout()
preset_layout.setSpacing(8)
presets = [
("5 min", 5 * 60),
("15 min", 15 * 60),
("30 min", 30 * 60),
("1 hour", 60 * 60),
]
for label, seconds in presets:
btn = QPushButton(label)
btn.setMinimumWidth(70)
btn.clicked.connect(lambda checked, s=seconds: self._accept_duration(s))
preset_layout.addWidget(btn)
preset_layout.addStretch()
layout.addLayout(preset_layout)
# Custom duration
custom_layout = QHBoxLayout()
custom_layout.setSpacing(8)
custom_label = QLabel("Custom:")
custom_layout.addWidget(custom_label)
self._custom_input = QLineEdit()
self._custom_input.setPlaceholderText("minutes")
self._custom_input.setMaximumWidth(80)
self._custom_input.returnPressed.connect(self._accept_custom)
custom_layout.addWidget(self._custom_input)
custom_btn = QPushButton("Start")
custom_btn.clicked.connect(self._accept_custom)
custom_layout.addWidget(custom_btn)
custom_layout.addStretch()
layout.addLayout(custom_layout)
# Cancel button
layout.addSpacing(8)
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.reject)
layout.addWidget(cancel_btn, alignment=Qt.AlignmentFlag.AlignRight)
def _accept_duration(self, seconds: int):
"""Accept with the given duration."""
self._duration_seconds = seconds
self.accept()
def _accept_custom(self):
"""Accept with custom duration from input field."""
text = self._custom_input.text().strip()
if not text:
return
try:
minutes = float(text)
if minutes <= 0:
return
self._duration_seconds = int(minutes * 60)
self.accept()
except ValueError:
pass
def get_duration(self) -> int:
"""Get the selected duration in seconds."""
return self._duration_seconds

View File

@ -54,6 +54,7 @@ class PaneWidget(QFrame):
self._pane_id = str(uuid.uuid4())
self._is_focused = False
self._current_project: Project | None = None
self._terminal_base_titles: dict[int, str] = {} # tab_index -> original title
self._setup_ui()
@property
@ -79,6 +80,8 @@ class PaneWidget(QFrame):
self.action_menu.dashboard_requested.connect(self._on_dashboard_clicked)
self.action_menu.global_dashboard_requested.connect(self._on_global_dashboard_clicked)
self.action_menu.terminal_requested.connect(self.terminal_requested.emit)
self.action_menu.auto_accept_requested.connect(self._on_auto_accept_requested)
self.action_menu.auto_accept_stop_requested.connect(self._on_auto_accept_stop_requested)
self.action_menu.launch_discussion_requested.connect(self.launch_discussion_requested.emit)
self.action_menu.standup_requested.connect(self.standup_requested.emit)
self.action_menu.update_docs_requested.connect(self.update_docs_requested.emit)
@ -152,7 +155,15 @@ class PaneWidget(QFrame):
pane._on_terminal_closed(terminal)
terminal.closed.connect(on_closed)
# Connect auto-accept signal
def on_auto_accept_changed(remaining: int):
pane = weak_self()
if pane is not None:
pane._update_terminal_badge(terminal, remaining)
terminal.auto_accept_changed.connect(on_auto_accept_changed)
idx = self.tab_widget.addTab(terminal, title)
self._terminal_base_titles[idx] = title
self.tab_widget.setCurrentIndex(idx)
terminal.setFocus()
return terminal
@ -219,6 +230,52 @@ class PaneWidget(QFrame):
self.add_dashboard(project)
return
def _on_auto_accept_requested(self):
"""Handle auto-accept menu action."""
terminal = self.get_current_terminal()
if not terminal:
return
from development_hub.dialogs import AutoAcceptDialog
dialog = AutoAcceptDialog(self)
if dialog.exec() == dialog.DialogCode.Accepted:
duration = dialog.get_duration()
if duration > 0:
terminal.start_auto_accept(duration)
self.action_menu.set_auto_accept_active(True)
def _on_auto_accept_stop_requested(self):
"""Handle auto-accept stop menu action."""
terminal = self.get_current_terminal()
if terminal:
terminal.stop_auto_accept()
self.action_menu.set_auto_accept_active(False)
def _update_terminal_badge(self, terminal: TerminalWidget, remaining_seconds: int):
"""Update terminal tab title with auto-accept badge."""
# Find the tab index for this terminal
for i in range(self.tab_widget.count()):
if self.tab_widget.widget(i) == terminal:
base_title = self._terminal_base_titles.get(i, "Terminal")
# Strip any existing badge suffix
if "" in base_title:
base_title = base_title.split("")[0]
if remaining_seconds > 0:
# Format time as MM:SS
mins = remaining_seconds // 60
secs = remaining_seconds % 60
badge = f"{mins}:{secs:02d}"
self.tab_widget.setTabText(i, base_title + badge)
else:
# Remove badge
self.tab_widget.setTabText(i, base_title)
# Update menu state if this is the current terminal
if self.tab_widget.currentWidget() == terminal:
self.action_menu.set_auto_accept_active(remaining_seconds > 0)
break
def _close_tab(self, index: int):
"""Close tab at index."""
widget = self.tab_widget.widget(index)
@ -229,6 +286,18 @@ class PaneWidget(QFrame):
widget.deleteLater()
# Clean up title tracking - shift indices after removed tab
if index in self._terminal_base_titles:
del self._terminal_base_titles[index]
# Shift remaining indices
new_titles = {}
for idx, title in self._terminal_base_titles.items():
if idx > index:
new_titles[idx - 1] = title
else:
new_titles[idx] = title
self._terminal_base_titles = new_titles
# If pane is now empty, add welcome tab instead of emitting empty
if self.tab_widget.count() == 0:
self.add_welcome_tab()
@ -243,9 +312,12 @@ class PaneWidget(QFrame):
try:
for i in range(self.tab_widget.count()):
if self.tab_widget.widget(i) == terminal:
current_title = self.tab_widget.tabText(i)
if not current_title.endswith(" (exited)"):
self.tab_widget.setTabText(i, f"{current_title} (exited)")
# Use base title if available, strip any badge
base_title = self._terminal_base_titles.get(i, "Terminal")
if "" in base_title:
base_title = base_title.split("")[0]
if not base_title.endswith(" (exited)"):
self.tab_widget.setTabText(i, f"{base_title} (exited)")
break
except RuntimeError:
# Widget was deleted

View File

@ -694,6 +694,12 @@ class TerminalDisplay(QWidget):
self.scroll_down(1)
return
# Handle Ctrl+Shift combinations
if modifiers == (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier):
if key == Qt.Key.Key_V:
self._paste_clipboard()
return
# Handle special key combinations
if modifiers == Qt.KeyboardModifier.ControlModifier:
if key == Qt.Key.Key_C:

View File

@ -3,15 +3,18 @@
import fcntl
import logging
import os
import re
import struct
import subprocess
import termios
import time
from pathlib import Path
import pty
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
@ -22,6 +25,121 @@ from PySide6.QtWidgets import (
logger = logging.getLogger(__name__)
# Default countdown seconds for auto-accept toast
AUTO_ACCEPT_COUNTDOWN = 5
class AutoAcceptToast(QFrame):
"""Toast widget showing auto-accept countdown with Skip/Accept buttons."""
accepted = Signal() # User clicked Accept or countdown expired
skipped = Signal() # User clicked Skip
def __init__(self, response_preview: str, parent=None):
super().__init__(parent)
self._countdown = AUTO_ACCEPT_COUNTDOWN
self._response_preview = response_preview
self._timer = QTimer()
self._timer.timeout.connect(self._on_tick)
self._setup_ui()
def _setup_ui(self):
"""Set up the toast UI."""
self.setStyleSheet("""
AutoAcceptToast {
background: #2d4a2d;
border: 1px solid #4a9e4a;
border-radius: 4px;
}
QLabel {
color: #e0e0e0;
font-size: 12px;
}
QPushButton {
padding: 2px 8px;
border: 1px solid #3d3d3d;
border-radius: 3px;
color: #e0e0e0;
font-size: 11px;
}
QPushButton#skip {
background: #4a3d3d;
border-color: #6a4d4d;
}
QPushButton#skip:hover {
background: #5a4d4d;
}
QPushButton#accept {
background: #3d5a3d;
border-color: #4d7a4d;
}
QPushButton#accept:hover {
background: #4d6a4d;
}
""")
self.setMaximumHeight(32)
layout = QHBoxLayout(self)
layout.setContentsMargins(8, 4, 8, 4)
layout.setSpacing(8)
# Icon and message
self._label = QLabel()
self._update_label()
layout.addWidget(self._label, 1)
# Skip button - don't steal focus from terminal
skip_btn = QPushButton("Skip")
skip_btn.setObjectName("skip")
skip_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
skip_btn.clicked.connect(self._on_skip)
layout.addWidget(skip_btn)
# Accept button - don't steal focus from terminal
accept_btn = QPushButton("Accept")
accept_btn.setObjectName("accept")
accept_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
accept_btn.clicked.connect(self._on_accept)
layout.addWidget(accept_btn)
def _update_label(self):
"""Update the countdown label."""
self._label.setText(
f"⏱ Auto-accept in {self._countdown}s → sending '{self._response_preview}'"
)
def start(self):
"""Start the countdown."""
self._timer.start(1000)
self.show()
def stop(self):
"""Stop the countdown without triggering accept/skip."""
self._timer.stop()
self.hide()
def _on_tick(self):
"""Handle countdown tick."""
self._countdown -= 1
if self._countdown <= 0:
self._timer.stop()
self.accepted.emit()
self.hide()
else:
self._update_label()
def _on_skip(self):
"""Handle skip button click."""
self._timer.stop()
self.skipped.emit()
self.hide()
def _on_accept(self):
"""Handle accept button click."""
self._timer.stop()
self.accepted.emit()
self.hide()
# Import from split modules
from development_hub.pty_manager import PtyReaderThread
from development_hub.terminal_display import (
@ -45,6 +163,22 @@ class TerminalWidget(QWidget):
closed = Signal()
shell_error = Signal(str) # Emitted when shell fails to start or crashes
auto_accept_changed = Signal(int) # Emits remaining seconds (0 = disabled)
# Patterns for auto-accept detection
# Each tuple: (compiled_regex, response_to_send)
# Patterns should be specific to avoid false positives on code display
AUTO_ACCEPT_PATTERNS = [
# Standard [Y/n] prompts - require question context
(re.compile(r'\?\s*\[Y/n\]\s*$', re.MULTILINE), b'y\n'),
(re.compile(r'\?\s*\[y/N\]\s*$', re.MULTILINE), b'y\n'),
(re.compile(r'\?\s*\(y/n\)\s*$', re.MULTILINE), b'y\n'),
# Claude Code style: "Do you want" followed by menu with Yes as option 1
# The indicates the selected option - just need Enter to confirm
(re.compile(r'Do you want.*\?\s*\n\s*[>]\s*1\.\s*Yes', re.IGNORECASE | re.DOTALL), b'\r'),
# Simpler Claude prompt: just the menu indicator on Yes line
(re.compile(r'^\s*[>]\s*1\.\s*Yes\s*$', re.MULTILINE), b'\r'),
]
def __init__(self, cwd: Path | None = None, parent=None):
super().__init__(parent)
@ -56,6 +190,19 @@ class TerminalWidget(QWidget):
self._shell_running = False
self._intentional_close = False
# Auto-accept state
self._auto_accept_enabled = False
self._auto_accept_end_time = 0
self._auto_accept_timer = QTimer()
self._auto_accept_timer.timeout.connect(self._update_auto_accept)
self._pending_response: bytes | None = None
self._auto_accept_toast: AutoAcceptToast | None = None
self._last_parsed_marker = "" # Last 2-3 lines we parsed, to find our place
# Debounce timer - wait for output to settle before checking patterns
self._pattern_check_timer = QTimer()
self._pattern_check_timer.setSingleShot(True)
self._pattern_check_timer.timeout.connect(self._do_pattern_check)
self._setup_ui()
self._start_shell()
@ -80,6 +227,9 @@ class TerminalWidget(QWidget):
)
layout.addWidget(self.display)
# Auto-accept toast (hidden by default, created on demand)
# Will be added to layout when shown
# Set up search shortcut (Ctrl+Shift+F)
self._search_shortcut = QShortcut(QKeySequence("Ctrl+Shift+F"), self)
self._search_shortcut.activated.connect(self.toggle_search)
@ -456,6 +606,10 @@ class TerminalWidget(QWidget):
"""Handle output from PTY."""
self.display.feed(data)
# Schedule auto-accept pattern check if enabled
if self._auto_accept_enabled:
self._schedule_auto_accept_check()
def _on_shell_exit(self):
"""Handle shell process exit."""
self._shell_running = False
@ -572,6 +726,9 @@ class TerminalWidget(QWidget):
self._intentional_close = True
self._shell_running = False
# Stop auto-accept
self.stop_auto_accept()
if self.reader_thread:
self.reader_thread.stop()
self.reader_thread.wait(1000)
@ -602,6 +759,201 @@ class TerminalWidget(QWidget):
"""Set focus to the display widget."""
self.display.setFocus()
# === Auto-accept functionality ===
def start_auto_accept(self, duration_seconds: int):
"""Start auto-accepting Y/yes prompts for the specified duration.
Args:
duration_seconds: How long to auto-accept prompts
"""
self._auto_accept_enabled = True
self._auto_accept_end_time = time.time() + duration_seconds
# Update every second for countdown
self._auto_accept_timer.start(1000)
# Emit initial remaining time
self.auto_accept_changed.emit(duration_seconds)
def stop_auto_accept(self):
"""Stop auto-accepting prompts."""
self._auto_accept_enabled = False
self._auto_accept_end_time = 0
self._auto_accept_timer.stop()
self._pattern_check_timer.stop()
self._pending_response = None
# Hide and clean up toast if showing
if self._auto_accept_toast is not None:
self._auto_accept_toast.stop()
self._auto_accept_toast.deleteLater()
self._auto_accept_toast = None
self.auto_accept_changed.emit(0)
def _update_auto_accept(self):
"""Update auto-accept timer and emit remaining time."""
if not self._auto_accept_enabled:
self._auto_accept_timer.stop()
return
remaining = int(self._auto_accept_end_time - time.time())
if remaining <= 0:
self.stop_auto_accept()
else:
self.auto_accept_changed.emit(remaining)
def _schedule_auto_accept_check(self):
"""Schedule pattern check after output settles.
Debounces to wait for TUI rendering to complete before
checking the screen buffer for prompt patterns.
"""
if not self._auto_accept_enabled:
return
# Don't check if toast is already showing
if self._auto_accept_toast is not None and self._auto_accept_toast.isVisible():
return
# Debounce: wait 200ms after last output before checking screen
self._pattern_check_timer.start(200)
def _do_pattern_check(self):
"""Actually check patterns after output has settled."""
if not self._auto_accept_enabled:
return
if self._auto_accept_toast is not None and self._auto_accept_toast.isVisible():
return
# Read directly from pyte screen buffer - this captures TUI-rendered content
screen_content = self._get_screen_text()
# Find where we last parsed and only check content after that
content_to_check = screen_content
found_marker = False
if self._last_parsed_marker and self._last_parsed_marker in screen_content:
# Find the marker and only check content after it
marker_pos = screen_content.rfind(self._last_parsed_marker)
content_to_check = screen_content[marker_pos + len(self._last_parsed_marker):]
found_marker = True
# If nothing to check, we're done
if not content_to_check.strip():
return
# Check patterns against the new content
for pattern, response in self.AUTO_ACCEPT_PATTERNS:
match = pattern.search(content_to_check)
if match:
# If we found our marker and the match is right at the start,
# this is the same prompt we already processed - skip it and
# search again from after this match
if found_marker and match.start() < 20:
# Search for another occurrence after this one
match = pattern.search(content_to_check, match.end())
if not match:
continue
# Save marker: include the match itself so we skip past it next time
content_through_match = content_to_check[:match.end()]
lines = content_through_match.rstrip().split('\n')
if len(lines) >= 2:
self._last_parsed_marker = '\n'.join(lines[-2:])
else:
self._last_parsed_marker = content_through_match.strip()
# Show toast with countdown
self._pending_response = response
self._show_auto_accept_toast(response)
return
# No prompt found - advance marker to end of content so we don't re-scan
lines = content_to_check.rstrip().split('\n')
if len(lines) >= 3:
self._last_parsed_marker = '\n'.join(lines[-3:])
elif content_to_check.strip():
self._last_parsed_marker = content_to_check.strip()
def _get_screen_text(self) -> str:
"""Get the current visible screen content as text."""
lines = []
screen = self.display.screen
for row in range(screen.lines):
line = ""
for col in range(screen.columns):
char = screen.buffer[row][col]
line += char.data if char.data else " "
lines.append(line.rstrip())
return "\n".join(lines)
def _show_auto_accept_toast(self, response: bytes):
"""Show the auto-accept toast with countdown."""
# Get human-readable response preview
response_preview = response.decode('utf-8', errors='replace').strip()
if response_preview == 'y':
response_preview = 'y (yes)'
elif response_preview == '1':
response_preview = '1 (Yes)'
# Create toast if needed
if self._auto_accept_toast is not None:
self._auto_accept_toast.stop()
self._auto_accept_toast.deleteLater()
self._auto_accept_toast = AutoAcceptToast(response_preview, self)
self._auto_accept_toast.accepted.connect(self._on_toast_accepted)
self._auto_accept_toast.skipped.connect(self._on_toast_skipped)
# Add to layout at bottom
self.layout().addWidget(self._auto_accept_toast)
self._auto_accept_toast.start()
# Keep focus on terminal display
self.display.setFocus()
def _on_toast_accepted(self):
"""Handle toast accept (countdown expired or Accept clicked)."""
# Ensure terminal has focus before sending
self.display.setFocus()
if self._pending_response:
# Delay sending to let focus settle - Qt focus is async
response = self._pending_response
self._pending_response = None
QTimer.singleShot(50, lambda: self._send_auto_accept_response(response))
else:
self._pending_response = None
def _send_auto_accept_response(self, response: bytes):
"""Send the auto-accept response after focus has settled."""
if self.master_fd is not None:
try:
os.write(self.master_fd, response)
except OSError:
pass
def _on_toast_skipped(self):
"""Handle toast skip (user clicked Skip)."""
# Just clear the pending response, auto-accept stays enabled for next prompt
self._pending_response = None
# Restore focus to terminal
self.display.setFocus()
@property
def auto_accept_remaining(self) -> int:
"""Get remaining auto-accept time in seconds."""
if not self._auto_accept_enabled:
return 0
return max(0, int(self._auto_accept_end_time - time.time()))
@property
def is_auto_accept_enabled(self) -> bool:
"""Check if auto-accept is currently enabled."""
return self._auto_accept_enabled
@property
def is_running(self) -> bool:
"""Check if the shell is currently running."""

View File

@ -20,6 +20,8 @@ class ActionMenu(QToolButton):
dashboard_requested = Signal()
global_dashboard_requested = Signal()
terminal_requested = Signal()
auto_accept_requested = Signal()
auto_accept_stop_requested = Signal()
launch_discussion_requested = Signal()
standup_requested = Signal()
update_docs_requested = Signal()
@ -90,6 +92,11 @@ class ActionMenu(QToolButton):
terminal_action.triggered.connect(self.terminal_requested.emit)
menu.addAction(terminal_action)
self.auto_accept_action = QAction("⏱ Auto-accept prompts...", self)
self.auto_accept_action.triggered.connect(self._on_auto_accept_triggered)
menu.addAction(self.auto_accept_action)
self._auto_accept_active = False
menu.addSeparator()
launch_discussion_action = QAction("💬 Launch Discussion", self)
@ -130,3 +137,22 @@ class ActionMenu(QToolButton):
self.setToolTip(f"Actions for {project_name}")
else:
self.setToolTip("Global actions")
def _on_auto_accept_triggered(self):
"""Handle auto-accept action click."""
if self._auto_accept_active:
self.auto_accept_stop_requested.emit()
else:
self.auto_accept_requested.emit()
def set_auto_accept_active(self, active: bool):
"""Update the auto-accept menu item based on state.
Args:
active: Whether auto-accept is currently active
"""
self._auto_accept_active = active
if active:
self.auto_accept_action.setText("⏹ Stop auto-accept")
else:
self.auto_accept_action.setText("⏱ Auto-accept prompts...")