Merge branch 'dev'
CI/CD / deploy (push) Failing after 4m36s Details

This commit is contained in:
Jeff Emmett 2026-04-13 10:26:56 -04:00
commit 57c4bf455d
4 changed files with 50 additions and 12 deletions

View File

@ -157,6 +157,8 @@ export class CommunitySync extends EventTarget {
#disconnectedIntentionally = false; #disconnectedIntentionally = false;
#communitySlug: string; #communitySlug: string;
#shapes: Map<string, FolkShape> = new Map(); #shapes: Map<string, FolkShape> = new Map();
#shapeListeners: Map<string, { transform: EventListener; content: EventListener }> = new Map();
#changeCount = 0;
#pendingChanges: boolean = false; #pendingChanges: boolean = false;
#reconnectAttempts = 0; #reconnectAttempts = 0;
#maxReconnectAttempts = 5; #maxReconnectAttempts = 5;
@ -532,15 +534,25 @@ export class CommunitySync extends EventTarget {
registerShape(shape: FolkShape): void { registerShape(shape: FolkShape): void {
this.#shapes.set(shape.id, shape); this.#shapes.set(shape.id, shape);
// Listen for transform events // Remove stale listeners if shape is re-registered
shape.addEventListener("folk-transform", ((e: CustomEvent) => { const old = this.#shapeListeners.get(shape.id);
this.#handleShapeChange(shape); if (old) {
}) as EventListener); shape.removeEventListener("folk-transform", old.transform);
shape.removeEventListener("content-change", old.content);
}
// Listen for content changes (for markdown shapes) // Create named listener refs so they can be removed later
shape.addEventListener("content-change", ((e: CustomEvent) => { const transformListener = (() => {
this.#handleShapeChange(shape); this.#handleShapeChange(shape);
}) as EventListener); }) as EventListener;
const contentListener = (() => {
this.#handleShapeChange(shape);
}) as EventListener;
this.#shapeListeners.set(shape.id, { transform: transformListener, content: contentListener });
shape.addEventListener("folk-transform", transformListener);
shape.addEventListener("content-change", contentListener);
// Add to document if not exists // Add to document if not exists
if (!this.#doc.shapes[shape.id]) { if (!this.#doc.shapes[shape.id]) {
@ -555,6 +567,13 @@ export class CommunitySync extends EventTarget {
* Unregister a shape * Unregister a shape
*/ */
unregisterShape(shapeId: string): void { unregisterShape(shapeId: string): void {
const shape = this.#shapes.get(shapeId);
const listeners = this.#shapeListeners.get(shapeId);
if (shape && listeners) {
shape.removeEventListener("folk-transform", listeners.transform);
shape.removeEventListener("content-change", listeners.content);
}
this.#shapeListeners.delete(shapeId);
this.#shapes.delete(shapeId); this.#shapes.delete(shapeId);
} }
@ -582,6 +601,12 @@ export class CommunitySync extends EventTarget {
doc.shapes[shape.id] = JSON.parse(JSON.stringify(shapeData)); doc.shapes[shape.id] = JSON.parse(JSON.stringify(shapeData));
}); });
// Compact Automerge history periodically to prevent unbounded WASM heap growth
this.#changeCount++;
if (this.#changeCount % 500 === 0) {
this.#doc = Automerge.clone(this.#doc);
}
// Record for undo (skip if this is a brand-new shape — registerShape handles that) // Record for undo (skip if this is a brand-new shape — registerShape handles that)
if (beforeData) { if (beforeData) {
this.#pushUndo(shape.id, beforeData, this.#cloneShapeData(shape.id)); this.#pushUndo(shape.id, beforeData, this.#cloneShapeData(shape.id));

View File

@ -615,7 +615,7 @@ export class FolkPrompt extends FolkShape {
const content = this.#promptInput?.value.trim(); const content = this.#promptInput?.value.trim();
if (!content || this.#isStreaming) return; if (!content || this.#isStreaming) return;
// Add user message with any pending images // Add user message with any pending images (cap at 30 to prevent unbounded growth with base64 images)
const userMessage: ChatMessage = { const userMessage: ChatMessage = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
role: "user", role: "user",
@ -624,6 +624,7 @@ export class FolkPrompt extends FolkShape {
timestamp: new Date(), timestamp: new Date(),
}; };
this.#messages.push(userMessage); this.#messages.push(userMessage);
if (this.#messages.length > 30) this.#messages = this.#messages.slice(-30);
// Clear input and pending images // Clear input and pending images
if (this.#promptInput) this.#promptInput.value = ""; if (this.#promptInput) this.#promptInput.value = "";

View File

@ -50,6 +50,7 @@ export class RStackMi extends HTMLElement {
#voiceDictation: SpeechDictation | null = null; #voiceDictation: SpeechDictation | null = null;
#voiceAccumulated = ""; #voiceAccumulated = "";
#voiceSilenceTimer: ReturnType<typeof setTimeout> | null = null; #voiceSilenceTimer: ReturnType<typeof setTimeout> | null = null;
#outsideClickHandler: ((e: PointerEvent) => void) | null = null;
constructor() { constructor() {
super(); super();
@ -67,6 +68,10 @@ export class RStackMi extends HTMLElement {
disconnectedCallback() { disconnectedCallback() {
document.removeEventListener("keydown", this.#keyHandler); document.removeEventListener("keydown", this.#keyHandler);
if (this.#outsideClickHandler) {
document.removeEventListener("pointerdown", this.#outsideClickHandler as EventListener);
this.#outsideClickHandler = null;
}
if (this.#placeholderTimer) clearInterval(this.#placeholderTimer); if (this.#placeholderTimer) clearInterval(this.#placeholderTimer);
if (this.#voiceMode) this.#deactivateVoiceMode(); if (this.#voiceMode) this.#deactivateVoiceMode();
} }
@ -261,14 +266,18 @@ export class RStackMi extends HTMLElement {
}); });
// Close panel on outside click — use composedPath to pierce Shadow DOM // Close panel on outside click — use composedPath to pierce Shadow DOM
document.addEventListener("pointerdown", (e) => { if (this.#outsideClickHandler) {
document.removeEventListener("pointerdown", this.#outsideClickHandler as EventListener);
}
this.#outsideClickHandler = (e: PointerEvent) => {
if (this.#voiceMode) return; // Keep panel open during voice conversation if (this.#voiceMode) return; // Keep panel open during voice conversation
const path = e.composedPath(); const path = e.composedPath();
if (!path.includes(this)) { if (!path.includes(this)) {
panel.classList.remove("open"); panel.classList.remove("open");
bar.classList.remove("focused"); bar.classList.remove("focused");
} }
}); };
document.addEventListener("pointerdown", this.#outsideClickHandler as EventListener);
// Prevent internal clicks from closing // Prevent internal clicks from closing
panel.addEventListener("pointerdown", (e) => e.stopPropagation()); panel.addEventListener("pointerdown", (e) => e.stopPropagation());
@ -687,8 +696,9 @@ export class RStackMi extends HTMLElement {
panel.classList.add("open"); panel.classList.add("open");
this.#shadow.getElementById("mi-bar")!.classList.add("focused"); this.#shadow.getElementById("mi-bar")!.classList.add("focused");
// Add user message // Add user message (cap at 50 to prevent unbounded growth)
this.#messages.push({ role: "user", content: query }); this.#messages.push({ role: "user", content: query });
if (this.#messages.length > 50) this.#messages = this.#messages.slice(-50);
this.#renderMessages(messagesEl); this.#renderMessages(messagesEl);
// Add placeholder for assistant // Add placeholder for assistant

View File

@ -3840,6 +3840,8 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
// Also remove wb-svg elements from the overlay // Also remove wb-svg elements from the overlay
const wbEl = wbOverlay?.querySelector(`[data-wb-id="${shapeId}"]`); const wbEl = wbOverlay?.querySelector(`[data-wb-id="${shapeId}"]`);
if (wbEl) wbEl.remove(); if (wbEl) wbEl.remove();
// Clean up stale position tracking for deleted shapes
shapeLastPos.delete(shapeId);
// Hide schedule icon and reminder widget if deleted shape was selected // Hide schedule icon and reminder widget if deleted shape was selected
selectedShapeIds.delete(shapeId); selectedShapeIds.delete(shapeId);
updateSelectionVisuals(); updateSelectionVisuals();
@ -7472,7 +7474,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
}); });
// Keep-alive ping to prevent WebSocket idle timeout // Keep-alive ping to prevent WebSocket idle timeout
setInterval(() => { const keepAliveInterval = setInterval(() => {
try { try {
sync.ping(); sync.ping();
} catch (e) { } catch (e) {