codehere 0.5.0-alpha.5 → 0.5.0-alpha.6

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.
@@ -22,7 +22,7 @@ import { buildGenericAdapter, loadAgentsConfig } from './generic-cli-adapter.js'
22
22
  import { buildConductorAdapter } from './conductor-adapter.js';
23
23
  import { seedAgentsConfig } from './agents-config-seed.js';
24
24
  import { AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
25
- import { appendSpawned, appendCompleted, loadRecent } from './session-persistence.js';
25
+ import { appendSpawned, appendCompleted, appendFinalReport, loadRecent } from './session-persistence.js';
26
26
  import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
27
27
  import { mirrorSpawnedToThread, mirrorCompletedToThread } from '../storage/project-thread-mirror.js';
28
28
  // ── Agent registry ─────────────────────────────────────────────────────────────
@@ -105,6 +105,11 @@ export class SessionManager {
105
105
  * Surfaced May 2 by the cold-start dogfood — `duration: NaNs` in /api/tasks
106
106
  * for completed-but-not-yet-restarted sessions. Bug 2 sister-fix. */
107
107
  completedAtBySession = new Map();
108
+ /** v0.6.x Slack-shape spike — cwd per session. The "Continue thread" intake
109
+ * button needs to find the most recent in-memory session for the current
110
+ * cwd; cwd isn't on the TaskSession interface and adapters don't all expose
111
+ * it, so we track it here at spawn time. */
112
+ cwdBySession = new Map();
108
113
  /** PM-UX Layer 3 · Final Report cache (iteration 11 MVP, UI-integration iter 12).
109
114
  * Populated when generateFinalReport resolves in the exit handler. Web UI
110
115
  * reads via getFinalReport() through the /api/tasks serializer so non-technical
@@ -124,6 +129,7 @@ export class SessionManager {
124
129
  // leaves an audit trail of what was attempted. Subscribe to exit so we can
125
130
  // capture the final status + steps + artifacts when the session settles.
126
131
  const cwd = options?.cwd ?? process.cwd();
132
+ this.cwdBySession.set(session.sessionId, cwd);
127
133
  appendSpawned({
128
134
  id: session.sessionId,
129
135
  agent: session.agent,
@@ -187,6 +193,17 @@ export class SessionManager {
187
193
  // Cache for web UI consumption via /api/tasks (iter 12 — non-technical
188
194
  // users live in the web UI, not the terminal).
189
195
  this.finalReportBySession.set(session.sessionId, report);
196
+ // v0.6.x — persist to disk so the report survives daemon restart.
197
+ // Pre-fix: founder's Mac restart 2026-05-03 left a session card
198
+ // showing "Generating report…" forever because reports lived only
199
+ // in this in-memory Map. Append-only finalReport record paired
200
+ // with the existing spawned/completed records by sessionId.
201
+ appendFinalReport({
202
+ id: session.sessionId,
203
+ summary: report.summary,
204
+ generatedAt: report.generatedAt,
205
+ durationMs: report.durationMs,
206
+ });
190
207
  try {
191
208
  printFinalReportToTerminal(reportInput, report);
192
209
  }
@@ -213,6 +230,10 @@ export class SessionManager {
213
230
  getCompletedAt(sessionId) {
214
231
  return this.completedAtBySession.get(sessionId);
215
232
  }
233
+ /** v0.6.x Slack-shape spike — return cwd for an in-memory session. */
234
+ getCwd(sessionId) {
235
+ return this.cwdBySession.get(sessionId);
236
+ }
216
237
  /** PM-UX Layer 3 · prose Final Report for an in-memory exited session.
217
238
  * Returns undefined while generation is in flight OR if the provider chain
218
239
  * failed (no claude-free, no claude, no credit, etc). The web UI reads this
@@ -46,6 +46,27 @@ export function appendCompleted(rec) {
46
46
  /* best-effort */
47
47
  }
48
48
  }
