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.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- ui/monitor.js +2662 -0
ui/monitor-tailwind.html
ADDED
|
@@ -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>
|