fix: add chat stop button
Co-authored-by: Nathan Broadbent <ndbroadbent@users.noreply.github.com>
This commit is contained in:
parent
92e794dc18
commit
6a7a1d7085
@ -18,6 +18,7 @@ Docs: https://docs.clawd.bot
|
|||||||
### Fixes
|
### Fixes
|
||||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
- 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: 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.
|
- Heartbeat: normalize target identifiers for consistent routing.
|
||||||
- TUI: reload history after gateway reconnect to restore session state. (#1663)
|
- 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)
|
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
||||||
|
|||||||
96
ui/src/ui/views/chat.test.ts
Normal file
96
ui/src/ui/views/chat.test.ts
Normal file
@ -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> = {}): 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -97,6 +97,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
|||||||
export function renderChat(props: ChatProps) {
|
export function renderChat(props: ChatProps) {
|
||||||
const canCompose = props.connected;
|
const canCompose = props.connected;
|
||||||
const isBusy = props.sending || props.stream !== null;
|
const isBusy = props.sending || props.stream !== null;
|
||||||
|
const canAbort = Boolean(props.canAbort && props.onAbort);
|
||||||
const activeSession = props.sessions?.sessions?.find(
|
const activeSession = props.sessions?.sessions?.find(
|
||||||
(row) => row.key === props.sessionKey,
|
(row) => row.key === props.sessionKey,
|
||||||
);
|
);
|
||||||
@ -254,10 +255,10 @@ export function renderChat(props: ChatProps) {
|
|||||||
<div class="chat-compose__actions">
|
<div class="chat-compose__actions">
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
?disabled=${!props.connected || props.sending}
|
?disabled=${!props.connected || (!canAbort && props.sending)}
|
||||||
@click=${props.onNewSession}
|
@click=${canAbort ? props.onAbort : props.onNewSession}
|
||||||
>
|
>
|
||||||
New session
|
${canAbort ? "Stop" : "New session"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn primary"
|
class="btn primary"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user