Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/desktop/src/main/workspace/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface AppConfig {
language?: 'en' | 'zh';
gateways: GatewayServerConfig[];
defaultGatewayId?: string;
// Legacy fields kept for migration detection
sendShortcut?: 'enter' | 'cmdEnter';
gatewayUrl?: string;
bootstrapToken?: string;
password?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/src/preload/clawwork.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface AppSettings {
language?: 'en' | 'zh';
gateways: GatewayServerConfig[];
defaultGatewayId?: string;
// Legacy fields kept for migration detection
sendShortcut?: 'enter' | 'cmdEnter';
gatewayUrl?: string;
bootstrapToken?: string;
password?: string;
Expand Down
42 changes: 41 additions & 1 deletion packages/desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Toaster } from 'sonner'
import LeftNav from './layouts/LeftNav'
Expand All @@ -7,6 +7,7 @@ import RightPanel from './layouts/RightPanel'
import Setup from './layouts/Setup'
import Settings from './layouts/Settings'
import { useUiStore } from './stores/uiStore'
import { useTaskStore } from './stores/taskStore'
import { useGatewayEventDispatcher } from './hooks/useGatewayDispatcher'
import { useTheme } from './hooks/useTheme'
import { useUpdateCheck } from './hooks/useUpdateCheck'
Expand All @@ -23,6 +24,9 @@ export default function App() {
const settingsOpen = useUiStore((s) => s.settingsOpen)
const setSettingsOpen = useUiStore((s) => s.setSettingsOpen)
const theme = useUiStore((s) => s.theme)
const setMainView = useUiStore((s) => s.setMainView)
const focusSearch = useUiStore((s) => s.focusSearch)
const createTask = useTaskStore((s) => s.createTask)

useGatewayEventDispatcher()
useTheme()
Expand All @@ -38,6 +42,42 @@ export default function App() {
})
}, [])

useEffect(() => {
if (!ready) return
window.clawwork.getSettings().then((settings) => {
if (settings?.sendShortcut) {
useUiStore.setState({ sendShortcut: settings.sendShortcut })
}
})
}, [ready])

const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
const meta = e.metaKey || e.ctrlKey
if (!meta) return

if (e.shiftKey && e.code === 'KeyO') {
e.preventDefault()
createTask()
return
}

if (e.shiftKey && e.code === 'KeyF') {
e.preventDefault()
setMainView('files')
return
}

if (!e.shiftKey && e.code === 'KeyK') {
e.preventDefault()
focusSearch()
}
}, [createTask, setMainView, focusSearch])

useEffect(() => {
window.addEventListener('keydown', handleGlobalKeyDown)
return () => window.removeEventListener('keydown', handleGlobalKeyDown)
}, [handleGlobalKeyDown])

