mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
feat(ui): steer queued chat messages
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Control UI/chat: add a Steer action on queued messages so a browser follow-up can be injected into the active run without retyping it.
|
||||
- Agents/tools: add optional per-call `timeoutMs` support for image, video, music, and TTS generation tools so agents can extend provider request timeouts only when a specific generation needs it.
|
||||
- Agents/subagents: add optional forked context for native `sessions_spawn` runs so agents can let a child inherit the requester transcript when needed, while keeping clean isolated sessions as the default; includes prompt guidance, context-engine hook metadata, docs, and QA coverage.
|
||||
- Codex harness: add structured debug logging for embedded harness selection decisions so `/status` stays simple while gateway logs explain auto-selection and Pi fallback reasons. (#70760) Thanks @100yenadmin.
|
||||
|
||||
@@ -153,6 +153,7 @@ Cron jobs panel notes:
|
||||
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
|
||||
- Stop:
|
||||
- Click **Stop** (calls `chat.abort`)
|
||||
- While a run is active, normal follow-ups queue. Click **Steer** on a queued message to inject that follow-up into the running turn.
|
||||
- Type `/stop` (or standalone abort phrases like `stop`, `stop action`, `stop run`, `stop openclaw`, `please stop`) to abort out-of-band
|
||||
- `chat.abort` supports `{ sessionKey }` (no `runId`) to abort all active runs for that session
|
||||
- Abort partial retention:
|
||||
|
||||
@@ -646,6 +646,59 @@
|
||||
background: color-mix(in srgb, var(--danger) 85%, #fff);
|
||||
}
|
||||
|
||||
.chat-queue__item--steered {
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
|
||||
}
|
||||
|
||||
.chat-queue__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-queue__actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chat-queue__steer {
|
||||
align-self: start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chat-queue__steer svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.chat-queue__steer:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.chat-queue__badge {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin-bottom: 6px;
|
||||
flex-shrink: 0;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
color: var(--accent);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
|
||||
19
ui/src/styles/chat/layout.test.ts
Normal file
19
ui/src/styles/chat/layout.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("chat steer styles", () => {
|
||||
it("styles queued-message steering controls and pending indicators", () => {
|
||||
const cssPath = [
|
||||
resolve(process.cwd(), "src/styles/chat/layout.css"),
|
||||
resolve(process.cwd(), "ui/src/styles/chat/layout.css"),
|
||||
].find((candidate) => existsSync(candidate));
|
||||
expect(cssPath).toBeTruthy();
|
||||
const css = readFileSync(cssPath!, "utf8");
|
||||
|
||||
expect(css).toContain(".chat-queue__steer");
|
||||
expect(css).toContain(".chat-queue__actions");
|
||||
expect(css).toContain(".chat-queue__item--steered");
|
||||
expect(css).toContain(".chat-queue__badge");
|
||||
});
|
||||
});
|
||||
@@ -13,13 +13,19 @@ vi.mock("./app-last-active-session.ts", () => ({
|
||||
}));
|
||||
|
||||
let handleSendChat: typeof import("./app-chat.ts").handleSendChat;
|
||||
let steerQueuedChatMessage: typeof import("./app-chat.ts").steerQueuedChatMessage;
|
||||
let handleAbortChat: typeof import("./app-chat.ts").handleAbortChat;
|
||||
let refreshChatAvatar: typeof import("./app-chat.ts").refreshChatAvatar;
|
||||
let clearPendingQueueItemsForRun: typeof import("./app-chat.ts").clearPendingQueueItemsForRun;
|
||||
|
||||
async function loadChatHelpers(): Promise<void> {
|
||||
({ handleSendChat, handleAbortChat, refreshChatAvatar, clearPendingQueueItemsForRun } =
|
||||
await import("./app-chat.ts"));
|
||||
({
|
||||
handleSendChat,
|
||||
steerQueuedChatMessage,
|
||||
handleAbortChat,
|
||||
refreshChatAvatar,
|
||||
clearPendingQueueItemsForRun,
|
||||
} = await import("./app-chat.ts"));
|
||||
}
|
||||
|
||||
function requestUrl(input: string | URL | Request): string {
|
||||
@@ -514,6 +520,42 @@ describe("handleSendChat", () => {
|
||||
expect(host.chatQueue).toEqual([
|
||||
expect.objectContaining({
|
||||
text: "/steer tighten the plan",
|
||||
kind: "steered",
|
||||
pendingRunId: "run-1",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("steers a queued message into the active run without replacing run tracking", async () => {
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
return { status: "started", runId: "steer-run" };
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatRunId: "run-1",
|
||||
chatStream: "Working...",
|
||||
chatQueue: [{ id: "queued-1", text: "tighten the plan", createdAt: 1 }],
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
await steerQueuedChatMessage(host, "queued-1");
|
||||
|
||||
expect(request).toHaveBeenCalledWith("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "tighten the plan",
|
||||
deliver: false,
|
||||
idempotencyKey: expect.any(String),
|
||||
attachments: undefined,
|
||||
});
|
||||
expect(host.chatRunId).toBe("run-1");
|
||||
expect(host.chatStream).toBe("Working...");
|
||||
expect(host.chatQueue).toEqual([
|
||||
expect.objectContaining({
|
||||
text: "tighten the plan",
|
||||
kind: "steered",
|
||||
pendingRunId: "run-1",
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
loadChatHistory,
|
||||
sendChatMessage,
|
||||
sendDetachedChatMessage,
|
||||
sendSteerChatMessage,
|
||||
type ChatState,
|
||||
} from "./controllers/chat.ts";
|
||||
import { loadModels } from "./controllers/models.ts";
|
||||
@@ -134,9 +135,15 @@ function enqueueChatMessage(
|
||||
];
|
||||
}
|
||||
|
||||
function enqueuePendingRunMessage(host: ChatHost, text: string, pendingRunId: string) {
|
||||
function enqueuePendingRunMessage(
|
||||
host: ChatHost,
|
||||
text: string,
|
||||
pendingRunId: string,
|
||||
attachments?: ChatAttachment[],
|
||||
) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
const hasAttachments = Boolean(attachments && attachments.length > 0);
|
||||
if (!trimmed && !hasAttachments) {
|
||||
return;
|
||||
}
|
||||
host.chatQueue = [
|
||||
@@ -145,6 +152,8 @@ function enqueuePendingRunMessage(host: ChatHost, text: string, pendingRunId: st
|
||||
id: generateUUID(),
|
||||
text: trimmed,
|
||||
createdAt: Date.now(),
|
||||
kind: "steered",
|
||||
attachments: hasAttachments ? attachments?.map((att) => ({ ...att })) : undefined,
|
||||
pendingRunId,
|
||||
},
|
||||
];
|
||||
@@ -226,6 +235,43 @@ async function sendDetachedBtwMessage(
|
||||
return ok;
|
||||
}
|
||||
|
||||
export async function steerQueuedChatMessage(host: ChatHost, id: string) {
|
||||
if (!host.connected || !host.chatRunId) {
|
||||
return;
|
||||
}
|
||||
const activeRunId = host.chatRunId;
|
||||
const item = host.chatQueue.find(
|
||||
(entry) => entry.id === id && !entry.pendingRunId && !entry.localCommandName,
|
||||
);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const message = item.text.trim();
|
||||
const attachments = item.attachments ?? [];
|
||||
const hasAttachments = attachments.length > 0;
|
||||
if (!message && !hasAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
host.chatQueue = host.chatQueue.map((entry) =>
|
||||
entry.id === id ? { ...entry, kind: "steered", pendingRunId: activeRunId } : entry,
|
||||
);
|
||||
const runId = await sendSteerChatMessage(
|
||||
host as unknown as ChatState,
|
||||
message,
|
||||
hasAttachments ? attachments : undefined,
|
||||
);
|
||||
if (!runId) {
|
||||
host.chatQueue = host.chatQueue.map((entry) => (entry.id === id ? item : entry));
|
||||
return;
|
||||
}
|
||||
setLastActiveSessionKey(
|
||||
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
|
||||
host.sessionKey,
|
||||
);
|
||||
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
|
||||
}
|
||||
|
||||
async function flushChatQueue(host: ChatHost) {
|
||||
if (!host.connected || isChatBusy(host)) {
|
||||
return;
|
||||
|
||||
@@ -2259,6 +2259,7 @@ export function renderApp(state: AppViewState) {
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onQueueSteer: (id) => void state.steerQueuedChatMessage(id),
|
||||
onDismissSideResult: () => {
|
||||
state.chatSideResult = null;
|
||||
},
|
||||
|
||||
@@ -425,6 +425,7 @@ export type AppViewState = {
|
||||
setPassword: (next: string) => void;
|
||||
setChatMessage: (next: string) => void;
|
||||
handleSendChat: (messageOverride?: string, opts?: { restoreDraft?: boolean }) => Promise<void>;
|
||||
steerQueuedChatMessage: (id: string) => Promise<void>;
|
||||
handleAbortChat: () => Promise<void>;
|
||||
removeQueuedMessage: (id: string) => void;
|
||||
handleChatScroll: (event: Event) => void;
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
handleAbortChat as handleAbortChatInternal,
|
||||
handleSendChat as handleSendChatInternal,
|
||||
removeQueuedMessage as removeQueuedMessageInternal,
|
||||
steerQueuedChatMessage as steerQueuedChatMessageInternal,
|
||||
} from "./app-chat.ts";
|
||||
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults.ts";
|
||||
import type { EventLogEntry } from "./app-events.ts";
|
||||
@@ -709,6 +710,13 @@ export class OpenClawApp extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
async steerQueuedChatMessage(id: string) {
|
||||
await steerQueuedChatMessageInternal(
|
||||
this as unknown as Parameters<typeof steerQueuedChatMessageInternal>[0],
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
async handleWhatsAppStart(force: boolean) {
|
||||
await handleWhatsAppStartInternal(this, force);
|
||||
}
|
||||
|
||||
@@ -25,18 +25,26 @@ describe("chat run controls", () => {
|
||||
it("switches between idle and abort actions", () => {
|
||||
const container = document.createElement("div");
|
||||
const onAbort = vi.fn();
|
||||
const onQueueSend = vi.fn();
|
||||
const onQueueStoreDraft = vi.fn();
|
||||
render(
|
||||
renderChatRunControls(
|
||||
createProps({
|
||||
canAbort: true,
|
||||
draft: " queue this ",
|
||||
sending: true,
|
||||
onAbort,
|
||||
onSend: onQueueSend,
|
||||
onStoreDraft: onQueueStoreDraft,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const queueButton = container.querySelector<HTMLButtonElement>('button[title="Queue"]');
|
||||
const stopButton = container.querySelector<HTMLButtonElement>('button[title="Stop"]');
|
||||
expect(queueButton).not.toBeNull();
|
||||
expect(queueButton?.disabled).toBe(true);
|
||||
expect(stopButton).not.toBeNull();
|
||||
stopButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onAbort).toHaveBeenCalledTimes(1);
|
||||
@@ -73,6 +81,30 @@ describe("chat run controls", () => {
|
||||
expect(container.textContent).not.toContain("Stop");
|
||||
});
|
||||
|
||||
it("queues draft text while an active run is abortable", () => {
|
||||
const container = document.createElement("div");
|
||||
const onSend = vi.fn();
|
||||
const onStoreDraft = vi.fn();
|
||||
render(
|
||||
renderChatRunControls(
|
||||
createProps({
|
||||
canAbort: true,
|
||||
draft: " follow up ",
|
||||
onSend,
|
||||
onStoreDraft,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const queueButton = container.querySelector<HTMLButtonElement>('button[title="Queue"]');
|
||||
expect(queueButton).not.toBeNull();
|
||||
expect(queueButton?.disabled).toBe(false);
|
||||
queueButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onStoreDraft).toHaveBeenCalledWith(" follow up ");
|
||||
expect(onSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps Stop clickable while disconnected when a run is abortable", () => {
|
||||
const container = document.createElement("div");
|
||||
const onAbort = vi.fn();
|
||||
|
||||
@@ -42,6 +42,20 @@ export function renderChatRunControls(props: ChatRunControlsProps) {
|
||||
|
||||
${props.canAbort
|
||||
? html`
|
||||
<button
|
||||
class="chat-send-btn"
|
||||
@click=${() => {
|
||||
if (props.draft.trim()) {
|
||||
props.onStoreDraft(props.draft);
|
||||
}
|
||||
props.onSend();
|
||||
}}
|
||||
?disabled=${!props.connected || props.sending}
|
||||
title="Queue"
|
||||
aria-label="Queue message"
|
||||
>
|
||||
${icons.send}
|
||||
</button>
|
||||
<button
|
||||
class="chat-send-btn chat-send-btn--stop"
|
||||
@click=${props.onAbort}
|
||||
|
||||
@@ -385,6 +385,30 @@ export async function sendDetachedChatMessage(
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendSteerChatMessage(
|
||||
state: ChatState,
|
||||
message: string,
|
||||
attachments?: ChatAttachment[],
|
||||
): Promise<string | null> {
|
||||
if (!state.client || !state.connected) {
|
||||
return null;
|
||||
}
|
||||
const msg = message.trim();
|
||||
const hasAttachments = attachments && attachments.length > 0;
|
||||
if (!msg && !hasAttachments) {
|
||||
return null;
|
||||
}
|
||||
state.lastError = null;
|
||||
const runId = generateUUID();
|
||||
try {
|
||||
await requestChatSend(state, { message: msg, attachments, runId });
|
||||
return runId;
|
||||
} catch (err) {
|
||||
state.lastError = formatConnectError(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function abortChatRun(state: ChatState): Promise<boolean> {
|
||||
if (!state.client || !state.connected) {
|
||||
return false;
|
||||
|
||||
@@ -128,6 +128,12 @@ export const icons = {
|
||||
<path d="m19 12-7 7-7-7" />
|
||||
</svg>
|
||||
`,
|
||||
cornerDownRight: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<polyline points="15 10 20 15 15 20" />
|
||||
<path d="M4 4v7a4 4 0 0 0 4 4h12" />
|
||||
</svg>
|
||||
`,
|
||||
copy: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ChatQueueItem = {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: number;
|
||||
kind?: "queued" | "steered";
|
||||
attachments?: ChatAttachment[];
|
||||
refreshSessions?: boolean;
|
||||
localCommandArgs?: string;
|
||||
|
||||
107
ui/src/ui/views/chat.test.ts
Normal file
107
ui/src/ui/views/chat.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChatQueueItem } from "../ui-types.ts";
|
||||
import { cleanupChatModuleState, renderChat, type ChatProps } from "./chat.ts";
|
||||
|
||||
function createProps(overrides: Partial<ChatProps> = {}): ChatProps {
|
||||
return {
|
||||
sessionKey: "agent:main:main",
|
||||
onSessionKeyChange: () => undefined,
|
||||
thinkingLevel: null,
|
||||
showThinking: true,
|
||||
showToolCalls: true,
|
||||
loading: false,
|
||||
sending: false,
|
||||
canAbort: true,
|
||||
messages: [],
|
||||
toolMessages: [],
|
||||
streamSegments: [],
|
||||
stream: "Working...",
|
||||
streamStartedAt: 1,
|
||||
draft: "",
|
||||
queue: [],
|
||||
connected: true,
|
||||
canSend: true,
|
||||
disabledReason: null,
|
||||
error: null,
|
||||
sessions: {
|
||||
ts: 0,
|
||||
path: "",
|
||||
count: 1,
|
||||
defaults: { modelProvider: null, model: null, contextTokens: null },
|
||||
sessions: [{ key: "agent:main:main", kind: "direct", status: "running", updatedAt: null }],
|
||||
},
|
||||
focusMode: false,
|
||||
assistantName: "Test Agent",
|
||||
assistantAvatar: null,
|
||||
onRefresh: () => undefined,
|
||||
onToggleFocusMode: () => undefined,
|
||||
onDraftChange: () => undefined,
|
||||
onSend: () => undefined,
|
||||
onAbort: () => undefined,
|
||||
onQueueRemove: () => undefined,
|
||||
onNewSession: () => undefined,
|
||||
agentsList: { agents: [{ id: "main", name: "Main" }], defaultId: "main" },
|
||||
currentAgentId: "main",
|
||||
onAgentChange: () => undefined,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderQueue(queue: ChatQueueItem[], onQueueSteer = vi.fn()) {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat(
|
||||
createProps({
|
||||
queue,
|
||||
onQueueSteer,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
return { container, onQueueSteer };
|
||||
}
|
||||
|
||||
describe("chat view queue steering", () => {
|
||||
afterEach(() => {
|
||||
cleanupChatModuleState();
|
||||
});
|
||||
|
||||
it("renders Steer only for queued messages during an active run", () => {
|
||||
const { container, onQueueSteer } = renderQueue([
|
||||
{ id: "queued-1", text: "tighten the plan", createdAt: 1 },
|
||||
{ id: "steered-1", text: "already sent", createdAt: 2, kind: "steered" },
|
||||
{ id: "local-1", text: "/status", createdAt: 3, localCommandName: "status" },
|
||||
]);
|
||||
|
||||
const steerButtons = container.querySelectorAll<HTMLButtonElement>(".chat-queue__steer");
|
||||
expect(steerButtons).toHaveLength(1);
|
||||
expect(steerButtons[0].textContent?.trim()).toBe("Steer");
|
||||
expect(container.querySelector(".chat-queue__badge")?.textContent?.trim()).toBe("Steered");
|
||||
|
||||
steerButtons[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(onQueueSteer).toHaveBeenCalledWith("queued-1");
|
||||
});
|
||||
|
||||
it("hides queued-message Steer when no run is active", () => {
|
||||
const { container } = renderQueue(
|
||||
[{ id: "queued-1", text: "tighten the plan", createdAt: 1 }],
|
||||
vi.fn(),
|
||||
);
|
||||
render(
|
||||
renderChat(
|
||||
createProps({
|
||||
canAbort: false,
|
||||
stream: null,
|
||||
queue: [{ id: "queued-1", text: "tighten the plan", createdAt: 1 }],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".chat-queue__steer")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -97,6 +97,7 @@ export type ChatProps = {
|
||||
onSend: () => void;
|
||||
onAbort?: () => void;
|
||||
onQueueRemove: (id: string) => void;
|
||||
onQueueSteer?: (id: string) => void;
|
||||
onDismissSideResult?: () => void;
|
||||
onNewSession: () => void;
|
||||
onClearHistory?: () => void;
|
||||
@@ -1131,19 +1132,47 @@ export function renderChat(props: ChatProps) {
|
||||
<div class="chat-queue__list">
|
||||
${props.queue.map(
|
||||
(item) => html`
|
||||
<div class="chat-queue__item">
|
||||
<div class="chat-queue__text">
|
||||
${item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")}
|
||||
<div
|
||||
class="chat-queue__item ${item.kind === "steered"
|
||||
? "chat-queue__item--steered"
|
||||
: ""}"
|
||||
>
|
||||
<div class="chat-queue__main">
|
||||
${item.kind === "steered"
|
||||
? html`<span class="chat-queue__badge">Steered</span>`
|
||||
: nothing}
|
||||
<div class="chat-queue__text">
|
||||
${item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-queue__actions">
|
||||
${props.canAbort &&
|
||||
props.onQueueSteer &&
|
||||
item.kind !== "steered" &&
|
||||
!item.localCommandName
|
||||
? html`
|
||||
<button
|
||||
class="btn chat-queue__steer"
|
||||
type="button"
|
||||
title="Steer now"
|
||||
aria-label="Steer queued message"
|
||||
@click=${() => props.onQueueSteer?.(item.id)}
|
||||
>
|
||||
${icons.cornerDownRight}
|
||||
<span>Steer</span>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
<button
|
||||
class="btn chat-queue__remove"
|
||||
type="button"
|
||||
aria-label="Remove queued message"
|
||||
@click=${() => props.onQueueRemove(item.id)}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn chat-queue__remove"
|
||||
type="button"
|
||||
aria-label="Remove queued message"
|
||||
@click=${() => props.onQueueRemove(item.id)}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user