From df8dfa97fd3a8fb0e23cb512d4a7f901c91d26f8 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Sun, 24 May 2026 14:10:06 -0700 Subject: [PATCH] feat(thread): add submission pending state and loader to composer - Add `submitPending` prop and `PixelLoader` spinner to `ThreadComposer` - Track `isSubmitting` state and `submittedRef` in `ThreadDraftComposerArea` - Disable submit button and prevent draft autosave during submission - Remove immediate clearing of attachments on submit --- src/renderer/components/thread/ThreadComposer.tsx | 7 ++++++- .../components/thread/ThreadDraftComposerArea.tsx | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/thread/ThreadComposer.tsx b/src/renderer/components/thread/ThreadComposer.tsx index cbbae3ae..f01fa3a9 100644 --- a/src/renderer/components/thread/ThreadComposer.tsx +++ b/src/renderer/components/thread/ThreadComposer.tsx @@ -155,6 +155,7 @@ export function ThreadComposer(props: { hideSubmitButton?: boolean; submitLabel: string; submitDisabled: boolean; + submitPending?: boolean; stopPending?: boolean; preserveDisabledControlStyle?: boolean; onPromptChange: (value: string) => void; @@ -178,6 +179,7 @@ export function ThreadComposer(props: { hideSubmitButton = false, submitLabel, submitDisabled, + submitPending = false, stopPending = false, preserveDisabledControlStyle = false, onPromptChange, @@ -569,10 +571,13 @@ export function ThreadComposer(props: { aria-label={submitLabel} className="lightcode-composer-send" isDisabled={submitDisabled || promptDisabled} + isPending={submitPending} onPress={onSubmit} size="sm" > - + {({ isPending }) => + isPending ? : + } ); }; diff --git a/src/renderer/components/thread/ThreadDraftComposerArea.tsx b/src/renderer/components/thread/ThreadDraftComposerArea.tsx index 5485e485..558da1c8 100644 --- a/src/renderer/components/thread/ThreadDraftComposerArea.tsx +++ b/src/renderer/components/thread/ThreadDraftComposerArea.tsx @@ -75,6 +75,7 @@ export function ThreadDraftComposerArea(props: { // launched agent would race with the still-running install and could pick up // either binary, which is a confusing state to debug. const [agentUpdating, setAgentUpdating] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const mentionRef = useRef(null); const attachments = useAttachments(); const inboxKey = props.paneId ?? `draft:${props.project.id}`; @@ -111,6 +112,7 @@ export function ThreadDraftComposerArea(props: { const latestSegmentsRef = useRef([]); const attachmentsRef = useRef(attachments.attachments); attachmentsRef.current = attachments.attachments; + const submittedRef = useRef(false); const initialDraftRef = useRef(useAppStore.getState().draftContents[props.project.id]); const availableCommands = resolveAvailableSlashCommands( undefined, @@ -210,6 +212,8 @@ export function ThreadDraftComposerArea(props: { } resetDraftRefs(); + submittedRef.current = true; + setIsSubmitting(true); const useWorktree = branchSelection?.isWorktree ?? props.worktreeMode; if (props.supportsModePicker) { props.onRememberPresentationMode(); @@ -235,7 +239,6 @@ export function ThreadDraftComposerArea(props: { } : {}), }); - attachments.clearAll(); } useLayoutEffect(() => { @@ -257,6 +260,7 @@ export function ThreadDraftComposerArea(props: { useEffect(() => { const pid = props.project.id; return () => { + if (submittedRef.current) return; const segments = latestSegmentsRef.current; const atts = attachmentsRef.current; if (segments.length > 0 || atts.length > 0) { @@ -382,8 +386,12 @@ export function ThreadDraftComposerArea(props: { placeholder="Send a message..." prompt={prompt} submitDisabled={ - authRequired || agentUpdating || !(hasContent || attachments.attachments.length > 0) + authRequired || + agentUpdating || + isSubmitting || + !(hasContent || attachments.attachments.length > 0) } + submitPending={isSubmitting} submitLabel="Launch thread" onPromptChange={setPrompt} onSubmit={() => {