From ce34ac87ead1db5911b09371a97ea6b836495775 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 08:14:48 +0000 Subject: [PATCH 1/7] test(server): a mod pane requesting focus must fail the Claude turn (#17531) Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index b231fbfca9d98..fede336bc56ce 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7431,6 +7431,83 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + // regression: #17531 a mod that holds a tool behind a focused pane must fail the turn, not hang it + it.effect("fails a turn whose mod requests focus for a pane T3 cannot show", () => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + const now = yield* DateTime.now; + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make("attempt-claude-mod-pane-focus"), + text: "Run rm -r build", + attachments: [], + }), + ); + // Captured from Claude 2.1.303 with the blast-radius mod holding a Bash + // call: the mod waits for keyboard input in its pane and nothing else + // arrives until it gives up. + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "ui_panes", + panes: [ + { id: "blast-radius", title: "Blast Radius", plugin: "blast-radius", rows: 10 }, + ], + shown_id: "blast-radius", + focused_id: null, + focus_requested_id: "blast-radius", + uuid: "00000000-0000-4000-8000-000000000795", + session_id: WAKE_NATIVE_SESSION, + }), + ); + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "ui_invalidate", + event: "ui.render", + uuid: "00000000-0000-4000-8000-000000000796", + session_id: WAKE_NATIVE_SESSION, + }), + ); + // Frames are handled in order, so a later notice proves the pane frame + // was handled without the turn ending. + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "informational", + content: "Processing barrier.", + level: "warning", + uuid: "00000000-0000-4000-8000-000000000797", + session_id: WAKE_NATIVE_SESSION, + }), + ); + yield* harness.until( + () => + harness.terminalEvents().length > 0 || + harness.events.some( + (event) => + event.type === "turn_item.updated" && event.turnItem.type === "system_notice", + ), + ); + const [terminal] = harness.terminalEvents(); + assert.equal(terminal?.status, "failed"); + if (terminal?.status !== "failed") return; + assert.include(terminal.failure.message, "blast-radius"); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + // regression: #16897 a prompt whose uuid the session already holds must not hang or end early it.effect("keeps a started prompt running until its result", () => Effect.scoped( From 45b81ecf16500280916bec7fb663ffc719630b8f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 02:36:01 +0000 Subject: [PATCH 2/7] fix(server): stop Claude turns that wait for unsupported mod panes Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 18 +++++++-- .../Adapters/ClaudeAdapterV2.ts | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index fede336bc56ce..ba4b80aaf269e 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7432,10 +7432,16 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); // regression: #17531 a mod that holds a tool behind a focused pane must fail the turn, not hang it - it.effect("fails a turn whose mod requests focus for a pane T3 cannot show", () => + it.effect.each([ + { case: "fails a turn whose mod requests focus for a pane T3 cannot show", focus: true }, + { case: "keeps a turn running beside a passive mod pane", focus: false }, + ] as const)("$case", ({ focus }) => Effect.scoped( Effect.gen(function* () { - const harness = yield* makeWakeHarness; + const interrupted = yield* Deferred.make(); + const harness = yield* makeWakeHarnessWithOptions({ + interrupt: Deferred.succeed(interrupted, undefined), + }); const now = yield* DateTime.now; yield* harness.runtime.startTurn( makeClaudeTestTurnInput({ @@ -7460,7 +7466,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { ], shown_id: "blast-radius", focused_id: null, - focus_requested_id: "blast-radius", + focus_requested_id: focus ? "blast-radius" : null, uuid: "00000000-0000-4000-8000-000000000795", session_id: WAKE_NATIVE_SESSION, }), @@ -7497,9 +7503,15 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); const [terminal] = harness.terminalEvents(); + if (!focus) { + assert.equal(terminal, undefined); + return; + } assert.equal(terminal?.status, "failed"); if (terminal?.status !== "failed") return; assert.include(terminal.failure.message, "blast-radius"); + // The held tool call is abandoned by interrupting the query, never answered. + yield* Deferred.await(interrupted); }).pipe( Effect.provide( Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 97ebde00ac277..fe63dead39e73 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3730,6 +3730,20 @@ function claudeTaskString(value: unknown, key: string): string | undefined { return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined; } +// A Claude Code mod can hold a tool call until the user acts in its terminal +// pane. T3 shows no panes, so a focus request would wait forever (#17531). +// Passive panes never request focus and keep working. +function claudeFocusRequestedPanePlugin(message: SDKMessage): string | null { + if (message.type !== "system" || claudeTaskField(message, "subtype") !== "ui_panes") return null; + const paneId = claudeTaskString(message, "focus_requested_id"); + if (paneId === undefined) return null; + const panes = claudeTaskField(message, "panes"); + const pane = Array.isArray(panes) + ? panes.find((entry) => claudeTaskField(entry, "id") === paneId) + : undefined; + return claudeTaskString(pane, "plugin") ?? paneId; +} + function claudeTaskStatus(value: unknown): OrchestrationV2PlanStep["status"] | undefined { switch (value) { case "pending": @@ -7568,6 +7582,32 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( } return; } + const focusedPlugin = claudeFocusRequestedPanePlugin(message); + if (focusedPlugin !== null && context === (yield* Ref.get(activeTurn))) { + yield* Effect.logWarning("orchestration-v2.claude-mod-pane-focus-unsupported", { + providerTurnId: context.providerTurnId, + plugin: focusedPlugin, + }); + liveQuery.stopping = true; + yield* finalizeActiveTurn({ + context, + status: "failed", + completedAt: yield* DateTime.now, + failure: makeProviderFailure({ + class: "provider_error", + code: "claude_mod_ui_unsupported", + message: `The Claude Code mod "${focusedPlugin}" is waiting for input in its terminal pane, which T3 Code cannot show. Disable the mod for sessions run from T3 Code, or run the task in the Claude Code terminal.`, + }), + }); + // Interrupting abandons the held tool call without answering it. + // Forked because closing waits for the reader running this frame. + yield* liveQuery.query.interrupt.pipe( + Effect.andThen(liveQuery.query.close), + Effect.ignore, + Effect.forkIn(sessionScope), + ); + return; + } yield* trackClaudeGoal({ nativeThreadId: liveQuery.nativeThreadId, context, message }); if (message.type === "stream_event" && !message.parent_tool_use_id) { From 13dbd8edc98edaf8cc447a58cfb3e94e835c20f6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 03:06:13 +0000 Subject: [PATCH 3/7] fix(server): replace Claude queries stopped by unsupported mod input --- .../Adapters/ClaudeAdapterV2.test.ts | 85 ++++++++++++++++++- .../Adapters/ClaudeAdapterV2.ts | 12 ++- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index ba4b80aaf269e..db9668ca9c838 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7438,9 +7438,9 @@ describe("ClaudeAdapterV2 background wake turns", () => { ] as const)("$case", ({ focus }) => Effect.scoped( Effect.gen(function* () { - const interrupted = yield* Deferred.make(); + const closed = yield* Deferred.make(); const harness = yield* makeWakeHarnessWithOptions({ - interrupt: Deferred.succeed(interrupted, undefined), + close: () => Deferred.succeed(closed, undefined), }); const now = yield* DateTime.now; yield* harness.runtime.startTurn( @@ -7510,8 +7510,85 @@ describe("ClaudeAdapterV2 background wake turns", () => { assert.equal(terminal?.status, "failed"); if (terminal?.status !== "failed") return; assert.include(terminal.failure.message, "blast-radius"); - // The held tool call is abandoned by interrupting the query, never answered. - yield* Deferred.await(interrupted); + // The held tool call is abandoned by closing the query, never answered. + yield* Deferred.await(closed); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + + // regression: #17531 a follow-up sent while the failed mod turn's process closes gets a new process + it.effect("runs a follow-up in a fresh query while the mod-blocked query is closing", () => + Effect.scoped( + Effect.gen(function* () { + const releaseClose = yield* Deferred.make(); + const replacementClosedOld = yield* Deferred.make(); + let oldCloses = 0; + const harness = yield* makeWakeHarnessWithOptions({ + // The held CLI never answers an interrupt; closure must not need one. + interrupt: Effect.never, + close: (sdkMessages) => + Effect.suspend(() => + ++oldCloses === 2 ? Deferred.succeed(replacementClosedOld, undefined) : Effect.void, + ).pipe( + Effect.andThen(Deferred.await(releaseClose)), + Effect.andThen(Queue.shutdown(sdkMessages)), + ), + freshQueueOnReopen: true, + }); + // Added after the harness so it runs first: a failed assertion must not + // leave teardown waiting on the held close. + yield* Effect.addFinalizer(() => Deferred.succeed(releaseClose, undefined)); + const now = yield* DateTime.now; + const turnInput = makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make("attempt-claude-mod-pane-blocked"), + text: "Run rm -r build", + attachments: [], + }); + yield* harness.runtime.startTurn(turnInput); + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "ui_panes", + panes: [ + { id: "blast-radius", title: "Blast Radius", plugin: "blast-radius", rows: 10 }, + ], + shown_id: "blast-radius", + focused_id: null, + focus_requested_id: "blast-radius", + uuid: "00000000-0000-4000-8000-000000000798", + session_id: WAKE_NATIVE_SESSION, + }), + ); + assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "failed"); + + const followUp = yield* harness.runtime + .startTurn({ + ...turnInput, + attemptId: RunAttemptId.make("attempt-claude-after-mod-pane"), + providerTurnOrdinal: 2, + }) + .pipe(Effect.forkScoped); + // Reusing the closing process would offer the prompt to it instead. + yield* Effect.raceFirst( + Deferred.await(replacementClosedOld), + harness.until(() => harness.offeredMessages.length >= 2), + ); + assert.isTrue(yield* Deferred.isDone(replacementClosedOld)); + yield* Deferred.succeed(releaseClose, undefined); + yield* Fiber.join(followUp); + assert.equal(harness.processQueues.length, 2); + yield* harness.offerAndWait( + makeResultFrame({ uuid: "after-mod-pane-result", result: "Done." }), + ); + assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed"); }).pipe( Effect.provide( Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index fe63dead39e73..a46b9426a0e48 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -7599,13 +7599,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( message: `The Claude Code mod "${focusedPlugin}" is waiting for input in its terminal pane, which T3 Code cannot show. Disable the mod for sessions run from T3 Code, or run the task in the Claude Code terminal.`, }), }); - // Interrupting abandons the held tool call without answering it. - // Forked because closing waits for the reader running this frame. - yield* liveQuery.query.interrupt.pipe( - Effect.andThen(liveQuery.query.close), - Effect.ignore, - Effect.forkIn(sessionScope), - ); + // Closing ends the CLI process and abandons the held tool call + // without answering it. Not interrupt: the held CLI may never ack it. + yield* liveQuery.query.close.pipe(Effect.ignore); return; } yield* trackClaudeGoal({ nativeThreadId: liveQuery.nativeThreadId, context, message }); @@ -9211,6 +9207,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( if ( existing !== null && existing.nativeThreadId === nativeThreadId && + !existing.stopping && isClaudeProviderContinuationTurn(turnInput) ) { return existing; @@ -9242,6 +9239,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( existing !== null && existing.nativeThreadId === nativeThreadId && existing.queryPolicyKey === queryPolicyKey && + !existing.stopping && existing.selection.queryIdentity === compiledSelection.queryIdentity ) { // A denied ExitPlanMode leaves Claude in plan mode. Restore only from plan: From 81ca23875b1e6fb0bac6d30849856a34fc132153 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 03:15:23 +0000 Subject: [PATCH 4/7] test(server): seed the Claude mod hold reproduction --- scripts/fixtures/issue-17531/seed.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/fixtures/issue-17531/seed.py diff --git a/scripts/fixtures/issue-17531/seed.py b/scripts/fixtures/issue-17531/seed.py new file mode 100644 index 0000000000000..7cc08ce03505d --- /dev/null +++ b/scripts/fixtures/issue-17531/seed.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Create an isolated Bash/mod reproduction; configure provider credentials separately.""" + +import argparse +import os +from pathlib import Path +import subprocess + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("directory", type=Path, help="new disposable directory") +args = parser.parse_args() +root = args.directory.resolve() +root.mkdir(parents=True, exist_ok=False) +project = root / "project" +(project / "build").mkdir(parents=True) +(project / "build" / "sample.txt").write_text("fixture\n") +subprocess.run(["git", "init", str(project)], check=True) +config = root / "claude" +config.mkdir(mode=0o700) +source = root / "playground" +subprocess.run(["git", "clone", "--no-checkout", "https://github.com/anthropics/claude-code-playground.git", str(source)], check=True) +subprocess.run(["git", "-C", str(source), "checkout", "569c5283d9a0a7ee7938df85bb32e4f48cbb8c86"], check=True) +environment = {**os.environ, "CLAUDE_CONFIG_DIR": str(config)} +subprocess.run(["claude", "plugin", "marketplace", "add", str(source / "claude-code" / "mods")], env=environment, check=True) +subprocess.run(["claude", "plugin", "install", "blast-radius@claude-code-playground-mods", "--scope", "user"], env=environment, check=True) +print(f"Project: {project}\nCLAUDE_CONFIG_DIR: {config}") +print("In T3, select this Claude config and ask: Use Bash to run exactly rm -r build. Do not add -f.") From 4290a617fb2888145d436e33dd40ca2c81544916 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 03:18:39 +0000 Subject: [PATCH 5/7] test(mobile): make the mod error presentation fixture reproducible --- scripts/fixtures/issue-17531/seed-native.py | 96 +++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 scripts/fixtures/issue-17531/seed-native.py diff --git a/scripts/fixtures/issue-17531/seed-native.py b/scripts/fixtures/issue-17531/seed-native.py new file mode 100644 index 0000000000000..b997bd535e054 --- /dev/null +++ b/scripts/fixtures/issue-17531/seed-native.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Create an offline native presentation fixture in an isolated checkout only. +Usage: python seed-native-fixture.py /absolute/isolated/checkout +Does not change the renderer, use a server, or seed a live database. +""" +import json, subprocess, sys +from pathlib import Path +root=Path(sys.argv[1]).resolve() +if root.name != 'issue-17531-native-evidence': raise SystemExit('expected dedicated evidence checkout') +sha=subprocess.check_output(['git','rev-parse','HEAD'],cwd=root,text=True).strip() +if sha != '26c3625f484eadd4d5b6ca5a3dd1f8f8bd5533b4': raise SystemExit('unexpected source SHA') +mobile=root/'apps/mobile' +fixture=r'''import { useEffect } from "react"; +import { View, StatusBar, ScrollView } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; +import { NavigationContainer } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { RegistryContext } from "@effect/atom-react"; +import * as DateTime from "effect/DateTime"; +import { EnvironmentId, ThreadId, TurnItemId, RunId, type OrchestrationV2ProjectedTurnItem } from "@t3tools/contracts"; +import { AppearancePreferencesProvider, useAppearancePreferences } from "./src/features/settings/appearance/AppearancePreferencesProvider"; +import { appAtomRegistry } from "./src/state/atom-registry"; +import { ThreadWorkLog } from "./src/features/threads/thread-work-log"; +import { buildThreadFeed } from "./src/lib/threadActivity"; +import { deriveThreadWorkLogSizing } from "./src/lib/layout"; +import { AppText as Text } from "./src/components/ui/AppText"; +import "./global.css"; + +const failure = { + class: "provider_error" as const, + code: "claude_mod_ui_unsupported", + message: 'The Claude Code mod "blast-radius" is waiting for input in its terminal pane, which T3 Code cannot show. Disable the mod for sessions run from T3 Code, or run the task in the Claude Code terminal.', + retryable: null, +}; +const threadId = ThreadId.make("issue17531-static-fixture"); +const at = DateTime.makeUnsafe("2026-10-24T09:41:00.000Z"); +const projected: OrchestrationV2ProjectedTurnItem = { + position: 0, visibility: "local", sourceThreadId: threadId, + sourceItemId: TurnItemId.make("issue17531-failure"), + item: { + id: TurnItemId.make("issue17531-failure"), threadId, + runId: RunId.make("issue17531-fixture-run"), nodeId: null, + providerThreadId: null, providerTurnId: null, nativeItemRef: null, + parentItemId: null, ordinal: 0, status: "failed", title: "Provider error", + startedAt: at, completedAt: at, updatedAt: at, + type: "error", failure, + }, +}; +const activities = buildThreadFeed([projected]).flatMap(entry => entry.type === "activity-group" ? entry.activities : []); +const Stack = createNativeStackNavigator(); +function FailureFixture() { + const appearance = useAppearancePreferences(); + useEffect(() => { appearance.setThemeMode("dark"); }, []); + return + + + + STATIC FIXTURE · Issue 17531 + Current native failure renderer · mobile 26c3625f48 + Presentation only; no provider turn was executed. + + {}} onToggleRow={() => {}} renderImage={() => null} + renderReasoning={text => {text}} onPressPreview={() => {}} /> + Fixture failure.code: {failure.code} + + ; +} +export default function NativeEvidenceFixture() { + return + + + + + + ; +} +''' +(mobile/'issue17531-fixture.tsx').write_text(fixture) +entry=mobile/'index.ts' +source=subprocess.check_output(['git','show','HEAD:apps/mobile/index.ts'],cwd=root,text=True) +entry.write_text(source.replace('import App from "./src/App";', 'import App from "./issue17531-fixture";')) +config=json.loads((mobile/'eas.json').read_text()) +config['build']['issue17531-evidence']={ + 'extends':'preview', 'developmentClient':False, 'channel':'issue17531-evidence', + 'autoIncrement':False, 'env':{'APP_VARIANT':'preview','EXPO_PUBLIC_SHOWCASE':'1','NODE_OPTIONS':'--max-old-space-size=4096'}, + 'ios':{'simulator':True,'image':'macos-tahoe-26.6-xcode-27.1'} +} +(mobile/'eas.json').write_text(json.dumps(config,indent=2)+'\n') +ignore=mobile/'.gitignore' +text=ignore.read_text() if ignore.exists() else '' +if '.env.eas-simulator' not in text: ignore.write_text(text+'\n.env.eas-simulator\n') +print('offline fixture injected; renderer unchanged; no server/data writes') From e703b2d92342a5b034bffab624e40eac61ed6455 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 03:19:56 +0000 Subject: [PATCH 6/7] docs: clarify unsupported pane interaction and fixture usage --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 2 +- scripts/fixtures/issue-17531/seed-native.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 9518186829eda..94e5663bfbc44 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3765,7 +3765,7 @@ function claudeTaskString(value: unknown, key: string): string | undefined { } // A Claude Code mod can hold a tool call until the user acts in its terminal -// pane. T3 shows no panes, so a focus request would wait forever (#17531). +// pane. T3 cannot provide the requested pane interaction (#17531). // Passive panes never request focus and keep working. function claudeFocusRequestedPanePlugin(message: SDKMessage): string | null { if (message.type !== "system" || claudeTaskField(message, "subtype") !== "ui_panes") return null; diff --git a/scripts/fixtures/issue-17531/seed-native.py b/scripts/fixtures/issue-17531/seed-native.py index b997bd535e054..2914613f8655b 100644 --- a/scripts/fixtures/issue-17531/seed-native.py +++ b/scripts/fixtures/issue-17531/seed-native.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Create an offline native presentation fixture in an isolated checkout only. -Usage: python seed-native-fixture.py /absolute/isolated/checkout +Usage: python seed-native.py /absolute/isolated/checkout Does not change the renderer, use a server, or seed a live database. """ import json, subprocess, sys From 69b6a99d7585b4fadd083ae6a21b4dbb775178b7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 12:17:42 +0000 Subject: [PATCH 7/7] chore: drop issue-17531 reproduction-only fixture scripts They are reproduction aids no test reads; the PR body links uploaded copies. Co-Authored-By: Claude Opus 5.5 --- scripts/fixtures/issue-17531/seed-native.py | 96 --------------------- scripts/fixtures/issue-17531/seed.py | 27 ------ 2 files changed, 123 deletions(-) delete mode 100644 scripts/fixtures/issue-17531/seed-native.py delete mode 100644 scripts/fixtures/issue-17531/seed.py diff --git a/scripts/fixtures/issue-17531/seed-native.py b/scripts/fixtures/issue-17531/seed-native.py deleted file mode 100644 index 2914613f8655b..0000000000000 --- a/scripts/fixtures/issue-17531/seed-native.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -"""Create an offline native presentation fixture in an isolated checkout only. -Usage: python seed-native.py /absolute/isolated/checkout -Does not change the renderer, use a server, or seed a live database. -""" -import json, subprocess, sys -from pathlib import Path -root=Path(sys.argv[1]).resolve() -if root.name != 'issue-17531-native-evidence': raise SystemExit('expected dedicated evidence checkout') -sha=subprocess.check_output(['git','rev-parse','HEAD'],cwd=root,text=True).strip() -if sha != '26c3625f484eadd4d5b6ca5a3dd1f8f8bd5533b4': raise SystemExit('unexpected source SHA') -mobile=root/'apps/mobile' -fixture=r'''import { useEffect } from "react"; -import { View, StatusBar, ScrollView } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; -import { NavigationContainer } from "@react-navigation/native"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { RegistryContext } from "@effect/atom-react"; -import * as DateTime from "effect/DateTime"; -import { EnvironmentId, ThreadId, TurnItemId, RunId, type OrchestrationV2ProjectedTurnItem } from "@t3tools/contracts"; -import { AppearancePreferencesProvider, useAppearancePreferences } from "./src/features/settings/appearance/AppearancePreferencesProvider"; -import { appAtomRegistry } from "./src/state/atom-registry"; -import { ThreadWorkLog } from "./src/features/threads/thread-work-log"; -import { buildThreadFeed } from "./src/lib/threadActivity"; -import { deriveThreadWorkLogSizing } from "./src/lib/layout"; -import { AppText as Text } from "./src/components/ui/AppText"; -import "./global.css"; - -const failure = { - class: "provider_error" as const, - code: "claude_mod_ui_unsupported", - message: 'The Claude Code mod "blast-radius" is waiting for input in its terminal pane, which T3 Code cannot show. Disable the mod for sessions run from T3 Code, or run the task in the Claude Code terminal.', - retryable: null, -}; -const threadId = ThreadId.make("issue17531-static-fixture"); -const at = DateTime.makeUnsafe("2026-10-24T09:41:00.000Z"); -const projected: OrchestrationV2ProjectedTurnItem = { - position: 0, visibility: "local", sourceThreadId: threadId, - sourceItemId: TurnItemId.make("issue17531-failure"), - item: { - id: TurnItemId.make("issue17531-failure"), threadId, - runId: RunId.make("issue17531-fixture-run"), nodeId: null, - providerThreadId: null, providerTurnId: null, nativeItemRef: null, - parentItemId: null, ordinal: 0, status: "failed", title: "Provider error", - startedAt: at, completedAt: at, updatedAt: at, - type: "error", failure, - }, -}; -const activities = buildThreadFeed([projected]).flatMap(entry => entry.type === "activity-group" ? entry.activities : []); -const Stack = createNativeStackNavigator(); -function FailureFixture() { - const appearance = useAppearancePreferences(); - useEffect(() => { appearance.setThemeMode("dark"); }, []); - return - - - - STATIC FIXTURE · Issue 17531 - Current native failure renderer · mobile 26c3625f48 - Presentation only; no provider turn was executed. - - {}} onToggleRow={() => {}} renderImage={() => null} - renderReasoning={text => {text}} onPressPreview={() => {}} /> - Fixture failure.code: {failure.code} - - ; -} -export default function NativeEvidenceFixture() { - return - - - - - - ; -} -''' -(mobile/'issue17531-fixture.tsx').write_text(fixture) -entry=mobile/'index.ts' -source=subprocess.check_output(['git','show','HEAD:apps/mobile/index.ts'],cwd=root,text=True) -entry.write_text(source.replace('import App from "./src/App";', 'import App from "./issue17531-fixture";')) -config=json.loads((mobile/'eas.json').read_text()) -config['build']['issue17531-evidence']={ - 'extends':'preview', 'developmentClient':False, 'channel':'issue17531-evidence', - 'autoIncrement':False, 'env':{'APP_VARIANT':'preview','EXPO_PUBLIC_SHOWCASE':'1','NODE_OPTIONS':'--max-old-space-size=4096'}, - 'ios':{'simulator':True,'image':'macos-tahoe-26.6-xcode-27.1'} -} -(mobile/'eas.json').write_text(json.dumps(config,indent=2)+'\n') -ignore=mobile/'.gitignore' -text=ignore.read_text() if ignore.exists() else '' -if '.env.eas-simulator' not in text: ignore.write_text(text+'\n.env.eas-simulator\n') -print('offline fixture injected; renderer unchanged; no server/data writes') diff --git a/scripts/fixtures/issue-17531/seed.py b/scripts/fixtures/issue-17531/seed.py deleted file mode 100644 index 7cc08ce03505d..0000000000000 --- a/scripts/fixtures/issue-17531/seed.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -"""Create an isolated Bash/mod reproduction; configure provider credentials separately.""" - -import argparse -import os -from pathlib import Path -import subprocess - -parser = argparse.ArgumentParser(description=__doc__) -parser.add_argument("directory", type=Path, help="new disposable directory") -args = parser.parse_args() -root = args.directory.resolve() -root.mkdir(parents=True, exist_ok=False) -project = root / "project" -(project / "build").mkdir(parents=True) -(project / "build" / "sample.txt").write_text("fixture\n") -subprocess.run(["git", "init", str(project)], check=True) -config = root / "claude" -config.mkdir(mode=0o700) -source = root / "playground" -subprocess.run(["git", "clone", "--no-checkout", "https://github.com/anthropics/claude-code-playground.git", str(source)], check=True) -subprocess.run(["git", "-C", str(source), "checkout", "569c5283d9a0a7ee7938df85bb32e4f48cbb8c86"], check=True) -environment = {**os.environ, "CLAUDE_CONFIG_DIR": str(config)} -subprocess.run(["claude", "plugin", "marketplace", "add", str(source / "claude-code" / "mods")], env=environment, check=True) -subprocess.run(["claude", "plugin", "install", "blast-radius@claude-code-playground-mods", "--scope", "user"], env=environment, check=True) -print(f"Project: {project}\nCLAUDE_CONFIG_DIR: {config}") -print("In T3, select this Claude config and ask: Use Bash to run exactly rm -r build. Do not add -f.")