nexus-prime 6.5.0 → 6.6.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/dist/cli/install-wizard.js +30 -7
- package/dist/cli/watch.d.ts +10 -0
- package/dist/cli/watch.js +117 -0
- package/dist/cli.js +14 -0
- package/dist/daemon/client.js +24 -1
- package/dist/daemon/lock.d.ts +2 -0
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +59 -1
- package/dist/dashboard/app/styles/runtime.css +113 -0
- package/dist/dashboard/app/views/runtime.js +136 -24
- package/dist/dashboard/stream/sse-broker.js +57 -11
- package/dist/dashboard/welcome.html +685 -0
- package/dist/engines/event-bus.d.ts +38 -1
- package/dist/engines/memory.js +6 -3
- package/dist/engines/orchestrator/types.d.ts +2 -0
- package/dist/engines/orchestrator.js +102 -22
- package/dist/index.js +25 -2
- package/dist/licensing/license-manager.js +16 -0
- package/dist/licensing/upgrade-prompts.js +10 -1
- package/package.json +2 -2
|
@@ -364,17 +364,40 @@ export async function cliSetup(opts = []) {
|
|
|
364
364
|
if (cleanup.staleLockfilesRemoved + cleanup.staleDaemonsKilled > 0) {
|
|
365
365
|
log(` ${c('↺', 'dim')} Cleaned ${cleanup.staleLockfilesRemoved} lockfiles, stopped ${cleanup.staleDaemonsKilled} stale daemons`);
|
|
366
366
|
}
|
|
367
|
-
// Step 1 — detect + configure IDEs
|
|
367
|
+
// Step 1 — detect + configure IDEs (per-IDE animated checklist)
|
|
368
368
|
const totalSteps = options.licenseKey ? 5 : 4;
|
|
369
369
|
step(1, totalSteps, 'Detecting IDEs…');
|
|
370
|
+
// Emit install.step event helper (best-effort; daemon may not be running yet)
|
|
371
|
+
const emitInstallStep = (stepN, total, label, status, detail) => {
|
|
372
|
+
try {
|
|
373
|
+
import('../engines/event-bus.js').then(({ nexusEventBus }) => {
|
|
374
|
+
nexusEventBus.emit('install.step', { step: stepN, total, label, status, detail });
|
|
375
|
+
}).catch(() => { });
|
|
376
|
+
}
|
|
377
|
+
catch { /* non-fatal */ }
|
|
378
|
+
};
|
|
370
379
|
const result = await runInstallWizard({ dryRun, verbose: false });
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
const allIDEs = [...result.configured, ...result.skipped, ...result.errors.map(e => e.ide)];
|
|
381
|
+
if (allIDEs.length > 0) {
|
|
382
|
+
for (const ide of allIDEs) {
|
|
383
|
+
const isConfigured = result.configured.includes(ide);
|
|
384
|
+
const isSkipped = result.skipped.includes(ide);
|
|
385
|
+
const errEntry = result.errors.find(e => e.ide === ide);
|
|
386
|
+
if (isConfigured) {
|
|
387
|
+
await withSpinner(`${ide} · configuring`, Promise.resolve());
|
|
388
|
+
emitInstallStep(1, totalSteps, ide, 'ok', 'MCP config written');
|
|
389
|
+
}
|
|
390
|
+
else if (isSkipped) {
|
|
391
|
+
log(` ${c('─', 'dim')} ${c(ide, 'dim')} already set up`);
|
|
392
|
+
emitInstallStep(1, totalSteps, ide, 'skip', 'already configured');
|
|
393
|
+
}
|
|
394
|
+
else if (errEntry) {
|
|
395
|
+
log(` ${c('✗', 'yellow')} ${c(ide, 'yellow')} · ${errEntry.reason}`);
|
|
396
|
+
emitInstallStep(1, totalSteps, ide, 'fail', errEntry.reason);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
376
399
|
}
|
|
377
|
-
|
|
400
|
+
else {
|
|
378
401
|
log(` ${c('—', 'dim')} No IDEs auto-detected. Run: nexus-prime setup <ide>`);
|
|
379
402
|
}
|
|
380
403
|
// Step 2 — MCP configs confirmed
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Prime — `nexus-prime watch`
|
|
3
|
+
* Tail live SSE events from the running daemon in a colored terminal feed.
|
|
4
|
+
*/
|
|
5
|
+
export interface WatchOptions {
|
|
6
|
+
port?: number;
|
|
7
|
+
all?: boolean;
|
|
8
|
+
filter?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runWatch(opts?: WatchOptions): Promise<void>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Prime — `nexus-prime watch`
|
|
3
|
+
* Tail live SSE events from the running daemon in a colored terminal feed.
|
|
4
|
+
*/
|
|
5
|
+
import * as http from 'http';
|
|
6
|
+
const ANSI = {
|
|
7
|
+
cyan: '\x1b[36m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
magenta: '\x1b[35m',
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
};
|
|
16
|
+
const isTTY = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
17
|
+
const c = (s, k) => isTTY ? `${ANSI[k]}${s}${ANSI.reset}` : s;
|
|
18
|
+
const DEFAULT_FILTER = /^(mcp\.|tokens\.|planner\.|phantom\.|license\.|install\.|orchestrator\.run)/;
|
|
19
|
+
function colorForType(type) {
|
|
20
|
+
if (type.startsWith('mcp.'))
|
|
21
|
+
return 'cyan';
|
|
22
|
+
if (type.startsWith('tokens.'))
|
|
23
|
+
return 'yellow';
|
|
24
|
+
if (type.startsWith('license.'))
|
|
25
|
+
return 'magenta';
|
|
26
|
+
if (type.startsWith('phantom.'))
|
|
27
|
+
return 'green';
|
|
28
|
+
if (type.startsWith('install.'))
|
|
29
|
+
return 'green';
|
|
30
|
+
if (type.includes('error') || type.includes('fail') || type.includes('bad'))
|
|
31
|
+
return 'red';
|
|
32
|
+
return 'dim';
|
|
33
|
+
}
|
|
34
|
+
export async function runWatch(opts = {}) {
|
|
35
|
+
const port = opts.port ?? Number(process.env.NEXUS_DASHBOARD_PORT ?? 3377);
|
|
36
|
+
const filterRe = opts.all
|
|
37
|
+
? null
|
|
38
|
+
: opts.filter
|
|
39
|
+
? new RegExp(opts.filter)
|
|
40
|
+
: DEFAULT_FILTER;
|
|
41
|
+
console.log(c('◆ Nexus Prime · Live Event Tail', 'cyan'));
|
|
42
|
+
console.log(c(` Stream: http://localhost:${port}/dashboard/events`, 'dim'));
|
|
43
|
+
console.log(c(` Filter: ${filterRe ? filterRe.source : 'all events'}`, 'dim'));
|
|
44
|
+
console.log(c(' Press Ctrl-C to exit\n', 'dim'));
|
|
45
|
+
let buf = '';
|
|
46
|
+
const req = http.get({ hostname: 'localhost', port, path: '/dashboard/events', headers: { Accept: 'text/event-stream' } }, (res) => {
|
|
47
|
+
if (res.statusCode !== 200) {
|
|
48
|
+
console.error(c(` ✗ Daemon not reachable (HTTP ${res.statusCode}). Start: nexus-prime daemon`, 'red'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
res.setEncoding('utf8');
|
|
52
|
+
res.on('data', (chunk) => {
|
|
53
|
+
buf += chunk;
|
|
54
|
+
const lines = buf.split('\n');
|
|
55
|
+
buf = lines.pop() ?? '';
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (!line.startsWith('data: '))
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const evt = JSON.parse(line.slice(6));
|
|
61
|
+
const type = evt.type ?? evt.t ?? '';
|
|
62
|
+
if (filterRe && !filterRe.test(type))
|
|
63
|
+
continue;
|
|
64
|
+
const payload = evt.payload ?? evt.data ?? evt.p ?? {};
|
|
65
|
+
const summary = summarize(type, payload);
|
|
66
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
67
|
+
process.stdout.write(`${c(ts, 'dim')} ${c(type.padEnd(36), colorForType(type))} ${summary}\n`);
|
|
68
|
+
}
|
|
69
|
+
catch { /* malformed JSON — skip */ }
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
res.on('error', (err) => {
|
|
73
|
+
console.error(c(` Stream error: ${err.message}`, 'red'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
res.on('close', () => {
|
|
77
|
+
console.log(c('\n Stream closed.', 'dim'));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
req.on('error', (err) => {
|
|
82
|
+
console.error(c(` ✗ Cannot connect to daemon on port ${port}: ${err.message}`, 'red'));
|
|
83
|
+
console.error(c(' Start daemon: nexus-prime daemon', 'dim'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
|
86
|
+
process.on('SIGINT', () => {
|
|
87
|
+
req.destroy();
|
|
88
|
+
console.log(c('\n Bye.', 'dim'));
|
|
89
|
+
process.exit(0);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function summarize(type, payload) {
|
|
93
|
+
switch (type) {
|
|
94
|
+
case 'mcp.call.start':
|
|
95
|
+
return `→ ${payload.toolName ?? 'tool'}`;
|
|
96
|
+
case 'mcp.call.complete':
|
|
97
|
+
return payload.error
|
|
98
|
+
? c(`✗ ${payload.toolName ?? 'tool'} · ${String(payload.error).slice(0, 60)}`, 'red')
|
|
99
|
+
: c(`✓ ${payload.toolName ?? 'tool'} · ${payload.durationMs ?? 0}ms`, 'green');
|
|
100
|
+
case 'tokens.optimized':
|
|
101
|
+
return `saved ${payload.savings ?? 0} · ${payload.pct ?? 0}% · src:${payload.source ?? 'unknown'}`;
|
|
102
|
+
case 'planner.stage':
|
|
103
|
+
return `${payload.stage ?? ''} · ${payload.status ?? ''}`;
|
|
104
|
+
case 'orchestrator.run.start':
|
|
105
|
+
return String(payload.goal ?? '').slice(0, 80);
|
|
106
|
+
case 'orchestrator.run.complete':
|
|
107
|
+
return `done · ${payload.durationMs ?? ''}ms`;
|
|
108
|
+
case 'license.tierChanged':
|
|
109
|
+
return `${payload.from ?? '?'} → ${String(payload.to ?? '?').toUpperCase()}`;
|
|
110
|
+
case 'license.upgradeNudge':
|
|
111
|
+
return String(payload.message ?? '').slice(0, 80);
|
|
112
|
+
case 'install.step':
|
|
113
|
+
return `[${payload.status ?? ''}] ${payload.label ?? ''} (${payload.step ?? '?'}/${payload.total ?? '?'})`;
|
|
114
|
+
default:
|
|
115
|
+
return String(payload.message ?? payload.summary ?? payload.goal ?? '').slice(0, 80);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1894,6 +1894,20 @@ program
|
|
|
1894
1894
|
timeoutMs: opts.timeout ? Number(opts.timeout) : undefined,
|
|
1895
1895
|
});
|
|
1896
1896
|
});
|
|
1897
|
+
program
|
|
1898
|
+
.command('watch')
|
|
1899
|
+
.description('Tail live SSE events from the running daemon in a colored terminal feed')
|
|
1900
|
+
.option('--port <port>', 'Dashboard port (default: 3377 or $NEXUS_DASHBOARD_PORT)')
|
|
1901
|
+
.option('--all', 'Show all event types (default: mcp, tokens, planner, license, install)')
|
|
1902
|
+
.option('--filter <regex>', 'Custom event type filter (regex string)')
|
|
1903
|
+
.action(async (opts) => {
|
|
1904
|
+
const { runWatch } = await import('./cli/watch.js');
|
|
1905
|
+
await runWatch({
|
|
1906
|
+
port: opts.port ? Number(opts.port) : undefined,
|
|
1907
|
+
all: opts.all,
|
|
1908
|
+
filter: opts.filter,
|
|
1909
|
+
});
|
|
1910
|
+
});
|
|
1897
1911
|
// Default: running `nexus-prime` with no subcommand runs `setup auto`
|
|
1898
1912
|
if (process.argv.length === 2) {
|
|
1899
1913
|
process.argv.push('setup', 'auto');
|
package/dist/daemon/client.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
function readCliPkgVersion() {
|
|
8
|
+
for (const rel of ['../../package.json', '../package.json']) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(readFileSync(path.join(__dirname, rel), 'utf8'));
|
|
11
|
+
if (parsed.version)
|
|
12
|
+
return parsed.version;
|
|
13
|
+
}
|
|
14
|
+
catch { /* try next */ }
|
|
15
|
+
}
|
|
16
|
+
return 'unknown';
|
|
17
|
+
}
|
|
3
18
|
import { acquireDaemonLock, getDaemonLockPath, isProcessAlive, readDaemonLock, removeDaemonLock, } from './lock.js';
|
|
4
19
|
function sleep(ms) {
|
|
5
20
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -99,7 +114,15 @@ export async function ensureDaemonReady(workspace, options = {}) {
|
|
|
99
114
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
100
115
|
const existing = acquireDaemonLock(workspace);
|
|
101
116
|
if (existing.status === 'running') {
|
|
102
|
-
|
|
117
|
+
const currentVersion = readCliPkgVersion();
|
|
118
|
+
const daemonVersion = existing.record.pkgVersion;
|
|
119
|
+
if (currentVersion !== 'unknown' && daemonVersion && daemonVersion !== currentVersion) {
|
|
120
|
+
// Version mismatch — stop the stale daemon before spawning the new one
|
|
121
|
+
await stopDaemon(workspace).catch(() => { });
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
return waitForDaemonReady(workspace, timeoutMs);
|
|
125
|
+
}
|
|
103
126
|
}
|
|
104
127
|
removeDaemonLock(existing.lockPath, { pid: existing.record.pid });
|
|
105
128
|
if (options.spawnIfMissing === false) {
|
package/dist/daemon/lock.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface DaemonLockRecord {
|
|
|
16
16
|
stateRoot: string;
|
|
17
17
|
workspaceRoot: string;
|
|
18
18
|
repoRoot: string;
|
|
19
|
+
/** Package version of the daemon process that wrote this lock. */
|
|
20
|
+
pkgVersion?: string;
|
|
19
21
|
}
|
|
20
22
|
export interface AcquireDaemonLockResult {
|
|
21
23
|
status: 'acquired' | 'running';
|
package/dist/daemon/server.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
2
5
|
import { randomBytes, randomUUID } from 'crypto';
|
|
3
6
|
import { URL } from 'url';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
function readDaemonPkgVersion() {
|
|
10
|
+
for (const rel of ['../../package.json', '../package.json']) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(readFileSync(path.join(__dirname, rel), 'utf8'));
|
|
13
|
+
if (parsed.version)
|
|
14
|
+
return parsed.version;
|
|
15
|
+
}
|
|
16
|
+
catch { /* try next */ }
|
|
17
|
+
}
|
|
18
|
+
return 'unknown';
|
|
19
|
+
}
|
|
20
|
+
const DAEMON_PKG_VERSION = readDaemonPkgVersion();
|
|
4
21
|
import { createNexusPrime } from '../index.js';
|
|
5
22
|
import { MCPAdapter } from '../agents/adapters/mcp.js';
|
|
6
23
|
import { nexusEventBus } from '../engines/event-bus.js';
|
|
@@ -89,9 +106,48 @@ export class NexusDaemonServer {
|
|
|
89
106
|
this.httpServer = http.createServer((req, res) => {
|
|
90
107
|
void this.handleRequest(req, res);
|
|
91
108
|
});
|
|
92
|
-
this.cleanupTimer = setInterval(() =>
|
|
109
|
+
this.cleanupTimer = setInterval(() => {
|
|
110
|
+
try {
|
|
111
|
+
this.pruneState();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Never let cleanup crash the daemon.
|
|
115
|
+
console.error('[nexus-daemon] pruneState failed:', err?.message ?? err);
|
|
116
|
+
}
|
|
117
|
+
}, 60_000);
|
|
93
118
|
this.cleanupTimer.unref?.();
|
|
94
119
|
}
|
|
120
|
+
installProcessErrorHandlers() {
|
|
121
|
+
// Prevent a single unhandled rejection / uncaught throw from killing the
|
|
122
|
+
// daemon. Engines like orchestrator + SSE + synapse emit fire-and-forget
|
|
123
|
+
// promises; without this the first background throw takes the whole
|
|
124
|
+
// control plane down and orchestration starts "failing" silently for the
|
|
125
|
+
// client.
|
|
126
|
+
process.on('uncaughtException', (err) => {
|
|
127
|
+
try {
|
|
128
|
+
console.error('[nexus-daemon] uncaughtException:', err?.stack ?? err?.message ?? err);
|
|
129
|
+
nexusEventBus.emit('nexus.daemon.error', {
|
|
130
|
+
kind: 'uncaughtException',
|
|
131
|
+
message: err?.message ?? String(err),
|
|
132
|
+
stack: err?.stack ?? null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch { /* last-resort logging — swallow */ }
|
|
136
|
+
});
|
|
137
|
+
process.on('unhandledRejection', (reason) => {
|
|
138
|
+
try {
|
|
139
|
+
const message = reason?.message ?? String(reason);
|
|
140
|
+
const stack = reason?.stack ?? null;
|
|
141
|
+
console.error('[nexus-daemon] unhandledRejection:', stack ?? message);
|
|
142
|
+
nexusEventBus.emit('nexus.daemon.error', {
|
|
143
|
+
kind: 'unhandledRejection',
|
|
144
|
+
message,
|
|
145
|
+
stack,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch { /* last-resort logging — swallow */ }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
95
151
|
async start() {
|
|
96
152
|
const lock = acquireDaemonLock(this.workspace, {
|
|
97
153
|
token: this.authToken,
|
|
@@ -146,6 +202,7 @@ export class NexusDaemonServer {
|
|
|
146
202
|
port,
|
|
147
203
|
token: this.authToken,
|
|
148
204
|
heartbeatAt: Date.now(),
|
|
205
|
+
pkgVersion: DAEMON_PKG_VERSION,
|
|
149
206
|
});
|
|
150
207
|
// Heartbeat: keep lockfile fresh so stale-lock detection (30 s TTL) works
|
|
151
208
|
// correctly after a SIGKILL. The interval is intentionally unref'd so it
|
|
@@ -165,6 +222,7 @@ export class NexusDaemonServer {
|
|
|
165
222
|
}, HEARTBEAT_INTERVAL_MS);
|
|
166
223
|
this.heartbeatTimer.unref?.();
|
|
167
224
|
this.installSignalHandlers();
|
|
225
|
+
this.installProcessErrorHandlers();
|
|
168
226
|
return {
|
|
169
227
|
started: true,
|
|
170
228
|
record: this.lockRecord,
|
|
@@ -228,3 +228,116 @@
|
|
|
228
228
|
align-self: center;
|
|
229
229
|
white-space: nowrap;
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
/* ── MCP Live Activity Strip ─────────────────────────────────────────────────── */
|
|
233
|
+
.rt-mcp-strip {
|
|
234
|
+
min-height: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.rt-mcp-strip-inner {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-wrap: wrap;
|
|
240
|
+
gap: 6px;
|
|
241
|
+
padding: 2px 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.rt-mcp-pill {
|
|
245
|
+
display: inline-flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
gap: 5px;
|
|
248
|
+
border-radius: 20px;
|
|
249
|
+
padding: 3px 10px 3px 7px;
|
|
250
|
+
font: 11px var(--font-mono, ui-monospace, monospace);
|
|
251
|
+
animation: rt-pill-in 0.15s ease;
|
|
252
|
+
white-space: nowrap;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.rt-mcp-active {
|
|
256
|
+
background: oklch(87% 0.19 152 / 8%);
|
|
257
|
+
border: 1px solid oklch(87% 0.19 152 / 28%);
|
|
258
|
+
color: var(--accent);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.rt-mcp-long {
|
|
262
|
+
background: oklch(85% 0.18 80 / 8%);
|
|
263
|
+
border: 1px solid oklch(85% 0.18 80 / 35%);
|
|
264
|
+
color: var(--warn);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.rt-mcp-done {
|
|
268
|
+
background: var(--surface);
|
|
269
|
+
border: 1px solid var(--border);
|
|
270
|
+
color: var(--text-muted);
|
|
271
|
+
opacity: 0.75;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.rt-mcp-err {
|
|
275
|
+
background: oklch(65% 0.22 25 / 8%);
|
|
276
|
+
border: 1px solid oklch(65% 0.22 25 / 25%);
|
|
277
|
+
color: var(--bad, #ef4444);
|
|
278
|
+
opacity: 0.85;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.rt-mcp-tool { letter-spacing: 0.01em; }
|
|
282
|
+
.rt-mcp-elapsed { opacity: 0.65; font-size: 10px; }
|
|
283
|
+
|
|
284
|
+
.rt-mcp-badge {
|
|
285
|
+
font-size: 10px;
|
|
286
|
+
padding: 1px 5px;
|
|
287
|
+
border-radius: 8px;
|
|
288
|
+
margin-left: 3px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.rt-mcp-badge-ok { background: oklch(87% 0.19 152 / 12%); color: var(--ok, #22c55e); }
|
|
292
|
+
.rt-mcp-badge-err { background: oklch(65% 0.22 25 / 12%); color: var(--bad, #ef4444); }
|
|
293
|
+
|
|
294
|
+
/* ── Toast system ─────────────────────────────────────────────────────────────── */
|
|
295
|
+
.rt-toast-container {
|
|
296
|
+
position: fixed;
|
|
297
|
+
bottom: 24px;
|
|
298
|
+
right: 24px;
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-direction: column-reverse;
|
|
301
|
+
gap: 8px;
|
|
302
|
+
z-index: 9999;
|
|
303
|
+
pointer-events: none;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.rt-toast {
|
|
307
|
+
padding: 10px 16px;
|
|
308
|
+
border-radius: 8px;
|
|
309
|
+
font: 13px ui-sans-serif, system-ui, sans-serif;
|
|
310
|
+
max-width: 360px;
|
|
311
|
+
box-shadow: 0 4px 16px oklch(0% 0 0 / 40%);
|
|
312
|
+
animation: rt-toast-in 0.2s ease;
|
|
313
|
+
pointer-events: auto;
|
|
314
|
+
transition: opacity 0.4s;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.rt-toast-info { background: var(--surface); border: 1px solid var(--accent); color: var(--text); }
|
|
318
|
+
.rt-toast-warn { background: var(--surface); border: 1px solid var(--warn); color: var(--warn); }
|
|
319
|
+
.rt-toast-bad { background: var(--surface); border: 1px solid var(--bad, #ef4444); color: var(--bad, #ef4444); }
|
|
320
|
+
|
|
321
|
+
.rt-toast-fade { opacity: 0; }
|
|
322
|
+
|
|
323
|
+
@keyframes rt-toast-in {
|
|
324
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
325
|
+
to { opacity: 1; transform: none; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ── Upgrade nudge bar ───────────────────────────────────────────────────────── */
|
|
329
|
+
.rt-upgrade-bar {
|
|
330
|
+
display: flex;
|
|
331
|
+
align-items: center;
|
|
332
|
+
gap: 12px;
|
|
333
|
+
background: oklch(85% 0.18 80 / 8%);
|
|
334
|
+
border: 1px solid oklch(85% 0.18 80 / 30%);
|
|
335
|
+
border-radius: 8px;
|
|
336
|
+
padding: 10px 14px;
|
|
337
|
+
margin-bottom: 14px;
|
|
338
|
+
font-size: 13px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.rt-upgrade-msg { flex: 1; color: var(--warn); }
|
|
342
|
+
.rt-upgrade-btn { padding: 4px 12px; border-radius: 6px; background: var(--warn); color: #000; font-weight: 600; font-size: 12px; text-decoration: none; }
|
|
343
|
+
.rt-upgrade-dismiss { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 14px; padding: 0 4px; }
|