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.
- package/dist/infrastructure/orchestration/session-manager.js +22 -1
- package/dist/infrastructure/orchestration/session-persistence.js +28 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +32 -0
- package/dist/infrastructure/protocols/ui-page.js +131 -34
- package/dist/infrastructure/storage/project-thread-mirror.js +33 -0
- package/package.json +1 -1
|
@@ -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">
|
|
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="
|
|
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"
|
|
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
|
-
|
|
2094
|
-
|
|
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') {
|
|
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
|
-
/*
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
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.
|
|
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",
|