49
+ /**
50
+ * v0.6.x — persist a Final Report so it survives daemon restart.
51
+ *
52
+ * Pre-fix: Final Reports lived only in SessionManager.finalReportBySession
53
+ * (in-memory Map) and were lost on every daemon bounce. Founder hit this
54
+ * 2026-05-03 after a Mac restart: completed sessions showed
55
+ * "Generating report…" forever because there was no on-disk record + no
56
+ * "report-unavailable" state in the UI.
57
+ *
58
+ * Append-only: Final Report fires AFTER the CompletedRecord (generateFinalReport
59
+ * is async post-exit), so it gets its own record type. Reader merges by id.
60
+ */
61
+ export function appendFinalReport(rec) {
62
+ try {
63
+ ensureDir();
64
+ appendFileSync(SESSIONS_FILE, JSON.stringify({ t: 'finalReport', ...rec }) + '\n', 'utf-8');
65
+ }
66
+ catch {
67
+ /* best-effort */
68
+ }
69
+ }
49
70
  /**
50
71
  * Walk the session log backward, pair spawned+completed records by id, return
51
72
  * the most recent `limit` completed pairs sorted newest-first.
@@ -67,6 +88,9 @@ export function loadRecent(limit = 20) {
67
88
  }
68
89
  const spawned = new Map();
69
90
  const completed = [];
91
+ /** v0.6.x — Final Reports keyed by sessionId. Last write wins
92
+ * (rare in practice; reports fire once per session). */
93
+ const finalReports = new Map();
70
94
  for (const line of content.split('\n')) {
71
95
  const trimmed = line.trim();
72
96
  if (!trimmed)
@@ -82,6 +106,8 @@ export function loadRecent(limit = 20) {
82
106
  spawned.set(rec.id, rec);
83
107
  else if (rec.t === 'completed')
84
108
  completed.push(rec);
109
+ else if (rec.t === 'finalReport')
110
+ finalReports.set(rec.id, rec);
85
111
  }
86
112
  const merged = [];
87
113
  // Walk completed in reverse so we return newest-first
@@ -90,6 +116,7 @@ export function loadRecent(limit = 20) {
90
116
  const s = spawned.get(c.id);
91
117
  if (!s)
92
118
  continue; // orphan completed (shouldn't happen but harmless)
119
+ const fr = finalReports.get(c.id);
93
120
  merged.push({
94
121
  sessionId: s.id,
95
122
  agent: s.agent,
@@ -113,6 +140,7 @@ export function loadRecent(limit = 20) {
113
140
  completedAt: c.completedAt,
114
141
  steps: c.steps,
115
142
  artifacts: c.artifacts,
143
+ ...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
116
144
  provenance: 'persisted',
117
145
  });
118
146
  }
@@ -30,6 +30,8 @@ import { SessionManager, availableAgents, readinessForAllAgents, getAdapter, all
30
30
  import { isAbsolute } from 'path';
31
31
  import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
32
32
  import { listProjects, readProjectThread } from '../storage/project-thread-reader.js';
33
+ import { mirrorReplyToThread } from '../storage/project-thread-mirror.js';
34
+ import { randomUUID } from 'crypto';
33
35
  import { installAgent } from '../orchestration/agent-installer.js';
34
36
  import { detectReadiness } from '../orchestration/readiness.js';
35
37
  // ── Tool definitions ─────────────────────────────────────────────────────────
@@ -653,6 +655,7 @@ export function startMCPServer(options = {}) {
653
655
  // in completedAtBySession map (populated when exit event fires).
654
656
  const completedAt = manager.getCompletedAt(s.sessionId);
655
657
  const finalReport = manager.getFinalReport(s.sessionId);
658
+ const cwd = manager.getCwd(s.sessionId);
656
659
  return {
657
660
  sessionId: s.sessionId,
658
661
  prompt: s.prompt,
@@ -666,6 +669,10 @@ export function startMCPServer(options = {}) {
666
669
  // Iter 12 — surface canAppendMessage so the UI knows whether to
667
670
  // render the reply input or a disabled "follow-up unsupported" state.
668
671
  canAppendMessage: s.canAppendMessage === true,
672
+ // v0.6.x Slack-shape spike — surface cwd so the dashboard's
673
+ // "Continue thread" intake button can find the most recent
674
+ // in-memory session for the current folder.
675
+ ...(cwd !== undefined ? { cwd } : {}),
669
676
  provenance: 'spawned',
670
677
  };
671
678
  });
@@ -743,7 +750,32 @@ export function startMCPServer(options = {}) {
743
750
  res.end(JSON.stringify({ error: 'text is required' }));
744
751
  return;
745
752
  }
753
+ const replyStartedAt = Date.now();
746
754
  const out = await session.appendMessage(text);
755
+ const replyCompletedAt = Date.now();
756
+ // v0.6.x — forward-mirror the reply turn into the project thread
757
+ // so /threads sees follow-ups as visible chronological turns
758
+ // (not just the original spawn). Best-effort: mirror failures
759
+ // never break the reply path.
760
+ try {
761
+ const replyCwd = manager.getCwd(sessionId);
762
+ if (replyCwd) {
763
+ mirrorReplyToThread({
764
+ parentSessionId: sessionId,
765
+ replySessionId: randomUUID(),
766
+ agent: session.agent,
767
+ prompt: text,
768
+ cwd: replyCwd,
769
+ startedAt: replyStartedAt,
770
+ completedAt: replyCompletedAt,
771
+ durationMs: out.durationMs,
772
+ result: out.result,
773
+ });
774
+ }
775
+ }
776
+ catch {
777
+ /* mirror is best-effort */
778
+ }
747
779
  res.writeHead(200, { 'Content-Type': 'application/json' });
748
780
  res.end(JSON.stringify({ sessionId, ...out }));
749
781
  }
@@ -515,12 +515,14 @@ ${SHARED_STYLES}
515
515
  <a href="/" class="nav-link">Tasks</a>
516
516
  <a href="/threads" class="nav-link">Threads</a>
517
517
  <a href="/history" class="nav-link active">History</a>
518
- <span style="color:var(--fg-40);font-family:var(--mono);font-size:11px;">
519
- <span id="live-dot" class="dot"></span><span id="conn">connecting</span>
520
- </span>
521
518
  </nav>
522
519
 
523
- <div class="tagline" style="margin-top:90px">One feed. Every agent. Every action.</div>
520
+ <div class="tagline" style="margin-top:90px">
521
+ One feed. Every agent. Every action.
522
+ <span class="conn-inline" style="margin-left:12px;font-family:var(--mono);font-size:10px;color:var(--fg-40);">
523
+ <span id="live-dot" class="dot"></span><span id="conn">connecting</span>
524
+ </span>
525
+ </div>
524
526
 
525
527
  <div class="status">
526
528
  <div class="stat">
@@ -865,6 +867,17 @@ ${SHARED_STYLES}
865
867
  }
866
868
  .intake button.spawn:hover { filter: brightness(1.05); }
867
869
  .intake button.spawn[disabled] { opacity: 0.45; cursor: default; }
870
+ /* v0.6.x Slack-shape spike — alternative submit that routes the prompt as a
871
+ reply to the most-recent task in the current cwd, exercising the existing
872
+ appendMessage path. Lets a single intake feel like an ongoing conversation
873
+ when the user wants continuity, while preserving "Assign" as the explicit
874
+ new-task path. Disabled when no thread exists for the current cwd. */
875
+ .intake button.continue-thread {
876
+ background: rgba(255,154,118,0.10); color: var(--accent); font-weight: 600;
877
+ border-color: rgba(255,154,118,0.3);
878
+ }
879
+ .intake button.continue-thread:hover:not([disabled]) { background: rgba(255,154,118,0.18); }
880
+ .intake button.continue-thread[disabled] { opacity: 0.4; cursor: default; }
868
881
  .intake .hint { margin-left: auto; color: var(--fg-40); font-family: var(--mono); font-size: 11px; }
869
882
 
870
883
  /* ── Setup panel (onboarding — appears when a non-ready agent is picked) ───
@@ -1233,11 +1246,12 @@ ${SHARED_STYLES}
1233
1246
  <div class="intake-row">
1234
1247
  <select id="agent" title="Auto-pick agent · codehere routes. Override below."><option value="">Auto-pick agent</option></select>
1235
1248
  <input type="text" id="cwd" class="cwd" placeholder="folder" spellcheck="false" />
1236
- <button class="spawn" id="spawn">Assign</button>
1249
+ <button class="continue-thread" id="continue-thread" type="button" disabled title="Send as a reply to the most recent task in this folder (Slack-shape spike)">Continue thread</button>
1250
+ <button class="spawn" id="spawn">Assign new</button>
1237
1251
  </div>
1238
1252
  <div class="setup-panel" id="setup-panel" data-state=""></div>
1239
1253
  <div class="intake-row" style="margin-top:6px">
1240
- <span class="hint" style="margin-left:auto">⌘ + Enter</span>
1254
+ <span class="hint" style="margin-left:auto">⌘+Enter = Assign new · ⌘+Shift+Enter = Continue thread</span>
1241
1255
  </div>
1242
1256
  </div>
1243
1257
 
@@ -2085,13 +2099,22 @@ ${SHARED_STYLES}
2085
2099
  // Iter 12 — prose summary (3 paragraphs, plain English) prepended above
2086
2100
  // the artifact list. Non-technical users see prose first; raw artifact
2087
2101
  // detail (files/tests/tokens) below as supporting context.
2088
- function renderArtifacts(artifacts, finalReport, status) {
2102
+ function renderArtifacts(artifacts, finalReport, status, completedAt) {
2089
2103
  // Show the block if either prose summary OR artifacts exist OR if a
2090
2104
  // completed task is still generating its report (placeholder state).
2091
2105
  const isCompleted = status === 'completed' || status === 'killed' || status === 'error';
2092
2106
  const hasContent = artifacts || finalReport;
2093
- const isAwaitingReport = isCompleted && !finalReport;
2094
- if (!hasContent && !isAwaitingReport) { finalReportEl.style.display = 'none'; return; }
2107
+ // v0.6.x — bound the "Generating report…" placeholder to the actual
2108
+ // generation window. Final Report typically returns within 30s; after
2109
+ // 90s post-completion with no report, the report either failed silently
2110
+ // or the daemon was restarted (in-memory finalReportBySession Map gets
2111
+ // wiped). Either way, "Generating report…" forever is dishonest.
2112
+ const REPORT_WAIT_BUDGET_MS = 90_000;
2113
+ const sinceCompleted = completedAt ? Date.now() - completedAt : 0;
2114
+ const stillWaitingForReport = isCompleted && !finalReport && sinceCompleted < REPORT_WAIT_BUDGET_MS;
2115
+ const reportTimedOut = isCompleted && !finalReport && sinceCompleted >= REPORT_WAIT_BUDGET_MS;
2116
+ const isAwaitingReport = stillWaitingForReport;
2117
+ if (!hasContent && !isAwaitingReport && !reportTimedOut) { finalReportEl.style.display = 'none'; return; }
2095
2118
  finalReportEl.style.display = 'block';
2096
2119
  const files = Array.isArray(artifacts?.files) ? artifacts.files : [];
2097
2120
  const tests = artifacts?.tests;
@@ -2106,10 +2129,13 @@ ${SHARED_STYLES}
2106
2129
  paragraphs.map(p => '<p>' + escape(p.trim()) + '</p>').join('') +
2107
2130
  '</div>';
2108
2131
  } else if (isAwaitingReport) {
2109
- // Placeholder — task done, report still generating (or provider failed silently).
2110
- // After ~30s with no report, the placeholder persists. Iter 13 will replace
2111
- // with explicit "report unavailable — start FCC proxy or set ANTHROPIC_API_KEY".
2112
2132
  html += '<div class="fr-summary fr-summary-pending"><p>Generating report…</p></div>';
2133
+ } else if (reportTimedOut) {
2134
+ // Honest message instead of perpetual "Generating report…". Most
2135
+ // common cause: daemon restarted (Final Report is in-memory only,
2136
+ // doesn't persist to disk yet — v0.6.x cleanup item). Less common:
2137
+ // the report provider (claude-free / claude) failed silently.
2138
+ html += '<div class="fr-summary fr-summary-pending"><p>Final Report unavailable for this session. (Most often this means the daemon restarted — reports aren\\'t persisted yet. Re-spawn to get a fresh one.)</p></div>';
2113
2139
  }
2114
2140
  if (files.length > 0) {
2115
2141
  html += '<div class="fr-files">' + files.map(f => {
@@ -2147,7 +2173,7 @@ ${SHARED_STYLES}
2147
2173
  // Render Final Report block if there's anything to show (prose summary,
2148
2174
  // artifacts, or "generating…" placeholder for a freshly-completed session).
2149
2175
  if (session.artifacts || session.finalReport || session.status === 'completed' || session.status === 'killed' || session.status === 'error') {
2150
- renderArtifacts(session.artifacts, session.finalReport, session.status);
2176
+ renderArtifacts(session.artifacts, session.finalReport, session.status, session.completedAt);
2151
2177
  }
2152
2178
 
2153
2179
  // Persisted sessions have no live event stream and no subprocess to kill.
@@ -2164,7 +2190,7 @@ ${SHARED_STYLES}
2164
2190
 
2165
2191
  // Layer 2/3 synthetic events — handle before renderEvent (so they don't leak into the raw log)
2166
2192
  if (event.type === 'codehere:steps') { renderSteps(event.data?.steps); return; }
2167
- if (event.type === 'codehere:artifacts') { renderArtifacts(event.data, undefined, session.status); return; }
2193
+ if (event.type === 'codehere:artifacts') { renderArtifacts(event.data, undefined, session.status, session.completedAt); return; }
2168
2194
 
2169
2195
  // Any structured error flips the status pill immediately — don't wait for
2170
2196
  // the subprocess to exit. Many CLIs print a logical error to stdout and
@@ -2234,10 +2260,95 @@ ${SHARED_STYLES}
2234
2260
  }
2235
2261
 
2236
2262
  spawnBtn.addEventListener('click', assignTask);
2263
+
2264
+ // v0.6.x Slack-shape spike — Continue thread button + Cmd+Shift+Enter.
2265
+ // Routes the prompt as a reply to the most recent in-memory session for
2266
+ // the current cwd via the existing /api/task/:id/message endpoint.
2267
+ // Disabled when no eligible session exists; auto-refresh state every 5s
2268
+ // alongside the existing tasks polling.
2269
+ const continueBtn = document.getElementById('continue-thread');
2270
+ let continueTargetSessionId = null;
2271
+
2272
+ function findContinueTarget(spawnedList, cwd) {
2273
+ if (!cwd) return null;
2274
+ const eligible = (spawnedList || []).filter(s => s.cwd === cwd && s.canAppendMessage === true);
2275
+ if (eligible.length === 0) return null;
2276
+ eligible.sort((a, b) => b.startedAt - a.startedAt);
2277
+ return eligible[0];
2278
+ }
2279
+
2280
+ function refreshContinueAvailability() {
2281
+ const cwd = cwdInput.value.trim();
2282
+ if (!cwd) {
2283
+ continueTargetSessionId = null;
2284
+ continueBtn.disabled = true;
2285
+ continueBtn.title = 'Set a folder first to continue its thread';
2286
+ return;
2287
+ }
2288
+ fetch('/api/tasks').then(r => r.json()).then(d => {
2289
+ const target = findContinueTarget(d.spawned, cwd);
2290
+ if (target) {
2291
+ continueTargetSessionId = target.sessionId;
2292
+ continueBtn.disabled = false;
2293
+ const promptPreview = (target.prompt || '').slice(0, 40);
2294
+ continueBtn.title = 'Reply to last task in this folder: "' + promptPreview + (target.prompt && target.prompt.length > 40 ? '…' : '') + '" (' + target.agent + ')';
2295
+ } else {
2296
+ continueTargetSessionId = null;
2297
+ continueBtn.disabled = true;
2298
+ continueBtn.title = 'No live session for this folder. Use Assign new instead.';
2299
+ }
2300
+ }).catch(() => {});
2301
+ }
2302
+
2303
+ async function continueThread() {
2304
+ const prompt = promptEl.value.trim();
2305
+ if (!prompt) return;
2306
+ if (!continueTargetSessionId) { alert('No live session for this folder. Use Assign new instead.'); return; }
2307
+ const targetId = continueTargetSessionId;
2308
+ continueBtn.disabled = true;
2309
+ const originalText = continueBtn.textContent;
2310
+ continueBtn.textContent = '...';
2311
+ try {
2312
+ const res = await fetch('/api/task/' + encodeURIComponent(targetId) + '/message', {
2313
+ method: 'POST',
2314
+ headers: { 'Content-Type': 'application/json' },
2315
+ body: JSON.stringify({ text: prompt }),
2316
+ });
2317
+ const data = await res.json();
2318
+ if (!res.ok) { alert('Failed: ' + (data.error || res.status)); return; }
2319
+ promptEl.value = '';
2320
+ // Scroll the existing card into view; existing 5s polling refreshes content.
2321
+ const card = document.querySelector('[data-session-id="' + targetId + '"]');
2322
+ if (card) card.scrollIntoView({ behavior: 'smooth', block: 'start' });
2323
+ } catch (e) {
2324
+ alert('Network error: ' + (e && e.message ? e.message : e));
2325
+ } finally {
2326
+ continueBtn.disabled = false;
2327
+ continueBtn.textContent = originalText;
2328
+ // Re-check availability (target may have settled differently)
2329
+ refreshContinueAvailability();
2330
+ }
2331
+ }
2332
+
2333
+ continueBtn.addEventListener('click', continueThread);
2334
+ cwdInput.addEventListener('input', refreshContinueAvailability);
2335
+ cwdInput.addEventListener('change', refreshContinueAvailability);
2336
+
2237
2337
  promptEl.addEventListener('keydown', (e) => {
2238
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); assignTask(); }
2338
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
2339
+ e.preventDefault();
2340
+ if (e.shiftKey) {
2341
+ continueThread();
2342
+ } else {
2343
+ assignTask();
2344
+ }
2345
+ }
2239
2346
  });
2240
2347
 
2348
+ // Refresh continue-target availability alongside the existing tasks polling
2349
+ setInterval(refreshContinueAvailability, 5000);
2350
+ refreshContinueAvailability();
2351
+
2241
2352
  // ── Observed sessions (F1d) ──────────────────────────────────────────
2242
2353
  function shortId(id) { return String(id || '').slice(0, 8); }
2243
2354
 
@@ -2371,7 +2482,7 @@ ${SHARED_STYLES}
2371
2482
  for (const s of (d.spawned || d.sessions || [])) {
2372
2483
  const entry = sessions.get(s.sessionId);
2373
2484
  if (entry && entry.renderArtifacts) {
2374
- entry.renderArtifacts(s.artifacts, s.finalReport, s.status);
2485
+ entry.renderArtifacts(s.artifacts, s.finalReport, s.status, s.completedAt);
2375
2486
  }
2376
2487
  }
2377
2488
  }).catch(() => { /* fine */ });
