@anastops/cli 1.0.0 → 1.2.0

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 (74) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +8 -6
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/init.d.ts +17 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +315 -17
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/ranger.d.ts +7 -2
  9. package/dist/commands/ranger.d.ts.map +1 -1
  10. package/dist/commands/ranger.js +99 -21
  11. package/dist/commands/ranger.js.map +1 -1
  12. package/dist/commands/uninstall.d.ts +16 -0
  13. package/dist/commands/uninstall.d.ts.map +1 -0
  14. package/dist/commands/uninstall.js +206 -0
  15. package/dist/commands/uninstall.js.map +1 -0
  16. package/dist/index.js +11 -0
  17. package/dist/index.js.map +1 -1
  18. package/package.json +8 -6
  19. package/ranger-tui/pyproject.toml +51 -0
  20. package/ranger-tui/ranger_tui/__init__.py +5 -0
  21. package/ranger-tui/ranger_tui/__main__.py +16 -0
  22. package/ranger-tui/ranger_tui/__pycache__/__init__.cpython-314.pyc +0 -0
  23. package/ranger-tui/ranger_tui/__pycache__/__main__.cpython-314.pyc +0 -0
  24. package/ranger-tui/ranger_tui/__pycache__/accessibility.cpython-314.pyc +0 -0
  25. package/ranger-tui/ranger_tui/__pycache__/app.cpython-314.pyc +0 -0
  26. package/ranger-tui/ranger_tui/__pycache__/config.cpython-314.pyc +0 -0
  27. package/ranger-tui/ranger_tui/__pycache__/theme.cpython-314.pyc +0 -0
  28. package/ranger-tui/ranger_tui/accessibility.py +499 -0
  29. package/ranger-tui/ranger_tui/actions/__init__.py +13 -0
  30. package/ranger-tui/ranger_tui/actions/agent_actions.py +74 -0
  31. package/ranger-tui/ranger_tui/actions/session_actions.py +110 -0
  32. package/ranger-tui/ranger_tui/actions/task_actions.py +107 -0
  33. package/ranger-tui/ranger_tui/app.py +93 -0
  34. package/ranger-tui/ranger_tui/assets/ranger_head.png +0 -0
  35. package/ranger-tui/ranger_tui/config.py +100 -0
  36. package/ranger-tui/ranger_tui/data/__init__.py +16 -0
  37. package/ranger-tui/ranger_tui/data/__pycache__/__init__.cpython-314.pyc +0 -0
  38. package/ranger-tui/ranger_tui/data/__pycache__/client.cpython-314.pyc +0 -0
  39. package/ranger-tui/ranger_tui/data/__pycache__/models.cpython-314.pyc +0 -0
  40. package/ranger-tui/ranger_tui/data/client.py +858 -0
  41. package/ranger-tui/ranger_tui/data/models.py +151 -0
  42. package/ranger-tui/ranger_tui/screens/__init__.py +16 -0
  43. package/ranger-tui/ranger_tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  44. package/ranger-tui/ranger_tui/screens/__pycache__/dashboard.cpython-314.pyc +0 -0
  45. package/ranger-tui/ranger_tui/screens/__pycache__/modals.cpython-314.pyc +0 -0
  46. package/ranger-tui/ranger_tui/screens/__pycache__/session.cpython-314.pyc +0 -0
  47. package/ranger-tui/ranger_tui/screens/__pycache__/task.cpython-314.pyc +0 -0
  48. package/ranger-tui/ranger_tui/screens/command_palette.py +357 -0
  49. package/ranger-tui/ranger_tui/screens/dashboard.py +232 -0
  50. package/ranger-tui/ranger_tui/screens/help.py +103 -0
  51. package/ranger-tui/ranger_tui/screens/modals.py +95 -0
  52. package/ranger-tui/ranger_tui/screens/session.py +289 -0
  53. package/ranger-tui/ranger_tui/screens/task.py +187 -0
  54. package/ranger-tui/ranger_tui/styles/ranger.tcss +254 -0
  55. package/ranger-tui/ranger_tui/theme.py +93 -0
  56. package/ranger-tui/ranger_tui/widgets/__init__.py +23 -0
  57. package/ranger-tui/ranger_tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
  58. package/ranger-tui/ranger_tui/widgets/__pycache__/accessible.cpython-314.pyc +0 -0
  59. package/ranger-tui/ranger_tui/widgets/__pycache__/logo.cpython-314.pyc +0 -0
  60. package/ranger-tui/ranger_tui/widgets/__pycache__/logo_assets.cpython-314.pyc +0 -0
  61. package/ranger-tui/ranger_tui/widgets/__pycache__/ranger_image.cpython-314.pyc +0 -0
  62. package/ranger-tui/ranger_tui/widgets/__pycache__/sidebar.cpython-314.pyc +0 -0
  63. package/ranger-tui/ranger_tui/widgets/__pycache__/topbar.cpython-314.pyc +0 -0
  64. package/ranger-tui/ranger_tui/widgets/accessible.py +176 -0
  65. package/ranger-tui/ranger_tui/widgets/agents_table.py +151 -0
  66. package/ranger-tui/ranger_tui/widgets/header.py +141 -0
  67. package/ranger-tui/ranger_tui/widgets/logo.py +258 -0
  68. package/ranger-tui/ranger_tui/widgets/logo_assets.py +62 -0
  69. package/ranger-tui/ranger_tui/widgets/metrics_panel.py +121 -0
  70. package/ranger-tui/ranger_tui/widgets/ranger_image.py +91 -0
  71. package/ranger-tui/ranger_tui/widgets/sessions_table.py +191 -0
  72. package/ranger-tui/ranger_tui/widgets/sidebar.py +91 -0
  73. package/ranger-tui/ranger_tui/widgets/tasks_table.py +189 -0
  74. package/ranger-tui/ranger_tui/widgets/topbar.py +168 -0
