gobby 0.2.6__py3-none-any.whl → 0.2.8__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.
Files changed (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
gobby/tui/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- """Gobby TUI - Textual-based dashboard for Gobby daemon."""
2
-
3
- from gobby.tui.app import GobbyApp, run_tui
4
-
5
- __all__ = ["GobbyApp", "run_tui"]
gobby/tui/api_client.py DELETED
@@ -1,278 +0,0 @@
1
- """HTTP client for Gobby daemon API."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- import httpx
8
-
9
-
10
- class GobbyAPIClient:
11
- """HTTP client for communicating with Gobby daemon."""
12
-
13
- def __init__(self, base_url: str = "http://localhost:60887") -> None:
14
- self.base_url = base_url.rstrip("/")
15
- self._client: httpx.AsyncClient | None = None
16
-
17
- async def __aenter__(self) -> GobbyAPIClient:
18
- self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
19
- return self
20
-
21
- async def __aexit__(self, *args: Any) -> None:
22
- if self._client:
23
- await self._client.aclose()
24
- self._client = None
25
-
26
- @property
27
- def client(self) -> httpx.AsyncClient:
28
- if self._client is None:
29
- raise RuntimeError("Client not initialized. Use 'async with' context manager.")
30
- return self._client
31
-
32
- # ==================== Admin Endpoints ====================
33
-
34
- async def get_status(self) -> dict[str, Any]:
35
- """Get comprehensive daemon status."""
36
- response = await self.client.get("/admin/status")
37
- response.raise_for_status()
38
- result: dict[str, Any] = response.json()
39
- return result
40
-
41
- async def get_config(self) -> dict[str, Any]:
42
- """Get daemon configuration."""
43
- response = await self.client.get("/admin/config")
44
- response.raise_for_status()
45
- result: dict[str, Any] = response.json()
46
- return result
47
-
48
- async def get_metrics(self) -> str:
49
- """Get Prometheus metrics."""
50
- response = await self.client.get("/admin/metrics")
51
- response.raise_for_status()
52
- return response.text
53
-
54
- # ==================== Session Endpoints ====================
55
-
56
- async def list_sessions(
57
- self,
58
- status: str | None = None,
59
- project_id: str | None = None,
60
- limit: int = 50,
61
- ) -> list[dict[str, Any]]:
62
- """List sessions with optional filtering."""
63
- params: dict[str, Any] = {"limit": limit}
64
- if status:
65
- params["status"] = status
66
- if project_id:
67
- params["project_id"] = project_id
68
- response = await self.client.get("/sessions", params=params)
69
- response.raise_for_status()
70
- result: list[dict[str, Any]] = response.json()
71
- return result
72
-
73
- async def get_session(self, session_id: str) -> dict[str, Any]:
74
- """Get session details."""
75
- response = await self.client.get(f"/sessions/{session_id}")
76
- response.raise_for_status()
77
- result: dict[str, Any] = response.json()
78
- return result
79
-
80
- # ==================== MCP Tool Endpoints ====================
81
-
82
- async def list_mcp_servers(self) -> dict[str, Any]:
83
- """List available MCP servers."""
84
- response = await self.client.get("/mcp/servers")
85
- response.raise_for_status()
86
- result: dict[str, Any] = response.json()
87
- return result
88
-
89
- async def list_tools(self, server_name: str) -> dict[str, Any]:
90
- """List tools from an MCP server."""
91
- response = await self.client.get(f"/mcp/{server_name}/tools")
92
- response.raise_for_status()
93
- result: dict[str, Any] = response.json()
94
- return result
95
-
96
- async def call_tool(
97
- self,
98
- server_name: str,
99
- tool_name: str,
100
- arguments: dict[str, Any] | None = None,
101
- ) -> dict[str, Any]:
102
- """Execute a tool on an MCP server."""
103
- response = await self.client.post(
104
- f"/mcp/{server_name}/tools/{tool_name}",
105
- json=arguments or {},
106
- )
107
- response.raise_for_status()
108
- result: dict[str, Any] = response.json()
109
- return result
110
-
111
- # ==================== Task Helpers (via MCP) ====================
112
-
113
- async def list_tasks(
114
- self,
115
- status: str | None = None,
116
- task_type: str | None = None,
117
- parent_id: str | None = None,
118
- ) -> list[dict[str, Any]]:
119
- """List tasks with optional filtering."""
120
- args: dict[str, Any] = {}
121
- if status:
122
- args["status"] = status
123
- if task_type:
124
- args["task_type"] = task_type
125
- if parent_id:
126
- args["parent_id"] = parent_id
127
- response = await self.call_tool("gobby-tasks", "list_tasks", args)
128
- result = response.get("result", {})
129
- tasks: list[dict[str, Any]] = result.get("tasks", [])
130
- return tasks
131
-
132
- async def get_task(self, task_id: str) -> dict[str, Any]:
133
- """Get task details."""
134
- response = await self.call_tool("gobby-tasks", "get_task", {"task_id": task_id})
135
- result = response.get("result", {})
136
- task: dict[str, Any] = result.get("task", {})
137
- return task
138
-
139
- async def create_task(
140
- self,
141
- title: str,
142
- task_type: str = "task",
143
- description: str | None = None,
144
- priority: int = 3,
145
- session_id: str | None = None,
146
- ) -> dict[str, Any]:
147
- """Create a new task."""
148
- args: dict[str, Any] = {
149
- "title": title,
150
- "task_type": task_type,
151
- "priority": priority,
152
- }
153
- if description:
154
- args["description"] = description
155
- if session_id:
156
- args["session_id"] = session_id
157
- return await self.call_tool("gobby-tasks", "create_task", args)
158
-
159
- async def update_task(
160
- self,
161
- task_id: str,
162
- status: str | None = None,
163
- title: str | None = None,
164
- priority: int | None = None,
165
- ) -> dict[str, Any]:
166
- """Update task properties."""
167
- args: dict[str, Any] = {"task_id": task_id}
168
- if status:
169
- args["status"] = status
170
- if title:
171
- args["title"] = title
172
- if priority is not None:
173
- args["priority"] = priority
174
- return await self.call_tool("gobby-tasks", "update_task", args)
175
-
176
- async def close_task(
177
- self,
178
- task_id: str,
179
- commit_sha: str | None = None,
180
- reason: str | None = None,
181
- ) -> dict[str, Any]:
182
- """Close a task."""
183
- args: dict[str, Any] = {"task_id": task_id}
184
- if commit_sha:
185
- args["commit_sha"] = commit_sha
186
- if reason:
187
- args["reason"] = reason
188
- return await self.call_tool("gobby-tasks", "close_task", args)
189
-
190
- async def suggest_next_task(self) -> dict[str, Any]:
191
- """Get the recommended next task."""
192
- return await self.call_tool("gobby-tasks", "suggest_next_task", {})
193
-
194
- # ==================== Memory Helpers (via MCP) ====================
195
-
196
- async def recall(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
197
- """Search memories."""
198
- response = await self.call_tool(
199
- "gobby-memory",
200
- "recall",
201
- {"query": query, "limit": limit},
202
- )
203
- result = response.get("result", {})
204
- memories: list[dict[str, Any]] = result.get("memories", [])
205
- return memories
206
-
207
- async def remember(self, content: str, importance: float = 0.5) -> dict[str, Any]:
208
- """Store a memory."""
209
- return await self.call_tool(
210
- "gobby-memory",
211
- "remember",
212
- {"content": content, "importance": importance},
213
- )
214
-
215
- # ==================== Agent Helpers (via MCP) ====================
216
-
217
- async def list_agents(self) -> list[dict[str, Any]]:
218
- """List running agents."""
219
- response = await self.call_tool("gobby-agents", "list_agents", {})
220
- result = response.get("result", {})
221
- agents: list[dict[str, Any]] = result.get("agents", [])
222
- return agents
223
-
224
- async def start_agent(
225
- self,
226
- prompt: str,
227
- mode: str = "terminal",
228
- workflow: str | None = None,
229
- parent_session_id: str | None = None,
230
- ) -> dict[str, Any]:
231
- """Spawn a new agent."""
232
- args: dict[str, Any] = {"prompt": prompt, "mode": mode}
233
- if workflow:
234
- args["workflow"] = workflow
235
- if parent_session_id:
236
- args["parent_session_id"] = parent_session_id
237
- return await self.call_tool("gobby-agents", "start_agent", args)
238
-
239
- async def cancel_agent(self, run_id: str) -> dict[str, Any]:
240
- """Cancel a running agent."""
241
- return await self.call_tool("gobby-agents", "cancel_agent", {"run_id": run_id})
242
-
243
- # ==================== Worktree Helpers (via MCP) ====================
244
-
245
- async def list_worktrees(self) -> list[dict[str, Any]]:
246
- """List git worktrees."""
247
- response = await self.call_tool("gobby-worktrees", "list_worktrees", {})
248
- result = response.get("result", {})
249
- worktrees: list[dict[str, Any]] = result.get("worktrees", [])
250
- return worktrees
251
-
252
- # ==================== Workflow Helpers (via MCP) ====================
253
-
254
- async def get_workflow_status(self) -> dict[str, Any]:
255
- """Get current workflow status."""
256
- return await self.call_tool("gobby-workflows", "get_status", {})
257
-
258
- async def activate_workflow(self, name: str) -> dict[str, Any]:
259
- """Activate a workflow."""
260
- return await self.call_tool("gobby-workflows", "activate_workflow", {"name": name})
261
-
262
- async def set_workflow_variable(self, name: str, value: Any) -> dict[str, Any]:
263
- """Set a workflow variable."""
264
- return await self.call_tool(
265
- "gobby-workflows",
266
- "set_variable",
267
- {"name": name, "value": value},
268
- )
269
-
270
- # ==================== Health Check ====================
271
-
272
- async def is_healthy(self) -> bool:
273
- """Check if daemon is responsive."""
274
- try:
275
- await self.get_status()
276
- return True
277
- except Exception:
278
- return False
gobby/tui/app.py DELETED
@@ -1,329 +0,0 @@
1
- """Gobby TUI - Main application entry point."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- from datetime import datetime
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- from textual.app import App, ComposeResult
11
- from textual.binding import Binding
12
- from textual.containers import Container, Horizontal
13
- from textual.reactive import reactive
14
- from textual.widgets import Static
15
-
16
- from gobby.tui.api_client import GobbyAPIClient
17
- from gobby.tui.screens.agents import AgentsScreen
18
- from gobby.tui.screens.chat import ChatScreen
19
- from gobby.tui.screens.dashboard import DashboardScreen
20
- from gobby.tui.screens.memory import MemoryScreen
21
- from gobby.tui.screens.metrics import MetricsScreen
22
- from gobby.tui.screens.orchestrator import OrchestratorScreen
23
- from gobby.tui.screens.sessions import SessionsScreen
24
- from gobby.tui.screens.tasks import TasksScreen
25
- from gobby.tui.screens.workflows import WorkflowsScreen
26
- from gobby.tui.screens.worktrees import WorktreesScreen
27
- from gobby.tui.widgets.menu import MenuPanel
28
- from gobby.tui.ws_client import GobbyWebSocketClient, WebSocketEventBridge
29
-
30
-
31
- class GobbyHeader(Static):
32
- """Custom header with title and time."""
33
-
34
- DEFAULT_CSS = """
35
- GobbyHeader {
36
- dock: top;
37
- height: 3;
38
- background: #7c3aed;
39
- color: white;
40
- layout: horizontal;
41
- }
42
-
43
- GobbyHeader #header-title {
44
- width: 1fr;
45
- content-align: center middle;
46
- text-style: bold;
47
- }
48
-
49
- GobbyHeader #header-time {
50
- width: auto;
51
- padding: 0 2;
52
- content-align: center middle;
53
- }
54
- """
55
-
56
- current_time = reactive("")
57
-
58
- def compose(self) -> ComposeResult:
59
- yield Static("GOBBY TUI", id="header-title")
60
- yield Static("", id="header-time")
61
-
62
- def on_mount(self) -> None:
63
- self.set_interval(1.0, self.update_time)
64
- self.update_time()
65
-
66
- def update_time(self) -> None:
67
- self.current_time = datetime.now().strftime("%H:%M:%S")
68
- self.query_one("#header-time", Static).update(self.current_time)
69
-
70
-
71
- class GobbyFooter(Static):
72
- """Custom footer with key hints and connection status."""
73
-
74
- DEFAULT_CSS = """
75
- GobbyFooter {
76
- dock: bottom;
77
- height: 1;
78
- background: #313244;
79
- color: #a6adc8;
80
- layout: horizontal;
81
- }
82
-
83
- GobbyFooter #footer-hints {
84
- width: 1fr;
85
- padding: 0 1;
86
- }
87
-
88
- GobbyFooter #footer-status {
89
- width: auto;
90
- padding: 0 1;
91
- }
92
-
93
- GobbyFooter .connected {
94
- color: #22c55e;
95
- }
96
-
97
- GobbyFooter .disconnected {
98
- color: #ef4444;
99
- }
100
- """
101
-
102
- connected = reactive(False)
103
-
104
- def compose(self) -> ComposeResult:
105
- yield Static("[Q] Quit [?] Help [Tab] Focus [/] Search", id="footer-hints")
106
- yield Static("Disconnected", id="footer-status", classes="disconnected")
107
-
108
- def watch_connected(self, connected: bool) -> None:
109
- status_widget = self.query_one("#footer-status", Static)
110
- if connected:
111
- status_widget.update("Connected")
112
- status_widget.remove_class("disconnected")
113
- status_widget.add_class("connected")
114
- else:
115
- status_widget.update("Disconnected")
116
- status_widget.remove_class("connected")
117
- status_widget.add_class("disconnected")
118
-
119
-
120
- class ContentArea(Container):
121
- """Container for the main content area that switches screens."""
122
-
123
- DEFAULT_CSS = """
124
- ContentArea {
125
- width: 1fr;
126
- height: 1fr;
127
- }
128
- """
129
-
130
-
131
- class GobbyApp(App[None]):
132
- """Gobby TUI Dashboard Application."""
133
-
134
- TITLE = "Gobby TUI"
135
- CSS_PATH = Path(__file__).parent / "styles" / "gobby.tcss"
136
-
137
- BINDINGS = [
138
- Binding("q", "quit", "Quit"),
139
- Binding("question_mark", "help", "Help"),
140
- Binding("tab", "focus_next", "Focus Next"),
141
- Binding("shift+tab", "focus_previous", "Focus Previous"),
142
- Binding("slash", "search", "Search"),
143
- Binding("r", "refresh", "Refresh"),
144
- # Screen navigation
145
- Binding("d", "switch_screen('dashboard')", "Dashboard", show=False),
146
- Binding("t", "switch_screen('tasks')", "Tasks", show=False),
147
- Binding("s", "switch_screen('sessions')", "Sessions", show=False),
148
- Binding("c", "switch_screen('chat')", "Chat", show=False),
149
- Binding("a", "switch_screen('agents')", "Agents", show=False),
150
- Binding("w", "switch_screen('worktrees')", "Worktrees", show=False),
151
- Binding("f", "switch_screen('workflows')", "Workflows", show=False),
152
- Binding("m", "switch_screen('memory')", "Memory", show=False),
153
- Binding("e", "switch_screen('metrics')", "Metrics", show=False),
154
- Binding("o", "switch_screen('orchestrator')", "Orchestrator", show=False),
155
- ]
156
-
157
- current_screen_id = reactive("dashboard")
158
-
159
- def __init__(
160
- self,
161
- daemon_url: str = "http://localhost:60887",
162
- ws_url: str = "ws://localhost:60888",
163
- ) -> None:
164
- super().__init__()
165
- self.daemon_url = daemon_url
166
- self.ws_url = ws_url
167
-
168
- # API and WebSocket clients
169
- self.api_client = GobbyAPIClient(daemon_url)
170
- self.ws_client = GobbyWebSocketClient(ws_url)
171
- self.ws_bridge = WebSocketEventBridge(self.ws_client)
172
-
173
- # Screen instances (created lazily)
174
- self._screens: dict[str, Any] = {}
175
-
176
- # WebSocket task
177
- self._ws_task: asyncio.Task[None] | None = None
178
-
179
- def compose(self) -> ComposeResult:
180
- yield GobbyHeader()
181
- with Horizontal():
182
- yield MenuPanel(id="menu-panel")
183
- yield ContentArea(id="content-area")
184
- yield GobbyFooter(id="footer")
185
-
186
- async def on_mount(self) -> None:
187
- """Initialize the app on mount."""
188
- # Set up WebSocket event bridge
189
- self.ws_bridge.bind_app(self)
190
- self.ws_bridge.setup_handlers()
191
-
192
- # Register connection callbacks
193
- self.ws_client.on_connect(self._on_ws_connect)
194
- self.ws_client.on_disconnect(self._on_ws_disconnect)
195
-
196
- # Subscribe to events
197
- await self.ws_client.subscribe(
198
- [
199
- "hook_event",
200
- "agent_event",
201
- "autonomous_event",
202
- "session_message",
203
- "worktree_event",
204
- ]
205
- )
206
-
207
- # Start WebSocket connection in background
208
- self._ws_task = asyncio.create_task(self._connect_ws())
209
-
210
- # Show initial screen
211
- await self._show_screen("dashboard")
212
-
213
- async def _connect_ws(self) -> None:
214
- """Connect to WebSocket server."""
215
- try:
216
- await self.ws_client.connect()
217
- except Exception as e:
218
- self.log.error(f"WebSocket connection failed: {e}")
219
-
220
- def _on_ws_connect(self) -> None:
221
- """Handle WebSocket connection established."""
222
- footer = self.query_one("#footer", GobbyFooter)
223
- footer.connected = True
224
-
225
- def _on_ws_disconnect(self) -> None:
226
- """Handle WebSocket disconnection."""
227
- footer = self.query_one("#footer", GobbyFooter)
228
- footer.connected = False
229
-
230
- def post_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
231
- """Handle WebSocket events posted from the bridge."""
232
- # Dispatch to current screen if it has a handler
233
- content_area = self.query_one("#content-area", ContentArea)
234
- for child in content_area.children:
235
- if hasattr(child, "on_ws_event"):
236
- child.on_ws_event(event_type, data)
237
-
238
- async def _show_screen(self, screen_id: str) -> None:
239
- """Show a screen in the content area."""
240
- content_area = self.query_one("#content-area", ContentArea)
241
-
242
- # Clear current content
243
- await content_area.remove_children()
244
-
245
- # Create or get screen instance
246
- screen = self._get_or_create_screen(screen_id)
247
-
248
- # Mount the screen widget
249
- await content_area.mount(screen)
250
-
251
- # Update menu selection
252
- menu = self.query_one("#menu-panel", MenuPanel)
253
- menu.current_screen = screen_id
254
-
255
- self.current_screen_id = screen_id
256
-
257
- def _get_or_create_screen(self, screen_id: str) -> Any:
258
- """Get or create a screen instance."""
259
- if screen_id not in self._screens:
260
- screen_class = self._get_screen_class(screen_id)
261
- self._screens[screen_id] = screen_class(
262
- api_client=self.api_client,
263
- ws_client=self.ws_client,
264
- )
265
- return self._screens[screen_id]
266
-
267
- def _get_screen_class(self, screen_id: str) -> type:
268
- """Get the screen class for a screen ID."""
269
- screen_map = {
270
- "dashboard": DashboardScreen,
271
- "tasks": TasksScreen,
272
- "sessions": SessionsScreen,
273
- "chat": ChatScreen,
274
- "agents": AgentsScreen,
275
- "worktrees": WorktreesScreen,
276
- "workflows": WorkflowsScreen,
277
- "memory": MemoryScreen,
278
- "metrics": MetricsScreen,
279
- "orchestrator": OrchestratorScreen,
280
- }
281
- return screen_map.get(screen_id, DashboardScreen)
282
-
283
- async def action_switch_screen(self, screen_id: str) -> None:
284
- """Switch to a different screen."""
285
- await self._show_screen(screen_id)
286
-
287
- async def action_refresh(self) -> None:
288
- """Refresh the current screen."""
289
- content_area = self.query_one("#content-area", ContentArea)
290
- for child in content_area.children:
291
- if hasattr(child, "refresh_data"):
292
- await child.refresh_data()
293
-
294
- def action_help(self) -> None:
295
- """Show help overlay."""
296
- self.notify(
297
- "Help: Press D/T/S/C/A/W/F/M/E/O to switch screens. Q to quit.",
298
- title="Keyboard Shortcuts",
299
- )
300
-
301
- def action_search(self) -> None:
302
- """Activate search in current screen."""
303
- content_area = self.query_one("#content-area", ContentArea)
304
- for child in content_area.children:
305
- if hasattr(child, "activate_search"):
306
- child.activate_search()
307
-
308
- def on_menu_panel_item_selected(self, event: MenuPanel.ItemSelected) -> None:
309
- """Handle menu item selection."""
310
- self.run_worker(self._show_screen(event.item.screen_id))
311
-
312
- async def on_unmount(self) -> None:
313
- """Clean up on app exit."""
314
- if self._ws_task:
315
- self._ws_task.cancel()
316
- try:
317
- await self._ws_task
318
- except asyncio.CancelledError:
319
- pass
320
-
321
- await self.ws_client.disconnect()
322
-
323
-
324
- def run_tui(
325
- daemon_url: str = "http://localhost:60887", ws_url: str = "ws://localhost:60888"
326
- ) -> None:
327
- """Entry point for the TUI application."""
328
- app = GobbyApp(daemon_url=daemon_url, ws_url=ws_url)
329
- app.run()
@@ -1,25 +0,0 @@
1
- """TUI screens for different views."""
2
-
3
- from gobby.tui.screens.agents import AgentsScreen
4
- from gobby.tui.screens.chat import ChatScreen
5
- from gobby.tui.screens.dashboard import DashboardScreen
6
- from gobby.tui.screens.memory import MemoryScreen
7
- from gobby.tui.screens.metrics import MetricsScreen
8
- from gobby.tui.screens.orchestrator import OrchestratorScreen
9
- from gobby.tui.screens.sessions import SessionsScreen
10
- from gobby.tui.screens.tasks import TasksScreen
11
- from gobby.tui.screens.workflows import WorkflowsScreen
12
- from gobby.tui.screens.worktrees import WorktreesScreen
13
-
14
- __all__ = [
15
- "DashboardScreen",
16
- "TasksScreen",
17
- "SessionsScreen",
18
- "ChatScreen",
19
- "AgentsScreen",
20
- "WorktreesScreen",
21
- "WorkflowsScreen",
22
- "MemoryScreen",
23
- "MetricsScreen",
24
- "OrchestratorScreen",
25
- ]