@@ -2440,24 +2551,10 @@ ${SHARED_STYLES}
2440
2551
  html, body { height: 100%; }
2441
2552
  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
2553
 
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); }
2554
+ /* Floating nav inherits from SHARED_STYLES (no override here — keeps the
2555
+ active-state look identical to / and /history. Founder caught
2556
+ 2026-05-03 that the local override gave /threads a different orange-bg
2557
+ active state, fragmenting the design system across pages.) */
2461
2558
 
2462
2559
  /* ── Slack-shape shell (3-column — channels | thread | members) ─────── */
2463
2560
  .shell.slack-shape {
@@ -156,4 +156,37 @@ export function mirrorCompletedToThread(rec) {
156
156
  artifacts: rec.artifacts,
157
157
  });
158
158
  }
159
+ /**
160
+ * v0.6.x — mirror a successful appendMessage() reply to the project thread.
161
+ *
162
+ * Pre-fix: only initial spawn + completion wrote to thread.jsonl. Reply turns
163
+ * (the `/api/task/:id/message` endpoint, the dashboard reply box, and the new
164
+ * /threads reply bar) updated only the in-memory session. Result: /threads
165
+ * showed the original spawn but every follow-up turn was invisible. Founder
166
+ * flagged it 2026-05-03 as a v0.6.x cleanup item.
167
+ *
168
+ * Schema choice: write a single complete `{type: 'task', sessionId: <new>,
169
+ * parentSessionId: <orig>, ...}` entry rather than splitting spawn+complete.
170
+ * appendMessage is synchronous-from-caller's-POV (no streaming), so the whole
171
+ * turn is known when this fires. The reader's sessionId merge logic surfaces
172
+ * each reply as its own chronological row on /threads.
173
+ */
174
+ export function mirrorReplyToThread(rec) {
175
+ if (isTempDir(rec.cwd))
176
+ return;
177
+ const slug = slugForCwd(rec.cwd);
178
+ appendThreadEntry(slug, {
179
+ type: 'task',
180
+ sessionId: rec.replySessionId,
181
+ parentSessionId: rec.parentSessionId,
182
+ agent: rec.agent,
183
+ prompt: rec.prompt,
184
+ cwd: rec.cwd,
185
+ startedAt: rec.startedAt,
186
+ completedAt: rec.completedAt,
187
+ durationMs: rec.durationMs,
188
+ status: 'completed',
189
+ ...(rec.result ? { result: rec.result } : {}),
190
+ });
191
+ }
159
192
  //# sourceMappingURL=project-thread-mirror.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehere",
3
- "version": "0.5.0-alpha.5",
3
+ "version": "0.5.0-alpha.6",
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",