soothe-cli 0.1.0__py3-none-any.whl
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.
- soothe_cli/__init__.py +5 -0
- soothe_cli/cli/__init__.py +1 -0
- soothe_cli/cli/commands/__init__.py +1 -0
- soothe_cli/cli/commands/autopilot_cmd.py +410 -0
- soothe_cli/cli/commands/config_cmd.py +277 -0
- soothe_cli/cli/commands/run_cmd.py +87 -0
- soothe_cli/cli/commands/status_cmd.py +121 -0
- soothe_cli/cli/commands/subagent_names.py +17 -0
- soothe_cli/cli/commands/thread_cmd.py +657 -0
- soothe_cli/cli/execution/__init__.py +6 -0
- soothe_cli/cli/execution/daemon.py +194 -0
- soothe_cli/cli/execution/headless.py +99 -0
- soothe_cli/cli/execution/launcher.py +31 -0
- soothe_cli/cli/main.py +509 -0
- soothe_cli/cli/renderer.py +444 -0
- soothe_cli/cli/stream/__init__.py +17 -0
- soothe_cli/cli/stream/context.py +138 -0
- soothe_cli/cli/stream/display_line.py +83 -0
- soothe_cli/cli/stream/formatter.py +412 -0
- soothe_cli/cli/stream/pipeline.py +521 -0
- soothe_cli/cli/utils.py +46 -0
- soothe_cli/config/__init__.py +5 -0
- soothe_cli/config/cli_config.py +155 -0
- soothe_cli/plan/__init__.py +5 -0
- soothe_cli/plan/rich_tree.py +54 -0
- soothe_cli/shared/__init__.py +107 -0
- soothe_cli/shared/command_router.py +246 -0
- soothe_cli/shared/config_loader.py +68 -0
- soothe_cli/shared/display_policy.py +413 -0
- soothe_cli/shared/essential_events.py +68 -0
- soothe_cli/shared/event_processor.py +823 -0
- soothe_cli/shared/message_processing.py +393 -0
- soothe_cli/shared/presentation_engine.py +173 -0
- soothe_cli/shared/processor_state.py +80 -0
- soothe_cli/shared/renderer_protocol.py +158 -0
- soothe_cli/shared/rendering.py +43 -0
- soothe_cli/shared/slash_commands.py +354 -0
- soothe_cli/shared/subagent_routing.py +63 -0
- soothe_cli/shared/suppression_state.py +188 -0
- soothe_cli/shared/tool_formatters/__init__.py +27 -0
- soothe_cli/shared/tool_formatters/base.py +109 -0
- soothe_cli/shared/tool_formatters/execution.py +297 -0
- soothe_cli/shared/tool_formatters/fallback.py +128 -0
- soothe_cli/shared/tool_formatters/file_ops.py +299 -0
- soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
- soothe_cli/shared/tool_formatters/media.py +291 -0
- soothe_cli/shared/tool_formatters/structured.py +202 -0
- soothe_cli/shared/tool_formatters/web.py +143 -0
- soothe_cli/shared/tool_output_formatter.py +227 -0
- soothe_cli/shared/tui_trace_log.py +40 -0
- soothe_cli/tui/__init__.py +5 -0
- soothe_cli/tui/_ask_user_types.py +50 -0
- soothe_cli/tui/_cli_context.py +27 -0
- soothe_cli/tui/_env_vars.py +56 -0
- soothe_cli/tui/_session_stats.py +114 -0
- soothe_cli/tui/_version.py +21 -0
- soothe_cli/tui/app.py +4992 -0
- soothe_cli/tui/app.tcss +302 -0
- soothe_cli/tui/command_registry.py +310 -0
- soothe_cli/tui/config.py +2381 -0
- soothe_cli/tui/daemon_session.py +233 -0
- soothe_cli/tui/file_ops.py +409 -0
- soothe_cli/tui/formatting.py +28 -0
- soothe_cli/tui/hooks.py +23 -0
- soothe_cli/tui/input.py +782 -0
- soothe_cli/tui/media_utils.py +471 -0
- soothe_cli/tui/model_config.py +518 -0
- soothe_cli/tui/output.py +69 -0
- soothe_cli/tui/project_utils.py +188 -0
- soothe_cli/tui/sessions.py +1248 -0
- soothe_cli/tui/skills/__init__.py +5 -0
- soothe_cli/tui/skills/invocation.py +74 -0
- soothe_cli/tui/skills/load.py +93 -0
- soothe_cli/tui/textual_adapter.py +1430 -0
- soothe_cli/tui/theme.py +838 -0
- soothe_cli/tui/tool_display.py +297 -0
- soothe_cli/tui/unicode_security.py +502 -0
- soothe_cli/tui/update_check.py +447 -0
- soothe_cli/tui/widgets/__init__.py +9 -0
- soothe_cli/tui/widgets/_links.py +63 -0
- soothe_cli/tui/widgets/approval.py +430 -0
- soothe_cli/tui/widgets/ask_user.py +392 -0
- soothe_cli/tui/widgets/autocomplete.py +666 -0
- soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
- soothe_cli/tui/widgets/autopilot_screen.py +64 -0
- soothe_cli/tui/widgets/chat_input.py +1834 -0
- soothe_cli/tui/widgets/clipboard.py +128 -0
- soothe_cli/tui/widgets/diff.py +240 -0
- soothe_cli/tui/widgets/editor.py +140 -0
- soothe_cli/tui/widgets/history.py +221 -0
- soothe_cli/tui/widgets/loading.py +194 -0
- soothe_cli/tui/widgets/mcp_viewer.py +352 -0
- soothe_cli/tui/widgets/message_store.py +693 -0
- soothe_cli/tui/widgets/messages.py +1720 -0
- soothe_cli/tui/widgets/model_selector.py +988 -0
- soothe_cli/tui/widgets/notification_settings.py +155 -0
- soothe_cli/tui/widgets/status.py +403 -0
- soothe_cli/tui/widgets/theme_selector.py +158 -0
- soothe_cli/tui/widgets/thread_selector.py +1865 -0
- soothe_cli/tui/widgets/tool_renderers.py +148 -0
- soothe_cli/tui/widgets/tool_widgets.py +254 -0
- soothe_cli/tui/widgets/tools.py +165 -0
- soothe_cli/tui/widgets/welcome.py +330 -0
- soothe_cli-0.1.0.dist-info/METADATA +100 -0
- soothe_cli-0.1.0.dist-info/RECORD +107 -0
- soothe_cli-0.1.0.dist-info/WHEEL +4 -0
- soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Abstract callback interface for CLI/TUI event rendering.
|
|
2
|
+
|
|
3
|
+
This module defines the RendererProtocol that CLI and TUI renderers implement.
|
|
4
|
+
The EventProcessor calls these callbacks; implementations handle mode-specific display.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from soothe_sdk import Plan
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RendererProtocol(Protocol):
|
|
16
|
+
"""Abstract callback interface for CLI/TUI event rendering.
|
|
17
|
+
|
|
18
|
+
Implementations handle mode-specific display while EventProcessor
|
|
19
|
+
handles unified event routing and state management.
|
|
20
|
+
|
|
21
|
+
Core callbacks are required for basic functionality.
|
|
22
|
+
Optional fine-grained hooks can be implemented for specific event handling.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# === Core Callbacks (Required) ===
|
|
26
|
+
|
|
27
|
+
def on_assistant_text(
|
|
28
|
+
self,
|
|
29
|
+
text: str,
|
|
30
|
+
*,
|
|
31
|
+
is_main: bool,
|
|
32
|
+
is_streaming: bool,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Assistant text chunk or complete message.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
text: Text content to display.
|
|
38
|
+
is_main: True if from main agent, False if from subagent.
|
|
39
|
+
is_streaming: True if partial chunk, False if complete.
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def on_tool_call(
|
|
44
|
+
self,
|
|
45
|
+
name: str,
|
|
46
|
+
args: dict[str, Any],
|
|
47
|
+
tool_call_id: str,
|
|
48
|
+
*,
|
|
49
|
+
is_main: bool,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Tool invocation started.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: Tool name (snake_case internal name).
|
|
55
|
+
args: Parsed argument dictionary.
|
|
56
|
+
tool_call_id: Unique identifier for correlation with result.
|
|
57
|
+
is_main: True if from main agent.
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def on_tool_result(
|
|
62
|
+
self,
|
|
63
|
+
name: str,
|
|
64
|
+
result: str,
|
|
65
|
+
tool_call_id: str,
|
|
66
|
+
*,
|
|
67
|
+
is_error: bool,
|
|
68
|
+
is_main: bool,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Tool returned a result.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
name: Tool name.
|
|
74
|
+
result: Result content (may be truncated).
|
|
75
|
+
tool_call_id: Correlates with on_tool_call.
|
|
76
|
+
is_error: True if result indicates failure.
|
|
77
|
+
is_main: True if from main agent.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
def on_status_change(self, state: str) -> None:
|
|
82
|
+
"""Daemon state changed.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
state: One of "idle", "running", "stopped".
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def on_error(self, error: str, *, context: str | None = None) -> None:
|
|
90
|
+
"""Error occurred.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
error: Error message.
|
|
94
|
+
context: Optional context (e.g., "tool_execution", "daemon").
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
def on_progress_event(
|
|
99
|
+
self,
|
|
100
|
+
event_type: str,
|
|
101
|
+
data: dict[str, Any],
|
|
102
|
+
*,
|
|
103
|
+
namespace: tuple[str, ...],
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Protocol/subagent progress event.
|
|
106
|
+
|
|
107
|
+
Catch-all for events not covered by specific callbacks.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
event_type: Full event type string (e.g., "soothe.capability.browser.step.running").
|
|
111
|
+
data: Event payload.
|
|
112
|
+
namespace: Subagent namespace tuple (empty for main agent).
|
|
113
|
+
"""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
# === Optional Fine-Grained Hooks ===
|
|
117
|
+
# These have default no-op implementations in base renderers
|
|
118
|
+
|
|
119
|
+
def on_plan_created(self, plan: Plan) -> None:
|
|
120
|
+
"""Plan was created.
|
|
121
|
+
|
|
122
|
+
Default implementation may delegate to on_progress_event.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
plan: The created plan object.
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
def on_plan_step_started(self, step_id: str, description: str) -> None:
|
|
130
|
+
"""Plan step began execution.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
step_id: Unique step identifier.
|
|
134
|
+
description: Step description.
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def on_plan_step_completed(
|
|
139
|
+
self,
|
|
140
|
+
step_id: str,
|
|
141
|
+
success: bool, # noqa: FBT001
|
|
142
|
+
duration_ms: int,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Plan step finished.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
step_id: Unique step identifier.
|
|
148
|
+
success: True if step succeeded.
|
|
149
|
+
duration_ms: Execution duration in milliseconds.
|
|
150
|
+
"""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
def on_turn_end(self) -> None:
|
|
154
|
+
"""Current turn completed.
|
|
155
|
+
|
|
156
|
+
Use for finalizing streaming buffers and cleanup.
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared TUI utilities for state management and rendering.
|
|
2
|
+
|
|
3
|
+
This module contains reusable display helpers used by both TUI and headless modes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from soothe_sdk import _TASK_NAME_RE
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"update_name_map_from_tool_calls",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _display_subagent_name(name: str) -> str:
|
|
16
|
+
"""Return friendly display name for a subagent id."""
|
|
17
|
+
from soothe_cli.shared.subagent_routing import SUBAGENT_DISPLAY_NAMES
|
|
18
|
+
|
|
19
|
+
return SUBAGENT_DISPLAY_NAMES.get(name.lower(), name.replace("_", " ").title())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def update_name_map_from_tool_calls(message_obj: object, name_map: dict[str, str]) -> None:
|
|
23
|
+
"""Update tool-call-id -> display name mapping from AIMessage/tool calls.
|
|
24
|
+
|
|
25
|
+
This is the shared implementation used by both TUI and headless modes.
|
|
26
|
+
"""
|
|
27
|
+
tool_calls = getattr(message_obj, "tool_calls", None) or []
|
|
28
|
+
for tc in tool_calls:
|
|
29
|
+
if not isinstance(tc, dict):
|
|
30
|
+
continue
|
|
31
|
+
if tc.get("name") != "task":
|
|
32
|
+
continue
|
|
33
|
+
call_id = str(tc.get("id", ""))
|
|
34
|
+
args = tc.get("args", {})
|
|
35
|
+
raw_name = ""
|
|
36
|
+
if isinstance(args, dict):
|
|
37
|
+
raw_name = str(args.get("agent", "") or args.get("name", ""))
|
|
38
|
+
elif args:
|
|
39
|
+
match = _TASK_NAME_RE.search(str(args))
|
|
40
|
+
if match:
|
|
41
|
+
raw_name = match.group(1)
|
|
42
|
+
if call_id and raw_name:
|
|
43
|
+
name_map[call_id] = _display_subagent_name(raw_name)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Slash command handlers for CLI and TUI (RFC-404).
|
|
2
|
+
|
|
3
|
+
Unified command registry with metadata-based routing:
|
|
4
|
+
- CLI-only commands: handled locally
|
|
5
|
+
- Daemon RPC commands: structured data rendering
|
|
6
|
+
- Daemon routing commands: behavior indicators
|
|
7
|
+
|
|
8
|
+
This module provides the COMMANDS registry and rendering functions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Rendering Functions (must be defined before COMMANDS registry)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def show_commands(console: Console) -> None:
|
|
28
|
+
"""Show available slash commands (CLI-only)."""
|
|
29
|
+
table = Table(title="Available Commands", show_lines=False)
|
|
30
|
+
table.add_column("Command", style="bold cyan")
|
|
31
|
+
table.add_column("Description")
|
|
32
|
+
|
|
33
|
+
# Import COMMANDS here to avoid circular reference at module load
|
|
34
|
+
from soothe_cli.shared.slash_commands import COMMANDS
|
|
35
|
+
|
|
36
|
+
for cmd, entry in COMMANDS.items():
|
|
37
|
+
table.add_row(cmd, entry.get("description", ""))
|
|
38
|
+
|
|
39
|
+
console.print(table)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def show_keymaps(console: Console) -> None:
|
|
43
|
+
"""Show keyboard shortcuts (CLI-only)."""
|
|
44
|
+
table = Table(title="Keyboard Shortcuts", show_lines=False)
|
|
45
|
+
table.add_column("Shortcut", style="bold cyan")
|
|
46
|
+
table.add_column("Action")
|
|
47
|
+
|
|
48
|
+
for k, v in KEYBOARD_SHORTCUTS.items():
|
|
49
|
+
table.add_row(k, v)
|
|
50
|
+
|
|
51
|
+
console.print(table)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def show_memory(console: Console, data: dict[str, Any]) -> None:
|
|
55
|
+
"""Render memory stats from daemon RPC response."""
|
|
56
|
+
stats = data.get("memory_stats", {})
|
|
57
|
+
console.print(
|
|
58
|
+
Panel(
|
|
59
|
+
json.dumps(stats, indent=2, default=str),
|
|
60
|
+
title="Memory Stats",
|
|
61
|
+
border_style="cyan",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def show_policy(console: Console, data: dict[str, Any]) -> None:
|
|
67
|
+
"""Render policy profile from daemon RPC response."""
|
|
68
|
+
policy = data.get("policy", {})
|
|
69
|
+
console.print(f"[dim]Policy profile: {policy.get('profile', 'unknown')}[/dim]")
|
|
70
|
+
console.print(f"[dim]Planner routing: {policy.get('planner_routing', 'unknown')}[/dim]")
|
|
71
|
+
console.print(f"[dim]Memory backend: {policy.get('memory_backend', 'unknown')}[/dim]")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def show_history(console: Console, data: dict[str, Any]) -> None:
|
|
75
|
+
"""Render input history from daemon RPC response."""
|
|
76
|
+
history = data.get("history", [])
|
|
77
|
+
if not history:
|
|
78
|
+
console.print("[dim]No recent history.[/dim]")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
table = Table(title="Recent Input History", show_lines=False)
|
|
82
|
+
table.add_column("Time", style="dim")
|
|
83
|
+
table.add_column("Input", style="cyan")
|
|
84
|
+
|
|
85
|
+
for item in history[:10]: # Show last 10
|
|
86
|
+
timestamp = item.get("timestamp", "")
|
|
87
|
+
text = item.get("text", "")
|
|
88
|
+
if len(text) > 50:
|
|
89
|
+
text = text[:47] + "..."
|
|
90
|
+
table.add_row(timestamp, text)
|
|
91
|
+
|
|
92
|
+
console.print(table)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def show_config(console: Console, data: dict[str, Any]) -> None:
|
|
96
|
+
"""Render configuration summary from daemon RPC response."""
|
|
97
|
+
config = data.get("config", {})
|
|
98
|
+
console.print(
|
|
99
|
+
Panel(
|
|
100
|
+
json.dumps(config, indent=2, default=str),
|
|
101
|
+
title="Configuration Summary",
|
|
102
|
+
border_style="cyan",
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def show_review(console: Console, data: dict[str, Any]) -> None:
|
|
108
|
+
"""Render conversation/action history from daemon RPC response."""
|
|
109
|
+
history = data.get("review", [])
|
|
110
|
+
if not history:
|
|
111
|
+
console.print("[dim]No conversation history.[/dim]")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
table = Table(title="Conversation Review", show_lines=False)
|
|
115
|
+
table.add_column("Time", style="dim")
|
|
116
|
+
table.add_column("Type", style="cyan")
|
|
117
|
+
table.add_column("Content", style="white")
|
|
118
|
+
|
|
119
|
+
for item in history[:20]:
|
|
120
|
+
timestamp = item.get("timestamp", "")
|
|
121
|
+
item_type = item.get("type", "unknown")
|
|
122
|
+
content = item.get("content", "")
|
|
123
|
+
if len(content) > 60:
|
|
124
|
+
content = content[:57] + "..."
|
|
125
|
+
table.add_row(timestamp, item_type, content)
|
|
126
|
+
|
|
127
|
+
console.print(table)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def show_autopilot_dashboard(console: Console, data: dict[str, Any]) -> None:
|
|
131
|
+
"""Render autopilot dashboard from daemon RPC response."""
|
|
132
|
+
dashboard = data.get("autopilot_dashboard", {})
|
|
133
|
+
|
|
134
|
+
table = Table(title="Autopilot Dashboard", show_lines=False)
|
|
135
|
+
table.add_column("Metric", style="bold cyan")
|
|
136
|
+
table.add_column("Value", style="white")
|
|
137
|
+
|
|
138
|
+
# Display key metrics
|
|
139
|
+
table.add_row("Status", dashboard.get("status", "idle"))
|
|
140
|
+
table.add_row("Iterations", str(dashboard.get("iterations", 0)))
|
|
141
|
+
table.add_row("Goals Completed", str(dashboard.get("goals_completed", 0)))
|
|
142
|
+
table.add_row("Goals Active", str(dashboard.get("goals_active", 0)))
|
|
143
|
+
|
|
144
|
+
console.print(table)
|
|
145
|
+
|
|
146
|
+
# Display active goals if present
|
|
147
|
+
active_goals = dashboard.get("active_goals", [])
|
|
148
|
+
if active_goals:
|
|
149
|
+
console.print("\n[bold cyan]Active Goals:[/bold cyan]")
|
|
150
|
+
for goal in active_goals:
|
|
151
|
+
console.print(f" • {goal.get('description', 'unknown')}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Keyboard Shortcuts
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
KEYBOARD_SHORTCUTS: dict[str, str] = {
|
|
159
|
+
"Ctrl+Q": "Quit TUI: Stop thread (confirm) and exit client",
|
|
160
|
+
"Ctrl+D": "Detach TUI: Leave thread running (confirm) and exit client",
|
|
161
|
+
"Ctrl+C": "Cancel running job, press twice within 1s to quit",
|
|
162
|
+
"Ctrl+E": "Focus chat input",
|
|
163
|
+
"Ctrl+Y": "Copy last message to clipboard",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Unified Command Registry (RFC-404)
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
COMMANDS: dict[str, dict[str, Any]] = {
|
|
172
|
+
# CLI-only commands (2)
|
|
173
|
+
"/help": {
|
|
174
|
+
"location": "cli",
|
|
175
|
+
"handler": show_commands,
|
|
176
|
+
"description": "Show available commands",
|
|
177
|
+
},
|
|
178
|
+
"/keymaps": {
|
|
179
|
+
"location": "cli",
|
|
180
|
+
"handler": show_keymaps,
|
|
181
|
+
"description": "Show keyboard shortcuts",
|
|
182
|
+
},
|
|
183
|
+
# Daemon RPC commands (12)
|
|
184
|
+
"/clear": {
|
|
185
|
+
"location": "daemon",
|
|
186
|
+
"type": "rpc",
|
|
187
|
+
"daemon_command": "clear",
|
|
188
|
+
"description": "Clear thread history",
|
|
189
|
+
"requires_thread": True,
|
|
190
|
+
},
|
|
191
|
+
"/exit": {
|
|
192
|
+
"location": "daemon",
|
|
193
|
+
"type": "rpc",
|
|
194
|
+
"daemon_command": "exit",
|
|
195
|
+
"description": "Stop thread and exit client",
|
|
196
|
+
},
|
|
197
|
+
"/quit": {
|
|
198
|
+
"location": "daemon",
|
|
199
|
+
"type": "rpc",
|
|
200
|
+
"daemon_command": "quit",
|
|
201
|
+
"description": "Stop thread and exit client",
|
|
202
|
+
},
|
|
203
|
+
"/detach": {
|
|
204
|
+
"location": "daemon",
|
|
205
|
+
"type": "rpc",
|
|
206
|
+
"daemon_command": "detach",
|
|
207
|
+
"description": "Leave thread running and exit client",
|
|
208
|
+
},
|
|
209
|
+
"/cancel": {
|
|
210
|
+
"location": "daemon",
|
|
211
|
+
"type": "rpc",
|
|
212
|
+
"daemon_command": "cancel",
|
|
213
|
+
"description": "Cancel the current running job",
|
|
214
|
+
"requires_thread": True,
|
|
215
|
+
},
|
|
216
|
+
"/memory": {
|
|
217
|
+
"location": "daemon",
|
|
218
|
+
"type": "rpc",
|
|
219
|
+
"daemon_command": "memory",
|
|
220
|
+
"description": "Show memory stats",
|
|
221
|
+
"requires_thread": True,
|
|
222
|
+
"handler": show_memory,
|
|
223
|
+
},
|
|
224
|
+
"/policy": {
|
|
225
|
+
"location": "daemon",
|
|
226
|
+
"type": "rpc",
|
|
227
|
+
"daemon_command": "policy",
|
|
228
|
+
"description": "Show active policy profile",
|
|
229
|
+
"handler": show_policy,
|
|
230
|
+
},
|
|
231
|
+
"/history": {
|
|
232
|
+
"location": "daemon",
|
|
233
|
+
"type": "rpc",
|
|
234
|
+
"daemon_command": "history",
|
|
235
|
+
"description": "Show recent prompt history",
|
|
236
|
+
"requires_thread": True,
|
|
237
|
+
"handler": show_history,
|
|
238
|
+
},
|
|
239
|
+
"/config": {
|
|
240
|
+
"location": "daemon",
|
|
241
|
+
"type": "rpc",
|
|
242
|
+
"daemon_command": "config",
|
|
243
|
+
"description": "Show active configuration summary",
|
|
244
|
+
"handler": show_config,
|
|
245
|
+
},
|
|
246
|
+
"/review": {
|
|
247
|
+
"location": "daemon",
|
|
248
|
+
"type": "rpc",
|
|
249
|
+
"daemon_command": "review",
|
|
250
|
+
"description": "Review recent conversation and action history",
|
|
251
|
+
"requires_thread": True,
|
|
252
|
+
"handler": show_review,
|
|
253
|
+
},
|
|
254
|
+
"/thread": {
|
|
255
|
+
"location": "daemon",
|
|
256
|
+
"type": "rpc",
|
|
257
|
+
"daemon_command": "thread",
|
|
258
|
+
"description": "Thread operations (archive <id>)",
|
|
259
|
+
"params_schema": {
|
|
260
|
+
"action": {"type": "string", "required": True},
|
|
261
|
+
"id": {"type": "string", "required": False},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
"/resume": {
|
|
265
|
+
"location": "daemon",
|
|
266
|
+
"type": "rpc",
|
|
267
|
+
"daemon_command": "resume",
|
|
268
|
+
"description": "Resume a recent thread",
|
|
269
|
+
"params_schema": {"thread_id": {"type": "string", "required": True}},
|
|
270
|
+
},
|
|
271
|
+
"/autopilot": {
|
|
272
|
+
"location": "daemon",
|
|
273
|
+
"type": "rpc",
|
|
274
|
+
"daemon_command": "autopilot_dashboard",
|
|
275
|
+
"description": "Show autopilot dashboard",
|
|
276
|
+
"requires_thread": True,
|
|
277
|
+
"handler": show_autopilot_dashboard,
|
|
278
|
+
},
|
|
279
|
+
# Daemon routing commands (5)
|
|
280
|
+
"/plan": {"location": "daemon", "type": "routing", "description": "Trigger plan mode"},
|
|
281
|
+
"/browser": {
|
|
282
|
+
"location": "daemon",
|
|
283
|
+
"type": "routing",
|
|
284
|
+
"description": "Route query to Browser subagent",
|
|
285
|
+
"requires_query": True,
|
|
286
|
+
},
|
|
287
|
+
"/claude": {
|
|
288
|
+
"location": "daemon",
|
|
289
|
+
"type": "routing",
|
|
290
|
+
"description": "Route query to Claude subagent",
|
|
291
|
+
"requires_query": True,
|
|
292
|
+
},
|
|
293
|
+
"/research": {
|
|
294
|
+
"location": "daemon",
|
|
295
|
+
"type": "routing",
|
|
296
|
+
"description": "Route query to Research subagent",
|
|
297
|
+
"requires_query": True,
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# Legacy compatibility (used by tests/old code)
|
|
303
|
+
SLASH_COMMANDS: dict[str, str] = {
|
|
304
|
+
cmd: entry.get("description", "") for cmd, entry in COMMANDS.items()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Legacy helper (used by tests/old code)
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def parse_autonomous_command(cmd: str) -> tuple[int | None, str] | None:
|
|
314
|
+
"""Parse `/autopilot` command payload (legacy helper)."""
|
|
315
|
+
stripped = cmd.strip()
|
|
316
|
+
if not stripped.startswith("/autopilot"):
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
parts = stripped.split(maxsplit=2)
|
|
320
|
+
if len(parts) == 1:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
if len(parts) == 2:
|
|
324
|
+
single = parts[1].strip()
|
|
325
|
+
if not single or single.isdigit():
|
|
326
|
+
return None
|
|
327
|
+
return (None, single)
|
|
328
|
+
|
|
329
|
+
maybe_num = parts[1].strip()
|
|
330
|
+
if maybe_num.isdigit():
|
|
331
|
+
prompt = parts[2].strip()
|
|
332
|
+
if not prompt:
|
|
333
|
+
return None
|
|
334
|
+
max_iterations = int(maybe_num)
|
|
335
|
+
return (max_iterations if max_iterations > 0 else None, prompt)
|
|
336
|
+
|
|
337
|
+
prompt = f"{parts[1]} {parts[2]}".strip()
|
|
338
|
+
return (None, prompt) if prompt else None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
__all__ = [
|
|
342
|
+
"COMMANDS",
|
|
343
|
+
"SLASH_COMMANDS",
|
|
344
|
+
"KEYBOARD_SHORTCUTS",
|
|
345
|
+
"parse_autonomous_command",
|
|
346
|
+
"show_commands",
|
|
347
|
+
"show_keymaps",
|
|
348
|
+
"show_memory",
|
|
349
|
+
"show_policy",
|
|
350
|
+
"show_history",
|
|
351
|
+
"show_config",
|
|
352
|
+
"show_review",
|
|
353
|
+
"show_autopilot_dashboard",
|
|
354
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Subagent display names and input routing (shared by CLI and TUI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
SUBAGENT_DISPLAY_NAMES: dict[str, str] = {
|
|
6
|
+
"browser": "Browser",
|
|
7
|
+
"claude": "Claude",
|
|
8
|
+
"research": "Research",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
BUILTIN_SUBAGENT_NAMES: list[str] = list(SUBAGENT_DISPLAY_NAMES.keys())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_subagent_display_name(technical_name: str) -> str:
|
|
15
|
+
"""Get display name for a subagent.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
technical_name: Internal subagent name.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
PascalCase display name.
|
|
22
|
+
"""
|
|
23
|
+
return SUBAGENT_DISPLAY_NAMES.get(
|
|
24
|
+
technical_name,
|
|
25
|
+
technical_name.replace("_", " ").title().replace(" ", ""),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_subagent_from_input(user_input: str) -> tuple[str | None, str]:
|
|
30
|
+
"""Parse subagent subcommand from user input.
|
|
31
|
+
|
|
32
|
+
Detects subagent subcommands (e.g., /browser, /claude) anywhere in the text
|
|
33
|
+
and extracts the subagent name along with the cleaned input text.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
user_input: Raw user input string.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of ``(subagent_name, cleaned_text)``.
|
|
40
|
+
``subagent_name`` is ``None`` if no valid subcommand found.
|
|
41
|
+
The subcommand is removed from ``cleaned_text``.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
``"/browser check this"`` -> ``("browser", "check this")``
|
|
45
|
+
``"Can you /claude analyze this"`` -> ``("claude", "Can you analyze this")``
|
|
46
|
+
``"hello world"`` -> ``(None, "hello world")``
|
|
47
|
+
"""
|
|
48
|
+
first_match: tuple[int, str] | None = None
|
|
49
|
+
|
|
50
|
+
for subagent_name in BUILTIN_SUBAGENT_NAMES:
|
|
51
|
+
subcommand = f"/{subagent_name}"
|
|
52
|
+
idx = user_input.lower().find(subcommand)
|
|
53
|
+
if idx != -1 and (first_match is None or idx < first_match[0]):
|
|
54
|
+
first_match = (idx, subagent_name)
|
|
55
|
+
|
|
56
|
+
if first_match:
|
|
57
|
+
idx, subagent_name = first_match
|
|
58
|
+
subcommand = f"/{subagent_name}"
|
|
59
|
+
cleaned = user_input[:idx] + user_input[idx + len(subcommand) :]
|
|
60
|
+
cleaned = " ".join(cleaned.split())
|
|
61
|
+
return (subagent_name, cleaned)
|
|
62
|
+
|
|
63
|
+
return (None, user_input)
|