codetether 1.2.2__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 (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. ui/monitor.js +2662 -0
@@ -0,0 +1,1790 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full bg-gray-100 dark:bg-gray-900">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>A2A Agent Monitor</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: {
16
+ 50: '#eef2ff',
17
+ 100: '#e0e7ff',
18
+ 200: '#c7d2fe',
19
+ 300: '#a5b4fc',
20
+ 400: '#818cf8',
21
+ 500: '#6366f1',
22
+ 600: '#4f46e5',
23
+ 700: '#4338ca',
24
+ 800: '#3730a3',
25
+ 900: '#312e81',
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ </script>
32
+ <style>
33
+ @keyframes pulse-dot {
34
+
35
+ 0%,
36
+ 100% {
37
+ opacity: 1;
38
+ transform: scale(1);
39
+ }
40
+
41
+ 50% {
42
+ opacity: 0.5;
43
+ transform: scale(1.1);
44
+ }
45
+ }
46
+
47
+ .pulse-dot {
48
+ animation: pulse-dot 2s ease-in-out infinite;
49
+ }
50
+
51
+ .scrollbar-thin::-webkit-scrollbar {
52
+ width: 6px;
53
+ }
54
+
55
+ .scrollbar-thin::-webkit-scrollbar-track {
56
+ background: transparent;
57
+ }
58
+
59
+ .scrollbar-thin::-webkit-scrollbar-thumb {
60
+ background: #cbd5e1;
61
+ border-radius: 3px;
62
+ }
63
+
64
+ .dark .scrollbar-thin::-webkit-scrollbar-thumb {
65
+ background: #475569;
66
+ }
67
+ </style>
68
+ </head>
69
+
70
+ <body class="h-full">
71
+ <!-- Mobile sidebar backdrop -->
72
+ <div id="mobile-sidebar-backdrop" class="hidden fixed inset-0 z-50 bg-gray-900/80 lg:hidden"
73
+ onclick="closeMobileSidebar()"></div>
74
+
75
+ <!-- Mobile sidebar -->
76
+ <div id="mobile-sidebar"
77
+ class="hidden fixed inset-y-0 left-0 z-50 w-72 bg-primary-700 dark:bg-gray-800 lg:hidden transform -translate-x-full transition-transform duration-300 ease-in-out">
78
+ <div
79
+ class="flex h-16 shrink-0 items-center justify-between px-6 border-b border-primary-600 dark:border-gray-700">
80
+ <div class="flex items-center gap-2">
81
+ <div class="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10">
82
+ <svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
83
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
84
+ d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
85
+ </svg>
86
+ </div>
87
+ <span class="text-lg font-semibold text-white">A2A Monitor</span>
88
+ </div>
89
+ <button onclick="closeMobileSidebar()" class="text-primary-200 hover:text-white">
90
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
92
+ </svg>
93
+ </button>
94
+ </div>
95
+ <nav class="flex flex-1 flex-col p-4">
96
+ <ul class="space-y-1">
97
+ <li>
98
+ <button onclick="switchTab('codebases'); closeMobileSidebar()" id="mobile-tab-codebases"
99
+ class="mobile-tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-white bg-white/10">
100
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
101
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
102
+ d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
103
+ </svg>
104
+ Codebases
105
+ </button>
106
+ </li>
107
+ <li>
108
+ <button onclick="switchTab('tasks'); closeMobileSidebar()" id="mobile-tab-tasks"
109
+ class="mobile-tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
110
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
111
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
112
+ d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
113
+ </svg>
114
+ Task Queue
115
+ </button>
116
+ </li>
117
+ <li>
118
+ <button onclick="switchTab('sessions'); closeMobileSidebar()" id="mobile-tab-sessions"
119
+ class="mobile-tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
120
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
121
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
122
+ d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
123
+ </svg>
124
+ Sessions
125
+ </button>
126
+ </li>
127
+ <li>
128
+ <button onclick="switchTab('output'); closeMobileSidebar()" id="mobile-tab-output"
129
+ class="mobile-tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
130
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
132
+ d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
133
+ </svg>
134
+ Agent Output
135
+ </button>
136
+ </li>
137
+ <li>
138
+ <button onclick="switchTab('activity'); closeMobileSidebar()" id="mobile-tab-activity"
139
+ class="mobile-tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
140
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
141
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
142
+ d="M13 10V3L4 14h7v7l9-11h-7z" />
143
+ </svg>
144
+ Activity Feed
145
+ </button>
146
+ </li>
147
+ </ul>
148
+ </nav>
149
+ </div>
150
+
151
+ <!-- Desktop sidebar -->
152
+ <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
153
+ <div class="flex grow flex-col overflow-y-auto bg-primary-700 dark:bg-gray-800">
154
+ <div class="flex h-16 shrink-0 items-center px-4 border-b border-primary-600 dark:border-gray-700">
155
+ <div class="flex items-center gap-2">
156
+ <div class="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10">
157
+ <svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
158
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
159
+ d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
160
+ </svg>
161
+ </div>
162
+ <span class="text-lg font-semibold text-white">A2A Monitor</span>
163
+ </div>
164
+ </div>
165
+ <nav class="flex flex-1 flex-col p-3">
166
+ <ul class="space-y-1">
167
+ <li>
168
+ <button onclick="switchTab('codebases')" id="tab-codebases"
169
+ class="tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-white bg-white/10">
170
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
171
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
172
+ d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
173
+ </svg>
174
+ Codebases
175
+ </button>
176
+ </li>
177
+ <li>
178
+ <button onclick="switchTab('tasks')" id="tab-tasks"
179
+ class="tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
180
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
181
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
182
+ d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
183
+ </svg>
184
+ Task Queue
185
+ </button>
186
+ </li>
187
+ <li>
188
+ <button onclick="switchTab('sessions')" id="tab-sessions"
189
+ class="tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
190
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
191
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
192
+ d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
193
+ </svg>
194
+ Sessions
195
+ </button>
196
+ </li>
197
+ <li>
198
+ <button onclick="switchTab('output')" id="tab-output"
199
+ class="tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
200
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
201
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
202
+ d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
203
+ </svg>
204
+ Agent Output
205
+ </button>
206
+ </li>
207
+ <li>
208
+ <button onclick="switchTab('activity')" id="tab-activity"
209
+ class="tab-btn w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-primary-100 hover:bg-white/10">
210
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
211
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
212
+ d="M13 10V3L4 14h7v7l9-11h-7z" />
213
+ </svg>
214
+ Activity Feed
215
+ </button>
216
+ </li>
217
+ </ul>
218
+ <!-- Stats in sidebar -->
219
+ <div class="mt-auto pt-4 border-t border-primary-600 dark:border-gray-700">
220
+ <div class="grid grid-cols-2 gap-2 text-center">
221
+ <div class="rounded-lg bg-white/5 p-2">
222
+ <div id="stat-codebases" class="text-lg font-bold text-white">0</div>
223
+ <div class="text-xs text-primary-200">Codebases</div>
224
+ </div>
225
+ <div class="rounded-lg bg-white/5 p-2">
226
+ <div id="stat-tasks" class="text-lg font-bold text-white">0</div>
227
+ <div class="text-xs text-primary-200">Tasks</div>
228
+ </div>
229
+ <div class="rounded-lg bg-white/5 p-2">
230
+ <div id="stat-completed" class="text-lg font-bold text-green-400">0</div>
231
+ <div class="text-xs text-primary-200">Completed</div>
232
+ </div>
233
+ <div class="rounded-lg bg-white/5 p-2">
234
+ <div id="stat-running" class="text-lg font-bold text-blue-400">0</div>
235
+ <div class="text-xs text-primary-200">Running</div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </nav>
240
+ </div>
241
+ </div>
242
+
243
+ <!-- Main content wrapper -->
244
+ <div class="lg:pl-56">
245
+ <!-- Top navbar -->
246
+ <div
247
+ class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:gap-x-6 sm:px-6 lg:px-8">
248
+ <!-- Mobile menu button -->
249
+ <button onclick="openMobileSidebar()" class="lg:hidden -m-2.5 p-2.5 text-gray-700 dark:text-gray-200">
250
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
251
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
252
+ </svg>
253
+ </button>
254
+
255
+ <!-- Separator -->
256
+ <div class="h-6 w-px bg-gray-200 dark:bg-gray-700 lg:hidden"></div>
257
+
258
+ <div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
259
+ <!-- Page title - shown on mobile -->
260
+ <div class="flex items-center lg:hidden">
261
+ <h1 id="current-page-title" class="text-lg font-semibold text-gray-900 dark:text-white">Codebases
262
+ </h1>
263
+ </div>
264
+
265
+ <!-- Status indicators -->
266
+ <div class="hidden md:flex items-center gap-4 ml-auto">
267
+ <div class="flex items-center gap-2">
268
+ <span id="connection-dot" class="h-2.5 w-2.5 rounded-full bg-green-500 pulse-dot"></span>
269
+ <span id="connection-status" class="text-sm text-gray-600 dark:text-gray-300">Connected</span>
270
+ </div>
271
+ <div class="text-sm text-gray-500 dark:text-gray-400">
272
+ <span id="active-agents-count">0</span> Agents
273
+ </div>
274
+ <div class="text-sm text-gray-500 dark:text-gray-400">
275
+ <span id="total-tasks-count">0</span> Tasks
276
+ </div>
277
+ </div>
278
+
279
+ <!-- Right actions -->
280
+ <div class="flex items-center gap-x-3 ml-auto md:ml-0">
281
+ <button onclick="toggleDarkMode()"
282
+ class="rounded-full p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
283
+ <svg id="dark-icon" class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
284
+ <path
285
+ d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" />
286
+ </svg>
287
+ <svg id="light-icon" class="h-5 w-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
288
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
289
+ </svg>
290
+ </button>
291
+ <a href="https://docs.codetether.run" target="_blank" title="Documentation"
292
+ class="rounded-full p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
293
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
294
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
295
+ d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
296
+ </svg>
297
+ </a>
298
+ <button onclick="openSettingsModal()"
299
+ class="rounded-full p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
300
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
301
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
302
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
303
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
304
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
305
+ </svg>
306
+ </button>
307
+ </div>
308
+ </div>
309
+ </div>
310
+
311
+ <!-- Main content -->
312
+ <main class="py-6">
313
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
314
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-4">
315
+
316
+ <!-- Left sidebar - Codebases -->
317
+ <div class="lg:col-span-1">
318
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
319
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
320
+ <div class="flex items-center justify-between">
321
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Codebases</h2>
322
+ <button onclick="openRegisterModal()"
323
+ class="rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">
324
+ + Add
325
+ </button>
326
+ </div>
327
+ </div>
328
+ <div id="codebases-list"
329
+ class="divide-y divide-gray-200 dark:divide-gray-700 max-h-[calc(100vh-300px)] overflow-y-auto scrollbar-thin">
330
+ <!-- Codebases will be rendered here -->
331
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
332
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24"
333
+ stroke="currentColor">
334
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
335
+ d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
336
+ </svg>
337
+ <p class="mt-2 text-sm">No codebases registered</p>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+
343
+ <!-- Main content area -->
344
+ <div class="lg:col-span-2">
345
+ <!-- Codebases tab content -->
346
+ <div id="content-codebases" class="tab-content">
347
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
348
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
349
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Trigger Agent</h2>
350
+ <p class="text-sm text-gray-500 dark:text-gray-400">Select a codebase and run an AI
351
+ agent</p>
352
+ </div>
353
+ <div class="p-6">
354
+ <div class="space-y-4">
355
+ <div>
356
+ <label
357
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Codebase</label>
358
+ <select id="trigger-codebase"
359
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
360
+ <option value="">Select a codebase...</option>
361
+ </select>
362
+ </div>
363
+ <div>
364
+ <label
365
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Agent
366
+ Type</label>
367
+ <select id="trigger-agent"
368
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
369
+ <option value="build">🔧 Build - Full access agent</option>
370
+ <option value="plan">📋 Plan - Read-only analysis</option>
371
+ <option value="coder">💻 Coder - Code writing focused</option>
372
+ <option value="explore">🔍 Explore - Codebase search</option>
373
+ </select>
374
+ </div>
375
+ <div>
376
+ <label
377
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Model</label>
378
+ <select id="trigger-model"
379
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
380
+ <option value="">🤖 Loading models...</option>
381
+ </select>
382
+ </div>
383
+ <div>
384
+ <label
385
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Prompt</label>
386
+ <textarea id="trigger-prompt" rows="4"
387
+ placeholder="Enter your instructions for the AI agent..."
388
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500 placeholder-gray-400"></textarea>
389
+ </div>
390
+ <button onclick="triggerAgent()"
391
+ class="w-full rounded-md bg-primary-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600">
392
+ 🚀 Run Agent
393
+ </button>
394
+ </div>
395
+ </div>
396
+ </div>
397
+ </div>
398
+
399
+ <!-- Tasks tab content -->
400
+ <div id="content-tasks" class="tab-content hidden">
401
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
402
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
403
+ <div class="flex items-center justify-between">
404
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Task Queue</h2>
405
+ <div class="flex gap-2">
406
+ <button onclick="filterTasks('all')"
407
+ class="task-filter-btn rounded-md px-3 py-1 text-xs font-medium bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300"
408
+ data-filter="all">All</button>
409
+ <button onclick="filterTasks('pending')"
410
+ class="task-filter-btn rounded-md px-3 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
411
+ data-filter="pending">Pending</button>
412
+ <button onclick="filterTasks('running')"
413
+ class="task-filter-btn rounded-md px-3 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
414
+ data-filter="running">Running</button>
415
+ <button onclick="filterTasks('completed')"
416
+ class="task-filter-btn rounded-md px-3 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
417
+ data-filter="completed">Completed</button>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ <div id="tasks-list"
422
+ class="divide-y divide-gray-200 dark:divide-gray-700 max-h-[calc(100vh-350px)] overflow-y-auto scrollbar-thin">
423
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
424
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24"
425
+ stroke="currentColor">
426
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
427
+ d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
428
+ </svg>
429
+ <p class="mt-2 text-sm">No tasks in queue</p>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ </div>
434
+
435
+ <!-- Output tab content -->
436
+ <div id="content-output" class="tab-content hidden">
437
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
438
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
439
+ <div class="flex items-center justify-between">
440
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Agent Output
441
+ </h2>
442
+ <select id="output-codebase-select" onchange="selectOutputCodebase(this.value)"
443
+ class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm">
444
+ <option value="">Select codebase...</option>
445
+ </select>
446
+ </div>
447
+ </div>
448
+ <div id="output-container"
449
+ class="p-4 max-h-[calc(100vh-350px)] overflow-y-auto scrollbar-thin font-mono text-sm">
450
+ <div class="text-center text-gray-500 dark:text-gray-400 py-8">
451
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24"
452
+ stroke="currentColor">
453
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
454
+ d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
455
+ </svg>
456
+ <p class="mt-2">Select a codebase to view agent output</p>
457
+ </div>
458
+ </div>
459
+ </div>
460
+ </div>
461
+
462
+ <!-- Activity tab content -->
463
+ <div id="content-activity" class="tab-content hidden">
464
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
465
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
466
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Activity Feed</h2>
467
+ </div>
468
+ <div id="activity-feed"
469
+ class="divide-y divide-gray-200 dark:divide-gray-700 max-h-[calc(100vh-350px)] overflow-y-auto scrollbar-thin">
470
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
471
+ <p class="text-sm">No recent activity</p>
472
+ </div>
473
+ </div>
474
+ </div>
475
+ </div>
476
+
477
+ <!-- Sessions tab content -->
478
+ <div id="content-sessions" class="tab-content hidden">
479
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
480
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
481
+ <div class="flex items-center justify-between">
482
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Session History
483
+ </h2>
484
+ <select id="sessions-codebase-select" onchange="loadSessions(this.value)"
485
+ class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm">
486
+ <option value="">Select codebase...</option>
487
+ </select>
488
+ </div>
489
+ </div>
490
+ <div id="sessions-list"
491
+ class="divide-y divide-gray-200 dark:divide-gray-700 max-h-[calc(100vh-450px)] overflow-y-auto scrollbar-thin">
492
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
493
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24"
494
+ stroke="currentColor">
495
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
496
+ d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
497
+ </svg>
498
+ <p class="mt-2 text-sm">Select a codebase to view sessions</p>
499
+ </div>
500
+ </div>
501
+ <!-- Session detail panel -->
502
+ <div id="session-detail" class="hidden border-t border-gray-200 dark:border-gray-700">
503
+ <div class="p-4">
504
+ <div class="flex items-center justify-between mb-4">
505
+ <h3 id="session-detail-title"
506
+ class="text-sm font-semibold text-gray-900 dark:text-white">Session
507
+ Messages</h3>
508
+ <button onclick="closeSessionDetail()"
509
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
510
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24"
511
+ stroke="currentColor">
512
+ <path stroke-linecap="round" stroke-linejoin="round"
513
+ stroke-width="2" d="M6 18L18 6M6 6l12 12" />
514
+ </svg>
515
+ </button>
516
+ </div>
517
+ <div id="session-messages"
518
+ class="space-y-3 max-h-64 overflow-y-auto scrollbar-thin"></div>
519
+ <div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
520
+ <div class="flex gap-2">
521
+ <input id="session-resume-prompt" type="text"
522
+ placeholder="Continue the conversation..."
523
+ class="flex-1 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white text-sm">
524
+ <button onclick="resumeSelectedSession()"
525
+ class="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500">
526
+ Resume
527
+ </button>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+ </div>
534
+ </div>
535
+
536
+ <!-- Right sidebar - Quick Actions & Workers -->
537
+ <div class="lg:col-span-1 space-y-6">
538
+ <!-- Quick Actions -->
539
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
540
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
541
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-white">Quick Actions</h3>
542
+ </div>
543
+ <div class="p-4 space-y-2">
544
+ <button onclick="openRegisterModal()"
545
+ class="w-full text-left px-3 py-2 rounded-md text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2">
546
+ <span>📁</span> Register Codebase
547
+ </button>
548
+ <button onclick="refreshAll()"
549
+ class="w-full text-left px-3 py-2 rounded-md text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2">
550
+ <span>🔄</span> Refresh All
551
+ </button>
552
+ <button onclick="openCreateTaskModal()"
553
+ class="w-full text-left px-3 py-2 rounded-md text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2">
554
+ <span>📝</span> Create Task
555
+ </button>
556
+ </div>
557
+ </div>
558
+
559
+ <!-- Workers -->
560
+ <div class="rounded-lg bg-white shadow-sm dark:bg-gray-800 dark:ring-1 dark:ring-white/10">
561
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
562
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-white">Connected Workers</h3>
563
+ </div>
564
+ <div id="workers-list" class="p-4">
565
+ <p class="text-xs text-gray-500 dark:text-gray-400 text-center">No workers connected</p>
566
+ </div>
567
+ </div>
568
+ </div>
569
+ </div>
570
+ </div>
571
+ </main>
572
+ </div>
573
+
574
+ <!-- Register Codebase Modal -->
575
+ <div id="register-modal" class="hidden fixed inset-0 z-50">
576
+ <div class="fixed inset-0 bg-gray-500/75 dark:bg-gray-900/75" onclick="closeRegisterModal()"></div>
577
+ <div class="fixed inset-0 z-10 overflow-y-auto">
578
+ <div class="flex min-h-full items-center justify-center p-4">
579
+ <div class="relative w-full max-w-lg rounded-lg bg-white dark:bg-gray-800 shadow-xl">
580
+ <div class="p-6">
581
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Register Codebase</h3>
582
+ <div class="space-y-4">
583
+ <div>
584
+ <label
585
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
586
+ <input id="register-name" type="text" placeholder="my-project"
587
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
588
+ </div>
589
+ <div>
590
+ <label
591
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Path</label>
592
+ <input id="register-path" type="text" placeholder="/home/user/projects/my-project"
593
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
594
+ </div>
595
+ <div>
596
+ <label
597
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description
598
+ (optional)</label>
599
+ <input id="register-description" type="text" placeholder="A brief description"
600
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
601
+ </div>
602
+ <div>
603
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Worker ID
604
+ (optional)</label>
605
+ <input id="register-worker" type="text" placeholder="For remote codebases"
606
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
607
+ </div>
608
+ </div>
609
+ <div class="mt-6 flex gap-3 justify-end">
610
+ <button onclick="closeRegisterModal()"
611
+ class="rounded-md px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Cancel</button>
612
+ <button onclick="registerCodebase()"
613
+ class="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500">Register</button>
614
+ </div>
615
+ </div>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+
621
+ <!-- Create Task Modal -->
622
+ <div id="create-task-modal" class="hidden fixed inset-0 z-50">
623
+ <div class="fixed inset-0 bg-gray-500/75 dark:bg-gray-900/75" onclick="closeCreateTaskModal()"></div>
624
+ <div class="fixed inset-0 z-10 overflow-y-auto">
625
+ <div class="flex min-h-full items-center justify-center p-4">
626
+ <div class="relative w-full max-w-lg rounded-lg bg-white dark:bg-gray-800 shadow-xl">
627
+ <div class="p-6">
628
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Create Task</h3>
629
+ <form id="create-task-form" onsubmit="createTask(event)">
630
+ <div class="space-y-4">
631
+ <div>
632
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
633
+ <input name="title" type="text" placeholder="Task title"
634
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
635
+ </div>
636
+ <div>
637
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description / Prompt</label>
638
+ <textarea name="prompt" rows="4" placeholder="Enter task instructions..."
639
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500 placeholder-gray-400"></textarea>
640
+ </div>
641
+ <div>
642
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Codebase</label>
643
+ <select name="codebase" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
644
+ <option value="">Global (no codebase)</option>
645
+ </select>
646
+ </div>
647
+ <div>
648
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Agent Type</label>
649
+ <select name="agent_type" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
650
+ <option value="build">🔧 Build - Full access agent</option>
651
+ <option value="plan">📋 Plan - Read-only analysis</option>
652
+ <option value="general">💬 General - General purpose</option>
653
+ <option value="explore">🔍 Explore - Codebase search</option>
654
+ </select>
655
+ </div>
656
+ <div>
657
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Priority</label>
658
+ <select name="priority" class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500">
659
+ <option value="0">Normal</option>
660
+ <option value="1">Low</option>
661
+ <option value="3">High</option>
662
+ <option value="4">Urgent</option>
663
+ </select>
664
+ </div>
665
+ </div>
666
+ <div class="mt-6 flex gap-3 justify-end">
667
+ <button type="button" onclick="closeCreateTaskModal()"
668
+ class="rounded-md px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Cancel</button>
669
+ <button type="submit"
670
+ class="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-500">Create Task</button>
671
+ </div>
672
+ </form>
673
+ </div>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </div>
678
+
679
+ <!-- Toast container -->
680
+ <div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
681
+
682
+ <script>
683
+ // State
684
+ const state = {
685
+ serverUrl: 'https://a2a.quantum-forge.net',
686
+ codebases: [],
687
+ tasks: [],
688
+ workers: [],
689
+ models: [],
690
+ defaultModel: '',
691
+ currentOutputCodebase: null,
692
+ agentOutputs: new Map(),
693
+ eventStreams: new Map(),
694
+ darkMode: localStorage.getItem('darkMode') === 'true',
695
+ taskFilter: 'all',
696
+ sessions: [],
697
+ currentSessionCodebase: null,
698
+ selectedSession: null
699
+ };
700
+
701
+ // Initialize
702
+ document.addEventListener('DOMContentLoaded', () => {
703
+ if (state.darkMode) {
704
+ document.documentElement.classList.add('dark');
705
+ }
706
+ init();
707
+ });
708
+
709
+ async function init() {
710
+ await loadModels();
711
+ await loadCodebases();
712
+ await loadTasks();
713
+ await loadWorkers();
714
+ await loadActivity();
715
+ startPolling();
716
+ connectMonitorStream();
717
+ }
718
+
719
+ async function loadModels() {
720
+ try {
721
+ const response = await fetch(`${state.serverUrl}/v1/opencode/models`);
722
+ const data = await response.json();
723
+ state.models = data.models || [];
724
+ state.defaultModel = data.default || '';
725
+ renderModelSelect();
726
+ } catch (error) {
727
+ console.error('Failed to load models:', error);
728
+ // Fallback to basic models
729
+ state.models = [
730
+ { id: 'google/gemini-3-flash-preview', name: 'Gemini 3 Flash', provider: 'Google' },
731
+ { id: 'anthropic/claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'Anthropic' },
732
+ { id: 'openai/gpt-4o', name: 'GPT-4o', provider: 'OpenAI' },
733
+ ];
734
+ renderModelSelect();
735
+ }
736
+ }
737
+
738
+ function renderModelSelect() {
739
+ const select = document.getElementById('trigger-model');
740
+ if (!select) return;
741
+
742
+ // Group models by provider
743
+ const byProvider = {};
744
+ state.models.forEach(m => {
745
+ const provider = m.provider || 'Other';
746
+ if (!byProvider[provider]) byProvider[provider] = [];
747
+ byProvider[provider].push(m);
748
+ });
749
+
750
+ let html = '<option value="">🤖 Default Model</option>';
751
+
752
+ // Custom models first (from config)
753
+ const customModels = state.models.filter(m => m.custom);
754
+ if (customModels.length > 0) {
755
+ html += '<optgroup label="⭐ Custom (from config)">';
756
+ customModels.forEach(m => {
757
+ const selected = m.id === state.defaultModel ? 'selected' : '';
758
+ const badge = m.capabilities?.reasoning ? ' 🧠' : '';
759
+ html += `<option value="${m.id}" ${selected}>${m.name}${badge}</option>`;
760
+ });
761
+ html += '</optgroup>';
762
+ }
763
+
764
+ // Standard providers
765
+ const standardProviders = ['Anthropic', 'OpenAI', 'Google', 'DeepSeek', 'xAI', 'Z.AI Coding Plan', 'Azure AI Foundry'];
766
+ standardProviders.forEach(provider => {
767
+ const models = (byProvider[provider] || []).filter(m => !m.custom);
768
+ if (models.length > 0) {
769
+ html += `<optgroup label="${provider}">`;
770
+ models.forEach(m => {
771
+ html += `<option value="${m.id}">${m.name}</option>`;
772
+ });
773
+ html += '</optgroup>';
774
+ }
775
+ });
776
+
777
+ select.innerHTML = html;
778
+ }
779
+
780
+ function startPolling() {
781
+ setInterval(loadCodebases, 10000);
782
+ setInterval(loadTasks, 5000);
783
+ setInterval(loadWorkers, 30000);
784
+ setInterval(loadActivity, 10000);
785
+ }
786
+
787
+ // Connect to monitor SSE stream for real-time activity
788
+ function connectMonitorStream() {
789
+ const eventSource = new EventSource(`${state.serverUrl}/v1/monitor/stream`);
790
+
791
+ eventSource.onmessage = (e) => {
792
+ try {
793
+ const data = JSON.parse(e.data);
794
+ if (data.type === 'connected') return;
795
+
796
+ // Add to activity feed
797
+ addActivityItem({
798
+ agent_name: data.agent_name || 'System',
799
+ content: data.content || data.message || '',
800
+ type: data.type || 'agent',
801
+ timestamp: data.timestamp || new Date().toISOString(),
802
+ metadata: data.metadata
803
+ });
804
+ } catch (err) {
805
+ console.debug('Monitor stream parse:', e.data);
806
+ }
807
+ };
808
+
809
+ eventSource.onerror = () => {
810
+ console.log('Monitor stream disconnected, reconnecting...');
811
+ setTimeout(connectMonitorStream, 5000);
812
+ };
813
+ }
814
+
815
+ // Load recent activity from API
816
+ async function loadActivity() {
817
+ try {
818
+ const response = await fetch(`${state.serverUrl}/v1/monitor/messages?limit=50`);
819
+ const messages = await response.json();
820
+ renderActivityFeed(messages);
821
+ } catch (error) {
822
+ console.error('Failed to load activity:', error);
823
+ }
824
+ }
825
+
826
+ function renderActivityFeed(messages) {
827
+ const container = document.getElementById('activity-feed');
828
+ if (!messages || messages.length === 0) {
829
+ container.innerHTML = `
830
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
831
+ <p class="text-sm">No recent activity</p>
832
+ </div>
833
+ `;
834
+ return;
835
+ }
836
+
837
+ container.innerHTML = messages.slice(0, 50).map(msg => {
838
+ const typeIcon = {
839
+ 'agent': '🤖',
840
+ 'human': '👤',
841
+ 'system': '⚙️',
842
+ 'tool': '🔧',
843
+ 'error': '❌'
844
+ }[msg.type] || '📝';
845
+
846
+ const content = typeof msg.content === 'string'
847
+ ? msg.content
848
+ : (msg.content?.text || JSON.stringify(msg.content));
849
+
850
+ return `
851
+ <div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50">
852
+ <div class="flex items-start gap-3">
853
+ <span class="text-lg">${typeIcon}</span>
854
+ <div class="min-w-0 flex-1">
855
+ <div class="flex items-center gap-2">
856
+ <span class="text-sm font-medium text-gray-900 dark:text-white">${escapeHtml(msg.agent_name || 'Unknown')}</span>
857
+ <span class="text-xs text-gray-500 dark:text-gray-400">${formatTime(msg.timestamp)}</span>
858
+ </div>
859
+ <p class="text-sm text-gray-600 dark:text-gray-300 mt-1 line-clamp-3">${escapeHtml(content?.substring(0, 300) || '')}</p>
860
+ ${msg.metadata?.conversation_id ? `<span class="text-xs text-gray-400">Conv: ${msg.metadata.conversation_id.substring(0, 8)}...</span>` : ''}
861
+ </div>
862
+ </div>
863
+ </div>
864
+ `;
865
+ }).join('');
866
+ }
867
+
868
+ function addActivityItem(item) {
869
+ const container = document.getElementById('activity-feed');
870
+ const typeIcon = {
871
+ 'agent': '🤖',
872
+ 'human': '👤',
873
+ 'system': '⚙️',
874
+ 'tool': '🔧',
875
+ 'error': '❌'
876
+ }[item.type] || '📝';
877
+
878
+ const content = typeof item.content === 'string'
879
+ ? item.content
880
+ : (item.content?.text || JSON.stringify(item.content));
881
+
882
+ const div = document.createElement('div');
883
+ div.className = 'p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700';
884
+ div.innerHTML = `
885
+ <div class="flex items-start gap-3">
886
+ <span class="text-lg">${typeIcon}</span>
887
+ <div class="min-w-0 flex-1">
888
+ <div class="flex items-center gap-2">
889
+ <span class="text-sm font-medium text-gray-900 dark:text-white">${escapeHtml(item.agent_name)}</span>
890
+ <span class="text-xs text-gray-500 dark:text-gray-400">${formatTime(item.timestamp)}</span>
891
+ </div>
892
+ <p class="text-sm text-gray-600 dark:text-gray-300 mt-1 line-clamp-3">${escapeHtml(content?.substring(0, 300) || '')}</p>
893
+ </div>
894
+ </div>
895
+ `;
896
+
897
+ // Remove "no activity" placeholder
898
+ const placeholder = container.querySelector('.text-center');
899
+ if (placeholder) placeholder.remove();
900
+
901
+ // Insert at top
902
+ container.insertBefore(div, container.firstChild);
903
+
904
+ // Limit items
905
+ while (container.children.length > 100) {
906
+ container.removeChild(container.lastChild);
907
+ }
908
+ }
909
+
910
+ function formatTime(timestamp) {
911
+ if (!timestamp) return '';
912
+ const date = new Date(timestamp);
913
+ return date.toLocaleTimeString();
914
+ }
915
+
916
+ // Dark mode
917
+ function toggleDarkMode() {
918
+ state.darkMode = !state.darkMode;
919
+ localStorage.setItem('darkMode', state.darkMode);
920
+ document.documentElement.classList.toggle('dark');
921
+ }
922
+
923
+ // Mobile sidebar functions
924
+ function openMobileSidebar() {
925
+ const sidebar = document.getElementById('mobile-sidebar');
926
+ const backdrop = document.getElementById('mobile-sidebar-backdrop');
927
+ sidebar.classList.remove('hidden', '-translate-x-full');
928
+ sidebar.classList.add('translate-x-0');
929
+ backdrop.classList.remove('hidden');
930
+ }
931
+
932
+ function closeMobileSidebar() {
933
+ const sidebar = document.getElementById('mobile-sidebar');
934
+ const backdrop = document.getElementById('mobile-sidebar-backdrop');
935
+ sidebar.classList.add('-translate-x-full');
936
+ sidebar.classList.remove('translate-x-0');
937
+ setTimeout(() => {
938
+ sidebar.classList.add('hidden');
939
+ backdrop.classList.add('hidden');
940
+ }, 300);
941
+ }
942
+
943
+ // Tab switching
944
+ const tabTitles = {
945
+ 'codebases': 'Codebases',
946
+ 'tasks': 'Task Queue',
947
+ 'sessions': 'Sessions',
948
+ 'output': 'Agent Output',
949
+ 'activity': 'Activity Feed'
950
+ };
951
+
952
+ function switchTab(tab) {
953
+ // Update desktop tabs
954
+ document.querySelectorAll('.tab-btn').forEach(btn => {
955
+ btn.classList.remove('bg-white/10', 'text-white');
956
+ btn.classList.add('text-primary-100');
957
+ });
958
+ const desktopTab = document.getElementById(`tab-${tab}`);
959
+ if (desktopTab) {
960
+ desktopTab.classList.add('bg-white/10', 'text-white');
961
+ desktopTab.classList.remove('text-primary-100');
962
+ }
963
+
964
+ // Update mobile tabs
965
+ document.querySelectorAll('.mobile-tab-btn').forEach(btn => {
966
+ btn.classList.remove('bg-white/10', 'text-white');
967
+ btn.classList.add('text-primary-100');
968
+ });
969
+ const mobileTab = document.getElementById(`mobile-tab-${tab}`);
970
+ if (mobileTab) {
971
+ mobileTab.classList.add('bg-white/10', 'text-white');
972
+ mobileTab.classList.remove('text-primary-100');
973
+ }
974
+
975
+ // Update page title for mobile
976
+ const pageTitle = document.getElementById('current-page-title');
977
+ if (pageTitle) {
978
+ pageTitle.textContent = tabTitles[tab] || tab;
979
+ }
980
+
981
+ // Show/hide content
982
+ document.querySelectorAll('.tab-content').forEach(content => {
983
+ content.classList.add('hidden');
984
+ });
985
+ document.getElementById(`content-${tab}`).classList.remove('hidden');
986
+ }
987
+
988
+ // API calls
989
+ async function loadCodebases() {
990
+ try {
991
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases`);
992
+ state.codebases = await response.json();
993
+ renderCodebases();
994
+ updateCodebaseSelects();
995
+ updateStats();
996
+ } catch (error) {
997
+ console.error('Failed to load codebases:', error);
998
+ }
999
+ }
1000
+
1001
+ async function loadTasks() {
1002
+ try {
1003
+ const response = await fetch(`${state.serverUrl}/v1/opencode/tasks`);
1004
+ state.tasks = await response.json();
1005
+ renderTasks();
1006
+ updateStats();
1007
+ } catch (error) {
1008
+ console.error('Failed to load tasks:', error);
1009
+ }
1010
+ }
1011
+
1012
+ async function loadWorkers() {
1013
+ try {
1014
+ const response = await fetch(`${state.serverUrl}/v1/opencode/workers`);
1015
+ if (response.ok) {
1016
+ state.workers = await response.json();
1017
+ renderWorkers();
1018
+ }
1019
+ } catch (error) {
1020
+ console.error('Failed to load workers:', error);
1021
+ }
1022
+ }
1023
+
1024
+ // Render functions
1025
+ function getPriorityBadge(priority) {
1026
+ switch (priority) {
1027
+ case 4: return '<span class="px-2 py-1 text-xs rounded-full bg-red-100 text-red-800">Urgent</span>';
1028
+ case 3: return '<span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-800">High</span>';
1029
+ case 1: return '<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">Low</span>';
1030
+ default: return '<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">Normal</span>';
1031
+ }
1032
+ }
1033
+
1034
+ function renderCodebases() {
1035
+ const container = document.getElementById('codebases-list');
1036
+ if (state.codebases.length === 0) {
1037
+ container.innerHTML = `
1038
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
1039
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1040
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
1041
+ </svg>
1042
+ <p class="mt-2 text-sm">No codebases registered</p>
1043
+ </div>
1044
+ `;
1045
+ return;
1046
+ }
1047
+
1048
+ container.innerHTML = state.codebases.map(cb => `
1049
+ <div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer" onclick="selectCodebase('${cb.id}')">
1050
+ <div class="flex items-start justify-between">
1051
+ <div class="min-w-0 flex-1">
1052
+ <p class="text-sm font-medium text-gray-900 dark:text-white truncate">${escapeHtml(cb.name)}</p>
1053
+ <p class="text-xs text-gray-500 dark:text-gray-400 truncate">${escapeHtml(cb.path)}</p>
1054
+ </div>
1055
+ <span class="ml-2 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${getStatusClasses(cb.status)}">
1056
+ ${cb.status}
1057
+ </span>
1058
+ </div>
1059
+ ${cb.worker_id ? `<p class="mt-1 text-xs text-gray-400">Worker: ${cb.worker_id}</p>` : ''}
1060
+ <div class="mt-2 flex gap-2">
1061
+ <button onclick="event.stopPropagation(); startWatchMode('${cb.id}')" class="text-xs text-primary-600 dark:text-primary-400 hover:underline">
1062
+ ${cb.status === 'watching' ? '⏹️ Stop' : '👁️ Watch'}
1063
+ </button>
1064
+ <button onclick="event.stopPropagation(); deleteCodebase('${cb.id}')" class="text-xs text-red-600 dark:text-red-400 hover:underline">
1065
+ 🗑️ Delete
1066
+ </button>
1067
+ </div>
1068
+ </div>
1069
+ `).join('');
1070
+ }
1071
+
1072
+ function renderTasks() {
1073
+ const container = document.getElementById('tasks-list');
1074
+ let filteredTasks = state.tasks;
1075
+
1076
+ if (state.taskFilter !== 'all') {
1077
+ filteredTasks = state.tasks.filter(t => t.status === state.taskFilter);
1078
+ }
1079
+
1080
+ if (filteredTasks.length === 0) {
1081
+ container.innerHTML = `
1082
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
1083
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1084
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
1085
+ </svg>
1086
+ <p class="mt-2 text-sm">No tasks found</p>
1087
+ </div>
1088
+ `;
1089
+ return;
1090
+ }
1091
+
1092
+ container.innerHTML = filteredTasks.map(task => `
1093
+ <div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50">
1094
+ <div class="flex items-start justify-between">
1095
+ <div class="min-w-0 flex-1">
1096
+ <p class="text-sm font-medium text-gray-900 dark:text-white">${escapeHtml(task.title || task.prompt?.substring(0, 50) || 'Untitled')}</p>
1097
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">${task.agent_type} • ${new Date(task.created_at).toLocaleTimeString()}</p>
1098
+ </div>
1099
+ <div class="flex flex-col items-end gap-2">
1100
+ ${getPriorityBadge(task.priority)}
1101
+ <span class="ml-2 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${getStatusClasses(task.status)}">
1102
+ ${task.status}
1103
+ </span>
1104
+ </div>
1105
+ </div>
1106
+ ${task.result ? `
1107
+ <button onclick="viewTaskResult('${task.id}')" class="mt-2 text-xs text-primary-600 dark:text-primary-400 hover:underline">
1108
+ View Result
1109
+ </button>
1110
+ ` : ''}
1111
+ </div>
1112
+ `).join('');
1113
+ }
1114
+
1115
+ function renderWorkers() {
1116
+ const container = document.getElementById('workers-list');
1117
+ if (state.workers.length === 0) {
1118
+ container.innerHTML = `<p class="text-xs text-gray-500 dark:text-gray-400 text-center">No workers connected</p>`;
1119
+ return;
1120
+ }
1121
+
1122
+ container.innerHTML = state.workers.map(w => `
1123
+ <div class="flex items-center justify-between py-2">
1124
+ <div>
1125
+ <p class="text-sm font-medium text-gray-900 dark:text-white">${escapeHtml(w.name)}</p>
1126
+ <p class="text-xs text-gray-500 dark:text-gray-400">${w.id?.substring(0, 8)}</p>
1127
+ </div>
1128
+ <span class="h-2 w-2 rounded-full bg-green-400"></span>
1129
+ </div>
1130
+ `).join('');
1131
+ }
1132
+
1133
+ function updateStats() {
1134
+ document.getElementById('stat-codebases').textContent = state.codebases.length;
1135
+ document.getElementById('stat-tasks').textContent = state.tasks.length;
1136
+ document.getElementById('stat-completed').textContent = state.tasks.filter(t => t.status === 'completed').length;
1137
+ document.getElementById('stat-running').textContent = state.tasks.filter(t => t.status === 'running').length;
1138
+ document.getElementById('total-tasks-count').textContent = state.tasks.length;
1139
+ document.getElementById('active-agents-count').textContent = state.codebases.filter(c => c.status === 'running' || c.status === 'watching').length;
1140
+ }
1141
+
1142
+ function updateCodebaseSelects() {
1143
+ const options = state.codebases.map(cb => `<option value="${cb.id}">${escapeHtml(cb.name)}</option>`).join('');
1144
+ document.getElementById('trigger-codebase').innerHTML = `<option value="">Select a codebase...</option>${options}`;
1145
+ document.getElementById('output-codebase-select').innerHTML = `<option value="">Select codebase...</option>${options}`;
1146
+ document.getElementById('sessions-codebase-select').innerHTML = `<option value="">Select codebase...</option>${options}`;
1147
+ }
1148
+
1149
+ // ========================================
1150
+ // Session Management
1151
+ // ========================================
1152
+
1153
+ async function loadSessions(codebaseId) {
1154
+ if (!codebaseId) {
1155
+ state.sessions = [];
1156
+ state.currentSessionCodebase = null;
1157
+ renderSessions();
1158
+ return;
1159
+ }
1160
+
1161
+ state.currentSessionCodebase = codebaseId;
1162
+
1163
+ try {
1164
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases/${codebaseId}/sessions`);
1165
+ const data = await response.json();
1166
+ state.sessions = data.sessions || [];
1167
+ renderSessions();
1168
+ } catch (error) {
1169
+ console.error('Failed to load sessions:', error);
1170
+ showToast('Failed to load sessions', 'error');
1171
+ }
1172
+ }
1173
+
1174
+ function renderSessions() {
1175
+ const container = document.getElementById('sessions-list');
1176
+
1177
+ if (state.sessions.length === 0) {
1178
+ container.innerHTML = `
1179
+ <div class="p-8 text-center text-gray-500 dark:text-gray-400">
1180
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1181
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
1182
+ d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
1183
+ </svg>
1184
+ <p class="mt-2 text-sm">${state.currentSessionCodebase ? 'No sessions found' : 'Select a codebase to view sessions'}</p>
1185
+ </div>
1186
+ `;
1187
+ return;
1188
+ }
1189
+
1190
+ container.innerHTML = state.sessions.map(session => `
1191
+ <div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer" onclick="selectSession('${session.id}')">
1192
+ <div class="flex items-start justify-between">
1193
+ <div class="min-w-0 flex-1">
1194
+ <p class="text-sm font-medium text-gray-900 dark:text-white truncate">
1195
+ ${escapeHtml(session.title || 'Untitled Session')}
1196
+ </p>
1197
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
1198
+ ${session.agent || 'build'} • ${session.messageCount || 0} messages
1199
+ </p>
1200
+ <p class="text-xs text-gray-400 dark:text-gray-500">
1201
+ ${session.updated ? formatDate(session.updated) : formatDate(session.created)}
1202
+ </p>
1203
+ </div>
1204
+ <button onclick="event.stopPropagation(); quickResumeSession('${session.id}')"
1205
+ class="ml-2 text-xs text-primary-600 dark:text-primary-400 hover:underline">
1206
+ ▶️ Resume
1207
+ </button>
1208
+ </div>
1209
+ </div>
1210
+ `).join('');
1211
+ }
1212
+
1213
+ async function selectSession(sessionId) {
1214
+ state.selectedSession = sessionId;
1215
+
1216
+ // Load session messages
1217
+ try {
1218
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases/${state.currentSessionCodebase}/sessions/${sessionId}/messages`);
1219
+ const data = await response.json();
1220
+ renderSessionMessages(data.messages || []);
1221
+
1222
+ // Show detail panel
1223
+ document.getElementById('session-detail').classList.remove('hidden');
1224
+ document.getElementById('session-detail-title').textContent = `Session ${sessionId.substring(0, 8)}...`;
1225
+ } catch (error) {
1226
+ console.error('Failed to load session messages:', error);
1227
+ showToast('Failed to load session messages', 'error');
1228
+ }
1229
+ }
1230
+
1231
+ function renderSessionMessages(messages) {
1232
+ const container = document.getElementById('session-messages');
1233
+
1234
+ if (messages.length === 0) {
1235
+ container.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No messages in this session</p>';
1236
+ return;
1237
+ }
1238
+
1239
+ container.innerHTML = messages.slice(-20).map(msg => {
1240
+ const info = msg.info || msg;
1241
+ const parts = msg.parts || [];
1242
+ const role = info.role || 'unknown';
1243
+ const isUser = role === 'user' || role === 'human';
1244
+
1245
+ // Extract text content from parts
1246
+ let content = '';
1247
+ for (const part of parts) {
1248
+ if (part.type === 'text' && part.text) {
1249
+ content += part.text;
1250
+ }
1251
+ }
1252
+ if (!content && info.content) {
1253
+ content = typeof info.content === 'string' ? info.content : JSON.stringify(info.content);
1254
+ }
1255
+
1256
+ return `
1257
+ <div class="p-2 rounded-lg ${isUser ? 'bg-primary-50 dark:bg-primary-900/20' : 'bg-gray-50 dark:bg-gray-700/50'}">
1258
+ <div class="flex items-center gap-2 mb-1">
1259
+ <span class="text-xs font-medium ${isUser ? 'text-primary-700 dark:text-primary-300' : 'text-gray-700 dark:text-gray-300'}">
1260
+ ${isUser ? '👤 User' : '🤖 Assistant'}
1261
+ </span>
1262
+ ${info.model ? `<span class="text-xs text-gray-400">${info.model}</span>` : ''}
1263
+ </div>
1264
+ <p class="text-sm text-gray-800 dark:text-gray-200 line-clamp-4">${escapeHtml(content.substring(0, 500))}</p>
1265
+ </div>
1266
+ `;
1267
+ }).join('');
1268
+ }
1269
+
1270
+ function closeSessionDetail() {
1271
+ document.getElementById('session-detail').classList.add('hidden');
1272
+ state.selectedSession = null;
1273
+ }
1274
+
1275
+ async function resumeSelectedSession() {
1276
+ if (!state.selectedSession || !state.currentSessionCodebase) {
1277
+ showToast('No session selected', 'error');
1278
+ return;
1279
+ }
1280
+
1281
+ const prompt = document.getElementById('session-resume-prompt').value;
1282
+
1283
+ try {
1284
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases/${state.currentSessionCodebase}/sessions/${state.selectedSession}/resume`, {
1285
+ method: 'POST',
1286
+ headers: { 'Content-Type': 'application/json' },
1287
+ body: JSON.stringify({
1288
+ prompt: prompt || null,
1289
+ agent: 'build'
1290
+ })
1291
+ });
1292
+
1293
+ const result = await response.json();
1294
+ if (result.success) {
1295
+ showToast('Session resumed!', 'success');
1296
+ document.getElementById('session-resume-prompt').value = '';
1297
+ closeSessionDetail();
1298
+
1299
+ // Switch to output tab if there's new output
1300
+ if (result.task_id) {
1301
+ await loadTasks();
1302
+ switchTab('tasks');
1303
+ }
1304
+ } else {
1305
+ showToast(result.error || 'Failed to resume session', 'error');
1306
+ }
1307
+ } catch (error) {
1308
+ console.error('Failed to resume session:', error);
1309
+ showToast('Failed to resume session', 'error');
1310
+ }
1311
+ }
1312
+
1313
+ async function quickResumeSession(sessionId) {
1314
+ if (!state.currentSessionCodebase) return;
1315
+
1316
+ try {
1317
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases/${state.currentSessionCodebase}/sessions/${sessionId}/resume`, {
1318
+ method: 'POST',
1319
+ headers: { 'Content-Type': 'application/json' },
1320
+ body: JSON.stringify({ agent: 'build' })
1321
+ });
1322
+
1323
+ const result = await response.json();
1324
+ if (result.success) {
1325
+ showToast('Session resumed!', 'success');
1326
+ if (result.task_id) {
1327
+ await loadTasks();
1328
+ switchTab('tasks');
1329
+ }
1330
+ } else {
1331
+ showToast(result.error || 'Failed to resume session', 'error');
1332
+ }
1333
+ } catch (error) {
1334
+ showToast('Failed to resume session', 'error');
1335
+ }
1336
+ }
1337
+
1338
+ function formatDate(dateStr) {
1339
+ if (!dateStr) return '';
1340
+ const date = new Date(dateStr);
1341
+ const now = new Date();
1342
+ const diff = now - date;
1343
+
1344
+ if (diff < 60000) return 'Just now';
1345
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
1346
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
1347
+ if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
1348
+ return date.toLocaleDateString();
1349
+ }
1350
+
1351
+ // Actions
1352
+ async function triggerAgent() {
1353
+ const codebaseId = document.getElementById('trigger-codebase').value;
1354
+ const agent = document.getElementById('trigger-agent').value;
1355
+ const model = document.getElementById('trigger-model').value;
1356
+ const prompt = document.getElementById('trigger-prompt').value;
1357
+
1358
+ if (!codebaseId) {
1359
+ showToast('Please select a codebase', 'error');
1360
+ return;
1361
+ }
1362
+ if (!prompt.trim()) {
1363
+ showToast('Please enter a prompt', 'error');
1364
+ return;
1365
+ }
1366
+
1367
+ try {
1368
+ const payload = { prompt, agent };
1369
+ if (model) payload.model = model;
1370
+
1371
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases/${codebaseId}/trigger`, {
1372
+ method: 'POST',
1373
+ headers: { 'Content-Type': 'application/json' },
1374
+ body: JSON.stringify(payload)
1375
+ });
1376
+
1377
+ const result = await response.json();
1378
+ if (result.success) {
1379
+ showToast('Agent triggered successfully!', 'success');
1380
+ document.getElementById('trigger-prompt').value = '';
1381
+ await loadTasks();
1382
+ switchTab('tasks');
1383
+ } else {
1384
+ showToast(result.error || 'Failed to trigger agent', 'error');
1385
+ }
1386
+ } catch (error) {
1387
+ showToast('Failed to trigger agent', 'error');
1388
+ }
1389
+ }
1390
+
1391
+ async function registerCodebase() {
1392
+ const name = document.getElementById('register-name').value;
1393
+ const path = document.getElementById('register-path').value;
1394
+ const description = document.getElementById('register-description').value;
1395
+ const workerId = document.getElementById('register-worker').value;
1396
+
1397
+ if (!name || !path) {
1398
+ showToast('Name and path are required', 'error');
1399
+ return;
1400
+ }
1401
+
1402
+ try {
1403
+ const payload = { name, path };
1404
+ if (description) payload.description = description;
1405
+ if (workerId) payload.worker_id = workerId;
1406
+
1407
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases`, {
1408
+ method: 'POST',
1409
+ headers: { 'Content-Type': 'application/json' },
1410
+ body: JSON.stringify(payload)
1411
+ });
1412
+
1413
+ if (response.ok) {
1414
+ showToast('Codebase registered!', 'success');
1415
+ closeRegisterModal();
1416
+ await loadCodebases();
1417
+ } else {
1418
+ const error = await response.json();
1419
+ showToast(error.detail || 'Failed to register', 'error');
1420
+ }
1421
+ } catch (error) {
1422
+ showToast('Failed to register codebase', 'error');
1423
+ }
1424
+ }
1425
+
1426
+ async function deleteCodebase(id) {
1427
+ if (!confirm('Delete this codebase?')) return;
1428
+ try {
1429
+ await fetch(`${state.serverUrl}/v1/opencode/codebases/${id}`, { method: 'DELETE' });
1430
+ showToast('Codebase deleted', 'success');
1431
+ await loadCodebases();
1432
+ } catch (error) {
1433
+ showToast('Failed to delete', 'error');
1434
+ }
1435
+ }
1436
+
1437
+ async function startWatchMode(id) {
1438
+ const codebase = state.codebases.find(c => c.id === id);
1439
+ const endpoint = codebase?.status === 'watching' ? 'stop' : 'start';
1440
+ try {
1441
+ await fetch(`${state.serverUrl}/v1/opencode/codebases/${id}/watch/${endpoint}`, { method: 'POST' });
1442
+ showToast(`Watch mode ${endpoint}ed`, 'success');
1443
+ await loadCodebases();
1444
+ } catch (error) {
1445
+ showToast('Failed to toggle watch mode', 'error');
1446
+ }
1447
+ }
1448
+
1449
+ function selectCodebase(id) {
1450
+ document.getElementById('trigger-codebase').value = id;
1451
+ showToast('Codebase selected', 'info');
1452
+ }
1453
+
1454
+ function selectOutputCodebase(id) {
1455
+ state.currentOutputCodebase = id;
1456
+ if (id) {
1457
+ connectEventStream(id);
1458
+ }
1459
+ }
1460
+
1461
+ function connectEventStream(codebaseId) {
1462
+ if (state.eventStreams.has(codebaseId)) {
1463
+ state.eventStreams.get(codebaseId).close();
1464
+ }
1465
+
1466
+ const container = document.getElementById('output-container');
1467
+ container.innerHTML = '<div class="text-gray-500 dark:text-gray-400">Connecting to event stream...</div>';
1468
+
1469
+ const eventSource = new EventSource(`${state.serverUrl}/v1/opencode/codebases/${codebaseId}/events`);
1470
+ state.eventStreams.set(codebaseId, eventSource);
1471
+
1472
+ eventSource.onopen = () => {
1473
+ container.innerHTML = '';
1474
+ };
1475
+
1476
+ eventSource.addEventListener('connected', (e) => {
1477
+ addOutput('status', 'Connected to agent stream');
1478
+ });
1479
+
1480
+ eventSource.addEventListener('message', (e) => {
1481
+ try {
1482
+ const data = JSON.parse(e.data);
1483
+
1484
+ // Handle different message formats
1485
+ if (data.type === 'text') {
1486
+ const content = data.content || data.part?.text || '';
1487
+ if (typeof content === 'string' && content.includes('\n')) {
1488
+ // Try to parse as newline-delimited JSON
1489
+ const lines = content.split('\n').filter(l => l.trim());
1490
+ for (const line of lines) {
1491
+ try {
1492
+ const event = JSON.parse(line);
1493
+ processEvent(event);
1494
+ } catch {
1495
+ addOutput('text', line);
1496
+ }
1497
+ }
1498
+ } else {
1499
+ processEvent(data);
1500
+ }
1501
+ } else {
1502
+ processEvent(data);
1503
+ }
1504
+ } catch (err) {
1505
+ // Not JSON, just display as text
1506
+ if (e.data && e.data.trim()) {
1507
+ addOutput('text', e.data);
1508
+ }
1509
+ }
1510
+ });
1511
+
1512
+ eventSource.addEventListener('status', (e) => {
1513
+ const data = JSON.parse(e.data);
1514
+ addOutput('status', data.message || data.status);
1515
+ });
1516
+
1517
+ eventSource.onerror = () => {
1518
+ addOutput('error', 'Event stream disconnected');
1519
+ };
1520
+ }
1521
+
1522
+ function processEvent(event) {
1523
+ if (!event) return;
1524
+
1525
+ const type = event.type || event.event_type;
1526
+ const part = event.part || {};
1527
+
1528
+ // Handle content that may be string or object
1529
+ const getTextContent = (obj) => {
1530
+ if (!obj) return '';
1531
+ if (typeof obj === 'string') return obj;
1532
+ if (obj.text) return obj.text;
1533
+ if (obj.content) {
1534
+ if (typeof obj.content === 'string') return obj.content;
1535
+ if (Array.isArray(obj.content)) return obj.content.map(c => c.text || c).join('');
1536
+ }
1537
+ return '';
1538
+ };
1539
+
1540
+ switch (type) {
1541
+ case 'text':
1542
+ case 'part.text':
1543
+ const text = getTextContent(part) || getTextContent(event);
1544
+ if (text) {
1545
+ // Check if text is pure JSON that should be processed as an event
1546
+ if (text.startsWith('{') && text.includes('"type"')) {
1547
+ try {
1548
+ const embedded = JSON.parse(text);
1549
+ if (embedded.type && embedded.type !== 'text') {
1550
+ processEvent(embedded);
1551
+ break;
1552
+ }
1553
+ } catch {
1554
+ // Not valid JSON, continue to display as text
1555
+ }
1556
+ }
1557
+ // Check for concatenated content with embedded JSON
1558
+ const jsonStartIdx = text.indexOf('{"type":');
1559
+ if (jsonStartIdx > 0) {
1560
+ // Has text before JSON - output the text part
1561
+ const textPart = text.substring(0, jsonStartIdx).trim();
1562
+ if (textPart) addOutput('text', textPart);
1563
+ // Try to process the JSON part
1564
+ const jsonPart = text.substring(jsonStartIdx);
1565
+ try {
1566
+ const embedded = JSON.parse(jsonPart);
1567
+ if (embedded.type) processEvent(embedded);
1568
+ } catch {
1569
+ // Invalid JSON, skip the raw JSON display
1570
+ }
1571
+ } else {
1572
+ addOutput('text', text);
1573
+ }
1574
+ }
1575
+ break;
1576
+ case 'tool_use':
1577
+ case 'part.tool':
1578
+ const state_info = part.state || event.state || {};
1579
+ const toolName = part.tool || part.tool_name || event.tool_name || 'Tool';
1580
+ const toolOutput = state_info.output || '';
1581
+ const toolTitle = state_info.title || '';
1582
+ addOutput('tool', `Tool: ${toolName}${toolTitle ? ' - ' + toolTitle : ''}\n${toolOutput}`);
1583
+ break;
1584
+ case 'step_start':
1585
+ case 'part.step-start':
1586
+ addOutput('status', '--- Step started ---');
1587
+ break;
1588
+ case 'step_finish':
1589
+ case 'part.step-finish':
1590
+ const tokens = part.tokens || event.tokens;
1591
+ if (tokens) {
1592
+ const total = (tokens.input || 0) + (tokens.output || 0);
1593
+ addOutput('status', `Step finished (${total} tokens)`);
1594
+ } else {
1595
+ addOutput('status', '--- Step finished ---');
1596
+ }
1597
+ break;
1598
+ case 'message':
1599
+ case 'message.updated':
1600
+ // Handle full message updates
1601
+ const msgContent = getTextContent(event);
1602
+ if (msgContent) addOutput('text', msgContent);
1603
+ break;
1604
+ case 'status':
1605
+ case 'session.status':
1606
+ addOutput('status', event.status || event.message || 'Status update');
1607
+ break;
1608
+ case 'error':
1609
+ addOutput('error', event.error || event.message || 'Error occurred');
1610
+ break;
1611
+ default:
1612
+ // Try to extract any text content
1613
+ const fallbackText = getTextContent(event) || getTextContent(part);
1614
+ if (fallbackText) {
1615
+ addOutput('text', fallbackText);
1616
+ }
1617
+ }
1618
+ }
1619
+
1620
+ function addOutput(type, content) {
1621
+ const container = document.getElementById('output-container');
1622
+ const colors = {
1623
+ text: 'text-gray-900 dark:text-gray-100',
1624
+ status: 'text-blue-600 dark:text-blue-400',
1625
+ error: 'text-red-600 dark:text-red-400',
1626
+ tool: 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20 p-2 rounded'
1627
+ };
1628
+ const div = document.createElement('div');
1629
+ div.className = `${colors[type] || colors.text} mb-2 whitespace-pre-wrap`;
1630
+ div.textContent = content;
1631
+ container.appendChild(div);
1632
+ container.scrollTop = container.scrollHeight;
1633
+ }
1634
+
1635
+ function viewTaskResult(taskId) {
1636
+ const task = state.tasks.find(t => t.id === taskId);
1637
+ if (task?.result) {
1638
+ // Try to parse and format the result
1639
+ try {
1640
+ const lines = task.result.split('\n').filter(l => l.trim());
1641
+ let output = [];
1642
+ for (const line of lines) {
1643
+ try {
1644
+ const event = JSON.parse(line);
1645
+ if (event.type === 'text' && event.part?.text) {
1646
+ output.push(event.part.text);
1647
+ } else if (event.type === 'tool_use') {
1648
+ output.push(`[Tool: ${event.part?.tool}] ${event.part?.state?.output || ''}`);
1649
+ }
1650
+ } catch {
1651
+ output.push(line);
1652
+ }
1653
+ }
1654
+ alert(output.join('\n\n') || task.result);
1655
+ } catch {
1656
+ alert(task.result);
1657
+ }
1658
+ }
1659
+ }
1660
+
1661
+ function filterTasks(filter) {
1662
+ state.taskFilter = filter;
1663
+ document.querySelectorAll('.task-filter-btn').forEach(btn => {
1664
+ if (btn.dataset.filter === filter) {
1665
+ btn.className = 'task-filter-btn rounded-md px-3 py-1 text-xs font-medium bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300';
1666
+ } else {
1667
+ btn.className = 'task-filter-btn rounded-md px-3 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700';
1668
+ }
1669
+ });
1670
+ renderTasks();
1671
+ }
1672
+
1673
+ function refreshAll() {
1674
+ loadCodebases();
1675
+ loadTasks();
1676
+ loadWorkers();
1677
+ showToast('Refreshed', 'success');
1678
+ }
1679
+
1680
+ // Modals
1681
+ function openRegisterModal() {
1682
+ document.getElementById('register-modal').classList.remove('hidden');
1683
+ }
1684
+
1685
+ function closeRegisterModal() {
1686
+ document.getElementById('register-modal').classList.add('hidden');
1687
+ document.getElementById('register-name').value = '';
1688
+ document.getElementById('register-path').value = '';
1689
+ document.getElementById('register-description').value = '';
1690
+ document.getElementById('register-worker').value = '';
1691
+ }
1692
+
1693
+ function openSettingsModal() {
1694
+ showToast('Settings coming soon', 'info');
1695
+ }
1696
+
1697
+ function openCreateTaskModal() {
1698
+ const modal = document.getElementById('create-task-modal');
1699
+ const codebaseSelect = modal.querySelector('select[name="codebase"]');
1700
+ codebaseSelect.innerHTML = '<option value="">Global (no codebase)</option>' +
1701
+ state.codebases.map(cb => `<option value="${cb.id}">${escapeHtml(cb.name)}</option>`).join('');
1702
+ modal.classList.remove('hidden');
1703
+ }
1704
+
1705
+ function closeCreateTaskModal() {
1706
+ document.getElementById('create-task-modal').classList.add('hidden');
1707
+ document.getElementById('create-task-form').reset();
1708
+ }
1709
+
1710
+ async function createTask(e) {
1711
+ e.preventDefault();
1712
+ const form = e.target;
1713
+ const payload = {
1714
+ title: form.title.value,
1715
+ prompt: form.prompt.value,
1716
+ codebase_id: form.codebase.value || 'global',
1717
+ agent_type: form.agent_type.value || 'build',
1718
+ priority: parseInt(form.priority.value) || 0
1719
+ };
1720
+
1721
+ if (!payload.title || !payload.prompt) {
1722
+ showToast('Title and prompt are required', 'error');
1723
+ return;
1724
+ }
1725
+
1726
+ try {
1727
+ const codebaseId = form.codebase.value || 'global';
1728
+ const response = await fetch(`${state.serverUrl}/v1/opencode/codebases/${codebaseId}/tasks`, {
1729
+ method: 'POST',
1730
+ headers: { 'Content-Type': 'application/json' },
1731
+ body: JSON.stringify(payload)
1732
+ });
1733
+
1734
+ if (response.ok) {
1735
+ showToast('Task created successfully!', 'success');
1736
+ closeCreateTaskModal();
1737
+ await loadTasks();
1738
+ switchTab('tasks');
1739
+ } else {
1740
+ const error = await response.json();
1741
+ showToast(error.detail || 'Failed to create task', 'error');
1742
+ }
1743
+ } catch (error) {
1744
+ console.error('Failed to create task:', error);
1745
+ showToast('Failed to create task', 'error');
1746
+ }
1747
+ }
1748
+
1749
+ // Helpers
1750
+ function getStatusClasses(status) {
1751
+ const classes = {
1752
+ idle: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
1753
+ running: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
1754
+ watching: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300',
1755
+ completed: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
1756
+ failed: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
1757
+ pending: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
1758
+ error: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'
1759
+ };
1760
+ return classes[status] || classes.idle;
1761
+ }
1762
+
1763
+ function escapeHtml(text) {
1764
+ if (!text) return '';
1765
+ const div = document.createElement('div');
1766
+ div.textContent = text;
1767
+ return div.innerHTML;
1768
+ }
1769
+
1770
+ function showToast(message, type = 'info') {
1771
+ const container = document.getElementById('toast-container');
1772
+ const colors = {
1773
+ success: 'bg-green-500',
1774
+ error: 'bg-red-500',
1775
+ info: 'bg-blue-500'
1776
+ };
1777
+ const toast = document.createElement('div');
1778
+ toast.className = `${colors[type]} text-white px-4 py-2 rounded-lg shadow-lg text-sm transform transition-all duration-300`;
1779
+ toast.textContent = message;
1780
+ container.appendChild(toast);
1781
+
1782
+ setTimeout(() => {
1783
+ toast.classList.add('opacity-0', 'translate-x-4');
1784
+ setTimeout(() => toast.remove(), 300);
1785
+ }, 3000);
1786
+ }
1787
+ </script>
1788
+ </body>
1789
+
1790
+ </html>