mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +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',
|
appearance: 'Appearance & Language',
|
||||||
roleUser: 'User',
|
roleUser: 'User',
|
||||||
roleAdmin: 'Admin',
|
roleAdmin: 'Admin',
|
||||||
|
shortcutAgents: 'Agents',
|
||||||
|
shortcutNew: 'New',
|
||||||
|
shortcutsHint: 'Ctrl+K {agents} | Ctrl+N {newAction}',
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
pendingApprovals: '{n} tool call(s) pending approval',
|
pendingApprovals: '{n} tool call(s) pending approval',
|
||||||
|
|||||||
@ -400,6 +400,9 @@ export default {
|
|||||||
appearance: '外观与语言',
|
appearance: '外观与语言',
|
||||||
roleUser: '用户',
|
roleUser: '用户',
|
||||||
roleAdmin: '管理员',
|
roleAdmin: '管理员',
|
||||||
|
shortcutAgents: '智能体',
|
||||||
|
shortcutNew: '新建',
|
||||||
|
shortcutsHint: 'Ctrl+K {agents} | Ctrl+N {newAction}',
|
||||||
},
|
},
|
||||||
// Issue #81: provider-level hint / status text shared across chat popup and ModelSelector.
|
// Issue #81: provider-level hint / status text shared across chat popup and ModelSelector.
|
||||||
provider: {
|
provider: {
|
||||||
|
|||||||
@ -1153,7 +1153,7 @@ onMounted(async () => {
|
|||||||
mediumQuery.addEventListener('change', handleConvMediumChange)
|
mediumQuery.addEventListener('change', handleConvMediumChange)
|
||||||
await Promise.all([loadAgents(), loadModelState(), loadConversations()])
|
await Promise.all([loadAgents(), loadModelState(), loadConversations()])
|
||||||
await hydrateStateFromRoute()
|
await hydrateStateFromRoute()
|
||||||
consumeRouteAction()
|
applyPendingRouteAction()
|
||||||
activityPollTimer = window.setInterval(pollActivity, ACTIVITY_POLL_MS)
|
activityPollTimer = window.setInterval(pollActivity, ACTIVITY_POLL_MS)
|
||||||
elapsedTickTimer = window.setInterval(() => {
|
elapsedTickTimer = window.setInterval(() => {
|
||||||
if (activeCronRuns.value.length > 0) elapsedNow.value = Date.now()
|
if (activeCronRuns.value.length > 0) elapsedNow.value = Date.now()
|
||||||
@ -1186,8 +1186,11 @@ onBeforeUnmount(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(() => route.query, () => {
|
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()
|
void hydrateStateFromRoute()
|
||||||
consumeRouteAction()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([selectedAgentId, currentConversationId], () => {
|
watch([selectedAgentId, currentConversationId], () => {
|
||||||
|
|||||||
@ -126,6 +126,14 @@
|
|||||||
<el-icon :size="16"><SwitchButton /></el-icon>
|
<el-icon :size="16"><SwitchButton /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<template v-else>
|
<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 () => {
|
onMounted(async () => {
|
||||||
mobileQuery = window.matchMedia('(max-width: 768px)')
|
mobileQuery = window.matchMedia('(max-width: 768px)')
|
||||||
handleMobileChange(mobileQuery)
|
handleMobileChange(mobileQuery)
|
||||||
@ -259,6 +302,8 @@ onMounted(async () => {
|
|||||||
handleMediumChange(mediumQuery)
|
handleMediumChange(mediumQuery)
|
||||||
mediumQuery.addEventListener('change', handleMediumChange)
|
mediumQuery.addEventListener('change', handleMediumChange)
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onGlobalKeydown)
|
||||||
|
|
||||||
// Check onboarding status
|
// Check onboarding status
|
||||||
if (!localStorage.getItem('mc-onboarding-done')) {
|
if (!localStorage.getItem('mc-onboarding-done')) {
|
||||||
try {
|
try {
|
||||||
@ -280,6 +325,7 @@ onMounted(async () => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
mobileQuery?.removeEventListener('change', handleMobileChange)
|
mobileQuery?.removeEventListener('change', handleMobileChange)
|
||||||
mediumQuery?.removeEventListener('change', handleMediumChange)
|
mediumQuery?.removeEventListener('change', handleMediumChange)
|
||||||
|
window.removeEventListener('keydown', onGlobalKeydown)
|
||||||
})
|
})
|
||||||
|
|
||||||
function onNavClick() {
|
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; }
|
.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 {
|
.theme-toggle-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user