overcode 0.2.3__tar.gz → 0.2.4__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.
Files changed (111) hide show
  1. {overcode-0.2.3/src/overcode.egg-info → overcode-0.2.4}/PKG-INFO +1 -1
  2. {overcode-0.2.3 → overcode-0.2.4}/pyproject.toml +1 -1
  3. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/implementations.py +11 -0
  4. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/settings.py +7 -0
  5. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/status_constants.py +82 -4
  6. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/summary_columns.py +52 -35
  7. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tmux_manager.py +11 -0
  8. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui.py +189 -15
  9. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui.tcss +16 -0
  10. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_actions/session.py +2 -1
  11. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_actions/view.py +21 -0
  12. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_helpers.py +3 -2
  13. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/__init__.py +2 -0
  14. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/command_bar.py +1 -0
  15. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/help_overlay.py +4 -3
  16. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/session_summary.py +9 -5
  17. overcode-0.2.4/src/overcode/tui_widgets/sister_selection_modal.py +154 -0
  18. {overcode-0.2.3 → overcode-0.2.4/src/overcode.egg-info}/PKG-INFO +1 -1
  19. {overcode-0.2.3 → overcode-0.2.4}/src/overcode.egg-info/SOURCES.txt +1 -0
  20. {overcode-0.2.3 → overcode-0.2.4}/LICENSE +0 -0
  21. {overcode-0.2.3 → overcode-0.2.4}/MANIFEST.in +0 -0
  22. {overcode-0.2.3 → overcode-0.2.4}/README.md +0 -0
  23. {overcode-0.2.3 → overcode-0.2.4}/setup.cfg +0 -0
  24. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/__init__.py +0 -0
  25. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/agent_scanner.py +0 -0
  26. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/bundled_skills.py +0 -0
  27. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/claude_config.py +0 -0
  28. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/__init__.py +0 -0
  29. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/__main__.py +0 -0
  30. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/_shared.py +0 -0
  31. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/agent.py +0 -0
  32. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/budget.py +0 -0
  33. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/config.py +0 -0
  34. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/daemon.py +0 -0
  35. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/hooks.py +0 -0
  36. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/monitoring.py +0 -0
  37. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/perms.py +0 -0
  38. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/sister.py +0 -0
  39. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/cli/skills.py +0 -0
  40. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/config.py +0 -0
  41. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/daemon_claude_skill.md +0 -0
  42. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/daemon_logging.py +0 -0
  43. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/daemon_utils.py +0 -0
  44. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/data_export.py +0 -0
  45. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/dependency_check.py +0 -0
  46. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/exceptions.py +0 -0
  47. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/follow_mode.py +0 -0
  48. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/history_reader.py +0 -0
  49. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/hook_handler.py +0 -0
  50. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/hook_status_detector.py +0 -0
  51. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/interfaces.py +0 -0
  52. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/launcher.py +0 -0
  53. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/logging_config.py +0 -0
  54. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/mocks.py +0 -0
  55. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/monitor_daemon.py +0 -0
  56. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/monitor_daemon_core.py +0 -0
  57. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/monitor_daemon_state.py +0 -0
  58. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/notifier.py +0 -0
  59. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/pid_utils.py +0 -0
  60. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/presence_logger.py +0 -0
  61. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/protocols.py +0 -0
  62. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/session_manager.py +0 -0
  63. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/sister_controller.py +0 -0
  64. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/sister_poller.py +0 -0
  65. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/standing_instructions.py +0 -0
  66. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/status_detector.py +0 -0
  67. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/status_detector_factory.py +0 -0
  68. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/status_history.py +0 -0
  69. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/status_patterns.py +0 -0
  70. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/summarizer_client.py +0 -0
  71. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/summarizer_component.py +0 -0
  72. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/summary_groups.py +0 -0
  73. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/supervisor_daemon.py +0 -0
  74. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/supervisor_daemon_core.py +0 -0
  75. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/supervisor_layout.sh +0 -0
  76. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/testing/__init__.py +0 -0
  77. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/testing/renderer.py +0 -0
  78. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/testing/tmux_driver.py +0 -0
  79. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/testing/tui_eye.py +0 -0
  80. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/testing/tui_eye_skill.md +0 -0
  81. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/time_context.py +0 -0
  82. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tmux_utils.py +0 -0
  83. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_actions/__init__.py +0 -0
  84. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_actions/daemon.py +0 -0
  85. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_actions/input.py +0 -0
  86. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_actions/navigation.py +0 -0
  87. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_logic.py +0 -0
  88. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_render.py +0 -0
  89. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  90. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  91. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  92. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  93. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  94. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/preview_pane.py +0 -0
  95. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/status_timeline.py +0 -0
  96. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  97. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/usage_monitor.py +0 -0
  98. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web/__init__.py +0 -0
  99. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web/templates/analytics.html +0 -0
  100. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web/templates/dashboard.html +0 -0
  101. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web_api.py +0 -0
  102. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web_chartjs.py +0 -0
  103. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web_control_api.py +0 -0
  104. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web_server.py +0 -0
  105. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web_server_runner.py +0 -0
  106. {overcode-0.2.3 → overcode-0.2.4}/src/overcode/web_templates.py +0 -0
  107. {overcode-0.2.3 → overcode-0.2.4}/src/overcode.egg-info/dependency_links.txt +0 -0
  108. {overcode-0.2.3 → overcode-0.2.4}/src/overcode.egg-info/entry_points.txt +0 -0
  109. {overcode-0.2.3 → overcode-0.2.4}/src/overcode.egg-info/requires.txt +0 -0
  110. {overcode-0.2.3 → overcode-0.2.4}/src/overcode.egg-info/top_level.txt +0 -0
  111. {overcode-0.2.3 → overcode-0.2.4}/tests/test_e2e_multi_agent_jokes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overcode
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A supervisor for managing multiple Claude Code instances in tmux
5
5
  Author: Mike Bond
6
6
  Project-URL: Homepage, https://github.com/mkb23/overcode
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -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
@@ -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
- def get_status_emoji(status: str) -> str:
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
- return STATUS_EMOJIS.get(status, "⚪")
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
- return STATUS_SYMBOLS.get(status, ("⚪", "dim"))
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 "".join(emojis) + suffix
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)
@@ -156,6 +159,13 @@ class ColumnContext:
156
159
  """Return simplified style when monochrome is enabled."""
157
160
  return simple if self.monochrome else colored
158
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
168
+
159
169
 
160
170
  # ---------------------------------------------------------------------------
161
171
  # SummaryColumn definition
@@ -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 = _make_simple_render("stats.steers_count", format_str=" 🤖{v:>3}", colored_style="bold cyan")
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
- return [(" ⏳ --:--", ctx.mono(f"yellow{ctx.bg}", "dim"))]
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 [(" 0s ", ctx.mono(f"bold blink red{ctx.bg}", "bold"))]
548
+ return [(f" {hg} 0s ", ctx.mono(f"bold blink red{ctx.bg}", "bold"))]
535
549
 
536
550
  if remaining < 60:
537
- text = f" {remaining:>3.0f}s"
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" {mins:>2}m{secs:02d}s"
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" {hrs:>2}h{mins:02d}m"
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 [(" --:--", ctx.mono(f"dim{ctx.bg}", "dim"))]
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" 💓{freq_str:>5}", ctx.mono(f"bold magenta{ctx.bg}", "bold"))]
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" 💓{freq_str:>5}", ctx.mono(f"dim{ctx.bg}", "dim")),
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 [(" 💓 - @--:--", ctx.mono(f"dim{ctx.bg}", "dim"))]
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
- status_symbol, _ = get_status_symbol(status)
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