diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 4889dac5..a83c29fa 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -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',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 853d957b..4ca1f6e6 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -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: {
diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue
index 92cbee0c..7480ccdc 100644
--- a/mateclaw-ui/src/views/ChatConsole.vue
+++ b/mateclaw-ui/src/views/ChatConsole.vue
@@ -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], () => {
diff --git a/mateclaw-ui/src/views/layout/MainLayout.vue b/mateclaw-ui/src/views/layout/MainLayout.vue
index 49175a07..bdc3582e 100644
--- a/mateclaw-ui/src/views/layout/MainLayout.vue
+++ b/mateclaw-ui/src/views/layout/MainLayout.vue
@@ -126,6 +126,14 @@
+
+
+ Ctrl+K
+ {{ t('nav.shortcutAgents') }}
+ |
+ Ctrl+N
+ {{ t('nav.shortcutNew') }}
+
@@ -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;