- Vendor ArkScope decorative layers (ScrambleTitle / IsometricWave) with provenance notes; decorative-only per AGENTS.md red lines - Migrate theme tokens to Arco, register the Arco theme, and add hook adapter composables (useMessage / useDialog / useNotification) with matching signatures - Sample pages: LoginView and AppSidebar on the new baseline; docs/arco-mapping.md and docs/ui-baseline.md land - agent-bridge: prefer the PM-selected venv from facts.json over legacy in-tree venvs (legacy lacks the mcp extra, so every MCP server showed 0 tools / not connected) - agent-bridge python (vendored, deliberate deviation): MCP discovery import fallback and pyyaml -> hermes_yaml so plugins and MCP work under the PM env; install the mcp extra via `hermes pm install --extra mcp` - Chat-chain fragment: docs/chat-chain-changes/2026-10-17-phase005-arkscope-baseline.md Verified: harness:check, test (2020 passed), test:e2e (30 passed), build all green; manual: plugins 100/0 warnings, MCP 6/6 servers connected with 104 tools.
131 lines
3.5 KiB
Vue
131 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
import { onUnmounted, computed, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useTheme } from '@/composables/useTheme'
|
|
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
|
import { useKeyboard } from '@/composables/useKeyboard'
|
|
import { useAppStore } from '@/stores/hermes/app'
|
|
import SessionSearchModal from '@/components/hermes/chat/SessionSearchModal.vue'
|
|
import AuthEventListener from '@/components/auth/AuthEventListener.vue'
|
|
import DefaultCredentialPrompt from '@/components/auth/DefaultCredentialPrompt.vue'
|
|
|
|
const { isDark } = useTheme()
|
|
const { t } = useI18n()
|
|
const appStore = useAppStore()
|
|
const route = useRoute()
|
|
|
|
const isLoginPage = computed(() => route.name === 'login')
|
|
const usesPageSidebar = computed(() =>
|
|
['hermes.chat', 'hermes.session', 'hermes.history', 'hermes.historySession', 'hermes.globalAgent', 'hermes.globalAgentSession', 'hermes.groupChat', 'hermes.groupChatRoom', 'hermes.workflow'].includes(route.name as string),
|
|
)
|
|
const showAppSidebar = computed(() => !isLoginPage.value && !usesPageSidebar.value)
|
|
const showMobileMenuButton = computed(() => !isLoginPage.value && (showAppSidebar.value || usesPageSidebar.value))
|
|
|
|
const nodeVersionLow = computed(() => {
|
|
const v = appStore.nodeVersion
|
|
const major = parseInt(v.split('.')[0], 10)
|
|
return !isNaN(major) && major < 23
|
|
})
|
|
|
|
function handleMobileMenuClick() {
|
|
if (usesPageSidebar.value) {
|
|
window.dispatchEvent(new CustomEvent('hermes:open-page-sidebar'))
|
|
return
|
|
}
|
|
appStore.toggleSidebar()
|
|
}
|
|
|
|
watch(isLoginPage, (loginPage) => {
|
|
if (loginPage) {
|
|
appStore.stopHealthPolling()
|
|
return
|
|
}
|
|
appStore.loadModels()
|
|
appStore.startHealthPolling()
|
|
}, {
|
|
immediate: true,
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
appStore.stopHealthPolling()
|
|
})
|
|
|
|
useKeyboard()
|
|
</script>
|
|
|
|
<template>
|
|
<div class="app-root" :class="{ dark: isDark }">
|
|
<AuthEventListener />
|
|
<div class="app-shell">
|
|
<div v-if="nodeVersionLow" class="node-warning-bar">
|
|
{{ t('sidebar.nodeVersionWarning', { version: appStore.nodeVersion }) }}
|
|
</div>
|
|
<div class="app-layout" :class="{ 'no-sidebar': isLoginPage || !showAppSidebar }">
|
|
<button v-if="showMobileMenuButton" class="hamburger-btn" @click="handleMobileMenuClick">
|
|
<img src="/logo.png" alt="Menu" style="width: 24px; height: 24px;" />
|
|
</button>
|
|
<div v-if="!isLoginPage && showAppSidebar && appStore.sidebarOpen" class="mobile-backdrop" @click="appStore.closeSidebar" />
|
|
<AppSidebar v-if="!isLoginPage && showAppSidebar" />
|
|
<main class="app-main">
|
|
<router-view />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<SessionSearchModal />
|
|
<DefaultCredentialPrompt />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
@use '@/styles/variables' as *;
|
|
|
|
.app-shell {
|
|
height: calc(100 * var(--vh));
|
|
width: 100%;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: $bg-primary;
|
|
}
|
|
|
|
.app-layout {
|
|
display: flex;
|
|
flex: 1;
|
|
min-height: 0;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
|
|
&.no-sidebar {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.app-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow-y: auto;
|
|
background-color: $bg-primary;
|
|
|
|
.no-sidebar & {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.node-warning-bar {
|
|
flex: 0 0 auto;
|
|
width: 100%;
|
|
z-index: 100;
|
|
padding: 4px 16px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #b45309;
|
|
background-color: #fef3c7;
|
|
border-bottom: 1px solid #fde68a;
|
|
text-align: center;
|
|
line-height: 1.4;
|
|
}
|
|
</style>
|