From 1282ba5325a8436692f6eebac8b7b0e0001f7572 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 17:56:13 -0700 Subject: [PATCH] feat(history): humanize activity entries with icons and timestamps Replace raw Automerge change messages (e.g. "Update shape abc-123-uuid") with human-readable text and contextual icons. Add per-entry timestamps for clearer chronology within author groups. Co-Authored-By: Claude Opus 4.6 --- shared/components/rstack-history-panel.ts | 85 ++++++++++++++++++----- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/shared/components/rstack-history-panel.ts b/shared/components/rstack-history-panel.ts index ebeb244..54b0424 100644 --- a/shared/components/rstack-history-panel.ts +++ b/shared/components/rstack-history-panel.ts @@ -216,16 +216,49 @@ export class RStackHistoryPanel extends HTMLElement { const actions: Record = {}; for (const e of entries) { const msg = e.message.toLowerCase(); - if (msg.startsWith("update")) actions["edited"] = (actions["edited"] || 0) + 1; - else if (msg.startsWith("add")) actions["added"] = (actions["added"] || 0) + 1; + if (msg.startsWith("update shape")) actions["moved/edited shapes"] = (actions["moved/edited shapes"] || 0) + 1; + else if (msg.startsWith("update layer")) actions["updated layers"] = (actions["updated layers"] || 0) + 1; + else if (msg.startsWith("update")) actions["updated items"] = (actions["updated items"] || 0) + 1; + else if (msg.startsWith("add shape") || msg.startsWith("add ")) actions["added"] = (actions["added"] || 0) + 1; else if (msg.startsWith("delete") || msg.startsWith("remove")) actions["removed"] = (actions["removed"] || 0) + 1; else if (msg.startsWith("forget")) actions["forgot"] = (actions["forgot"] || 0) + 1; - else if (msg.startsWith("remember")) actions["remembered"] = (actions["remembered"] || 0) + 1; + else if (msg.startsWith("remember")) actions["restored"] = (actions["restored"] || 0) + 1; + else if (msg.startsWith("undo")) actions["undid"] = (actions["undid"] || 0) + 1; + else if (msg.startsWith("redo")) actions["redid"] = (actions["redid"] || 0) + 1; else actions["changed"] = (actions["changed"] || 0) + 1; } return Object.entries(actions) - .map(([action, count]) => `${action} ${count} item${count > 1 ? "s" : ""}`) - .join(", "); + .map(([action, count]) => `${action}: ${count}`) + .join(" ยท "); + } + + /** Turn raw Automerge change messages into human-readable text with icons. */ + private _humanizeMessage(msg: string): { icon: string; text: string } { + // Strip shape/layer/flow UUIDs for readability + const short = msg.replace(/\s+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "") + .replace(/\s+[a-z]+-[a-z0-9]{6,}/gi, ""); // short IDs like folk-abc123 + + const m = msg.toLowerCase(); + if (m.startsWith("update shape")) return { icon: "โœ๏ธ", text: "Edited shape" }; + if (m.startsWith("add shape")) return { icon: "โž•", text: "Added shape" }; + if (m.startsWith("delete shape")) return { icon: "๐Ÿ—‘", text: "Deleted shape" }; + if (m.startsWith("forget shape")) return { icon: "๐Ÿ‘ป", text: "Forgot shape" }; + if (m.startsWith("remember shape")) return { icon: "๐Ÿ’ก", text: "Restored shape" }; + if (m.startsWith("undo delete")) return { icon: "โ†ฉ๏ธ", text: "Undid delete" }; + if (m.startsWith("undo")) return { icon: "โ†ฉ๏ธ", text: "Undo" }; + if (m.startsWith("redo")) return { icon: "โ†ช๏ธ", text: "Redo" }; + if (m.startsWith("add layer")) return { icon: "๐Ÿ“‘", text: "Added layer" }; + if (m.startsWith("remove layer")) return { icon: "๐Ÿ“‘", text: "Removed layer" }; + if (m.startsWith("update layer")) return { icon: "๐Ÿ“‘", text: "Updated layer" }; + if (m.startsWith("add flow")) return { icon: "๐Ÿ”€", text: "Added flow" }; + if (m.startsWith("remove flow")) return { icon: "๐Ÿ”€", text: "Removed flow" }; + if (m.startsWith("add connection")) return { icon: "๐Ÿ”—", text: "Added connection" }; + if (m.startsWith("remove connection")) return { icon: "๐Ÿ”—", text: "Removed connection" }; + if (m.startsWith("event:")) return { icon: "โšก", text: short.trim() }; + if (m.startsWith("subscribe")) return { icon: "๐Ÿ“ก", text: "Subscribed shape" }; + if (m.startsWith("set view mode")) return { icon: "๐Ÿ‘", text: short.trim() }; + if (m.startsWith("initialize")) return { icon: "๐Ÿ—", text: "Initialized space" }; + return { icon: "ยท", text: short.trim() || msg }; } // โ”€โ”€ Time Machine โ”€โ”€ @@ -278,13 +311,17 @@ export class RStackHistoryPanel extends HTMLElement {
${this._esc(g.summary)}
- ${g.entries.slice(0, 5).map(e => ` + ${g.entries.slice(0, 8).map(e => { + const h = this._humanizeMessage(e.message); + const t = e.time ? this._shortTime(e.time) : ""; + return `
- ${this._esc(e.message)} - ${e.opsCount} op${e.opsCount !== 1 ? "s" : ""} -
- `).join("")} - ${g.entries.length > 5 ? `
+${g.entries.length - 5} more
` : ""} + ${h.icon} + ${this._esc(h.text)} + ${t} +
`; + }).join("")} + ${g.entries.length > 8 ? `
+${g.entries.length - 8} more
` : ""} `).join("")} @@ -426,6 +463,12 @@ export class RStackHistoryPanel extends HTMLElement { return new Date(ts).toLocaleDateString(); } + private _shortTime(ts: number): string { + if (!ts) return ""; + const d = new Date(ts); + return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } + private _esc(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } @@ -612,10 +655,17 @@ const PANEL_CSS = ` .activity-entry { display: flex; align-items: center; - justify-content: space-between; - padding: 2px 0; - font-size: 0.72rem; - color: var(--rs-text-muted, #64748b); + gap: 6px; + padding: 3px 0; + font-size: 0.75rem; + color: var(--rs-text-secondary, #94a3b8); +} + +.activity-entry__icon { + flex-shrink: 0; + width: 18px; + text-align: center; + font-size: 0.7rem; } .activity-entry__msg { @@ -626,11 +676,10 @@ const PANEL_CSS = ` white-space: nowrap; } -.activity-entry__ops { +.activity-entry__time { flex-shrink: 0; - margin-left: 8px; font-size: 0.65rem; - color: var(--rs-text-muted); + color: var(--rs-text-muted, #64748b); opacity: 0.7; }