brainctl 0.1.1 → 0.1.3

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.
@@ -0,0 +1,228 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { loadConfig } from '../config.js';
4
+ import { parseConfigPayload } from '../config.js';
5
+ import { loadMemory } from '../context/memory.js';
6
+ import { createConfigWriteService } from '../services/config-write-service.js';
7
+ import { createRunService } from '../services/run-service.js';
8
+ import { createStatusService } from '../services/status-service.js';
9
+ import { startSseStream, writeSseEvent } from './streaming.js';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ const uiAssetRoot = resolveUiAssetRoot();
13
+ export function createUiRouteHandler(dependencies) {
14
+ const statusService = dependencies.statusService ?? createStatusService();
15
+ const runService = dependencies.runService ?? createRunService();
16
+ const configWriteService = createConfigWriteService();
17
+ return async (request, response) => {
18
+ const url = new URL(request.url ?? '/', 'http://localhost');
19
+ switch (url.pathname) {
20
+ case '/api/overview': {
21
+ if (request.method !== 'GET') {
22
+ return sendJson(response, 405, { error: 'Method not allowed' });
23
+ }
24
+ const overview = await statusService.execute({ cwd: dependencies.cwd });
25
+ return sendJson(response, 200, overview);
26
+ }
27
+ case '/api/run/stream': {
28
+ if (request.method !== 'GET') {
29
+ return sendJson(response, 405, { error: 'Method not allowed' });
30
+ }
31
+ const runRequest = parseRunRequest(url);
32
+ if (runRequest === null) {
33
+ return sendJson(response, 400, {
34
+ error: 'Missing skill, inputFile, or primaryAgent'
35
+ });
36
+ }
37
+ if ('error' in runRequest) {
38
+ return sendJson(response, 400, {
39
+ error: runRequest.error
40
+ });
41
+ }
42
+ startSseStream(response);
43
+ try {
44
+ const trace = await runService.execute({
45
+ ...runRequest.request,
46
+ cwd: dependencies.cwd
47
+ }, {
48
+ onOutputChunk: (chunk) => {
49
+ writeSseEvent(response, 'output', chunk);
50
+ },
51
+ streamOutput: false
52
+ });
53
+ writeSseEvent(response, 'result', trace);
54
+ response.end();
55
+ }
56
+ catch (error) {
57
+ writeSseEvent(response, 'run-error', {
58
+ error: error instanceof Error ? error.message : 'Unexpected server error'
59
+ });
60
+ response.end();
61
+ }
62
+ return;
63
+ }
64
+ case '/api/config': {
65
+ if (request.method === 'PUT') {
66
+ const body = await readJsonBody(request);
67
+ if (!body.ok) {
68
+ return sendJson(response, 400, { error: 'Invalid JSON body' });
69
+ }
70
+ const config = parseConfigPayload(body.value);
71
+ await configWriteService.execute({
72
+ cwd: dependencies.cwd,
73
+ config
74
+ });
75
+ const savedConfig = await loadConfig({ cwd: dependencies.cwd });
76
+ return sendJson(response, 200, savedConfig);
77
+ }
78
+ if (request.method !== 'GET') {
79
+ return sendJson(response, 405, { error: 'Method not allowed' });
80
+ }
81
+ const config = await loadConfig({ cwd: dependencies.cwd });
82
+ return sendJson(response, 200, config);
83
+ }
84
+ case '/api/memory': {
85
+ if (request.method !== 'GET') {
86
+ return sendJson(response, 405, { error: 'Method not allowed' });
87
+ }
88
+ const config = await loadConfig({ cwd: dependencies.cwd });
89
+ const memory = await loadMemory({ paths: config.memory.paths });
90
+ return sendJson(response, 200, memory);
91
+ }
92
+ case '/api/agents': {
93
+ if (request.method !== 'GET') {
94
+ return sendJson(response, 405, { error: 'Method not allowed' });
95
+ }
96
+ const overview = await statusService.execute({ cwd: dependencies.cwd });
97
+ return sendJson(response, 200, overview.agents);
98
+ }
99
+ default:
100
+ if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
101
+ return sendJson(response, 404, { error: 'Not found' });
102
+ }
103
+ if (request.method !== 'GET') {
104
+ return sendJson(response, 405, { error: 'Method not allowed' });
105
+ }
106
+ return serveUiResponse(url.pathname, response);
107
+ }
108
+ };
109
+ }
110
+ async function readJsonBody(request) {
111
+ const chunks = [];
112
+ for await (const chunk of request) {
113
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
114
+ }
115
+ const rawBody = Buffer.concat(chunks).toString('utf8').trim();
116
+ if (rawBody.length === 0) {
117
+ return { ok: false };
118
+ }
119
+ try {
120
+ return { ok: true, value: JSON.parse(rawBody) };
121
+ }
122
+ catch {
123
+ return { ok: false };
124
+ }
125
+ }
126
+ function parseRunRequest(url) {
127
+ const skill = url.searchParams.get('skill');
128
+ const inputFile = url.searchParams.get('inputFile');
129
+ const primaryAgent = parseAgentName(url.searchParams.get('primaryAgent'));
130
+ const fallbackAgentParam = url.searchParams.get('fallbackAgent');
131
+ const fallbackAgent = fallbackAgentParam === null ? null : parseAgentName(fallbackAgentParam);
132
+ if (!skill || !inputFile || !primaryAgent || fallbackAgentParam !== null && !fallbackAgent) {
133
+ return null;
134
+ }
135
+ if (fallbackAgent !== null && fallbackAgent === primaryAgent) {
136
+ return { error: 'fallbackAgent must differ from primaryAgent' };
137
+ }
138
+ return {
139
+ request: {
140
+ skill,
141
+ inputFile,
142
+ primaryAgent,
143
+ fallbackAgent: fallbackAgent ?? undefined
144
+ }
145
+ };
146
+ }
147
+ function parseAgentName(value) {
148
+ if (value === 'claude' || value === 'codex') {
149
+ return value;
150
+ }
151
+ return null;
152
+ }
153
+ function sendJson(response, statusCode, body) {
154
+ response.statusCode = statusCode;
155
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
156
+ response.end(JSON.stringify(body));
157
+ }
158
+ async function serveUiResponse(pathname, response) {
159
+ if (!uiAssetRoot) {
160
+ return sendNotFound(response);
161
+ }
162
+ if (pathname === '/' || pathname === '/index.html') {
163
+ return sendAsset(response, path.join(uiAssetRoot, 'index.html'), 'text/html; charset=utf-8');
164
+ }
165
+ const isAssetPath = pathname.startsWith('/assets/') || path.extname(pathname).length > 0;
166
+ if (isAssetPath) {
167
+ const assetPath = path.resolve(uiAssetRoot, `.${pathname}`);
168
+ if (!isWithinDirectory(uiAssetRoot, assetPath) || !existsSync(assetPath)) {
169
+ return sendNotFound(response);
170
+ }
171
+ return sendAsset(response, assetPath, getContentType(assetPath));
172
+ }
173
+ return sendAsset(response, path.join(uiAssetRoot, 'index.html'), 'text/html; charset=utf-8');
174
+ }
175
+ async function sendAsset(response, filePath, contentType) {
176
+ const body = await readFile(filePath);
177
+ response.statusCode = 200;
178
+ response.setHeader('Content-Type', contentType);
179
+ response.end(body);
180
+ }
181
+ function sendNotFound(response) {
182
+ response.statusCode = 404;
183
+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
184
+ response.end('Not found');
185
+ }
186
+ function getContentType(filePath) {
187
+ switch (path.extname(filePath).toLowerCase()) {
188
+ case '.html':
189
+ return 'text/html; charset=utf-8';
190
+ case '.js':
191
+ return 'text/javascript; charset=utf-8';
192
+ case '.css':
193
+ return 'text/css; charset=utf-8';
194
+ case '.json':
195
+ return 'application/json; charset=utf-8';
196
+ case '.svg':
197
+ return 'image/svg+xml';
198
+ case '.ico':
199
+ return 'image/x-icon';
200
+ case '.png':
201
+ return 'image/png';
202
+ case '.map':
203
+ return 'application/json; charset=utf-8';
204
+ default:
205
+ return 'application/octet-stream';
206
+ }
207
+ }
208
+ function isWithinDirectory(parentDirectory, targetPath) {
209
+ const relativePath = path.relative(parentDirectory, targetPath);
210
+ if (relativePath === '') {
211
+ return true;
212
+ }
213
+ return !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..' && !path.isAbsolute(relativePath);
214
+ }
215
+ function resolveUiAssetRoot() {
216
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
217
+ const candidates = [
218
+ path.resolve(moduleDir, '../web'),
219
+ path.resolve(moduleDir, '../../dist/web'),
220
+ path.resolve(process.cwd(), 'dist/web')
221
+ ];
222
+ for (const candidate of candidates) {
223
+ if (existsSync(path.join(candidate, 'index.html'))) {
224
+ return candidate;
225
+ }
226
+ }
227
+ return null;
228
+ }
@@ -0,0 +1,14 @@
1
+ import { type Server } from 'node:http';
2
+ import type { StatusService } from '../services/status-service.js';
3
+ export interface StartUiServerOptions {
4
+ cwd?: string;
5
+ host?: string;
6
+ port?: number;
7
+ statusService?: StatusService;
8
+ }
9
+ export interface UiServer {
10
+ server: Server;
11
+ url: string;
12
+ close: () => Promise<void>;
13
+ }
14
+ export declare function startUiServer(options?: StartUiServerOptions): Promise<UiServer>;
@@ -0,0 +1,47 @@
1
+ import { createServer } from 'node:http';
2
+ import { BrainctlError } from '../errors.js';
3
+ import { createUiRouteHandler } from './routes.js';
4
+ export async function startUiServer(options = {}) {
5
+ const cwd = options.cwd ?? process.cwd();
6
+ const host = options.host ?? '127.0.0.1';
7
+ const port = options.port ?? 3333;
8
+ const handler = createUiRouteHandler({
9
+ cwd,
10
+ statusService: options.statusService
11
+ });
12
+ const server = createServer(async (request, response) => {
13
+ try {
14
+ await handler(request, response);
15
+ }
16
+ catch (error) {
17
+ const isUserError = error instanceof BrainctlError && error.category === 'user';
18
+ response.statusCode = isUserError ? 400 : 500;
19
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
20
+ response.end(JSON.stringify({
21
+ error: error instanceof Error ? error.message : 'Unexpected server error'
22
+ }));
23
+ }
24
+ });
25
+ await new Promise((resolve, reject) => {
26
+ server.once('error', reject);
27
+ server.listen(port, host, () => resolve());
28
+ });
29
+ const address = server.address();
30
+ if (!address || typeof address === 'string') {
31
+ throw new Error('UI server did not bind to a TCP port.');
32
+ }
33
+ const actualAddress = address;
34
+ const url = `http://${formatHost(host)}:${actualAddress.port}`;
35
+ return {
36
+ server,
37
+ url,
38
+ close: async () => {
39
+ await new Promise((resolve) => {
40
+ server.close(() => resolve());
41
+ });
42
+ }
43
+ };
44
+ }
45
+ function formatHost(host) {
46
+ return host.includes(':') ? `[${host}]` : host;
47
+ }
@@ -0,0 +1,3 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ export declare function startSseStream(response: ServerResponse): void;
3
+ export declare function writeSseEvent(response: ServerResponse, event: string, data: unknown): void;
@@ -0,0 +1,16 @@
1
+ export function startSseStream(response) {
2
+ response.statusCode = 200;
3
+ response.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
4
+ response.setHeader('Cache-Control', 'no-cache, no-transform');
5
+ response.setHeader('Connection', 'keep-alive');
6
+ response.setHeader('X-Accel-Buffering', 'no');
7
+ response.flushHeaders?.();
8
+ }
9
+ export function writeSseEvent(response, event, data) {
10
+ response.write(`event: ${event}\n`);
11
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
12
+ for (const line of payload.split(/\r?\n/)) {
13
+ response.write(`data: ${line}\n`);
14
+ }
15
+ response.write('\n');
16
+ }
@@ -0,0 +1 @@
1
+ :root{color-scheme:light;--bg: #f4f5f2;--bg-accent: rgba(255, 255, 255, .7);--panel: rgba(255, 255, 255, .82);--panel-strong: rgba(255, 255, 255, .94);--border: rgba(17, 24, 39, .1);--border-strong: rgba(17, 24, 39, .16);--text: #101114;--muted: rgba(16, 17, 20, .64);--shadow: 0 26px 70px rgba(17, 24, 39, .08);--shadow-soft: 0 12px 28px rgba(17, 24, 39, .06);--radius-xl: 28px;--radius-lg: 22px;--radius-md: 16px;--radius-sm: 12px;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,SF Pro Display,Segoe UI,sans-serif;background:var(--bg);color:var(--text)}*{box-sizing:border-box}html,body,#root{min-height:100%;margin:0}body{min-height:100vh;background:radial-gradient(circle at top left,rgba(255,255,255,.8),transparent 26%),radial-gradient(circle at top right,rgba(17,24,39,.05),transparent 28%),linear-gradient(180deg,#f7f8f6,#eef1eb);color:var(--text)}button,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default}#root{min-height:100vh}.app-shell{min-height:100vh;padding:24px}.shell{width:min(1440px,100%);margin:0 auto;display:grid;gap:18px}.panel{border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--panel);box-shadow:var(--shadow);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.topbar{padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:20px}.brand-block{display:flex;align-items:center;gap:14px;min-width:0}.brand-mark{width:44px;height:44px;border-radius:14px;display:grid;place-items:center;background:#101114;color:#fff;box-shadow:inset 0 1px #ffffff14}.eyebrow{margin:0 0 6px;text-transform:uppercase;letter-spacing:.14em;font-size:.72rem;font-weight:700;color:var(--muted)}.brand-block h1,.workspace-header h2,.panel-heading h2,.section-header h3,.empty-state h2{margin:0;font-size:clamp(1.4rem,2.5vw,2.3rem);line-height:1.05;letter-spacing:-.04em}.workspace-header h2,.panel-heading h2,.section-header h3{font-size:clamp(1.05rem,1.7vw,1.35rem)}.status-strip{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px}.status-chip,.muted-pill,.skill-chip,.agent-command{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--border);border-radius:999px;background:#ffffffa8;color:#101114bd;padding:8px 12px;font-size:.84rem;line-height:1}.muted-pill{background:#ffffff7a;color:var(--muted)}.shell-grid{display:grid;grid-template-columns:320px minmax(0,1fr);gap:18px;align-items:start}.nav-card,.workspace-card{padding:20px}.nav-card{position:sticky;top:24px}.panel-heading,.workspace-header,.section-header,.metric-card-header,.agent-row,.agent-row-meta{display:flex;align-items:center;justify-content:space-between;gap:16px}.panel-heading{margin-bottom:18px}.section-nav{display:grid;gap:10px}.section-button{border:1px solid var(--border);border-radius:var(--radius-lg);background:#ffffffa8;color:var(--text);padding:16px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;text-align:left;transition:transform .14s ease,border-color .14s ease,background-color .14s ease,box-shadow .14s ease}.section-button:hover:not(:disabled){transform:translateY(-1px);border-color:var(--border-strong);box-shadow:var(--shadow-soft)}.section-button:disabled{opacity:.62}.section-button.is-active{background:#101114;color:#fff;border-color:#101114;box-shadow:0 14px 30px #1011142e}.section-button.is-disabled{opacity:.7}.section-button-icon{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:#1011140f}.section-button.is-active .section-button-icon{background:#ffffff1a}.section-button-label{display:grid;gap:4px;min-width:0}.section-button-label span:first-child{font-weight:650}.section-button-subtitle{color:var(--muted);font-size:.82rem}.section-button.is-active .section-button-subtitle{color:#ffffffc2}.section-button-chevron{color:var(--muted)}.section-button.is-active .section-button-chevron{color:#fffc}.workspace-card{display:grid;gap:20px;min-width:0}.workspace-header{padding-bottom:18px;border-bottom:1px solid var(--border);align-items:flex-start}.workspace-header-meta{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px}.muted-copy{margin:8px 0 0;color:var(--muted);line-height:1.5;word-break:break-word}.view-stack{display:grid;gap:18px}.metrics-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.metric-card{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--panel-strong);padding:18px;min-width:0;box-shadow:var(--shadow-soft)}.metric-card.is-wide{grid-column:1 / -1}.metric-card-header{margin-bottom:14px;justify-content:flex-start}.metric-card-icon{width:32px;height:32px;border-radius:11px;display:grid;place-items:center;background:#1011140f}.metric-card-label{font-size:.86rem;color:var(--muted)}.metric-card-value{display:block;font-size:clamp(1rem,1.8vw,1.2rem);line-height:1.35;letter-spacing:-.03em;word-break:break-word}.metric-card-note{margin:10px 0 0;color:var(--muted);line-height:1.45}.panel-inner{border:1px solid var(--border);border-radius:var(--radius-lg);background:#ffffffb8;padding:18px}.chip-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px}.skill-chip{padding-inline:14px}.agent-list,.memory-file-list{display:grid;gap:12px}.agent-row{justify-content:space-between;border:1px solid var(--border);border-radius:var(--radius-md);background:#fffc;padding:14px 16px}.agent-row-meta{justify-content:flex-start;min-width:0}.agent-row-meta strong{display:block;text-transform:capitalize}.agent-row-meta p{margin:3px 0 0;color:var(--muted)}.agent-status-dot{width:10px;height:10px;border-radius:999px;background:#1011143d;flex:0 0 auto}.agent-status-dot.is-online{background:#101114}.agent-status-dot.is-offline{background:#10111447}.agent-command{flex:0 0 auto}.memory-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:16px;min-width:0}.memory-list,.memory-preview{min-width:0}.memory-file-button{border:1px solid var(--border);border-radius:var(--radius-md);background:#ffffffd1;padding:14px 16px;text-align:left;display:grid;gap:6px;transition:transform .14s ease,border-color .14s ease,background-color .14s ease}.memory-file-button:hover{transform:translateY(-1px);border-color:var(--border-strong)}.memory-file-button.is-selected{background:#101114;color:#fff;border-color:#101114}.memory-file-button strong,.memory-file-button span{min-width:0;overflow:hidden;text-overflow:ellipsis}.memory-file-button span{color:var(--muted);font-size:.8rem}.memory-file-button.is-selected span{color:#ffffffbd}.memory-preview{display:grid;gap:14px}.memory-editor{width:100%;min-height:420px;resize:vertical;border:1px solid var(--border);border-radius:var(--radius-lg);background:#fffffff0;color:var(--text);padding:18px;line-height:1.55;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,Consolas,Liberation Mono,monospace;box-shadow:var(--shadow-soft)}.memory-editor:focus{outline:2px solid rgba(16,17,20,.18);outline-offset:2px}.run-view,.config-view{gap:16px}.config-panel{display:grid;gap:18px}.config-toolbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px}.config-list{display:grid;gap:14px}.config-card,.config-empty-state{border:1px solid var(--border);border-radius:var(--radius-lg);background:#ffffffd6;padding:18px;box-shadow:var(--shadow-soft)}.config-card{display:grid;gap:16px}.config-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.config-card-header h4,.config-empty-state strong{margin:0;font-size:1rem;line-height:1.25;letter-spacing:-.02em}.config-empty-state p{margin:8px 0 0;color:var(--muted);line-height:1.5}.config-field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.secondary-button,.danger-button{border-radius:999px;padding:11px 15px;display:inline-flex;align-items:center;justify-content:center;gap:9px;font-weight:650;transition:transform .14s ease,border-color .14s ease,background-color .14s ease,opacity .14s ease}.secondary-button{border:1px solid var(--border);background:#ffffffe0;color:var(--text)}.danger-button{border:1px solid rgba(16,17,20,.12);background:#1011140a;color:var(--text)}.secondary-button:hover:not(:disabled),.danger-button:hover:not(:disabled){transform:translateY(-1px);border-color:var(--border-strong)}.secondary-button:disabled,.danger-button:disabled{opacity:.5}.editor-textarea{min-height:180px;resize:vertical}.editor-code,.editor-textarea{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,Consolas,Liberation Mono,monospace;line-height:1.55}.save-feedback{border:1px solid var(--border);border-radius:var(--radius-md);background:#ffffffe0;padding:13px 15px;display:inline-flex;align-items:center;gap:10px;font-size:.92rem}.save-feedback.saved{border-color:#10111424}.save-feedback.error{border-color:#1011142e}.run-panel{display:grid;gap:18px}.run-form{display:grid;gap:16px}.field{display:grid;gap:8px}.field-label{font-size:.86rem;font-weight:650;letter-spacing:-.01em}.field-control{width:100%;border:1px solid var(--border);border-radius:var(--radius-md);background:#fffffff0;color:var(--text);padding:13px 14px;box-shadow:var(--shadow-soft);min-width:0}.field-control:focus{outline:2px solid rgba(16,17,20,.18);outline-offset:2px}.field-control:disabled{opacity:.72}.field-help{margin:0;color:var(--muted);font-size:.82rem;line-height:1.45}.run-agent-grid,.run-detail-grid,.run-summary-grid{display:grid;gap:14px}.run-agent-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.run-detail-grid{grid-template-columns:minmax(0,1.35fr) minmax(320px,.95fr);align-items:start}.run-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.run-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px}.run-button{border:1px solid #101114;border-radius:999px;background:#101114;color:#fff;padding:13px 18px;display:inline-flex;align-items:center;gap:10px;font-weight:650;box-shadow:0 14px 30px #1011142e;transition:transform .14s ease,opacity .14s ease}.run-button:hover:not(:disabled){transform:translateY(-1px)}.run-button:disabled{opacity:.55}.run-button-spinner,.run-state-spinner{animation:spin .9s linear infinite}.run-state{white-space:nowrap}.run-state.running{border-color:#1011142e;background:#10111414}.run-state.success{border-color:#10111424;background:#1011140f}.run-state.error{border-color:#1011142e;background:#10111414}.run-output-panel,.run-summary-panel{min-width:0}.run-output{width:100%;min-height:380px;resize:vertical;border:1px solid var(--border);border-radius:var(--radius-lg);background:#fffffff0;color:var(--text);padding:18px;line-height:1.55;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Monaco,Consolas,Liberation Mono,monospace;box-shadow:var(--shadow-soft)}.run-output:focus{outline:2px solid rgba(16,17,20,.18);outline-offset:2px}.run-summary-tile{border:1px solid var(--border);border-radius:var(--radius-md);background:#ffffffe0;padding:14px 15px;display:grid;gap:8px}.run-summary-tile-icon{width:32px;height:32px;border-radius:11px;display:grid;place-items:center;background:#1011140f}.run-summary-tile-label{color:var(--muted);font-size:.82rem}.run-summary-tile-value{font-size:1rem;line-height:1.4;word-break:break-word}.run-empty-state{border:1px solid var(--border);border-radius:var(--radius-lg);background:#fffc;padding:18px;min-height:220px;display:grid;grid-template-columns:auto minmax(0,1fr);gap:14px;align-items:start}.run-empty-state.is-error{background:#ffffffe6}.run-empty-state strong{display:block;margin-bottom:6px}.run-empty-state p{margin:0;color:var(--muted);line-height:1.55}.empty-state{border:1px solid var(--border);border-radius:var(--radius-xl);background:#ffffffc2;padding:30px;min-height:260px;display:grid;align-content:start;gap:12px;box-shadow:var(--shadow-soft)}.empty-state p{margin:0;color:var(--muted);line-height:1.6;max-width:60ch}@keyframes spin{to{transform:rotate(360deg)}}@media(max-width:1080px){.shell-grid,.memory-layout,.run-detail-grid,.metrics-grid,.config-field-grid,.run-agent-grid,.run-summary-grid{grid-template-columns:1fr}.nav-card{position:static}}@media(max-width:720px){.app-shell{padding:16px}.topbar{padding:18px;align-items:flex-start;flex-direction:column}.nav-card,.workspace-card,.panel-inner{padding:16px}.run-actions{align-items:stretch}.config-toolbar,.config-card-header{flex-direction:column;align-items:stretch}.run-button{width:100%;justify-content:center}.secondary-button,.danger-button{width:100%}}.empty-state-icon{width:42px;height:42px;border-radius:14px;display:grid;place-items:center;background:#10111412}@media(max-width:1100px){.shell-grid,.memory-layout,.metrics-grid{grid-template-columns:1fr}.nav-card{position:static}}@media(max-width:720px){.app-shell{padding:14px}.topbar,.workspace-header{flex-direction:column;align-items:flex-start}.status-strip,.workspace-header-meta{justify-content:flex-start}.panel,.nav-card,.workspace-card{padding:16px;border-radius:22px}.memory-editor{min-height:300px}}