[codex] Fix profile-aware session deep links (#962)
* feat: add session deep links for chats * feat: add deep links for history and group chat * Fix profile-aware session deep links --------- Co-authored-by: Maxim Kirilyuk <werserk@inbox.ru>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type DropdownOption,
|
||||
} from "naive-ui";
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import FolderPicker from "./FolderPicker.vue";
|
||||
@@ -30,6 +31,7 @@ const chatStore = useChatStore();
|
||||
const appStore = useAppStore();
|
||||
const profilesStore = useProfilesStore();
|
||||
const sessionBrowserPrefsStore = useSessionBrowserPrefsStore();
|
||||
const router = useRouter();
|
||||
const message = useMessage();
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -58,8 +60,13 @@ const showSessions = ref(
|
||||
let mobileQuery: MediaQueryList | null = null;
|
||||
const isMobile = ref(false);
|
||||
|
||||
function handleSessionClick(sessionId: string) {
|
||||
chatStore.switchSession(sessionId);
|
||||
async function handleSessionClick(sessionId: string) {
|
||||
const session = chatStore.sessions.find((item) => item.id === sessionId);
|
||||
await router.push({
|
||||
name: "hermes.session",
|
||||
params: { sessionId },
|
||||
query: session?.profile ? { profile: session.profile } : undefined,
|
||||
});
|
||||
if (mobileQuery?.matches) showSessions.value = false;
|
||||
}
|
||||
|
||||
@@ -242,12 +249,17 @@ function handleNewChatProviderChange(value: string) {
|
||||
newChatModel.value = newChatModelOptions.value[0]?.value || "";
|
||||
}
|
||||
|
||||
function confirmNewChat() {
|
||||
chatStore.newChat({
|
||||
async function confirmNewChat() {
|
||||
const session = chatStore.newChat({
|
||||
profile: newChatProfile.value,
|
||||
provider: newChatProvider.value,
|
||||
model: newChatModel.value,
|
||||
});
|
||||
await router.push({
|
||||
name: "hermes.session",
|
||||
params: { sessionId: session.id },
|
||||
query: session.profile ? { profile: session.profile } : undefined,
|
||||
});
|
||||
showNewChatModal.value = false;
|
||||
}
|
||||
|
||||
@@ -255,6 +267,28 @@ function handleApproval(choice: "once" | "session" | "always" | "deny") {
|
||||
chatStore.respondApproval(choice);
|
||||
}
|
||||
|
||||
function sessionProfile(sessionId: string): string | null {
|
||||
return chatStore.sessions.find((session) => session.id === sessionId)?.profile || null;
|
||||
}
|
||||
|
||||
function buildSessionUrl(sessionId: string, profile?: string | null): string {
|
||||
const href = router.resolve({
|
||||
name: "hermes.session",
|
||||
params: { sessionId },
|
||||
query: profile ? { profile } : undefined,
|
||||
}).href;
|
||||
return `${window.location.origin}${window.location.pathname}${href}`;
|
||||
}
|
||||
|
||||
async function copySessionLink(id?: string) {
|
||||
const sessionId = id || chatStore.activeSessionId;
|
||||
if (sessionId) {
|
||||
const ok = await copyToClipboard(buildSessionUrl(sessionId, sessionProfile(sessionId)));
|
||||
if (ok) message.success(t("common.copied"));
|
||||
else message.error(t("common.copied") + " ✗");
|
||||
}
|
||||
}
|
||||
|
||||
async function copySessionId(id?: string) {
|
||||
const sessionId = id || chatStore.activeSessionId;
|
||||
if (sessionId) {
|
||||
@@ -397,6 +431,7 @@ const contextMenuOptions = computed(() => {
|
||||
},
|
||||
],
|
||||
})
|
||||
options.push({ label: t("chat.copySessionLink"), key: "copy-link" })
|
||||
options.push({ label: t("chat.copySessionId"), key: "copy-id" })
|
||||
return options
|
||||
});
|
||||
@@ -428,7 +463,9 @@ async function handleContextMenuSelect(key: string) {
|
||||
sessionBrowserPrefsStore.togglePinned(contextSessionId.value);
|
||||
return;
|
||||
}
|
||||
if (key === "copy-id") {
|
||||
if (key === "copy-link") {
|
||||
copySessionLink(contextSessionId.value);
|
||||
} else if (key === "copy-id") {
|
||||
copySessionId(contextSessionId.value);
|
||||
} else if (parseExportKey(key)) {
|
||||
const { mode, ext } = parseExportKey(key)!;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage, NInput, NButton, NSpace, NSelect, NPopover, NPopconfirm, NInputNumber } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, NInput, NButton, NSpace, NSelect, NPopover, NPopconfirm, NInputNumber, NDropdown, type DropdownOption } from 'naive-ui'
|
||||
import { useGroupChatStore } from '@/stores/hermes/group-chat'
|
||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||
import { updateRoomConfig, forceCompress } from '@/api/hermes/group-chat'
|
||||
import GroupMessageList from './GroupMessageList.vue'
|
||||
import GroupChatInput from './GroupChatInput.vue'
|
||||
import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import type { Attachment } from '@/stores/hermes/chat'
|
||||
import type { RoomAgent } from '@/api/hermes/group-chat'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const store = useGroupChatStore()
|
||||
const profilesStore = useProfilesStore()
|
||||
@@ -29,6 +32,10 @@ const agentDescription = ref('')
|
||||
const cloneSourceRoomId = ref<string | null>(null)
|
||||
const cloneRoomName = ref('')
|
||||
const cloneInviteCode = ref('')
|
||||
const contextRoomId = ref<string | null>(null)
|
||||
const showRoomContextMenu = ref(false)
|
||||
const roomContextMenuX = ref(0)
|
||||
const roomContextMenuY = ref(0)
|
||||
|
||||
const profileOptions = computed(() =>
|
||||
profilesStore.profiles.map(p => ({ label: p.name, value: p.name }))
|
||||
@@ -94,7 +101,7 @@ async function handleCreateRoom(name: string, inviteCode: string, userName: stri
|
||||
const failureMessage = formatAgentFailures(res.agentResults)
|
||||
if (failureMessage) message.warning(failureMessage)
|
||||
else message.success(t('groupChat.roomCreated'))
|
||||
await store.joinRoom(res.room.id)
|
||||
await router.push({ name: 'hermes.groupChatRoom', params: { roomId: res.room.id } })
|
||||
} catch {
|
||||
message.error(t('common.saveFailed'))
|
||||
}
|
||||
@@ -103,12 +110,54 @@ async function handleCreateRoom(name: string, inviteCode: string, userName: stri
|
||||
async function handleDeleteRoom(roomId: string) {
|
||||
try {
|
||||
await store.deleteRoom(roomId)
|
||||
if (store.currentRoomId === roomId) {
|
||||
await router.replace({ name: 'hermes.groupChat' })
|
||||
}
|
||||
message.success(t('groupChat.roomDeleted'))
|
||||
} catch {
|
||||
message.error(t('common.saveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function buildRoomUrl(roomId: string) {
|
||||
const href = router.resolve({ name: 'hermes.groupChatRoom', params: { roomId } }).href
|
||||
return `${window.location.origin}${window.location.pathname}${href}`
|
||||
}
|
||||
|
||||
async function copyRoomLink(roomId: string) {
|
||||
const ok = await copyToClipboard(buildRoomUrl(roomId))
|
||||
if (ok) message.success(t('common.copied'))
|
||||
else message.error(t('common.copied') + ' ✗')
|
||||
}
|
||||
|
||||
const roomContextMenuOptions = computed<DropdownOption[]>(() => [
|
||||
{ label: t('groupChat.copyRoomLink'), key: 'copy-link' },
|
||||
{ label: t('groupChat.cloneRoom'), key: 'clone-room' },
|
||||
])
|
||||
|
||||
function handleRoomContextMenu(event: MouseEvent, roomId: string) {
|
||||
event.preventDefault()
|
||||
contextRoomId.value = roomId
|
||||
roomContextMenuX.value = event.clientX
|
||||
roomContextMenuY.value = event.clientY
|
||||
showRoomContextMenu.value = true
|
||||
}
|
||||
|
||||
function handleRoomContextClickOutside() {
|
||||
showRoomContextMenu.value = false
|
||||
}
|
||||
|
||||
function handleRoomContextSelect(key: string) {
|
||||
showRoomContextMenu.value = false
|
||||
const roomId = contextRoomId.value
|
||||
if (!roomId) return
|
||||
if (key === 'copy-link') {
|
||||
void copyRoomLink(roomId)
|
||||
} else if (key === 'clone-room') {
|
||||
handleOpenCloneRoom(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenCloneRoom(roomId: string) {
|
||||
const room = store.rooms.find(r => r.id === roomId)
|
||||
cloneSourceRoomId.value = roomId
|
||||
@@ -128,7 +177,7 @@ async function confirmCloneRoom() {
|
||||
cloneSourceRoomId.value = null
|
||||
cloneRoomName.value = ''
|
||||
cloneInviteCode.value = ''
|
||||
await store.joinRoom(res.room.id)
|
||||
await router.push({ name: 'hermes.groupChatRoom', params: { roomId: res.room.id } })
|
||||
const failureMessage = formatAgentFailures(res.agentResults)
|
||||
if (failureMessage) message.warning(failureMessage)
|
||||
else message.success(t('groupChat.roomCloned'))
|
||||
@@ -153,7 +202,7 @@ async function handleClearRoomContext() {
|
||||
|
||||
async function handleSelectRoom(roomId: string) {
|
||||
try {
|
||||
await store.joinRoom(roomId)
|
||||
await router.push({ name: 'hermes.groupChatRoom', params: { roomId } })
|
||||
if (window.innerWidth <= 768) showSidebar.value = false
|
||||
} catch {
|
||||
message.error(t('groupChat.joinFailed'))
|
||||
@@ -299,6 +348,7 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
class="room-item"
|
||||
:class="{ active: store.currentRoomId === room.id }"
|
||||
@click="handleSelectRoom(room.id)"
|
||||
@contextmenu="handleRoomContextMenu($event, room.id)"
|
||||
>
|
||||
<svg class="room-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
@@ -308,11 +358,6 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
<span v-if="room.inviteCode" class="room-code">{{ room.inviteCode }}</span>
|
||||
<span class="room-tokens">{{ formatTokens(room.totalTokens || 0) }}</span>
|
||||
</div>
|
||||
<button class="room-action-btn" :title="t('groupChat.cloneRoom')" @click.stop="handleOpenCloneRoom(room.id)">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="8" y="8" width="12" height="12" rx="2" /><path d="M4 16V6a2 2 0 0 1 2-2h10" />
|
||||
</svg>
|
||||
</button>
|
||||
<NPopconfirm @positive-click="handleDeleteRoom(room.id)">
|
||||
<template #trigger>
|
||||
<button class="room-action-btn danger" @click.stop>
|
||||
@@ -328,6 +373,17 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NDropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:x="roomContextMenuX"
|
||||
:y="roomContextMenuY"
|
||||
:options="roomContextMenuOptions"
|
||||
:show="showRoomContextMenu"
|
||||
@select="handleRoomContextSelect"
|
||||
@clickoutside="handleRoomContextClickOutside"
|
||||
/>
|
||||
|
||||
<!-- Main chat area -->
|
||||
<div class="chat-main">
|
||||
<div class="chat-header">
|
||||
|
||||
@@ -18,7 +18,12 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const { openSessionSearch } = useSessionSearch();
|
||||
const selectedKey = computed(() => route.name as string);
|
||||
const selectedKey = computed(() => {
|
||||
if (route.name === "hermes.session") return "hermes.chat";
|
||||
if (route.name === "hermes.historySession") return "hermes.history";
|
||||
if (route.name === "hermes.groupChatRoom") return "hermes.groupChat";
|
||||
return route.name as string;
|
||||
});
|
||||
const isSuperAdmin = computed(() => isStoredSuperAdmin());
|
||||
const logoPath = '/logo.png';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user