-
Notifications
You must be signed in to change notification settings - Fork 6.5k
fix(mobile): make Stop work during preparing worktree phase #12188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4871e96
30d9c19
cdfeb1d
fde7210
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,6 +137,10 @@ export interface ThreadComposerProps { | |
| readonly onNativePasteImages: (uris: ReadonlyArray<string>) => Promise<void>; | ||
| readonly onRemoveDraftImage: (imageId: string) => void; | ||
| readonly onStopThread: () => void; | ||
| /** Optimistic Stop feedback: disables Stop while the interrupt/cancel settles. */ | ||
| readonly isStoppingThread: boolean; | ||
| /** True while the thread creation is still preparing (e.g. worktree checkout). */ | ||
| readonly isPreparingCreation: boolean; | ||
| readonly onSendMessage: () => Promise<MessageId | null>; | ||
| /** `/usage-limits` resolves locally; the host decides where the report shows. Null clears it. */ | ||
| readonly onShowUsageLimits: (report: UsageLimitsReport | null) => void; | ||
|
|
@@ -308,7 +312,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | |
| const showStopAction = | ||
| !hasContent && | ||
| (props.selectedThread.session?.status === "running" || | ||
| props.selectedThread.session?.status === "starting"); | ||
| props.selectedThread.session?.status === "starting" || | ||
| props.isPreparingCreation); | ||
|
Comment on lines
+315
to
+316
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Keep Stop visible when the preparation draft has content.
Proposed fix const showStopAction =
- !hasContent &&
- (props.selectedThread.session?.status === "running" ||
- props.selectedThread.session?.status === "starting" ||
- props.isPreparingCreation);
+ props.isPreparingCreation ||
+ (!hasContent &&
+ (props.selectedThread.session?.status === "running" ||
+ props.selectedThread.session?.status === "starting"));🤖 Prompt for AI Agents |
||
| const stopAccessibilityLabel = props.isStoppingThread | ||
| ? "Stopping agent" | ||
| : props.isPreparingCreation && | ||
| props.selectedThread.session?.status !== "running" && | ||
| props.selectedThread.session?.status !== "starting" | ||
| ? "Cancel task setup" | ||
| : "Stop agent"; | ||
|
|
||
| const uploadStates = useAtomValue(composerAttachmentUploadsAtom); | ||
| const attachmentsUploading = | ||
|
|
@@ -804,9 +816,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | |
| /> | ||
| {showStopAction ? ( | ||
| <ComposerActionButton | ||
| accessibilityLabel="Stop agent" | ||
| accessibilityLabel={stopAccessibilityLabel} | ||
| icon="stop.fill" | ||
| variant="danger" | ||
| disabled={props.isStoppingThread} | ||
| onPress={props.onStopThread} | ||
| /> | ||
| ) : ( | ||
|
|
@@ -895,9 +908,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | |
| /> | ||
| {showStopAction ? ( | ||
| <ComposerActionButton | ||
| accessibilityLabel="Stop agent" | ||
| accessibilityLabel={stopAccessibilityLabel} | ||
| icon="stop.fill" | ||
| variant="danger" | ||
| disabled={props.isStoppingThread} | ||
| onPress={props.onStopThread} | ||
| /> | ||
| ) : voicePresentation.showsSend ? ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,7 +26,11 @@ import { Alert, Platform, ScrollView, View } from "react-native"; | |||||||||||||||||||||||||||||
| import { useSafeAreaInsets } from "react-native-safe-area-context"; | ||||||||||||||||||||||||||||||
| import { useWorkspaceState } from "../../state/workspace"; | ||||||||||||||||||||||||||||||
| import { restoredNewTaskDraftKey } from "../../state/new-task-draft-key"; | ||||||||||||||||||||||||||||||
| import { clearPendingThreadCreationOutcome } from "../../state/pending-thread-creation"; | ||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||
| clearPendingThreadCreationOutcome, | ||||||||||||||||||||||||||||||
| resolvePreparingStopAction, | ||||||||||||||||||||||||||||||
| } from "../../state/pending-thread-creation"; | ||||||||||||||||||||||||||||||
| import { removeThreadOutboxMessage } from "../../state/thread-outbox-removal"; | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High The cancellation path can report success and navigate Home after the task has already been delivered, hiding a task that 🤖 Copy this AI Prompt to have your agent fix this: |
||||||||||||||||||||||||||||||
| import { recoverFailedThreadDraft } from "../../state/recover-failed-thread-draft"; | ||||||||||||||||||||||||||||||
| import { useEnvironmentQuery } from "../../state/query"; | ||||||||||||||||||||||||||||||
| import { dismissGitActionResult, useGitActionProgress } from "../../state/use-vcs-action-state"; | ||||||||||||||||||||||||||||||
|
|
@@ -498,24 +502,104 @@ function ThreadRouteContent( | |||||||||||||||||||||||||||||
| const handleOpenConnectionEditor = useCallback(() => { | ||||||||||||||||||||||||||||||
| void navigation.navigate("Connections"); | ||||||||||||||||||||||||||||||
| }, [navigation]); | ||||||||||||||||||||||||||||||
| const [isStoppingThread, setIsStoppingThread] = useState(false); | ||||||||||||||||||||||||||||||
| const handleStopThread = useCallback(() => { | ||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||
| !selectedThread || | ||||||||||||||||||||||||||||||
| (selectedThread.session?.status !== "running" && | ||||||||||||||||||||||||||||||
| selectedThread.session?.status !== "starting") | ||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||
| const stopAction = resolvePreparingStopAction({ | ||||||||||||||||||||||||||||||
| isPreparing: | ||||||||||||||||||||||||||||||
| selectedThreadCreation !== null && selectedThreadCreation.outcome?.kind !== "failed", | ||||||||||||||||||||||||||||||
| creationOutcomeKind: | ||||||||||||||||||||||||||||||
| selectedThreadCreation?.outcome?.kind === "delivered" | ||||||||||||||||||||||||||||||
| ? "delivered" | ||||||||||||||||||||||||||||||
| : selectedThreadCreation?.outcome?.kind === "failed" | ||||||||||||||||||||||||||||||
| ? "failed" | ||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||
| creationMessageId: selectedThreadCreation | ||||||||||||||||||||||||||||||
| ? String(selectedThreadCreation.message.messageId) | ||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||
| dispatchingMessageId: composer.dispatchingQueuedMessageId | ||||||||||||||||||||||||||||||
| ? String(composer.dispatchingQueuedMessageId) | ||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||
| sessionStatus: selectedThread?.session?.status ?? null, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| // A still-queued creation has no server turn to interrupt: cancel it | ||||||||||||||||||||||||||||||
| // locally with immediate feedback instead of a silent no-op (#12187). | ||||||||||||||||||||||||||||||
| if (stopAction.kind === "cancel-queued-creation" && selectedThreadCreation) { | ||||||||||||||||||||||||||||||
| const queuedMessage = selectedThreadCreation.message; | ||||||||||||||||||||||||||||||
| const threadKey = routeThreadIdentity; | ||||||||||||||||||||||||||||||
| setIsStoppingThread(true); | ||||||||||||||||||||||||||||||
| void (async () => { | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const removed = await removeThreadOutboxMessage(queuedMessage); | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline apps/mobile/src/state/thread-outbox-removal.ts --items all --view expanded
rg -n -C 8 \
'removeThreadOutboxMessage|expectedRevision|canRemove|threadOutboxManager\.remove|editPendingThreadMessage' \
apps/mobile/srcRepository: pingdotgg/t3code Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- thread-outbox manager removal ---'
rg -n -C 30 'remove\s*\(|async remove|class .*Outbox|revisionOf|editingQueuedMessageIdsAtom' apps/mobile/src/state/thread-outbox.ts apps/mobile/src/state apps/mobile/src/features/threads/ThreadRouteScreen.tsx | head -n 260
printf '%s\n' '--- route cancellation context ---'
sed -n '470,555p' apps/mobile/src/features/threads/ThreadRouteScreen.tsx
printf '%s\n' '--- focused removal tests ---'
sed -n '90,180p' apps/mobile/src/state/thread-outbox-removal.test.ts
sed -n '1010,1080p' apps/mobile/src/state/thread-outbox.test.tsRepository: pingdotgg/t3code Length of output: 33058 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- manager file ---'
fd -i 'thread-outbox-manager' apps/mobile/src --type f
manager_file="$(fd -i 'thread-outbox-manager' apps/mobile/src --type f | head -n 1)"
test -n "$manager_file"
wc -l "$manager_file"
rg -n -C 24 'remove|revision|predicate|expected' "$manager_file" | head -n 260
printf '%s\n' '--- editor ownership and stop state ---'
rg -n -C 18 'holdEditingQueuedMessage|releaseEditingQueuedMessage|editingQueuedMessageIdsAtom|selectedThreadCreation|cancel-queued-creation' \
apps/mobile/src/state apps/mobile/src/features/threads/ThreadRouteScreen.tsx | head -n 320Repository: pingdotgg/t3code Length of output: 42622 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- manager removal completion ---'
sed -n '214,285p' apps/mobile/src/state/thread-outbox-manager.ts
printf '%s\n' '--- outbox editor-state exports ---'
rg -n -C 12 'editingQueuedMessageIdsAtom|holdEditingQueuedMessage|releaseEditingQueuedMessage|threadOutboxRevision' apps/mobile/src/state/use-thread-outbox.ts apps/mobile/src/state/thread-outbox.ts
printf '%s\n' '--- route imports ---'
sed -n '1,125p' apps/mobile/src/features/threads/ThreadRouteScreen.tsx
printf '%s\n' '--- creation editor bindings ---'
rg -n -C 12 'editPendingThreadMessage|queuedMessages|selectedThreadCreation|onEdit|edit.*queued|creation' \
apps/mobile/src/features/threads apps/mobile/src/components apps/mobile/src/state/use-thread-outbox.ts | head -n 320Repository: pingdotgg/t3code Length of output: 44605 Preserve the outbox compare-and-set guards when cancelling. Suggested fix+import { appAtomRegistry } from "../../state/atom-registry";
import { removeThreadOutboxMessage } from "../../state/thread-outbox-removal";
+import { threadOutboxRevision } from "../../state/thread-outbox";
+import { editingQueuedMessageIdsAtom } from "../../state/use-thread-outbox";
...
const queuedMessage = selectedThreadCreation.message;
+ const expectedRevision = threadOutboxRevision(queuedMessage.messageId);
...
- const removed = await removeThreadOutboxMessage(queuedMessage);
+ const removed = await removeThreadOutboxMessage(
+ queuedMessage,
+ expectedRevision,
+ () => !appAtomRegistry.get(editingQueuedMessageIdsAtom)[queuedMessage.messageId],
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| if (!removed) { | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
|
Comment on lines
+533
to
+534
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Report a failed queued-creation removal.
Proposed fix const removed = await removeThreadOutboxMessage(queuedMessage);
if (!removed) {
+ Alert.alert(
+ "Could not cancel task",
+ "Task setup could not be canceled. It may already be starting.",
+ );
return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (threadKey) { | ||||||||||||||||||||||||||||||
| clearPendingThreadCreationOutcome(threadKey); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (navigation.canGoBack()) { | ||||||||||||||||||||||||||||||
| navigation.goBack(); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| navigation.dispatch(StackActions.replace("Home")); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||
| Alert.alert( | ||||||||||||||||||||||||||||||
| "Could not cancel task", | ||||||||||||||||||||||||||||||
| error instanceof Error ? error.message : String(error), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||
| setIsStoppingThread(false); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (stopAction.kind !== "interrupt-running-turn") { | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return interruptThreadTurn({ | ||||||||||||||||||||||||||||||
| if (!selectedThread) { | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| const activeSession = selectedThread.session; | ||||||||||||||||||||||||||||||
| if (!activeSession) { | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| setIsStoppingThread(true); | ||||||||||||||||||||||||||||||
| const interrupt = interruptThreadTurn({ | ||||||||||||||||||||||||||||||
| environmentId: selectedThread.environmentId, | ||||||||||||||||||||||||||||||
| input: { | ||||||||||||||||||||||||||||||
| threadId: selectedThread.id, | ||||||||||||||||||||||||||||||
| ...(selectedThread.session.activeTurnId | ||||||||||||||||||||||||||||||
| ? { turnId: selectedThread.session.activeTurnId } | ||||||||||||||||||||||||||||||
| : {}), | ||||||||||||||||||||||||||||||
| ...(activeSession.activeTurnId ? { turnId: activeSession.activeTurnId } : {}), | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| }, [interruptThreadTurn, selectedThread]); | ||||||||||||||||||||||||||||||
| void interrupt.then((result) => { | ||||||||||||||||||||||||||||||
| if (result._tag === "Failure") { | ||||||||||||||||||||||||||||||
| setIsStoppingThread(false); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| }, [ | ||||||||||||||||||||||||||||||
| composer.dispatchingQueuedMessageId, | ||||||||||||||||||||||||||||||
| interruptThreadTurn, | ||||||||||||||||||||||||||||||
| navigation, | ||||||||||||||||||||||||||||||
| routeThreadIdentity, | ||||||||||||||||||||||||||||||
| selectedThread, | ||||||||||||||||||||||||||||||
| selectedThreadCreation, | ||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||
| // Optimistic Stop feedback clears once work actually ends, when switching | ||||||||||||||||||||||||||||||
| // threads, or immediately on a failed interrupt above (#12187). | ||||||||||||||||||||||||||||||
| const selectedThreadSessionStatus = selectedThread?.session?.status ?? null; | ||||||||||||||||||||||||||||||
| const selectedThreadIdentity = selectedThread | ||||||||||||||||||||||||||||||
| ? scopedThreadKey(selectedThread.environmentId, selectedThread.id) | ||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||
| const previousStopThreadIdentityRef = useRef<string | null>(null); | ||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| if (previousStopThreadIdentityRef.current !== selectedThreadIdentity) { | ||||||||||||||||||||||||||||||
| previousStopThreadIdentityRef.current = selectedThreadIdentity; | ||||||||||||||||||||||||||||||
| setIsStoppingThread(false); | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (selectedThreadSessionStatus !== "running" && selectedThreadSessionStatus !== "starting") { | ||||||||||||||||||||||||||||||
| setIsStoppingThread(false); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }, [selectedThreadIdentity, selectedThreadSessionStatus]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const handleOpenTerminal = useCallback( | ||||||||||||||||||||||||||||||
| (nextTerminalId?: string | null) => { | ||||||||||||||||||||||||||||||
|
|
@@ -894,6 +978,10 @@ function ThreadRouteContent( | |||||||||||||||||||||||||||||
| onRemoveDraftImage={composer.onRemoveDraftImage} | ||||||||||||||||||||||||||||||
| serverConfig={serverConfig} | ||||||||||||||||||||||||||||||
| onStopThread={handleStopThread} | ||||||||||||||||||||||||||||||
| isStoppingThread={isStoppingThread} | ||||||||||||||||||||||||||||||
| isPreparingCreation={ | ||||||||||||||||||||||||||||||
| selectedThreadCreation !== null && selectedThreadCreation.outcome?.kind !== "failed" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| onSendMessage={composer.onSendMessage} | ||||||||||||||||||||||||||||||
| onReconnectEnvironment={handleReconnectEnvironment} | ||||||||||||||||||||||||||||||
| onUpdateThreadModelSelection={composer.onUpdateModelSelection} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,10 @@ import * as Effect from "effect/Effect"; | |
|
|
||
| import { createModelCapabilities } from "@t3tools/shared/model"; | ||
| import { compareSemverVersions } from "@t3tools/shared/semver"; | ||
| import { | ||
| sanitizeTerminalValue, | ||
| stripTerminalEscapes, | ||
| } from "@t3tools/shared/stripTerminalEscapes"; | ||
| import { | ||
| buildServerProvider, | ||
| COMPACT_SLASH_COMMAND, | ||
|
|
@@ -209,15 +213,20 @@ function openCodeCapabilitiesForModel(input: { | |
| // Codex/Grok experience where reasoning is always configurable). The set | ||
| // covers the common OpenCode variant spectrum; `inferDefaultVariant` | ||
| // picks the provider-appropriate default (e.g. medium for openai/opencode). | ||
| const variantValues = | ||
| rawVariantValues.length > 0 ? rawVariantValues : ["low", "medium", "high", "xhigh"]; | ||
| const variantValues = ( | ||
| rawVariantValues.length > 0 ? rawVariantValues : ["low", "medium", "high", "xhigh"] | ||
| ).map(sanitizeTerminalValue); | ||
| const defaultVariant = inferDefaultVariant(input.providerID, variantValues); | ||
| const variantOptions = variantValues.map((value) => | ||
| defaultVariant === value | ||
| ? { id: value, label: titleCaseSlug(value), isDefault: true as const } | ||
| : { id: value, label: titleCaseSlug(value) }, | ||
| ); | ||
| const primaryAgents = input.agents.filter( | ||
| const sanitizedAgents = input.agents.map((agent) => ({ | ||
| ...agent, | ||
| name: sanitizeTerminalValue(agent.name), | ||
|
Comment on lines
+218
to
+227
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '190,250p' apps/server/src/provider/Layers/OpenCodeProvider.ts
rg -n 'variantOptions|agentOptions|sanitizedAgents|openCodeCapabilitiesForModel' apps/server/src/provider/Layers/OpenCodeProvider.ts apps/server/src/provider/Layers/OpenCodeProvider.test.ts
rg -n 'getProviderOptionStringSelectionValue' packages/shared/src apps/server/srcRepository: pingdotgg/t3code Length of output: 4398 🏁 Script executed: #!/bin/bash
rg -n -C 8 'function sanitizeTerminalValue|const sanitizeTerminalValue|sanitizeTerminalValue\s*=|sanitizeTerminalValue' apps/server packages/shared
printf '\n--- selection helper ---\n'
sed -n '1,90p' packages/shared/src/model.ts
printf '\n--- capability construction ---\n'
sed -n '238,275p' apps/server/src/provider/Layers/OpenCodeProvider.ts
rg -n -C 8 'function createModelCapabilities|const createModelCapabilities|createModelCapabilities\s*=|type ModelCapabilities|interface ModelCapabilities' apps/server packages/sharedRepository: pingdotgg/t3code Length of output: 28110 🏁 Script executed: #!/bin/bash
rg -n -C 10 'buildProviderOptionSelectionsFromDescriptors|buildExplicitProviderOptionSelectionsFromDescriptors|resolveDescriptorChoiceValue|getProviderOptionCurrentValue|option\.id|options\.find|options\.map' packages/shared/src/model.ts apps packages | head -n 260Repository: pingdotgg/t3code Length of output: 17993 Filter empty values after sanitization.
Filter empty sanitized values before building 🤖 Prompt for AI Agents |
||
| })); | ||
| const primaryAgents = sanitizedAgents.filter( | ||
| (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"), | ||
| ); | ||
| const defaultAgent = inferDefaultAgent(primaryAgents); | ||
|
|
@@ -447,7 +456,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu | |
| if (versionExit._tag === "Failure") { | ||
| return fallback(Cause.squash(versionExit.cause)); | ||
| } | ||
| version = parseGenericCliVersion(versionExit.value.stdout) ?? null; | ||
| version = parseGenericCliVersion(stripTerminalEscapes(versionExit.value.stdout)) ?? null; | ||
|
|
||
| if (!version) { | ||
| return fallback( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
threads/ThreadComposer.tsx:316The composer shows a “Cancel task setup” button while creation is
deliveredwithsession: null, butresolvePreparingStopActionreturnsnoopfor that state. Tapping the visible stop affordance therefore does nothing during worktree setup; hide or disable it for this action state, or provide a real cancellation path.🤖 Copy this AI Prompt to have your agent fix this: