diff --git a/.changeset/attachment-sending-indicator.md b/.changeset/attachment-sending-indicator.md new file mode 100644 index 0000000..3c7eab7 --- /dev/null +++ b/.changeset/attachment-sending-indicator.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Show a sending indicator in the chat composer while messages with image attachments are uploading. Previously the composer cleared instantly while the upload, server-side image resizing, and first-session open happened in the background, so it looked like nothing was happening. The Send button now shows "Sending…" and a "Sending your files…" (or "Saving your files…" for folder mode) hint until the message lands, and the composer is disabled while in flight. diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 713b672..531a89f 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -1664,10 +1664,10 @@ export class PiWebApp extends LitElement { if (isThinkingLevel(value)) await this.sessions.setThinkingLevel(value); } - private sendPrompt(text: string, streamingBehavior?: "steer" | "followUp", attachments?: import("../api").PromptAttachment[]): void { + private async sendPrompt(text: string, streamingBehavior?: "steer" | "followUp", attachments?: import("../api").PromptAttachment[]): Promise { const hasAttachments = attachments !== undefined && attachments.length > 0; if (!hasAttachments && streamingBehavior === undefined && this.auth.handleSlashCommand(text)) return; - void this.sessions.send(text, streamingBehavior, attachments); + await this.sessions.send(text, streamingBehavior, attachments); } private renderContextBar() { @@ -1729,7 +1729,7 @@ export class PiWebApp extends LitElement {
${this.appShell.isMobileNavigationLayout ? this.renderNavigationPanel() : null}
${state.selectedSession ? html` 0} .loadingMore=${state.isLoadingEarlierMessages} .isReceivingPartialStream=${state.isReceivingPartialStream} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}> - 0} .status=${state.status} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp", attachments?: import("../api").PromptAttachment[]) => { this.sendPrompt(text, streamingBehavior, attachments); }} .onSaveAttachments=${(attachments: import("../api").PromptAttachment[]) => this.sessions.saveAttachments(attachments)} .onStop=${() => this.sessions.stopActiveWork()} .onSelectModel=${() => { void this.openModelDialog(); }} .onSelectThinking=${() => { void this.openThinkingDialog(); }}> + 0} .status=${state.status} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp", attachments?: import("../api").PromptAttachment[]) => this.sendPrompt(text, streamingBehavior, attachments)} .onSaveAttachments=${(attachments: import("../api").PromptAttachment[]) => this.sessions.saveAttachments(attachments)} .onStop=${() => this.sessions.stopActiveWork()} .onSelectModel=${() => { void this.openModelDialog(); }} .onSelectThinking=${() => { void this.openThinkingDialog(); }}> ${state.commandDialog !== undefined ? html` this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}>` : null} ${state.modelDialog !== undefined ? html` { void this.pickModel(value); }} .onCancel=${() => { this.setState({ modelDialog: undefined }); }}>` : null} diff --git a/src/client/src/components/PromptEditor.ts b/src/client/src/components/PromptEditor.ts index 6083598..11c3594 100644 --- a/src/client/src/components/PromptEditor.ts +++ b/src/client/src/components/PromptEditor.ts @@ -35,7 +35,7 @@ export class PromptEditor extends LitElement { @property({ type: Boolean }) isCompacting = false; @property({ type: Boolean }) canStop = false; @property({ attribute: false }) status?: SessionStatus; - @property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp", attachments?: PromptAttachment[]) => void; + @property({ attribute: false }) onSend?: (text: string, streamingBehavior?: "steer" | "followUp", attachments?: PromptAttachment[]) => void | Promise; @property({ attribute: false }) onSaveAttachments?: (attachments: PromptAttachment[]) => Promise<{ path: string }[]>; @property({ attribute: false }) onStop?: () => void; @property({ attribute: false }) onSelectModel?: () => void; @@ -49,6 +49,7 @@ export class PromptEditor extends LitElement { @state() private attachmentDelivery: PromptAttachmentDelivery = loadAttachmentDelivery(); @state() private attachmentError: string | undefined = undefined; @state() private isSavingAttachments = false; + @state() private isSending = false; private attachmentSeq = 0; private requestVersion = 0; private editor: EditorView | undefined; @@ -86,13 +87,16 @@ export class PromptEditor extends LitElement { const inputMode = inputModeForDraft(this.draft); const shellMode = inputMode.kind === "shell"; const queuesInput = this.canSteer || this.isCompacting; - const busy = this.disabled || this.isSavingAttachments; + const uploading = this.isSavingAttachments || this.isSending; + const busy = this.disabled || uploading; + const sendLabel = uploading ? "Sending…" : queuesInput ? "Queue" : "Send"; return html`