- MessageSchema: status enum unread|delivered|read|acked + replyTo (additive, RW-compatible) - messageService: listInbox transitions unread->delivered on fetch (agent-scoped); markMessageDelivered (idempotent, never downgrades); ackMessage(cwd,id,by?) - routes: GET /messages HTML branch -> renderMessagesHtml; GET /messages/:id; POST /messages/:id/ack (mirror of /read, emits message/updated status=acked) - server/messages.ts: 2-column conversation view (list grouped by pair + unread badge, thread bubbles aligned by ?as=, replyTo indentation, TSK pill, live via /events) - activity.ts: drop message rows (tasks-only hard separation) - ui-shared: HeaderPage +messages + nav link - CLI: message ack <id> [--by], message reply <parentId> --from --text [--task]; remoteClient ackMessage + getMessage - tests: +message-receipts.test.ts, server.test.ts activity/messages/receipt-chain Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
61 lines
2.2 KiB
TypeScript
61 lines
2.2 KiB
TypeScript
import { createMessage, listInbox, markMessageRead, ackMessage, getMessage } from '../../core/services/messageService.js';
|
|
import type { Message } from '../../core/schema.js';
|
|
|
|
export function messageSend(
|
|
cwd: string,
|
|
opts: { from: string; to: string; text: string; taskId?: string },
|
|
): void {
|
|
const m = createMessage(cwd, opts);
|
|
console.log(`AgentHub: Message sent ${m.id} (${m.from} → ${m.to})`);
|
|
}
|
|
|
|
export function inboxList(cwd: string, opts: { agent: string; unreadOnly?: boolean }): void {
|
|
const msgs = listInbox(cwd, opts.agent, { unreadOnly: opts.unreadOnly });
|
|
if (msgs.length === 0) {
|
|
console.log(`No messages for ${opts.agent}.`);
|
|
return;
|
|
}
|
|
for (const m of msgs) {
|
|
const flag = m.status === 'unread' ? '●' : ' ';
|
|
console.log(`${flag} ${m.id} ${m.from} → ${m.to}${m.taskId ? ` [${m.taskId}]` : ''}: ${m.text}`);
|
|
}
|
|
}
|
|
|
|
export function messageRead(cwd: string, id: string): void {
|
|
const m = markMessageRead(cwd, id);
|
|
console.log(`AgentHub: Message read ${m.id} (${m.from} → ${m.to})`);
|
|
}
|
|
|
|
export function inboxMarkRead(cwd: string, opts: { agent: string; unreadOnly?: boolean }): void {
|
|
const msgs = listInbox(cwd, opts.agent, { unreadOnly: opts.unreadOnly });
|
|
for (const m of msgs) markMessageRead(cwd, m.id);
|
|
console.log(`AgentHub: marked ${msgs.length} message${msgs.length === 1 ? '' : 's'} read for ${opts.agent}`);
|
|
}
|
|
|
|
export function messageAck(cwd: string, id: string, by?: string): void {
|
|
const m = ackMessage(cwd, id, by);
|
|
console.log(`AgentHub: Message acked ${m.id} (${m.from} → ${m.to})${by ? ` by ${by}` : ''}`);
|
|
}
|
|
|
|
/**
|
|
* Reply to a message: loads the parent, sends a new message back to the parent's
|
|
* sender (to = parent.from), links it via replyTo, and inherits the parent's
|
|
* taskId unless one is given.
|
|
*/
|
|
export function messageReply(
|
|
cwd: string,
|
|
parentId: string,
|
|
opts: { from: string; text: string; taskId?: string },
|
|
): Message {
|
|
const { message: parent } = getMessage(cwd, parentId);
|
|
const m = createMessage(cwd, {
|
|
from: opts.from,
|
|
to: parent.from,
|
|
text: opts.text,
|
|
taskId: opts.taskId ?? parent.taskId,
|
|
replyTo: parentId,
|
|
});
|
|
console.log(`AgentHub: Reply sent ${m.id} (${m.from} → ${m.to}) ↩ ${parentId}`);
|
|
return m;
|
|
}
|