+48
-10
@@ -11,6 +11,7 @@
|
||||
* id: string,
|
||||
* label: string,
|
||||
* description: string,
|
||||
* activity?: string,
|
||||
* match?: (q: string) => boolean,
|
||||
* run: (tools: { run: (name: string, args?: object) => Promise<any> }) => Promise<any>,
|
||||
* summarize: (result: any) => string,
|
||||
@@ -22,7 +23,8 @@ export const AGENT_SPECS = [
|
||||
{
|
||||
id: 'host',
|
||||
label: 'Host investigator',
|
||||
description: 'KPIs, findings, health, top processes pack',
|
||||
description: 'KPIs, findings, health pack',
|
||||
activity: 'Calling investigate_host…',
|
||||
match: (q) =>
|
||||
!q ||
|
||||
/health|wrong|diagnos|investigat|summar|status|load|cpu|ram|memory|host|alert|anomal/.test(
|
||||
@@ -42,7 +44,8 @@ export const AGENT_SPECS = [
|
||||
{
|
||||
id: 'hot',
|
||||
label: 'Hot metrics',
|
||||
description: 'Charts with strongest recent change',
|
||||
description: 'Spiking / changing charts',
|
||||
activity: 'Scanning hot_metrics window…',
|
||||
match: (q) =>
|
||||
!q ||
|
||||
/hot|spik|unusual|metric|chart|cpu|disk|io|net|redis|docker|postgres|nginx/.test(q),
|
||||
@@ -58,7 +61,8 @@ export const AGENT_SPECS = [
|
||||
{
|
||||
id: 'processes',
|
||||
label: 'Process scout',
|
||||
description: 'Top CPU processes',
|
||||
description: 'Top CPU / RSS processes',
|
||||
activity: 'Listing top processes by CPU…',
|
||||
match: (q) =>
|
||||
!q || /process|top|cpu|load|rss|memory|pid|runaway|hung/.test(q),
|
||||
run: (tools) => tools.run('list_processes', { sort: 'cpu', limit: 12, filter: 'all' }),
|
||||
@@ -74,7 +78,8 @@ export const AGENT_SPECS = [
|
||||
{
|
||||
id: 'anomalies',
|
||||
label: 'Anomaly / alerts',
|
||||
description: 'Recent anomalies and open alerts',
|
||||
description: 'Anomalies + open alerts',
|
||||
activity: 'Fetching anomalies & alerts…',
|
||||
match: (q) =>
|
||||
!q || /anomal|alert|warn|crit|threshold|silence|page/.test(q),
|
||||
run: async (tools) => {
|
||||
@@ -99,7 +104,8 @@ export const AGENT_SPECS = [
|
||||
{
|
||||
id: 'fleet',
|
||||
label: 'Fleet scout',
|
||||
description: 'Parent/child fleet health',
|
||||
description: 'Parent / child fleet health',
|
||||
activity: 'Querying fleet_health…',
|
||||
match: (q) => /fleet|child|peer|parent|cluster|all hosts/.test(q),
|
||||
run: async (tools) => {
|
||||
const [health, children] = await Promise.all([
|
||||
@@ -117,7 +123,8 @@ export const AGENT_SPECS = [
|
||||
{
|
||||
id: 'storage',
|
||||
label: 'Storage / retention',
|
||||
description: 'Agent disk and retention config',
|
||||
description: 'Disk usage & retention config',
|
||||
activity: 'Reading storage_info…',
|
||||
match: (q) => /storage|retention|prune|disk|warm|history|size|hyperdb/.test(q),
|
||||
run: (tools) => tools.run('storage_info', {}),
|
||||
summarize: (r) => {
|
||||
@@ -228,10 +235,13 @@ export function pickAgents(userText, maxAgents = 3) {
|
||||
* query: string,
|
||||
* maxAgents?: number,
|
||||
* isAborted?: () => boolean,
|
||||
* onPlan?: (agents: AgentSpec[]) => void,
|
||||
* onAgent?: (ev: {
|
||||
* id: string,
|
||||
* label: string,
|
||||
* status: 'start'|'done'|'error',
|
||||
* description?: string,
|
||||
* status: 'queued'|'start'|'running'|'done'|'error'|'stopped',
|
||||
* activity?: string,
|
||||
* summary?: string,
|
||||
* error?: string,
|
||||
* }) => void,
|
||||
@@ -242,6 +252,8 @@ export async function runSubAgents(opts) {
|
||||
const tools = opts.tools
|
||||
const aborted = () => Boolean(opts.isAborted?.())
|
||||
|
||||
opts.onPlan?.(specs)
|
||||
|
||||
/** @type {Array<{ id: string, label: string, status: string, summary: string, result?: any, error?: string }>} */
|
||||
const agents = []
|
||||
|
||||
@@ -251,23 +263,45 @@ export async function runSubAgents(opts) {
|
||||
agents.push({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
status: 'error',
|
||||
status: 'stopped',
|
||||
summary: 'stopped',
|
||||
error: 'stopped',
|
||||
})
|
||||
opts.onAgent?.({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
description: spec.description,
|
||||
status: 'stopped',
|
||||
activity: 'Stopped',
|
||||
error: 'stopped',
|
||||
})
|
||||
return
|
||||
}
|
||||
opts.onAgent?.({ id: spec.id, label: spec.label, status: 'start' })
|
||||
opts.onAgent?.({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
description: spec.description,
|
||||
status: 'start',
|
||||
activity: spec.activity || `Running ${spec.label}…`,
|
||||
})
|
||||
try {
|
||||
const result = await spec.run(tools)
|
||||
if (aborted()) {
|
||||
agents.push({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
status: 'error',
|
||||
status: 'stopped',
|
||||
summary: 'stopped',
|
||||
error: 'stopped',
|
||||
})
|
||||
opts.onAgent?.({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
description: spec.description,
|
||||
status: 'stopped',
|
||||
activity: 'Stopped',
|
||||
error: 'stopped',
|
||||
})
|
||||
return
|
||||
}
|
||||
const summary = spec.summarize(result)
|
||||
@@ -283,7 +317,9 @@ export async function runSubAgents(opts) {
|
||||
opts.onAgent?.({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
description: spec.description,
|
||||
status: row.status === 'error' ? 'error' : 'done',
|
||||
activity: row.status === 'error' ? 'Failed' : 'Complete',
|
||||
summary,
|
||||
error: row.error,
|
||||
})
|
||||
@@ -299,7 +335,9 @@ export async function runSubAgents(opts) {
|
||||
opts.onAgent?.({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
description: spec.description,
|
||||
status: 'error',
|
||||
activity: 'Failed',
|
||||
error: msg,
|
||||
})
|
||||
}
|
||||
|
||||
+26
-37
@@ -12,6 +12,7 @@ import {
|
||||
synthesizeFromAgents,
|
||||
} from './agents.js'
|
||||
import { chartIdsFromToolLog, mountChartEmbeds } from './chart-embed.js'
|
||||
import { createSwarmPanel } from './swarm-ui.js'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
@@ -269,41 +270,12 @@ export function createQvacView(opts) {
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {Map<string, HTMLElement>} */
|
||||
const agentChipEls = new Map()
|
||||
const swarm = createSwarmPanel(opts.els.agentBar, {
|
||||
onStop: () => stopInference(),
|
||||
})
|
||||
|
||||
function clearAgentBar() {
|
||||
const bar = opts.els.agentBar
|
||||
if (!bar) return
|
||||
bar.innerHTML = ''
|
||||
bar.classList.add('hidden')
|
||||
agentChipEls.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ id: string, label: string, status: string, summary?: string, error?: string }} ev
|
||||
*/
|
||||
function paintAgentEvent(ev) {
|
||||
const bar = opts.els.agentBar
|
||||
if (!bar) return
|
||||
bar.classList.remove('hidden')
|
||||
let chip = agentChipEls.get(ev.id)
|
||||
if (!chip) {
|
||||
chip = document.createElement('span')
|
||||
chip.className = 'qvac-agent-chip'
|
||||
chip.dataset.agent = ev.id
|
||||
bar.appendChild(chip)
|
||||
agentChipEls.set(ev.id, chip)
|
||||
}
|
||||
chip.dataset.status = ev.status
|
||||
const tip = ev.summary || ev.error || ''
|
||||
chip.title = tip
|
||||
chip.textContent =
|
||||
ev.status === 'start'
|
||||
? `⟳ ${ev.label}`
|
||||
: ev.status === 'error'
|
||||
? `✕ ${ev.label}`
|
||||
: `✓ ${ev.label}`
|
||||
swarm.hide()
|
||||
}
|
||||
|
||||
/** Finish onboarding in tools-only mode (no model download). */
|
||||
@@ -810,6 +782,7 @@ export function createQvacView(opts) {
|
||||
async function stopInference() {
|
||||
if (!busy) return
|
||||
setStatus('Stopping…', 'busy')
|
||||
if (swarm.isOpen?.()) swarm.fail('Stopped by user')
|
||||
try {
|
||||
await engine.stop?.()
|
||||
} catch (err) {
|
||||
@@ -855,27 +828,43 @@ export function createQvacView(opts) {
|
||||
opts.isConnected?.() &&
|
||||
!engine.isAborted?.()
|
||||
) {
|
||||
setStatus('Spinning up agents…', 'busy')
|
||||
setStatus('Multi-agent swarm…', 'busy')
|
||||
if (streamBody) {
|
||||
streamBody.innerHTML = formatMdLite('_Dispatching multi-agent investigation…_')
|
||||
streamBody.innerHTML = formatMdLite(
|
||||
'_Swarm is running — watch specialists above while tools gather live data…_'
|
||||
)
|
||||
}
|
||||
const pack = await runSubAgents({
|
||||
tools,
|
||||
query: text,
|
||||
maxAgents: prefs.maxSubAgents,
|
||||
isAborted: () => Boolean(engine.isAborted?.()),
|
||||
onPlan: (specs) => {
|
||||
swarm.begin(
|
||||
specs.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
description: s.description,
|
||||
})),
|
||||
text
|
||||
)
|
||||
},
|
||||
onAgent: (ev) => {
|
||||
if (engine.isAborted?.()) return
|
||||
paintAgentEvent(ev)
|
||||
setStatus(`Agent: ${ev.label}…`, 'busy')
|
||||
swarm.update(ev)
|
||||
if (ev.status === 'start' || ev.status === 'running') {
|
||||
setStatus(`${ev.label}…`, 'busy')
|
||||
}
|
||||
},
|
||||
})
|
||||
if (engine.isAborted?.()) {
|
||||
swarm.fail('Stopped')
|
||||
acc = '_(Stopped during multi-agent investigation.)_'
|
||||
paintAssistant(streamBody, acc, false)
|
||||
setStatus('Stopped', 'warn')
|
||||
return
|
||||
}
|
||||
swarm.complete('All agents done — synthesizing answer…')
|
||||
completeOpts.agentBriefing = pack.contextText
|
||||
completeOpts.agentFallbackText = synthesizeFromAgents(text, pack)
|
||||
completeOpts.agentToolCalls = pack.agents.map((a) => ({
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Multi-agent swarm panel — animated cards, live activity, progress.
|
||||
*/
|
||||
|
||||
/** @type {Record<string, { icon: string, hue: number, short: string }>} */
|
||||
export const AGENT_VISUAL = {
|
||||
host: { icon: '◎', hue: 168, short: 'Host' },
|
||||
hot: { icon: '⚡', hue: 28, short: 'Hot' },
|
||||
processes: { icon: '⧉', hue: 210, short: 'Procs' },
|
||||
anomalies: { icon: '⚑', hue: 0, short: 'Alerts' },
|
||||
fleet: { icon: '⬡', hue: 265, short: 'Fleet' },
|
||||
storage: { icon: '▣', hue: 45, short: 'Store' },
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement|null|undefined} host
|
||||
* @param {{
|
||||
* onStop?: () => void,
|
||||
* }} [opts]
|
||||
*/
|
||||
export function createSwarmPanel(host, opts = {}) {
|
||||
/** @type {Map<string, {
|
||||
* el: HTMLElement,
|
||||
* status: string,
|
||||
* label: string,
|
||||
* description?: string,
|
||||
* activity: string,
|
||||
* summary?: string,
|
||||
* startedAt?: number,
|
||||
* finishedAt?: number,
|
||||
* }>} */
|
||||
const cards = new Map()
|
||||
/** @type {Array<{ t: number, text: string, kind: string }>} */
|
||||
let log = []
|
||||
let total = 0
|
||||
let done = 0
|
||||
let panelOpen = false
|
||||
/** @type {ReturnType<typeof setInterval>|null} */
|
||||
let tickTimer = null
|
||||
|
||||
function vis(id) {
|
||||
return AGENT_VISUAL[id] || { icon: '◆', hue: 200, short: id }
|
||||
}
|
||||
|
||||
function ensureShell() {
|
||||
if (!host) return null
|
||||
if (host.querySelector('.qvac-swarm')) return host.querySelector('.qvac-swarm')
|
||||
host.innerHTML = `
|
||||
<div class="qvac-swarm" data-state="idle">
|
||||
<header class="qvac-swarm-head">
|
||||
<div class="qvac-swarm-brand">
|
||||
<span class="qvac-swarm-orb" aria-hidden="true">
|
||||
<span class="qvac-swarm-orb-core"></span>
|
||||
<span class="qvac-swarm-orb-ring"></span>
|
||||
<span class="qvac-swarm-orb-ring qvac-swarm-orb-ring-2"></span>
|
||||
</span>
|
||||
<div class="qvac-swarm-titles">
|
||||
<strong class="qvac-swarm-title">Multi-agent swarm</strong>
|
||||
<span class="qvac-swarm-subtitle muted">Dispatching specialists…</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qvac-swarm-progress-wrap" title="Agents finished">
|
||||
<svg class="qvac-swarm-ring" viewBox="0 0 36 36" aria-hidden="true">
|
||||
<path class="qvac-swarm-ring-bg" d="M18 2.5a15.5 15.5 0 1 1 0 31 15.5 15.5 0 0 1 0-31z"/>
|
||||
<path class="qvac-swarm-ring-fg" d="M18 2.5a15.5 15.5 0 1 1 0 31 15.5 15.5 0 0 1 0-31z"
|
||||
stroke-dasharray="0 100" pathLength="100"/>
|
||||
</svg>
|
||||
<span class="qvac-swarm-count">0/0</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="qvac-swarm-grid" role="list"></div>
|
||||
<div class="qvac-swarm-log" aria-live="polite">
|
||||
<div class="qvac-swarm-log-inner"></div>
|
||||
</div>
|
||||
<footer class="qvac-swarm-foot muted">
|
||||
<span class="qvac-swarm-foot-status">Idle</span>
|
||||
<span class="qvac-swarm-foot-hint">Live tool specialists · parallel</span>
|
||||
</footer>
|
||||
</div>
|
||||
`
|
||||
return host.querySelector('.qvac-swarm')
|
||||
}
|
||||
|
||||
function startTick() {
|
||||
stopTick()
|
||||
tickTimer = setInterval(() => {
|
||||
for (const [id, st] of cards) {
|
||||
if (st.status !== 'running' && st.status !== 'start') continue
|
||||
const el = st.el
|
||||
const timer = el?.querySelector('.qvac-agent-card-timer')
|
||||
if (timer && st.startedAt) {
|
||||
timer.textContent = `${((Date.now() - st.startedAt) / 1000).toFixed(1)}s`
|
||||
}
|
||||
}
|
||||
}, 120)
|
||||
}
|
||||
|
||||
function stopTick() {
|
||||
if (tickTimer) clearInterval(tickTimer)
|
||||
tickTimer = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{ id: string, label: string, description?: string }>} agents
|
||||
* @param {string} [query]
|
||||
*/
|
||||
function begin(agents, query = '') {
|
||||
if (!host) return
|
||||
host.classList.remove('hidden')
|
||||
panelOpen = true
|
||||
cards.clear()
|
||||
log = []
|
||||
total = agents.length
|
||||
done = 0
|
||||
const shell = ensureShell()
|
||||
if (!shell) return
|
||||
shell.dataset.state = 'running'
|
||||
const grid = shell.querySelector('.qvac-swarm-grid')
|
||||
const logInner = shell.querySelector('.qvac-swarm-log-inner')
|
||||
if (grid) grid.innerHTML = ''
|
||||
if (logInner) logInner.innerHTML = ''
|
||||
const sub = shell.querySelector('.qvac-swarm-subtitle')
|
||||
if (sub) {
|
||||
sub.textContent = query
|
||||
? `Investigating: ${String(query).slice(0, 72)}${query.length > 72 ? '…' : ''}`
|
||||
: 'Dispatching specialists…'
|
||||
}
|
||||
updateProgress(shell)
|
||||
setFoot(shell, 'Spawning agents…', true)
|
||||
pushLog(shell, 'swarm', `Launching ${agents.length} specialist${agents.length === 1 ? '' : 's'}`)
|
||||
|
||||
agents.forEach((a, i) => {
|
||||
const v = vis(a.id)
|
||||
const el = document.createElement('article')
|
||||
el.className = 'qvac-agent-card'
|
||||
el.dataset.status = 'queued'
|
||||
el.dataset.agent = a.id
|
||||
el.style.setProperty('--agent-hue', String(v.hue))
|
||||
el.style.setProperty('--agent-delay', `${i * 60}ms`)
|
||||
el.setAttribute('role', 'listitem')
|
||||
el.innerHTML = `
|
||||
<div class="qvac-agent-card-glow" aria-hidden="true"></div>
|
||||
<header class="qvac-agent-card-head">
|
||||
<span class="qvac-agent-avatar" aria-hidden="true">
|
||||
<span class="qvac-agent-avatar-ico">${v.icon}</span>
|
||||
<span class="qvac-agent-pulse"></span>
|
||||
</span>
|
||||
<div class="qvac-agent-card-meta">
|
||||
<strong class="qvac-agent-card-name">${esc(a.label)}</strong>
|
||||
<span class="qvac-agent-card-desc muted">${esc(a.description || v.short)}</span>
|
||||
</div>
|
||||
<span class="qvac-agent-card-badge">queued</span>
|
||||
</header>
|
||||
<div class="qvac-agent-card-activity">
|
||||
<span class="qvac-agent-activity-dot"></span>
|
||||
<span class="qvac-agent-activity-text">Waiting in queue…</span>
|
||||
</div>
|
||||
<div class="qvac-agent-card-result muted hidden"></div>
|
||||
<footer class="qvac-agent-card-foot">
|
||||
<span class="qvac-agent-card-timer">—</span>
|
||||
<span class="qvac-agent-card-state-ico" aria-hidden="true"></span>
|
||||
</footer>
|
||||
`
|
||||
grid?.appendChild(el)
|
||||
cards.set(a.id, {
|
||||
el,
|
||||
status: 'queued',
|
||||
label: a.label,
|
||||
description: a.description,
|
||||
activity: 'Waiting in queue…',
|
||||
})
|
||||
// Stagger entrance
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => el.classList.add('is-in'))
|
||||
})
|
||||
})
|
||||
startTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* id: string,
|
||||
* label?: string,
|
||||
* description?: string,
|
||||
* status: string,
|
||||
* activity?: string,
|
||||
* summary?: string,
|
||||
* error?: string,
|
||||
* }} ev
|
||||
*/
|
||||
function update(ev) {
|
||||
if (!host || !panelOpen) return
|
||||
const shell = host.querySelector('.qvac-swarm')
|
||||
if (!shell) return
|
||||
let st = cards.get(ev.id)
|
||||
if (!st) {
|
||||
// Late card
|
||||
begin(
|
||||
[{ id: ev.id, label: ev.label || ev.id, description: ev.description }],
|
||||
''
|
||||
)
|
||||
st = cards.get(ev.id)
|
||||
}
|
||||
if (!st) return
|
||||
|
||||
const prev = st.status
|
||||
st.status = ev.status
|
||||
if (ev.activity) st.activity = ev.activity
|
||||
if (ev.summary) st.summary = ev.summary
|
||||
if (ev.error) st.summary = ev.error
|
||||
|
||||
const el = st.el
|
||||
el.dataset.status = normalizeStatus(ev.status)
|
||||
|
||||
if (ev.status === 'start' || ev.status === 'running') {
|
||||
if (!st.startedAt) st.startedAt = Date.now()
|
||||
el.dataset.status = 'running'
|
||||
setCardActivity(el, ev.activity || `Running ${ev.label || st.label}…`)
|
||||
setBadge(el, 'running')
|
||||
if (prev !== 'running' && prev !== 'start') {
|
||||
pushLog(shell, ev.id, `${st.label} started`)
|
||||
}
|
||||
} else if (ev.status === 'done') {
|
||||
st.finishedAt = Date.now()
|
||||
done = Math.min(total, done + (prev === 'done' ? 0 : 1))
|
||||
setCardActivity(el, ev.activity || 'Complete')
|
||||
setBadge(el, 'done')
|
||||
setResult(el, ev.summary || 'ok')
|
||||
const ms = st.startedAt ? Date.now() - st.startedAt : 0
|
||||
const timer = el.querySelector('.qvac-agent-card-timer')
|
||||
if (timer) timer.textContent = ms ? `${(ms / 1000).toFixed(1)}s` : 'ok'
|
||||
el.classList.add('is-done-pop')
|
||||
pushLog(shell, ev.id, `${st.label} finished${ev.summary ? ` · ${clip(ev.summary, 80)}` : ''}`)
|
||||
} else if (ev.status === 'error' || ev.status === 'stopped') {
|
||||
st.finishedAt = Date.now()
|
||||
if (prev !== 'done' && prev !== 'error') done = Math.min(total, done + 1)
|
||||
setCardActivity(el, ev.error || ev.summary || 'Failed')
|
||||
setBadge(el, ev.status === 'stopped' ? 'stopped' : 'error')
|
||||
setResult(el, ev.error || ev.summary || 'error', true)
|
||||
pushLog(
|
||||
shell,
|
||||
ev.id,
|
||||
`${st.label} ${ev.status === 'stopped' ? 'stopped' : 'error'}${ev.error ? ` · ${clip(ev.error, 60)}` : ''}`
|
||||
)
|
||||
}
|
||||
|
||||
updateProgress(shell)
|
||||
const running = [...cards.values()].filter(
|
||||
(c) => c.status === 'start' || c.status === 'running'
|
||||
).length
|
||||
if (done >= total && total > 0) {
|
||||
shell.dataset.state = 'complete'
|
||||
setFoot(shell, 'Swarm complete — synthesizing…', false)
|
||||
stopTick()
|
||||
} else {
|
||||
setFoot(
|
||||
shell,
|
||||
running ? `${running} agent${running === 1 ? '' : 's'} active` : 'Waiting…',
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function complete(note = 'Synthesizing final answer…') {
|
||||
if (!host) return
|
||||
const shell = host.querySelector('.qvac-swarm')
|
||||
if (!shell) return
|
||||
shell.dataset.state = 'complete'
|
||||
setFoot(shell, note, false)
|
||||
pushLog(shell, 'swarm', note)
|
||||
stopTick()
|
||||
}
|
||||
|
||||
function fail(note = 'Stopped') {
|
||||
if (!host) return
|
||||
const shell = host.querySelector('.qvac-swarm')
|
||||
if (!shell) return
|
||||
shell.dataset.state = 'stopped'
|
||||
setFoot(shell, note, false)
|
||||
for (const st of cards.values()) {
|
||||
if (st.status === 'running' || st.status === 'start' || st.status === 'queued') {
|
||||
update({
|
||||
id: st.el.dataset.agent || '',
|
||||
status: 'stopped',
|
||||
summary: 'stopped',
|
||||
})
|
||||
}
|
||||
}
|
||||
stopTick()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
panelOpen = false
|
||||
stopTick()
|
||||
if (!host) return
|
||||
host.classList.add('hidden')
|
||||
host.innerHTML = ''
|
||||
cards.clear()
|
||||
log = []
|
||||
}
|
||||
|
||||
function setCardActivity(el, text) {
|
||||
const t = el.querySelector('.qvac-agent-activity-text')
|
||||
if (t) t.textContent = text
|
||||
}
|
||||
|
||||
function setBadge(el, status) {
|
||||
const b = el.querySelector('.qvac-agent-card-badge')
|
||||
if (!b) return
|
||||
b.textContent = status
|
||||
b.dataset.status = status
|
||||
}
|
||||
|
||||
function setResult(el, text, isErr = false) {
|
||||
const r = el.querySelector('.qvac-agent-card-result')
|
||||
if (!r) return
|
||||
r.classList.remove('hidden')
|
||||
r.classList.toggle('is-error', isErr)
|
||||
r.textContent = clip(text, 160)
|
||||
}
|
||||
|
||||
function updateProgress(shell) {
|
||||
const fg = shell.querySelector('.qvac-swarm-ring-fg')
|
||||
const count = shell.querySelector('.qvac-swarm-count')
|
||||
const pct = total ? Math.round((done / total) * 100) : 0
|
||||
if (fg) fg.setAttribute('stroke-dasharray', `${pct} 100`)
|
||||
if (count) count.textContent = `${done}/${total}`
|
||||
}
|
||||
|
||||
function setFoot(shell, status, live) {
|
||||
const s = shell.querySelector('.qvac-swarm-foot-status')
|
||||
if (s) {
|
||||
s.textContent = status
|
||||
s.classList.toggle('is-live', Boolean(live))
|
||||
}
|
||||
}
|
||||
|
||||
function pushLog(shell, kind, text) {
|
||||
log.push({ t: Date.now(), text, kind })
|
||||
const inner = shell.querySelector('.qvac-swarm-log-inner')
|
||||
if (!inner) return
|
||||
const line = document.createElement('div')
|
||||
line.className = 'qvac-swarm-log-line'
|
||||
line.dataset.kind = kind
|
||||
const v = vis(kind)
|
||||
line.innerHTML = `<span class="qvac-swarm-log-ico" style="--agent-hue:${v.hue}">${kind === 'swarm' ? '✦' : v.icon}</span><span class="qvac-swarm-log-text">${esc(text)}</span><span class="qvac-swarm-log-time muted">${timeNow()}</span>`
|
||||
inner.appendChild(line)
|
||||
// Keep last ~40 lines
|
||||
while (inner.children.length > 40) inner.removeChild(inner.firstChild)
|
||||
const logEl = shell.querySelector('.qvac-swarm-log')
|
||||
if (logEl) logEl.scrollTop = logEl.scrollHeight
|
||||
line.classList.add('is-in')
|
||||
}
|
||||
|
||||
return { begin, update, complete, fail, hide, isOpen: () => panelOpen }
|
||||
}
|
||||
|
||||
function normalizeStatus(s) {
|
||||
if (s === 'start') return 'running'
|
||||
return s
|
||||
}
|
||||
|
||||
function clip(s, n) {
|
||||
const t = String(s || '')
|
||||
return t.length > n ? t.slice(0, n - 1) + '…' : t
|
||||
}
|
||||
|
||||
function timeNow() {
|
||||
try {
|
||||
return new Date().toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
+527
-15
@@ -3721,40 +3721,552 @@ html[data-theme='light'] .proc-detail-cmd {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ── Multi-agent swarm panel ─────────────────────────────────────── */
|
||||
.qvac-agent-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 0 2px 8px;
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
|
||||
.qvac-agent-bar.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.qvac-agent-chip {
|
||||
.qvac-swarm {
|
||||
--swarm-bg: color-mix(in srgb, var(--bg-secondary) 92%, #0a0e18);
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent-primary) 28%, var(--border-color));
|
||||
background:
|
||||
radial-gradient(120% 80% at 10% -20%, color-mix(in srgb, var(--accent-primary) 18%, transparent), transparent 55%),
|
||||
radial-gradient(90% 60% at 100% 0%, color-mix(in srgb, #818cf8 14%, transparent), transparent 50%),
|
||||
var(--swarm-bg);
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, #fff 4%, transparent) inset,
|
||||
0 12px 40px rgba(0, 0, 0, 0.28);
|
||||
animation: qvac-swarm-in 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
@keyframes qvac-swarm-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-swarm::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
mask-image: linear-gradient(180deg, #000 0%, transparent 85%);
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.qvac-swarm-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px 10px;
|
||||
}
|
||||
|
||||
.qvac-swarm-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.qvac-swarm-orb {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qvac-swarm-orb-core {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #fff 0%, var(--accent-primary) 45%, transparent 70%);
|
||||
box-shadow: 0 0 16px color-mix(in srgb, var(--accent-primary) 70%, transparent);
|
||||
}
|
||||
|
||||
.qvac-swarm[data-state='running'] .qvac-swarm-orb-core {
|
||||
animation: qvac-orb-breathe 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes qvac-orb-breathe {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.88);
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-swarm-orb-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid color-mix(in srgb, var(--accent-primary) 55%, transparent);
|
||||
animation: qvac-orb-spin 3.2s linear infinite;
|
||||
}
|
||||
|
||||
.qvac-swarm-orb-ring-2 {
|
||||
inset: 4px;
|
||||
border-color: color-mix(in srgb, #818cf8 40%, transparent);
|
||||
animation-duration: 4.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
@keyframes qvac-orb-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-swarm-titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.qvac-swarm-title {
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.qvac-swarm-subtitle {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: min(52vw, 420px);
|
||||
}
|
||||
|
||||
.qvac-swarm-progress-wrap {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qvac-swarm-ring {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.qvac-swarm-ring-bg {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.qvac-swarm-ring-fg {
|
||||
fill: none;
|
||||
stroke: var(--accent-primary);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dasharray 0.45s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent-primary) 60%, transparent));
|
||||
}
|
||||
|
||||
.qvac-swarm-count {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.qvac-swarm-grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
padding: 4px 14px 12px;
|
||||
}
|
||||
|
||||
.qvac-agent-card {
|
||||
--agent-hue: 168;
|
||||
position: relative;
|
||||
border-radius: 14px;
|
||||
border: 1px solid color-mix(in srgb, hsl(var(--agent-hue) 70% 55%) 35%, var(--border-color));
|
||||
background:
|
||||
linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, hsl(var(--agent-hue) 80% 50%) 12%, transparent),
|
||||
transparent 55%
|
||||
),
|
||||
color-mix(in srgb, var(--bg-primary, #0b1020) 55%, var(--bg-secondary));
|
||||
padding: 12px;
|
||||
opacity: 0;
|
||||
transform: translateY(12px) scale(0.96);
|
||||
transition:
|
||||
border-color 0.25s ease,
|
||||
box-shadow 0.25s ease,
|
||||
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 0.4s ease;
|
||||
transition-delay: var(--agent-delay, 0ms);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qvac-agent-card.is-in {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.qvac-agent-card-glow {
|
||||
position: absolute;
|
||||
inset: -40% -20% auto;
|
||||
height: 80%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
color-mix(in srgb, hsl(var(--agent-hue) 90% 55%) 22%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='running'] {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, hsl(var(--agent-hue) 80% 55%) 40%, transparent),
|
||||
0 8px 28px color-mix(in srgb, hsl(var(--agent-hue) 70% 40%) 18%, transparent);
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='running'] .qvac-agent-card-glow {
|
||||
animation: qvac-card-glow 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes qvac-card-glow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='done'] {
|
||||
border-color: color-mix(in srgb, #34d399 45%, var(--border-color));
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='error'],
|
||||
.qvac-agent-card[data-status='stopped'] {
|
||||
border-color: color-mix(in srgb, #f87171 45%, var(--border-color));
|
||||
}
|
||||
|
||||
.qvac-agent-card.is-done-pop {
|
||||
animation: qvac-done-pop 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@keyframes qvac-done-pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-agent-card-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.qvac-agent-avatar {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: color-mix(in srgb, hsl(var(--agent-hue) 70% 50%) 22%, transparent);
|
||||
border: 1px solid color-mix(in srgb, hsl(var(--agent-hue) 70% 55%) 40%, transparent);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.qvac-agent-pulse {
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid hsl(var(--agent-hue) 80% 55%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='running'] .qvac-agent-pulse {
|
||||
animation: qvac-pulse-ring 1.4s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes qvac-pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.92);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.25);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-agent-card-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.qvac-agent-card-name {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.qvac-agent-card-desc {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.qvac-agent-card-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
|
||||
color: var(--text-primary, var(--text));
|
||||
max-width: 220px;
|
||||
background: color-mix(in srgb, var(--bg-primary, #000) 40%, transparent);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.qvac-agent-card-badge[data-status='running'],
|
||||
.qvac-agent-card-badge[data-status='start'] {
|
||||
border-color: color-mix(in srgb, #60a5fa 50%, transparent);
|
||||
color: #93c5fd;
|
||||
background: color-mix(in srgb, #3b82f6 18%, transparent);
|
||||
}
|
||||
|
||||
.qvac-agent-card-badge[data-status='done'] {
|
||||
border-color: color-mix(in srgb, #34d399 50%, transparent);
|
||||
color: #6ee7b7;
|
||||
background: color-mix(in srgb, #10b981 16%, transparent);
|
||||
}
|
||||
|
||||
.qvac-agent-card-badge[data-status='error'],
|
||||
.qvac-agent-card-badge[data-status='stopped'] {
|
||||
border-color: color-mix(in srgb, #f87171 50%, transparent);
|
||||
color: #fca5a5;
|
||||
background: color-mix(in srgb, #ef4444 14%, transparent);
|
||||
}
|
||||
|
||||
.qvac-agent-card-activity {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding: 7px 9px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, #000 28%, transparent);
|
||||
font-size: 0.72rem;
|
||||
min-height: 2.1em;
|
||||
}
|
||||
|
||||
.qvac-agent-activity-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--agent-hue) 80% 58%);
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px hsl(var(--agent-hue) 80% 55%);
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='running'] .qvac-agent-activity-dot {
|
||||
animation: qvac-dot-blink 0.9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes qvac-dot-blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='done'] .qvac-agent-activity-dot {
|
||||
background: #34d399;
|
||||
box-shadow: 0 0 8px #34d399;
|
||||
}
|
||||
|
||||
.qvac-agent-card[data-status='error'] .qvac-agent-activity-dot,
|
||||
.qvac-agent-card[data-status='stopped'] .qvac-agent-activity-dot {
|
||||
background: #f87171;
|
||||
box-shadow: 0 0 8px #f87171;
|
||||
}
|
||||
|
||||
.qvac-agent-activity-text {
|
||||
opacity: 0.92;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.qvac-agent-card-result {
|
||||
margin-top: 8px;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.35;
|
||||
font-family: var(--font-mono);
|
||||
max-height: 3.6em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qvac-agent-card-result.is-error {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.qvac-agent-card-result.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.qvac-agent-card-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
font-size: 0.65rem;
|
||||
font-family: var(--font-mono);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.qvac-swarm-log {
|
||||
position: relative;
|
||||
max-height: 110px;
|
||||
overflow: auto;
|
||||
margin: 0 14px 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, #000 35%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.qvac-swarm-log-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.qvac-swarm-log-line {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0;
|
||||
transform: translateX(-6px);
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s ease;
|
||||
}
|
||||
|
||||
.qvac-swarm-log-line.is-in {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.qvac-swarm-log-ico {
|
||||
color: hsl(var(--agent-hue, 168) 70% 60%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qvac-swarm-log-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qvac-agent-chip[data-status='start'] {
|
||||
border-color: color-mix(in srgb, #60a5fa 50%, var(--border-color));
|
||||
.qvac-swarm-log-time {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
.qvac-agent-chip[data-status='done'] {
|
||||
border-color: color-mix(in srgb, #34d399 50%, var(--border-color));
|
||||
.qvac-swarm-foot {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 16px 12px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.qvac-agent-chip[data-status='error'] {
|
||||
border-color: color-mix(in srgb, #f87171 55%, var(--border-color));
|
||||
.qvac-swarm-foot-status.is-live::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-primary);
|
||||
box-shadow: 0 0 8px var(--accent-primary);
|
||||
animation: qvac-dot-blink 1s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.qvac-swarm[data-state='complete'] .qvac-swarm-title::after {
|
||||
content: ' · complete';
|
||||
font-weight: 500;
|
||||
opacity: 0.65;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.qvac-swarm[data-state='stopped'] {
|
||||
border-color: color-mix(in srgb, #f87171 40%, var(--border-color));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.qvac-swarm,
|
||||
.qvac-agent-card,
|
||||
.qvac-swarm-orb-core,
|
||||
.qvac-swarm-orb-ring,
|
||||
.qvac-agent-pulse,
|
||||
.qvac-agent-activity-dot,
|
||||
.qvac-swarm-log-line,
|
||||
.qvac-agent-card-glow {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.qvac-agent-card {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.qvac-chart-embeds {
|
||||
|
||||
Reference in New Issue
Block a user