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
@@ -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
@@ -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