@@ -17,6 +17,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
1717import { useUniwindTheme } from "../../lib/useUniwindTheme" ;
1818import { useFontFamily } from "../../lib/useFontFamily" ;
1919
20+ import { getProviderAdmissionUnavailableReason } from "@t3tools/client-runtime/providerAvailability" ;
2021import {
2122 isAtomCommandInterrupted ,
2223 squashAtomCommandFailure ,
@@ -38,6 +39,7 @@ import { SymbolView } from "../../components/AppSymbol";
3839import { AppText as Text } from "../../components/AppText" ;
3940import { COMPOSER_LAYOUT_TRANSITION , ComposerSurface } from "./ThreadComposer" ;
4041import { ComposerCommandPopover } from "./ComposerCommandPopover" ;
42+ import { ProviderUnavailableNotice } from "./ProviderUnavailableNotice" ;
4143import { useComposerCommandMenu } from "./use-composer-command-menu" ;
4244import {
4345 ComposerDictationCancelAction ,
@@ -158,6 +160,15 @@ export function NewTaskDraftScreen(props: {
158160 connectedEnvironments . find (
159161 ( environment ) => environment . environmentId === selectedProject . environmentId ,
160162 ) ?. connectionState === "connected" ;
163+ const providerAdmissionReason = getProviderAdmissionUnavailableReason ( {
164+ provider : flow . selectedProviderStatus ,
165+ instanceId : flow . selectedModel ? String ( flow . selectedModel . instanceId ) : undefined ,
166+ providerSnapshotKnown : selectedEnvironmentServerConfig != null ,
167+ } ) ;
168+ const providerUnavailable =
169+ providerAdmissionReason === null
170+ ? null
171+ : { headline : "Unavailable" as const , detail : providerAdmissionReason } ;
161172 const promptInputRef = useRef < ComposerEditorHandle > ( null ) ;
162173 const loadedBranchesProjectKeyRef = useRef < string | null > ( null ) ;
163174 const [ isComposerFocused , setIsComposerFocused ] = useState ( false ) ;
@@ -794,18 +805,20 @@ export function NewTaskDraftScreen(props: {
794805 if ( voiceInput . blocksSubmission ) return ;
795806 const selectedProject = flow . selectedProject ;
796807 const draftKey = flow . draftKey ;
797- if ( ! selectedProject || ! draftKey ) {
808+ if ( ! selectedProject || ! draftKey || providerUnavailable ) {
798809 return ;
799810 }
800811 const draft = getComposerDraftSnapshot ( draftKey ) ;
801- // Snapshot read keeps just-typed selector state; the availability gate
802- // still applies so a stored selection on a disabled provider falls back
803- // to the flow's resolved model .
812+ // Snapshot read keeps just-typed selector state. Ambient stale defaults
813+ // may fall back, but a human/recovered exact provider choice must remain
814+ // blocked rather than silently switch accounts .
804815 const modelSelection =
805- resolveSelectableModelSelection (
806- selectedEnvironmentServerConfig ,
807- draft . modelSelection ?? null ,
808- ) ?? flow . selectedModel ;
816+ draft . providerSelectionExplicit === true && draft . modelSelection !== undefined
817+ ? resolveSelectableModelSelection ( selectedEnvironmentServerConfig , draft . modelSelection )
818+ : ( resolveSelectableModelSelection (
819+ selectedEnvironmentServerConfig ,
820+ draft . modelSelection ?? null ,
821+ ) ?? flow . selectedModel ) ;
809822 const workspaceMode = draft . workspaceSelection ?. mode ?? flow . workspaceMode ;
810823 const selectedBranchName = draft . workspaceSelection ?. branch ?? flow . selectedBranchName ;
811824 const selectedWorktreePath =
@@ -837,26 +850,43 @@ export function NewTaskDraftScreen(props: {
837850 }
838851
839852 const editingPendingTask = flow . editingPendingTask ;
853+ const retryTurnMetadata =
854+ editingPendingTask ?. deliveryHold === undefined ? null : makeTurnCommandMetadata ( ) ;
840855
841856 if ( ! environmentConnected ) {
842857 // Offline: park the task in the outbox; the drain sends it when the
843- // environment reconnects. Editing an existing pending task re-queues it
844- // under its original identifiers.
845- const metadata = editingPendingTask
846- ? {
847- threadId : editingPendingTask . threadId ,
848- commandId : editingPendingTask . commandId ,
849- messageId : editingPendingTask . messageId ,
850- createdAt : editingPendingTask . createdAt ,
851- }
852- : makeTurnCommandMetadata ( ) ;
858+ // environment reconnects. Ordinary edits preserve their identifiers;
859+ // explicitly submitting a held retarget uses the fresh metadata above.
860+ const metadata =
861+ retryTurnMetadata ??
862+ ( editingPendingTask
863+ ? {
864+ threadId : editingPendingTask . threadId ,
865+ commandId : editingPendingTask . commandId ,
866+ messageId : editingPendingTask . messageId ,
867+ createdAt : editingPendingTask . createdAt ,
868+ }
869+ : makeTurnCommandMetadata ( ) ) ;
853870 const message = flow . buildPendingTaskMessage ( metadata ) ;
854871 if ( ! message ) {
855872 return ;
856873 }
857874 flow . setSubmitting ( true ) ;
858875 try {
859876 await enqueueThreadOutboxMessage ( message ) ;
877+ if (
878+ editingPendingTask !== null &&
879+ editingPendingTask . deliveryHold !== undefined &&
880+ editingPendingTask . messageId !== message . messageId
881+ ) {
882+ try {
883+ await removeThreadOutboxMessage ( editingPendingTask ) ;
884+ } catch ( error ) {
885+ // The replacement is already durable and the old entry remains
886+ // held, so neither copy can lose or double-send the content.
887+ console . warn ( "[new-task] failed to remove retargeted held task" , error ) ;
888+ }
889+ }
860890 } catch ( error ) {
861891 Alert . alert (
862892 "Could not queue task" ,
@@ -912,7 +942,7 @@ export function NewTaskDraftScreen(props: {
912942 } ,
913943 ...( editingPendingTask
914944 ? {
915- turnMetadata : {
945+ turnMetadata : retryTurnMetadata ?? {
916946 threadId : editingPendingTask . threadId ,
917947 commandId : editingPendingTask . commandId ,
918948 messageId : editingPendingTask . messageId ,
@@ -972,8 +1002,9 @@ export function NewTaskDraftScreen(props: {
9721002
9731003 const isAndroid = Platform . OS === "android" ;
9741004 const canStart =
975- Boolean ( flow . selectedProject ) &&
1005+ Boolean ( flow . selectedProject ?. workspaceRoot ?. trim ( ) ) &&
9761006 Boolean ( flow . selectedModel ) &&
1007+ providerUnavailable === null &&
9771008 flow . prompt . trim ( ) . length > 0 &&
9781009 isIncomingShareReady &&
9791010 ! isImportingShare &&
@@ -1140,6 +1171,11 @@ export function NewTaskDraftScreen(props: {
11401171 ) : null }
11411172 < View className = "pb-1" > { workspaceControls } </ View >
11421173
1174+ < ProviderUnavailableNotice
1175+ provider = { flow . selectedProviderStatus }
1176+ reason = { providerAdmissionReason }
1177+ />
1178+
11431179 < ComposerSurface
11441180 style = { {
11451181 borderRadius : 26 ,
@@ -1247,11 +1283,15 @@ export function NewTaskDraftScreen(props: {
12471283 { voicePresentation . showsSend ? (
12481284 < ComposerActionButton
12491285 accessibilityLabel = {
1250- flow . submitting
1251- ? "Starting task"
1252- : environmentConnected
1253- ? "Start task"
1254- : "Queue task"
1286+ providerUnavailable
1287+ ? `Start unavailable. ${ providerUnavailable . detail } `
1288+ : flow . submitting
1289+ ? "Starting task"
1290+ : ! canStart
1291+ ? "Start unavailable. Add a message and complete the task setup."
1292+ : environmentConnected
1293+ ? "Start task"
1294+ : "Queue task. The environment is disconnected; this task will remain queued."
12551295 }
12561296 disabled = { ! canStart }
12571297 icon = { environmentConnected ? "arrow.up" : "tray.and.arrow.up" }
0 commit comments