mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-06 08:09:54 +08:00
chore: release v0.14.0
集中发版: 新功能(10) - 心甜Claw 引擎入口(第 3 个引擎模式) - Hermes 22 个 Provider 注册表 + 安装/仪表盘动态加载 - Hermes .env 高级编辑(拒绝触碰托管 Provider 密钥) - Hermes 会话与用量分析增强 - Hermes Dashboard 自动拉起 + Windows POSIX-only 兼容模态 - Hermes Skills 工具集面板 - 官网 Hermes Agent 黑金特色区 + 图文指南 - Boot Manifest 启动页(双语 + 错峰动画) - 官网 Markdown 阅读器图片 lightbox - Hermes Memory 概览卡 改进(9) - Hermes 仪表盘/扩展页全面本地化 - 记忆编辑大尺寸模态 - 日志下载 Web/桌面分流 - 侧边栏导航补全 - 模型备选管理 UI(PR #232) - 模型加载错误 UX 重做(错误卡 + 详情 + 重试) - .page 布局 clamp + .page-narrow - Memory 单列断点提早到 1100px - Web 模式跳过前端热更新检查 修复(12) - Gateway 启动 platforms.api_server.enabled 自修复(含 7 unit test) - Memory 页 overview 卡穿模(旧 flex 列约束 → 自然块流) - Skills 页 hero/toolsets 被压缩(flex-shrink:0) - Web 模式 Skills ReferenceError(补 _readHermesDisabledSkills) - 日志/记忆下载行为分流 - src/pages/models.js 5 处 typo - 删除 56 行 .hm-memory-* 死代码 + line-clamp 标准属性 - Dependabot rustls-webpki / postcss / rand
This commit is contained in:
@@ -34,19 +34,21 @@ const HERMES_DASHBOARD_URL = 'http://127.0.0.1:9119/'
|
||||
|
||||
/**
|
||||
* Open `url` in the user's system browser. Tauri desktop uses the shell
|
||||
* plugin (which respects `xdg-open` / `start` / `open`); Web mode falls back
|
||||
* to `window.open` with a `noopener` to avoid tab-jacking.
|
||||
* plugin (which respects `xdg-open` / `start` / `open`); Web mode uses
|
||||
* `window.open` with `noopener` to avoid tab-jacking. Errors propagate so
|
||||
* the caller can decide how to surface them — silent fallback hid real
|
||||
* scope/CSP errors and made "9119 打不开" hard to diagnose.
|
||||
*/
|
||||
async function openExternalUrl(url) {
|
||||
if (!url) return
|
||||
try {
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
const { open } = await import('@tauri-apps/plugin-shell')
|
||||
await open(url)
|
||||
return
|
||||
}
|
||||
} catch (_) { /* fall through to window.open */ }
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
const { open } = await import('@tauri-apps/plugin-shell')
|
||||
await open(url)
|
||||
return
|
||||
}
|
||||
// Web 模式:打开用户浏览器中的新标签
|
||||
const win = window.open(url, '_blank', 'noopener,noreferrer')
|
||||
if (!win) throw new Error('popup blocked')
|
||||
}
|
||||
|
||||
export function render() {
|
||||
@@ -463,14 +465,235 @@ export function render() {
|
||||
// Open panel card
|
||||
el.querySelector('.hm-dash-open-panel')?.addEventListener('click', () => { window.location.hash = '#/h/chat' })
|
||||
// Open Hermes native dashboard in system browser
|
||||
// 流程:Probe → 没起就 auto-start → start 失败再看是否依赖缺失走安装流程
|
||||
el.querySelector('.hm-dash-open-native')?.addEventListener('click', async (e) => {
|
||||
const href = e.currentTarget.dataset.href
|
||||
const btn = e.currentTarget
|
||||
const href = btn.dataset.href
|
||||
if (!href) return
|
||||
const origText = btn.textContent
|
||||
btn.disabled = true
|
||||
btn.textContent = t('engine.dashNativePanelChecking')
|
||||
|
||||
const tryOpen = async (port) => {
|
||||
const url = href.replace(/:9119(\/?$)/, ':' + port + '$1')
|
||||
await openExternalUrl(url)
|
||||
}
|
||||
|
||||
// 共用:调用 hermesDashboardStart,带"首次启动"提示,端口起来后开浏览器
|
||||
// 返回 { ok, kind?, port, log_tail? } —— ok=true 时已经打开浏览器
|
||||
const startAndOpen = async () => {
|
||||
btn.textContent = t('engine.dashNativePanelStarting')
|
||||
// 首次启动可能慢(Hermes 会跑 npm build 构建前端),给用户一个 toast 安抚
|
||||
let firstHintTimer = null
|
||||
const showFirstHint = async () => {
|
||||
const { toast } = await import('../../../components/toast.js')
|
||||
toast(t('engine.dashNativePanelStartFirstHint'), 'info', { duration: 8000 })
|
||||
}
|
||||
firstHintTimer = setTimeout(showFirstHint, 5000)
|
||||
try {
|
||||
const result = await api.hermesDashboardStart().catch((err) => ({
|
||||
started: false, kind: 'spawn_failed', port: 9119,
|
||||
log_tail: String(err?.message || err),
|
||||
}))
|
||||
if (result?.started) {
|
||||
try {
|
||||
await tryOpen(result.port || 9119)
|
||||
return { ok: true, ...result }
|
||||
} catch (err) {
|
||||
const { toast } = await import('../../../components/toast.js')
|
||||
toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error')
|
||||
return { ok: false, kind: 'open_failed', ...result }
|
||||
}
|
||||
}
|
||||
return { ok: false, ...(result || {}) }
|
||||
} finally {
|
||||
if (firstHintTimer) clearTimeout(firstHintTimer)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await openExternalUrl(href)
|
||||
const probe = await api.hermesDashboardProbe().catch(() => ({ running: false, port: 9119 }))
|
||||
if (probe?.running) {
|
||||
await tryOpen(probe.port || 9119)
|
||||
return
|
||||
}
|
||||
|
||||
// 自动启动 Dashboard
|
||||
const startResult = await startAndOpen()
|
||||
if (startResult.ok) return
|
||||
|
||||
// 启动失败,按 kind 分发
|
||||
const port = startResult.port || probe?.port || 9119
|
||||
const { toast } = await import('../../../components/toast.js')
|
||||
|
||||
if (startResult.kind === 'timeout') {
|
||||
toast(t('engine.dashNativePanelStartTimeout', { port }), 'warning', { duration: 6000 })
|
||||
return
|
||||
}
|
||||
if (startResult.kind === 'port_in_use') {
|
||||
toast(t('engine.dashNativePanelStartPortBusy', { port }), 'warning', { duration: 6000 })
|
||||
return
|
||||
}
|
||||
if (startResult.kind === 'posix_only_module') {
|
||||
// Hermes Agent 上游 bug:pty_bridge.py / memory_tool.py 在 Windows 上 import fcntl 等 POSIX-only 模块
|
||||
// 见 https://github.com/NousResearch/hermes-agent/issues/5246
|
||||
// 没办法在前端绕过——只能告诉用户原因和替代方案
|
||||
const { showContentModal } = await import('../../../components/modal.js')
|
||||
const m = showContentModal({
|
||||
title: t('engine.dashNativePanelWindowsTitle'),
|
||||
width: 580,
|
||||
content: `
|
||||
<p style="margin:0 0 14px;line-height:1.6;color:var(--text-secondary)">
|
||||
${t('engine.dashNativePanelWindowsDesc')}
|
||||
</p>
|
||||
<ul style="margin:0 0 12px 20px;padding:0;line-height:1.7;color:var(--text-primary)">
|
||||
<li>${t('engine.dashNativePanelWindowsAlt1')}</li>
|
||||
<li>${t('engine.dashNativePanelWindowsAlt2')}</li>
|
||||
</ul>
|
||||
<pre style="margin:0;padding:10px 12px;background:var(--surface-2,#f5f5f4);border:1px solid var(--border,#e5e5e5);border-radius:6px;font-family:var(--hm-font-mono,monospace);font-size:11px;color:var(--text-tertiary,#888);max-height:120px;overflow:auto;white-space:pre-wrap;word-break:break-all">${(startResult.log_tail || '').split('\n').slice(-6).join('\n').replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'})[c])}</pre>
|
||||
`,
|
||||
buttons: [
|
||||
{ label: t('engine.dashNativePanelWindowsReportLink'), className: 'btn btn-secondary btn-sm', id: 'hm-dash-issue-link' },
|
||||
],
|
||||
})
|
||||
m.querySelector('#hm-dash-issue-link')?.addEventListener('click', async () => {
|
||||
try { await openExternalUrl('https://github.com/NousResearch/hermes-agent/issues/5246') }
|
||||
catch {}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (startResult.kind !== 'deps_missing') {
|
||||
// spawn_failed / 其他未知 → 显示日志尾部摘要
|
||||
const detail = (startResult.log_tail || '').split('\n').slice(-3).join('\n').trim()
|
||||
toast(t('engine.dashNativePanelStartGeneric') + (detail ? ': ' + detail : ''), 'error', { duration: 8000 })
|
||||
return
|
||||
}
|
||||
|
||||
// —— 依赖缺失(fastapi/uvicorn):弹安装引导 modal ——
|
||||
const { showContentModal, showUpgradeModal } = await import('../../../components/modal.js')
|
||||
const overlay = showContentModal({
|
||||
title: t('engine.dashNativePanelDownTitle'),
|
||||
width: 560,
|
||||
content: `
|
||||
<p style="margin:0 0 10px;line-height:1.6;color:var(--text-secondary)">
|
||||
${t('engine.dashNativePanelDepHint')}
|
||||
</p>
|
||||
<pre style="margin:0 0 12px;padding:12px 14px;background:var(--surface-2,#f5f5f4);border:1px solid var(--border,#e5e5e5);border-radius:6px;font-family:var(--hm-font-mono,monospace);font-size:13px;color:var(--text-primary);user-select:all;white-space:pre-wrap;word-break:break-all"><code>uv tool install --force 'hermes-agent[web] @ git+https://github.com/NousResearch/hermes-agent.git'</code></pre>
|
||||
<p style="margin:0;font-size:12px;color:var(--text-tertiary,#999);line-height:1.6">
|
||||
${t('engine.dashNativePanelDown', { port })}
|
||||
</p>
|
||||
`,
|
||||
buttons: [
|
||||
{ label: t('common.copy') || 'Copy', className: 'btn btn-secondary btn-sm', id: 'hm-dash-copy-cmd' },
|
||||
{ label: t('common.retry') || 'Retry', className: 'btn btn-secondary btn-sm', id: 'hm-dash-retry' },
|
||||
{ label: t('engine.dashNativePanelInstallWeb'), className: 'btn btn-primary btn-sm', id: 'hm-dash-install-web' },
|
||||
],
|
||||
})
|
||||
overlay.querySelector('#hm-dash-copy-cmd')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(`uv tool install --force 'hermes-agent[web] @ git+https://github.com/NousResearch/hermes-agent.git'`)
|
||||
toast(t('common.copied') || 'Copied', 'success')
|
||||
} catch {}
|
||||
})
|
||||
overlay.querySelector('#hm-dash-retry')?.addEventListener('click', async () => {
|
||||
// 重试:先 probe,再 auto-start
|
||||
overlay.close()
|
||||
const retryProbe = await api.hermesDashboardProbe().catch(() => ({ running: false, port }))
|
||||
if (retryProbe?.running) {
|
||||
try { await tryOpen(retryProbe.port || port) }
|
||||
catch (err) { toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error') }
|
||||
return
|
||||
}
|
||||
const r = await startAndOpen()
|
||||
if (!r.ok) {
|
||||
toast(t('engine.dashNativePanelDown', { port: r.port || port }), 'warning')
|
||||
}
|
||||
})
|
||||
overlay.querySelector('#hm-dash-install-web')?.addEventListener('click', async () => {
|
||||
overlay.close()
|
||||
// 进度 modal 复用现有 showUpgradeModal(已有日志窗 + 进度条 + 任务栏最小化)
|
||||
const um = showUpgradeModal(t('engine.dashNativePanelInstallWebTitle'))
|
||||
um.setProgressLabels({
|
||||
preparing: t('engine.dashNativePanelInstallWebTitle'),
|
||||
downloading: t('engine.dashNativePanelInstallWebTitle'),
|
||||
installing: t('engine.dashNativePanelInstallWebTitle'),
|
||||
done: t('engine.dashNativePanelInstallWebDone'),
|
||||
})
|
||||
let unlisten = null
|
||||
// Gateway 是否运行 → 装前停、装后重启。Windows 下 uv 无法覆盖被占用的
|
||||
// ~/.local/bin/hermes.exe(os error 32),所以必须先释放文件锁。
|
||||
let gatewayWasRunning = false
|
||||
try {
|
||||
await api.hermesHealthCheck()
|
||||
gatewayWasRunning = true
|
||||
} catch { /* gateway not running, no pre-stop needed */ }
|
||||
|
||||
let installOk = false
|
||||
try {
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
const { listen } = await import('@tauri-apps/api/event')
|
||||
const u1 = await listen('hermes-install-log', (ev) => um.appendLog(String(ev.payload)))
|
||||
const u2 = await listen('hermes-install-progress', (ev) => um.setProgress(Number(ev.payload) || 0))
|
||||
unlisten = () => { u1(); u2() }
|
||||
}
|
||||
|
||||
if (gatewayWasRunning) {
|
||||
um.appendLog(t('engine.dashNativePanelInstallStoppingGw'))
|
||||
try {
|
||||
await api.hermesGatewayAction('stop')
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
um.appendLog(t('engine.dashNativePanelInstallGwStopped'))
|
||||
} catch (err) {
|
||||
um.appendLog(t('engine.dashNativePanelInstallGwWarn') + ': ' + (err?.message || err))
|
||||
}
|
||||
}
|
||||
|
||||
await api.installHermes('uv-tool', ['web'])
|
||||
um.setDone(t('engine.dashNativePanelInstallWebDone'))
|
||||
installOk = true
|
||||
} catch (err) {
|
||||
const msg = String(err?.message || err).replace(/^Error:\s*/, '')
|
||||
um.setError(t('engine.dashNativePanelInstallWebFailed') + ': ' + msg)
|
||||
} finally {
|
||||
if (unlisten) { unlisten() }
|
||||
if (gatewayWasRunning) {
|
||||
um.appendLog(t('engine.dashNativePanelInstallRestartingGw'))
|
||||
try {
|
||||
await api.hermesGatewayAction('start')
|
||||
um.appendLog(t('engine.dashNativePanelInstallGwRestarted'))
|
||||
} catch (err) {
|
||||
um.appendLog(t('engine.dashNativePanelInstallGwWarn') + ': ' + (err?.message || err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 安装成功 → 自动启动 dashboard 并打开浏览器,省去用户跑命令的步骤
|
||||
if (installOk) {
|
||||
um.appendLog('')
|
||||
um.appendLog('▶ ' + t('engine.dashNativePanelStarting'))
|
||||
const startRes = await api.hermesDashboardStart().catch((err) => ({
|
||||
started: false, kind: 'spawn_failed',
|
||||
log_tail: String(err?.message || err),
|
||||
}))
|
||||
if (startRes?.started) {
|
||||
um.appendLog('✓ Dashboard @ 127.0.0.1:' + (startRes.port || port))
|
||||
try {
|
||||
await tryOpen(startRes.port || port)
|
||||
} catch (err) {
|
||||
um.appendLog('⚠ ' + t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err))
|
||||
}
|
||||
} else {
|
||||
const detail = (startRes?.log_tail || '').split('\n').slice(-3).join('\n').trim()
|
||||
um.appendLog('⚠ ' + t('engine.dashNativePanelStartGeneric') + (detail ? ': ' + detail : ''))
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
const { toast } = await import('../../../components/toast.js')
|
||||
toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error')
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
btn.textContent = origText
|
||||
}
|
||||
})
|
||||
// Provider presets — 点击填充 URL
|
||||
|
||||
@@ -123,6 +123,42 @@ export function render() {
|
||||
|
||||
el.querySelector('#hm-ext-refresh')?.addEventListener('click', load)
|
||||
el.querySelector('#hm-ext-rescan')?.addEventListener('click', rescan)
|
||||
// 拦截 Dashboard 本地链接:probe → auto-start → 打开。避免直接打开浏览器看到 ERR_CONNECTION_REFUSED
|
||||
el.querySelectorAll('a[href^="http://127.0.0.1:9119"]').forEach(a => {
|
||||
a.addEventListener('click', async (ev) => {
|
||||
ev.preventDefault()
|
||||
const openWith = async (port) => {
|
||||
const url = a.href.replace(/:9119(\/?)/, ':' + port + '$1')
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
const { open } = await import('@tauri-apps/plugin-shell')
|
||||
await open(url)
|
||||
} else {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
// 1. probe
|
||||
const probe = await api.hermesDashboardProbe().catch(() => ({ running: false, port: 9119 }))
|
||||
if (probe?.running) {
|
||||
try { await openWith(probe.port || 9119) }
|
||||
catch (err) { toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error') }
|
||||
return
|
||||
}
|
||||
// 2. auto-start
|
||||
const r = await api.hermesDashboardStart().catch(() => ({ started: false, kind: 'spawn_failed', port: probe?.port || 9119 }))
|
||||
if (r?.started) {
|
||||
try { await openWith(r.port || 9119) }
|
||||
catch (err) { toast(t('engine.dashNativePanelOpenFail') + ': ' + (err?.message || err), 'error') }
|
||||
return
|
||||
}
|
||||
// 3. 失败 → toast(dashboard 页面有完整安装流程,这里只引导)
|
||||
const port = r?.port || probe?.port || 9119
|
||||
if (r?.kind === 'deps_missing') {
|
||||
toast(t('engine.dashNativePanelDepHint'), 'warning', { duration: 6000 })
|
||||
} else {
|
||||
toast(t('engine.dashNativePanelDown', { port }), 'warning')
|
||||
}
|
||||
})
|
||||
})
|
||||
el.querySelectorAll('.hm-theme-choice').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const name = btn.dataset.theme
|
||||
|
||||
@@ -219,6 +219,44 @@ export function render() {
|
||||
draw()
|
||||
}
|
||||
|
||||
function renderOverview() {
|
||||
const all = SECTIONS.map(section => ({
|
||||
section,
|
||||
stats: contentStats(data[section.key] || ''),
|
||||
filled: Boolean((data[section.key] || '').trim()),
|
||||
}))
|
||||
const totalWords = all.reduce((sum, item) => sum + item.stats.words, 0)
|
||||
const filledCount = all.filter(item => item.filled).length
|
||||
const latest = Math.max(0, ...SECTIONS.map(section => mtimes[section.key] || 0))
|
||||
return `
|
||||
<div class="hm-mem-overview">
|
||||
<div class="hm-mem-overview-copy">
|
||||
<div class="hm-mem-kicker">${t('engine.memoryOverviewKicker')}</div>
|
||||
<div class="hm-mem-overview-title">${t('engine.memoryOverviewTitle')}</div>
|
||||
<div class="hm-mem-overview-desc">${t('engine.memoryOverviewDesc')}</div>
|
||||
</div>
|
||||
<div class="hm-mem-overview-stats">
|
||||
<div class="hm-mem-stat">
|
||||
<span class="hm-mem-stat-label">${t('engine.memoryFiles')}</span>
|
||||
<strong>3</strong>
|
||||
</div>
|
||||
<div class="hm-mem-stat">
|
||||
<span class="hm-mem-stat-label">${t('engine.memoryFilled')}</span>
|
||||
<strong>${filledCount}/3</strong>
|
||||
</div>
|
||||
<div class="hm-mem-stat">
|
||||
<span class="hm-mem-stat-label">${t('engine.memoryTotalWords')}</span>
|
||||
<strong>${totalWords}</strong>
|
||||
</div>
|
||||
<div class="hm-mem-stat">
|
||||
<span class="hm-mem-stat-label">${t('engine.memoryLatest')}</span>
|
||||
<strong>${latest ? escHtml(fmtMtime(latest)) : '—'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderSection(section) {
|
||||
const content = data[section.key] || ''
|
||||
const { chars, words } = contentStats(content)
|
||||
@@ -231,7 +269,7 @@ export function render() {
|
||||
</span>`
|
||||
|
||||
return `
|
||||
<div class="hm-panel hm-mem-panel" data-key="${section.key}">
|
||||
<div class="hm-panel hm-mem-panel hm-mem-panel--${section.key}" data-key="${section.key}">
|
||||
<div class="hm-panel-header">
|
||||
<div class="hm-panel-title">
|
||||
<span class="hm-panel-title-icon">${section.icon}</span>
|
||||
@@ -243,12 +281,17 @@ export function render() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
<div class="hm-mem-card-topline">
|
||||
<div class="hm-mem-card-index">${section.key.toUpperCase()}</div>
|
||||
<div class="hm-mem-card-meter"><span style="width:${Math.min(100, Math.max(8, words / 8))}%"></span></div>
|
||||
</div>
|
||||
<div class="hm-mem-desc">${t(section.descKey)}</div>
|
||||
${content.trim()
|
||||
? `<div class="hm-mem-rendered markdown-body">${mdToHtml(content)}</div>`
|
||||
: `<div class="hm-mem-empty">
|
||||
<span class="hm-mem-empty-title">${t('engine.memoryEmpty')}</span>
|
||||
<span class="hm-muted">${t(section.descKey)}</span>
|
||||
<button class="hm-btn hm-btn--ghost hm-btn--sm hm-mem-edit hm-mem-empty-cta" data-key="${section.key}">${ICONS.edit} ${t('engine.memoryEdit')}</button>
|
||||
</div>`}
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,6 +316,8 @@ export function render() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${!loading && !loadError ? renderOverview() : ''}
|
||||
|
||||
${loadError ? `
|
||||
<div class="hm-panel" style="margin-bottom:18px">
|
||||
<div class="hm-panel-body hm-panel-body--tight">
|
||||
|
||||
@@ -108,6 +108,12 @@ export function render() {
|
||||
let fileContent = ''
|
||||
let loadingFile = false
|
||||
|
||||
// Toolsets state — backend returns { raw: <stdout> }; we parse rows on the fly.
|
||||
// toolsets is null when never loaded, [] when loaded but empty/parse-failed.
|
||||
let toolsets = null // [{ name, enabled, description }]
|
||||
let toolsetsRaw = '' // raw stdout, kept for fallback display when parsing fails
|
||||
let toolsetsLoading = true
|
||||
|
||||
// ============================================================ loaders
|
||||
|
||||
async function loadSkills() {
|
||||
@@ -124,6 +130,61 @@ export function render() {
|
||||
draw()
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip ANSI escape sequences (color/style/cursor) from a string.
|
||||
* Hermes' `tools list` may include them when stdout is detected as a TTY,
|
||||
* even though we capture via pipe — be defensive.
|
||||
*/
|
||||
function stripAnsi(s) {
|
||||
if (!s) return ''
|
||||
// Standard CSI sequences: ESC [ ... letter
|
||||
return String(s).replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `hermes tools list --platform <p>` stdout. Format observed
|
||||
* (Hermes 0.6+):
|
||||
*
|
||||
* Built-in toolsets (cli):
|
||||
* ✓ enabled web 🔍 Web Search & Scraping
|
||||
* ✗ disabled image_gen 🎨 Image Generation
|
||||
* ...
|
||||
*
|
||||
* Returns an array; empty array means parse failed or no rows.
|
||||
*/
|
||||
function parseToolsets(raw) {
|
||||
const clean = stripAnsi(raw || '')
|
||||
const out = []
|
||||
for (const line of clean.split(/\r?\n/)) {
|
||||
// Use [^\s] explicitly because emoji/multi-codepoint description part needs greedy tail.
|
||||
const m = line.match(/^\s*([✓✗])\s+(enabled|disabled)\s+(\S+)\s+(.+?)\s*$/u)
|
||||
if (!m) continue
|
||||
out.push({
|
||||
name: m[3],
|
||||
enabled: m[1] === '✓' || m[2] === 'enabled',
|
||||
description: m[4],
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function loadToolsets() {
|
||||
toolsetsLoading = true
|
||||
draw()
|
||||
try {
|
||||
const r = await api.hermesToolsetsList()
|
||||
toolsetsRaw = r?.raw || ''
|
||||
toolsets = parseToolsets(toolsetsRaw)
|
||||
} catch (e) {
|
||||
console.error('Failed to load toolsets:', e)
|
||||
toolsetsRaw = ''
|
||||
toolsets = []
|
||||
} finally {
|
||||
toolsetsLoading = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(skill) {
|
||||
activeSkill = skill
|
||||
loadingDetail = true
|
||||
@@ -301,6 +362,103 @@ export function render() {
|
||||
`
|
||||
}
|
||||
|
||||
function renderToolsets() {
|
||||
// 加载中骨架屏
|
||||
if (toolsetsLoading) {
|
||||
return `
|
||||
<section class="hm-toolsets">
|
||||
<div class="hm-toolsets-head">
|
||||
<div class="hm-toolsets-title-block">
|
||||
<div class="hm-toolsets-title">${t('engine.toolsetsTitle')}</div>
|
||||
<div class="hm-toolsets-sub">${t('engine.toolsetsSubtitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-toolsets-grid">
|
||||
${Array.from({ length: 8 }).map(() =>
|
||||
`<div class="hm-toolset-card hm-toolset-card--skel"><div class="hm-skel" style="width:55%;height:14px;margin-bottom:8px"></div><div class="hm-skel" style="width:80%;height:11px"></div></div>`
|
||||
).join('')}
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
const items = toolsets || []
|
||||
const activeCount = items.filter(x => x.enabled).length
|
||||
const total = items.length
|
||||
|
||||
// 解析失败但有 raw 输出 → 显示原始内容
|
||||
if (total === 0 && toolsetsRaw && toolsetsRaw.trim()) {
|
||||
return `
|
||||
<section class="hm-toolsets">
|
||||
<div class="hm-toolsets-head">
|
||||
<div class="hm-toolsets-title-block">
|
||||
<div class="hm-toolsets-title">${t('engine.toolsetsTitle')}</div>
|
||||
<div class="hm-toolsets-sub">${t('engine.toolsetsSubtitle')}</div>
|
||||
</div>
|
||||
<button class="hm-btn hm-btn--ghost hm-btn--sm" id="hm-toolsets-refresh">
|
||||
${ICONS.refresh} ${t('engine.skillsRefresh')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="hm-toolsets-fallback">
|
||||
<div class="hm-toolsets-fallback-hint">${t('engine.toolsetsParseFailed')}</div>
|
||||
<pre class="hm-toolsets-fallback-pre">${escHtml(stripAnsi(toolsetsRaw))}</pre>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
// 完全空(hermes 没装/版本太老)
|
||||
if (total === 0) {
|
||||
return `
|
||||
<section class="hm-toolsets">
|
||||
<div class="hm-toolsets-head">
|
||||
<div class="hm-toolsets-title-block">
|
||||
<div class="hm-toolsets-title">${t('engine.toolsetsTitle')}</div>
|
||||
<div class="hm-toolsets-sub">${t('engine.toolsetsSubtitle')}</div>
|
||||
</div>
|
||||
<button class="hm-btn hm-btn--ghost hm-btn--sm" id="hm-toolsets-refresh">
|
||||
${ICONS.refresh} ${t('engine.skillsRefresh')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="hm-toolsets-empty">${t('engine.toolsetsEmpty')}</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
// 正常态
|
||||
const countLabel = t('engine.toolsetsActiveCount')
|
||||
.replace('{n}', String(activeCount))
|
||||
.replace('{total}', String(total))
|
||||
return `
|
||||
<section class="hm-toolsets">
|
||||
<div class="hm-toolsets-head">
|
||||
<div class="hm-toolsets-title-block">
|
||||
<div class="hm-toolsets-title">
|
||||
${t('engine.toolsetsTitle')}
|
||||
<span class="hm-toolsets-count">${countLabel}</span>
|
||||
</div>
|
||||
<div class="hm-toolsets-sub">${t('engine.toolsetsSubtitle')}</div>
|
||||
</div>
|
||||
<button class="hm-btn hm-btn--ghost hm-btn--sm" id="hm-toolsets-refresh">
|
||||
${ICONS.refresh} ${t('engine.skillsRefresh')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="hm-toolsets-grid">
|
||||
${items.map(it => `
|
||||
<div class="hm-toolset-card ${it.enabled ? 'is-on' : 'is-off'}" title="${escHtml(it.description)}">
|
||||
<div class="hm-toolset-card-row">
|
||||
<span class="hm-toolset-status ${it.enabled ? 'is-on' : 'is-off'}">${it.enabled ? '✓' : '✗'}</span>
|
||||
<span class="hm-toolset-name">${escHtml(it.name)}</span>
|
||||
</div>
|
||||
<div class="hm-toolset-desc">${escHtml(it.description)}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="hm-toolsets-hint">${t('engine.toolsetsHint')}</div>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
if (!activeSkill) return renderEmpty()
|
||||
if (loadingDetail) {
|
||||
@@ -394,6 +552,8 @@ export function render() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${renderToolsets()}
|
||||
|
||||
<div class="hm-skills-layout">
|
||||
${renderSidebar()}
|
||||
<section class="hm-skills-main">${renderDetail()}</section>
|
||||
@@ -411,6 +571,7 @@ export function render() {
|
||||
})
|
||||
|
||||
el.querySelector('#hm-skills-refresh')?.addEventListener('click', () => loadSkills())
|
||||
el.querySelector('#hm-toolsets-refresh')?.addEventListener('click', () => loadToolsets())
|
||||
|
||||
el.querySelectorAll('.hm-skill-cat-header').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -454,5 +615,6 @@ export function render() {
|
||||
}
|
||||
|
||||
loadSkills()
|
||||
loadToolsets()
|
||||
return el
|
||||
}
|
||||
|
||||
@@ -135,9 +135,9 @@
|
||||
* ==========================================================================
|
||||
*/
|
||||
[data-engine="hermes"].page {
|
||||
padding: 40px 48px 64px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
/* 移除 1280px 硬限,宽屏填满;水平 padding 自适应避免内容贴边 */
|
||||
padding: 40px clamp(20px, 3.5vw, 64px) 64px;
|
||||
width: 100%;
|
||||
font-family: var(--hm-font-sans);
|
||||
color: var(--hm-text-primary);
|
||||
background: var(--hm-surface-0);
|
||||
@@ -978,11 +978,9 @@
|
||||
`data-engine`. The descendant form silently misses every Hermes page
|
||||
that uses the shared shell. */
|
||||
[data-engine="hermes"].page {
|
||||
/* Generous 40/48 gutter matches the custom editorial shells so switching
|
||||
between dashboard / cron / services / memory doesn't feel like hopping
|
||||
between two different products. */
|
||||
padding: 40px 48px 56px;
|
||||
max-width: 1280px;
|
||||
/* Generous gutter via clamp() so dashboard / cron / services / memory feel
|
||||
like one product on any width. No max-width — fill the viewport. */
|
||||
padding: 40px clamp(20px, 3.5vw, 56px) 56px;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
[data-engine="hermes"].page {
|
||||
@@ -1842,12 +1840,120 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
parent/child pair. The descendant-combinator form silently failed and
|
||||
left every page shell padding-less, hugging the viewport edges. */
|
||||
[data-engine="hermes"].hermes-memory-page {
|
||||
padding: 40px 48px 64px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 40px clamp(20px, 3.5vw, 64px) 64px;
|
||||
width: 100%;
|
||||
font-family: var(--hm-font-sans);
|
||||
color: var(--hm-text-primary);
|
||||
background: var(--hm-surface-0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-engine="hermes"].hermes-memory-page::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 0;
|
||||
width: min(42vw, 520px);
|
||||
height: min(42vw, 520px);
|
||||
background: radial-gradient(circle, var(--hm-accent-soft), transparent 68%);
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 420px);
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
margin: -10px 0 28px;
|
||||
padding: 28px;
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: var(--hm-radius-lg);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(202, 138, 4, 0.10), transparent 36%),
|
||||
var(--hm-surface-1);
|
||||
box-shadow: var(--hm-shadow-md);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--hm-border-subtle) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--hm-border-subtle) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
mask-image: linear-gradient(90deg, transparent, #000 18%, transparent 78%);
|
||||
pointer-events: none;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview-copy,
|
||||
[data-engine="hermes"] .hm-mem-overview-stats {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-kicker {
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--hm-accent);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview-title {
|
||||
font-family: var(--hm-font-serif);
|
||||
font-size: clamp(24px, 3vw, 36px);
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.025em;
|
||||
max-width: 620px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview-desc {
|
||||
color: var(--hm-text-secondary);
|
||||
line-height: 1.85;
|
||||
max-width: 660px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: var(--hm-radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--hm-border);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-stat {
|
||||
padding: 18px;
|
||||
background: color-mix(in srgb, var(--hm-surface-1) 88%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-stat-label {
|
||||
display: block;
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--hm-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-stat strong {
|
||||
font-family: var(--hm-font-serif);
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-stats {
|
||||
@@ -1871,6 +1977,59 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-panel {
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--hm-surface-1) 96%, var(--hm-accent-soft)), var(--hm-surface-1));
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(180deg, transparent, var(--hm-accent), transparent);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-panel:hover {
|
||||
border-color: color-mix(in srgb, var(--hm-accent) 38%, var(--hm-border));
|
||||
box-shadow: var(--hm-shadow-lg);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-panel .hm-panel-header {
|
||||
background: linear-gradient(90deg, var(--hm-surface-1), transparent);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-card-topline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-card-index {
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--hm-accent);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-card-meter {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--hm-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-card-meter span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--hm-accent), transparent);
|
||||
}
|
||||
|
||||
/* Memory edit button — promote the ghost CTA to a visible outline chip
|
||||
so users recognise it as an action, not inline meta-text. */
|
||||
[data-engine="hermes"] .hm-mem-edit {
|
||||
@@ -1946,10 +2105,13 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-empty {
|
||||
padding: 18px 0 4px;
|
||||
padding: 22px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
border: 1px dashed var(--hm-border-strong);
|
||||
border-radius: var(--hm-radius-md);
|
||||
background: color-mix(in srgb, var(--hm-surface-2) 72%, transparent);
|
||||
}
|
||||
[data-engine="hermes"] .hm-mem-empty-title {
|
||||
font-family: var(--hm-font-serif);
|
||||
@@ -1958,11 +2120,22 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-empty-cta {
|
||||
width: fit-content;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-rendered {
|
||||
font-family: var(--hm-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
color: var(--hm-text-primary);
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--hm-border-subtle);
|
||||
border-radius: var(--hm-radius-md);
|
||||
background: color-mix(in srgb, var(--hm-surface-0) 74%, transparent);
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
}
|
||||
[data-engine="hermes"] .hm-mem-rendered h2,
|
||||
[data-engine="hermes"] .hm-mem-rendered h3,
|
||||
@@ -2007,12 +2180,52 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 注意:viewport 宽度并不等于实际内容区宽度——ClawPanel 有 ~280px 侧栏。
|
||||
在 1100px viewport 下内容区只有 ~820px,2 列布局(copy + 320~420px stats)
|
||||
会把 copy 压到 230px,标题被迫 4 行。所以 1100px 就该切到单列。 */
|
||||
@media (max-width: 1100px) {
|
||||
[data-engine="hermes"] .hm-mem-overview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
[data-engine="hermes"].hermes-memory-page {
|
||||
padding: 28px 24px 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
[data-engine="hermes"].hermes-memory-page {
|
||||
padding: 22px 16px 36px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview {
|
||||
padding: 20px;
|
||||
border-radius: var(--hm-radius-md);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-overview-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-panel .hm-panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-mem-panel .hm-panel-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* 16 · Logs page (editorial layout with sidebar + live entries)
|
||||
* ==========================================================================
|
||||
*/
|
||||
[data-engine="hermes"].hermes-logs-page {
|
||||
padding: 40px 48px 48px;
|
||||
padding: 40px clamp(20px, 3.5vw, 64px) 48px;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -2479,8 +2692,8 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
/* ---- Page shell ---- */
|
||||
[data-engine="hermes"].hermes-skills-page {
|
||||
/* Bottom padding restored so the two-column card doesn't kiss the viewport
|
||||
edge — matches the logs page gutter (40/48/48). */
|
||||
padding: 40px 48px 48px;
|
||||
edge — matches the logs page gutter. */
|
||||
padding: 40px clamp(20px, 3.5vw, 64px) 48px;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -3028,6 +3241,172 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Skills page · prevent hero + toolsets from being squashed ---------
|
||||
.hermes-skills-page is `display:flex;flex-direction:column;height:100%`
|
||||
(defined in pages.css) so .hm-skills-layout (flex:1) can fill the viewport
|
||||
and let the two-column sidebar scroll independently. Without these
|
||||
shrink-locks, the flex distribution squeezes hero (240→165px) and toolsets
|
||||
when they share viewport with the layout. */
|
||||
[data-engine="hermes"].hermes-skills-page > .hm-hero,
|
||||
[data-engine="hermes"].hermes-skills-page > .hm-toolsets {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- Skills · Toolsets section ---------------------------------------- */
|
||||
[data-engine="hermes"] .hm-toolsets {
|
||||
margin: 24px 0 0;
|
||||
padding: 18px 22px 14px;
|
||||
background: var(--hm-surface-1);
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: var(--hm-radius-md);
|
||||
box-shadow: 0 1px 2px rgba(28, 25, 23, 0.03), 0 2px 6px rgba(28, 25, 23, 0.035);
|
||||
}
|
||||
[data-theme="dark"] [data-engine="hermes"] .hm-toolsets { box-shadow: none; }
|
||||
|
||||
[data-engine="hermes"] .hm-toolsets-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolsets-title-block {
|
||||
min-width: 0;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolsets-title {
|
||||
font-family: var(--hm-font-serif);
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--hm-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolsets-count {
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: var(--hm-text-tertiary);
|
||||
background: var(--hm-surface-2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolsets-sub {
|
||||
font-size: 11.5px;
|
||||
color: var(--hm-text-tertiary);
|
||||
margin-top: 4px;
|
||||
font-family: var(--hm-font-mono);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-toolsets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-toolset-card {
|
||||
padding: 12px 14px;
|
||||
background: var(--hm-surface-0);
|
||||
border: 1px solid var(--hm-border-subtle);
|
||||
border-radius: var(--hm-radius-sm);
|
||||
transition: border-color 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolset-card.is-on {
|
||||
border-color: var(--hm-border);
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolset-card.is-off {
|
||||
background: transparent;
|
||||
opacity: 0.55;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolset-card:hover {
|
||||
border-color: var(--hm-accent, var(--hm-text-primary));
|
||||
background: var(--hm-surface-1);
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolset-card--skel {
|
||||
pointer-events: none;
|
||||
border-color: var(--hm-border-subtle);
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-toolset-card-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolset-status {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
width: 14px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolset-status.is-on { color: var(--hm-success, #2d7d4f); }
|
||||
[data-engine="hermes"] .hm-toolset-status.is-off { color: var(--hm-text-tertiary); }
|
||||
|
||||
[data-engine="hermes"] .hm-toolset-name {
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: var(--hm-text-primary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-toolset-desc {
|
||||
font-size: 11.5px;
|
||||
color: var(--hm-text-secondary);
|
||||
line-height: 1.5;
|
||||
/* 限两行 + 截断,emoji + 文字混排时避免一个 toolset 的描述把卡片撑得很高 */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-toolsets-empty,
|
||||
[data-engine="hermes"] .hm-toolsets-fallback-hint {
|
||||
padding: 12px 0 4px;
|
||||
font-style: italic;
|
||||
font-family: var(--hm-font-serif);
|
||||
color: var(--hm-text-tertiary);
|
||||
font-size: 13px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolsets-fallback-pre {
|
||||
margin: 8px 0 0;
|
||||
padding: 12px 14px;
|
||||
background: var(--hm-surface-0);
|
||||
border: 1px solid var(--hm-border-subtle);
|
||||
border-radius: var(--hm-radius-sm);
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.6;
|
||||
color: var(--hm-text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[data-engine="hermes"] .hm-toolsets-hint {
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--hm-border-subtle);
|
||||
font-size: 11px;
|
||||
color: var(--hm-text-tertiary);
|
||||
line-height: 1.6;
|
||||
font-family: var(--hm-font-mono);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
[data-engine="hermes"] .hm-toolsets {
|
||||
padding: 14px 16px 12px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-toolsets-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
* 19 · Chat page (editorial luxury re-write)
|
||||
* ==========================================================================
|
||||
@@ -5121,9 +5500,8 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
}
|
||||
|
||||
[data-engine="hermes"].hm-usage-page {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 30px 40px;
|
||||
width: 100%;
|
||||
padding: 28px clamp(16px, 3vw, 56px) 40px;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-usage-hero {
|
||||
@@ -5383,9 +5761,8 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
}
|
||||
|
||||
[data-engine="hermes"].hm-services-page {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 30px 40px;
|
||||
width: 100%;
|
||||
padding: 28px clamp(16px, 3vw, 56px) 40px;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-services-desc {
|
||||
|
||||
@@ -99,10 +99,11 @@
|
||||
}
|
||||
|
||||
[data-engine="xintian"] .xt-stage {
|
||||
/* 移除 1240px 硬限,宽屏填满;水平 padding 自适应。
|
||||
内部 .xt-hero / .xt-section-head 自带 max-width 保证可读性。 */
|
||||
position: relative;
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
padding: 56px 48px 96px;
|
||||
width: 100%;
|
||||
padding: 56px clamp(20px, 4vw, 80px) 96px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
|
||||
Reference in New Issue
Block a user