diff --git a/src/features/composer/components/Composer.tsx b/src/features/composer/components/Composer.tsx
index 574a7baf0..0e3769471 100644
--- a/src/features/composer/components/Composer.tsx
+++ b/src/features/composer/components/Composer.tsx
@@ -118,6 +118,13 @@ export function Composer({
const textareaRef = externalTextareaRef ?? internalRef;
const isDictationBusy = dictationState !== "idle";
const canSend = text.trim().length > 0 || attachedImages.length > 0;
+ const isMac =
+ typeof navigator !== "undefined" &&
+ ((navigator as Navigator & { userAgentData?: { platform?: string } })
+ .userAgentData?.platform ?? navigator.platform ?? "")
+ .toLowerCase()
+ .includes("mac");
+ const sendShortcutLabel = isMac ? "⌘+Enter" : "Ctrl+Enter";
useEffect(() => {
setText((prev) => (prev === draftText ? prev : draftText));
@@ -244,6 +251,7 @@ export function Composer({
canStop={canStop}
canSend={canSend}
isProcessing={isProcessing}
+ sendShortcutLabel={sendShortcutLabel}
onStop={onStop}
onSend={handleSend}
dictationEnabled={dictationEnabled}
@@ -262,7 +270,9 @@ export function Composer({
onTextChange={handleTextChange}
onSelectionChange={handleSelectionChange}
onKeyDown={(event) => {
- if (event.key === "Enter" && event.shiftKey) {
+ const isEnter = event.key === "Enter";
+ const isSendShortcut = isMac ? event.metaKey : event.ctrlKey;
+ if (isEnter && !isSendShortcut) {
event.preventDefault();
const textarea = textareaRef.current;
if (!textarea) {
@@ -295,7 +305,7 @@ export function Composer({
if (event.defaultPrevented) {
return;
}
- if (event.key === "Enter" && !event.shiftKey) {
+ if (isEnter && isSendShortcut) {
if (isDictationBusy) {
event.preventDefault();
return;
diff --git a/src/features/composer/components/ComposerInput.tsx b/src/features/composer/components/ComposerInput.tsx
index 13cd983e9..e9f239278 100644
--- a/src/features/composer/components/ComposerInput.tsx
+++ b/src/features/composer/components/ComposerInput.tsx
@@ -13,6 +13,7 @@ type ComposerInputProps = {
canStop: boolean;
canSend: boolean;
isProcessing: boolean;
+ sendShortcutLabel: string;
onStop: () => void;
onSend: () => void;
dictationState?: "idle" | "listening" | "processing";
@@ -46,6 +47,7 @@ export function ComposerInput({
canStop,
canSend,
isProcessing,
+ sendShortcutLabel,
onStop,
onSend,
dictationState = "idle",
@@ -317,33 +319,38 @@ export function ComposerInput({
>
{isDictating ? : }
-
+
+
);
}
diff --git a/src/styles/composer.css b/src/styles/composer.css
index 95ba08f57..311f15f51 100644
--- a/src/styles/composer.css
+++ b/src/styles/composer.css
@@ -198,6 +198,35 @@
position: relative;
}
+.composer-action-wrap {
+ display: inline-flex;
+ position: relative;
+}
+
+.composer-action-wrap::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ left: 50%;
+ bottom: calc(100% + 6px);
+ transform: translateX(-50%) translateY(4px);
+ padding: 4px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ color: var(--text-emphasis);
+ background: var(--surface-command);
+ border: 1px solid var(--border-subtle);
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 120ms ease, transform 120ms ease;
+ z-index: 2;
+}
+
+.composer-action-wrap:hover::after {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+}
+
.composer-action--mic.is-active {
border-color: rgba(120, 235, 190, 0.6);
background: rgba(120, 235, 190, 0.12);