fix(desktop): show a staging spinner in the edit composer while OS drops upload

The message-edit composer staged dropped OS files asynchronously with no
visible state, so confirming the edit before the upload resolved could send
the message without the gateway-side ref (helix4u review note on #43109).

Add a staging flag: while uploadOsDropRefs is in flight, show a small spinner
pill in the bubble and block submit (disabled send button + submitEdit guard)
so the edit can't outrace the ref insertion. New `attachingFile` i18n string
across en/zh/zh-hant/ja.
This commit is contained in:
Brooklyn Nicholson 2026-06-09 18:26:54 -05:00
parent 891c9a6823
commit b021497bc8
6 changed files with 32 additions and 11 deletions

View file

@ -971,6 +971,10 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
const [triggerPlacement, setTriggerPlacement] = useState<'bottom' | 'top'>('top')
const [focusRequestId, setFocusRequestId] = useState(0)
const [submitting, setSubmitting] = useState(false)
// True while OS-drop files are being staged/uploaded into the session. Blocks
// submit and shows a spinner so confirming the edit can't race the async
// upload and drop the gateway-side ref before it lands in the draft.
const [staging, setStaging] = useState(false)
const expanded = draft.includes('\n')
const canSubmit = draft.trim().length > 0
const at = useAtCompletions({ cwd, gateway, sessionId })
@ -1324,11 +1328,14 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
}
if (osDrops.length) {
void uploadOsDropRefs(osDrops).then(refs => {
if (insertRefStrings(refs)) {
triggerHaptic('selection')
}
})
setStaging(true)
void uploadOsDropRefs(osDrops)
.then(refs => {
if (insertRefStrings(refs)) {
triggerHaptic('selection')
}
})
.finally(() => setStaging(false))
}
}
@ -1360,7 +1367,7 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
const submitEdit = (editor: HTMLDivElement) => {
const nextDraft = syncDraftFromEditor(editor)
if (submitting || !nextDraft.trim()) {
if (submitting || staging || !nextDraft.trim()) {
return
}
@ -1517,10 +1524,19 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
suppressContentEditableWarning
/>
<ComposerPrimitive.Input className="sr-only" tabIndex={-1} unstable_focusOnScrollToBottom={false} />
{staging && (
<span
className="pointer-events-none absolute bottom-2 left-2 inline-flex items-center gap-1 rounded-full bg-background/80 px-1.5 py-0.5 text-[0.62rem] text-muted-foreground backdrop-blur-[1px]"
data-slot="aui_edit-staging"
>
<Loader2Icon className="size-3 animate-spin" />
{copy.attachingFile}
</span>
)}
<button
aria-label={copy.sendEdited}
className={cn('absolute right-2 bottom-2 size-5', USER_ACTION_ICON_BUTTON_CLASS)}
disabled={!canSubmit || submitting}
disabled={!canSubmit || submitting || staging}
onClick={() => {
const editor = editorRef.current

View file

@ -1622,7 +1622,8 @@ export const en: Translations = {
restoreCheckpoint: 'Restore checkpoint',
restoreNext: 'Restore next checkpoint',
goForward: 'Go forward',
sendEdited: 'Send edited message'
sendEdited: 'Send edited message',
attachingFile: 'Attaching…'
},
approval: {
gatewayDisconnected: 'Hermes gateway is not connected',

View file

@ -1766,7 +1766,8 @@ export const ja = defineLocale({
restoreCheckpoint: 'チェックポイントを復元',
restoreNext: '次のチェックポイントに戻す',
goForward: '進む',
sendEdited: '編集済みメッセージを送信'
sendEdited: '編集済みメッセージを送信',
attachingFile: '添付中…'
},
approval: {
gatewayDisconnected: 'Hermes ゲートウェイが接続されていません',

View file

@ -1293,6 +1293,7 @@ export interface Translations {
restoreNext: string
goForward: string
sendEdited: string
attachingFile: string
}
approval: {
gatewayDisconnected: string

View file

@ -1727,7 +1727,8 @@ export const zhHant = defineLocale({
restoreCheckpoint: '還原檢查點',
restoreNext: '還原至下一個檢查點',
goForward: '前進',
sendEdited: '傳送編輯後的訊息'
sendEdited: '傳送編輯後的訊息',
attachingFile: '正在附加…'
},
approval: {
gatewayDisconnected: 'Hermes 閘道未連線',

View file

@ -1802,7 +1802,8 @@ export const zh: Translations = {
restoreCheckpoint: '恢复检查点',
restoreNext: '恢复下一个检查点',
goForward: '前进',
sendEdited: '发送编辑后的消息'
sendEdited: '发送编辑后的消息',
attachingFile: '正在附加…'
},
approval: {
gatewayDisconnected: 'Hermes 网关未连接',