overcode 0.5.5__tar.gz → 0.5.6__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 (171) hide show
  1. {overcode-0.5.5/src/overcode.egg-info → overcode-0.5.6}/PKG-INFO +1 -1
  2. {overcode-0.5.5 → overcode-0.5.6}/pyproject.toml +1 -1
  3. overcode-0.5.6/src/overcode/command_palette.py +538 -0
  4. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/settings.py +5 -2
  5. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summary_columns.py +222 -5
  6. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui.py +211 -85
  7. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui.tcss +9 -10
  8. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/view.py +31 -7
  9. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_logic.py +94 -35
  10. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/__init__.py +5 -2
  11. overcode-0.5.6/src/overcode/tui_widgets/column_header.py +99 -0
  12. overcode-0.5.6/src/overcode/tui_widgets/command_palette.py +609 -0
  13. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/help_overlay.py +2 -1
  14. overcode-0.5.6/src/overcode/tui_widgets/jump_modal.py +40 -0
  15. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/summary_config_modal.py +23 -1
  16. {overcode-0.5.5 → overcode-0.5.6/src/overcode.egg-info}/PKG-INFO +1 -1
  17. {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/SOURCES.txt +3 -0
  18. overcode-0.5.5/src/overcode/tui_widgets/jump_modal.py +0 -163
  19. {overcode-0.5.5 → overcode-0.5.6}/LICENSE +0 -0
  20. {overcode-0.5.5 → overcode-0.5.6}/MANIFEST.in +0 -0
  21. {overcode-0.5.5 → overcode-0.5.6}/README.md +0 -0
  22. {overcode-0.5.5 → overcode-0.5.6}/setup.cfg +0 -0
  23. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/__init__.py +0 -0
  24. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/agent_scanner.py +0 -0
  25. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/__init__.py +0 -0
  26. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/base.py +0 -0
  27. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/claude_code.py +0 -0
  28. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/codex.py +0 -0
  29. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/codex_stats.py +0 -0
  30. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/grok.py +0 -0
  31. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/grok_stats.py +0 -0
  32. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/hermes.py +0 -0
  33. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/hermes_stats.py +0 -0
  34. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode.py +0 -0
  35. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2.py +0 -0
  36. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2_patterns.py +0 -0
  37. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2_plugin_install.py +0 -0
  38. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2_stats.py +0 -0
  39. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode_stats.py +0 -0
  40. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/bundled_skills.py +0 -0
  41. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/claude_config.py +0 -0
  42. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/claude_pid.py +0 -0
  43. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/__init__.py +0 -0
  44. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/__main__.py +0 -0
  45. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/_shared.py +0 -0
  46. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/agent.py +0 -0
  47. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/budget.py +0 -0
  48. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/config.py +0 -0
  49. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/daemon.py +0 -0
  50. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/doctor.py +0 -0
  51. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/focal.py +0 -0
  52. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/hooks.py +0 -0
  53. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/jobs.py +0 -0
  54. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/models.py +0 -0
  55. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/monitoring.py +0 -0
  56. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/parallelism.py +0 -0
  57. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/perms.py +0 -0
  58. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/sister.py +0 -0
  59. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/skills.py +0 -0
  60. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/split.py +0 -0
  61. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/tags.py +0 -0
  62. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/wrappers.py +0 -0
  63. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/config.py +0 -0
  64. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/daemon_claude_skill.md +0 -0
  65. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/daemon_logging.py +0 -0
  66. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/daemon_utils.py +0 -0
  67. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/data/model_metadata.json +0 -0
  68. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/data_export.py +0 -0
  69. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/dependency_check.py +0 -0
  70. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/doctor.py +0 -0
  71. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/duration.py +0 -0
  72. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/entrypoint.py +0 -0
  73. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/exceptions.py +0 -0
  74. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/follow_mode.py +0 -0
  75. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hermes_plugin/__init__.py +0 -0
  76. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hermes_plugin/plugin.yaml +0 -0
  77. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/history_reader.py +0 -0
  78. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hook_handler.py +0 -0
  79. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hook_status_detector.py +0 -0
  80. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/implementations.py +0 -0
  81. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/interfaces.py +0 -0
  82. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/job_launcher.py +0 -0
  83. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/job_manager.py +0 -0
  84. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/launcher.py +0 -0
  85. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/logging_config.py +0 -0
  86. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/mocks.py +0 -0
  87. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/model_metadata.py +0 -0
  88. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/monitor_daemon.py +0 -0
  89. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/monitor_daemon_core.py +0 -0
  90. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/monitor_daemon_state.py +0 -0
  91. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/notifier.py +0 -0
  92. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry-v2/index.js +0 -0
  93. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry-v2/overcode-telemetry-core.mjs +0 -0
  94. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry-v2/tui.js +0 -0
  95. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry.js +0 -0
  96. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/pane_capture_gate.py +0 -0
  97. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/pid_utils.py +0 -0
  98. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/presence_logger.py +0 -0
  99. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/pricing.py +0 -0
  100. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/process_resources.py +0 -0
  101. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/protocols.py +0 -0
  102. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/sandbox_detect.py +0 -0
  103. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/session_manager.py +0 -0
  104. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/sister_controller.py +0 -0
  105. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/sister_poller.py +0 -0
  106. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/ssh_provisioner.py +0 -0
  107. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/standing_instructions.py +0 -0
  108. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/stat_gate.py +0 -0
  109. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/stats_reader.py +0 -0
  110. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_constants.py +0 -0
  111. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_detector.py +0 -0
  112. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_detector_factory.py +0 -0
  113. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_history.py +0 -0
  114. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_patterns.py +0 -0
  115. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summarizer_client.py +0 -0
  116. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summarizer_component.py +0 -0
  117. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summary_groups.py +0 -0
  118. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/supervisor_daemon.py +0 -0
  119. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/supervisor_daemon_core.py +0 -0
  120. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/supervisor_layout.sh +0 -0
  121. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/__init__.py +0 -0
  122. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/renderer.py +0 -0
  123. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/tmux_driver.py +0 -0
  124. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/tui_eye.py +0 -0
  125. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/tui_eye_skill.md +0 -0
  126. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/time_context.py +0 -0
  127. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tmux_manager.py +0 -0
  128. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tmux_utils.py +0 -0
  129. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/transcript_index.py +0 -0
  130. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/__init__.py +0 -0
  131. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/daemon.py +0 -0
  132. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/input.py +0 -0
  133. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/navigation.py +0 -0
  134. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/session.py +0 -0
  135. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_helpers.py +0 -0
  136. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_render.py +0 -0
  137. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  138. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/command_bar.py +0 -0
  139. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  140. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  141. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  142. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  143. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/job_summary.py +0 -0
  144. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/modal_base.py +0 -0
  145. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  146. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/new_agent_modal.py +0 -0
  147. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/passthru_config_modal.py +0 -0
  148. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/preview_pane.py +0 -0
  149. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/rename_agent_modal.py +0 -0
  150. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/session_summary.py +0 -0
  151. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  152. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/status_timeline.py +0 -0
  153. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/tmux_config_modal.py +0 -0
  154. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/tui_log_panel.py +0 -0
  155. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/usage_monitor.py +0 -0
  156. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web/__init__.py +0 -0
  157. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web/templates/analytics.html +0 -0
  158. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web/templates/dashboard.html +0 -0
  159. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_api.py +0 -0
  160. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_chartjs.py +0 -0
  161. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_control_api.py +0 -0
  162. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_server.py +0 -0
  163. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_server_runner.py +0 -0
  164. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_templates.py +0 -0
  165. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/worker_guard.py +0 -0
  166. {overcode-0.5.5 → overcode-0.5.6}/src/overcode/wrapper.py +0 -0
  167. {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/dependency_links.txt +0 -0
  168. {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/entry_points.txt +0 -0
  169. {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/requires.txt +0 -0
  170. {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/top_level.txt +0 -0
  171. {overcode-0.5.5 → overcode-0.5.6}/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.5.5
3
+ Version: 0.5.6
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.5.5"
7
+ version = "0.5.6"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -0,0 +1,538 @@
1
+ """
2
+ Command palette registry and matcher (#482).
3
+
4
+ Pure logic, no Textual: the list of commands the palette offers, how their
5
+ keys are labelled, how each stateful command reports its states, and the
6
+ fuzzy matcher that ranks them. The widget in tui_widgets/command_palette.py
7
+ only draws what this module decides.
8
+
9
+ Keys are never written here. They are read from SupervisorTUI.BINDINGS at
10
+ runtime, so the palette cannot drift from what a key actually does.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # States
21
+ # ---------------------------------------------------------------------------
22
+
23
+ @dataclass(frozen=True)
24
+ class StateView:
25
+ """What a stateful command is set to right now.
26
+
27
+ `options` is the cycle in order and `current` indexes it. A stepper with
28
+ no fixed cycle (the baseline) has one option — its value — at index 0.
29
+ `current` is None when the state is unknown or does not apply (no agent
30
+ focused, no heartbeat configured).
31
+ """
32
+ options: Tuple[str, ...]
33
+ current: Optional[int]
34
+ note: str = "" # e.g. the agent a per-agent state belongs to
35
+
36
+ @property
37
+ def current_label(self) -> Optional[str]:
38
+ if self.current is None or not (0 <= self.current < len(self.options)):
39
+ return None
40
+ return self.options[self.current]
41
+
42
+
43
+ OFF_ON = ("off", "on")
44
+
45
+
46
+ def _toggle(value: Optional[bool], note: str = "") -> StateView:
47
+ return StateView(OFF_ON, None if value is None else int(bool(value)), note)
48
+
49
+
50
+ def _cycle(options: Sequence[str], value: Any, values: Optional[Sequence[Any]] = None) -> StateView:
51
+ values = list(values) if values is not None else list(options)
52
+ try:
53
+ idx: Optional[int] = values.index(value)
54
+ except ValueError:
55
+ idx = None
56
+ return StateView(tuple(options), idx)
57
+
58
+
59
+ def _focused_session(app: Any):
60
+ """The focused agent's Session, or None (jobs view, no agents)."""
61
+ if getattr(app, "tui_mode", "agents") != "agents":
62
+ return None
63
+ try:
64
+ widget = app._get_focused_widget()
65
+ except Exception:
66
+ return None
67
+ return getattr(widget, "session", None)
68
+
69
+
70
+ def _widget_shown(app: Any, widget_id: str) -> Optional[bool]:
71
+ try:
72
+ return bool(app.query_one(f"#{widget_id}").display)
73
+ except Exception:
74
+ return None
75
+
76
+
77
+ def _agent_toggle(attr: str) -> Callable[[Any], StateView]:
78
+ def state(app: Any) -> StateView:
79
+ s = _focused_session(app)
80
+ if s is None:
81
+ return _toggle(None)
82
+ return _toggle(bool(getattr(s, attr, False)), s.name)
83
+ return state
84
+
85
+
86
+ def _heartbeat_state(app: Any) -> StateView:
87
+ s = _focused_session(app)
88
+ options = ("running", "paused")
89
+ if s is None or not getattr(s, "heartbeat_enabled", False):
90
+ return StateView(options, None, "no heartbeat" if s is not None else "")
91
+ return StateView(options, int(bool(s.heartbeat_paused)), s.name)
92
+
93
+
94
+ def _detection_state(app: Any) -> StateView:
95
+ s = _focused_session(app)
96
+ options = ("hooks", "polling")
97
+ if s is None:
98
+ return StateView(options, None)
99
+ try:
100
+ from .status_detector_factory import resolve_session_detection_mode
101
+ mode = resolve_session_detection_mode(s, app.detector.mode)
102
+ except Exception:
103
+ return StateView(options, None)
104
+ return StateView(options, options.index(mode) if mode in options else None, s.name)
105
+
106
+
107
+ def _cost_state(app: Any) -> StateView:
108
+ options = ("tokens", "cost", "joules", "all")
109
+ try:
110
+ level = app.SUMMARY_LEVELS[app.summary_level_index]
111
+ idx = app._match_cost_preset(app._prefs.column_config.get(level, {}))
112
+ except Exception:
113
+ return StateView(options, None)
114
+ return StateView(options, idx)
115
+
116
+
117
+ def _baseline_state(app: Any) -> StateView:
118
+ minutes = getattr(app, "baseline_minutes", 0) or 0
119
+ if minutes == 0:
120
+ label = "now"
121
+ elif minutes % 60 == 0:
122
+ label = f"-{minutes // 60}h"
123
+ elif minutes < 60:
124
+ label = f"-{minutes}m"
125
+ else:
126
+ label = f"-{minutes // 60}h{minutes % 60}m"
127
+ return StateView((label,), 0)
128
+
129
+
130
+ def _summarizer_state(app: Any) -> StateView:
131
+ try:
132
+ return _toggle(bool(app._summarizer.config.enabled))
133
+ except Exception:
134
+ return _toggle(None)
135
+
136
+
137
+ def _web_state(app: Any) -> StateView:
138
+ try:
139
+ from .web_server import is_web_server_running
140
+ return _toggle(is_web_server_running(app.tmux_session))
141
+ except Exception:
142
+ return _toggle(None)
143
+
144
+
145
+ def _sort_state(app: Any) -> StateView:
146
+ prefs = getattr(app, "_prefs", None)
147
+ if prefs is None:
148
+ return StateView(("?",), None)
149
+ from .tui_logic import get_sort_mode_display_name, sort_descending
150
+ label = get_sort_mode_display_name(prefs.sort_mode)
151
+ if prefs.sort_mode != "by_tree":
152
+ label += " ▼" if sort_descending(prefs.sort_mode, prefs.sort_reversed) else " ▲"
153
+ return StateView((label,), 0)
154
+
155
+
156
+ def _sort_direction_state(app: Any) -> StateView:
157
+ prefs = getattr(app, "_prefs", None)
158
+ options = ("▲ asc", "▼ desc")
159
+ if prefs is None or prefs.sort_mode == "by_tree":
160
+ return StateView(options, None, "tree order" if prefs is not None else "")
161
+ from .tui_logic import sort_descending
162
+ return StateView(options, int(sort_descending(prefs.sort_mode, prefs.sort_reversed)))
163
+
164
+
165
+ def _notifications_state(app: Any) -> StateView:
166
+ options = ("off", "sound", "banner", "both")
167
+ try:
168
+ return _cycle(options, app._notifier.mode)
169
+ except Exception:
170
+ return StateView(options, None)
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Registry
175
+ # ---------------------------------------------------------------------------
176
+
177
+ @dataclass(frozen=True)
178
+ class PaletteCommand:
179
+ """One command the palette can run.
180
+
181
+ action: the SupervisorTUI action name (method `action_<name>`).
182
+ title: what the palette shows; nouns for toggles ("Preview pane"),
183
+ since the state column already says off/on.
184
+ keywords: extra words the matcher accepts but does not display.
185
+ state: reports the current state; None for one-shot commands.
186
+ agent: acts on the focused agent (its state is that agent's).
187
+ """
188
+ action: str
189
+ title: str
190
+ category: str
191
+ keywords: str = ""
192
+ state: Optional[Callable[[Any], StateView]] = field(default=None, compare=False)
193
+ agent: bool = False
194
+
195
+ @property
196
+ def repeatable(self) -> bool:
197
+ """Safe to run with the palette left open (Tab): changes a setting
198
+ in place and opens nothing."""
199
+ return self.state is not None
200
+
201
+
202
+ CATEGORIES = (
203
+ "Navigate", "View", "Display", "Agent", "Send to agent",
204
+ "Daemons", "Settings", "App",
205
+ )
206
+
207
+ _C = PaletteCommand
208
+
209
+ COMMANDS: Tuple[PaletteCommand, ...] = (
210
+ # Navigate
211
+ _C("jump_to_agent", "Jump to agent…", "Navigate", "switch go find goto"),
212
+ _C("jump_to_attention", "Next agent needing you", "Navigate", "bell red stalled waiting"),
213
+ _C("focus_next_session", "Next agent", "Navigate", "down"),
214
+ _C("focus_previous_session", "Previous agent", "Navigate", "up"),
215
+ _C("filter_by_tag", "Filter agents by tag…", "Navigate", "tags"),
216
+ _C("toggle_tui_mode", "Agents / jobs view", "Navigate", "jobs mode",
217
+ state=lambda app: _cycle(("agents", "jobs"), getattr(app, "tui_mode", "agents"))),
218
+ _C("cycle_focal_repo", "Cycle focal repo", "Navigate", "workspace directory", agent=True),
219
+
220
+ # View — panels and layout
221
+ _C("toggle_preview", "Preview pane", "View", "output terminal",
222
+ state=lambda app: _toggle(getattr(app, "preview_visible", None))),
223
+ _C("expand_preview", "Fullscreen preview", "View", "expand scrollback zoom"),
224
+ _C("toggle_timeline", "Timeline", "View", "tl history bar",
225
+ state=lambda app: _toggle(_widget_shown(app, "timeline"))),
226
+ _C("toggle_daemon", "Daemon panel", "View", "logs supervisor monitor",
227
+ state=lambda app: _toggle(_widget_shown(app, "daemon-panel"))),
228
+ _C("toggle_tui_log", "TUI diagnostic log", "View", "debug logs",
229
+ state=lambda app: _toggle(_widget_shown(app, "tui-log-panel"))),
230
+ _C("toggle_column_headers", "Column headers", "View", "header row",
231
+ state=lambda app: _toggle(getattr(getattr(app, "_prefs", None), "show_column_headers", None))),
232
+ _C("toggle_copy_mode", "Copy mode", "View", "mouse select clipboard text",
233
+ state=lambda app: _toggle(getattr(app, "_copy_mode", False))),
234
+ _C("toggle_tmux_sync", "Tmux pane sync", "View", "pane sync follow",
235
+ state=lambda app: _toggle(getattr(app, "tmux_sync", None))),
236
+ _C("resize_focused_window", "Resize agent to pane", "View", "fit size", agent=True),
237
+ _C("split_grow", "Grow monitor pane", "View", "split bigger taller resize"),
238
+ _C("split_shrink", "Shrink monitor pane", "View", "split smaller shorter resize"),
239
+ _C("toggle_help", "Help and status legend", "View", "keyboard shortcuts cheat sheet"),
240
+
241
+ # Display — what the agent list shows
242
+ _C("cycle_summary", "Summary detail", "Display", "level verbosity",
243
+ state=lambda app: _cycle(app.SUMMARY_LEVELS, getattr(app, "summary_level_index", None),
244
+ range(len(app.SUMMARY_LEVELS)))),
245
+ _C("cycle_summary_content", "Summary content", "Display", "ai orders annotation heartbeat",
246
+ state=lambda app: _cycle(("short", "context", "orders", "note", "heartbeat", "command"),
247
+ getattr(app, "summary_content_mode", None),
248
+ app.SUMMARY_CONTENT_MODES)),
249
+ _C("choose_sort", "Sort agents by…", "Display", "order column", state=_sort_state),
250
+ _C("reverse_sort", "Reverse sort", "Display", "order direction ascending descending flip",
251
+ state=_sort_direction_state),
252
+ _C("toggle_cost_display", "Cost units", "Display", "tokens dollars joules energy money",
253
+ state=_cost_state),
254
+ _C("cycle_timeline_hours", "Timeline scope", "Display", "hours range window",
255
+ state=lambda app: _cycle(tuple(f"{h}h" for h in app.TIMELINE_PRESETS),
256
+ getattr(getattr(app, "_prefs", None), "timeline_hours", None),
257
+ app.TIMELINE_PRESETS)),
258
+ _C("baseline_back", "Baseline 15 min earlier", "Display", "mean spin back", state=_baseline_state),
259
+ _C("baseline_forward", "Baseline 15 min later", "Display", "mean spin forward", state=_baseline_state),
260
+ _C("baseline_reset", "Reset baseline to now", "Display", "mean spin", state=_baseline_state),
261
+ _C("toggle_show_terminated", "Show killed agents", "Display", "terminated ghost",
262
+ state=lambda app: _toggle(getattr(app, "show_terminated", None))),
263
+ _C("toggle_hide_asleep", "Hide sleeping agents", "Display", "asleep",
264
+ state=lambda app: _toggle(getattr(app, "hide_asleep", None))),
265
+ _C("toggle_show_done", "Show done child agents", "Display", "finished children",
266
+ state=lambda app: _toggle(getattr(app, "show_done", None))),
267
+ _C("toggle_collapse_children", "Fold / unfold children", "Display", "collapse expand tree", agent=True),
268
+ _C("toggle_monochrome", "Monochrome", "Display", "black white colour color ansi",
269
+ state=lambda app: _toggle(getattr(app, "monochrome", None))),
270
+ _C("toggle_emoji_free", "Emoji-free", "Display", "ascii",
271
+ state=lambda app: _toggle(getattr(app, "emoji_free", None))),
272
+ _C("open_column_config", "Configure columns…", "Display", "fields"),
273
+
274
+ # Agent — acts on the focused agent
275
+ _C("new_agent", "New agent…", "Agent", "launch create start remote"),
276
+ _C("rename_focused", "Rename agent…", "Agent", "name", agent=True),
277
+ _C("fork_focused", "Fork agent", "Agent", "clone copy child", agent=True),
278
+ _C("restart_focused", "Restart agent", "Agent", "revive", agent=True),
279
+ _C("kill_focused", "Kill / clean up agent", "Agent", "stop delete remove", agent=True),
280
+ _C("sync_to_main_and_clear", "Sync to main and clear", "Agent", "git reset", agent=True),
281
+ _C("toggle_sleep", "Sleep", "Agent", "asleep wake pause", state=_agent_toggle("is_asleep"), agent=True),
282
+ _C("toggle_heartbeat_pause", "Heartbeat", "Agent", "hb pause resume", state=_heartbeat_state, agent=True),
283
+ _C("configure_heartbeat", "Heartbeat config…", "Agent", "interval instruction", agent=True),
284
+ _C("focus_standing_orders", "Standing orders…", "Agent", "instructions", agent=True),
285
+ _C("focus_human_annotation", "Annotation…", "Agent", "note comment", agent=True),
286
+ _C("edit_agent_value", "Agent value…", "Agent", "priority", agent=True),
287
+ _C("edit_cost_budget", "Cost budget…", "Agent", "money limit dollars", agent=True),
288
+ _C("toggle_enhanced_context", "Enhanced context", "Agent", "hook inject",
289
+ state=_agent_toggle("enhanced_context_enabled"), agent=True),
290
+ _C("toggle_hook_detection", "Status detection", "Agent", "hooks polling mode",
291
+ state=_detection_state, agent=True),
292
+ _C("open_instruction_history", "Instruction history…", "Agent", "sent previous", agent=True),
293
+
294
+ # Send to agent — keystrokes forwarded to the focused agent's pane
295
+ _C("focus_command_bar", "Send instruction…", "Send to agent", "message prompt type", agent=True),
296
+ _C("send_enter_to_focused", "Approve (send Enter)", "Send to agent", "yes accept confirm", agent=True),
297
+ _C("send_escape_to_focused", "Interrupt (send Esc)", "Send to agent", "cancel stop", agent=True),
298
+ _C("send_1_to_focused", "Send 1", "Send to agent", "option choice number", agent=True),
299
+ _C("send_2_to_focused", "Send 2", "Send to agent", "option choice number", agent=True),
300
+ _C("send_3_to_focused", "Send 3", "Send to agent", "option choice number", agent=True),
301
+ _C("send_4_to_focused", "Send 4", "Send to agent", "option choice number", agent=True),
302
+ _C("send_5_to_focused", "Send 5", "Send to agent", "option choice number", agent=True),
303
+ _C("send_ctrl_o_to_focused", "Send Ctrl+O", "Send to agent", "passthru expand", agent=True),
304
+
305
+ # Daemons and services
306
+ _C("toggle_summarizer", "AI summarizer", "Daemons", "openai summary", state=_summarizer_state),
307
+ _C("toggle_web_server", "Web dashboard", "Daemons", "server browser http", state=_web_state),
308
+ _C("cycle_notifications", "macOS notifications", "Daemons", "sound banner alert",
309
+ state=_notifications_state),
310
+ _C("supervisor_start", "Start supervisor daemon", "Daemons", "run"),
311
+ _C("supervisor_stop", "Stop supervisor daemon", "Daemons", "halt"),
312
+ _C("monitor_restart", "Restart monitor daemon", "Daemons", "reload"),
313
+
314
+ # Settings dialogs
315
+ _C("open_new_agent_defaults", "New agent defaults…", "Settings", "launch config"),
316
+ _C("open_sister_selection", "Sister visibility…", "Settings", "remote hosts machines"),
317
+ _C("open_tmux_config", "Tmux toggle key…", "Settings", "split pane focus"),
318
+ _C("open_passthru_config", "Passthru keys…", "Settings", "forward ctrl"),
319
+
320
+ _C("quit", "Quit", "App", "exit close detach"),
321
+ )
322
+
323
+ # Commands the palette handles itself by switching list rather than closing.
324
+ MODE_SWITCHES = {"jump_to_agent": "agents", "filter_by_tag": "tags", "choose_sort": "sort"}
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Sort choices (#487)
329
+ # ---------------------------------------------------------------------------
330
+
331
+ @dataclass(frozen=True)
332
+ class SortChoice:
333
+ """One row of the sort picker (S): a column, or tree order.
334
+
335
+ `descending` is the direction choosing it would sort — the current
336
+ direction for the active row, since choosing it again reverses.
337
+ """
338
+ mode: str
339
+ name: str
340
+ header: str = ""
341
+ description: str = ""
342
+ active: bool = False
343
+ descending: bool = False
344
+
345
+
346
+ TREE_CHOICE_DESCRIPTION = "Parents with their children indented beneath (X folds)"
347
+
348
+
349
+ def sort_choices(sort_mode: str, sort_reversed: bool) -> List[SortChoice]:
350
+ """Every sortable column in list order, then tree order."""
351
+ from .summary_columns import SUMMARY_COLUMNS
352
+ from .tui_logic import sort_descending, sort_mode_for_column
353
+
354
+ choices: List[SortChoice] = []
355
+ for col in SUMMARY_COLUMNS:
356
+ if col.sort_key is None or col.cli_only:
357
+ continue
358
+ mode = sort_mode_for_column(col.id)
359
+ active = mode == sort_mode
360
+ choices.append(SortChoice(
361
+ mode=mode, name=col.name or col.id, header=col.header,
362
+ description=col.description, active=active,
363
+ descending=sort_descending(mode, sort_reversed) if active else col.sort_desc,
364
+ ))
365
+ choices.append(SortChoice(
366
+ mode="by_tree", name="Tree", description=TREE_CHOICE_DESCRIPTION,
367
+ active=sort_mode == "by_tree",
368
+ ))
369
+ return choices
370
+
371
+
372
+ def filter_sort_choices(choices: Sequence[SortChoice], query: str) -> List[Tuple[SortChoice, Tuple[int, ...]]]:
373
+ """Choices matching `query`, best first, with name positions to light.
374
+
375
+ Each word must match the name (fuzzily, as commands do) or appear in
376
+ the header code or description; an exact header ("cpu", "tok") ranks
377
+ first.
378
+ """
379
+ query = query.strip()
380
+ if not query:
381
+ return [(c, ()) for c in choices]
382
+ words = query.split()
383
+ scored = []
384
+ for i, c in enumerate(choices):
385
+ total, positions = 0, set()
386
+ for w in words:
387
+ m = fuzzy_match(w, c.name)
388
+ if c.header and w.lower() == c.header.lower():
389
+ total += 2000
390
+ elif m is not None:
391
+ total += m[0]
392
+ positions.update(m[1])
393
+ elif w.lower() in f"{c.header} {c.description}".lower():
394
+ total += 300
395
+ else:
396
+ total = None
397
+ break
398
+ if total is not None:
399
+ scored.append((-total, i, c, tuple(sorted(positions))))
400
+ scored.sort(key=lambda t: (t[0], t[1]))
401
+ return [(c, pos) for _, _, c, pos in scored]
402
+
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # Key labels
406
+ # ---------------------------------------------------------------------------
407
+
408
+ _KEY_LABELS = {
409
+ "left_square_bracket": "[", "right_square_bracket": "]", "backslash": "\\",
410
+ "question_mark": "?", "colon": ":", "dollar_sign": "$", "comma": ",",
411
+ "full_stop": ".", "less_than_sign": "<", "greater_than_sign": ">",
412
+ "equals_sign": "=", "minus": "-", "slash": "/",
413
+ "down": "↓", "up": "↑", "left": "←", "right": "→",
414
+ "enter": "Enter", "escape": "Esc", "tab": "Tab", "space": "Space",
415
+ }
416
+
417
+
418
+ def key_label(key: str) -> str:
419
+ """Display form of a Textual key name: `ctrl+p` → `^P`, `comma` → `,`."""
420
+ if key.startswith("ctrl+"):
421
+ return "^" + key_label(key[len("ctrl+"):]).upper()
422
+ return _KEY_LABELS.get(key, key)
423
+
424
+
425
+ def keys_by_action(bindings: Iterable[Any]) -> Dict[str, List[str]]:
426
+ """Map each action to its key labels, in binding order (`j` before `↓`)."""
427
+ out: Dict[str, List[str]] = {}
428
+ for b in bindings:
429
+ key, action = (b[0], b[1]) if isinstance(b, tuple) else (b.key, b.action)
430
+ for k in key.split(","):
431
+ out.setdefault(action, []).append(key_label(k.strip()))
432
+ return out
433
+
434
+
435
+ # ---------------------------------------------------------------------------
436
+ # Matching
437
+ # ---------------------------------------------------------------------------
438
+
439
+ def fuzzy_match(query: str, text: str) -> Optional[Tuple[int, Tuple[int, ...]]]:
440
+ """Score `query` against `text`; None when it does not match.
441
+
442
+ Two ways to match, VSCode-style:
443
+ - a contiguous substring ("mary" in Summary), scored higher at a word
444
+ start and higher still at the very start;
445
+ - word prefixes: each character either starts a word or directly
446
+ follows the previous matched character, so "sd" and "sumdet" find
447
+ Summary detail but "sum" does not find "Stop supervisor daemon".
448
+ Returns (score, positions) — positions index `text` for highlighting.
449
+ """
450
+ if not query:
451
+ return (0, ())
452
+ q, t = query.lower(), text.lower()
453
+
454
+ def word_start(i: int) -> bool:
455
+ return i == 0 or (not t[i - 1].isalnum() and t[i].isalnum()) or (
456
+ text[i].isupper() and text[i - 1].islower())
457
+
458
+ best = None
459
+ start = t.find(q)
460
+ while start != -1:
461
+ score = 1000 - start + (400 if word_start(start) else 0) + (200 if start == 0 else 0)
462
+ if best is None or score > best[0]:
463
+ best = (score, tuple(range(start, start + len(q))))
464
+ start = t.find(q, start + 1)
465
+ if best is not None:
466
+ return best
467
+
468
+ # Word-prefix match: depth-first, trying the earliest candidate first
469
+ # (titles are short and queries shorter, so this stays tiny).
470
+ def search(qi: int, prev: int) -> Optional[List[int]]:
471
+ if qi == len(q):
472
+ return []
473
+ j = t.find(q[qi], prev + 1)
474
+ while j != -1:
475
+ if j == prev + 1 or word_start(j):
476
+ rest = search(qi + 1, j)
477
+ if rest is not None:
478
+ return [j] + rest
479
+ j = t.find(q[qi], j + 1)
480
+ return None
481
+
482
+ found = search(0, -1)
483
+ if found is None or not word_start(found[0]):
484
+ return None
485
+ words = sum(1 for p in found if word_start(p))
486
+ return (600 - 20 * words - found[0], tuple(found))
487
+
488
+
489
+ @dataclass(frozen=True)
490
+ class Match:
491
+ command: PaletteCommand
492
+ positions: Tuple[int, ...] = () # into command.title
493
+ by_key: bool = False # query named this command's key
494
+
495
+
496
+ def rank_commands(
497
+ commands: Sequence[PaletteCommand],
498
+ query: str,
499
+ keymap: Dict[str, List[str]],
500
+ ) -> List[Match]:
501
+ """Commands matching `query`, best first.
502
+
503
+ Space-separated words must all match; each is tried against the title
504
+ (highlighted) and then the category and keywords (not highlighted).
505
+ A query that is exactly one of a command's keys, case-sensitively,
506
+ puts that command first — so "S" answers "what does S do?" and "$"
507
+ finds the cost cycle, which no title contains.
508
+ """
509
+ query = query.strip()
510
+ if not query:
511
+ return [Match(c) for c in commands]
512
+ order = {c.action: i for i, c in enumerate(commands)}
513
+ scored: List[Tuple[int, int, Match]] = []
514
+ words = query.split()
515
+ # "^n" names the same key as "^N"; every other key is case-sensitive.
516
+ key_query = "^" + query[1:].upper() if query.startswith("^") and len(query) > 1 else query
517
+ for cmd in commands:
518
+ if key_query in keymap.get(cmd.action, ()):
519
+ scored.append((10_000, order[cmd.action], Match(cmd, (), by_key=True)))
520
+ continue
521
+ total: Optional[int] = 0
522
+ positions: set = set()
523
+ extra = f"{cmd.category} {cmd.keywords}".lower()
524
+ for w in words:
525
+ m = fuzzy_match(w, cmd.title)
526
+ if m is not None:
527
+ total += m[0]
528
+ positions.update(m[1])
529
+ elif w.lower() in extra: # keywords match as substrings only
530
+ total += 300
531
+ else:
532
+ total = None
533
+ break
534
+ if total is None:
535
+ continue
536
+ scored.append((total, order[cmd.action], Match(cmd, tuple(sorted(positions)))))
537
+ scored.sort(key=lambda s: (-s[0], s[1]))
538
+ return [m for _, _, m in scored]
@@ -22,7 +22,7 @@ import os
22
22
  DAEMON_VERSION = 2 # Increment when daemon behavior changes
23
23
  from dataclasses import dataclass, field
24
24
  from pathlib import Path
25
- from typing import Optional, Set
25
+ from typing import List, Optional, Set
26
26
 
27
27
  import yaml
28
28
 
@@ -811,7 +811,8 @@ class TUIPreferences:
811
811
  show_terminated: bool = False # keep killed sessions visible in timeline
812
812
  hide_asleep: bool = False # hide sleeping agents from display
813
813
  show_done: bool = False # show "done" child agents (#244)
814
- sort_mode: str = "alphabetical" # alphabetical, by_status, by_value (#61)
814
+ sort_mode: str = "alphabetical" # alphabetical, by_status, by_value, by_tree (#61), col:<id> (#487)
815
+ sort_reversed: bool = False # flip the sort column's natural direction (#487)
815
816
  summary_content_mode: str = "ai_short" # ai_short, ai_long, orders, annotation, heartbeat (#98, #171)
816
817
  baseline_minutes: int = 60 # 0=now (instantaneous), 15/30/.../180 = minutes back for mean spin
817
818
  monochrome: bool = False # B&W mode for terminals with ANSI issues (#138)
@@ -831,6 +832,8 @@ class TUIPreferences:
831
832
  # Parents whose children are folded in tree sort mode (X key, #244),
832
833
  # persisted so a fold survives a TUI restart (#464)
833
834
  collapsed_parents: Set[str] = field(default_factory=set)
835
+ # Actions last run from the command palette, newest first (#482)
836
+ recent_commands: List[str] = field(default_factory=list)
834
837
  # Log every status change to diagnostics CSV (off by default)
835
838
  status_change_logging: bool = False
836
839