summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 16:37:23 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 16:37:23 +0200
commit391db2f2197b5f7fbdba0c918a24830a5cbe6ee4 (patch)
tree570480f075de3b8f6609675b9ced67815771d961 /packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
parent56776934c2ddfbad4884b252da6f5fb25864b8db (diff)
downloadmeshbay-391db2f2197b5f7fbdba0c918a24830a5cbe6ee4.tar.gz
fix(spa): a reconnect must give the Chat composer back
The Chat tab froze about every other day — the textbox stopped taking clicks — and it never recovered on its own: no timeout ends this one, only leaving the group or restarting the client. A console dump of a session it happened in ruled out everything it could and named nothing. What that dump established was almost entirely negative, and that was the useful part. No `Response timeout`, no `unsolicited`/`unrouted`/`with nothing waiting` — so the 2026-08-30 routing defect, which produces this exact symptom for thirty seconds, had not recurred. No `PC state: disconnected|failed`, no second ICE cycle, no `Reconnected after N attempt(s)` — so the connection was alive and untouched. The freeze was in the page, and no path that logs anything had run. The composer is `disabled=${sending || cannotSend}`, and `cannotSend` was `transport.connected && !transport.devicePk`, read off a **ref** during render. `devicePk` is settled inside connect(), so every reconnect clears it and settles it again; a ref changing re-renders nothing, and nothing else announced it. So the panel went disabled on whatever unrelated re-render came next — a message arriving — long after the identity was actually lost, and had no event that would open it again. group-page.js never touches `status` after 'connected', and `onReconnected` is claimed by video-player.js, so there was no second chance. It was silent as well as sticky. `_announceDevice` had three exits that wrote `devicePk` without a word: two early returns that left the *previous* connection's value standing, and a reply that is not `device_hello_ack` — an `error` reply does not throw, so the `.catch()` at the call site never saw it. Reproduced in chat_send_probe.py, which mounts the real ChatPanel over the real transport: with the old code, identity cleared leaves the composer open, an arriving message latches it shut, and restoring the identity does not reopen it. Every write to `devicePk` now goes through `_setDevicePk(pk, why)`, which logs, traces and calls `onDeviceIdentity`; group-page holds the answer as state and ChatPanel takes it as `deviceReady`. Defaulting that prop to `true` fails open — a wiring mistake here must not be able to leave anyone with a dead textbox. Two things found on the same path and fixed with it. `_send` throwing inside _sendAndWait's executor left the pending entry and its 30s timer behind, so a request that never reached the wire still logged a "Response timeout" half a minute later. And the instrumentation this was meant to be diagnosed with (3be8bd2) writes to localStorage behind ?trace=1, not to the console, so the dump could not have carried it: the two lines that decide the composer's state are now logged unconditionally, and MeshBayTrace gains `record` so the composer writes into the same timeline as the channel events. Hub suite 2264 passed, 4 skipped. chat_send_probe.py gains a `reconnect` scenario and test_chat_send.py four cases, each checked against the unfixed source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GXmScYB1uR29YCt74si9J
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/chat-app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/chat-app.js33
1 files changed, 29 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js b/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
index a793527..6e9de5f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js
@@ -203,9 +203,17 @@ function ChatImage({ filename, entries, transportRef, gekRef }) {
// read one answer. Empty means the group has no writable root right now — every
// root is read-only, or the one drive that was writable is unplugged — and the
// paperclip says so rather than producing a refusal from the node.
+//
+// `deviceReady` is "this connection has identified a device to the node", the
+// one thing chat needs beyond being connected. It arrives as a prop, and that
+// is the correction: it used to be read off the transport during render
+// (`transportRef.current.devicePk`), and a ref changing re-renders nothing — so
+// once a reconnect cleared it the composer stayed disabled for the rest of the
+// session. Defaulting to `true` fails open: a wiring mistake here must never be
+// able to leave someone with a dead textbox.
function ChatPanel({ transportRef, username, userId, entries, gekRef,
onRefreshIndex, onPreview, attachRoot = '', attachDir = '',
- onActivity, status }) {
+ onActivity, status, deviceReady = true }) {
const [messages, setMessages] = useState([]);
const [hasMore, setHasMore] = useState(false);
const [loadingOlder, setLoadingOlder] = useState(false);
@@ -519,9 +527,26 @@ function ChatPanel({ transportRef, username, userId, entries, gekRef,
// refuse a member claiming somebody else's key. Without it there is nothing
// to send with, so the composer says so before anything is typed rather than
// producing a refusal the reader cannot act on.
- const transportNow = transportRef.current;
- const cannotSend = !!(transportNow && transportNow.connected
- && !transportNow.devicePk);
+ //
+ // Only while connected: before that the composer is enabled and `sendMessage`
+ // simply declines, which is what it always did — saying "this device cannot
+ // post" at someone who is merely still connecting names the wrong problem.
+ const cannotSend = status === 'connected' && !deviceReady;
+
+ // The composer's disabled state has exactly two inputs, and neither of them
+ // was observable from outside the component. A "chat hangs, the textbox is
+ // not clickable" report arrived with a complete console dump that could not
+ // say which of the two it had been, nor when it started. This is what makes
+ // the next one answer that in one line.
+ useEffect(() => {
+ const why = sending ? 'send in flight'
+ : cannotSend ? 'device not identified to the node'
+ : null;
+ console.log('[MeshBay] chat composer:', why ? 'disabled (' + why + ')' : 'enabled');
+ if (window.MeshBayTrace && window.MeshBayTrace.record) {
+ window.MeshBayTrace.record('chat_composer', { disabled: !!why, why });
+ }
+ }, [sending, cannotSend]);
const onKeyDown = useCallback((e) => {
if (e.key === 'Enter' && !e.shiftKey) {