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.
- {overcode-0.5.5/src/overcode.egg-info → overcode-0.5.6}/PKG-INFO +1 -1
- {overcode-0.5.5 → overcode-0.5.6}/pyproject.toml +1 -1
- overcode-0.5.6/src/overcode/command_palette.py +538 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/settings.py +5 -2
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summary_columns.py +222 -5
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui.py +211 -85
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui.tcss +9 -10
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/view.py +31 -7
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_logic.py +94 -35
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/__init__.py +5 -2
- overcode-0.5.6/src/overcode/tui_widgets/column_header.py +99 -0
- overcode-0.5.6/src/overcode/tui_widgets/command_palette.py +609 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/help_overlay.py +2 -1
- overcode-0.5.6/src/overcode/tui_widgets/jump_modal.py +40 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/summary_config_modal.py +23 -1
- {overcode-0.5.5 → overcode-0.5.6/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/SOURCES.txt +3 -0
- overcode-0.5.5/src/overcode/tui_widgets/jump_modal.py +0 -163
- {overcode-0.5.5 → overcode-0.5.6}/LICENSE +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/MANIFEST.in +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/README.md +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/setup.cfg +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/base.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/claude_code.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/codex.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/codex_stats.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/grok.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/grok_stats.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/hermes.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/hermes_stats.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2_patterns.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2_plugin_install.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode2_stats.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/backends/opencode_stats.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/bundled_skills.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/claude_config.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/claude_pid.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/agent.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/budget.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/config.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/doctor.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/focal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/jobs.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/models.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/parallelism.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/perms.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/sister.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/skills.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/split.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/tags.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/cli/wrappers.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/config.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/data/model_metadata.json +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/data_export.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/dependency_check.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/doctor.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/duration.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/entrypoint.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/exceptions.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/follow_mode.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hermes_plugin/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hermes_plugin/plugin.yaml +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/history_reader.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hook_handler.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/implementations.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/interfaces.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/job_launcher.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/job_manager.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/launcher.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/logging_config.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/mocks.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/model_metadata.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/monitor_daemon.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/monitor_daemon_state.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/notifier.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry-v2/index.js +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry-v2/overcode-telemetry-core.mjs +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry-v2/tui.js +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/opencode_plugin/overcode-telemetry.js +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/pane_capture_gate.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/pid_utils.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/presence_logger.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/pricing.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/process_resources.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/protocols.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/sandbox_detect.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/session_manager.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/sister_controller.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/sister_poller.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/ssh_provisioner.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/stat_gate.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/stats_reader.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_constants.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_detector.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_history.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/status_patterns.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/summary_groups.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/supervisor_layout.sh +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/time_context.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tmux_manager.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tmux_utils.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/transcript_index.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_actions/session.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_render.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/command_bar.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/job_summary.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/modal_base.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/new_agent_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/passthru_config_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/rename_agent_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/session_summary.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/tmux_config_modal.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/tui_widgets/tui_log_panel.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web/__init__.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_api.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_control_api.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_server.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/web_templates.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/worker_guard.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode/wrapper.py +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.5.5 → overcode-0.5.6}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -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
|
|