import { action, computed, type Default, handler, NAME, nonPrivateRandom, pattern, safeDateNow, UI, type VNode, Writable, } from "commonfabric"; // ===== Types ===== /** * A piece that can be mentioned via wiki-links. * Mirrors the shape used in notes/schemas.tsx. */ interface MentionablePiece { [NAME]?: string; isHidden?: boolean; mentioned: MentionablePiece[]; backlinks: MentionablePiece[]; } export interface ActivityEvent { id: string; timestamp: string; // ISO 8601 agent: string; action: string; // short verb phrase e.g. "deployed", "populated" pieceName?: string; pieceRef?: MentionablePiece; note?: string; } interface ActivityLogInput { events?: Writable>; mentioned?: Writable>; } /** An #activityLog for recording structured agent events. */ export interface ActivityLogOutput { [NAME]: string; [UI]: VNode; logEvent: unknown; clearLog: unknown; summary: string; } // ===== Module-scope Handlers ===== const logEventHandler = handler< Omit, { events: Writable; mentioned: Writable } >((args, { events, mentioned }) => { const now = safeDateNow(); events.push({ id: `${now.toString(36)}-${nonPrivateRandom().toString(36).slice(2, 11)}`, timestamp: new Date(now).toISOString(), ...args, }); if (args.pieceRef) mentioned.push(args.pieceRef); }); const clearLogHandler = handler< unknown, { events: Writable } >((_, { events }) => events.set([])); // ===== Pattern ===== export default pattern( ({ events, mentioned }) => { // Bind handlers const logEvent = logEventHandler({ events, mentioned }); const clearLog = clearLogHandler({ events }); // Filter state — local, use action not handler const filterAgent = new Writable(null); const setFilter = action(({ agent }: { agent: string | null }) => filterAgent.set(agent) ); // Derived values const agents = computed(() => [ ...new Set( events.get().filter(Boolean).map((e: ActivityEvent) => e.agent), ), ]); const filtered = computed(() => { const all = events.get().filter(Boolean) as ActivityEvent[]; return filterAgent.get() ? all.filter((e) => e.agent === filterAgent.get()) : all; }); const summary = computed(() => { const all = events.get().filter(Boolean) as ActivityEvent[]; if (!all.length) return "No activity logged."; return all .slice(-20) .map((e: ActivityEvent) => { const ts = new Date(e.timestamp).toLocaleString(); const parts = [`[${ts}]`, `${e.agent}:`, e.action]; if (e.pieceName) parts.push(e.pieceName); if (e.note) parts.push(`— ${e.note}`); return parts.join(" "); }) .join("\n"); }); return { [NAME]: computed(() => `Activity Log (${events.get().length})`), [UI]: ( Activity Log Clear {/* Agent filter chips */} setFilter.send({ agent: null })} > All {agents.map((agent: string) => ( setFilter.send({ agent })} > {agent} ))} {/* Event list */} {filtered.map((event: ActivityEvent) => ( {event.agent} {event.action} {event.pieceName ? ( {event.pieceName} ) : null} {event.pieceRef ? : null} {event.note ? ( {event.note} ) : null} {new Date(event.timestamp).toLocaleTimeString()} ))} {computed(() => filtered.length === 0 ? ( No events yet ) : null )} ), logEvent, clearLog, summary, }; }, );