From 6a7a1d708525b8f734ee6925d3fc68d988d19c87 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 01:00:15 +0000 Subject: [PATCH] fix: add chat stop button Co-authored-by: Nathan Broadbent --- CHANGELOG.md | 1 + ui/src/ui/views/chat.test.ts | 96 ++++++++++++++++++++++++++++++++++++ ui/src/ui/views/chat.ts | 7 +-- 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 ui/src/ui/views/chat.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8809332bb..e4d31e635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.clawd.bot ### Fixes - BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing. - Web UI: hide internal `message_id` hints in chat bubbles. +- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent. - Heartbeat: normalize target identifiers for consistent routing. - TUI: reload history after gateway reconnect to restore session state. (#1663) - Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639) diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts new file mode 100644 index 000000000..6cd469558 --- /dev/null +++ b/ui/src/ui/views/chat.test.ts @@ -0,0 +1,96 @@ +import { render } from "lit"; +import { describe, expect, it, vi } from "vitest"; + +import type { SessionsListResult } from "../types"; +import { renderChat, type ChatProps } from "./chat"; + +function createSessions(): SessionsListResult { + return { + ts: 0, + path: "", + count: 0, + defaults: { model: null, contextTokens: null }, + sessions: [], + }; +} + +function createProps(overrides: Partial = {}): ChatProps { + return { + sessionKey: "main", + onSessionKeyChange: () => undefined, + thinkingLevel: null, + showThinking: false, + loading: false, + sending: false, + canAbort: false, + compactionStatus: null, + messages: [], + toolMessages: [], + stream: null, + streamStartedAt: null, + assistantAvatarUrl: null, + draft: "", + queue: [], + connected: true, + canSend: true, + disabledReason: null, + error: null, + sessions: createSessions(), + focusMode: false, + assistantName: "Clawdbot", + assistantAvatar: null, + onRefresh: () => undefined, + onToggleFocusMode: () => undefined, + onDraftChange: () => undefined, + onSend: () => undefined, + onQueueRemove: () => undefined, + onNewSession: () => undefined, + ...overrides, + }; +} + +describe("chat view", () => { + it("shows a stop button when aborting is available", () => { + const container = document.createElement("div"); + const onAbort = vi.fn(); + render( + renderChat( + createProps({ + canAbort: true, + onAbort, + }), + ), + container, + ); + + const stopButton = Array.from(container.querySelectorAll("button")).find( + (btn) => btn.textContent?.trim() === "Stop", + ); + expect(stopButton).not.toBeUndefined(); + stopButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onAbort).toHaveBeenCalledTimes(1); + expect(container.textContent).not.toContain("New session"); + }); + + it("shows a new session button when aborting is unavailable", () => { + const container = document.createElement("div"); + const onNewSession = vi.fn(); + render( + renderChat( + createProps({ + canAbort: false, + onNewSession, + }), + ), + container, + ); + + const newSessionButton = Array.from(container.querySelectorAll("button")).find( + (btn) => btn.textContent?.trim() === "New session", + ); + expect(newSessionButton).not.toBeUndefined(); + newSessionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onNewSession).toHaveBeenCalledTimes(1); + expect(container.textContent).not.toContain("Stop"); + }); +}); diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index 9bae523ed..677c2a183 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -97,6 +97,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un export function renderChat(props: ChatProps) { const canCompose = props.connected; const isBusy = props.sending || props.stream !== null; + const canAbort = Boolean(props.canAbort && props.onAbort); const activeSession = props.sessions?.sessions?.find( (row) => row.key === props.sessionKey, ); @@ -254,10 +255,10 @@ export function renderChat(props: ChatProps) {