Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions renderer/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,23 @@ html, body {

.send-btn:active, .ptt-btn:active { transform: translateY(0); }
.send-btn:disabled { background: var(--text-muted); cursor: not-allowed; opacity: 0.5; }
.send-btn.hidden { display: none !important; }

.stop-btn {
width: 42px; height: 42px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 2px;
color: #ef4444;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
transition: all 0.15s;
flex-shrink: 0;
}
.stop-btn:hover { background: rgba(239, 68, 68, 0.15); border-color: #ef4444; }
.stop-btn:active { transform: translateY(0); }
.stop-btn.hidden { display: none !important; }

/* ── Bottom toolbar ──────────────────────────────────────────────────────── */
.ptt-btn .waveform {
Expand Down
1 change: 1 addition & 0 deletions renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
</span>
</button>
<button class="send-btn" id="send-btn" title="Send">➤</button>
<button class="stop-btn hidden" id="stop-btn" title="Stop">⏹</button>
</div>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions renderer/js/panel/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export function createPanelController({
}

function bindEvents() {
dom.textInput.addEventListener("focus", () => {
if (state.isStreaming()) {
messaging.cancelMessage();
}
});

dom.textInput.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
Expand All @@ -86,6 +92,10 @@ export function createPanelController({
});

dom.sendBtn.addEventListener("click", messaging.sendMessage);
const stopBtn = doc.getElementById("stop-btn");
if (stopBtn) {
stopBtn.addEventListener("click", messaging.cancelMessage);
}
dom.btnPlanPrev.addEventListener("click", () => api.invoke("previous-step"));
dom.btnPlanDone.addEventListener("click", () => api.invoke("mark-step-done"));
dom.btnPlanSkip.addEventListener("click", () => api.invoke("skip-current-step"));
Expand Down
63 changes: 61 additions & 2 deletions renderer/js/panel/messaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ export function createMessagingController({
ui,
}) {
let syncQueue = Promise.resolve();
let currentAbortController = null;
let requestTimeoutId = null;

function cancelMessage() {
if (!state.isStreaming()) return;

if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}

if (requestTimeoutId) {
clearTimeout(requestTimeoutId);
requestTimeoutId = null;
}

log("ai:cancel-message invoke");
api.invoke("abort-message").catch(err => log("ipc:abort-message error", err));

state.setStreaming(false);
dom.sendBtn.classList.remove("hidden");
const stopBtn = doc.getElementById("stop-btn");
if (stopBtn) stopBtn.classList.add("hidden");
dom.sendBtn.disabled = false;

ui.renderAgentState("idle");
ui.removeAllTypingIndicators();

if (state.getStreamingBubble()) {
state.clearStreamingSession();
}
}

function syncSession(session) {
syncQueue = syncQueue.then(() => syncSessionInternal(session)).catch((error) => {
Expand Down Expand Up @@ -93,9 +125,23 @@ export function createMessagingController({
dom.textInput.value = "";
dom.textInput.style.height = "auto";
state.setStreaming(true);

dom.sendBtn.classList.add("hidden");
const stopBtn = doc.getElementById("stop-btn");
if (stopBtn) stopBtn.classList.remove("hidden");
dom.sendBtn.disabled = true;

ui.renderAgentState("thinking");

currentAbortController = new AbortController();
requestTimeoutId = window.setTimeout(() => {
if (state.isStreaming()) {
log("ai:send-message timeout 60s triggered");
cancelMessage();
ui.showToast("Request timed out", true);
}
}, 60000);

let typingId = null;
log("ai:send-message start", {
hasImages: Boolean(images && images.length),
Expand Down Expand Up @@ -128,8 +174,6 @@ export function createMessagingController({
} catch (error) {
onAIError(error.message);
} finally {
state.setStreaming(false);
dom.sendBtn.disabled = false;
if (typingId !== null) {
ui.removeTypingIndicator(typingId);
}
Expand All @@ -151,7 +195,14 @@ export function createMessagingController({

function onAIDone(parsed) {
const result = parsed || {};
if (requestTimeoutId) {
clearTimeout(requestTimeoutId);
requestTimeoutId = null;
}
state.setStreaming(false);
dom.sendBtn.classList.remove("hidden");
const stopBtn = doc.getElementById("stop-btn");
if (stopBtn) stopBtn.classList.add("hidden");
dom.sendBtn.disabled = false;
ui.renderAgentState("idle");

Expand Down Expand Up @@ -189,7 +240,14 @@ export function createMessagingController({
? { message: errorMessage, code: "unknown_error", action: "open-settings", requestId: "" }
: (errorMessage || {});
const safeMessage = payload.message || "Unexpected error";
if (requestTimeoutId) {
clearTimeout(requestTimeoutId);
requestTimeoutId = null;
}
state.setStreaming(false);
dom.sendBtn.classList.remove("hidden");
const stopBtn = doc.getElementById("stop-btn");
if (stopBtn) stopBtn.classList.add("hidden");
dom.sendBtn.disabled = false;
ui.renderAgentState("idle");
ui.removeAllTypingIndicators();
Expand Down Expand Up @@ -222,5 +280,6 @@ export function createMessagingController({
onAIDone,
onAIError,
sendMessage,
cancelMessage,
};
}
Loading