authorgravatar for chris@xenforo.comChris Deeming <chris@xenforo.com> 2026-08-14 12:35:10+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-14 13:35:10+02:00
log184d8ef33b8f42869fb84f66a33984185b81dc47
tree71b80a6f1b61967865671fadbb6d6f7f3985bd77
parent038560e58036d51b2576b3c2cd9170a194cefe9e
signature Signed by PGP key B5690EEEBB952194

fix(mobile): steer active turns by default (#6543)


7 files changed, 27 insertions(+), 27 deletions(-)

apps/mobile/src/features/threads/ThreadComposer.tsx+1-4
...@@ -105,7 +105,6 @@ export interface ThreadComposerProps {...@@ -105,7 +105,6 @@ export interface ThreadComposerProps {
105 readonly selectedThread: OrchestrationThreadShell;105 readonly selectedThread: OrchestrationThreadShell;
106 readonly serverConfig: T3ServerConfig | null;106 readonly serverConfig: T3ServerConfig | null;
107 readonly queueCount: number;107 readonly queueCount: number;
108 readonly activeThreadBusy: boolean;
109 readonly environmentId: EnvironmentId;108 readonly environmentId: EnvironmentId;
110 readonly projectCwd: string | null;109 readonly projectCwd: string | null;
111 readonly editorRef?: RefObject<ComposerEditorHandle | null>;110 readonly editorRef?: RefObject<ComposerEditorHandle | null>;
...@@ -327,9 +326,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -327,9 +326,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
327 props.selectedThread.session?.status === "starting";326 props.selectedThread.session?.status === "starting";
328327
329 const sendLabel =328 const sendLabel =
330 props.connectionState !== "connected" || props.activeThreadBusy || props.queueCount > 0329 props.connectionState !== "connected" || props.queueCount > 0 ? "Queue" : "Send";
331 ? "Queue"
332 : "Send";
333 const currentModelSelection = props.selectedThread.modelSelection;330 const currentModelSelection = props.selectedThread.modelSelection;
334 const currentRuntimeMode = props.selectedThread.runtimeMode;331 const currentRuntimeMode = props.selectedThread.runtimeMode;
335 const connectionStatus = composerConnectionStatus({332 const connectionStatus = composerConnectionStatus({
apps/mobile/src/features/threads/ThreadDetailScreen.tsx-2
...@@ -101,7 +101,6 @@ export interface ThreadDetailScreenProps {...@@ -101,7 +101,6 @@ export interface ThreadDetailScreenProps {
101 readonly threadSyncStatus?: EnvironmentThreadStatus;101 readonly threadSyncStatus?: EnvironmentThreadStatus;
102 /** Non-null when older turns exist beyond the loaded window. */102 /** Non-null when older turns exist beyond the loaded window. */
103 readonly loadEarlier?: { readonly loading: boolean; readonly onLoadEarlier: () => void } | null;103 readonly loadEarlier?: { readonly loading: boolean; readonly onLoadEarlier: () => void } | null;
104 readonly activeThreadBusy: boolean;
105 readonly environmentId: EnvironmentId;104 readonly environmentId: EnvironmentId;
106 readonly projectWorkspaceRoot: string | null;105 readonly projectWorkspaceRoot: string | null;
107 readonly threadCwd: string | null;106 readonly threadCwd: string | null;
...@@ -727,7 +726,6 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread...@@ -727,7 +726,6 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
727 selectedThread={props.selectedThread}726 selectedThread={props.selectedThread}
728 serverConfig={props.serverConfig}727 serverConfig={props.serverConfig}
729 queueCount={props.selectedThreadQueueCount}728 queueCount={props.selectedThreadQueueCount}
730 activeThreadBusy={props.activeThreadBusy}
731 environmentId={props.environmentId}729 environmentId={props.environmentId}
732 projectCwd={props.projectWorkspaceRoot}730 projectCwd={props.projectWorkspaceRoot}
733 bottomInset={composerBottomInset}731 bottomInset={composerBottomInset}
apps/mobile/src/features/threads/ThreadRouteScreen.tsx-1
...@@ -785,7 +785,6 @@ function ThreadRouteContent(...@@ -785,7 +785,6 @@ function ThreadRouteContent(
785 connectionStateLabel={routeConnectionState}785 connectionStateLabel={routeConnectionState}
786 threadSyncStatus={selectedThreadDetailState.status}786 threadSyncStatus={selectedThreadDetailState.status}
787 loadEarlier={loadEarlierTurns}787 loadEarlier={loadEarlierTurns}
788 activeThreadBusy={composer.activeThreadBusy}
789 environmentId={selectedThread.environmentId}788 environmentId={selectedThread.environmentId}
790 projectWorkspaceRoot={selectedThreadProject?.workspaceRoot ?? null}789 projectWorkspaceRoot={selectedThreadProject?.workspaceRoot ?? null}
791 threadCwd={selectedThreadCwd}790 threadCwd={selectedThreadCwd}
apps/mobile/src/state/thread-outbox-model.ts+1-1
...@@ -169,7 +169,7 @@ export function resolveThreadOutboxDeliveryAction(input: {...@@ -169,7 +169,7 @@ export function resolveThreadOutboxDeliveryAction(input: {
169 if (!input.threadExists) {169 if (!input.threadExists) {
170 return input.shellStatus === "live" ? "remove" : "wait";170 return input.shellStatus === "live" ? "remove" : "wait";
171 }171 }
172 return input.environmentConnected && !input.threadBusy ? "send" : "wait";172 return input.environmentConnected ? "send" : "wait";
173}173}
174174
175/**175/**
apps/mobile/src/state/thread-outbox.test.ts+21
...@@ -487,6 +487,27 @@ describe("thread outbox", () => {...@@ -487,6 +487,27 @@ describe("thread outbox", () => {
487 ).toBe("send");487 ).toBe("send");
488 });488 });
489489
490 it("sends existing-thread messages whenever connected so queued messages can steer", () => {
491 expect(
492 resolveThreadOutboxDeliveryAction({
493 isCreation: false,
494 threadExists: true,
495 shellStatus: "live",
496 environmentConnected: true,
497 threadBusy: true,
498 }),
499 ).toBe("send");
500 expect(
501 resolveThreadOutboxDeliveryAction({
502 isCreation: false,
503 threadExists: true,
504 shellStatus: "live",
505 environmentConnected: false,
506 threadBusy: true,
507 }),
508 ).toBe("wait");
509 });
510
490 it("sends queued creations once connected and live, removing already-created ones", () => {511 it("sends queued creations once connected and live, removing already-created ones", () => {
491 expect(512 expect(
492 resolveThreadOutboxDeliveryAction({513 resolveThreadOutboxDeliveryAction({
apps/mobile/src/state/use-thread-composer-state.ts-5
...@@ -129,10 +129,6 @@ export function useThreadComposerState() {...@@ -129,10 +129,6 @@ export function useThreadComposerState() {
129 );129 );
130 }, [selectedThreadDetail, selectedThreadSessionActivity, selectedThreadShell]);130 }, [selectedThreadDetail, selectedThreadSessionActivity, selectedThreadShell]);
131131
132 const activeThreadBusy =
133 !!selectedThread &&
134 (selectedThread.session?.status === "running" || selectedThread.session?.status === "starting");
135
136 const onSendMessage = useCallback(async () => {132 const onSendMessage = useCallback(async () => {
137 if (!selectedThreadShell) {133 if (!selectedThreadShell) {
138 return null;134 return null;
...@@ -308,7 +304,6 @@ export function useThreadComposerState() {...@@ -308,7 +304,6 @@ export function useThreadComposerState() {
308 modelSelection,304 modelSelection,
309 runtimeMode,305 runtimeMode,
310 interactionMode,306 interactionMode,
311 activeThreadBusy,
312 onChangeDraftMessage,307 onChangeDraftMessage,
313 onPickDraftImages,308 onPickDraftImages,
314 onPasteIntoDraft,309 onPasteIntoDraft,
apps/mobile/src/state/use-thread-outbox-drain.ts+4-14
...@@ -37,7 +37,7 @@ import {...@@ -37,7 +37,7 @@ import {
37 type QueuedThreadMessage,37 type QueuedThreadMessage,
38 type ThreadOutboxCommandStage,38 type ThreadOutboxCommandStage,
39} from "./thread-outbox-model";39} from "./thread-outbox-model";
40import { environmentThreadShells, threadEnvironment } from "./threads";40import { threadEnvironment } from "./threads";
41import { useAtomCommand } from "./use-atom-command";41import { useAtomCommand } from "./use-atom-command";
42import {42import {
43 editingQueuedMessageIdsAtom,43 editingQueuedMessageIdsAtom,
...@@ -362,22 +362,12 @@ export function useThreadOutboxDrain(): void {...@@ -362,22 +362,12 @@ export function useThreadOutboxDrain(): void {
362 return true;362 return true;
363 }363 }
364 // The guards evaluated before the confirmation await are stale by now:364 // The guards evaluated before the confirmation await are stale by now:
365 // the thread may have gone busy, or the user may have opened this365 // the user may have opened this message in the editor. Re-read that
366 // message in the editor. Re-read both and defer to the next drain pass366 // guard and defer to the next drain pass (returning true skips the
367 // (returning true skips the failure/backoff path) rather than sending367 // failure/backoff path) rather than sending a payload being edited.
368 // a payload the user is editing or racing an active turn.
369 if (appAtomRegistry.get(editingQueuedMessageIdsAtom)[nextQueuedMessage.messageId]) {368 if (appAtomRegistry.get(editingQueuedMessageIdsAtom)[nextQueuedMessage.messageId]) {
370 return true;369 return true;
371 }370 }
372 const freshThread = findThread(
373 appAtomRegistry.get(environmentThreadShells.threadShellsAtom),
374 nextQueuedMessage,
375 );
376 const freshThreadBusy =
377 freshThread?.session?.status === "running" || freshThread?.session?.status === "starting";
378 if (deliveryAction === "send" && creation === undefined && freshThreadBusy) {
379 return true;
380 }
381 return deliveryAction === "remove"371 return deliveryAction === "remove"
382 ? removeQueuedMessage("[thread-outbox] failed to remove message for a missing thread")372 ? removeQueuedMessage("[thread-outbox] failed to remove message for a missing thread")
383 : creation !== undefined373 : creation !== undefined