Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EnvironmentId, ProjectId, ThreadId, TurnId } from "@t3tools/contracts";
import { afterEach, describe, expect, it, vi } from "vitest";
import { type EnvironmentState, useStore } from "../store";
import { type Thread } from "../types";
import { INLINE_DIFF_CONTEXT_COMMENT_PLACEHOLDER } from "../lib/diffContextComments";

import {
MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
Expand Down Expand Up @@ -65,6 +66,17 @@ describe("deriveComposerSendState", () => {
expect(state.expiredTerminalContextCount).toBe(1);
expect(state.hasSendableContent).toBe(true);
});

it("strips diff comment placeholders from visible prompt text", () => {
const state = deriveComposerSendState({
prompt: INLINE_DIFF_CONTEXT_COMMENT_PLACEHOLDER,
imageCount: 0,
terminalContexts: [],
});

expect(state.trimmedPrompt).toBe("");
expect(state.hasSendableContent).toBe(false);
});
});

describe("buildExpiredTerminalContextToastCopy", () => {
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
stripInlineTerminalContextPlaceholders,
type TerminalContextDraft,
} from "../lib/terminalContext";
import { stripInlineDiffContextCommentPlaceholders } from "../lib/diffContextComments";
import type { DraftThreadEnvMode } from "../composerDraftStore";

export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
Expand Down Expand Up @@ -189,7 +190,9 @@ export function deriveComposerSendState(options: {
expiredTerminalContextCount: number;
hasSendableContent: boolean;
} {
const trimmedPrompt = stripInlineTerminalContextPlaceholders(options.prompt).trim();
const trimmedPrompt = stripInlineDiffContextCommentPlaceholders(
stripInlineTerminalContextPlaceholders(options.prompt),
).trim();
const sendableTerminalContexts = filterTerminalContextsWithText(options.terminalContexts);
const expiredTerminalContextCount =
options.terminalContexts.length - sendableTerminalContexts.length;
Expand Down
43 changes: 31 additions & 12 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
import {
type ComposerImageAttachment,
type DraftThreadEnvMode,
flushComposerDraftStorage,
useComposerDraftStore,
type DraftId,
} from "../composerDraftStore";
Expand All @@ -132,6 +133,7 @@
type TerminalContextDraft,
type TerminalContextSelection,
} from "../lib/terminalContext";
import { appendDiffContextCommentsToPrompt } from "../lib/diffContextComments";
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer";
import { ExpandedImageDialog } from "./chat/ExpandedImageDialog";
Expand Down Expand Up @@ -631,17 +633,15 @@
const composerActiveProvider = useComposerDraftStore(
(store) => store.getComposerDraft(composerDraftTarget)?.activeProvider ?? null,
);
const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt);
const addComposerDraftImages = useComposerDraftStore((store) => store.addImages);
const setComposerDraftTerminalContexts = useComposerDraftStore(
(store) => store.setTerminalContexts,
);
const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection);
const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode);
const setComposerDraftInteractionMode = useComposerDraftStore(
(store) => store.setInteractionMode,
);
const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent);
const restoreComposerDraftSendContent = useComposerDraftStore(
(store) => store.restoreComposerSendContent,
);
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
const getDraftSessionByLogicalProjectKey = useComposerDraftStore(
(store) => store.getDraftSessionByLogicalProjectKey,
Expand Down Expand Up @@ -1556,7 +1556,7 @@
);

