diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
index 16f697d11f..100ee00bec 100644
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -1,3 +1,4 @@
+import { resolvePairState } from "@t3tools/client-runtime/state/pair";
import { delegatedThreadRows } from "@t3tools/client-runtime/state/delegated-threads";
import { prepareRevertedMessageContext } from "../lib/composerRewindContext";
import { useLoadBalancedEnvironment } from "../hooks/useLoadBalancedEnvironment";
@@ -3027,6 +3028,16 @@ export default function ChatView(props: ChatViewProps) {
[agentSessionLive, threadActivities],
);
const allThreadShells = useThreadShells();
+ const activeThreadPaired = useMemo(
+ () =>
+ activeThread
+ ? resolvePairState({
+ threads: allThreadShells,
+ lead: { environmentId, threadId: activeThread.id, driverKind: null },
+ }).kind === "on"
+ : false,
+ [activeThread, allThreadShells, environmentId],
+ );
const delegatedThreads = useMemo(
() => (activeThreadRef ? delegatedThreadRows(allThreadShells, activeThreadRef) : []),
[allThreadShells, activeThreadRef],
@@ -7788,6 +7799,7 @@ export default function ChatView(props: ChatViewProps) {
const followUp = resolvePlanFollowUpSubmission({
draftText: promptForSend,
planMarkdown: activeProposedPlan.planMarkdown,
+ options: { paired: activeThreadPaired },
});
const outgoingFollowUpText = formatOutgoingPrompt({
provider: ctxSelectedProvider,
@@ -10666,6 +10678,7 @@ export default function ChatView(props: ChatViewProps) {
respondingRequestIds={respondingRequestIds}
showPlanFollowUpPrompt={showPlanFollowUpPrompt}
activeProposedPlan={activeProposedPlan}
+ pairedImplement={activeThreadPaired}
activeTasksProgress={activeComposerTasksProgress}
activeTaskSteps={activeComposerTaskSteps}
activeDelegatedWork={activeComposerDelegatedWork}
diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx
index c0411a3cfa..e52f269703 100644
--- a/apps/web/src/components/chat/ChatComposer.tsx
+++ b/apps/web/src/components/chat/ChatComposer.tsx
@@ -1301,6 +1301,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(
onPreviousPendingQuestion: () => void;
onInterrupt: () => void;
onImplementPlanInNewThread: () => void;
+ pairedImplement?: boolean;
}) {
return (
<>
@@ -1336,6 +1337,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(
onPreviousPendingQuestion={props.onPreviousPendingQuestion}
onInterrupt={props.onInterrupt}
onImplementPlanInNewThread={props.onImplementPlanInNewThread}
+ pairedImplement={props.pairedImplement ?? false}
/>
>
);
@@ -1475,6 +1477,7 @@ export interface ChatComposerProps {
// Plan
showPlanFollowUpPrompt: boolean;
activeProposedPlan: Thread["proposedPlans"][number] | null;
+ pairedImplement?: boolean;
activeTasksProgress: ComposerTasksProgress | null;
activeTaskSteps: readonly ComposerTaskStep[] | null;
activeDelegatedWork: ComposerDelegatedWorkSummary | null;
@@ -7890,6 +7893,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion}
onInterrupt={handleInterruptPrimaryAction}
onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction}
+ pairedImplement={props.pairedImplement ?? false}
/>
diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx
index 37b2b408d5..2250ae7e23 100644
--- a/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx
+++ b/apps/web/src/components/chat/ComposerPrimaryActions.test.tsx
@@ -159,6 +159,30 @@ function renderSendButton(
);
}
+function renderImplement(pairedImplement: boolean | undefined) {
+ return renderToStaticMarkup(
+ createElement(ComposerPrimaryActions, {
+ compact: true,
+ pendingAction: null,
+ isRunning: false,
+ canQueueFollowUp: false,
+ onQueueFollowUp: () => {},
+ showPlanFollowUpPrompt: true,
+ promptHasText: false,
+ isSendBusy: false,
+ sendDisabledReason: null,
+ isConnecting: false,
+ isEnvironmentUnavailable: false,
+ isPreparingWorktree: false,
+ hasSendableContent: false,
+ onPreviousPendingQuestion: () => {},
+ onInterrupt: () => {},
+ onImplementPlanInNewThread: () => {},
+ ...(pairedImplement === undefined ? {} : { pairedImplement }),
+ }),
+ );
+}
+
afterEach(() => {
stageArtworkState.mode = "none";
stageArtworkState.variant = null;
@@ -276,3 +300,12 @@ describe("ComposerPrimaryActions", () => {
expect(markup).not.toContain('aria-label="Send message"');
});
});
+
+describe("implementing a plan on a paired thread", () => {
+ it("says the work goes through the executor, and only then", () => {
+ expect(renderImplement(true)).toContain(">Implement with executor<");
+ expect(renderImplement(false)).toContain(">Implement<");
+ expect(renderImplement(undefined)).toContain(">Implement<");
+ expect(renderImplement(false)).not.toContain("with executor");
+ });
+});
diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx
index d316fd4305..2e2a2acf16 100644
--- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx
+++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx
@@ -38,6 +38,8 @@ interface ComposerPrimaryActionsProps {
onPreviousPendingQuestion: () => void;
onInterrupt: () => void;
onImplementPlanInNewThread: () => void;
+ /** The thread has a pair executor, so implementing a plan goes through it. */
+ pairedImplement?: boolean;
}
const formatPendingPrimaryActionLabel = (input: {
@@ -82,6 +84,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
onPreviousPendingQuestion,
onInterrupt,
onImplementPlanInNewThread,
+ pairedImplement,
}: ComposerPrimaryActionsProps) {
const pointerFocusProps = preserveComposerFocusOnPointerDown
? { onPointerDown: preventPointerFocus }
@@ -195,7 +198,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
{...pointerFocusProps}
disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
>
- {isConnecting || isSendBusy ? "Sending..." : "Implement"}
+ {isConnecting || isSendBusy
+ ? "Sending..."
+ : pairedImplement
+ ? "Implement with executor"
+ : "Implement"}