Agent Charts
This commit is contained in:
@@ -327,8 +327,21 @@ const qvacView = createQvacView({
|
|||||||
return bm?.alias || `${String(active.publicKeyHex).slice(0, 12)}…`
|
return bm?.alias || `${String(active.publicKeyHex).slice(0, 12)}…`
|
||||||
},
|
},
|
||||||
getCatalog: () => chartCatalog,
|
getCatalog: () => chartCatalog,
|
||||||
onOpenChart: (chartId, ts) => {
|
onOpenChart: (chartId, ts, chartOpts = {}) => {
|
||||||
showView('charts')
|
showView('charts')
|
||||||
|
if (metricsDashboard.showCharts) {
|
||||||
|
metricsDashboard.showCharts({
|
||||||
|
charts: [chartId],
|
||||||
|
pin: chartOpts.pin !== false,
|
||||||
|
boardOnly: chartOpts.pin !== false,
|
||||||
|
focus: chartId,
|
||||||
|
ts,
|
||||||
|
mode: chartOpts.mode,
|
||||||
|
preset: chartOpts.preset,
|
||||||
|
openFocus: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (ts) metricsDashboard.focusChartAt(chartId, ts)
|
if (ts) metricsDashboard.focusChartAt(chartId, ts)
|
||||||
else {
|
else {
|
||||||
metricsDashboard.scrollToChart?.(chartId)
|
metricsDashboard.scrollToChart?.(chartId)
|
||||||
@@ -336,6 +349,21 @@ const qvacView = createQvacView({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpenView: (view) => showView(view),
|
onOpenView: (view) => showView(view),
|
||||||
|
charts: {
|
||||||
|
showCharts: (o) => {
|
||||||
|
showView('charts')
|
||||||
|
return metricsDashboard.showCharts?.(o) || { ok: false, error: 'unavailable' }
|
||||||
|
},
|
||||||
|
setPinned: (ids, o) => metricsDashboard.setPinned?.(ids, o),
|
||||||
|
setChartMode: (id, mode) => metricsDashboard.setChartMode?.(id, mode),
|
||||||
|
setTimeWindow: (o) => metricsDashboard.setTimeWindow?.(o),
|
||||||
|
setFilter: (q) => metricsDashboard.setFilter?.(q),
|
||||||
|
showRelated: (id) => metricsDashboard.showRelated?.(id),
|
||||||
|
openFocus: (id) => metricsDashboard.openFocus?.(id),
|
||||||
|
scrollToChart: (id) => metricsDashboard.scrollToChart?.(id),
|
||||||
|
correlateAround: (ts, o) => metricsDashboard.correlateAround?.(ts, o),
|
||||||
|
runMetricCorrelations: (o) => metricsDashboard.runMetricCorrelations?.(o),
|
||||||
|
},
|
||||||
log: (msg) => log(msg),
|
log: (msg) => log(msg),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+19
-2
@@ -200,10 +200,27 @@ Tools are **tiered** so small models stay within context:
|
|||||||
|
|
||||||
| Tier | When | Tools |
|
| Tier | When | Tools |
|
||||||
|------|------|-------|
|
|------|------|-------|
|
||||||
| **core** | All tool-enabled profiles | `investigate_host`, `host_snapshot`, `search_charts`, `summarize_chart`, `list_anomalies`, `list_alerts`, `list_processes`, `local_knowledge`, `open_chart`, `open_view` |
|
| **core** | All tool-enabled profiles | Investigation + **live charts**: `show_charts`, `open_chart`, `pin_charts`, `set_chart_type`, `set_time_window`, `filter_charts`, `show_related_ui`, plus `investigate_host`, snapshots, search/summarize, processes, `local_knowledge`, `open_view` |
|
||||||
| **deep** | Recommended / Strong | `hot_metrics`, `related_charts`, `compare_chart_windows`, `summarize_charts`, `query_metric`, `get_weights`, `query_logs`, `fleet_health`, `list_child_peers`, `storage_info`, `agent_health`, `node_info`, `db_info`, `list_contexts`, `get_chart`, `list_jobs`, `get_alert` |
|
| **deep** | Recommended / Strong | `run_correlations`, `hot_metrics`, `related_charts`, `compare_chart_windows`, `summarize_charts`, `query_metric`, `get_weights`, logs, fleet, storage, agent meta, `get_alert` |
|
||||||
| **write** | Operator role + confirm | `silence_alert`, `ack_alert`, `run_job` |
|
| **write** | Operator role + confirm | `silence_alert`, `ack_alert`, `run_job` |
|
||||||
|
|
||||||
|
### Live charts (desktop)
|
||||||
|
|
||||||
|
The model can drive the Charts wall in real time:
|
||||||
|
|
||||||
|
| Tool | Effect |
|
||||||
|
|------|--------|
|
||||||
|
| `show_charts` | Open Charts, pin board, set window/type, focus |
|
||||||
|
| `open_chart` | Focus one chart (optional pin / mode / preset / event `ts`) |
|
||||||
|
| `pin_charts` | Pin or unpin board cards |
|
||||||
|
| `set_chart_type` | `line` · `area` · `stacked` · `bar` · `multibar` · `pie` |
|
||||||
|
| `set_time_window` | Preset or seconds + play/pause |
|
||||||
|
| `filter_charts` | Wall search box |
|
||||||
|
| `show_related_ui` | Related panel for a seed chart |
|
||||||
|
| `run_correlations` | Metric Correlations around a time |
|
||||||
|
|
||||||
|
Chat also embeds **sparklines** for charts the tools touched (click opens the full wall).
|
||||||
|
|
||||||
On context overflow the engine first drops to **core** tools, then drops tool schemas entirely and retries.
|
On context overflow the engine first drops to **core** tools, then drops tool schemas entirely and retries.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|||||||
+152
@@ -1053,6 +1053,150 @@ export function createMetricsDashboard(opts) {
|
|||||||
if (wall) wall.scrollTop = top
|
if (wall) wall.scrollTop = top
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin or unpin one or more charts (agent / automation API).
|
||||||
|
* @param {string|string[]} chartIds
|
||||||
|
* @param {{ pin?: boolean }} [optsIn] pin=true force pin, false force unpin, omit=toggle each
|
||||||
|
*/
|
||||||
|
function setPinned(chartIds, optsIn = {}) {
|
||||||
|
const ids = (Array.isArray(chartIds) ? chartIds : [chartIds])
|
||||||
|
.map(String)
|
||||||
|
.filter(Boolean)
|
||||||
|
const force = optsIn.pin
|
||||||
|
for (const id of ids) {
|
||||||
|
if (force === true) state.pinned.add(id)
|
||||||
|
else if (force === false) state.pinned.delete(id)
|
||||||
|
else if (state.pinned.has(id)) state.pinned.delete(id)
|
||||||
|
else state.pinned.add(id)
|
||||||
|
}
|
||||||
|
persistPrefs({ pinned: [...state.pinned] })
|
||||||
|
const wall = opts.els.wall
|
||||||
|
const top = wall?.scrollTop || 0
|
||||||
|
render()
|
||||||
|
if (wall) wall.scrollTop = top
|
||||||
|
return { ok: true, pinned: [...state.pinned], touched: ids }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set chart visualization mode: line|area|bar|pie|stack.
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} mode
|
||||||
|
*/
|
||||||
|
function setChartMode(id, mode) {
|
||||||
|
const card = state.cards.get(id) || ensureCard(id, opts.getCatalog()?.[id] || {})
|
||||||
|
if (!card) return { ok: false, error: 'unknown chart' }
|
||||||
|
const next = normalizeChartMode(mode)
|
||||||
|
card.mode = next
|
||||||
|
state.chartTypes.set(id, next)
|
||||||
|
persistPrefs({ chartTypes: Object.fromEntries(state.chartTypes) })
|
||||||
|
const typeBtn = state.cardEls.get(id)?.querySelector('.metric-type-btn')
|
||||||
|
if (typeBtn) {
|
||||||
|
typeBtn.textContent = CHART_MODE_LABEL[next] || next
|
||||||
|
typeBtn.title = chartModeHint(next)
|
||||||
|
}
|
||||||
|
paintCard(id)
|
||||||
|
return { ok: true, chart: id, mode: next }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the charts wall by free text (same as search box).
|
||||||
|
* @param {string} q
|
||||||
|
*/
|
||||||
|
function setFilter(q) {
|
||||||
|
state.filter = String(q || '')
|
||||||
|
if (opts.els.search) opts.els.search.value = state.filter
|
||||||
|
render()
|
||||||
|
return { ok: true, filter: state.filter }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time window control for the wall.
|
||||||
|
* @param {{
|
||||||
|
* preset?: string,
|
||||||
|
* seconds?: number,
|
||||||
|
* endOffset?: number,
|
||||||
|
* playing?: boolean,
|
||||||
|
* }} optsIn
|
||||||
|
*/
|
||||||
|
function setTimeWindow(optsIn = {}) {
|
||||||
|
if (optsIn.preset) setPreset(String(optsIn.preset))
|
||||||
|
else if (optsIn.seconds != null) {
|
||||||
|
setWindow(Number(optsIn.seconds), optsIn.endOffset != null ? Number(optsIn.endOffset) : state.endOffset)
|
||||||
|
}
|
||||||
|
if (optsIn.playing != null) setPlaying(Boolean(optsIn.playing))
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
afterSeconds: state.afterSeconds,
|
||||||
|
endOffset: state.endOffset,
|
||||||
|
playing: state.playing,
|
||||||
|
preset: state.preset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level “show these charts now” for QVAC / automation.
|
||||||
|
* Opens board, pins charts, applies window/type, optional focus.
|
||||||
|
* @param {{
|
||||||
|
* charts?: string[],
|
||||||
|
* pin?: boolean,
|
||||||
|
* boardOnly?: boolean,
|
||||||
|
* focus?: string,
|
||||||
|
* ts?: number,
|
||||||
|
* preset?: string,
|
||||||
|
* seconds?: number,
|
||||||
|
* mode?: string,
|
||||||
|
* filter?: string,
|
||||||
|
* related?: boolean,
|
||||||
|
* playing?: boolean,
|
||||||
|
* }} optsIn
|
||||||
|
*/
|
||||||
|
function showCharts(optsIn = {}) {
|
||||||
|
const charts = (Array.isArray(optsIn.charts) ? optsIn.charts : [])
|
||||||
|
.map(String)
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 24)
|
||||||
|
if (optsIn.filter != null) setFilter(optsIn.filter)
|
||||||
|
if (optsIn.preset || optsIn.seconds != null) {
|
||||||
|
setTimeWindow({
|
||||||
|
preset: optsIn.preset,
|
||||||
|
seconds: optsIn.seconds,
|
||||||
|
playing: optsIn.playing,
|
||||||
|
})
|
||||||
|
} else if (optsIn.playing != null) {
|
||||||
|
setPlaying(Boolean(optsIn.playing))
|
||||||
|
}
|
||||||
|
if (optsIn.pin !== false && charts.length) {
|
||||||
|
setPinned(charts, { pin: true })
|
||||||
|
}
|
||||||
|
if (optsIn.boardOnly != null) setBoardOnly(Boolean(optsIn.boardOnly))
|
||||||
|
else if (charts.length >= 1 && optsIn.pin !== false) setBoardOnly(true)
|
||||||
|
|
||||||
|
if (optsIn.mode && charts.length) {
|
||||||
|
for (const id of charts) setChartMode(id, optsIn.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusId = optsIn.focus || charts[0]
|
||||||
|
if (focusId) {
|
||||||
|
if (optsIn.ts) focusChartAt(focusId, optsIn.ts)
|
||||||
|
else {
|
||||||
|
scrollToChart(focusId)
|
||||||
|
if (optsIn.openFocus !== false && charts.length === 1) openFocus(focusId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (optsIn.related && focusId) {
|
||||||
|
showRelated(focusId).catch(() => {})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
charts,
|
||||||
|
pinned: [...state.pinned],
|
||||||
|
boardOnly: state.boardOnly,
|
||||||
|
focus: focusId || null,
|
||||||
|
afterSeconds: state.afterSeconds,
|
||||||
|
playing: state.playing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function showRelated(seedId) {
|
async function showRelated(seedId) {
|
||||||
state.relatedSeed = seedId
|
state.relatedSeed = seedId
|
||||||
/** @type {Array<{ id: string, weight: number }>|undefined} */
|
/** @type {Array<{ id: string, weight: number }>|undefined} */
|
||||||
@@ -2258,6 +2402,14 @@ export function createMetricsDashboard(opts) {
|
|||||||
correlateAround,
|
correlateAround,
|
||||||
refreshRetention,
|
refreshRetention,
|
||||||
getState: () => state,
|
getState: () => state,
|
||||||
|
// Agent / automation surface
|
||||||
|
setPinned,
|
||||||
|
setChartMode,
|
||||||
|
setFilter,
|
||||||
|
setTimeWindow,
|
||||||
|
showCharts,
|
||||||
|
togglePin,
|
||||||
|
cycleChartType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Inline sparkline embeds for QVAC chat from tool results / agent chart actions.
|
||||||
|
*/
|
||||||
|
import { drawChart, seriesColor } from '../charts.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract chart ids from tool log for embedding.
|
||||||
|
* @param {Array<{ name: string, args?: object, result?: any }>} toolLog
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function chartIdsFromToolLog(toolLog) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
const ids = []
|
||||||
|
const push = (id) => {
|
||||||
|
const s = String(id || '').trim()
|
||||||
|
if (s && !ids.includes(s)) ids.push(s)
|
||||||
|
}
|
||||||
|
for (const t of toolLog || []) {
|
||||||
|
const n = t.name || ''
|
||||||
|
const a = t.args || {}
|
||||||
|
const r = t.result || {}
|
||||||
|
if (
|
||||||
|
n === 'show_charts' ||
|
||||||
|
n === 'open_chart' ||
|
||||||
|
n === 'pin_charts' ||
|
||||||
|
n === 'set_chart_type' ||
|
||||||
|
n === 'summarize_chart' ||
|
||||||
|
n === 'query_metric' ||
|
||||||
|
n === 'compare_chart_windows'
|
||||||
|
) {
|
||||||
|
if (a.chart) push(a.chart)
|
||||||
|
if (a.id) push(a.id)
|
||||||
|
if (a.charts) {
|
||||||
|
String(a.charts)
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(push)
|
||||||
|
}
|
||||||
|
if (Array.isArray(r.charts)) r.charts.forEach(push)
|
||||||
|
if (r.chart) push(r.chart)
|
||||||
|
if (r.focus) push(r.focus)
|
||||||
|
}
|
||||||
|
if (n === 'hot_metrics' && Array.isArray(r.results)) {
|
||||||
|
for (const row of r.results.slice(0, 4)) push(row.chart)
|
||||||
|
}
|
||||||
|
if (n === 'search_charts' && Array.isArray(r.results)) {
|
||||||
|
for (const row of r.results.slice(0, 3)) push(row.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids.slice(0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount sparklines under a message element.
|
||||||
|
* @param {HTMLElement} msgEl
|
||||||
|
* @param {string[]} chartIds
|
||||||
|
* @param {{
|
||||||
|
* request: (method: string, args?: object) => Promise<any>,
|
||||||
|
* catalog?: Record<string, object>,
|
||||||
|
* onOpen?: (id: string) => void,
|
||||||
|
* }} deps
|
||||||
|
*/
|
||||||
|
export async function mountChartEmbeds(msgEl, chartIds, deps) {
|
||||||
|
if (!msgEl || !chartIds?.length || !deps?.request) return
|
||||||
|
let host = msgEl.querySelector('.qvac-chart-embeds')
|
||||||
|
if (host) host.remove()
|
||||||
|
host = document.createElement('div')
|
||||||
|
host.className = 'qvac-chart-embeds'
|
||||||
|
msgEl.appendChild(host)
|
||||||
|
|
||||||
|
for (const id of chartIds) {
|
||||||
|
const card = document.createElement('button')
|
||||||
|
card.type = 'button'
|
||||||
|
card.className = 'qvac-chart-embed'
|
||||||
|
card.title = `Open ${id} on Charts wall`
|
||||||
|
const title = deps.catalog?.[id]?.title || id
|
||||||
|
card.innerHTML = `<header><span class="qvac-chart-embed-id">${escapeHtml(id)}</span><span class="muted">${escapeHtml(title)}</span></header><canvas width="320" height="72"></canvas><footer class="muted">Loading…</footer>`
|
||||||
|
card.addEventListener('click', () => deps.onOpen?.(id))
|
||||||
|
host.appendChild(card)
|
||||||
|
const canvas = card.querySelector('canvas')
|
||||||
|
const footer = card.querySelector('footer')
|
||||||
|
try {
|
||||||
|
const q = await deps.request('queryData', {
|
||||||
|
chart: id,
|
||||||
|
after: -180,
|
||||||
|
points: 90,
|
||||||
|
group: 'average',
|
||||||
|
})
|
||||||
|
const labels = (q.labels || []).filter((l) => l && l !== 'time')
|
||||||
|
const data = Array.isArray(q.data) ? q.data : []
|
||||||
|
if (!labels.length || !data.length) {
|
||||||
|
if (footer) footer.textContent = 'No points'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Prefer up to 3 interesting dims
|
||||||
|
/** @type {Array<{ values: number[], color: string, label: string }>} */
|
||||||
|
const lines = []
|
||||||
|
for (let di = 0; di < Math.min(3, labels.length); di++) {
|
||||||
|
const vals = data
|
||||||
|
.map((row) => Number(row[di + 1]))
|
||||||
|
.filter((v) => Number.isFinite(v))
|
||||||
|
if (vals.length < 2) continue
|
||||||
|
// Pad to full length with nulls as 0 for sparkline simplicity
|
||||||
|
const full = data.map((row) => {
|
||||||
|
const v = Number(row[di + 1])
|
||||||
|
return Number.isFinite(v) ? v : 0
|
||||||
|
})
|
||||||
|
lines.push({
|
||||||
|
values: full,
|
||||||
|
color: seriesColor(di),
|
||||||
|
label: labels[di],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!lines.length) {
|
||||||
|
if (footer) footer.textContent = 'No numeric dims'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (canvas) {
|
||||||
|
drawChart(canvas, lines, {
|
||||||
|
maxPoints: 90,
|
||||||
|
showYAxis: false,
|
||||||
|
padLeft: 4,
|
||||||
|
mode: 'area',
|
||||||
|
emptyMessage: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const last = lines[0].values[lines[0].values.length - 1]
|
||||||
|
if (footer) {
|
||||||
|
footer.textContent = `${lines.map((l) => l.label).join(', ')} · last ${formatNum(last)}`
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (footer) footer.textContent = err?.message || 'query failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNum(v) {
|
||||||
|
const n = Number(v)
|
||||||
|
if (!Number.isFinite(n)) return '—'
|
||||||
|
if (Math.abs(n) >= 100) return n.toFixed(0)
|
||||||
|
if (Math.abs(n) >= 10) return n.toFixed(1)
|
||||||
|
return n.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
+26
-1
@@ -11,6 +11,7 @@ import {
|
|||||||
runSubAgents,
|
runSubAgents,
|
||||||
synthesizeFromAgents,
|
synthesizeFromAgents,
|
||||||
} from './agents.js'
|
} from './agents.js'
|
||||||
|
import { chartIdsFromToolLog, mountChartEmbeds } from './chart-embed.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
@@ -39,8 +40,9 @@ import {
|
|||||||
* saveSettings: (patch: object) => void,
|
* saveSettings: (patch: object) => void,
|
||||||
* getPeerLabel: () => string,
|
* getPeerLabel: () => string,
|
||||||
* getCatalog?: () => Record<string, object>,
|
* getCatalog?: () => Record<string, object>,
|
||||||
* onOpenChart?: (id: string, ts?: number) => void,
|
* onOpenChart?: (id: string, ts?: number, opts?: object) => void,
|
||||||
* onOpenView?: (view: string) => void,
|
* onOpenView?: (view: string) => void,
|
||||||
|
* charts?: object,
|
||||||
* log?: (msg: string) => void,
|
* log?: (msg: string) => void,
|
||||||
* }} opts
|
* }} opts
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +62,7 @@ export function createQvacView(opts) {
|
|||||||
getCatalog: () => opts.getCatalog?.() || {},
|
getCatalog: () => opts.getCatalog?.() || {},
|
||||||
onOpenChart: opts.onOpenChart,
|
onOpenChart: opts.onOpenChart,
|
||||||
onOpenView: opts.onOpenView,
|
onOpenView: opts.onOpenView,
|
||||||
|
charts: opts.charts,
|
||||||
confirmAction: (msg) => {
|
confirmAction: (msg) => {
|
||||||
try {
|
try {
|
||||||
return typeof confirm === 'function' ? confirm(msg) : true
|
return typeof confirm === 'function' ? confirm(msg) : true
|
||||||
@@ -892,6 +895,28 @@ export function createQvacView(opts) {
|
|||||||
if (chips) chips.remove()
|
if (chips) chips.remove()
|
||||||
assistantUi.div.appendChild(renderToolChips(toolLog))
|
assistantUi.div.appendChild(renderToolChips(toolLog))
|
||||||
}
|
}
|
||||||
|
// Live sparklines under the answer for chart-related tools
|
||||||
|
const embedIds = chartIdsFromToolLog(toolLog)
|
||||||
|
if (embedIds.length && assistantUi?.div && opts.manager?.request) {
|
||||||
|
mountChartEmbeds(assistantUi.div, embedIds, {
|
||||||
|
request: (m, a) => opts.manager.request(m, a || {}),
|
||||||
|
catalog: opts.getCatalog?.() || {},
|
||||||
|
onOpen: (id) => {
|
||||||
|
opts.onOpenView?.('charts')
|
||||||
|
if (opts.charts?.showCharts) {
|
||||||
|
opts.charts.showCharts({
|
||||||
|
charts: [id],
|
||||||
|
pin: true,
|
||||||
|
boardOnly: true,
|
||||||
|
focus: id,
|
||||||
|
openFocus: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
opts.onOpenChart?.(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
setStatus(result.mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
setStatus(result.mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err?.message || String(err)
|
const msg = err?.message || String(err)
|
||||||
|
|||||||
+16
-9
@@ -28,7 +28,7 @@ export function buildSystemPrompt(ctx = {}) {
|
|||||||
'- Cite chart ids (e.g. system.cpu) when discussing metrics.',
|
'- Cite chart ids (e.g. system.cpu) when discussing metrics.',
|
||||||
'- You are read-only unless the user explicitly asks for an operator action and their role allows it.',
|
'- You are read-only unless the user explicitly asks for an operator action and their role allows it.',
|
||||||
'- Do not claim cloud access; all inference is local via QVAC.',
|
'- Do not claim cloud access; all inference is local via QVAC.',
|
||||||
'- Prefer open_chart / open_view when the user asks to see something in the UI.',
|
'- Prefer show_charts / open_chart / open_view when the user asks to see graphs or the UI.',
|
||||||
multi
|
multi
|
||||||
? '- Multi-agent mode: specialist investigators may already have collected a briefing. Trust those facts; only call extra tools for gaps.'
|
? '- Multi-agent mode: specialist investigators may already have collected a briefing. Trust those facts; only call extra tools for gaps.'
|
||||||
: '',
|
: '',
|
||||||
@@ -36,12 +36,18 @@ export function buildSystemPrompt(ctx = {}) {
|
|||||||
'Tool playbook (use these exact tool names and chart ids):',
|
'Tool playbook (use these exact tool names and chart ids):',
|
||||||
'- Host health / "what\'s wrong" / diagnose / investigate → investigate_host FIRST (one-shot findings).',
|
'- Host health / "what\'s wrong" / diagnose / investigate → investigate_host FIRST (one-shot findings).',
|
||||||
' Lighter alternative: host_snapshot. Do NOT use md.health for general health.',
|
' Lighter alternative: host_snapshot. Do NOT use md.health for general health.',
|
||||||
'- Spikes / unusual activity → hot_metrics, then summarize_chart or compare_chart_windows on top hits.',
|
'- "Show me a graph/chart/plot" → search_charts if needed, then show_charts with concrete chart ids.',
|
||||||
'- CPU high / load → investigate_host or host_snapshot + summarize_chart chart=system.cpu + list_processes.',
|
' show_charts pins to the board, sets window (preset=5m|1h|…), optional mode=line|area|bar|pie, focus.',
|
||||||
'- Memory → summarize_chart chart=system.ram (and search_charts q=mem if needed).',
|
' Example: show_charts charts=system.cpu,system.ram preset=15m mode=area boardOnly=true.',
|
||||||
'- Disk / IO / docker / redis / nginx / postgres → search_charts, then summarize_chart on a concrete id.',
|
'- Single chart focus → open_chart chart=system.cpu (optional pin, mode, preset, ts).',
|
||||||
|
'- Pin/unpin board → pin_charts; change type → set_chart_type; window → set_time_window; search wall → filter_charts.',
|
||||||
|
'- Related wall panel → show_related_ui; Metric Correlations UI → run_correlations.',
|
||||||
|
'- Spikes / unusual activity → hot_metrics, then show_charts on top hits + summarize_chart.',
|
||||||
|
'- CPU high / load → investigate_host + show_charts charts=system.cpu,system.load + list_processes.',
|
||||||
|
'- Memory → show_charts charts=system.ram + summarize_chart chart=system.ram.',
|
||||||
|
'- Disk / IO / docker / redis / nginx / postgres → search_charts, then show_charts on concrete ids.',
|
||||||
'- Follow-up on one chart → related_charts, get_weights, compare_chart_windows, summarize_charts (batch).',
|
'- Follow-up on one chart → related_charts, get_weights, compare_chart_windows, summarize_charts (batch).',
|
||||||
'- Alerts / anomalies → list_anomalies and/or list_alerts; get_alert for one id.',
|
'- Alerts / anomalies → list_anomalies and/or list_alerts; get_alert for one id; open_chart with ts on the chart.',
|
||||||
'- Fleet → fleet_health + list_child_peers.',
|
'- Fleet → fleet_health + list_child_peers.',
|
||||||
'- Storage / retention / prune questions → storage_info (+ local_knowledge for product how-to).',
|
'- Storage / retention / prune questions → storage_info (+ local_knowledge for product how-to).',
|
||||||
'- Logs → query_logs source=anomaly (or journal/audit when role allows).',
|
'- Logs → query_logs source=anomaly (or journal/audit when role allows).',
|
||||||
@@ -51,6 +57,7 @@ export function buildSystemPrompt(ctx = {}) {
|
|||||||
'- NEVER use chart id "md.health" for general host health — that is MD RAID only and is often empty.',
|
'- NEVER use chart id "md.health" for general host health — that is MD RAID only and is often empty.',
|
||||||
'- Prefer chart ids that appear in tool results. If summarize_chart returns points=0, try investigate_host or another chart.',
|
'- Prefer chart ids that appear in tool results. If summarize_chart returns points=0, try investigate_host or another chart.',
|
||||||
'- After tools return, give a clear human summary with numbers; do not only restate empty charts.',
|
'- After tools return, give a clear human summary with numbers; do not only restate empty charts.',
|
||||||
|
'- When you show charts, name the chart ids you opened so the operator can find them.',
|
||||||
'',
|
'',
|
||||||
`Session: agent=${peer} (${conn}), role=${role}${ctx.hostname ? `, host hints may appear in tool results` : ''}${multi ? ', multi-agent=on' : ''}.`,
|
`Session: agent=${peer} (${conn}), role=${role}${ctx.hostname ? `, host hints may appear in tool results` : ''}${multi ? ', multi-agent=on' : ''}.`,
|
||||||
]
|
]
|
||||||
@@ -61,12 +68,12 @@ export function buildSystemPrompt(ctx = {}) {
|
|||||||
export const SAMPLE_PROMPTS = [
|
export const SAMPLE_PROMPTS = [
|
||||||
'Summarize host health right now',
|
'Summarize host health right now',
|
||||||
"What's wrong — investigate this host",
|
"What's wrong — investigate this host",
|
||||||
|
'Show me live graphs for CPU and RAM (15m)',
|
||||||
|
'Plot disk / io charts as an area board',
|
||||||
'Why might CPU be high?',
|
'Why might CPU be high?',
|
||||||
'Show hot / spiking metrics',
|
'Show hot / spiking metrics and open them',
|
||||||
'List charts related to disk or io',
|
|
||||||
'Any open alerts or anomalies?',
|
'Any open alerts or anomalies?',
|
||||||
'Top processes by CPU if available',
|
'Top processes by CPU if available',
|
||||||
'Run a multi-agent investigation of this host',
|
'Run a multi-agent investigation of this host',
|
||||||
'How do Charts time presets work?',
|
'How do Charts time presets work?',
|
||||||
'What is retention / warm history?',
|
|
||||||
]
|
]
|
||||||
|
|||||||
+290
-7
@@ -129,15 +129,163 @@ export const TOOL_DEFS = [
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
name: 'open_chart',
|
name: 'open_chart',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description: 'Navigate the desktop UI to a chart (optional pause near timestamp ms).',
|
description:
|
||||||
|
'Open one chart on the Charts wall (scroll + focus). Prefer show_charts when pinning multiple or changing window/type.',
|
||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
chart: { type: 'string', description: 'Chart id' },
|
chart: { type: 'string', description: 'Chart id' },
|
||||||
ts: { type: 'number', description: 'Event time ms' },
|
ts: { type: 'number', description: 'Event time ms (pauses near event)' },
|
||||||
|
pin: { type: 'boolean', description: 'Pin chart to board (default true)' },
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Chart type: line|area|stacked|bar|multibar|pie',
|
||||||
|
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
||||||
|
},
|
||||||
|
preset: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Time preset e.g. 5m|15m|1h|6h|24h',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
['chart']
|
['chart']
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'show_charts',
|
||||||
|
tier: 'core',
|
||||||
|
description:
|
||||||
|
'PRIMARY live chart tool. Opens Charts view, pins charts to the board, sets time window/type, optional focus and related panel. Use for "show me graphs", "plot CPU", multi-chart boards.',
|
||||||
|
parameters: params(
|
||||||
|
{
|
||||||
|
charts: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Comma/space separated chart ids (max 24), e.g. system.cpu,system.ram',
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Pin charts to board (default true)',
|
||||||
|
},
|
||||||
|
boardOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Show only pinned board (default true when pinning)',
|
||||||
|
},
|
||||||
|
focus: { type: 'string', description: 'Chart id to focus/expand' },
|
||||||
|
ts: { type: 'number', description: 'Event time ms for focus' },
|
||||||
|
preset: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Time preset: 5m|15m|30m|1h|3h|6h|12h|24h',
|
||||||
|
},
|
||||||
|
seconds: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Custom lookback window seconds (alternative to preset)',
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Apply chart type to all listed charts',
|
||||||
|
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Charts wall search filter text',
|
||||||
|
},
|
||||||
|
related: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Open related-charts panel for focus chart',
|
||||||
|
},
|
||||||
|
playing: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Live play (true) or pause (false)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['charts']
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'pin_charts',
|
||||||
|
tier: 'core',
|
||||||
|
description: 'Pin or unpin charts on the Charts board without changing focus.',
|
||||||
|
parameters: params(
|
||||||
|
{
|
||||||
|
charts: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Comma/space separated chart ids',
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'true=pin (default), false=unpin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['charts']
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'set_chart_type',
|
||||||
|
tier: 'core',
|
||||||
|
description: 'Change visualization for a chart: line, area, stacked, bar, multibar, pie.',
|
||||||
|
parameters: params(
|
||||||
|
{
|
||||||
|
chart: { type: 'string', description: 'Chart id' },
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'line|area|stacked|bar|multibar|pie',
|
||||||
|
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['chart', 'mode']
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'set_time_window',
|
||||||
|
tier: 'core',
|
||||||
|
description: 'Set Charts wall time window (preset or seconds) and play/pause live.',
|
||||||
|
parameters: params({
|
||||||
|
preset: {
|
||||||
|
type: 'string',
|
||||||
|
description: '5m|15m|30m|1h|3h|6h|12h|24h',
|
||||||
|
},
|
||||||
|
seconds: { type: 'number', description: 'Lookback seconds' },
|
||||||
|
playing: { type: 'boolean', description: 'Live updates on/off' },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'filter_charts',
|
||||||
|
tier: 'core',
|
||||||
|
description: 'Filter the Charts wall by free text (same as the search box).',
|
||||||
|
parameters: params(
|
||||||
|
{
|
||||||
|
q: { type: 'string', description: 'Filter query (empty clears)' },
|
||||||
|
},
|
||||||
|
['q']
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'show_related_ui',
|
||||||
|
tier: 'core',
|
||||||
|
description: 'Open the Related charts panel for a seed chart on the Charts wall.',
|
||||||
|
parameters: params(
|
||||||
|
{
|
||||||
|
chart: { type: 'string', description: 'Seed chart id' },
|
||||||
|
},
|
||||||
|
['chart']
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'run_correlations',
|
||||||
|
tier: 'deep',
|
||||||
|
description:
|
||||||
|
'Run Metric Correlations around a chart or timestamp and show results on the Charts wall.',
|
||||||
|
parameters: params({
|
||||||
|
chart: { type: 'string', description: 'Optional seed chart id' },
|
||||||
|
ts: { type: 'number', description: 'Center timestamp ms (default now)' },
|
||||||
|
window: { type: 'number', description: 'Highlight window seconds' },
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'function',
|
type: 'function',
|
||||||
name: 'open_view',
|
name: 'open_view',
|
||||||
@@ -423,8 +571,19 @@ export const TOOL_DEFS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
/** Tools that work without an agent connection. */
|
/** Tools that work without an agent connection (desktop UI / local RAG). */
|
||||||
const LOCAL_TOOLS = new Set(['open_view', 'open_chart', 'local_knowledge'])
|
const LOCAL_TOOLS = new Set([
|
||||||
|
'open_view',
|
||||||
|
'open_chart',
|
||||||
|
'show_charts',
|
||||||
|
'pin_charts',
|
||||||
|
'set_chart_type',
|
||||||
|
'set_time_window',
|
||||||
|
'filter_charts',
|
||||||
|
'show_related_ui',
|
||||||
|
'run_correlations',
|
||||||
|
'local_knowledge',
|
||||||
|
])
|
||||||
|
|
||||||
/** Operator write tools. */
|
/** Operator write tools. */
|
||||||
const WRITE_TOOLS = new Set(
|
const WRITE_TOOLS = new Set(
|
||||||
@@ -473,12 +632,32 @@ function wireDefs(defs) {
|
|||||||
* getRole: () => string,
|
* getRole: () => string,
|
||||||
* isConnected: () => boolean,
|
* isConnected: () => boolean,
|
||||||
* getCatalog?: () => Record<string, object>,
|
* getCatalog?: () => Record<string, object>,
|
||||||
* onOpenChart?: (chartId: string, ts?: number) => void,
|
* onOpenChart?: (chartId: string, ts?: number, opts?: object) => void,
|
||||||
* onOpenView?: (view: string) => void,
|
* onOpenView?: (view: string) => void,
|
||||||
|
* charts?: {
|
||||||
|
* showCharts?: (opts: object) => object,
|
||||||
|
* setPinned?: (ids: string|string[], opts?: object) => object,
|
||||||
|
* setChartMode?: (id: string, mode: string) => object,
|
||||||
|
* setTimeWindow?: (opts: object) => object,
|
||||||
|
* setFilter?: (q: string) => object,
|
||||||
|
* showRelated?: (id: string) => Promise<void>|void,
|
||||||
|
* openFocus?: (id: string) => void,
|
||||||
|
* correlateAround?: (ts: number, opts?: object) => void,
|
||||||
|
* runMetricCorrelations?: (opts?: object) => Promise<any>,
|
||||||
|
* scrollToChart?: (id: string) => void,
|
||||||
|
* },
|
||||||
* confirmAction?: (message: string) => boolean|Promise<boolean>,
|
* confirmAction?: (message: string) => boolean|Promise<boolean>,
|
||||||
* }} deps
|
* }} deps
|
||||||
*/
|
*/
|
||||||
export function createToolRunner(deps) {
|
export function createToolRunner(deps) {
|
||||||
|
function parseChartList(raw) {
|
||||||
|
if (Array.isArray(raw)) return raw.map(String).filter(Boolean)
|
||||||
|
return String(raw || '')
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {object} args
|
* @param {object} args
|
||||||
@@ -494,6 +673,7 @@ export function createToolRunner(deps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const req = (m, a) => deps.manager.request(m, a || {})
|
const req = (m, a) => deps.manager.request(m, a || {})
|
||||||
|
const chartsApi = deps.charts || {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@@ -616,8 +796,111 @@ export function createToolRunner(deps) {
|
|||||||
return { context: ctx || 'No matching local knowledge.', q: args.q }
|
return { context: ctx || 'No matching local knowledge.', q: args.q }
|
||||||
}
|
}
|
||||||
case 'open_chart': {
|
case 'open_chart': {
|
||||||
deps.onOpenChart?.(String(args.chart), args.ts)
|
const chart = String(args.chart || args.id || '')
|
||||||
return { ok: true, opened: args.chart }
|
if (!chart) return { error: 'chart required' }
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
if (chartsApi.showCharts) {
|
||||||
|
return chartsApi.showCharts({
|
||||||
|
charts: [chart],
|
||||||
|
pin: args.pin !== false,
|
||||||
|
boardOnly: args.pin !== false,
|
||||||
|
focus: chart,
|
||||||
|
ts: args.ts,
|
||||||
|
mode: args.mode,
|
||||||
|
preset: args.preset,
|
||||||
|
openFocus: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
deps.onOpenChart?.(chart, args.ts, {
|
||||||
|
pin: args.pin !== false,
|
||||||
|
mode: args.mode,
|
||||||
|
preset: args.preset,
|
||||||
|
})
|
||||||
|
return { ok: true, opened: chart }
|
||||||
|
}
|
||||||
|
case 'show_charts': {
|
||||||
|
const list = parseChartList(args.charts || args.chart || args.ids)
|
||||||
|
if (!list.length) return { error: 'charts required' }
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
if (!chartsApi.showCharts) {
|
||||||
|
// Fallback: open first chart only
|
||||||
|
deps.onOpenChart?.(list[0], args.ts)
|
||||||
|
return { ok: true, charts: list, partial: true }
|
||||||
|
}
|
||||||
|
return chartsApi.showCharts({
|
||||||
|
charts: list,
|
||||||
|
pin: args.pin !== false,
|
||||||
|
boardOnly: args.boardOnly != null ? Boolean(args.boardOnly) : args.pin !== false,
|
||||||
|
focus: args.focus || list[0],
|
||||||
|
ts: args.ts,
|
||||||
|
preset: args.preset,
|
||||||
|
seconds: args.seconds,
|
||||||
|
mode: args.mode,
|
||||||
|
filter: args.filter,
|
||||||
|
related: Boolean(args.related),
|
||||||
|
playing: args.playing,
|
||||||
|
openFocus: list.length === 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case 'pin_charts': {
|
||||||
|
const list = parseChartList(args.charts || args.chart)
|
||||||
|
if (!list.length) return { error: 'charts required' }
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
if (!chartsApi.setPinned) return { error: 'charts API unavailable' }
|
||||||
|
return chartsApi.setPinned(list, {
|
||||||
|
pin: args.pin !== false && args.pin !== 'false',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case 'set_chart_type': {
|
||||||
|
const chart = String(args.chart || args.id || '')
|
||||||
|
const mode = String(args.mode || args.type || 'line')
|
||||||
|
if (!chart) return { error: 'chart required' }
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
if (!chartsApi.setChartMode) return { error: 'charts API unavailable' }
|
||||||
|
return chartsApi.setChartMode(chart, mode)
|
||||||
|
}
|
||||||
|
case 'set_time_window': {
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
if (!chartsApi.setTimeWindow) return { error: 'charts API unavailable' }
|
||||||
|
return chartsApi.setTimeWindow({
|
||||||
|
preset: args.preset,
|
||||||
|
seconds: args.seconds ?? args.after,
|
||||||
|
playing: args.playing,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case 'filter_charts': {
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
if (!chartsApi.setFilter) return { error: 'charts API unavailable' }
|
||||||
|
return chartsApi.setFilter(args.q != null ? args.q : args.filter || '')
|
||||||
|
}
|
||||||
|
case 'show_related_ui': {
|
||||||
|
const chart = String(args.chart || args.id || '')
|
||||||
|
if (!chart) return { error: 'chart required' }
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
chartsApi.scrollToChart?.(chart)
|
||||||
|
await chartsApi.showRelated?.(chart)
|
||||||
|
return { ok: true, chart, related: true }
|
||||||
|
}
|
||||||
|
case 'run_correlations': {
|
||||||
|
deps.onOpenView?.('charts')
|
||||||
|
const ts =
|
||||||
|
args.ts != null ? Number(args.ts) : Date.now()
|
||||||
|
if (chartsApi.correlateAround) {
|
||||||
|
chartsApi.correlateAround(ts, {
|
||||||
|
chart: args.chart,
|
||||||
|
window: args.window,
|
||||||
|
})
|
||||||
|
return { ok: true, ts, chart: args.chart || null }
|
||||||
|
}
|
||||||
|
if (chartsApi.runMetricCorrelations) {
|
||||||
|
const res = await chartsApi.runMetricCorrelations({
|
||||||
|
chart: args.chart,
|
||||||
|
ts,
|
||||||
|
window: args.window,
|
||||||
|
})
|
||||||
|
return { ok: true, ...(res || {}) }
|
||||||
|
}
|
||||||
|
return { error: 'correlations API unavailable' }
|
||||||
}
|
}
|
||||||
case 'open_view': {
|
case 'open_view': {
|
||||||
deps.onOpenView?.(String(args.view || 'overview'))
|
deps.onOpenView?.(String(args.view || 'overview'))
|
||||||
|
|||||||
@@ -3757,6 +3757,57 @@ html[data-theme='light'] .proc-detail-cmd {
|
|||||||
border-color: color-mix(in srgb, #f87171 55%, var(--border-color));
|
border-color: color-mix(in srgb, #f87171 55%, var(--border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embeds {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embed:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-primary) 45%, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embed header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embed-id {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embed canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: color-mix(in srgb, var(--bg-primary, #0b1020) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qvac-chart-embed footer {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.qvac-model-chip {
|
.qvac-model-chip {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
Reference in New Issue
Block a user