refactor(minecraft): redesign debug dashboard conversation view with turn-based grouping and context boundary visualization
Replaces flat message list with collapsible turn cards (user+assistant pairs). Parses user messages into tagged sections ([EVENT], [FEEDBACK], [PERCEPTION], etc.) with inline/collapsible rendering based on content type. Adds context status bar showing active context label and archived summaries. Introduces Brain.broadcastConversationState() to emit full context metadata (active
This commit is contained in:
@@ -813,11 +813,32 @@ export class Brain {
|
||||
return JSON.parse(JSON.stringify(messages)) as Message[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-emit the current conversation state with full context metadata.
|
||||
* Used by the debug dashboard's `request_conversation` handler on reconnect.
|
||||
*/
|
||||
public broadcastConversationState(): void {
|
||||
this.emitConversationUpdate(this.isProcessing)
|
||||
}
|
||||
|
||||
private emitConversationUpdate(isProcessing: boolean, sessionBoundary?: boolean): void {
|
||||
this.debugService.emitConversationUpdate({
|
||||
messages: this.cloneMessages(this.conversationHistory),
|
||||
isProcessing,
|
||||
...(sessionBoundary && { sessionBoundary }),
|
||||
activeContext: {
|
||||
label: this.activeContextState.label,
|
||||
startTurnId: this.activeContextState.startTurnId,
|
||||
messageCount: this.conversationHistory.length - this.activeContextStartIndex,
|
||||
},
|
||||
archivedContexts: this.archivedContexts.map(ctx => ({
|
||||
label: ctx.label,
|
||||
summary: ctx.summary,
|
||||
turns: ctx.endTurnId - ctx.startTurnId + 1,
|
||||
archivedAt: ctx.archivedAt,
|
||||
})),
|
||||
activeContextStartIndex: this.activeContextStartIndex,
|
||||
contextHistoryMessage: this.getContextHistoryMessage(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,7 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
|
||||
})
|
||||
|
||||
debugService.onCommand('request_conversation', () => {
|
||||
const snapshot = brain.getDebugSnapshot()
|
||||
debugService.emitConversationUpdate({
|
||||
messages: snapshot.conversationHistory,
|
||||
isProcessing: snapshot.isProcessing,
|
||||
})
|
||||
brain.broadcastConversationState()
|
||||
})
|
||||
|
||||
debugService.onCommand('execute_repl', async (command) => {
|
||||
|
||||
@@ -96,6 +96,16 @@ export interface TraceBatchEvent {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Archived context summary for the debug dashboard
|
||||
*/
|
||||
export interface ContextBoundaryInfo {
|
||||
label: string
|
||||
summary: string
|
||||
turns: number
|
||||
archivedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Live conversation state update from the brain
|
||||
*/
|
||||
@@ -103,6 +113,18 @@ export interface ConversationUpdateEvent {
|
||||
messages: Array<{ role: string, content: string, reasoning?: string }>
|
||||
isProcessing: boolean
|
||||
sessionBoundary?: boolean
|
||||
/** Active context boundary state */
|
||||
activeContext?: {
|
||||
label: string | null
|
||||
startTurnId: number
|
||||
messageCount: number
|
||||
}
|
||||
/** Archived context summaries */
|
||||
archivedContexts?: ContextBoundaryInfo[]
|
||||
/** Index in messages where the active context starts */
|
||||
activeContextStartIndex?: number
|
||||
/** The [CONTEXT_HISTORY] prefix message content, if any */
|
||||
contextHistoryMessage?: string | null
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
|
||||
@@ -566,13 +566,39 @@ class LogsPanel {
|
||||
// Conversation Panel (Live Chat View)
|
||||
// =============================================================================
|
||||
|
||||
// --- User message parser: extracts structured sections from brain's buildUserMessage output ---
|
||||
function parseUserMessage(content) {
|
||||
if (typeof content !== 'string')
|
||||
return { sections: [], raw: '' }
|
||||
const sections = []
|
||||
// Known section tags in order they may appear
|
||||
const tagPattern = /^\[(EVENT|FEEDBACK|PERCEPTION|STATE|ERROR_BURST_GUARD|ERROR_BURST|MANDATORY|SCRIPT|ACTION_QUEUE|NO_ACTION_BUDGET|CONTEXT)\]\s*/
|
||||
// Split on double-newline (the separator used by buildUserMessage)
|
||||
const blocks = content.split(/\n\n/)
|
||||
for (const block of blocks) {
|
||||
const trimmed = block.trim()
|
||||
if (!trimmed)
|
||||
continue
|
||||
const m = trimmed.match(tagPattern)
|
||||
if (m) {
|
||||
sections.push({ tag: m[1], text: trimmed.slice(m[0].length) })
|
||||
}
|
||||
else {
|
||||
// Could be a continuation of PERCEPTION or unknown block
|
||||
sections.push({ tag: 'OTHER', text: trimmed })
|
||||
}
|
||||
}
|
||||
return { sections, raw: content }
|
||||
}
|
||||
|
||||
class ConversationPanel {
|
||||
constructor(client) {
|
||||
this.client = client
|
||||
// Each session: { messages: Message[], greyed: boolean }
|
||||
this.sessions = [{ messages: [], greyed: false }]
|
||||
this._mkSession = () => ({ messages: [], greyed: false, activeContext: null, archivedContexts: [], activeContextStartIndex: 0, contextHistoryMessage: null })
|
||||
this.sessions = [this._mkSession()]
|
||||
this.isProcessing = false
|
||||
this.autoScroll = true
|
||||
this.turnCounter = 0
|
||||
this.elements = {
|
||||
container: document.getElementById('conversation-container'),
|
||||
count: document.getElementById('conversation-count'),
|
||||
@@ -588,44 +614,63 @@ class ConversationPanel {
|
||||
this.reset()
|
||||
this.client.send({ type: 'request_conversation' })
|
||||
})
|
||||
|
||||
// Unified collapsible toggle handler (event delegation — attached once)
|
||||
if (this.elements.container) {
|
||||
this.elements.container.addEventListener('click', (e) => {
|
||||
const toggle = e.target.closest('[data-toggle]')
|
||||
if (!toggle)
|
||||
return
|
||||
const targetId = toggle.getAttribute('data-toggle')
|
||||
const body = document.getElementById(targetId)
|
||||
if (!body)
|
||||
return
|
||||
const isOpen = body.classList.toggle('cv-open')
|
||||
const arrow = toggle.querySelector('.cv-arrow')
|
||||
if (arrow)
|
||||
arrow.textContent = isOpen ? '\u25BC' : '\u25B6'
|
||||
})
|
||||
}
|
||||
|
||||
this.render()
|
||||
}
|
||||
|
||||
handleUpdate(data) {
|
||||
if (data.sessionBoundary) {
|
||||
// Grey out current session and start a new one
|
||||
const currentSession = this.sessions[this.sessions.length - 1]
|
||||
if (currentSession) {
|
||||
currentSession.greyed = true
|
||||
}
|
||||
this.sessions.push({ messages: [], greyed: false })
|
||||
const cur = this.sessions[this.sessions.length - 1]
|
||||
if (cur)
|
||||
cur.greyed = true
|
||||
this.sessions.push(this._mkSession())
|
||||
}
|
||||
else {
|
||||
// Update current session messages
|
||||
const currentSession = this.sessions[this.sessions.length - 1]
|
||||
if (currentSession) {
|
||||
currentSession.messages = data.messages || []
|
||||
const cur = this.sessions[this.sessions.length - 1]
|
||||
if (cur) {
|
||||
cur.messages = data.messages || []
|
||||
cur.activeContext = data.activeContext || null
|
||||
cur.archivedContexts = data.archivedContexts || []
|
||||
cur.activeContextStartIndex = data.activeContextStartIndex ?? 0
|
||||
cur.contextHistoryMessage = data.contextHistoryMessage || null
|
||||
}
|
||||
}
|
||||
|
||||
this.isProcessing = !!data.isProcessing
|
||||
this.updateStats()
|
||||
this.render()
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.sessions = [{ messages: [], greyed: false }]
|
||||
this.sessions = [this._mkSession()]
|
||||
this.isProcessing = false
|
||||
this.turnCounter = 0
|
||||
this.updateStats()
|
||||
this.render()
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
const totalMessages = this.sessions.reduce((sum, s) => sum + s.messages.length, 0)
|
||||
const total = this.sessions.reduce((s, sess) => s + sess.messages.length, 0)
|
||||
if (this.elements.count)
|
||||
this.elements.count.textContent = totalMessages
|
||||
this.elements.count.textContent = total
|
||||
if (this.elements.statLlm)
|
||||
this.elements.statLlm.textContent = totalMessages
|
||||
this.elements.statLlm.textContent = total
|
||||
if (this.elements.processingBadge)
|
||||
this.elements.processingBadge.classList.toggle('hidden', !this.isProcessing)
|
||||
}
|
||||
@@ -633,100 +678,305 @@ class ConversationPanel {
|
||||
render() {
|
||||
if (!this.elements.container)
|
||||
return
|
||||
this.turnCounter = 0
|
||||
|
||||
const html = this.sessions.map((session, sessionIdx) => {
|
||||
const sessionClass = session.greyed ? 'chat-session chat-session-greyed' : 'chat-session'
|
||||
const messagesHtml = this.renderSessionMessages(session.messages)
|
||||
const dividerHtml = session.greyed
|
||||
? '<div class="chat-session-divider"><span>Session cleared</span></div>'
|
||||
: ''
|
||||
|
||||
return `<div class="${sessionClass}">${messagesHtml}</div>${dividerHtml}`
|
||||
const html = this.sessions.map((session) => {
|
||||
const cls = session.greyed ? 'chat-session chat-session-greyed' : 'chat-session'
|
||||
const body = this.renderSession(session)
|
||||
const divider = session.greyed ? '<div class="chat-session-divider"><span>Session cleared</span></div>' : ''
|
||||
return `<div class="${cls}">${body}</div>${divider}`
|
||||
}).join('')
|
||||
|
||||
const typingHtml = this.isProcessing
|
||||
? `<div class="chat-typing-indicator">
|
||||
<span class="chat-typing-dot"></span>
|
||||
<span class="chat-typing-dot"></span>
|
||||
<span class="chat-typing-dot"></span>
|
||||
</div>`
|
||||
const typing = this.isProcessing
|
||||
? '<div class="chat-typing-indicator"><span class="chat-typing-dot"></span><span class="chat-typing-dot"></span><span class="chat-typing-dot"></span></div>'
|
||||
: ''
|
||||
|
||||
this.elements.container.innerHTML = html + typingHtml
|
||||
|
||||
// Attach toggle handlers for system messages
|
||||
this.elements.container.querySelectorAll('.chat-system-toggle').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const body = btn.closest('.chat-message-system')?.querySelector('.chat-system-body')
|
||||
if (body) {
|
||||
body.classList.toggle('open')
|
||||
btn.textContent = body.classList.contains('open') ? '▼ System Prompt' : '▶ System Prompt'
|
||||
}
|
||||
})
|
||||
})
|
||||
this.elements.container.innerHTML = html + typing
|
||||
|
||||
if (this.autoScroll) {
|
||||
const scrollEl = this.elements.scroll
|
||||
if (scrollEl) {
|
||||
requestAnimationFrame(() => {
|
||||
scrollEl.scrollTop = scrollEl.scrollHeight
|
||||
})
|
||||
}
|
||||
const el = this.elements.scroll
|
||||
if (el)
|
||||
requestAnimationFrame(() => { el.scrollTop = el.scrollHeight })
|
||||
}
|
||||
}
|
||||
|
||||
renderSessionMessages(messages) {
|
||||
if (!messages || messages.length === 0) {
|
||||
// --- Session rendering ---
|
||||
|
||||
renderSession(session) {
|
||||
const { messages, activeContext, archivedContexts, contextHistoryMessage } = session
|
||||
if (!messages || messages.length === 0)
|
||||
return '<div class="empty-state">No messages yet</div>'
|
||||
|
||||
const parts = []
|
||||
|
||||
// Context status bar
|
||||
if (archivedContexts?.length > 0 || activeContext) {
|
||||
parts.push(this.renderContextStatusBar(activeContext, archivedContexts, contextHistoryMessage))
|
||||
}
|
||||
|
||||
return messages.map((msg) => {
|
||||
const role = msg.role || 'unknown'
|
||||
|
||||
if (role === 'system') {
|
||||
return this.renderSystemMessage(msg)
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
return this.renderUserMessage(msg)
|
||||
}
|
||||
|
||||
if (role === 'assistant') {
|
||||
return this.renderAssistantMessage(msg)
|
||||
}
|
||||
|
||||
return `<div class="chat-message chat-message-unknown">
|
||||
<div class="chat-role">${escapeHtml(role)}</div>
|
||||
<div class="chat-bubble chat-bubble-unknown">${escapeHtml(msg.content || '')}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
// Group messages into turns (user+assistant pairs)
|
||||
const turns = this.groupIntoTurns(messages)
|
||||
for (const turn of turns) {
|
||||
parts.push(this.renderTurn(turn))
|
||||
}
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
renderSystemMessage(msg) {
|
||||
const preview = formatSystemMessageContent(msg.content || '')
|
||||
return `<div class="chat-message chat-message-system">
|
||||
<button class="chat-system-toggle">▶ System Prompt</button>
|
||||
<div class="chat-system-body">
|
||||
<div class="chat-bubble chat-bubble-system">${escapeHtml(preview)}</div>
|
||||
groupIntoTurns(messages) {
|
||||
const turns = []
|
||||
let current = null
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'system') {
|
||||
turns.push({ type: 'system', system: msg })
|
||||
}
|
||||
else if (msg.role === 'user') {
|
||||
current = { type: 'turn', user: msg, assistant: null }
|
||||
turns.push(current)
|
||||
}
|
||||
else if (msg.role === 'assistant') {
|
||||
if (current && !current.assistant) {
|
||||
current.assistant = msg
|
||||
}
|
||||
else {
|
||||
turns.push({ type: 'turn', user: null, assistant: msg })
|
||||
}
|
||||
}
|
||||
}
|
||||
return turns
|
||||
}
|
||||
|
||||
renderTurn(turn) {
|
||||
if (turn.type === 'system')
|
||||
return this.renderSystemMessage(turn.system)
|
||||
|
||||
this.turnCounter++
|
||||
const n = this.turnCounter
|
||||
const userParsed = turn.user ? parseUserMessage(turn.user.content || '') : null
|
||||
const eventSection = userParsed?.sections.find(s => s.tag === 'EVENT' || s.tag === 'FEEDBACK')
|
||||
const contextSection = userParsed?.sections.find(s => s.tag === 'CONTEXT')
|
||||
|
||||
// Build a short summary for the turn header
|
||||
let summary = `Turn ${n}`
|
||||
if (eventSection) {
|
||||
summary = this.summarizeEvent(eventSection)
|
||||
}
|
||||
|
||||
// Detect context label from the [CONTEXT] section
|
||||
let ctxBadge = ''
|
||||
if (contextSection) {
|
||||
const ctxMatch = contextSection.text.match(/active="([^"]+)"/)
|
||||
if (ctxMatch) {
|
||||
ctxBadge = `<span class="cv-ctx-badge cv-ctx-active">${escapeHtml(ctxMatch[1])}</span>`
|
||||
}
|
||||
else if (contextSection.text.includes('no active context')) {
|
||||
ctxBadge = '<span class="cv-ctx-badge cv-ctx-none">no ctx</span>'
|
||||
}
|
||||
}
|
||||
|
||||
const turnId = `cv-turn-${n}`
|
||||
const userHtml = turn.user ? this.renderParsedUserMessage(userParsed, n) : ''
|
||||
const assistantHtml = turn.assistant ? this.renderParsedAssistantMessage(turn.assistant, n) : ''
|
||||
|
||||
return `<div class="cv-turn">
|
||||
<button class="cv-turn-header" data-toggle="${turnId}">
|
||||
<span class="cv-arrow">\u25B6</span>
|
||||
<span class="cv-turn-num">#${n}</span>
|
||||
<span class="cv-turn-summary">${escapeHtml(summary)}</span>
|
||||
${ctxBadge}
|
||||
</button>
|
||||
<div class="cv-turn-body" id="${turnId}">
|
||||
${userHtml}
|
||||
${assistantHtml}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
renderUserMessage(msg) {
|
||||
return `<div class="chat-message chat-message-user">
|
||||
<div class="chat-role">user</div>
|
||||
<div class="chat-bubble chat-bubble-user">${escapeHtml(msg.content || '')}</div>
|
||||
// --- Event summary helpers ---
|
||||
|
||||
summarizeEvent(section) {
|
||||
const text = section.text
|
||||
// Chat messages: "Chat from X: "message""
|
||||
const chatMatch = text.match(/^Chat from (\w+):\s*"(.+)"$/s)
|
||||
if (chatMatch)
|
||||
return `Chat from ${chatMatch[1]}: "${chatMatch[2].slice(0, 60)}${chatMatch[2].length > 60 ? '...' : ''}"`
|
||||
|
||||
// Perception Signal
|
||||
if (text.startsWith('Perception Signal:'))
|
||||
return text.slice(0, 70) + (text.length > 70 ? '...' : '')
|
||||
|
||||
// system_alert with JSON — extract reason + returnValue preview
|
||||
if (text.startsWith('system_alert:')) {
|
||||
try {
|
||||
const json = JSON.parse(text.slice('system_alert:'.length).trim())
|
||||
const reason = json.reason || 'unknown'
|
||||
const rv = json.returnValue
|
||||
const rvPreview = typeof rv === 'string' ? rv.slice(0, 50) : ''
|
||||
return `system: ${reason}${rvPreview ? ` → ${rvPreview}${rv.length > 50 ? '...' : ''}` : ''}`
|
||||
}
|
||||
catch { /* fall through */ }
|
||||
}
|
||||
|
||||
// FEEDBACK: "toolName: Success/Failed. details"
|
||||
if (section.tag === 'FEEDBACK') {
|
||||
const fbMatch = text.match(/^(\w+):\s*(Success|Failed)\.?\s*(.*)$/s)
|
||||
if (fbMatch) {
|
||||
const detail = fbMatch[3].slice(0, 50)
|
||||
return `${fbMatch[1]}: ${fbMatch[2]}${detail ? ` — ${detail}${fbMatch[3].length > 50 ? '...' : ''}` : ''}`
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: truncate
|
||||
const flat = text.replace(/\n/g, ' ').slice(0, 70)
|
||||
return flat + (text.length > 70 ? '...' : '')
|
||||
}
|
||||
|
||||
// --- Parsed user message rendering ---
|
||||
|
||||
renderParsedUserMessage(parsed, turnNum) {
|
||||
if (!parsed)
|
||||
return ''
|
||||
const cards = []
|
||||
for (const section of parsed.sections) {
|
||||
cards.push(this.renderUserSection(section, turnNum))
|
||||
}
|
||||
return `<div class="cv-user">${cards.join('')}</div>`
|
||||
}
|
||||
|
||||
renderUserSection(section, turnNum) {
|
||||
const id = `cv-s-${turnNum}-${section.tag}-${Math.random().toString(36).slice(2, 6)}`
|
||||
const tagColors = {
|
||||
EVENT: 'cv-tag-event',
|
||||
FEEDBACK: 'cv-tag-feedback',
|
||||
PERCEPTION: 'cv-tag-perception',
|
||||
SCRIPT: 'cv-tag-script',
|
||||
ACTION_QUEUE: 'cv-tag-queue',
|
||||
NO_ACTION_BUDGET: 'cv-tag-budget',
|
||||
CONTEXT: 'cv-tag-context',
|
||||
ERROR_BURST: 'cv-tag-error',
|
||||
ERROR_BURST_GUARD: 'cv-tag-error',
|
||||
MANDATORY: 'cv-tag-error',
|
||||
STATE: 'cv-tag-state',
|
||||
OTHER: 'cv-tag-other',
|
||||
}
|
||||
const colorCls = tagColors[section.tag] || 'cv-tag-other'
|
||||
|
||||
// Some sections are compact enough to show inline
|
||||
if (['ACTION_QUEUE', 'NO_ACTION_BUDGET', 'CONTEXT', 'ERROR_BURST', 'STATE'].includes(section.tag)) {
|
||||
return `<div class="cv-section cv-section-inline ${colorCls}">
|
||||
<span class="cv-section-tag">${section.tag}</span>
|
||||
<span class="cv-section-inline-text">${escapeHtml(section.text)}</span>
|
||||
</div>`
|
||||
}
|
||||
|
||||
// EVENT / FEEDBACK — show prominently
|
||||
if (section.tag === 'EVENT' || section.tag === 'FEEDBACK') {
|
||||
return `<div class="cv-section cv-section-event ${colorCls}">
|
||||
<span class="cv-section-tag">${section.tag}</span>
|
||||
<span class="cv-section-text">${escapeHtml(section.text)}</span>
|
||||
</div>`
|
||||
}
|
||||
|
||||
// SCRIPT, PERCEPTION, OTHER — collapsible
|
||||
const preview = section.text.slice(0, 60).replace(/\n/g, ' ')
|
||||
return `<div class="cv-section ${colorCls}">
|
||||
<button class="cv-section-toggle" data-toggle="${id}">
|
||||
<span class="cv-arrow">\u25B6</span>
|
||||
<span class="cv-section-tag">${section.tag}</span>
|
||||
<span class="cv-section-preview">${escapeHtml(preview)}${section.text.length > 60 ? '...' : ''}</span>
|
||||
</button>
|
||||
<div class="cv-section-body" id="${id}">
|
||||
<pre class="cv-section-content">${escapeHtml(section.text)}</pre>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
renderAssistantMessage(msg) {
|
||||
const reasoningHtml = msg.reasoning
|
||||
? `<div class="chat-reasoning">${escapeHtml(msg.reasoning)}</div>`
|
||||
: ''
|
||||
return `<div class="chat-message chat-message-assistant">
|
||||
<div class="chat-role">assistant</div>
|
||||
${reasoningHtml}
|
||||
<div class="chat-bubble chat-bubble-assistant">${escapeHtml(msg.content || '')}</div>
|
||||
// --- Parsed assistant message rendering ---
|
||||
|
||||
renderParsedAssistantMessage(msg, turnNum) {
|
||||
const reasoning = msg.reasoning || ''
|
||||
const code = msg.content || ''
|
||||
const parts = []
|
||||
|
||||
if (reasoning) {
|
||||
const rid = `cv-reason-${turnNum}`
|
||||
const preview = reasoning.slice(0, 80).replace(/\n/g, ' ')
|
||||
parts.push(`<div class="cv-reasoning">
|
||||
<button class="cv-reasoning-toggle" data-toggle="${rid}">
|
||||
<span class="cv-arrow">\u25B6</span>
|
||||
<span class="cv-reasoning-label">Reasoning</span>
|
||||
<span class="cv-reasoning-preview">${escapeHtml(preview)}${reasoning.length > 80 ? '...' : ''}</span>
|
||||
</button>
|
||||
<div class="cv-reasoning-body" id="${rid}">
|
||||
<pre class="cv-reasoning-content">${escapeHtml(reasoning)}</pre>
|
||||
</div>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
if (code) {
|
||||
parts.push(`<div class="cv-code">
|
||||
<div class="cv-code-label">Code</div>
|
||||
<pre class="cv-code-content">${escapeHtml(code)}</pre>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
return `<div class="cv-assistant">${parts.join('')}</div>`
|
||||
}
|
||||
|
||||
// --- Context status bar ---
|
||||
|
||||
renderContextStatusBar(activeContext, archivedContexts, contextHistoryMessage) {
|
||||
const parts = []
|
||||
// Active context indicator
|
||||
if (activeContext?.label) {
|
||||
parts.push(`<span class="cv-ctx-status-active"><span class="cv-ctx-dot"></span> ${escapeHtml(activeContext.label)} (${activeContext.messageCount} msgs)</span>`)
|
||||
}
|
||||
else {
|
||||
parts.push('<span class="cv-ctx-status-idle"><span class="cv-ctx-dot cv-ctx-dot-idle"></span> No active context</span>')
|
||||
}
|
||||
|
||||
// Archived count
|
||||
if (archivedContexts?.length > 0) {
|
||||
const archId = `cv-archived-${Math.random().toString(36).slice(2, 6)}`
|
||||
const items = archivedContexts.map((ctx, i) => {
|
||||
const time = new Date(ctx.archivedAt).toLocaleTimeString()
|
||||
return `<div class="cv-arch-item">
|
||||
<span class="cv-arch-idx">#${i + 1}</span>
|
||||
<strong>${escapeHtml(ctx.label || 'unnamed')}</strong>
|
||||
<span class="cv-arch-meta">${ctx.turns}t · ${time}</span>
|
||||
<div class="cv-arch-summary">${escapeHtml(ctx.summary)}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
parts.push(`<button class="cv-ctx-arch-btn" data-toggle="${archId}">
|
||||
<span class="cv-arrow">\u25B6</span> ${archivedContexts.length} archived
|
||||
</button>`)
|
||||
// Append the collapsible body after the status bar
|
||||
parts.push(`<div class="cv-ctx-arch-body" id="${archId}">${items}</div>`)
|
||||
}
|
||||
|
||||
// Context history prefix
|
||||
if (contextHistoryMessage) {
|
||||
const chId = `cv-ctxhist-${Math.random().toString(36).slice(2, 6)}`
|
||||
parts.push(`<button class="cv-ctx-hist-btn" data-toggle="${chId}">
|
||||
<span class="cv-arrow">\u25B6</span> prefix
|
||||
</button>`)
|
||||
parts.push(`<div class="cv-ctx-hist-body" id="${chId}"><pre class="cv-ctx-hist-content">${escapeHtml(contextHistoryMessage)}</pre></div>`)
|
||||
}
|
||||
|
||||
return `<div class="cv-ctx-bar">${parts.join('')}</div>`
|
||||
}
|
||||
|
||||
// --- System message ---
|
||||
|
||||
renderSystemMessage(msg) {
|
||||
const id = `cv-sys-${Math.random().toString(36).slice(2, 6)}`
|
||||
const preview = formatSystemMessageContent(msg.content || '')
|
||||
return `<div class="cv-system">
|
||||
<button class="cv-system-toggle" data-toggle="${id}">
|
||||
<span class="cv-arrow">\u25B6</span> System Prompt
|
||||
</button>
|
||||
<div class="cv-system-body" id="${id}">
|
||||
<pre class="cv-system-content">${escapeHtml(preview)}</pre>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
/* ==========================================================================
|
||||
Conversation Panel v2 — Structured Turn Cards
|
||||
========================================================================== */
|
||||
|
||||
/* Unified collapsible mechanism */
|
||||
.cv-arrow {
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-toggle] { cursor: pointer; }
|
||||
|
||||
/* All collapsible bodies hidden by default */
|
||||
.cv-turn-body,
|
||||
.cv-section-body,
|
||||
.cv-reasoning-body,
|
||||
.cv-system-body,
|
||||
.cv-ctx-arch-body,
|
||||
.cv-ctx-hist-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cv-open {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* --- Context status bar --- */
|
||||
.cv-ctx-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.cv-ctx-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-success);
|
||||
margin-right: 4px;
|
||||
animation: cvPulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.cv-ctx-dot-idle {
|
||||
background: var(--text-muted);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.cv-ctx-status-active { color: var(--accent-success); }
|
||||
.cv-ctx-status-idle { color: var(--text-muted); }
|
||||
|
||||
.cv-ctx-arch-btn,
|
||||
.cv-ctx-hist-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
background: rgba(163, 113, 247, 0.1);
|
||||
border: 1px solid rgba(163, 113, 247, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #a371f7;
|
||||
}
|
||||
|
||||
.cv-ctx-arch-btn:hover,
|
||||
.cv-ctx-hist-btn:hover {
|
||||
background: rgba(163, 113, 247, 0.18);
|
||||
}
|
||||
|
||||
.cv-ctx-arch-body,
|
||||
.cv-ctx-hist-body {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.cv-arch-item {
|
||||
padding: 5px 8px;
|
||||
margin-bottom: 3px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid rgba(163, 113, 247, 0.5);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.cv-arch-idx { color: var(--text-muted); margin-right: 4px; }
|
||||
.cv-arch-meta { color: var(--text-muted); margin-left: 6px; font-size: 10px; }
|
||||
.cv-arch-summary {
|
||||
margin-top: 3px;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cv-ctx-hist-content {
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid rgba(163, 113, 247, 0.25);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@keyframes cvPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* --- Turn card --- */
|
||||
.cv-turn {
|
||||
margin-bottom: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.cv-turn-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.cv-turn-header:hover { background: var(--border-color); }
|
||||
|
||||
.cv-turn-num {
|
||||
color: var(--accent-info);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cv-turn-summary {
|
||||
color: var(--text-secondary);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cv-ctx-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cv-ctx-active {
|
||||
background: rgba(63, 185, 80, 0.15);
|
||||
color: var(--accent-success);
|
||||
border: 1px solid rgba(63, 185, 80, 0.3);
|
||||
}
|
||||
|
||||
.cv-ctx-none {
|
||||
background: rgba(110, 118, 129, 0.1);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.cv-turn-body {
|
||||
padding: 6px 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* --- User message sections --- */
|
||||
.cv-user {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.cv-section {
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cv-section-tag {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tag colors */
|
||||
.cv-tag-event .cv-section-tag { background: rgba(63, 185, 80, 0.15); color: var(--accent-success); }
|
||||
.cv-tag-feedback .cv-section-tag { background: rgba(88, 166, 255, 0.15); color: var(--accent-info); }
|
||||
.cv-tag-perception .cv-section-tag { background: rgba(210, 153, 34, 0.15); color: var(--accent-warning); }
|
||||
.cv-tag-script .cv-section-tag { background: rgba(163, 113, 247, 0.15); color: #a371f7; }
|
||||
.cv-tag-queue .cv-section-tag { background: rgba(110, 118, 129, 0.12); color: var(--text-secondary); }
|
||||
.cv-tag-budget .cv-section-tag { background: rgba(110, 118, 129, 0.12); color: var(--text-secondary); }
|
||||
.cv-tag-context .cv-section-tag { background: rgba(63, 185, 80, 0.12); color: var(--accent-success); }
|
||||
.cv-tag-error .cv-section-tag { background: rgba(248, 81, 73, 0.15); color: var(--accent-error); }
|
||||
.cv-tag-state .cv-section-tag { background: rgba(210, 153, 34, 0.12); color: var(--accent-warning); }
|
||||
.cv-tag-other .cv-section-tag { background: rgba(110, 118, 129, 0.1); color: var(--text-muted); }
|
||||
|
||||
/* Inline sections (compact one-liners) */
|
||||
.cv-section-inline {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.cv-section-inline-text {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Event/Feedback sections (prominent) */
|
||||
.cv-section-event {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.cv-section-text {
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Collapsible sections (SCRIPT, PERCEPTION, OTHER) */
|
||||
.cv-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 3px 6px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cv-section-toggle:hover { background: var(--bg-tertiary); }
|
||||
|
||||
.cv-section-preview {
|
||||
color: var(--text-muted);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cv-section-body {
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.cv-section-content {
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* --- Assistant message --- */
|
||||
.cv-assistant {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Reasoning (collapsible) */
|
||||
.cv-reasoning { border-radius: 4px; }
|
||||
|
||||
.cv-reasoning-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 3px 6px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
background: rgba(210, 153, 34, 0.06);
|
||||
border: 1px solid rgba(210, 153, 34, 0.25);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-warning);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.cv-reasoning-toggle:hover { background: rgba(210, 153, 34, 0.12); }
|
||||
|
||||
.cv-reasoning-label {
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cv-reasoning-preview {
|
||||
color: var(--text-muted);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cv-reasoning-body {
|
||||
border: 1px solid rgba(210, 153, 34, 0.2);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.cv-reasoning-content {
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
background: rgba(210, 153, 34, 0.03);
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.cv-code {
|
||||
border: 1px solid rgba(88, 166, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cv-code-label {
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 600;
|
||||
color: var(--accent-info);
|
||||
background: rgba(88, 166, 255, 0.08);
|
||||
border-bottom: 1px solid rgba(88, 166, 255, 0.15);
|
||||
}
|
||||
|
||||
.cv-code-content {
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* --- System message --- */
|
||||
.cv-system {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cv-system-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid rgba(163, 113, 247, 0.3);
|
||||
border-radius: 4px;
|
||||
color: #a371f7;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.cv-system-toggle:hover { background: rgba(163, 113, 247, 0.1); }
|
||||
|
||||
.cv-system-body {
|
||||
border: 1px solid rgba(163, 113, 247, 0.2);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.cv-system-content {
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<title>Debug Dashboard</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="tools.css">
|
||||
<link rel="stylesheet" href="conversation.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1020,3 +1020,5 @@ button:hover,
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Context boundary styles moved to conversation.css */
|
||||
|
||||
Reference in New Issue
Block a user