Skip to content

Commit 774296e

Browse files
authored
🤖 feat: add send button to chat input (#675)
Adds a clickable send button to the ChatInput component that mirrors the existing Enter key behavior.\n\n- Button is shown for both workspace and creation variants\n- Uses shared canSend guard so behavior matches keyboard send\n- Tooltip documents the send keybind\n\n_Generated with _
1 parent c92953c commit 774296e

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

src/browser/components/ChatInput/index.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
} from "@/browser/utils/ui/keybinds";
4848
import { ModelSelector, type ModelSelectorRef } from "../ModelSelector";
4949
import { useModelLRU } from "@/browser/hooks/useModelLRU";
50+
import { SendHorizontal } from "lucide-react";
5051
import { VimTextArea } from "../VimTextArea";
5152
import { ImageAttachments, type ImageAttachment } from "../ImageAttachments";
5253
import {
@@ -61,6 +62,7 @@ import { useTelemetry } from "@/browser/hooks/useTelemetry";
6162
import { setTelemetryEnabled } from "@/common/telemetry";
6263
import { getTokenCountPromise } from "@/browser/utils/tokenizer/rendererClient";
6364
import { CreationCenterContent } from "./CreationCenterContent";
65+
import { cn } from "@/common/lib/utils";
6466
import { CreationControls } from "./CreationControls";
6567
import { useCreationWorkspace } from "./useCreationWorkspace";
6668

@@ -163,6 +165,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
163165
[tokenCountPromise]
164166
);
165167
const hasTypedText = input.trim().length > 0;
168+
const hasImages = imageAttachments.length > 0;
169+
const canSend = (hasTypedText || hasImages) && !disabled && !isSending;
166170
// Setter for model - updates localStorage directly so useSendMessageOptions picks it up
167171
const setPreferredModel = useCallback(
168172
(model: string) => {
@@ -449,8 +453,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
449453
);
450454

451455
const handleSend = async () => {
452-
// Allow sending if there's text or images
453-
if ((!input.trim() && imageAttachments.length === 0) || disabled || isSending) {
456+
if (!canSend) {
454457
return;
455458
}
456459

@@ -912,7 +915,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
912915
/>
913916
)}
914917

915-
<div className="flex items-end gap-2.5" data-component="ChatInputControls">
918+
<div className="flex items-end" data-component="ChatInputControls">
916919
<VimTextArea
917920
ref={inputRef}
918921
value={input}
@@ -1011,6 +1014,25 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10111014

10121015
<div className="ml-auto flex items-center gap-2" data-component="ModelControls">
10131016
<ModeSelector mode={mode} onChange={setMode} />
1017+
<TooltipWrapper inline>
1018+
<button
1019+
type="button"
1020+
onClick={() => void handleSend()}
1021+
disabled={!canSend}
1022+
aria-label="Send message"
1023+
className={cn(
1024+
"inline-flex items-center gap-1 rounded-sm border border-border-light px-2 py-1 text-[11px] font-medium text-white transition-colors duration-200 disabled:opacity-50",
1025+
mode === "plan"
1026+
? "bg-plan-mode hover:bg-plan-mode-hover disabled:hover:bg-plan-mode"
1027+
: "bg-exec-mode hover:bg-exec-mode-hover disabled:hover:bg-exec-mode"
1028+
)}
1029+
>
1030+
<SendHorizontal className="h-3.5 w-3.5" strokeWidth={2.5} />
1031+
</button>
1032+
<Tooltip className="tooltip" align="center">
1033+
Send message ({formatKeybind(KEYBINDS.SEND_MESSAGE)})
1034+
</Tooltip>
1035+
</TooltipWrapper>
10141036
</div>
10151037
</div>
10161038

src/browser/components/VimTextArea.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ export interface VimTextAreaProps
3131
mode: UIMode; // for styling (plan/exec focus color)
3232
isEditing?: boolean;
3333
suppressKeys?: string[]; // keys for which Vim should not interfere (e.g. ["Tab","ArrowUp","ArrowDown","Escape"]) when popovers are open
34+
trailingAction?: React.ReactNode;
3435
}
3536

3637
type VimMode = vim.VimMode;
3738

3839
export const VimTextArea = React.forwardRef<HTMLTextAreaElement, VimTextAreaProps>(
39-
({ value, onChange, mode, isEditing, suppressKeys, onKeyDown, ...rest }, ref) => {
40+
({ value, onChange, mode, isEditing, suppressKeys, onKeyDown, trailingAction, ...rest }, ref) => {
4041
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
4142
// Expose DOM ref to parent
4243
useEffect(() => {
@@ -219,10 +220,15 @@ export const VimTextArea = React.forwardRef<HTMLTextAreaElement, VimTextAreaProp
219220
autoCapitalize="none"
220221
autoComplete="off"
221222
{...rest}
223+
style={{
224+
...(rest.style ?? {}),
225+
...(trailingAction ? { scrollbarGutter: "stable both-edges" } : {}),
226+
}}
222227
className={cn(
223228
"w-full border text-light py-1.5 px-2 rounded font-mono text-[13px] resize-none min-h-8 max-h-[50vh] overflow-y-auto",
224229
"placeholder:text-placeholder",
225230
"focus:outline-none",
231+
trailingAction && "pr-10",
226232
isEditing
227233
? "bg-editing-mode-alpha border-editing-mode focus:border-editing-mode"
228234
: "bg-dark border-border-light",
@@ -232,6 +238,11 @@ export const VimTextArea = React.forwardRef<HTMLTextAreaElement, VimTextAreaProp
232238
: "caret-white selection:bg-selection"
233239
)}
234240
/>
241+
{trailingAction && (
242+
<div className="pointer-events-none absolute right-3.5 bottom-2.5 flex items-center">
243+
<div className="pointer-events-auto">{trailingAction}</div>
244+
</div>
245+
)}
235246
{vimEnabled && vimMode === "normal" && value.length === 0 && (
236247
<div className="pointer-events-none absolute top-1.5 left-2 h-4 w-2 bg-white/50" />
237248
)}

0 commit comments

Comments
 (0)