Skip to content

Commit bbc2db0

Browse files
NikitaMGrimmpersonal-nightly
authored andcommitted
fix(composer): steer instead of queueing during automatic compaction
1 parent 66ee628 commit bbc2db0

6 files changed

Lines changed: 71 additions & 26 deletions

File tree

‎apps/mobile/src/state/use-thread-composer-state.ts‎

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
useComposerDraftSettings,
8282
} from "./use-composer-drafts";
8383
import {
84+
isCompactCommandText,
8485
resolveComposerDispatchMode,
8586
type ActiveTurnComposerAction,
8687
} from "@t3tools/client-runtime/state/composer-dispatch";
@@ -333,24 +334,19 @@ export function useThreadComposerState() {
333334
? deriveThreadActivityRun(selectedThreadProjection.projection)
334335
: (selectedThreadShell?.latestRun ?? null);
335336

336-
const isCompacting = (() => {
337+
const activeRunId =
338+
selectedThreadRuntime?.activeRunId && threadRuntimeIsActive(selectedThreadRuntime)
339+
? selectedThreadRuntime.activeRunId
340+
: null;
341+
const compactCommandIsActive = (() => {
337342
const queuedCompact = selectedThreadQueuedMessages.some(
338343
(message) =>
339344
message.messageId === dispatchingQueuedMessageId &&
340345
message.text.trim().toLowerCase() === "/compact" &&
341346
message.attachments.length === 0,
342347
);
343348
if (queuedCompact) return true;
344-
const activeRunId = selectedThreadRuntime?.activeRunId;
345-
if (!activeRunId || !threadRuntimeIsActive(selectedThreadRuntime)) return false;
346-
if (
347-
selectedThreadVisibleTurnItems.some(
348-
({ item }) =>
349-
item.runId === activeRunId && item.type === "compaction" && item.status === "running",
350-
)
351-
) {
352-
return true;
353-
}
349+
if (!activeRunId) return false;
354350
const compactMessage = selectedThreadVisibleTurnItems.findLast(
355351
({ item }) =>
356352
item.runId === activeRunId &&
@@ -369,8 +365,26 @@ export function useThreadComposerState() {
369365
item.status === "cancelled"),
370366
);
371367
})();
372-
// A compacting turn cannot take steering input, so follow-ups queue meanwhile.
373-
const canSteerActiveTurn = queueWorkflow?.canPromoteToSteer === true && !isCompacting;
368+
const compactionRunning =
369+
activeRunId !== null &&
370+
selectedThreadVisibleTurnItems.some(
371+
({ item }) =>
372+
item.runId === activeRunId && item.type === "compaction" && item.status === "running",
373+
);
374+
const isCompacting = compactCommandIsActive || compactionRunning;
375+
// Only an explicit compaction refuses steering. Automatic compaction runs
376+
// inside an ordinary turn, which still accepts it.
377+
const explicitCompactionIsActive =
378+
compactCommandIsActive ||
379+
(compactionRunning &&
380+
selectedThreadVisibleTurnItems.some(
381+
({ item }) =>
382+
item.runId === activeRunId &&
383+
item.type === "user_message" &&
384+
isCompactCommandText(item.text),
385+
));
386+
const canSteerActiveTurn =
387+
queueWorkflow?.canPromoteToSteer === true && !explicitCompactionIsActive;
374388
// A stop or a restart holds the queue, completion wakes included, until it
375389
// is resumed; the queue sheet lists only the user's own messages.
376390
const queueHeld = queueWorkflow?.isHeld === true;

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
workflowOpeningParentThreadId,
8585
} from "@t3tools/client-runtime/state/subagentRuntime";
8686
import { threadSupportsProviderHandoff } from "@t3tools/client-runtime/state/thread-workflows";
87+
import { isCompactCommandText } from "@t3tools/client-runtime/state/composer-dispatch";
8788
import { resolvePendingHandoff } from "@t3tools/client-runtime/handoff";
8889
import { HandoffRow } from "./chat/V2LifecycleRow";
8990
import {
@@ -1398,9 +1399,18 @@ export default function ChatView(props: ChatViewProps) {
13981399
item.runId === activeRuntime?.activeRunId &&
13991400
item.status === "running",
14001401
);
1402+
const compactCommandIsActive = compactRequestIsActive && !compactionSettled;
14011403
const isCompacting =
14021404
(isSendBusy || phase === "connecting" || phase === "running") &&
1403-
(compactionRunning || (compactRequestIsActive && !compactionSettled));
1405+
(compactionRunning || compactCommandIsActive);
1406+
// Only an explicit compaction refuses steering. Automatic compaction runs
1407+
// inside an ordinary turn, which still accepts it.
1408+
const explicitCompactionIsActive =
1409+
compactCommandIsActive ||
1410+
(compactionRunning &&
1411+
latestServerUserItem?.type === "user_message" &&
1412+
latestServerUserItem.runId === activeRuntime?.activeRunId &&
1413+
isCompactCommandText(latestServerUserItem.text));
14041414
const isWorking =
14051415
phase === "running" ||
14061416
isSendBusy ||
@@ -3523,7 +3533,7 @@ export default function ChatView(props: ChatViewProps) {
35233533
isLocalDraftThread && activeProject === null
35243534
}
35253535
phase={phase}
3526-
isCompacting={isCompacting}
3536+
canSteer={!explicitCompactionIsActive}
35273537
canInterrupt={canInterruptRunningThread}
35283538
isConnecting={isConnecting}
35293539
isSendBusy={

‎apps/web/src/components/chat/ChatComposer.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export interface ChatComposerProps {
398398
isRevertingCheckpoint?: boolean;
399399
sendDisabledReason: string | null;
400400
isPreparingWorktree: boolean;
401-
isCompacting: boolean;
401+
canSteer: boolean;
402402
bannerItems: readonly ComposerBannerStackItem[];
403403
resumeCompactionTokens: number | null;
404404
keepFullHistory: boolean;
@@ -562,7 +562,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
562562
isRevertingCheckpoint = false,
563563
sendDisabledReason: externalSendDisabledReason,
564564
isPreparingWorktree,
565-
isCompacting,
565+
canSteer,
566566
environmentUnavailable,
567567
activePendingApproval: incomingPendingApproval,
568568
pendingApprovals,
@@ -2154,7 +2154,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
21542154
running: phase === "running",
21552155
alternateModifier: false,
21562156
activeTurnDefault: followUpBehavior,
2157-
canSteer: !isCompacting,
2157+
canSteer,
21582158
}),
21592159
submissionIntent,
21602160
);
@@ -2177,7 +2177,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
21772177
running: phase === "running",
21782178
alternateModifier: event.metaKey || event.ctrlKey,
21792179
activeTurnDefault: followUpBehavior,
2180-
canSteer: !isCompacting,
2180+
canSteer,
21812181
}),
21822182
);
21832183
};
@@ -2189,7 +2189,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
21892189
running: phase === "running",
21902190
alternateModifier: false,
21912191
activeTurnDefault: followUpBehavior,
2192-
canSteer: !isCompacting,
2192+
canSteer,
21932193
}),
21942194
);
21952195
};
@@ -2248,7 +2248,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
22482248
sendShortcut: settings.sendShortcut,
22492249
routeKind,
22502250
phase,
2251-
isCompacting,
2251+
canSteer,
22522252
followUpBehavior,
22532253
isSoftwareKeyboardOpen,
22542254
planModeUiEnabled,
@@ -3913,7 +3913,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
39133913
isRunning={phase === "running"}
39143914
canInterrupt={canInterrupt}
39153915
followUpBehavior={followUpBehavior}
3916-
canSteer={!isCompacting}
3916+
canSteer={canSteer}
39173917
alternateShortcutLabel={shortcutLabelForCommand(
39183918
keybindings,
39193919
"composer.sendAlternate",

‎apps/web/src/components/chat/useComposerCommandKey.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function useComposerCommandKey(input: {
3232
readonly sendShortcut: ChatComposerProps["settings"]["sendShortcut"];
3333
readonly routeKind: ChatComposerProps["routeKind"];
3434
readonly phase: ChatComposerProps["phase"];
35-
readonly isCompacting: boolean;
35+
readonly canSteer: boolean;
3636
readonly followUpBehavior: ChatComposerProps["settings"]["followUpBehavior"];
3737
readonly isSoftwareKeyboardOpen: boolean;
3838
readonly planModeUiEnabled: boolean;
@@ -84,7 +84,7 @@ export function useComposerCommandKey(input: {
8484
sendShortcut,
8585
routeKind,
8686
phase,
87-
isCompacting,
87+
canSteer,
8888
followUpBehavior,
8989
isSoftwareKeyboardOpen,
9090
planModeUiEnabled,
@@ -226,7 +226,7 @@ export function useComposerCommandKey(input: {
226226
running: phase === "running",
227227
alternateModifier: submissionIntent === "alternate",
228228
activeTurnDefault: followUpBehavior,
229-
canSteer: !isCompacting,
229+
canSteer,
230230
}),
231231
submissionIntent,
232232
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect, it } from "vite-plus/test";
2+
3+
import { isCompactCommandText } from "./composerDispatch.ts";
4+
5+
it("recognizes explicit compaction commands, with or without instructions", () => {
6+
expect(isCompactCommandText("/compact")).toBe(true);
7+
expect(isCompactCommandText(" /COMPACT ")).toBe(true);
8+
expect(isCompactCommandText("/compact keep the debugging notes")).toBe(true);
9+
expect(isCompactCommandText("/compactify")).toBe(false);
10+
expect(isCompactCommandText("please /compact")).toBe(false);
11+
expect(isCompactCommandText("Also end your reply with PINEAPPLE.")).toBe(false);
12+
});

‎packages/client-runtime/src/state/composerDispatch.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function resolveComposerDispatchMode(input: {
55
readonly running: boolean;
66
readonly alternateModifier: boolean;
77
readonly activeTurnDefault?: ActiveTurnComposerAction;
8-
/** False while the active turn cannot take steering input, such as during compaction. */
8+
/** False while the active turn cannot take steering input, such as an explicit /compact. */
99
readonly canSteer?: boolean;
1010
}): ComposerDispatchMode {
1111
if (!input.running) return "auto";
@@ -15,6 +15,15 @@ export function resolveComposerDispatchMode(input: {
1515
return defaultAction;
1616
}
1717

18+
/**
19+
* Whether a run's user message asks for an explicit compaction. Its running
20+
* compaction refuses steering, unlike automatic compaction inside an ordinary
21+
* turn. Pi and Claude also compact for `/compact <instructions>`.
22+
*/
23+
export function isCompactCommandText(text: string): boolean {
24+
return /^\/compact(?:\s|$)/i.test(text.trim());
25+
}
26+
1827
export function alternateComposerDispatchAction(
1928
activeTurnDefault?: ActiveTurnComposerAction,
2029
): ActiveTurnComposerAction {

0 commit comments

Comments
 (0)