@@ -16,6 +16,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
1616import { useUniwindTheme } from "../../lib/useUniwindTheme" ;
1717import { useFontFamily } from "../../lib/useFontFamily" ;
1818
19+ import { getProviderAdmissionUnavailableReason } from "@t3tools/client-runtime/providerAvailability" ;
1920import {
2021 isAtomCommandInterrupted ,
2122 squashAtomCommandFailure ,
@@ -35,6 +36,7 @@ import { SymbolView } from "../../components/AppSymbol";
3536import { AppText as Text } from "../../components/AppText" ;
3637import { ComposerSurface } from "./ThreadComposer" ;
3738import { ComposerCommandPopover } from "./ComposerCommandPopover" ;
39+ import { ProviderUnavailableNotice } from "./ProviderUnavailableNotice" ;
3840import { useComposerCommandMenu } from "./use-composer-command-menu" ;
3941import {
4042 useThreadSettingsSheetPresentation ,
@@ -137,6 +139,15 @@ export function NewTaskDraftScreen(props: {
137139 connectedEnvironments . find (
138140 ( environment ) => environment . environmentId === selectedProject . environmentId ,
139141 ) ?. connectionState === "connected" ;
142+ const providerAdmissionReason = getProviderAdmissionUnavailableReason ( {
143+ provider : flow . selectedProviderStatus ,
144+ instanceId : flow . selectedModel ? String ( flow . selectedModel . instanceId ) : undefined ,
145+ providerSnapshotKnown : selectedEnvironmentServerConfig != null ,
146+ } ) ;
147+ const providerUnavailable =
148+ providerAdmissionReason === null
149+ ? null
150+ : { headline : "Unavailable" as const , detail : providerAdmissionReason } ;
140151 const promptInputRef = useRef < ComposerEditorHandle > ( null ) ;
141152 const loadedBranchesProjectKeyRef = useRef < string | null > ( null ) ;
142153 const [ isComposerFocused , setIsComposerFocused ] = useState ( false ) ;
@@ -666,7 +677,7 @@ export function NewTaskDraftScreen(props: {
666677 async function handleStart ( ) : Promise < void > {
667678 const selectedProject = flow . selectedProject ;
668679 const draftKey = flow . draftKey ;
669- if ( ! selectedProject || ! draftKey ) {
680+ if ( ! selectedProject || ! draftKey || providerUnavailable ) {
670681 return ;
671682 }
672683 const draft = getComposerDraftSnapshot ( draftKey ) ;
@@ -699,26 +710,43 @@ export function NewTaskDraftScreen(props: {
699710 }
700711
701712 const editingPendingTask = flow . editingPendingTask ;
713+ const retryTurnMetadata =
714+ editingPendingTask ?. deliveryHold === undefined ? null : makeTurnCommandMetadata ( ) ;
702715
703716 if ( ! environmentConnected ) {
704717 // Offline: park the task in the outbox; the drain sends it when the
705- // environment reconnects. Editing an existing pending task re-queues it
706- // under its original identifiers.
707- const metadata = editingPendingTask
708- ? {
709- threadId : editingPendingTask . threadId ,
710- commandId : editingPendingTask . commandId ,
711- messageId : editingPendingTask . messageId ,
712- createdAt : editingPendingTask . createdAt ,
713- }
714- : makeTurnCommandMetadata ( ) ;
718+ // environment reconnects. Ordinary edits preserve their identifiers;
719+ // explicitly submitting a held retarget uses the fresh metadata above.
720+ const metadata =
721+ retryTurnMetadata ??
722+ ( editingPendingTask
723+ ? {
724+ threadId : editingPendingTask . threadId ,
725+ commandId : editingPendingTask . commandId ,
726+ messageId : editingPendingTask . messageId ,
727+ createdAt : editingPendingTask . createdAt ,
728+ }
729+ : makeTurnCommandMetadata ( ) ) ;
715730 const message = flow . buildPendingTaskMessage ( metadata ) ;
716731 if ( ! message ) {
717732 return ;
718733 }
719734 flow . setSubmitting ( true ) ;
720735 try {
721736 await enqueueThreadOutboxMessage ( message ) ;
737+ if (
738+ editingPendingTask !== null &&
739+ editingPendingTask . deliveryHold !== undefined &&
740+ editingPendingTask . messageId !== message . messageId
741+ ) {
742+ try {
743+ await removeThreadOutboxMessage ( editingPendingTask ) ;
744+ } catch ( error ) {
745+ // The replacement is already durable and the old entry remains
746+ // held, so neither copy can lose or double-send the content.
747+ console . warn ( "[new-task] failed to remove retargeted held task" , error ) ;
748+ }
749+ }
722750 } catch ( error ) {
723751 Alert . alert (
724752 "Could not queue task" ,
@@ -770,7 +798,7 @@ export function NewTaskDraftScreen(props: {
770798 initialAttachments : draft . attachments ,
771799 ...( editingPendingTask
772800 ? {
773- turnMetadata : {
801+ turnMetadata : retryTurnMetadata ?? {
774802 threadId : editingPendingTask . threadId ,
775803 commandId : editingPendingTask . commandId ,
776804 messageId : editingPendingTask . messageId ,
@@ -830,8 +858,9 @@ export function NewTaskDraftScreen(props: {
830858
831859 const isAndroid = Platform . OS === "android" ;
832860 const canStart =
833- Boolean ( flow . selectedProject ) &&
861+ Boolean ( flow . selectedProject ?. workspaceRoot ?. trim ( ) ) &&
834862 Boolean ( flow . selectedModel ) &&
863+ providerUnavailable === null &&
835864 flow . prompt . trim ( ) . length > 0 &&
836865 isIncomingShareReady &&
837866 ! isImportingShare &&
@@ -997,6 +1026,11 @@ export function NewTaskDraftScreen(props: {
9971026 ) : null }
9981027 < View className = "pb-1" > { workspaceControls } </ View >
9991028
1029+ < ProviderUnavailableNotice
1030+ provider = { flow . selectedProviderStatus }
1031+ reason = { providerAdmissionReason }
1032+ />
1033+
10001034 < ComposerSurface
10011035 animateLayout = { false }
10021036 style = { {
@@ -1062,7 +1096,15 @@ export function NewTaskDraftScreen(props: {
10621096 </ ComposerToolbarScroller >
10631097 < ComposerToolbarButton
10641098 accessibilityLabel = {
1065- flow . submitting ? "Starting task" : environmentConnected ? "Start task" : "Queue task"
1099+ providerUnavailable
1100+ ? `Start unavailable. ${ providerUnavailable . detail } `
1101+ : flow . submitting
1102+ ? "Starting task"
1103+ : ! canStart
1104+ ? "Start unavailable. Add a message and complete the task setup."
1105+ : environmentConnected
1106+ ? "Start task"
1107+ : "Queue task. The environment is disconnected; this task will remain queued."
10661108 }
10671109 disabled = { ! canStart }
10681110 icon = { environmentConnected ? "arrow.up" : "tray.and.arrow.up" }
0 commit comments