codehere 0.5.0-alpha.3 → 0.5.0-alpha.5
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/application/use-cases/generate-final-report.js +22 -6
- package/dist/application/use-cases/generate-thread-title.js +120 -0
- package/dist/index.js +73 -0
- package/dist/infrastructure/orchestration/aider-adapter.js +59 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +30 -7
- package/dist/infrastructure/orchestration/codex-adapter.js +76 -0
- package/dist/infrastructure/orchestration/conductor-adapter.js +11 -1
- package/dist/infrastructure/orchestration/gemini-adapter.js +58 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +62 -0
- package/dist/infrastructure/orchestration/session-manager.js +20 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +102 -4
- package/dist/infrastructure/protocols/ui-page.js +509 -19
- package/dist/infrastructure/storage/cross-tool-agents.js +251 -0
- package/dist/infrastructure/storage/project-thread-migration.js +232 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +159 -0
- package/dist/infrastructure/storage/project-thread-reader.js +140 -0
- package/dist/infrastructure/storage/project-thread-title-backfill.js +182 -0
- package/package.json +4 -2
|
@@ -513,6 +513,7 @@ ${SHARED_STYLES}
|
|
|
513
513
|
<nav class="floating-nav">
|
|
514
514
|
<a href="/" class="logo">codehere</a>
|
|
515
515
|
<a href="/" class="nav-link">Tasks</a>
|
|
516
|
+
<a href="/threads" class="nav-link">Threads</a>
|
|
516
517
|
<a href="/history" class="nav-link active">History</a>
|
|
517
518
|
<span style="color:var(--fg-40);font-family:var(--mono);font-size:11px;">
|
|
518
519
|
<span id="live-dot" class="dot"></span><span id="conn">connecting</span>
|
|
@@ -1196,6 +1197,7 @@ ${SHARED_STYLES}
|
|
|
1196
1197
|
<nav class="floating-nav">
|
|
1197
1198
|
<a href="/" class="logo">codehere</a>
|
|
1198
1199
|
<a href="/" class="nav-link active">Tasks</a>
|
|
1200
|
+
<a href="/threads" class="nav-link">Threads</a>
|
|
1199
1201
|
<a href="/history" class="nav-link">History</a>
|
|
1200
1202
|
</nav>
|
|
1201
1203
|
|
|
@@ -1212,7 +1214,7 @@ ${SHARED_STYLES}
|
|
|
1212
1214
|
</div>
|
|
1213
1215
|
</div>
|
|
1214
1216
|
</div>
|
|
1215
|
-
|
|
1217
|
+
<!-- intentionally empty: multi-project sidebar arrives in v0.6.x -->
|
|
1216
1218
|
</aside>
|
|
1217
1219
|
|
|
1218
1220
|
<section class="thread-pane">
|
|
@@ -1569,22 +1571,10 @@ ${SHARED_STYLES}
|
|
|
1569
1571
|
localStorage.setItem('codehere:cwd', cwdInput.value.trim());
|
|
1570
1572
|
});
|
|
1571
1573
|
|
|
1572
|
-
// Iter
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
// flow handles spawning the task in that path.
|
|
1577
|
-
const newProjectBtn = document.getElementById('new-project-btn');
|
|
1578
|
-
if (newProjectBtn) {
|
|
1579
|
-
newProjectBtn.addEventListener('click', () => {
|
|
1580
|
-
cwdInput.value = '';
|
|
1581
|
-
localStorage.removeItem('codehere:cwd');
|
|
1582
|
-
// Smooth-scroll the intake form into view (project sidebar is on the left,
|
|
1583
|
-
// intake is on the right; on narrow screens the user might be scrolled).
|
|
1584
|
-
try { cwdInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* old browsers */ }
|
|
1585
|
-
cwdInput.focus();
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1574
|
+
// Iter 13 — sidebar 'add project' button + handler removed (was iter 12 work).
|
|
1575
|
+
// The handler cleared cwd input + scrolled + focused, but the label promised
|
|
1576
|
+
// a new sidebar entry that we do not ship until v0.6.x multi-project work.
|
|
1577
|
+
// Mismatch caused real founder confusion in iter-13 tasting. Removed cleanly.
|
|
1588
1578
|
|
|
1589
1579
|
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1590
1580
|
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
@@ -1851,7 +1841,11 @@ ${SHARED_STYLES}
|
|
|
1851
1841
|
const replyStatusEl = card.querySelector('.reply-status');
|
|
1852
1842
|
if (!session.canAppendMessage) {
|
|
1853
1843
|
threadInputEl.classList.add('disabled');
|
|
1854
|
-
|
|
1844
|
+
// All adapters wire appendMessage as of v0.6.0 Slices 1-3. This branch
|
|
1845
|
+
// now only fires for sessions evicted from in-memory tracking (the
|
|
1846
|
+
// canAppendMessage flag persists from spawn but the API endpoint
|
|
1847
|
+
// requires the session to still be live in SessionManager).
|
|
1848
|
+
replyTextEl.placeholder = 'Replies require a live session — re-spawn to continue this thread';
|
|
1855
1849
|
replyTextEl.disabled = true;
|
|
1856
1850
|
replySendBtn.disabled = true;
|
|
1857
1851
|
} else {
|
|
@@ -2230,7 +2224,7 @@ ${SHARED_STYLES}
|
|
|
2230
2224
|
});
|
|
2231
2225
|
const data = await res.json();
|
|
2232
2226
|
if (!res.ok) { alert('Failed: ' + (data.error || res.status)); return; }
|
|
2233
|
-
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor' });
|
|
2227
|
+
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor', canAppendMessage: data.canAppendMessage === true });
|
|
2234
2228
|
promptEl.value = '';
|
|
2235
2229
|
} catch (e) {
|
|
2236
2230
|
alert('Network error: ' + (e && e.message ? e.message : e));
|
|
@@ -2422,4 +2416,500 @@ ${SHARED_STYLES}
|
|
|
2422
2416
|
</script>
|
|
2423
2417
|
</body>
|
|
2424
2418
|
</html>`;
|
|
2419
|
+
/**
|
|
2420
|
+
* Project thread view — v0.6.0 M1 phase 1.
|
|
2421
|
+
*
|
|
2422
|
+
* Reads from /api/projects + /api/projects/:slug (shipped in 397a66a) and
|
|
2423
|
+
* renders a single per-project thread instead of session-card-per-task.
|
|
2424
|
+
* Phase 2 (next) promotes this to the dashboard default; this phase ships
|
|
2425
|
+
* it as a navigable surface so it can be tasted without breaking the
|
|
2426
|
+
* existing dashboard.
|
|
2427
|
+
*
|
|
2428
|
+
* Polling: 5s interval (matches existing /api/tasks polling cadence).
|
|
2429
|
+
* SSE upgrade is a v0.6.x patch.
|
|
2430
|
+
*/
|
|
2431
|
+
export const THREAD_HTML = `<!doctype html>
|
|
2432
|
+
<html lang="en">
|
|
2433
|
+
<head>
|
|
2434
|
+
<meta charset="utf-8" />
|
|
2435
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
2436
|
+
<title>codehere · Threads</title>
|
|
2437
|
+
<style>
|
|
2438
|
+
${SHARED_STYLES}
|
|
2439
|
+
* { box-sizing: border-box; }
|
|
2440
|
+
html, body { height: 100%; }
|
|
2441
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); font-size: 13px; line-height: 1.55; -webkit-font-smoothing: antialiased; }
|
|
2442
|
+
|
|
2443
|
+
/* ── Floating nav (matches dashboard exactly) ───────────────────────── */
|
|
2444
|
+
.floating-nav {
|
|
2445
|
+
position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
|
|
2446
|
+
z-index: 100;
|
|
2447
|
+
display: flex; align-items: center; gap: 4px;
|
|
2448
|
+
padding: 6px 10px;
|
|
2449
|
+
background: rgba(20,20,22,0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
|
2450
|
+
border: 1px solid var(--border);
|
|
2451
|
+
border-radius: 999px;
|
|
2452
|
+
font-family: var(--mono); font-size: 11.5px;
|
|
2453
|
+
}
|
|
2454
|
+
.floating-nav .logo { color: var(--accent); padding: 4px 10px; text-decoration: none; }
|
|
2455
|
+
.floating-nav .nav-link {
|
|
2456
|
+
color: var(--fg-50); padding: 4px 10px; border-radius: 999px;
|
|
2457
|
+
text-decoration: none;
|
|
2458
|
+
}
|
|
2459
|
+
.floating-nav .nav-link:hover { color: var(--fg); background: rgba(255,255,255,0.05); }
|
|
2460
|
+
.floating-nav .nav-link.active { color: var(--fg); background: rgba(255,154,118,0.10); }
|
|
2461
|
+
|
|
2462
|
+
/* ── Slack-shape shell (3-column — channels | thread | members) ─────── */
|
|
2463
|
+
.shell.slack-shape {
|
|
2464
|
+
max-width: 1440px;
|
|
2465
|
+
margin: 90px auto 24px;
|
|
2466
|
+
padding: 0 24px;
|
|
2467
|
+
display: grid;
|
|
2468
|
+
grid-template-columns: 200px 1fr 220px;
|
|
2469
|
+
gap: 16px;
|
|
2470
|
+
align-items: start;
|
|
2471
|
+
}
|
|
2472
|
+
@media (max-width: 1100px) {
|
|
2473
|
+
.shell.slack-shape { grid-template-columns: 1fr; }
|
|
2474
|
+
.shell.slack-shape .channels-pane, .shell.slack-shape .members-pane {
|
|
2475
|
+
position: static; order: 99;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
.channels-pane, .members-pane {
|
|
2479
|
+
background: rgba(255,255,255,0.025);
|
|
2480
|
+
border: 1px solid var(--border-soft);
|
|
2481
|
+
border-radius: 10px;
|
|
2482
|
+
padding: 14px 12px;
|
|
2483
|
+
position: sticky; top: 88px;
|
|
2484
|
+
font-family: var(--mono);
|
|
2485
|
+
z-index: 1;
|
|
2486
|
+
}
|
|
2487
|
+
.pane-head {
|
|
2488
|
+
font-size: 9.5px; letter-spacing: 0.16em;
|
|
2489
|
+
color: var(--fg-30); margin-bottom: 10px;
|
|
2490
|
+
text-transform: uppercase;
|
|
2491
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
2492
|
+
}
|
|
2493
|
+
.pane-head .pane-count { color: var(--fg-50); }
|
|
2494
|
+
.channel, .member {
|
|
2495
|
+
display: flex; align-items: center; gap: 8px;
|
|
2496
|
+
padding: 6px 8px; border-radius: 6px;
|
|
2497
|
+
font-size: 11.5px;
|
|
2498
|
+
cursor: pointer;
|
|
2499
|
+
color: var(--fg-70);
|
|
2500
|
+
border: 1px solid transparent;
|
|
2501
|
+
margin-bottom: 2px;
|
|
2502
|
+
}
|
|
2503
|
+
.channel:hover, .member:hover { background: rgba(255,255,255,0.04); color: var(--fg); }
|
|
2504
|
+
.channel.active { background: rgba(255,154,118,0.08); color: var(--fg); border-color: rgba(255,154,118,0.25); }
|
|
2505
|
+
.channel-glyph, .member-presence {
|
|
2506
|
+
color: var(--fg-40); flex: 0 0 14px; text-align: center;
|
|
2507
|
+
}
|
|
2508
|
+
.member-presence.ready { color: var(--allow); }
|
|
2509
|
+
.member-presence.warn { color: var(--warn); }
|
|
2510
|
+
.member-presence.offline { color: var(--fg-30); }
|
|
2511
|
+
.channel-name, .member-label {
|
|
2512
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2513
|
+
flex: 1; min-width: 0;
|
|
2514
|
+
}
|
|
2515
|
+
.member-cwd-hint, .channel-cwd-hint {
|
|
2516
|
+
font-size: 9.5px; color: var(--fg-30);
|
|
2517
|
+
margin-top: 4px; padding-left: 22px;
|
|
2518
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2519
|
+
}
|
|
2520
|
+
.pane-empty {
|
|
2521
|
+
color: var(--fg-30); font-size: 10.5px;
|
|
2522
|
+
padding: 8px; text-align: center;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/* ── Thread pane (center) ───────────────────────────────────────────── */
|
|
2526
|
+
.thread-pane { min-width: 0; }
|
|
2527
|
+
|
|
2528
|
+
.meta-panel {
|
|
2529
|
+
border: 1px solid var(--border); background: rgba(255,255,255,0.02);
|
|
2530
|
+
border-radius: 10px; padding: 18px 20px; margin-bottom: 16px;
|
|
2531
|
+
}
|
|
2532
|
+
.meta-panel h1 {
|
|
2533
|
+
font-size: 22px; font-weight: 600; color: var(--fg);
|
|
2534
|
+
margin: 0 0 6px 0; line-height: 1.3;
|
|
2535
|
+
}
|
|
2536
|
+
.meta-panel h1.untitled { color: var(--fg-40); font-style: italic; font-weight: 400; }
|
|
2537
|
+
.meta-panel .cwd {
|
|
2538
|
+
font-family: var(--mono); font-size: 11px; color: var(--fg-50);
|
|
2539
|
+
margin-bottom: 12px; word-break: break-all;
|
|
2540
|
+
}
|
|
2541
|
+
.meta-panel .stats {
|
|
2542
|
+
display: flex; gap: 22px; font-family: var(--mono); font-size: 11px; color: var(--fg-50);
|
|
2543
|
+
flex-wrap: wrap;
|
|
2544
|
+
}
|
|
2545
|
+
.meta-panel .stats .stat { display: flex; gap: 6px; }
|
|
2546
|
+
.meta-panel .stats .stat .label { color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
|
|
2547
|
+
.meta-panel .stats .stat .val { color: var(--fg); font-weight: 600; }
|
|
2548
|
+
|
|
2549
|
+
.section-label {
|
|
2550
|
+
padding: 8px 4px;
|
|
2551
|
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
|
|
2552
|
+
color: var(--fg-40); text-transform: uppercase;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/* ── Task entries ───────────────────────────────────────────────────── */
|
|
2556
|
+
.task {
|
|
2557
|
+
border: 1px solid var(--border); background: rgba(255,255,255,0.015);
|
|
2558
|
+
border-radius: 8px; padding: 14px 16px; margin-bottom: 10px;
|
|
2559
|
+
}
|
|
2560
|
+
.task .row1 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
2561
|
+
.task .agent {
|
|
2562
|
+
font-family: var(--mono); font-size: 10px; padding: 2px 8px; border-radius: 10px;
|
|
2563
|
+
background: rgba(120, 180, 255, 0.10); color: #9bbfff; border: 1px solid rgba(120, 180, 255, 0.18);
|
|
2564
|
+
}
|
|
2565
|
+
.task .status {
|
|
2566
|
+
font-family: var(--mono); font-size: 10px; padding: 2px 8px; border-radius: 10px;
|
|
2567
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
2568
|
+
}
|
|
2569
|
+
.task .status.completed { background: rgba(40, 200, 64, 0.10); color: var(--allow); border: 1px solid rgba(40, 200, 64, 0.18); }
|
|
2570
|
+
.task .status.running { background: rgba(255, 200, 0, 0.10); color: var(--warn); border: 1px solid rgba(255, 200, 0, 0.18); }
|
|
2571
|
+
.task .status.error, .task .status.failed { background: rgba(220, 60, 60, 0.10); color: var(--block); border: 1px solid rgba(220, 60, 60, 0.18); }
|
|
2572
|
+
.task .status.abandoned { background: rgba(255,255,255,0.04); color: var(--fg-40); border: 1px solid var(--border); }
|
|
2573
|
+
.task .ts { font-family: var(--mono); font-size: 11px; color: var(--fg-40); margin-left: auto; }
|
|
2574
|
+
.task .prompt { color: var(--fg); font-size: 13px; line-height: 1.5; word-wrap: break-word; }
|
|
2575
|
+
.task .duration { font-family: var(--mono); font-size: 11px; color: var(--fg-50); margin-top: 8px; }
|
|
2576
|
+
.task .duration b { color: var(--fg-70); font-weight: 600; }
|
|
2577
|
+
.task .steps { margin-top: 8px; padding-left: 12px; border-left: 2px solid var(--border); font-family: var(--mono); font-size: 11px; color: var(--fg-50); }
|
|
2578
|
+
.task .steps .step { padding: 2px 0; }
|
|
2579
|
+
.task .steps .step.done::before { content: '✓ '; color: var(--allow); }
|
|
2580
|
+
|
|
2581
|
+
.empty {
|
|
2582
|
+
text-align: center; padding: 60px 24px; color: var(--fg-50); font-family: var(--mono); font-size: 13px;
|
|
2583
|
+
}
|
|
2584
|
+
.empty .pill {
|
|
2585
|
+
display: inline-block; padding: 6px 12px; border: 1px solid var(--border); border-radius: 16px;
|
|
2586
|
+
margin-bottom: 12px; font-size: 11px;
|
|
2587
|
+
}
|
|
2588
|
+
.empty .hint { font-size: 12px; color: var(--fg-40); }
|
|
2589
|
+
.empty a { color: var(--accent); }
|
|
2590
|
+
|
|
2591
|
+
/* ── Reply bar at thread bottom (Slack-shape) ───────────────────────── */
|
|
2592
|
+
.reply-bar {
|
|
2593
|
+
margin-top: 16px;
|
|
2594
|
+
border: 1px solid var(--border); background: rgba(255,255,255,0.02);
|
|
2595
|
+
border-radius: 10px; padding: 12px 14px;
|
|
2596
|
+
}
|
|
2597
|
+
.reply-bar.disabled { opacity: 0.6; }
|
|
2598
|
+
.reply-bar .reply-label {
|
|
2599
|
+
font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.16em;
|
|
2600
|
+
color: var(--fg-40); text-transform: uppercase; margin-bottom: 8px;
|
|
2601
|
+
}
|
|
2602
|
+
.reply-bar textarea {
|
|
2603
|
+
width: 100%; min-height: 60px; padding: 10px 12px;
|
|
2604
|
+
background: var(--bg-2); color: var(--fg); border: 1px solid var(--border);
|
|
2605
|
+
border-radius: 6px; font-family: var(--sans); font-size: 13px; line-height: 1.5;
|
|
2606
|
+
resize: vertical; outline: none;
|
|
2607
|
+
}
|
|
2608
|
+
.reply-bar textarea:focus { border-color: var(--accent); }
|
|
2609
|
+
.reply-bar textarea::placeholder { color: var(--fg-30); }
|
|
2610
|
+
.reply-bar .row {
|
|
2611
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
2612
|
+
margin-top: 8px;
|
|
2613
|
+
}
|
|
2614
|
+
.reply-bar .hint { font-family: var(--mono); font-size: 11px; color: var(--fg-40); }
|
|
2615
|
+
.reply-bar button {
|
|
2616
|
+
background: rgba(255,154,118,0.10); color: var(--accent); border: 1px solid rgba(255,154,118,0.3);
|
|
2617
|
+
padding: 6px 16px; border-radius: 6px; font-family: var(--mono); font-size: 12px; cursor: pointer;
|
|
2618
|
+
}
|
|
2619
|
+
.reply-bar button:hover { background: rgba(255,154,118,0.18); }
|
|
2620
|
+
.reply-bar button:disabled { opacity: 0.4; cursor: default; }
|
|
2621
|
+
.reply-bar .status { font-family: var(--mono); font-size: 11px; color: var(--fg-50); margin-top: 8px; }
|
|
2622
|
+
.reply-bar .status.error { color: var(--block); }
|
|
2623
|
+
|
|
2624
|
+
code { font-family: var(--mono); background: rgba(255,255,255,0.05); padding: 1px 6px; border-radius: 4px; }
|
|
2625
|
+
</style>
|
|
2626
|
+
</head>
|
|
2627
|
+
<body>
|
|
2628
|
+
<nav class="floating-nav">
|
|
2629
|
+
<a href="/" class="logo">codehere</a>
|
|
2630
|
+
<a href="/" class="nav-link">Tasks</a>
|
|
2631
|
+
<a href="/threads" class="nav-link active">Threads</a>
|
|
2632
|
+
<a href="/history" class="nav-link">History</a>
|
|
2633
|
+
</nav>
|
|
2634
|
+
|
|
2635
|
+
<div class="shell slack-shape">
|
|
2636
|
+
|
|
2637
|
+
<aside class="channels-pane">
|
|
2638
|
+
<div class="pane-head"><span>Projects</span><span class="pane-count" id="projects-count">0</span></div>
|
|
2639
|
+
<div id="projects-list"><div class="pane-empty">Loading…</div></div>
|
|
2640
|
+
</aside>
|
|
2641
|
+
|
|
2642
|
+
<section class="thread-pane">
|
|
2643
|
+
<div id="thread-container"><div class="empty"><div class="pill">No project selected</div></div></div>
|
|
2644
|
+
|
|
2645
|
+
<!-- v0.6.0 Slice 4 — single reply input at thread bottom (Slack-shape).
|
|
2646
|
+
Routes to the LAST task's session in the thread via POST /api/task/<lastSessionId>/message. -->
|
|
2647
|
+
<div class="reply-bar disabled" id="reply-bar" style="display:none">
|
|
2648
|
+
<div class="reply-label">Reply to thread</div>
|
|
2649
|
+
<textarea id="reply-text" placeholder="Reply to this thread… (Cmd+Enter / Ctrl+Enter to send)" rows="2" disabled></textarea>
|
|
2650
|
+
<div class="row">
|
|
2651
|
+
<span class="hint" id="reply-hint">No active session — spawn a task first</span>
|
|
2652
|
+
<button type="button" id="reply-send" disabled>Send</button>
|
|
2653
|
+
</div>
|
|
2654
|
+
<div class="status" id="reply-status" style="display:none"></div>
|
|
2655
|
+
</div>
|
|
2656
|
+
</section>
|
|
2657
|
+
|
|
2658
|
+
<aside class="members-pane">
|
|
2659
|
+
<div class="pane-head"><span>Agents</span><span class="pane-count" id="agents-count">0</span></div>
|
|
2660
|
+
<div id="agents-list"><div class="pane-empty">Loading…</div></div>
|
|
2661
|
+
</aside>
|
|
2662
|
+
|
|
2663
|
+
</div>
|
|
2664
|
+
|
|
2665
|
+
<script>
|
|
2666
|
+
(function () {
|
|
2667
|
+
const projectsList = document.getElementById('projects-list');
|
|
2668
|
+
const projectsCount = document.getElementById('projects-count');
|
|
2669
|
+
const agentsList = document.getElementById('agents-list');
|
|
2670
|
+
const agentsCount = document.getElementById('agents-count');
|
|
2671
|
+
const container = document.getElementById('thread-container');
|
|
2672
|
+
const replyBar = document.getElementById('reply-bar');
|
|
2673
|
+
const replyText = document.getElementById('reply-text');
|
|
2674
|
+
const replySend = document.getElementById('reply-send');
|
|
2675
|
+
const replyHint = document.getElementById('reply-hint');
|
|
2676
|
+
const replyStatus = document.getElementById('reply-status');
|
|
2677
|
+
|
|
2678
|
+
let currentSlug = null;
|
|
2679
|
+
let lastSessionId = null;
|
|
2680
|
+
|
|
2681
|
+
function escapeHtml(s) {
|
|
2682
|
+
return String(s == null ? '' : s)
|
|
2683
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
2684
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
2685
|
+
}
|
|
2686
|
+
function timeAgo(ts) {
|
|
2687
|
+
if (!ts) return '';
|
|
2688
|
+
const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
2689
|
+
if (s < 60) return s + 's ago';
|
|
2690
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
2691
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
2692
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
2693
|
+
}
|
|
2694
|
+
function fmtTime(ts) {
|
|
2695
|
+
if (!ts) return '';
|
|
2696
|
+
const d = new Date(ts);
|
|
2697
|
+
return d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
2698
|
+
}
|
|
2699
|
+
function fmtDuration(ms) {
|
|
2700
|
+
if (ms == null) return '';
|
|
2701
|
+
if (ms < 1000) return ms + 'ms';
|
|
2702
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
2703
|
+
return Math.floor(ms / 60000) + 'm ' + Math.floor((ms % 60000) / 1000) + 's';
|
|
2704
|
+
}
|
|
2705
|
+
function shortPath(p) {
|
|
2706
|
+
if (!p) return '~';
|
|
2707
|
+
const parts = p.split('/');
|
|
2708
|
+
return parts.length <= 3 ? p : '…/' + parts.slice(-2).join('/');
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
function setReplyState(sessionId, agent) {
|
|
2712
|
+
lastSessionId = sessionId;
|
|
2713
|
+
if (sessionId) {
|
|
2714
|
+
replyBar.style.display = 'block';
|
|
2715
|
+
replyBar.classList.remove('disabled');
|
|
2716
|
+
replyText.disabled = false;
|
|
2717
|
+
replySend.disabled = false;
|
|
2718
|
+
replyHint.textContent = 'Replying to last task (' + (agent || 'unknown') + ')';
|
|
2719
|
+
} else {
|
|
2720
|
+
replyBar.style.display = 'block';
|
|
2721
|
+
replyBar.classList.add('disabled');
|
|
2722
|
+
replyText.disabled = true;
|
|
2723
|
+
replySend.disabled = true;
|
|
2724
|
+
replyHint.textContent = 'No active session — spawn a task first';
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
async function sendReply() {
|
|
2729
|
+
const text = (replyText.value || '').trim();
|
|
2730
|
+
if (!text || !lastSessionId) return;
|
|
2731
|
+
replySend.disabled = true;
|
|
2732
|
+
replySend.textContent = '…';
|
|
2733
|
+
replyStatus.style.display = 'none';
|
|
2734
|
+
replyStatus.classList.remove('error');
|
|
2735
|
+
try {
|
|
2736
|
+
const r = await fetch('/api/task/' + encodeURIComponent(lastSessionId) + '/message', {
|
|
2737
|
+
method: 'POST',
|
|
2738
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2739
|
+
body: JSON.stringify({ text }),
|
|
2740
|
+
});
|
|
2741
|
+
if (!r.ok) {
|
|
2742
|
+
const data = await r.json().catch(() => ({}));
|
|
2743
|
+
throw new Error(data.error || ('HTTP ' + r.status));
|
|
2744
|
+
}
|
|
2745
|
+
const data = await r.json();
|
|
2746
|
+
replyText.value = '';
|
|
2747
|
+
replyStatus.style.display = 'block';
|
|
2748
|
+
replyStatus.textContent = 'Reply sent in ' + fmtDuration(data.durationMs) + ' — refreshing thread…';
|
|
2749
|
+
if (currentSlug) loadThread(currentSlug);
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
replyStatus.style.display = 'block';
|
|
2752
|
+
replyStatus.classList.add('error');
|
|
2753
|
+
replyStatus.textContent = 'Send failed: ' + (err && err.message ? err.message : err);
|
|
2754
|
+
} finally {
|
|
2755
|
+
replySend.disabled = false;
|
|
2756
|
+
replySend.textContent = 'Send';
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
replySend.addEventListener('click', sendReply);
|
|
2761
|
+
replyText.addEventListener('keydown', (e) => {
|
|
2762
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); sendReply(); }
|
|
2763
|
+
});
|
|
2764
|
+
|
|
2765
|
+
function renderThread(thread) {
|
|
2766
|
+
if (!thread) {
|
|
2767
|
+
container.innerHTML = '<div class="empty"><div class="pill">Project not found</div></div>';
|
|
2768
|
+
setReplyState(null, null);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const titleHtml = thread.title
|
|
2772
|
+
? '<h1>' + escapeHtml(thread.title) + '</h1>'
|
|
2773
|
+
: '<h1 class="untitled">(untitled — Title agent will name this on first new spawn)</h1>';
|
|
2774
|
+
const cwdHtml = thread.cwd ? '<div class="cwd">' + escapeHtml(thread.cwd) + '</div>' : '';
|
|
2775
|
+
const lastActivity = thread.lastActivityAt ? fmtTime(thread.lastActivityAt) + ' · ' + timeAgo(thread.lastActivityAt) : '—';
|
|
2776
|
+
|
|
2777
|
+
let html = '<div class="meta-panel">' +
|
|
2778
|
+
titleHtml +
|
|
2779
|
+
cwdHtml +
|
|
2780
|
+
'<div class="stats">' +
|
|
2781
|
+
'<div class="stat"><span class="label">Tasks</span> <span class="val">' + thread.taskCount + '</span></div>' +
|
|
2782
|
+
'<div class="stat"><span class="label">Last activity</span> <span class="val">' + escapeHtml(lastActivity) + '</span></div>' +
|
|
2783
|
+
'<div class="stat"><span class="label">Slug</span> <span class="val">' + escapeHtml(thread.slug) + '</span></div>' +
|
|
2784
|
+
'</div>' +
|
|
2785
|
+
'</div>';
|
|
2786
|
+
|
|
2787
|
+
if (!thread.tasks || thread.tasks.length === 0) {
|
|
2788
|
+
html += '<div class="empty"><div class="pill">No tasks yet</div><div class="hint">Spawn one from the <a href="/">Tasks</a> page.</div></div>';
|
|
2789
|
+
container.innerHTML = html;
|
|
2790
|
+
setReplyState(null, null);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
html += '<div class="section-label">Thread · ' + thread.tasks.length + ' turn' + (thread.tasks.length === 1 ? '' : 's') + '</div>';
|
|
2795
|
+
|
|
2796
|
+
for (const t of thread.tasks) {
|
|
2797
|
+
const status = (t.status || '').toLowerCase();
|
|
2798
|
+
const stepsHtml = (t.steps && t.steps.length)
|
|
2799
|
+
? '<div class="steps">' + t.steps.map(s => '<div class="step ' + escapeHtml((s.state||'').toLowerCase()) + '">' + escapeHtml(s.label || '') + '</div>').join('') + '</div>'
|
|
2800
|
+
: '';
|
|
2801
|
+
const durHtml = (t.durationMs != null)
|
|
2802
|
+
? '<div class="duration">Duration: <b>' + fmtDuration(t.durationMs) + '</b></div>'
|
|
2803
|
+
: (status === 'running' ? '<div class="duration">Running…</div>' : '');
|
|
2804
|
+
|
|
2805
|
+
html += '<div class="task">' +
|
|
2806
|
+
'<div class="row1">' +
|
|
2807
|
+
'<span class="agent">' + escapeHtml(t.agent || 'unknown') + '</span>' +
|
|
2808
|
+
'<span class="status ' + escapeHtml(status) + '">' + escapeHtml(status || 'unknown') + '</span>' +
|
|
2809
|
+
'<span class="ts">' + escapeHtml(fmtTime(t.startedAt)) + ' · ' + escapeHtml(timeAgo(t.startedAt)) + '</span>' +
|
|
2810
|
+
'</div>' +
|
|
2811
|
+
'<div class="prompt">' + escapeHtml(t.prompt || '(no prompt)') + '</div>' +
|
|
2812
|
+
durHtml +
|
|
2813
|
+
stepsHtml +
|
|
2814
|
+
'</div>';
|
|
2815
|
+
}
|
|
2816
|
+
container.innerHTML = html;
|
|
2817
|
+
const lastTask = thread.tasks[thread.tasks.length - 1];
|
|
2818
|
+
setReplyState(lastTask.sessionId, lastTask.agent);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function renderProjects(list) {
|
|
2822
|
+
projectsCount.textContent = String(list.length);
|
|
2823
|
+
if (list.length === 0) {
|
|
2824
|
+
projectsList.innerHTML = '<div class="pane-empty">No projects yet</div>';
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
projectsList.innerHTML = list.map(p => {
|
|
2828
|
+
const active = p.slug === currentSlug ? ' active' : '';
|
|
2829
|
+
const name = escapeHtml(p.title || '(untitled)');
|
|
2830
|
+
const cwdHint = escapeHtml(shortPath(p.cwd));
|
|
2831
|
+
return '<div class="channel' + active + '" data-slug="' + escapeHtml(p.slug) + '">' +
|
|
2832
|
+
'<span class="channel-glyph">#</span>' +
|
|
2833
|
+
'<div style="flex:1;min-width:0">' +
|
|
2834
|
+
'<div class="channel-name">' + name + '</div>' +
|
|
2835
|
+
'<div class="channel-cwd-hint">' + cwdHint + '</div>' +
|
|
2836
|
+
'</div>' +
|
|
2837
|
+
'</div>';
|
|
2838
|
+
}).join('');
|
|
2839
|
+
projectsList.querySelectorAll('.channel').forEach(el => {
|
|
2840
|
+
el.addEventListener('click', () => {
|
|
2841
|
+
const slug = el.getAttribute('data-slug');
|
|
2842
|
+
if (!slug || slug === currentSlug) return;
|
|
2843
|
+
currentSlug = slug;
|
|
2844
|
+
renderProjects(list); // re-highlight active
|
|
2845
|
+
loadThread(slug);
|
|
2846
|
+
const url = new URL(window.location.href);
|
|
2847
|
+
url.searchParams.set('project', slug);
|
|
2848
|
+
history.replaceState(null, '', url.toString());
|
|
2849
|
+
});
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function presenceClass(state) {
|
|
2854
|
+
if (state === 'ready') return 'ready';
|
|
2855
|
+
if (state === 'needs_install' || state === 'needs_auth' || state === 'billed_key_required') return 'warn';
|
|
2856
|
+
return 'offline';
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function renderAgents(adapters) {
|
|
2860
|
+
agentsCount.textContent = String(adapters.length);
|
|
2861
|
+
agentsList.innerHTML = adapters.map(a => {
|
|
2862
|
+
const cls = presenceClass(a.state);
|
|
2863
|
+
return '<div class="member">' +
|
|
2864
|
+
'<span class="member-presence ' + cls + '">●</span>' +
|
|
2865
|
+
'<span class="member-label">' + escapeHtml(a.label || a.id) + '</span>' +
|
|
2866
|
+
'</div>';
|
|
2867
|
+
}).join('');
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
function loadThread(slug) {
|
|
2871
|
+
if (!slug) { container.innerHTML = '<div class="empty"><div class="pill">No project selected</div></div>'; setReplyState(null, null); return; }
|
|
2872
|
+
fetch('/api/projects/' + encodeURIComponent(slug))
|
|
2873
|
+
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
|
2874
|
+
.then(data => renderThread(data.thread))
|
|
2875
|
+
.catch(err => { container.innerHTML = '<div class="empty"><div class="pill">Load error: ' + escapeHtml(String(err)) + '</div></div>'; });
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
function loadProjects() {
|
|
2879
|
+
fetch('/api/projects')
|
|
2880
|
+
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
|
2881
|
+
.then(data => {
|
|
2882
|
+
const list = data.projects || [];
|
|
2883
|
+
renderProjects(list);
|
|
2884
|
+
if (list.length > 0 && !currentSlug) {
|
|
2885
|
+
const urlSlug = new URLSearchParams(window.location.search).get('project');
|
|
2886
|
+
const slugToUse = (urlSlug && list.some(p => p.slug === urlSlug)) ? urlSlug : list[0].slug;
|
|
2887
|
+
currentSlug = slugToUse;
|
|
2888
|
+
renderProjects(list);
|
|
2889
|
+
loadThread(slugToUse);
|
|
2890
|
+
} else if (list.length === 0) {
|
|
2891
|
+
container.innerHTML = '<div class="empty"><div class="pill">No projects yet</div><div class="hint">Spawn a task from the <a href="/">Tasks</a> page — it will appear here as a project thread.</div></div>';
|
|
2892
|
+
setReplyState(null, null);
|
|
2893
|
+
}
|
|
2894
|
+
})
|
|
2895
|
+
.catch(() => {});
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function loadAgents() {
|
|
2899
|
+
fetch('/api/readiness/agents')
|
|
2900
|
+
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
|
2901
|
+
.then(data => renderAgents(data.agents || []))
|
|
2902
|
+
.catch(() => {});
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
loadProjects();
|
|
2906
|
+
loadAgents();
|
|
2907
|
+
setInterval(() => {
|
|
2908
|
+
if (currentSlug) loadThread(currentSlug);
|
|
2909
|
+
if (Math.random() < 0.25) { loadProjects(); loadAgents(); }
|
|
2910
|
+
}, 5000);
|
|
2911
|
+
})();
|
|
2912
|
+
</script>
|
|
2913
|
+
</body>
|
|
2914
|
+
</html>`;
|
|
2425
2915
|
//# sourceMappingURL=ui-page.js.map
|