Skip to content

Commit ba598aa

Browse files
committed
Fix composer mention completion cursors
Requirements and issue report: pingdotgg#291
1 parent ff6a66d commit ba598aa

File tree

4 files changed

+257
-66
lines changed

4 files changed

+257
-66
lines changed

apps/web/src/components/ChatView.tsx

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuer
5454
import { isElectron } from "../env";
5555
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
5656
import {
57+
clampCollapsedComposerCursor,
5758
type ComposerSlashCommand,
5859
type ComposerTrigger,
5960
type ComposerTriggerKind,
61+
collapseExpandedComposerCursor,
6062
detectComposerTrigger,
6163
expandCollapsedComposerCursor,
6264
parseStandaloneComposerSlashCommand,
@@ -663,7 +665,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
663665
const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState<
664666
Record<string, string[]>
665667
>({});
666-
const [composerCursor, setComposerCursor] = useState(() => prompt.length);
668+
const [composerCursor, setComposerCursor] = useState(() =>
669+
collapseExpandedComposerCursor(prompt, prompt.length),
670+
);
667671
const [composerTrigger, setComposerTrigger] = useState<ComposerTrigger | null>(() =>
668672
detectComposerTrigger(prompt, prompt.length),
669673
);
@@ -1035,14 +1039,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
10351039
return;
10361040
}
10371041
promptRef.current = activePendingProgress.customAnswer;
1038-
setComposerCursor(activePendingProgress.customAnswer.length);
1042+
const nextCursor = collapseExpandedComposerCursor(
1043+
activePendingProgress.customAnswer,
1044+
activePendingProgress.customAnswer.length,
1045+
);
1046+
setComposerCursor(nextCursor);
10391047
setComposerTrigger(
10401048
detectComposerTrigger(
10411049
activePendingProgress.customAnswer,
1042-
expandCollapsedComposerCursor(
1043-
activePendingProgress.customAnswer,
1044-
activePendingProgress.customAnswer.length,
1045-
),
1050+
expandCollapsedComposerCursor(activePendingProgress.customAnswer, nextCursor),
10461051
),
10471052
);
10481053
setComposerHighlightedItemId(null);
@@ -2116,7 +2121,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
21162121

21172122
useEffect(() => {
21182123
promptRef.current = prompt;
2119-
setComposerCursor((existing) => Math.min(Math.max(0, existing), prompt.length));
2124+
setComposerCursor((existing) => clampCollapsedComposerCursor(prompt, existing));
21202125
}, [prompt]);
21212126

21222127
useEffect(() => {
@@ -2129,7 +2134,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
21292134
setSendPhase("idle");
21302135
setSendStartedAt(null);
21312136
setComposerHighlightedItemId(null);
2132-
setComposerCursor(promptRef.current.length);
2137+
setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length));
21332138
setComposerTrigger(detectComposerTrigger(promptRef.current, promptRef.current.length));
21342139
dragDepthRef.current = 0;
21352140
setIsDragOverComposer(false);
@@ -2812,7 +2817,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
28122817
});
28132818
promptRef.current = trimmed;
28142819
setPrompt(trimmed);
2815-
setComposerCursor(trimmed.length);
2820+
setComposerCursor(collapseExpandedComposerCursor(trimmed, trimmed.length));
28162821
addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry));
28172822
setComposerTrigger(detectComposerTrigger(trimmed, trimmed.length));
28182823
}
@@ -3285,6 +3290,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
32853290
return false;
32863291
}
32873292
const next = replaceTextRange(promptRef.current, rangeStart, rangeEnd, replacement);
3293+
const nextCursor = collapseExpandedComposerCursor(next.text, next.cursor);
32883294
promptRef.current = next.text;
32893295
const activePendingQuestion = activePendingProgress?.activeQuestion;
32903296
if (activePendingQuestion && activePendingUserInput) {
@@ -3301,33 +3307,51 @@ export default function ChatView({ threadId }: ChatViewProps) {
33013307
} else {
33023308
setPrompt(next.text);
33033309
}
3304-
setComposerCursor(next.cursor);
3305-
setComposerTrigger(detectComposerTrigger(next.text, next.cursor));
3310+
setComposerCursor(nextCursor);
3311+
setComposerTrigger(
3312+
detectComposerTrigger(next.text, expandCollapsedComposerCursor(next.text, nextCursor)),
3313+
);
33063314
window.requestAnimationFrame(() => {
3307-
composerEditorRef.current?.focusAt(next.cursor);
3315+
composerEditorRef.current?.focusAt(nextCursor);
33083316
});
33093317
return true;
33103318
},
33113319
[activePendingProgress?.activeQuestion, activePendingUserInput, setPrompt],
33123320
);
33133321

