Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
11ec9d7
feat(web): add queue and steer follow-up behavior
leonardoxr Mar 28, 2026
8f0600a
Merge branch 'main' into feat/follow-up-behavior-1462
leonardoxr Mar 28, 2026
3a033b0
feat: move queued follow-ups into orchestration
leonardoxr Mar 28, 2026
81ba2c4
Merge origin/main into feat/follow-up-behavior-1462
leonardoxr Mar 28, 2026
91df7c2
fix: address queued follow-up review feedback
leonardoxr Mar 28, 2026
b263589
fix: revoke queued follow-up preview urls on clear
leonardoxr Mar 28, 2026
775ee42
fix: harden queued follow-up composer flow
leonardoxr Mar 28, 2026
cace8fc
fix: address queued follow-up review regressions
leonardoxr Mar 28, 2026
f4587c9
Merge branch 'main' into feat/follow-up-behavior-1462
leonardoxr Mar 28, 2026
06d68cd
fix: harden queued follow-up dispatch
leonardoxr Mar 28, 2026
46f95da
fix: resolve queued steer review feedback
leonardoxr Mar 28, 2026
d92e28b
Merge origin/main into feat/follow-up-behavior-1462
leonardoxr Mar 29, 2026
1ad44cc
fix: resolve remaining queued follow-up review comments
leonardoxr Mar 29, 2026
f049043
Merge branch 'main' into feat/follow-up-behavior-1462
leonardoxr Mar 29, 2026
52c2d73
Merge origin/main into feat/follow-up-behavior-1462
leonardoxr Mar 29, 2026
8162f86
Merge remote-tracking branch 'leonardoxr/feat/follow-up-behavior-1462…
leonardoxr Mar 29, 2026
711f86d
fix: address remaining follow-up review feedback
leonardoxr Mar 29, 2026
6f39aeb
fix: address remaining follow-up review threads
leonardoxr Mar 29, 2026
a2754fa
fix: address queued follow-up review feedback
leonardoxr Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
521 changes: 521 additions & 0 deletions apps/web/src/components/ChatView.browser.tsx

Large diffs are not rendered by default.

109 changes: 108 additions & 1 deletion apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vitest";

import { buildExpiredTerminalContextToastCopy, deriveComposerSendState } from "./ChatView.logic";
import {
buildExpiredTerminalContextToastCopy,
buildQueuedFollowUpDraft,
canAutoDispatchQueuedFollowUp,
deriveComposerSendState,
followUpBehaviorShortcutLabel,
resolveFollowUpBehavior,
shouldInvertFollowUpBehaviorFromKeyEvent,
} from "./ChatView.logic";

describe("deriveComposerSendState", () => {
it("treats expired terminal pills as non-sendable content", () => {
Expand Down Expand Up @@ -67,3 +75,102 @@ describe("buildExpiredTerminalContextToastCopy", () => {
});
});
});

describe("follow-up behavior helpers", () => {
it("inverts the configured behavior when requested", () => {
expect(resolveFollowUpBehavior("steer", false)).toBe("steer");
expect(resolveFollowUpBehavior("steer", true)).toBe("queue");
expect(resolveFollowUpBehavior("queue", true)).toBe("steer");
});

it("detects the opposite-submit keyboard shortcut across platforms", () => {
expect(
shouldInvertFollowUpBehaviorFromKeyEvent(
{
ctrlKey: true,
metaKey: false,
shiftKey: true,
altKey: false,
},
"Win32",
),
).toBe(true);
expect(
shouldInvertFollowUpBehaviorFromKeyEvent(
{
ctrlKey: false,
metaKey: true,
shiftKey: true,
altKey: false,
},
"MacIntel",
),
).toBe(true);
expect(
shouldInvertFollowUpBehaviorFromKeyEvent(
{
ctrlKey: false,
metaKey: false,
shiftKey: true,
altKey: false,
},
"Win32",
),
).toBe(false);
expect(followUpBehaviorShortcutLabel("MacIntel")).toBe("Cmd+Shift+Enter");
expect(followUpBehaviorShortcutLabel("Win32")).toBe("Ctrl+Shift+Enter");
});

it("builds a queued follow-up snapshot and auto-dispatch rules", () => {
const snapshot = buildQueuedFollowUpDraft({
prompt: "next step",
attachments: [],
terminalContexts: [
{
id: "ctx-1",
threadId: ThreadId.makeUnsafe("thread-1"),
terminalId: "default",
terminalLabel: "Terminal 1",
lineStart: 1,
lineEnd: 1,
text: "hello",
createdAt: "2026-03-27T12:00:00.000Z",
},
],
modelSelection: {
provider: "codex",
model: "gpt-5.3-codex",
},
runtimeMode: "full-access",
interactionMode: "default",
createdAt: "2026-03-27T12:00:00.000Z",
});

expect(snapshot.id).toBeTruthy();
expect(snapshot.terminalContexts[0]?.text).toBe("hello");
expect(
canAutoDispatchQueuedFollowUp({
phase: "ready",
queuedFollowUpCount: 2,
isConnecting: false,
isSendBusy: false,
isRevertingCheckpoint: false,
hasThreadError: false,
hasPendingApproval: false,
hasPendingUserInput: false,
}),
).toBe(true);
expect(
canAutoDispatchQueuedFollowUp({
phase: "running",
queuedFollowUpCount: 2,
isConnecting: false,
isSendBusy: false,
isRevertingCheckpoint: false,
hasThreadError: false,
hasPendingApproval: false,
hasPendingUserInput: false,
}),
).toBe(false);
});
});
107 changes: 105 additions & 2 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { ProjectId, type ModelSelection, type ThreadId } from "@t3tools/contracts";
import {
ProjectId,
ProviderInteractionMode,
RuntimeMode,
type ModelSelection,
type ThreadId,
} from "@t3tools/contracts";
import { type FollowUpBehavior } from "@t3tools/contracts/settings";
import { type ChatMessage, type Thread } from "../types";
import { randomUUID } from "~/lib/utils";
import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";
import {
type ComposerImageAttachment,
type DraftThreadState,
type PersistedComposerImageAttachment,
type QueuedFollowUpDraft,
} from "../composerDraftStore";
import { Schema } from "effect";
import {
filterTerminalContextsWithText,
stripInlineTerminalContextPlaceholders,
type TerminalContextDraft,
} from "../lib/terminalContext";
import { isMacPlatform } from "../lib/utils";

