@getmegabrain/cli 0.1.5 → 0.1.7
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/dist/device-auth.js +29 -17
- package/dist/index.js +9 -0
- package/dist/science/agent/agent.js +99 -0
- package/dist/science/agent/model.js +46 -0
- package/dist/science/index.js +77 -0
- package/dist/science/kernel-protocol.js +7 -0
- package/dist/science/kernel.js +159 -0
- package/dist/science/kernel_driver.py +93 -0
- package/dist/science/nonce.js +53 -0
- package/dist/science/pages.js +346 -0
- package/dist/science/server.js +353 -0
- package/dist/science/session-client.js +165 -0
- package/dist/science/store.js +160 -0
- package/package.json +3 -3
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-rendered HTML for the sign-in hand-off. Mirrors the Claude Science screens
|
|
3
|
+
* (docs/megabrain-science/reference/claude-science-flow.md §1.1), MegaBrain-branded. These
|
|
4
|
+
* are deliberately dependency-free inline pages — the rich session UI (reused
|
|
5
|
+
* `cloud-agent-next`) mounts on the shell later; this is just the auth entry.
|
|
6
|
+
*/
|
|
7
|
+
function shell(title, body) {
|
|
8
|
+
return `<!doctype html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
13
|
+
<title>${title}</title>
|
|
14
|
+
<style>
|
|
15
|
+
:root { color-scheme: light dark; }
|
|
16
|
+
* { box-sizing: border-box; }
|
|
17
|
+
body {
|
|
18
|
+
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
19
|
+
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
20
|
+
background: #fafafa; color: #111;
|
|
21
|
+
}
|
|
22
|
+
@media (prefers-color-scheme: dark) { body { background: #0a0a0a; color: #ededed; } }
|
|
23
|
+
.card {
|
|
24
|
+
width: min(92vw, 420px); padding: 40px 36px; border-radius: 20px; text-align: center;
|
|
25
|
+
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 12px 40px rgba(0,0,0,.08);
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: dark) { .card { background: #161616; box-shadow: none; border: 1px solid #262626; } }
|
|
28
|
+
.mark { width: 56px; height: 56px; margin: 0 auto 20px; border-radius: 14px;
|
|
29
|
+
background: linear-gradient(135deg, #e8623a, #c8492a); display: grid; place-items: center;
|
|
30
|
+
color: #fff; font-weight: 700; font-size: 20px; }
|
|
31
|
+
h1 { font-size: 22px; margin: 0 0 6px; letter-spacing: -.01em; }
|
|
32
|
+
.beta { font-size: 12px; color: #888; margin-bottom: 24px; }
|
|
33
|
+
p { color: #555; margin: 0 0 24px; }
|
|
34
|
+
@media (prefers-color-scheme: dark) { p { color: #a3a3a3; } }
|
|
35
|
+
code { background: rgba(0,0,0,.06); padding: 1px 6px; border-radius: 5px; font-size: .9em; }
|
|
36
|
+
@media (prefers-color-scheme: dark) { code { background: rgba(255,255,255,.1); } }
|
|
37
|
+
a.btn, button.btn {
|
|
38
|
+
display: block; width: 100%; padding: 12px 16px; margin-top: 10px; border-radius: 12px;
|
|
39
|
+
font: inherit; font-weight: 600; cursor: pointer; border: 1px solid transparent;
|
|
40
|
+
text-decoration: none; text-align: center;
|
|
41
|
+
}
|
|
42
|
+
.btn-primary { background: #111; color: #fff; }
|
|
43
|
+
@media (prefers-color-scheme: dark) { .btn-primary { background: #ededed; color: #111; } }
|
|
44
|
+
.btn-ghost { background: transparent; border-color: #d4d4d4; color: inherit; }
|
|
45
|
+
@media (prefers-color-scheme: dark) { .btn-ghost { border-color: #333; } }
|
|
46
|
+
.status { margin-top: 18px; font-size: 13px; color: #888; min-height: 1.5em; }
|
|
47
|
+
.status a { color: inherit; }
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<div class="card">${body}</div>
|
|
52
|
+
</body>
|
|
53
|
+
</html>`;
|
|
54
|
+
}
|
|
55
|
+
/** Screen 01 — the nonce landing that authorizes this browser for this daemon. */
|
|
56
|
+
export function nonceLandingPage(nonce) {
|
|
57
|
+
return shell('Sign in — MegaBrain Science', `<div class="mark">MB</div>
|
|
58
|
+
<h1>Sign in to MegaBrain Science</h1>
|
|
59
|
+
<p>Click below to finish signing in on this browser. This link works once, and expires
|
|
60
|
+
3 minutes after it was printed — if the button fails, run
|
|
61
|
+
<code>megabrain science url</code> for a fresh link.</p>
|
|
62
|
+
<a class="btn btn-primary" href="/login?nonce=${encodeURIComponent(nonce)}">Sign in</a>`);
|
|
63
|
+
}
|
|
64
|
+
/** Shown when a nonce link is reused/expired/unknown. */
|
|
65
|
+
export function nonceInvalidPage() {
|
|
66
|
+
return shell('Link expired — MegaBrain Science', `<div class="mark">MB</div>
|
|
67
|
+
<h1>This link has expired</h1>
|
|
68
|
+
<p>Sign-in links work once and expire after 3 minutes. Run
|
|
69
|
+
<code>megabrain science url</code> in your terminal for a fresh link.</p>`);
|
|
70
|
+
}
|
|
71
|
+
/** Screen 02 — the login card (MegaBrain OAuth / paste code). */
|
|
72
|
+
export function loginPage() {
|
|
73
|
+
return shell('MegaBrain Science', `<div class="mark">MB</div>
|
|
74
|
+
<h1>MegaBrain Science</h1>
|
|
75
|
+
<div class="beta">Beta</div>
|
|
76
|
+
<button class="btn btn-primary" id="signin">Sign in with MegaBrain</button>
|
|
77
|
+
<button class="btn btn-ghost" id="paste">Paste code instead</button>
|
|
78
|
+
<div class="status" id="status"></div>
|
|
79
|
+
<script>
|
|
80
|
+
const statusEl = document.getElementById('status');
|
|
81
|
+
async function start() {
|
|
82
|
+
document.getElementById('signin').disabled = true;
|
|
83
|
+
statusEl.textContent = 'Starting sign-in…';
|
|
84
|
+
const res = await fetch('/api/signin', { method: 'POST' });
|
|
85
|
+
if (!res.ok) { statusEl.textContent = 'Could not start sign-in. Try again.'; return; }
|
|
86
|
+
const { verificationUrl, code } = await res.json();
|
|
87
|
+
statusEl.innerHTML = 'Confirm code <code>' + code + '</code> at <a href="' +
|
|
88
|
+
verificationUrl + '" target="_blank" rel="noreferrer">' + verificationUrl + '</a>';
|
|
89
|
+
poll();
|
|
90
|
+
}
|
|
91
|
+
async function poll() {
|
|
92
|
+
const res = await fetch('/api/auth/status');
|
|
93
|
+
const s = await res.json();
|
|
94
|
+
if (s.signedIn) { statusEl.textContent = 'Signed in. Opening…'; location.href = '/'; return; }
|
|
95
|
+
setTimeout(poll, 2000);
|
|
96
|
+
}
|
|
97
|
+
document.getElementById('signin').addEventListener('click', start);
|
|
98
|
+
// The code is minted server-side and shown here, so "paste" starts the same flow.
|
|
99
|
+
document.getElementById('paste').addEventListener('click', start);
|
|
100
|
+
</script>`);
|
|
101
|
+
}
|
|
102
|
+
function esc(s) {
|
|
103
|
+
return s.replace(/[&<>"']/g, c => c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Home — Projects + Recent sessions (#270). A thin server-rendered placeholder that reads the
|
|
107
|
+
* daemon's workspace API; the reused cloud-agent-next UI replaces the markup later, but the
|
|
108
|
+
* `/api/*` contract it exercises here is the durable part.
|
|
109
|
+
*/
|
|
110
|
+
export function homePage(userEmail) {
|
|
111
|
+
return shell('MegaBrain Science', `<div class="mark">MB</div>
|
|
112
|
+
<h1>MegaBrain Science</h1>
|
|
113
|
+
<div class="beta">${esc(userEmail)}</div>
|
|
114
|
+
<div style="text-align:left;margin-top:8px">
|
|
115
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
116
|
+
<strong>Projects</strong>
|
|
117
|
+
<button class="btn btn-ghost" style="width:auto;padding:6px 12px;margin:0" id="new">+ New project</button>
|
|
118
|
+
</div>
|
|
119
|
+
<div id="projects" class="status">Loading…</div>
|
|
120
|
+
<strong style="display:block;margin-top:20px">Recent sessions</strong>
|
|
121
|
+
<div id="recent" class="status"></div>
|
|
122
|
+
</div>
|
|
123
|
+
<button class="btn btn-ghost" id="out" style="margin-top:20px">Sign out</button>
|
|
124
|
+
<script>
|
|
125
|
+
async function load() {
|
|
126
|
+
const [projects, recent] = await Promise.all([
|
|
127
|
+
fetch('/api/projects').then(r => r.json()),
|
|
128
|
+
fetch('/api/sessions/recent').then(r => r.json()),
|
|
129
|
+
]);
|
|
130
|
+
document.getElementById('projects').innerHTML = projects.length
|
|
131
|
+
? projects.map(p => '<a href="/projects/' + p.id + '" style="display:block;color:inherit">' +
|
|
132
|
+
p.name + ' · ' + p.sessionCount + ' session(s)</a>').join('')
|
|
133
|
+
: 'No projects yet.';
|
|
134
|
+
document.getElementById('recent').innerHTML = recent.length
|
|
135
|
+
? recent.map(s => '<a href="/projects/' + s.projectId + '/frames/' + s.id +
|
|
136
|
+
'" style="display:block;color:inherit">' + s.title + '</a>').join('')
|
|
137
|
+
: 'No sessions yet.';
|
|
138
|
+
}
|
|
139
|
+
document.getElementById('new').addEventListener('click', async () => {
|
|
140
|
+
const name = prompt('Project name:'); if (!name) return;
|
|
141
|
+
const p = await fetch('/api/projects', { method: 'POST',
|
|
142
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name }) }).then(r => r.json());
|
|
143
|
+
location.href = '/projects/' + p.id;
|
|
144
|
+
});
|
|
145
|
+
document.getElementById('out').addEventListener('click', async () => {
|
|
146
|
+
await fetch('/api/signout', { method: 'POST' }); location.href = '/login';
|
|
147
|
+
});
|
|
148
|
+
load();
|
|
149
|
+
</script>`);
|
|
150
|
+
}
|
|
151
|
+
/** A single project: its sessions + New session (#271). Placeholder markup, real API. */
|
|
152
|
+
export function projectPage(projectId) {
|
|
153
|
+
return shell('Project — MegaBrain Science', `<div class="mark">MB</div>
|
|
154
|
+
<h1 id="title">Project</h1>
|
|
155
|
+
<div style="text-align:left">
|
|
156
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
157
|
+
<strong>Sessions</strong>
|
|
158
|
+
<button class="btn btn-ghost" style="width:auto;padding:6px 12px;margin:0" id="new">+ New</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div id="sessions" class="status">Loading…</div>
|
|
161
|
+
</div>
|
|
162
|
+
<a class="btn btn-ghost" href="/" style="margin-top:20px">← Home</a>
|
|
163
|
+
<script>
|
|
164
|
+
const pid = ${JSON.stringify(projectId)};
|
|
165
|
+
async function load() {
|
|
166
|
+
const p = await fetch('/api/projects/' + pid).then(r => r.json());
|
|
167
|
+
if (p.project) document.getElementById('title').textContent = p.project.name;
|
|
168
|
+
document.getElementById('sessions').innerHTML = (p.sessions || []).length
|
|
169
|
+
? p.sessions.map(s => '<a href="/projects/' + pid + '/frames/' + s.id +
|
|
170
|
+
'" style="display:block;color:inherit">' + s.title + '</a>').join('')
|
|
171
|
+
: 'No sessions yet.';
|
|
172
|
+
}
|
|
173
|
+
document.getElementById('new').addEventListener('click', async () => {
|
|
174
|
+
const s = await fetch('/api/projects/' + pid + '/sessions', { method: 'POST',
|
|
175
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'New session' }) }).then(r => r.json());
|
|
176
|
+
location.href = '/projects/' + pid + '/frames/' + s.id;
|
|
177
|
+
});
|
|
178
|
+
load();
|
|
179
|
+
</script>`);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `/start` — the 5-step first-run onboarding wizard (reference §1.3, #286): scientific-web
|
|
183
|
+
* egress → connectors & skills → research profile → first task. Server-rendered; persists via
|
|
184
|
+
* `/api/onboarding` and creates the first project/session on "Start".
|
|
185
|
+
*/
|
|
186
|
+
export function startWizardPage(d) {
|
|
187
|
+
const checks = (items, on, group) => items
|
|
188
|
+
.map(i => `<label style="display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid rgba(128,128,128,.15)">
|
|
189
|
+
<span>${esc(i)}</span>
|
|
190
|
+
<input type="checkbox" data-group="${group}" value="${esc(i)}" ${on.includes(i) ? 'checked' : ''} />
|
|
191
|
+
</label>`)
|
|
192
|
+
.join('');
|
|
193
|
+
const TASKS = [
|
|
194
|
+
[
|
|
195
|
+
'Map the recent literature of your subfield',
|
|
196
|
+
'Pull the last year of papers, extract key quantitative findings, and write a structured review with citations and a suggested next experiment.',
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
'Run a first-pass analysis on a dataset you already have',
|
|
200
|
+
'Point the agent at your data — it runs QC and a first analysis pass and returns figures plus a written summary.',
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
'Turn a workflow you repeat by hand into a reusable pipeline',
|
|
204
|
+
'Describe the steps you do manually — the agent builds a documented, rerunnable pipeline and demonstrates it end-to-end.',
|
|
205
|
+
],
|
|
206
|
+
];
|
|
207
|
+
const taskCards = TASKS.map(([t, sub], i) => `<label style="display:block;text-align:left;border:1px solid rgba(128,128,128,.25);border-radius:12px;padding:12px;margin:8px 0;cursor:pointer">
|
|
208
|
+
<input type="radio" name="task" value="${esc(t)}" ${i === 0 ? 'checked' : ''} />
|
|
209
|
+
<strong>${esc(t)}</strong><br /><span class="status">${esc(sub)}</span>
|
|
210
|
+
</label>`).join('');
|
|
211
|
+
return shell('Set up — MegaBrain Science', `<div style="text-align:left">
|
|
212
|
+
<section data-step="0"><h1>Connect to the scientific web</h1>
|
|
213
|
+
<p>Choose which sources the agent can pull papers, sequences, and structures from.</p>
|
|
214
|
+
${checks(d.networkCategories, d.network, 'network')}</section>
|
|
215
|
+
<section data-step="1" hidden><h1>Connectors & skills</h1>
|
|
216
|
+
<p>Work directly with databases, research tools, and open science models.</p>
|
|
217
|
+
${checks(d.connectorCatalog, d.connectors, 'connectors')}</section>
|
|
218
|
+
<section data-step="2" hidden><h1>What do you work on?</h1>
|
|
219
|
+
<p>Describe your field, methods, and current project.</p>
|
|
220
|
+
<textarea id="profile" rows="5" style="width:100%" placeholder="e.g. I'm a postdoc doing single-cell RNA-seq on zebrafish heart regeneration — mostly Seurat + our own Nextflow pipelines.">${esc(d.profile)}</textarea></section>
|
|
221
|
+
<section data-step="3" hidden><h1>Where should we start?</h1>
|
|
222
|
+
<p>Pick a first task, or describe your own.</p>${taskCards}
|
|
223
|
+
<input id="customTask" style="width:100%;margin-top:8px" placeholder="Describe your own first task (optional)" /></section>
|
|
224
|
+
<div style="display:flex;justify-content:space-between;margin-top:20px">
|
|
225
|
+
<button class="btn btn-ghost" style="width:auto;padding:8px 16px;margin:0" id="back">Back</button>
|
|
226
|
+
<span class="status" id="dots"></span>
|
|
227
|
+
<button class="btn btn-primary" style="width:auto;padding:8px 16px;margin:0" id="next">Continue</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<script>
|
|
231
|
+
const steps = [...document.querySelectorAll('section[data-step]')];
|
|
232
|
+
let i = 0;
|
|
233
|
+
const gather = g => [...document.querySelectorAll('input[data-group="' + g + '"]:checked')].map(e => e.value);
|
|
234
|
+
function render() {
|
|
235
|
+
steps.forEach((s, n) => (s.hidden = n !== i));
|
|
236
|
+
document.getElementById('back').style.visibility = i === 0 ? 'hidden' : 'visible';
|
|
237
|
+
document.getElementById('next').textContent = i === steps.length - 1 ? 'Start' : 'Continue';
|
|
238
|
+
document.getElementById('dots').textContent = (i + 1) + ' / ' + steps.length;
|
|
239
|
+
}
|
|
240
|
+
document.getElementById('back').addEventListener('click', () => { if (i > 0) { i--; render(); } });
|
|
241
|
+
document.getElementById('next').addEventListener('click', async () => {
|
|
242
|
+
if (i < steps.length - 1) { i++; render(); return; }
|
|
243
|
+
const profile = document.getElementById('profile').value;
|
|
244
|
+
await fetch('/api/onboarding', { method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({ network: gather('network'), connectors: gather('connectors'), profile, onboarded: true }) });
|
|
246
|
+
const task = document.getElementById('customTask').value.trim() ||
|
|
247
|
+
(document.querySelector('input[name="task"]:checked') || {}).value || '';
|
|
248
|
+
const r = await fetch('/api/onboarding/start', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
249
|
+
body: JSON.stringify({ task, profile }) }).then(r => r.json());
|
|
250
|
+
location.href = '/projects/' + r.projectId + '/frames/' + r.sessionId;
|
|
251
|
+
});
|
|
252
|
+
render();
|
|
253
|
+
</script>`);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The working session — a full-window Claude-Science-like workspace (#271/#276/#277): a left
|
|
257
|
+
* sidebar of sessions, a chat transcript of the agent's steps, and an "Ask anything" composer
|
|
258
|
+
* that drives the agent (Gateway → local kernel). Full-page layout (not the centered card),
|
|
259
|
+
* with the client logic served as /session-client.js.
|
|
260
|
+
*/
|
|
261
|
+
export function sessionPage(projectId, sessionId) {
|
|
262
|
+
const boot = JSON.stringify({ sid: sessionId, pid: projectId });
|
|
263
|
+
return `<!doctype html>
|
|
264
|
+
<html lang="en">
|
|
265
|
+
<head>
|
|
266
|
+
<meta charset="utf-8" />
|
|
267
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
268
|
+
<title>Session — MegaBrain Science</title>
|
|
269
|
+
<style>
|
|
270
|
+
:root { color-scheme: light dark; --bg:#fff; --panel:#faf9f8; --border:#eceae7; --fg:#1a1a1a; --muted:#8a8a8a; --accent:#c8492a; }
|
|
271
|
+
@media (prefers-color-scheme: dark) { :root { --bg:#111; --panel:#161513; --border:#262626; --fg:#ededed; --muted:#8a8a8a; --accent:#e8623a; } }
|
|
272
|
+
* { box-sizing: border-box; }
|
|
273
|
+
html, body { margin:0; height:100%; }
|
|
274
|
+
body { display:flex; height:100vh; overflow:hidden; background:var(--bg); color:var(--fg);
|
|
275
|
+
font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; }
|
|
276
|
+
/* Sidebar */
|
|
277
|
+
.sidebar { width:264px; flex-shrink:0; border-right:1px solid var(--border); background:var(--panel);
|
|
278
|
+
display:flex; flex-direction:column; padding:14px; gap:10px; }
|
|
279
|
+
.brand { display:flex; align-items:center; gap:8px; font-weight:600; font-size:14px; }
|
|
280
|
+
.mark { width:26px; height:26px; border-radius:7px; background:linear-gradient(135deg,#e8623a,#c8492a);
|
|
281
|
+
color:#fff; display:grid; place-items:center; font-size:12px; font-weight:700; }
|
|
282
|
+
.newbtn { border:1px solid var(--border); background:var(--bg); color:inherit; border-radius:10px;
|
|
283
|
+
padding:9px 12px; font:inherit; font-weight:600; cursor:pointer; text-align:left; }
|
|
284
|
+
.newbtn:hover { border-color:var(--accent); }
|
|
285
|
+
.proj { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-top:6px; }
|
|
286
|
+
.sessions { display:flex; flex-direction:column; gap:2px; overflow:auto; flex:1; margin:0 -6px; }
|
|
287
|
+
.session { display:block; padding:8px 10px; border-radius:8px; color:inherit; text-decoration:none;
|
|
288
|
+
font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
289
|
+
.session:hover { background:var(--bg); }
|
|
290
|
+
.session.active { background:var(--bg); font-weight:600; }
|
|
291
|
+
.side-foot { display:flex; gap:12px; font-size:13px; }
|
|
292
|
+
.side-foot a { color:var(--muted); text-decoration:none; } .side-foot a:hover { color:var(--fg); }
|
|
293
|
+
/* Main */
|
|
294
|
+
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
|
|
295
|
+
.topbar { height:52px; flex-shrink:0; border-bottom:1px solid var(--border); display:flex; align-items:center;
|
|
296
|
+
gap:10px; padding:0 20px; font-weight:600; }
|
|
297
|
+
.kstate { font-size:12px; color:var(--muted); font-weight:400; margin-left:auto; }
|
|
298
|
+
.transcript { flex:1; overflow:auto; padding:24px; display:flex; flex-direction:column; gap:16px; }
|
|
299
|
+
.wrap { width:100%; max-width:760px; margin:0 auto; display:flex; flex-direction:column; gap:16px; }
|
|
300
|
+
.msg.user { display:flex; justify-content:flex-end; }
|
|
301
|
+
.msg.user .bubble { background:var(--accent); color:#fff; border-radius:14px 14px 4px 14px; padding:9px 14px; max-width:80%; white-space:pre-wrap; }
|
|
302
|
+
.msg.agent .prose { white-space:pre-wrap; }
|
|
303
|
+
.step { border:1px solid var(--border); border-radius:12px; overflow:hidden; background:var(--panel); }
|
|
304
|
+
.step-head { display:flex; align-items:center; gap:8px; padding:9px 12px; font-size:13px; font-weight:600; }
|
|
305
|
+
.step-dot { width:7px; height:7px; border-radius:50%; background:var(--accent); }
|
|
306
|
+
.step .code, .step .out { margin:0; padding:10px 12px; font:12px/1.5 ui-monospace,Menlo,monospace;
|
|
307
|
+
white-space:pre-wrap; overflow-x:auto; border-top:1px solid var(--border); }
|
|
308
|
+
.step .out { color:var(--muted); background:var(--bg); }
|
|
309
|
+
.step .out.err { color:#c0392b; }
|
|
310
|
+
/* Composer */
|
|
311
|
+
.composer { flex-shrink:0; border-top:1px solid var(--border); padding:14px 20px; }
|
|
312
|
+
.composer-inner { max-width:760px; margin:0 auto; display:flex; gap:10px; align-items:flex-end;
|
|
313
|
+
border:1px solid var(--border); border-radius:16px; padding:10px 12px; background:var(--panel); }
|
|
314
|
+
.composer-inner:focus-within { border-color:var(--accent); }
|
|
315
|
+
textarea#composer { flex:1; border:0; background:transparent; resize:none; color:inherit; font:inherit;
|
|
316
|
+
outline:none; max-height:160px; }
|
|
317
|
+
.model { font-size:12px; color:var(--muted); align-self:center; white-space:nowrap; }
|
|
318
|
+
#send { border:0; border-radius:10px; background:var(--accent); color:#fff; font:inherit; font-weight:600;
|
|
319
|
+
padding:8px 16px; cursor:pointer; }
|
|
320
|
+
#send:disabled { opacity:.5; cursor:default; }
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
<aside class="sidebar">
|
|
325
|
+
<div class="brand"><span class="mark">MB</span> MegaBrain Science</div>
|
|
326
|
+
<button class="newbtn" id="new-session">+ New session</button>
|
|
327
|
+
<div class="proj" id="project-name">Project</div>
|
|
328
|
+
<nav class="sessions" id="session-list"></nav>
|
|
329
|
+
<div class="side-foot"><a href="/">Home</a><a href="/projects/${esc(projectId)}">Project</a></div>
|
|
330
|
+
</aside>
|
|
331
|
+
<main class="main">
|
|
332
|
+
<div class="topbar"><span id="session-title">Session</span><span class="kstate" id="kstate">kernel: idle</span></div>
|
|
333
|
+
<div class="transcript"><div class="wrap" id="transcript"></div></div>
|
|
334
|
+
<div class="composer">
|
|
335
|
+
<div class="composer-inner">
|
|
336
|
+
<textarea id="composer" rows="1" placeholder="Ask anything — the agent runs code locally…"></textarea>
|
|
337
|
+
<span class="model">Opus 4.8</span>
|
|
338
|
+
<button id="send">Send</button>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</main>
|
|
342
|
+
<script>window.__MB__ = ${boot};</script>
|
|
343
|
+
<script src="/session-client.js"></script>
|
|
344
|
+
</body>
|
|
345
|
+
</html>`;
|
|
346
|
+
}
|