feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(web): queue messages while a chat is running (outbox parity with mobile) - #26

Merged
radroid merged 1 commit into
mainfrom
t3code/web-message-queue
Jul 27, 2026
Merged

feat(web): queue messages while a chat is running (outbox parity with mobile)#26
radroid merged 1 commit into
mainfrom
t3code/web-message-queue

Conversation

@radroid

Copy link
Copy Markdown
Owner

Closes#20.

Ports the mobile thread outbox to the web app. Submitting while a thread is busy or disconnected queues the message instead of erroring; a drain sends the queue FIFO once the thread is idle and connected.

What's included

  • Composer UX — the primary action becomes Queue when a submit would enqueue (thread busy, environment disconnected, or the outbox is already non-empty); Stop is demoted to a secondary control while running. Queued messages show above the composer with inline edit/remove.
  • Drain engine (useThreadOutboxDrain) — a single global in-flight lock, one send at a time, canSettle idle-gate (survives a just-adopted turn), settings-sync before start-turn, transient-vs-deterministic retry classification with backoff.
  • Persistence — one localStorage key per queued message ("skip invalid, never fatal"); all mutations serialized through a FIFO promise chain (storage-first, then atom).
  • Reuses the shared client-runtime thread-settled predicates and environment scoping — no forked logic.

Scope (matches mobile except)

  • Image attachments are not queued (blob previews can't survive a localStorage reload).
  • Offline new-thread creation is out of scope for v1 (existing-thread queuing only).

Pre-land adversarial review — fixed here

A 4-lens adversarial review + refutation pass found 6 findings (2 medium, 4 low). Fixed:

  • [medium] Removed-environment resurrection — removing an environment now purges its queued messages (clearEnvironment, wired into EnvironmentOwnedDataCleanup), so they can't linger and auto-send if the same server is re-added. Covered by a new regression test (incl. the persisted-only "zombie" case).
  • [medium] Image-only queue submit — no longer enqueues a bootstrap prompt that claims images are attached while carrying none (which also discarded the image); it warns and keeps the composer intact.
  • [low] Warning ordering — the "images aren't queued" toast now fires only after a successful enqueue.
  • [low] Drain heartbeat — while messages are queued, the drain re-evaluates the time-based idle-gate on a cadence so a head can't stall until an unrelated re-render.

Known v1 limitation (deferred)

  • No cross-tab reconciliation — two open tabs run independent drains over the same localStorage queue (no mobile parity source; narrow scenario). Documented for a follow-up.

Verification

  • tsgo --noEmit (web): 0 errors.
  • vp test run src/outbox: 19 passed.
  • Browser E2E is manual QA (self-tested by the author).

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35e665fb-6903-4ced-91d7-5cbbc28c6dfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/web-message-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroidforce-pushed the t3code/web-message-queue branch from 2bd9ee1 to bb89478CompareJuly 27, 2026 14:54
Port the mobile "thread outbox" to the web app: submitting while a thread
is busy or disconnected enqueues the message instead of erroring, and a
drain sends the queue FIFO once the thread is idle and connected. The
composer's primary action becomes "Queue" (Stop demoted to a secondary
control) while busy; queued messages are editable/removable above the
composer and persist across reloads via localStorage.
Scope matches mobile except: image attachments are not queued (blob
previews can't survive a localStorage reload) and offline new-thread
creation is out of scope for v1.
Includes fixes from an adversarial pre-land review:
- Purge an environment's queued messages when it is removed, so they
can't resurrect and auto-send if the same server is re-added (ports
clearEnvironment; wires it into EnvironmentOwnedDataCleanup).
- Never queue an image-only submit (it would enqueue a prompt claiming
images are attached while carrying none, and discard the image); warn
and keep the composer intact instead.
- Show the "images aren't queued" warning only after a successful
enqueue so a failed enqueue can't misinform.
- Heartbeat the drain while messages are queued so a head held back only
by the time-based idle-gate is delivered promptly.
Known v1 limitation: no cross-tab reconciliation — two open tabs run
independent drains over the same localStorage queue.
Closes#20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3code/web-message-queue branch from bb89478 to 3cced85CompareJuly 27, 2026 15:11
@radroid
radroid merged commit 72b2a16 into mainJul 27, 2026
2 of 3 checks passed
@radroid
radroid deleted the t3code/web-message-queue branch July 27, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): queue messages while a chat is running (outbox parity with mobile)

1 participant

@radroid