const focusComposer = useCallback(() => {
composerRef.current?.focusAtEnd();

Check warning on line 1559 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}, []);
const scheduleComposerFocus = useCallback(() => {
window.requestAnimationFrame(() => {
Expand All @@ -1564,7 +1564,7 @@
});
}, [focusComposer]);
const addTerminalContextToDraft = useCallback((selection: TerminalContextSelection) => {
composerRef.current?.addTerminalContext(selection);

Check warning on line 1567 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}, []);
const setTerminalOpen = useCallback(
(open: boolean) => {
Expand Down Expand Up @@ -2235,7 +2235,7 @@
const shortcutContext = {
terminalFocus: isTerminalFocused(),
terminalOpen: Boolean(terminalState.terminalOpen),
modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false,

Check warning on line 2238 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useEffect has a missing dependency: 'composerRef.current'
};

const command = resolveShortcutCommand(event, keybindings, {
Expand Down Expand Up @@ -2379,7 +2379,9 @@
if (!sendCtx) return;
const {
images: composerImages,
persistedAttachments: composerPersistedAttachments,
terminalContexts: composerTerminalContexts,
diffContextComments: composerDiffContextComments,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send state derived inconsistently for diff context comments

Medium Severity

deriveComposerSendState doesn't account for diffContextComments, so hasSendableContent is false when only diff comments are pending. ChatComposer patches this by overriding hasSendableContent after the fact, but the onSend handler in ChatView.tsx calls deriveComposerSendState independently and works around it with a separate hasPendingDiffContextComments check. This split means the expired-terminal-context toast logic at line 2431 can still fire even when there are valid diff comments to send — the hasSendableContent is false, so the code enters the "no sendable content" branch and shows a misleading warning before continuing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f98db7. Configure here.

selectedProvider: ctxSelectedProvider,
selectedModel: ctxSelectedModel,
selectedProviderModels: ctxSelectedProviderModels,
Expand Down Expand Up @@ -2412,7 +2414,9 @@
return;
}
const standaloneSlashCommand =
composerImages.length === 0 && sendableComposerTerminalContexts.length === 0
composerImages.length === 0 &&
sendableComposerTerminalContexts.length === 0 &&
composerDiffContextComments.length === 0
? parseStandaloneComposerSlashCommand(trimmed)
: null;
if (standaloneSlashCommand) {
Expand All @@ -2422,7 +2426,8 @@
composerRef.current?.resetCursorState();
return;
}
if (!hasSendableContent) {
const hasPendingDiffContextComments = composerDiffContextComments.length > 0;
if (!hasSendableContent && !hasPendingDiffContextComments) {
if (expiredTerminalContextCount > 0) {
const toastCopy = buildExpiredTerminalContextToastCopy(
expiredTerminalContextCount,
Expand Down Expand Up @@ -2459,11 +2464,17 @@
beginLocalDispatch({ preparingWorktree: Boolean(baseBranchForWorktree) });

const composerImagesSnapshot = [...composerImages];
const composerPersistedAttachmentsSnapshot = [...composerPersistedAttachments];
const composerTerminalContextsSnapshot = [...sendableComposerTerminalContexts];
const messageTextForSend = appendTerminalContextsToPrompt(
const composerDiffContextCommentsSnapshot = [...composerDiffContextComments];
const messageTextWithTerminalContexts = appendTerminalContextsToPrompt(
promptForSend,
composerTerminalContextsSnapshot,
);
const messageTextForSend = appendDiffContextCommentsToPrompt(
messageTextWithTerminalContexts,
composerDiffContextCommentsSnapshot,
);
const messageIdForSend = newMessageId();
const messageCreatedAt = new Date().toISOString();
const outgoingMessageText = formatOutgoingPrompt({
Expand Down Expand Up @@ -2526,6 +2537,7 @@
}
promptRef.current = "";
clearComposerDraftContent(composerDraftTarget);
flushComposerDraftStorage();
composerRef.current?.resetCursorState();

let turnStartSucceeded = false;
Expand Down Expand Up @@ -2630,7 +2642,9 @@
!turnStartSucceeded &&
promptRef.current.length === 0 &&
composerImagesRef.current.length === 0 &&
composerTerminalContextsRef.current.length === 0
composerTerminalContextsRef.current.length === 0 &&
(useComposerDraftStore.getState().getComposerDraft(composerDraftTarget)?.diffContextComments
.length ?? 0) === 0
) {
setOptimisticUserMessages((existing) => {
const removed = existing.filter((message) => message.id === messageIdForSend);
Expand All @@ -2644,9 +2658,14 @@
const retryComposerImages = composerImagesSnapshot.map(cloneComposerImageForRetry);
composerImagesRef.current = retryComposerImages;
composerTerminalContextsRef.current = composerTerminalContextsSnapshot;
setComposerDraftPrompt(composerDraftTarget, promptForSend);
addComposerDraftImages(composerDraftTarget, retryComposerImages);
setComposerDraftTerminalContexts(composerDraftTarget, composerTerminalContextsSnapshot);
restoreComposerDraftSendContent(composerDraftTarget, {
prompt: promptForSend,
images: retryComposerImages,
persistedAttachments: composerPersistedAttachmentsSnapshot,
terminalContexts: composerTerminalContextsSnapshot,
diffContextComments: composerDiffContextCommentsSnapshot,
});
flushComposerDraftStorage();
composerRef.current?.resetCursorState({
cursor: collapseExpandedComposerCursor(promptForSend, promptForSend.length),
prompt: promptForSend,
Expand Down Expand Up @@ -2772,7 +2791,7 @@
};
});
promptRef.current = "";
composerRef.current?.resetCursorState({ cursor: 0 });

Check warning on line 2794 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
},
[activePendingProgress?.activeQuestion, activePendingUserInput],
);
Expand All @@ -2799,7 +2818,7 @@
),
},
}));
const snapshot = composerRef.current?.readSnapshot();

Check warning on line 2821 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (
snapshot?.value !== value ||
snapshot.cursor !== nextCursor ||
Expand Down Expand Up @@ -2862,7 +2881,7 @@
return;
}

const sendCtx = composerRef.current?.getSendContext();

Check warning on line 2884 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (!sendCtx) {
return;
}
Expand Down
Loading
Loading