fix(webchat): support image-only sends

This commit is contained in:
Peter Steinberger
2026-01-26 05:32:29 +00:00
parent 9ba4b1e32b
commit 6859e1e6a6
11 changed files with 93 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
import { abortChatRun, loadChatHistory, sendChatMessage, type ChatAttachment } from "./controllers/chat";
import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat";
import { loadSessions } from "./controllers/sessions";
import { generateUUID } from "./uuid";
import { resetToolStream } from "./app-tool-stream";
@@ -8,12 +8,13 @@ import { normalizeBasePath } from "./navigation";
import type { GatewayHelloOk } from "./gateway";
import { parseAgentSessionKey } from "../../../src/sessions/session-key-utils.js";
import type { ClawdbotApp } from "./app";
import type { ChatAttachment, ChatQueueItem } from "./ui-types";
type ChatHost = {
connected: boolean;
chatMessage: string;
chatAttachments: ChatAttachment[];
chatQueue: Array<{ id: string; text: string; createdAt: number }>;
chatQueue: ChatQueueItem[];
chatRunId: string | null;
chatSending: boolean;
sessionKey: string;
@@ -46,15 +47,17 @@ export async function handleAbortChat(host: ChatHost) {
await abortChatRun(host as unknown as ClawdbotApp);
}
function enqueueChatMessage(host: ChatHost, text: string) {
function enqueueChatMessage(host: ChatHost, text: string, attachments?: ChatAttachment[]) {
const trimmed = text.trim();
if (!trimmed) return;
const hasAttachments = Boolean(attachments && attachments.length > 0);
if (!trimmed && !hasAttachments) return;
host.chatQueue = [
...host.chatQueue,
{
id: generateUUID(),
text: trimmed,
createdAt: Date.now(),
attachments: hasAttachments ? attachments?.map((att) => ({ ...att })) : undefined,
},
];
}
@@ -62,19 +65,31 @@ function enqueueChatMessage(host: ChatHost, text: string) {
async function sendChatMessageNow(
host: ChatHost,
message: string,
opts?: { previousDraft?: string; restoreDraft?: boolean; attachments?: ChatAttachment[] },
opts?: {
previousDraft?: string;
restoreDraft?: boolean;
attachments?: ChatAttachment[];
previousAttachments?: ChatAttachment[];
restoreAttachments?: boolean;
},
) {
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
const ok = await sendChatMessage(host as unknown as ClawdbotApp, message, opts?.attachments);
if (!ok && opts?.previousDraft != null) {
host.chatMessage = opts.previousDraft;
}
if (!ok && opts?.previousAttachments) {
host.chatAttachments = opts.previousAttachments;
}
if (ok) {
setLastActiveSessionKey(host as unknown as Parameters<typeof setLastActiveSessionKey>[0], host.sessionKey);
}
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
host.chatMessage = opts.previousDraft;
}
if (ok && opts?.restoreAttachments && opts.previousAttachments?.length) {
host.chatAttachments = opts.previousAttachments;
}
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
if (ok && !host.chatRunId) {
void flushChatQueue(host);
@@ -87,7 +102,7 @@ async function flushChatQueue(host: ChatHost) {
const [next, ...rest] = host.chatQueue;
if (!next) return;
host.chatQueue = rest;
const ok = await sendChatMessageNow(host, next.text);
const ok = await sendChatMessageNow(host, next.text, { attachments: next.attachments });
if (!ok) {
host.chatQueue = [next, ...host.chatQueue];
}
@@ -106,7 +121,8 @@ export async function handleSendChat(
const previousDraft = host.chatMessage;
const message = (messageOverride ?? host.chatMessage).trim();
const attachments = host.chatAttachments ?? [];
const hasAttachments = attachments.length > 0;
const attachmentsToSend = messageOverride == null ? attachments : [];
const hasAttachments = attachmentsToSend.length > 0;
// Allow sending with just attachments (no message text required)
if (!message && !hasAttachments) return;
@@ -123,14 +139,16 @@ export async function handleSendChat(
}
if (isChatBusy(host)) {
enqueueChatMessage(host, message);
enqueueChatMessage(host, message, attachmentsToSend);
return;
}
await sendChatMessageNow(host, message, {
previousDraft: messageOverride == null ? previousDraft : undefined,
restoreDraft: Boolean(messageOverride && opts?.restoreDraft),
attachments: hasAttachments ? attachments : undefined,
attachments: hasAttachments ? attachmentsToSend : undefined,
previousAttachments: messageOverride == null ? attachments : undefined,
restoreAttachments: Boolean(messageOverride && opts?.restoreDraft),
});
}

View File

@@ -431,6 +431,7 @@ export function renderApp(state: AppViewState) {
onSessionKeyChange: (next) => {
state.sessionKey = next;
state.chatMessage = "";
state.chatAttachments = [];
state.chatStream = null;
state.chatStreamStartedAt = null;
state.chatRunId = null;

View File

@@ -19,7 +19,7 @@ import type {
SkillStatusReport,
StatusSummary,
} from "./types";
import type { ChatQueueItem, CronFormState } from "./ui-types";
import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types";
import type { EventLogEntry } from "./app-events";
import type { SkillMessage } from "./controllers/skills";
import type {
@@ -49,6 +49,7 @@ export type AppViewState = {
chatLoading: boolean;
chatSending: boolean;
chatMessage: string;
chatAttachments: ChatAttachment[];
chatMessages: unknown[];
chatToolMessages: unknown[];
chatStream: string | null;

View File

@@ -24,7 +24,7 @@ import type {
StatusSummary,
NostrProfile,
} from "./types";
import { type ChatQueueItem, type CronFormState } from "./ui-types";
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types";
import type { EventLogEntry } from "./app-events";
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults";
import type {
@@ -129,7 +129,7 @@ export class ClawdbotApp extends LitElement {
@state() chatAvatarUrl: string | null = null;
@state() chatThinkingLevel: string | null = null;
@state() chatQueue: ChatQueueItem[] = [];
@state() chatAttachments: Array<{ id: string; dataUrl: string; mimeType: string }> = [];
@state() chatAttachments: ChatAttachment[] = [];
// Sidebar state for tool output viewing
@state() sidebarOpen = false;
@state() sidebarContent: string | null = null;

View File

@@ -1,12 +1,7 @@
import { extractText } from "../chat/message-extract";
import type { GatewayBrowserClient } from "../gateway";
import { generateUUID } from "../uuid";
export type ChatAttachment = {
id: string;
dataUrl: string;
mimeType: string;
};
import type { ChatAttachment } from "../ui-types";
export type ChatState = {
client: GatewayBrowserClient | null;

View File

@@ -1,7 +1,14 @@
export type ChatAttachment = {
id: string;
dataUrl: string;
mimeType: string;
};
export type ChatQueueItem = {
id: string;
text: string;
createdAt: number;
attachments?: ChatAttachment[];
};
export const CRON_CHANNEL_LAST = "last";

View File

@@ -1,7 +1,7 @@
import { html, nothing } from "lit";
import { repeat } from "lit/directives/repeat.js";
import type { SessionsListResult } from "../types";
import type { ChatQueueItem } from "../ui-types";
import type { ChatAttachment, ChatQueueItem } from "../ui-types";
import type { ChatItem, MessageGroup } from "../types/chat-types";
import { icons } from "../icons";
import {
@@ -22,12 +22,6 @@ export type CompactionIndicatorStatus = {
completedAt: number | null;
};
export type ChatAttachment = {
id: string;
dataUrl: string;
mimeType: string;
};
export type ChatProps = {
sessionKey: string;
onSessionKeyChange: (next: string) => void;
@@ -305,7 +299,12 @@ export function renderChat(props: ChatProps) {
${props.queue.map(
(item) => html`
<div class="chat-queue__item">
<div class="chat-queue__text">${item.text}</div>
<div class="chat-queue__text">
${item.text ||
(item.attachments?.length
? `Image (${item.attachments.length})`
: "")}
</div>
<button
class="btn chat-queue__remove"
type="button"