codehere 0.6.0-alpha.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +14 -0
- package/dist/infrastructure/orchestration/context-handoff.js +143 -0
- package/dist/infrastructure/orchestration/failover-config.js +77 -0
- package/dist/infrastructure/orchestration/graphify-context.js +256 -0
- package/dist/infrastructure/orchestration/session-manager.js +420 -6
- package/dist/infrastructure/protocols/cloud-sync.js +208 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +122 -10
- package/dist/infrastructure/protocols/ui-page-v2.js +505 -0
- package/dist/infrastructure/protocols/ui-page.js +303 -37
- package/dist/infrastructure/storage/project-thread-mirror.js +25 -15
- package/dist/presentation/cli/repl.js +29 -0
- package/package.json +2 -1
|
@@ -1460,6 +1460,14 @@ ${AGENT_BADGE_JS}
|
|
|
1460
1460
|
feed.insertBefore(li, feed.firstChild);
|
|
1461
1461
|
applyFilterToRow(li);
|
|
1462
1462
|
empty.style.display = 'none';
|
|
1463
|
+
// v0.6 — cap DOM rows so a long-open /history tab doesn't accumulate
|
|
1464
|
+
// thousands of <li>s (~50 events/min × hours = browser slowdown).
|
|
1465
|
+
// Same cap-pattern as /threads (turns) and dashboard (sessions).
|
|
1466
|
+
// Newest-first prepend means the OLDEST is the LAST child; trim that.
|
|
1467
|
+
const MAX_FEED_ROWS = 500;
|
|
1468
|
+
while (feed.children.length > MAX_FEED_ROWS) {
|
|
1469
|
+
feed.removeChild(feed.lastChild);
|
|
1470
|
+
}
|
|
1463
1471
|
}
|
|
1464
1472
|
|
|
1465
1473
|
// Bug A5 — filter state. Active outcome filter and active tool filter
|
|
@@ -1587,6 +1595,12 @@ ${AGENT_BADGE_JS}
|
|
|
1587
1595
|
const entry = JSON.parse(ev.data);
|
|
1588
1596
|
entry._ts = parseTs(entry);
|
|
1589
1597
|
events.push(entry);
|
|
1598
|
+
// v0.6 — paired with the DOM cap above. Stats only need recent
|
|
1599
|
+
// events; trimming the head keeps recomputeStats O(MAX) not O(forever).
|
|
1600
|
+
const MAX_EVENTS_IN_MEMORY = 500;
|
|
1601
|
+
if (events.length > MAX_EVENTS_IN_MEMORY) {
|
|
1602
|
+
events.splice(0, events.length - MAX_EVENTS_IN_MEMORY);
|
|
1603
|
+
}
|
|
1590
1604
|
render(entry);
|
|
1591
1605
|
recomputeStats();
|
|
1592
1606
|
} catch { /* skip malformed */ }
|
|
@@ -1831,6 +1845,33 @@ ${SHARED_STYLES}
|
|
|
1831
1845
|
}
|
|
1832
1846
|
.readiness .r-action button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
1833
1847
|
|
|
1848
|
+
/* v0.6.x P0 — failover offer banner. Amber stripe: "Agent A rate-limited,
|
|
1849
|
+
switch to Agent B?" with one-click dismiss + accept buttons. */
|
|
1850
|
+
.failover-banner {
|
|
1851
|
+
margin: 14px 0 0; padding: 14px 20px;
|
|
1852
|
+
border: 1px solid rgba(254,188,46,0.3);
|
|
1853
|
+
background: rgba(254,188,46,0.06);
|
|
1854
|
+
border-radius: 10px;
|
|
1855
|
+
font-size: 13px; line-height: 1.5;
|
|
1856
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1857
|
+
gap: 12px; flex-wrap: wrap;
|
|
1858
|
+
}
|
|
1859
|
+
.failover-banner .fb-msg { flex: 1; color: var(--fg-70); min-width: 200px; }
|
|
1860
|
+
.failover-banner .fb-msg b { color: var(--fg); font-weight: 600; }
|
|
1861
|
+
.failover-banner .fb-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
|
1862
|
+
.failover-banner .fb-actions button {
|
|
1863
|
+
font-family: var(--mono); font-size: 11px;
|
|
1864
|
+
padding: 5px 14px; border-radius: 6px; cursor: pointer;
|
|
1865
|
+
border: 1px solid var(--border); background: transparent;
|
|
1866
|
+
color: var(--fg-70); transition: color 0.15s, border-color 0.15s;
|
|
1867
|
+
}
|
|
1868
|
+
.failover-banner .fb-actions button:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
1869
|
+
.failover-banner .fb-actions button.fb-accept {
|
|
1870
|
+
background: rgba(254,188,46,0.12); border-color: rgba(254,188,46,0.4);
|
|
1871
|
+
color: var(--warn); font-weight: 600;
|
|
1872
|
+
}
|
|
1873
|
+
.failover-banner .fb-actions button.fb-accept:hover { background: rgba(254,188,46,0.2); }
|
|
1874
|
+
|
|
1834
1875
|
/* v0.6 S2 — first-time onboarding card. Quiet authority: peach
|
|
1835
1876
|
accent border + subtle glass, doesn't compete with the textarea
|
|
1836
1877
|
below. Removed once dismissed OR once any session exists. */
|
|
@@ -2171,18 +2212,23 @@ ${SHARED_STYLES}
|
|
|
2171
2212
|
.session.observed { display: none; }
|
|
2172
2213
|
body.advanced-on .session.observed { display: block; }
|
|
2173
2214
|
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2215
|
+
/* claim_user_persona — session list limit. Default shows the 5
|
|
2216
|
+
most-recent cards; "Show all" reveals the rest. .show-all-on body
|
|
2217
|
+
class lifts the cap. CSS-only via :nth-child so attachSession
|
|
2218
|
+
prepend semantics stay simple.
|
|
2219
|
+
CRITICAL: running sessions are NEVER capped — a user must always
|
|
2220
|
+
see their actively-working session, even if it's the 10th card. */
|
|
2221
|
+
#sessions > .session:nth-child(n+6):not(.status-running) { display: none; }
|
|
2222
|
+
body.show-all-sessions #sessions > .session:nth-child(n+6) { display: block; }
|
|
2180
2223
|
/* When advanced is also off, observed rows beyond cap stay hidden. */
|
|
2181
2224
|
body.show-all-sessions #sessions > .session.observed { display: none; }
|
|
2182
2225
|
body.show-all-sessions.advanced-on #sessions > .session.observed { display: block; }
|
|
2226
|
+
.sessions-actions {
|
|
2227
|
+
display: flex; gap: 10px; justify-content: center; align-items: center;
|
|
2228
|
+
margin: 14px 0 0;
|
|
2229
|
+
}
|
|
2183
2230
|
.show-more-sessions {
|
|
2184
2231
|
display: none; /* shown via JS when count > 5 */
|
|
2185
|
-
margin: 14px auto;
|
|
2186
2232
|
padding: 8px 16px;
|
|
2187
2233
|
background: transparent;
|
|
2188
2234
|
color: var(--fg-50);
|
|
@@ -2194,6 +2240,17 @@ ${SHARED_STYLES}
|
|
|
2194
2240
|
}
|
|
2195
2241
|
.show-more-sessions:hover { color: var(--fg); border-color: var(--fg-30); }
|
|
2196
2242
|
body.show-all-sessions .show-more-sessions { display: none; }
|
|
2243
|
+
.stop-all-sessions {
|
|
2244
|
+
padding: 8px 16px;
|
|
2245
|
+
background: transparent;
|
|
2246
|
+
color: var(--block);
|
|
2247
|
+
border: 1px solid rgba(255,95,87,0.3);
|
|
2248
|
+
border-radius: 999px;
|
|
2249
|
+
font-family: var(--mono); font-size: 11px;
|
|
2250
|
+
cursor: pointer;
|
|
2251
|
+
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
|
2252
|
+
}
|
|
2253
|
+
.stop-all-sessions:hover { color: var(--fg); border-color: var(--block); background: rgba(255,95,87,0.08); }
|
|
2197
2254
|
|
|
2198
2255
|
.session-controls { display: flex; gap: 8px; }
|
|
2199
2256
|
.session-controls button {
|
|
@@ -2226,6 +2283,39 @@ ${SHARED_STYLES}
|
|
|
2226
2283
|
|
|
2227
2284
|
.session-body { padding: 0 20px 16px; }
|
|
2228
2285
|
|
|
2286
|
+
/* Session threading — visual link between original and failover sessions.
|
|
2287
|
+
The original card shows "↻ continued as [agent]" with a link to the new card.
|
|
2288
|
+
The new card shows "Continued from [agent]" at the top. */
|
|
2289
|
+
.session-continued-as {
|
|
2290
|
+
margin: 0 20px 12px;
|
|
2291
|
+
padding: 8px 12px;
|
|
2292
|
+
border: 1px dashed rgba(255,154,118,0.3);
|
|
2293
|
+
border-radius: 8px;
|
|
2294
|
+
background: rgba(255,154,118,0.04);
|
|
2295
|
+
font-family: var(--mono);
|
|
2296
|
+
font-size: 11px;
|
|
2297
|
+
color: var(--fg-70);
|
|
2298
|
+
}
|
|
2299
|
+
.session-continued-as a {
|
|
2300
|
+
color: var(--accent);
|
|
2301
|
+
text-decoration: none;
|
|
2302
|
+
font-weight: 500;
|
|
2303
|
+
}
|
|
2304
|
+
.session-continued-as a:hover { text-decoration: underline; }
|
|
2305
|
+
.session-continued-from {
|
|
2306
|
+
margin: 0 0 8px;
|
|
2307
|
+
padding: 6px 12px;
|
|
2308
|
+
border-left: 2px solid rgba(255,154,118,0.3);
|
|
2309
|
+
font-family: var(--mono);
|
|
2310
|
+
font-size: 10.5px;
|
|
2311
|
+
color: var(--fg-50);
|
|
2312
|
+
}
|
|
2313
|
+
.session-continued-from a {
|
|
2314
|
+
color: var(--accent);
|
|
2315
|
+
text-decoration: none;
|
|
2316
|
+
}
|
|
2317
|
+
.session-continued-from a:hover { text-decoration: underline; }
|
|
2318
|
+
|
|
2229
2319
|
/* P18 slice 4 — permission gate panel. Renders inside the session
|
|
2230
2320
|
card when the per-tool hook blocked something. Per the dogfood-
|
|
2231
2321
|
learning + overconfidence-guard: framed as "Codehere noticed",
|
|
@@ -2551,6 +2641,8 @@ ${SHARED_STYLES}
|
|
|
2551
2641
|
</div>
|
|
2552
2642
|
|
|
2553
2643
|
<div class="intake">
|
|
2644
|
+
<h1>Delegate coding tasks to any AI agent.</h1>
|
|
2645
|
+
<p class="sub">Type what needs doing — codehere picks the right agent, watches every action, and reports back.</p>
|
|
2554
2646
|
<textarea id="prompt" placeholder="What needs doing? Describe a task — codehere picks the right agent."></textarea>
|
|
2555
2647
|
<!-- Pattern 5 · Composer meta line. UX-simplification iter (May 7):
|
|
2556
2648
|
dropped the cm-cwd pill — the cwd input below is the authoritative
|
|
@@ -2592,6 +2684,12 @@ ${SHARED_STYLES}
|
|
|
2592
2684
|
Goal: textarea is the focal point above the fold; chrome subordinates. -->
|
|
2593
2685
|
<div class="readiness" id="readiness" style="position:relative;z-index:1"></div>
|
|
2594
2686
|
|
|
2687
|
+
<!-- v0.6.x P0 — failover offer banner. Surfaces when an adapter hits
|
|
2688
|
+
rate-limit / quota so the user can manually switch agents instead
|
|
2689
|
+
of waiting for auto-failover. Hidden by default; shown via JS when
|
|
2690
|
+
offers arrive from /api/failover-offers. -->
|
|
2691
|
+
<div class="failover-banner" id="failover-banner" style="display:none"></div>
|
|
2692
|
+
|
|
2595
2693
|
<!-- May 6 2026 design polish (Tier 1 #3) + dogfood Bug A3 fix:
|
|
2596
2694
|
The prior 4-stat row mixed two definitions of "agents" (the AGENTS
|
|
2597
2695
|
right-pane shows installed-and-ready agents; "agents on machine" stat
|
|
@@ -2611,9 +2709,12 @@ ${SHARED_STYLES}
|
|
|
2611
2709
|
divider here. The .section-label "Sessions" below already provides
|
|
2612
2710
|
visual separation; an extra 1px gradient line was decoration not
|
|
2613
2711
|
information. -->
|
|
2614
|
-
<div class="section-label" style="position:relative;z-index:1">Sessions</div>
|
|
2615
|
-
<div id="sessions" style="position:relative;z-index:1"></div>
|
|
2616
|
-
<
|
|
2712
|
+
<div class="section-label" style="position:relative;z-index:1">Sessions</div>
|
|
2713
|
+
<div id="sessions" style="position:relative;z-index:1"></div>
|
|
2714
|
+
<div class="sessions-actions" id="sessions-actions">
|
|
2715
|
+
<button class="show-more-sessions" id="show-more-sessions" type="button">Show all sessions</button>
|
|
2716
|
+
<button class="stop-all-sessions" id="stop-all-sessions" type="button" style="display:none">Stop all running</button>
|
|
2717
|
+
</div>
|
|
2617
2718
|
<div id="empty">
|
|
2618
2719
|
<div class="pill">No sessions yet. Assign one above.</div>
|
|
2619
2720
|
</div>
|
|
@@ -3468,6 +3569,7 @@ ${AGENT_BADGE_JS}
|
|
|
3468
3569
|
const card = document.createElement('div');
|
|
3469
3570
|
card.className = 'session';
|
|
3470
3571
|
card.dataset.sessionId = session.sessionId;
|
|
3572
|
+
card.dataset.status = session.status;
|
|
3471
3573
|
// C3 · Persisted sessions are from a prior daemon run — no live stream, no
|
|
3472
3574
|
// kill. Still useful: user sees their previous Final reports across restarts.
|
|
3473
3575
|
const isPersisted = session.provenance === 'persisted';
|
|
@@ -3503,6 +3605,8 @@ ${AGENT_BADGE_JS}
|
|
|
3503
3605
|
'</div>' +
|
|
3504
3606
|
'</div>' +
|
|
3505
3607
|
'<div class="session-body" data-view="thread">' +
|
|
3608
|
+
// Session threading — "Continued from" indicator for failover sessions
|
|
3609
|
+
(session.continuedFrom ? '<div class="session-continued-from">Continued from <a href="#' + escape(session.continuedFrom) + '">' + escape(agentLabel(session.agent)) + '</a> session</div>' : '') +
|
|
3506
3610
|
// Order: thread messages (chronological) → final report (closing
|
|
3507
3611
|
// summary, rendered as a block when artifacts arrive) → reply input.
|
|
3508
3612
|
// Apr 29 bug fix: previously final-report was BEFORE thread-messages
|
|
@@ -3555,6 +3659,16 @@ ${AGENT_BADGE_JS}
|
|
|
3555
3659
|
// storage, but for the typical flow this is "I'm using it now".
|
|
3556
3660
|
dismissOnboarding();
|
|
3557
3661
|
|
|
3662
|
+
// Session threading — "continued as" indicator for sessions that already
|
|
3663
|
+
// have a failover successor (e.g., after page refresh).
|
|
3664
|
+
if (session.continuedAs) {
|
|
3665
|
+
var continuedAsEl = document.createElement('div');
|
|
3666
|
+
continuedAsEl.className = 'session-continued-as';
|
|
3667
|
+
continuedAsEl.innerHTML = '↻ Continued as <a href="#' + escape(session.continuedAs) + '">new session</a> · session ' + escape(session.continuedAs.slice(0, 8));
|
|
3668
|
+
var headEl2 = card.querySelector('.session-head');
|
|
3669
|
+
if (headEl2 && headEl2.parentNode) headEl2.parentNode.insertBefore(continuedAsEl, headEl2.nextSibling);
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3558
3672
|
const logEl = card.querySelector('.event-log');
|
|
3559
3673
|
const pillEl = card.querySelector('.status-pill');
|
|
3560
3674
|
const killBtn = card.querySelector('.kill');
|
|
@@ -3832,6 +3946,7 @@ ${AGENT_BADGE_JS}
|
|
|
3832
3946
|
const replyStatusEl = card.querySelector('.reply-status');
|
|
3833
3947
|
if (!session.canAppendMessage) {
|
|
3834
3948
|
threadInputEl.classList.add('disabled');
|
|
3949
|
+
threadInputEl.title = 'Replies require a live session — re-spawn to continue this thread';
|
|
3835
3950
|
// All adapters wire appendMessage as of v0.6.0 Slices 1-3. This branch
|
|
3836
3951
|
// now only fires for sessions evicted from in-memory tracking (the
|
|
3837
3952
|
// canAppendMessage flag persists from spawn but the API endpoint
|
|
@@ -4100,21 +4215,28 @@ ${AGENT_BADGE_JS}
|
|
|
4100
4215
|
// Pattern 13: tool-prefixed verb takes over while a step is active.
|
|
4101
4216
|
// Most-recent active step's label becomes the micro-activity text;
|
|
4102
4217
|
// when no step is active we fall back to whimsy rotation.
|
|
4103
|
-
// Pattern 17:
|
|
4104
|
-
//
|
|
4105
|
-
// running. Simple slice — doesn't yet upgrade to the bar / ESC hint
|
|
4106
|
-
// at 15-30s thresholds; that's deferred until the affordance
|
|
4107
|
-
// demands it in real dogfood.
|
|
4218
|
+
// Pattern 17: 3-bucket long-op indicator — 5s timer → 15s bar → 30s
|
|
4219
|
+
// still-running + ESC hint.
|
|
4108
4220
|
const active = steps.filter(s => String(s.state || '') === 'active').slice(-1)[0];
|
|
4109
4221
|
if (active && session.status === 'running') {
|
|
4110
4222
|
let label = String(active.label || 'Working on it…');
|
|
4111
4223
|
const startedAt = Number(active.startedAt || 0);
|
|
4112
4224
|
if (startedAt > 0) {
|
|
4113
|
-
const
|
|
4225
|
+
const elapsedMs = Date.now() - startedAt;
|
|
4226
|
+
const elapsedSec = Math.floor(elapsedMs / 1000);
|
|
4114
4227
|
if (elapsedSec >= 5) {
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
if (elapsedSec >=
|
|
4228
|
+
const timer = fmtElapsed(elapsedMs);
|
|
4229
|
+
// 15s threshold: add indeterminate progress bar
|
|
4230
|
+
if (elapsedSec >= 15) {
|
|
4231
|
+
const barFill = Math.floor((elapsedSec % 15) / 15 * 8);
|
|
4232
|
+
const barEmpty = 8 - barFill;
|
|
4233
|
+
const bar = '▮'.repeat(barFill) + '░'.repeat(barEmpty);
|
|
4234
|
+
label += ' · ' + timer + ' ' + bar;
|
|
4235
|
+
} else {
|
|
4236
|
+
// 5–15s: elapsed timer only
|
|
4237
|
+
label += ' · ' + timer;
|
|
4238
|
+
}
|
|
4239
|
+
if (elapsedSec >= 30) label += ' · still running · ESC to cancel';
|
|
4118
4240
|
}
|
|
4119
4241
|
}
|
|
4120
4242
|
setMicroState('tool', label);
|
|
@@ -4136,6 +4258,7 @@ ${AGENT_BADGE_JS}
|
|
|
4136
4258
|
// completed task is still generating its report (placeholder state).
|
|
4137
4259
|
const isCompleted = status === 'completed' || status === 'killed' || status === 'error';
|
|
4138
4260
|
const hasContent = artifacts || finalReport;
|
|
4261
|
+
const reportFailed = session.failedReport === true;
|
|
4139
4262
|
// v0.6.x — bound the "Generating report…" placeholder to the actual
|
|
4140
4263
|
// generation window. Final Report typically returns within 30s; after
|
|
4141
4264
|
// 90s post-completion with no report, the report either failed silently
|
|
@@ -4143,10 +4266,10 @@ ${AGENT_BADGE_JS}
|
|
|
4143
4266
|
// wiped). Either way, "Generating report…" forever is dishonest.
|
|
4144
4267
|
const REPORT_WAIT_BUDGET_MS = 90_000;
|
|
4145
4268
|
const sinceCompleted = completedAt ? Date.now() - completedAt : 0;
|
|
4146
|
-
const stillWaitingForReport = isCompleted && !finalReport && sinceCompleted < REPORT_WAIT_BUDGET_MS;
|
|
4147
|
-
const reportTimedOut = isCompleted && !finalReport && sinceCompleted >= REPORT_WAIT_BUDGET_MS;
|
|
4269
|
+
const stillWaitingForReport = isCompleted && !finalReport && !reportFailed && sinceCompleted < REPORT_WAIT_BUDGET_MS;
|
|
4270
|
+
const reportTimedOut = isCompleted && !finalReport && !reportFailed && sinceCompleted >= REPORT_WAIT_BUDGET_MS;
|
|
4148
4271
|
const isAwaitingReport = stillWaitingForReport;
|
|
4149
|
-
if (!hasContent && !isAwaitingReport && !reportTimedOut) { finalReportEl.style.display = 'none'; return; }
|
|
4272
|
+
if (!hasContent && !isAwaitingReport && !reportTimedOut && !reportFailed) { finalReportEl.style.display = 'none'; return; }
|
|
4150
4273
|
finalReportEl.style.display = 'block';
|
|
4151
4274
|
const files = Array.isArray(artifacts?.files) ? artifacts.files : [];
|
|
4152
4275
|
const tests = artifacts?.tests;
|
|
@@ -4162,6 +4285,8 @@ ${AGENT_BADGE_JS}
|
|
|
4162
4285
|
'</div>';
|
|
4163
4286
|
} else if (isAwaitingReport) {
|
|
4164
4287
|
html += '<div class="fr-summary fr-summary-pending"><p>Generating report…</p></div>';
|
|
4288
|
+
} else if (reportFailed) {
|
|
4289
|
+
html += '<div class="fr-summary fr-summary-pending"><p>Final Report generation failed. The agent finished but the summary couldn\\'t be created. Re-spawn to get a fresh report.</p></div>';
|
|
4165
4290
|
} else if (reportTimedOut) {
|
|
4166
4291
|
// Honest message instead of perpetual "Generating report…". Most
|
|
4167
4292
|
// common cause: daemon restarted (Final Report is in-memory only,
|
|
@@ -4265,29 +4390,39 @@ ${AGENT_BADGE_JS}
|
|
|
4265
4390
|
});
|
|
4266
4391
|
built.el.querySelector('.ra-primary').addEventListener('click', async function() {
|
|
4267
4392
|
if (!built.copy.canFailover) {
|
|
4268
|
-
// Auth/setup path — open the agents pane (already in DOM)
|
|
4269
|
-
// instead of spawning a new session with the same broken auth.
|
|
4270
4393
|
built.el.remove();
|
|
4271
4394
|
return;
|
|
4272
4395
|
}
|
|
4273
4396
|
var primary = built.el.querySelector('.ra-primary');
|
|
4274
4397
|
primary.disabled = true;
|
|
4398
|
+
primary.textContent = 'Switching…';
|
|
4275
4399
|
try {
|
|
4276
|
-
await fetch('/api/
|
|
4400
|
+
var r = await fetch('/api/failover-accept', {
|
|
4277
4401
|
method: 'POST',
|
|
4278
4402
|
headers: { 'Content-Type': 'application/json' },
|
|
4279
|
-
body: JSON.stringify({
|
|
4280
|
-
prompt: session.prompt,
|
|
4281
|
-
agent: nextAgent,
|
|
4282
|
-
// Reuse the current intake cwd — the failover spawns
|
|
4283
|
-
// against whichever repo the user is currently scoped
|
|
4284
|
-
// to. Acceptable trade-off vs. a per-session lookup.
|
|
4285
|
-
cwd: (cwdInput && cwdInput.value && cwdInput.value.trim()) || ''
|
|
4286
|
-
})
|
|
4403
|
+
body: JSON.stringify({ sessionId: session.sessionId, targetAgent: nextAgent })
|
|
4287
4404
|
});
|
|
4288
|
-
|
|
4405
|
+
var d = await r.json();
|
|
4406
|
+
if (d.error || (!d.ok || !d.newSessionId)) {
|
|
4407
|
+
built.el.querySelector('.ra-msg').innerHTML = '<b>Failover failed:</b> ' + escape(d.detail || 'Target agent not ready');
|
|
4408
|
+
primary.style.display = 'none';
|
|
4409
|
+
built.el.querySelector('.ra-secondary').textContent = 'Dismiss';
|
|
4410
|
+
return;
|
|
4411
|
+
}
|
|
4412
|
+
// Session threading: add "continued as" link on the original card
|
|
4413
|
+
var continuedAsEl = document.createElement('div');
|
|
4414
|
+
continuedAsEl.className = 'session-continued-as';
|
|
4415
|
+
continuedAsEl.innerHTML = '↻ Continued as <a href="#' + escape(d.newSessionId) + '">' + escape(nextAgent) + '</a> · session ' + escape(d.newSessionId.slice(0, 8));
|
|
4416
|
+
// Insert after session-head
|
|
4417
|
+
var headEl = card.querySelector('.session-head');
|
|
4418
|
+
if (headEl && headEl.parentNode) headEl.parentNode.insertBefore(continuedAsEl, headEl.nextSibling);
|
|
4419
|
+
built.el.querySelector('.ra-msg').innerHTML = '↻ Continued as <b>' + escape(nextAgent) + '</b>';
|
|
4420
|
+
primary.style.display = 'none';
|
|
4421
|
+
built.el.querySelector('.ra-secondary').textContent = 'OK';
|
|
4289
4422
|
} catch {
|
|
4423
|
+
built.el.querySelector('.ra-msg').innerHTML = '<b>Failover failed:</b> Network error';
|
|
4290
4424
|
primary.disabled = false;
|
|
4425
|
+
primary.textContent = 'Retry';
|
|
4291
4426
|
}
|
|
4292
4427
|
});
|
|
4293
4428
|
}
|
|
@@ -4663,6 +4798,7 @@ ${AGENT_BADGE_JS}
|
|
|
4663
4798
|
pruneStaleAgents(seen);
|
|
4664
4799
|
updateBanner(d);
|
|
4665
4800
|
updateShowMoreButton();
|
|
4801
|
+
updateStopAllButton();
|
|
4666
4802
|
}).catch(() => { /* fine */ });
|
|
4667
4803
|
}
|
|
4668
4804
|
// claim_user_persona — show "Show all sessions (N more)" button only
|
|
@@ -4684,6 +4820,24 @@ ${AGENT_BADGE_JS}
|
|
|
4684
4820
|
document.body.classList.add('show-all-sessions');
|
|
4685
4821
|
updateShowMoreButton();
|
|
4686
4822
|
});
|
|
4823
|
+
|
|
4824
|
+
// Stop all running sessions — one click kills every active session.
|
|
4825
|
+
const stopAllBtn = document.getElementById('stop-all-sessions');
|
|
4826
|
+
function updateStopAllButton() {
|
|
4827
|
+
if (!stopAllBtn) return;
|
|
4828
|
+
const runningCards = sessionsEl.querySelectorAll('.session[data-status="running"]');
|
|
4829
|
+
stopAllBtn.style.display = runningCards.length > 1 ? 'block' : 'none';
|
|
4830
|
+
stopAllBtn.textContent = 'Stop all running (' + runningCards.length + ')';
|
|
4831
|
+
}
|
|
4832
|
+
if (stopAllBtn) stopAllBtn.addEventListener('click', function() {
|
|
4833
|
+
if (!confirm('Stop all running sessions?')) return;
|
|
4834
|
+
const runningCards = sessionsEl.querySelectorAll('.session[data-status="running"]');
|
|
4835
|
+
runningCards.forEach(function(card) {
|
|
4836
|
+
const killBtn = card.querySelector('.session-controls button.kill');
|
|
4837
|
+
if (killBtn && !killBtn.disabled) killBtn.click();
|
|
4838
|
+
});
|
|
4839
|
+
updateStopAllButton();
|
|
4840
|
+
});
|
|
4687
4841
|
loadSessions();
|
|
4688
4842
|
// Refresh detected-agents list + banner every 5s. Iter 12 — also patches
|
|
4689
4843
|
// existing session cards with new finalReport prose when it arrives async.
|
|
@@ -4753,6 +4907,88 @@ ${AGENT_BADGE_JS}
|
|
|
4753
4907
|
}
|
|
4754
4908
|
|
|
4755
4909
|
checkReadiness();
|
|
4910
|
+
|
|
4911
|
+
// ── Failover offer banner (P0) ──────────────────────────────────
|
|
4912
|
+
// Polls /api/failover-offers every 5s. When offers exist, shows a
|
|
4913
|
+
// banner: "Claude rate-limited — switch to Codex?" with one-click
|
|
4914
|
+
// accept (re-spawns on the target agent) and dismiss buttons.
|
|
4915
|
+
const fbEl = document.getElementById('failover-banner');
|
|
4916
|
+
let fbDismissed = false;
|
|
4917
|
+
|
|
4918
|
+
function renderFailoverOffers(offers) {
|
|
4919
|
+
if (!offers || offers.length === 0 || fbDismissed) {
|
|
4920
|
+
fbEl.style.display = 'none';
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
fbEl.style.display = 'flex';
|
|
4924
|
+
const parts = offers.map(function(o) {
|
|
4925
|
+
var msg = '<b>' + escape(o.signal.fromAgent) + '</b> hit ' + escape(o.signal.errorCode) +
|
|
4926
|
+
' — switch to <b>' + escape(o.target) + '</b>?';
|
|
4927
|
+
var actions = '<div class="fb-actions">' +
|
|
4928
|
+
'<button class="fb-accept" data-session="' + escape(o.sessionId) + '" data-target="' + escape(o.target) + '">Switch</button>' +
|
|
4929
|
+
'<button class="fb-dismiss" data-session="' + escape(o.sessionId) + '">Dismiss</button>' +
|
|
4930
|
+
'</div>';
|
|
4931
|
+
return '<div class="fb-msg">' + msg + '</div>' + actions;
|
|
4932
|
+
});
|
|
4933
|
+
fbEl.innerHTML = parts.join('');
|
|
4934
|
+
|
|
4935
|
+
// Wire accept buttons
|
|
4936
|
+
fbEl.querySelectorAll('.fb-accept').forEach(function(btn) {
|
|
4937
|
+
btn.addEventListener('click', function() {
|
|
4938
|
+
var sessionId = btn.dataset.session;
|
|
4939
|
+
var target = btn.dataset.target;
|
|
4940
|
+
btn.disabled = true;
|
|
4941
|
+
btn.textContent = 'Switching…';
|
|
4942
|
+
fetch('/api/failover-accept', {
|
|
4943
|
+
method: 'POST',
|
|
4944
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4945
|
+
body: JSON.stringify({ sessionId: sessionId, targetAgent: target }),
|
|
4946
|
+
}).then(function(r) { return r.json(); }).then(function(d) {
|
|
4947
|
+
if (d.error) {
|
|
4948
|
+
fbEl.innerHTML = '<div class="fb-msg"><b>Failover failed:</b> ' + escape(d.error) + (d.detail ? ' — ' + escape(d.detail) : '') + '</div>';
|
|
4949
|
+
return;
|
|
4950
|
+
}
|
|
4951
|
+
if (!d.ok || !d.newSessionId) {
|
|
4952
|
+
fbEl.innerHTML = '<div class="fb-msg"><b>Failover unavailable:</b> ' + escape(d.detail || 'Target agent not ready') + '</div>';
|
|
4953
|
+
return;
|
|
4954
|
+
}
|
|
4955
|
+
// Session threading: add "continued as" link on the original card
|
|
4956
|
+
var origCard = document.querySelector('[data-session-id="' + escape(sessionId) + '"]');
|
|
4957
|
+
if (origCard && !origCard.querySelector('.session-continued-as')) {
|
|
4958
|
+
var headEl = origCard.querySelector('.session-head');
|
|
4959
|
+
if (headEl && headEl.parentNode) {
|
|
4960
|
+
var continuedAsEl = document.createElement('div');
|
|
4961
|
+
continuedAsEl.className = 'session-continued-as';
|
|
4962
|
+
continuedAsEl.innerHTML = '↻ Continued as <a href="#' + escape(d.newSessionId) + '">' + escape(target) + '</a> · session ' + escape(d.newSessionId.slice(0, 8));
|
|
4963
|
+
headEl.parentNode.insertBefore(continuedAsEl, headEl.nextSibling);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
fbEl.innerHTML = '<div class="fb-msg">↻ Switched to <b>' + escape(target) + '</b>. New session: <b>' + escape(d.newSessionId.slice(0, 8)) + '</b></div>' +
|
|
4967
|
+
'<div class="fb-actions"><button class="fb-dismiss" id="fb-success-dismiss">OK</button></div>';
|
|
4968
|
+
document.getElementById('fb-success-dismiss').addEventListener('click', function() {
|
|
4969
|
+
fbDismissed = true;
|
|
4970
|
+
fbEl.style.display = 'none';
|
|
4971
|
+
});
|
|
4972
|
+
}).catch(function() {
|
|
4973
|
+
fbEl.innerHTML = '<div class="fb-msg"><b>Failover failed:</b> Network error. Check if the daemon is running.</div>';
|
|
4974
|
+
});
|
|
4975
|
+
});
|
|
4976
|
+
});
|
|
4977
|
+
|
|
4978
|
+
// Wire dismiss buttons (initial render)
|
|
4979
|
+
fbEl.querySelectorAll('.fb-dismiss').forEach(function(btn) {
|
|
4980
|
+
btn.addEventListener('click', function() {
|
|
4981
|
+
fbDismissed = true;
|
|
4982
|
+
fbEl.style.display = 'none';
|
|
4983
|
+
});
|
|
4984
|
+
});
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
setInterval(function() {
|
|
4988
|
+
fetch('/api/failover-offers').then(function(r) { return r.json(); }).then(function(d) {
|
|
4989
|
+
renderFailoverOffers(d.offers);
|
|
4990
|
+
}).catch(function() { /* fine */ });
|
|
4991
|
+
}, 5000);
|
|
4756
4992
|
})();
|
|
4757
4993
|
</script>
|
|
4758
4994
|
</body>
|
|
@@ -4948,6 +5184,26 @@ ${SHARED_STYLES}
|
|
|
4948
5184
|
.task .steps .step { padding: 2px 0; }
|
|
4949
5185
|
.task .steps .step.done::before { content: '✓ '; color: var(--allow); }
|
|
4950
5186
|
|
|
5187
|
+
/* Pattern 21 · Buffered reveal — cascade animation for completed steps */
|
|
5188
|
+
.task .steps.buffered-reveal .step {
|
|
5189
|
+
opacity: 0;
|
|
5190
|
+
transform: translateY(4px);
|
|
5191
|
+
animation: br-cascade 80ms ease-out forwards;
|
|
5192
|
+
}
|
|
5193
|
+
.task .steps.buffered-reveal .step:nth-child(1) { animation-delay: 0ms; }
|
|
5194
|
+
.task .steps.buffered-reveal .step:nth-child(2) { animation-delay: 80ms; }
|
|
5195
|
+
.task .steps.buffered-reveal .step:nth-child(3) { animation-delay: 160ms; }
|
|
5196
|
+
.task .steps.buffered-reveal .step:nth-child(4) { animation-delay: 240ms; }
|
|
5197
|
+
.task .steps.buffered-reveal .step:nth-child(5) { animation-delay: 320ms; }
|
|
5198
|
+
.task .steps.buffered-reveal .step:nth-child(6) { animation-delay: 400ms; }
|
|
5199
|
+
.task .steps.buffered-reveal .step:nth-child(7) { animation-delay: 480ms; }
|
|
5200
|
+
.task .steps.buffered-reveal .step:nth-child(8) { animation-delay: 560ms; }
|
|
5201
|
+
.task .steps.buffered-reveal .step:nth-child(9) { animation-delay: 640ms; }
|
|
5202
|
+
.task .steps.buffered-reveal .step:nth-child(10) { animation-delay: 720ms; }
|
|
5203
|
+
@keyframes br-cascade {
|
|
5204
|
+
to { opacity: 1; transform: translateY(0); }
|
|
5205
|
+
}
|
|
5206
|
+
|
|
4951
5207
|
.empty {
|
|
4952
5208
|
text-align: center; padding: 60px 24px; color: var(--fg-50); font-family: var(--mono); font-size: 13px;
|
|
4953
5209
|
}
|
|
@@ -5192,9 +5448,19 @@ ${AGENT_BADGE_JS}
|
|
|
5192
5448
|
|
|
5193
5449
|
for (const t of renderedTurns) {
|
|
5194
5450
|
const status = (t.status || '').toLowerCase();
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5451
|
+
// v0.6 — cap per-task steps at last 5 (mirrors dashboard renderSteps
|
|
5452
|
+
// tail-3 pattern). A long task with 100+ steps × 25 turns rendered
|
|
5453
|
+
// = 2500 step DOM nodes. Show last 5 + "+N more" pill.
|
|
5454
|
+
let stepsHtml = '';
|
|
5455
|
+
if (t.steps && t.steps.length) {
|
|
5456
|
+
const STEP_TAIL = 5;
|
|
5457
|
+
const stepTail = t.steps.slice(-STEP_TAIL);
|
|
5458
|
+
const stepHidden = t.steps.length - stepTail.length;
|
|
5459
|
+
stepsHtml = '<div class="steps">' +
|
|
5460
|
+
stepTail.map(s => '<div class="step ' + escapeHtml((s.state||'').toLowerCase()) + '">' + escapeHtml(s.label || '') + '</div>').join('') +
|
|
5461
|
+
(stepHidden > 0 ? '<div class="step muted">+' + stepHidden + ' earlier step' + (stepHidden === 1 ? '' : 's') + '</div>' : '') +
|
|
5462
|
+
'</div>';
|
|
5463
|
+
}
|
|
5198
5464
|
const durHtml = (t.durationMs != null)
|
|
5199
5465
|
? '<div class="duration">Duration: <b>' + fmtDuration(t.durationMs) + '</b></div>'
|
|
5200
5466
|
: (status === 'running' ? '<div class="duration">Running…</div>' : '');
|
|
@@ -478,21 +478,31 @@ export function mirrorCompletedToThread(rec) {
|
|
|
478
478
|
artifacts: rec.artifacts,
|
|
479
479
|
});
|
|
480
480
|
}
|
|
481
|
-
/**
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
481
|
+
/** Label map for raw error codes → user-facing text. */
|
|
482
|
+
const FAILOVER_LABELS = {
|
|
483
|
+
rate_limit: 'hit a rate limit',
|
|
484
|
+
quota_exceeded: 'exhausted its quota',
|
|
485
|
+
credit_low: 'ran low on credits',
|
|
486
|
+
error: 'encountered an error',
|
|
487
|
+
};
|
|
488
|
+
export function mirrorFailoverToThread(rec) {
|
|
489
|
+
if (isTempDir(rec.cwd))
|
|
490
|
+
return;
|
|
491
|
+
const slug = slugForCwd(rec.cwd);
|
|
492
|
+
const reasonLabel = FAILOVER_LABELS[rec.reason] ?? 'became unavailable';
|
|
493
|
+
const fromLabel = rec.fromAgent;
|
|
494
|
+
const toLabel = rec.toAgent;
|
|
495
|
+
appendThreadEntry(slug, {
|
|
496
|
+
type: 'task',
|
|
497
|
+
sessionId: rec.sessionId,
|
|
498
|
+
agent: 'system',
|
|
499
|
+
prompt: `↻ Switched from ${fromLabel} to ${toLabel} (${reasonLabel})`,
|
|
500
|
+
status: 'completed',
|
|
501
|
+
startedAt: Date.now(),
|
|
502
|
+
completedAt: Date.now(),
|
|
503
|
+
durationMs: 0,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
496
506
|
export function mirrorReplyToThread(rec) {
|
|
497
507
|
if (isTempDir(rec.cwd))
|
|
498
508
|
return;
|
|
@@ -692,6 +692,35 @@ function renderEvent(event) {
|
|
|
692
692
|
colors.dim(' · ' + (data.reason ?? '')));
|
|
693
693
|
return null;
|
|
694
694
|
}
|
|
695
|
+
case 'codehere:steps': {
|
|
696
|
+
const data = event.data;
|
|
697
|
+
const steps = data?.steps;
|
|
698
|
+
if (!steps || steps.length === 0)
|
|
699
|
+
return null;
|
|
700
|
+
const lastStep = steps[steps.length - 1];
|
|
701
|
+
if (!lastStep)
|
|
702
|
+
return null;
|
|
703
|
+
const state = lastStep.state || 'pending';
|
|
704
|
+
const label = String(lastStep.label || '');
|
|
705
|
+
if (state === 'active') {
|
|
706
|
+
console.log(colors.dim(' │ ') + colors.cyan('●') + ' ' + label);
|
|
707
|
+
return label;
|
|
708
|
+
}
|
|
709
|
+
if (state === 'done' || state === 'failed') {
|
|
710
|
+
const icon = state === 'done' ? colors.green('✓') : colors.red('✗');
|
|
711
|
+
console.log(colors.dim(' │ ') + icon + ' ' + label);
|
|
712
|
+
// P22 / Track B #8 — render last 3 lines of step output for done/failed
|
|
713
|
+
if (lastStep.output && String(lastStep.output).trim()) {
|
|
714
|
+
const outLines = String(lastStep.output).split(String.fromCharCode(10)).filter(l => l.trim());
|
|
715
|
+
const preview = outLines.slice(-3);
|
|
716
|
+
for (const ln of preview) {
|
|
717
|
+
console.log(colors.dim(' │ ') + colors.gray(ln));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return label;
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
695
724
|
case 'stdout':
|
|
696
725
|
case 'stderr': {
|
|
697
726
|
const data = event.data;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.6.0
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"homepage": "https://codehere.uk",
|
|
61
61
|
"dependencies": {
|
|
62
|
+
"@mohammednagy/graphify-ts": "^0.22.3",
|
|
62
63
|
"boxen": "^8.0.1",
|
|
63
64
|
"chalk": "^5.3.0",
|
|
64
65
|
"cli-progress": "^3.12.0",
|