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 <noreply@anthropic.com>
This commit is contained in:
parent
5775e810d6
commit
1282ba5325
|
|
@ -216,16 +216,49 @@ export class RStackHistoryPanel extends HTMLElement {
|
|||
const actions: Record<string, number> = {};
|
||||
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 {
|
|||
</div>
|
||||
<div class="activity-group__summary">${this._esc(g.summary)}</div>
|
||||
<div class="activity-group__details">
|
||||
${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 `
|
||||
<div class="activity-entry">
|
||||
<span class="activity-entry__msg">${this._esc(e.message)}</span>
|
||||
<span class="activity-entry__ops">${e.opsCount} op${e.opsCount !== 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
`).join("")}
|
||||
${g.entries.length > 5 ? `<div class="activity-entry activity-entry--more">+${g.entries.length - 5} more</div>` : ""}
|
||||
<span class="activity-entry__icon">${h.icon}</span>
|
||||
<span class="activity-entry__msg">${this._esc(h.text)}</span>
|
||||
<span class="activity-entry__time">${t}</span>
|
||||
</div>`;
|
||||
}).join("")}
|
||||
${g.entries.length > 8 ? `<div class="activity-entry activity-entry--more">+${g.entries.length - 8} more</div>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).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, ">").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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue