Unified activity timeline in Conversation: joins, questions, resolutions, contract, agreements
deploy / deploy (push) Canceled after 0s

This commit is contained in:
2026-09-06 22:26:48 +03:00
parent fde1ae152f
commit 2e55a383ea
8 changed files with 198 additions and 36 deletions
+1
View File
@@ -105,6 +105,7 @@ export function buildRouter(service: RendezvousService, cfg: ServerConfig) {
resolvedQuestions: view.resolved_questions,
contractMarkdown: view.current_contract?.markdown ?? '',
contractVersion: view.current_contract?.version ?? 0,
contract: view.current_contract,
secretsPolicyShort: secretsPolicyShort(cfg.baseUrl),
}), 'text/markdown; charset=utf-8');
} else {
+31 -16
View File
@@ -1,7 +1,28 @@
import type { ObserverView, RoomView } from '@ai-rendezvous/core';
import { secretsPolicyShort } from '@ai-rendezvous/core';
import { secretsPolicyShort, buildTimeline } from '@ai-rendezvous/core';
import { escapeHtml } from './http.js';
/** Unified activity timeline as HTML: every participant action in chronological order. */
function timelineHtml(
participants: ObserverView['participants'],
conversation: ObserverView['conversation'],
openQuestions: ObserverView['open_questions'],
resolvedQuestions: ObserverView['resolved_questions'],
contract: ObserverView['current_contract'],
): string {
const events = buildTimeline({ participants, conversation, openQuestions, resolvedQuestions, contract });
return (
events
.map((e) => {
const body = e.body
? `<pre>${escapeHtml(e.body)}</pre>`
: '';
return `<div class="msg msg-${e.kind}"><div class="meta"><b>${escapeHtml(e.role)}</b> · ${escapeHtml(e.action)} · ${escapeHtml(e.at)}</div>${body}</div>`;
})
.join('') || '<p class="hint">(no activity yet)</p>'
);
}
/** Machine-readable instruction page served at /create.md — this is what an agent reads first. */
export function createMarkdownDoc(baseUrl: string): string {
return `# AI Rendezvous — create a room
@@ -92,6 +113,9 @@ const PAGE_CSS = `
code, pre { font-family: ui-monospace, monospace; font-size: 0.85rem; }
pre { background: rgba(127,127,127,.12); padding: .75rem 1rem; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
.msg { border-left: 3px solid rgba(127,127,127,.4); padding: .25rem 0 .25rem .75rem; margin: .75rem 0; }
.msg-joined, .msg-agreed, .msg-contract { border-left-color: rgba(60,140,80,.75); }
.msg-asked { border-left-color: rgba(210,130,30,.8); }
.msg-resolved { opacity: .75; }
.meta { color: rgba(127,127,127,.9); font-size: .8rem; }
.q { padding: .5rem .75rem; border-radius: 6px; margin: .5rem 0; }
.q.blocking { background: rgba(200,60,60,.14); }
@@ -196,12 +220,7 @@ export function observerHtmlPage(o: ObserverView, token: string): string {
return `${mark} (${joined})`;
})
.join(', ');
const msgs = o.conversation
.map(
(m) =>
`<div class="msg"><div class="meta"><b>${escapeHtml(m.role)}</b> · ${escapeHtml(m.created_at)}</div><pre>${escapeHtml(m.content)}</pre></div>`,
)
.join('');
const msgs = timelineHtml(o.participants, o.conversation, o.open_questions, o.resolved_questions, o.current_contract);
const openQs = o.open_questions
.map(
(q) =>
@@ -225,8 +244,8 @@ export function observerHtmlPage(o: ObserverView, token: string): string {
${resolvedQs ? `<h2>Resolved questions</h2>${resolvedQs}` : ''}
<h2>Agreed Contract (v${contract ? contract.version : 0})</h2>
<pre>${escapeHtml(contract ? contract.markdown : '(not drafted yet)')}</pre>
<h2>Conversation</h2>${msgs || '<p class="hint">(no messages yet)</p>'}
<p class="hint">Read-only observer view; refreshes every 10s. Also available as Markdown: append <code>.md</code> to this URL.</p>`,
<h2>Conversation</h2>${msgs}
<p class="hint">Full activity timeline: joins, messages, questions, resolutions, contract proposals and agreements. Read-only observer view; refreshes every 10s. Also available as Markdown: append <code>.md</code> to this URL.</p>`,
o.room_status === 'open',
);
}
@@ -351,12 +370,7 @@ If a secret must move between the sides, record SECRET_TRANSFER_REQUIRED plus th
/** Read-only human view of the negotiation (same token auth as the API). */
export function roomHtmlPage(v: RoomView, token: string): string {
const msgs = v.conversation
.map(
(m) =>
`<div class="msg"><div class="meta"><b>${escapeHtml(m.role)}</b> · ${escapeHtml(m.created_at)}</div><pre>${escapeHtml(m.content)}</pre></div>`,
)
.join('');
const msgs = timelineHtml(v.participants, v.conversation, v.open_questions, v.resolved_questions, v.current_contract);
const openQs = v.open_questions
.map(
(q) =>
@@ -379,7 +393,8 @@ export function roomHtmlPage(v: RoomView, token: string): string {
${resolvedQs ? `<h2>Resolved questions</h2>${resolvedQs}` : ''}
<h2>Agreed Contract (v${contract ? contract.version : 0})</h2>
<pre>${escapeHtml(contract ? contract.markdown : '(not drafted yet)')}</pre>
<h2>Conversation</h2>${msgs || '<p class="hint">(no messages yet)</p>'}
<h2>Conversation</h2>${msgs}
<p class="hint">Full activity timeline: joins, messages, questions, resolutions, contract proposals and agreements.</p>
<p class="hint">Agent endpoints: <code>/r/${escapeHtml(v.room.id)}/&lt;token&gt;.md</code> · <code>GET /api/rooms/${escapeHtml(v.room.id)}</code> · <a href="/api/rooms/${escapeHtml(v.room.id)}?token=">JSON</a> · <a href="/api/rooms/${escapeHtml(v.room.id)}/final.md">final.md</a></p>
<p class="hint">Read-only view; refreshes every 10s.</p>`,
v.room.status === 'open',