@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.
Files changed (83) hide show
  1. package/.github/workflows/test.yml +47 -0
  2. package/README.md +257 -0
  3. package/bun.lock +587 -0
  4. package/examples/services/board/board.test.ts +215 -0
  5. package/examples/services/board/index.ts +155 -0
  6. package/examples/services/board/routes.ts +335 -0
  7. package/examples/services/board/store.ts +329 -0
  8. package/examples/services/board/tools.ts +214 -0
  9. package/examples/services/commits/commits.test.ts +74 -0
  10. package/examples/services/commits/index.ts +14 -0
  11. package/examples/services/commits/routes.ts +43 -0
  12. package/examples/services/commits/store.ts +114 -0
  13. package/examples/services/feed/behaviors.ts +23 -0
  14. package/examples/services/feed/feed.test.ts +101 -0
  15. package/examples/services/feed/index.ts +117 -0
  16. package/examples/services/feed/routes.ts +224 -0
  17. package/examples/services/feed/store.ts +194 -0
  18. package/examples/services/feed/tools.ts +83 -0
  19. package/examples/services/journal/index.ts +15 -0
  20. package/examples/services/journal/journal.test.ts +57 -0
  21. package/examples/services/journal/routes.ts +45 -0
  22. package/examples/services/journal/store.ts +119 -0
  23. package/examples/services/journal/tools.ts +32 -0
  24. package/examples/services/log/index.ts +15 -0
  25. package/examples/services/log/log.test.ts +70 -0
  26. package/examples/services/log/routes.ts +44 -0
  27. package/examples/services/log/store.ts +105 -0
  28. package/examples/services/log/tools.ts +57 -0
  29. package/examples/services/registry/behaviors.ts +128 -0
  30. package/examples/services/registry/index.ts +37 -0
  31. package/examples/services/registry/registry.test.ts +135 -0
  32. package/examples/services/registry/routes.ts +76 -0
  33. package/examples/services/registry/store.ts +224 -0
  34. package/examples/services/registry/tools.ts +116 -0
  35. package/examples/services/reports/index.ts +14 -0
  36. package/examples/services/reports/reports.test.ts +75 -0
  37. package/examples/services/reports/routes.ts +42 -0
  38. package/examples/services/reports/store.ts +110 -0
  39. package/examples/services/ui/auth.ts +61 -0
  40. package/examples/services/ui/index.ts +16 -0
  41. package/examples/services/ui/routes.ts +160 -0
  42. package/examples/services/ui/static/app.js +369 -0
  43. package/examples/services/ui/static/index.html +42 -0
  44. package/examples/services/ui/static/style.css +157 -0
  45. package/examples/services/usage/behaviors.ts +166 -0
  46. package/examples/services/usage/index.ts +19 -0
  47. package/examples/services/usage/routes.ts +53 -0
  48. package/examples/services/usage/store.ts +341 -0
  49. package/examples/services/usage/tools.ts +75 -0
  50. package/examples/services/usage/usage.test.ts +91 -0
  51. package/package.json +29 -0
  52. package/services/agent/index.ts +465 -0
  53. package/services/board/index.ts +155 -0
  54. package/services/board/routes.ts +335 -0
  55. package/services/board/store.ts +329 -0
  56. package/services/board/tools.ts +214 -0
  57. package/services/docs/index.ts +391 -0
  58. package/services/feed/behaviors.ts +23 -0
  59. package/services/feed/index.ts +117 -0
  60. package/services/feed/routes.ts +224 -0
  61. package/services/feed/store.ts +194 -0
  62. package/services/feed/tools.ts +83 -0
  63. package/services/installer/index.ts +574 -0
  64. package/services/services/index.ts +165 -0
  65. package/services/ui/auth.ts +61 -0
  66. package/services/ui/index.ts +16 -0
  67. package/services/ui/routes.ts +160 -0
  68. package/services/ui/static/app.js +369 -0
  69. package/services/ui/static/index.html +42 -0
  70. package/services/ui/static/style.css +157 -0
  71. package/skills/create-service/SKILL.md +698 -0
  72. package/src/core/auth.ts +28 -0
  73. package/src/core/client.ts +99 -0
  74. package/src/core/discover.ts +152 -0
  75. package/src/core/events.ts +44 -0
  76. package/src/core/extension.ts +66 -0
  77. package/src/core/server.ts +262 -0
  78. package/src/core/testing.ts +155 -0
  79. package/src/core/types.ts +194 -0
  80. package/src/extension.ts +16 -0
  81. package/src/main.ts +11 -0
  82. package/tests/server.test.ts +1338 -0
  83. 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
+ }