export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
const WORKTREE_BRANCH_PREFIX = "t3code";
Expand Down Expand Up @@ -160,3 +173,93 @@ export function buildExpiredTerminalContextToastCopy(
description: "Re-add it if you want that terminal output included.",
};
}

export function resolveFollowUpBehavior(
followUpBehavior: FollowUpBehavior,
invert: boolean,
): FollowUpBehavior {
if (!invert) {
return followUpBehavior;
}
return followUpBehavior === "queue" ? "steer" : "queue";
}

export function shouldInvertFollowUpBehaviorFromKeyEvent(
event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "shiftKey">,
platform = navigator.platform,
): boolean {
if (!event.shiftKey || event.altKey) {
return false;
}
if (isMacPlatform(platform)) {
return event.metaKey && !event.ctrlKey;
}
return event.ctrlKey && !event.metaKey;
}

export function followUpBehaviorShortcutLabel(platform = navigator.platform): string {
return isMacPlatform(platform) ? "Cmd+Shift+Enter" : "Ctrl+Shift+Enter";
}

export function buildQueuedFollowUpDraft(input: {
prompt: string;
attachments: ReadonlyArray<PersistedComposerImageAttachment>;
terminalContexts: ReadonlyArray<TerminalContextDraft>;
modelSelection: ModelSelection;
runtimeMode: RuntimeMode;
interactionMode: ProviderInteractionMode;
createdAt: string;
}): QueuedFollowUpDraft {
return {
id: randomUUID(),
createdAt: input.createdAt,
prompt: input.prompt,
attachments: [...input.attachments],
terminalContexts: input.terminalContexts.map((context) => ({ ...context })),
modelSelection: input.modelSelection,
runtimeMode: input.runtimeMode,
interactionMode: input.interactionMode,
};
}

export function canAutoDispatchQueuedFollowUp(input: {
phase: "disconnected" | "connecting" | "ready" | "running";
queuedFollowUpCount: number;
isConnecting: boolean;
isSendBusy: boolean;
isRevertingCheckpoint: boolean;
hasThreadError: boolean;
hasPendingApproval: boolean;
hasPendingUserInput: boolean;
}): boolean {
return (
input.phase === "ready" &&
input.queuedFollowUpCount > 0 &&
!input.isConnecting &&
!input.isSendBusy &&
!input.isRevertingCheckpoint &&
!input.hasThreadError &&
!input.hasPendingApproval &&
!input.hasPendingUserInput
);
}

export function describeQueuedFollowUp(
followUp: Pick<QueuedFollowUpDraft, "attachments" | "prompt" | "terminalContexts">,
): string {
const trimmedPrompt = stripInlineTerminalContextPlaceholders(followUp.prompt).trim();
if (trimmedPrompt.length > 0) {
return trimmedPrompt;
}
if (followUp.attachments.length > 0) {
return followUp.attachments.length === 1
? "1 image attached"
: `${followUp.attachments.length} images attached`;
}
if (followUp.terminalContexts.length > 0) {
return followUp.terminalContexts.length === 1
? (followUp.terminalContexts[0]?.terminalLabel ?? "1 terminal context")
: `${followUp.terminalContexts.length} terminal contexts`;
}
return "Follow-up";
}
Loading