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
19 changes: 17 additions & 2 deletions interface/src/components/CortexChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useCallback, useEffect, useRef, useState} from "react";
import {useCortexChat, type ToolActivity} from "@/hooks/useCortexChat";
import {useChatInputPrefs} from "@/hooks/useChatInputPrefs";
import {Markdown} from "@/components/Markdown";
import {ToolCall, type ToolCallPair} from "@/components/ToolCall";
import {
Expand Down Expand Up @@ -184,6 +185,7 @@ function CortexChatInput({
isStreaming: boolean;
}) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const {prefs} = useChatInputPrefs();

useEffect(() => {
textareaRef.current?.focus();
Expand All @@ -206,8 +208,17 @@ function CortexChatInput({
return () => textarea.removeEventListener("input", adjustHeight);
}, [value]);

const canSend = !isStreaming && value.trim().length > 0;
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter" && !event.shiftKey) {
if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
const hasSubmitModifier =
event.metaKey || event.ctrlKey || event.altKey;
if (prefs.enterToSubmit) {
if (event.shiftKey) return;
event.preventDefault();
if (canSend) onSubmit();
Comment on lines +216 to +219
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Alt+Enter incorrectly submits in Enter-to-submit mode.

Line 217 only exempts Shift+Enter, so Alt+Enter currently submits. That conflicts with the documented mode-A behavior where Shift/Option+Enter should insert a newline.

Suggested patch
 		if (prefs.enterToSubmit) {
-			if (event.shiftKey) return;
+			if (event.shiftKey || event.altKey) return;
 			event.preventDefault();
 			if (canSend) onSubmit();
 		} else if (hasSubmitModifier) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (prefs.enterToSubmit) {
if (event.shiftKey) return;
event.preventDefault();
if (canSend) onSubmit();
if (prefs.enterToSubmit) {
if (event.shiftKey || event.altKey) return;
event.preventDefault();
if (canSend) onSubmit();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interface/src/components/CortexChatPanel.tsx` around lines 216 - 219, The
Enter-to-submit branch inside the key handler (where prefs.enterToSubmit is
checked) only ignores Shift+Enter but not Alt/Option+Enter, so Alt+Enter still
submits; update the condition in the handler (the block referencing
prefs.enterToSubmit and canSend/onSubmit) to also return when event.altKey
(Option on macOS) is true (e.g., if (event.shiftKey || event.altKey) return;) so
Shift/Option+Enter inserts a newline instead of submitting.

} else if (hasSubmitModifier) {
if (!canSend) return;
event.preventDefault();
onSubmit();
}
Expand All @@ -222,7 +233,11 @@ function CortexChatInput({
onChange={(event) => onChange(event.target.value)}
onKeyDown={handleKeyDown}
placeholder={
isStreaming ? "Waiting for response..." : "Message the cortex..."
isStreaming
? "Waiting for response..."
: prefs.enterToSubmit
? "Message the cortex..."
: "Message the cortex... (⌘/Ctrl+Enter to send)"
}
disabled={isStreaming}
rows={1}
Expand Down
11 changes: 10 additions & 1 deletion interface/src/components/portal/PortalComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useRef, useState } from "react";
import { ChatComposer, type ModelOption } from "@spacedrive/ai";
import { usePopover } from "@spacedrive/primitives";
import { Paperclip, X } from "@phosphor-icons/react";
import { useChatInputPrefs } from "@/hooks/useChatInputPrefs";

interface PortalComposerProps {
agentName: string;
Expand Down Expand Up @@ -44,6 +45,7 @@ export function PortalComposer({
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
const dragCounter = useRef(0);
const {prefs} = useChatInputPrefs();

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? []);
Expand Down Expand Up @@ -133,8 +135,15 @@ export function PortalComposer({
draft={draft}
onDraftChange={onDraftChange}
onSend={onSend}
placeholder={disabled ? "Waiting for response..." : `Message ${agentName}...`}
placeholder={
disabled
? "Waiting for response..."
: prefs.enterToSubmit
? `Message ${agentName}...`
: `Message ${agentName}... (⌘/Ctrl+Enter to send)`
}
isSending={disabled}
enterToSubmit={prefs.enterToSubmit}
toolbarExtra={paperclipButton}
projectSelector={
projectOptions.length > 0
Expand Down
62 changes: 62 additions & 0 deletions interface/src/components/settings/CompositionSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {useChatInputPrefs} from "@/hooks/useChatInputPrefs";

export function CompositionSection() {
const {prefs, setEnterToSubmit} = useChatInputPrefs();

const options: {
value: boolean;
title: string;
description: string;
}[] = [
{
value: true,
title: "Enter to send",
description: "Enter sends the message. Shift+Enter inserts a new line.",
},
{
value: false,
title: "Enter for new line",
description:
"Enter inserts a new line. ⌘/Ctrl+Enter (or Option+Enter) sends.",
},
];

return (
<div className="mx-auto max-w-2xl px-6 py-6">
<div className="mb-6">
<h2 className="font-plex text-sm font-semibold text-ink">
Message input
</h2>
<p className="mt-1 text-sm text-ink-dull">
Choose how the Enter key behaves in the chat composer.
</p>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{options.map((opt) => {
const active = prefs.enterToSubmit === opt.value;
return (
<button
key={String(opt.value)}
type="button"
aria-pressed={active}
onClick={() => setEnterToSubmit(opt.value)}
className={`flex flex-col items-start rounded-lg border p-4 text-left transition-colors ${
active
? "border-accent bg-accent/10"
: "border-app-line bg-app-box hover:border-app-line/80 hover:bg-app-hover"
}`}
>
<div className="flex w-full items-center justify-between">
<span className="text-sm font-medium text-ink">
{opt.title}
</span>
{active && <span className="h-2 w-2 rounded-full bg-accent" />}
</div>
<p className="mt-1 text-sm text-ink-dull">{opt.description}</p>
</button>
);
})}
</div>
</div>
);
}
6 changes: 6 additions & 0 deletions interface/src/components/settings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export const SECTIONS = [
group: "general" as const,
description: "Theme and display settings",
},
{
id: "composition" as const,
label: "Composition",
group: "general" as const,
description: "Chat composer and message input behavior",
},
{
id: "config-file" as const,
label: "Config File",
Expand Down
1 change: 1 addition & 0 deletions interface/src/components/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {InstanceSection} from "./InstanceSection";
export {AppearanceSection} from "./AppearanceSection";
export {CompositionSection} from "./CompositionSection";
export {ChannelsSection} from "./ChannelsSection";
export {SecretsSection} from "./SecretsSection";
export {ApiKeysSection} from "./ApiKeysSection";
Expand Down
1 change: 1 addition & 0 deletions interface/src/components/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {GlobalSettingsResponse} from "@/api/client";
export type SectionId =
| "instance"
| "appearance"
| "composition"
| "providers"
| "channels"
| "api-keys"
Expand Down
47 changes: 47 additions & 0 deletions interface/src/hooks/useChatInputPrefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {useCallback, useEffect, useState} from "react";

export interface ChatInputPrefs {
enterToSubmit: boolean;
}

const STORAGE_KEY = "spacebot-chat-input-prefs";
const DEFAULTS: ChatInputPrefs = {enterToSubmit: true};

function readStored(): ChatInputPrefs {
if (typeof window === "undefined") return DEFAULTS;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULTS;
const parsed = JSON.parse(raw);
return {
enterToSubmit:
typeof parsed?.enterToSubmit === "boolean"
? parsed.enterToSubmit
: DEFAULTS.enterToSubmit,
};
} catch {
return DEFAULTS;
}
}

export function useChatInputPrefs() {
const [prefs, setPrefs] = useState<ChatInputPrefs>(readStored);

useEffect(() => {
const onStorage = (e: StorageEvent) => {
if (e.key === STORAGE_KEY) setPrefs(readStored());
};
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);

const setEnterToSubmit = useCallback((value: boolean) => {
setPrefs((prev) => {
const next = {...prev, enterToSubmit: value};
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
return next;
});
}, []);

return {prefs, setEnterToSubmit};
}
3 changes: 3 additions & 0 deletions interface/src/routes/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {ModelSelect} from "@/components/ModelSelect";
import {
InstanceSection,
AppearanceSection,
CompositionSection,
ChannelsSection,
SecretsSection,
ApiKeysSection,
Expand Down Expand Up @@ -556,6 +557,8 @@ export function Settings() {
/>
) : activeSection === "appearance" ? (
<AppearanceSection />
) : activeSection === "composition" ? (
<CompositionSection />
) : activeSection === "providers" ? (
<div className="mx-auto max-w-2xl px-6 py-6">
{/* Section header */}
Expand Down