codehere 0.5.0-alpha.4 → 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 +18 -0
- package/dist/infrastructure/orchestration/aider-adapter.js +59 -0
- 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 +97 -4
- package/dist/infrastructure/protocols/ui-page.js +503 -1
- package/dist/infrastructure/storage/cross-tool-agents.js +251 -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
|
@@ -25,9 +25,11 @@ import { fileURLToPath } from 'url';
|
|
|
25
25
|
import { collectFilesForMCP, scoreFileForMCP } from './mcp-helpers.js';
|
|
26
26
|
import { scanPayload, calculateRiskScore, AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
|
|
27
27
|
import { getVersion } from '../../utils/version.js';
|
|
28
|
-
import { TASK_HTML, HISTORY_HTML } from './ui-page.js';
|
|
28
|
+
import { TASK_HTML, HISTORY_HTML, THREAD_HTML } from './ui-page.js';
|
|
29
29
|
import { SessionManager, availableAgents, readinessForAllAgents, getAdapter, allAdapters, } from '../orchestration/session-manager.js';
|
|
30
30
|
import { isAbsolute } from 'path';
|
|
31
|
+
import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
|
|
32
|
+
import { listProjects, readProjectThread } from '../storage/project-thread-reader.js';
|
|
31
33
|
import { installAgent } from '../orchestration/agent-installer.js';
|
|
32
34
|
import { detectReadiness } from '../orchestration/readiness.js';
|
|
33
35
|
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
@@ -81,6 +83,28 @@ const TOOLS = [
|
|
|
81
83
|
// ── Tool handlers ────────────────────────────────────────────────────────────
|
|
82
84
|
import { homedir } from 'os';
|
|
83
85
|
const AUDIT_FILE_PATH = join(homedir(), '.codehere', 'audit', 'hook-events.jsonl');
|
|
86
|
+
/**
|
|
87
|
+
* Dev-only behavior-observer injection (v0.6.0 — elu joined the team roster
|
|
88
|
+
* 2026-05-03 as behavior reviewer). Activated only when CODEHERE_DEV_ELU=1
|
|
89
|
+
* is set in the founder's local env; never in shipped releases. The script's
|
|
90
|
+
* public key is meant for client-side embedding (analytics tracking key, not
|
|
91
|
+
* a secret) so committing the URL is safe.
|
|
92
|
+
*
|
|
93
|
+
* Why runtime injection instead of editing TASK_HTML/HISTORY_HTML directly:
|
|
94
|
+
* the templates stay identical between dev and release builds. The only
|
|
95
|
+
* delta is the env var. This keeps the release surface clean (zero-telemetry
|
|
96
|
+
* value per PRD §1.5 #2 stays intact) while letting elu observe founder
|
|
97
|
+
* dogfood sessions.
|
|
98
|
+
*
|
|
99
|
+
* Removal trigger lives in team_roster.md memory: if elu produces zero
|
|
100
|
+
* unique findings within one month, remove this helper + roster entry.
|
|
101
|
+
*/
|
|
102
|
+
const ELU_DEV_SCRIPT = '<script async src="https://elu.dev/v1/elu_pk_live_HrKo9yFFLx03tz9xuSKFcE2Jpv.js"></script>';
|
|
103
|
+
function withDevElu(html) {
|
|
104
|
+
if (process.env.CODEHERE_DEV_ELU !== '1')
|
|
105
|
+
return html;
|
|
106
|
+
return html.replace('</head>', `${ELU_DEV_SCRIPT}\n</head>`);
|
|
107
|
+
}
|
|
84
108
|
function handleTrust(params) {
|
|
85
109
|
const { filepath, code, verbose } = params;
|
|
86
110
|
let content;
|
|
@@ -270,13 +294,19 @@ export function startMCPServer(options = {}) {
|
|
|
270
294
|
// v0.3 primary UI: task intake + active sessions
|
|
271
295
|
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
|
|
272
296
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
273
|
-
res.end(TASK_HTML);
|
|
297
|
+
res.end(withDevElu(TASK_HTML));
|
|
274
298
|
return;
|
|
275
299
|
}
|
|
276
300
|
// History / audit feed (what we built Day 0; now secondary)
|
|
277
301
|
if (req.method === 'GET' && (req.url === '/history' || req.url === '/history/')) {
|
|
278
302
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
279
|
-
res.end(HISTORY_HTML);
|
|
303
|
+
res.end(withDevElu(HISTORY_HTML));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// v0.6.0 M1 — project thread view; consumes /api/projects + /api/projects/:slug
|
|
307
|
+
if (req.method === 'GET' && (req.url === '/threads' || req.url === '/threads/' || req.url?.startsWith('/threads?'))) {
|
|
308
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
309
|
+
res.end(withDevElu(THREAD_HTML));
|
|
280
310
|
return;
|
|
281
311
|
}
|
|
282
312
|
// Backwards-compat: /ui used to be the Mission Control audit view — now at /history
|
|
@@ -453,8 +483,28 @@ export function startMCPServer(options = {}) {
|
|
|
453
483
|
const all = readinessForAllAgents();
|
|
454
484
|
const auto = all.find((a) => a.id === 'conductor');
|
|
455
485
|
const agents = all.filter((a) => a.id !== 'conductor');
|
|
486
|
+
// v0.6.0 M8 — surface cross-tool agent definitions (.opencode/agents/,
|
|
487
|
+
// .claude/agents/, .codehere/agents/) alongside the built-in adapters.
|
|
488
|
+
// These don't have a `state` (ready/needs_install) — they're definitions
|
|
489
|
+
// not runtimes. UI can render them as "available specialists" tagged
|
|
490
|
+
// with their source format. Best-effort: failures fall back to no
|
|
491
|
+
// cross-tool agents listed.
|
|
492
|
+
let crossTool = [];
|
|
493
|
+
try {
|
|
494
|
+
const result = readCrossToolAgents(process.cwd());
|
|
495
|
+
crossTool = result.agents.map((a) => ({
|
|
496
|
+
name: a.name,
|
|
497
|
+
description: a.description.length > 240 ? a.description.slice(0, 237) + '…' : a.description,
|
|
498
|
+
sourceFormat: a.sourceFormat,
|
|
499
|
+
mode: a.mode,
|
|
500
|
+
sourcePath: a.sourcePath,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// best-effort; ignore parse failures
|
|
505
|
+
}
|
|
456
506
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
457
|
-
res.end(JSON.stringify({ agents, auto }));
|
|
507
|
+
res.end(JSON.stringify({ agents, auto, crossTool }));
|
|
458
508
|
}
|
|
459
509
|
catch (e) {
|
|
460
510
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -475,6 +525,49 @@ export function startMCPServer(options = {}) {
|
|
|
475
525
|
}
|
|
476
526
|
return;
|
|
477
527
|
}
|
|
528
|
+
// ── v0.6.0 M1 project thread routes ──────────────────────────────────────
|
|
529
|
+
// GET /api/projects — list all known projects (slug, cwd, title, taskCount, lastActivityAt)
|
|
530
|
+
// GET /api/projects/:slug — single project thread (meta + tasks merged by sessionId)
|
|
531
|
+
// Reader source of truth: ~/.codehere/projects/<slug>/thread.jsonl (per-project append-only log
|
|
532
|
+
// populated by migration + forward-mirror). Drives the new thread-view UI.
|
|
533
|
+
if (req.method === 'GET' && req.url === '/api/projects') {
|
|
534
|
+
try {
|
|
535
|
+
const projects = listProjects();
|
|
536
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
537
|
+
res.end(JSON.stringify({ projects }));
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
541
|
+
res.end(JSON.stringify({ error: 'list_projects_failed', detail: String(e) }));
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (req.method === 'GET' && req.url?.startsWith('/api/projects/')) {
|
|
546
|
+
try {
|
|
547
|
+
// Slug is the segment after /api/projects/. Strip optional query string.
|
|
548
|
+
const slug = req.url.replace('/api/projects/', '').split('?')[0];
|
|
549
|
+
// Slug shape is sha1[:12] (12 hex chars) OR the literal 'unknown' bucket.
|
|
550
|
+
// Reject anything else to keep the surface tight (and stop ../ traversal).
|
|
551
|
+
if (!/^([a-f0-9]{12}|unknown)$/.test(slug)) {
|
|
552
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
553
|
+
res.end(JSON.stringify({ error: 'invalid_slug' }));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const thread = readProjectThread(slug);
|
|
557
|
+
if (!thread) {
|
|
558
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
559
|
+
res.end(JSON.stringify({ error: 'project_not_found', slug }));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
563
|
+
res.end(JSON.stringify({ thread }));
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
567
|
+
res.end(JSON.stringify({ error: 'read_project_failed', detail: String(e) }));
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
478
571
|
// ── F1 task orchestration routes ─────────────────────────────────────────
|
|
479
572
|
// POST /api/task — { prompt, cwd? } → { sessionId }
|
|
480
573
|
// GET /api/tasks — list all sessions
|
|
@@ -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
|
|
|
@@ -1839,7 +1841,11 @@ ${SHARED_STYLES}
|
|
|
1839
1841
|
const replyStatusEl = card.querySelector('.reply-status');
|
|
1840
1842
|
if (!session.canAppendMessage) {
|
|
1841
1843
|
threadInputEl.classList.add('disabled');
|
|
1842
|
-
|
|
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';
|
|
1843
1849
|
replyTextEl.disabled = true;
|
|
1844
1850
|
replySendBtn.disabled = true;
|
|
1845
1851
|
} else {
|
|
@@ -2410,4 +2416,500 @@ ${SHARED_STYLES}
|
|
|
2410
2416
|
</script>
|
|
2411
2417
|
</body>
|
|
2412
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>`;
|
|
2413
2915
|
//# sourceMappingURL=ui-page.js.map
|