From fb100acc1f3904241a01db006ec80b26da5a2f4b Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:31:39 -0500 Subject: [PATCH 1/4] fix(composer): detect standalone trigger selections --- packages/shared/src/composerTrigger.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/shared/src/composerTrigger.ts b/packages/shared/src/composerTrigger.ts index 6153978edb92..f5215ae8b487 100644 --- a/packages/shared/src/composerTrigger.ts +++ b/packages/shared/src/composerTrigger.ts @@ -140,3 +140,17 @@ export function replaceTextRange( const nextText = `${text.slice(0, safeStart)}${replacement}${text.slice(safeEnd)}`; return { text: nextText, cursor: safeStart + replacement.length }; } + + +/** + * True when removing the active autocomplete trigger leaves no other prompt text. + * Used by no-argument provider commands so selecting a command only auto-runs a + * standalone command and never discards surrounding draft text. + */ +export function composerTriggerIsOnlyText( + text: string, + rangeStart: number, + rangeEnd: number, +): boolean { + return replaceTextRange(text, rangeStart, rangeEnd, "").text.trim().length === 0; +} From 00c5ddd508397a1fc2c00567c3b84c64799fb71d Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:31:41 -0500 Subject: [PATCH 2/4] test(composer): cover standalone provider command gate --- packages/shared/src/composerTrigger.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/composerTrigger.test.ts b/packages/shared/src/composerTrigger.test.ts index 06b7038dad2b..a70fd255c619 100644 --- a/packages/shared/src/composerTrigger.test.ts +++ b/packages/shared/src/composerTrigger.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vite-plus/test"; -import { detectComposerTrigger, serializeComposerFileLink } from "./composerTrigger.ts"; +import { + composerTriggerIsOnlyText, + detectComposerTrigger, + serializeComposerFileLink, +} from "./composerTrigger.ts"; describe("detectComposerTrigger", () => { it.each(["$", "€", "£", "¥", "₹", "₩", "₿", "𑿝"])( @@ -17,6 +21,18 @@ describe("detectComposerTrigger", () => { ); }); +describe("composerTriggerIsOnlyText", () => { + it("accepts a standalone slash-command trigger", () => { + expect(composerTriggerIsOnlyText("/com", 0, 4)).toBe(true); + expect(composerTriggerIsOnlyText(" /com ", 2, 6)).toBe(true); + }); + + it("rejects a trigger when other draft text would remain", () => { + expect(composerTriggerIsOnlyText("/com keep this draft", 0, 4)).toBe(false); + expect(composerTriggerIsOnlyText("keep /com", 5, 9)).toBe(false); + }); +}); + describe("serializeComposerFileLink", () => { it("uses the basename as the markdown label", () => { expect(serializeComposerFileLink("path/to/package.json")).toBe( From fa74f2f514f6dfd0028c8d362f52ff65d7d84dad Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:31:44 -0500 Subject: [PATCH 3/4] fix(web): preserve draft around provider commands --- apps/web/src/components/chat/ChatComposer.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index b4967db4287b..411bfe1a9885 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -48,7 +48,10 @@ import { pastedTextDisposition, wouldTextPasteExceedLimit, } from "@t3tools/client-runtime/text-paste"; -import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger"; +import { + composerTriggerIsOnlyText, + serializeComposerFileLink, +} from "@t3tools/shared/composerTrigger"; import { folderDropTarget, resolveDroppedFolderPath } from "./folderDrop"; import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model"; import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits"; @@ -3640,7 +3643,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) } return; } - if (item.command.argumentMode === "none" && onProviderCommand) { + if ( + item.command.argumentMode === "none" && + onProviderCommand && + composerTriggerIsOnlyText(snapshot.value, trigger.rangeStart, trigger.rangeEnd) + ) { const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd), }); From f733096cce99571c915722dc03d8d485a4bf29d1 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:31:47 -0500 Subject: [PATCH 4/4] fix(mobile): preserve draft around provider commands --- apps/mobile/src/features/threads/use-composer-command-menu.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.ts b/apps/mobile/src/features/threads/use-composer-command-menu.ts index e6cfe6d52256..48b05457de6d 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.ts @@ -16,6 +16,7 @@ import { } from "../../state/use-composer-drafts"; import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits"; import { + composerTriggerIsOnlyText, detectComposerTrigger, replaceTextRange, serializeComposerFileLink, @@ -534,7 +535,8 @@ export function useComposerCommandMenu({ if ( item.type === "provider-slash-command" && item.command.argumentMode === "none" && - onProviderCommand + onProviderCommand && + composerTriggerIsOnlyText(draftMessage, trigger.rangeStart, trigger.rangeEnd) ) { const cleared = replaceTextRange(draftMessage, trigger.rangeStart, trigger.rangeEnd, ""); setSelection({ start: cleared.cursor, end: cleared.cursor });