From 4a56527d1b5ac41a49d89c76faff7ff58bdf75d0 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 29 Jul 2026 18:28:19 +0300 Subject: [PATCH] fix: pass draft starter text as command arguments --- packages/ui/src/components/chat/ChatContainer.tsx | 2 +- packages/ui/src/components/chat/ChatInput.tsx | 13 +++++++++---- .../ui/src/components/chat/DraftPresetChips.tsx | 10 +++++----- packages/ui/src/sync/input-store.ts | 8 ++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index 5af4cd64..603e9b5c 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -516,7 +516,7 @@ const DraftWelcome: React.FC = () => { )} useInputStore.getState().requestPresetSubmit(text)} + onSubmit={(starter) => useInputStore.getState().requestPresetSubmit(starter.submitText, starter.ref.type)} className="oc-draft-starters mt-8 max-w-md" /> diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 51a0d889..cbb4e972 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -1271,12 +1271,14 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo }, [inputMode, getCurrentInputSnapshot, currentSessionId, sessionPhase, autoReviewRunning, followUpBehavior, handleQueueMessage]); // Draft welcome presets: submit immediately. - const submitPresetPrompt = React.useCallback((text: string) => { + const submitPresetPrompt = React.useCallback((text: string, type: 'command' | 'skill') => { // The text goes straight into the submit (see SubmitOptions.presetText) // instead of through the composer input — the collapsed mobile pill has // no mounted textarea to stage it in. const draft = (composerRef.current?.getValue() ?? messageRef.current).trim(); - const presetText = draft ? `${text}\n${draft}` : text; + // OpenCode recognizes slash commands only when their arguments follow + // the command on the same line. Skills retain the multiline prompt form. + const presetText = draft ? `${text}${type === 'command' ? ' ' : '\n'}${draft}` : text; void handleSubmitRef.current({ presetText }); }, []); @@ -1308,7 +1310,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo React.useEffect(() => { if (pendingPresetSubmit == null) return; const text = useInputStore.getState().consumePendingPresetSubmit(); - if (text) submitPresetPrompt(text); + if (text) submitPresetPrompt(text.text, text.type); }, [pendingPresetSubmit, submitPresetPrompt]); const handleKeyDown = (e: KeyboardEvent) => { @@ -2718,7 +2720,10 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo ) : null} {newSessionDraftOpen && !isDesktopExpanded && !isMobile && !isVSCode && !isMiniChatSurface ? ( - + submitPresetPrompt(starter.submitText, starter.ref.type)} + className="chat-input-column mt-4" + /> ) : null} diff --git a/packages/ui/src/components/chat/DraftPresetChips.tsx b/packages/ui/src/components/chat/DraftPresetChips.tsx index d02b3665..956fc721 100644 --- a/packages/ui/src/components/chat/DraftPresetChips.tsx +++ b/packages/ui/src/components/chat/DraftPresetChips.tsx @@ -41,8 +41,8 @@ import { } from './useDraftStarters'; type DraftPresetChipsProps = { - /** Called with the resolved text (command or skill invocation) when a chip is clicked. */ - onSubmit: (text: string) => void; + /** Called with the resolved starter invocation when a chip is clicked. */ + onSubmit: (starter: ResolvedStarter) => void; /** Extra classes for the wrapper (e.g. width/spacing per surface). */ className?: string; }; @@ -66,7 +66,7 @@ const PICKER_SECTIONS: { key: PinnableSection; headingKey: 'chat.draftStarters.s const SortableChip: React.FC<{ item: ResolvedStarter; - onSubmit: (text: string) => void; + onSubmit: (starter: ResolvedStarter) => void; onRemove: () => void; /** Hide the per-chip hover "x" (mobile uses the trash drop-zone instead). */ hideRemove?: boolean; @@ -91,7 +91,7 @@ const SortableChip: React.FC<{ type="button" {...attributes} {...listeners} - onClick={() => onSubmit(item.submitText)} + onClick={() => onSubmit(item)} className="group inline-flex touch-none select-none items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-[var(--interactive-hover)] hover:text-foreground" style={chipStyle} > @@ -116,7 +116,7 @@ const SortableChip: React.FC<{ const StarterGroup: React.FC<{ items: ResolvedStarter[]; - onSubmit: (text: string) => void; + onSubmit: (starter: ResolvedStarter) => void; onRemove: (item: ResolvedStarter) => void; hideRemove?: boolean; }> = ({ items, onSubmit, onRemove, hideRemove }) => ( diff --git a/packages/ui/src/sync/input-store.ts b/packages/ui/src/sync/input-store.ts index 1f923198..8f4d0635 100644 --- a/packages/ui/src/sync/input-store.ts +++ b/packages/ui/src/sync/input-store.ts @@ -105,14 +105,14 @@ export type InputState = { * render the chips outside ChatInput (e.g. under the welcome message on * narrow layouts); consumed by ChatInput, which owns the command-aware submit. */ - pendingPresetSubmit: string | null + pendingPresetSubmit: { text: string; type: "command" | "skill" } | null attachedFiles: AttachedFile[] activeEditorFile: VSCodeActiveEditorFile | null setPendingInputText: (text: string | null, mode?: "replace" | "append" | "append-inline") => void consumePendingInputText: () => { text: string; mode: "replace" | "append" | "append-inline" } | null - requestPresetSubmit: (text: string) => void - consumePendingPresetSubmit: () => string | null + requestPresetSubmit: (text: string, type: "command" | "skill") => void + consumePendingPresetSubmit: () => { text: string; type: "command" | "skill" } | null setPendingSyntheticParts: (parts: SyntheticContextPart[] | null) => void consumePendingSyntheticParts: () => SyntheticContextPart[] | null addAttachedFile: (file: File) => Promise @@ -144,7 +144,7 @@ export const useInputStore = create()((set, get) => ({ return { text: pendingInputText, mode: pendingInputMode } }, - requestPresetSubmit: (text) => set({ pendingPresetSubmit: text }), + requestPresetSubmit: (text, type) => set({ pendingPresetSubmit: { text, type } }), consumePendingPresetSubmit: () => { const { pendingPresetSubmit } = get()