Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 15 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 @@ -207,7 +209,14 @@ function CortexChatInput({
}, [value]);

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();
onSubmit();
} else if (hasSubmitModifier) {
event.preventDefault();
onSubmit();
}
Expand All @@ -222,7 +231,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
60 changes: 60 additions & 0 deletions interface/src/components/settings/CompositionSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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)}
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