feat(planning): full chat history + auto-advance phases
Three fixes that together restore the planning UX:
- Dashboard reopen showed only a 500-char truncated excerpt of the last
assistant message. Backend now reads the Claude session JSONL directly
and returns full per-turn history; frontend iterates and renders all
bubbles, falling back to last_text_excerpt when the JSONL is missing.
- Phases never advanced because the agent ran /plan-* skills inline as
tool calls and the marker protocol was loose. Tightened the planning
prompt (mandatory PHASE_STATUS marker on the last line of every turn,
ban on inline phase invocation), and the frontend now auto-calls
/plan/advance when phase_ready=true.
- The phase strip never showed visual state because data-phase values
("office-hours") didn't match orchestrator phase names ("/office-hours").
Added normalizePhase + cleanup of PHASE_STATUS markers from rendered
bubbles.
Also bumps eco.py session-content truncation from 2k to 20k so /eco
session views aren't cut mid-response either.
Bumps last_text_excerpt fallback in planning_session.py from 500 to
50_000 so even when the JSONL is unavailable, the bubble isn't sliced
mid-word.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1954,18 +1954,29 @@
|
||||
// ──────────────────────────────────────────
|
||||
const PHASES = ['office-hours', 'ceo-review', 'eng-review'];
|
||||
|
||||
function normalizePhase(phase) {
|
||||
return (phase || '').replace(/^\//, '').replace(/^plan-/, '');
|
||||
}
|
||||
|
||||
function setPhase(phase, completed) {
|
||||
state.planning.phase = phase;
|
||||
state.planning.phasesCompleted = completed || [];
|
||||
const norm = normalizePhase(phase);
|
||||
const completedNorm = (completed || []).map(normalizePhase);
|
||||
const steps = phaseStepper.querySelectorAll('.phase-step');
|
||||
steps.forEach(step => {
|
||||
const ph = step.dataset.phase;
|
||||
step.classList.remove('active', 'complete');
|
||||
if ((completed || []).indexOf(ph) >= 0) step.classList.add('complete');
|
||||
if (ph === phase) step.classList.add('active');
|
||||
if (completedNorm.indexOf(ph) >= 0) step.classList.add('complete');
|
||||
if (ph === norm) step.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
function stripPhaseMarkers(text) {
|
||||
if (!text) return text;
|
||||
return text.replace(/\s*\nPHASE_STATUS:\s*(ready_to_advance|needs_input)\s*$/, '').trimEnd();
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
try {
|
||||
@@ -1977,15 +1988,16 @@
|
||||
}
|
||||
|
||||
function appendMessage(role, content) {
|
||||
state.planning.messages.push({ role, content, ts: Date.now() });
|
||||
const display = role === 'assistant' ? stripPhaseMarkers(content) : content;
|
||||
state.planning.messages.push({ role, content: display, ts: Date.now() });
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'chat-msg ' + role;
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'chat-bubble';
|
||||
if (role === 'assistant') {
|
||||
bubble.innerHTML = renderMarkdown(content);
|
||||
bubble.innerHTML = renderMarkdown(display);
|
||||
} else {
|
||||
bubble.textContent = content;
|
||||
bubble.textContent = display;
|
||||
}
|
||||
msg.appendChild(bubble);
|
||||
chatStream.appendChild(msg);
|
||||
@@ -2075,14 +2087,24 @@
|
||||
if (tRes.ok) {
|
||||
const data = await safeJson(tRes);
|
||||
if (data) {
|
||||
if (data.last_text_excerpt) {
|
||||
let rendered = false;
|
||||
if (Array.isArray(data.history) && data.history.length > 0) {
|
||||
for (const m of data.history) {
|
||||
if (m && m.text) appendMessage(m.role || 'assistant', m.text);
|
||||
}
|
||||
rendered = true;
|
||||
} else if (data.last_text_excerpt) {
|
||||
appendMessage('assistant', data.last_text_excerpt);
|
||||
rendered = true;
|
||||
}
|
||||
const phasesCompleted = data.phases_completed || [];
|
||||
const phase = data.phase || 'office-hours';
|
||||
setPhase(phase === '__complete__' ? null : phase, phasesCompleted);
|
||||
// If we already have content, we're resuming
|
||||
if (data.last_text_excerpt) {
|
||||
if (rendered) {
|
||||
requestAnimationFrame(() => {
|
||||
chatStream.scrollTop = chatStream.scrollHeight;
|
||||
});
|
||||
try { composerInput.focus(); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
@@ -2189,6 +2211,9 @@
|
||||
}
|
||||
setPhase(data.phase === '__complete__' ? null : data.phase, completed);
|
||||
}
|
||||
if (data.phase_ready) {
|
||||
await autoAdvancePhase(slug);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
removeTypingIndicator();
|
||||
@@ -2203,6 +2228,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function autoAdvancePhase(slug) {
|
||||
const prevPhase = state.planning.phase;
|
||||
appendMessage('assistant', '_Faza completă. Pornesc subprocess pentru următoarea fază..._');
|
||||
appendTypingIndicator();
|
||||
startElapsedCounter();
|
||||
try {
|
||||
const res = await apiFetch('/echo/api/projects/' + encodeURIComponent(slug) + '/plan/advance', {
|
||||
method: 'POST',
|
||||
});
|
||||
removeTypingIndicator();
|
||||
stopElapsedCounter();
|
||||
if (!res.ok) {
|
||||
const d = await safeJson(res);
|
||||
appendMessage('assistant', '_Nu am putut avansa: ' + ((d && d.message) || ('HTTP ' + res.status)) + '_');
|
||||
return;
|
||||
}
|
||||
const data = await safeJson(res);
|
||||
if (!data) return;
|
||||
const completed = state.planning.phasesCompleted.slice();
|
||||
if (prevPhase && !completed.includes(prevPhase)) completed.push(prevPhase);
|
||||
if (data.completed) {
|
||||
setPhase(null, completed);
|
||||
if (data.message) appendMessage('assistant', data.message);
|
||||
} else {
|
||||
if (data.message) appendMessage('assistant', data.message);
|
||||
setPhase(data.phase, completed);
|
||||
}
|
||||
} catch (err) {
|
||||
removeTypingIndicator();
|
||||
stopElapsedCounter();
|
||||
if (err.message !== 'unauthorized') {
|
||||
appendMessage('assistant', '_Eroare la avans fază: ' + (err.message || err) + '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
planCancelBtn.addEventListener('click', async () => {
|
||||
const slug = state.planning.slug;
|
||||
if (!slug) { closeModal(planModal); return; }
|
||||
|
||||
Reference in New Issue
Block a user