From acdf18793cbf0f6e49d50766b18215815b8b022f Mon Sep 17 00:00:00 2001 From: Maxim Kirilyuk Date: Sun, 24 May 2026 14:13:42 +0300 Subject: [PATCH] feat: make navigation use native links (#973) --- .../src/components/common/RouteLinkItem.vue | 28 ++++ .../src/components/hermes/chat/ChatPanel.vue | 20 +++ .../hermes/chat/SessionListItem.vue | 21 ++- .../src/components/layout/AppSidebar.vue | 86 ++++++----- .../src/composables/usePersistentRecord.ts | 23 +++ packages/client/src/i18n/locales/de.ts | 2 + packages/client/src/i18n/locales/en.ts | 2 + packages/client/src/i18n/locales/es.ts | 2 + packages/client/src/i18n/locales/fr.ts | 2 + packages/client/src/i18n/locales/ja.ts | 2 + packages/client/src/i18n/locales/ko.ts | 2 + packages/client/src/i18n/locales/pt.ts | 2 + packages/client/src/i18n/locales/zh-TW.ts | 2 + packages/client/src/i18n/locales/zh.ts | 2 + tests/client/route-link-item.test.ts | 34 +++++ tests/client/session-list-item.test.ts | 138 ++++++++++++++++++ tests/client/sidebar-search.test.ts | 24 +++ tests/client/use-persistent-record.test.ts | 28 ++++ tests/e2e/authenticated-shell.spec.ts | 8 +- tests/e2e/native-navigation.spec.ts | 37 +++++ 20 files changed, 419 insertions(+), 46 deletions(-) create mode 100644 packages/client/src/components/common/RouteLinkItem.vue create mode 100644 packages/client/src/composables/usePersistentRecord.ts create mode 100644 tests/client/route-link-item.test.ts create mode 100644 tests/client/session-list-item.test.ts create mode 100644 tests/client/use-persistent-record.test.ts create mode 100644 tests/e2e/native-navigation.spec.ts diff --git a/packages/client/src/components/common/RouteLinkItem.vue b/packages/client/src/components/common/RouteLinkItem.vue new file mode 100644 index 0000000..637027b --- /dev/null +++ b/packages/client/src/components/common/RouteLinkItem.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/client/src/components/hermes/chat/ChatPanel.vue b/packages/client/src/components/hermes/chat/ChatPanel.vue index ef646bb..7414a7e 100644 --- a/packages/client/src/components/hermes/chat/ChatPanel.vue +++ b/packages/client/src/components/hermes/chat/ChatPanel.vue @@ -60,6 +60,20 @@ const showSessions = ref( let mobileQuery: MediaQueryList | null = null; const isMobile = ref(false); +function sessionHref(sessionId: string) { + const profile = sessionProfile(sessionId); + return router.resolve({ + name: "hermes.session", + params: { sessionId }, + query: profile ? { profile } : undefined, + }).href; +} + +function openSessionInNewTab(sessionId: string) { + if (typeof window === "undefined") return; + window.open(sessionHref(sessionId), "_blank", "noopener,noreferrer"); +} + async function handleSessionClick(sessionId: string) { const session = chatStore.sessions.find((item) => item.id === sessionId); await router.push({ @@ -442,6 +456,7 @@ const contextMenuOptions = computed(() => { }, ], }) + options.push({ label: t("chat.openSessionInNewTab"), key: "open-link" }) options.push({ label: t("chat.copySessionLink"), key: "copy-link" }) options.push({ label: t("chat.copySessionId"), key: "copy-id" }) return options @@ -478,6 +493,8 @@ async function handleContextMenuSelect(key: string) { copySessionLink(contextSessionId.value); } else if (key === "copy-id") { copySessionId(contextSessionId.value); + } else if (key === "open-link") { + openSessionInNewTab(contextSessionId.value); } else if (parseExportKey(key)) { const { mode, ext } = parseExportKey(key)!; const loadingMsg = mode === "compressed" ? message.loading(t("chat.exportCompressing"), { duration: 0 }) : null; @@ -846,6 +863,7 @@ async function handleSessionModelCustomSubmit() { :selectable="isBatchMode" :selected="isSessionSelected(s.id)" :show-profile="true" + :to="sessionHref(s.id)" @select="handleSessionClick(s.id)" @contextmenu="handleContextMenu($event, s.id)" @delete="handleDeleteSession(s.id)" @@ -867,6 +885,7 @@ async function handleSessionModelCustomSubmit() { :selectable="isBatchMode" :selected="isSessionSelected(s.id)" :show-profile="true" + :to="sessionHref(s.id)" @select="handleSessionClick(s.id)" @contextmenu="handleContextMenu($event, s.id)" @delete="handleDeleteSession(s.id)" @@ -1714,6 +1733,7 @@ async function handleSessionModelCustomSubmit() { border-radius: $radius-sm; cursor: pointer; text-align: left; + text-decoration: none; color: $text-secondary; transition: all $transition-fast; margin-bottom: 2px; diff --git a/packages/client/src/components/hermes/chat/SessionListItem.vue b/packages/client/src/components/hermes/chat/SessionListItem.vue index bf1c0d7..99a7e86 100644 --- a/packages/client/src/components/hermes/chat/SessionListItem.vue +++ b/packages/client/src/components/hermes/chat/SessionListItem.vue @@ -17,6 +17,7 @@ const props = withDefaults(defineProps<{ selectable?: boolean selected?: boolean showProfile?: boolean + to?: string }>(), { showProfile: true, }) @@ -77,11 +78,18 @@ function onTouchMove() { } } -function onClick() { +function isModifiedNavigation(event?: MouseEvent) { + return !!event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) +} + +function onClick(event?: MouseEvent) { if (longPressTriggered.value) { longPressTriggered.value = false + event?.preventDefault() return } + if (isModifiedNavigation(event)) return + if (props.to && !props.selectable) event?.preventDefault() emit('select') } @@ -91,10 +99,13 @@ onUnmounted(() => {