opencode-agent-flow 0.0.0-stage → 0.1.0
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/CONTRIBUTING.md +3 -0
- package/LICENSE +21 -0
- package/README.md +100 -2
- package/SECURITY.md +3 -0
- package/bin/cli.mjs +52 -0
- package/docs/architecture.md +58 -0
- package/docs/opencode-v2-api-notes.md +89 -0
- package/docs/screenshot.png +0 -0
- package/lib/demo.mjs +10 -0
- package/lib/privacy.mjs +9 -0
- package/lib/upstream.mjs +190 -0
- package/package.json +15 -4
- package/prices.json +10 -0
- package/public/app.js +223 -0
- package/public/flow-webgl.js +155 -0
- package/public/index.html +10 -0
- package/public/style.css +6 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/postprocessing/EffectComposer.js +363 -0
- package/public/vendor/three/postprocessing/MaskPass.js +195 -0
- package/public/vendor/three/postprocessing/OutputPass.js +139 -0
- package/public/vendor/three/postprocessing/Pass.js +191 -0
- package/public/vendor/three/postprocessing/RenderPass.js +183 -0
- package/public/vendor/three/postprocessing/ShaderPass.js +135 -0
- package/public/vendor/three/postprocessing/UnrealBloomPass.js +492 -0
- package/public/vendor/three/shaders/CopyShader.js +52 -0
- package/public/vendor/three/shaders/LuminosityHighPassShader.js +68 -0
- package/public/vendor/three/shaders/OutputShader.js +103 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/server.mjs +217 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const $ = s => document.querySelector(s);
|
|
2
|
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
3
|
+
const fmt = n => n >= 1e6 ? `${(n / 1e6).toFixed(1)}m` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n || 0));
|
|
4
|
+
const agentCalls = (groups, calls) => `${groups} agent${groups === 1 ? '' : 's'} · ${calls} call${calls === 1 ? '' : 's'}`;
|
|
5
|
+
const shortModel = m => /opus|sonnet|gemini|gpt/i.exec(m)?.[0] || m.slice(0, 12);
|
|
6
|
+
const put = (selector, text) => { const el = $(selector); if (el.textContent !== text) el.textContent = text; };
|
|
7
|
+
const html = (selector, text) => { const el = $(selector); if (el.innerHTML !== text) el.innerHTML = text; };
|
|
8
|
+
let state, rootId, agents = [], edges = [], paused = matchMedia('(prefers-reduced-motion: reduce)').matches, connected = false;
|
|
9
|
+
const storageKey = 'agent-flow.root';
|
|
10
|
+
const urlRoot = new URL(location.href).searchParams.get('session');
|
|
11
|
+
try { rootId = urlRoot || localStorage.getItem(storageKey); } catch { rootId = urlRoot; }
|
|
12
|
+
const params = new URL(location.href).searchParams;
|
|
13
|
+
let view = 'all', sessionId = null, sessionList = [], stream, requestVersion = 0;
|
|
14
|
+
try { view = params.get('view') || (urlRoot ? 'session' : localStorage.getItem('agent-flow.view')) || 'all'; sessionId = params.has('view') || urlRoot ? urlRoot : localStorage.getItem('agent-flow.session'); } catch { view = params.get('view') || (urlRoot ? 'session' : 'all'); sessionId = urlRoot; }
|
|
15
|
+
if (view !== 'session') view = 'all';
|
|
16
|
+
function saveView() {
|
|
17
|
+
const url = new URL(location.href); url.searchParams.set('view', view);
|
|
18
|
+
if (view === 'session' ? sessionId : rootId) url.searchParams.set('session', view === 'session' ? sessionId : rootId); else url.searchParams.delete('session');
|
|
19
|
+
history.replaceState(null, '', url);
|
|
20
|
+
try { localStorage.setItem('agent-flow.view', view); if (sessionId) localStorage.setItem('agent-flow.session', sessionId); } catch { /* Storage may be disabled. */ }
|
|
21
|
+
}
|
|
22
|
+
function renderPicker() {
|
|
23
|
+
const query = $('#session-search').value.toLowerCase(), list = sessionList.filter(s => `${s.title} ${s.agent} ${s.model} ${s.id}`.toLowerCase().includes(query));
|
|
24
|
+
const summary = s => `${s.agent} · ${s.model} · ${activityTime(s.lastActivity)} · ${agentCalls(s.agentCount, s.callCount)} · ${fmt(s.tokens)} tokens · ${s.status}`;
|
|
25
|
+
html('#root', '<option value="">select a session…</option>' + list.map(s => `<option value="${esc(s.id)}">${esc(s.title)} · ${esc(summary(s))}</option>`).join('') + (sessionId && !list.some(s => s.id === sessionId) ? `<option value="${esc(sessionId)}">${esc(state?.roots[0]?.title || sessionId)}</option>` : ''));
|
|
26
|
+
$('#root').value = sessionId || ''; $('#root').disabled = false;
|
|
27
|
+
html('#session-cards', list.map(s => `<button type="button" class="session-card" data-session="${esc(s.id)}"><strong>${esc(s.title || s.id)}</strong><span class="${s.status}">● ${esc(s.status)} · ${esc(s.agent)} · ${esc(s.model)}</span><span>${activityTime(s.lastActivity)} · ${agentCalls(s.agentCount, s.callCount)} · ${fmt(s.tokens)} tokens</span></button>`).join('') || '<p class="muted">No sessions match this search.</p>');
|
|
28
|
+
}
|
|
29
|
+
function applyView() {
|
|
30
|
+
const picker = view === 'session' && !sessionId;
|
|
31
|
+
document.body.dataset.view = view;
|
|
32
|
+
$('#session-search').hidden = view !== 'session'; $('#session-picker').hidden = !picker; $('main').hidden = picker;
|
|
33
|
+
for (const mode of ['all', 'session']) $(`#view-${mode}`).setAttribute('aria-pressed', String(mode === view));
|
|
34
|
+
$('.panel:nth-child(5) h3').textContent = view === 'session' ? '// tokens · session' : '// tokens today';
|
|
35
|
+
if (view === 'session') renderPicker();
|
|
36
|
+
if (picker) { put('#title', 'select a session'); put('#subtitle', `opencode · ${sessionList.length} root sessions / live window`); put('#counts', ''); put('#connection', 'select session'); }
|
|
37
|
+
}
|
|
38
|
+
async function loadSessions() {
|
|
39
|
+
try { const response = await fetch('/api/sessions'); if (!response.ok) throw new Error(); sessionList = await response.json(); if (view === 'session') { renderPicker(); if (!sessionId) applyView(); } } catch { if (view === 'session' && !sessionId) put('#connection', 'sessions unavailable'); }
|
|
40
|
+
}
|
|
41
|
+
function selectSession(id) { sessionId = id || null; popover.hidePopover(); saveView(); connect(); }
|
|
42
|
+
$('#session-search').addEventListener('input', renderPicker);
|
|
43
|
+
$('#session-cards').addEventListener('click', e => { const card = e.target.closest('[data-session]'); if (card) selectSession(card.dataset.session); });
|
|
44
|
+
for (const mode of ['all', 'session']) $(`#view-${mode}`).addEventListener('click', () => { if (view === mode) return; view = mode; saveView(); connect(); });
|
|
45
|
+
const validDate = time => { const date = new Date(time); return Number.isFinite(date.getTime()) ? date : new Date(0); };
|
|
46
|
+
const activityTime = time => validDate(time).toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
47
|
+
const totalTokens = a => a.tokens.input + a.tokens.output + a.tokens.cacheRead;
|
|
48
|
+
function rememberRoot() { try { if (rootId) localStorage.setItem(storageKey, rootId); } catch { /* Storage may be disabled. */ } }
|
|
49
|
+
const popover = document.createElement('div');
|
|
50
|
+
popover.id = 'group-sessions'; popover.setAttribute('popover', 'auto'); document.body.append(popover);
|
|
51
|
+
popover.setAttribute('role', 'dialog'); popover.setAttribute('aria-label', 'Group sessions');
|
|
52
|
+
let openGroupId, popoverAnchor;
|
|
53
|
+
function showSessions(id, anchor) {
|
|
54
|
+
const group = agents.find(a => a.id === id);
|
|
55
|
+
if (!group) { popover.hidePopover(); return; }
|
|
56
|
+
openGroupId = id; popoverAnchor = anchor;
|
|
57
|
+
const scrollTop = popover.scrollTop, closeFocused = document.activeElement === popover.querySelector('button');
|
|
58
|
+
const content = `<div class="popover-heading"><strong>${esc(group.agent)} · ${group.count} sessions</strong><button type="button" aria-label="Close">×</button></div><div class="session-list">${group.sessions.map(s => `<div class="session-item"><strong title="${esc(s.title)}">${esc(s.title || s.id)}</strong><div><b class="${s.status}">${esc(s.status)}</b><span>${fmt(totalTokens(s))} tokens</span><time datetime="${validDate(s.lastActivity).toISOString()}">${activityTime(s.lastActivity)}</time></div></div>`).join('')}</div>`;
|
|
59
|
+
if (popover.innerHTML !== content) { popover.innerHTML = content; popover.scrollTop = scrollTop; if (closeFocused) popover.querySelector('button').focus({ preventScroll: true }); }
|
|
60
|
+
popover.querySelector('button').onclick = () => popover.hidePopover();
|
|
61
|
+
if (!popover.matches(':popover-open')) popover.showPopover();
|
|
62
|
+
const rect = anchor.getBoundingClientRect();
|
|
63
|
+
popover.style.left = `${Math.max(12, Math.min(rect.left, innerWidth - popover.offsetWidth - 12))}px`;
|
|
64
|
+
popover.style.top = `${Math.max(12, Math.min(rect.bottom + 8, innerHeight - popover.offsetHeight - 12))}px`;
|
|
65
|
+
}
|
|
66
|
+
popover.addEventListener('toggle', () => { if (!popover.matches(':popover-open')) openGroupId = null; for (const [id, el] of nodeElements) el.setAttribute('aria-expanded', String(id === openGroupId)); });
|
|
67
|
+
let networkHeight = 540, hub = [682, 270], lastFrame = 0;
|
|
68
|
+
const nodeElements = new Map();
|
|
69
|
+
const canvas = $('#wires'), ctx = canvas.getContext('2d');
|
|
70
|
+
let flow, motionTime = 1;
|
|
71
|
+
window.__renderer = 'canvas2d';
|
|
72
|
+
import('/flow-webgl.js').then(({ createFlow }) => {
|
|
73
|
+
flow = createFlow(canvas, () => { flow = null; canvas.hidden = false; window.__renderer = 'canvas2d'; draw(performance.now(), true); });
|
|
74
|
+
canvas.hidden = true; window.__renderer = 'webgl'; resize();
|
|
75
|
+
}).catch(() => { window.__renderer = 'canvas2d'; draw(performance.now(), true); });
|
|
76
|
+
$('#root').addEventListener('change', e => { if (view === 'session') return selectSession(e.target.value); rootId = e.target.value; rememberRoot(); saveView(); popover.hidePopover(); render(); });
|
|
77
|
+
$('#motion').addEventListener('click', () => { paused = !paused; motionLabel(); draw(performance.now(), true); });
|
|
78
|
+
function motionLabel() { put('#motion', paused ? 'resume motion' : 'pause motion'); $('#motion').setAttribute('aria-pressed', String(paused)); }
|
|
79
|
+
motionLabel();
|
|
80
|
+
function resize() {
|
|
81
|
+
const width = $('.viewport').clientWidth;
|
|
82
|
+
const scale = width < 650 ? .7 : Math.min(1, width / 1364);
|
|
83
|
+
$('.network').style.transform = width < 650 ? `scale(${scale})` : `translateX(-50%) scale(${scale})`;
|
|
84
|
+
$('.network').style.transformOrigin = width < 650 ? '0 0' : '50% 0';
|
|
85
|
+
$('.scene').style.height = `${Math.max(width < 650 ? 440 : 500, networkHeight * scale)}px`;
|
|
86
|
+
$('.network').style.height = `${networkHeight}px`;
|
|
87
|
+
const dpr = Math.min(devicePixelRatio || 1, 2);
|
|
88
|
+
canvas.width = 1364 * dpr; canvas.height = networkHeight * dpr;
|
|
89
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
90
|
+
flow?.resize(1364, networkHeight, dpr);
|
|
91
|
+
updateAnchors();
|
|
92
|
+
draw(performance.now(), true);
|
|
93
|
+
}
|
|
94
|
+
addEventListener('resize', resize);
|
|
95
|
+
function render() {
|
|
96
|
+
if (!state) return;
|
|
97
|
+
if (view === 'session') rootId = sessionId;
|
|
98
|
+
else if (!state.roots.some(r => r.id === rootId)) rootId = state.roots[0]?.id;
|
|
99
|
+
if (view === 'all') rememberRoot();
|
|
100
|
+
const options = state.roots.map(r => `<option value="${esc(r.id)}">${esc(r.title || r.id)} · ${activityTime(r.lastActivity)} · ${agentCalls(r.agentCount, r.callCount)}</option>`).join('');
|
|
101
|
+
if (view === 'all') { html('#root', options); $('#root').value = rootId || ''; $('#root').disabled = !rootId; } else renderPicker();
|
|
102
|
+
const selected = new Set(rootId ? [rootId] : []);
|
|
103
|
+
let added = true;
|
|
104
|
+
while (added) { added = false; for (const a of state.agents) if (selected.has(a.parentId) && !selected.has(a.id)) { selected.add(a.id); added = true; } }
|
|
105
|
+
agents = state.agents.filter(a => selected.has(a.id));
|
|
106
|
+
const root = agents.find(a => a.id === rootId), children = agents.filter(a => a.id !== rootId);
|
|
107
|
+
put('#title', root?.title || 'no active sessions');
|
|
108
|
+
put('#subtitle', `opencode ${state.serverVersion || ''}${state.demo ? ' · demo' : ''} · orchestration topology / ${agentCalls(agents.length, state.roots.find(r => r.id === rootId)?.callCount || 0)}`);
|
|
109
|
+
put('#connection', state.connection === 'connected' ? 'live' : state.connection || 'connecting');
|
|
110
|
+
$('.status').classList.toggle('offline', state.connection !== 'connected');
|
|
111
|
+
put('#counts', `${agents.filter(a => a.status === 'working').length} running · ${state.approvals.filter(p => agents.some(a => a.sessions.some(s => s.id === p.sessionID))).length} approvals · ${fmt(state.aggregates.totalToday)} tokens ${view === 'session' ? '· session' : 'today'}`);
|
|
112
|
+
put('#graphMeta', ` · ${children.length} children / ${state.windowHours}h window`);
|
|
113
|
+
$('.hub').hidden = !root; $('#empty').hidden = !!root;
|
|
114
|
+
put('#empty', view === 'session' ? 'Session not found' : `No active sessions in the last ${state.windowHours} hours`);
|
|
115
|
+
if (root) {
|
|
116
|
+
const ratio = root.context / root.contextLimit;
|
|
117
|
+
put('#lead', root.agent); put('#context', `${root.status} · ctx ${root.contextEstimated ? '≈ ' : ''}${fmt(root.context)}`); put('#model', root.model);
|
|
118
|
+
$('#context').title = root.contextEstimated ? 'Last reported usage; current response is streaming' : 'Latest assistant input + cache read + cache write';
|
|
119
|
+
put('#leadTool', root.currentTool ? `${root.currentTool.name} → ${root.currentTool.target}` : `${fmt(root.tokens.input + root.tokens.output + root.tokens.cacheRead)} tokens`);
|
|
120
|
+
$('#leadTool').title = root.currentTool?.target || '';
|
|
121
|
+
$('#contextRing').setAttribute('stroke-dasharray', `${Math.min(1, ratio) * 635} 635`);
|
|
122
|
+
put('#sceneSummary', `all agents connected to ${root.agent} / ${Math.round(ratio * 100)}% context pressure`);
|
|
123
|
+
} else put('#sceneSummary', 'read-only · waiting for session activity');
|
|
124
|
+
const sessionIds = new Set(agents.flatMap(a => a.sessions.map(s => s.id)));
|
|
125
|
+
const pending = state.approvals.filter(p => sessionIds.has(p.sessionID));
|
|
126
|
+
const items = [...children, ...(pending.length ? [{ id: '__approval', agent: 'user · approval', model: `${pending.length} pending`, status: 'waiting', currentTool: { name: pending[0].action, target: pending[0].resource }, tokens: { input: 0, output: 0, cacheRead: 0 } }] : [])];
|
|
127
|
+
const rows = Math.max(3, Math.ceil(items.length / 4));
|
|
128
|
+
networkHeight = Math.max(540, 120 + rows * 105);
|
|
129
|
+
hub = [682, networkHeight / 2]; $('.hub').style.top = `${hub[1] - 118}px`;
|
|
130
|
+
const points = new Map();
|
|
131
|
+
const ring = [[75,165],[330,55],[840,55],[1090,165],[75,355],[330,415],[840,415],[1090,355]];
|
|
132
|
+
items.forEach((a, i) => {
|
|
133
|
+
const p = items.length <= 8 ? ring[i] : [[55,295,865,1105][i % 4], 65 + Math.floor(i / 4) * 105];
|
|
134
|
+
points.set(a.id, [p[0] + 100, p[1] + 48]);
|
|
135
|
+
let el = nodeElements.get(a.id);
|
|
136
|
+
if (!el) { el = document.createElement('article'); $('#nodes').append(el); nodeElements.set(a.id, el); }
|
|
137
|
+
if (a.id !== '__approval') {
|
|
138
|
+
el.tabIndex = 0; el.setAttribute('role', 'button'); el.setAttribute('aria-haspopup', 'dialog');
|
|
139
|
+
el.setAttribute('aria-expanded', String(openGroupId === a.id));
|
|
140
|
+
el.onclick = () => openGroupId === a.id ? popover.hidePopover() : showSessions(a.id, el);
|
|
141
|
+
el.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } };
|
|
142
|
+
}
|
|
143
|
+
el.className = `node ${a.status}${a.id === '__approval' ? ' approval' : ''}`;
|
|
144
|
+
el.style.transform = `translate(${p[0]}px,${p[1]}px)`;
|
|
145
|
+
el.title = `${a.agent} · ${a.model}\n${a.title || ''}\n${a.currentTool?.target || ''}`;
|
|
146
|
+
const content = `<div class="cap"><strong>${esc(a.agent)}${a.count > 1 ? ` <small>×${a.count}</small>` : ''}</strong><span class="tag">${esc(a.id === '__approval' ? a.model : shortModel(a.model))}</span></div><div class="rows"><div class="row"><span>${esc(a.currentTool?.name || 'state')}</span><span>${esc(a.currentTool?.target || a.status)}</span></div><div class="row"><span>${a.id === '__approval' ? 'action' : 'tokens'}</span><span>${a.id === '__approval' ? 'confirm in OpenCode' : fmt(totalTokens(a))}</span></div></div>`;
|
|
147
|
+
if (el.innerHTML !== content) el.innerHTML = content;
|
|
148
|
+
});
|
|
149
|
+
for (const [id, el] of nodeElements) if (!items.some(a => a.id === id)) { el.remove(); nodeElements.delete(id); }
|
|
150
|
+
edges = children.map((a, i) => {
|
|
151
|
+
const p = points.get(a.id), dx = p[0] - hub[0], dy = p[1] - hub[1], k = 116 / Math.hypot(dx, dy);
|
|
152
|
+
return { id: a.id, a: [hub[0] + dx * k, hub[1] + dy * k], b: p, status: a.status, active: a.status === 'working' || a.status === 'waiting', bend: i % 2 ? 30 : -25, returning: a.status === 'done' };
|
|
153
|
+
});
|
|
154
|
+
if (pending.length) for (const p of pending) { const sourceId = agents.find(a => a.sessions.some(s => s.id === p.sessionID))?.id; const source = points.get(sourceId) || hub; edges.push({ id: '__approval', sourceId, a: source, b: points.get('__approval'), status: 'waiting', active: true, bend: 22 }); }
|
|
155
|
+
put('#transport', `${edges.length} connections · ${paused ? 'motion paused' : 'white → pink / token transport'}`);
|
|
156
|
+
const events = state.events.filter(e => selected.has(e.groupId)).slice(-60).reverse();
|
|
157
|
+
html('#events', events.map(e => `<div class="event" title="${esc(e.agent + ' · ' + e.kind + ' · ' + e.text)}"><time>${validDate(e.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</time><em>${esc(e.kind === 'permission asked' ? 'wait' : e.kind === 'tool complete' ? 'done' : e.kind === 'tool start' ? 'tool' : e.kind)}</em>${esc(e.agent)} → ${esc(e.text)}</div>`).join('') || '<div class="muted">no recent events</div>');
|
|
158
|
+
const ag = state.aggregates, max = Math.max(1, ...ag.hourly.map(b => b.total));
|
|
159
|
+
html('#hourly', ag.hourly.map(b => `<div class="bars" title="${esc(Object.entries(b.models).map(([m, n]) => `${m}: ${fmt(n)}`).join('\n') || 'no tokens')}"><time>${validDate(b.time).getHours().toString().padStart(2, '0')}</time><div class="track"><i style="width:${b.total / max * 100}%"></i></div><b>${fmt(b.total)}</b></div>`).join(''));
|
|
160
|
+
put('#cacheValue', `${Math.round(ag.cacheHit * 100)}%`); $('#cacheRing').setAttribute('stroke-dasharray', `${ag.cacheHit * 270} 270`);
|
|
161
|
+
const heat = ag.heat.filter(a => selected.has(a.id)), peak = Math.max(1, ...heat.flatMap(a => a.values));
|
|
162
|
+
html('#heat', heat.map(a => `<div class="heat-row"><span title="${esc(a.agent)}">${esc(a.agent)}</span>${a.values.map((n, i) => `<i title="${esc(a.agent)} / ${(12 - i) * 5}–${(11 - i) * 5} min ago: ${n} events" style="opacity:${.1 + .9 * n / peak}"></i>`).join('')}</div>`).join('') || '<div class="muted">no activity</div>');
|
|
163
|
+
put('#total', fmt(ag.totalToday)); put('#estimate', `≈ $${ag.estimate.toFixed(2)} est.`);
|
|
164
|
+
html('#today', Object.entries(ag.today).sort((a, b) => b[1].total - a[1].total).map(([m, t]) => `<div title="${esc(m)} · input ${t.input} / output ${t.output} / cache ${t.cacheRead}">${esc(shortModel(m))} · ${fmt(t.total)} · ≈ $${t.estimate.toFixed(2)}</div>`).join('') || 'no tokens today');
|
|
165
|
+
html('#ledger', agents.map(a => `<div title="${esc(a.title)}"><b class="${a.status}">[ ${a.status.padEnd(7, ' ')} ]</b> ${esc(a.agent)}</div>`).join('') || '<span class="muted">no agents</span>');
|
|
166
|
+
put('#health', `transport / public HTTP + SSE · read-only${state.permissionHealth.ok ? '' : ' · approvals unavailable'}`);
|
|
167
|
+
put('#clock', `${validDate(state.updatedAt).toLocaleString()} · snapshot`);
|
|
168
|
+
resize();
|
|
169
|
+
if (openGroupId) showSessions(openGroupId, popoverAnchor);
|
|
170
|
+
}
|
|
171
|
+
// Seven-strand Bezier harnesses and particle transport, adapted from concept5-flow.
|
|
172
|
+
function updateAnchors() {
|
|
173
|
+
const rect = $('.network').getBoundingClientRect(), scale = rect.width / 1364;
|
|
174
|
+
const center = el => { const r = el.getBoundingClientRect(); return [(r.left + r.width / 2 - rect.left) / scale, (r.top + r.height / 2 - rect.top) / scale]; };
|
|
175
|
+
if (!$('.hub').hidden) hub = center($('.hub'));
|
|
176
|
+
const dock = (el, target) => {
|
|
177
|
+
const c = center(el), r = el.getBoundingClientRect(), dx = target[0] - c[0], dy = target[1] - c[1];
|
|
178
|
+
const k = Math.min((r.width / scale / 2 + 3) / (Math.abs(dx) || .001), (r.height / scale / 2 + 3) / (Math.abs(dy) || .001));
|
|
179
|
+
return [c[0] + dx * k, c[1] + dy * k];
|
|
180
|
+
};
|
|
181
|
+
for (const e of edges) {
|
|
182
|
+
const node = nodeElements.get(e.id), source = nodeElements.get(e.sourceId);
|
|
183
|
+
if (!node) continue;
|
|
184
|
+
const target = center(node), start = source ? center(source) : hub;
|
|
185
|
+
const dx = target[0] - start[0], dy = target[1] - start[1], k = 116 / (Math.hypot(dx, dy) || 1);
|
|
186
|
+
e.a = source ? dock(source, target) : [hub[0] + dx * k, hub[1] + dy * k];
|
|
187
|
+
e.b = dock(node, start);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function curve(e, t, j) {
|
|
191
|
+
const [x,y] = e.a, [X,Y] = e.b, dx = X-x, dy = Y-y, l = Math.hypot(dx,dy) || 1, off = (j-3)*5+e.bend, u = 1-t;
|
|
192
|
+
const cx=x+dx*.34-dy/l*off, cy=y+dy*.34+dx/l*off, ex=x+dx*.68+dy/l*off*.6, ey=y+dy*.68-dx/l*off*.6;
|
|
193
|
+
return [u*u*u*x+3*u*u*t*cx+3*u*t*t*ex+t*t*t*X,u*u*u*y+3*u*u*t*cy+3*u*t*t*ey+t*t*t*Y];
|
|
194
|
+
}
|
|
195
|
+
function ball(x,y,r,alpha=1) { ctx.globalAlpha=alpha;ctx.shadowColor='#ff3d8b';ctx.shadowBlur=r*7;ctx.fillStyle='#ffeaf5';ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0; }
|
|
196
|
+
function draw(ms, force = false) {
|
|
197
|
+
if (document.hidden) return;
|
|
198
|
+
if (!force && (paused || document.hidden || ms - lastFrame < 32)) return;
|
|
199
|
+
if (!paused && !document.hidden) motionTime += Math.min(.1, Math.max(0, (ms - lastFrame) / 1000));
|
|
200
|
+
lastFrame = ms; const t = motionTime;
|
|
201
|
+
if (flow) { flow.draw(edges, hub, agents.length > 0, t); return; }
|
|
202
|
+
ctx.clearRect(0,0,1364,networkHeight);ctx.globalCompositeOperation='lighter';
|
|
203
|
+
edges.forEach((e,ei)=>{for(let j=0;j<7;j++){ctx.globalAlpha=e.active?.29:.1;ctx.strokeStyle=j%3?'#ff448c':'#b57bff';ctx.lineWidth=.7;ctx.beginPath();for(let k=0;k<=45;k++){const p=curve(e,k/45,j);k?ctx.lineTo(...p):ctx.moveTo(...p)}ctx.stroke();for(let p=0;p<(e.active?4:1);p++){const f=(t*(.075+j*.008)+p*.237+j*.133+ei*.173)%1;const pos=curve(e,e.returning?1-f:f,j);ball(...pos,(p+j)%5===0?2.5:1.1,e.active?.9:.28)}}ball(...e.a,e.active?3:1.6,.9);ball(...e.b,e.active?3.4:1.8,.95)});
|
|
204
|
+
if (agents.length) for(let i=0;i<35;i++){const a=i*2.399+t*.012,r=118+Math.sin(i*7)*6;ball(hub[0]+Math.cos(a)*r,hub[1]+Math.sin(a)*r,i%6===0?1.5:.65,.35+(i%4)*.15)}
|
|
205
|
+
ctx.globalAlpha=1;ctx.globalCompositeOperation='source-over';
|
|
206
|
+
}
|
|
207
|
+
function animate(ms) { draw(ms); requestAnimationFrame(animate); }
|
|
208
|
+
requestAnimationFrame(animate);
|
|
209
|
+
function connect() {
|
|
210
|
+
stream?.close(); const version = ++requestVersion; connected = false; state = null; applyView();
|
|
211
|
+
if (view === 'session' && !sessionId) return;
|
|
212
|
+
const query = view === 'session' ? `?session=${encodeURIComponent(sessionId)}` : '';
|
|
213
|
+
put('#connection', 'connecting');
|
|
214
|
+
// Clear previous-session content immediately; late requests cannot replace the new scope.
|
|
215
|
+
$('main').hidden = true;
|
|
216
|
+
const accept = data => { if (version !== requestVersion) return; state = data; $('main').hidden = false; render(); };
|
|
217
|
+
stream = new EventSource(`/api/stream${query}`);
|
|
218
|
+
stream.addEventListener('state', e => { try { accept(JSON.parse(e.data)); } catch { put('#connection', 'invalid state'); } });
|
|
219
|
+
stream.onopen = () => { connected = true; put('#connection', 'live'); $('.status').classList.remove('offline'); };
|
|
220
|
+
stream.onerror = () => { connected = false; put('#connection', 'reconnecting'); $('.status').classList.add('offline'); };
|
|
221
|
+
fetch(`/api/state${query}`).then(r => { if (!r.ok) throw new Error(); return r.json(); }).then(data => { if (!state) accept(data); }).catch(() => { if (version === requestVersion && !connected) put('#connection', 'server unavailable'); });
|
|
222
|
+
}
|
|
223
|
+
saveView(); connect(); loadSessions(); setInterval(loadSessions, 10000);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
3
|
+
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|
4
|
+
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
|
5
|
+
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
|
|
6
|
+
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
|
|
7
|
+
|
|
8
|
+
// Screen-space ribbon meshes retain their width on GPUs with one-pixel GL lines.
|
|
9
|
+
const STRANDS = 7, STEPS = 56, MAX_PARTICLES = 3000;
|
|
10
|
+
function sample(e, u, strand, time) {
|
|
11
|
+
const dx = e.b[0] - e.a[0], dy = e.b[1] - e.a[1], len = Math.hypot(dx, dy) || 1, v = 1 - u;
|
|
12
|
+
const spread = e.bend + (strand - 3) * 8 + Math.sin(time * .65 + strand * 1.7) * 10;
|
|
13
|
+
const wave = Math.sin(u * Math.PI * 3 + strand * 1.4 + time * .7) * Math.sin(u * Math.PI) * 5;
|
|
14
|
+
const offset = 3 * v * v * u * spread - 3 * v * u * u * spread * .65 + wave;
|
|
15
|
+
return [e.a[0] + dx * u - dy / len * offset, e.a[1] + dy * u + dx / len * offset];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createFlow(fallbackCanvas, onLost) {
|
|
19
|
+
const canvas = document.createElement('canvas');
|
|
20
|
+
canvas.setAttribute('aria-hidden', 'true'); canvas.style.pointerEvents = 'none';
|
|
21
|
+
let renderer, composer;
|
|
22
|
+
try {
|
|
23
|
+
const options = { alpha: true, premultipliedAlpha: true, antialias: false, powerPreference: 'low-power' };
|
|
24
|
+
const context = canvas.getContext('webgl2', options);
|
|
25
|
+
if (!context) throw new Error('WebGL2 unavailable; using Canvas 2D');
|
|
26
|
+
renderer = new THREE.WebGLRenderer({ canvas, context, ...options });
|
|
27
|
+
renderer.setClearColor(0x000000, 0);
|
|
28
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
29
|
+
renderer.toneMappingExposure = 1.15;
|
|
30
|
+
const scene = new THREE.Scene();
|
|
31
|
+
const camera = new THREE.OrthographicCamera(0, 1364, 0, 540, -10, 10);
|
|
32
|
+
camera.position.z = 1;
|
|
33
|
+
composer = new EffectComposer(renderer);
|
|
34
|
+
composer.addPass(new RenderPass(scene, camera));
|
|
35
|
+
const bloom = new UnrealBloomPass(new THREE.Vector2(1364, 540), 1.3, .65, .3);
|
|
36
|
+
// The stock truncated Gaussian still has ~60% weight at its edge, making
|
|
37
|
+
// isolated sparks bloom into boxes. Let each separable kernel decay first.
|
|
38
|
+
for (const material of bloom.separableBlurMaterials) {
|
|
39
|
+
const weights = material.uniforms.gaussianCoefficients.value;
|
|
40
|
+
const sigma = weights.length / 4;
|
|
41
|
+
for (let i = 0; i < weights.length; i++) weights[i] = Math.exp(-.5 * i * i / (sigma * sigma));
|
|
42
|
+
}
|
|
43
|
+
composer.addPass(bloom);
|
|
44
|
+
composer.addPass(new OutputPass());
|
|
45
|
+
// UnrealBloomPass writes opaque alpha. Rebuild coverage from the final,
|
|
46
|
+
// tone-mapped light, with premultiplied RGB for native page compositing.
|
|
47
|
+
// Black becomes fully transparent; even the faint bloom retains soft edges.
|
|
48
|
+
composer.addPass(new ShaderPass({
|
|
49
|
+
uniforms: { tDiffuse: { value: null } },
|
|
50
|
+
vertexShader: `varying vec2 vUv;
|
|
51
|
+
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); }`,
|
|
52
|
+
fragmentShader: `uniform sampler2D tDiffuse; varying vec2 vUv;
|
|
53
|
+
void main() { vec3 light = texture2D(tDiffuse, vUv).rgb;
|
|
54
|
+
float coverage = clamp(max(light.r, max(light.g, light.b)), 0., 1.);
|
|
55
|
+
gl_FragColor = vec4(light, coverage); }`
|
|
56
|
+
}));
|
|
57
|
+
fallbackCanvas.after(canvas);
|
|
58
|
+
const ribbons = new THREE.Group(); scene.add(ribbons);
|
|
59
|
+
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xff3d8b, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false });
|
|
60
|
+
// Explicit billboard UVs avoid driver-dependent point-sprite coordinates.
|
|
61
|
+
const particleGeometry = new THREE.InstancedBufferGeometry();
|
|
62
|
+
particleGeometry.setAttribute('position', new THREE.Float32BufferAttribute([-.5,-.5,0, .5,-.5,0, .5,.5,0, -.5,.5,0], 3));
|
|
63
|
+
particleGeometry.setIndex([0, 1, 2, 0, 2, 3]);
|
|
64
|
+
const positions = new Float32Array(MAX_PARTICLES * 3), sizes = new Float32Array(MAX_PARTICLES), intensities = new Float32Array(MAX_PARTICLES), whites = new Float32Array(MAX_PARTICLES);
|
|
65
|
+
for (const [name, array, size] of [['offset', positions, 3], ['size', sizes, 1], ['intensity', intensities, 1], ['white', whites, 1]]) particleGeometry.setAttribute(name, new THREE.InstancedBufferAttribute(array, size).setUsage(THREE.DynamicDrawUsage));
|
|
66
|
+
const particleMaterial = new THREE.ShaderMaterial({
|
|
67
|
+
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
|
|
68
|
+
side: THREE.DoubleSide,
|
|
69
|
+
vertexShader: `attribute vec3 offset; attribute float size; attribute float intensity; attribute float white;
|
|
70
|
+
varying float power; varying float pale; varying vec2 spriteUv;
|
|
71
|
+
void main() { power = intensity; pale = white; spriteUv = position.xy * 2.;
|
|
72
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(offset + position * size, 1.); }`,
|
|
73
|
+
fragmentShader: `varying float power; varying float pale; varying vec2 spriteUv;
|
|
74
|
+
void main() { float r = length(spriteUv); if (r >= 1.) discard;
|
|
75
|
+
float halo = exp(-r*r*5.) * (1. - smoothstep(.55, 1., r));
|
|
76
|
+
float core = exp(-r*r*36.);
|
|
77
|
+
vec3 pink = mix(vec3(1., .045, .26), vec3(1., .55, .72), pale);
|
|
78
|
+
gl_FragColor = vec4((pink * halo * .65 + vec3(1.8) * core) * power, halo); }`
|
|
79
|
+
});
|
|
80
|
+
const particles = new THREE.Mesh(particleGeometry, particleMaterial); particles.frustumCulled = false; scene.add(particles);
|
|
81
|
+
let dpr = 1, count = 0, budget = 1800, slow = 0, dimensions = '';
|
|
82
|
+
function particle(x, y, size, intensity, white = 0) {
|
|
83
|
+
if (count >= MAX_PARTICLES) return;
|
|
84
|
+
positions[count * 3] = x; positions[count * 3 + 1] = y; sizes[count] = size; intensities[count] = intensity; whites[count] = white; count++;
|
|
85
|
+
}
|
|
86
|
+
function dispose() {
|
|
87
|
+
for (const mesh of [...ribbons.children]) { mesh.geometry.dispose(); mesh.material.dispose(); ribbons.remove(mesh); }
|
|
88
|
+
particleGeometry.dispose(); particleMaterial.dispose(); lineMaterial.dispose(); composer.dispose(); renderer.dispose(); canvas.remove();
|
|
89
|
+
}
|
|
90
|
+
canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); dispose(); onLost(); }, { once: true });
|
|
91
|
+
return {
|
|
92
|
+
resize(width, height, ratio) {
|
|
93
|
+
const displayWidth = canvas.parentElement.getBoundingClientRect().width || width;
|
|
94
|
+
const key = `${width}:${height}:${ratio}:${displayWidth}`;
|
|
95
|
+
if (dimensions === key) return;
|
|
96
|
+
dimensions = key;
|
|
97
|
+
dpr = Math.min(2, ratio); renderer.setPixelRatio(dpr); renderer.setSize(width, height, false);
|
|
98
|
+
composer.setPixelRatio(dpr); composer.setSize(width, height);
|
|
99
|
+
camera.right = width; camera.bottom = height; camera.updateProjectionMatrix();
|
|
100
|
+
budget = displayWidth < 700 || navigator.hardwareConcurrency <= 4 ? 900 : 2400;
|
|
101
|
+
},
|
|
102
|
+
draw(edges, hub, visible, time) {
|
|
103
|
+
const start = performance.now(); count = 0;
|
|
104
|
+
const needed = edges.length * STRANDS;
|
|
105
|
+
while (ribbons.children.length > needed) { const mesh = ribbons.children.at(-1); ribbons.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); }
|
|
106
|
+
while (ribbons.children.length < needed) {
|
|
107
|
+
const geometry = new THREE.BufferGeometry(), vertices = new Float32Array(STEPS * 6 * 3);
|
|
108
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3).setUsage(THREE.DynamicDrawUsage));
|
|
109
|
+
const mesh = new THREE.Mesh(geometry, lineMaterial.clone()); mesh.frustumCulled = false; ribbons.add(mesh);
|
|
110
|
+
}
|
|
111
|
+
edges.forEach((e, index) => {
|
|
112
|
+
const waiting = e.status === 'waiting', active = e.active;
|
|
113
|
+
const power = active ? (waiting ? .85 + Math.sin(time * 2.4) * .2 : 1.1) : .19;
|
|
114
|
+
for (let j = 0; j < STRANDS; j++) {
|
|
115
|
+
const mesh = ribbons.children[index * STRANDS + j], array = mesh.geometry.attributes.position.array;
|
|
116
|
+
mesh.material.color.setRGB(power * 1.25, power * (waiting ? .8 : .07), power * (waiting ? 1 : .35));
|
|
117
|
+
const width = active ? 1.6 : .8;
|
|
118
|
+
let prev = sample(e, 0, j, time);
|
|
119
|
+
for (let k = 0; k < STEPS; k++) {
|
|
120
|
+
const next = sample(e, (k + 1) / STEPS, j, time), dx = next[0] - prev[0], dy = next[1] - prev[1], len = Math.hypot(dx, dy) || 1;
|
|
121
|
+
const nx = -dy / len * width / 2, ny = dx / len * width / 2;
|
|
122
|
+
array.set([prev[0]+nx,prev[1]+ny,0, prev[0]-nx,prev[1]-ny,0, next[0]+nx,next[1]+ny,0, next[0]+nx,next[1]+ny,0, prev[0]-nx,prev[1]-ny,0, next[0]-nx,next[1]-ny,0], k * 18);
|
|
123
|
+
prev = next;
|
|
124
|
+
}
|
|
125
|
+
mesh.geometry.attributes.position.needsUpdate = true;
|
|
126
|
+
const density = Math.max(1, Math.min(active ? 8 : 2, Math.floor((budget - 90) / Math.max(1, needed))));
|
|
127
|
+
for (let p = 0; p < density; p++) {
|
|
128
|
+
const phase = (time * (waiting ? .12 : active ? .22 : .035) + p / density + j * .137 + index * .173) % 1;
|
|
129
|
+
const xy = sample(e, e.returning ? 1 - phase : phase, j, time);
|
|
130
|
+
particle(...xy, p % 5 ? 8 : 13, active ? 1.65 : .5, waiting ? 1 : .15);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Large soft dock flashes, with a hot white nucleus at the card border.
|
|
134
|
+
for (const xy of [e.a, e.b]) {
|
|
135
|
+
particle(...xy, active ? 54 : 25, power * .9, waiting ? 1 : 0);
|
|
136
|
+
particle(...xy, active ? 20 + Math.sin(time * 3 + index) * 3 : 9, power * 3.8, .5);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (visible) {
|
|
140
|
+
particle(...hub, 380, .22, 0);
|
|
141
|
+
for (let i = 0; i < 70; i++) {
|
|
142
|
+
const a = i * 2.399 + time * .025, r = 117 + Math.sin(i * 7 + time * .4) * 4;
|
|
143
|
+
particle(hub[0] + Math.cos(a) * r, hub[1] + Math.sin(a) * r, i % 7 ? 9 : 18, i % 7 ? .65 : 1.2);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
particleGeometry.instanceCount = count;
|
|
147
|
+
for (const attribute of Object.values(particleGeometry.attributes)) if (attribute.isInstancedBufferAttribute) attribute.needsUpdate = true;
|
|
148
|
+
composer.render();
|
|
149
|
+
// Reduce density on sustained slow frames, not on a single shader compilation.
|
|
150
|
+
if (performance.now() - start > 28) { if (++slow > 25) { budget = Math.max(350, budget * .8); slow = 0; } } else slow = Math.max(0, slow - 1);
|
|
151
|
+
},
|
|
152
|
+
dispose
|
|
153
|
+
};
|
|
154
|
+
} catch (error) { composer?.dispose(); renderer?.dispose(); canvas.remove(); throw error; }
|
|
155
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>agent flow</title><link rel="stylesheet" href="/style.css"><script type="importmap">{"imports":{"three":"/vendor/three/three.module.min.js","three/addons/":"/vendor/three/"}}</script><script src="/app.js" defer></script></head>
|
|
4
|
+
<body>
|
|
5
|
+
<header><div class="heading"><h1>agent flow<span> / </span><span id="title">connecting…</span></h1><small id="subtitle">opencode · orchestration topology</small></div><div class="controls"><div class="view-switch" role="group" aria-label="View mode"><button id="view-all" type="button">all sessions</button><button id="view-session" type="button">session</button></div><input id="session-search" type="search" placeholder="search sessions…" aria-label="Search sessions" hidden><label for="root">session</label><select id="root" aria-label="Root session"></select><button id="motion" type="button" aria-pressed="false">pause motion</button><div class="status"><i></i><span id="connection">connecting</span><span id="counts"></span></div></div></header>
|
|
6
|
+
<section id="session-picker" hidden aria-label="Select session"><h2>// select a session</h2><p>One root. Every agent in its subtree. Live metrics, scoped to your session.</p><div id="session-cards"></div></section>
|
|
7
|
+
<main><section class="scene"><div class="scene-label">// execution graph <span id="graphMeta"></span></div><div class="viewport"><div class="network"><canvas id="wires" aria-label="Agent connections"></canvas><div id="nodes"></div><div class="hub" hidden><svg viewBox="0 0 236 236" aria-hidden="true"><circle cx="118" cy="118" r="114" fill="none" stroke="#c470983f" stroke-dasharray="2 7"/><circle cx="118" cy="118" r="101" fill="none" stroke="#573044" stroke-width="3"/><circle id="contextRing" cx="118" cy="118" r="101" fill="none" stroke="#ff5c9d" stroke-width="3" transform="rotate(-90 118 118)"/></svg><h2 id="lead"></h2><p id="context"></p><b id="model"></b><small id="leadTool"></small></div></div></div><div id="empty" hidden></div><div class="scene-foot"><span id="sceneSummary"></span><span id="transport">white → pink / token transport</span></div></section>
|
|
8
|
+
<section class="dash" aria-label="Metrics"><article class="panel"><h3>// event log</h3><div id="events" class="scroll"></div></article><article class="panel"><h3>// tokens / hour</h3><div id="hourly" class="scroll"></div></article><article class="panel"><h3>// cache hit</h3><div class="gauge"><svg viewBox="0 0 100 100" aria-hidden="true"><circle cx="50" cy="50" r="43" fill="none" stroke="#442032" stroke-width="5"/><circle id="cacheRing" cx="50" cy="50" r="43" fill="none" stroke="#ff6fae" stroke-width="5"/></svg><div class="value"><span id="cacheValue">—</span><small>hit / window</small></div></div></article><article class="panel"><h3>// activity heat</h3><div id="heat" class="scroll"></div><div class="legend">quiet ░▒▓█ active · 12 × 5 min</div></article><article class="panel"><h3>// tokens today</h3><div class="big" id="total">—</div><div class="estimate" id="estimate"></div><div id="today" class="breakdown scroll"></div></article><article class="panel"><h3>// ledger</h3><div id="ledger" class="ledger scroll"></div></article></section></main>
|
|
9
|
+
<footer><span id="health">transport / public HTTP + SSE · read-only</span><span id="clock"></span></footer>
|
|
10
|
+
</body></html>
|
package/public/style.css
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
:root{--pink:#ff3d8b;--soft:#ff6fae;--muted:#ac8d9d;--line:#4c2537}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:#0d0609;color:#f5e5ec;font:11px 'JetBrains Mono','SFMono-Regular',Consolas,monospace;background-image:linear-gradient(#ff80b005 1px,transparent 1px),linear-gradient(90deg,#ff80b005 1px,transparent 1px);background-size:40px 40px}header{min-height:96px;margin:0 38px;border-bottom:1px solid #472033;display:flex;justify-content:space-between;align-items:center;gap:24px}.heading{min-width:0;flex:1}h1{font-size:17px;font-weight:500;letter-spacing:-.6px;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}h1>span:first-child{color:var(--soft)}header small{display:block;margin-top:9px;color:var(--muted);font-size:10px}.controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end;max-width:520px}.controls label{color:var(--muted);font-size:9px}select,button{font:10px monospace;background:#1b0d16;color:#ddbdce;border:1px solid #654152;padding:7px;max-width:230px;cursor:pointer}select:focus-visible,button:focus-visible{outline:2px solid var(--soft);outline-offset:3px}button:hover{background:#392032}.status{color:var(--soft);font-size:10px;width:100%;text-align:right;display:flex;justify-content:flex-end;align-items:center;gap:10px}.status i{display:inline-block;width:5px;height:5px;background:var(--pink);box-shadow:0 0 12px var(--pink);border-radius:50%}.status.offline{color:#bba1cb}.status.offline i{background:#aaa;box-shadow:none}.scene{height:540px;position:relative;overflow:hidden;background:radial-gradient(ellipse at 50% 48%,#8c174e22,transparent 60%)}.viewport{position:absolute;inset:0;overflow:hidden}.network{position:absolute;width:1364px;height:540px;left:50%;transform:translateX(-50%);transform-origin:50% 0}canvas{position:absolute;inset:0;width:100%;height:100%}.node{position:absolute;width:200px;background:#160b12f5;border:1px solid #654152;z-index:2;box-shadow:0 8px 30px #0005;transition:opacity .25s,border-color .25s,transform .3s cubic-bezier(.2,0,0,1);animation:enter .3s ease-out}.cap{height:27px;background:#392b34;color:#e1d3db;padding:5px 9px;display:flex;justify-content:space-between;gap:8px;align-items:center;font-weight:600}.cap strong{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag{font-size:8px;font-weight:500;opacity:.8;flex-shrink:0}.rows{padding:8px 9px;line-height:21px;color:#b194a4;font-size:9px}.row{display:flex;gap:10px;justify-content:space-between}.row span:last-child{color:#f5d4e4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:132px}.node.working{border-color:#b63b70}.node.working .cap{background:#ff508f;color:#240d18}.node.waiting{border-color:#f5eaf0;box-shadow:0 0 26px #ff3d8b16}.node.waiting .cap{background:#f5eaf0;color:#25121c}.node.done{opacity:.67}.hub{position:absolute;left:564px;top:152px;width:236px;height:236px;z-index:3;text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:50%;background:radial-gradient(circle,#30101f 5%,#180c15 65%,transparent 66%)}.hub svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}#contextRing{filter:drop-shadow(0 0 5px #ff3d8b);transition:stroke-dasharray .35s}.hub h2{font-size:35px;max-width:185px;overflow:hidden;text-overflow:ellipsis;letter-spacing:-2px;margin:0 0 9px;font-weight:800;text-shadow:0 0 32px #ff77ae66}.hub p{font-size:9px;color:#d3a9bb;margin:0}.hub b{font-weight:400;font-size:9px;color:var(--soft);margin-top:15px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hub small{font-size:8px;color:var(--muted);margin-top:10px;max-width:170px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.scene-label{position:absolute;left:38px;top:20px;color:#af8a9d;font-size:9px;letter-spacing:1px;z-index:4}.scene-foot{position:absolute;bottom:16px;left:38px;right:38px;display:flex;gap:20px;justify-content:space-between;color:#ba8fa2;font-size:9px;z-index:4}.dash{display:grid;grid-template-columns:1.35fr 1fr .86fr 1fr 1.05fr 1fr;gap:10px;margin:0 28px 25px}.panel{height:221px;min-width:0;border:1px solid #492335;background:#160b11d9;padding:16px 13px;overflow:hidden}.panel h3{color:#c09aab;font-size:10px;font-weight:400;margin:0 0 20px}.scroll{overflow:auto;max-height:158px;scrollbar-width:thin;scrollbar-color:#65314b transparent}.event{font-size:9px;margin-bottom:11px;white-space:nowrap;color:#c3a7b7;overflow:hidden;text-overflow:ellipsis}.event em{font-style:normal;color:var(--soft);display:inline-block;width:42px}.event time{font-size:8px;color:var(--muted);margin-right:7px}.bars{display:flex;align-items:center;margin-bottom:6px;gap:7px;color:#bd94a8;font-size:8px}.bars time{min-width:14px}.bars .track{flex:1}.bars i{display:block;height:5px;background:linear-gradient(90deg,#782549,#ff6ba7);box-shadow:0 0 10px #ff3d8b22;min-width:1px}.bars b{font-weight:400;width:37px;text-align:right}.gauge{width:111px;height:111px;margin:0 auto;position:relative}.gauge svg{width:100%;height:100%;transform:rotate(-90deg)}.gauge .value{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:25px}.gauge small{font-size:8px;color:#b28b9d;margin-top:4px}.heat-row{display:grid;grid-template-columns:43px repeat(12,1fr);gap:3px;margin-bottom:5px;align-items:center}.heat-row span{font-size:7px;overflow:hidden;text-overflow:ellipsis;color:#bc98ab}.heat-row i{height:10px;background:#ff3d8b}.legend{color:#b38ca0;font-size:8px;margin-top:12px}#heat{max-height:128px}.big{font-size:30px;color:#ffe7f1;letter-spacing:-2px;margin-top:-4px}.estimate{color:var(--soft);font-size:10px;margin:7px 0}.breakdown{font-size:8px;color:#c09aae;line-height:19px;max-height:90px}.breakdown div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ledger{font-size:9px;color:#c4a3b5;line-height:25px;white-space:nowrap}.ledger b{font-weight:400;color:#ff6fae}.ledger .idle,.ledger .done{color:#bba1cb}.ledger .waiting{color:#fff}footer{margin:0 38px 22px;color:#a07c8f;display:flex;gap:20px;justify-content:space-between;font-size:9px}#empty{position:absolute;inset:0;display:grid;place-items:center;color:#c19aaf;font-size:13px}.muted{color:var(--muted);font-size:10px}.approval .cap{background:#f5eaf0;color:#25121c}@keyframes enter{from{opacity:0;translate:0 8px}to{opacity:1;translate:0 0}}@media(max-width:1100px){.dash{grid-template-columns:repeat(3,1fr)}header{margin:0 24px}.scene-label{left:24px}}@media(max-width:650px){header{padding:18px 0;align-items:flex-start;flex-direction:column;gap:16px}.controls{justify-content:flex-start;max-width:100%}.status{justify-content:flex-start;text-align:left}h1{font-size:14px;max-width:calc(100vw - 48px)}select{max-width:190px}.scene{height:460px}.viewport{overflow-x:auto}.network{left:0;transform:none}.dash{grid-template-columns:1fr;margin:0 18px 20px}.panel{height:215px}.scene-foot{left:24px;right:24px;flex-direction:column;gap:6px}footer{margin:0 24px 20px;flex-direction:column}.scene-label{font-size:8px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
|
|
2
|
+
@media(min-width:651px){header{min-height:76px}}
|
|
3
|
+
.bars{height:8px;line-height:8px;margin-bottom:5px}
|
|
4
|
+
.view-switch{display:flex;flex-shrink:0}.view-switch button{border-radius:0}.view-switch button+button{border-left:0}.view-switch button[aria-pressed=true]{color:#fff;background:#64243f;border-color:#d36b99}#session-search{font:10px monospace;width:145px;min-width:0;padding:7px;color:#f5e5ec;background:#1b0d16;border:1px solid #654152}#session-search:focus-visible{outline:2px solid var(--soft);outline-offset:3px}.controls{max-width:690px}body[data-view=session] header{min-height:102px}#session-picker{padding:35px 38px;min-height:70vh}#session-picker h2{font-size:15px;font-weight:500;color:var(--soft)}#session-picker>p{color:#c9abbc;line-height:1.6}#session-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,360px),1fr));gap:16px;margin-top:24px}.session-card{display:flex;flex-direction:column;align-items:flex-start;text-align:left;gap:13px;max-width:none;width:100%;padding:20px;background:#160b12;line-height:1.5}.session-card strong{font-size:12px;font-weight:500;overflow-wrap:anywhere}.session-card span{color:#c9abbc;overflow-wrap:anywhere}.session-card .working{color:#ff85b9}@media(max-width:900px){header{flex-wrap:wrap;padding:16px 0;gap:14px}.controls{max-width:100%;justify-content:flex-start}.status{justify-content:flex-start;text-align:left}#session-picker{padding:24px}}
|
|
5
|
+
#group-sessions:popover-open{max-height:min(320px,calc(100vh - 24px))}
|
|
6
|
+
.node[role=button]{cursor:pointer}.node[role=button]:focus-visible{outline:2px solid var(--soft);outline-offset:4px}.cap strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cap strong small{font-size:10px;white-space:nowrap}.cap .tag{flex-shrink:0}#group-sessions{position:fixed;inset:auto;margin:0;width:min(410px,calc(100vw - 24px));max-height:min(420px,calc(100vh - 24px));padding:14px;border:1px solid #ac567c;background:#1b0d16;color:#f5e5ec;box-shadow:0 14px 60px #000c;font:11px monospace;overflow:auto}#group-sessions::backdrop{background:transparent}.popover-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.popover-heading button{padding:4px 10px}.session-item{padding:12px 0;border-top:1px solid var(--line)}.session-item>strong{display:block;overflow-wrap:anywhere;font-weight:500;line-height:1.5}.session-item>div{display:flex;gap:12px;flex-wrap:wrap;margin-top:7px;color:#c9abbc}.session-item .working{color:#ff85b9}.session-item .waiting{color:white}.session-item .done,.session-item .idle{color:#c9abbc}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2010-2025 three.js authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|