@@ -0,0 +1,232 @@
1
+ """
2
+ Dashboard screen - Main view with sessions list.
3
+ """
4
+
5
+ from datetime import datetime, UTC
6
+ from textual.app import ComposeResult
7
+ from textual.screen import Screen
8
+ from textual.binding import Binding
9
+ from textual.containers import Horizontal
10
+ from textual.widgets import DataTable, Footer
11
+
12
+ from ranger_tui.widgets import TopBar, Sidebar
13
+ from ranger_tui.theme import format_status
14
+
15
+
16
+ class DashboardScreen(Screen):
17
+ """Main dashboard screen showing all sessions."""
18
+
19
+ BINDINGS = [
20
+ Binding("enter", "open_session", "Open", show=True),
21
+ Binding("n", "new_session", "New", show=True),
22
+ Binding("escape", "noop", show=False), # Disable escape on dashboard
23
+ ]
24
+
25
+ def compose(self) -> ComposeResult:
26
+ """Compose the dashboard layout."""
27
+ yield TopBar()
28
+ with Horizontal(id="main"):
29
+ yield DataTable(id="sessions-table", cursor_type="row")
30
+ yield Sidebar(title="Summary")
31
+ yield Footer()
32
+
33
+ async def on_mount(self) -> None:
34
+ """Called when screen is mounted."""
35
+ # Setup table after a brief delay to ensure layout is complete
36
+ self.call_later(self._setup_table)
37
+
38
+ # Auto-refresh
39
+ self.set_interval(2.0, self._refresh)
40
+
41
+ # Focus on sessions table after setup
42
+ self.call_later(self._focus_sessions_table)
43
+
44
+ def _focus_sessions_table(self) -> None:
45
+ """Focus on the sessions table."""
46
+ try:
47
+ table = self.query_one("#sessions-table", DataTable)
48
+ table.focus()
49
+ if table.row_count > 0:
50
+ table.cursor_coordinate = (0, 0)
51
+ except Exception:
52
+ pass
53
+
54
+ async def _setup_table(self) -> None:
55
+ """Setup table with proper column widths after layout is complete."""
56
+ table = self.query_one("#sessions-table", DataTable)
57
+
58
+ # Skip if columns already added
59
+ if table.columns:
60
+ return
61
+
62
+ table.zebra_stripes = True
63
+
64
+ # Get actual table content width (excluding borders)
65
+ try:
66
+ # Use the table's content region width
67
+ table_width = table.content_region.width
68
+ if table_width < 40:
69
+ # Fallback: calculate from app size
70
+ table_width = self.app.size.width - 34 # Sidebar(28) + padding(6)
71
+ except Exception:
72
+ table_width = 100 # Fallback for wide terminal
73
+
74
+ # Ensure minimum reasonable width
75
+ table_width = max(table_width, 50)
76
+
77
+ # Subtract space for column separators (4 separators * ~1 char each)
78
+ usable_width = table_width - 4
79
+
80
+ # Proportional distribution to fill the table width:
81
+ # Status: 15%, Tasks: 10%, Tokens: 12%, Age: 8%, Objective: 55%
82
+ status_w = int(usable_width * 0.15)
83
+ tasks_w = int(usable_width * 0.10)
84
+ tokens_w = int(usable_width * 0.12)
85
+ age_w = int(usable_width * 0.08)
86
+ objective_w = usable_width - status_w - tasks_w - tokens_w - age_w
87
+
88
+ # Ensure reasonable minimums without exceeding total
89
+ status_w = max(8, status_w)
90
+ tasks_w = max(5, tasks_w)
91
+ tokens_w = max(6, tokens_w)
92
+ age_w = max(4, age_w)
93
+ objective_w = max(15, objective_w)
94
+
95
+ table.add_column("Status", key="status", width=status_w)
96
+ table.add_column("Objective", key="objective", width=objective_w)
97
+ table.add_column("Tasks", key="tasks", width=tasks_w)
98
+ table.add_column("Tokens", key="tokens", width=tokens_w)
99
+ table.add_column("Age", key="age", width=age_w)
100
+
101
+ # Initial load
102
+ await self._refresh()
103
+
104
+ async def _refresh(self) -> None:
105
+ """Refresh the display."""
106
+ self._update_connection_status()
107
+ await self._update_sessions()
108
+ self._update_summary()
109
+
110
+ def _update_connection_status(self) -> None:
111
+ """Update connection status in topbar."""
112
+ topbar = self.query_one(TopBar)
113
+ topbar.set_connection_status(
114
+ self.app.mongo_connected,
115
+ self.app.redis_connected
116
+ )
117
+
118
+ async def _update_sessions(self) -> None:
119
+ """Update sessions table."""
120
+ sessions = self.app.sessions
121
+ table = self.query_one("#sessions-table", DataTable)
122
+
123
+ # Remember selection
124
+ selected_row = table.cursor_row
125
+
126
+ # Clear and repopulate
127
+ table.clear()
128
+
129
+ for session in sessions:
130
+ status = format_status(session.status)
131
+ objective = session.objective
132
+ tasks = self._format_tasks(session.metadata)
133
+ tokens = f"{session.metadata.total_tokens:,}"
134
+ age = self._format_age(session.created_at)
135
+
136
+ table.add_row(status, objective, tasks, tokens, age, key=session.id)
137
+
138
+ # Restore selection
139
+ if selected_row is not None and selected_row < table.row_count:
140
+ table.cursor_coordinate = (selected_row, 0)
141
+
142
+ def _update_summary(self) -> None:
143
+ """Update summary sidebar."""
144
+ sessions = self.app.sessions
145
+
146
+ total = len(sessions)
147
+ active = sum(1 for s in sessions if s.status == "active")
148
+ total_tasks = sum(s.metadata.tasks_total for s in sessions)
149
+ completed_tasks = sum(s.metadata.tasks_completed for s in sessions)
150
+ running_tasks = sum(s.metadata.tasks_running for s in sessions)
151
+ total_tokens = sum(s.metadata.total_tokens for s in sessions)
152
+ total_cost = sum(s.metadata.total_cost for s in sessions)
153
+
154
+ sidebar = self.query_one(Sidebar)
155
+ running_indicator = f" ([cyan]●[/] {running_tasks} running)" if running_tasks > 0 else ""
156
+ sidebar.update_content(
157
+ f"Sessions: {total} ({active} active)\n"
158
+ f"Tasks: {completed_tasks}/{total_tasks}{running_indicator}\n"
159
+ f"Tokens: {total_tokens:,}\n"
160
+ f"Cost: ${total_cost:.4f}"
161
+ )
162
+
163
+ def _format_tasks(self, metadata) -> str:
164
+ """Format tasks as completed/total with running indicator."""
165
+ total = metadata.tasks_total
166
+ completed = metadata.tasks_completed
167
+ running = metadata.tasks_running
168
+
169
+ if running > 0:
170
+ # Animated spinner chars for running tasks
171
+ spinner_frames = ["◐", "◓", "◑", "◒"]
172
+ # Use current time to animate
173
+ frame_idx = int(datetime.now(UTC).timestamp() * 2) % len(spinner_frames)
174
+ spinner = spinner_frames[frame_idx]
175
+ return f"[cyan]{spinner}[/] {completed}/{total}"
176
+ elif total == 0:
177
+ return "[dim]0/0[/]"
178
+ elif completed == total:
179
+ return f"[green]✓[/] {completed}/{total}"
180
+ else:
181
+ return f"{completed}/{total}"
182
+
183
+ def _format_age(self, created_at: datetime) -> str:
184
+ """Format age in human-readable form."""
185
+ # Handle naive datetimes from MongoDB (assume UTC)
186
+ if created_at.tzinfo is None:
187
+ created_at = created_at.replace(tzinfo=UTC)
188
+ age = datetime.now(UTC) - created_at
189
+ seconds = age.total_seconds()
190
+
191
+ if seconds < 60:
192
+ return f"{int(seconds)}s"
193
+ elif seconds < 3600:
194
+ return f"{int(seconds / 60)}m"
195
+ elif seconds < 86400:
196
+ return f"{int(seconds / 3600)}h"
197
+ else:
198
+ return f"{int(seconds / 86400)}d"
199
+
200
+ async def action_open_session(self) -> None:
201
+ """Open selected session."""
202
+ table = self.query_one("#sessions-table", DataTable)
203
+ if table.cursor_row is not None and table.row_count > 0:
204
+ row_key = table.coordinate_to_cell_key((table.cursor_row, 0)).row_key
205
+ session_id = str(row_key.value) if row_key else None
206
+
207
+ if session_id:
208
+ from ranger_tui.screens.session import SessionScreen
209
+ await self.app.push_screen(SessionScreen(session_id))
210
+
211
+ async def action_new_session(self) -> None:
212
+ """Create a new session."""
213
+ from ranger_tui.screens.modals import NewSessionModal
214
+
215
+ async def on_submit(objective: str) -> None:
216
+ if objective:
217
+ session = await self.app.client.create_session(objective)
218
+ self.app.notify(f"Created: {session.id[:8]}")
219
+ await self.app._refresh_data()
220
+
221
+ await self.app.push_screen(NewSessionModal(), on_submit)
222
+
223
+ def action_noop(self) -> None:
224
+ """Do nothing - escape disabled on dashboard."""
225
+ pass
226
+
227
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
228
+ """Handle double-click / enter on row."""
229
+ session_id = str(event.row_key.value) if event.row_key else None
230
+ if session_id:
231
+ from ranger_tui.screens.session import SessionScreen
232
+ self.app.push_screen(SessionScreen(session_id))
@@ -0,0 +1,103 @@
1
+ """
2
+ Help screen - Shows keyboard shortcuts and usage information.
3
+ """
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import ModalScreen
7
+ from textual.binding import Binding
8
+ from textual.containers import Container, VerticalScroll
9
+ from textual.widgets import Static, Button, Markdown
10
+
11
+
12
+ HELP_CONTENT = """
13
+ # Ranger TUI Help
14
+
15
+ ## Global Shortcuts
16
+
17
+ | Key | Action |
18
+ |-----|--------|
19
+ | `q` | Quit application |
20
+ | `?` | Show this help |
21
+ | `r` | Refresh data |
22
+ | `d` | Toggle dark/light mode |
23
+ | `Ctrl+P` | Open command palette |
24
+ | `Escape` | Go back / Close modal |
25
+
26
+ ## Dashboard (Sessions List)
27
+
28
+ | Key | Action |
29
+ |-----|--------|
30
+ | `n` | Create new session |
31
+ | `Enter` | Open selected session |
32
+ | `a` | Archive selected session |
33
+ | `p` | Purge selected session |
34
+ | `f` | Fork selected session |
35
+ | `j` / `↓` | Move down |
36
+ | `k` / `↑` | Move up |
37
+
38
+ ## Session Detail
39
+
40
+ | Key | Action |
41
+ |-----|--------|
42
+ | `t` | Create new task |
43
+ | `Enter` | Open selected task |
44
+ | `e` | Execute/queue task |
45
+ | `c` | Cancel task |
46
+ | `y` | Retry failed task |
47
+ | `Tab` | Switch tabs |
48
+
49
+ ## Task Detail
50
+
51
+ | Key | Action |
52
+ |-----|--------|
53
+ | `e` | Execute task |
54
+ | `c` | Cancel task |
55
+ | `y` | Retry task |
56
+ | `Tab` | Switch tabs |
57
+
58
+ ## Navigation
59
+
60
+ - Use `j`/`k` or arrow keys to move through lists
61
+ - Press `Enter` to select/open items
62
+ - Press `Escape` to go back
63
+ - Use `Tab` to switch between tabs
64
+
65
+ ## Command Palette
66
+
67
+ Press `Ctrl+P` to open the command palette for quick access to all commands:
68
+
69
+ - Session operations (spawn, archive, purge, fork)
70
+ - Task operations (create, execute, cancel, retry)
71
+ - Agent operations (create, deploy, retire)
72
+ - System operations (health check, metrics)
73
+
74
+ ---
75
+
76
+ Press `Escape` or `Enter` to close this help.
77
+ """
78
+
79
+
80
+ class HelpScreen(ModalScreen):
81
+ """Help modal screen."""
82
+
83
+ BINDINGS = [
84
+ Binding("escape", "close", "Close", show=True),
85
+ Binding("enter", "close", "Close", show=False),
86
+ ]
87
+
88
+ def compose(self) -> ComposeResult:
89
+ """Compose the help modal."""
90
+ with Container(id="help-modal"):
91
+ yield Static("Help", id="help-title")
92
+ with VerticalScroll(id="help-content"):
93
+ yield Markdown(HELP_CONTENT)
94
+ yield Button("Close", id="help-close", variant="primary")
95
+
96
+ def action_close(self) -> None:
97
+ """Close the help modal."""
98
+ self.dismiss()
99
+
100
+ def on_button_pressed(self, event: Button.Pressed) -> None:
101
+ """Handle button press."""
102
+ if event.button.id == "help-close":
103
+ self.dismiss()
@@ -0,0 +1,95 @@
1
+ """
2
+ Modal screens for user input.
3
+ """
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import ModalScreen
7
+ from textual.binding import Binding
8
+ from textual.containers import Container, Vertical, Horizontal
9
+ from textual.widgets import Static, Button, Input, Label, Select
10
+
11
+
12
+ class NewSessionModal(ModalScreen[str]):
13
+ """Modal for creating a new session."""
14
+
15
+ BINDINGS = [
16
+ Binding("escape", "cancel", "Cancel"),
17
+ ]
18
+
19
+ def compose(self) -> ComposeResult:
20
+ """Compose the modal layout."""
21
+ with Container(id="modal"):
22
+ yield Static("New Session", id="modal-title")
23
+ yield Input(placeholder="What do you want to accomplish?", id="objective")
24
+ with Horizontal(id="modal-buttons"):
25
+ yield Button("Create", variant="primary", id="btn-create")
26
+ yield Button("Cancel", id="btn-cancel")
27
+
28
+ def on_mount(self) -> None:
29
+ self.query_one("#objective", Input).focus()
30
+
31
+ def on_button_pressed(self, event: Button.Pressed) -> None:
32
+ if event.button.id == "btn-create":
33
+ objective = self.query_one("#objective", Input).value
34
+ if objective.strip():
35
+ self.dismiss(objective.strip())
36
+ else:
37
+ self.dismiss(None)
38
+
39
+ def on_input_submitted(self, event: Input.Submitted) -> None:
40
+ objective = event.value
41
+ if objective.strip():
42
+ self.dismiss(objective.strip())
43
+
44
+ def action_cancel(self) -> None:
45
+ self.dismiss(None)
46
+
47
+
48
+ class NewTaskModal(ModalScreen[dict]):
49
+ """Modal for creating a new task."""
50
+
51
+ BINDINGS = [
52
+ Binding("escape", "cancel", "Cancel"),
53
+ ]
54
+
55
+ def compose(self) -> ComposeResult:
56
+ """Compose the modal layout."""
57
+ with Container(id="modal"):
58
+ yield Static("New Task", id="modal-title")
59
+ yield Label("Description")
60
+ yield Input(placeholder="Brief description", id="description")
61
+ yield Label("Prompt")
62
+ yield Input(placeholder="Detailed instructions", id="prompt")
63
+ yield Label("Type")
64
+ yield Select([
65
+ ("Code", "code"),
66
+ ("Test", "test"),
67
+ ("Docs", "documentation"),
68
+ ("Analysis", "analysis"),
69
+ ("Other", "other"),
70
+ ], id="type-select", value="code")
71
+ with Horizontal(id="modal-buttons"):
72
+ yield Button("Create", variant="primary", id="btn-create")
73
+ yield Button("Cancel", id="btn-cancel")
74
+
75
+ def on_mount(self) -> None:
76
+ self.query_one("#description", Input).focus()
77
+
78
+ def on_button_pressed(self, event: Button.Pressed) -> None:
79
+ if event.button.id == "btn-create":
80
+ desc = self.query_one("#description", Input).value
81
+ prompt = self.query_one("#prompt", Input).value
82
+ task_type = self.query_one("#type-select", Select).value
83
+
84
+ if desc.strip() and prompt.strip():
85
+ self.dismiss({
86
+ "description": desc.strip(),
87
+ "prompt": prompt.strip(),
88
+ "type": task_type,
89
+ "provider": "claude",
90
+ })
91
+ else:
92
+ self.dismiss(None)
93
+
94
+ def action_cancel(self) -> None:
95
+ self.dismiss(None)