codehere 0.5.0-alpha.2 → 0.5.0-alpha.3
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 +144 -0
- package/dist/index.js +5 -5
- package/dist/infrastructure/orchestration/claude-code-adapter.js +61 -0
- package/dist/infrastructure/orchestration/session-manager.js +65 -2
- package/dist/infrastructure/protocols/mcp-server-impl.js +56 -11
- package/dist/infrastructure/protocols/ui-page.js +202 -13
- package/dist/presentation/cli/commands/init-command.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final Report — PM-UX Layer 3 · MVP (v0.5.0-alpha.3+).
|
|
3
|
+
*
|
|
4
|
+
* At task exit, generates a 3-paragraph plain-English summary of what the
|
|
5
|
+
* agent did, what was touched, and what the user should look at. Prints to
|
|
6
|
+
* stdout so the dogfood-while-building loop sees the artifact immediately.
|
|
7
|
+
*
|
|
8
|
+
* Provider strategy (MVP — try free first, fall back to paid):
|
|
9
|
+
* - Tries `claude-free --bare -p "<prompt>"` first. The `claude-free`
|
|
10
|
+
* wrapper is what actually routes through the FCC proxy at :8082
|
|
11
|
+
* (verified 2026-05-02 dogfood: plain `claude --bare` does NOT route
|
|
12
|
+
* through FCC — it hits paid Claude. The May 1 memory rule was wrong.)
|
|
13
|
+
* - Falls back to `claude --bare -p "<prompt>"` if `claude-free` is not
|
|
14
|
+
* installed (paid path; will fail gracefully if no credit).
|
|
15
|
+
* - If neither binary is installed, returns null silently (no report).
|
|
16
|
+
*
|
|
17
|
+
* Env override: set CODEHERE_REPORT_BINARY=<binary-name> to force a
|
|
18
|
+
* specific provider (e.g. for testing or non-default setups).
|
|
19
|
+
*
|
|
20
|
+
* Iterations 5-9 (2026-05-02) refuted the "we own the supervisory layer"
|
|
21
|
+
* thesis. Final Report is the keystone delivery moment per the v3 chain
|
|
22
|
+
* (claim_market_position) — it's the artifact the dogfood loop produces
|
|
23
|
+
* that none of the surveyed competitors (ComposioHQ, Overstory, Praktor,
|
|
24
|
+
* Claude Squad, AI-Agents-Orchestrator) advertise.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn } from 'child_process';
|
|
27
|
+
const PROMPT_TEMPLATE = (i) => `You are a PM reporting back to a developer about what their AI coding agent
|
|
28
|
+
just did. Write 3 short paragraphs in plain English. No headers, no bullet
|
|
29
|
+
lists, no markdown — just three paragraphs separated by blank lines.
|
|
30
|
+
|
|
31
|
+
Paragraph 1: What the agent DID. Verb-led. Name files specifically. 1-3 sentences.
|
|
32
|
+
Paragraph 2: What was TOUCHED. Files modified, line counts, anything that looks risky. 1-3 sentences.
|
|
33
|
+
Paragraph 3: What the developer should LOOK AT. Open concerns, things to verify.
|
|
34
|
+
If everything looks clean, say so explicitly. 1-2 sentences.
|
|
35
|
+
|
|
36
|
+
Be specific. Don't pad. Don't editorialize. Don't praise the agent.
|
|
37
|
+
|
|
38
|
+
The task was: ${i.prompt}
|
|
39
|
+
The agent used: ${i.agent}
|
|
40
|
+
Duration: ${i.durationMs}ms
|
|
41
|
+
Status: ${i.status}
|
|
42
|
+
|
|
43
|
+
Steps the agent took:
|
|
44
|
+
${i.steps.length ? i.steps.map((s) => `- ${s.label} (${s.state})`).join('\n') : '(no structured steps captured)'}
|
|
45
|
+
|
|
46
|
+
Files changed:
|
|
47
|
+
${i.artifacts?.files?.length
|
|
48
|
+
? i.artifacts.files.map((f) => `- ${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust: ${f.trust}` : ''})`).join('\n')
|
|
49
|
+
: '(no file artifacts captured)'}
|
|
50
|
+
|
|
51
|
+
Token usage: ${i.artifacts?.usage?.inputTokens ?? 0} in / ${i.artifacts?.usage?.outputTokens ?? 0} out
|
|
52
|
+
|
|
53
|
+
Now write the 3 paragraphs.`;
|
|
54
|
+
const REPORT_TIMEOUT_MS = 30_000;
|
|
55
|
+
/** Provider chain: try free first, then paid. Override with CODEHERE_REPORT_BINARY. */
|
|
56
|
+
function selectProviderBinary() {
|
|
57
|
+
const override = process.env.CODEHERE_REPORT_BINARY;
|
|
58
|
+
if (override)
|
|
59
|
+
return [override];
|
|
60
|
+
return ['claude-free', 'claude'];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate a Final Report. Returns null if no provider is reachable.
|
|
64
|
+
* Never throws — failure modes return null + log a debug line.
|
|
65
|
+
*/
|
|
66
|
+
export async function generateFinalReport(input) {
|
|
67
|
+
const prompt = PROMPT_TEMPLATE(input);
|
|
68
|
+
for (const binary of selectProviderBinary()) {
|
|
69
|
+
const result = await tryProvider(binary, prompt, input.sessionId);
|
|
70
|
+
if (result)
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function tryProvider(binary, prompt, sessionId) {
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let stdout = '';
|
|
79
|
+
let stderr = '';
|
|
80
|
+
let settled = false;
|
|
81
|
+
const finish = (result) => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
84
|
+
settled = true;
|
|
85
|
+
resolve(result);
|
|
86
|
+
};
|
|
87
|
+
const child = spawn(binary, ['--bare', '-p', prompt], {
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
});
|
|
90
|
+
const timeout = setTimeout(() => {
|
|
91
|
+
child.kill('SIGTERM');
|
|
92
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
93
|
+
console.error(`[final-report] ${binary} timeout after ${REPORT_TIMEOUT_MS}ms for ${sessionId}`);
|
|
94
|
+
}
|
|
95
|
+
finish(null);
|
|
96
|
+
}, REPORT_TIMEOUT_MS);
|
|
97
|
+
child.on('error', (err) => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
// ENOENT = binary not installed. Silent fallback to next provider in chain.
|
|
100
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
101
|
+
console.error(`[final-report] ${binary} unavailable: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
finish(null);
|
|
104
|
+
});
|
|
105
|
+
child.stdout?.on('data', (chunk) => {
|
|
106
|
+
stdout += chunk.toString('utf8');
|
|
107
|
+
});
|
|
108
|
+
child.stderr?.on('data', (chunk) => {
|
|
109
|
+
stderr += chunk.toString('utf8');
|
|
110
|
+
});
|
|
111
|
+
child.on('exit', (code) => {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
if (code !== 0) {
|
|
114
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
115
|
+
console.error(`[final-report] ${binary} exited ${code}: ${stderr.slice(0, 200)}`);
|
|
116
|
+
}
|
|
117
|
+
finish(null);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const summary = stdout.trim();
|
|
121
|
+
if (!summary) {
|
|
122
|
+
finish(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
finish({ summary, generatedAt: Date.now(), durationMs: Date.now() - startedAt });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Pretty-print to terminal. Used by session-manager when a spawned task exits.
|
|
131
|
+
* Wrapped in try/catch upstream — never let a print failure surface.
|
|
132
|
+
*/
|
|
133
|
+
export function printFinalReportToTerminal(input, output) {
|
|
134
|
+
const banner = '─'.repeat(60);
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
process.stdout.write(`${banner}\n`);
|
|
137
|
+
process.stdout.write(`Final Report · ${input.agent} · ${input.sessionId.slice(0, 8)}\n`);
|
|
138
|
+
process.stdout.write(`${banner}\n`);
|
|
139
|
+
process.stdout.write(`${output.summary}\n`);
|
|
140
|
+
process.stdout.write(`${banner}\n`);
|
|
141
|
+
process.stdout.write(`(generated in ${output.durationMs}ms)\n`);
|
|
142
|
+
process.stdout.write('\n');
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=generate-final-report.js.map
|
package/dist/index.js
CHANGED
|
@@ -139,7 +139,7 @@ program
|
|
|
139
139
|
// trust-scorer pattern bank to .codehere/protocols/hook_patterns.json so
|
|
140
140
|
// auditors / cross-tool consumers can read codehere's safety surface.
|
|
141
141
|
program
|
|
142
|
-
.command('protocols')
|
|
142
|
+
.command('protocols', { hidden: true }) // hidden from --help (May 2 cut: power-user F3 export, see docs/MEMORY.md)
|
|
143
143
|
.description('F3 protocols layer · export (write hook_patterns.json from trust-scorer bank)')
|
|
144
144
|
.argument('[action]', 'Action: export, help')
|
|
145
145
|
.option('--rebuild', 'Refresh built-in patterns (project patterns are preserved)')
|
|
@@ -345,7 +345,7 @@ program
|
|
|
345
345
|
// ── serve (MCP server) ───────────────────────────────────────────────────────
|
|
346
346
|
program
|
|
347
347
|
.command('serve')
|
|
348
|
-
.description('
|
|
348
|
+
.description('Run codehere as a background MCP server (no web UI) — for Claude Desktop / Cursor integration')
|
|
349
349
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
350
350
|
.option('--host <host>', 'Host to bind to (default: 127.0.0.1)', '127.0.0.1')
|
|
351
351
|
.option('--token <token>', 'Require Bearer token for authentication')
|
|
@@ -406,7 +406,7 @@ program
|
|
|
406
406
|
// ── start (Mission Control: daemon + web UI) ─────────────────────────────────
|
|
407
407
|
program
|
|
408
408
|
.command('start')
|
|
409
|
-
.description('Start
|
|
409
|
+
.description('Start codehere — interactive PM dashboard at localhost:3773 (delegate tasks, watch agents work)')
|
|
410
410
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
411
411
|
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
412
412
|
.option('--no-open', "Don't auto-open the browser")
|
|
@@ -599,7 +599,7 @@ program
|
|
|
599
599
|
});
|
|
600
600
|
// ── scan-tool (called by Claude Code hooks system, not users directly) ───────
|
|
601
601
|
program
|
|
602
|
-
.command('scan-tool')
|
|
602
|
+
.command('scan-tool', { hidden: true }) // hidden from --help (May 2 cut: internal hook handler, called by Claude Code via stdin)
|
|
603
603
|
.description('Hook handler: reads Claude Code tool event from stdin, outputs block/allow (used by hook system)')
|
|
604
604
|
.option('--log-only', 'Never block, log only')
|
|
605
605
|
.option('--block-all', 'Block on any finding, not just critical')
|
|
@@ -613,7 +613,7 @@ program
|
|
|
613
613
|
});
|
|
614
614
|
// ── completion (shell completion script generator) ──────────────────────────
|
|
615
615
|
program
|
|
616
|
-
.command('completion')
|
|
616
|
+
.command('completion', { hidden: true }) // hidden from --help (May 2 cut: shell-completion is npm convention, not product surface)
|
|
617
617
|
.description('Generate shell completion script')
|
|
618
618
|
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
619
619
|
.action(async (shell) => {
|
|
@@ -284,6 +284,7 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
284
284
|
startedAt,
|
|
285
285
|
prompt,
|
|
286
286
|
agent: 'claude-code',
|
|
287
|
+
canAppendMessage: true,
|
|
287
288
|
memoryInjection: injected.record,
|
|
288
289
|
get status() {
|
|
289
290
|
return statusValue;
|
|
@@ -317,6 +318,66 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
317
318
|
}
|
|
318
319
|
}, 3000);
|
|
319
320
|
},
|
|
321
|
+
/**
|
|
322
|
+
* Iter 12 — follow-up message to a completed session. Spawns a fresh
|
|
323
|
+
* `claude -p "<text>" -c` in the same cwd, which uses Claude Code's
|
|
324
|
+
* native -c flag to continue the most recent conversation in that dir.
|
|
325
|
+
*
|
|
326
|
+
* Scope: one-shot Q&A. No streaming, no event piping into the parent
|
|
327
|
+
* emitter. Returns the assistant's response text. Iter 13 may upgrade
|
|
328
|
+
* to a true thread-streaming model if the dogfood signal warrants.
|
|
329
|
+
*/
|
|
330
|
+
appendMessage(text) {
|
|
331
|
+
const startedAt = Date.now();
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
let stdout = '';
|
|
334
|
+
let stderr = '';
|
|
335
|
+
const followup = spawn('claude', ['-p', text, '-c', ...HEADLESS_FLAGS], {
|
|
336
|
+
cwd,
|
|
337
|
+
env: spawnEnv,
|
|
338
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
339
|
+
});
|
|
340
|
+
followup.on('error', (err) => reject(err));
|
|
341
|
+
followup.stdout?.on('data', (chunk) => {
|
|
342
|
+
stdout += chunk.toString('utf-8');
|
|
343
|
+
});
|
|
344
|
+
followup.stderr?.on('data', (chunk) => {
|
|
345
|
+
stderr += chunk.toString('utf-8');
|
|
346
|
+
});
|
|
347
|
+
followup.on('exit', (code) => {
|
|
348
|
+
if (code !== 0) {
|
|
349
|
+
reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Parse stream-json — extract assistant message text from the
|
|
353
|
+
// 'assistant' typed lines and the final 'result' line.
|
|
354
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
355
|
+
const fragments = [];
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
try {
|
|
358
|
+
const obj = JSON.parse(line);
|
|
359
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
360
|
+
for (const block of obj.message.content) {
|
|
361
|
+
if (block.type === 'text' && block.text)
|
|
362
|
+
fragments.push(block.text);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
366
|
+
fragments.push(obj.result);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Non-JSON line — skip
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Dedupe trivially: if 'result' contained the same text as the
|
|
374
|
+
// last assistant block, take just one. Conservative: prefer 'result'
|
|
375
|
+
// if present (it's the final summary), else join assistants.
|
|
376
|
+
const result = fragments.length ? fragments[fragments.length - 1] : '(no response captured)';
|
|
377
|
+
resolve({ result, durationMs: Date.now() - startedAt });
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
},
|
|
320
381
|
};
|
|
321
382
|
}
|
|
322
383
|
// ── AgentAdapter implementation ─────────────────────────────────────────────
|
|
@@ -23,6 +23,7 @@ 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
25
|
import { appendSpawned, appendCompleted, loadRecent } from './session-persistence.js';
|
|
26
|
+
import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
|
|
26
27
|
// ── Agent registry ─────────────────────────────────────────────────────────────
|
|
27
28
|
// All backends codehere can spawn. Order here drives the default UI dropdown
|
|
28
29
|
// order. Built-in adapters first; user-config-driven ones appended from
|
|
@@ -96,6 +97,18 @@ export class SessionManager {
|
|
|
96
97
|
* These subprocesses are long-dead; we surface them so the user doesn't lose
|
|
97
98
|
* their Final reports across restarts. */
|
|
98
99
|
persistedCache = loadRecent(20);
|
|
100
|
+
/** When a spawned session exits, we capture the completion timestamp here
|
|
101
|
+
* so the API layer can show duration on still-in-memory sessions (the
|
|
102
|
+
* TaskSession interface intentionally has no completedAt; only PersistedSession
|
|
103
|
+
* does, so anything reading session.completedAt got undefined pre-fix).
|
|
104
|
+
* Surfaced May 2 by the cold-start dogfood — `duration: NaNs` in /api/tasks
|
|
105
|
+
* for completed-but-not-yet-restarted sessions. Bug 2 sister-fix. */
|
|
106
|
+
completedAtBySession = new Map();
|
|
107
|
+
/** PM-UX Layer 3 · Final Report cache (iteration 11 MVP, UI-integration iter 12).
|
|
108
|
+
* Populated when generateFinalReport resolves in the exit handler. Web UI
|
|
109
|
+
* reads via getFinalReport() through the /api/tasks serializer so non-technical
|
|
110
|
+
* users see the prose summary without opening a terminal. */
|
|
111
|
+
finalReportBySession = new Map();
|
|
99
112
|
spawn(prompt, agentId = 'claude-code', options) {
|
|
100
113
|
const adapter = getAdapter(agentId);
|
|
101
114
|
if (!adapter) {
|
|
@@ -121,14 +134,50 @@ export class SessionManager {
|
|
|
121
134
|
if (event.type !== 'exit')
|
|
122
135
|
return;
|
|
123
136
|
const data = event.data;
|
|
137
|
+
const completedAt = Date.now();
|
|
138
|
+
// Surface completedAt to API consumers reading the still-in-memory
|
|
139
|
+
// session (Bug 2 sister-fix, May 2). Without this, /api/tasks shows
|
|
140
|
+
// status=completed but no completedAt → NaN duration in the UI.
|
|
141
|
+
this.completedAtBySession.set(session.sessionId, completedAt);
|
|
124
142
|
appendCompleted({
|
|
125
143
|
id: session.sessionId,
|
|
126
144
|
status: session.status,
|
|
127
|
-
durationMs: Number(data?.durationMs ??
|
|
128
|
-
completedAt
|
|
145
|
+
durationMs: Number(data?.durationMs ?? completedAt - session.startedAt),
|
|
146
|
+
completedAt,
|
|
129
147
|
steps: session.steps ? [...session.steps] : undefined,
|
|
130
148
|
artifacts: session.artifacts,
|
|
131
149
|
});
|
|
150
|
+
// PM-UX Layer 3 · Final Report MVP. Fire-and-forget; the report appears
|
|
151
|
+
// in the terminal a few seconds after exit. Failure modes return null
|
|
152
|
+
// silently (claude CLI not installed, timeout, non-zero exit). No SSE,
|
|
153
|
+
// no UI integration in this MVP — terminal output only so the dogfood
|
|
154
|
+
// loop sees the artifact immediately. See `generate-final-report.ts`.
|
|
155
|
+
const reportInput = {
|
|
156
|
+
sessionId: session.sessionId,
|
|
157
|
+
prompt: session.prompt,
|
|
158
|
+
agent: session.agent,
|
|
159
|
+
status: session.status,
|
|
160
|
+
durationMs: completedAt - session.startedAt,
|
|
161
|
+
steps: session.steps ? [...session.steps] : [],
|
|
162
|
+
artifacts: session.artifacts,
|
|
163
|
+
};
|
|
164
|
+
generateFinalReport(reportInput)
|
|
165
|
+
.then((report) => {
|
|
166
|
+
if (report) {
|
|
167
|
+
// Cache for web UI consumption via /api/tasks (iter 12 — non-technical
|
|
168
|
+
// users live in the web UI, not the terminal).
|
|
169
|
+
this.finalReportBySession.set(session.sessionId, report);
|
|
170
|
+
try {
|
|
171
|
+
printFinalReportToTerminal(reportInput, report);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Don't let a print failure crash the daemon.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.catch(() => {
|
|
179
|
+
// generateFinalReport never throws by contract; defensive only.
|
|
180
|
+
});
|
|
132
181
|
unsubscribe();
|
|
133
182
|
});
|
|
134
183
|
return session;
|
|
@@ -137,6 +186,20 @@ export class SessionManager {
|
|
|
137
186
|
listPersisted() {
|
|
138
187
|
return this.persistedCache;
|
|
139
188
|
}
|
|
189
|
+
/** Completion timestamp for an in-memory session, if it has exited.
|
|
190
|
+
* Returns undefined if the session is still running OR was never spawned
|
|
191
|
+
* through this manager. Bug 2 sister-fix (May 2): TaskSession itself has
|
|
192
|
+
* no completedAt field; this method lets the API layer compute durations. */
|
|
193
|
+
getCompletedAt(sessionId) {
|
|
194
|
+
return this.completedAtBySession.get(sessionId);
|
|
195
|
+
}
|
|
196
|
+
/** PM-UX Layer 3 · prose Final Report for an in-memory exited session.
|
|
197
|
+
* Returns undefined while generation is in flight OR if the provider chain
|
|
198
|
+
* failed (no claude-free, no claude, no credit, etc). The web UI reads this
|
|
199
|
+
* through the /api/tasks serializer to render the prose summary inline. */
|
|
200
|
+
getFinalReport(sessionId) {
|
|
201
|
+
return this.finalReportBySession.get(sessionId);
|
|
202
|
+
}
|
|
140
203
|
get(sessionId) {
|
|
141
204
|
return this.sessions.get(sessionId);
|
|
142
205
|
}
|
|
@@ -548,16 +548,29 @@ export function startMCPServer(options = {}) {
|
|
|
548
548
|
return;
|
|
549
549
|
}
|
|
550
550
|
if (req.method === 'GET' && req.url === '/api/tasks') {
|
|
551
|
-
const spawned = manager.list().map((s) =>
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
551
|
+
const spawned = manager.list().map((s) => {
|
|
552
|
+
// Bug 2 sister-fix (May 2): include completedAt for in-memory exited
|
|
553
|
+
// sessions so the UI can compute duration. TaskSession interface
|
|
554
|
+
// intentionally lacks completedAt; SessionManager tracks it separately
|
|
555
|
+
// in completedAtBySession map (populated when exit event fires).
|
|
556
|
+
const completedAt = manager.getCompletedAt(s.sessionId);
|
|
557
|
+
const finalReport = manager.getFinalReport(s.sessionId);
|
|
558
|
+
return {
|
|
559
|
+
sessionId: s.sessionId,
|
|
560
|
+
prompt: s.prompt,
|
|
561
|
+
startedAt: s.startedAt,
|
|
562
|
+
status: s.status,
|
|
563
|
+
agent: s.agent,
|
|
564
|
+
steps: s.steps ?? [],
|
|
565
|
+
artifacts: s.artifacts,
|
|
566
|
+
...(completedAt !== undefined ? { completedAt, durationMs: completedAt - s.startedAt } : {}),
|
|
567
|
+
...(finalReport !== undefined ? { finalReport } : {}),
|
|
568
|
+
// Iter 12 — surface canAppendMessage so the UI knows whether to
|
|
569
|
+
// render the reply input or a disabled "follow-up unsupported" state.
|
|
570
|
+
canAppendMessage: s.canAppendMessage === true,
|
|
571
|
+
provenance: 'spawned',
|
|
572
|
+
};
|
|
573
|
+
});
|
|
561
574
|
const activeAgents = manager.listActiveAgents();
|
|
562
575
|
const persisted = manager.listPersisted();
|
|
563
576
|
// Layer 7: timestamp of most recent hook-audit line (= "last edit on this machine")
|
|
@@ -581,7 +594,7 @@ export function startMCPServer(options = {}) {
|
|
|
581
594
|
return;
|
|
582
595
|
}
|
|
583
596
|
// /api/task/:id/events or /api/task/:id/kill
|
|
584
|
-
const m = req.url.match(/^\/api\/task\/([^/]+)\/(events|kill)$/);
|
|
597
|
+
const m = req.url.match(/^\/api\/task\/([^/]+)\/(events|kill|message)$/);
|
|
585
598
|
if (m) {
|
|
586
599
|
const [, sessionId, verb] = m;
|
|
587
600
|
const session = manager.get(sessionId);
|
|
@@ -612,6 +625,38 @@ export function startMCPServer(options = {}) {
|
|
|
612
625
|
res.end(JSON.stringify({ sessionId, status: session.status }));
|
|
613
626
|
return;
|
|
614
627
|
}
|
|
628
|
+
// Iter 12 — POST /api/task/:id/message — follow-up message to a
|
|
629
|
+
// completed session. The reply-to-thread surface in the web UI calls
|
|
630
|
+
// this. Body: { text }. Response: { result, durationMs }.
|
|
631
|
+
if (verb === 'message' && req.method === 'POST') {
|
|
632
|
+
if (!session.canAppendMessage || !session.appendMessage) {
|
|
633
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
634
|
+
res.end(JSON.stringify({ error: `Agent ${session.agent} does not support follow-up messages yet.` }));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
let body = '';
|
|
638
|
+
req.on('data', (c) => { body += c.toString('utf-8'); });
|
|
639
|
+
req.on('end', async () => {
|
|
640
|
+
try {
|
|
641
|
+
const parsed = JSON.parse(body || '{}');
|
|
642
|
+
const text = (parsed.text ?? '').trim();
|
|
643
|
+
if (!text) {
|
|
644
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
645
|
+
res.end(JSON.stringify({ error: 'text is required' }));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const out = await session.appendMessage(text);
|
|
649
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
650
|
+
res.end(JSON.stringify({ sessionId, ...out }));
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
654
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
655
|
+
res.end(JSON.stringify({ error: msg }));
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
615
660
|
}
|
|
616
661
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
617
662
|
res.end(JSON.stringify({ error: 'Method not allowed for this path' }));
|
|
@@ -253,6 +253,31 @@ const SHARED_STYLES = `
|
|
|
253
253
|
color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.12em; font-size: 10px;
|
|
254
254
|
margin-bottom: 8px;
|
|
255
255
|
}
|
|
256
|
+
/* Iter 12 — prose summary block. Plain English for non-technical users.
|
|
257
|
+
Sans-serif breaks the monospace context so it reads as narrative, not data. */
|
|
258
|
+
.final-report .fr-summary {
|
|
259
|
+
font-family: var(--sans);
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
line-height: 1.55;
|
|
262
|
+
color: var(--fg);
|
|
263
|
+
margin-bottom: 12px;
|
|
264
|
+
padding-bottom: 12px;
|
|
265
|
+
border-bottom: 1px solid var(--border-soft);
|
|
266
|
+
}
|
|
267
|
+
.final-report .fr-summary p {
|
|
268
|
+
margin: 0 0 10px 0;
|
|
269
|
+
}
|
|
270
|
+
.final-report .fr-summary p:last-child { margin-bottom: 0; }
|
|
271
|
+
.final-report .fr-summary-pending p {
|
|
272
|
+
color: var(--fg-50);
|
|
273
|
+
font-style: italic;
|
|
274
|
+
animation: shimmer 1.6s linear infinite;
|
|
275
|
+
background: linear-gradient(90deg, var(--fg-30) 0%, var(--fg-70) 50%, var(--fg-30) 100%);
|
|
276
|
+
background-size: 200% auto;
|
|
277
|
+
-webkit-background-clip: text;
|
|
278
|
+
background-clip: text;
|
|
279
|
+
color: transparent;
|
|
280
|
+
}
|
|
256
281
|
.final-report .fr-files { display: flex; flex-direction: column; gap: 3px; }
|
|
257
282
|
.final-report .fr-file {
|
|
258
283
|
display: grid;
|
|
@@ -1091,8 +1116,30 @@ ${SHARED_STYLES}
|
|
|
1091
1116
|
color: var(--fg-50); font-size: 11px; font-family: var(--mono);
|
|
1092
1117
|
display: flex; align-items: center; gap: 8px;
|
|
1093
1118
|
}
|
|
1094
|
-
.
|
|
1095
|
-
.thread-input
|
|
1119
|
+
/* Iter 12 — reply surface now functional. Wraps textarea + send button. */
|
|
1120
|
+
.thread-input { padding: 8px; flex-direction: column; align-items: stretch; gap: 6px; }
|
|
1121
|
+
.thread-input::before { display: none; }
|
|
1122
|
+
.thread-input .reply-text {
|
|
1123
|
+
width: 100%; min-height: 38px; resize: vertical;
|
|
1124
|
+
background: transparent; border: 0; outline: none;
|
|
1125
|
+
color: var(--fg); font-family: var(--sans); font-size: 12px; line-height: 1.5;
|
|
1126
|
+
padding: 4px 0;
|
|
1127
|
+
}
|
|
1128
|
+
.thread-input .reply-text::placeholder { color: var(--fg-30); }
|
|
1129
|
+
.thread-input .reply-send {
|
|
1130
|
+
align-self: flex-end;
|
|
1131
|
+
padding: 5px 14px; border-radius: 6px;
|
|
1132
|
+
background: var(--accent-wash); color: var(--accent);
|
|
1133
|
+
border: 1px solid rgba(255,154,118,0.25);
|
|
1134
|
+
font-family: var(--sans); font-size: 11px; font-weight: 600;
|
|
1135
|
+
cursor: pointer;
|
|
1136
|
+
}
|
|
1137
|
+
.thread-input .reply-send:hover { background: rgba(255,154,118,0.18); }
|
|
1138
|
+
.thread-input .reply-send:disabled { opacity: 0.4; cursor: default; }
|
|
1139
|
+
.thread-input .reply-status { color: var(--fg-50); font-size: 10px; font-family: var(--mono); }
|
|
1140
|
+
.thread-input .reply-status.error { color: var(--block); }
|
|
1141
|
+
.thread-input.disabled .reply-text,
|
|
1142
|
+
.thread-input.disabled .reply-send { opacity: 0.5; cursor: not-allowed; }
|
|
1096
1143
|
|
|
1097
1144
|
.view-toggle {
|
|
1098
1145
|
display: inline-flex; gap: 0;
|
|
@@ -1165,7 +1212,7 @@ ${SHARED_STYLES}
|
|
|
1165
1212
|
</div>
|
|
1166
1213
|
</div>
|
|
1167
1214
|
</div>
|
|
1168
|
-
<button class="pane-add" type="button" title="
|
|
1215
|
+
<button class="pane-add" type="button" id="new-project-btn" title="Set a new working folder for the next task">+ New project</button>
|
|
1169
1216
|
</aside>
|
|
1170
1217
|
|
|
1171
1218
|
<section class="thread-pane">
|
|
@@ -1459,11 +1506,37 @@ ${SHARED_STYLES}
|
|
|
1459
1506
|
list.innerHTML = all.map((a) => {
|
|
1460
1507
|
const cls = presenceCls(a.state);
|
|
1461
1508
|
const glyph = presenceGlyph(a.state);
|
|
1462
|
-
|
|
1509
|
+
// Iter 12 — members are now clickable. Non-technical user expects
|
|
1510
|
+
// clicking an agent in the right pane to pick that agent for the
|
|
1511
|
+
// next task. Click handler below sets the agent <select> + scrolls
|
|
1512
|
+
// intake into view + focuses the prompt textarea.
|
|
1513
|
+
const stateTitle = a.state === 'ready' ? 'ready · click to select' :
|
|
1514
|
+
a.state === 'needs_install' ? 'needs install · click for setup' :
|
|
1515
|
+
a.state === 'needs_auth' ? 'needs auth · click for setup' :
|
|
1516
|
+
(a.state || 'unknown');
|
|
1517
|
+
return '<div class="member" data-agent-id="' + escape(a.id) + '" title="' + escape(stateTitle) + '">' +
|
|
1463
1518
|
'<span class="member-presence ' + cls + '">' + glyph + '</span>' +
|
|
1464
1519
|
'<span class="member-label">' + escape(a.label || a.id) + '</span>' +
|
|
1465
1520
|
'</div>';
|
|
1466
1521
|
}).join('');
|
|
1522
|
+
// Wire click → select-this-agent. One delegated handler is cheaper than
|
|
1523
|
+
// attaching one per item, and survives re-renders without leak.
|
|
1524
|
+
list.onclick = (ev) => {
|
|
1525
|
+
const item = ev.target && ev.target.closest && ev.target.closest('.member');
|
|
1526
|
+
if (!item) return;
|
|
1527
|
+
const id = item.getAttribute('data-agent-id');
|
|
1528
|
+
if (!id) return;
|
|
1529
|
+
// Set the agent dropdown to this agent. Trigger 'change' so the
|
|
1530
|
+
// updateSetupPanel code (existing) fires and shows install/auth help
|
|
1531
|
+
// for needs_install / needs_auth states.
|
|
1532
|
+
if (agentSelect) {
|
|
1533
|
+
agentSelect.value = id;
|
|
1534
|
+
try { agentSelect.dispatchEvent(new Event('change')); } catch { /* old browsers */ }
|
|
1535
|
+
}
|
|
1536
|
+
// Scroll intake into view + focus the prompt textarea.
|
|
1537
|
+
try { promptEl && promptEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* noop */ }
|
|
1538
|
+
if (promptEl) promptEl.focus();
|
|
1539
|
+
};
|
|
1467
1540
|
}
|
|
1468
1541
|
|
|
1469
1542
|
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
@@ -1496,6 +1569,23 @@ ${SHARED_STYLES}
|
|
|
1496
1569
|
localStorage.setItem('codehere:cwd', cwdInput.value.trim());
|
|
1497
1570
|
});
|
|
1498
1571
|
|
|
1572
|
+
// Iter 12 — "+ New project" button now wired. Previously dead chrome with
|
|
1573
|
+
// title="Coming soon". Multi-project sidebar is iter 13+; for now the button
|
|
1574
|
+
// does the simplest useful thing: clear the cwd input + scroll-and-focus
|
|
1575
|
+
// so the user can type a new folder path. The intake form's existing Assign
|
|
1576
|
+
// flow handles spawning the task in that path.
|
|
1577
|
+
const newProjectBtn = document.getElementById('new-project-btn');
|
|
1578
|
+
if (newProjectBtn) {
|
|
1579
|
+
newProjectBtn.addEventListener('click', () => {
|
|
1580
|
+
cwdInput.value = '';
|
|
1581
|
+
localStorage.removeItem('codehere:cwd');
|
|
1582
|
+
// Smooth-scroll the intake form into view (project sidebar is on the left,
|
|
1583
|
+
// intake is on the right; on narrow screens the user might be scrolled).
|
|
1584
|
+
try { cwdInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* old browsers */ }
|
|
1585
|
+
cwdInput.focus();
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1499
1589
|
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1500
1590
|
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
1501
1591
|
const starterPromptsEl = document.getElementById('starter-prompts');
|
|
@@ -1731,7 +1821,15 @@ ${SHARED_STYLES}
|
|
|
1731
1821
|
// seed prompt, breaking chronological order in thread view.
|
|
1732
1822
|
'<div class="thread-messages"></div>' +
|
|
1733
1823
|
'<div class="final-report" style="display:none"></div>' +
|
|
1734
|
-
|
|
1824
|
+
// Iter 12 — reply surface now functional; replaced the prior dead
|
|
1825
|
+
// placeholder. Active for adapters with canAppendMessage=true
|
|
1826
|
+
// (claude-code today); disabled state for adapters that don't yet
|
|
1827
|
+
// implement appendMessage.
|
|
1828
|
+
'<div class="thread-input">' +
|
|
1829
|
+
'<textarea class="reply-text" placeholder="Reply to thread… (Cmd+Enter to send)" rows="2"></textarea>' +
|
|
1830
|
+
'<button class="reply-send" type="button">Send</button>' +
|
|
1831
|
+
'<div class="reply-status" style="display:none"></div>' +
|
|
1832
|
+
'</div>' +
|
|
1735
1833
|
'<div class="event-log"></div>' +
|
|
1736
1834
|
'</div>';
|
|
1737
1835
|
|
|
@@ -1746,6 +1844,57 @@ ${SHARED_STYLES}
|
|
|
1746
1844
|
const finalReportEl = card.querySelector('.final-report');
|
|
1747
1845
|
const threadEl = card.querySelector('.thread-messages');
|
|
1748
1846
|
const bodyEl = card.querySelector('.session-body');
|
|
1847
|
+
// Iter 12 — reply-to-thread refs + handler.
|
|
1848
|
+
const threadInputEl = card.querySelector('.thread-input');
|
|
1849
|
+
const replyTextEl = card.querySelector('.reply-text');
|
|
1850
|
+
const replySendBtn = card.querySelector('.reply-send');
|
|
1851
|
+
const replyStatusEl = card.querySelector('.reply-status');
|
|
1852
|
+
if (!session.canAppendMessage) {
|
|
1853
|
+
threadInputEl.classList.add('disabled');
|
|
1854
|
+
replyTextEl.placeholder = 'Reply unsupported for ' + agentLabel(session.agent) + ' yet';
|
|
1855
|
+
replyTextEl.disabled = true;
|
|
1856
|
+
replySendBtn.disabled = true;
|
|
1857
|
+
} else {
|
|
1858
|
+
const sendReply = async () => {
|
|
1859
|
+
const text = (replyTextEl.value || '').trim();
|
|
1860
|
+
if (!text) return;
|
|
1861
|
+
replySendBtn.disabled = true;
|
|
1862
|
+
replySendBtn.textContent = '...';
|
|
1863
|
+
replyStatusEl.style.display = 'none';
|
|
1864
|
+
replyStatusEl.classList.remove('error');
|
|
1865
|
+
// Optimistic: render the user's message immediately in the thread.
|
|
1866
|
+
appendThreadMessage(threadMessage({
|
|
1867
|
+
cls: 'user', author: 'You', time: new Date(), content: text,
|
|
1868
|
+
}));
|
|
1869
|
+
replyTextEl.value = '';
|
|
1870
|
+
try {
|
|
1871
|
+
const r = await fetch('/api/task/' + session.sessionId + '/message', {
|
|
1872
|
+
method: 'POST',
|
|
1873
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1874
|
+
body: JSON.stringify({ text }),
|
|
1875
|
+
});
|
|
1876
|
+
const data = await r.json();
|
|
1877
|
+
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
|
1878
|
+
// Render assistant response in the thread.
|
|
1879
|
+
appendThreadMessage(threadMessage({
|
|
1880
|
+
cls: 'agent', author: agentLabel(session.agent), time: new Date(),
|
|
1881
|
+
content: String(data.result || '(empty response)'),
|
|
1882
|
+
}));
|
|
1883
|
+
} catch (err) {
|
|
1884
|
+
replyStatusEl.textContent = 'Reply failed: ' + (err && err.message ? err.message : String(err));
|
|
1885
|
+
replyStatusEl.classList.add('error');
|
|
1886
|
+
replyStatusEl.style.display = 'block';
|
|
1887
|
+
} finally {
|
|
1888
|
+
replySendBtn.disabled = false;
|
|
1889
|
+
replySendBtn.textContent = 'Send';
|
|
1890
|
+
replyTextEl.focus();
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
replySendBtn.addEventListener('click', sendReply);
|
|
1894
|
+
replyTextEl.addEventListener('keydown', (ev) => {
|
|
1895
|
+
if ((ev.metaKey || ev.ctrlKey) && ev.key === 'Enter') { ev.preventDefault(); sendReply(); }
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1749
1898
|
const safetyPillEl = card.querySelector('.safety-pill');
|
|
1750
1899
|
const safetySafeEl = safetyPillEl.querySelector('.safe');
|
|
1751
1900
|
const safetyBlockedEl = safetyPillEl.querySelector('.blocked');
|
|
@@ -1939,12 +2088,35 @@ ${SHARED_STYLES}
|
|
|
1939
2088
|
// Layer 3: render the final report block when the task completes.
|
|
1940
2089
|
// Principles #2 + #7 — density (files/tests/trust in one view) and
|
|
1941
2090
|
// agent-provenance (the report attributes to a specific agent).
|
|
1942
|
-
|
|
1943
|
-
|
|
2091
|
+
// Iter 12 — prose summary (3 paragraphs, plain English) prepended above
|
|
2092
|
+
// the artifact list. Non-technical users see prose first; raw artifact
|
|
2093
|
+
// detail (files/tests/tokens) below as supporting context.
|
|
2094
|
+
function renderArtifacts(artifacts, finalReport, status) {
|
|
2095
|
+
// Show the block if either prose summary OR artifacts exist OR if a
|
|
2096
|
+
// completed task is still generating its report (placeholder state).
|
|
2097
|
+
const isCompleted = status === 'completed' || status === 'killed' || status === 'error';
|
|
2098
|
+
const hasContent = artifacts || finalReport;
|
|
2099
|
+
const isAwaitingReport = isCompleted && !finalReport;
|
|
2100
|
+
if (!hasContent && !isAwaitingReport) { finalReportEl.style.display = 'none'; return; }
|
|
1944
2101
|
finalReportEl.style.display = 'block';
|
|
1945
|
-
const files = Array.isArray(artifacts
|
|
1946
|
-
const tests = artifacts
|
|
2102
|
+
const files = Array.isArray(artifacts?.files) ? artifacts.files : [];
|
|
2103
|
+
const tests = artifacts?.tests;
|
|
1947
2104
|
let html = '<div class="fr-label">Final report · ' + escape(agentLabel(session.agent)) + '</div>';
|
|
2105
|
+
if (finalReport && finalReport.summary) {
|
|
2106
|
+
// Plain-English prose. Split paragraphs on blank lines, render each as <p>.
|
|
2107
|
+
// NOTE: this code is inside a TS template literal — backslashes in
|
|
2108
|
+
// regex literals must be doubled so the served JS gets the right
|
|
2109
|
+
// sequence (test/cli/ui-served-html-syntax.test.ts guards this).
|
|
2110
|
+
const paragraphs = String(finalReport.summary).split(/\\n\\s*\\n/).filter(p => p.trim());
|
|
2111
|
+
html += '<div class="fr-summary">' +
|
|
2112
|
+
paragraphs.map(p => '<p>' + escape(p.trim()) + '</p>').join('') +
|
|
2113
|
+
'</div>';
|
|
2114
|
+
} else if (isAwaitingReport) {
|
|
2115
|
+
// Placeholder — task done, report still generating (or provider failed silently).
|
|
2116
|
+
// After ~30s with no report, the placeholder persists. Iter 13 will replace
|
|
2117
|
+
// with explicit "report unavailable — start FCC proxy or set ANTHROPIC_API_KEY".
|
|
2118
|
+
html += '<div class="fr-summary fr-summary-pending"><p>Generating report…</p></div>';
|
|
2119
|
+
}
|
|
1948
2120
|
if (files.length > 0) {
|
|
1949
2121
|
html += '<div class="fr-files">' + files.map(f => {
|
|
1950
2122
|
const lines = '<span class="add">+' + (f.added || 0) + '</span> <span class="rem">−' + (f.removed || 0) + '</span>';
|
|
@@ -1978,7 +2150,11 @@ ${SHARED_STYLES}
|
|
|
1978
2150
|
|
|
1979
2151
|
// Hydrate from initial snapshot (late-joiner support + persisted sessions)
|
|
1980
2152
|
if (Array.isArray(session.steps) && session.steps.length) renderSteps(session.steps);
|
|
1981
|
-
if (
|
|
2153
|
+
// Render Final Report block if there's anything to show (prose summary,
|
|
2154
|
+
// artifacts, or "generating…" placeholder for a freshly-completed session).
|
|
2155
|
+
if (session.artifacts || session.finalReport || session.status === 'completed' || session.status === 'killed' || session.status === 'error') {
|
|
2156
|
+
renderArtifacts(session.artifacts, session.finalReport, session.status);
|
|
2157
|
+
}
|
|
1982
2158
|
|
|
1983
2159
|
// Persisted sessions have no live event stream and no subprocess to kill.
|
|
1984
2160
|
if (isPersisted) {
|
|
@@ -1994,7 +2170,7 @@ ${SHARED_STYLES}
|
|
|
1994
2170
|
|
|
1995
2171
|
// Layer 2/3 synthetic events — handle before renderEvent (so they don't leak into the raw log)
|
|
1996
2172
|
if (event.type === 'codehere:steps') { renderSteps(event.data?.steps); return; }
|
|
1997
|
-
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data); return; }
|
|
2173
|
+
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data, undefined, session.status); return; }
|
|
1998
2174
|
|
|
1999
2175
|
// Any structured error flips the status pill immediately — don't wait for
|
|
2000
2176
|
// the subprocess to exit. Many CLIs print a logical error to stdout and
|
|
@@ -2032,7 +2208,10 @@ ${SHARED_STYLES}
|
|
|
2032
2208
|
killBtn.textContent = 'Kill';
|
|
2033
2209
|
});
|
|
2034
2210
|
|
|
2035
|
-
|
|
2211
|
+
// Iter 12 — expose renderArtifacts so the polling loop can refresh the
|
|
2212
|
+
// Final Report block when the prose summary arrives async (~2-5s after
|
|
2213
|
+
// task exit). Without this, non-technical users would have to refresh.
|
|
2214
|
+
sessions.set(session.sessionId, { card, logEl, es, renderArtifacts });
|
|
2036
2215
|
}
|
|
2037
2216
|
|
|
2038
2217
|
async function assignTask() {
|
|
@@ -2181,7 +2360,10 @@ ${SHARED_STYLES}
|
|
|
2181
2360
|
}).catch(() => { /* fine */ });
|
|
2182
2361
|
}
|
|
2183
2362
|
loadSessions();
|
|
2184
|
-
// Refresh detected-agents list + banner every 5s
|
|
2363
|
+
// Refresh detected-agents list + banner every 5s. Iter 12 — also patches
|
|
2364
|
+
// existing session cards with new finalReport prose when it arrives async.
|
|
2365
|
+
// Cheap re-render (innerHTML on a small div); non-technical user sees the
|
|
2366
|
+
// report appear on its own, no manual refresh.
|
|
2185
2367
|
setInterval(() => {
|
|
2186
2368
|
fetch('/api/tasks').then(r => r.json()).then(d => {
|
|
2187
2369
|
const seen = new Set();
|
|
@@ -2191,6 +2373,13 @@ ${SHARED_STYLES}
|
|
|
2191
2373
|
}
|
|
2192
2374
|
pruneStaleAgents(seen);
|
|
2193
2375
|
updateBanner(d);
|
|
2376
|
+
// Patch existing session cards with finalReport when it arrives.
|
|
2377
|
+
for (const s of (d.spawned || d.sessions || [])) {
|
|
2378
|
+
const entry = sessions.get(s.sessionId);
|
|
2379
|
+
if (entry && entry.renderArtifacts) {
|
|
2380
|
+
entry.renderArtifacts(s.artifacts, s.finalReport, s.status);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2194
2383
|
}).catch(() => { /* fine */ });
|
|
2195
2384
|
}, 5000);
|
|
2196
2385
|
|
|
@@ -550,8 +550,11 @@ export function executeInitCommand(options = {}) {
|
|
|
550
550
|
console.log(` 1. Open ${colors.cyan('.codehere/CONTEXT.md')} and fill in what this repo is`);
|
|
551
551
|
console.log(` 2. Run ${colors.cyan('codehere start')} and delegate your first task`);
|
|
552
552
|
console.log(` 3. Track in-flight work in ${colors.cyan('.codehere/HANDOVER.md')} as you go`);
|
|
553
|
-
|
|
554
|
-
|
|
553
|
+
// Power-user / advanced layout entry points moved to docs/advanced/ per the
|
|
554
|
+
// "one canonical entry" rule (May 2 CUT pass). Used to print two extra
|
|
555
|
+
// dim lines pointing at `codehere memory show injection` and the F3
|
|
556
|
+
// canonical layout migration — both confused new users in real-task
|
|
557
|
+
// dogfood. Power users find them in docs/MEMORY.md.
|
|
555
558
|
newline();
|
|
556
559
|
}
|
|
557
560
|
catch (e) {
|
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.3",
|
|
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",
|