Unified activity timeline in Conversation: joins, questions, resolutions, contract, agreements
deploy / deploy (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
This commit is contained in:
@@ -3,5 +3,7 @@ export { RendezvousService, newToken } from './service.js';
|
|||||||
export { LIMITS } from './limits.js';
|
export { LIMITS } from './limits.js';
|
||||||
export { computeAdvice, computeTurn, availableActions } from './advice.js';
|
export { computeAdvice, computeTurn, availableActions } from './advice.js';
|
||||||
export { renderRoomMarkdown, renderFinalMarkdown, renderObserverMarkdown } from './markdown.js';
|
export { renderRoomMarkdown, renderFinalMarkdown, renderObserverMarkdown } from './markdown.js';
|
||||||
|
export { buildTimeline } from './timeline.js';
|
||||||
|
export type { TimelineEvent, TimelineInput } from './timeline.js';
|
||||||
export { secretsPolicyFull, secretsPolicyShort, redactSecrets } from './security.js';
|
export { secretsPolicyFull, secretsPolicyShort, redactSecrets } from './security.js';
|
||||||
export * from './types.js';
|
export * from './types.js';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { MessageView, ObserverView, ParticipantView, QuestionView, RoomView } from './types.js';
|
import type { ContractView, MessageView, ObserverView, ParticipantView, QuestionView, RoomView } from './types.js';
|
||||||
|
import { buildTimeline } from './timeline.js';
|
||||||
|
|
||||||
/** Read-only Markdown state for the human observer of a room. */
|
/** Read-only Markdown state for the human observer of a room. */
|
||||||
export function renderObserverMarkdown(o: ObserverView, baseUrl: string): string {
|
export function renderObserverMarkdown(o: ObserverView, baseUrl: string): string {
|
||||||
@@ -35,13 +36,22 @@ export function renderObserverMarkdown(o: ObserverView, baseUrl: string): string
|
|||||||
}
|
}
|
||||||
parts.push('');
|
parts.push('');
|
||||||
}
|
}
|
||||||
parts.push('## Conversation');
|
parts.push('## Conversation (full activity timeline)');
|
||||||
if (o.conversation.length === 0) parts.push('(no messages yet)');
|
const timeline = buildTimeline({
|
||||||
|
participants: o.participants,
|
||||||
|
conversation: o.conversation,
|
||||||
|
openQuestions: o.open_questions,
|
||||||
|
resolvedQuestions: o.resolved_questions,
|
||||||
|
contract: o.current_contract,
|
||||||
|
});
|
||||||
|
if (timeline.length === 0) parts.push('(no activity yet)');
|
||||||
else
|
else
|
||||||
for (const m of o.conversation) {
|
for (const e of timeline) {
|
||||||
parts.push(`**${m.role}** (${m.created_at}):`);
|
parts.push(`**${e.role}** — ${e.action} (${e.at})`);
|
||||||
parts.push('');
|
if (e.body) {
|
||||||
parts.push(m.content);
|
parts.push('');
|
||||||
|
parts.push(e.body);
|
||||||
|
}
|
||||||
parts.push('');
|
parts.push('');
|
||||||
}
|
}
|
||||||
parts.push(`---\n_Observer link for room ${o.room.id}. Final artifact: ${baseUrl}/api/rooms/${o.room.id}/final.md (requires a participant token)._`);
|
parts.push(`---\n_Observer link for room ${o.room.id}. Final artifact: ${baseUrl}/api/rooms/${o.room.id}/final.md (requires a participant token)._`);
|
||||||
@@ -62,6 +72,8 @@ export interface MarkdownInput {
|
|||||||
resolvedQuestions: QuestionView[];
|
resolvedQuestions: QuestionView[];
|
||||||
contractMarkdown: string;
|
contractMarkdown: string;
|
||||||
contractVersion: number;
|
contractVersion: number;
|
||||||
|
/** Full contract view (with agreements) for the activity timeline; falls back to contractMarkdown/Version. */
|
||||||
|
contract?: ContractView | null;
|
||||||
/** Short secrets policy (links to the canonical /security.md). */
|
/** Short secrets policy (links to the canonical /security.md). */
|
||||||
secretsPolicyShort: string;
|
secretsPolicyShort: string;
|
||||||
}
|
}
|
||||||
@@ -119,14 +131,27 @@ export function renderRoomMarkdown(i: MarkdownInput): string {
|
|||||||
parts.push(i.contractMarkdown.trim() ? i.contractMarkdown : `(not drafted yet, version ${i.contractVersion})`);
|
parts.push(i.contractMarkdown.trim() ? i.contractMarkdown : `(not drafted yet, version ${i.contractVersion})`);
|
||||||
parts.push('');
|
parts.push('');
|
||||||
|
|
||||||
parts.push('## Conversation');
|
parts.push('## Conversation (full activity timeline)');
|
||||||
if (i.conversation.length === 0) {
|
const timeline = buildTimeline({
|
||||||
parts.push('(no messages yet)');
|
participants: i.participants,
|
||||||
|
conversation: i.conversation,
|
||||||
|
openQuestions: i.openQuestions,
|
||||||
|
resolvedQuestions: i.resolvedQuestions,
|
||||||
|
contract:
|
||||||
|
i.contract ??
|
||||||
|
(i.contractVersion > 0
|
||||||
|
? { version: i.contractVersion, markdown: i.contractMarkdown, updated_at: null, agreements: [] }
|
||||||
|
: null),
|
||||||
|
});
|
||||||
|
if (timeline.length === 0) {
|
||||||
|
parts.push('(no activity yet)');
|
||||||
} else {
|
} else {
|
||||||
for (const m of i.conversation) {
|
for (const e of timeline) {
|
||||||
parts.push(`**${m.role}** (${m.created_at}, id: ${m.id}):`);
|
parts.push(`**${e.role}** — ${e.action} (${e.at})`);
|
||||||
parts.push('');
|
if (e.body) {
|
||||||
parts.push(m.content);
|
parts.push('');
|
||||||
|
parts.push(e.body);
|
||||||
|
}
|
||||||
parts.push('');
|
parts.push('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,10 +234,10 @@ export class RendezvousService {
|
|||||||
|
|
||||||
const agreements = this.db
|
const agreements = this.db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT a.participant_id, a.contract_version, p.role FROM agreements a
|
`SELECT a.participant_id, a.contract_version, a.created_at AS agreed_at, p.role FROM agreements a
|
||||||
JOIN participants p ON p.id = a.participant_id WHERE a.room_id = ?`,
|
JOIN participants p ON p.id = a.participant_id WHERE a.room_id = ?`,
|
||||||
)
|
)
|
||||||
.all(roomId) as { participant_id: string; contract_version: number; role: string }[];
|
.all(roomId) as { participant_id: string; contract_version: number; role: string; agreed_at: string | null }[];
|
||||||
|
|
||||||
const contract =
|
const contract =
|
||||||
room.contract_version > 0
|
room.contract_version > 0
|
||||||
@@ -249,6 +249,7 @@ export class RendezvousService {
|
|||||||
participant_id: a.participant_id,
|
participant_id: a.participant_id,
|
||||||
role: a.role,
|
role: a.role,
|
||||||
version: a.contract_version,
|
version: a.contract_version,
|
||||||
|
agreed_at: a.agreed_at,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
@@ -302,10 +303,10 @@ export class RendezvousService {
|
|||||||
const resolvedQuestions = questions.filter((q) => q.status === 'resolved');
|
const resolvedQuestions = questions.filter((q) => q.status === 'resolved');
|
||||||
const agreements = this.db
|
const agreements = this.db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT a.participant_id, a.contract_version FROM agreements a
|
`SELECT a.participant_id, a.contract_version, a.created_at AS agreed_at FROM agreements a
|
||||||
JOIN participants p ON p.id = a.participant_id WHERE a.room_id = ?`,
|
JOIN participants p ON p.id = a.participant_id WHERE a.room_id = ?`,
|
||||||
)
|
)
|
||||||
.all(roomId) as { participant_id: string; contract_version: number }[];
|
.all(roomId) as { participant_id: string; contract_version: number; agreed_at: string | null }[];
|
||||||
const contract =
|
const contract =
|
||||||
room.contract_version > 0
|
room.contract_version > 0
|
||||||
? {
|
? {
|
||||||
@@ -316,6 +317,7 @@ export class RendezvousService {
|
|||||||
participant_id: a.participant_id,
|
participant_id: a.participant_id,
|
||||||
role: participants.find((p) => p.id === a.participant_id)?.role ?? '?',
|
role: participants.find((p) => p.id === a.participant_id)?.role ?? '?',
|
||||||
version: a.contract_version,
|
version: a.contract_version,
|
||||||
|
agreed_at: a.agreed_at,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { ContractView, MessageView, ParticipantView, QuestionView } from './types.js';
|
||||||
|
|
||||||
|
/** One entry of the unified activity timeline shown as "Conversation". */
|
||||||
|
export interface TimelineEvent {
|
||||||
|
at: string;
|
||||||
|
role: string;
|
||||||
|
kind: 'joined' | 'message' | 'asked' | 'resolved' | 'contract' | 'agreed';
|
||||||
|
/** Short human label, e.g. "asked (BLOCKING → verifier)". */
|
||||||
|
action: string;
|
||||||
|
/** Full text body (message content, question, resolution, contract markdown). */
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineInput {
|
||||||
|
participants: ParticipantView[];
|
||||||
|
conversation: MessageView[];
|
||||||
|
openQuestions: QuestionView[];
|
||||||
|
resolvedQuestions: QuestionView[];
|
||||||
|
contract: ContractView | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges every room activity — joins, messages, questions, resolutions,
|
||||||
|
* contract proposals and agreements — into one chronological timeline, so a
|
||||||
|
* watcher sees each participant's every action instead of plain chat only.
|
||||||
|
*/
|
||||||
|
export function buildTimeline(i: TimelineInput): TimelineEvent[] {
|
||||||
|
const events: TimelineEvent[] = [];
|
||||||
|
|
||||||
|
for (const p of i.participants) {
|
||||||
|
if (p.joined_at)
|
||||||
|
events.push({
|
||||||
|
at: p.joined_at,
|
||||||
|
role: p.role,
|
||||||
|
kind: 'joined',
|
||||||
|
action: 'took the room into work',
|
||||||
|
body: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const m of i.conversation)
|
||||||
|
events.push({ at: m.created_at, role: m.role, kind: 'message', action: 'message', body: m.content });
|
||||||
|
|
||||||
|
for (const q of [...i.openQuestions, ...i.resolvedQuestions]) {
|
||||||
|
const to = q.addressed_to_role ? ` → ${q.addressed_to_role}` : '';
|
||||||
|
events.push({
|
||||||
|
at: q.created_at,
|
||||||
|
role: q.author_role,
|
||||||
|
kind: 'asked',
|
||||||
|
action: `asked (${q.blocking ? 'BLOCKING' : 'non-blocking'}${to})`,
|
||||||
|
body: q.question,
|
||||||
|
});
|
||||||
|
if (q.status === 'resolved' && q.resolved_at)
|
||||||
|
events.push({
|
||||||
|
at: q.resolved_at,
|
||||||
|
role: q.author_role,
|
||||||
|
kind: 'resolved',
|
||||||
|
action: 'question resolved',
|
||||||
|
body: `${q.question}\n\n→ ${q.resolution ?? ''}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = i.contract;
|
||||||
|
if (c) {
|
||||||
|
if (c.updated_at)
|
||||||
|
events.push({
|
||||||
|
at: c.updated_at,
|
||||||
|
role: '—',
|
||||||
|
kind: 'contract',
|
||||||
|
action: `proposed contract v${c.version}`,
|
||||||
|
body: c.markdown,
|
||||||
|
});
|
||||||
|
for (const a of c.agreements)
|
||||||
|
if (a.agreed_at)
|
||||||
|
events.push({
|
||||||
|
at: a.agreed_at,
|
||||||
|
role: a.role,
|
||||||
|
kind: 'agreed',
|
||||||
|
action: `agreed to contract v${a.version}`,
|
||||||
|
body: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
events.sort((x, y) => (x.at < y.at ? -1 : x.at > y.at ? 1 : 0));
|
||||||
|
return events;
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ export interface ContractView {
|
|||||||
version: number;
|
version: number;
|
||||||
markdown: string;
|
markdown: string;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
agreements: { participant_id: string; role: string; version: number }[];
|
agreements: { participant_id: string; role: string; version: number; agreed_at: string | null }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomView {
|
export interface RoomView {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { RendezvousService, Store, RendezvousError, redactSecrets } from '../src/index.js';
|
import { RendezvousService, Store, RendezvousError, redactSecrets, buildTimeline } from '../src/index.js';
|
||||||
|
|
||||||
function makeService(): { svc: RendezvousService; dbPath: string } {
|
function makeService(): { svc: RendezvousService; dbPath: string } {
|
||||||
const dbPath = `:memory:`;
|
const dbPath = `:memory:`;
|
||||||
@@ -220,3 +220,34 @@ test('only addressee or author can resolve a question', () => {
|
|||||||
const resolved = svc.resolveQuestion(created.room_id, A, q.id, 'checked: ok');
|
const resolved = svc.resolveQuestion(created.room_id, A, q.id, 'checked: ok');
|
||||||
assert.equal(resolved.status, 'resolved');
|
assert.equal(resolved.status, 'resolved');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('activity timeline includes every action of every participant', () => {
|
||||||
|
const { svc } = makeService();
|
||||||
|
const created = createTwoPartyRoom(svc);
|
||||||
|
const [A, B] = created.participants.map((p) => p.token);
|
||||||
|
svc.join(created.room_id, A);
|
||||||
|
svc.join(created.room_id, B);
|
||||||
|
svc.postMessage(created.room_id, A, 'fact from A');
|
||||||
|
const q = svc.openQuestion(created.room_id, B, 'check IIS logs', true, created.participants[0].id);
|
||||||
|
svc.resolveQuestion(created.room_id, A, q.id, 'checked: every 15 min');
|
||||||
|
svc.proposeContract(created.room_id, B, '## Facts\n- ok');
|
||||||
|
svc.agree(created.room_id, A);
|
||||||
|
svc.agree(created.room_id, B);
|
||||||
|
|
||||||
|
const view = svc.getRoomView(created.room_id, A);
|
||||||
|
const timeline = buildTimeline({
|
||||||
|
participants: view.participants,
|
||||||
|
conversation: view.conversation,
|
||||||
|
openQuestions: view.open_questions,
|
||||||
|
resolvedQuestions: view.resolved_questions,
|
||||||
|
contract: view.current_contract,
|
||||||
|
});
|
||||||
|
const kinds: string[] = timeline.map((e) => e.kind);
|
||||||
|
for (const expected of ['joined', 'joined', 'message', 'asked', 'resolved', 'contract', 'agreed', 'agreed'])
|
||||||
|
assert.ok(kinds.includes(expected), `timeline must contain "${expected}", got: ${kinds.join(',')}`);
|
||||||
|
// chronological order
|
||||||
|
for (let i = 1; i < timeline.length; i++)
|
||||||
|
assert.ok(timeline[i - 1].at <= timeline[i].at, 'timeline must be sorted by time');
|
||||||
|
// every event is attributed to a role
|
||||||
|
for (const e of timeline) assert.ok(e.role.length > 0 && e.action.length > 0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export function buildRouter(service: RendezvousService, cfg: ServerConfig) {
|
|||||||
resolvedQuestions: view.resolved_questions,
|
resolvedQuestions: view.resolved_questions,
|
||||||
contractMarkdown: view.current_contract?.markdown ?? '',
|
contractMarkdown: view.current_contract?.markdown ?? '',
|
||||||
contractVersion: view.current_contract?.version ?? 0,
|
contractVersion: view.current_contract?.version ?? 0,
|
||||||
|
contract: view.current_contract,
|
||||||
secretsPolicyShort: secretsPolicyShort(cfg.baseUrl),
|
secretsPolicyShort: secretsPolicyShort(cfg.baseUrl),
|
||||||
}), 'text/markdown; charset=utf-8');
|
}), 'text/markdown; charset=utf-8');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,28 @@
|
|||||||
import type { ObserverView, RoomView } from '@ai-rendezvous/core';
|
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';
|
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. */
|
/** Machine-readable instruction page served at /create.md — this is what an agent reads first. */
|
||||||
export function createMarkdownDoc(baseUrl: string): string {
|
export function createMarkdownDoc(baseUrl: string): string {
|
||||||
return `# AI Rendezvous — create a room
|
return `# AI Rendezvous — create a room
|
||||||
@@ -92,6 +113,9 @@ const PAGE_CSS = `
|
|||||||
code, pre { font-family: ui-monospace, monospace; font-size: 0.85rem; }
|
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; }
|
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 { 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; }
|
.meta { color: rgba(127,127,127,.9); font-size: .8rem; }
|
||||||
.q { padding: .5rem .75rem; border-radius: 6px; margin: .5rem 0; }
|
.q { padding: .5rem .75rem; border-radius: 6px; margin: .5rem 0; }
|
||||||
.q.blocking { background: rgba(200,60,60,.14); }
|
.q.blocking { background: rgba(200,60,60,.14); }
|
||||||
@@ -196,12 +220,7 @@ export function observerHtmlPage(o: ObserverView, token: string): string {
|
|||||||
return `${mark} (${joined})`;
|
return `${mark} (${joined})`;
|
||||||
})
|
})
|
||||||
.join(', ');
|
.join(', ');
|
||||||
const msgs = o.conversation
|
const msgs = timelineHtml(o.participants, o.conversation, o.open_questions, o.resolved_questions, o.current_contract);
|
||||||
.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 openQs = o.open_questions
|
const openQs = o.open_questions
|
||||||
.map(
|
.map(
|
||||||
(q) =>
|
(q) =>
|
||||||
@@ -225,8 +244,8 @@ export function observerHtmlPage(o: ObserverView, token: string): string {
|
|||||||
${resolvedQs ? `<h2>Resolved questions</h2>${resolvedQs}` : ''}
|
${resolvedQs ? `<h2>Resolved questions</h2>${resolvedQs}` : ''}
|
||||||
<h2>Agreed Contract (v${contract ? contract.version : 0})</h2>
|
<h2>Agreed Contract (v${contract ? contract.version : 0})</h2>
|
||||||
<pre>${escapeHtml(contract ? contract.markdown : '(not drafted yet)')}</pre>
|
<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">Read-only observer view; refreshes every 10s. Also available as Markdown: append <code>.md</code> to this URL.</p>`,
|
<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',
|
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). */
|
/** Read-only human view of the negotiation (same token auth as the API). */
|
||||||
export function roomHtmlPage(v: RoomView, token: string): string {
|
export function roomHtmlPage(v: RoomView, token: string): string {
|
||||||
const msgs = v.conversation
|
const msgs = timelineHtml(v.participants, v.conversation, v.open_questions, v.resolved_questions, v.current_contract);
|
||||||
.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 openQs = v.open_questions
|
const openQs = v.open_questions
|
||||||
.map(
|
.map(
|
||||||
(q) =>
|
(q) =>
|
||||||
@@ -379,7 +393,8 @@ export function roomHtmlPage(v: RoomView, token: string): string {
|
|||||||
${resolvedQs ? `<h2>Resolved questions</h2>${resolvedQs}` : ''}
|
${resolvedQs ? `<h2>Resolved questions</h2>${resolvedQs}` : ''}
|
||||||
<h2>Agreed Contract (v${contract ? contract.version : 0})</h2>
|
<h2>Agreed Contract (v${contract ? contract.version : 0})</h2>
|
||||||
<pre>${escapeHtml(contract ? contract.markdown : '(not drafted yet)')}</pre>
|
<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)}/<token>.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">Agent endpoints: <code>/r/${escapeHtml(v.room.id)}/<token>.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>`,
|
<p class="hint">Read-only view; refreshes every 10s.</p>`,
|
||||||
v.room.status === 'open',
|
v.room.status === 'open',
|
||||||
|
|||||||
Reference in New Issue
Block a user