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(() => {