if (needsSetup) {
return (
<TooltipProvider>
Expand Down
23 changes: 16 additions & 7 deletions packages/desktop/src/renderer/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { Send, Paperclip, X, ChevronDown, Cpu, Brain } from 'lucide-react';
import type { MessageImageAttachment } from '@clawwork/shared';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { cn, modKey } from '@/lib/utils';
import { motion as motionPresets } from '@/styles/design-tokens';
import { Button } from '@/components/ui/button';
import {
Expand Down Expand Up @@ -78,6 +78,8 @@ export default function ChatInput() {
const [slashIndex, setSlashIndex] = useState(0);
const slashCommands = filterSlashCommands(slashQuery);

const sendShortcut = useUiStore((s) => s.sendShortcut);

/** Evaluate the current textarea value and cursor position to determine
* whether to show the slash command menu. */
const updateSlashMenu = useCallback(() => {
Expand Down Expand Up @@ -247,13 +249,18 @@ export default function ChatInput() {
}
}

// ── Normal send ───────────────────────────────────────────────────────────
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
// ── Send ──────────────────────────────────────────────────────────────────
if (e.key === 'Enter') {
const isCmdEnterMode = sendShortcut === 'cmdEnter';
const meta = e.metaKey || e.ctrlKey;
const shouldSend = isCmdEnterMode ? (meta && !e.shiftKey) : (!e.shiftKey && !meta);
if (shouldSend) {
e.preventDefault();
handleSend();
}
}
},
[slashMenuVisible, slashCommands, slashIndex, commitSlashCommand, handleSend],
[slashMenuVisible, slashCommands, slashIndex, commitSlashCommand, handleSend, sendShortcut],
);

const handleInput = useCallback(() => {
Expand Down Expand Up @@ -491,7 +498,9 @@ export default function ChatInput() {
<p className="text-xs text-[var(--text-muted)] text-center mt-2.5 tracking-wide">
{isOffline
? t('chatInput.offlineHint')
: t('chatInput.poweredBy')}
: sendShortcut === 'cmdEnter'
? t('chatInput.poweredBy') + ' · ' + t('chatInput.toSend', { mod: modKey })
: t('chatInput.poweredBy')}
</p>
</div>
</div>
Expand Down
14 changes: 10 additions & 4 deletions packages/desktop/src/renderer/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
"appearance": "Appearance",
"theme": "Theme",
"themeUpdated": "Theme updated",
"shortcuts": "Shortcuts",
"sendShortcut": "Send message",
"sendEnter": "Enter",
"sendCmdEnter": "{{mod}} Enter",
"shortcutUpdated": "Shortcut updated",
"gateways": "Gateways",
"addGateway": "Add Gateway",
"editGateway": "Edit Gateway",
Expand Down Expand Up @@ -137,11 +142,12 @@
"sendFailed": "Send failed"
},
"chatInput": {
"describeTask": "Describe your task\u2026",
"offlineReadOnly": "Offline \u2014 read-only",
"createTaskFirst": "Create a task first\u2026",
"describeTask": "Describe your task",
"offlineReadOnly": "Offline read-only",
"createTaskFirst": "Create a task first",
"poweredBy": "AI can make mistakes. Check important info.",
"offlineHint": "Offline \u2014 browse past tasks and files",
"offlineHint": "Offline — browse past tasks and files",
"toSend": "{{mod}}↵ to send",
"image": "Image",
"model": "Model",
"thinking": "Thinking",
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop/src/renderer/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
"appearance": "外观",
"theme": "主题",
"themeUpdated": "主题已更新",
"shortcuts": "快捷键",
"sendShortcut": "发送消息",
"sendEnter": "回车",
"sendCmdEnter": "{{mod}} 回车",
"shortcutUpdated": "快捷键已更新",
"gateways": "网关",
"addGateway": "添加网关",
"editGateway": "编辑网关",
Expand Down Expand Up @@ -142,6 +147,7 @@
"createTaskFirst": "请先创建一个任务…",
"poweredBy": "由 OpenClaw 驱动 · 任务文件自动 Git 归档",
"offlineHint": "离线模式 — 可浏览历史任务和文件",
"toSend": "{{mod}}↵ 发送",
"image": "图片",
"model": "模型",
"thinking": "思考",
Expand Down
9 changes: 9 additions & 0 deletions packages/desktop/src/renderer/layouts/LeftNav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function LeftNav() {
const hasUpdate = useUiStore((s) => s.hasUpdate)
const agentCatalog = useUiStore((s) => s.agentCatalog)
const hasMultipleAgents = agentCatalog.length > 1
const searchFocusTrigger = useUiStore((s) => s.searchFocusTrigger)

// Aggregate: if any gateway connected → connected; any connecting → connecting; else disconnected
const gwStatusValues = Object.values(gwStatusMap)
Expand All @@ -46,6 +47,13 @@ export default function LeftNav() {
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
const searchInputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
if (searchFocusTrigger === 0) return
searchInputRef.current?.focus()
searchInputRef.current?.select()
}, [searchFocusTrigger])

useEffect(() => {
clearTimeout(timerRef.current)
Expand Down Expand Up @@ -114,6 +122,7 @@ export default function LeftNav() {
<div className="titlebar-no-drag relative">
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)]" />
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
Expand Down
31 changes: 29 additions & 2 deletions packages/desktop/src/renderer/layouts/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { cn, modKey } from '@/lib/utils'
import { motion as motionPresets } from '@/styles/design-tokens'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { useUiStore, type Language, type GatewayConnectionStatus } from '@/stores/uiStore'
import { useUiStore, type Language, type GatewayConnectionStatus, type SendShortcut } from '@/stores/uiStore'

interface UpdateCheckResult {
currentVersion: string
Expand Down Expand Up @@ -148,6 +148,8 @@ export default function Settings({ onClose }: SettingsProps) {
const setTheme = useUiStore((s) => s.setTheme)
const language = useUiStore((s) => s.language)
const setLanguage = useUiStore((s) => s.setLanguage)
const sendShortcut = useUiStore((s) => s.sendShortcut)
const setSendShortcut = useUiStore((s) => s.setSendShortcut)
const gatewayStatusMap = useUiStore((s) => s.gatewayStatusMap)
const setDefaultGatewayId = useUiStore((s) => s.setDefaultGatewayId)
const setGatewayInfoMap = useUiStore((s) => s.setGatewayInfoMap)
Expand Down Expand Up @@ -206,6 +208,12 @@ export default function Settings({ onClose }: SettingsProps) {
toast.success(t('settings.themeUpdated'))
}, [setTheme, t])

const handleShortcutChange = useCallback((next: SendShortcut) => {
if (next === sendShortcut) return
setSendShortcut(next)
toast.success(t('settings.shortcutUpdated'))
}, [sendShortcut, setSendShortcut, t])

const openAddForm = useCallback(() => {
setEditingId(null)
setForm(EMPTY_FORM)
Expand Down Expand Up @@ -366,6 +374,25 @@ export default function Settings({ onClose }: SettingsProps) {
))}
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--text-primary)]">{t('settings.sendShortcut')}</span>
<div className="flex rounded-lg border border-[var(--border)] overflow-hidden">
{(['enter', 'cmdEnter'] as const).map((s) => (
<button
key={s}
onClick={() => handleShortcutChange(s)}
className={cn(
'px-3.5 py-1.5 text-sm transition-colors',
sendShortcut === s
? 'bg-[var(--accent)] text-[var(--bg-primary)] font-medium'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]',
)}
>
{s === 'enter' ? t('settings.sendEnter') : t('settings.sendCmdEnter', { mod: modKey })}
</button>
))}
</div>
</div>
</div>
</section>

Expand Down
4 changes: 4 additions & 0 deletions packages/desktop/src/renderer/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ export function formatTokenCount(tokens: number): string {
if (tokens < 100_000) return `${(tokens / 1000).toFixed(1)}k`
return `${(tokens / 1000).toFixed(0)}k`
}

export const isMac = navigator.platform.toUpperCase().includes('MAC')

export const modKey = isMac ? '⌘' : 'Ctrl'
17 changes: 17 additions & 0 deletions packages/desktop/src/renderer/stores/uiStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type Theme = 'dark' | 'light';

export type Language = 'en' | 'zh';

export type SendShortcut = 'enter' | 'cmdEnter';

export type GatewayConnectionStatus = 'connected' | 'connecting' | 'disconnected';

export interface GatewayInfo {
Expand Down Expand Up @@ -62,6 +64,12 @@ interface UiState {
agentCatalog: AgentInfo[];
defaultAgentId: string;
setAgentCatalog: (agents: AgentInfo[], defaultId: string) => void;

sendShortcut: SendShortcut;
setSendShortcut: (shortcut: SendShortcut) => void;

searchFocusTrigger: number;
focusSearch: () => void;
}

export const useUiStore = create<UiState>((set) => ({
Expand Down Expand Up @@ -120,4 +128,13 @@ export const useUiStore = create<UiState>((set) => ({
agentCatalog: [],
defaultAgentId: 'main',
setAgentCatalog: (agents, defaultId) => set({ agentCatalog: agents, defaultAgentId: defaultId }),

sendShortcut: 'enter',
setSendShortcut: (shortcut) => {
set({ sendShortcut: shortcut });
window.clawwork.updateSettings({ sendShortcut: shortcut });
},

searchFocusTrigger: 0,
focusSearch: () => set((s) => ({ searchFocusTrigger: s.searchFocusTrigger + 1 })),
}));
Loading