gobby 0.2.7__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.
- gobby/adapters/claude_code.py +96 -35
- gobby/adapters/gemini.py +140 -38
- gobby/agents/isolation.py +130 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn_executor.py +43 -13
- gobby/agents/spawners/macos.py +26 -1
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/clones/git.py +177 -0
- gobby/config/skills.py +31 -0
- gobby/hooks/event_handlers.py +109 -10
- gobby/hooks/hook_manager.py +19 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/registries.py +21 -4
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
- gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
- gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
- gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
- gobby/mcp_proxy/tools/spawn_agent.py +44 -6
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/extractor.py +15 -1
- gobby/runner.py +13 -0
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +23 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +43 -3
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/blocking.py +13 -1
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/memory_actions.py +11 -0
- gobby/workflows/safe_evaluator.py +8 -0
- gobby/workflows/summary_actions.py +123 -50
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/RECORD +56 -80
- gobby/cli/tui.py +0 -34
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
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()
|
gobby/tui/screens/__init__.py
DELETED
|
@@ -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
|
-
]
|