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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +96 -35
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- 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 +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +13 -0
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +217 -51
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- 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/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
gobby/tui/screens/dashboard.py
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
"""Dashboard screen with overview widgets."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from textual.app import ComposeResult
|
|
9
|
-
from textual.containers import Container, Grid, Vertical
|
|
10
|
-
from textual.reactive import reactive
|
|
11
|
-
from textual.widget import Widget
|
|
12
|
-
from textual.widgets import LoadingIndicator, Static
|
|
13
|
-
|
|
14
|
-
from gobby.tui.api_client import GobbyAPIClient
|
|
15
|
-
from gobby.tui.ws_client import GobbyWebSocketClient
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class HaikuPanel(Static):
|
|
19
|
-
"""Panel displaying TARS-style haiku status."""
|
|
20
|
-
|
|
21
|
-
DEFAULT_CSS = """
|
|
22
|
-
HaikuPanel {
|
|
23
|
-
border: round #7c3aed;
|
|
24
|
-
padding: 1 2;
|
|
25
|
-
height: auto;
|
|
26
|
-
min-height: 8;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
HaikuPanel .haiku-title {
|
|
30
|
-
text-style: bold;
|
|
31
|
-
color: #a78bfa;
|
|
32
|
-
text-align: center;
|
|
33
|
-
padding-bottom: 1;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
HaikuPanel .haiku-line {
|
|
37
|
-
text-align: center;
|
|
38
|
-
color: #cdd6f4;
|
|
39
|
-
padding: 0 1;
|
|
40
|
-
}
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
haiku_lines: reactive[list[str]] = reactive(list)
|
|
44
|
-
_default_haiku = ["Tasks await your hands", "Agents stand ready to serve", "Begin your journey"]
|
|
45
|
-
|
|
46
|
-
def compose(self) -> ComposeResult:
|
|
47
|
-
yield Static("🎭 Gobby", classes="haiku-title")
|
|
48
|
-
lines = self.haiku_lines if self.haiku_lines else self._default_haiku
|
|
49
|
-
for i, line in enumerate(lines):
|
|
50
|
-
yield Static(line, classes="haiku-line", id=f"haiku-line-{i}")
|
|
51
|
-
|
|
52
|
-
def watch_haiku_lines(self, lines: list[str]) -> None:
|
|
53
|
-
for i, line in enumerate(lines[:3]):
|
|
54
|
-
try:
|
|
55
|
-
widget = self.query_one(f"#haiku-line-{i}", Static)
|
|
56
|
-
widget.update(line)
|
|
57
|
-
except Exception:
|
|
58
|
-
pass # nosec B110 - widget may not be mounted yet
|
|
59
|
-
|
|
60
|
-
def update_haiku(self, lines: list[str]) -> None:
|
|
61
|
-
"""Update the haiku display."""
|
|
62
|
-
self.haiku_lines = lines[:3] if lines else self.haiku_lines
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class StatsPanel(Static):
|
|
66
|
-
"""Panel displaying system statistics."""
|
|
67
|
-
|
|
68
|
-
DEFAULT_CSS = """
|
|
69
|
-
StatsPanel {
|
|
70
|
-
border: round #45475a;
|
|
71
|
-
padding: 1 2;
|
|
72
|
-
height: auto;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
StatsPanel .panel-title {
|
|
76
|
-
text-style: bold;
|
|
77
|
-
color: #a78bfa;
|
|
78
|
-
padding-bottom: 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
StatsPanel .stat-row {
|
|
82
|
-
layout: horizontal;
|
|
83
|
-
height: 1;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
StatsPanel .stat-label {
|
|
87
|
-
width: 1fr;
|
|
88
|
-
color: #a6adc8;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
StatsPanel .stat-value {
|
|
92
|
-
width: auto;
|
|
93
|
-
text-style: bold;
|
|
94
|
-
}
|
|
95
|
-
"""
|
|
96
|
-
|
|
97
|
-
def compose(self) -> ComposeResult:
|
|
98
|
-
yield Static("📊 Statistics", classes="panel-title")
|
|
99
|
-
stats = [
|
|
100
|
-
("Tasks Open", "tasks-open", "0"),
|
|
101
|
-
("Tasks In Progress", "tasks-progress", "0"),
|
|
102
|
-
("Active Sessions", "sessions-active", "0"),
|
|
103
|
-
("Running Agents", "agents-running", "0"),
|
|
104
|
-
("MCP Servers", "mcp-servers", "0"),
|
|
105
|
-
("Memory Items", "memory-items", "0"),
|
|
106
|
-
]
|
|
107
|
-
for label, stat_id, default in stats:
|
|
108
|
-
with Container(classes="stat-row"):
|
|
109
|
-
yield Static(label, classes="stat-label")
|
|
110
|
-
yield Static(default, classes="stat-value", id=f"stat-{stat_id}")
|
|
111
|
-
|
|
112
|
-
def update_stat(self, stat_id: str, value: str | int) -> None:
|
|
113
|
-
"""Update a specific stat value."""
|
|
114
|
-
try:
|
|
115
|
-
widget = self.query_one(f"#stat-{stat_id}", Static)
|
|
116
|
-
widget.update(str(value))
|
|
117
|
-
except Exception:
|
|
118
|
-
pass # nosec B110 - widget may not be mounted yet
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class ActivityPanel(Static):
|
|
122
|
-
"""Panel displaying recent activity."""
|
|
123
|
-
|
|
124
|
-
DEFAULT_CSS = """
|
|
125
|
-
ActivityPanel {
|
|
126
|
-
border: round #45475a;
|
|
127
|
-
padding: 1 2;
|
|
128
|
-
height: 1fr;
|
|
129
|
-
min-height: 10;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
ActivityPanel .panel-title {
|
|
133
|
-
text-style: bold;
|
|
134
|
-
color: #a78bfa;
|
|
135
|
-
padding-bottom: 1;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
ActivityPanel .activity-list {
|
|
139
|
-
height: 1fr;
|
|
140
|
-
overflow-y: auto;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
ActivityPanel .activity-item {
|
|
144
|
-
height: 1;
|
|
145
|
-
color: #a6adc8;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
ActivityPanel .activity-time {
|
|
149
|
-
color: #6c7086;
|
|
150
|
-
width: 10;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
ActivityPanel .activity-event {
|
|
154
|
-
width: 1fr;
|
|
155
|
-
}
|
|
156
|
-
"""
|
|
157
|
-
|
|
158
|
-
activities: reactive[list[dict[str, Any]]] = reactive(list)
|
|
159
|
-
|
|
160
|
-
def compose(self) -> ComposeResult:
|
|
161
|
-
yield Static("📜 Recent Activity", classes="panel-title")
|
|
162
|
-
yield Vertical(id="activity-list", classes="activity-list")
|
|
163
|
-
|
|
164
|
-
def watch_activities(self, activities: list[dict[str, Any]]) -> None:
|
|
165
|
-
self._refresh_activities()
|
|
166
|
-
|
|
167
|
-
def _refresh_activities(self) -> None:
|
|
168
|
-
"""Refresh the activity list display."""
|
|
169
|
-
try:
|
|
170
|
-
container = self.query_one("#activity-list", Vertical)
|
|
171
|
-
container.remove_children()
|
|
172
|
-
for activity in self.activities[-10:]: # Show last 10
|
|
173
|
-
time_str = activity.get("time", "")[:8]
|
|
174
|
-
event = activity.get("event", "Unknown event")
|
|
175
|
-
container.mount(Static(f"[{time_str}] {event}", classes="activity-item"))
|
|
176
|
-
except Exception:
|
|
177
|
-
pass # nosec B110 - TUI update failure is non-critical
|
|
178
|
-
|
|
179
|
-
def add_activity(self, event: str, time_str: str | None = None) -> None:
|
|
180
|
-
"""Add a new activity to the list."""
|
|
181
|
-
from datetime import datetime
|
|
182
|
-
|
|
183
|
-
if time_str is None:
|
|
184
|
-
time_str = datetime.now().strftime("%H:%M:%S")
|
|
185
|
-
new_activities = list(self.activities)
|
|
186
|
-
new_activities.append({"time": time_str, "event": event})
|
|
187
|
-
# Keep last 50 activities
|
|
188
|
-
self.activities = new_activities[-50:]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
class DashboardScreen(Widget):
|
|
192
|
-
"""Dashboard screen showing overview of Gobby status."""
|
|
193
|
-
|
|
194
|
-
DEFAULT_CSS = """
|
|
195
|
-
DashboardScreen {
|
|
196
|
-
width: 1fr;
|
|
197
|
-
height: 1fr;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
DashboardScreen #dashboard-grid {
|
|
201
|
-
layout: grid;
|
|
202
|
-
grid-size: 2 2;
|
|
203
|
-
grid-gutter: 1;
|
|
204
|
-
padding: 1;
|
|
205
|
-
height: 1fr;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
DashboardScreen #haiku-panel {
|
|
209
|
-
column-span: 1;
|
|
210
|
-
row-span: 1;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
DashboardScreen #stats-panel {
|
|
214
|
-
column-span: 1;
|
|
215
|
-
row-span: 1;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
DashboardScreen #activity-panel {
|
|
219
|
-
column-span: 2;
|
|
220
|
-
row-span: 1;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
DashboardScreen .loading-container {
|
|
224
|
-
width: 1fr;
|
|
225
|
-
height: 1fr;
|
|
226
|
-
content-align: center middle;
|
|
227
|
-
}
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
loading = reactive(True)
|
|
231
|
-
|
|
232
|
-
def __init__(
|
|
233
|
-
self,
|
|
234
|
-
api_client: GobbyAPIClient,
|
|
235
|
-
ws_client: GobbyWebSocketClient,
|
|
236
|
-
**kwargs: Any,
|
|
237
|
-
) -> None:
|
|
238
|
-
super().__init__(**kwargs)
|
|
239
|
-
self.api_client = api_client
|
|
240
|
-
self.ws_client = ws_client
|
|
241
|
-
|
|
242
|
-
def compose(self) -> ComposeResult:
|
|
243
|
-
if self.loading:
|
|
244
|
-
with Container(classes="loading-container"):
|
|
245
|
-
yield LoadingIndicator()
|
|
246
|
-
else:
|
|
247
|
-
with Grid(id="dashboard-grid"):
|
|
248
|
-
yield HaikuPanel(id="haiku-panel")
|
|
249
|
-
yield StatsPanel(id="stats-panel")
|
|
250
|
-
yield ActivityPanel(id="activity-panel")
|
|
251
|
-
|
|
252
|
-
async def on_mount(self) -> None:
|
|
253
|
-
"""Load data when mounted."""
|
|
254
|
-
await self.refresh_data()
|
|
255
|
-
|
|
256
|
-
async def refresh_data(self) -> None:
|
|
257
|
-
"""Refresh all dashboard data."""
|
|
258
|
-
try:
|
|
259
|
-
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
260
|
-
# Fetch all data first before any UI updates
|
|
261
|
-
status = await client.get_status()
|
|
262
|
-
agents = await client.list_agents()
|
|
263
|
-
|
|
264
|
-
# Now update UI - set loading to false and recompose if needed
|
|
265
|
-
if self.loading:
|
|
266
|
-
self.loading = False
|
|
267
|
-
await self.recompose()
|
|
268
|
-
|
|
269
|
-
# Update stats panel
|
|
270
|
-
stats_panel = self.query_one("#stats-panel", StatsPanel)
|
|
271
|
-
|
|
272
|
-
# Task counts
|
|
273
|
-
tasks_summary = status.get("tasks", {})
|
|
274
|
-
stats_panel.update_stat("tasks-open", tasks_summary.get("open", 0))
|
|
275
|
-
stats_panel.update_stat("tasks-progress", tasks_summary.get("in_progress", 0))
|
|
276
|
-
|
|
277
|
-
# Session count
|
|
278
|
-
sessions_summary = status.get("sessions", {})
|
|
279
|
-
stats_panel.update_stat("sessions-active", sessions_summary.get("active", 0))
|
|
280
|
-
|
|
281
|
-
# Agent count
|
|
282
|
-
running_agents = len([a for a in agents if a.get("status") == "running"])
|
|
283
|
-
stats_panel.update_stat("agents-running", running_agents)
|
|
284
|
-
|
|
285
|
-
# MCP servers
|
|
286
|
-
mcp_status = status.get("mcp_servers", {})
|
|
287
|
-
stats_panel.update_stat("mcp-servers", mcp_status.get("connected", 0))
|
|
288
|
-
|
|
289
|
-
# Memory count
|
|
290
|
-
memory_status = status.get("memory", {})
|
|
291
|
-
stats_panel.update_stat("memory-items", memory_status.get("count", 0))
|
|
292
|
-
|
|
293
|
-
# Update haiku based on status
|
|
294
|
-
haiku_panel = self.query_one("#haiku-panel", HaikuPanel)
|
|
295
|
-
haiku = self._generate_status_haiku(status)
|
|
296
|
-
haiku_panel.update_haiku(haiku)
|
|
297
|
-
|
|
298
|
-
# Add initial activity
|
|
299
|
-
activity_panel = self.query_one("#activity-panel", ActivityPanel)
|
|
300
|
-
activity_panel.add_activity("Dashboard loaded")
|
|
301
|
-
|
|
302
|
-
except Exception as e:
|
|
303
|
-
# Show error state
|
|
304
|
-
self.loading = False
|
|
305
|
-
self.notify(f"Failed to load dashboard: {e}", severity="error")
|
|
306
|
-
|
|
307
|
-
def _generate_status_haiku(self, status: dict[str, Any]) -> list[str]:
|
|
308
|
-
"""Generate a haiku based on system status."""
|
|
309
|
-
tasks = status.get("tasks", {})
|
|
310
|
-
open_count = tasks.get("open", 0)
|
|
311
|
-
in_progress = tasks.get("in_progress", 0)
|
|
312
|
-
|
|
313
|
-
if open_count == 0 and in_progress == 0:
|
|
314
|
-
return [
|
|
315
|
-
"All tasks complete",
|
|
316
|
-
"The queue stands empty now",
|
|
317
|
-
"Peace in the system",
|
|
318
|
-
]
|
|
319
|
-
elif in_progress > 0:
|
|
320
|
-
return [
|
|
321
|
-
f"{in_progress} task{'s' if in_progress != 1 else ''} in flight",
|
|
322
|
-
"Code flows through eager hands",
|
|
323
|
-
"Progress unfolds",
|
|
324
|
-
]
|
|
325
|
-
else:
|
|
326
|
-
return [
|
|
327
|
-
f"{open_count} await your call",
|
|
328
|
-
"Ready to begin the work",
|
|
329
|
-
"Choose your first task",
|
|
330
|
-
]
|
|
331
|
-
|
|
332
|
-
def on_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
|
|
333
|
-
"""Handle WebSocket events."""
|
|
334
|
-
# Add to activity feed
|
|
335
|
-
try:
|
|
336
|
-
activity_panel = self.query_one("#activity-panel", ActivityPanel)
|
|
337
|
-
|
|
338
|
-
if event_type == "agent_event":
|
|
339
|
-
event = data.get("event", "")
|
|
340
|
-
run_id = data.get("run_id", "")[:8]
|
|
341
|
-
activity_panel.add_activity(f"Agent {run_id}: {event}")
|
|
342
|
-
|
|
343
|
-
elif event_type == "hook_event":
|
|
344
|
-
hook_type = data.get("event_type", "")
|
|
345
|
-
activity_panel.add_activity(f"Hook: {hook_type}")
|
|
346
|
-
|
|
347
|
-
elif event_type == "worktree_event":
|
|
348
|
-
event = data.get("event", "")
|
|
349
|
-
branch = data.get("branch_name", "")
|
|
350
|
-
activity_panel.add_activity(f"Worktree {branch}: {event}")
|
|
351
|
-
|
|
352
|
-
elif event_type == "autonomous_event":
|
|
353
|
-
event = data.get("event", "")
|
|
354
|
-
task_id = data.get("task_id", "")
|
|
355
|
-
activity_panel.add_activity(f"Auto: {event} ({task_id})")
|
|
356
|
-
|
|
357
|
-
except Exception:
|
|
358
|
-
pass # nosec B110 - TUI event handling failure is non-critical
|
|
359
|
-
|
|
360
|
-
# Refresh stats periodically on events
|
|
361
|
-
asyncio.create_task(self._refresh_stats_quietly())
|
|
362
|
-
|
|
363
|
-
async def _refresh_stats_quietly(self) -> None:
|
|
364
|
-
"""Refresh stats without full reload."""
|
|
365
|
-
try:
|
|
366
|
-
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
367
|
-
status = await client.get_status()
|
|
368
|
-
stats_panel = self.query_one("#stats-panel", StatsPanel)
|
|
369
|
-
|
|
370
|
-
tasks_summary = status.get("tasks", {})
|
|
371
|
-
stats_panel.update_stat("tasks-open", tasks_summary.get("open", 0))
|
|
372
|
-
stats_panel.update_stat("tasks-progress", tasks_summary.get("in_progress", 0))
|
|
373
|
-
|
|
374
|
-
sessions_summary = status.get("sessions", {})
|
|
375
|
-
stats_panel.update_stat("sessions-active", sessions_summary.get("active", 0))
|
|
376
|
-
except Exception:
|
|
377
|
-
pass # nosec B110 - background refresh failure is non-critical
|
gobby/tui/screens/memory.py
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
"""Memory screen with search and list."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from textual.app import ComposeResult
|
|
9
|
-
from textual.containers import Container, Horizontal, Vertical
|
|
10
|
-
from textual.reactive import reactive
|
|
11
|
-
from textual.widget import Widget
|
|
12
|
-
from textual.widgets import (
|
|
13
|
-
Button,
|
|
14
|
-
DataTable,
|
|
15
|
-
Input,
|
|
16
|
-
LoadingIndicator,
|
|
17
|
-
Static,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from gobby.tui.api_client import GobbyAPIClient
|
|
21
|
-
from gobby.tui.ws_client import GobbyWebSocketClient
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class MemoryDetailPanel(Widget):
|
|
25
|
-
"""Panel showing memory item details."""
|
|
26
|
-
|
|
27
|
-
DEFAULT_CSS = """
|
|
28
|
-
MemoryDetailPanel {
|
|
29
|
-
width: 40%;
|
|
30
|
-
height: 1fr;
|
|
31
|
-
padding: 1;
|
|
32
|
-
border-left: solid #45475a;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
MemoryDetailPanel .detail-title {
|
|
36
|
-
text-style: bold;
|
|
37
|
-
color: #a78bfa;
|
|
38
|
-
padding-bottom: 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
MemoryDetailPanel .detail-row {
|
|
42
|
-
layout: horizontal;
|
|
43
|
-
height: 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
MemoryDetailPanel .detail-label {
|
|
47
|
-
color: #a6adc8;
|
|
48
|
-
width: 12;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
MemoryDetailPanel .detail-value {
|
|
52
|
-
width: 1fr;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
MemoryDetailPanel .content-area {
|
|
56
|
-
height: 1fr;
|
|
57
|
-
padding: 1;
|
|
58
|
-
border: round #45475a;
|
|
59
|
-
overflow-y: auto;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
MemoryDetailPanel .empty-state {
|
|
63
|
-
content-align: center middle;
|
|
64
|
-
height: 1fr;
|
|
65
|
-
color: #a6adc8;
|
|
66
|
-
}
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
memory: reactive[dict[str, Any] | None] = reactive(None)
|
|
70
|
-
|
|
71
|
-
def compose(self) -> ComposeResult:
|
|
72
|
-
if self.memory is None:
|
|
73
|
-
yield Static("Select a memory to view details", classes="empty-state")
|
|
74
|
-
else:
|
|
75
|
-
yield Static("📝 Memory Details", classes="detail-title")
|
|
76
|
-
|
|
77
|
-
with Horizontal(classes="detail-row"):
|
|
78
|
-
yield Static("ID:", classes="detail-label")
|
|
79
|
-
yield Static(self.memory.get("id", "")[:16], classes="detail-value")
|
|
80
|
-
|
|
81
|
-
with Horizontal(classes="detail-row"):
|
|
82
|
-
yield Static("Importance:", classes="detail-label")
|
|
83
|
-
importance = self.memory.get("importance", 0.5)
|
|
84
|
-
yield Static(f"{importance:.2f}", classes="detail-value")
|
|
85
|
-
|
|
86
|
-
with Horizontal(classes="detail-row"):
|
|
87
|
-
yield Static("Created:", classes="detail-label")
|
|
88
|
-
created = self.memory.get("created_at", "")[:19]
|
|
89
|
-
yield Static(created, classes="detail-value")
|
|
90
|
-
|
|
91
|
-
yield Static("Content:", classes="detail-label")
|
|
92
|
-
yield Static(self.memory.get("content", ""), classes="content-area")
|
|
93
|
-
|
|
94
|
-
def watch_memory(self, memory: dict[str, Any] | None) -> None:
|
|
95
|
-
"""Recompose when memory changes."""
|
|
96
|
-
self.call_after_refresh(self.recompose)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class MemoryScreen(Widget):
|
|
100
|
-
"""Memory screen with search and list."""
|
|
101
|
-
|
|
102
|
-
DEFAULT_CSS = """
|
|
103
|
-
MemoryScreen {
|
|
104
|
-
width: 1fr;
|
|
105
|
-
height: 1fr;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
MemoryScreen .screen-header {
|
|
109
|
-
height: auto;
|
|
110
|
-
padding: 1;
|
|
111
|
-
background: #313244;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
MemoryScreen .header-row {
|
|
115
|
-
layout: horizontal;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
MemoryScreen .panel-title {
|
|
119
|
-
text-style: bold;
|
|
120
|
-
color: #a78bfa;
|
|
121
|
-
width: 1fr;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
MemoryScreen .search-row {
|
|
125
|
-
layout: horizontal;
|
|
126
|
-
height: 3;
|
|
127
|
-
margin-top: 1;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
MemoryScreen #search-input {
|
|
131
|
-
width: 1fr;
|
|
132
|
-
margin-right: 1;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
MemoryScreen #content-container {
|
|
136
|
-
layout: horizontal;
|
|
137
|
-
height: 1fr;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
MemoryScreen #memories-table {
|
|
141
|
-
width: 60%;
|
|
142
|
-
height: 1fr;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
MemoryScreen .loading-container {
|
|
146
|
-
width: 1fr;
|
|
147
|
-
height: 1fr;
|
|
148
|
-
content-align: center middle;
|
|
149
|
-
}
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
loading = reactive(True)
|
|
153
|
-
memories: reactive[list[dict[str, Any]]] = reactive(list)
|
|
154
|
-
selected_memory_id: reactive[str | None] = reactive(None)
|
|
155
|
-
search_query = ""
|
|
156
|
-
|
|
157
|
-
def __init__(
|
|
158
|
-
self,
|
|
159
|
-
api_client: GobbyAPIClient,
|
|
160
|
-
ws_client: GobbyWebSocketClient,
|
|
161
|
-
**kwargs: Any,
|
|
162
|
-
) -> None:
|
|
163
|
-
super().__init__(**kwargs)
|
|
164
|
-
self.api_client = api_client
|
|
165
|
-
self.ws_client = ws_client
|
|
166
|
-
self._memory_map: dict[str, dict[str, Any]] = {}
|
|
167
|
-
|
|
168
|
-
def compose(self) -> ComposeResult:
|
|
169
|
-
with Vertical(classes="screen-header"):
|
|
170
|
-
with Horizontal(classes="header-row"):
|
|
171
|
-
yield Static("🧠 Memory", classes="panel-title")
|
|
172
|
-
yield Button("+ Remember", variant="primary", id="btn-remember")
|
|
173
|
-
yield Button("Forget", id="btn-forget")
|
|
174
|
-
yield Button("Refresh", id="btn-refresh")
|
|
175
|
-
with Horizontal(classes="search-row"):
|
|
176
|
-
yield Input(placeholder="Search memories...", id="search-input")
|
|
177
|
-
yield Button("Search", id="btn-search")
|
|
178
|
-
|
|
179
|
-
if self.loading:
|
|
180
|
-
with Container(classes="loading-container"):
|
|
181
|
-
yield LoadingIndicator()
|
|
182
|
-
else:
|
|
183
|
-
with Horizontal(id="content-container"):
|
|
184
|
-
yield DataTable(id="memories-table")
|
|
185
|
-
yield MemoryDetailPanel(id="detail-panel")
|
|
186
|
-
|
|
187
|
-
async def on_mount(self) -> None:
|
|
188
|
-
"""Load data when mounted."""
|
|
189
|
-
await self.refresh_data()
|
|
190
|
-
|
|
191
|
-
async def refresh_data(self) -> None:
|
|
192
|
-
"""Refresh memory list."""
|
|
193
|
-
try:
|
|
194
|
-
if self.search_query:
|
|
195
|
-
memories = await self.api_client.recall(self.search_query, limit=50)
|
|
196
|
-
else:
|
|
197
|
-
result = await self.api_client.call_tool(
|
|
198
|
-
"gobby-memory",
|
|
199
|
-
"list_memories",
|
|
200
|
-
{"limit": 50},
|
|
201
|
-
)
|
|
202
|
-
memories = result.get("memories", [])
|
|
203
|
-
|
|
204
|
-
self.memories = memories
|
|
205
|
-
self._memory_map = {m.get("id", ""): m for m in memories}
|
|
206
|
-
|
|
207
|
-
except Exception as e:
|
|
208
|
-
self.notify(f"Failed to load memories: {e}", severity="error")
|
|
209
|
-
finally:
|
|
210
|
-
self.loading = False
|
|
211
|
-
await self.recompose()
|
|
212
|
-
await self._setup_table()
|
|
213
|
-
|
|
214
|
-
async def _setup_table(self) -> None:
|
|
215
|
-
"""Set up and populate the memories table."""
|
|
216
|
-
try:
|
|
217
|
-
table = self.query_one("#memories-table", DataTable)
|
|
218
|
-
table.clear(columns=True)
|
|
219
|
-
table.add_columns("ID", "Content", "Importance")
|
|
220
|
-
table.cursor_type = "row"
|
|
221
|
-
|
|
222
|
-
for memory in self.memories:
|
|
223
|
-
mem_id = memory.get("id", "")[:12]
|
|
224
|
-
content = memory.get("content", "")[:40]
|
|
225
|
-
if len(memory.get("content", "")) > 40:
|
|
226
|
-
content += "..."
|
|
227
|
-
importance = f"{memory.get('importance', 0.5):.2f}"
|
|
228
|
-
|
|
229
|
-
table.add_row(mem_id, content, importance, key=memory.get("id"))
|
|
230
|
-
|
|
231
|
-
except Exception:
|
|
232
|
-
pass # nosec B110 - TUI update failure is non-critical
|
|
233
|
-
|
|
234
|
-
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
235
|
-
"""Handle memory selection."""
|
|
236
|
-
memory_id = str(event.row_key.value) if event.row_key else None
|
|
237
|
-
if memory_id and memory_id in self._memory_map:
|
|
238
|
-
self.selected_memory_id = memory_id
|
|
239
|
-
try:
|
|
240
|
-
panel = self.query_one("#detail-panel", MemoryDetailPanel)
|
|
241
|
-
panel.memory = self._memory_map[memory_id]
|
|
242
|
-
except Exception:
|
|
243
|
-
pass # nosec B110 - widget may not be mounted yet
|
|
244
|
-
|
|
245
|
-
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
246
|
-
"""Handle button presses."""
|
|
247
|
-
button_id = event.button.id
|
|
248
|
-
|
|
249
|
-
if button_id == "btn-remember":
|
|
250
|
-
self.notify("Remember dialog coming soon", severity="information")
|
|
251
|
-
|
|
252
|
-
elif button_id == "btn-forget":
|
|
253
|
-
await self._forget_memory()
|
|
254
|
-
|
|
255
|
-
elif button_id == "btn-search":
|
|
256
|
-
await self._do_search()
|
|
257
|
-
|
|
258
|
-
elif button_id == "btn-refresh":
|
|
259
|
-
self.search_query = ""
|
|
260
|
-
self.loading = True
|
|
261
|
-
await self.refresh_data()
|
|
262
|
-
|
|
263
|
-
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
264
|
-
"""Handle search submission."""
|
|
265
|
-
if event.input.id == "search-input":
|
|
266
|
-
asyncio.create_task(self._do_search())
|
|
267
|
-
|
|
268
|
-
async def _do_search(self) -> None:
|
|
269
|
-
"""Perform memory search."""
|
|
270
|
-
try:
|
|
271
|
-
search_input = self.query_one("#search-input", Input)
|
|
272
|
-
self.search_query = search_input.value
|
|
273
|
-
self.loading = True
|
|
274
|
-
await self.refresh_data()
|
|
275
|
-
except Exception:
|
|
276
|
-
pass # nosec B110 - Widget may not be mounted yet
|
|
277
|
-
|
|
278
|
-
async def _forget_memory(self) -> None:
|
|
279
|
-
"""Forget the selected memory."""
|
|
280
|
-
if not self.selected_memory_id:
|
|
281
|
-
self.notify("No memory selected", severity="warning")
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
try:
|
|
285
|
-
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
286
|
-
await client.call_tool(
|
|
287
|
-
"gobby-memory",
|
|
288
|
-
"forget",
|
|
289
|
-
{"memory_id": self.selected_memory_id},
|
|
290
|
-
)
|
|
291
|
-
self.notify("Memory forgotten")
|
|
292
|
-
|
|
293
|
-
self.selected_memory_id = None
|
|
294
|
-
await self.refresh_data()
|
|
295
|
-
|
|
296
|
-
except Exception as e:
|
|
297
|
-
self.notify(f"Failed to forget memory: {e}", severity="error")
|
|
298
|
-
|
|
299
|
-
def activate_search(self) -> None:
|
|
300
|
-
"""Focus the search input."""
|
|
301
|
-
try:
|
|
302
|
-
search = self.query_one("#search-input", Input)
|
|
303
|
-
search.focus()
|
|
304
|
-
except Exception:
|
|
305
|
-
pass # nosec B110 - widget may not be mounted yet
|