Commit 03af76f2 authored by edy's avatar edy

fix chat attachment message display

parent 3fd16a36
...@@ -763,6 +763,21 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -763,6 +763,21 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
})); }));
}; };
const readImageAttachmentDataUrl = async (attachment: ChatAttachment): Promise<string | null> => {
const normalized = normalizeChatAttachmentCandidate(attachment);
if (!normalized || normalized.kind !== "image") {
return null;
}
try {
const buffer = await readFile(normalized.localPath);
const mimeType = normalized.mimeType?.trim() || inferAttachmentMimeType(normalized.localPath, normalized.name);
return `data:${mimeType};base64,${buffer.toString("base64")}`;
} catch {
return null;
}
};
const extractChatCompletionText = (payload: unknown): string => { const extractChatCompletionText = (payload: unknown): string => {
const response = payload as { const response = payload as {
choices?: Array<{ choices?: Array<{
...@@ -1548,15 +1563,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1548,15 +1563,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
role: ChatMessage["role"], role: ChatMessage["role"],
content: string, content: string,
overrides: Partial<ChatMessage> = {} overrides: Partial<ChatMessage> = {}
): ChatMessage => ({ ): ChatMessage => {
id: overrides.id ?? randomUUID(), const attachments = normalizeChatAttachments(overrides.attachments);
role, return {
content, id: overrides.id ?? randomUUID(),
createdAt: overrides.createdAt ?? new Date().toISOString(), role,
streamState: overrides.streamState, content,
statusLabel: overrides.statusLabel, createdAt: overrides.createdAt ?? new Date().toISOString(),
statusDetail: overrides.statusDetail ...(attachments.length ? { attachments } : {}),
}); streamState: overrides.streamState,
statusLabel: overrides.statusLabel,
statusDetail: overrides.statusDetail
};
};
const sendHomeImagePrompt = async ( const sendHomeImagePrompt = async (
sessionId: string, sessionId: string,
...@@ -1569,7 +1588,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1569,7 +1588,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.setSessionSelectedSkill(sessionId, null); await projectStore.setSessionSelectedSkill(sessionId, null);
await projectStore.updateSessionLastActive(sessionId); await projectStore.updateSessionLastActive(sessionId);
await ensureLocalTranscript(sessionId); await ensureLocalTranscript(sessionId);
await projectStore.appendSessionMessage(sessionId, createChatMessage("user", prompt)); await projectStore.appendSessionMessage(sessionId, createChatMessage("user", prompt, {
attachments: normalizedAttachments
}));
runtimeCloudSupervisor.noteMessageReceived(sessionId, prompt, undefined); runtimeCloudSupervisor.noteMessageReceived(sessionId, prompt, undefined);
try { try {
const replyContent = await requestHomeImageChatCompletion(prompt, normalizedAttachments); const replyContent = await requestHomeImageChatCompletion(prompt, normalizedAttachments);
...@@ -1735,7 +1756,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1735,7 +1756,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const shouldScheduleContextRefresh = shouldRefreshProjectContextAfterExecution(preparedExecution.decision); const shouldScheduleContextRefresh = shouldRefreshProjectContextAfterExecution(preparedExecution.decision);
await projectStore.updateSessionLastActive(executionSessionId); await projectStore.updateSessionLastActive(executionSessionId);
await ensureLocalTranscript(executionSessionId); await ensureLocalTranscript(executionSessionId);
await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt)); await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, {
attachments: preparedExecution.attachments
}));
runtimeCloudSupervisor.noteMessageReceived(executionSessionId, prompt, executionSkillId); runtimeCloudSupervisor.noteMessageReceived(executionSessionId, prompt, executionSkillId);
try { try {
if (preparedExecution.decision.kind === "workspace-entry") { if (preparedExecution.decision.kind === "workspace-entry") {
...@@ -1882,7 +1905,8 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1882,7 +1905,8 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.updateSessionLastActive(executionSessionId); await projectStore.updateSessionLastActive(executionSessionId);
await ensureLocalTranscript(executionSessionId); await ensureLocalTranscript(executionSessionId);
await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, { await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, {
id: userMessageId id: userMessageId,
attachments: normalizedAttachments
})); }));
await queueAssistantTranscriptWrite(createChatMessage("assistant", "", { await queueAssistantTranscriptWrite(createChatMessage("assistant", "", {
id: assistantMessageId, id: assistantMessageId,
...@@ -2007,7 +2031,8 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2007,7 +2031,8 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.updateSessionLastActive(executionSessionId); await projectStore.updateSessionLastActive(executionSessionId);
await ensureLocalTranscript(executionSessionId); await ensureLocalTranscript(executionSessionId);
await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, { await projectStore.appendSessionMessage(executionSessionId, createChatMessage("user", prompt, {
id: userMessageId id: userMessageId,
attachments: preparedExecution.attachments
})); }));
await queueAssistantTranscriptWrite(createChatMessage("assistant", "", { await queueAssistantTranscriptWrite(createChatMessage("assistant", "", {
id: assistantMessageId, id: assistantMessageId,
...@@ -2538,6 +2563,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2538,6 +2563,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
ipcMain.handle(IPC_CHANNELS.chatListMessages, async (_event, sessionId: string) => listChatMessages(sessionId)); ipcMain.handle(IPC_CHANNELS.chatListMessages, async (_event, sessionId: string) => listChatMessages(sessionId));
ipcMain.handle(IPC_CHANNELS.chatPickAttachments, async (event) => pickAttachments(BrowserWindow.fromWebContents(event.sender))); ipcMain.handle(IPC_CHANNELS.chatPickAttachments, async (event) => pickAttachments(BrowserWindow.fromWebContents(event.sender)));
ipcMain.handle(IPC_CHANNELS.chatPickImageAttachment, async (event) => pickImageAttachment(BrowserWindow.fromWebContents(event.sender))); ipcMain.handle(IPC_CHANNELS.chatPickImageAttachment, async (event) => pickImageAttachment(BrowserWindow.fromWebContents(event.sender)));
ipcMain.handle(IPC_CHANNELS.chatReadImageAttachmentDataUrl, async (_event, attachment: ChatAttachment) => readImageAttachmentDataUrl(attachment));
ipcMain.handle(IPC_CHANNELS.chatSendPrompt, async (_event, sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => { ipcMain.handle(IPC_CHANNELS.chatSendPrompt, async (_event, sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => {
return sendPrompt(sessionId, prompt, skillId, attachments); return sendPrompt(sessionId, prompt, skillId, attachments);
}); });
...@@ -2656,6 +2682,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2656,6 +2682,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
listMessages: (sessionId: string) => listChatMessages(sessionId), listMessages: (sessionId: string) => listChatMessages(sessionId),
pickAttachments: async () => pickAttachments(BrowserWindow.getFocusedWindow() ?? null), pickAttachments: async () => pickAttachments(BrowserWindow.getFocusedWindow() ?? null),
pickImageAttachment: async () => pickImageAttachment(BrowserWindow.getFocusedWindow() ?? null), pickImageAttachment: async () => pickImageAttachment(BrowserWindow.getFocusedWindow() ?? null),
readImageAttachmentDataUrl: async (attachment: ChatAttachment) => readImageAttachmentDataUrl(attachment),
sendPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => sendPrompt(sessionId, prompt, skillId, attachments), sendPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => sendPrompt(sessionId, prompt, skillId, attachments),
streamPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => streamPrompt(sessionId, prompt, skillId, attachments), streamPrompt: async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => streamPrompt(sessionId, prompt, skillId, attachments),
onStreamEvent: (listener) => { onStreamEvent: (listener) => {
......
...@@ -87,6 +87,7 @@ const desktopApi: DesktopApi = { ...@@ -87,6 +87,7 @@ const desktopApi: DesktopApi = {
listMessages: (sessionId: string) => ipcRenderer.invoke(IPC_CHANNELS.chatListMessages, sessionId), listMessages: (sessionId: string) => ipcRenderer.invoke(IPC_CHANNELS.chatListMessages, sessionId),
pickAttachments: () => ipcRenderer.invoke(IPC_CHANNELS.chatPickAttachments), pickAttachments: () => ipcRenderer.invoke(IPC_CHANNELS.chatPickAttachments),
pickImageAttachment: () => ipcRenderer.invoke(IPC_CHANNELS.chatPickImageAttachment), pickImageAttachment: () => ipcRenderer.invoke(IPC_CHANNELS.chatPickImageAttachment),
readImageAttachmentDataUrl: (attachment: ChatAttachment) => ipcRenderer.invoke(IPC_CHANNELS.chatReadImageAttachmentDataUrl, attachment),
sendPrompt: (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => ipcRenderer.invoke(IPC_CHANNELS.chatSendPrompt, sessionId, prompt, skillId, attachments), sendPrompt: (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => ipcRenderer.invoke(IPC_CHANNELS.chatSendPrompt, sessionId, prompt, skillId, attachments),
streamPrompt: (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => ipcRenderer.invoke(IPC_CHANNELS.chatStreamPrompt, sessionId, prompt, skillId, attachments), streamPrompt: (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]) => ipcRenderer.invoke(IPC_CHANNELS.chatStreamPrompt, sessionId, prompt, skillId, attachments),
onStreamEvent: (listener: ChatStreamListener) => { onStreamEvent: (listener: ChatStreamListener) => {
...@@ -109,4 +110,3 @@ const smokeEnabled = process.argv.includes("--qjc-smoke") || Boolean(process.env ...@@ -109,4 +110,3 @@ const smokeEnabled = process.argv.includes("--qjc-smoke") || Boolean(process.env
contextBridge.exposeInMainWorld("qjcDesktop", desktopApi); contextBridge.exposeInMainWorld("qjcDesktop", desktopApi);
contextBridge.exposeInMainWorld("qjcSmokeEnabled", smokeEnabled); contextBridge.exposeInMainWorld("qjcSmokeEnabled", smokeEnabled);
import type { ChatMessage } from "@qjclaw/shared-types" import type { ChatAttachment, ChatMessage } from "@qjclaw/shared-types"
import type { ReactNode, RefObject, UIEvent } from "react" import { useEffect, useMemo, useState, type ReactNode, type RefObject, type UIEvent } from "react"
import { desktopApi } from "../../lib/desktop-api"
import { getTraceLineClassName, getTraceLineLabels } from "./messageTraceDisplay" import { getTraceLineClassName, getTraceLineLabels } from "./messageTraceDisplay"
import type { MessageTraceState } from "./useMessageTraces" import type { MessageTraceState } from "./useMessageTraces"
...@@ -59,6 +60,120 @@ interface MessageListProps { ...@@ -59,6 +60,120 @@ interface MessageListProps {
onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void
} }
function getAttachmentKey(attachment: ChatAttachment, index: number): string {
return `${attachment.localPath || attachment.name}:${index}`
}
function getAttachmentTypeLabel(attachment: ChatAttachment): string {
const mimeType = attachment.mimeType?.toLowerCase() ?? ""
const extension = attachment.name.split(".").pop()?.toLowerCase() ?? ""
if (attachment.kind === "image" || mimeType.startsWith("image/")) {
return "IMAGE"
}
if (mimeType.includes("pdf") || extension === "pdf") {
return "PDF"
}
if (mimeType.includes("mpeg") || mimeType.includes("audio") || extension === "mp3") {
return "MP3"
}
if (["doc", "docx"].includes(extension)) {
return "DOC"
}
if (["xls", "xlsx", "csv", "tsv"].includes(extension)) {
return "SHEET"
}
if (["ppt", "pptx"].includes(extension)) {
return "PPT"
}
if (["txt", "md", "json"].includes(extension)) {
return "TEXT"
}
return "FILE"
}
function AttachmentFileCard({ attachment }: { attachment: ChatAttachment }) {
const label = getAttachmentTypeLabel(attachment)
return (
<div className="message-attachment-file-card" title={attachment.name}>
<span className="message-attachment-file-icon" aria-hidden="true">
<span />
</span>
<span className="message-attachment-file-body">
<span className="message-attachment-file-name">{attachment.name}</span>
<span className="message-attachment-file-type">{label}</span>
</span>
</div>
)
}
function ImageAttachmentPreview({ attachment }: { attachment: ChatAttachment }) {
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const [previewFailed, setPreviewFailed] = useState(false)
useEffect(() => {
let cancelled = false
setPreviewUrl(null)
setPreviewFailed(false)
void desktopApi.chat.readImageAttachmentDataUrl(attachment)
.then((dataUrl) => {
if (cancelled) {
return
}
if (dataUrl) {
setPreviewUrl(dataUrl)
} else {
setPreviewFailed(true)
}
})
.catch(() => {
if (!cancelled) {
setPreviewFailed(true)
}
})
return () => {
cancelled = true
}
}, [attachment.kind, attachment.localPath, attachment.mimeType, attachment.name])
if (!previewUrl || previewFailed) {
return <AttachmentFileCard attachment={attachment} />
}
return (
<figure className="message-attachment-image">
<img src={previewUrl} alt={attachment.name} loading="lazy" onError={() => setPreviewFailed(true)} />
<figcaption>{attachment.name}</figcaption>
</figure>
)
}
function MessageAttachmentStrip({ attachments }: { attachments: ChatAttachment[] | undefined }) {
const visibleAttachments = useMemo(
() => (Array.isArray(attachments) ? attachments.filter((attachment) => attachment.localPath && attachment.name) : []),
[attachments]
)
if (!visibleAttachments.length) {
return null
}
return (
<div className="message-attachment-strip" aria-label="消息附件">
{visibleAttachments.map((attachment, index) => (
<div key={getAttachmentKey(attachment, index)} className="message-attachment-item">
{attachment.kind === "image" ? (
<ImageAttachmentPreview attachment={attachment} />
) : (
<AttachmentFileCard attachment={attachment} />
)}
</div>
))}
</div>
)
}
export function MessageList({ export function MessageList({
messages, messages,
viewMode, viewMode,
...@@ -139,6 +254,7 @@ export function MessageList({ ...@@ -139,6 +254,7 @@ export function MessageList({
</p> </p>
) )
) : null} ) : null}
{message.role === "user" ? <MessageAttachmentStrip attachments={message.attachments} /> : null}
{hasTrace ? ( {hasTrace ? (
<div className="message-trace"> <div className="message-trace">
<button type="button" className="trace-inline-toggle" onClick={() => onTraceExpandedChange(message.id, !isTraceExpanded)}> <button type="button" className="trace-inline-toggle" onClick={() => onTraceExpandedChange(message.id, !isTraceExpanded)}>
......
...@@ -449,7 +449,7 @@ export function useChatStreamingController(deps: UseChatStreamingControllerDeps) ...@@ -449,7 +449,7 @@ export function useChatStreamingController(deps: UseChatStreamingControllerDeps)
} }
const renderedPrompt = trimmedPrompt || (attachmentsToSend?.length ? buildAttachmentPromptSummary(attachmentsToSend) : "") const renderedPrompt = trimmedPrompt || (attachmentsToSend?.length ? buildAttachmentPromptSummary(attachmentsToSend) : "")
const userMessage = buildUserMessage(renderedPrompt) const userMessage = buildUserMessage(renderedPrompt, attachmentsToSend)
const assistantMessage = buildAssistantPlaceholder(ui.preparingReply) const assistantMessage = buildAssistantPlaceholder(ui.preparingReply)
setSendPhase("preparing") setSendPhase("preparing")
......
import type { ChatMessage } from "@qjclaw/shared-types" import type { ChatAttachment, ChatMessage } from "@qjclaw/shared-types"
import { import {
COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT, COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT,
COMPOSER_TEXTAREA_MAX_HEIGHT, COMPOSER_TEXTAREA_MAX_HEIGHT,
...@@ -120,12 +120,13 @@ export function appendSmokeStatusLabel(currentLabels: string[] | undefined, labe ...@@ -120,12 +120,13 @@ export function appendSmokeStatusLabel(currentLabels: string[] | undefined, labe
return [...labels, trimmed].slice(-20) return [...labels, trimmed].slice(-20)
} }
export function buildUserMessage(content: string): UiChatMessage { export function buildUserMessage(content: string, attachments?: ChatAttachment[]): UiChatMessage {
return { return {
id: createClientMessageId("user"), id: createClientMessageId("user"),
role: "user", role: "user",
content, content,
createdAt: new Date().toISOString() createdAt: new Date().toISOString(),
attachments
} }
} }
......
...@@ -385,6 +385,7 @@ export const mockDesktopApi = { ...@@ -385,6 +385,7 @@ export const mockDesktopApi = {
listMessages: async () => [], listMessages: async () => [],
pickAttachments: async () => [], pickAttachments: async () => [],
pickImageAttachment: async () => null, pickImageAttachment: async () => null,
readImageAttachmentDataUrl: async () => null,
sendPrompt: async (sessionId: string, prompt: string, skillId?: string, _attachments?: ChatAttachment[]) => ({ sessionId: sessionId || "project:xiaohongshu:default", reply: { id: "reply-1", role: "assistant", content: "Mock: " + prompt, createdAt: new Date().toISOString() }, executionPolicy: { source: "client-config", modelId: "qwen3.6-plus", modelLabel: "qwen3.6-plus", routingMode: "platform-managed", skillId, skillName: skillId, message: "mock" } }), sendPrompt: async (sessionId: string, prompt: string, skillId?: string, _attachments?: ChatAttachment[]) => ({ sessionId: sessionId || "project:xiaohongshu:default", reply: { id: "reply-1", role: "assistant", content: "Mock: " + prompt, createdAt: new Date().toISOString() }, executionPolicy: { source: "client-config", modelId: "qwen3.6-plus", modelLabel: "qwen3.6-plus", routingMode: "platform-managed", skillId, skillName: skillId, message: "mock" } }),
streamPrompt: async (_sessionId: string, prompt: string, skillId?: string, _attachments?: ChatAttachment[]) => { streamPrompt: async (_sessionId: string, prompt: string, skillId?: string, _attachments?: ChatAttachment[]) => {
const requestId = createClientMessageId("mock-request"); const requestId = createClientMessageId("mock-request");
......
...@@ -323,6 +323,120 @@ ...@@ -323,6 +323,120 @@
line-height: 1.82; line-height: 1.82;
} }
.message-attachment-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 100%;
}
.message-attachment-item {
min-width: 0;
}
.message-attachment-image {
width: 220px;
max-width: 100%;
margin: 0;
overflow: hidden;
border: 1px solid rgba(148, 163, 184, 0.34);
border-radius: 8px;
background: #ffffff;
color: #334155;
}
.message-attachment-image img {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
background: #e2e8f0;
}
.message-attachment-image figcaption {
padding: 6px 8px;
overflow: hidden;
color: #334155;
font-size: 12px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-attachment-file-card {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
align-items: center;
gap: 10px;
width: 260px;
max-width: 100%;
min-height: 52px;
padding: 8px 10px;
border: 1px solid rgba(148, 163, 184, 0.34);
border-radius: 8px;
background: rgba(255, 255, 255, 0.78);
color: #1e293b;
}
.message-attachment-file-icon {
position: relative;
width: 34px;
height: 36px;
border: 1px solid rgba(37, 99, 235, 0.24);
border-radius: 6px;
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
}
.message-attachment-file-icon::after {
content: "";
position: absolute;
top: -1px;
right: -1px;
width: 10px;
height: 10px;
border-left: 1px solid rgba(37, 99, 235, 0.24);
border-bottom: 1px solid rgba(37, 99, 235, 0.24);
border-bottom-left-radius: 4px;
background: #ffffff;
}
.message-attachment-file-icon span {
position: absolute;
left: 8px;
right: 8px;
bottom: 9px;
height: 2px;
border-radius: 999px;
background: #2563eb;
box-shadow: 0 -6px 0 rgba(37, 99, 235, 0.58);
}
.message-attachment-file-body {
min-width: 0;
display: grid;
gap: 3px;
}
.message-attachment-file-name {
overflow: hidden;
font-size: 13px;
font-weight: 600;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-attachment-file-type {
width: fit-content;
padding: 1px 6px;
border-radius: 6px;
background: #dbeafe;
color: #1d4ed8;
font-size: 11px;
font-weight: 700;
line-height: 1.45;
}
.markdown-body { .markdown-body {
display: grid; display: grid;
gap: 14px; gap: 14px;
...@@ -1072,4 +1186,3 @@ select:focus-visible { ...@@ -1072,4 +1186,3 @@ select:focus-visible {
.custom-scrollbar::-webkit-scrollbar-thumb:hover { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8; background: #94a3b8;
} }
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
chatListMessages: "chat:list-messages", chatListMessages: "chat:list-messages",
chatPickAttachments: "chat:pick-attachments", chatPickAttachments: "chat:pick-attachments",
chatPickImageAttachment: "chat:pick-image-attachment", chatPickImageAttachment: "chat:pick-image-attachment",
chatReadImageAttachmentDataUrl: "chat:read-image-attachment-data-url",
chatSendPrompt: "chat:send-prompt", chatSendPrompt: "chat:send-prompt",
chatStreamPrompt: "chat:stream-prompt", chatStreamPrompt: "chat:stream-prompt",
chatStreamEvent: "chat:stream-event", chatStreamEvent: "chat:stream-event",
...@@ -461,6 +462,7 @@ export interface ChatMessage { ...@@ -461,6 +462,7 @@ export interface ChatMessage {
role: MessageRole; role: MessageRole;
content: string; content: string;
createdAt: string; createdAt: string;
attachments?: ChatAttachment[];
streamState?: "streaming" | "error"; streamState?: "streaming" | "error";
statusLabel?: string; statusLabel?: string;
statusDetail?: string; statusDetail?: string;
...@@ -914,6 +916,7 @@ export interface DesktopApi { ...@@ -914,6 +916,7 @@ export interface DesktopApi {
listMessages(sessionId: string): Promise<ChatMessage[]>; listMessages(sessionId: string): Promise<ChatMessage[]>;
pickAttachments(): Promise<ChatAttachment[]>; pickAttachments(): Promise<ChatAttachment[]>;
pickImageAttachment(): Promise<ChatAttachment | null>; pickImageAttachment(): Promise<ChatAttachment | null>;
readImageAttachmentDataUrl(attachment: ChatAttachment): Promise<string | null>;
sendPrompt(sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]): Promise<PromptResult>; sendPrompt(sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]): Promise<PromptResult>;
streamPrompt(sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]): Promise<ChatStreamPromptResult>; streamPrompt(sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[]): Promise<ChatStreamPromptResult>;
onStreamEvent(listener: ChatStreamListener): () => void; onStreamEvent(listener: ChatStreamListener): () => void;
...@@ -923,4 +926,3 @@ export interface DesktopApi { ...@@ -923,4 +926,3 @@ export interface DesktopApi {
exportSnapshot(): Promise<DiagnosticsExportResult>; exportSnapshot(): Promise<DiagnosticsExportResult>;
}; };
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment