overcode 0.2.3__tar.gz → 0.2.5__tar.gz
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-0.2.3/src/overcode.egg-info → overcode-0.2.5}/PKG-INFO +1 -1
- {overcode-0.2.3 → overcode-0.2.5}/pyproject.toml +1 -1
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/agent.py +14 -1
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/implementations.py +11 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/monitor_daemon.py +59 -8
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/monitor_daemon_state.py +3 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/settings.py +7 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/status_constants.py +82 -4
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/summary_columns.py +54 -37
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tmux_manager.py +11 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui.py +206 -17
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui.tcss +16 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_actions/session.py +2 -1
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_actions/view.py +21 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_helpers.py +3 -2
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/__init__.py +2 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/command_bar.py +3 -2
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/daemon_status_bar.py +3 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/help_overlay.py +5 -4
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/session_summary.py +11 -12
- overcode-0.2.5/src/overcode/tui_widgets/sister_selection_modal.py +154 -0
- {overcode-0.2.3 → overcode-0.2.5/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode.egg-info/SOURCES.txt +1 -0
- {overcode-0.2.3 → overcode-0.2.5}/LICENSE +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/MANIFEST.in +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/README.md +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/setup.cfg +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/__init__.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/bundled_skills.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/claude_config.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/budget.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/config.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/perms.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/sister.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/cli/skills.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/config.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/data_export.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/dependency_check.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/exceptions.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/follow_mode.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/history_reader.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/hook_handler.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/interfaces.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/launcher.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/logging_config.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/mocks.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/notifier.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/pid_utils.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/presence_logger.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/protocols.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/session_manager.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/sister_controller.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/sister_poller.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/status_detector.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/status_history.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/status_patterns.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/summary_groups.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/time_context.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_logic.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_render.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web/__init__.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web_api.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web_control_api.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web_server.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode/web_templates.py +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.2.3 → overcode-0.2.5}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -464,6 +464,11 @@ def list_agents(
|
|
|
464
464
|
if terminated_count > 0:
|
|
465
465
|
rprint(f"\n[dim]{terminated_count} terminated session(s). Run 'overcode cleanup' to remove.[/dim]")
|
|
466
466
|
|
|
467
|
+
# Hint about untracked tmux windows (#344)
|
|
468
|
+
if use_daemon and daemon_state.untracked_window_count > 0:
|
|
469
|
+
n = daemon_state.untracked_window_count
|
|
470
|
+
rprint(f"\n[dim]{n} untracked tmux window(s). Run 'overcode cleanup --untracked' to remove.[/dim]")
|
|
471
|
+
|
|
467
472
|
|
|
468
473
|
@app.command()
|
|
469
474
|
def attach(
|
|
@@ -598,6 +603,9 @@ def cleanup(
|
|
|
598
603
|
done: Annotated[
|
|
599
604
|
bool, typer.Option("--done", help="Also archive 'done' child agents (#244)")
|
|
600
605
|
] = False,
|
|
606
|
+
untracked: Annotated[
|
|
607
|
+
bool, typer.Option("--untracked", help="Kill tmux windows not tracked by any agent (#344)")
|
|
608
|
+
] = False,
|
|
601
609
|
session: SessionOption = "agents",
|
|
602
610
|
):
|
|
603
611
|
"""Remove terminated sessions from tracking.
|
|
@@ -606,6 +614,7 @@ def cleanup(
|
|
|
606
614
|
(e.g., after a machine reboot). Use 'overcode list' to see them.
|
|
607
615
|
|
|
608
616
|
Use --done to also archive done child agents (kill tmux window, move to archive).
|
|
617
|
+
Use --untracked to kill tmux windows that exist but aren't tracked by any agent.
|
|
609
618
|
"""
|
|
610
619
|
launcher = ClaudeLauncher(session)
|
|
611
620
|
count = launcher.cleanup_terminated_sessions()
|
|
@@ -619,6 +628,10 @@ def cleanup(
|
|
|
619
628
|
launcher._kill_single_session(sess)
|
|
620
629
|
done_count += 1
|
|
621
630
|
|
|
631
|
+
# Kill untracked tmux windows (#344)
|
|
632
|
+
if untracked:
|
|
633
|
+
launcher.list_sessions(kill_untracked=True)
|
|
634
|
+
|
|
622
635
|
total = count + done_count
|
|
623
636
|
if total > 0:
|
|
624
637
|
parts = []
|
|
@@ -627,7 +640,7 @@ def cleanup(
|
|
|
627
640
|
if done_count > 0:
|
|
628
641
|
parts.append(f"{done_count} done")
|
|
629
642
|
rprint(f"[green]✓ Cleaned up {' + '.join(parts)} session(s)[/green]")
|
|
630
|
-
|
|
643
|
+
elif not untracked:
|
|
631
644
|
rprint("[dim]No sessions to clean up[/dim]")
|
|
632
645
|
|
|
633
646
|
|
|
@@ -151,6 +151,17 @@ class RealTmux:
|
|
|
151
151
|
if rest:
|
|
152
152
|
pane.send_keys(rest, enter=False)
|
|
153
153
|
time.sleep(0.1)
|
|
154
|
+
elif keys.startswith('/') and len(keys) > 1:
|
|
155
|
+
# Special handling for slash commands (#307)
|
|
156
|
+
# Claude Code shows a command menu when / is typed;
|
|
157
|
+
# send / separately so the menu has time to appear
|
|
158
|
+
# before the rest of the command and Enter arrive.
|
|
159
|
+
pane.send_keys('/', enter=False)
|
|
160
|
+
time.sleep(0.3)
|
|
161
|
+
rest = keys[1:]
|
|
162
|
+
if rest:
|
|
163
|
+
pane.send_keys(rest, enter=False)
|
|
164
|
+
time.sleep(0.15)
|
|
154
165
|
else:
|
|
155
166
|
pane.send_keys(keys, enter=False)
|
|
156
167
|
# Small delay for Claude Code to process text
|
|
@@ -679,14 +679,37 @@ class MonitorDaemon:
|
|
|
679
679
|
|
|
680
680
|
# Archive: kill tmux window and mark terminated
|
|
681
681
|
try:
|
|
682
|
-
from .
|
|
683
|
-
tmux =
|
|
682
|
+
from .implementations import RealTmux
|
|
683
|
+
tmux = RealTmux()
|
|
684
684
|
tmux.kill_window(self.tmux_session, session.tmux_window)
|
|
685
685
|
except Exception:
|
|
686
686
|
pass # Window may already be gone
|
|
687
687
|
self.session_manager.update_session_status(session.id, "terminated")
|
|
688
688
|
self.log.info(f"Auto-archived done agent: {session.name}")
|
|
689
689
|
|
|
690
|
+
def _count_untracked_windows(self, sessions: list) -> int:
|
|
691
|
+
"""Count tmux windows not tracked by any active session (#344).
|
|
692
|
+
|
|
693
|
+
Returns count of windows that exist in tmux but aren't tracked
|
|
694
|
+
(excluding window 0 which is the default shell).
|
|
695
|
+
"""
|
|
696
|
+
try:
|
|
697
|
+
from .implementations import RealTmux
|
|
698
|
+
tmux = RealTmux()
|
|
699
|
+
if not tmux.session_exists(self.tmux_session):
|
|
700
|
+
return 0
|
|
701
|
+
tmux_windows = tmux.list_windows(self.tmux_session)
|
|
702
|
+
active_sessions = [s for s in sessions if s.status != "terminated"]
|
|
703
|
+
tracked_windows = {s.tmux_window for s in active_sessions}
|
|
704
|
+
count = 0
|
|
705
|
+
for window_info in tmux_windows:
|
|
706
|
+
window_idx = int(window_info['index'])
|
|
707
|
+
if window_idx != 0 and window_idx not in tracked_windows:
|
|
708
|
+
count += 1
|
|
709
|
+
return count
|
|
710
|
+
except Exception:
|
|
711
|
+
return 0
|
|
712
|
+
|
|
690
713
|
def _enforce_oversight_timeouts(self, sessions: list) -> None:
|
|
691
714
|
"""Enforce oversight timeouts for waiting_oversight sessions."""
|
|
692
715
|
now = datetime.now()
|
|
@@ -849,12 +872,30 @@ class MonitorDaemon:
|
|
|
849
872
|
session_states = []
|
|
850
873
|
all_waiting_user = True
|
|
851
874
|
|
|
875
|
+
# Detect window index collisions: when multiple sessions share
|
|
876
|
+
# a window index, only the most recently launched one owns it.
|
|
877
|
+
# Others are truly terminated (their window was reused).
|
|
878
|
+
from collections import Counter
|
|
879
|
+
window_counts = Counter(s.tmux_window for s in sessions if s.status != "done")
|
|
880
|
+
# For colliding windows, find the rightful owner (latest start_time)
|
|
881
|
+
window_owner = {}
|
|
882
|
+
for s in sessions:
|
|
883
|
+
if s.status == "done":
|
|
884
|
+
continue
|
|
885
|
+
w = s.tmux_window
|
|
886
|
+
if window_counts[w] > 1:
|
|
887
|
+
# Multiple sessions claim this window — latest launch wins
|
|
888
|
+
if w not in window_owner or s.start_time > window_owner[w].start_time:
|
|
889
|
+
window_owner[w] = s
|
|
890
|
+
|
|
852
891
|
for session in sessions:
|
|
853
|
-
#
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
892
|
+
# If this window is contested and we're not the owner, skip
|
|
893
|
+
pane_content = ""
|
|
894
|
+
if (window_counts.get(session.tmux_window, 0) > 1
|
|
895
|
+
and window_owner.get(session.tmux_window) is not session):
|
|
857
896
|
status, activity = STATUS_TERMINATED, "Session terminated"
|
|
897
|
+
if session.status != "terminated":
|
|
898
|
+
self.session_manager.update_session_status(session.id, "terminated")
|
|
858
899
|
elif session.status == "done":
|
|
859
900
|
status, activity = STATUS_DONE, "Completed"
|
|
860
901
|
else:
|
|
@@ -912,9 +953,17 @@ class MonitorDaemon:
|
|
|
912
953
|
else:
|
|
913
954
|
effective_status = status
|
|
914
955
|
|
|
915
|
-
# Persist terminated status
|
|
916
|
-
|
|
956
|
+
# Persist terminated status when window is truly gone.
|
|
957
|
+
# Only persist when pane_content is empty (window gone), not when
|
|
958
|
+
# a shell prompt is briefly visible (e.g. during agent revival).
|
|
959
|
+
if (effective_status == STATUS_TERMINATED
|
|
960
|
+
and session.status != "terminated"
|
|
961
|
+
and not pane_content):
|
|
917
962
|
self.session_manager.update_session_status(session.id, "terminated")
|
|
963
|
+
# Un-persist terminated if agent is found alive (revival or false positive)
|
|
964
|
+
elif (session.status == "terminated"
|
|
965
|
+
and effective_status != STATUS_TERMINATED):
|
|
966
|
+
self.session_manager.update_session_status(session.id, "running")
|
|
918
967
|
|
|
919
968
|
session_state = self.track_session_stats(session, effective_status)
|
|
920
969
|
session_state.current_activity = activity
|
|
@@ -981,8 +1030,10 @@ class MonitorDaemon:
|
|
|
981
1030
|
self._enforce_oversight_timeouts(sessions)
|
|
982
1031
|
|
|
983
1032
|
# Auto-archive "done" agents after 1 hour (#244)
|
|
1033
|
+
# Count untracked tmux windows every 2 minutes (#344)
|
|
984
1034
|
if self.state.loop_count % 60 == 0:
|
|
985
1035
|
self._auto_archive_done_agents(sessions)
|
|
1036
|
+
self.state.untracked_window_count = self._count_untracked_windows(sessions)
|
|
986
1037
|
|
|
987
1038
|
# Log summary
|
|
988
1039
|
green = sum(1 for s in session_states if s.current_status == STATUS_RUNNING)
|
|
@@ -174,6 +174,9 @@ class MonitorDaemonState:
|
|
|
174
174
|
relay_last_push: Optional[str] = None # ISO timestamp of last successful push
|
|
175
175
|
relay_last_status: str = "disabled" # "ok", "error", "disabled"
|
|
176
176
|
|
|
177
|
+
# Untracked tmux windows (#344)
|
|
178
|
+
untracked_window_count: int = 0
|
|
179
|
+
|
|
177
180
|
def to_dict(self) -> dict:
|
|
178
181
|
"""Convert to dictionary for JSON serialization."""
|
|
179
182
|
return dataclasses.asdict(self)
|
|
@@ -425,6 +425,7 @@ class TUIPreferences:
|
|
|
425
425
|
summary_content_mode: str = "ai_short" # ai_short, ai_long, orders, annotation, heartbeat (#98, #171)
|
|
426
426
|
baseline_minutes: int = 60 # 0=now (instantaneous), 15/30/.../180 = minutes back for mean spin
|
|
427
427
|
monochrome: bool = False # B&W mode for terminals with ANSI issues (#138)
|
|
428
|
+
emoji_free: bool = False # ASCII fallbacks for terminals without emoji (#315)
|
|
428
429
|
show_cost: bool = False # Show $ cost instead of token counts
|
|
429
430
|
timeline_hours: float = 3.0 # 1, 3, 6, 12, 24 — timeline scope (#191)
|
|
430
431
|
notifications: str = "off" # "off", "sound", "banner", "both" — macOS notifications (#235)
|
|
@@ -435,6 +436,8 @@ class TUIPreferences:
|
|
|
435
436
|
column_config: dict = field(default_factory=dict)
|
|
436
437
|
# Show abbreviated column headers above summary lines
|
|
437
438
|
show_column_headers: bool = False
|
|
439
|
+
# Sister instances hidden from agent list (#323)
|
|
440
|
+
disabled_sisters: Set[str] = field(default_factory=set)
|
|
438
441
|
|
|
439
442
|
@classmethod
|
|
440
443
|
def load(cls, session: str) -> "TUIPreferences":
|
|
@@ -470,12 +473,14 @@ class TUIPreferences:
|
|
|
470
473
|
summary_content_mode=data.get("summary_content_mode", "ai_short"),
|
|
471
474
|
baseline_minutes=data.get("baseline_minutes", 0),
|
|
472
475
|
monochrome=data.get("monochrome", False),
|
|
476
|
+
emoji_free=data.get("emoji_free", False),
|
|
473
477
|
show_cost=data.get("show_cost", False),
|
|
474
478
|
visited_stalled_agents=set(data.get("visited_stalled_agents", [])),
|
|
475
479
|
column_config=data.get("column_config", {}),
|
|
476
480
|
show_column_headers=data.get("show_column_headers", False),
|
|
477
481
|
timeline_hours=data.get("timeline_hours", 3.0),
|
|
478
482
|
notifications=data.get("notifications", "off"),
|
|
483
|
+
disabled_sisters=set(data.get("disabled_sisters", [])),
|
|
479
484
|
)
|
|
480
485
|
except (json.JSONDecodeError, IOError):
|
|
481
486
|
return cls()
|
|
@@ -502,12 +507,14 @@ class TUIPreferences:
|
|
|
502
507
|
"summary_content_mode": self.summary_content_mode,
|
|
503
508
|
"baseline_minutes": self.baseline_minutes,
|
|
504
509
|
"monochrome": self.monochrome,
|
|
510
|
+
"emoji_free": self.emoji_free,
|
|
505
511
|
"show_cost": self.show_cost,
|
|
506
512
|
"visited_stalled_agents": list(self.visited_stalled_agents),
|
|
507
513
|
"column_config": self.column_config,
|
|
508
514
|
"show_column_headers": self.show_column_headers,
|
|
509
515
|
"timeline_hours": self.timeline_hours,
|
|
510
516
|
"notifications": self.notifications,
|
|
517
|
+
"disabled_sisters": sorted(self.disabled_sisters),
|
|
511
518
|
}, f, indent=2)
|
|
512
519
|
except (IOError, OSError):
|
|
513
520
|
pass # Best effort
|
|
@@ -93,9 +93,86 @@ STATUS_EMOJIS = {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
# ASCII fallbacks for all emoji used in the TUI (#315)
|
|
97
|
+
# Used when emoji_free mode is active (terminals without emoji font support).
|
|
98
|
+
EMOJI_ASCII = {
|
|
99
|
+
# Status indicators
|
|
100
|
+
"🟢": "[R]",
|
|
101
|
+
"🔴": "[W]",
|
|
102
|
+
"⚫": "[X]",
|
|
103
|
+
"💤": "[Z]",
|
|
104
|
+
"💚": "[H]",
|
|
105
|
+
"🟠": "[A]",
|
|
106
|
+
"💛": "[h]",
|
|
107
|
+
"🟣": "[E]",
|
|
108
|
+
"☑️": "[D]",
|
|
109
|
+
"👁️": "[O]",
|
|
110
|
+
"🟡": "[S]",
|
|
111
|
+
"⚪": "[?]",
|
|
112
|
+
# Tool indicators
|
|
113
|
+
"🖥️": "Sh",
|
|
114
|
+
"📖": "Rd",
|
|
115
|
+
"✏️": "Wr",
|
|
116
|
+
"🔧": "Ed",
|
|
117
|
+
"🔍": "Gl",
|
|
118
|
+
"🔎": "Gr",
|
|
119
|
+
"🌐": "Wf",
|
|
120
|
+
"🕵️": "Ws",
|
|
121
|
+
"🧵": "Tk",
|
|
122
|
+
"📓": "Nb",
|
|
123
|
+
"📋": "Cb",
|
|
124
|
+
"📝": "Tw",
|
|
125
|
+
"🔹": "--",
|
|
126
|
+
# Permission modes
|
|
127
|
+
"🔥": "B!",
|
|
128
|
+
"🏃": "P>",
|
|
129
|
+
"👮": "N:",
|
|
130
|
+
# Activity/metrics
|
|
131
|
+
"🔔": "(!)",
|
|
132
|
+
"⏰": "AL",
|
|
133
|
+
"📚": "CW",
|
|
134
|
+
"💓": "<3",
|
|
135
|
+
"💰": "$$",
|
|
136
|
+
"⏳": "~~",
|
|
137
|
+
"🤖": "Ro",
|
|
138
|
+
"👤": "Hu",
|
|
139
|
+
"🤿": "Su",
|
|
140
|
+
"🐚": "Bg",
|
|
141
|
+
"👶": "Ch",
|
|
142
|
+
"🤝": "Tm",
|
|
143
|
+
"🕐": "Tc",
|
|
144
|
+
# Content modes
|
|
145
|
+
"💬": "Sm",
|
|
146
|
+
"🎯": "SO",
|
|
147
|
+
# Presence states
|
|
148
|
+
"⏻": "Pw",
|
|
149
|
+
"🔒": "Lk",
|
|
150
|
+
"🧘": "Id",
|
|
151
|
+
"🚶": "Ac",
|
|
152
|
+
# Value arrows
|
|
153
|
+
"⏫️": "^^",
|
|
154
|
+
"⏬️": "vv",
|
|
155
|
+
"⏹️": "==",
|
|
156
|
+
# Misc
|
|
157
|
+
"⚠": "!W",
|
|
158
|
+
"➖": "--",
|
|
159
|
+
"✓": "ok",
|
|
160
|
+
"▼": "v ",
|
|
161
|
+
"▶": "> ",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def emoji_or_ascii(char: str, emoji_free: bool) -> str:
|
|
166
|
+
"""Return ASCII fallback if emoji_free mode is active, else the emoji."""
|
|
167
|
+
if emoji_free:
|
|
168
|
+
return EMOJI_ASCII.get(char, char)
|
|
169
|
+
return char
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_status_emoji(status: str, emoji_free: bool = False) -> str:
|
|
97
173
|
"""Get emoji for an agent status."""
|
|
98
|
-
|
|
174
|
+
e = STATUS_EMOJIS.get(status, "⚪")
|
|
175
|
+
return emoji_or_ascii(e, emoji_free)
|
|
99
176
|
|
|
100
177
|
|
|
101
178
|
# =============================================================================
|
|
@@ -143,9 +220,10 @@ STATUS_SYMBOLS = {
|
|
|
143
220
|
}
|
|
144
221
|
|
|
145
222
|
|
|
146
|
-
def get_status_symbol(status: str) -> Tuple[str, str]:
|
|
223
|
+
def get_status_symbol(status: str, emoji_free: bool = False) -> Tuple[str, str]:
|
|
147
224
|
"""Get (emoji, color) tuple for an agent status."""
|
|
148
|
-
|
|
225
|
+
symbol, color = STATUS_SYMBOLS.get(status, ("⚪", "dim"))
|
|
226
|
+
return (emoji_or_ascii(symbol, emoji_free), color)
|
|
149
227
|
|
|
150
228
|
|
|
151
229
|
# =============================================================================
|
|
@@ -49,14 +49,16 @@ TOOL_EMOJI_DEFAULT = "🔹" # Fallback for unknown tools
|
|
|
49
49
|
MAX_TOOL_EMOJI = 10 # Configurable cap
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def _tool_emojis(allowed_tools: Optional[str], max_n: int = MAX_TOOL_EMOJI) -> str:
|
|
52
|
+
def _tool_emojis(allowed_tools: Optional[str], max_n: int = MAX_TOOL_EMOJI, emoji_free: bool = False) -> str:
|
|
53
53
|
"""Convert comma-separated tool names to emoji string."""
|
|
54
54
|
if not allowed_tools:
|
|
55
55
|
return ""
|
|
56
|
+
from .status_constants import emoji_or_ascii
|
|
56
57
|
tools = [t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
57
|
-
emojis = [TOOL_EMOJI.get(t, TOOL_EMOJI_DEFAULT) for t in tools[:max_n]]
|
|
58
|
+
emojis = [emoji_or_ascii(TOOL_EMOJI.get(t, TOOL_EMOJI_DEFAULT), emoji_free) for t in tools[:max_n]]
|
|
59
|
+
sep = " " if emoji_free else ""
|
|
58
60
|
suffix = "…" if len(tools) > max_n else ""
|
|
59
|
-
return
|
|
61
|
+
return sep.join(emojis) + suffix
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
# ---------------------------------------------------------------------------
|
|
@@ -93,6 +95,7 @@ class ColumnContext:
|
|
|
93
95
|
status_color: str
|
|
94
96
|
bg: str # background style suffix, e.g. " on #0d2137" or ""
|
|
95
97
|
monochrome: bool
|
|
98
|
+
emoji_free: bool
|
|
96
99
|
summary_detail: str
|
|
97
100
|
show_cost: bool
|
|
98
101
|
any_has_budget: bool # True if any agent has a cost budget (#173)
|
|
@@ -153,8 +156,15 @@ class ColumnContext:
|
|
|
153
156
|
local_hostname: str = ""
|
|
154
157
|
|
|
155
158
|
def mono(self, colored: str, simple: str = "bold") -> str:
|
|
156
|
-
"""Return
|
|
157
|
-
return
|
|
159
|
+
"""Return colored style (monochrome only applies to preview pane, not summaries)."""
|
|
160
|
+
return colored
|
|
161
|
+
|
|
162
|
+
def e(self, char: str) -> str:
|
|
163
|
+
"""Return ASCII fallback if emoji_free mode is active (#315)."""
|
|
164
|
+
if self.emoji_free:
|
|
165
|
+
from .status_constants import EMOJI_ASCII
|
|
166
|
+
return EMOJI_ASCII.get(char, char)
|
|
167
|
+
return char
|
|
158
168
|
|
|
159
169
|
|
|
160
170
|
# ---------------------------------------------------------------------------
|
|
@@ -251,7 +261,7 @@ def render_status_symbol(ctx: ColumnContext) -> ColumnOutput:
|
|
|
251
261
|
|
|
252
262
|
def render_unvisited_alert(ctx: ColumnContext) -> ColumnOutput:
|
|
253
263
|
if ctx.is_unvisited_stalled:
|
|
254
|
-
return [("🔔", ctx.mono(f"bold blink red{ctx.bg}", "bold"))]
|
|
264
|
+
return [(ctx.e("🔔"), ctx.mono(f"bold blink red{ctx.bg}", "bold"))]
|
|
255
265
|
else:
|
|
256
266
|
return [(" ", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
257
267
|
|
|
@@ -278,7 +288,7 @@ def render_sleep_countdown(ctx: ColumnContext) -> ColumnOutput:
|
|
|
278
288
|
"""
|
|
279
289
|
if ctx.sleep_wake_estimate is not None:
|
|
280
290
|
remaining = max(0, (ctx.sleep_wake_estimate - datetime.now()).total_seconds())
|
|
281
|
-
return [(f" ⏰{format_duration(remaining):>5} ", ctx.mono(f"yellow{ctx.bg}", "bold"))]
|
|
291
|
+
return [(f" {ctx.e('⏰')}{format_duration(remaining):>5} ", ctx.mono(f"yellow{ctx.bg}", "bold"))]
|
|
282
292
|
return None
|
|
283
293
|
|
|
284
294
|
|
|
@@ -440,21 +450,21 @@ render_median_work_time = _make_simple_render("median_work", format_duration, "
|
|
|
440
450
|
def render_subagent_count(ctx: ColumnContext) -> ColumnOutput:
|
|
441
451
|
count = ctx.live_subagent_count
|
|
442
452
|
style = ctx.mono(f"bold purple{ctx.bg}", "bold") if count > 0 else ctx.mono(f"dim{ctx.bg}", "dim")
|
|
443
|
-
return [(f" 🤿{count:>2}", style)]
|
|
453
|
+
return [(f" {ctx.e('🤿')}{count:>2}", style)]
|
|
444
454
|
|
|
445
455
|
|
|
446
456
|
def render_bash_count(ctx: ColumnContext) -> ColumnOutput:
|
|
447
457
|
count = ctx.background_bash_count
|
|
448
458
|
style = ctx.mono(f"bold yellow{ctx.bg}", "bold") if count > 0 else ctx.mono(f"dim{ctx.bg}", "dim")
|
|
449
|
-
return [(f" 🐚{count:>2}", style)]
|
|
459
|
+
return [(f" {ctx.e('🐚')}{count:>2}", style)]
|
|
450
460
|
|
|
451
461
|
|
|
452
462
|
def render_child_count(ctx: ColumnContext) -> ColumnOutput:
|
|
453
463
|
count = ctx.child_count
|
|
454
464
|
if count == 0:
|
|
455
|
-
return [(" 👶 0", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
465
|
+
return [(f" {ctx.e('👶')} 0", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
456
466
|
style = ctx.mono(f"bold cyan{ctx.bg}", "bold")
|
|
457
|
-
return [(f" 👶{count:>2}", style)]
|
|
467
|
+
return [(f" {ctx.e('👶')}{count:>2}", style)]
|
|
458
468
|
|
|
459
469
|
|
|
460
470
|
render_permission_mode = _make_simple_render("perm_emoji", format_str=" {v}", colored_style="bold white")
|
|
@@ -462,7 +472,7 @@ render_permission_mode = _make_simple_render("perm_emoji", format_str=" {v}", co
|
|
|
462
472
|
|
|
463
473
|
def render_agent_teams(ctx: ColumnContext) -> ColumnOutput:
|
|
464
474
|
if ctx.session.agent_teams:
|
|
465
|
-
return [(" 🤝", ctx.mono(f"bold cyan{ctx.bg}", "bold"))]
|
|
475
|
+
return [(f" {ctx.e('🤝')}", ctx.mono(f"bold cyan{ctx.bg}", "bold"))]
|
|
466
476
|
return None
|
|
467
477
|
|
|
468
478
|
|
|
@@ -473,7 +483,7 @@ def render_teams_plain(ctx: ColumnContext) -> Optional[str]:
|
|
|
473
483
|
|
|
474
484
|
|
|
475
485
|
def render_allowed_tools(ctx: ColumnContext) -> ColumnOutput:
|
|
476
|
-
emojis = _tool_emojis(ctx.session.allowed_tools)
|
|
486
|
+
emojis = _tool_emojis(ctx.session.allowed_tools, emoji_free=ctx.emoji_free)
|
|
477
487
|
if not emojis:
|
|
478
488
|
return None
|
|
479
489
|
return [(f" {emojis}", ctx.mono(f"white{ctx.bg}", ""))]
|
|
@@ -481,7 +491,7 @@ def render_allowed_tools(ctx: ColumnContext) -> ColumnOutput:
|
|
|
481
491
|
|
|
482
492
|
def render_time_context(ctx: ColumnContext) -> ColumnOutput:
|
|
483
493
|
if ctx.session.time_context_enabled:
|
|
484
|
-
return [(" 🕐", ctx.mono(f"bold white{ctx.bg}", "bold"))]
|
|
494
|
+
return [(f" {ctx.e('🕐')}", ctx.mono(f"bold white{ctx.bg}", "bold"))]
|
|
485
495
|
else:
|
|
486
496
|
return [(" ·", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
487
497
|
|
|
@@ -489,24 +499,26 @@ def render_time_context(ctx: ColumnContext) -> ColumnOutput:
|
|
|
489
499
|
def render_human_count(ctx: ColumnContext) -> ColumnOutput:
|
|
490
500
|
if ctx.claude_stats is not None:
|
|
491
501
|
human_count = max(0, ctx.claude_stats.interaction_count - ctx.stats.steers_count)
|
|
492
|
-
return [(f" 👤{human_count:>3}", ctx.mono(f"bold yellow{ctx.bg}", "bold"))]
|
|
502
|
+
return [(f" {ctx.e('👤')}{human_count:>3}", ctx.mono(f"bold yellow{ctx.bg}", "bold"))]
|
|
493
503
|
else:
|
|
494
|
-
return [(" 👤 -", ctx.mono(f"dim yellow{ctx.bg}", "dim"))]
|
|
504
|
+
return [(f" {ctx.e('👤')} -", ctx.mono(f"dim yellow{ctx.bg}", "dim"))]
|
|
495
505
|
|
|
496
506
|
|
|
497
|
-
render_robot_count
|
|
507
|
+
def render_robot_count(ctx: ColumnContext) -> ColumnOutput:
|
|
508
|
+
v = ctx.stats.steers_count
|
|
509
|
+
return [(f" {ctx.e('🤖')}{v:>3}", ctx.mono(f"bold cyan{ctx.bg}", "bold"))]
|
|
498
510
|
|
|
499
511
|
|
|
500
512
|
def render_standing_orders(ctx: ColumnContext) -> ColumnOutput:
|
|
501
513
|
s = ctx.session
|
|
502
514
|
if s.standing_instructions:
|
|
503
515
|
if s.standing_orders_complete:
|
|
504
|
-
return [(" ✓", ctx.mono(f"bold green{ctx.bg}", "bold"))]
|
|
516
|
+
return [(f" {ctx.e('✓')}", ctx.mono(f"bold green{ctx.bg}", "bold"))]
|
|
505
517
|
elif s.standing_instructions_preset:
|
|
506
518
|
preset_display = f" {s.standing_instructions_preset[:8]}"
|
|
507
519
|
return [(preset_display, ctx.mono(f"bold cyan{ctx.bg}", "bold"))]
|
|
508
520
|
else:
|
|
509
|
-
return [(" 📋", ctx.mono(f"bold yellow{ctx.bg}", "bold"))]
|
|
521
|
+
return [(f" {ctx.e('📋')}", ctx.mono(f"bold yellow{ctx.bg}", "bold"))]
|
|
510
522
|
else:
|
|
511
523
|
return [(" ➖", ctx.mono(f"bold dim{ctx.bg}", "dim"))]
|
|
512
524
|
|
|
@@ -525,24 +537,26 @@ def render_oversight_countdown(ctx: ColumnContext) -> ColumnOutput:
|
|
|
525
537
|
|
|
526
538
|
deadline_str = ctx.oversight_deadline
|
|
527
539
|
if not deadline_str:
|
|
528
|
-
|
|
540
|
+
hg = ctx.e("⏳")
|
|
541
|
+
return [(f" {hg} --:--", ctx.mono(f"yellow{ctx.bg}", "dim"))]
|
|
529
542
|
|
|
530
543
|
try:
|
|
544
|
+
hg = ctx.e("⏳")
|
|
531
545
|
deadline = datetime.fromisoformat(deadline_str)
|
|
532
546
|
remaining = (deadline - datetime.now()).total_seconds()
|
|
533
547
|
if remaining <= 0:
|
|
534
|
-
return [("
|
|
548
|
+
return [(f" {hg} 0s ", ctx.mono(f"bold blink red{ctx.bg}", "bold"))]
|
|
535
549
|
|
|
536
550
|
if remaining < 60:
|
|
537
|
-
text = f"
|
|
551
|
+
text = f" {hg} {remaining:>3.0f}s"
|
|
538
552
|
elif remaining < 3600:
|
|
539
553
|
mins = int(remaining // 60)
|
|
540
554
|
secs = int(remaining % 60)
|
|
541
|
-
text = f"
|
|
555
|
+
text = f" {hg}{mins:>2}m{secs:02d}s"
|
|
542
556
|
else:
|
|
543
557
|
hrs = int(remaining // 3600)
|
|
544
558
|
mins = int((remaining % 3600) // 60)
|
|
545
|
-
text = f"
|
|
559
|
+
text = f" {hg}{hrs:>2}h{mins:02d}m"
|
|
546
560
|
|
|
547
561
|
if remaining < 30:
|
|
548
562
|
style = ctx.mono(f"bold blink red{ctx.bg}", "bold")
|
|
@@ -552,14 +566,15 @@ def render_oversight_countdown(ctx: ColumnContext) -> ColumnOutput:
|
|
|
552
566
|
style = ctx.mono(f"bold yellow{ctx.bg}", "bold")
|
|
553
567
|
return [(text, style)]
|
|
554
568
|
except (ValueError, TypeError):
|
|
555
|
-
return [("
|
|
569
|
+
return [(f" {hg} --:--", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
556
570
|
|
|
557
571
|
|
|
558
572
|
def render_heartbeat(ctx: ColumnContext) -> ColumnOutput:
|
|
559
573
|
s = ctx.session
|
|
574
|
+
hb = ctx.e("💓")
|
|
560
575
|
if s.heartbeat_enabled and not s.heartbeat_paused:
|
|
561
576
|
freq_str = format_duration(s.heartbeat_frequency_seconds)
|
|
562
|
-
segments = [(f"
|
|
577
|
+
segments = [(f" {hb}{freq_str:>5}", ctx.mono(f"bold magenta{ctx.bg}", "bold"))]
|
|
563
578
|
# Next heartbeat time in 24hr format
|
|
564
579
|
next_time_str = None
|
|
565
580
|
if s.last_heartbeat_time:
|
|
@@ -584,24 +599,24 @@ def render_heartbeat(ctx: ColumnContext) -> ColumnOutput:
|
|
|
584
599
|
elif s.heartbeat_enabled and s.heartbeat_paused:
|
|
585
600
|
freq_str = format_duration(s.heartbeat_frequency_seconds)
|
|
586
601
|
return [
|
|
587
|
-
(f"
|
|
602
|
+
(f" {hb}{freq_str:>5}", ctx.mono(f"dim{ctx.bg}", "dim")),
|
|
588
603
|
(" ⏸ ", ctx.mono(f"bold yellow{ctx.bg}", "bold")),
|
|
589
604
|
]
|
|
590
605
|
else:
|
|
591
|
-
return [("
|
|
606
|
+
return [(f" {hb} - @--:--", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
592
607
|
|
|
593
608
|
|
|
594
609
|
def render_agent_value(ctx: ColumnContext) -> ColumnOutput:
|
|
595
610
|
s = ctx.session
|
|
596
611
|
if ctx.summary_detail in ("full", "high"):
|
|
597
|
-
return [(f" 💰{s.agent_value:>4}", ctx.mono(f"bold magenta{ctx.bg}", "bold"))]
|
|
612
|
+
return [(f" {ctx.e('💰')}{s.agent_value:>4}", ctx.mono(f"bold magenta{ctx.bg}", "bold"))]
|
|
598
613
|
else:
|
|
599
614
|
if s.agent_value > 1000:
|
|
600
|
-
return [(" ⏫️", ctx.mono(f"bold red{ctx.bg}", "bold"))]
|
|
615
|
+
return [(f" {ctx.e('⏫️')}", ctx.mono(f"bold red{ctx.bg}", "bold"))]
|
|
601
616
|
elif s.agent_value < 1000:
|
|
602
|
-
return [(" ⏬️", ctx.mono(f"bold blue{ctx.bg}", "bold"))]
|
|
617
|
+
return [(f" {ctx.e('⏬️')}", ctx.mono(f"bold blue{ctx.bg}", "bold"))]
|
|
603
618
|
else:
|
|
604
|
-
return [(" ⏹️ ", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
619
|
+
return [(f" {ctx.e('⏹️')} ", ctx.mono(f"dim{ctx.bg}", "dim"))]
|
|
605
620
|
|
|
606
621
|
|
|
607
622
|
# ---------------------------------------------------------------------------
|
|
@@ -901,14 +916,15 @@ def build_cli_context(
|
|
|
901
916
|
any_has_budget: bool = False, child_count: int = 0, any_is_sleeping: bool = False,
|
|
902
917
|
any_has_oversight_timeout: bool = False, oversight_deadline: Optional[str] = None,
|
|
903
918
|
pr_number: Optional[int] = None, any_has_pr: bool = False,
|
|
904
|
-
monochrome: bool = True, summary_detail: str = "full",
|
|
919
|
+
monochrome: bool = True, emoji_free: bool = False, summary_detail: str = "full",
|
|
905
920
|
has_sisters: bool = False, local_hostname: str = "",
|
|
906
921
|
max_name_width: int = 16, max_repo_width: int = 10,
|
|
907
922
|
max_branch_width: int = 10, all_names_match_repos: bool = False,
|
|
908
923
|
subtree_cost_usd: float = 0.0, any_has_subtree_cost: bool = False,
|
|
909
924
|
) -> ColumnContext:
|
|
910
925
|
"""Build a ColumnContext from CLI data (no TUI widget needed)."""
|
|
911
|
-
|
|
926
|
+
from .status_constants import emoji_or_ascii
|
|
927
|
+
status_symbol, _ = get_status_symbol(status, emoji_free=emoji_free)
|
|
912
928
|
uptime = calculate_uptime(session.start_time) if session.start_time else "-"
|
|
913
929
|
green_time, non_green_time, sleep_time = get_current_state_times(
|
|
914
930
|
stats, is_asleep=session.is_asleep
|
|
@@ -917,11 +933,11 @@ def build_cli_context(
|
|
|
917
933
|
|
|
918
934
|
# Permissiveness mode emoji
|
|
919
935
|
if session.permissiveness_mode == "bypass":
|
|
920
|
-
perm_emoji = "🔥"
|
|
936
|
+
perm_emoji = emoji_or_ascii("🔥", emoji_free)
|
|
921
937
|
elif session.permissiveness_mode == "permissive":
|
|
922
|
-
perm_emoji = "🏃"
|
|
938
|
+
perm_emoji = emoji_or_ascii("🏃", emoji_free)
|
|
923
939
|
else:
|
|
924
|
-
perm_emoji = "👮"
|
|
940
|
+
perm_emoji = emoji_or_ascii("👮", emoji_free)
|
|
925
941
|
|
|
926
942
|
# Parse state_since for time-in-state
|
|
927
943
|
status_changed_at = None
|
|
@@ -947,6 +963,7 @@ def build_cli_context(
|
|
|
947
963
|
status_color="bold",
|
|
948
964
|
bg="",
|
|
949
965
|
monochrome=monochrome,
|
|
966
|
+
emoji_free=emoji_free,
|
|
950
967
|
summary_detail=summary_detail,
|
|
951
968
|
show_cost=True,
|
|
952
969
|
any_has_budget=any_has_budget,
|
|
@@ -146,6 +146,17 @@ class TmuxManager:
|
|
|
146
146
|
if rest:
|
|
147
147
|
pane.send_keys(rest, enter=False)
|
|
148
148
|
time.sleep(0.1)
|
|
149
|
+
elif keys.startswith('/') and len(keys) > 1:
|
|
150
|
+
# Special handling for slash commands (#307)
|
|
151
|
+
# Claude Code shows a command menu when / is typed;
|
|
152
|
+
# send / separately so the menu has time to appear
|
|
153
|
+
# before the rest of the command and Enter arrive.
|
|
154
|
+
pane.send_keys('/', enter=False)
|
|
155
|
+
time.sleep(0.3)
|
|
156
|
+
rest = keys[1:]
|
|
157
|
+
if rest:
|
|
158
|
+
pane.send_keys(rest, enter=False)
|
|
159
|
+
time.sleep(0.15)
|
|
149
160
|
else:
|
|
150
161
|
pane.send_keys(keys, enter=False)
|
|
151
162
|
# Small delay for Claude Code to process text
|