@versdotsh/reef 0.1.2
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.
- package/.github/workflows/test.yml +47 -0
- package/README.md +257 -0
- package/bun.lock +587 -0
- package/examples/services/board/board.test.ts +215 -0
- package/examples/services/board/index.ts +155 -0
- package/examples/services/board/routes.ts +335 -0
- package/examples/services/board/store.ts +329 -0
- package/examples/services/board/tools.ts +214 -0
- package/examples/services/commits/commits.test.ts +74 -0
- package/examples/services/commits/index.ts +14 -0
- package/examples/services/commits/routes.ts +43 -0
- package/examples/services/commits/store.ts +114 -0
- package/examples/services/feed/behaviors.ts +23 -0
- package/examples/services/feed/feed.test.ts +101 -0
- package/examples/services/feed/index.ts +117 -0
- package/examples/services/feed/routes.ts +224 -0
- package/examples/services/feed/store.ts +194 -0
- package/examples/services/feed/tools.ts +83 -0
- package/examples/services/journal/index.ts +15 -0
- package/examples/services/journal/journal.test.ts +57 -0
- package/examples/services/journal/routes.ts +45 -0
- package/examples/services/journal/store.ts +119 -0
- package/examples/services/journal/tools.ts +32 -0
- package/examples/services/log/index.ts +15 -0
- package/examples/services/log/log.test.ts +70 -0
- package/examples/services/log/routes.ts +44 -0
- package/examples/services/log/store.ts +105 -0
- package/examples/services/log/tools.ts +57 -0
- package/examples/services/registry/behaviors.ts +128 -0
- package/examples/services/registry/index.ts +37 -0
- package/examples/services/registry/registry.test.ts +135 -0
- package/examples/services/registry/routes.ts +76 -0
- package/examples/services/registry/store.ts +224 -0
- package/examples/services/registry/tools.ts +116 -0
- package/examples/services/reports/index.ts +14 -0
- package/examples/services/reports/reports.test.ts +75 -0
- package/examples/services/reports/routes.ts +42 -0
- package/examples/services/reports/store.ts +110 -0
- package/examples/services/ui/auth.ts +61 -0
- package/examples/services/ui/index.ts +16 -0
- package/examples/services/ui/routes.ts +160 -0
- package/examples/services/ui/static/app.js +369 -0
- package/examples/services/ui/static/index.html +42 -0
- package/examples/services/ui/static/style.css +157 -0
- package/examples/services/usage/behaviors.ts +166 -0
- package/examples/services/usage/index.ts +19 -0
- package/examples/services/usage/routes.ts +53 -0
- package/examples/services/usage/store.ts +341 -0
- package/examples/services/usage/tools.ts +75 -0
- package/examples/services/usage/usage.test.ts +91 -0
- package/package.json +29 -0
- package/services/agent/index.ts +465 -0
- package/services/board/index.ts +155 -0
- package/services/board/routes.ts +335 -0
- package/services/board/store.ts +329 -0
- package/services/board/tools.ts +214 -0
- package/services/docs/index.ts +391 -0
- package/services/feed/behaviors.ts +23 -0
- package/services/feed/index.ts +117 -0
- package/services/feed/routes.ts +224 -0
- package/services/feed/store.ts +194 -0
- package/services/feed/tools.ts +83 -0
- package/services/installer/index.ts +574 -0
- package/services/services/index.ts +165 -0
- package/services/ui/auth.ts +61 -0
- package/services/ui/index.ts +16 -0
- package/services/ui/routes.ts +160 -0
- package/services/ui/static/app.js +369 -0
- package/services/ui/static/index.html +42 -0
- package/services/ui/static/style.css +157 -0
- package/skills/create-service/SKILL.md +698 -0
- package/src/core/auth.ts +28 -0
- package/src/core/client.ts +99 -0
- package/src/core/discover.ts +152 -0
- package/src/core/events.ts +44 -0
- package/src/core/extension.ts +66 -0
- package/src/core/server.ts +262 -0
- package/src/core/testing.ts +155 -0
- package/src/core/types.ts +194 -0
- package/src/extension.ts +16 -0
- package/src/main.ts +11 -0
- package/tests/server.test.ts +1338 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// reef UI — dynamic panel discovery + built-in chat
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
const API = PANEL_API; // set in index.html
|
|
6
|
+
|
|
7
|
+
function esc(s) {
|
|
8
|
+
const d = document.createElement('div');
|
|
9
|
+
d.textContent = s || '';
|
|
10
|
+
return d.innerHTML;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function timeAgo(iso) {
|
|
14
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
15
|
+
if (ms < 60000) return `${Math.floor(ms / 1000)}s ago`;
|
|
16
|
+
if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`;
|
|
17
|
+
if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`;
|
|
18
|
+
return `${Math.floor(ms / 86400000)}d ago`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Panel discovery
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
const tabsEl = document.getElementById('tabs');
|
|
26
|
+
const panelsEl = document.getElementById('panels');
|
|
27
|
+
const statusEl = document.getElementById('status');
|
|
28
|
+
let activeTab = null;
|
|
29
|
+
const loadedPanels = new Map(); // name → container element
|
|
30
|
+
|
|
31
|
+
async function discoverPanels() {
|
|
32
|
+
try {
|
|
33
|
+
// Get loaded services
|
|
34
|
+
const res = await fetch(`${API}/services`);
|
|
35
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
const services = data.modules || data.services || (Array.isArray(data) ? data : []);
|
|
38
|
+
|
|
39
|
+
// Try to fetch _panel for each service (skip ui itself)
|
|
40
|
+
const panelResults = await Promise.allSettled(
|
|
41
|
+
services
|
|
42
|
+
.filter(s => s.name !== 'ui')
|
|
43
|
+
.map(async (s) => {
|
|
44
|
+
const r = await fetch(`${API}/${s.name}/_panel`);
|
|
45
|
+
if (!r.ok) return null;
|
|
46
|
+
const ct = r.headers.get('content-type') || '';
|
|
47
|
+
if (!ct.includes('html')) return null;
|
|
48
|
+
return { name: s.name, html: await r.text() };
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const panels = panelResults
|
|
53
|
+
.filter(r => r.status === 'fulfilled' && r.value)
|
|
54
|
+
.map(r => r.value);
|
|
55
|
+
|
|
56
|
+
// Build tabs: discovered panels first, then chat
|
|
57
|
+
tabsEl.innerHTML = '';
|
|
58
|
+
|
|
59
|
+
for (const panel of panels) {
|
|
60
|
+
addTab(panel.name, panel.name);
|
|
61
|
+
}
|
|
62
|
+
addTab('chat', 'Chat');
|
|
63
|
+
|
|
64
|
+
// Inject panel HTML
|
|
65
|
+
for (const panel of panels) {
|
|
66
|
+
if (!loadedPanels.has(panel.name)) {
|
|
67
|
+
const container = document.createElement('div');
|
|
68
|
+
container.className = 'panel-view';
|
|
69
|
+
container.id = `view-${panel.name}`;
|
|
70
|
+
container.dataset.api = API;
|
|
71
|
+
panelsEl.appendChild(container);
|
|
72
|
+
injectPanel(container, panel.html);
|
|
73
|
+
loadedPanels.set(panel.name, container);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Remove panels for services that were unloaded
|
|
78
|
+
const activeNames = new Set(panels.map(p => p.name));
|
|
79
|
+
for (const [name, el] of loadedPanels) {
|
|
80
|
+
if (!activeNames.has(name)) {
|
|
81
|
+
el.remove();
|
|
82
|
+
loadedPanels.delete(name);
|
|
83
|
+
// Remove tab
|
|
84
|
+
tabsEl.querySelector(`[data-view="${name}"]`)?.remove();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Activate first tab if none active
|
|
89
|
+
if (!activeTab || !document.getElementById(`view-${activeTab}`)) {
|
|
90
|
+
const first = panels[0]?.name || 'chat';
|
|
91
|
+
switchTab(first);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setStatus('ok', `${panels.length} panels`);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
setStatus('err', e.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addTab(name, label) {
|
|
101
|
+
const btn = document.createElement('button');
|
|
102
|
+
btn.className = 'tab' + (activeTab === name ? ' active' : '');
|
|
103
|
+
btn.dataset.view = name;
|
|
104
|
+
btn.textContent = label;
|
|
105
|
+
btn.addEventListener('click', () => switchTab(name));
|
|
106
|
+
tabsEl.appendChild(btn);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function switchTab(name) {
|
|
110
|
+
activeTab = name;
|
|
111
|
+
|
|
112
|
+
// Update tab highlight
|
|
113
|
+
tabsEl.querySelectorAll('.tab').forEach(t => {
|
|
114
|
+
t.classList.toggle('active', t.dataset.view === name);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Show/hide panels
|
|
118
|
+
document.querySelectorAll('.panel-view').forEach(v => {
|
|
119
|
+
v.classList.toggle('active', v.id === `view-${name}`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Lazy-start chat session
|
|
123
|
+
if (name === 'chat') {
|
|
124
|
+
if (!chatSessionId) chatCreateSession();
|
|
125
|
+
document.getElementById('chat-input')?.focus();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function injectPanel(container, html) {
|
|
130
|
+
// Inject HTML without scripts
|
|
131
|
+
const temp = document.createElement('div');
|
|
132
|
+
temp.innerHTML = html;
|
|
133
|
+
|
|
134
|
+
// Extract scripts
|
|
135
|
+
const scripts = [];
|
|
136
|
+
temp.querySelectorAll('script').forEach(s => {
|
|
137
|
+
scripts.push(s.textContent);
|
|
138
|
+
s.remove();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Inject HTML
|
|
142
|
+
container.innerHTML = temp.innerHTML;
|
|
143
|
+
|
|
144
|
+
// Execute scripts in order
|
|
145
|
+
for (const code of scripts) {
|
|
146
|
+
const s = document.createElement('script');
|
|
147
|
+
s.textContent = code;
|
|
148
|
+
container.appendChild(s);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function setStatus(state, text) {
|
|
153
|
+
statusEl.className = 'status ' + state;
|
|
154
|
+
statusEl.querySelector('.label').textContent = text;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Chat
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
let chatSessionId = null;
|
|
162
|
+
let chatStreaming = false;
|
|
163
|
+
let chatCurrentEl = null;
|
|
164
|
+
let chatCurrentText = '';
|
|
165
|
+
|
|
166
|
+
function chatEl(id) { return document.getElementById(id); }
|
|
167
|
+
|
|
168
|
+
async function chatCreateSession() {
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(`${API}/agent/sessions`, { method: 'POST' });
|
|
171
|
+
const data = await res.json();
|
|
172
|
+
if (data.error) throw new Error(data.error);
|
|
173
|
+
chatSessionId = data.id;
|
|
174
|
+
chatConnectSSE();
|
|
175
|
+
const empty = chatEl('chat-messages').querySelector('.chat-empty');
|
|
176
|
+
if (empty) empty.remove();
|
|
177
|
+
} catch (e) {
|
|
178
|
+
chatAddMsg('system', `Failed to start session: ${e.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function chatConnectSSE() {
|
|
183
|
+
if (!chatSessionId) return;
|
|
184
|
+
fetch(`${API}/agent/sessions/${chatSessionId}/events`)
|
|
185
|
+
.then(res => {
|
|
186
|
+
if (!res.ok) throw new Error(`SSE ${res.status}`);
|
|
187
|
+
chatReadSSE(res.body.getReader());
|
|
188
|
+
})
|
|
189
|
+
.catch(e => {
|
|
190
|
+
chatAddMsg('system', `Disconnected: ${e.message}`);
|
|
191
|
+
setTimeout(() => { if (chatSessionId) chatConnectSSE(); }, 3000);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function chatReadSSE(reader) {
|
|
196
|
+
const dec = new TextDecoder();
|
|
197
|
+
let buf = '';
|
|
198
|
+
try {
|
|
199
|
+
while (true) {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
buf += dec.decode(value, { stream: true });
|
|
203
|
+
const lines = buf.split('\n');
|
|
204
|
+
buf = lines.pop() || '';
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
if (line.startsWith('data: ')) {
|
|
207
|
+
try { chatHandleEvent(JSON.parse(line.slice(6))); } catch {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function chatHandleEvent(e) {
|
|
215
|
+
switch (e.type) {
|
|
216
|
+
case 'agent_start':
|
|
217
|
+
chatStreaming = true;
|
|
218
|
+
chatEl('chat-send').textContent = 'Stop';
|
|
219
|
+
break;
|
|
220
|
+
case 'agent_end':
|
|
221
|
+
chatStreaming = false;
|
|
222
|
+
chatFinish();
|
|
223
|
+
chatEl('chat-send').textContent = 'Send';
|
|
224
|
+
break;
|
|
225
|
+
case 'message_update': {
|
|
226
|
+
const d = e.assistantMessageEvent;
|
|
227
|
+
if (d?.type === 'text_delta') {
|
|
228
|
+
chatEnsure();
|
|
229
|
+
chatCurrentText += d.delta;
|
|
230
|
+
chatRender();
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 'tool_execution_start':
|
|
235
|
+
chatEnsure();
|
|
236
|
+
chatAddTool(e.toolCallId, e.toolName, e.args);
|
|
237
|
+
break;
|
|
238
|
+
case 'tool_execution_update':
|
|
239
|
+
chatUpdateTool(e.toolCallId, e.partialResult);
|
|
240
|
+
break;
|
|
241
|
+
case 'tool_execution_end':
|
|
242
|
+
chatUpdateTool(e.toolCallId, e.result, e.isError);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function chatEnsure() {
|
|
248
|
+
if (chatCurrentEl) return;
|
|
249
|
+
chatCurrentEl = document.createElement('div');
|
|
250
|
+
chatCurrentEl.className = 'chat-msg';
|
|
251
|
+
chatCurrentEl.innerHTML = '<div class="chat-msg-role assistant">assistant</div><div class="chat-msg-content"></div>';
|
|
252
|
+
chatEl('chat-messages').appendChild(chatCurrentEl);
|
|
253
|
+
chatCurrentText = '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function chatRender() {
|
|
257
|
+
if (!chatCurrentEl) return;
|
|
258
|
+
let t = chatCurrentEl.querySelector('.chat-text');
|
|
259
|
+
if (!t) {
|
|
260
|
+
t = document.createElement('span');
|
|
261
|
+
t.className = 'chat-text';
|
|
262
|
+
const c = chatCurrentEl.querySelector('.chat-msg-content');
|
|
263
|
+
c.insertBefore(t, c.firstChild);
|
|
264
|
+
}
|
|
265
|
+
t.innerHTML = chatMd(chatCurrentText) + '<span class="chat-cursor"></span>';
|
|
266
|
+
chatScroll();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function chatFinish() {
|
|
270
|
+
if (!chatCurrentEl) return;
|
|
271
|
+
const t = chatCurrentEl.querySelector('.chat-text');
|
|
272
|
+
if (t) t.innerHTML = chatMd(chatCurrentText);
|
|
273
|
+
chatCurrentEl.querySelector('.chat-cursor')?.remove();
|
|
274
|
+
chatCurrentEl = null;
|
|
275
|
+
chatCurrentText = '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function chatAddTool(id, name, args) {
|
|
279
|
+
const preview = args
|
|
280
|
+
? Object.values(args).map(v => { const s = typeof v === 'string' ? v : JSON.stringify(v); return s.length > 50 ? s.slice(0, 50) + '…' : s; }).join(', ')
|
|
281
|
+
: '';
|
|
282
|
+
const el = document.createElement('div');
|
|
283
|
+
el.className = 'chat-tool';
|
|
284
|
+
el.dataset.toolCallId = id;
|
|
285
|
+
el.innerHTML = `
|
|
286
|
+
<div class="chat-tool-header" onclick="this.querySelector('.chat-tool-arrow').classList.toggle('open');this.nextElementSibling.classList.toggle('open')">
|
|
287
|
+
<span class="chat-tool-arrow">▶</span>
|
|
288
|
+
<span>${esc(name)}(${esc(preview)})</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="chat-tool-body"></div>`;
|
|
291
|
+
chatCurrentEl.querySelector('.chat-msg-content').appendChild(el);
|
|
292
|
+
chatScroll();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function chatUpdateTool(id, result, isError) {
|
|
296
|
+
const el = chatCurrentEl?.querySelector(`[data-tool-call-id="${id}"]`);
|
|
297
|
+
if (!el) return;
|
|
298
|
+
const body = el.querySelector('.chat-tool-body');
|
|
299
|
+
const text = result?.content?.filter(c => c.type === 'text').map(c => c.text).join('') || '';
|
|
300
|
+
body.textContent = text.slice(-2000);
|
|
301
|
+
if (isError) body.classList.add('chat-tool-error');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function chatAddMsg(role, text) {
|
|
305
|
+
const el = document.createElement('div');
|
|
306
|
+
el.className = 'chat-msg';
|
|
307
|
+
el.innerHTML = `<div class="chat-msg-role ${role}">${role === 'user' ? 'you' : role}</div><div class="chat-msg-content">${esc(text)}</div>`;
|
|
308
|
+
chatEl('chat-messages').appendChild(el);
|
|
309
|
+
chatScroll();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function chatMd(text) {
|
|
313
|
+
text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, l, c) => `<pre><code>${esc(c.trimEnd())}</code></pre>`);
|
|
314
|
+
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
315
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
316
|
+
return text.split(/(<pre>[\s\S]*?<\/pre>)/g).map(p => p.startsWith('<pre>') ? p : p.replace(/\n/g, '<br>')).join('');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function chatScroll() {
|
|
320
|
+
requestAnimationFrame(() => {
|
|
321
|
+
const el = chatEl('chat-messages');
|
|
322
|
+
el.scrollTop = el.scrollHeight;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function chatSend() {
|
|
327
|
+
if (!chatSessionId) return;
|
|
328
|
+
if (chatStreaming) {
|
|
329
|
+
fetch(`${API}/agent/sessions/${chatSessionId}/abort`, { method: 'POST' }).catch(() => {});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const input = chatEl('chat-input');
|
|
333
|
+
const text = input.value.trim();
|
|
334
|
+
if (!text) return;
|
|
335
|
+
input.value = '';
|
|
336
|
+
input.style.height = '36px';
|
|
337
|
+
chatAddMsg('user', text);
|
|
338
|
+
chatFinish();
|
|
339
|
+
try {
|
|
340
|
+
const res = await fetch(`${API}/agent/sessions/${chatSessionId}/message`, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
343
|
+
body: JSON.stringify({ message: text }),
|
|
344
|
+
});
|
|
345
|
+
const data = await res.json();
|
|
346
|
+
if (data.error) chatAddMsg('system', data.error);
|
|
347
|
+
} catch (e) {
|
|
348
|
+
chatAddMsg('system', e.message);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Chat input handlers
|
|
353
|
+
chatEl('chat-send').addEventListener('click', chatSend);
|
|
354
|
+
chatEl('chat-input').addEventListener('keydown', (e) => {
|
|
355
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); chatSend(); }
|
|
356
|
+
});
|
|
357
|
+
chatEl('chat-input').addEventListener('input', () => {
|
|
358
|
+
const el = chatEl('chat-input');
|
|
359
|
+
el.style.height = '36px';
|
|
360
|
+
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// =============================================================================
|
|
364
|
+
// Init
|
|
365
|
+
// =============================================================================
|
|
366
|
+
|
|
367
|
+
discoverPanels();
|
|
368
|
+
// Re-discover periodically (picks up loaded/unloaded services)
|
|
369
|
+
setInterval(discoverPanels, 30000);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>reef</title>
|
|
7
|
+
<link rel="stylesheet" href="/ui/static/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app">
|
|
11
|
+
<header>
|
|
12
|
+
<h1>▸ reef</h1>
|
|
13
|
+
<nav class="tabs" id="tabs"></nav>
|
|
14
|
+
<div class="status" id="status">
|
|
15
|
+
<span class="dot"></span>
|
|
16
|
+
<span class="label">loading</span>
|
|
17
|
+
</div>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<div id="panels"></div>
|
|
21
|
+
|
|
22
|
+
<!-- Chat is always built-in, not a panel -->
|
|
23
|
+
<div class="panel-view" id="view-chat">
|
|
24
|
+
<div class="chat-container">
|
|
25
|
+
<div class="chat-messages" id="chat-messages">
|
|
26
|
+
<div class="chat-empty">
|
|
27
|
+
<div class="chat-empty-title">reef agent</div>
|
|
28
|
+
<div class="chat-empty-sub">Send a message to start a session</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="chat-input-area">
|
|
32
|
+
<textarea id="chat-input" placeholder="Send a message… (Enter to send, Shift+Enter for newline)" rows="1"></textarea>
|
|
33
|
+
<button id="chat-send">Send</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script>const PANEL_API = '/ui/api';</script>
|
|
40
|
+
<script src="/ui/static/app.js"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #0a0a0a;
|
|
5
|
+
--bg-panel: #111;
|
|
6
|
+
--bg-card: #1a1a1a;
|
|
7
|
+
--border: #2a2a2a;
|
|
8
|
+
--text: #ccc;
|
|
9
|
+
--text-dim: #666;
|
|
10
|
+
--text-bright: #eee;
|
|
11
|
+
--accent: #4f9;
|
|
12
|
+
--yellow: #fd0;
|
|
13
|
+
--red: #f55;
|
|
14
|
+
--blue: #5af;
|
|
15
|
+
--purple: #a7f;
|
|
16
|
+
--orange: #f93;
|
|
17
|
+
--font: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html, body {
|
|
21
|
+
height: 100%; background: var(--bg); color: var(--text);
|
|
22
|
+
font-family: var(--font); font-size: 13px; line-height: 1.5;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#app { display: flex; flex-direction: column; height: 100%; }
|
|
26
|
+
|
|
27
|
+
/* ─── Header ─── */
|
|
28
|
+
|
|
29
|
+
header {
|
|
30
|
+
display: flex; align-items: center; gap: 16px;
|
|
31
|
+
padding: 10px 20px; border-bottom: 1px solid var(--border);
|
|
32
|
+
background: var(--bg-panel); flex-shrink: 0;
|
|
33
|
+
}
|
|
34
|
+
header h1 { font-size: 14px; color: var(--accent); font-weight: 600; white-space: nowrap; }
|
|
35
|
+
|
|
36
|
+
.tabs { display: flex; gap: 2px; flex: 1; }
|
|
37
|
+
.tab {
|
|
38
|
+
background: none; border: none; color: var(--text-dim); cursor: pointer;
|
|
39
|
+
padding: 4px 12px; font-size: 11px; font-family: var(--font);
|
|
40
|
+
border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
41
|
+
transition: all 0.15s;
|
|
42
|
+
}
|
|
43
|
+
.tab:hover { color: var(--text); background: var(--bg-card); }
|
|
44
|
+
.tab.active { color: var(--accent); background: var(--bg-card); font-weight: 600; }
|
|
45
|
+
|
|
46
|
+
.status {
|
|
47
|
+
display: flex; align-items: center; gap: 6px;
|
|
48
|
+
font-size: 11px; color: var(--text-dim); white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
.status .dot {
|
|
51
|
+
width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim);
|
|
52
|
+
}
|
|
53
|
+
.status.ok .dot { background: var(--accent); }
|
|
54
|
+
.status.err .dot { background: var(--red); }
|
|
55
|
+
|
|
56
|
+
/* ─── Panels ─── */
|
|
57
|
+
|
|
58
|
+
.panel-view {
|
|
59
|
+
flex: 1; overflow-y: auto; display: none;
|
|
60
|
+
}
|
|
61
|
+
.panel-view.active { display: block; }
|
|
62
|
+
|
|
63
|
+
.panel-loading {
|
|
64
|
+
color: var(--text-dim); font-style: italic;
|
|
65
|
+
padding: 40px; text-align: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ─── Chat ─── */
|
|
69
|
+
|
|
70
|
+
.chat-container {
|
|
71
|
+
display: flex; flex-direction: column; height: calc(100vh - 50px);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.chat-messages {
|
|
75
|
+
flex: 1; overflow-y: auto; padding: 16px 20px;
|
|
76
|
+
display: flex; flex-direction: column; gap: 16px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.chat-empty {
|
|
80
|
+
flex: 1; display: flex; flex-direction: column;
|
|
81
|
+
align-items: center; justify-content: center; gap: 6px;
|
|
82
|
+
}
|
|
83
|
+
.chat-empty-title { font-size: 16px; color: var(--text-bright); font-weight: 600; }
|
|
84
|
+
.chat-empty-sub { font-size: 12px; color: var(--text-dim); }
|
|
85
|
+
|
|
86
|
+
.chat-msg { max-width: 100%; }
|
|
87
|
+
.chat-msg-role {
|
|
88
|
+
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
89
|
+
letter-spacing: 0.5px; margin-bottom: 3px; color: var(--text-dim);
|
|
90
|
+
}
|
|
91
|
+
.chat-msg-role.user { color: var(--blue); }
|
|
92
|
+
.chat-msg-role.assistant { color: var(--accent); }
|
|
93
|
+
.chat-msg-role.system { color: var(--red); }
|
|
94
|
+
|
|
95
|
+
.chat-msg-content {
|
|
96
|
+
line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;
|
|
97
|
+
}
|
|
98
|
+
.chat-msg-content code {
|
|
99
|
+
background: var(--bg-card); padding: 1px 5px; border-radius: 3px; font-size: 12px;
|
|
100
|
+
}
|
|
101
|
+
.chat-msg-content pre {
|
|
102
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
103
|
+
border-radius: 4px; padding: 10px; margin: 6px 0; overflow-x: auto;
|
|
104
|
+
}
|
|
105
|
+
.chat-msg-content pre code { background: none; padding: 0; }
|
|
106
|
+
|
|
107
|
+
.chat-tool {
|
|
108
|
+
margin: 6px 0; border: 1px solid var(--border); border-radius: 4px; overflow: hidden;
|
|
109
|
+
}
|
|
110
|
+
.chat-tool-header {
|
|
111
|
+
padding: 6px 10px; background: var(--bg-panel); font-size: 11px;
|
|
112
|
+
color: var(--yellow); cursor: pointer; display: flex; align-items: center; gap: 6px;
|
|
113
|
+
user-select: none;
|
|
114
|
+
}
|
|
115
|
+
.chat-tool-header:hover { background: var(--bg-card); }
|
|
116
|
+
.chat-tool-arrow { font-size: 9px; transition: transform 0.15s; }
|
|
117
|
+
.chat-tool-arrow.open { transform: rotate(90deg); }
|
|
118
|
+
.chat-tool-body {
|
|
119
|
+
display: none; padding: 6px 10px; background: var(--bg);
|
|
120
|
+
font-size: 11px; white-space: pre-wrap; color: var(--text-dim);
|
|
121
|
+
max-height: 250px; overflow-y: auto; border-top: 1px solid var(--border);
|
|
122
|
+
}
|
|
123
|
+
.chat-tool-body.open { display: block; }
|
|
124
|
+
.chat-tool-error { color: var(--red); }
|
|
125
|
+
|
|
126
|
+
.chat-cursor {
|
|
127
|
+
display: inline-block; width: 2px; height: 1em;
|
|
128
|
+
background: var(--accent); animation: blink 1s step-end infinite;
|
|
129
|
+
vertical-align: text-bottom; margin-left: 1px;
|
|
130
|
+
}
|
|
131
|
+
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
132
|
+
|
|
133
|
+
.chat-input-area {
|
|
134
|
+
border-top: 1px solid var(--border); padding: 10px 20px;
|
|
135
|
+
display: flex; gap: 8px; background: var(--bg-panel); flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
#chat-input {
|
|
138
|
+
flex: 1; background: var(--bg-card); border: 1px solid var(--border);
|
|
139
|
+
border-radius: 4px; padding: 8px 12px; color: var(--text);
|
|
140
|
+
font-family: var(--font); font-size: 13px; outline: none;
|
|
141
|
+
resize: none; min-height: 36px; max-height: 200px;
|
|
142
|
+
}
|
|
143
|
+
#chat-input:focus { border-color: #444; }
|
|
144
|
+
#chat-input::placeholder { color: var(--text-dim); }
|
|
145
|
+
#chat-send {
|
|
146
|
+
background: #1a3a1a; color: var(--accent); border: 1px solid #2a4a2a;
|
|
147
|
+
border-radius: 4px; padding: 0 14px; font-size: 12px; font-family: var(--font);
|
|
148
|
+
font-weight: 600; cursor: pointer; white-space: nowrap; align-self: flex-end;
|
|
149
|
+
height: 36px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
150
|
+
}
|
|
151
|
+
#chat-send:hover { background: #2a4a2a; }
|
|
152
|
+
#chat-send:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
153
|
+
|
|
154
|
+
/* ─── Active chat view override ─── */
|
|
155
|
+
.panel-view.active#view-chat {
|
|
156
|
+
display: flex; flex-direction: column;
|
|
157
|
+
}
|