3314-
const readComposerSnapshot = useCallback((): { value: string; cursor: number } => {
3315-
const editorSnapshot = composerEditorRef.current?.readSnapshot();
3316-
if (editorSnapshot) {
3317-
return editorSnapshot;
3318-
}
3319-
return { value: promptRef.current, cursor: composerCursor };
3320-
}, [composerCursor]);
3322+
const readComposerSnapshot = useCallback(
3323+
(): { value: string; cursor: number; expandedCursor: number } => {
3324+
const editorSnapshot = composerEditorRef.current?.readSnapshot();
3325+
if (editorSnapshot) {
3326+
return editorSnapshot;
3327+
}
3328+
return {
3329+
value: promptRef.current,
3330+
cursor: composerCursor,
3331+
expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor),
3332+
};
3333+
},
3334+
[composerCursor],
3335+
);
3336+
3337+
const extendReplacementRangeForTrailingSpace = useCallback(
3338+
(text: string, rangeEnd: number, replacement: string): number => {
3339+
if (!replacement.endsWith(" ")) {
3340+
return rangeEnd;
3341+
}
3342+
return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd;
3343+
},
3344+
[],
3345+
);
33213346

33223347
const resolveActiveComposerTrigger = useCallback((): {
3323-
snapshot: { value: string; cursor: number };
3348+
snapshot: { value: string; cursor: number; expandedCursor: number };
33243349
trigger: ComposerTrigger | null;
33253350
} => {
33263351
const snapshot = readComposerSnapshot();
3327-
const expandedCursor = expandCollapsedComposerCursor(snapshot.value, snapshot.cursor);
33283352
return {
33293353
snapshot,
3330-
trigger: detectComposerTrigger(snapshot.value, expandedCursor),
3354+
trigger: detectComposerTrigger(snapshot.value, snapshot.expandedCursor),
33313355
};
33323356
}, [readComposerSnapshot]);
33333357

@@ -3340,13 +3364,20 @@ export default function ChatView({ threadId }: ChatViewProps) {
33403364
});
33413365
const { snapshot, trigger } = resolveActiveComposerTrigger();
33423366
if (!trigger) return;
3343-
const expectedToken = snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd);
33443367
if (item.type === "path") {
3368+
const replacement = `@${item.path} `;
3369+
const replacementRangeEnd = extendReplacementRangeForTrailingSpace(
3370+
snapshot.value,
3371+
trigger.rangeEnd,
3372+
replacement,
3373+
);
33453374
const applied = applyPromptReplacement(
33463375
trigger.rangeStart,
3347-
trigger.rangeEnd,
3348-
`@${item.path} `,
3349-
{ expectedText: expectedToken },
3376+
replacementRangeEnd,
3377+
replacement,
3378+
{
3379+
expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd),
3380+
},
33503381
);
33513382
if (applied) {
33523383
setComposerHighlightedItemId(null);
@@ -3355,17 +3386,28 @@ export default function ChatView({ threadId }: ChatViewProps) {
33553386
}
33563387
if (item.type === "slash-command") {
33573388
if (item.command === "model") {
3358-
const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "/model ", {
3359-
expectedText: expectedToken,
3360-
});
3389+
const replacement = "/model ";
3390+
const replacementRangeEnd = extendReplacementRangeForTrailingSpace(
3391+
snapshot.value,
3392+
trigger.rangeEnd,
3393+
replacement,
3394+
);
3395+
const applied = applyPromptReplacement(
3396+
trigger.rangeStart,
3397+
replacementRangeEnd,
3398+
replacement,
3399+
{
3400+
expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd),
3401+
},
3402+
);
33613403
if (applied) {
33623404
setComposerHighlightedItemId(null);
33633405
}
33643406
return;
33653407
}
33663408
void handleInteractionModeChange(item.command === "plan" ? "plan" : "default");
33673409
const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", {
3368-
expectedText: expectedToken,
3410+
expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd),
33693411
});
33703412
if (applied) {
33713413
setComposerHighlightedItemId(null);
@@ -3374,14 +3416,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
33743416
}
33753417
onProviderModelSelect(item.provider, item.model);
33763418
const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", {
3377-
expectedText: expectedToken,
3419+
expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd),
33783420
});
33793421
if (applied) {
33803422
setComposerHighlightedItemId(null);
33813423
}
33823424
},
33833425
[
33843426
applyPromptReplacement,
3427+
extendReplacementRangeForTrailingSpace,
33853428
handleInteractionModeChange,
33863429
onProviderModelSelect,
33873430
resolveActiveComposerTrigger,
@@ -3415,7 +3458,12 @@ export default function ChatView({ threadId }: ChatViewProps) {
34153458
workspaceEntriesQuery.isFetching);
34163459

34173460
const onPromptChange = useCallback(
3418-
(nextPrompt: string, nextCursor: number, cursorAdjacentToMention: boolean) => {
3461+
(
3462+
nextPrompt: string,
3463+
nextCursor: number,
3464+
nextExpandedCursor: number,
3465+
cursorAdjacentToMention: boolean,
3466+
) => {
34193467
if (activePendingProgress?.activeQuestion && activePendingUserInput) {
34203468
onChangeActivePendingUserInputCustomAnswer(
34213469
activePendingProgress.activeQuestion.id,
@@ -3431,10 +3479,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
34313479
setComposerTrigger(
34323480
cursorAdjacentToMention
34333481
? null
3434-
: detectComposerTrigger(
3435-
nextPrompt,
3436-
expandCollapsedComposerCursor(nextPrompt, nextCursor),
3437-
),
3482+
: detectComposerTrigger(nextPrompt, nextExpandedCursor),
34383483
);
34393484
},
34403485
[

0 commit comments

Comments
 (0)