mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
feat(ui): global Ctrl+K / Ctrl+N shortcuts with sidebar hint
This commit is contained in:
parent
e10dd9d0a0
commit
c20c066cbc
@ -400,6 +400,9 @@ export default {
|
||||
appearance: 'Appearance & Language',
|
||||
roleUser: 'User',
|
||||
roleAdmin: 'Admin',
|
||||
shortcutAgents: 'Agents',
|
||||
shortcutNew: 'New',
|
||||
shortcutsHint: 'Ctrl+K {agents} | Ctrl+N {newAction}',
|
||||
},
|
||||
notifications: {
|
||||
pendingApprovals: '{n} tool call(s) pending approval',
|
||||
|
||||
@ -400,6 +400,9 @@ export default {
|
||||
appearance: '外观与语言',
|
||||
roleUser: '用户',
|
||||
roleAdmin: '管理员',
|
||||
shortcutAgents: '智能体',
|
||||
shortcutNew: '新建',
|
||||
shortcutsHint: 'Ctrl+K {agents} | Ctrl+N {newAction}',
|
||||
},
|
||||
// Issue #81: provider-level hint / status text shared across chat popup and ModelSelector.
|
||||
provider: {
|
||||
|
||||
@ -1153,7 +1153,7 @@ onMounted(async () => {
|
||||
mediumQuery.addEventListener('change', handleConvMediumChange)
|
||||
await Promise.all([loadAgents(), loadModelState(), loadConversations()])
|
||||
await hydrateStateFromRoute()
|
||||
consumeRouteAction()
|
||||
applyPendingRouteAction()
|
||||
activityPollTimer = window.setInterval(pollActivity, ACTIVITY_POLL_MS)
|
||||
elapsedTickTimer = window.setInterval(() => {
|
||||
if (activeCronRuns.value.length > 0) elapsedNow.value = Date.now()
|
||||
@ -1186,8 +1186,11 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
watch(() => route.query, () => {
|
||||
// If a fresh action arrives (e.g. user re-fires Ctrl+K via the URL while
|
||||
// the view is already alive), pick it up immediately.
|
||||
captureRouteAction()
|
||||
if (pendingRouteAction) applyPendingRouteAction()
|
||||
void hydrateStateFromRoute()
|
||||
consumeRouteAction()
|
||||
})
|
||||
|
||||
watch([selectedAgentId, currentConversationId], () => {
|
||||
|
||||
@ -126,6 +126,14 @@
|
||||
<el-icon :size="16"><SwitchButton /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-hint" :title="shortcutsHintText">
|
||||
<kbd>Ctrl+K</kbd>
|
||||
<span>{{ t('nav.shortcutAgents') }}</span>
|
||||
<span class="shortcuts-hint__sep">|</span>
|
||||
<kbd>Ctrl+N</kbd>
|
||||
<span>{{ t('nav.shortcutNew') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@ -250,6 +258,41 @@ function handleMediumChange(e: MediaQueryListEvent | MediaQueryList) {
|
||||
}
|
||||
}
|
||||
|
||||
type ChatShortcutAction = 'newChat' | 'selectAgent'
|
||||
|
||||
const shortcutsHintText = computed(() =>
|
||||
`Ctrl+K ${t('nav.shortcutAgents')} | Ctrl+N ${t('nav.shortcutNew')}`,
|
||||
)
|
||||
|
||||
function fireChatShortcut(action: ChatShortcutAction) {
|
||||
if (route.path === '/chat') {
|
||||
window.dispatchEvent(new CustomEvent('mc:chat-shortcut', { detail: action }))
|
||||
} else {
|
||||
router.push({ path: '/chat', query: { action } })
|
||||
}
|
||||
}
|
||||
|
||||
function isEditableTarget(el: EventTarget | null): boolean {
|
||||
if (!(el instanceof HTMLElement)) return false
|
||||
if (el.isContentEditable) return true
|
||||
const tag = el.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
|
||||
return false
|
||||
}
|
||||
|
||||
function onGlobalKeydown(e: KeyboardEvent) {
|
||||
const mod = e.metaKey || e.ctrlKey
|
||||
if (!mod || e.altKey) return
|
||||
const key = e.key.toLowerCase()
|
||||
if (key !== 'k' && key !== 'n') return
|
||||
// Ctrl+N within an editable field should keep its native behavior; Ctrl+K
|
||||
// is rarely used by browsers (Firefox uses it for search-bar focus), but we
|
||||
// still want to let the chat input handle native paste / undo unblocked.
|
||||
if (key === 'n' && isEditableTarget(e.target)) return
|
||||
e.preventDefault()
|
||||
fireChatShortcut(key === 'k' ? 'selectAgent' : 'newChat')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
mobileQuery = window.matchMedia('(max-width: 768px)')
|
||||
handleMobileChange(mobileQuery)
|
||||
@ -259,6 +302,8 @@ onMounted(async () => {
|
||||
handleMediumChange(mediumQuery)
|
||||
mediumQuery.addEventListener('change', handleMediumChange)
|
||||
|
||||
window.addEventListener('keydown', onGlobalKeydown)
|
||||
|
||||
// Check onboarding status
|
||||
if (!localStorage.getItem('mc-onboarding-done')) {
|
||||
try {
|
||||
@ -280,6 +325,7 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
mobileQuery?.removeEventListener('change', handleMobileChange)
|
||||
mediumQuery?.removeEventListener('change', handleMediumChange)
|
||||
window.removeEventListener('keydown', onGlobalKeydown)
|
||||
})
|
||||
|
||||
function onNavClick() {
|
||||
@ -759,6 +805,36 @@ watch(() => workspaceStore.currentWorkspaceId, () => {
|
||||
|
||||
.utility-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--mc-text-tertiary); margin: 0 0 8px; padding-left: 2px; }
|
||||
|
||||
.shortcuts-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 6px;
|
||||
margin-top: 8px;
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
color: var(--mc-text-tertiary);
|
||||
letter-spacing: 0.02em;
|
||||
user-select: none;
|
||||
}
|
||||
.shortcuts-hint kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--mc-border-light);
|
||||
background: var(--mc-bg-muted);
|
||||
color: var(--mc-text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 9.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.shortcuts-hint__sep {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* 主题切换 */
|
||||
.theme-toggle-row {
|
||||
display: flex;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user