@bobfrankston/mailx 1.0.442 → 1.0.443

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/app.js CHANGED
@@ -696,9 +696,17 @@ async function openCompose(mode) {
696
696
  sessionStorage.setItem("composeInit", JSON.stringify(init));
697
697
  // Inline compose: load compose.html in an overlay iframe (same origin, same IPC)
698
698
  // Popup windows don't work in IPC mode (custom protocol doesn't propagate to child windows)
699
- showComposeOverlay();
699
+ // Title reflects mode + subject so the user can see what they're replying to
700
+ // ("Re: Stars of STEM 2026" instead of just "Compose"). Forward shows the
701
+ // forward target subject; new compose stays generic.
702
+ const titlePrefix = mode === "reply" ? "Reply" :
703
+ mode === "replyAll" ? "Reply All" :
704
+ mode === "forward" ? "Forward" :
705
+ "Compose";
706
+ const titleSubject = mode === "new" ? "" : (msg?.subject || init.subject || "");
707
+ showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
700
708
  }
701
- function showComposeOverlay() {
709
+ function showComposeOverlay(title = "Compose") {
702
710
  const wrapper = document.createElement("div");
703
711
  wrapper.className = "compose-overlay";
704
712
  // Full-screen on small/short screens, floating on larger
@@ -712,7 +720,7 @@ function showComposeOverlay() {
712
720
  // Title bar — drag to move, close button
713
721
  const titleBar = document.createElement("div");
714
722
  titleBar.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:#e8ecf0;border-radius:8px 8px 0 0;cursor:move;user-select:none;flex-shrink:0;";
715
- titleBar.textContent = "Compose";
723
+ titleBar.textContent = title;
716
724
  const closeBtn = document.createElement("button");
717
725
  closeBtn.textContent = "✕";
718
726
  closeBtn.title = "Save draft and close";
@@ -92,10 +92,59 @@ catch { /* private-mode / SecurityError — default quill */ }
92
92
  }
93
93
  catch { /* non-fatal */ }
94
94
  })();
95
- await loadEditorAssets(editorType);
95
+ // Whatever happens in editor init, surface failures to the mailx log.
96
+ // Until this round the failures were swallowed silently — Quill CDN
97
+ // timing out or createEditor throwing left the iframe with empty body
98
+ // + dead buttons (because the rest of the script after the await never
99
+ // ran). With logClientEvent here, the failure mode is visible in the
100
+ // service log instead of "Reply just shows blank compose".
101
+ let editor;
102
+ try {
103
+ await loadEditorAssets(editorType);
104
+ }
105
+ catch (e) {
106
+ logClientEvent("compose-editor-assets-failed", { type: editorType, error: String(e?.message || e) });
107
+ throw e;
108
+ }
96
109
  const container = document.getElementById("compose-editor");
97
110
  container.classList.add(editorType === "tiptap" ? "editor-tiptap" : "editor-quill");
98
- const editor = await createEditor(container, editorType);
111
+ try {
112
+ editor = await createEditor(container, editorType);
113
+ }
114
+ catch (e) {
115
+ logClientEvent("compose-editor-create-failed", { type: editorType, error: String(e?.message || e) });
116
+ // Render a minimal contenteditable fallback so the user can still type
117
+ // SOMETHING. Without this, an editor failure leaves the compose form
118
+ // half-functional (To/Cc/Bcc work, body doesn't) and the user doesn't
119
+ // know why. The fallback is a plain div — no toolbar, no rich text.
120
+ container.innerHTML = `<div class="compose-fallback-editor" contenteditable="true" style="border:1px solid #c00;padding:8px;min-height:200px;background:#fff" data-fallback="true"></div>`;
121
+ const fallback = container.querySelector(".compose-fallback-editor");
122
+ editor = {
123
+ root: fallback,
124
+ setHtml: (html) => { fallback.innerHTML = html; },
125
+ getHtml: () => fallback.innerHTML,
126
+ getText: () => fallback.innerText,
127
+ focus: () => fallback.focus(),
128
+ setCursor: () => { },
129
+ getScrollContainer: () => fallback,
130
+ onContentChange: (handler) => { fallback.addEventListener("input", handler); },
131
+ onKeyDown: (handler) => { fallback.addEventListener("keydown", handler); },
132
+ insertTextAtCursor: (text) => {
133
+ const sel = window.getSelection();
134
+ if (sel && sel.rangeCount > 0) {
135
+ const range = sel.getRangeAt(0);
136
+ range.deleteContents();
137
+ range.insertNode(document.createTextNode(text));
138
+ }
139
+ else {
140
+ fallback.append(document.createTextNode(text));
141
+ }
142
+ },
143
+ };
144
+ // Surface the failure to the user in the status bar so they know
145
+ // why the toolbar is missing and the editor is plain.
146
+ setTimeout(() => showDraftStatus(`Editor failed to load (${editorType}). Plain-text fallback in use. Open log for details.`, true), 0);
147
+ }
99
148
  // Ctrl+scroll / Ctrl+= / Ctrl+- / Ctrl+0 zoom for the compose editor body.
100
149
  // Persists per-session in localStorage so zoom survives window pop/close cycles.
101
150
  (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.442",
3
+ "version": "1.0.443",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -559,11 +559,23 @@ export class MailxService {
559
559
  // in PATH) fall back to the OS default handler so the user still gets
560
560
  // *some* editor. Report which one ran so the UI can say "Opening in
561
561
  // Word…" vs "Opening in default editor…".
562
- const { spawnSync, spawn } = await import("node:child_process");
562
+ //
563
+ // CRITICAL: must use async spawn (not spawnSync). spawnSync blocks
564
+ // the Node event loop until the spawned process exits — and on
565
+ // Windows, `cmd /c start ... <gui-app>` sometimes does not return
566
+ // immediately when the GUI app hangs around. That froze the entire
567
+ // mailx IPC bridge on Edit-in-Word click; subsequent clicks
568
+ // (Discard, X, anything) hung waiting for a response that never
569
+ // came back. Async spawn launches and returns immediately;
570
+ // success/failure of the GUI launch is invisible from here, but
571
+ // the file is written and the watcher is armed regardless.
572
+ const { spawn } = await import("node:child_process");
563
573
  const tryLaunch = (cmd, args) => {
564
574
  try {
565
- const r = spawnSync(cmd, args, { stdio: "ignore", windowsHide: true });
566
- return r.status === 0 && !r.error;
575
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
576
+ child.on("error", () => { });
577
+ child.unref();
578
+ return true;
567
579
  }
568
580
  catch {
569
581
  return false;