mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
feat(ui): add steer-now composer shortcut (#124826)
This commit is contained in:
committed by
GitHub
parent
eab2b8fdca
commit
8754be900a
@@ -3,6 +3,7 @@ import { GATEWAY_SERVER_CAPS } from "../../../packages/gateway-protocol/src/inde
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
createChatFlowE2eSuite,
|
||||
expectRequestCountStable,
|
||||
installMockGateway,
|
||||
requireRecord,
|
||||
requireString,
|
||||
@@ -696,6 +697,124 @@ suite.define(() => {
|
||||
}
|
||||
});
|
||||
|
||||
it("steers a queued follow-up with modified Enter in Enter shortcut mode", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
...(artifactDir
|
||||
? { recordVideo: { dir: artifactDir, size: { height: 900, width: 1280 } } }
|
||||
: {}),
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page);
|
||||
|
||||
try {
|
||||
await page.goto(`${suite.server.baseUrl}settings/appearance`);
|
||||
await page.locator("[data-settings-follow-up-mode]").selectOption("queue");
|
||||
await page.locator("[data-settings-send-shortcut]").selectOption("enter");
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
|
||||
const composer = page.locator(".agent-chat__composer-combobox textarea");
|
||||
await composer.fill("keep the first shortcut run active");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
const firstSend = requireRecord((await gateway.waitForRequest("chat.send")).params);
|
||||
const firstRunId = requireString(firstSend.idempotencyKey, "first active run id");
|
||||
await page.getByRole("button", { name: "Stop generating" }).waitFor({ timeout: 10_000 });
|
||||
|
||||
const steerText = "steer this keyboard follow-up now";
|
||||
await composer.fill(steerText);
|
||||
const enterQueueButton = page.getByRole("button", { name: "Queue message" });
|
||||
const enterTooltip = await enterQueueButton
|
||||
.locator("..")
|
||||
.evaluate((element) => (element as HTMLElement & { content?: string }).content);
|
||||
expect(enterTooltip).toBe("Queue ⏎ · Steer ⌘/Ctrl+Enter");
|
||||
if (artifactDir) {
|
||||
await enterQueueButton.hover();
|
||||
await expect
|
||||
.poll(() =>
|
||||
enterQueueButton.evaluate((button) => {
|
||||
const tooltip = button
|
||||
.closest("openclaw-tooltip")
|
||||
?.shadowRoot?.querySelector("wa-tooltip");
|
||||
return (tooltip as (HTMLElement & { open?: boolean }) | null)?.open === true;
|
||||
}),
|
||||
)
|
||||
.toBe(true);
|
||||
await page.screenshot({
|
||||
path: `${artifactDir}/queue-steer-shortcut.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
await composer.press("Control+Enter");
|
||||
|
||||
const firstRunSends = await waitForRequests(gateway, "chat.send", 2);
|
||||
const steerParams = requireRecord(firstRunSends[1]?.params);
|
||||
expect(steerParams).toMatchObject({
|
||||
deliver: false,
|
||||
expectedRunId: firstRunId,
|
||||
message: steerText,
|
||||
queueMode: "steer",
|
||||
sessionKey: "main",
|
||||
});
|
||||
const steeredRow = page.locator(".chat-queue__item--steered", { hasText: steerText });
|
||||
await steeredRow.waitFor({ timeout: 10_000 });
|
||||
await gateway.emitGatewayEvent("chat", {
|
||||
runId: requireString(steerParams.idempotencyKey, "steer send id"),
|
||||
sessionKey: "main",
|
||||
state: "final",
|
||||
});
|
||||
await steeredRow.waitFor({ state: "detached", timeout: 10_000 });
|
||||
|
||||
await gateway.emitChatFinal({ runId: firstRunId, text: "First shortcut run finished." });
|
||||
await page
|
||||
.getByRole("button", { name: "Stop generating" })
|
||||
.waitFor({ state: "detached", timeout: 10_000 });
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps modified Enter queued in modifier-enter shortcut mode", async () => {
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page);
|
||||
|
||||
try {
|
||||
await page.goto(`${suite.server.baseUrl}settings/appearance`);
|
||||
await page.locator("[data-settings-follow-up-mode]").selectOption("queue");
|
||||
await page.locator("[data-settings-send-shortcut]").selectOption("modifier-enter");
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
|
||||
const composer = page.locator(".agent-chat__composer-combobox textarea");
|
||||
await composer.fill("keep the modifier shortcut run active");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
await gateway.waitForRequest("chat.send");
|
||||
await page.getByRole("button", { name: "Stop generating" }).waitFor({ timeout: 10_000 });
|
||||
|
||||
const queuedText = "leave this modifier follow-up queued";
|
||||
await composer.fill(queuedText);
|
||||
const queueButton = page.getByRole("button", { name: "Queue message" });
|
||||
const tooltip = await queueButton
|
||||
.locator("..")
|
||||
.evaluate((element) => (element as HTMLElement & { content?: string }).content);
|
||||
expect(tooltip).toBe("Queue");
|
||||
await composer.press("Control+Enter");
|
||||
|
||||
const queuedRow = page.locator(".chat-queue__item", { hasText: queuedText });
|
||||
await queuedRow.waitFor({ timeout: 10_000 });
|
||||
await queuedRow.getByText("Waiting for current run").waitFor({ timeout: 10_000 });
|
||||
await expectRequestCountStable(gateway, "chat.send", 1);
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("honors a session interrupt override ahead of the webchat config default", async () => {
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -13,6 +13,24 @@ afterEach(async () => {
|
||||
await resetComposerFixture();
|
||||
});
|
||||
|
||||
function pressComposerEnter(
|
||||
container: Element,
|
||||
modifiers: Pick<KeyboardEventInit, "altKey" | "ctrlKey" | "metaKey" | "shiftKey"> = {},
|
||||
) {
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>("textarea");
|
||||
if (!textarea) {
|
||||
throw new Error("expected composer textarea");
|
||||
}
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
key: "Enter",
|
||||
...modifiers,
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
describe("renderChatComposer controls", () => {
|
||||
it.each([
|
||||
{
|
||||
@@ -235,6 +253,137 @@ describe("renderChatComposer controls", () => {
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Meta", { metaKey: true }],
|
||||
["Control", { ctrlKey: true }],
|
||||
] as const)("uses %s+Enter to steer an active queued follow-up", (_name, modifiers) => {
|
||||
const onSend = vi.fn();
|
||||
const { container } = renderComposer({
|
||||
canAbort: true,
|
||||
draft: "Steer this now",
|
||||
followUpMode: "queue",
|
||||
onAbort: vi.fn(),
|
||||
onSend,
|
||||
sendShortcut: "enter",
|
||||
});
|
||||
|
||||
pressComposerEnter(container, modifiers);
|
||||
|
||||
expect(onSend).toHaveBeenCalledOnce();
|
||||
expect(onSend).toHaveBeenCalledWith({ followUpMode: "steer" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["modifier-enter", true, "queue", false],
|
||||
["enter", false, "queue", false],
|
||||
["enter", true, "steer", false],
|
||||
["enter", true, "queue", true],
|
||||
] as const)(
|
||||
"keeps ordinary send semantics for shortcut=%s active=%s mode=%s alt=%s",
|
||||
(sendShortcut, active, followUpMode, altKey) => {
|
||||
const onSend = vi.fn();
|
||||
const { container } = renderComposer({
|
||||
canAbort: active,
|
||||
draft: "Keep the ordinary send path",
|
||||
followUpMode,
|
||||
onAbort: active ? vi.fn() : undefined,
|
||||
onSend,
|
||||
sendShortcut,
|
||||
});
|
||||
|
||||
pressComposerEnter(container, { altKey, ctrlKey: true });
|
||||
|
||||
expect(onSend.mock.calls).toEqual([[]]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps empty modified Enter on the existing empty-draft path", () => {
|
||||
const onSend = vi.fn();
|
||||
const { container } = renderComposer({
|
||||
canAbort: true,
|
||||
draft: "",
|
||||
followUpMode: "queue",
|
||||
onAbort: vi.fn(),
|
||||
onSend,
|
||||
sendShortcut: "enter",
|
||||
});
|
||||
|
||||
pressComposerEnter(container, { ctrlKey: true });
|
||||
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps Shift+modified Enter as a newline", () => {
|
||||
const onSend = vi.fn();
|
||||
const { container } = renderComposer({
|
||||
canAbort: true,
|
||||
draft: "Keep editing",
|
||||
followUpMode: "queue",
|
||||
onAbort: vi.fn(),
|
||||
onSend,
|
||||
sendShortcut: "enter",
|
||||
});
|
||||
|
||||
const event = pressComposerEnter(container, { ctrlKey: true, shiftKey: true });
|
||||
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("teaches the steer shortcut only when the force-steer action is available", () => {
|
||||
const available = renderComposer({
|
||||
canAbort: true,
|
||||
draft: "Follow up now",
|
||||
followUpMode: "queue",
|
||||
onAbort: vi.fn(),
|
||||
sendShortcut: "enter",
|
||||
});
|
||||
const availablePrimary = primaryButton(available.container);
|
||||
const availableTooltip = availablePrimary.closest("openclaw-tooltip") as
|
||||
| (HTMLElement & { content?: string })
|
||||
| null;
|
||||
expect(availablePrimary.getAttribute("aria-label")).toBe(t("chat.runControls.queueMessage"));
|
||||
expect(availableTooltip?.content).toBe("Queue ⏎ · Steer ⌘/Ctrl+Enter");
|
||||
|
||||
const unavailable = [
|
||||
{
|
||||
overrides: {
|
||||
canAbort: true,
|
||||
draft: "Follow up later",
|
||||
followUpMode: "queue" as const,
|
||||
onAbort: vi.fn(),
|
||||
sendShortcut: "modifier-enter" as const,
|
||||
},
|
||||
tooltip: t("chat.runControls.queue"),
|
||||
},
|
||||
{
|
||||
overrides: {
|
||||
draft: "Send without an active run",
|
||||
followUpMode: "queue" as const,
|
||||
sendShortcut: "enter" as const,
|
||||
},
|
||||
tooltip: t("chat.runControls.send"),
|
||||
},
|
||||
{
|
||||
overrides: {
|
||||
canAbort: true,
|
||||
draft: "Already steering",
|
||||
followUpMode: "steer" as const,
|
||||
onAbort: vi.fn(),
|
||||
sendShortcut: "enter" as const,
|
||||
},
|
||||
tooltip: t("chat.queue.steer"),
|
||||
},
|
||||
];
|
||||
for (const testCase of unavailable) {
|
||||
const view = renderComposer(testCase.overrides);
|
||||
const tooltip = primaryButton(view.container).closest("openclaw-tooltip") as
|
||||
| (HTMLElement & { content?: string })
|
||||
| null;
|
||||
expect(tooltip?.content).toBe(testCase.tooltip);
|
||||
}
|
||||
});
|
||||
|
||||
it("stops an abortable run with Escape unless reply or menu precedence owns it", () => {
|
||||
const onAbort = vi.fn();
|
||||
let view = renderComposer({ canAbort: true, onAbort });
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "./chat-pane-state.ts";
|
||||
import { dismissRealtimeTalkError } from "./chat-realtime.ts";
|
||||
import { activeChatRunStartupStatus } from "./chat-run-startup.ts";
|
||||
import type { ChatSendOptions } from "./chat-send-contract.ts";
|
||||
import { refreshChatCommands, refreshPageChat } from "./chat-state-refresh.ts";
|
||||
import {
|
||||
resolveChatAgentId,
|
||||
@@ -520,12 +521,12 @@ export class ChatPane extends ChatPaneBrowserAnnotationRender {
|
||||
state.requestUpdate?.();
|
||||
},
|
||||
onRemoveAttachment: this.removeBrowserAnnotation,
|
||||
onSend: () =>
|
||||
onSend: (options?: ChatSendOptions) =>
|
||||
catalogKey
|
||||
? void this.continueCatalogSession(catalogKey)
|
||||
: suggestionViewer
|
||||
? void this.addCurrentSessionSuggestion()
|
||||
: void state.handleSendChat(),
|
||||
: void state.handleSendChat(undefined, options),
|
||||
onCompact: sessionActionCallbacks.onCompact,
|
||||
// Checkpoint deep-link carries the archived filter so the row stays findable.
|
||||
onOpenSessionCheckpoints: () => {
|
||||
|
||||
@@ -17,6 +17,10 @@ type ChatAgentsListSnapshot = Partial<Omit<AgentsListResult, "agents">> & {
|
||||
agents?: AgentsListResult["agents"];
|
||||
};
|
||||
|
||||
export type ChatSendOptions = {
|
||||
followUpMode?: ControlUiFollowUpMode;
|
||||
};
|
||||
|
||||
export type ChatHost = ChatInputHistoryState &
|
||||
ChatScrollHost &
|
||||
ToolStreamHost &
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
submittedCommandScopeIsVisible,
|
||||
type ChatCommandComposerRecovery,
|
||||
} from "./chat-send-composer.ts";
|
||||
import type { ChatHost } from "./chat-send-contract.ts";
|
||||
import type { ChatHost, ChatSendOptions } from "./chat-send-contract.ts";
|
||||
import { chatOutboxDrainDependencies, deliverChatQueueItem } from "./chat-send-delivery.ts";
|
||||
import {
|
||||
canSendVolatileQueueItem,
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
sendQueuedChatMessageWithQueueMode as sendQueuedChatMessageWithQueueModeLifecycle,
|
||||
} from "./steer-lifecycle.ts";
|
||||
|
||||
type ChatSendOptions = {
|
||||
type ChatSendSubmitOptions = ChatSendOptions & {
|
||||
restoreDraft?: boolean;
|
||||
skillWorkshopRevision?: ChatQueueSkillWorkshopRevision;
|
||||
/** Lets request-scoped UI actions recover from rejected local commands. */
|
||||
@@ -194,7 +194,7 @@ async function sendDetachedCommandMessage(
|
||||
export async function handleSendChat(
|
||||
host: ChatHost,
|
||||
messageOverride?: string,
|
||||
opts?: ChatSendOptions,
|
||||
opts?: ChatSendSubmitOptions,
|
||||
) {
|
||||
const previousDraft = host.chatMessage;
|
||||
const userMessage = (messageOverride ?? host.chatMessage).trim();
|
||||
@@ -538,7 +538,9 @@ export async function handleSendChat(
|
||||
recordChatSendTiming(host, pending, "queued-busy", submittedAtMs);
|
||||
// Only an explicit browser override replaces inherited Gateway policy.
|
||||
const followUpMode =
|
||||
host.chatFollowUpMode ?? normalizeChatFollowUpModeOverride(host.settings?.chatFollowUpMode);
|
||||
opts?.followUpMode ??
|
||||
host.chatFollowUpMode ??
|
||||
normalizeChatFollowUpModeOverride(host.settings?.chatFollowUpMode);
|
||||
if (
|
||||
!skillWorkshopRevision &&
|
||||
followUpMode !== "queue" &&
|
||||
|
||||
@@ -3632,6 +3632,33 @@ describe("handleSendChat", () => {
|
||||
expect(host.chatMessage).toBe("queued while busy");
|
||||
});
|
||||
|
||||
it("lets a per-send steer override beat the effective queue setting", async () => {
|
||||
const host = makeChatHost({
|
||||
requestHandlers: {
|
||||
"chat.send": { status: "started", runId: "steer-run" },
|
||||
},
|
||||
chatMessage: "steer this queued follow-up now",
|
||||
chatRunId: "active-run",
|
||||
chatStream: "Working...",
|
||||
sessionKey: "agent:main:main",
|
||||
settings: { chatFollowUpMode: "queue" },
|
||||
});
|
||||
|
||||
await handleSendChat(host, undefined, { followUpMode: "steer" });
|
||||
|
||||
await waitForFast(() =>
|
||||
expect(host.request).toHaveBeenCalledWith(
|
||||
"chat.send",
|
||||
expect.objectContaining({
|
||||
expectedRunId: "active-run",
|
||||
message: "steer this queued follow-up now",
|
||||
queueMode: "steer",
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails visibly when a busy send cannot be parked after durable admission", async () => {
|
||||
const storage = createStorageMock();
|
||||
const setItem = storage.setItem.bind(storage);
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { ProviderUsageDisplayProps } from "../../lib/provider-quota-summary
|
||||
import type { SessionToolOverrides } from "../../lib/sessions/patch.ts";
|
||||
import type { UiSessionDefaultsHost } from "../../lib/sessions/session-key.ts";
|
||||
import type { ChatRunStartupStatus } from "./chat-run-startup.ts";
|
||||
import type { ChatSendOptions } from "./chat-send-contract.ts";
|
||||
import { type ChatCloudStartupNoticeProps, renderChatViewNotices } from "./chat-view-notices.ts";
|
||||
import { createChatAttachmentDropHandlers } from "./components/chat-attachments.ts";
|
||||
import type { BackgroundTasksProps } from "./components/chat-background-tasks.types.ts";
|
||||
@@ -192,7 +193,7 @@ export type ChatProps = ChatTaskSuggestionTrayProps &
|
||||
onRequestUpdate?: () => void;
|
||||
onHistoryKeydown?: (input: ChatInputHistoryKeyInput) => ChatInputHistoryKeyResult;
|
||||
onSlashIntent?: () => void | Promise<void>;
|
||||
onSend: () => void;
|
||||
onSend: (options?: ChatSendOptions) => void;
|
||||
onCompact?: () => void | Promise<void>;
|
||||
onOpenSessionCheckpoints?: () => void | Promise<void>;
|
||||
onToggleRealtimeTalk?: () => void;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { ref } from "lit/directives/ref.js";
|
||||
import type { ChatSendShortcut } from "../../../app/settings.ts";
|
||||
import { icons } from "../../../components/icons.ts";
|
||||
import { syncDropdownItemRadio } from "../../../components/web-awesome.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
@@ -22,6 +23,7 @@ export type ChatRunControlsProps = {
|
||||
hasAttachments?: boolean;
|
||||
isBusy: boolean;
|
||||
followUpMode?: ControlUiFollowUpMode;
|
||||
sendShortcut: ChatSendShortcut;
|
||||
suggestionComposer?: boolean;
|
||||
sending: boolean;
|
||||
voiceActive?: boolean;
|
||||
@@ -218,6 +220,15 @@ export function renderChatPrimaryActions(props: ChatRunControlsProps) {
|
||||
: interruptsActiveRun
|
||||
? t("chat.runControls.sendMessage")
|
||||
: t("chat.runControls.queueMessage");
|
||||
const queueSteerShortcutAvailable =
|
||||
props.canAbort &&
|
||||
props.canSend &&
|
||||
hasComposedContent &&
|
||||
props.followUpMode === "queue" &&
|
||||
props.sendShortcut === "enter";
|
||||
const activeRunActionTooltip = queueSteerShortcutAvailable
|
||||
? `${activeRunActionLabel} ⏎ · ${t("chat.queue.steer")} ${t("chat.sendShortcutModifierEnter")}`
|
||||
: activeRunActionLabel;
|
||||
const storeDraftAndSend = () => {
|
||||
if (props.draft.trim()) {
|
||||
props.onStoreDraft(props.draft);
|
||||
@@ -247,7 +258,7 @@ export function renderChatPrimaryActions(props: ChatRunControlsProps) {
|
||||
const voiceErrored = props.voiceStatus === "error";
|
||||
const voiceButton = renderComposerVoiceButton(props);
|
||||
const sendAction = html`
|
||||
<openclaw-tooltip .content=${activeRunActionLabel}>
|
||||
<openclaw-tooltip .content=${activeRunActionTooltip}>
|
||||
<button
|
||||
class="chat-send-btn"
|
||||
@pointerdown=${props.onPrimaryActionPointerDown}
|
||||
|
||||
@@ -209,7 +209,8 @@ export function createComposerKeyDownHandler({
|
||||
const sendShortcutMatches = sendShortcut === "enter" || event.metaKey || event.ctrlKey;
|
||||
if (event.key === "Enter" && !event.shiftKey && sendShortcutMatches) {
|
||||
const attachments = props.getAttachments?.() ?? props.attachments ?? [];
|
||||
if (!target.value.trim() && attachments.length === 0) {
|
||||
const hasComposedContent = Boolean(target.value.trim() || attachments.length);
|
||||
if (!hasComposedContent) {
|
||||
// Mirror the queue chip's Steer availability exactly (visible surface,
|
||||
// connected + composable gate), or offline Enter would swallow the key
|
||||
// and invoke a lifecycle that returns with no visible outcome.
|
||||
@@ -232,7 +233,19 @@ export function createComposerKeyDownHandler({
|
||||
}
|
||||
event.preventDefault();
|
||||
commitDraft(target.value);
|
||||
props.onSend();
|
||||
const steerImmediately =
|
||||
sendShortcut === "enter" &&
|
||||
showAbortableUi &&
|
||||
props.followUpMode === "queue" &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
hasComposedContent;
|
||||
if (steerImmediately) {
|
||||
props.onSend({ followUpMode: "steer" });
|
||||
} else {
|
||||
props.onSend();
|
||||
}
|
||||
syncDraftAfterSend(target);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SlashCommandDef } from "../../../lib/chat/commands.ts";
|
||||
import type { ControlUiFollowUpMode } from "../../../lib/chat/follow-up-mode.ts";
|
||||
import type { ProviderUsageDisplayProps } from "../../../lib/provider-quota-summary.ts";
|
||||
import type { SessionToolOverrides } from "../../../lib/sessions/patch.ts";
|
||||
import type { ChatSendOptions } from "../chat-send-contract.ts";
|
||||
import type { ComposerDictationController } from "../composer-dictation.ts";
|
||||
import type { ChatInputHistoryKeyInput, ChatInputHistoryKeyResult } from "../input-history.ts";
|
||||
import type { RealtimeTalkConversationEntry } from "../realtime-talk-conversation.ts";
|
||||
@@ -119,7 +120,7 @@ export type ChatComposerProps = ChatAttachmentControlsProps & {
|
||||
onDraftChange: (next: string) => void;
|
||||
onHistoryKeydown?: (input: ChatInputHistoryKeyInput) => ChatInputHistoryKeyResult;
|
||||
onSlashIntent?: () => void | Promise<void>;
|
||||
onSend: () => void;
|
||||
onSend: (options?: ChatSendOptions) => void;
|
||||
onCompact?: () => void | Promise<void>;
|
||||
onToggleRealtimeTalk?: () => void;
|
||||
onToggleRealtimeCamera?: () => void;
|
||||
|
||||
@@ -492,6 +492,7 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
hasAttachments: !props.suggestionComposer && Boolean(props.attachments?.length),
|
||||
isBusy,
|
||||
followUpMode: props.followUpMode,
|
||||
sendShortcut,
|
||||
suggestionComposer: props.suggestionComposer,
|
||||
sending: props.sending,
|
||||
voiceActive: props.realtimeTalkActive,
|
||||
|
||||
Reference in New Issue
Block a user