feat(ui): add stop/abort button to web remote control
When the agent is streaming, the send button becomes a red stop button
that sends a { type: "stop" } WebSocket message. The server handles this
by calling ctx.abort() to cancel the current agent operation.
This commit is contained in:
parent
4bc4cfa630
commit
37dc2b2f1e
|
|
@ -341,6 +341,11 @@ return /* html */ `<!DOCTYPE html>
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
#send-btn.ready:active { transform: scale(0.9); }
|
#send-btn.ready:active { transform: scale(0.9); }
|
||||||
|
#send-btn.stop {
|
||||||
|
background: var(--tool-err);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#send-btn.stop:active { transform: scale(0.9); }
|
||||||
#send-btn:disabled {
|
#send-btn:disabled {
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
@ -352,6 +357,15 @@ return /* html */ `<!DOCTYPE html>
|
||||||
height: 20px;
|
height: 20px;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
#send-btn .icon-stop {
|
||||||
|
display: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
#send-btn.stop svg { display: none; }
|
||||||
|
#send-btn.stop .icon-stop { display: block; }
|
||||||
|
|
||||||
#messages::-webkit-scrollbar { width: 6px; }
|
#messages::-webkit-scrollbar { width: 6px; }
|
||||||
#messages::-webkit-scrollbar-track { background: transparent; }
|
#messages::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
|
@ -369,7 +383,7 @@ return /* html */ `<!DOCTYPE html>
|
||||||
<div id="active-tools"></div>
|
<div id="active-tools"></div>
|
||||||
<div id="input-area">
|
<div id="input-area">
|
||||||
<textarea id="prompt" placeholder="Message\u2026" rows="1"></textarea>
|
<textarea id="prompt" placeholder="Message\u2026" rows="1"></textarea>
|
||||||
<button id="send-btn" disabled aria-label="Send"><svg viewBox="0 0 24 24"><path d="M3.4 20.4l17.45-7.48a1 1 0 000-1.84L3.4 3.6a.993.993 0 00-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z"/></svg></button>
|
<button id="send-btn" disabled aria-label="Send"><svg viewBox="0 0 24 24"><path d="M3.4 20.4l17.45-7.48a1 1 0 000-1.84L3.4 3.6a.993.993 0 00-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z"/></svg><div class="icon-stop"></div></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script nonce="${nonce}">
|
<script nonce="${nonce}">
|
||||||
|
|
@ -551,7 +565,26 @@ return /* html */ `<!DOCTYPE html>
|
||||||
? (S.streaming ? "Agent working\u2026" : "Connected")
|
? (S.streaming ? "Agent working\u2026" : "Connected")
|
||||||
: "Disconnected \u2014 reconnecting\u2026";
|
: "Disconnected \u2014 reconnecting\u2026";
|
||||||
$sendBtn.disabled = !connected;
|
$sendBtn.disabled = !connected;
|
||||||
if (!connected) $sendBtn.classList.remove("ready");
|
if (!connected) {
|
||||||
|
$sendBtn.classList.remove("ready");
|
||||||
|
$sendBtn.classList.remove("stop");
|
||||||
|
}
|
||||||
|
updateSendBtn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSendBtn() {
|
||||||
|
if (!ws || ws.readyState !== 1) return;
|
||||||
|
var hasText = $prompt.value.trim().length > 0;
|
||||||
|
if (S.streaming) {
|
||||||
|
$sendBtn.classList.add("stop");
|
||||||
|
$sendBtn.classList.remove("ready");
|
||||||
|
$sendBtn.disabled = false;
|
||||||
|
$sendBtn.setAttribute("aria-label", "Stop");
|
||||||
|
} else {
|
||||||
|
$sendBtn.classList.remove("stop");
|
||||||
|
$sendBtn.classList.toggle("ready", hasText);
|
||||||
|
$sendBtn.setAttribute("aria-label", "Send");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ws, timer;
|
var ws, timer;
|
||||||
|
|
@ -638,15 +671,18 @@ return /* html */ `<!DOCTYPE html>
|
||||||
function autoGrow() {
|
function autoGrow() {
|
||||||
$prompt.style.height = "auto";
|
$prompt.style.height = "auto";
|
||||||
$prompt.style.height = Math.min($prompt.scrollHeight, 120) + "px";
|
$prompt.style.height = Math.min($prompt.scrollHeight, 120) + "px";
|
||||||
// Toggle send button ready state
|
updateSendBtn();
|
||||||
var hasText = $prompt.value.trim().length > 0;
|
|
||||||
$sendBtn.classList.toggle("ready", hasText);
|
|
||||||
}
|
}
|
||||||
$prompt.addEventListener("input", autoGrow);
|
$prompt.addEventListener("input", autoGrow);
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
|
if (!ws || ws.readyState !== 1) return;
|
||||||
|
if (S.streaming) {
|
||||||
|
ws.send(JSON.stringify({ type: "stop" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
var text = $prompt.value.trim();
|
var text = $prompt.value.trim();
|
||||||
if (!text || !ws || ws.readyState !== 1) return;
|
if (!text) return;
|
||||||
ws.send(JSON.stringify({ type: "prompt", text: text }));
|
ws.send(JSON.stringify({ type: "prompt", text: text }));
|
||||||
$prompt.value = "";
|
$prompt.value = "";
|
||||||
autoGrow();
|
autoGrow();
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,12 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === "stop") {
|
||||||
|
if (!ctx.isIdle()) {
|
||||||
|
ctx.abort();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type === "prompt" && typeof msg.text === "string" && msg.text.trim()) {
|
if (msg.type === "prompt" && typeof msg.text === "string" && msg.text.trim()) {
|
||||||
const text = msg.text.trim();
|
const text = msg.text.trim();
|
||||||
// Sliding-window rate limit
|
// Sliding-window rate limit
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue