diff --git a/VERSION b/VERSION
index d788d43..78bae5b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.0.31
+0.0.32
diff --git a/src/version.h b/src/version.h
index cdc4b24..aa18dbb 100644
--- a/src/version.h
+++ b/src/version.h
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
-#define SB_VERSION "v0.0.31"
+#define SB_VERSION "v0.0.32"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
-#define SB_VERSION_PATCH 31
+#define SB_VERSION_PATCH 32
#endif /* SOVEREIGN_BROWSER_VERSION_H */
diff --git a/www/agents/chat.css b/www/agents/chat.css
index 9bb3273..86bcfa1 100644
--- a/www/agents/chat.css
+++ b/www/agents/chat.css
@@ -378,10 +378,6 @@ html.dark .skill-badge-unsaved {
#composer-host {
flex: 1; min-width: 0;
}
-#stop-btn {
- flex-shrink: 0; min-width: 80px; margin-left: 0;
-}
-
/* The shared post-composer library wraps the host element: it inserts a
* .post-composer-wrapper around #composer-host and appends the send
* button as a SIBLING of the host (inside the wrapper). So the send
@@ -408,6 +404,20 @@ html.dark .skill-badge-unsaved {
#input-area .post-composer-send-btn:disabled {
opacity: 0.5; cursor: not-allowed;
}
+/* Stop-mode: the send button transforms into a stop button while the
+ * agent is running. Use a distinct (red-ish) style to signal that
+ * clicking it will cancel the agent. */
+#input-area .post-composer-send-btn.is-stop-mode {
+ border-color: var(--danger, #c33);
+ color: var(--danger, #c33);
+ background: transparent;
+}
+#input-area .post-composer-send-btn.is-stop-mode:hover,
+#input-area .post-composer-send-btn.is-stop-mode:focus {
+ background: var(--danger, #c33);
+ color: #fff;
+ border-color: var(--danger, #c33);
+}
/* ── Responsive: narrow sidebar viewport (< 400px) ──────────────
* When the chat page is loaded in the agent sidebar (~350px wide),
diff --git a/www/agents/chat.html b/www/agents/chat.html
index 1a84b8e..c40ad25 100644
--- a/www/agents/chat.html
+++ b/www/agents/chat.html
@@ -26,7 +26,6 @@
diff --git a/www/agents/chat.js b/www/agents/chat.js
index 9f59faa..af0b802 100644
--- a/www/agents/chat.js
+++ b/www/agents/chat.js
@@ -467,7 +467,6 @@ function clearStatusMessage() {
* fallback. Posts status messages as system bubbles in the chat
* window instead of updating a status bar. */
function applyStatus(s) {
- var stopBtn = document.getElementById('stop-btn');
var map = {idle:'Idle', thinking:'Thinking...',
tool_call:'Calling tool', complete:'Complete',
error:'Error', cancelled:'Cancelled'};
@@ -492,13 +491,24 @@ function applyStatus(s) {
}
var running = (s.state === 'thinking' || s.state === 'tool_call');
- if (stopBtn) stopBtn.style.display = running ? '' : 'none';
- if (composerApi && typeof composerApi.setDisabled === 'function') {
- /* Disable while running OR when no chat is selected. The composer
- * must never be enabled if there is no active conversation. */
+ /* Convert the send button into a stop button while the agent is
+ * running, instead of showing a separate stop button. The user can't
+ * send again anyway while the agent is working. */
+ if (composerApi) {
var hasChat = (currentConvId !== null && currentConvId !== undefined &&
String(currentConvId) !== '');
- composerApi.setDisabled(running || !hasChat);
+ if (running) {
+ if (typeof composerApi.setStopMode === 'function') {
+ composerApi.setStopMode(cancelAgent);
+ }
+ } else {
+ if (typeof composerApi.clearStopMode === 'function') {
+ composerApi.clearStopMode();
+ }
+ if (typeof composerApi.setDisabled === 'function') {
+ composerApi.setDisabled(!hasChat);
+ }
+ }
}
/* On transition to complete/error/cancelled, fetch final
diff --git a/www/js/post-composer.js b/www/js/post-composer.js
index 2a621df..8ca3917 100644
--- a/www/js/post-composer.js
+++ b/www/js/post-composer.js
@@ -264,6 +264,15 @@ window.mountComposer = function mountComposer(hostEl, options = {}) {
sendButton.textContent = 'Send';
sendButton.disabled = true;
+ /* Stop-mode state: when active, the send button transforms into a
+ * stop button (different label, click handler, and styling). This is
+ * used by the agent chat to let the user cancel an in-progress agent
+ * run without needing a separate stop button. */
+ let stopModeActive = false;
+ let stopModeHandler = null;
+ const sendLabel = 'Send';
+ const stopLabel = 'Stop';
+
if (layout === 'inline') {
wrapper.classList.add('post-composer-wrapper--inline');
if (showPreview) wrapper.appendChild(previewEl);
@@ -733,6 +742,11 @@ window.mountComposer = function mountComposer(hostEl, options = {}) {
}
async function onSendClick() {
+ /* In stop mode, the button acts as Stop — route to the stop handler. */
+ if (stopModeActive) {
+ if (stopModeHandler) stopModeHandler();
+ return;
+ }
if (sendButton.disabled) return;
if (!onSubmit) return;
@@ -827,6 +841,27 @@ window.mountComposer = function mountComposer(hostEl, options = {}) {
externallyDisabled = !!disabled;
updateSendButtonVisibility();
},
+ /* Convert the send button into a stop button. The provided handler
+ * is called when the user clicks the (now stop) button. The text
+ * area is NOT disabled — the user can type their next message while
+ * the agent runs, but the button acts as Stop until clearStopMode()
+ * is called. */
+ setStopMode(handler) {
+ stopModeActive = true;
+ stopModeHandler = typeof handler === 'function' ? handler : null;
+ sendButton.textContent = stopLabel;
+ sendButton.classList.add('is-stop-mode');
+ sendButton.disabled = false;
+ sendButton.classList.add('is-visible');
+ },
+ /* Revert the stop button back to a send button. */
+ clearStopMode() {
+ stopModeActive = false;
+ stopModeHandler = null;
+ sendButton.textContent = sendLabel;
+ sendButton.classList.remove('is-stop-mode');
+ updateSendButtonVisibility();
+ },
show() {
showComposer();
},
diff --git a/www/js/post-composer.mjs b/www/js/post-composer.mjs
index 60048ee..ece2498 100644
--- a/www/js/post-composer.mjs
+++ b/www/js/post-composer.mjs
@@ -264,6 +264,15 @@ export function mountComposer(hostEl, options = {}) {
sendButton.textContent = 'Send';
sendButton.disabled = true;
+ /* Stop-mode state: when active, the send button transforms into a
+ * stop button (different label, click handler, and styling). This is
+ * used by the agent chat to let the user cancel an in-progress agent
+ * run without needing a separate stop button. */
+ let stopModeActive = false;
+ let stopModeHandler = null;
+ const sendLabel = 'Send';
+ const stopLabel = 'Stop';
+
if (layout === 'inline') {
wrapper.classList.add('post-composer-wrapper--inline');
if (showPreview) wrapper.appendChild(previewEl);
@@ -733,6 +742,11 @@ export function mountComposer(hostEl, options = {}) {
}
async function onSendClick() {
+ /* In stop mode, the button acts as Stop — route to the stop handler. */
+ if (stopModeActive) {
+ if (stopModeHandler) stopModeHandler();
+ return;
+ }
if (sendButton.disabled) return;
if (!onSubmit) return;
@@ -827,6 +841,21 @@ export function mountComposer(hostEl, options = {}) {
externallyDisabled = !!disabled;
updateSendButtonVisibility();
},
+ setStopMode(handler) {
+ stopModeActive = true;
+ stopModeHandler = typeof handler === 'function' ? handler : null;
+ sendButton.textContent = stopLabel;
+ sendButton.classList.add('is-stop-mode');
+ sendButton.disabled = false;
+ sendButton.classList.add('is-visible');
+ },
+ clearStopMode() {
+ stopModeActive = false;
+ stopModeHandler = null;
+ sendButton.textContent = sendLabel;
+ sendButton.classList.remove('is-stop-mode');
+ updateSendButtonVisibility();
+ },
show() {
showComposer();
},