clementine-agent 1.2.2 → 1.3.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/agent/assistant.js +12 -0
- package/dist/cli/dashboard.js +3034 -734
- package/dist/cli/static/LICENSE-NOTICES.md +12 -0
- package/dist/cli/static/drawflow.min.css +1 -0
- package/dist/cli/static/drawflow.min.js +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +16 -0
- package/dist/dashboard/builder/dry-run.d.ts +31 -0
- package/dist/dashboard/builder/dry-run.js +138 -0
- package/dist/dashboard/builder/events.d.ts +23 -0
- package/dist/dashboard/builder/events.js +28 -0
- package/dist/dashboard/builder/mcp-invoke.d.ts +25 -0
- package/dist/dashboard/builder/mcp-invoke.js +143 -0
- package/dist/dashboard/builder/runner.d.ts +68 -0
- package/dist/dashboard/builder/runner.js +418 -0
- package/dist/dashboard/builder/serializer.d.ts +79 -0
- package/dist/dashboard/builder/serializer.js +547 -0
- package/dist/dashboard/builder/snapshots.d.ts +32 -0
- package/dist/dashboard/builder/snapshots.js +138 -0
- package/dist/dashboard/builder/validation.d.ts +26 -0
- package/dist/dashboard/builder/validation.js +183 -0
- package/dist/gateway/router.js +31 -2
- package/dist/index.js +38 -0
- package/dist/memory/chunker.js +13 -2
- package/dist/memory/hot-cache.d.ts +38 -0
- package/dist/memory/hot-cache.js +73 -0
- package/dist/memory/integrity.d.ts +28 -0
- package/dist/memory/integrity.js +119 -0
- package/dist/memory/maintenance.d.ts +23 -2
- package/dist/memory/maintenance.js +140 -3
- package/dist/memory/store.d.ts +259 -2
- package/dist/memory/store.js +751 -21
- package/dist/memory/write-queue.d.ts +96 -0
- package/dist/memory/write-queue.js +165 -0
- package/dist/tools/builder-tools.d.ts +13 -0
- package/dist/tools/builder-tools.js +437 -0
- package/dist/tools/mcp-server.js +2 -0
- package/dist/tools/memory-tools.js +38 -1
- package/dist/types.d.ts +56 -2
- package/package.json +2 -2
- package/vault/00-System/skills/builder-canvas.md +126 -0
package/dist/cli/dashboard.js
CHANGED
|
@@ -18,7 +18,7 @@ import cron from 'node-cron';
|
|
|
18
18
|
import { TunnelManager } from './tunnel.js';
|
|
19
19
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
20
20
|
import { discoverMcpServers, getClaudeIntegrations } from '../agent/mcp-bridge.js';
|
|
21
|
-
import { AGENTS_DIR } from '../config.js';
|
|
21
|
+
import { AGENTS_DIR, SESSIONS_FILE } from '../config.js';
|
|
22
22
|
import { parseTasks } from '../tools/shared.js';
|
|
23
23
|
import { todayISO } from '../gateway/cron-scheduler.js';
|
|
24
24
|
import { goalsRouter } from './routes/goals.js';
|
|
@@ -1265,6 +1265,13 @@ export async function cmdDashboard(opts) {
|
|
|
1265
1265
|
const app = express();
|
|
1266
1266
|
const jsonParser = express.json({ limit: '5mb' });
|
|
1267
1267
|
const rawBodyParser = express.raw({ type: '*/*', limit: '5mb' });
|
|
1268
|
+
// Vendored frontend assets (Drawflow for the Builder canvas, etc.)
|
|
1269
|
+
// Public — no auth required.
|
|
1270
|
+
app.use('/static', express.static(path.join(__dirname, 'static'), {
|
|
1271
|
+
maxAge: '7d',
|
|
1272
|
+
etag: true,
|
|
1273
|
+
index: false,
|
|
1274
|
+
}));
|
|
1268
1275
|
// Health check — always responds, no auth, no middleware dependency
|
|
1269
1276
|
app.get('/health', (_req, res) => { res.json({ ok: true, ts: Date.now() }); });
|
|
1270
1277
|
// ── Webhook ingestion (raw-body, HMAC-authed) ─────────────────────
|
|
@@ -1439,44 +1446,92 @@ export async function cmdDashboard(opts) {
|
|
|
1439
1446
|
writeFileSync(tokenPath, dashboardToken, { mode: 0o600 });
|
|
1440
1447
|
// ── Remote access + session management ─────────────────────────────
|
|
1441
1448
|
const remoteConfig = loadRemoteConfig();
|
|
1442
|
-
const sessions = new Map();
|
|
1449
|
+
const sessions = new Map();
|
|
1443
1450
|
let tunnelManager = null;
|
|
1444
|
-
const
|
|
1451
|
+
const SESSION_TTL_DEFAULT = 24 * 60 * 60 * 1000; // 24 hours
|
|
1452
|
+
const SESSION_TTL_PERSISTENT = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
1445
1453
|
const loginRateLimit = { count: 0, resetAt: Date.now() + 15 * 60 * 1000 };
|
|
1454
|
+
function loadSessions() {
|
|
1455
|
+
if (!existsSync(SESSIONS_FILE))
|
|
1456
|
+
return;
|
|
1457
|
+
try {
|
|
1458
|
+
const raw = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
|
|
1459
|
+
const now = Date.now();
|
|
1460
|
+
for (const s of raw) {
|
|
1461
|
+
if (s && s.id && s.expiresAt > now)
|
|
1462
|
+
sessions.set(s.id, s);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
catch { /* corrupt file — start fresh */ }
|
|
1466
|
+
}
|
|
1467
|
+
function persistSessions() {
|
|
1468
|
+
try {
|
|
1469
|
+
writeFileSync(SESSIONS_FILE, JSON.stringify(Array.from(sessions.values()), null, 2), { mode: 0o600 });
|
|
1470
|
+
}
|
|
1471
|
+
catch { /* best-effort; in-memory store still works */ }
|
|
1472
|
+
}
|
|
1473
|
+
loadSessions();
|
|
1446
1474
|
function isRemoteRequest(req) {
|
|
1447
1475
|
// cloudflared sets CF-Connecting-IP for tunneled traffic
|
|
1448
1476
|
return Boolean(req.headers['cf-connecting-ip']);
|
|
1449
1477
|
}
|
|
1450
|
-
function
|
|
1478
|
+
function readSessionId(req) {
|
|
1451
1479
|
const cookie = req.headers.cookie ?? '';
|
|
1452
1480
|
const match = cookie.match(/__clem_session=([a-f0-9]+)/);
|
|
1453
|
-
|
|
1481
|
+
return match ? match[1] : null;
|
|
1482
|
+
}
|
|
1483
|
+
function hasValidSession(req) {
|
|
1484
|
+
const sessionId = readSessionId(req);
|
|
1485
|
+
if (!sessionId)
|
|
1454
1486
|
return false;
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1487
|
+
const record = sessions.get(sessionId);
|
|
1488
|
+
if (!record || Date.now() > record.expiresAt) {
|
|
1489
|
+
if (record) {
|
|
1490
|
+
sessions.delete(sessionId);
|
|
1491
|
+
persistSessions();
|
|
1492
|
+
}
|
|
1459
1493
|
return false;
|
|
1460
1494
|
}
|
|
1495
|
+
record.lastUsedAt = Date.now();
|
|
1496
|
+
// Don't persist on every request — the cleanup interval will pick it up
|
|
1461
1497
|
return true;
|
|
1462
1498
|
}
|
|
1463
|
-
function createSession(res) {
|
|
1499
|
+
function createSession(res, req, persistent = false) {
|
|
1464
1500
|
const sessionId = randomBytes(32).toString('hex');
|
|
1465
|
-
|
|
1501
|
+
const ttl = persistent ? SESSION_TTL_PERSISTENT : SESSION_TTL_DEFAULT;
|
|
1502
|
+
const now = Date.now();
|
|
1503
|
+
const record = {
|
|
1504
|
+
id: sessionId,
|
|
1505
|
+
expiresAt: now + ttl,
|
|
1506
|
+
persistent,
|
|
1507
|
+
createdAt: now,
|
|
1508
|
+
lastUsedAt: now,
|
|
1509
|
+
userAgent: typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'].slice(0, 200) : undefined,
|
|
1510
|
+
};
|
|
1511
|
+
sessions.set(sessionId, record);
|
|
1512
|
+
persistSessions();
|
|
1466
1513
|
res.cookie('__clem_session', sessionId, {
|
|
1467
1514
|
httpOnly: true,
|
|
1468
1515
|
sameSite: 'lax',
|
|
1469
|
-
maxAge:
|
|
1516
|
+
maxAge: ttl,
|
|
1470
1517
|
path: '/',
|
|
1471
1518
|
});
|
|
1519
|
+
return sessionId;
|
|
1472
1520
|
}
|
|
1473
|
-
|
|
1521
|
+
function revokeSession(sessionId) {
|
|
1522
|
+
const existed = sessions.delete(sessionId);
|
|
1523
|
+
if (existed)
|
|
1524
|
+
persistSessions();
|
|
1525
|
+
return existed;
|
|
1526
|
+
}
|
|
1527
|
+
// Clean expired sessions every 10 minutes; also persist lastUsedAt updates
|
|
1474
1528
|
setInterval(() => {
|
|
1475
1529
|
const now = Date.now();
|
|
1476
|
-
for (const [id,
|
|
1477
|
-
if (now >
|
|
1530
|
+
for (const [id, rec] of sessions) {
|
|
1531
|
+
if (now > rec.expiresAt)
|
|
1478
1532
|
sessions.delete(id);
|
|
1479
1533
|
}
|
|
1534
|
+
persistSessions();
|
|
1480
1535
|
}, 10 * 60 * 1000);
|
|
1481
1536
|
// Quick ping — bypasses all middleware, tests /api path routing
|
|
1482
1537
|
app.get('/api/ping', (_req, res) => { res.json({ pong: true }); });
|
|
@@ -1734,7 +1789,7 @@ export async function cmdDashboard(opts) {
|
|
|
1734
1789
|
res.status(429).json({ error: 'Too many login attempts. Try again later.' });
|
|
1735
1790
|
return;
|
|
1736
1791
|
}
|
|
1737
|
-
const { token } = req.body ?? {};
|
|
1792
|
+
const { token, remember } = req.body ?? {};
|
|
1738
1793
|
if (!token || typeof token !== 'string') {
|
|
1739
1794
|
res.status(400).json({ error: 'Token is required' });
|
|
1740
1795
|
return;
|
|
@@ -1748,13 +1803,48 @@ export async function cmdDashboard(opts) {
|
|
|
1748
1803
|
res.status(401).json({ error: 'Invalid access token' });
|
|
1749
1804
|
return;
|
|
1750
1805
|
}
|
|
1751
|
-
createSession(res);
|
|
1806
|
+
createSession(res, req, Boolean(remember));
|
|
1752
1807
|
res.json({ ok: true });
|
|
1753
1808
|
});
|
|
1754
|
-
app.get('/auth/logout', (
|
|
1809
|
+
app.get('/auth/logout', (req, res) => {
|
|
1810
|
+
const sessionId = readSessionId(req);
|
|
1811
|
+
if (sessionId)
|
|
1812
|
+
revokeSession(sessionId);
|
|
1755
1813
|
res.clearCookie('__clem_session', { path: '/' });
|
|
1756
1814
|
res.redirect('/');
|
|
1757
1815
|
});
|
|
1816
|
+
// List active sessions (cookie-authenticated; bearer-token gate doesn't apply at /auth/*)
|
|
1817
|
+
app.get('/auth/sessions', (req, res) => {
|
|
1818
|
+
if (!hasValidSession(req)) {
|
|
1819
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const currentId = readSessionId(req);
|
|
1823
|
+
const list = Array.from(sessions.values())
|
|
1824
|
+
.sort((a, b) => b.lastUsedAt - a.lastUsedAt)
|
|
1825
|
+
.map(s => ({
|
|
1826
|
+
id: s.id,
|
|
1827
|
+
createdAt: s.createdAt,
|
|
1828
|
+
lastUsedAt: s.lastUsedAt,
|
|
1829
|
+
expiresAt: s.expiresAt,
|
|
1830
|
+
persistent: s.persistent,
|
|
1831
|
+
userAgent: s.userAgent ?? null,
|
|
1832
|
+
current: s.id === currentId,
|
|
1833
|
+
}));
|
|
1834
|
+
res.json({ sessions: list });
|
|
1835
|
+
});
|
|
1836
|
+
app.delete('/auth/sessions/:id', (req, res) => {
|
|
1837
|
+
if (!hasValidSession(req)) {
|
|
1838
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const targetId = req.params.id;
|
|
1842
|
+
const existed = revokeSession(targetId);
|
|
1843
|
+
if (readSessionId(req) === targetId) {
|
|
1844
|
+
res.clearCookie('__clem_session', { path: '/' });
|
|
1845
|
+
}
|
|
1846
|
+
res.json({ ok: existed });
|
|
1847
|
+
});
|
|
1758
1848
|
// ── Anthropic OAuth routes ──────────────────────────────────────
|
|
1759
1849
|
// Check current auth status by spawning a lightweight SDK query
|
|
1760
1850
|
// Anthropic auth status — check daemon's .env for API key presence instead of importing the SDK
|
|
@@ -2320,6 +2410,260 @@ export async function cmdDashboard(opts) {
|
|
|
2320
2410
|
}
|
|
2321
2411
|
// Let the lazy-gateway dispatcher publish deep_result events through SSE.
|
|
2322
2412
|
dashboardSseBroadcast = broadcastEvent;
|
|
2413
|
+
// ── Builder event bridge ──────────────────────────────────────
|
|
2414
|
+
// Forward events from src/dashboard/builder/events.ts through SSE so the
|
|
2415
|
+
// Builder page can update its canvas live as the agent edits via MCP tools.
|
|
2416
|
+
(async () => {
|
|
2417
|
+
try {
|
|
2418
|
+
const { onAnyBuilderEvent } = await import('../dashboard/builder/events.js');
|
|
2419
|
+
onAnyBuilderEvent((e) => {
|
|
2420
|
+
broadcastEvent({ type: 'builder', data: e });
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
catch {
|
|
2424
|
+
// Builder event bridge optional; ignore if module fails to load.
|
|
2425
|
+
}
|
|
2426
|
+
})();
|
|
2427
|
+
// ── Builder API routes ─────────────────────────────────────────
|
|
2428
|
+
app.get('/api/builder/workflows', async (_req, res) => {
|
|
2429
|
+
try {
|
|
2430
|
+
const { listAllForBuilder } = await import('../dashboard/builder/serializer.js');
|
|
2431
|
+
res.json({ workflows: listAllForBuilder() });
|
|
2432
|
+
}
|
|
2433
|
+
catch (err) {
|
|
2434
|
+
res.status(500).json({ error: 'Failed to list workflows', detail: String(err) });
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
app.get('/api/builder/workflows/:id', async (req, res) => {
|
|
2438
|
+
try {
|
|
2439
|
+
const id = decodeURIComponent(req.params.id);
|
|
2440
|
+
const [{ readWorkflow, workflowToDrawflow }, { validateWorkflow }] = await Promise.all([
|
|
2441
|
+
import('../dashboard/builder/serializer.js'),
|
|
2442
|
+
import('../dashboard/builder/validation.js'),
|
|
2443
|
+
]);
|
|
2444
|
+
const wf = readWorkflow(id);
|
|
2445
|
+
if (!wf) {
|
|
2446
|
+
res.status(404).json({ error: 'Not found' });
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
res.json({
|
|
2450
|
+
id,
|
|
2451
|
+
workflow: wf,
|
|
2452
|
+
drawflow: workflowToDrawflow(wf),
|
|
2453
|
+
validation: validateWorkflow(wf),
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
catch (err) {
|
|
2457
|
+
res.status(500).json({ error: 'Failed to read workflow', detail: String(err) });
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
app.put('/api/builder/workflows/:id', async (req, res) => {
|
|
2461
|
+
try {
|
|
2462
|
+
const id = decodeURIComponent(req.params.id);
|
|
2463
|
+
const body = req.body;
|
|
2464
|
+
if (!body || typeof body.workflow !== 'object') {
|
|
2465
|
+
res.status(400).json({ error: 'Missing workflow body' });
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
const [{ readWorkflow, saveWorkflow }, { validateWorkflow }, { emitBuilderEvent }] = await Promise.all([
|
|
2469
|
+
import('../dashboard/builder/serializer.js'),
|
|
2470
|
+
import('../dashboard/builder/validation.js'),
|
|
2471
|
+
import('../dashboard/builder/events.js'),
|
|
2472
|
+
]);
|
|
2473
|
+
const existing = readWorkflow(id);
|
|
2474
|
+
if (!existing) {
|
|
2475
|
+
res.status(404).json({ error: 'Not found' });
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const incoming = body.workflow;
|
|
2479
|
+
const next = { ...incoming, sourceFile: existing.sourceFile };
|
|
2480
|
+
const v = validateWorkflow(next);
|
|
2481
|
+
if (!v.ok && !body.force) {
|
|
2482
|
+
res.status(400).json({ error: 'validation', validation: v });
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const result = saveWorkflow(id, next);
|
|
2486
|
+
if (!result.ok) {
|
|
2487
|
+
res.status(400).json({ error: result.error });
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
emitBuilderEvent({ type: 'workflow:patched', workflowId: id, payload: { workflow: next } });
|
|
2491
|
+
res.json({ ok: true, validation: v });
|
|
2492
|
+
}
|
|
2493
|
+
catch (err) {
|
|
2494
|
+
res.status(500).json({ error: 'Failed to save workflow', detail: String(err) });
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
app.get('/api/builder/mcp-discovery', async (_req, res) => {
|
|
2498
|
+
try {
|
|
2499
|
+
const { discoverMcpServers, loadToolInventory } = await import('../agent/mcp-bridge.js');
|
|
2500
|
+
const servers = discoverMcpServers();
|
|
2501
|
+
const inv = loadToolInventory();
|
|
2502
|
+
const out = servers.map((s) => ({
|
|
2503
|
+
name: s.name,
|
|
2504
|
+
enabled: s.enabled,
|
|
2505
|
+
tools: (inv?.tools ?? [])
|
|
2506
|
+
.filter((t) => t.startsWith(`mcp__${s.name}__`))
|
|
2507
|
+
.map((t) => t.split('__')[2])
|
|
2508
|
+
.filter((x) => !!x),
|
|
2509
|
+
}));
|
|
2510
|
+
res.json({ servers: out });
|
|
2511
|
+
}
|
|
2512
|
+
catch (err) {
|
|
2513
|
+
res.status(500).json({ error: 'mcp-discovery failed', detail: String(err) });
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
app.post('/api/builder/workflows/:id/save-from-drawflow', async (req, res) => {
|
|
2517
|
+
try {
|
|
2518
|
+
const id = decodeURIComponent(req.params.id);
|
|
2519
|
+
const body = req.body;
|
|
2520
|
+
if (!body || !body.drawflow) {
|
|
2521
|
+
res.status(400).json({ error: 'Missing drawflow body' });
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const [{ readWorkflow, drawflowToWorkflow, saveWorkflow, workflowToDrawflow }, { validateWorkflow }, { emitBuilderEvent }] = await Promise.all([
|
|
2525
|
+
import('../dashboard/builder/serializer.js'),
|
|
2526
|
+
import('../dashboard/builder/validation.js'),
|
|
2527
|
+
import('../dashboard/builder/events.js'),
|
|
2528
|
+
]);
|
|
2529
|
+
const existing = readWorkflow(id);
|
|
2530
|
+
if (!existing) {
|
|
2531
|
+
res.status(404).json({ error: 'Not found' });
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const next = drawflowToWorkflow(body.drawflow, existing);
|
|
2535
|
+
const v = validateWorkflow(next);
|
|
2536
|
+
if (!v.ok && !body.force) {
|
|
2537
|
+
res.status(400).json({ error: 'validation', validation: v });
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
const result = saveWorkflow(id, next);
|
|
2541
|
+
if (!result.ok) {
|
|
2542
|
+
res.status(400).json({ error: result.error });
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
emitBuilderEvent({ type: 'workflow:patched', workflowId: id, payload: { workflow: next, saveToken: body.saveToken ?? null } });
|
|
2546
|
+
res.json({ ok: true, validation: v, drawflow: workflowToDrawflow(next) });
|
|
2547
|
+
}
|
|
2548
|
+
catch (err) {
|
|
2549
|
+
res.status(500).json({ error: 'Save failed', detail: String(err) });
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
app.post('/api/builder/workflows/:id/validate', async (req, res) => {
|
|
2553
|
+
try {
|
|
2554
|
+
const id = decodeURIComponent(req.params.id);
|
|
2555
|
+
const [{ readWorkflow }, { validateWorkflow }] = await Promise.all([
|
|
2556
|
+
import('../dashboard/builder/serializer.js'),
|
|
2557
|
+
import('../dashboard/builder/validation.js'),
|
|
2558
|
+
]);
|
|
2559
|
+
const wf = readWorkflow(id);
|
|
2560
|
+
if (!wf) {
|
|
2561
|
+
res.status(404).json({ error: 'Not found' });
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
res.json(validateWorkflow(wf));
|
|
2565
|
+
}
|
|
2566
|
+
catch (err) {
|
|
2567
|
+
res.status(500).json({ error: 'Validate failed', detail: String(err) });
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
app.post('/api/builder/workflows/:id/test', async (req, res) => {
|
|
2571
|
+
try {
|
|
2572
|
+
const id = decodeURIComponent(req.params.id);
|
|
2573
|
+
const body = (req.body ?? {});
|
|
2574
|
+
const [{ readWorkflow }, { runWorkflowTest }] = await Promise.all([
|
|
2575
|
+
import('../dashboard/builder/serializer.js'),
|
|
2576
|
+
import('../dashboard/builder/runner.js'),
|
|
2577
|
+
]);
|
|
2578
|
+
const wf = readWorkflow(id);
|
|
2579
|
+
if (!wf) {
|
|
2580
|
+
res.status(404).json({ error: 'Not found' });
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
const runId = (await import('node:crypto')).randomUUID();
|
|
2584
|
+
// Kick off async — respond immediately with the runId; events stream over SSE.
|
|
2585
|
+
res.json({ ok: true, runId });
|
|
2586
|
+
runWorkflowTest(wf, {
|
|
2587
|
+
workflowId: id,
|
|
2588
|
+
runId,
|
|
2589
|
+
mode: body.mode ?? 'mock',
|
|
2590
|
+
perStepTimeoutMs: body.perStepTimeoutMs,
|
|
2591
|
+
totalBudgetMs: body.totalBudgetMs,
|
|
2592
|
+
}).catch(() => { });
|
|
2593
|
+
}
|
|
2594
|
+
catch (err) {
|
|
2595
|
+
res.status(500).json({ error: 'Test failed to start', detail: String(err) });
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
app.post('/api/builder/runs/:runId/cancel', async (req, res) => {
|
|
2599
|
+
try {
|
|
2600
|
+
const runId = decodeURIComponent(req.params.runId);
|
|
2601
|
+
const { cancelRun } = await import('../dashboard/builder/runner.js');
|
|
2602
|
+
const cancelled = cancelRun(runId);
|
|
2603
|
+
res.json({ ok: cancelled });
|
|
2604
|
+
}
|
|
2605
|
+
catch (err) {
|
|
2606
|
+
res.status(500).json({ error: 'Cancel failed', detail: String(err) });
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
app.post('/api/builder/workflows/:id/dry-run', async (req, res) => {
|
|
2610
|
+
try {
|
|
2611
|
+
const id = decodeURIComponent(req.params.id);
|
|
2612
|
+
const [{ readWorkflow }, { dryRunWorkflow }] = await Promise.all([
|
|
2613
|
+
import('../dashboard/builder/serializer.js'),
|
|
2614
|
+
import('../dashboard/builder/dry-run.js'),
|
|
2615
|
+
]);
|
|
2616
|
+
const wf = readWorkflow(id);
|
|
2617
|
+
if (!wf) {
|
|
2618
|
+
res.status(404).json({ error: 'Not found' });
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
res.json(dryRunWorkflow(wf));
|
|
2622
|
+
}
|
|
2623
|
+
catch (err) {
|
|
2624
|
+
res.status(500).json({ error: 'Dry-run failed', detail: String(err) });
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
app.post('/api/builder/workflows', async (req, res) => {
|
|
2628
|
+
try {
|
|
2629
|
+
const body = req.body;
|
|
2630
|
+
if (!body || !body.name) {
|
|
2631
|
+
res.status(400).json({ error: 'name required' });
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const [{ saveWorkflow, workflowId }, { emitBuilderEvent }] = await Promise.all([
|
|
2635
|
+
import('../dashboard/builder/serializer.js'),
|
|
2636
|
+
import('../dashboard/builder/events.js'),
|
|
2637
|
+
]);
|
|
2638
|
+
const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'workflow';
|
|
2639
|
+
const wf = {
|
|
2640
|
+
name: body.name,
|
|
2641
|
+
description: body.description ?? '',
|
|
2642
|
+
enabled: true,
|
|
2643
|
+
trigger: body.schedule ? { schedule: body.schedule, manual: false } : { manual: true },
|
|
2644
|
+
inputs: {},
|
|
2645
|
+
steps: [{
|
|
2646
|
+
id: 's1',
|
|
2647
|
+
prompt: body.initialPrompt ?? 'Describe what this workflow should do.',
|
|
2648
|
+
dependsOn: [],
|
|
2649
|
+
tier: 1,
|
|
2650
|
+
maxTurns: 15,
|
|
2651
|
+
}],
|
|
2652
|
+
sourceFile: '',
|
|
2653
|
+
};
|
|
2654
|
+
const id = workflowId(slug);
|
|
2655
|
+
const result = saveWorkflow(id, wf);
|
|
2656
|
+
if (!result.ok) {
|
|
2657
|
+
res.status(400).json({ error: result.error });
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
emitBuilderEvent({ type: 'workflow:created', workflowId: id, payload: { workflow: wf } });
|
|
2661
|
+
res.json({ ok: true, id });
|
|
2662
|
+
}
|
|
2663
|
+
catch (err) {
|
|
2664
|
+
res.status(500).json({ error: 'Create failed', detail: String(err) });
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2323
2667
|
// SSE events handler moved before auth middleware (see above)
|
|
2324
2668
|
// ── POST routes (actions) ──────────────────────────────────────
|
|
2325
2669
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
@@ -4521,6 +4865,51 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4521
4865
|
// instead of having to wait for auto-extraction to drift in the right
|
|
4522
4866
|
// direction. Soft-delete via deleted_at; FTS trigger keeps deleted
|
|
4523
4867
|
// content out of search results.
|
|
4868
|
+
// Memory Health snapshot — single endpoint feeding the dashboard tab.
|
|
4869
|
+
// Read-only aggregate over the existing tables; no caching needed (cheap).
|
|
4870
|
+
app.get('/api/memory/health', async (_req, res) => {
|
|
4871
|
+
try {
|
|
4872
|
+
const gateway = await getGateway();
|
|
4873
|
+
const store = gateway.assistant?.memoryStore;
|
|
4874
|
+
if (!store?.getMemoryHealth) {
|
|
4875
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4878
|
+
const graphStore = gateway.assistant?.graphStore;
|
|
4879
|
+
const health = store.getMemoryHealth({ graphStore, topCitedLimit: 10 });
|
|
4880
|
+
res.json({ ok: true, health });
|
|
4881
|
+
}
|
|
4882
|
+
catch (err) {
|
|
4883
|
+
res.status(500).json({ error: String(err) });
|
|
4884
|
+
}
|
|
4885
|
+
});
|
|
4886
|
+
app.post('/api/memory/health/action', async (req, res) => {
|
|
4887
|
+
try {
|
|
4888
|
+
const action = (req.body?.action ?? '');
|
|
4889
|
+
const gateway = await getGateway();
|
|
4890
|
+
const store = gateway.assistant?.memoryStore;
|
|
4891
|
+
if (!store) {
|
|
4892
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
if (action === 'janitor') {
|
|
4896
|
+
const { runJanitor } = await import('../memory/maintenance.js');
|
|
4897
|
+
const result = runJanitor(store);
|
|
4898
|
+
res.json({ ok: true, action, result });
|
|
4899
|
+
return;
|
|
4900
|
+
}
|
|
4901
|
+
if (action === 'rebuild-fts' || action === 'fix-orphans') {
|
|
4902
|
+
const { runIntegrityProbes } = await import('../memory/integrity.js');
|
|
4903
|
+
const report = runIntegrityProbes(store);
|
|
4904
|
+
res.json({ ok: true, action, report });
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
res.status(400).json({ error: 'Unknown action: ' + action });
|
|
4908
|
+
}
|
|
4909
|
+
catch (err) {
|
|
4910
|
+
res.status(500).json({ error: String(err) });
|
|
4911
|
+
}
|
|
4912
|
+
});
|
|
4524
4913
|
app.get('/api/memory/chunks/:id', async (req, res) => {
|
|
4525
4914
|
try {
|
|
4526
4915
|
const id = Number(req.params.id);
|
|
@@ -7730,6 +8119,7 @@ function getDashboardHTML(token) {
|
|
|
7730
8119
|
if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(r){r.forEach(function(sw){sw.unregister()})});caches.keys().then(function(k){k.forEach(function(n){caches.delete(n)})})}
|
|
7731
8120
|
</script>
|
|
7732
8121
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
|
8122
|
+
<link rel="stylesheet" href="/static/drawflow.min.css">
|
|
7733
8123
|
<title>${name} Command Center</title>
|
|
7734
8124
|
<style>
|
|
7735
8125
|
:root {
|
|
@@ -7989,101 +8379,414 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
7989
8379
|
letter-spacing: -0.02em;
|
|
7990
8380
|
}
|
|
7991
8381
|
|
|
7992
|
-
/* ──
|
|
7993
|
-
.
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
margin-bottom: 16px;
|
|
7999
|
-
overflow: hidden;
|
|
8000
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
|
8001
|
-
}
|
|
8002
|
-
.card-header {
|
|
8003
|
-
padding: 14px 18px;
|
|
8382
|
+
/* ── Standard page header (.page-head) — applied to most info pages ── */
|
|
8383
|
+
.page-head {
|
|
8384
|
+
display: flex;
|
|
8385
|
+
align-items: flex-start;
|
|
8386
|
+
gap: 14px;
|
|
8387
|
+
padding: 18px 22px 14px;
|
|
8004
8388
|
border-bottom: 1px solid var(--border);
|
|
8389
|
+
margin-bottom: 18px;
|
|
8390
|
+
flex-wrap: wrap;
|
|
8391
|
+
}
|
|
8392
|
+
.page-head .icon {
|
|
8393
|
+
width: 36px;
|
|
8394
|
+
height: 36px;
|
|
8395
|
+
border-radius: 8px;
|
|
8396
|
+
background: linear-gradient(135deg, rgba(255,140,33,0.15), rgba(255,140,33,0.04));
|
|
8397
|
+
color: var(--clementine);
|
|
8005
8398
|
display: flex;
|
|
8006
8399
|
align-items: center;
|
|
8007
|
-
justify-content:
|
|
8008
|
-
font-size:
|
|
8400
|
+
justify-content: center;
|
|
8401
|
+
font-size: 20px;
|
|
8402
|
+
flex-shrink: 0;
|
|
8403
|
+
}
|
|
8404
|
+
.page-head .title-block {
|
|
8405
|
+
flex: 1;
|
|
8406
|
+
min-width: 220px;
|
|
8407
|
+
}
|
|
8408
|
+
.page-head .title-block h1 {
|
|
8409
|
+
font-size: 20px;
|
|
8009
8410
|
font-weight: 600;
|
|
8411
|
+
margin: 0 0 2px;
|
|
8412
|
+
letter-spacing: -0.01em;
|
|
8010
8413
|
color: var(--text-primary);
|
|
8011
8414
|
}
|
|
8012
|
-
.
|
|
8013
|
-
padding: 18px;
|
|
8415
|
+
.page-head .title-block .desc {
|
|
8014
8416
|
font-size: 13px;
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
display: grid;
|
|
8019
|
-
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
|
8020
|
-
gap: 20px;
|
|
8021
|
-
}
|
|
8022
|
-
|
|
8023
|
-
/* ── Getting Started ────────────────────── */
|
|
8024
|
-
.getting-started {
|
|
8025
|
-
background: linear-gradient(135deg, var(--clementine-bg), rgba(77,158,255,0.06));
|
|
8026
|
-
border: 1px solid var(--border);
|
|
8027
|
-
border-radius: 16px;
|
|
8028
|
-
padding: 24px;
|
|
8029
|
-
margin-bottom: 24px;
|
|
8030
|
-
position: relative;
|
|
8417
|
+
color: var(--text-muted);
|
|
8418
|
+
margin: 0;
|
|
8419
|
+
line-height: 1.4;
|
|
8031
8420
|
}
|
|
8032
|
-
.
|
|
8421
|
+
.page-head .actions {
|
|
8033
8422
|
display: flex;
|
|
8423
|
+
gap: 8px;
|
|
8034
8424
|
align-items: center;
|
|
8035
|
-
|
|
8036
|
-
margin-bottom: 20px;
|
|
8425
|
+
flex-wrap: wrap;
|
|
8037
8426
|
}
|
|
8038
|
-
.
|
|
8039
|
-
|
|
8040
|
-
font-weight: 700;
|
|
8041
|
-
color: var(--text-primary);
|
|
8427
|
+
.page-section {
|
|
8428
|
+
padding: 0 22px 22px;
|
|
8042
8429
|
}
|
|
8043
|
-
|
|
8044
|
-
|
|
8430
|
+
/* ── First-time empty state with CTA (use when there's truly no data yet) ── */
|
|
8431
|
+
.empty-cta {
|
|
8432
|
+
text-align: center;
|
|
8433
|
+
padding: 48px 24px;
|
|
8045
8434
|
color: var(--text-muted);
|
|
8046
|
-
flex: 1;
|
|
8047
8435
|
}
|
|
8048
|
-
.
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
font-size: 18px;
|
|
8436
|
+
.empty-cta .icon {
|
|
8437
|
+
font-size: 36px;
|
|
8438
|
+
margin-bottom: 12px;
|
|
8439
|
+
opacity: 0.5;
|
|
8053
8440
|
}
|
|
8054
|
-
.
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8441
|
+
.empty-cta .label {
|
|
8442
|
+
font-size: 14px;
|
|
8443
|
+
margin-bottom: 4px;
|
|
8444
|
+
color: var(--text-secondary);
|
|
8445
|
+
font-weight: 500;
|
|
8058
8446
|
}
|
|
8059
|
-
.
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
padding: 20px 16px;
|
|
8064
|
-
text-align: center;
|
|
8065
|
-
position: relative;
|
|
8066
|
-
display: flex;
|
|
8067
|
-
flex-direction: column;
|
|
8068
|
-
align-items: center;
|
|
8069
|
-
gap: 8px;
|
|
8070
|
-
transition: transform 0.15s, box-shadow 0.15s;
|
|
8447
|
+
.empty-cta .hint {
|
|
8448
|
+
font-size: 12px;
|
|
8449
|
+
color: var(--text-muted);
|
|
8450
|
+
margin-bottom: 18px;
|
|
8071
8451
|
}
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8452
|
+
/* ── Skeleton shimmer for loading lists ── */
|
|
8453
|
+
.skel-block {
|
|
8454
|
+
padding: 12px 18px;
|
|
8075
8455
|
}
|
|
8076
|
-
.
|
|
8077
|
-
|
|
8078
|
-
border-
|
|
8456
|
+
.skel-row {
|
|
8457
|
+
height: 14px;
|
|
8458
|
+
border-radius: 4px;
|
|
8459
|
+
background: linear-gradient(90deg, var(--bg-tertiary) 0%, var(--bg-hover) 50%, var(--bg-tertiary) 100%);
|
|
8460
|
+
background-size: 200% 100%;
|
|
8461
|
+
animation: skelShimmer 1.4s ease-in-out infinite;
|
|
8462
|
+
margin-bottom: 10px;
|
|
8079
8463
|
}
|
|
8080
|
-
.
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8464
|
+
.skel-row.short { width: 40%; }
|
|
8465
|
+
.skel-row.med { width: 65%; }
|
|
8466
|
+
@keyframes skelShimmer {
|
|
8467
|
+
0% { background-position: 200% 0; }
|
|
8468
|
+
100% { background-position: -200% 0; }
|
|
8469
|
+
}
|
|
8470
|
+
/* ── Row actions (icon-style buttons that appear inline on a list row) ── */
|
|
8471
|
+
.row-actions {
|
|
8472
|
+
display: inline-flex;
|
|
8473
|
+
gap: 6px;
|
|
8474
|
+
align-items: center;
|
|
8475
|
+
}
|
|
8476
|
+
.row-actions button {
|
|
8477
|
+
background: none;
|
|
8478
|
+
border: 1px solid transparent;
|
|
8479
|
+
color: var(--text-muted);
|
|
8480
|
+
font-size: 11px;
|
|
8481
|
+
padding: 3px 8px;
|
|
8482
|
+
border-radius: 4px;
|
|
8483
|
+
cursor: pointer;
|
|
8484
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
8485
|
+
}
|
|
8486
|
+
.row-actions button:hover {
|
|
8487
|
+
background: var(--bg-hover);
|
|
8488
|
+
border-color: var(--border);
|
|
8489
|
+
color: var(--text-primary);
|
|
8490
|
+
}
|
|
8491
|
+
.row-actions button.primary { color: var(--clementine); }
|
|
8492
|
+
.row-actions button.danger:hover { color: var(--red); border-color: rgba(239,68,68,0.3); }
|
|
8493
|
+
.clickable-row {
|
|
8494
|
+
cursor: pointer;
|
|
8495
|
+
transition: background 0.12s;
|
|
8496
|
+
}
|
|
8497
|
+
.clickable-row:hover { background: var(--bg-hover); }
|
|
8498
|
+
|
|
8499
|
+
/* ── Status pip (used for daemon status, channel status, agent status) ── */
|
|
8500
|
+
.status-pip {
|
|
8501
|
+
display: inline-block;
|
|
8502
|
+
width: 8px;
|
|
8503
|
+
height: 8px;
|
|
8504
|
+
border-radius: 50%;
|
|
8505
|
+
background: #888;
|
|
8506
|
+
margin-right: 6px;
|
|
8507
|
+
flex-shrink: 0;
|
|
8508
|
+
}
|
|
8509
|
+
.status-pip.green { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.5); }
|
|
8510
|
+
.status-pip.amber { background: #f59e0b; }
|
|
8511
|
+
.status-pip.red { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.4); }
|
|
8512
|
+
.status-pip.muted { background: var(--text-muted); }
|
|
8513
|
+
|
|
8514
|
+
/* Cmd+K palette */
|
|
8515
|
+
.cmdk-row:hover { background: var(--bg-hover) !important; }
|
|
8516
|
+
|
|
8517
|
+
/* ── Home layout — chat-first daily driver ─── */
|
|
8518
|
+
.home-layout {
|
|
8519
|
+
display: grid;
|
|
8520
|
+
grid-template-columns: 1fr 320px;
|
|
8521
|
+
gap: 18px;
|
|
8522
|
+
height: calc(100vh - var(--header-h));
|
|
8523
|
+
padding: 18px;
|
|
8524
|
+
box-sizing: border-box;
|
|
8525
|
+
}
|
|
8526
|
+
.home-main {
|
|
8527
|
+
display: flex;
|
|
8528
|
+
flex-direction: column;
|
|
8529
|
+
gap: 14px;
|
|
8530
|
+
min-height: 0;
|
|
8531
|
+
overflow-y: auto;
|
|
8532
|
+
}
|
|
8533
|
+
.home-chat {
|
|
8534
|
+
display: flex;
|
|
8535
|
+
flex-direction: column;
|
|
8536
|
+
flex: 1;
|
|
8537
|
+
min-height: 320px;
|
|
8538
|
+
background: var(--bg-card);
|
|
8539
|
+
border: 1px solid var(--border);
|
|
8540
|
+
border-radius: 12px;
|
|
8541
|
+
overflow: hidden;
|
|
8542
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
|
8543
|
+
}
|
|
8544
|
+
.home-chat-messages {
|
|
8545
|
+
flex: 1;
|
|
8546
|
+
overflow-y: auto;
|
|
8547
|
+
padding: 18px 20px;
|
|
8548
|
+
min-height: 280px;
|
|
8549
|
+
}
|
|
8550
|
+
.home-chat-input-row {
|
|
8551
|
+
display: flex;
|
|
8552
|
+
gap: 8px;
|
|
8553
|
+
padding: 12px 16px;
|
|
8554
|
+
border-top: 1px solid var(--border);
|
|
8555
|
+
background: var(--bg-secondary);
|
|
8556
|
+
align-items: center;
|
|
8557
|
+
}
|
|
8558
|
+
.home-chat-input-row input[type="text"] {
|
|
8559
|
+
flex: 1;
|
|
8560
|
+
padding: 10px 14px;
|
|
8561
|
+
border: 1px solid var(--border);
|
|
8562
|
+
border-radius: 8px;
|
|
8563
|
+
background: var(--bg-input);
|
|
8564
|
+
color: var(--text-primary);
|
|
8565
|
+
font-size: 14px;
|
|
8566
|
+
}
|
|
8567
|
+
.home-chat-input-row select {
|
|
8568
|
+
padding: 6px 10px;
|
|
8569
|
+
border: 1px solid var(--border);
|
|
8570
|
+
border-radius: 6px;
|
|
8571
|
+
background: var(--bg-secondary);
|
|
8572
|
+
color: var(--text-primary);
|
|
8573
|
+
font-size: 12px;
|
|
8574
|
+
}
|
|
8575
|
+
.home-chat-input-row button { padding: 9px 18px; border-radius: 8px; }
|
|
8576
|
+
.home-activity { margin: 0; }
|
|
8577
|
+
|
|
8578
|
+
/* Right rail */
|
|
8579
|
+
.home-rail {
|
|
8580
|
+
display: flex;
|
|
8581
|
+
flex-direction: column;
|
|
8582
|
+
gap: 12px;
|
|
8583
|
+
overflow-y: auto;
|
|
8584
|
+
position: relative;
|
|
8585
|
+
}
|
|
8586
|
+
.home-rail.collapsed {
|
|
8587
|
+
display: none;
|
|
8588
|
+
}
|
|
8589
|
+
.rail-collapse-btn {
|
|
8590
|
+
position: absolute;
|
|
8591
|
+
top: -4px;
|
|
8592
|
+
right: -4px;
|
|
8593
|
+
background: none;
|
|
8594
|
+
border: 1px solid var(--border);
|
|
8595
|
+
color: var(--text-muted);
|
|
8596
|
+
width: 22px;
|
|
8597
|
+
height: 22px;
|
|
8598
|
+
border-radius: 50%;
|
|
8599
|
+
cursor: pointer;
|
|
8600
|
+
font-size: 14px;
|
|
8601
|
+
display: none;
|
|
8602
|
+
align-items: center;
|
|
8603
|
+
justify-content: center;
|
|
8604
|
+
z-index: 5;
|
|
8605
|
+
}
|
|
8606
|
+
.rail-card {
|
|
8607
|
+
background: var(--bg-card);
|
|
8608
|
+
border: 1px solid var(--border);
|
|
8609
|
+
border-radius: 10px;
|
|
8610
|
+
overflow: hidden;
|
|
8611
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
|
8612
|
+
}
|
|
8613
|
+
.rail-header {
|
|
8614
|
+
display: flex;
|
|
8615
|
+
align-items: center;
|
|
8616
|
+
justify-content: space-between;
|
|
8617
|
+
padding: 10px 14px;
|
|
8618
|
+
font-size: 12px;
|
|
8619
|
+
font-weight: 600;
|
|
8620
|
+
color: var(--text-secondary);
|
|
8621
|
+
border-bottom: 1px solid var(--border);
|
|
8622
|
+
background: var(--bg-secondary);
|
|
8623
|
+
}
|
|
8624
|
+
.rail-body { padding: 12px 14px; font-size: 12.5px; line-height: 1.5; }
|
|
8625
|
+
.rail-body .empty-state, .rail-body .skel-row { font-size: 11px; }
|
|
8626
|
+
.rail-badge {
|
|
8627
|
+
display: inline-flex;
|
|
8628
|
+
align-items: center;
|
|
8629
|
+
justify-content: center;
|
|
8630
|
+
min-width: 18px;
|
|
8631
|
+
height: 18px;
|
|
8632
|
+
padding: 0 6px;
|
|
8633
|
+
border-radius: 9px;
|
|
8634
|
+
background: var(--clementine);
|
|
8635
|
+
color: #fff;
|
|
8636
|
+
font-size: 10px;
|
|
8637
|
+
font-weight: 600;
|
|
8638
|
+
}
|
|
8639
|
+
.rail-row {
|
|
8640
|
+
display: flex;
|
|
8641
|
+
align-items: center;
|
|
8642
|
+
gap: 8px;
|
|
8643
|
+
padding: 6px 0;
|
|
8644
|
+
font-size: 12px;
|
|
8645
|
+
border-bottom: 1px dashed var(--border);
|
|
8646
|
+
}
|
|
8647
|
+
.rail-row:last-child { border-bottom: none; }
|
|
8648
|
+
.rail-row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
8649
|
+
.rail-row .meta { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
|
|
8650
|
+
|
|
8651
|
+
/* Floating "open rail" button when collapsed */
|
|
8652
|
+
.home-rail-toggle {
|
|
8653
|
+
position: fixed;
|
|
8654
|
+
top: 80px;
|
|
8655
|
+
right: 18px;
|
|
8656
|
+
z-index: 100;
|
|
8657
|
+
width: 36px;
|
|
8658
|
+
height: 36px;
|
|
8659
|
+
border-radius: 50%;
|
|
8660
|
+
background: var(--clementine);
|
|
8661
|
+
color: #fff;
|
|
8662
|
+
border: none;
|
|
8663
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
8664
|
+
cursor: pointer;
|
|
8665
|
+
font-size: 14px;
|
|
8666
|
+
display: none;
|
|
8667
|
+
}
|
|
8668
|
+
.home-rail.collapsed ~ .home-rail-toggle,
|
|
8669
|
+
.home-rail.collapsed + .home-rail-toggle { display: block; }
|
|
8670
|
+
|
|
8671
|
+
/* Narrow screens: rail becomes a slide-out drawer */
|
|
8672
|
+
@media (max-width: 1024px) {
|
|
8673
|
+
.home-layout { grid-template-columns: 1fr; }
|
|
8674
|
+
.home-rail {
|
|
8675
|
+
position: fixed;
|
|
8676
|
+
right: 0;
|
|
8677
|
+
top: var(--header-h);
|
|
8678
|
+
bottom: 0;
|
|
8679
|
+
width: 320px;
|
|
8680
|
+
max-width: 90vw;
|
|
8681
|
+
transform: translateX(100%);
|
|
8682
|
+
transition: transform 0.2s ease;
|
|
8683
|
+
background: var(--bg);
|
|
8684
|
+
border-left: 1px solid var(--border);
|
|
8685
|
+
box-shadow: -4px 0 20px rgba(0,0,0,0.15);
|
|
8686
|
+
padding: 14px;
|
|
8687
|
+
z-index: 50;
|
|
8688
|
+
}
|
|
8689
|
+
.home-rail.open { transform: translateX(0); }
|
|
8690
|
+
.rail-collapse-btn { display: flex; }
|
|
8691
|
+
.home-rail-toggle { display: block; }
|
|
8692
|
+
.home-rail.open ~ .home-rail-toggle { display: none; }
|
|
8693
|
+
}
|
|
8694
|
+
|
|
8695
|
+
/* ── Cards ──────────────────────────────── */
|
|
8696
|
+
.card {
|
|
8697
|
+
background: var(--bg-card);
|
|
8698
|
+
backdrop-filter: blur(8px);
|
|
8699
|
+
border: 1px solid var(--border);
|
|
8700
|
+
border-radius: var(--radius);
|
|
8701
|
+
margin-bottom: 16px;
|
|
8702
|
+
overflow: hidden;
|
|
8703
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
|
8704
|
+
}
|
|
8705
|
+
.card-header {
|
|
8706
|
+
padding: 14px 18px;
|
|
8707
|
+
border-bottom: 1px solid var(--border);
|
|
8708
|
+
display: flex;
|
|
8709
|
+
align-items: center;
|
|
8710
|
+
justify-content: space-between;
|
|
8711
|
+
font-size: 13px;
|
|
8712
|
+
font-weight: 600;
|
|
8713
|
+
color: var(--text-primary);
|
|
8714
|
+
}
|
|
8715
|
+
.card-body {
|
|
8716
|
+
padding: 18px;
|
|
8717
|
+
font-size: 13px;
|
|
8718
|
+
line-height: 1.7;
|
|
8719
|
+
}
|
|
8720
|
+
.grid-2 {
|
|
8721
|
+
display: grid;
|
|
8722
|
+
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
|
8723
|
+
gap: 20px;
|
|
8724
|
+
}
|
|
8725
|
+
|
|
8726
|
+
/* ── Getting Started ────────────────────── */
|
|
8727
|
+
.getting-started {
|
|
8728
|
+
background: linear-gradient(135deg, var(--clementine-bg), rgba(77,158,255,0.06));
|
|
8729
|
+
border: 1px solid var(--border);
|
|
8730
|
+
border-radius: 16px;
|
|
8731
|
+
padding: 24px;
|
|
8732
|
+
margin-bottom: 24px;
|
|
8733
|
+
position: relative;
|
|
8734
|
+
}
|
|
8735
|
+
.gs-header {
|
|
8736
|
+
display: flex;
|
|
8737
|
+
align-items: center;
|
|
8738
|
+
gap: 12px;
|
|
8739
|
+
margin-bottom: 20px;
|
|
8740
|
+
}
|
|
8741
|
+
.gs-title {
|
|
8742
|
+
font-size: 18px;
|
|
8743
|
+
font-weight: 700;
|
|
8744
|
+
color: var(--text-primary);
|
|
8745
|
+
}
|
|
8746
|
+
.gs-subtitle {
|
|
8747
|
+
font-size: 13px;
|
|
8748
|
+
color: var(--text-muted);
|
|
8749
|
+
flex: 1;
|
|
8750
|
+
}
|
|
8751
|
+
.gs-dismiss {
|
|
8752
|
+
position: absolute;
|
|
8753
|
+
top: 12px;
|
|
8754
|
+
right: 12px;
|
|
8755
|
+
font-size: 18px;
|
|
8756
|
+
}
|
|
8757
|
+
.gs-grid {
|
|
8758
|
+
display: grid;
|
|
8759
|
+
grid-template-columns: repeat(5, 1fr);
|
|
8760
|
+
gap: 16px;
|
|
8761
|
+
}
|
|
8762
|
+
.gs-card {
|
|
8763
|
+
background: var(--bg-card);
|
|
8764
|
+
border: 1px solid var(--border);
|
|
8765
|
+
border-radius: var(--radius);
|
|
8766
|
+
padding: 20px 16px;
|
|
8767
|
+
text-align: center;
|
|
8768
|
+
position: relative;
|
|
8769
|
+
display: flex;
|
|
8770
|
+
flex-direction: column;
|
|
8771
|
+
align-items: center;
|
|
8772
|
+
gap: 8px;
|
|
8773
|
+
transition: transform 0.15s, box-shadow 0.15s;
|
|
8774
|
+
}
|
|
8775
|
+
.gs-card:hover {
|
|
8776
|
+
transform: translateY(-2px);
|
|
8777
|
+
box-shadow: var(--shadow-sm);
|
|
8778
|
+
}
|
|
8779
|
+
.gs-card.gs-done {
|
|
8780
|
+
opacity: 0.5;
|
|
8781
|
+
border-color: var(--green);
|
|
8782
|
+
}
|
|
8783
|
+
.gs-card.gs-done::after {
|
|
8784
|
+
content: '\\2713';
|
|
8785
|
+
position: absolute;
|
|
8786
|
+
top: 8px;
|
|
8787
|
+
right: 10px;
|
|
8788
|
+
color: var(--green);
|
|
8789
|
+
font-size: 16px;
|
|
8087
8790
|
font-weight: 700;
|
|
8088
8791
|
}
|
|
8089
8792
|
.gs-step-num {
|
|
@@ -9908,11 +10611,92 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
9908
10611
|
flex-shrink: 0;
|
|
9909
10612
|
margin-top: 2px;
|
|
9910
10613
|
}
|
|
9911
|
-
|
|
10614
|
+
/* Build page is full-height flex (canvas + chat). Home page handles its own layout. */
|
|
10615
|
+
#page-build.active {
|
|
9912
10616
|
display: flex !important;
|
|
9913
10617
|
flex-direction: column;
|
|
9914
10618
|
height: calc(100vh - var(--header-h));
|
|
9915
10619
|
}
|
|
10620
|
+
/* === Builder canvas (Drawflow node kinds) === */
|
|
10621
|
+
#builder-canvas .drawflow-node {
|
|
10622
|
+
background: #2a3040;
|
|
10623
|
+
border: 1px solid #3a4255;
|
|
10624
|
+
border-radius: 8px;
|
|
10625
|
+
color: #fff;
|
|
10626
|
+
padding: 10px 12px;
|
|
10627
|
+
min-width: 180px;
|
|
10628
|
+
max-width: 240px;
|
|
10629
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
|
10630
|
+
}
|
|
10631
|
+
#builder-canvas .drawflow-node.selected { border-color: var(--clementine); }
|
|
10632
|
+
#builder-canvas .drawflow-node.cl-node-prompt { background: #2a3550; border-color: #3a4570; }
|
|
10633
|
+
#builder-canvas .drawflow-node.cl-node-mcp { background: #2c4039; border-color: #3a5a4f; }
|
|
10634
|
+
#builder-canvas .drawflow-node.cl-node-channel { background: #44324a; border-color: #5b3f6a; }
|
|
10635
|
+
#builder-canvas .drawflow-node.cl-node-transform { background: #3d3a28; border-color: #564f33; }
|
|
10636
|
+
#builder-canvas .drawflow-node.cl-node-conditional { background: #4a352a; border-color: #6b4737; }
|
|
10637
|
+
#builder-canvas .drawflow-node.cl-node-loop { background: #2e4350; border-color: #3f5a6c; }
|
|
10638
|
+
#builder-canvas .drawflow-node .input, #builder-canvas .drawflow-node .output {
|
|
10639
|
+
background: #555c70;
|
|
10640
|
+
border: 2px solid #2a3040;
|
|
10641
|
+
}
|
|
10642
|
+
#builder-canvas .drawflow .connection .main-path {
|
|
10643
|
+
stroke: #5b6580;
|
|
10644
|
+
stroke-width: 2px;
|
|
10645
|
+
}
|
|
10646
|
+
#builder-canvas .drawflow .connection .main-path:hover { stroke: var(--clementine); }
|
|
10647
|
+
/* Per-step run status (Phase 2e) */
|
|
10648
|
+
#builder-canvas .drawflow-node.cl-step-running {
|
|
10649
|
+
box-shadow: 0 0 0 2px var(--clementine), 0 2px 8px rgba(0,0,0,0.3);
|
|
10650
|
+
animation: cl-step-pulse 1.6s ease-in-out infinite;
|
|
10651
|
+
}
|
|
10652
|
+
#builder-canvas .drawflow-node.cl-step-done {
|
|
10653
|
+
box-shadow: 0 0 0 2px #4caf50, 0 2px 8px rgba(0,0,0,0.3);
|
|
10654
|
+
}
|
|
10655
|
+
#builder-canvas .drawflow-node.cl-step-failed,
|
|
10656
|
+
#builder-canvas .drawflow-node.cl-step-timeout {
|
|
10657
|
+
box-shadow: 0 0 0 2px #e64a4a, 0 2px 8px rgba(0,0,0,0.3);
|
|
10658
|
+
}
|
|
10659
|
+
#builder-canvas .drawflow-node.cl-step-skipped,
|
|
10660
|
+
#builder-canvas .drawflow-node.cl-step-cancelled {
|
|
10661
|
+
opacity: 0.55;
|
|
10662
|
+
box-shadow: 0 0 0 1px #888, 0 2px 8px rgba(0,0,0,0.2);
|
|
10663
|
+
}
|
|
10664
|
+
@keyframes cl-step-pulse {
|
|
10665
|
+
0%, 100% { box-shadow: 0 0 0 2px var(--clementine), 0 2px 8px rgba(0,0,0,0.3); }
|
|
10666
|
+
50% { box-shadow: 0 0 0 4px rgba(255,140,33,0.5), 0 2px 8px rgba(0,0,0,0.3); }
|
|
10667
|
+
}
|
|
10668
|
+
/* Node palette popover */
|
|
10669
|
+
.builder-palette-item {
|
|
10670
|
+
padding: 7px 12px;
|
|
10671
|
+
border-radius: 5px;
|
|
10672
|
+
font-size: 12px;
|
|
10673
|
+
color: var(--text-primary);
|
|
10674
|
+
cursor: pointer;
|
|
10675
|
+
}
|
|
10676
|
+
.builder-palette-item:hover { background: var(--bg-hover); }
|
|
10677
|
+
/* Config panel rows */
|
|
10678
|
+
#builder-config-panel .cfg-row { margin-bottom: 10px; }
|
|
10679
|
+
#builder-config-panel .cfg-row label {
|
|
10680
|
+
display: block;
|
|
10681
|
+
font-size: 10px;
|
|
10682
|
+
text-transform: uppercase;
|
|
10683
|
+
letter-spacing: 0.04em;
|
|
10684
|
+
color: var(--text-muted);
|
|
10685
|
+
margin-bottom: 4px;
|
|
10686
|
+
}
|
|
10687
|
+
#builder-config-panel .cfg-row input,
|
|
10688
|
+
#builder-config-panel .cfg-row select,
|
|
10689
|
+
#builder-config-panel .cfg-row textarea {
|
|
10690
|
+
width: 100%;
|
|
10691
|
+
padding: 6px 8px;
|
|
10692
|
+
border: 1px solid var(--border);
|
|
10693
|
+
border-radius: 5px;
|
|
10694
|
+
background: var(--bg-input);
|
|
10695
|
+
color: var(--text-primary);
|
|
10696
|
+
font-size: 12px;
|
|
10697
|
+
font-family: inherit;
|
|
10698
|
+
}
|
|
10699
|
+
#builder-config-panel .cfg-row textarea { font-family: monospace; resize: vertical; }
|
|
9916
10700
|
#builder-preview .preview-field {
|
|
9917
10701
|
margin-bottom:12px;
|
|
9918
10702
|
}
|
|
@@ -10077,10 +10861,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10077
10861
|
.stat-value { font-size: 20px; }
|
|
10078
10862
|
.stat-label { font-size: 10px; }
|
|
10079
10863
|
|
|
10080
|
-
/*
|
|
10081
|
-
#page-chat.active {
|
|
10082
|
-
height: calc(100vh - var(--header-h));
|
|
10083
|
-
}
|
|
10864
|
+
/* Mobile-friendly chat input (lives inside Home now). */
|
|
10084
10865
|
#chat-input {
|
|
10085
10866
|
font-size: 16px; /* prevents iOS zoom on focus */
|
|
10086
10867
|
min-height: 40px;
|
|
@@ -10352,70 +11133,39 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10352
11133
|
</div>
|
|
10353
11134
|
</header>
|
|
10354
11135
|
|
|
10355
|
-
<!-- Sidebar -->
|
|
11136
|
+
<!-- Sidebar — 5 destinations only. Sub-pages live as tabs within each. -->
|
|
10356
11137
|
<nav class="sidebar">
|
|
10357
11138
|
<div class="nav-section">
|
|
10358
|
-
<div class="nav-
|
|
10359
|
-
|
|
10360
|
-
<span class="nav-icon">●</span> Home
|
|
11139
|
+
<div class="nav-item active" data-page="home" title="Chat, today, activity">
|
|
11140
|
+
<span class="nav-icon">🍋</span> Home
|
|
10361
11141
|
</div>
|
|
10362
|
-
<div class="nav-item" data-page="
|
|
10363
|
-
<span class="nav-icon">&#
|
|
11142
|
+
<div class="nav-item" data-page="build" title="Workflows, crons, skills">
|
|
11143
|
+
<span class="nav-icon">🛠</span> Build
|
|
11144
|
+
<span class="nav-badge" id="nav-cron-count">0</span>
|
|
10364
11145
|
</div>
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
<div class="nav-section-title">Build</div>
|
|
10368
|
-
<div class="nav-item" data-page="builder">
|
|
10369
|
-
<span class="nav-icon">🛠</span> Builder
|
|
11146
|
+
<div class="nav-item" data-page="team" title="Agents, activity, goals">
|
|
11147
|
+
<span class="nav-icon">👥</span> Team
|
|
10370
11148
|
</div>
|
|
10371
|
-
<div class="nav-item" data-page="
|
|
10372
|
-
<span class="nav-icon">&#
|
|
11149
|
+
<div class="nav-item" data-page="brain" title="Memory, knowledge, ingestion, health">
|
|
11150
|
+
<span class="nav-icon">🧠</span> Brain
|
|
10373
11151
|
</div>
|
|
10374
|
-
<div class="nav-item" data-page="
|
|
10375
|
-
<span class="nav-icon">&#
|
|
11152
|
+
<div class="nav-item" data-page="settings" title="Channels, integrations, system">
|
|
11153
|
+
<span class="nav-icon">⚙</span> Settings
|
|
10376
11154
|
</div>
|
|
10377
11155
|
</div>
|
|
10378
|
-
|
|
10379
|
-
|
|
11156
|
+
<!-- Per-agent quick-jump (loaded from team-nav helper). -->
|
|
11157
|
+
<div class="nav-section" style="margin-top:18px">
|
|
11158
|
+
<div class="nav-section-title">Agents</div>
|
|
10380
11159
|
<div id="team-nav"></div>
|
|
10381
11160
|
<div class="team-hire-btn" onclick="showAgentCreateModal()">
|
|
10382
|
-
<span style="font-size:14px">+</span> Hire
|
|
11161
|
+
<span style="font-size:14px">+</span> Hire
|
|
10383
11162
|
</div>
|
|
10384
11163
|
</div>
|
|
11164
|
+
<div style="flex:1"></div>
|
|
10385
11165
|
<div class="nav-section">
|
|
10386
|
-
<div class="nav-
|
|
10387
|
-
|
|
10388
|
-
<
|
|
10389
|
-
<span class="nav-badge" id="nav-cron-count">0</span>
|
|
10390
|
-
</div>
|
|
10391
|
-
<div class="nav-item" onclick="openSkillStudio()">
|
|
10392
|
-
<span class="nav-icon">💡</span> Skill Studio
|
|
10393
|
-
<span class="nav-badge" id="nav-skill-count" style="display:none">0</span>
|
|
10394
|
-
</div>
|
|
10395
|
-
<div class="nav-item" data-page="workflows">
|
|
10396
|
-
<span class="nav-icon">🔄</span> Workflows
|
|
10397
|
-
</div>
|
|
10398
|
-
</div>
|
|
10399
|
-
<div class="nav-section">
|
|
10400
|
-
<div class="nav-section-title">Insights</div>
|
|
10401
|
-
<div class="nav-item" data-page="team-status">
|
|
10402
|
-
<span class="nav-icon">📊</span> Team Status
|
|
10403
|
-
</div>
|
|
10404
|
-
<div class="nav-item" data-page="intelligence">
|
|
10405
|
-
<span class="nav-icon">🧠</span> Brain
|
|
10406
|
-
</div>
|
|
10407
|
-
<div class="nav-item" data-page="claims">
|
|
10408
|
-
<span class="nav-icon">🔒</span> Trust & Claims
|
|
10409
|
-
<span class="nav-badge" id="nav-trust-score" style="display:none">--</span>
|
|
10410
|
-
</div>
|
|
10411
|
-
<div class="nav-item" data-page="logs">
|
|
10412
|
-
<span class="nav-icon">📜</span> Logs
|
|
10413
|
-
</div>
|
|
10414
|
-
</div>
|
|
10415
|
-
<div class="nav-section">
|
|
10416
|
-
<div class="nav-section-title">System</div>
|
|
10417
|
-
<div class="nav-item" data-page="settings">
|
|
10418
|
-
<span class="nav-icon">⚙</span> Settings
|
|
11166
|
+
<div class="nav-item" onclick="openCommandK()" style="font-size:12px;color:var(--text-muted);justify-content:space-between" title="Quick search (Cmd+K)">
|
|
11167
|
+
<span><span class="nav-icon">🔍</span> Search</span>
|
|
11168
|
+
<kbd style="font-size:10px;padding:1px 5px;border:1px solid var(--border);border-radius:3px">⌘K</kbd>
|
|
10419
11169
|
</div>
|
|
10420
11170
|
</div>
|
|
10421
11171
|
</nav>
|
|
@@ -10424,105 +11174,82 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10424
11174
|
<!-- Content -->
|
|
10425
11175
|
<div class="content">
|
|
10426
11176
|
|
|
10427
|
-
<!-- ═══ Home
|
|
11177
|
+
<!-- ═══ Home — chat-first daily driver ═══ -->
|
|
10428
11178
|
<div class="page active" id="page-home">
|
|
10429
|
-
<div class="
|
|
10430
|
-
<
|
|
10431
|
-
|
|
10432
|
-
<div class="
|
|
10433
|
-
<div class="
|
|
10434
|
-
|
|
10435
|
-
<
|
|
10436
|
-
<
|
|
11179
|
+
<div class="home-layout">
|
|
11180
|
+
<main class="home-main">
|
|
11181
|
+
<!-- Getting Started (shown for new users only) -->
|
|
11182
|
+
<div id="getting-started" class="getting-started" style="display:none">
|
|
11183
|
+
<div class="gs-header">
|
|
11184
|
+
<div class="gs-title">Get Started</div>
|
|
11185
|
+
<div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
|
|
11186
|
+
<button class="btn-ghost btn-sm gs-dismiss" onclick="dismissGettingStarted()" title="Dismiss">×</button>
|
|
11187
|
+
</div>
|
|
11188
|
+
<div class="gs-grid">
|
|
11189
|
+
<div class="gs-card" id="gs-step-auth">
|
|
11190
|
+
<div class="gs-step-num">1</div>
|
|
11191
|
+
<div class="gs-card-icon">🔒</div>
|
|
11192
|
+
<div class="gs-card-title">Login with Anthropic</div>
|
|
11193
|
+
<div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
|
|
11194
|
+
<button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
|
|
11195
|
+
</div>
|
|
11196
|
+
<div class="gs-card" id="gs-step-agent">
|
|
11197
|
+
<div class="gs-step-num">2</div>
|
|
11198
|
+
<div class="gs-card-icon">👥</div>
|
|
11199
|
+
<div class="gs-card-title">Create an Agent</div>
|
|
11200
|
+
<div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
|
|
11201
|
+
<button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
|
|
11202
|
+
</div>
|
|
11203
|
+
<div class="gs-card" id="gs-step-channel">
|
|
11204
|
+
<div class="gs-step-num">3</div>
|
|
11205
|
+
<div class="gs-card-icon">💬</div>
|
|
11206
|
+
<div class="gs-card-title">Connect a Channel</div>
|
|
11207
|
+
<div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
|
|
11208
|
+
<button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'channels' })">Open Settings</button>
|
|
11209
|
+
</div>
|
|
11210
|
+
<div class="gs-card" id="gs-step-task">
|
|
11211
|
+
<div class="gs-step-num">4</div>
|
|
11212
|
+
<div class="gs-card-icon">⏰</div>
|
|
11213
|
+
<div class="gs-card-title">Schedule a Task</div>
|
|
11214
|
+
<div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
|
|
11215
|
+
<button class="btn btn-sm" onclick="navigateTo('build', { tab: 'crons' })">Add a Task</button>
|
|
11216
|
+
</div>
|
|
11217
|
+
<div class="gs-card" id="gs-step-project">
|
|
11218
|
+
<div class="gs-step-num">5</div>
|
|
11219
|
+
<div class="gs-card-icon">📂</div>
|
|
11220
|
+
<div class="gs-card-title">Link a Project</div>
|
|
11221
|
+
<div class="gs-card-desc">Give agents context about your codebases and tools.</div>
|
|
11222
|
+
<button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'projects' })">Browse Projects</button>
|
|
11223
|
+
</div>
|
|
10437
11224
|
</div>
|
|
10438
|
-
<div class="agent-meta" id="agent-meta"></div>
|
|
10439
|
-
<div class="agent-channels" id="agent-channels"></div>
|
|
10440
|
-
</div>
|
|
10441
|
-
<div class="agent-controls" id="hero-controls"></div>
|
|
10442
|
-
</div>
|
|
10443
|
-
</div>
|
|
10444
|
-
|
|
10445
|
-
<!-- Getting Started (shown for new users, hidden when setup is complete) -->
|
|
10446
|
-
<div id="getting-started" class="getting-started" style="display:none">
|
|
10447
|
-
<div class="gs-header">
|
|
10448
|
-
<div class="gs-title">Get Started</div>
|
|
10449
|
-
<div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
|
|
10450
|
-
<button class="btn-ghost btn-sm gs-dismiss" onclick="dismissGettingStarted()" title="Dismiss">×</button>
|
|
10451
|
-
</div>
|
|
10452
|
-
<div class="gs-grid">
|
|
10453
|
-
<div class="gs-card" id="gs-step-auth">
|
|
10454
|
-
<div class="gs-step-num">1</div>
|
|
10455
|
-
<div class="gs-card-icon">🔒</div>
|
|
10456
|
-
<div class="gs-card-title">Login with Anthropic</div>
|
|
10457
|
-
<div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
|
|
10458
|
-
<button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
|
|
10459
|
-
</div>
|
|
10460
|
-
<div class="gs-card" id="gs-step-agent">
|
|
10461
|
-
<div class="gs-step-num">2</div>
|
|
10462
|
-
<div class="gs-card-icon">👥</div>
|
|
10463
|
-
<div class="gs-card-title">Create an Agent</div>
|
|
10464
|
-
<div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
|
|
10465
|
-
<button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
|
|
10466
|
-
</div>
|
|
10467
|
-
<div class="gs-card" id="gs-step-channel">
|
|
10468
|
-
<div class="gs-step-num">3</div>
|
|
10469
|
-
<div class="gs-card-icon">💬</div>
|
|
10470
|
-
<div class="gs-card-title">Connect a Channel</div>
|
|
10471
|
-
<div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
|
|
10472
|
-
<button class="btn btn-sm" onclick="navigateTo('settings')">Open Settings</button>
|
|
10473
|
-
</div>
|
|
10474
|
-
<div class="gs-card" id="gs-step-task">
|
|
10475
|
-
<div class="gs-step-num">4</div>
|
|
10476
|
-
<div class="gs-card-icon">⏰</div>
|
|
10477
|
-
<div class="gs-card-title">Schedule a Task</div>
|
|
10478
|
-
<div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
|
|
10479
|
-
<button class="btn btn-sm" onclick="navigateTo('automations')">Add a Task</button>
|
|
10480
|
-
</div>
|
|
10481
|
-
<div class="gs-card" id="gs-step-project">
|
|
10482
|
-
<div class="gs-step-num">5</div>
|
|
10483
|
-
<div class="gs-card-icon">📂</div>
|
|
10484
|
-
<div class="gs-card-title">Link a Project</div>
|
|
10485
|
-
<div class="gs-card-desc">Give agents context about your codebases and tools.</div>
|
|
10486
|
-
<button class="btn btn-sm" onclick="navigateTo('projects')">Browse Projects</button>
|
|
10487
11225
|
</div>
|
|
10488
|
-
</div>
|
|
10489
|
-
</div>
|
|
10490
|
-
|
|
10491
|
-
<div class="summary-grid" id="summary-cards"></div>
|
|
10492
11226
|
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
11227
|
+
<!-- Chat panel — primary surface -->
|
|
11228
|
+
<div class="home-chat">
|
|
11229
|
+
<div id="chat-messages" class="home-chat-messages">
|
|
11230
|
+
<div class="empty-state" style="margin-top:32px">
|
|
11231
|
+
<p style="margin-bottom:14px;color:var(--text-muted)">What can I help with?</p>
|
|
11232
|
+
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
|
|
11233
|
+
<button class="btn btn-sm quick-pill" onclick="quickChat("What's on my schedule?")">What's on my schedule?</button>
|
|
11234
|
+
<button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Check my email</button>
|
|
11235
|
+
<button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Run morning briefing</button>
|
|
11236
|
+
<button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">What did you do today?</button>
|
|
11237
|
+
</div>
|
|
11238
|
+
</div>
|
|
11239
|
+
</div>
|
|
11240
|
+
<div class="home-chat-input-row">
|
|
11241
|
+
<input type="text" id="chat-input" placeholder="Ask Clementine anything..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}">
|
|
11242
|
+
<select id="chat-profile-select" onchange="switchProfile(this.value)" title="Active profile">
|
|
11243
|
+
<option value="">Default</option>
|
|
11244
|
+
</select>
|
|
11245
|
+
<button class="btn-primary" id="chat-send-btn" onclick="sendChat()">Send</button>
|
|
10501
11246
|
</div>
|
|
10502
11247
|
</div>
|
|
10503
|
-
<div class="card-body" id="home-plan-content" style="max-height:320px;overflow-y:auto"><div class="empty-state">Loading plan...</div></div>
|
|
10504
|
-
</div>
|
|
10505
|
-
<div class="card">
|
|
10506
|
-
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
|
10507
|
-
<span>Team Pulse</span>
|
|
10508
|
-
<span style="font-size:11px;color:var(--text-muted)" id="team-pulse-count"></span>
|
|
10509
|
-
</div>
|
|
10510
|
-
<div class="card-body" id="home-team-pulse" style="max-height:320px;overflow-y:auto"><div class="empty-state">Loading team...</div></div>
|
|
10511
|
-
</div>
|
|
10512
|
-
</div>
|
|
10513
11248
|
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
<button class="active" onclick="switchTab('home','activity')">Activity</button>
|
|
10517
|
-
<button onclick="switchTab('home','metrics')">Metrics</button>
|
|
10518
|
-
<button onclick="switchTab('home','agents')">Agents</button>
|
|
10519
|
-
<button onclick="switchTab('home','sessions')">Sessions</button>
|
|
10520
|
-
</div>
|
|
10521
|
-
<div id="home-tab-content">
|
|
10522
|
-
<div class="tab-pane active" id="tab-home-activity">
|
|
10523
|
-
<div class="card">
|
|
11249
|
+
<!-- Activity feed -->
|
|
11250
|
+
<div class="card home-activity">
|
|
10524
11251
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
|
10525
|
-
<span>Live
|
|
11252
|
+
<span>Live activity</span>
|
|
10526
11253
|
<div style="display:flex;gap:6px;align-items:center">
|
|
10527
11254
|
<select id="activity-source-filter" onchange="refreshActivity()" style="font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary)">
|
|
10528
11255
|
<option value="">All Sources</option>
|
|
@@ -10537,44 +11264,81 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10537
11264
|
</select>
|
|
10538
11265
|
</div>
|
|
10539
11266
|
</div>
|
|
10540
|
-
<div class="card-body" id="panel-activity"><div class="
|
|
11267
|
+
<div class="card-body" id="panel-activity"><div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div></div>
|
|
10541
11268
|
</div>
|
|
10542
|
-
</
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
<div id="panel-agents-compare"><div class="empty-state">Loading agent stats...</div></div>
|
|
10548
|
-
</div>
|
|
10549
|
-
<div class="tab-pane" id="tab-home-sessions">
|
|
10550
|
-
<div id="panel-sessions-home"><div class="empty-state">Loading sessions...</div></div>
|
|
10551
|
-
</div>
|
|
10552
|
-
</div>
|
|
11269
|
+
</main>
|
|
11270
|
+
|
|
11271
|
+
<!-- Right rail: Today / Upcoming / Active / Time-saved / Approvals -->
|
|
11272
|
+
<aside class="home-rail" id="home-rail">
|
|
11273
|
+
<button class="rail-collapse-btn" onclick="toggleHomeRail()" title="Hide rail">×</button>
|
|
10553
11274
|
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
11275
|
+
<section class="rail-card">
|
|
11276
|
+
<div class="rail-header">
|
|
11277
|
+
<span><span class="status-pip green"></span>Daemon</span>
|
|
11278
|
+
<span id="rail-daemon-uptime" style="font-size:11px;color:var(--text-muted)">--</span>
|
|
11279
|
+
</div>
|
|
11280
|
+
<div class="rail-body" id="rail-daemon-body">
|
|
11281
|
+
<div class="agent-activity" id="agent-activity"><span class="agent-activity-dot"></span><span>Loading...</span></div>
|
|
11282
|
+
</div>
|
|
11283
|
+
</section>
|
|
11284
|
+
|
|
11285
|
+
<section class="rail-card">
|
|
11286
|
+
<div class="rail-header">
|
|
11287
|
+
<span>Today</span>
|
|
11288
|
+
<input type="date" id="plan-date-picker" style="padding:2px 6px;font-size:10px;background:var(--bg-input);border:1px solid var(--border);border-radius:3px;color:var(--text-primary)">
|
|
11289
|
+
</div>
|
|
11290
|
+
<div class="rail-body" id="home-plan-content"><div class="skel-row med"></div><div class="skel-row"></div></div>
|
|
11291
|
+
</section>
|
|
11292
|
+
|
|
11293
|
+
<section class="rail-card">
|
|
11294
|
+
<div class="rail-header"><span>Upcoming runs</span><span id="rail-upcoming-count" class="rail-badge">0</span></div>
|
|
11295
|
+
<div class="rail-body" id="rail-upcoming"><div class="skel-row short"></div></div>
|
|
11296
|
+
</section>
|
|
11297
|
+
|
|
11298
|
+
<section class="rail-card">
|
|
11299
|
+
<div class="rail-header"><span>Active runs</span><span id="rail-active-count" class="rail-badge" style="display:none">0</span></div>
|
|
11300
|
+
<div class="rail-body" id="rail-active"><div style="font-size:12px;color:var(--text-muted)">Nothing running.</div></div>
|
|
11301
|
+
</section>
|
|
11302
|
+
|
|
11303
|
+
<section class="rail-card">
|
|
11304
|
+
<div class="rail-header"><span>Time saved this week</span></div>
|
|
11305
|
+
<div class="rail-body" id="rail-time-saved"><div class="skel-row short"></div></div>
|
|
11306
|
+
</section>
|
|
11307
|
+
|
|
11308
|
+
<section class="rail-card">
|
|
11309
|
+
<div class="rail-header"><span>Approvals</span><span id="rail-approvals-count" class="rail-badge" style="display:none">0</span></div>
|
|
11310
|
+
<div class="rail-body" id="rail-approvals"><div style="font-size:12px;color:var(--text-muted)">Nothing pending.</div></div>
|
|
11311
|
+
</section>
|
|
11312
|
+
</aside>
|
|
11313
|
+
<button class="home-rail-toggle" onclick="toggleHomeRail()" title="Open status rail">☰</button>
|
|
11314
|
+
</div>
|
|
10558
11315
|
</div>
|
|
10559
11316
|
|
|
10560
11317
|
<!-- ═══ Builder Page — Conversational Artifact Creation ═══ -->
|
|
10561
|
-
<div class="page" id="page-
|
|
10562
|
-
<div style="
|
|
10563
|
-
<
|
|
10564
|
-
<
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
11318
|
+
<div class="page" id="page-build">
|
|
11319
|
+
<div class="tab-bar" id="build-tabs" style="margin:0;padding:0 18px;background:var(--bg-secondary);border-bottom:1px solid var(--border)">
|
|
11320
|
+
<button class="active" data-build-tab="workflows" onclick="switchBuildTab('workflows')">🔗 Workflows</button>
|
|
11321
|
+
<button data-build-tab="crons" onclick="switchBuildTab('crons')">⏰ Crons <span class="tab-badge" id="build-tab-cron-count" style="display:none">0</span></button>
|
|
11322
|
+
<button data-build-tab="skills" onclick="switchBuildTab('skills')">🛡 Skills <span class="tab-badge" id="build-tab-skill-count" style="display:none">0</span></button>
|
|
11323
|
+
<button data-build-tab="templates" onclick="switchBuildTab('templates')">📝 Templates</button>
|
|
11324
|
+
</div>
|
|
11325
|
+
<!-- Builder header strip — persists across tabs (except Templates) -->
|
|
11326
|
+
<div id="build-header-strip" style="display:flex;align-items:center;gap:12px;padding:10px 18px;border-bottom:1px solid var(--border)">
|
|
11327
|
+
<select id="builder-type" onchange="resetBuilder();updateBuilderMode()" style="display:none">
|
|
11328
|
+
<option value="skill">skill</option>
|
|
11329
|
+
<option value="cron">cron</option>
|
|
11330
|
+
<option value="agent">agent</option>
|
|
11331
|
+
<option value="workflow">workflow</option>
|
|
10569
11332
|
</select>
|
|
10570
|
-
<span id="builder-agent-label" style="padding:
|
|
11333
|
+
<span id="builder-agent-label" style="padding:0;font-size:13px;color:var(--text-secondary);font-weight:500"></span>
|
|
10571
11334
|
<input type="hidden" id="builder-agent" value="">
|
|
10572
11335
|
<span style="flex:1"></span>
|
|
10573
11336
|
<button class="btn-sm" onclick="resetBuilder()" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px">New</button>
|
|
10574
11337
|
<button class="btn-sm" id="builder-test-btn" onclick="testBuilderSkill()" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px;display:none">Test</button>
|
|
10575
11338
|
<button class="btn-sm btn-primary" id="builder-save-btn" onclick="saveBuilderArtifact()" style="padding:4px 16px;font-size:12px;display:none">Save</button>
|
|
10576
11339
|
</div>
|
|
10577
|
-
|
|
11340
|
+
<!-- Build tab content area -->
|
|
11341
|
+
<div id="build-tab-workflows" data-build-tabpane="workflows" style="display:flex;flex:1;min-height:0;overflow:hidden">
|
|
10578
11342
|
<!-- Left: Chat -->
|
|
10579
11343
|
<div style="flex:1;display:flex;flex-direction:column;border-right:1px solid var(--border)">
|
|
10580
11344
|
<div id="builder-messages" style="flex:1;overflow-y:auto;padding:16px">
|
|
@@ -10607,15 +11371,44 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10607
11371
|
<button class="btn-primary" onclick="sendBuilderChat()" style="padding:10px 18px;border-radius:8px">Send</button>
|
|
10608
11372
|
</div>
|
|
10609
11373
|
</div>
|
|
10610
|
-
<!-- Right: Live Preview + Existing Skills -->
|
|
10611
|
-
<div style="width:
|
|
10612
|
-
<div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:13px;color:var(--text-secondary)">
|
|
10613
|
-
Live Preview
|
|
10614
|
-
<span id="builder-preview-status" style="font-size:11px;color:var(--text-muted)
|
|
11374
|
+
<!-- Right: Live Preview / Canvas + Existing Skills -->
|
|
11375
|
+
<div id="builder-right-pane" style="width:520px;display:flex;flex-direction:column;background:var(--bg-secondary)">
|
|
11376
|
+
<div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:13px;color:var(--text-secondary);display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
11377
|
+
<span id="builder-right-pane-title">Live Preview</span>
|
|
11378
|
+
<span id="builder-preview-status" style="font-size:11px;color:var(--text-muted)"></span>
|
|
11379
|
+
<span style="flex:1"></span>
|
|
11380
|
+
<select id="builder-canvas-picker" onchange="openBuilderWorkflow(this.value)" style="display:none;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;max-width:240px">
|
|
11381
|
+
<option value="">— pick a workflow —</option>
|
|
11382
|
+
</select>
|
|
11383
|
+
<button id="builder-canvas-validate-btn" onclick="validateBuilderCanvas()" title="Static checks (cycles, missing fields, deps)" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">Validate</button>
|
|
11384
|
+
<button id="builder-canvas-dryrun-btn" onclick="dryRunBuilderCanvas()" title="Describe what each step would do (no execution)" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">Dry-run</button>
|
|
11385
|
+
<button id="builder-canvas-test-btn" onclick="testBuilderCanvas()" title="Test run (mock-safe by default)" style="display:none;background:var(--clementine);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Test</button>
|
|
11386
|
+
<button id="builder-canvas-cancel-btn" onclick="cancelBuilderTest()" title="Cancel test run" style="display:none;background:var(--red);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Cancel</button>
|
|
10615
11387
|
</div>
|
|
10616
11388
|
<div id="builder-preview" style="flex:1;overflow-y:auto;padding:16px">
|
|
10617
11389
|
<div class="empty-state" style="font-size:13px;color:var(--text-muted)">The artifact will appear here as you build it</div>
|
|
10618
11390
|
</div>
|
|
11391
|
+
<div id="builder-canvas-host" style="display:none;flex:1;flex-direction:column;min-height:0;position:relative">
|
|
11392
|
+
<div id="builder-canvas-banner" style="padding:8px 14px;background:var(--bg-tertiary);border-bottom:1px solid var(--border);font-size:11px;color:var(--text-muted);display:none"></div>
|
|
11393
|
+
<div id="builder-canvas" style="flex:1;background:var(--bg-tertiary);position:relative;overflow:hidden"></div>
|
|
11394
|
+
<!-- Floating add-node FAB + palette popover -->
|
|
11395
|
+
<button id="builder-palette-btn" onclick="toggleBuilderPalette()" title="Add a step" style="position:absolute;left:14px;bottom:48px;width:40px;height:40px;border-radius:50%;background:var(--clementine);color:#fff;border:none;font-size:20px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.25);z-index:10">+</button>
|
|
11396
|
+
<div id="builder-palette-pop" style="display:none;position:absolute;left:60px;bottom:48px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);z-index:11;min-width:160px">
|
|
11397
|
+
<div onclick="_builderAddNodeOfKind('prompt')" class="builder-palette-item" data-kind="prompt">prompt</div>
|
|
11398
|
+
<div onclick="_builderAddNodeOfKind('mcp')" class="builder-palette-item" data-kind="mcp">mcp tool</div>
|
|
11399
|
+
<div onclick="_builderAddNodeOfKind('channel')" class="builder-palette-item" data-kind="channel">channel</div>
|
|
11400
|
+
<div onclick="_builderAddNodeOfKind('transform')" class="builder-palette-item" data-kind="transform">transform</div>
|
|
11401
|
+
<div onclick="_builderAddNodeOfKind('conditional')" class="builder-palette-item" data-kind="conditional">conditional</div>
|
|
11402
|
+
<div onclick="_builderAddNodeOfKind('loop')" class="builder-palette-item" data-kind="loop">loop</div>
|
|
11403
|
+
</div>
|
|
11404
|
+
<!-- Slide-out config panel -->
|
|
11405
|
+
<div id="builder-config-panel" style="display:none;position:absolute;right:0;top:0;bottom:0;width:340px;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-4px 0 16px rgba(0,0,0,0.15);z-index:12;display:flex;flex-direction:column"></div>
|
|
11406
|
+
<div id="builder-canvas-footer" style="padding:6px 14px;border-top:1px solid var(--border);font-size:11px;color:var(--text-muted);display:flex;gap:14px;align-items:center">
|
|
11407
|
+
<span id="builder-canvas-status"></span>
|
|
11408
|
+
<span style="flex:1"></span>
|
|
11409
|
+
<span id="builder-canvas-id" style="font-family:monospace;opacity:0.6"></span>
|
|
11410
|
+
</div>
|
|
11411
|
+
</div>
|
|
10619
11412
|
<!-- Existing skills drawer (visible in skill mode) -->
|
|
10620
11413
|
<div id="builder-skills-drawer" style="display:none;border-top:2px solid var(--border);max-height:260px;overflow-y:auto">
|
|
10621
11414
|
<div style="padding:10px 16px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--bg-secondary);z-index:1">
|
|
@@ -10626,192 +11419,132 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10626
11419
|
</div>
|
|
10627
11420
|
</div>
|
|
10628
11421
|
</div>
|
|
10629
|
-
</div>
|
|
10630
|
-
|
|
10631
|
-
<!-- ═══ Agent Detail Page (full-screen management console) ═══ -->
|
|
10632
|
-
<div class="page" id="page-agent-detail">
|
|
10633
|
-
<div id="agent-detail-content"><div class="empty-state">Select an agent from the sidebar</div></div>
|
|
10634
|
-
</div>
|
|
10635
11422
|
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
<div id="automations-tab-content">
|
|
10648
|
-
<div class="tab-pane active" id="tab-automations-scheduled">
|
|
10649
|
-
<div id="panel-cron"><div class="empty-state">Loading...</div></div>
|
|
10650
|
-
</div>
|
|
10651
|
-
<div class="tab-pane" id="tab-automations-broken">
|
|
10652
|
-
<div class="card">
|
|
10653
|
-
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
10654
|
-
<span>Repeatedly Failing Jobs (last 48h)</span>
|
|
10655
|
-
<span class="badge badge-gray" id="broken-count-badge" style="font-size:10px">0 jobs</span>
|
|
11423
|
+
<!-- Templates tab — starter patterns -->
|
|
11424
|
+
<div id="build-tab-templates" data-build-tabpane="templates" style="display:none;padding:24px;overflow-y:auto">
|
|
11425
|
+
<div style="max-width:920px;margin:0 auto">
|
|
11426
|
+
<h2 style="font-size:18px;font-weight:600;margin:0 0 6px;color:var(--text-primary)">Start from a template</h2>
|
|
11427
|
+
<p style="font-size:13px;color:var(--text-muted);margin:0 0 18px">Pick a pre-built pattern to fork into a new editable workflow.</p>
|
|
11428
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px">
|
|
11429
|
+
<div class="card clickable-row" onclick="forkBuildTemplate('daily-news-digest')" style="padding:18px">
|
|
11430
|
+
<div style="font-size:24px;margin-bottom:8px">📰</div>
|
|
11431
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Daily news digest</div>
|
|
11432
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron 7am: pull RSS sources, summarize, send to Slack/email.</div>
|
|
11433
|
+
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 4 steps</div>
|
|
10656
11434
|
</div>
|
|
10657
|
-
<div class="card-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
<div class="card-body" id="panel-timers"><div class="empty-state">Loading...</div></div>
|
|
10663
|
-
</div>
|
|
10664
|
-
</div>
|
|
10665
|
-
<div class="tab-pane" id="tab-automations-self-improve">
|
|
10666
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|
10667
|
-
<div style="font-size:13px;color:var(--text-secondary)">Self-improvement runs nightly at 1 AM. You can also trigger it manually.</div>
|
|
10668
|
-
<button class="btn-sm btn-primary" onclick="siRunCycle()" id="si-run-btn">Run Now</button>
|
|
10669
|
-
</div>
|
|
10670
|
-
<div class="grid-2" id="si-status-cards"></div>
|
|
10671
|
-
<div class="card" style="margin-top:16px">
|
|
10672
|
-
<div class="card-header">Pending Proposals</div>
|
|
10673
|
-
<div class="card-body" id="si-pending-list"><div class="empty-state">No pending proposals</div></div>
|
|
10674
|
-
</div>
|
|
10675
|
-
<div class="card" style="margin-top:16px">
|
|
10676
|
-
<div class="card-header">Experiment History</div>
|
|
10677
|
-
<div class="card-body" id="si-history-list"><div class="empty-state">No experiments yet</div></div>
|
|
10678
|
-
</div>
|
|
10679
|
-
</div>
|
|
10680
|
-
<div class="tab-pane" id="tab-automations-skills">
|
|
10681
|
-
<div class="card" style="margin-bottom:16px">
|
|
10682
|
-
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
10683
|
-
<span>Teach a Skill</span>
|
|
10684
|
-
<button class="btn-sm btn-primary" onclick="toggleTeachSkill()" id="teach-skill-toggle" style="font-size:12px">+ New Skill</button>
|
|
11435
|
+
<div class="card clickable-row" onclick="forkBuildTemplate('lead-picker')" style="padding:18px">
|
|
11436
|
+
<div style="font-size:24px;margin-bottom:8px">📊</div>
|
|
11437
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Lead picker → Salesforce</div>
|
|
11438
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Manual workflow: search leads by ICP, review, push selected to SF.</div>
|
|
11439
|
+
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">manual · 3 steps</div>
|
|
10685
11440
|
</div>
|
|
10686
|
-
<div class="card-
|
|
10687
|
-
<div style="
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
</div>
|
|
10692
|
-
<div>
|
|
10693
|
-
<label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Description</label>
|
|
10694
|
-
<input type="text" id="skill-description" placeholder="1-2 sentence summary of what this skill does" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
|
|
10695
|
-
</div>
|
|
10696
|
-
<div>
|
|
10697
|
-
<label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Triggers <span style="font-weight:400;color:var(--text-muted)">(comma-separated keywords)</span></label>
|
|
10698
|
-
<input type="text" id="skill-triggers" placeholder="deploy, production, release, ship" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
|
|
10699
|
-
</div>
|
|
10700
|
-
<div>
|
|
10701
|
-
<label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Procedure <span style="font-weight:400;color:var(--text-muted)">(markdown steps)</span></label>
|
|
10702
|
-
<textarea id="skill-steps" rows="6" placeholder="1. Run tests: npm test\n2. Build: npm run build\n3. Push: git push origin main\n4. Verify deploy succeeded" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px;font-family:monospace;resize:vertical"></textarea>
|
|
10703
|
-
</div>
|
|
10704
|
-
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
10705
|
-
<button class="btn-sm" onclick="toggleTeachSkill()" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);padding:6px 16px;border-radius:6px;cursor:pointer">Cancel</button>
|
|
10706
|
-
<button class="btn-sm btn-primary" onclick="saveSkill()" style="padding:6px 16px">Save Skill</button>
|
|
10707
|
-
</div>
|
|
10708
|
-
</div>
|
|
11441
|
+
<div class="card clickable-row" onclick="forkBuildTemplate('pr-review-queue')" style="padding:18px">
|
|
11442
|
+
<div style="font-size:24px;margin-bottom:8px">📝</div>
|
|
11443
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px">PR review queue</div>
|
|
11444
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron 9am M-F: list open PRs, summarize risk, message to Slack.</div>
|
|
11445
|
+
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 3 steps</div>
|
|
10709
11446
|
</div>
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
<
|
|
10714
|
-
<
|
|
11447
|
+
<div class="card clickable-row" onclick="forkBuildTemplate('email-triage')" style="padding:18px">
|
|
11448
|
+
<div style="font-size:24px;margin-bottom:8px">📧</div>
|
|
11449
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Email triage</div>
|
|
11450
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron 8am: list unread emails, classify by intent, draft replies for review.</div>
|
|
11451
|
+
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 4 steps</div>
|
|
10715
11452
|
</div>
|
|
10716
|
-
<div class="card-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
<
|
|
10721
|
-
|
|
11453
|
+
<div class="card clickable-row" onclick="forkBuildTemplate('weekly-review')" style="padding:18px">
|
|
11454
|
+
<div style="font-size:24px;margin-bottom:8px">📅</div>
|
|
11455
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Weekly review</div>
|
|
11456
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron Fri 6pm: review the week's daily notes, generate review note.</div>
|
|
11457
|
+
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 3 steps</div>
|
|
11458
|
+
</div>
|
|
11459
|
+
<div class="card clickable-row" onclick="forkBuildTemplate('blank-workflow')" style="padding:18px;border-style:dashed">
|
|
11460
|
+
<div style="font-size:24px;margin-bottom:8px">➕</div>
|
|
11461
|
+
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Blank workflow</div>
|
|
11462
|
+
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Start from scratch with a single prompt step.</div>
|
|
11463
|
+
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">manual · 1 step</div>
|
|
10722
11464
|
</div>
|
|
10723
|
-
<div class="card-body" id="panel-skills"><div class="empty-state">No skills learned yet. Skills are auto-extracted from successful tasks or taught manually above.</div></div>
|
|
10724
11465
|
</div>
|
|
10725
11466
|
</div>
|
|
10726
|
-
<div class="tab-pane" id="tab-automations-workflows">
|
|
10727
|
-
<div id="panel-workflows"><div class="empty-state">Loading workflows...</div></div>
|
|
10728
|
-
</div>
|
|
10729
|
-
<div class="tab-pane" id="tab-automations-analytics">
|
|
10730
|
-
<div id="advisor-analytics-content"><div class="empty-state">Loading analytics...</div></div>
|
|
10731
|
-
</div>
|
|
10732
11467
|
</div>
|
|
10733
11468
|
</div>
|
|
10734
11469
|
|
|
10735
|
-
<!--
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
: ''
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
: ''
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
10786
|
-
const obs = new MutationObserver(() => {
|
|
10787
|
-
const page = document.getElementById('page-team-status');
|
|
10788
|
-
if (page && page.classList.contains('active')) renderTeamStatus();
|
|
10789
|
-
});
|
|
10790
|
-
const content = document.querySelector('.content');
|
|
10791
|
-
if (content) obs.observe(content, { subtree: true, attributes: true, attributeFilter: ['class'] });
|
|
11470
|
+
<!-- page-agent-detail merged into Team page; click an agent in Roster to drill down. -->
|
|
11471
|
+
|
|
11472
|
+
|
|
11473
|
+
<!-- DELETED in Session 5 — automations content moved to Build / Brain → Learning.
|
|
11474
|
+
(Block formerly housed: panel-cron, panel-broken-jobs, panel-timers,
|
|
11475
|
+
si-* status cards/proposals/history, teach-skill-form, panel-skills,
|
|
11476
|
+
pending-skills-card, panel-workflows, advisor-analytics-content.) -->
|
|
11477
|
+
<!-- (Session 5) page-automations parking removed. Self-Improve now lives in
|
|
11478
|
+
Brain → Learning; Build (Workflows/Crons/Skills/Templates) is the home for
|
|
11479
|
+
everything else that was here. -->
|
|
11480
|
+
|
|
11481
|
+
<!-- page-team-status merged into Team → Activity tab.
|
|
11482
|
+
Render is now triggered by switchTab('team','activity'). -->
|
|
11483
|
+
<script>
|
|
11484
|
+
(function() {
|
|
11485
|
+
window.renderTeamStatus = function renderTeamStatus() {
|
|
11486
|
+
fetch('/api/team/status').then(function(r) { return r.json(); }).then(function(data) {
|
|
11487
|
+
var grid = document.getElementById('team-status-grid');
|
|
11488
|
+
if (!grid) return;
|
|
11489
|
+
if (!data.agents || data.agents.length === 0) {
|
|
11490
|
+
grid.innerHTML = '<div class="empty-cta"><div class="label">No agents yet</div><div class="hint">Hire your first agent from the Roster tab.</div></div>';
|
|
11491
|
+
return;
|
|
11492
|
+
}
|
|
11493
|
+
grid.innerHTML = data.agents.map(function(a) {
|
|
11494
|
+
var avatarLetter = (a.name || a.slug || '?').charAt(0).toUpperCase();
|
|
11495
|
+
var tasksHtml = a.tasks
|
|
11496
|
+
? '<div style="margin:4px 0"><span style="font-size:12px;color:var(--text-secondary)">Tasks: </span><strong>' + (a.tasks.pending || 0) + ' pending</strong>' +
|
|
11497
|
+
(a.tasks.overdue > 0 ? ' <span style="color:#ef4444;font-weight:600">' + a.tasks.overdue + ' overdue</span>' : '') + '</div>'
|
|
11498
|
+
: '';
|
|
11499
|
+
var goalsHtml = a.activeGoals > 0
|
|
11500
|
+
? '<div style="margin:4px 0;font-size:12px"><span style="color:var(--text-secondary)">Goals: </span><strong>' + a.activeGoals + ' active</strong>' +
|
|
11501
|
+
(a.firstGoalTitle ? ' — ' + a.firstGoalTitle.slice(0, 60) : '') + '</div>'
|
|
11502
|
+
: '';
|
|
11503
|
+
var noteHtml = a.lastNoteDate
|
|
11504
|
+
? '<div style="margin:4px 0;font-size:12px;color:var(--text-secondary)">Last log: ' + a.lastNoteDate +
|
|
11505
|
+
(a.lastNoteSnippet ? ' — ' + a.lastNoteSnippet.slice(0, 100) : '') + '</div>'
|
|
11506
|
+
: '';
|
|
11507
|
+
var wmHtml = a.workingMemorySnippet
|
|
11508
|
+
? '<div style="margin-top:8px;padding:8px;background:var(--bg-input);border-radius:6px;font-size:11px;color:var(--text-secondary)">' + a.workingMemorySnippet + '</div>'
|
|
11509
|
+
: '';
|
|
11510
|
+
return '<div class="clickable-row" style="background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:16px" onclick="navigateTo(\\x27team\\x27,{tab:\\x27roster\\x27,agentSlug:\\x27' + a.slug + '\\x27})">' +
|
|
11511
|
+
'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">' +
|
|
11512
|
+
'<div style="width:40px;height:40px;border-radius:50%;background:var(--clementine);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px">' + avatarLetter + '</div>' +
|
|
11513
|
+
'<div><div style="font-weight:600">' + (a.name || a.slug) + '</div>' +
|
|
11514
|
+
'<div style="font-size:11px;color:var(--text-secondary)">' + (a.description || a.slug) + '</div></div></div>' +
|
|
11515
|
+
tasksHtml + goalsHtml + noteHtml + wmHtml + '</div>';
|
|
11516
|
+
}).join('');
|
|
11517
|
+
}).catch(function() {
|
|
11518
|
+
var grid = document.getElementById('team-status-grid');
|
|
11519
|
+
if (grid) grid.innerHTML = '<div class="empty-state">Failed to load team status.</div>';
|
|
10792
11520
|
});
|
|
10793
|
-
}
|
|
10794
|
-
|
|
10795
|
-
</
|
|
11521
|
+
};
|
|
11522
|
+
})();
|
|
11523
|
+
</script>
|
|
10796
11524
|
|
|
10797
11525
|
<!-- ═══ Brain Page (unified: Search + Graph + Stats + Sources + Seed + Runs) ═══ -->
|
|
10798
|
-
<div class="page" id="page-
|
|
10799
|
-
<div class="page-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
<
|
|
11526
|
+
<div class="page" id="page-brain">
|
|
11527
|
+
<div class="page-head">
|
|
11528
|
+
<div class="icon">🧠</div>
|
|
11529
|
+
<div class="title-block">
|
|
11530
|
+
<h1>Brain</h1>
|
|
11531
|
+
<p class="desc">Query what you know, feed new knowledge in, and watch the system learn.</p>
|
|
11532
|
+
</div>
|
|
11533
|
+
<div class="actions" style="flex:1;max-width:480px;display:flex;gap:8px">
|
|
11534
|
+
<input type="text" id="memory-search-input" placeholder="Search vault, notes, memory..." style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px" onkeydown="if(event.key==='Enter')runMemorySearch()">
|
|
11535
|
+
<button class="btn-primary btn-sm" onclick="runMemorySearch()">Search</button>
|
|
11536
|
+
</div>
|
|
10806
11537
|
</div>
|
|
10807
|
-
<div class="tab-bar" id="intelligence-tabs">
|
|
10808
|
-
<button class="active" onclick="switchTab('intelligence','search')">
|
|
10809
|
-
<button onclick="switchTab('intelligence','graph')">Knowledge
|
|
11538
|
+
<div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
|
|
11539
|
+
<button class="active" onclick="switchTab('intelligence','search')">Memory</button>
|
|
11540
|
+
<button onclick="switchTab('intelligence','graph')">Knowledge</button>
|
|
11541
|
+
<button onclick="switchTab('intelligence','sources')">Ingestion</button>
|
|
11542
|
+
<button onclick="switchTab('intelligence','health')">Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
|
|
10810
11543
|
<button onclick="switchTab('intelligence','user-model')">User Model</button>
|
|
10811
|
-
<button onclick="switchTab('intelligence','
|
|
10812
|
-
<button onclick="switchTab('intelligence','
|
|
10813
|
-
<button onclick="switchTab('intelligence','
|
|
10814
|
-
<button onclick="switchTab('intelligence','runs')">
|
|
11544
|
+
<button onclick="switchTab('intelligence','learning')">Learning <span class="tab-badge" id="brain-learning-badge" style="display:none;background:#f59e0b;color:#000">0</span></button>
|
|
11545
|
+
<button onclick="switchTab('intelligence','memory')">Stats</button>
|
|
11546
|
+
<button onclick="switchTab('intelligence','seed')">Seed</button>
|
|
11547
|
+
<button onclick="switchTab('intelligence','runs')">Runs</button>
|
|
10815
11548
|
</div>
|
|
10816
11549
|
<div id="intelligence-tab-content">
|
|
10817
11550
|
<div class="tab-pane active" id="tab-intelligence-search">
|
|
@@ -10835,6 +11568,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10835
11568
|
<div id="graph-detail-panel" style="margin-top:12px"></div>
|
|
10836
11569
|
</div>
|
|
10837
11570
|
<div class="tab-pane" id="tab-intelligence-memory">
|
|
11571
|
+
<div style="margin-bottom:12px;font-size:13px;color:var(--text-muted)">
|
|
11572
|
+
Stats and content browsing. For janitor, integrity, write queue, and staleness diagnostics see
|
|
11573
|
+
<a href="#" onclick="navigateTo('memory-health');return false" style="color:var(--accent)">Memory Health →</a>
|
|
11574
|
+
</div>
|
|
10838
11575
|
<div class="grid-2" id="memory-stats"></div>
|
|
10839
11576
|
<div class="card">
|
|
10840
11577
|
<div class="card-header">MEMORY.md</div>
|
|
@@ -11009,6 +11746,68 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
11009
11746
|
<div class="tab-pane" id="tab-intelligence-runs">
|
|
11010
11747
|
<div id="brain-runs-list"></div>
|
|
11011
11748
|
</div>
|
|
11749
|
+
<div class="tab-pane" id="tab-intelligence-health">
|
|
11750
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
|
11751
|
+
<button class="btn-sm" onclick="memoryHealthAction('janitor')" title="Run the janitor cleanup pass now">Run cleanup</button>
|
|
11752
|
+
<button class="btn-sm" onclick="memoryHealthAction('rebuild-fts')" title="Rebuild the FTS5 index">Rebuild FTS</button>
|
|
11753
|
+
<button class="btn-sm" onclick="memoryHealthAction('fix-orphans')" title="Null out missing derived_from refs">Fix orphans</button>
|
|
11754
|
+
<button class="btn-sm" onclick="refreshMemoryHealth()">Refresh</button>
|
|
11755
|
+
</div>
|
|
11756
|
+
<div id="memory-health-content">
|
|
11757
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
|
|
11758
|
+
</div>
|
|
11759
|
+
<div class="card" style="margin-top:18px">
|
|
11760
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
11761
|
+
<span>Trust & claim verification</span>
|
|
11762
|
+
<span id="trust-score-detail" style="font-size:11px;color:var(--text-muted)">Rolling 30-claim score</span>
|
|
11763
|
+
</div>
|
|
11764
|
+
<div class="card-body" style="padding:14px;display:flex;align-items:center;gap:14px">
|
|
11765
|
+
<span style="font-size:28px;font-weight:700" id="trust-score-big">--</span>
|
|
11766
|
+
<div style="flex:1;display:flex;gap:6px;flex-wrap:wrap">
|
|
11767
|
+
<button class="btn-sm" onclick="refreshClaims('all')" id="claims-filter-all">All</button>
|
|
11768
|
+
<button class="btn-sm" onclick="refreshClaims('pending')" id="claims-filter-pending">Pending</button>
|
|
11769
|
+
<button class="btn-sm" onclick="refreshClaims('verified')" id="claims-filter-verified">Verified</button>
|
|
11770
|
+
<button class="btn-sm" onclick="refreshClaims('failed')" id="claims-filter-failed">Failed</button>
|
|
11771
|
+
</div>
|
|
11772
|
+
</div>
|
|
11773
|
+
</div>
|
|
11774
|
+
<div class="card" style="margin-top:14px">
|
|
11775
|
+
<div class="card-header">Recent claims</div>
|
|
11776
|
+
<div class="card-body" id="panel-claims">
|
|
11777
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
11778
|
+
</div>
|
|
11779
|
+
</div>
|
|
11780
|
+
<div class="card" style="margin-top:14px">
|
|
11781
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
11782
|
+
<span>Team routing decisions</span>
|
|
11783
|
+
<span style="font-size:11px;color:var(--text-muted)">Owner-facing sessions only</span>
|
|
11784
|
+
</div>
|
|
11785
|
+
<div class="card-body" id="panel-routing-audit">
|
|
11786
|
+
<div class="skel-block"><div class="skel-row med"></div></div>
|
|
11787
|
+
</div>
|
|
11788
|
+
</div>
|
|
11789
|
+
</div>
|
|
11790
|
+
<div class="tab-pane" id="tab-intelligence-learning">
|
|
11791
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|
11792
|
+
<div style="font-size:13px;color:var(--text-secondary)">Self-improvement runs nightly at 1 AM. You can also trigger it manually.</div>
|
|
11793
|
+
<button class="btn-sm btn-primary" onclick="siRunCycle()" id="si-run-btn">Run Now</button>
|
|
11794
|
+
</div>
|
|
11795
|
+
<div class="grid-2" id="si-status-cards">
|
|
11796
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
11797
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
11798
|
+
</div>
|
|
11799
|
+
<div class="card" style="margin-top:16px">
|
|
11800
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
11801
|
+
<span>Pending Proposals</span>
|
|
11802
|
+
<span class="tab-badge" id="tab-si-pending" style="display:none;background:#f59e0b;color:#000">0</span>
|
|
11803
|
+
</div>
|
|
11804
|
+
<div class="card-body" id="si-pending-list"><div class="empty-state">No pending proposals</div></div>
|
|
11805
|
+
</div>
|
|
11806
|
+
<div class="card" style="margin-top:16px">
|
|
11807
|
+
<div class="card-header">Experiment History</div>
|
|
11808
|
+
<div class="card-body" id="si-history-list"><div class="empty-state">No experiments yet</div></div>
|
|
11809
|
+
</div>
|
|
11810
|
+
</div>
|
|
11012
11811
|
</div>
|
|
11013
11812
|
|
|
11014
11813
|
<script>
|
|
@@ -11963,127 +12762,47 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
11963
12762
|
</script>
|
|
11964
12763
|
</div>
|
|
11965
12764
|
|
|
11966
|
-
<!--
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
<!--
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
<div style="font-size:13px;font-weight:600">Clementine's trust score</div>
|
|
11979
|
-
<div style="font-size:11px;color:var(--text-muted)" id="trust-score-detail">
|
|
11980
|
-
Rolling over the last 30 verified or failed claims.
|
|
11981
|
-
</div>
|
|
11982
|
-
</div>
|
|
11983
|
-
<div style="display:flex;gap:6px">
|
|
11984
|
-
<button class="btn-sm" onclick="refreshClaims('all')" id="claims-filter-all" style="padding:4px 10px">All</button>
|
|
11985
|
-
<button class="btn-sm" onclick="refreshClaims('pending')" id="claims-filter-pending" style="padding:4px 10px">Pending</button>
|
|
11986
|
-
<button class="btn-sm" onclick="refreshClaims('verified')" id="claims-filter-verified" style="padding:4px 10px">Verified</button>
|
|
11987
|
-
<button class="btn-sm" onclick="refreshClaims('failed')" id="claims-filter-failed" style="padding:4px 10px">Failed</button>
|
|
11988
|
-
</div>
|
|
11989
|
-
</div>
|
|
11990
|
-
</div>
|
|
11991
|
-
<div class="card">
|
|
11992
|
-
<div class="card-header">Recent claims</div>
|
|
11993
|
-
<div class="card-body" id="panel-claims"><div class="empty-state">Loading...</div></div>
|
|
11994
|
-
</div>
|
|
11995
|
-
<div class="card" style="margin-top:16px">
|
|
11996
|
-
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
11997
|
-
<span>Team routing decisions</span>
|
|
11998
|
-
<span style="font-size:11px;color:var(--text-muted)">Only owner-facing Clementine sessions are classified — agent-bot DMs bypass routing entirely.</span>
|
|
11999
|
-
</div>
|
|
12000
|
-
<div class="card-body" id="panel-routing-audit"><div class="empty-state">Loading...</div></div>
|
|
12001
|
-
</div>
|
|
12002
|
-
</div>
|
|
12003
|
-
|
|
12004
|
-
<!-- ═══ Logs Page ═══ -->
|
|
12005
|
-
<div class="page" id="page-logs">
|
|
12006
|
-
<div class="page-title">Logs</div>
|
|
12007
|
-
<div class="log-toolbar">
|
|
12008
|
-
<input type="text" class="log-filter" id="log-filter" placeholder="Filter logs...">
|
|
12009
|
-
<select id="log-level-filter" style="background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;font-size:12px;color:var(--text-primary);font-family:inherit;cursor:pointer" onchange="applyLogFilter()">
|
|
12010
|
-
<option value="">All Levels</option>
|
|
12011
|
-
<option value="error">Error+</option>
|
|
12012
|
-
<option value="warn">Warn+</option>
|
|
12013
|
-
<option value="info">Info+</option>
|
|
12014
|
-
<option value="debug">Debug+</option>
|
|
12015
|
-
</select>
|
|
12016
|
-
<button onclick="refreshLogs()">Refresh</button>
|
|
12017
|
-
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer">
|
|
12018
|
-
<input type="checkbox" id="log-autoscroll" checked> Auto-scroll
|
|
12019
|
-
</label>
|
|
12020
|
-
</div>
|
|
12021
|
-
<div class="log-viewer" id="panel-logs"><div class="empty-state">Loading...</div></div>
|
|
12022
|
-
</div>
|
|
12023
|
-
|
|
12024
|
-
<!-- ═══ Chat Page ═══ -->
|
|
12025
|
-
<div class="page" id="page-chat">
|
|
12026
|
-
<div id="chat-messages" style="flex:1;overflow-y:auto;padding:16px">
|
|
12027
|
-
<div class="empty-state">
|
|
12028
|
-
<p style="margin-bottom:14px;color:var(--text-muted)">Send a message to start a conversation.</p>
|
|
12029
|
-
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
|
|
12030
|
-
<button class="btn btn-sm quick-pill" onclick="quickChat("What's on my schedule?")">What's on my schedule?</button>
|
|
12031
|
-
<button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Check my email</button>
|
|
12032
|
-
<button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Run morning briefing</button>
|
|
12033
|
-
<button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">What did you do today?</button>
|
|
12034
|
-
</div>
|
|
12035
|
-
</div>
|
|
12036
|
-
</div>
|
|
12037
|
-
<div style="border-top:1px solid var(--border);padding:8px 14px 0;display:flex;align-items:center;gap:8px" id="chat-profile-bar">
|
|
12038
|
-
<span style="font-size:11px;color:var(--text-muted)">Profile:</span>
|
|
12039
|
-
<select id="chat-profile-select" onchange="switchProfile(this.value)" style="font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary)">
|
|
12040
|
-
<option value="">Default</option>
|
|
12041
|
-
</select>
|
|
12042
|
-
</div>
|
|
12043
|
-
<div style="border-top:1px solid var(--border);padding:14px;display:flex;gap:10px">
|
|
12044
|
-
<input type="text" id="chat-input" placeholder="Type a message..." style="flex:1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}">
|
|
12045
|
-
<button class="btn-primary" id="chat-send-btn" onclick="sendChat()">Send</button>
|
|
12046
|
-
</div>
|
|
12047
|
-
</div>
|
|
12048
|
-
|
|
12049
|
-
<!-- Hidden: Metrics (shown in Home tabs in Phase 2) -->
|
|
12050
|
-
<div class="page" id="page-metrics" style="display:none">
|
|
12051
|
-
<div id="metrics-content"><div class="empty-state">Loading metrics...</div></div>
|
|
12765
|
+
<!-- Sessions, Trust & Claims, Logs, Chat, Metrics, Daily Plan pages
|
|
12766
|
+
removed in Session 2 — content lives on Home or migrates to other
|
|
12767
|
+
destinations in Sessions 3-4. Page references for these IDs are
|
|
12768
|
+
resolved via ROUTE_REDIRECTS in navigateTo. -->
|
|
12769
|
+
|
|
12770
|
+
<!-- Hidden mounting points for daily-plan content used by the Home rail
|
|
12771
|
+
(refreshDailyPlan looks for these by ID). Keeping the IDs avoids
|
|
12772
|
+
touching every refreshDailyPlan callsite right now. -->
|
|
12773
|
+
<div style="display:none">
|
|
12774
|
+
<div id="plan-diff-content"></div>
|
|
12775
|
+
<div id="plan-history-list"></div>
|
|
12776
|
+
<input type="date" id="plan-date-picker-secondary" disabled>
|
|
12052
12777
|
</div>
|
|
12053
12778
|
|
|
12054
|
-
<!--
|
|
12055
|
-
<div class="page" id="page-daily-plan">
|
|
12056
|
-
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
|
12057
|
-
<div class="page-title" style="margin-bottom:0">Daily Plan</div>
|
|
12058
|
-
<div style="display:flex;gap:8px;align-items:center">
|
|
12059
|
-
<input type="date" id="plan-date-picker" style="padding:6px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
|
|
12060
|
-
<button class="btn btn-sm" onclick="loadPlanForDate(document.getElementById('plan-date-picker').value)">Load</button>
|
|
12061
|
-
</div>
|
|
12062
|
-
</div>
|
|
12063
|
-
<div id="daily-plan-content"><div class="empty-state">Loading plan...</div></div>
|
|
12064
|
-
<div id="plan-diff-content" style="margin-top:16px"></div>
|
|
12065
|
-
<details style="margin-top:16px">
|
|
12066
|
-
<summary style="cursor:pointer;font-weight:600;color:var(--text-secondary);font-size:13px;padding:8px 0;user-select:none">Plan History</summary>
|
|
12067
|
-
<div id="plan-history-list" style="margin-top:8px"><div class="empty-state">Loading...</div></div>
|
|
12068
|
-
</details>
|
|
12069
|
-
</div>
|
|
12779
|
+
<!-- (Session 5) page-memory-health parking stub removed. Brain → Health is the live home. -->
|
|
12070
12780
|
|
|
12071
|
-
<!--
|
|
12072
|
-
<div class="page" id="page-goals" style="display:none">
|
|
12073
|
-
<div id="goals-progress-content"><div class="empty-state">Loading goals...</div></div>
|
|
12074
|
-
</div>
|
|
12781
|
+
<!-- page-goals merged into Team → Goals tab. -->
|
|
12075
12782
|
|
|
12076
12783
|
<!-- ═══ Team Page — The Office ═══ -->
|
|
12077
12784
|
<div class="page" id="page-team">
|
|
12078
|
-
<div
|
|
12079
|
-
<div class="
|
|
12080
|
-
<div
|
|
12081
|
-
<
|
|
12082
|
-
<
|
|
12083
|
-
|
|
12084
|
-
|
|
12785
|
+
<div class="page-head">
|
|
12786
|
+
<div class="icon">👥</div>
|
|
12787
|
+
<div class="title-block">
|
|
12788
|
+
<h1>The Office</h1>
|
|
12789
|
+
<p class="desc">Your team of agents — what they're doing, what they're contributing to.</p>
|
|
12790
|
+
</div>
|
|
12791
|
+
<div class="actions">
|
|
12792
|
+
<button class="btn-primary btn-sm" onclick="startHiringInterview()" style="background:var(--green);color:#000">Hire</button>
|
|
12793
|
+
<button class="btn-sm" onclick="applyRoleTemplate('sdr')" title="Pre-filled SDR role template">+ SDR</button>
|
|
12794
|
+
<button class="btn-sm" onclick="applyRoleTemplate('researcher')" title="Pre-filled researcher role template">+ Researcher</button>
|
|
12795
|
+
<button class="btn-sm" onclick="showAgentCreateModal()">Manual</button>
|
|
12085
12796
|
</div>
|
|
12086
12797
|
</div>
|
|
12798
|
+
<div class="tab-bar" id="team-tabs" style="margin:0 0 0 18px">
|
|
12799
|
+
<button class="active" onclick="switchTab('team','roster')">Roster</button>
|
|
12800
|
+
<button onclick="switchTab('team','activity')">Activity</button>
|
|
12801
|
+
<button onclick="switchTab('team','goals')">Goals</button>
|
|
12802
|
+
<button onclick="switchTab('team','comms')">Comms</button>
|
|
12803
|
+
</div>
|
|
12804
|
+
<div id="team-tab-content">
|
|
12805
|
+
<div class="tab-pane active" id="tab-team-roster">
|
|
12087
12806
|
<div id="office-hero-section"></div>
|
|
12088
12807
|
<div class="office-floor" id="team-agent-grid">
|
|
12089
12808
|
<div class="empty-state">No agents configured</div>
|
|
@@ -12288,16 +13007,60 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
12288
13007
|
</form>
|
|
12289
13008
|
</div>
|
|
12290
13009
|
</div>
|
|
13010
|
+
</div><!-- /tab-team-roster -->
|
|
13011
|
+
|
|
13012
|
+
<!-- Team → Activity tab (migrated from page-team-status) -->
|
|
13013
|
+
<div class="tab-pane" id="tab-team-activity" style="padding:18px">
|
|
13014
|
+
<div id="team-status-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px">
|
|
13015
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
|
|
13016
|
+
</div>
|
|
13017
|
+
</div>
|
|
13018
|
+
|
|
13019
|
+
<!-- Team → Goals tab (migrated from page-goals) -->
|
|
13020
|
+
<div class="tab-pane" id="tab-team-goals" style="padding:18px">
|
|
13021
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
13022
|
+
<p style="margin:0;font-size:13px;color:var(--text-muted)">Long-running objectives the team contributes to. Per-agent contribution + run success rate.</p>
|
|
13023
|
+
<button class="btn-primary btn-sm" onclick="openNewGoalForm()">+ New Goal</button>
|
|
13024
|
+
</div>
|
|
13025
|
+
<div id="new-goal-form" style="display:none;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:14px">
|
|
13026
|
+
<div style="font-weight:600;font-size:13px;margin-bottom:10px">New goal</div>
|
|
13027
|
+
<input type="text" id="new-goal-title" placeholder="Goal title (e.g., 'Pipeline: 12 qualified leads/week')" style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);margin-bottom:8px">
|
|
13028
|
+
<textarea id="new-goal-desc" placeholder="Why does this matter? (optional)" rows="2" style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-family:inherit;margin-bottom:10px"></textarea>
|
|
13029
|
+
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
13030
|
+
<button class="btn-sm" onclick="document.getElementById('new-goal-form').style.display='none'">Cancel</button>
|
|
13031
|
+
<button class="btn-sm btn-primary" onclick="submitNewGoal()">Create</button>
|
|
13032
|
+
</div>
|
|
13033
|
+
</div>
|
|
13034
|
+
<div id="goals-progress-content">
|
|
13035
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
|
|
13036
|
+
</div>
|
|
13037
|
+
</div>
|
|
13038
|
+
|
|
13039
|
+
<!-- Team → Comms tab — placeholder (Session 5 will fill). -->
|
|
13040
|
+
<div class="tab-pane" id="tab-team-comms" style="padding:18px">
|
|
13041
|
+
<div class="empty-cta">
|
|
13042
|
+
<div class="icon">📣</div>
|
|
13043
|
+
<div class="label">Inter-agent communications</div>
|
|
13044
|
+
<div class="hint">Coming soon — message log + topology visualization across agents.</div>
|
|
13045
|
+
</div>
|
|
13046
|
+
</div>
|
|
13047
|
+
</div><!-- /team-tab-content -->
|
|
12291
13048
|
</div>
|
|
12292
13049
|
|
|
13050
|
+
<!-- (Session 5) team-status / agent-detail / goals parking divs removed.
|
|
13051
|
+
Team's Roster / Activity / Goals tabs are the live homes. -->
|
|
13052
|
+
|
|
12293
13053
|
<!-- ═══ Settings Page (merged: General + Remote + Integrations + Projects) ═══ -->
|
|
12294
13054
|
<div class="page" id="page-settings">
|
|
12295
13055
|
<div class="page-title">Settings</div>
|
|
12296
13056
|
<div class="tab-bar" id="settings-tabs">
|
|
12297
|
-
<button class="active" onclick="switchTab('settings','general')">
|
|
12298
|
-
<button onclick="switchTab('settings','remote')">Remote Access</button>
|
|
13057
|
+
<button class="active" onclick="switchTab('settings','general')">Channels & Env</button>
|
|
12299
13058
|
<button onclick="switchTab('settings','integrations')">Integrations</button>
|
|
12300
|
-
<button onclick="switchTab('settings','
|
|
13059
|
+
<button onclick="switchTab('settings','projects')">Projects</button>
|
|
13060
|
+
<button onclick="switchTab('settings','security')">Security</button>
|
|
13061
|
+
<button onclick="switchTab('settings','logs')">Logs</button>
|
|
13062
|
+
<button onclick="switchTab('settings','remote')">Remote Access</button>
|
|
13063
|
+
<button onclick="switchTab('settings','advanced')">Advanced</button>
|
|
12301
13064
|
</div>
|
|
12302
13065
|
<div id="settings-tab-content">
|
|
12303
13066
|
<div class="tab-pane active" id="tab-settings-general">
|
|
@@ -12318,6 +13081,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
12318
13081
|
</div>
|
|
12319
13082
|
</div>
|
|
12320
13083
|
</div>
|
|
13084
|
+
<div class="tab-pane" id="tab-settings-security">
|
|
13085
|
+
<div class="card">
|
|
13086
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
13087
|
+
<span>Active Sessions</span>
|
|
13088
|
+
<button class="btn-sm" style="font-size:11px" onclick="refreshAuthSessions()">Refresh</button>
|
|
13089
|
+
</div>
|
|
13090
|
+
<div class="card-body" style="padding:0" id="sessions-list">
|
|
13091
|
+
<div class="empty-state" style="padding:24px">Loading sessions...</div>
|
|
13092
|
+
</div>
|
|
13093
|
+
</div>
|
|
13094
|
+
<p style="color:var(--text-muted);font-size:12px;margin-top:12px">
|
|
13095
|
+
Sessions persist across daemon restarts. "Remember me" sessions last 30 days; standard sessions expire after 24 hours.
|
|
13096
|
+
Revoke any device you no longer trust.
|
|
13097
|
+
</p>
|
|
13098
|
+
</div>
|
|
12321
13099
|
<div class="tab-pane" id="tab-settings-integrations">
|
|
12322
13100
|
<div class="card" style="margin-bottom:20px">
|
|
12323
13101
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
@@ -12396,47 +13174,62 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
12396
13174
|
</div>
|
|
12397
13175
|
</div>
|
|
12398
13176
|
</div>
|
|
12399
|
-
<div class="tab-pane" id="tab-settings-notifications">
|
|
12400
|
-
<div id="digest-settings-content"><div class="empty-state">Loading...</div></div>
|
|
12401
|
-
</div>
|
|
12402
13177
|
<div class="tab-pane" id="tab-settings-projects">
|
|
12403
|
-
<p style="color:var(--text-muted);margin-bottom:16px">Link projects to give Clementine automatic access to their tools and MCP servers. When you mention a linked project's keywords in chat, Clementine switches into that project's context automatically.</p>
|
|
13178
|
+
<p style="color:var(--text-muted);margin-bottom:16px;font-size:13px">Link projects to give Clementine automatic access to their tools and MCP servers. When you mention a linked project's keywords in chat, Clementine switches into that project's context automatically.</p>
|
|
12404
13179
|
<div class="card" style="margin-bottom:20px">
|
|
12405
13180
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
12406
13181
|
<span>Workspace Directories</span>
|
|
12407
13182
|
<button class="btn btn-sm btn-primary" onclick="promptAddWorkspaceDir()" style="font-size:11px">+ Add Path</button>
|
|
12408
13183
|
</div>
|
|
12409
|
-
<div class="card-body" id="workspace-dirs-list" style="font-size:13px">
|
|
12410
|
-
<div class="
|
|
13184
|
+
<div class="card-body" id="workspace-dirs-list-projects" style="font-size:13px">
|
|
13185
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
12411
13186
|
</div>
|
|
12412
13187
|
</div>
|
|
12413
|
-
<div id="panel-projects
|
|
13188
|
+
<div id="panel-projects-page">
|
|
13189
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div></div>
|
|
13190
|
+
</div>
|
|
12414
13191
|
</div>
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
13192
|
+
<div class="tab-pane" id="tab-settings-logs">
|
|
13193
|
+
<div style="display:flex;gap:8px;align-items:center;margin-bottom:14px;flex-wrap:wrap">
|
|
13194
|
+
<input type="text" class="log-filter" id="log-filter" placeholder="Filter logs..." style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:12px;background:var(--bg-input);color:var(--text-primary);min-width:180px;flex:1">
|
|
13195
|
+
<select id="log-level-filter" style="background:var(--bg-input);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text-primary);font-family:inherit;cursor:pointer" onchange="applyLogFilter()">
|
|
13196
|
+
<option value="">All Levels</option>
|
|
13197
|
+
<option value="error">Error+</option>
|
|
13198
|
+
<option value="warn">Warn+</option>
|
|
13199
|
+
<option value="info">Info+</option>
|
|
13200
|
+
<option value="debug">Debug+</option>
|
|
13201
|
+
</select>
|
|
13202
|
+
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer">
|
|
13203
|
+
<input type="checkbox" id="log-autoscroll" checked> Auto-scroll
|
|
13204
|
+
</label>
|
|
13205
|
+
<button class="btn-sm" onclick="refreshLogs()">Refresh</button>
|
|
13206
|
+
</div>
|
|
13207
|
+
<div class="log-viewer" id="panel-logs">
|
|
13208
|
+
<div class="skel-block" style="padding:18px"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
|
|
13209
|
+
</div>
|
|
12426
13210
|
</div>
|
|
12427
|
-
<div class="
|
|
12428
|
-
<div class="
|
|
13211
|
+
<div class="tab-pane" id="tab-settings-advanced">
|
|
13212
|
+
<div class="card" style="margin-bottom:16px">
|
|
13213
|
+
<div class="card-header">Diagnostics & maintenance</div>
|
|
13214
|
+
<div class="card-body" style="padding:16px;display:flex;gap:8px;flex-wrap:wrap">
|
|
13215
|
+
<button class="btn-sm" onclick="restartDashboard()">Restart Dashboard</button>
|
|
13216
|
+
<button class="btn-sm" onclick="if(confirm('Restart the daemon? Active sessions drain first.')) apiPost('/api/restart')">Restart Daemon</button>
|
|
13217
|
+
<button class="btn-sm" onclick="apiFetch('/api/doctor').then(function(r){return r.text()}).then(function(t){alert(t)})">Run Doctor</button>
|
|
13218
|
+
<button class="btn-sm" onclick="apiFetch('/api/version').then(function(r){return r.json()}).then(function(d){alert('Version: '+(d.version||'?')+'\\nNode: '+(d.node||'?'))})">Build info</button>
|
|
13219
|
+
</div>
|
|
13220
|
+
</div>
|
|
13221
|
+
<div class="card">
|
|
13222
|
+
<div class="card-header">Notifications & digest</div>
|
|
13223
|
+
<div class="card-body" style="padding:16px" id="digest-settings-content">
|
|
13224
|
+
<div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
13225
|
+
</div>
|
|
13226
|
+
</div>
|
|
12429
13227
|
</div>
|
|
12430
13228
|
</div>
|
|
12431
|
-
<div id="panel-projects-page"><div class="empty-state">Loading...</div></div>
|
|
12432
13229
|
</div>
|
|
12433
13230
|
|
|
12434
|
-
<!--
|
|
12435
|
-
|
|
12436
|
-
<div class="page-title">Workflows</div>
|
|
12437
|
-
<p style="color:var(--text-muted);margin-bottom:16px">Multi-step pipelines that orchestrate agents, tools, and data. Define workflows as .md files in <code>vault/00-System/workflows/</code>.</p>
|
|
12438
|
-
<div id="panel-workflows-page"><div class="empty-state">Loading workflows...</div></div>
|
|
12439
|
-
</div>
|
|
13231
|
+
<!-- (Session 5) page-workflows / page-unleashed parking divs removed.
|
|
13232
|
+
Build → Workflows + Home rail Active Runs are the live homes. -->
|
|
12440
13233
|
|
|
12441
13234
|
</div><!-- /content -->
|
|
12442
13235
|
</div><!-- /layout -->
|
|
@@ -13063,45 +13856,378 @@ let currentPage = 'home';
|
|
|
13063
13856
|
var currentAgentSlug = null;
|
|
13064
13857
|
var prevAgentSlugs = null;
|
|
13065
13858
|
|
|
13859
|
+
// ── Routing ────────────────────────────────────────────────────
|
|
13860
|
+
//
|
|
13861
|
+
// Five top-level destinations: home, build, team, brain, settings.
|
|
13862
|
+
// Sub-pages live as tabs within each destination.
|
|
13863
|
+
// Old routes redirect once for back-compat.
|
|
13864
|
+
|
|
13865
|
+
var DESTINATIONS = ['home', 'build', 'team', 'brain', 'settings'];
|
|
13866
|
+
|
|
13867
|
+
var ROUTE_REDIRECTS = {
|
|
13868
|
+
// old hash → new {page, tab}
|
|
13869
|
+
'chat': { page: 'home', tab: 'chat' },
|
|
13870
|
+
'sessions': { page: 'home', tab: 'activity' },
|
|
13871
|
+
'daily-plan': { page: 'home', tab: 'today' },
|
|
13872
|
+
'goals': { page: 'team', tab: 'goals' },
|
|
13873
|
+
'workflows': { page: 'build', tab: 'workflows' },
|
|
13874
|
+
'automations': { page: 'build', tab: 'crons' },
|
|
13875
|
+
'unleashed': { page: 'build', tab: 'workflows' },
|
|
13876
|
+
'builder': { page: 'build', tab: 'workflows' },
|
|
13877
|
+
'skill-studio': { page: 'build', tab: 'skills' },
|
|
13878
|
+
'team-status': { page: 'team', tab: 'activity' },
|
|
13879
|
+
'agent-detail': { page: 'team', tab: 'roster' },
|
|
13880
|
+
'intelligence': { page: 'brain', tab: 'memory' },
|
|
13881
|
+
'memory-health': { page: 'brain', tab: 'health' },
|
|
13882
|
+
'claims': { page: 'brain', tab: 'health' },
|
|
13883
|
+
'metrics': { page: 'team', tab: 'activity' },
|
|
13884
|
+
'logs': { page: 'settings', tab: 'logs' },
|
|
13885
|
+
'projects': { page: 'settings', tab: 'projects' },
|
|
13886
|
+
};
|
|
13887
|
+
|
|
13066
13888
|
function navigateTo(page, opts) {
|
|
13067
13889
|
opts = opts || {};
|
|
13890
|
+
|
|
13891
|
+
// Redirect old route names to the new IA so existing callers + bookmarks work.
|
|
13892
|
+
if (ROUTE_REDIRECTS[page]) {
|
|
13893
|
+
var r = ROUTE_REDIRECTS[page];
|
|
13894
|
+
return navigateTo(r.page, Object.assign({ tab: r.tab }, opts));
|
|
13895
|
+
}
|
|
13896
|
+
|
|
13897
|
+
if (DESTINATIONS.indexOf(page) === -1) page = 'home';
|
|
13068
13898
|
currentPage = page;
|
|
13069
|
-
|
|
13070
|
-
document.querySelectorAll('.nav-item').forEach(n
|
|
13071
|
-
document.querySelectorAll('.team-nav-item').forEach(n
|
|
13072
|
-
document.querySelectorAll('.page').forEach(p
|
|
13073
|
-
|
|
13899
|
+
|
|
13900
|
+
document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
|
|
13901
|
+
document.querySelectorAll('.team-nav-item').forEach(function(n) { n.classList.remove('active'); });
|
|
13902
|
+
document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
|
|
13903
|
+
|
|
13074
13904
|
var navEl = document.querySelector('.nav-item[data-page="' + page + '"]');
|
|
13075
13905
|
if (navEl) navEl.classList.add('active');
|
|
13076
13906
|
if (opts.agentSlug != null) {
|
|
13077
13907
|
var teamEl = document.querySelector('.team-nav-item[data-slug="' + opts.agentSlug + '"]');
|
|
13078
13908
|
if (teamEl) teamEl.classList.add('active');
|
|
13079
13909
|
}
|
|
13080
|
-
|
|
13081
|
-
var el = document.getElementById('page-' + page);
|
|
13082
|
-
if (el) { el.style.display = ''; el.classList.add('active'); }
|
|
13083
|
-
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13910
|
+
|
|
13911
|
+
var el = document.getElementById('page-' + page);
|
|
13912
|
+
if (el) { el.style.display = ''; el.classList.add('active'); }
|
|
13913
|
+
|
|
13914
|
+
// Per-destination init + tab routing
|
|
13915
|
+
switch (page) {
|
|
13916
|
+
case 'home':
|
|
13917
|
+
refreshAll();
|
|
13918
|
+
// tab is a soft hint on Home (one cohesive layout): focus the relevant area.
|
|
13919
|
+
var t = opts.tab || 'chat';
|
|
13920
|
+
setTimeout(function() {
|
|
13921
|
+
if (t === 'chat') {
|
|
13922
|
+
var ci = document.getElementById('chat-input');
|
|
13923
|
+
if (ci) ci.focus();
|
|
13924
|
+
} else if (t === 'today') {
|
|
13925
|
+
var rail = document.getElementById('home-rail');
|
|
13926
|
+
if (rail && window.matchMedia('(max-width: 1024px)').matches) rail.classList.add('open');
|
|
13927
|
+
var p = document.getElementById('home-plan-content');
|
|
13928
|
+
if (p) p.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
13929
|
+
} else if (t === 'activity') {
|
|
13930
|
+
var act = document.getElementById('panel-activity');
|
|
13931
|
+
if (act) act.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
13932
|
+
}
|
|
13933
|
+
}, 80);
|
|
13934
|
+
break;
|
|
13935
|
+
case 'build':
|
|
13936
|
+
switchBuildTab(opts.tab || 'workflows');
|
|
13937
|
+
var bp = currentAgentSlug || '';
|
|
13938
|
+
refreshBuilderAgents(bp);
|
|
13939
|
+
break;
|
|
13940
|
+
case 'team':
|
|
13941
|
+
refreshTeam();
|
|
13942
|
+
switchDestTab('team', opts.tab || 'roster');
|
|
13943
|
+
if (opts.agentSlug) {
|
|
13944
|
+
currentAgentSlug = opts.agentSlug;
|
|
13945
|
+
if (typeof openAgentDrawer === 'function') openAgentDrawer(opts.agentSlug);
|
|
13946
|
+
}
|
|
13947
|
+
break;
|
|
13948
|
+
case 'brain':
|
|
13949
|
+
if (typeof refreshMemory === 'function') refreshMemory();
|
|
13950
|
+
var bt = opts.tab || 'memory';
|
|
13951
|
+
// Spec tab names → internal intelligence-tab ids
|
|
13952
|
+
var intelTab = bt === 'memory' ? 'search'
|
|
13953
|
+
: bt === 'knowledge' ? 'graph'
|
|
13954
|
+
: bt === 'ingestion' ? 'sources'
|
|
13955
|
+
: bt === 'health' ? 'health'
|
|
13956
|
+
: bt === 'user-model' ? 'user-model'
|
|
13957
|
+
: bt === 'learning' ? 'learning'
|
|
13958
|
+
: bt;
|
|
13959
|
+
try { switchTab('intelligence', intelTab); } catch (e) { /* */ }
|
|
13960
|
+
if (bt === 'health') {
|
|
13961
|
+
if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
|
|
13962
|
+
if (typeof refreshClaims === 'function') refreshClaims();
|
|
13963
|
+
if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
|
|
13964
|
+
}
|
|
13965
|
+
if (bt === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
13966
|
+
break;
|
|
13967
|
+
case 'settings':
|
|
13968
|
+
switchDestTab('settings', opts.tab || 'channels');
|
|
13969
|
+
break;
|
|
13970
|
+
}
|
|
13971
|
+
|
|
13972
|
+
closeSidebar();
|
|
13973
|
+
}
|
|
13974
|
+
|
|
13975
|
+
/** Switch the active tab inside a destination. Tabs use [data-tab] markup. */
|
|
13976
|
+
function switchDestTab(page, tab) {
|
|
13977
|
+
if (!tab) return;
|
|
13978
|
+
var pageEl = document.getElementById('page-' + page);
|
|
13979
|
+
if (!pageEl) return;
|
|
13980
|
+
pageEl.querySelectorAll('[data-tab]').forEach(function(el) {
|
|
13981
|
+
var match = el.getAttribute('data-tab') === tab;
|
|
13982
|
+
if (el.tagName === 'BUTTON' || el.classList.contains('tab-btn')) {
|
|
13983
|
+
el.classList.toggle('active', match);
|
|
13984
|
+
} else {
|
|
13985
|
+
el.style.display = match ? '' : 'none';
|
|
13986
|
+
}
|
|
13987
|
+
});
|
|
13988
|
+
// Per-tab init hook (called after DOM update)
|
|
13989
|
+
var initFn = window['_init_' + page + '_' + tab];
|
|
13990
|
+
if (typeof initFn === 'function') {
|
|
13991
|
+
try { initFn(); } catch (err) { console.warn('Tab init failed:', err); }
|
|
13992
|
+
}
|
|
13993
|
+
}
|
|
13994
|
+
|
|
13995
|
+
// (Session 5) openAutomationsTab compat shim removed — all callers route via navigateTo + ROUTE_REDIRECTS.
|
|
13996
|
+
|
|
13997
|
+
// ── Build (Workflows / Crons / Skills / Templates) tabs ─────────────
|
|
13998
|
+
function switchBuildTab(tab) {
|
|
13999
|
+
if (!tab) tab = 'workflows';
|
|
14000
|
+
// Update tab-bar active state
|
|
14001
|
+
document.querySelectorAll('#build-tabs button').forEach(function(b) {
|
|
14002
|
+
b.classList.toggle('active', b.getAttribute('data-build-tab') === tab);
|
|
14003
|
+
});
|
|
14004
|
+
// Show/hide tab panes
|
|
14005
|
+
var workPane = document.getElementById('build-tab-workflows');
|
|
14006
|
+
var tplPane = document.getElementById('build-tab-templates');
|
|
14007
|
+
var headerStrip = document.getElementById('build-header-strip');
|
|
14008
|
+
if (tab === 'templates') {
|
|
14009
|
+
if (workPane) workPane.style.display = 'none';
|
|
14010
|
+
if (tplPane) tplPane.style.display = '';
|
|
14011
|
+
if (headerStrip) headerStrip.style.display = 'none';
|
|
14012
|
+
} else {
|
|
14013
|
+
if (workPane) workPane.style.display = 'flex';
|
|
14014
|
+
if (tplPane) tplPane.style.display = 'none';
|
|
14015
|
+
if (headerStrip) headerStrip.style.display = 'flex';
|
|
14016
|
+
// Map build-tab → builder-type so the canvas + chat reflect the tab.
|
|
14017
|
+
var typeSel = document.getElementById('builder-type');
|
|
14018
|
+
if (typeSel) {
|
|
14019
|
+
var nextType = tab === 'crons' ? 'cron' : tab === 'skills' ? 'skill' : 'workflow';
|
|
14020
|
+
if (typeSel.value !== nextType) {
|
|
14021
|
+
typeSel.value = nextType;
|
|
14022
|
+
if (typeof resetBuilder === 'function') resetBuilder();
|
|
14023
|
+
if (typeof updateBuilderMode === 'function') updateBuilderMode();
|
|
14024
|
+
} else if (typeof updateBuilderMode === 'function') {
|
|
14025
|
+
updateBuilderMode();
|
|
14026
|
+
}
|
|
14027
|
+
}
|
|
14028
|
+
// Focus chat input
|
|
14029
|
+
setTimeout(function() {
|
|
14030
|
+
var bi = document.getElementById('builder-input');
|
|
14031
|
+
if (bi) bi.focus();
|
|
14032
|
+
}, 60);
|
|
14033
|
+
}
|
|
14034
|
+
}
|
|
14035
|
+
|
|
14036
|
+
// ── Build templates: fork a starter pattern into a new workflow ─────
|
|
14037
|
+
async function forkBuildTemplate(templateId) {
|
|
14038
|
+
var templates = {
|
|
14039
|
+
'daily-news-digest': {
|
|
14040
|
+
name: 'Daily news digest',
|
|
14041
|
+
description: 'Morning news roundup pulled from configured RSS feeds.',
|
|
14042
|
+
schedule: '0 7 * * *',
|
|
14043
|
+
initialPrompt: 'Pull today headlines from configured RSS sources, summarize the top 5 stories, format as a brief digest, then send to my preferred channel.',
|
|
14044
|
+
},
|
|
14045
|
+
'lead-picker': {
|
|
14046
|
+
name: 'Lead picker (manual)',
|
|
14047
|
+
description: 'Search leads matching ICP, pick from canvas, push selected to Salesforce.',
|
|
14048
|
+
schedule: undefined,
|
|
14049
|
+
initialPrompt: 'Use Salesforce search to find prospects matching the ICP I describe. Show results so I can pick which to push as leads.',
|
|
14050
|
+
},
|
|
14051
|
+
'pr-review-queue': {
|
|
14052
|
+
name: 'PR review queue',
|
|
14053
|
+
description: 'Weekday morning summary of open PRs that need review.',
|
|
14054
|
+
schedule: '0 9 * * 1-5',
|
|
14055
|
+
initialPrompt: 'List open GitHub PRs that need my review, summarize each PRs risk level and key changes, send a digest to Slack.',
|
|
14056
|
+
},
|
|
14057
|
+
'email-triage': {
|
|
14058
|
+
name: 'Email triage',
|
|
14059
|
+
description: 'Morning unread-email triage with classified suggested actions.',
|
|
14060
|
+
schedule: '0 8 * * *',
|
|
14061
|
+
initialPrompt: 'List unread emails, classify each by intent (reply / archive / snooze / delegate), draft replies for the ones needing one, surface for my approval.',
|
|
14062
|
+
},
|
|
14063
|
+
'weekly-review': {
|
|
14064
|
+
name: 'Weekly review',
|
|
14065
|
+
description: 'Friday-evening review of the week and next-week priorities.',
|
|
14066
|
+
schedule: '0 18 * * 5',
|
|
14067
|
+
initialPrompt: 'Read this past 7 days of daily notes, summarize what got done, list whats still pending, suggest top 3 priorities for next week, append to today daily note as ## Weekly Review.',
|
|
14068
|
+
},
|
|
14069
|
+
'blank-workflow': {
|
|
14070
|
+
name: 'New workflow',
|
|
14071
|
+
description: '',
|
|
14072
|
+
schedule: undefined,
|
|
14073
|
+
initialPrompt: 'Describe what this workflow should do.',
|
|
14074
|
+
},
|
|
14075
|
+
};
|
|
14076
|
+
var tpl = templates[templateId];
|
|
14077
|
+
if (!tpl) { toast('Unknown template', 'error'); return; }
|
|
14078
|
+
var name = prompt('Name for the new workflow:', tpl.name);
|
|
14079
|
+
if (!name) return;
|
|
14080
|
+
try {
|
|
14081
|
+
var r = await apiJson('POST', '/api/builder/workflows', {
|
|
14082
|
+
name: name,
|
|
14083
|
+
description: tpl.description,
|
|
14084
|
+
schedule: tpl.schedule,
|
|
14085
|
+
initialPrompt: tpl.initialPrompt,
|
|
14086
|
+
});
|
|
14087
|
+
if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
|
|
14088
|
+
if (r && r.id) {
|
|
14089
|
+
switchBuildTab(tpl.schedule ? 'crons' : 'workflows');
|
|
14090
|
+
refreshBuilderCanvasPicker(tpl.schedule ? 'cron' : 'workflow');
|
|
14091
|
+
setTimeout(function() { openBuilderWorkflow(r.id); }, 200);
|
|
14092
|
+
toast('Forked template: ' + name, 'success');
|
|
14093
|
+
}
|
|
14094
|
+
} catch (err) {
|
|
14095
|
+
toast('Fork failed: ' + err, 'error');
|
|
14096
|
+
}
|
|
14097
|
+
}
|
|
14098
|
+
|
|
14099
|
+
// Cmd+K palette — keyboard-only quick navigation.
|
|
14100
|
+
function openCommandK() {
|
|
14101
|
+
var existing = document.getElementById('cmdk-overlay');
|
|
14102
|
+
if (existing) { existing.remove(); return; }
|
|
14103
|
+
var overlay = document.createElement('div');
|
|
14104
|
+
overlay.id = 'cmdk-overlay';
|
|
14105
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:2000;display:flex;align-items:flex-start;justify-content:center;padding-top:120px';
|
|
14106
|
+
overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
|
|
14107
|
+
var box = document.createElement('div');
|
|
14108
|
+
box.style.cssText = 'width:520px;max-width:92vw;background:var(--bg-secondary);border:1px solid var(--border);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,0.35);overflow:hidden';
|
|
14109
|
+
box.innerHTML =
|
|
14110
|
+
'<input id="cmdk-input" type="text" placeholder="Jump to… (home, build/workflows, team/goals, brain/health, settings/logs)" style="width:100%;padding:14px 18px;border:none;background:transparent;color:var(--text-primary);font-size:14px;outline:none;border-bottom:1px solid var(--border)">' +
|
|
14111
|
+
'<div id="cmdk-results" style="max-height:320px;overflow-y:auto"></div>';
|
|
14112
|
+
overlay.appendChild(box);
|
|
14113
|
+
document.body.appendChild(overlay);
|
|
14114
|
+
var input = document.getElementById('cmdk-input');
|
|
14115
|
+
var results = document.getElementById('cmdk-results');
|
|
14116
|
+
var entries = [
|
|
14117
|
+
{ kw: 'home chat', page: 'home', tab: 'chat', label: 'Home · Chat' },
|
|
14118
|
+
{ kw: 'home today plan', page: 'home', tab: 'today', label: 'Home · Today' },
|
|
14119
|
+
{ kw: 'home activity', page: 'home', tab: 'activity', label: 'Home · Activity' },
|
|
14120
|
+
{ kw: 'build workflows', page: 'build', tab: 'workflows', label: 'Build · Workflows' },
|
|
14121
|
+
{ kw: 'build crons', page: 'build', tab: 'crons', label: 'Build · Crons' },
|
|
14122
|
+
{ kw: 'build skills', page: 'build', tab: 'skills', label: 'Build · Skills' },
|
|
14123
|
+
{ kw: 'build templates', page: 'build', tab: 'templates', label: 'Build · Templates' },
|
|
14124
|
+
{ kw: 'team roster', page: 'team', tab: 'roster', label: 'Team · Roster' },
|
|
14125
|
+
{ kw: 'team activity', page: 'team', tab: 'activity', label: 'Team · Activity' },
|
|
14126
|
+
{ kw: 'team comms', page: 'team', tab: 'comms', label: 'Team · Comms' },
|
|
14127
|
+
{ kw: 'team goals', page: 'team', tab: 'goals', label: 'Team · Goals' },
|
|
14128
|
+
{ kw: 'brain memory', page: 'brain', tab: 'memory', label: 'Brain · Memory' },
|
|
14129
|
+
{ kw: 'brain knowledge', page: 'brain', tab: 'knowledge', label: 'Brain · Knowledge' },
|
|
14130
|
+
{ kw: 'brain ingestion', page: 'brain', tab: 'ingestion', label: 'Brain · Ingestion' },
|
|
14131
|
+
{ kw: 'brain health', page: 'brain', tab: 'health', label: 'Brain · Health' },
|
|
14132
|
+
{ kw: 'brain user model', page: 'brain', tab: 'user-model', label: 'Brain · User Model' },
|
|
14133
|
+
{ kw: 'settings channels', page: 'settings', tab: 'channels', label: 'Settings · Channels' },
|
|
14134
|
+
{ kw: 'settings integrations mcp', page: 'settings', tab: 'integrations', label: 'Settings · Integrations' },
|
|
14135
|
+
{ kw: 'settings projects', page: 'settings', tab: 'projects', label: 'Settings · Projects' },
|
|
14136
|
+
{ kw: 'settings security', page: 'settings', tab: 'security', label: 'Settings · Security' },
|
|
14137
|
+
{ kw: 'settings logs', page: 'settings', tab: 'logs', label: 'Settings · Logs' },
|
|
14138
|
+
{ kw: 'settings advanced', page: 'settings', tab: 'advanced', label: 'Settings · Advanced' },
|
|
14139
|
+
];
|
|
14140
|
+
function render() {
|
|
14141
|
+
var q = (input.value || '').toLowerCase().trim();
|
|
14142
|
+
var hits = q ? entries.filter(function(e) { return e.kw.indexOf(q) !== -1 || e.label.toLowerCase().indexOf(q) !== -1; }) : entries;
|
|
14143
|
+
results.innerHTML = hits.slice(0, 12).map(function(e, i) {
|
|
14144
|
+
return '<div class="cmdk-row" data-page="' + e.page + '" data-tab="' + e.tab + '" style="padding:10px 18px;font-size:13px;cursor:pointer;display:flex;justify-content:space-between;align-items:center' + (i === 0 ? ';background:var(--bg-hover)' : '') + '">' +
|
|
14145
|
+
'<span>' + e.label + '</span>' +
|
|
14146
|
+
'<span style="font-size:11px;color:var(--text-muted)">' + e.page + '/' + e.tab + '</span>' +
|
|
14147
|
+
'</div>';
|
|
14148
|
+
}).join('') || '<div style="padding:14px 18px;color:var(--text-muted);font-size:12px">No matches</div>';
|
|
14149
|
+
results.querySelectorAll('.cmdk-row').forEach(function(row) {
|
|
14150
|
+
row.onclick = function() {
|
|
14151
|
+
navigateTo(row.getAttribute('data-page'), { tab: row.getAttribute('data-tab') });
|
|
14152
|
+
overlay.remove();
|
|
14153
|
+
};
|
|
14154
|
+
});
|
|
14155
|
+
}
|
|
14156
|
+
input.oninput = render;
|
|
14157
|
+
input.onkeydown = function(e) {
|
|
14158
|
+
if (e.key === 'Escape') overlay.remove();
|
|
14159
|
+
if (e.key === 'Enter') {
|
|
14160
|
+
var first = results.querySelector('.cmdk-row');
|
|
14161
|
+
if (first) first.click();
|
|
14162
|
+
}
|
|
14163
|
+
};
|
|
14164
|
+
render();
|
|
14165
|
+
setTimeout(function() { input.focus(); }, 30);
|
|
14166
|
+
}
|
|
14167
|
+
|
|
14168
|
+
// Global keyboard shortcut for Cmd+K / Ctrl+K
|
|
14169
|
+
document.addEventListener('keydown', function(e) {
|
|
14170
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
14171
|
+
e.preventDefault();
|
|
14172
|
+
openCommandK();
|
|
14173
|
+
}
|
|
14174
|
+
});
|
|
14175
|
+
|
|
14176
|
+
async function refreshUnleashed() {
|
|
14177
|
+
var el = document.getElementById('panel-unleashed');
|
|
14178
|
+
if (!el) return;
|
|
14179
|
+
try {
|
|
14180
|
+
var r = await apiFetch('/api/unleashed');
|
|
14181
|
+
var d = await r.json();
|
|
14182
|
+
var tasks = d.tasks || [];
|
|
14183
|
+
var badge = document.getElementById('nav-unleashed-count');
|
|
14184
|
+
if (badge) {
|
|
14185
|
+
var running = tasks.filter(function(t) { return t.status === 'running'; }).length;
|
|
14186
|
+
if (running > 0) { badge.style.display = ''; badge.textContent = String(running); }
|
|
14187
|
+
else { badge.style.display = 'none'; }
|
|
14188
|
+
}
|
|
14189
|
+
if (tasks.length === 0) {
|
|
14190
|
+
el.innerHTML = '<div class="empty-state" style="padding:24px">No unleashed tasks. Schedule a cron job in <a href="#" onclick="navigateTo(\\'automations\\');return false">Scheduled Tasks</a> with mode = "unleashed" to start one.</div>';
|
|
14191
|
+
return;
|
|
14192
|
+
}
|
|
14193
|
+
var html = '<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr style="border-bottom:1px solid var(--border);background:var(--bg-tertiary)">';
|
|
14194
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Task</th>';
|
|
14195
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Status</th>';
|
|
14196
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Phase</th>';
|
|
14197
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Started</th>';
|
|
14198
|
+
html += '<th style="padding:8px 12px;text-align:right;color:var(--text-muted);font-weight:600">Action</th>';
|
|
14199
|
+
html += '</tr></thead><tbody>';
|
|
14200
|
+
tasks.forEach(function(t) {
|
|
14201
|
+
var statusColor = t.status === 'running' ? 'var(--green)' : t.status === 'completed' ? 'var(--blue)' : t.status === 'cancelled' ? 'var(--text-muted)' : 'var(--orange)';
|
|
14202
|
+
html += '<tr style="border-bottom:1px solid var(--border)">';
|
|
14203
|
+
html += '<td style="padding:10px 12px;font-weight:600">' + esc(t.name || '—') + '</td>';
|
|
14204
|
+
html += '<td style="padding:10px 12px"><span style="color:' + statusColor + ';font-size:11px;font-weight:600;text-transform:uppercase">' + esc(t.status || 'unknown') + '</span></td>';
|
|
14205
|
+
html += '<td style="padding:10px 12px;color:var(--text-muted)">' + esc(t.phase != null ? String(t.phase) : '—') + '</td>';
|
|
14206
|
+
html += '<td style="padding:10px 12px;color:var(--text-muted)">' + esc(t.startedAt || '—') + '</td>';
|
|
14207
|
+
html += '<td style="padding:10px 12px;text-align:right">';
|
|
14208
|
+
if (t.status === 'running') {
|
|
14209
|
+
html += '<button class="btn-sm" style="font-size:11px;color:#ef4444" onclick="cancelUnleashed(\\'' + esc(t.name) + '\\')">Cancel</button>';
|
|
14210
|
+
}
|
|
14211
|
+
html += '</td></tr>';
|
|
14212
|
+
});
|
|
14213
|
+
html += '</tbody></table>';
|
|
14214
|
+
el.innerHTML = html;
|
|
14215
|
+
} catch (ex) {
|
|
14216
|
+
el.innerHTML = '<div class="empty-state" style="padding:24px">Error: ' + esc(String(ex)) + '</div>';
|
|
14217
|
+
}
|
|
14218
|
+
}
|
|
14219
|
+
|
|
14220
|
+
async function cancelUnleashed(name) {
|
|
14221
|
+
if (!confirm('Cancel unleashed task "' + name + '"? It will stop at the next phase boundary.')) return;
|
|
14222
|
+
try {
|
|
14223
|
+
var r = await apiFetch('/api/unleashed/' + encodeURIComponent(name) + '/cancel', { method: 'POST' });
|
|
14224
|
+
var d = await r.json();
|
|
14225
|
+
if (d.ok) toast('Cancel requested', 'success');
|
|
14226
|
+
else toast(d.error || 'Cancel failed', 'error');
|
|
14227
|
+
refreshUnleashed();
|
|
14228
|
+
} catch (ex) {
|
|
14229
|
+
toast('Cancel failed: ' + ex, 'error');
|
|
13103
14230
|
}
|
|
13104
|
-
closeSidebar();
|
|
13105
14231
|
}
|
|
13106
14232
|
|
|
13107
14233
|
// Bind static nav items
|
|
@@ -13129,29 +14255,110 @@ function switchTab(group, tab) {
|
|
|
13129
14255
|
if (pane) pane.classList.add('active');
|
|
13130
14256
|
}
|
|
13131
14257
|
// Tab-specific refresh
|
|
13132
|
-
if (group === 'automations') {
|
|
13133
|
-
if (tab === 'scheduled') refreshCron();
|
|
13134
|
-
if (tab === 'broken') refreshBrokenJobs();
|
|
13135
|
-
if (tab === 'timers') refreshTimers();
|
|
13136
|
-
if (tab === 'self-improve') refreshSelfImprove();
|
|
13137
|
-
if (tab === 'workflows') refreshWorkflows();
|
|
13138
|
-
if (tab === 'analytics') refreshAdvisorAnalytics();
|
|
13139
|
-
}
|
|
13140
14258
|
if (group === 'intelligence') {
|
|
13141
14259
|
if (tab === 'graph') refreshGraph();
|
|
13142
14260
|
if (tab === 'memory') refreshMemory();
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
|
|
13146
|
-
|
|
13147
|
-
|
|
13148
|
-
if (tab === '
|
|
14261
|
+
if (tab === 'health') {
|
|
14262
|
+
if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
|
|
14263
|
+
if (typeof refreshClaims === 'function') refreshClaims();
|
|
14264
|
+
if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
|
|
14265
|
+
}
|
|
14266
|
+
if (tab === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
13149
14267
|
}
|
|
13150
14268
|
if (group === 'settings') {
|
|
13151
14269
|
if (tab === 'integrations') refreshSalesforce();
|
|
13152
|
-
if (tab === 'projects') refreshProjects();
|
|
13153
14270
|
if (tab === 'remote') refreshRemoteAccess();
|
|
13154
|
-
if (tab === '
|
|
14271
|
+
if (tab === 'security') refreshAuthSessions();
|
|
14272
|
+
if (tab === 'projects' && typeof refreshProjects === 'function') refreshProjects();
|
|
14273
|
+
if (tab === 'logs' && typeof refreshLogs === 'function') refreshLogs();
|
|
14274
|
+
if (tab === 'advanced' && typeof refreshDigestSettings === 'function') refreshDigestSettings();
|
|
14275
|
+
}
|
|
14276
|
+
if (group === 'team') {
|
|
14277
|
+
if (tab === 'activity' && typeof renderTeamStatus === 'function') renderTeamStatus();
|
|
14278
|
+
if (tab === 'goals' && typeof refreshGoalsProgress === 'function') refreshGoalsProgress();
|
|
14279
|
+
}
|
|
14280
|
+
}
|
|
14281
|
+
|
|
14282
|
+
async function refreshAuthSessions() {
|
|
14283
|
+
var el = document.getElementById('sessions-list');
|
|
14284
|
+
if (!el) return;
|
|
14285
|
+
try {
|
|
14286
|
+
var r = await fetch('/auth/sessions', { credentials: 'same-origin' });
|
|
14287
|
+
if (!r.ok) {
|
|
14288
|
+
if (r.status === 401) {
|
|
14289
|
+
el.innerHTML = '<div class="empty-state" style="padding:24px">Session-based view is only available for tunneled remote access. Localhost runs do not need login.</div>';
|
|
14290
|
+
return;
|
|
14291
|
+
}
|
|
14292
|
+
el.innerHTML = '<div class="empty-state" style="padding:24px">Failed to load sessions.</div>';
|
|
14293
|
+
return;
|
|
14294
|
+
}
|
|
14295
|
+
var d = await r.json();
|
|
14296
|
+
var rows = (d.sessions || []);
|
|
14297
|
+
if (rows.length === 0) {
|
|
14298
|
+
el.innerHTML = '<div class="empty-state" style="padding:24px">No active sessions.</div>';
|
|
14299
|
+
return;
|
|
14300
|
+
}
|
|
14301
|
+
function fmt(t) {
|
|
14302
|
+
if (!t) return '—';
|
|
14303
|
+
var diff = Date.now() - t;
|
|
14304
|
+
var s = Math.floor(diff / 1000);
|
|
14305
|
+
if (s < 60) return s + 's ago';
|
|
14306
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
14307
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
14308
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
14309
|
+
}
|
|
14310
|
+
function fmtExpires(t) {
|
|
14311
|
+
var diff = t - Date.now();
|
|
14312
|
+
if (diff <= 0) return 'expired';
|
|
14313
|
+
var d = Math.floor(diff / 86400000);
|
|
14314
|
+
if (d > 1) return 'in ' + d + ' days';
|
|
14315
|
+
var h = Math.floor(diff / 3600000);
|
|
14316
|
+
if (h > 1) return 'in ' + h + ' hours';
|
|
14317
|
+
return 'in ' + Math.floor(diff / 60000) + ' min';
|
|
14318
|
+
}
|
|
14319
|
+
var html = '<table style="width:100%;border-collapse:collapse;font-size:13px">';
|
|
14320
|
+
html += '<thead><tr style="border-bottom:1px solid var(--border);background:var(--bg-tertiary)">';
|
|
14321
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Device</th>';
|
|
14322
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Created</th>';
|
|
14323
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Last used</th>';
|
|
14324
|
+
html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Expires</th>';
|
|
14325
|
+
html += '<th style="padding:8px 12px;text-align:right;color:var(--text-muted);font-weight:600">Action</th>';
|
|
14326
|
+
html += '</tr></thead><tbody>';
|
|
14327
|
+
rows.forEach(function(s) {
|
|
14328
|
+
var ua = s.userAgent || 'Unknown device';
|
|
14329
|
+
var label = s.persistent ? '<span style="background:#f97316;color:#fff;padding:1px 6px;border-radius:3px;font-size:10px;margin-left:6px">Remember</span>' : '';
|
|
14330
|
+
var current = s.current ? '<span style="background:#10b981;color:#fff;padding:1px 6px;border-radius:3px;font-size:10px;margin-left:6px">This device</span>' : '';
|
|
14331
|
+
html += '<tr style="border-bottom:1px solid var(--border)">';
|
|
14332
|
+
html += '<td style="padding:10px 12px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(ua) + label + current + '</td>';
|
|
14333
|
+
html += '<td style="padding:10px 12px;color:var(--text-muted)">' + fmt(s.createdAt) + '</td>';
|
|
14334
|
+
html += '<td style="padding:10px 12px;color:var(--text-muted)">' + fmt(s.lastUsedAt) + '</td>';
|
|
14335
|
+
html += '<td style="padding:10px 12px;color:var(--text-muted)">' + fmtExpires(s.expiresAt) + '</td>';
|
|
14336
|
+
html += '<td style="padding:10px 12px;text-align:right">';
|
|
14337
|
+
html += '<button class="btn-sm" style="font-size:11px;color:#ef4444" onclick="revokeSession(\\'' + esc(s.id) + '\\',' + (s.current ? 'true' : 'false') + ')">Revoke</button>';
|
|
14338
|
+
html += '</td></tr>';
|
|
14339
|
+
});
|
|
14340
|
+
html += '</tbody></table>';
|
|
14341
|
+
el.innerHTML = html;
|
|
14342
|
+
} catch (ex) {
|
|
14343
|
+
el.innerHTML = '<div class="empty-state" style="padding:24px">Error: ' + esc(String(ex)) + '</div>';
|
|
14344
|
+
}
|
|
14345
|
+
}
|
|
14346
|
+
|
|
14347
|
+
async function revokeSession(id, isCurrent) {
|
|
14348
|
+
var msg = isCurrent
|
|
14349
|
+
? 'Revoke this session? You will be signed out immediately.'
|
|
14350
|
+
: 'Revoke this session?';
|
|
14351
|
+
if (!confirm(msg)) return;
|
|
14352
|
+
try {
|
|
14353
|
+
var r = await fetch('/auth/sessions/' + encodeURIComponent(id), { method: 'DELETE', credentials: 'same-origin' });
|
|
14354
|
+
if (!r.ok) { alert('Revoke failed'); return; }
|
|
14355
|
+
if (isCurrent) {
|
|
14356
|
+
window.location.href = '/';
|
|
14357
|
+
return;
|
|
14358
|
+
}
|
|
14359
|
+
refreshAuthSessions();
|
|
14360
|
+
} catch (ex) {
|
|
14361
|
+
alert('Revoke failed: ' + ex);
|
|
13155
14362
|
}
|
|
13156
14363
|
}
|
|
13157
14364
|
|
|
@@ -14135,7 +15342,7 @@ async function refreshSessions() {
|
|
|
14135
15342
|
var s = d[key];
|
|
14136
15343
|
var friendly = friendlySession(key);
|
|
14137
15344
|
var safeKey = esc(key).replace(/'/g, '');
|
|
14138
|
-
html += '<div class="session-card
|
|
15345
|
+
html += '<div class="session-card clickable-row" onclick="viewSession(\\x27' + encodeURIComponent(key) + '\\x27)">'
|
|
14139
15346
|
+ '<div class="session-card-header">'
|
|
14140
15347
|
+ '<span class="session-card-icon">' + friendly.icon + '</span>'
|
|
14141
15348
|
+ '<span class="session-card-name">' + esc(friendly.label) + '</span>'
|
|
@@ -14144,6 +15351,7 @@ async function refreshSessions() {
|
|
|
14144
15351
|
+ '<div class="session-card-meta">Last active: ' + timeAgo(s.timestamp) + '</div>'
|
|
14145
15352
|
+ '<div class="session-card-meta" style="font-family:monospace;font-size:10px">' + esc(key) + '</div>'
|
|
14146
15353
|
+ '<div class="session-card-actions">'
|
|
15354
|
+
+ '<button class="btn-sm btn-primary" onclick="event.stopPropagation();resumeSession(\\x27' + encodeURIComponent(key) + '\\x27)" title="Open in chat">Resume</button>'
|
|
14147
15355
|
+ '<button class="btn-danger btn-sm" onclick="event.stopPropagation();if(confirm(\\x27Clear session ' + safeKey + '?\\x27))apiPost(\\x27/api/sessions/' + encodeURIComponent(key) + '/clear\\x27)">Clear</button>'
|
|
14148
15356
|
+ '</div></div>';
|
|
14149
15357
|
}
|
|
@@ -14152,6 +15360,17 @@ async function refreshSessions() {
|
|
|
14152
15360
|
} catch(e) { }
|
|
14153
15361
|
}
|
|
14154
15362
|
|
|
15363
|
+
function resumeSession(encodedKey) {
|
|
15364
|
+
var key = decodeURIComponent(encodedKey);
|
|
15365
|
+
// The dashboard chat session is fixed at "dashboard:web" today, so we
|
|
15366
|
+
// navigate to chat and surface a hint. If/when chat gains multi-session
|
|
15367
|
+
// support, this is the place to bind the active session id.
|
|
15368
|
+
navigateTo('chat');
|
|
15369
|
+
var indicator = document.getElementById('chat-session-indicator');
|
|
15370
|
+
if (indicator) indicator.textContent = key;
|
|
15371
|
+
toast('Switched to ' + (friendlySession(key).label || key), 'info');
|
|
15372
|
+
}
|
|
15373
|
+
|
|
14155
15374
|
async function viewSession(encodedKey) {
|
|
14156
15375
|
var key = decodeURIComponent(encodedKey);
|
|
14157
15376
|
var panel = document.getElementById('panel-sessions');
|
|
@@ -16835,128 +18054,810 @@ async function editChunk(id) {
|
|
|
16835
18054
|
}
|
|
16836
18055
|
}
|
|
16837
18056
|
|
|
16838
|
-
async function saveEditChunk(id) {
|
|
16839
|
-
var contentEl = document.getElementById('edit-content-' + id);
|
|
16840
|
-
var sectionEl = document.getElementById('edit-section-' + id);
|
|
16841
|
-
if (!contentEl) return;
|
|
16842
|
-
try {
|
|
16843
|
-
var r = await apiFetch('/api/memory/chunks/' + id, {
|
|
16844
|
-
method: 'PUT',
|
|
16845
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16846
|
-
body: JSON.stringify({
|
|
16847
|
-
content: contentEl.value,
|
|
16848
|
-
section: sectionEl ? sectionEl.value : undefined,
|
|
16849
|
-
}),
|
|
16850
|
-
});
|
|
16851
|
-
var d = await r.json();
|
|
16852
|
-
if (d.ok) {
|
|
16853
|
-
toast('Chunk saved', 'success');
|
|
16854
|
-
runMemorySearch(); // re-render with updated data
|
|
16855
|
-
} else {
|
|
16856
|
-
toast('Save failed: ' + (d.error || 'unknown'), 'error');
|
|
16857
|
-
}
|
|
16858
|
-
} catch (e) { toast('Save failed: ' + String(e), 'error'); }
|
|
18057
|
+
async function saveEditChunk(id) {
|
|
18058
|
+
var contentEl = document.getElementById('edit-content-' + id);
|
|
18059
|
+
var sectionEl = document.getElementById('edit-section-' + id);
|
|
18060
|
+
if (!contentEl) return;
|
|
18061
|
+
try {
|
|
18062
|
+
var r = await apiFetch('/api/memory/chunks/' + id, {
|
|
18063
|
+
method: 'PUT',
|
|
18064
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18065
|
+
body: JSON.stringify({
|
|
18066
|
+
content: contentEl.value,
|
|
18067
|
+
section: sectionEl ? sectionEl.value : undefined,
|
|
18068
|
+
}),
|
|
18069
|
+
});
|
|
18070
|
+
var d = await r.json();
|
|
18071
|
+
if (d.ok) {
|
|
18072
|
+
toast('Chunk saved', 'success');
|
|
18073
|
+
runMemorySearch(); // re-render with updated data
|
|
18074
|
+
} else {
|
|
18075
|
+
toast('Save failed: ' + (d.error || 'unknown'), 'error');
|
|
18076
|
+
}
|
|
18077
|
+
} catch (e) { toast('Save failed: ' + String(e), 'error'); }
|
|
18078
|
+
}
|
|
18079
|
+
|
|
18080
|
+
function cancelEditChunk(id) {
|
|
18081
|
+
// Easiest: re-run the search to restore original rendering
|
|
18082
|
+
runMemorySearch();
|
|
18083
|
+
}
|
|
18084
|
+
|
|
18085
|
+
async function togglePinChunk(id, pinned) {
|
|
18086
|
+
try {
|
|
18087
|
+
var r = await apiFetch('/api/memory/chunks/' + id + '/pin', {
|
|
18088
|
+
method: 'POST',
|
|
18089
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18090
|
+
body: JSON.stringify({ pinned: pinned }),
|
|
18091
|
+
});
|
|
18092
|
+
var d = await r.json();
|
|
18093
|
+
if (d.ok) {
|
|
18094
|
+
toast(pinned ? 'Pinned' : 'Unpinned', 'success');
|
|
18095
|
+
runMemorySearch();
|
|
18096
|
+
} else {
|
|
18097
|
+
toast('Pin failed: ' + (d.error || 'unknown'), 'error');
|
|
18098
|
+
}
|
|
18099
|
+
} catch (e) { toast('Pin failed: ' + String(e), 'error'); }
|
|
18100
|
+
}
|
|
18101
|
+
|
|
18102
|
+
async function deleteChunk(id) {
|
|
18103
|
+
if (!confirm('Delete this chunk? It will be excluded from search and retrieval. (Soft-delete — recoverable via the database.)')) return;
|
|
18104
|
+
try {
|
|
18105
|
+
var r = await apiFetch('/api/memory/chunks/' + id, { method: 'DELETE' });
|
|
18106
|
+
var d = await r.json();
|
|
18107
|
+
if (d.ok) {
|
|
18108
|
+
toast(d.removed ? 'Chunk deleted' : 'Chunk was already deleted', 'success');
|
|
18109
|
+
runMemorySearch();
|
|
18110
|
+
} else {
|
|
18111
|
+
toast('Delete failed: ' + (d.error || 'unknown'), 'error');
|
|
18112
|
+
}
|
|
18113
|
+
} catch (e) { toast('Delete failed: ' + String(e), 'error'); }
|
|
18114
|
+
}
|
|
18115
|
+
|
|
18116
|
+
// ── Profile Switching ─────────────────────
|
|
18117
|
+
async function loadProfiles() {
|
|
18118
|
+
try {
|
|
18119
|
+
var r = await apiFetch('/api/profiles');
|
|
18120
|
+
var d = await r.json();
|
|
18121
|
+
var sel = document.getElementById('chat-profile-select');
|
|
18122
|
+
sel.innerHTML = '<option value="">Default</option>';
|
|
18123
|
+
for (var p of (d.profiles || [])) {
|
|
18124
|
+
var opt = document.createElement('option');
|
|
18125
|
+
opt.value = p.slug;
|
|
18126
|
+
opt.textContent = p.name + (p.description ? ' — ' + p.description : '');
|
|
18127
|
+
if (p.slug === d.active) opt.selected = true;
|
|
18128
|
+
sel.appendChild(opt);
|
|
18129
|
+
}
|
|
18130
|
+
} catch(e) { /* profiles are optional */ }
|
|
18131
|
+
}
|
|
18132
|
+
|
|
18133
|
+
async function switchProfile(slug) {
|
|
18134
|
+
try {
|
|
18135
|
+
await apiJson('POST', '/api/profiles/switch', { slug: slug || null });
|
|
18136
|
+
// Clear chat display since session was reset
|
|
18137
|
+
var container = document.getElementById('chat-messages');
|
|
18138
|
+
container.innerHTML = '<div class="empty-state"><p style="margin-bottom:14px;color:var(--text-muted)">Profile switched' + (slug ? ' to <strong>' + esc(slug) + '</strong>' : '') + '. Session cleared.</p></div>';
|
|
18139
|
+
toast(slug ? 'Switched to ' + slug : 'Profile cleared', 'success');
|
|
18140
|
+
} catch(e) { toast('Failed to switch profile: ' + e, 'error'); }
|
|
18141
|
+
}
|
|
18142
|
+
|
|
18143
|
+
// ── Skill Studio — opens builder in skill-focused mode ──────────
|
|
18144
|
+
|
|
18145
|
+
function openSkillStudio() {
|
|
18146
|
+
// Pre-set type to skill before navigating
|
|
18147
|
+
var typeSelect = document.getElementById('builder-type');
|
|
18148
|
+
if (typeSelect) typeSelect.value = 'skill';
|
|
18149
|
+
navigateTo('builder');
|
|
18150
|
+
// Update the UI for skill mode
|
|
18151
|
+
updateBuilderMode();
|
|
18152
|
+
// Show skill-specific empty state
|
|
18153
|
+
var emptyState = document.getElementById('builder-empty-state');
|
|
18154
|
+
if (emptyState && !builderArtifact) {
|
|
18155
|
+
emptyState.innerHTML = '<p style="color:var(--text-muted);margin-bottom:8px;font-size:15px;font-weight:600">Skill Studio</p>'
|
|
18156
|
+
+ '<p style="color:var(--text-muted);margin-bottom:16px;font-size:13px">Teach a new skill by describing it, attach reference files, link tools, test it, then save.</p>'
|
|
18157
|
+
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">'
|
|
18158
|
+
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for deploying to production — run tests, build, push, verify\\x27)">Deploy to prod</button>'
|
|
18159
|
+
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for writing a weekly status report from git commits and calendar\\x27)">Weekly status report</button>'
|
|
18160
|
+
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for onboarding a new team member — set up accounts, send welcome email\\x27)">Onboard team member</button>'
|
|
18161
|
+
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for researching a company before a sales call\\x27)">Company research</button>'
|
|
18162
|
+
+ '</div>';
|
|
18163
|
+
}
|
|
18164
|
+
}
|
|
18165
|
+
|
|
18166
|
+
function updateBuilderMode() {
|
|
18167
|
+
var type = (document.getElementById('builder-type') || {}).value || 'skill';
|
|
18168
|
+
var title = document.getElementById('builder-page-title');
|
|
18169
|
+
var drawer = document.getElementById('builder-skills-drawer');
|
|
18170
|
+
var preview = document.getElementById('builder-preview');
|
|
18171
|
+
var canvasHost = document.getElementById('builder-canvas-host');
|
|
18172
|
+
var picker = document.getElementById('builder-canvas-picker');
|
|
18173
|
+
var validateBtn = document.getElementById('builder-canvas-validate-btn');
|
|
18174
|
+
var dryrunBtn = document.getElementById('builder-canvas-dryrun-btn');
|
|
18175
|
+
var paneTitle = document.getElementById('builder-right-pane-title');
|
|
18176
|
+
|
|
18177
|
+
if (type === 'skill') {
|
|
18178
|
+
if (title) title.textContent = 'Skill Studio';
|
|
18179
|
+
if (drawer) { drawer.style.display = ''; refreshBuilderSkills(); }
|
|
18180
|
+
document.getElementById('builder-input').placeholder = 'Describe the skill you want to teach...';
|
|
18181
|
+
} else {
|
|
18182
|
+
if (title) title.textContent = 'Builder';
|
|
18183
|
+
if (drawer) drawer.style.display = 'none';
|
|
18184
|
+
document.getElementById('builder-input').placeholder = 'Describe what you want to build...';
|
|
18185
|
+
}
|
|
18186
|
+
|
|
18187
|
+
// Canvas mode for cron + workflow types
|
|
18188
|
+
var canvasMode = (type === 'cron' || type === 'workflow');
|
|
18189
|
+
var testBtn = document.getElementById('builder-canvas-test-btn');
|
|
18190
|
+
if (canvasMode) {
|
|
18191
|
+
if (preview) preview.style.display = 'none';
|
|
18192
|
+
if (canvasHost) canvasHost.style.display = 'flex';
|
|
18193
|
+
if (picker) picker.style.display = '';
|
|
18194
|
+
if (validateBtn) validateBtn.style.display = '';
|
|
18195
|
+
if (dryrunBtn) dryrunBtn.style.display = '';
|
|
18196
|
+
if (testBtn) testBtn.style.display = '';
|
|
18197
|
+
if (paneTitle) paneTitle.textContent = (type === 'cron' ? 'Crons' : 'Workflows');
|
|
18198
|
+
refreshBuilderCanvasPicker(type);
|
|
18199
|
+
} else {
|
|
18200
|
+
if (preview) preview.style.display = '';
|
|
18201
|
+
if (canvasHost) canvasHost.style.display = 'none';
|
|
18202
|
+
if (picker) picker.style.display = 'none';
|
|
18203
|
+
if (validateBtn) validateBtn.style.display = 'none';
|
|
18204
|
+
if (dryrunBtn) dryrunBtn.style.display = 'none';
|
|
18205
|
+
if (testBtn) testBtn.style.display = 'none';
|
|
18206
|
+
if (paneTitle) paneTitle.textContent = 'Live Preview';
|
|
18207
|
+
closeBuilderCanvas();
|
|
18208
|
+
}
|
|
18209
|
+
}
|
|
18210
|
+
|
|
18211
|
+
// ── Builder visual canvas (Phase 1: read-only, agent edits via MCP tools) ──
|
|
18212
|
+
|
|
18213
|
+
var _builderCanvasEditor = null;
|
|
18214
|
+
var _builderCanvasOpenId = null;
|
|
18215
|
+
var _builderCanvasLastWorkflow = null;
|
|
18216
|
+
var _builderDrawflowLoading = null;
|
|
18217
|
+
|
|
18218
|
+
function _ensureDrawflowLoaded() {
|
|
18219
|
+
if (window.Drawflow) return Promise.resolve();
|
|
18220
|
+
if (_builderDrawflowLoading) return _builderDrawflowLoading;
|
|
18221
|
+
_builderDrawflowLoading = new Promise(function(resolve, reject) {
|
|
18222
|
+
var s = document.createElement('script');
|
|
18223
|
+
s.src = '/static/drawflow.min.js';
|
|
18224
|
+
s.onload = function() { resolve(); };
|
|
18225
|
+
s.onerror = function() { reject(new Error('Failed to load Drawflow')); };
|
|
18226
|
+
document.head.appendChild(s);
|
|
18227
|
+
});
|
|
18228
|
+
return _builderDrawflowLoading;
|
|
18229
|
+
}
|
|
18230
|
+
|
|
18231
|
+
async function refreshBuilderCanvasPicker(type) {
|
|
18232
|
+
var picker = document.getElementById('builder-canvas-picker');
|
|
18233
|
+
if (!picker) return;
|
|
18234
|
+
try {
|
|
18235
|
+
var r = await apiFetch('/api/builder/workflows');
|
|
18236
|
+
var d = await r.json();
|
|
18237
|
+
var items = (d.workflows || []).filter(function(w) { return w.origin === type; });
|
|
18238
|
+
var opts = '<option value="">' + (items.length ? '— pick a ' + type + ' —' : '(none yet)') + '</option>';
|
|
18239
|
+
for (var i = 0; i < items.length; i++) {
|
|
18240
|
+
var w = items[i];
|
|
18241
|
+
var lbl = w.name + (w.schedule ? ' · ' + w.schedule : '') + (w.enabled ? '' : ' · off');
|
|
18242
|
+
opts += '<option value="' + esc(w.id) + '">' + esc(lbl) + '</option>';
|
|
18243
|
+
}
|
|
18244
|
+
picker.innerHTML = opts;
|
|
18245
|
+
if (_builderCanvasOpenId) picker.value = _builderCanvasOpenId;
|
|
18246
|
+
} catch (err) {
|
|
18247
|
+
picker.innerHTML = '<option value="">(failed to load)</option>';
|
|
18248
|
+
}
|
|
18249
|
+
}
|
|
18250
|
+
|
|
18251
|
+
async function openBuilderWorkflow(id) {
|
|
18252
|
+
if (!id) { closeBuilderCanvas(); return; }
|
|
18253
|
+
try {
|
|
18254
|
+
await _ensureDrawflowLoaded();
|
|
18255
|
+
var r = await apiFetch('/api/builder/workflows/' + encodeURIComponent(id));
|
|
18256
|
+
if (!r.ok) {
|
|
18257
|
+
var msg = await r.json().catch(function() { return {}; });
|
|
18258
|
+
toast('Failed to open: ' + (msg.error || r.status), 'error');
|
|
18259
|
+
return;
|
|
18260
|
+
}
|
|
18261
|
+
var d = await r.json();
|
|
18262
|
+
_builderCanvasOpenId = id;
|
|
18263
|
+
_builderCanvasLastWorkflow = d.workflow;
|
|
18264
|
+
_renderBuilderCanvas(d.drawflow);
|
|
18265
|
+
|
|
18266
|
+
var idEl = document.getElementById('builder-canvas-id');
|
|
18267
|
+
if (idEl) idEl.textContent = id;
|
|
18268
|
+
|
|
18269
|
+
var banner = document.getElementById('builder-canvas-banner');
|
|
18270
|
+
if (banner) {
|
|
18271
|
+
var issues = (d.validation && d.validation.issues) || [];
|
|
18272
|
+
var errors = issues.filter(function(i) { return i.severity === 'error'; });
|
|
18273
|
+
if (errors.length) {
|
|
18274
|
+
banner.style.display = '';
|
|
18275
|
+
banner.style.background = 'rgba(255,80,80,0.12)';
|
|
18276
|
+
banner.style.color = 'var(--red)';
|
|
18277
|
+
banner.textContent = errors.length + ' validation error' + (errors.length === 1 ? '' : 's') + ' — open Validate for details';
|
|
18278
|
+
} else {
|
|
18279
|
+
banner.style.display = 'none';
|
|
18280
|
+
}
|
|
18281
|
+
}
|
|
18282
|
+
} catch (err) {
|
|
18283
|
+
toast('Canvas error: ' + err, 'error');
|
|
18284
|
+
}
|
|
18285
|
+
}
|
|
18286
|
+
|
|
18287
|
+
function _renderBuilderCanvas(drawflowData) {
|
|
18288
|
+
var host = document.getElementById('builder-canvas');
|
|
18289
|
+
if (!host) return;
|
|
18290
|
+
// Tear down previous editor
|
|
18291
|
+
if (_builderCanvasEditor) {
|
|
18292
|
+
try { _builderCanvasEditor.clear(); } catch (e) { /* ignore */ }
|
|
18293
|
+
host.innerHTML = '';
|
|
18294
|
+
}
|
|
18295
|
+
var editor = new window.Drawflow(host);
|
|
18296
|
+
editor.reroute = true;
|
|
18297
|
+
editor.editor_mode = 'edit';
|
|
18298
|
+
editor.start();
|
|
18299
|
+
try {
|
|
18300
|
+
editor.import(drawflowData || { drawflow: { Home: { data: {} } } });
|
|
18301
|
+
_decorateBuilderNodes(host, _builderCanvasLastWorkflow);
|
|
18302
|
+
} catch (err) {
|
|
18303
|
+
host.innerHTML = '<div style="padding:24px;color:var(--red)">Failed to render canvas: ' + esc(String(err)) + '</div>';
|
|
18304
|
+
}
|
|
18305
|
+
_builderCanvasEditor = editor;
|
|
18306
|
+
_bindBuilderCanvasEvents(editor);
|
|
18307
|
+
}
|
|
18308
|
+
|
|
18309
|
+
var _builderSaveTimer = null;
|
|
18310
|
+
var _builderSavePending = false;
|
|
18311
|
+
var _builderRecentSaveTokens = new Set();
|
|
18312
|
+
|
|
18313
|
+
function _bindBuilderCanvasEvents(editor) {
|
|
18314
|
+
// Drawflow event names: nodeMoved, connectionCreated, connectionRemoved,
|
|
18315
|
+
// nodeDataChanged, nodeRemoved.
|
|
18316
|
+
['nodeMoved', 'connectionCreated', 'connectionRemoved', 'nodeDataChanged', 'nodeRemoved'].forEach(function(evt) {
|
|
18317
|
+
try {
|
|
18318
|
+
editor.on(evt, function() {
|
|
18319
|
+
if (evt === 'nodeMoved') _scheduleBuilderSave(800);
|
|
18320
|
+
else _scheduleBuilderSave(300);
|
|
18321
|
+
});
|
|
18322
|
+
} catch (e) { /* drawflow always exposes .on, but be safe */ }
|
|
18323
|
+
});
|
|
18324
|
+
try {
|
|
18325
|
+
editor.on('nodeSelected', function(nodeId) { _openNodeConfigPanel(nodeId); });
|
|
18326
|
+
editor.on('nodeUnselected', function() { _closeNodeConfigPanel(); });
|
|
18327
|
+
} catch (e) { /* */ }
|
|
18328
|
+
}
|
|
18329
|
+
|
|
18330
|
+
function _scheduleBuilderSave(delay) {
|
|
18331
|
+
if (_builderSaveTimer) clearTimeout(_builderSaveTimer);
|
|
18332
|
+
_builderSaveTimer = setTimeout(function() { _flushBuilderSave(); }, delay || 500);
|
|
18333
|
+
}
|
|
18334
|
+
|
|
18335
|
+
async function _flushBuilderSave() {
|
|
18336
|
+
if (!_builderCanvasEditor || !_builderCanvasOpenId) return;
|
|
18337
|
+
if (_builderSavePending) {
|
|
18338
|
+
_scheduleBuilderSave(400);
|
|
18339
|
+
return;
|
|
18340
|
+
}
|
|
18341
|
+
_builderSavePending = true;
|
|
18342
|
+
_setBuilderSaveStatus('saving');
|
|
18343
|
+
var saveToken = 'sv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
|
|
18344
|
+
_builderRecentSaveTokens.add(saveToken);
|
|
18345
|
+
setTimeout(function() { _builderRecentSaveTokens.delete(saveToken); }, 5000);
|
|
18346
|
+
try {
|
|
18347
|
+
var data = _builderCanvasEditor.export();
|
|
18348
|
+
var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/save-from-drawflow', { drawflow: data, saveToken: saveToken });
|
|
18349
|
+
if (r.error) {
|
|
18350
|
+
if (r.validation && r.validation.issues) {
|
|
18351
|
+
_setBuilderSaveStatus('error', r.validation.issues.length + ' validation error' + (r.validation.issues.length === 1 ? '' : 's'));
|
|
18352
|
+
} else {
|
|
18353
|
+
_setBuilderSaveStatus('error', r.error);
|
|
18354
|
+
}
|
|
18355
|
+
} else {
|
|
18356
|
+
_setBuilderSaveStatus('saved');
|
|
18357
|
+
// Refresh banner from validation if warnings
|
|
18358
|
+
_updateBuilderBannerFromValidation(r.validation);
|
|
18359
|
+
}
|
|
18360
|
+
} catch (err) {
|
|
18361
|
+
_setBuilderSaveStatus('error', String(err));
|
|
18362
|
+
} finally {
|
|
18363
|
+
_builderSavePending = false;
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
|
|
18367
|
+
function _setBuilderSaveStatus(state, detail) {
|
|
18368
|
+
var el = document.getElementById('builder-canvas-status');
|
|
18369
|
+
if (!el) return;
|
|
18370
|
+
if (state === 'saving') { el.textContent = 'Saving…'; el.style.color = 'var(--text-muted)'; }
|
|
18371
|
+
else if (state === 'saved') { el.textContent = 'Saved'; el.style.color = 'var(--green, #4caf50)'; setTimeout(function() { if (el.textContent === 'Saved') el.textContent = ''; }, 1500); }
|
|
18372
|
+
else if (state === 'error') { el.textContent = 'Save error: ' + (detail || ''); el.style.color = 'var(--red)'; }
|
|
18373
|
+
}
|
|
18374
|
+
|
|
18375
|
+
function _updateBuilderBannerFromValidation(v) {
|
|
18376
|
+
var banner = document.getElementById('builder-canvas-banner');
|
|
18377
|
+
if (!banner || !v) return;
|
|
18378
|
+
var issues = v.issues || [];
|
|
18379
|
+
var errors = issues.filter(function(i) { return i.severity === 'error'; });
|
|
18380
|
+
var warnings = issues.filter(function(i) { return i.severity === 'warning'; });
|
|
18381
|
+
if (errors.length) {
|
|
18382
|
+
banner.style.display = '';
|
|
18383
|
+
banner.style.background = 'rgba(255,80,80,0.12)';
|
|
18384
|
+
banner.style.color = 'var(--red)';
|
|
18385
|
+
banner.textContent = errors.length + ' validation error' + (errors.length === 1 ? '' : 's') + ' — open Validate for details';
|
|
18386
|
+
} else if (warnings.length) {
|
|
18387
|
+
banner.style.display = '';
|
|
18388
|
+
banner.style.background = 'rgba(240,180,0,0.12)';
|
|
18389
|
+
banner.style.color = '#a37100';
|
|
18390
|
+
banner.textContent = warnings.length + ' warning' + (warnings.length === 1 ? '' : 's') + ' — Validate for details';
|
|
18391
|
+
} else {
|
|
18392
|
+
banner.style.display = 'none';
|
|
18393
|
+
}
|
|
18394
|
+
}
|
|
18395
|
+
|
|
18396
|
+
function _decorateBuilderNodes(host, wf) {
|
|
18397
|
+
if (!wf) return;
|
|
18398
|
+
var byStepId = {};
|
|
18399
|
+
for (var i = 0; i < wf.steps.length; i++) byStepId[wf.steps[i].id] = wf.steps[i];
|
|
18400
|
+
// Drawflow renders blank node bodies by default — overlay our own content.
|
|
18401
|
+
var nodes = host.querySelectorAll('.drawflow-node');
|
|
18402
|
+
nodes.forEach(function(nodeEl) {
|
|
18403
|
+
var contentEl = nodeEl.querySelector('.drawflow_content_node');
|
|
18404
|
+
if (!contentEl || contentEl.dataset._decorated) return;
|
|
18405
|
+
contentEl.dataset._decorated = '1';
|
|
18406
|
+
var dataAttr = nodeEl.querySelector('input[df-stepId]');
|
|
18407
|
+
var stepId = dataAttr ? dataAttr.value : null;
|
|
18408
|
+
// Drawflow doesn't auto-bind without templates — derive from class instead
|
|
18409
|
+
var classNames = (nodeEl.className || '').split(/\s+/).filter(function(c) { return c.indexOf('cl-node-') === 0; });
|
|
18410
|
+
var kind = classNames.length ? classNames[0].replace('cl-node-', '') : 'prompt';
|
|
18411
|
+
var title = '';
|
|
18412
|
+
var body = '';
|
|
18413
|
+
var matchedStep = null;
|
|
18414
|
+
for (var j = 0; j < wf.steps.length; j++) {
|
|
18415
|
+
var s = wf.steps[j];
|
|
18416
|
+
if ((s.kind || 'prompt') === kind) { matchedStep = s; break; }
|
|
18417
|
+
}
|
|
18418
|
+
if (!matchedStep) {
|
|
18419
|
+
// Best-effort: use first step
|
|
18420
|
+
matchedStep = wf.steps[0];
|
|
18421
|
+
}
|
|
18422
|
+
title = (matchedStep ? matchedStep.id : '?');
|
|
18423
|
+
body = _summarizeStep(matchedStep, kind);
|
|
18424
|
+
contentEl.innerHTML =
|
|
18425
|
+
'<div style="font-weight:600;font-size:12px;margin-bottom:4px;color:#fff;text-transform:lowercase">' + esc(kind) + ' · ' + esc(title) + '</div>' +
|
|
18426
|
+
'<div style="font-size:11px;color:rgba(255,255,255,0.85);line-height:1.35;max-height:80px;overflow:hidden">' + esc(body) + '</div>';
|
|
18427
|
+
});
|
|
18428
|
+
}
|
|
18429
|
+
|
|
18430
|
+
function _summarizeStep(step, kind) {
|
|
18431
|
+
if (!step) return '';
|
|
18432
|
+
if (kind === 'mcp' && step.mcp) return step.mcp.server + '.' + step.mcp.tool;
|
|
18433
|
+
if (kind === 'channel' && step.channel) return step.channel.channel + ' → ' + step.channel.target;
|
|
18434
|
+
if (kind === 'transform' && step.transform) return step.transform.expression;
|
|
18435
|
+
if (kind === 'conditional' && step.conditional) return 'if ' + step.conditional.condition;
|
|
18436
|
+
if (kind === 'loop' && step.loop) return 'for each ' + step.loop.items;
|
|
18437
|
+
return (step.prompt || '').slice(0, 200);
|
|
18438
|
+
}
|
|
18439
|
+
|
|
18440
|
+
function closeBuilderCanvas() {
|
|
18441
|
+
_builderCanvasOpenId = null;
|
|
18442
|
+
_builderCanvasLastWorkflow = null;
|
|
18443
|
+
if (_builderCanvasEditor) {
|
|
18444
|
+
try { _builderCanvasEditor.clear(); } catch (e) { /* ignore */ }
|
|
18445
|
+
_builderCanvasEditor = null;
|
|
18446
|
+
}
|
|
18447
|
+
var host = document.getElementById('builder-canvas');
|
|
18448
|
+
if (host) host.innerHTML = '';
|
|
18449
|
+
var idEl = document.getElementById('builder-canvas-id');
|
|
18450
|
+
if (idEl) idEl.textContent = '';
|
|
18451
|
+
var banner = document.getElementById('builder-canvas-banner');
|
|
18452
|
+
if (banner) banner.style.display = 'none';
|
|
18453
|
+
}
|
|
18454
|
+
|
|
18455
|
+
async function validateBuilderCanvas() {
|
|
18456
|
+
if (!_builderCanvasOpenId) { toast('Open a workflow first', 'info'); return; }
|
|
18457
|
+
try {
|
|
18458
|
+
var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/validate', {});
|
|
18459
|
+
if (!r.issues || r.issues.length === 0) { toast('OK — no issues', 'success'); return; }
|
|
18460
|
+
var lines = r.issues.map(function(i) { return '[' + i.severity + '] ' + (i.stepId ? '(' + i.stepId + ') ' : '') + i.message; });
|
|
18461
|
+
alert('Validation:\\n\\n' + lines.join('\\n'));
|
|
18462
|
+
} catch (err) { toast('Validate failed: ' + err, 'error'); }
|
|
18463
|
+
}
|
|
18464
|
+
|
|
18465
|
+
async function dryRunBuilderCanvas() {
|
|
18466
|
+
if (!_builderCanvasOpenId) { toast('Open a workflow first', 'info'); return; }
|
|
18467
|
+
try {
|
|
18468
|
+
var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/dry-run', {});
|
|
18469
|
+
var lines = [];
|
|
18470
|
+
lines.push(r.ok ? 'DRY RUN — would execute:' : 'DRY RUN (validation issues found):');
|
|
18471
|
+
lines.push('');
|
|
18472
|
+
for (var i = 0; i < r.steps.length; i++) {
|
|
18473
|
+
var s = r.steps[i];
|
|
18474
|
+
lines.push('[wave ' + s.wave + '] ' + s.description);
|
|
18475
|
+
for (var k = 0; k < s.warnings.length; k++) lines.push(' ⚠ ' + s.warnings[k]);
|
|
18476
|
+
}
|
|
18477
|
+
if (r.estimatedTokens) lines.push('', '~' + r.estimatedTokens.total.toLocaleString() + ' tokens estimate (' + r.estimatedTokens.promptSteps + ' prompt step' + (r.estimatedTokens.promptSteps === 1 ? '' : 's') + ')');
|
|
18478
|
+
if (r.notes && r.notes.length) { lines.push(''); for (var n = 0; n < r.notes.length; n++) lines.push(r.notes[n]); }
|
|
18479
|
+
alert(lines.join('\\n'));
|
|
18480
|
+
} catch (err) { toast('Dry-run failed: ' + err, 'error'); }
|
|
18481
|
+
}
|
|
18482
|
+
|
|
18483
|
+
function _handleBuilderEvent(evt) {
|
|
18484
|
+
if (!evt || !evt.workflowId) return;
|
|
18485
|
+
// Run events are routed to the test-run handler.
|
|
18486
|
+
if (evt.type && evt.type.indexOf && evt.type.indexOf('run:') === 0) {
|
|
18487
|
+
_onRunEvent(evt);
|
|
18488
|
+
return;
|
|
18489
|
+
}
|
|
18490
|
+
// Suppress echoes of our own saves: server reflects the saveToken back.
|
|
18491
|
+
var token = evt.payload && evt.payload.saveToken;
|
|
18492
|
+
if (token && _builderRecentSaveTokens.has(token)) {
|
|
18493
|
+
_builderRecentSaveTokens.delete(token);
|
|
18494
|
+
return;
|
|
18495
|
+
}
|
|
18496
|
+
// Re-render if event is for the open workflow.
|
|
18497
|
+
if (evt.workflowId === _builderCanvasOpenId) {
|
|
18498
|
+
if (evt.type === 'workflow:deleted') { closeBuilderCanvas(); refreshBuilderCanvasPicker(document.getElementById('builder-type').value); return; }
|
|
18499
|
+
openBuilderWorkflow(_builderCanvasOpenId);
|
|
18500
|
+
}
|
|
18501
|
+
// Refresh picker when list changes
|
|
18502
|
+
if (evt.type === 'workflow:created' || evt.type === 'workflow:deleted' || evt.type === 'workflow:renamed') {
|
|
18503
|
+
refreshBuilderCanvasPicker(document.getElementById('builder-type').value);
|
|
18504
|
+
}
|
|
18505
|
+
}
|
|
18506
|
+
|
|
18507
|
+
// ── Node palette (Phase 2b) ─────────────────────────────────────
|
|
18508
|
+
|
|
18509
|
+
var _builderPaletteOpen = false;
|
|
18510
|
+
|
|
18511
|
+
function toggleBuilderPalette() {
|
|
18512
|
+
var pop = document.getElementById('builder-palette-pop');
|
|
18513
|
+
if (!pop) return;
|
|
18514
|
+
_builderPaletteOpen = !_builderPaletteOpen;
|
|
18515
|
+
pop.style.display = _builderPaletteOpen ? '' : 'none';
|
|
18516
|
+
}
|
|
18517
|
+
|
|
18518
|
+
function _builderAddNodeOfKind(kind) {
|
|
18519
|
+
if (!_builderCanvasEditor) return;
|
|
18520
|
+
toggleBuilderPalette();
|
|
18521
|
+
var canvas = document.getElementById('builder-canvas');
|
|
18522
|
+
if (!canvas) return;
|
|
18523
|
+
var rect = canvas.getBoundingClientRect();
|
|
18524
|
+
var posX = (rect.width / 2) - 100;
|
|
18525
|
+
var posY = (rect.height / 2) - 40;
|
|
18526
|
+
var stepId = _generateUniqueStepId(kind);
|
|
18527
|
+
var data = _defaultDataForKind(kind, stepId);
|
|
18528
|
+
var className = 'cl-node cl-node-' + kind;
|
|
18529
|
+
var nodeId = _builderCanvasEditor.addNode(_nodeNameForKind(kind), 1, 1, posX, posY, className, data, '');
|
|
18530
|
+
// Drawflow needs a tick before we can decorate
|
|
18531
|
+
setTimeout(function() {
|
|
18532
|
+
_decorateBuilderNodeById(nodeId, kind, data);
|
|
18533
|
+
_scheduleBuilderSave(200);
|
|
18534
|
+
}, 50);
|
|
18535
|
+
}
|
|
18536
|
+
|
|
18537
|
+
function _nodeNameForKind(kind) {
|
|
18538
|
+
switch (kind) {
|
|
18539
|
+
case 'mcp': return 'MCP Tool';
|
|
18540
|
+
case 'channel': return 'Channel';
|
|
18541
|
+
case 'transform': return 'Transform';
|
|
18542
|
+
case 'conditional': return 'Conditional';
|
|
18543
|
+
case 'loop': return 'Loop';
|
|
18544
|
+
default: return 'Prompt';
|
|
18545
|
+
}
|
|
18546
|
+
}
|
|
18547
|
+
|
|
18548
|
+
function _defaultDataForKind(kind, stepId) {
|
|
18549
|
+
var base = { stepId: stepId, prompt: '', tier: 1, maxTurns: 15, kind: kind };
|
|
18550
|
+
if (kind === 'prompt') return Object.assign({}, base, { prompt: 'Describe what this step should do.' });
|
|
18551
|
+
if (kind === 'mcp') return Object.assign({}, base, { mcp: { server: '', tool: '', inputs: {} } });
|
|
18552
|
+
if (kind === 'channel') return Object.assign({}, base, { channel: { channel: 'slack', target: '#me', content: '' } });
|
|
18553
|
+
if (kind === 'transform') return Object.assign({}, base, { transform: { expression: 'input' } });
|
|
18554
|
+
if (kind === 'conditional') return Object.assign({}, base, { conditional: { condition: 'true', trueNext: [], falseNext: [] } });
|
|
18555
|
+
if (kind === 'loop') return Object.assign({}, base, { loop: { items: 'input', bodyStepIds: [] } });
|
|
18556
|
+
return base;
|
|
18557
|
+
}
|
|
18558
|
+
|
|
18559
|
+
function _generateUniqueStepId(kind) {
|
|
18560
|
+
var used = new Set();
|
|
18561
|
+
if (_builderCanvasEditor) {
|
|
18562
|
+
var data = _builderCanvasEditor.export();
|
|
18563
|
+
var nodes = data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data ? data.drawflow.Home.data : {};
|
|
18564
|
+
for (var k in nodes) {
|
|
18565
|
+
var d = nodes[k].data || {};
|
|
18566
|
+
if (d.stepId) used.add(d.stepId);
|
|
18567
|
+
}
|
|
18568
|
+
}
|
|
18569
|
+
var prefix = kind.slice(0, 4);
|
|
18570
|
+
var i = 1;
|
|
18571
|
+
while (used.has(prefix + i)) i++;
|
|
18572
|
+
return prefix + i;
|
|
18573
|
+
}
|
|
18574
|
+
|
|
18575
|
+
function _decorateBuilderNodeById(nodeId, kind, data) {
|
|
18576
|
+
var nodeEl = document.querySelector('.drawflow-node[id="node-' + nodeId + '"]');
|
|
18577
|
+
if (!nodeEl) return;
|
|
18578
|
+
var content = nodeEl.querySelector('.drawflow_content_node');
|
|
18579
|
+
if (!content) return;
|
|
18580
|
+
content.dataset._decorated = '1';
|
|
18581
|
+
content.innerHTML =
|
|
18582
|
+
'<div style="font-weight:600;font-size:12px;margin-bottom:4px;color:#fff;text-transform:lowercase">' + esc(kind) + ' · ' + esc(data.stepId) + '</div>' +
|
|
18583
|
+
'<div style="font-size:11px;color:rgba(255,255,255,0.85);line-height:1.35;max-height:80px;overflow:hidden">' + esc(_summarizeStepData(data, kind)) + '</div>';
|
|
18584
|
+
}
|
|
18585
|
+
|
|
18586
|
+
function _summarizeStepData(d, kind) {
|
|
18587
|
+
if (!d) return '';
|
|
18588
|
+
if (kind === 'mcp' && d.mcp) return (d.mcp.server || '?') + '.' + (d.mcp.tool || '?');
|
|
18589
|
+
if (kind === 'channel' && d.channel) return d.channel.channel + ' → ' + d.channel.target;
|
|
18590
|
+
if (kind === 'transform' && d.transform) return d.transform.expression || '';
|
|
18591
|
+
if (kind === 'conditional' && d.conditional) return 'if ' + (d.conditional.condition || '');
|
|
18592
|
+
if (kind === 'loop' && d.loop) return 'for each ' + (d.loop.items || '');
|
|
18593
|
+
return (d.prompt || '').slice(0, 200);
|
|
18594
|
+
}
|
|
18595
|
+
|
|
18596
|
+
// ── Per-node config panel (Phase 2c) ───────────────────────────
|
|
18597
|
+
|
|
18598
|
+
var _builderConfigOpenNodeId = null;
|
|
18599
|
+
var _builderMcpToolsCache = null;
|
|
18600
|
+
|
|
18601
|
+
function _openNodeConfigPanel(nodeId) {
|
|
18602
|
+
if (!_builderCanvasEditor) return;
|
|
18603
|
+
_builderConfigOpenNodeId = nodeId;
|
|
18604
|
+
var node = _builderCanvasEditor.getNodeFromId(nodeId);
|
|
18605
|
+
if (!node) return;
|
|
18606
|
+
var data = node.data || {};
|
|
18607
|
+
var kind = data.kind || 'prompt';
|
|
18608
|
+
var panel = document.getElementById('builder-config-panel');
|
|
18609
|
+
if (!panel) return;
|
|
18610
|
+
panel.style.display = '';
|
|
18611
|
+
panel.innerHTML = _renderConfigPanel(nodeId, kind, data);
|
|
18612
|
+
_bindConfigPanelInputs(nodeId, kind);
|
|
18613
|
+
if (kind === 'mcp') _populateMcpDropdowns();
|
|
18614
|
+
}
|
|
18615
|
+
|
|
18616
|
+
function _closeNodeConfigPanel() {
|
|
18617
|
+
_builderConfigOpenNodeId = null;
|
|
18618
|
+
var panel = document.getElementById('builder-config-panel');
|
|
18619
|
+
if (panel) panel.style.display = 'none';
|
|
16859
18620
|
}
|
|
16860
18621
|
|
|
16861
|
-
function
|
|
16862
|
-
|
|
16863
|
-
|
|
18622
|
+
function _renderConfigPanel(nodeId, kind, d) {
|
|
18623
|
+
var common = (
|
|
18624
|
+
'<div class="cfg-row"><label>Step id</label><input type="text" data-cfg="stepId" value="' + esc(d.stepId || '') + '"></div>' +
|
|
18625
|
+
'<div class="cfg-row"><label>Tier</label><input type="number" min="1" max="5" data-cfg="tier" value="' + (d.tier || 1) + '"></div>' +
|
|
18626
|
+
'<div class="cfg-row"><label>Max turns</label><input type="number" min="1" data-cfg="maxTurns" value="' + (d.maxTurns || 15) + '"></div>' +
|
|
18627
|
+
'<div class="cfg-row"><label>Model</label><input type="text" data-cfg="model" placeholder="default" value="' + esc(d.model || '') + '"></div>'
|
|
18628
|
+
);
|
|
18629
|
+
var kindHtml = '';
|
|
18630
|
+
if (kind === 'prompt') {
|
|
18631
|
+
kindHtml = '<div class="cfg-row"><label>Prompt</label><textarea data-cfg="prompt" rows="6">' + esc(d.prompt || '') + '</textarea></div>';
|
|
18632
|
+
} else if (kind === 'mcp') {
|
|
18633
|
+
var m = d.mcp || {};
|
|
18634
|
+
kindHtml = (
|
|
18635
|
+
'<div class="cfg-row"><label>MCP server</label><select data-cfg="mcp.server" id="cfg-mcp-server"><option value="' + esc(m.server || '') + '">' + esc(m.server || '— pick —') + '</option></select></div>' +
|
|
18636
|
+
'<div class="cfg-row"><label>MCP tool</label><select data-cfg="mcp.tool" id="cfg-mcp-tool"><option value="' + esc(m.tool || '') + '">' + esc(m.tool || '— pick —') + '</option></select></div>' +
|
|
18637
|
+
'<div class="cfg-row"><label>Inputs (JSON)</label><textarea data-cfg="mcp.inputs" rows="4">' + esc(JSON.stringify(m.inputs || {}, null, 2)) + '</textarea></div>' +
|
|
18638
|
+
'<div class="cfg-row"><label>Description</label><textarea data-cfg="prompt" rows="3">' + esc(d.prompt || '') + '</textarea></div>'
|
|
18639
|
+
);
|
|
18640
|
+
} else if (kind === 'channel') {
|
|
18641
|
+
var c = d.channel || {};
|
|
18642
|
+
kindHtml = (
|
|
18643
|
+
'<div class="cfg-row"><label>Channel</label><select data-cfg="channel.channel">' +
|
|
18644
|
+
['discord','slack','telegram','whatsapp','email','webhook'].map(function(k) { return '<option' + (c.channel === k ? ' selected' : '') + '>' + k + '</option>'; }).join('') +
|
|
18645
|
+
'</select></div>' +
|
|
18646
|
+
'<div class="cfg-row"><label>Target</label><input type="text" data-cfg="channel.target" placeholder="#channel, user id, email…" value="' + esc(c.target || '') + '"></div>' +
|
|
18647
|
+
'<div class="cfg-row"><label>Content</label><textarea data-cfg="channel.content" rows="6">' + esc(c.content || '') + '</textarea></div>'
|
|
18648
|
+
);
|
|
18649
|
+
} else if (kind === 'transform') {
|
|
18650
|
+
var t = d.transform || {};
|
|
18651
|
+
kindHtml = '<div class="cfg-row"><label>Expression</label><textarea data-cfg="transform.expression" rows="6" placeholder="JS expression that returns shaped data">' + esc(t.expression || '') + '</textarea></div>';
|
|
18652
|
+
} else if (kind === 'conditional') {
|
|
18653
|
+
var co = d.conditional || {};
|
|
18654
|
+
kindHtml = (
|
|
18655
|
+
'<div class="cfg-row"><label>Condition</label><textarea data-cfg="conditional.condition" rows="3" placeholder="JS expression — truthy/falsy">' + esc(co.condition || '') + '</textarea></div>' +
|
|
18656
|
+
'<div class="cfg-row"><label>True → step ids</label><input type="text" data-cfg="conditional.trueNext" placeholder="comma-separated" value="' + esc((co.trueNext || []).join(',')) + '"></div>' +
|
|
18657
|
+
'<div class="cfg-row"><label>False → step ids</label><input type="text" data-cfg="conditional.falseNext" placeholder="comma-separated" value="' + esc((co.falseNext || []).join(',')) + '"></div>'
|
|
18658
|
+
);
|
|
18659
|
+
} else if (kind === 'loop') {
|
|
18660
|
+
var l = d.loop || {};
|
|
18661
|
+
kindHtml = (
|
|
18662
|
+
'<div class="cfg-row"><label>Items</label><input type="text" data-cfg="loop.items" placeholder="JS expression returning iterable" value="' + esc(l.items || '') + '"></div>' +
|
|
18663
|
+
'<div class="cfg-row"><label>Body step ids</label><input type="text" data-cfg="loop.bodyStepIds" placeholder="comma-separated" value="' + esc((l.bodyStepIds || []).join(',')) + '"></div>'
|
|
18664
|
+
);
|
|
18665
|
+
}
|
|
18666
|
+
var lastRun = _builderRunStepResults && _builderRunStepResults[d.stepId || ''];
|
|
18667
|
+
var lastRunBtn = lastRun
|
|
18668
|
+
? '<button onclick="showStepOutput(\\x27' + esc(d.stepId || '') + '\\x27)" style="margin-top:4px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px;width:100%">Show last run output (' + esc(lastRun.status || '') + ')</button>'
|
|
18669
|
+
: '';
|
|
18670
|
+
return (
|
|
18671
|
+
'<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px">' +
|
|
18672
|
+
'<span>' + esc(kind) + ' step</span>' +
|
|
18673
|
+
'<span style="flex:1"></span>' +
|
|
18674
|
+
'<button onclick="_deleteSelectedNode()" title="Delete this node" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:16px">×</button>' +
|
|
18675
|
+
'</div>' +
|
|
18676
|
+
'<div style="padding:10px 14px;flex:1;overflow-y:auto" id="builder-config-fields">' +
|
|
18677
|
+
common +
|
|
18678
|
+
'<div style="border-top:1px dashed var(--border);margin:10px 0"></div>' +
|
|
18679
|
+
kindHtml +
|
|
18680
|
+
(lastRunBtn ? '<div style="border-top:1px dashed var(--border);margin:14px 0 8px"></div>' + lastRunBtn : '') +
|
|
18681
|
+
'</div>'
|
|
18682
|
+
);
|
|
16864
18683
|
}
|
|
16865
18684
|
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
var d = await r.json();
|
|
16874
|
-
if (d.ok) {
|
|
16875
|
-
toast(pinned ? 'Pinned' : 'Unpinned', 'success');
|
|
16876
|
-
runMemorySearch();
|
|
16877
|
-
} else {
|
|
16878
|
-
toast('Pin failed: ' + (d.error || 'unknown'), 'error');
|
|
18685
|
+
function _bindConfigPanelInputs(nodeId) {
|
|
18686
|
+
var panel = document.getElementById('builder-config-panel');
|
|
18687
|
+
if (!panel) return;
|
|
18688
|
+
panel.querySelectorAll('[data-cfg]').forEach(function(el) {
|
|
18689
|
+
el.addEventListener('change', function() { _applyConfigField(nodeId, el); });
|
|
18690
|
+
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
|
|
18691
|
+
el.addEventListener('blur', function() { _applyConfigField(nodeId, el); });
|
|
16879
18692
|
}
|
|
16880
|
-
}
|
|
18693
|
+
});
|
|
16881
18694
|
}
|
|
16882
18695
|
|
|
16883
|
-
|
|
16884
|
-
if (!
|
|
16885
|
-
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
18696
|
+
function _applyConfigField(nodeId, el) {
|
|
18697
|
+
if (!_builderCanvasEditor) return;
|
|
18698
|
+
var node = _builderCanvasEditor.getNodeFromId(nodeId);
|
|
18699
|
+
if (!node) return;
|
|
18700
|
+
var pathStr = el.getAttribute('data-cfg') || '';
|
|
18701
|
+
var pathParts = pathStr.split('.');
|
|
18702
|
+
var raw = el.value;
|
|
18703
|
+
var parsed;
|
|
18704
|
+
if (pathStr.endsWith('.inputs')) {
|
|
18705
|
+
try { parsed = raw ? JSON.parse(raw) : {}; } catch (e) { toast('Invalid JSON in inputs', 'error'); return; }
|
|
18706
|
+
} else if (pathStr === 'tier' || pathStr === 'maxTurns') {
|
|
18707
|
+
parsed = Number(raw) || 0;
|
|
18708
|
+
} else if (pathStr === 'conditional.trueNext' || pathStr === 'conditional.falseNext' || pathStr === 'loop.bodyStepIds') {
|
|
18709
|
+
parsed = raw ? raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
|
|
18710
|
+
} else {
|
|
18711
|
+
parsed = raw;
|
|
18712
|
+
}
|
|
18713
|
+
var newData = JSON.parse(JSON.stringify(node.data || {}));
|
|
18714
|
+
var cur = newData;
|
|
18715
|
+
for (var i = 0; i < pathParts.length - 1; i++) {
|
|
18716
|
+
if (!cur[pathParts[i]] || typeof cur[pathParts[i]] !== 'object') cur[pathParts[i]] = {};
|
|
18717
|
+
cur = cur[pathParts[i]];
|
|
18718
|
+
}
|
|
18719
|
+
cur[pathParts[pathParts.length - 1]] = parsed;
|
|
18720
|
+
_builderCanvasEditor.updateNodeDataFromId(nodeId, newData);
|
|
18721
|
+
// Re-decorate the node visually
|
|
18722
|
+
_decorateBuilderNodeById(nodeId, newData.kind || 'prompt', newData);
|
|
18723
|
+
_scheduleBuilderSave(300);
|
|
16895
18724
|
}
|
|
16896
18725
|
|
|
16897
|
-
|
|
16898
|
-
|
|
18726
|
+
function _deleteSelectedNode() {
|
|
18727
|
+
if (!_builderCanvasEditor || !_builderConfigOpenNodeId) return;
|
|
18728
|
+
if (!confirm('Delete this node?')) return;
|
|
18729
|
+
_builderCanvasEditor.removeNodeId('node-' + _builderConfigOpenNodeId);
|
|
18730
|
+
_closeNodeConfigPanel();
|
|
18731
|
+
_scheduleBuilderSave(150);
|
|
18732
|
+
}
|
|
18733
|
+
|
|
18734
|
+
// ── Test runs (Phase 2d/2e) ─────────────────────────────────────
|
|
18735
|
+
|
|
18736
|
+
var _builderActiveRunId = null;
|
|
18737
|
+
var _builderRunStepResults = {};
|
|
18738
|
+
|
|
18739
|
+
async function testBuilderCanvas() {
|
|
18740
|
+
if (!_builderCanvasOpenId) { toast('Open a workflow first', 'info'); return; }
|
|
18741
|
+
if (_builderActiveRunId) { toast('A test is already running', 'info'); return; }
|
|
18742
|
+
// Always flush any pending save so the test sees the latest graph
|
|
18743
|
+
if (_builderSaveTimer) { clearTimeout(_builderSaveTimer); _builderSaveTimer = null; await _flushBuilderSave(); }
|
|
18744
|
+
_clearRunVisualState();
|
|
16899
18745
|
try {
|
|
16900
|
-
var r = await
|
|
16901
|
-
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
} catch(e) { /* profiles are optional */ }
|
|
18746
|
+
var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/test', { mode: 'mock' });
|
|
18747
|
+
if (r.error) { toast(r.error, 'error'); return; }
|
|
18748
|
+
_builderActiveRunId = r.runId;
|
|
18749
|
+
_builderRunStepResults = {};
|
|
18750
|
+
var cancel = document.getElementById('builder-canvas-cancel-btn');
|
|
18751
|
+
if (cancel) cancel.style.display = '';
|
|
18752
|
+
_setBuilderSaveStatus('saved'); // override status with a more useful one below
|
|
18753
|
+
_setRunFooter('Running test… (' + r.runId.slice(0, 8) + ')');
|
|
18754
|
+
} catch (err) {
|
|
18755
|
+
toast('Test failed to start: ' + err, 'error');
|
|
18756
|
+
}
|
|
16912
18757
|
}
|
|
16913
18758
|
|
|
16914
|
-
async function
|
|
18759
|
+
async function cancelBuilderTest() {
|
|
18760
|
+
if (!_builderActiveRunId) return;
|
|
16915
18761
|
try {
|
|
16916
|
-
await apiJson('POST', '/api/
|
|
16917
|
-
|
|
16918
|
-
var container = document.getElementById('chat-messages');
|
|
16919
|
-
container.innerHTML = '<div class="empty-state"><p style="margin-bottom:14px;color:var(--text-muted)">Profile switched' + (slug ? ' to <strong>' + esc(slug) + '</strong>' : '') + '. Session cleared.</p></div>';
|
|
16920
|
-
toast(slug ? 'Switched to ' + slug : 'Profile cleared', 'success');
|
|
16921
|
-
} catch(e) { toast('Failed to switch profile: ' + e, 'error'); }
|
|
18762
|
+
await apiJson('POST', '/api/builder/runs/' + encodeURIComponent(_builderActiveRunId) + '/cancel', {});
|
|
18763
|
+
} catch (err) { /* SSE will still report the cancellation */ }
|
|
16922
18764
|
}
|
|
16923
18765
|
|
|
16924
|
-
|
|
18766
|
+
function _clearRunVisualState() {
|
|
18767
|
+
document.querySelectorAll('#builder-canvas .drawflow-node').forEach(function(n) {
|
|
18768
|
+
n.classList.remove('cl-step-running', 'cl-step-done', 'cl-step-failed', 'cl-step-skipped', 'cl-step-cancelled', 'cl-step-timeout');
|
|
18769
|
+
});
|
|
18770
|
+
}
|
|
16925
18771
|
|
|
16926
|
-
function
|
|
16927
|
-
|
|
16928
|
-
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
16933
|
-
|
|
16934
|
-
var
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">'
|
|
16939
|
-
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for deploying to production — run tests, build, push, verify\\x27)">Deploy to prod</button>'
|
|
16940
|
-
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for writing a weekly status report from git commits and calendar\\x27)">Weekly status report</button>'
|
|
16941
|
-
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for onboarding a new team member — set up accounts, send welcome email\\x27)">Onboard team member</button>'
|
|
16942
|
-
+ '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for researching a company before a sales call\\x27)">Company research</button>'
|
|
16943
|
-
+ '</div>';
|
|
18772
|
+
function _setRunFooter(text) {
|
|
18773
|
+
var el = document.getElementById('builder-canvas-status');
|
|
18774
|
+
if (el) { el.textContent = text || ''; el.style.color = 'var(--text-muted)'; }
|
|
18775
|
+
}
|
|
18776
|
+
|
|
18777
|
+
function _findNodeElForStepId(stepId) {
|
|
18778
|
+
if (!_builderCanvasEditor) return null;
|
|
18779
|
+
var data = _builderCanvasEditor.export();
|
|
18780
|
+
var nodes = data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data ? data.drawflow.Home.data : {};
|
|
18781
|
+
for (var k in nodes) {
|
|
18782
|
+
var d = nodes[k].data || {};
|
|
18783
|
+
if (d.stepId === stepId) return document.querySelector('.drawflow-node[id="node-' + k + '"]');
|
|
16944
18784
|
}
|
|
18785
|
+
return null;
|
|
16945
18786
|
}
|
|
16946
18787
|
|
|
16947
|
-
function
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
18788
|
+
function _onRunEvent(evt) {
|
|
18789
|
+
if (!evt || !evt.runId || evt.workflowId !== _builderCanvasOpenId) return;
|
|
18790
|
+
if (evt.type === 'run:started') {
|
|
18791
|
+
_setRunFooter('Running test… (' + evt.runId.slice(0, 8) + ')');
|
|
18792
|
+
return;
|
|
18793
|
+
}
|
|
18794
|
+
if (evt.type === 'run:step-status') {
|
|
18795
|
+
var p = evt.payload || {};
|
|
18796
|
+
var nodeEl = _findNodeElForStepId(p.stepId);
|
|
18797
|
+
if (nodeEl) {
|
|
18798
|
+
nodeEl.classList.remove('cl-step-running', 'cl-step-done', 'cl-step-failed', 'cl-step-skipped', 'cl-step-cancelled', 'cl-step-timeout');
|
|
18799
|
+
nodeEl.classList.add('cl-step-' + p.status);
|
|
18800
|
+
}
|
|
18801
|
+
return;
|
|
18802
|
+
}
|
|
18803
|
+
if (evt.type === 'run:step-output') {
|
|
18804
|
+
var po = evt.payload || {};
|
|
18805
|
+
_builderRunStepResults[po.stepId] = po;
|
|
18806
|
+
return;
|
|
18807
|
+
}
|
|
18808
|
+
if (evt.type === 'run:completed' || evt.type === 'run:cancelled') {
|
|
18809
|
+
var ec = (evt.payload && evt.payload.status) || (evt.type === 'run:cancelled' ? 'cancelled' : 'ok');
|
|
18810
|
+
var dur = (evt.payload && evt.payload.durationMs) || 0;
|
|
18811
|
+
_builderActiveRunId = null;
|
|
18812
|
+
var cancel = document.getElementById('builder-canvas-cancel-btn');
|
|
18813
|
+
if (cancel) cancel.style.display = 'none';
|
|
18814
|
+
_setRunFooter('Test ' + ec + ' in ' + dur + 'ms — click a node for output');
|
|
18815
|
+
return;
|
|
18816
|
+
}
|
|
18817
|
+
}
|
|
16951
18818
|
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
18819
|
+
function showStepOutput(stepId) {
|
|
18820
|
+
var po = _builderRunStepResults[stepId];
|
|
18821
|
+
if (!po) { toast('No output for ' + stepId + ' yet', 'info'); return; }
|
|
18822
|
+
var lines = ['Step: ' + stepId, 'Status: ' + po.status, ''];
|
|
18823
|
+
if (po.error) lines.push('Error: ' + po.error, '');
|
|
18824
|
+
if (po.output != null) lines.push(typeof po.output === 'string' ? po.output : JSON.stringify(po.output, null, 2));
|
|
18825
|
+
alert(lines.join('\\n'));
|
|
18826
|
+
}
|
|
18827
|
+
|
|
18828
|
+
async function _populateMcpDropdowns() {
|
|
18829
|
+
try {
|
|
18830
|
+
if (!_builderMcpToolsCache) {
|
|
18831
|
+
// Fetch from the agent-side discovery — proxied through a simple endpoint
|
|
18832
|
+
// (we build a small client-side bridge by listing available servers via /api/builder/mcp-discovery)
|
|
18833
|
+
var r = await apiFetch('/api/builder/mcp-discovery');
|
|
18834
|
+
_builderMcpToolsCache = await r.json();
|
|
18835
|
+
}
|
|
18836
|
+
var serverSel = document.getElementById('cfg-mcp-server');
|
|
18837
|
+
var toolSel = document.getElementById('cfg-mcp-tool');
|
|
18838
|
+
if (!serverSel || !toolSel) return;
|
|
18839
|
+
var current = serverSel.value;
|
|
18840
|
+
var opts = '<option value="">— pick —</option>';
|
|
18841
|
+
var servers = (_builderMcpToolsCache && _builderMcpToolsCache.servers) || [];
|
|
18842
|
+
for (var i = 0; i < servers.length; i++) {
|
|
18843
|
+
var s = servers[i];
|
|
18844
|
+
opts += '<option value="' + esc(s.name) + '"' + (s.name === current ? ' selected' : '') + '>' + esc(s.name) + (s.enabled ? '' : ' (off)') + '</option>';
|
|
18845
|
+
}
|
|
18846
|
+
serverSel.innerHTML = opts;
|
|
18847
|
+
var rebuildTools = function() {
|
|
18848
|
+
var s = servers.filter(function(x) { return x.name === serverSel.value; })[0];
|
|
18849
|
+
var tools = (s && s.tools) || [];
|
|
18850
|
+
var curT = toolSel.value;
|
|
18851
|
+
var t = '<option value="">— pick —</option>';
|
|
18852
|
+
for (var j = 0; j < tools.length; j++) {
|
|
18853
|
+
t += '<option value="' + esc(tools[j]) + '"' + (tools[j] === curT ? ' selected' : '') + '>' + esc(tools[j]) + '</option>';
|
|
18854
|
+
}
|
|
18855
|
+
toolSel.innerHTML = t;
|
|
18856
|
+
};
|
|
18857
|
+
rebuildTools();
|
|
18858
|
+
serverSel.addEventListener('change', rebuildTools);
|
|
18859
|
+
} catch (err) {
|
|
18860
|
+
/* non-fatal — dropdowns just stay sparse */
|
|
16960
18861
|
}
|
|
16961
18862
|
}
|
|
16962
18863
|
|
|
@@ -17551,6 +19452,262 @@ function formatTokens(n) {
|
|
|
17551
19452
|
return String(n);
|
|
17552
19453
|
}
|
|
17553
19454
|
|
|
19455
|
+
function formatBytes(n) {
|
|
19456
|
+
if (n == null) return '—';
|
|
19457
|
+
if (n < 1024) return n + ' B';
|
|
19458
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
|
19459
|
+
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB';
|
|
19460
|
+
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
19461
|
+
}
|
|
19462
|
+
|
|
19463
|
+
async function memoryHealthAction(action) {
|
|
19464
|
+
var labels = { 'janitor': 'cleanup', 'rebuild-fts': 'FTS rebuild', 'fix-orphans': 'orphan fix' };
|
|
19465
|
+
if (!confirm('Run ' + (labels[action] || action) + ' now?')) return;
|
|
19466
|
+
try {
|
|
19467
|
+
var r = await apiJson('POST', '/api/memory/health/action', { action: action });
|
|
19468
|
+
if (r.error) { toast('Action failed: ' + r.error, 'error'); return; }
|
|
19469
|
+
var detail = '';
|
|
19470
|
+
if (r.result) detail = ' — ' + Object.entries(r.result).slice(0, 4).map(function(p) { return p[0] + ':' + p[1]; }).join(', ');
|
|
19471
|
+
if (r.report) detail = ' — orphans nulled: ' + (r.report.orphanRefsNulled || 0) + ', FTS rebuilds: ' + (r.report.ftsRebuilds || 0);
|
|
19472
|
+
toast(action + ' complete' + detail, 'success');
|
|
19473
|
+
refreshMemoryHealth();
|
|
19474
|
+
} catch (err) {
|
|
19475
|
+
toast('Action error: ' + err, 'error');
|
|
19476
|
+
}
|
|
19477
|
+
}
|
|
19478
|
+
|
|
19479
|
+
// ── Goals: inline create form ────────────────────────────────────
|
|
19480
|
+
function openNewGoalForm() {
|
|
19481
|
+
var el = document.getElementById('new-goal-form');
|
|
19482
|
+
if (!el) return;
|
|
19483
|
+
el.style.display = '';
|
|
19484
|
+
setTimeout(function() {
|
|
19485
|
+
var t = document.getElementById('new-goal-title');
|
|
19486
|
+
if (t) t.focus();
|
|
19487
|
+
}, 50);
|
|
19488
|
+
}
|
|
19489
|
+
|
|
19490
|
+
async function submitNewGoal() {
|
|
19491
|
+
var titleEl = document.getElementById('new-goal-title');
|
|
19492
|
+
var descEl = document.getElementById('new-goal-desc');
|
|
19493
|
+
var title = (titleEl?.value || '').trim();
|
|
19494
|
+
if (!title) { toast('Goal needs a title', 'error'); return; }
|
|
19495
|
+
try {
|
|
19496
|
+
var r = await apiJson('POST', '/api/goals', { title: title, description: (descEl?.value || '').trim() });
|
|
19497
|
+
if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
|
|
19498
|
+
if (titleEl) titleEl.value = '';
|
|
19499
|
+
if (descEl) descEl.value = '';
|
|
19500
|
+
document.getElementById('new-goal-form').style.display = 'none';
|
|
19501
|
+
toast('Goal created', 'success');
|
|
19502
|
+
if (typeof refreshGoals === 'function') refreshGoals();
|
|
19503
|
+
} catch (err) { toast('Create error: ' + err, 'error'); }
|
|
19504
|
+
}
|
|
19505
|
+
|
|
19506
|
+
// ── Workflows: open Builder for a brand-new workflow ─────────────
|
|
19507
|
+
function openBuilderForNewWorkflow() {
|
|
19508
|
+
navigateTo('builder');
|
|
19509
|
+
setTimeout(function() {
|
|
19510
|
+
var typeSel = document.getElementById('builder-type');
|
|
19511
|
+
if (typeSel) { typeSel.value = 'workflow'; updateBuilderMode(); }
|
|
19512
|
+
var name = prompt('Name your new workflow:');
|
|
19513
|
+
if (!name) return;
|
|
19514
|
+
apiJson('POST', '/api/builder/workflows', { name: name }).then(function(r) {
|
|
19515
|
+
if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
|
|
19516
|
+
if (r && r.id) {
|
|
19517
|
+
// Refresh picker, then open the new workflow
|
|
19518
|
+
refreshBuilderCanvasPicker('workflow');
|
|
19519
|
+
setTimeout(function() { openBuilderWorkflow(r.id); }, 200);
|
|
19520
|
+
}
|
|
19521
|
+
});
|
|
19522
|
+
}, 100);
|
|
19523
|
+
}
|
|
19524
|
+
|
|
19525
|
+
// ── Unleashed: open the start-task picker ────────────────────────
|
|
19526
|
+
function openStartUnleashedTask() {
|
|
19527
|
+
// Reuse the existing cron list — pick a cron job, run it in unleashed mode.
|
|
19528
|
+
// For v1 just route to Automations where the existing controls live.
|
|
19529
|
+
navigateTo('automations');
|
|
19530
|
+
toast('Pick a cron job and click "Run unleashed" — kicks off long-running mode.', 'info');
|
|
19531
|
+
}
|
|
19532
|
+
|
|
19533
|
+
async function refreshMemoryHealth() {
|
|
19534
|
+
var el = document.getElementById('memory-health-content');
|
|
19535
|
+
if (!el) return;
|
|
19536
|
+
try {
|
|
19537
|
+
var r = await apiFetch('/api/memory/health');
|
|
19538
|
+
var d = await r.json();
|
|
19539
|
+
if (!d.ok || !d.health) {
|
|
19540
|
+
el.innerHTML = '<div class="empty-state">' + esc(d.error || 'No data') + '</div>';
|
|
19541
|
+
return;
|
|
19542
|
+
}
|
|
19543
|
+
var h = d.health;
|
|
19544
|
+
var consolidatedPct = h.chunks.total > 0
|
|
19545
|
+
? ((h.chunks.consolidated / h.chunks.total) * 100).toFixed(1)
|
|
19546
|
+
: '0.0';
|
|
19547
|
+
var zombiePct = h.chunks.total > 0
|
|
19548
|
+
? ((h.chunks.zombieCount / h.chunks.total) * 100).toFixed(1)
|
|
19549
|
+
: '0.0';
|
|
19550
|
+
|
|
19551
|
+
var html = '';
|
|
19552
|
+
|
|
19553
|
+
// Hero tiles row.
|
|
19554
|
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:20px">';
|
|
19555
|
+
html += '<div class="metric-hero"><div class="metric-hero-value">' + (h.chunks.total || 0)
|
|
19556
|
+
+ '</div><div class="metric-hero-label">Total Chunks</div>'
|
|
19557
|
+
+ '<div class="metric-hero-sub">' + (h.chunks.pinned || 0) + ' pinned · ' + (h.chunks.softDeleted || 0) + ' soft-deleted</div></div>';
|
|
19558
|
+
html += '<div class="metric-hero"><div class="metric-hero-value">' + consolidatedPct
|
|
19559
|
+
+ '%</div><div class="metric-hero-label">Consolidated</div>'
|
|
19560
|
+
+ '<div class="metric-hero-sub">' + (h.chunks.consolidated || 0) + ' of ' + (h.chunks.total || 0) + ' chunks</div></div>';
|
|
19561
|
+
html += '<div class="metric-hero"><div class="metric-hero-value">' + (h.chunks.zombieCount || 0)
|
|
19562
|
+
+ '</div><div class="metric-hero-label">Zombies</div>'
|
|
19563
|
+
+ '<div class="metric-hero-sub">' + zombiePct + '% of total · eligible to expire</div></div>';
|
|
19564
|
+
html += '<div class="metric-hero"><div class="metric-hero-value">' + formatBytes(h.dbSizeBytes)
|
|
19565
|
+
+ '</div><div class="metric-hero-label">DB File Size</div>'
|
|
19566
|
+
+ '<div class="metric-hero-sub">last vacuum: ' + esc(h.lastVacuumAt || 'never') + '</div></div>';
|
|
19567
|
+
html += '</div>';
|
|
19568
|
+
|
|
19569
|
+
// Two-column layout: categories + table sizes on left, top cited on right.
|
|
19570
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">';
|
|
19571
|
+
|
|
19572
|
+
// Left column: categories.
|
|
19573
|
+
html += '<div class="card"><div class="card-header"><h3>Chunks by Category</h3></div><div class="card-body">';
|
|
19574
|
+
if (!h.chunksByCategory || h.chunksByCategory.length === 0) {
|
|
19575
|
+
html += '<div class="empty-state">No chunks yet.</div>';
|
|
19576
|
+
} else {
|
|
19577
|
+
html += '<table class="data-table"><thead><tr><th>Category</th><th style="text-align:right">Count</th></tr></thead><tbody>';
|
|
19578
|
+
for (var i = 0; i < h.chunksByCategory.length; i++) {
|
|
19579
|
+
var c = h.chunksByCategory[i];
|
|
19580
|
+
html += '<tr><td>' + esc(c.category || '—') + '</td><td style="text-align:right">' + c.count + '</td></tr>';
|
|
19581
|
+
}
|
|
19582
|
+
html += '</tbody></table>';
|
|
19583
|
+
}
|
|
19584
|
+
html += '</div></div>';
|
|
19585
|
+
|
|
19586
|
+
// Right column: top cited.
|
|
19587
|
+
html += '<div class="card"><div class="card-header"><h3>Top Cited (last 30d)</h3></div><div class="card-body">';
|
|
19588
|
+
if (!h.topCitedLast30d || h.topCitedLast30d.length === 0) {
|
|
19589
|
+
html += '<div class="empty-state">No outcomes recorded in the last 30 days.</div>';
|
|
19590
|
+
} else {
|
|
19591
|
+
html += '<table class="data-table"><thead><tr><th>Source</th><th>Section</th><th style="text-align:right">Refs</th></tr></thead><tbody>';
|
|
19592
|
+
for (var j = 0; j < h.topCitedLast30d.length; j++) {
|
|
19593
|
+
var t = h.topCitedLast30d[j];
|
|
19594
|
+
html += '<tr><td>' + esc(t.sourceFile || '—') + '</td><td>' + esc(t.section || '—')
|
|
19595
|
+
+ '</td><td style="text-align:right">' + t.refCount + '</td></tr>';
|
|
19596
|
+
}
|
|
19597
|
+
html += '</tbody></table>';
|
|
19598
|
+
}
|
|
19599
|
+
html += '</div></div>';
|
|
19600
|
+
|
|
19601
|
+
html += '</div>';
|
|
19602
|
+
|
|
19603
|
+
// Staleness section — high-salience drift + user-model age.
|
|
19604
|
+
var staleSlots = h.staleUserModelSlots || [];
|
|
19605
|
+
var staleChunks = h.staleHighSalienceChunks || [];
|
|
19606
|
+
if (staleSlots.length > 0 || staleChunks.length > 0) {
|
|
19607
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">';
|
|
19608
|
+
|
|
19609
|
+
html += '<div class="card"><div class="card-header"><h3>Stale User-Model Slots</h3></div><div class="card-body">';
|
|
19610
|
+
if (staleSlots.length === 0) {
|
|
19611
|
+
html += '<div class="empty-state">All slots fresh.</div>';
|
|
19612
|
+
} else {
|
|
19613
|
+
html += '<table class="data-table"><thead><tr><th>Slot</th><th>Agent</th><th style="text-align:right">Age (days)</th></tr></thead><tbody>';
|
|
19614
|
+
for (var ss = 0; ss < staleSlots.length; ss++) {
|
|
19615
|
+
var s = staleSlots[ss];
|
|
19616
|
+
html += '<tr><td>' + esc(s.slot) + '</td><td>' + esc(s.agentSlug || 'global') + '</td><td style="text-align:right">' + s.ageDays + '</td></tr>';
|
|
19617
|
+
}
|
|
19618
|
+
html += '</tbody></table>';
|
|
19619
|
+
}
|
|
19620
|
+
html += '</div></div>';
|
|
19621
|
+
|
|
19622
|
+
html += '<div class="card"><div class="card-header"><h3>Stale High-Salience Chunks</h3><div style="font-size:11px;color:var(--text-muted)">High salience but EMA gone negative — ranked but not cited</div></div><div class="card-body">';
|
|
19623
|
+
if (staleChunks.length === 0) {
|
|
19624
|
+
html += '<div class="empty-state">No drift detected.</div>';
|
|
19625
|
+
} else {
|
|
19626
|
+
html += '<table class="data-table"><thead><tr><th>Source</th><th>Section</th><th style="text-align:right">Salience</th><th style="text-align:right">EMA</th></tr></thead><tbody>';
|
|
19627
|
+
for (var sc = 0; sc < staleChunks.length; sc++) {
|
|
19628
|
+
var sk = staleChunks[sc];
|
|
19629
|
+
html += '<tr><td>' + esc(sk.sourceFile || '') + '</td><td>' + esc(sk.section || '') + '</td><td style="text-align:right">' + sk.salience.toFixed(2) + '</td><td style="text-align:right">' + sk.lastOutcomeScore.toFixed(2) + '</td></tr>';
|
|
19630
|
+
}
|
|
19631
|
+
html += '</tbody></table>';
|
|
19632
|
+
}
|
|
19633
|
+
html += '</div></div>';
|
|
19634
|
+
html += '</div>';
|
|
19635
|
+
}
|
|
19636
|
+
|
|
19637
|
+
// Cache + write queue + integrity row.
|
|
19638
|
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin-top:16px">';
|
|
19639
|
+
|
|
19640
|
+
if (h.chunkCacheStats) {
|
|
19641
|
+
var cs = h.chunkCacheStats;
|
|
19642
|
+
var hitRatePct = (cs.hitRate * 100).toFixed(1);
|
|
19643
|
+
html += '<div class="card"><div class="card-header"><h3>Hot Chunk Cache</h3></div><div class="card-body">';
|
|
19644
|
+
html += '<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:13px">';
|
|
19645
|
+
html += '<div><strong>' + hitRatePct + '%</strong> hit rate</div>';
|
|
19646
|
+
html += '<div><strong>' + cs.hits + '</strong> hits</div>';
|
|
19647
|
+
html += '<div><strong>' + cs.misses + '</strong> misses</div>';
|
|
19648
|
+
html += '<div><strong>' + cs.size + ' / ' + cs.capacity + '</strong> entries</div>';
|
|
19649
|
+
html += '<div><strong>' + cs.evictions + '</strong> evictions</div>';
|
|
19650
|
+
html += '</div></div></div>';
|
|
19651
|
+
}
|
|
19652
|
+
|
|
19653
|
+
// Async write queue status.
|
|
19654
|
+
var wq = h.writeQueue;
|
|
19655
|
+
html += '<div class="card"><div class="card-header"><h3>Async Write Queue</h3></div><div class="card-body">';
|
|
19656
|
+
if (!wq) {
|
|
19657
|
+
html += '<div style="font-size:13px;color:var(--text-muted)">Sync mode (queue disabled)</div>';
|
|
19658
|
+
} else {
|
|
19659
|
+
html += '<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:13px">';
|
|
19660
|
+
html += '<div><strong>' + wq.size + '</strong> pending</div>';
|
|
19661
|
+
html += '<div><strong>' + wq.dropped + '</strong> dropped (back-pressure)</div>';
|
|
19662
|
+
html += '</div>';
|
|
19663
|
+
}
|
|
19664
|
+
html += '</div></div>';
|
|
19665
|
+
|
|
19666
|
+
// Integrity report.
|
|
19667
|
+
var ir = h.lastIntegrityReport;
|
|
19668
|
+
html += '<div class="card"><div class="card-header"><h3>Integrity</h3></div><div class="card-body">';
|
|
19669
|
+
if (!ir) {
|
|
19670
|
+
html += '<div style="font-size:13px;color:var(--text-muted)">No probes have run yet.</div>';
|
|
19671
|
+
} else {
|
|
19672
|
+
var ftsLabel = ir.ftsOk
|
|
19673
|
+
? '<span style="color:var(--green,#3a3)">ok</span>'
|
|
19674
|
+
: (ir.ftsRebuilt ? '<span style="color:var(--orange,#f80)">rebuilt</span>' : '<span style="color:var(--red,#c33)">failing</span>');
|
|
19675
|
+
html += '<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:13px">';
|
|
19676
|
+
html += '<div>FTS5: ' + ftsLabel + '</div>';
|
|
19677
|
+
html += '<div><strong>' + ir.orphanRefsNulled + '</strong> orphan refs nulled</div>';
|
|
19678
|
+
html += '<div><strong>' + ir.missingEmbeddings + '</strong> missing embeddings</div>';
|
|
19679
|
+
html += '<div style="color:var(--text-muted)">last: ' + esc(ir.ranAt || '') + '</div>';
|
|
19680
|
+
html += '</div>';
|
|
19681
|
+
}
|
|
19682
|
+
html += '</div></div>';
|
|
19683
|
+
|
|
19684
|
+
html += '</div>';
|
|
19685
|
+
|
|
19686
|
+
// Table row counts (full width).
|
|
19687
|
+
html += '<div class="card" style="margin-top:16px"><div class="card-header"><h3>Table Sizes</h3></div><div class="card-body">';
|
|
19688
|
+
var tableRows = Object.keys(h.tableRowCounts || {}).sort(function(a, b) {
|
|
19689
|
+
return (h.tableRowCounts[b] || 0) - (h.tableRowCounts[a] || 0);
|
|
19690
|
+
});
|
|
19691
|
+
if (tableRows.length === 0) {
|
|
19692
|
+
html += '<div class="empty-state">No table data.</div>';
|
|
19693
|
+
} else {
|
|
19694
|
+
html += '<table class="data-table"><thead><tr><th>Table</th><th style="text-align:right">Rows</th></tr></thead><tbody>';
|
|
19695
|
+
for (var k = 0; k < tableRows.length; k++) {
|
|
19696
|
+
var tn = tableRows[k];
|
|
19697
|
+
var rowCount = h.tableRowCounts[tn];
|
|
19698
|
+
var label = rowCount === -1 ? '<span style="color:var(--text-muted)">missing</span>' : String(rowCount);
|
|
19699
|
+
html += '<tr><td>' + esc(tn) + '</td><td style="text-align:right">' + label + '</td></tr>';
|
|
19700
|
+
}
|
|
19701
|
+
html += '</tbody></table>';
|
|
19702
|
+
}
|
|
19703
|
+
html += '</div></div>';
|
|
19704
|
+
|
|
19705
|
+
el.innerHTML = html;
|
|
19706
|
+
} catch (err) {
|
|
19707
|
+
el.innerHTML = '<div class="empty-state">Failed to load: ' + esc(String(err)) + '</div>';
|
|
19708
|
+
}
|
|
19709
|
+
}
|
|
19710
|
+
|
|
17554
19711
|
async function refreshMetrics() {
|
|
17555
19712
|
try {
|
|
17556
19713
|
const [r, ur] = await Promise.all([apiFetch('/api/metrics'), apiFetch('/api/metrics/usage')]);
|
|
@@ -18088,6 +20245,131 @@ async function saveDiscordSetup() {
|
|
|
18088
20245
|
} catch(e) { toast(String(e), 'error'); }
|
|
18089
20246
|
}
|
|
18090
20247
|
|
|
20248
|
+
function toggleHomeRail() {
|
|
20249
|
+
var rail = document.getElementById('home-rail');
|
|
20250
|
+
if (!rail) return;
|
|
20251
|
+
// Mobile: open/close. Desktop: collapse/show.
|
|
20252
|
+
if (window.matchMedia('(max-width: 1024px)').matches) {
|
|
20253
|
+
rail.classList.toggle('open');
|
|
20254
|
+
} else {
|
|
20255
|
+
rail.classList.toggle('collapsed');
|
|
20256
|
+
}
|
|
20257
|
+
}
|
|
20258
|
+
|
|
20259
|
+
async function refreshHomeRail() {
|
|
20260
|
+
// Daemon status
|
|
20261
|
+
try {
|
|
20262
|
+
var rs = await apiFetch('/api/status');
|
|
20263
|
+
var ds = await rs.json();
|
|
20264
|
+
var pip = document.querySelector('#rail-daemon-body .agent-activity-dot');
|
|
20265
|
+
var label = document.querySelector('#rail-daemon-body .agent-activity span:last-child');
|
|
20266
|
+
if (label) label.textContent = ds.running ? 'Daemon running' : 'Daemon stopped';
|
|
20267
|
+
if (pip) pip.style.background = ds.running ? '#22c55e' : '#ef4444';
|
|
20268
|
+
var up = document.getElementById('rail-daemon-uptime');
|
|
20269
|
+
if (up && ds.uptimeMs) up.textContent = Math.round(ds.uptimeMs / 60000) + 'm';
|
|
20270
|
+
} catch { /* */ }
|
|
20271
|
+
|
|
20272
|
+
// Today's plan (compact)
|
|
20273
|
+
try {
|
|
20274
|
+
var rp = await apiFetch('/api/daily-plan');
|
|
20275
|
+
var dp = await rp.json();
|
|
20276
|
+
var planEl = document.getElementById('home-plan-content');
|
|
20277
|
+
if (planEl) {
|
|
20278
|
+
if (!dp || !dp.plan) {
|
|
20279
|
+
planEl.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No plan yet today.</div>';
|
|
20280
|
+
} else {
|
|
20281
|
+
var items = (dp.plan.items || []).slice(0, 4);
|
|
20282
|
+
if (items.length === 0) {
|
|
20283
|
+
planEl.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No items in today\\x27s plan.</div>';
|
|
20284
|
+
} else {
|
|
20285
|
+
planEl.innerHTML = items.map(function(it) {
|
|
20286
|
+
return '<div class="rail-row"><span class="label">' + esc(it.title || it.text || '') + '</span><span class="meta">' + esc(it.time || '') + '</span></div>';
|
|
20287
|
+
}).join('');
|
|
20288
|
+
}
|
|
20289
|
+
}
|
|
20290
|
+
}
|
|
20291
|
+
} catch {
|
|
20292
|
+
var pe = document.getElementById('home-plan-content');
|
|
20293
|
+
if (pe) pe.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No plan available.</div>';
|
|
20294
|
+
}
|
|
20295
|
+
|
|
20296
|
+
// Upcoming cron fires (next 3)
|
|
20297
|
+
try {
|
|
20298
|
+
var rc = await apiFetch('/api/cron');
|
|
20299
|
+
var dc = await rc.json();
|
|
20300
|
+
var jobs = (dc.jobs || []).filter(function(j) { return j.enabled && j.nextRun; });
|
|
20301
|
+
jobs.sort(function(a, b) { return new Date(a.nextRun).getTime() - new Date(b.nextRun).getTime(); });
|
|
20302
|
+
var top = jobs.slice(0, 3);
|
|
20303
|
+
var ue = document.getElementById('rail-upcoming');
|
|
20304
|
+
var uc = document.getElementById('rail-upcoming-count');
|
|
20305
|
+
if (uc) uc.textContent = String(jobs.length);
|
|
20306
|
+
if (ue) {
|
|
20307
|
+
ue.innerHTML = top.length ? top.map(function(j) {
|
|
20308
|
+
return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(j.name) + '</span><span class="meta">' + esc(timeUntil(j.nextRun)) + '</span></div>';
|
|
20309
|
+
}).join('') : '<div style="font-size:11px;color:var(--text-muted)">Nothing scheduled soon.</div>';
|
|
20310
|
+
}
|
|
20311
|
+
} catch { /* */ }
|
|
20312
|
+
|
|
20313
|
+
// Active unleashed runs
|
|
20314
|
+
try {
|
|
20315
|
+
var ru = await apiFetch('/api/unleashed');
|
|
20316
|
+
var du = await ru.json();
|
|
20317
|
+
var active = (du.tasks || []).filter(function(t) { return t.status === 'running'; });
|
|
20318
|
+
var ae = document.getElementById('rail-active');
|
|
20319
|
+
var ac = document.getElementById('rail-active-count');
|
|
20320
|
+
if (ac) {
|
|
20321
|
+
if (active.length > 0) { ac.style.display = ''; ac.textContent = String(active.length); }
|
|
20322
|
+
else ac.style.display = 'none';
|
|
20323
|
+
}
|
|
20324
|
+
if (ae) {
|
|
20325
|
+
ae.innerHTML = active.length ? active.map(function(t) {
|
|
20326
|
+
return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27workflows\\x27})"><span class="label">' + esc(t.name) + '</span><span class="meta">' + esc(t.phase || '') + '</span></div>';
|
|
20327
|
+
}).join('') : '<div style="font-size:11px;color:var(--text-muted)">Nothing running.</div>';
|
|
20328
|
+
}
|
|
20329
|
+
} catch { /* */ }
|
|
20330
|
+
|
|
20331
|
+
// Time saved (rough: cron runs * 5min + activity exchanges * 2min, this week)
|
|
20332
|
+
try {
|
|
20333
|
+
var rm = await apiFetch('/api/metrics?period=week');
|
|
20334
|
+
var dm = await rm.json();
|
|
20335
|
+
var minutes = ((dm.cronRuns || 0) * 5) + ((dm.exchanges || 0) * 2);
|
|
20336
|
+
var ts = document.getElementById('rail-time-saved');
|
|
20337
|
+
if (ts) {
|
|
20338
|
+
if (minutes >= 60) ts.innerHTML = '<div style="font-size:18px;font-weight:600">' + (minutes / 60).toFixed(1) + 'h</div><div style="font-size:11px;color:var(--text-muted)">across ' + (dm.cronRuns || 0) + ' cron runs + ' + (dm.exchanges || 0) + ' chats</div>';
|
|
20339
|
+
else ts.innerHTML = '<div style="font-size:18px;font-weight:600">' + minutes + 'm</div><div style="font-size:11px;color:var(--text-muted)">across ' + (dm.cronRuns || 0) + ' cron runs</div>';
|
|
20340
|
+
}
|
|
20341
|
+
} catch { /* */ }
|
|
20342
|
+
|
|
20343
|
+
// Approvals (self-improve proposals + pending skills)
|
|
20344
|
+
try {
|
|
20345
|
+
var rsi = await apiFetch('/api/self-improve');
|
|
20346
|
+
var dsi = await rsi.json();
|
|
20347
|
+
var pending = (dsi.proposals || []).filter(function(p) { return p.status === 'pending'; });
|
|
20348
|
+
var ae2 = document.getElementById('rail-approvals');
|
|
20349
|
+
var ac2 = document.getElementById('rail-approvals-count');
|
|
20350
|
+
if (ac2) {
|
|
20351
|
+
if (pending.length > 0) { ac2.style.display = ''; ac2.textContent = String(pending.length); }
|
|
20352
|
+
else ac2.style.display = 'none';
|
|
20353
|
+
}
|
|
20354
|
+
if (ae2) {
|
|
20355
|
+
if (pending.length === 0) ae2.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">Nothing pending.</div>';
|
|
20356
|
+
else ae2.innerHTML = pending.slice(0, 3).map(function(p) {
|
|
20357
|
+
return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27brain\\x27,{tab:\\x27learning\\x27})"><span class="label">' + esc(p.area || 'proposal') + ': ' + esc((p.target || '').slice(0, 40)) + '</span><span class="meta">' + esc(((p.score || 0) * 100).toFixed(0)) + '%</span></div>';
|
|
20358
|
+
}).join('');
|
|
20359
|
+
}
|
|
20360
|
+
} catch { /* */ }
|
|
20361
|
+
}
|
|
20362
|
+
|
|
20363
|
+
function timeUntil(iso) {
|
|
20364
|
+
if (!iso) return '';
|
|
20365
|
+
var ms = new Date(iso).getTime() - Date.now();
|
|
20366
|
+
if (ms < 0) return 'past';
|
|
20367
|
+
var min = Math.round(ms / 60000);
|
|
20368
|
+
if (min < 60) return 'in ' + min + 'm';
|
|
20369
|
+
if (min < 24 * 60) return 'in ' + Math.round(min / 60) + 'h';
|
|
20370
|
+
return 'in ' + Math.round(min / (24 * 60)) + 'd';
|
|
20371
|
+
}
|
|
20372
|
+
|
|
18091
20373
|
async function refreshAll() {
|
|
18092
20374
|
// Use batch init for core data — avoids concurrent requests that freeze the event loop
|
|
18093
20375
|
try {
|
|
@@ -18096,6 +20378,8 @@ async function refreshAll() {
|
|
|
18096
20378
|
if (d.status) refreshStatus(d.status);
|
|
18097
20379
|
if (d.activity) refreshActivity(false, d.activity);
|
|
18098
20380
|
if (d.office) refreshTeamNav(d.office);
|
|
20381
|
+
// Home rail data — fire and forget, doesn't block init render.
|
|
20382
|
+
if (currentPage === 'home') refreshHomeRail();
|
|
18099
20383
|
if (d.version) {
|
|
18100
20384
|
if (d.version.needsRestart && !_restartBannerShown) {
|
|
18101
20385
|
_restartBannerShown = true;
|
|
@@ -21465,6 +23749,9 @@ try {
|
|
|
21465
23749
|
toast('Daemon restarted \u2014 refreshing data...', 'info');
|
|
21466
23750
|
setTimeout(function() { refreshAll(); }, 1500);
|
|
21467
23751
|
}
|
|
23752
|
+
if (evt.type === 'builder') {
|
|
23753
|
+
try { _handleBuilderEvent(evt.data); } catch(e) { /* non-fatal */ }
|
|
23754
|
+
}
|
|
21468
23755
|
if (evt.type === 'deep_result') {
|
|
21469
23756
|
try {
|
|
21470
23757
|
var container = document.getElementById('chat-messages');
|
|
@@ -21573,6 +23860,14 @@ function getLoginPageHTML() {
|
|
|
21573
23860
|
margin-top: 24px; font-size: 12px;
|
|
21574
23861
|
color: #475569; text-align: center; line-height: 1.5;
|
|
21575
23862
|
}
|
|
23863
|
+
.remember-row {
|
|
23864
|
+
display: flex; align-items: center; gap: 8px;
|
|
23865
|
+
margin: 16px 0; font-size: 13px; color: #94a3b8;
|
|
23866
|
+
cursor: pointer; user-select: none;
|
|
23867
|
+
}
|
|
23868
|
+
.remember-row input[type="checkbox"] {
|
|
23869
|
+
accent-color: #f97316; width: 14px; height: 14px; cursor: pointer;
|
|
23870
|
+
}
|
|
21576
23871
|
</style>
|
|
21577
23872
|
</head>
|
|
21578
23873
|
<body>
|
|
@@ -21586,6 +23881,10 @@ function getLoginPageHTML() {
|
|
|
21586
23881
|
<input type="password" class="form-input" id="token-input"
|
|
21587
23882
|
placeholder="clem_XXXX-XXXX-XXXX" autocomplete="off" autofocus>
|
|
21588
23883
|
</div>
|
|
23884
|
+
<label class="remember-row">
|
|
23885
|
+
<input type="checkbox" id="remember-input">
|
|
23886
|
+
<span>Remember me on this device for 30 days</span>
|
|
23887
|
+
</label>
|
|
21589
23888
|
<button type="submit" class="btn-login" id="login-btn">Sign In</button>
|
|
21590
23889
|
<div class="error-msg" id="error-msg"></div>
|
|
21591
23890
|
</form>
|
|
@@ -21600,6 +23899,7 @@ function getLoginPageHTML() {
|
|
|
21600
23899
|
var btn = document.getElementById('login-btn');
|
|
21601
23900
|
var err = document.getElementById('error-msg');
|
|
21602
23901
|
var token = document.getElementById('token-input').value.trim();
|
|
23902
|
+
var remember = document.getElementById('remember-input').checked;
|
|
21603
23903
|
if (!token) return;
|
|
21604
23904
|
btn.disabled = true;
|
|
21605
23905
|
btn.textContent = 'Signing in...';
|
|
@@ -21608,7 +23908,7 @@ function getLoginPageHTML() {
|
|
|
21608
23908
|
var r = await fetch('/auth/login', {
|
|
21609
23909
|
method: 'POST',
|
|
21610
23910
|
headers: { 'Content-Type': 'application/json' },
|
|
21611
|
-
body: JSON.stringify({ token: token })
|
|
23911
|
+
body: JSON.stringify({ token: token, remember: remember })
|
|
21612
23912
|
});
|
|
21613
23913
|
var d = await r.json();
|
|
21614
23914
|
if (d.ok) {
|