Ask Mode and No More Auto Driven AI Confirm
This commit is contained in:
@@ -365,7 +365,8 @@ const qvacView = createQvacView({
|
|||||||
},
|
},
|
||||||
getCatalog: () => chartCatalog,
|
getCatalog: () => chartCatalog,
|
||||||
onOpenChart: (chartId, ts, chartOpts = {}) => {
|
onOpenChart: (chartId, ts, chartOpts = {}) => {
|
||||||
showView('charts')
|
// Tab switch is owned by tools.maybeNavigate — only prepare chart state here
|
||||||
|
if (chartOpts.navigate) showView('charts')
|
||||||
if (metricsDashboard.showCharts) {
|
if (metricsDashboard.showCharts) {
|
||||||
metricsDashboard.showCharts({
|
metricsDashboard.showCharts({
|
||||||
charts: [chartId],
|
charts: [chartId],
|
||||||
@@ -375,22 +376,22 @@ const qvacView = createQvacView({
|
|||||||
ts,
|
ts,
|
||||||
mode: chartOpts.mode,
|
mode: chartOpts.mode,
|
||||||
preset: chartOpts.preset,
|
preset: chartOpts.preset,
|
||||||
openFocus: true,
|
openFocus: Boolean(chartOpts.navigate),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (ts) metricsDashboard.focusChartAt(chartId, ts)
|
if (ts) metricsDashboard.focusChartAt(chartId, ts)
|
||||||
else {
|
else {
|
||||||
metricsDashboard.scrollToChart?.(chartId)
|
metricsDashboard.scrollToChart?.(chartId)
|
||||||
metricsDashboard.openFocus?.(chartId)
|
if (chartOpts.navigate) metricsDashboard.openFocus?.(chartId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpenView: (view) => showView(view),
|
onOpenView: (view) => showView(view),
|
||||||
|
getAutoNavigate: () => settings.qvacAutoNavigate || 'ask',
|
||||||
charts: {
|
charts: {
|
||||||
showCharts: (o) => {
|
// Do not showView here — tools call onOpenView only when navigation is allowed
|
||||||
showView('charts')
|
showCharts: (o) =>
|
||||||
return metricsDashboard.showCharts?.(o) || { ok: false, error: 'unavailable' }
|
metricsDashboard.showCharts?.(o) || { ok: false, error: 'unavailable' },
|
||||||
},
|
|
||||||
setPinned: (ids, o) => metricsDashboard.setPinned?.(ids, o),
|
setPinned: (ids, o) => metricsDashboard.setPinned?.(ids, o),
|
||||||
setChartMode: (id, mode) => metricsDashboard.setChartMode?.(id, mode),
|
setChartMode: (id, mode) => metricsDashboard.setChartMode?.(id, mode),
|
||||||
setTimeWindow: (o) => metricsDashboard.setTimeWindow?.(o),
|
setTimeWindow: (o) => metricsDashboard.setTimeWindow?.(o),
|
||||||
@@ -408,11 +409,7 @@ const qvacView = createQvacView({
|
|||||||
remove: (a) => customDashboardView.deleteDashboard(a),
|
remove: (a) => customDashboardView.deleteDashboard(a),
|
||||||
addCharts: (a) => customDashboardView.addDashboardCharts(a),
|
addCharts: (a) => customDashboardView.addDashboardCharts(a),
|
||||||
removeCharts: (a) => customDashboardView.removeDashboardCharts(a),
|
removeCharts: (a) => customDashboardView.removeDashboardCharts(a),
|
||||||
open: (a) => {
|
open: (a) => customDashboardView.openDashboard(a),
|
||||||
const r = customDashboardView.openDashboard(a)
|
|
||||||
showView('dashboard')
|
|
||||||
return r
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
log: (msg) => log(msg),
|
log: (msg) => log(msg),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const SETTINGS_LOCALSTORAGE_KEY = 'peardata.settings.v1'
|
|||||||
* qvacSubAgents?: boolean,
|
* qvacSubAgents?: boolean,
|
||||||
* qvacMaxSubAgents?: number,
|
* qvacMaxSubAgents?: number,
|
||||||
* qvacToolDepth?: 'auto'|'core'|'deep',
|
* qvacToolDepth?: 'auto'|'core'|'deep',
|
||||||
|
* qvacAutoNavigate?: 'off'|'ask'|'on',
|
||||||
* customDashboards?: Array<{
|
* customDashboards?: Array<{
|
||||||
* id: string,
|
* id: string,
|
||||||
* name: string,
|
* name: string,
|
||||||
@@ -83,6 +84,8 @@ export function defaultSettings() {
|
|||||||
qvacSubAgents: true,
|
qvacSubAgents: true,
|
||||||
qvacMaxSubAgents: 3,
|
qvacMaxSubAgents: 3,
|
||||||
qvacToolDepth: 'auto',
|
qvacToolDepth: 'auto',
|
||||||
|
/** off = never switch tabs; ask = confirm each time; on = always when tools request */
|
||||||
|
qvacAutoNavigate: 'ask',
|
||||||
customDashboards: [],
|
customDashboards: [],
|
||||||
activeDashboardId: null,
|
activeDashboardId: null,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,18 @@ With multi-agent **enabled** (default), specialists only run for **broad investi
|
|||||||
(“what’s wrong”, full health check, 3+ domains). Ordinary questions (graphs, one metric,
|
(“what’s wrong”, full health check, 3+ domains). Ordinary questions (graphs, one metric,
|
||||||
alerts list, dashboards) stay **single-agent** — no “Dispatching multi-agent…” flash.
|
alerts list, dashboards) stay **single-agent** — no “Dispatching multi-agent…” flash.
|
||||||
|
|
||||||
|
### Auto-navigate
|
||||||
|
|
||||||
|
Tools may prepare Charts boards / dashboards **without** leaving the QVAC tab.
|
||||||
|
|
||||||
|
| Setting (`qvacAutoNavigate`) | Behavior |
|
||||||
|
|------------------------------|----------|
|
||||||
|
| **off** | Never switch tabs unless tool passes `navigate=true` (user asked to “show/open”) or confirms an `open_view` |
|
||||||
|
| **ask** (default) | Confirm dialog before each tab switch |
|
||||||
|
| **on** | Always switch when a tool requests a view |
|
||||||
|
|
||||||
|
The model is instructed to set `navigate=true` only when the user explicitly wants to see that UI.
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+11
@@ -452,6 +452,17 @@
|
|||||||
Max parallel agents
|
Max parallel agents
|
||||||
<input id="qvac-set-max-agents" type="number" min="1" max="6" step="1" value="3" />
|
<input id="qvac-set-max-agents" type="number" min="1" max="6" step="1" value="3" />
|
||||||
</label>
|
</label>
|
||||||
|
<label class="settings-field">
|
||||||
|
Auto-navigate UI
|
||||||
|
<select id="qvac-set-autonav" title="When tools open Charts/Dashboard/other views">
|
||||||
|
<option value="off">Off — stay on current tab</option>
|
||||||
|
<option value="ask" selected>Ask each time</option>
|
||||||
|
<option value="on">On — always switch when tools request</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<p class="muted settings-hint">
|
||||||
|
Even when Off, tools can still pin charts / edit dashboards in the background. Tab switches only if you asked (“show me…”) with navigate=true, or you confirm.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="qvac-settings-block qvac-settings-actions">
|
<div class="qvac-settings-block qvac-settings-actions">
|
||||||
<h4>Actions</h4>
|
<h4>Actions</h4>
|
||||||
|
|||||||
+11
-2
@@ -64,11 +64,13 @@ export function createQvacView(opts) {
|
|||||||
onOpenView: opts.onOpenView,
|
onOpenView: opts.onOpenView,
|
||||||
charts: opts.charts,
|
charts: opts.charts,
|
||||||
dashboards: opts.dashboards,
|
dashboards: opts.dashboards,
|
||||||
|
getAutoNavigate: () => settings().qvacAutoNavigate || 'ask',
|
||||||
confirmAction: (msg) => {
|
confirmAction: (msg) => {
|
||||||
try {
|
try {
|
||||||
return typeof confirm === 'function' ? confirm(msg) : true
|
// Must be strict true — Cancel returns false
|
||||||
|
return typeof confirm === 'function' ? confirm(msg) : false
|
||||||
} catch {
|
} catch {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -89,6 +91,7 @@ export function createQvacView(opts) {
|
|||||||
subAgents: settings().qvacSubAgents !== false,
|
subAgents: settings().qvacSubAgents !== false,
|
||||||
maxSubAgents: Number(settings().qvacMaxSubAgents) || 3,
|
maxSubAgents: Number(settings().qvacMaxSubAgents) || 3,
|
||||||
toolDepth: settings().qvacToolDepth || 'auto',
|
toolDepth: settings().qvacToolDepth || 'auto',
|
||||||
|
autoNavigate: settings().qvacAutoNavigate || 'ask',
|
||||||
}),
|
}),
|
||||||
log: opts.log,
|
log: opts.log,
|
||||||
})
|
})
|
||||||
@@ -221,6 +224,7 @@ export function createQvacView(opts) {
|
|||||||
loaded ? `model=${profile.chatModel}` : 'no model loaded',
|
loaded ? `model=${profile.chatModel}` : 'no model loaded',
|
||||||
s.qvacMode ? `mode=${s.qvacMode}` : '',
|
s.qvacMode ? `mode=${s.qvacMode}` : '',
|
||||||
multi ? 'multi-agent=on' : 'multi-agent=off',
|
multi ? 'multi-agent=on' : 'multi-agent=off',
|
||||||
|
`nav=${s.qvacAutoNavigate || 'ask'}`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' · ')
|
.join(' · ')
|
||||||
@@ -237,6 +241,7 @@ export function createQvacView(opts) {
|
|||||||
set('#qvac-set-tool-depth', s.qvacToolDepth || 'auto')
|
set('#qvac-set-tool-depth', s.qvacToolDepth || 'auto')
|
||||||
set('#qvac-set-subagents', s.qvacSubAgents !== false, true)
|
set('#qvac-set-subagents', s.qvacSubAgents !== false, true)
|
||||||
set('#qvac-set-max-agents', s.qvacMaxSubAgents ?? 3)
|
set('#qvac-set-max-agents', s.qvacMaxSubAgents ?? 3)
|
||||||
|
set('#qvac-set-autonav', s.qvacAutoNavigate || 'ask')
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSettingsForm() {
|
function readSettingsForm() {
|
||||||
@@ -249,6 +254,8 @@ export function createQvacView(opts) {
|
|||||||
const subAgents = root.querySelector('#qvac-set-subagents')?.checked !== false
|
const subAgents = root.querySelector('#qvac-set-subagents')?.checked !== false
|
||||||
let maxAgents = Number(root.querySelector('#qvac-set-max-agents')?.value) || 3
|
let maxAgents = Number(root.querySelector('#qvac-set-max-agents')?.value) || 3
|
||||||
maxAgents = Math.min(6, Math.max(1, maxAgents))
|
maxAgents = Math.min(6, Math.max(1, maxAgents))
|
||||||
|
const autoNavRaw = root.querySelector('#qvac-set-autonav')?.value || 'ask'
|
||||||
|
const autoNav = ['off', 'ask', 'on'].includes(autoNavRaw) ? autoNavRaw : 'ask'
|
||||||
selectedProfile = profile
|
selectedProfile = profile
|
||||||
persist({
|
persist({
|
||||||
qvacProfile: profile,
|
qvacProfile: profile,
|
||||||
@@ -257,6 +264,7 @@ export function createQvacView(opts) {
|
|||||||
qvacToolDepth: toolDepth,
|
qvacToolDepth: toolDepth,
|
||||||
qvacSubAgents: subAgents,
|
qvacSubAgents: subAgents,
|
||||||
qvacMaxSubAgents: maxAgents,
|
qvacMaxSubAgents: maxAgents,
|
||||||
|
qvacAutoNavigate: autoNav,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,6 +1044,7 @@ export function createQvacView(opts) {
|
|||||||
'#qvac-set-tool-depth',
|
'#qvac-set-tool-depth',
|
||||||
'#qvac-set-subagents',
|
'#qvac-set-subagents',
|
||||||
'#qvac-set-max-agents',
|
'#qvac-set-max-agents',
|
||||||
|
'#qvac-set-autonav',
|
||||||
]) {
|
]) {
|
||||||
root?.querySelector(sel)?.addEventListener('change', () => {
|
root?.querySelector(sel)?.addEventListener('change', () => {
|
||||||
readSettingsForm()
|
readSettingsForm()
|
||||||
|
|||||||
+7
-7
@@ -28,7 +28,9 @@ 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 show_charts / open_chart / open_view when the user asks to see graphs or the UI.',
|
'- Navigation policy: NEVER switch the user away from their current tab unless they asked to see that UI, or they have auto-navigate on.',
|
||||||
|
' Pass navigate=true on show_charts / open_chart / open_view / open_dashboard / create_dashboard ONLY when the user said show/open/go to/take me to that view.',
|
||||||
|
' If they only want analysis (“is CPU high?”), call tools WITHOUT navigate — boards update in the background; stay on QVAC.',
|
||||||
multi
|
multi
|
||||||
? '- Multi-agent is DYNAMIC: the host only fans out specialists for broad investigations (what\'s wrong, full health, multi-domain). Single questions use normal tools only.'
|
? '- Multi-agent is DYNAMIC: the host only fans out specialists for broad investigations (what\'s wrong, full health, multi-domain). Single questions use normal tools only.'
|
||||||
: '',
|
: '',
|
||||||
@@ -36,12 +38,10 @@ 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.',
|
||||||
'- "Show me a graph/chart/plot" → search_charts if needed, then show_charts with concrete chart ids.',
|
'- "Show me a graph" (explicit) → show_charts … navigate=true. Analysis-only → show_charts without navigate (or just summarize_chart).',
|
||||||
' show_charts pins to the Charts wall board, sets window (preset=5m|1h|…), optional mode=line|area|bar|pie.',
|
' Example: show_charts charts=system.cpu,system.ram preset=15m mode=area navigate=true',
|
||||||
' Example: show_charts charts=system.cpu,system.ram preset=15m mode=area boardOnly=true.',
|
'- Custom dashboards → create_dashboard / add_dashboard_charts; open_dashboard navigate=true only if they asked to open Dashboard.',
|
||||||
'- Custom Dashboard tab (saved boards) → create_dashboard name=… charts=system.cpu,system.ram',
|
'- Single chart focus → open_chart chart=system.cpu navigate=true when they asked to see it.',
|
||||||
' then open_dashboard. Edit with add_dashboard_charts / remove_dashboard_charts / update_dashboard / list_dashboards.',
|
|
||||||
'- 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.',
|
'- 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.',
|
'- Related wall panel → show_related_ui; Metric Correlations UI → run_correlations.',
|
||||||
'- Spikes / unusual activity → hot_metrics, then show_charts on top hits + summarize_chart.',
|
'- Spikes / unusual activity → hot_metrics, then show_charts on top hits + summarize_chart.',
|
||||||
|
|||||||
+206
-46
@@ -29,6 +29,15 @@ function params(properties = {}, required) {
|
|||||||
* @typedef {{ type: 'function', name: string, description: string, parameters: object, tier?: ToolTier }} ToolDef
|
* @typedef {{ type: 'function', name: string, description: string, parameters: object, tier?: ToolTier }} ToolDef
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Shared optional navigate param for UI-affecting tools */
|
||||||
|
const NAVIGATE_PROP = {
|
||||||
|
navigate: {
|
||||||
|
type: 'boolean',
|
||||||
|
description:
|
||||||
|
'true only if the user asked to open/see that view, or they confirmed auto-navigate. Default false keeps them on the current tab.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {ToolDef[]} */
|
/** @type {ToolDef[]} */
|
||||||
export const TOOL_DEFS = [
|
export const TOOL_DEFS = [
|
||||||
// ── core: diagnosis + navigation ─────────────────────────────────────
|
// ── core: diagnosis + navigation ─────────────────────────────────────
|
||||||
@@ -130,7 +139,7 @@ export const TOOL_DEFS = [
|
|||||||
name: 'open_chart',
|
name: 'open_chart',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description:
|
description:
|
||||||
'Open one chart on the Charts wall (scroll + focus). Prefer show_charts when pinning multiple or changing window/type.',
|
'Prepare one chart on the Charts wall (pin/focus). Does NOT switch tabs unless navigate=true or user allows auto-navigate.',
|
||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
chart: { type: 'string', description: 'Chart id' },
|
chart: { type: 'string', description: 'Chart id' },
|
||||||
@@ -145,6 +154,7 @@ export const TOOL_DEFS = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Time preset e.g. 5m|15m|1h|6h|24h',
|
description: 'Time preset e.g. 5m|15m|1h|6h|24h',
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['chart']
|
['chart']
|
||||||
),
|
),
|
||||||
@@ -154,7 +164,7 @@ export const TOOL_DEFS = [
|
|||||||
name: 'show_charts',
|
name: 'show_charts',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description:
|
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.',
|
'PRIMARY chart tool: pin charts, set window/type on the Charts wall. Stays on QVAC unless navigate=true (user asked to see graphs) or auto-navigate allows.',
|
||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
charts: {
|
charts: {
|
||||||
@@ -196,6 +206,7 @@ export const TOOL_DEFS = [
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Live play (true) or pause (false)',
|
description: 'Live play (true) or pause (false)',
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['charts']
|
['charts']
|
||||||
),
|
),
|
||||||
@@ -204,7 +215,7 @@ export const TOOL_DEFS = [
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
name: 'pin_charts',
|
name: 'pin_charts',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description: 'Pin or unpin charts on the Charts board without changing focus.',
|
description: 'Pin or unpin charts on the Charts board (no tab switch unless navigate=true).',
|
||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
charts: {
|
charts: {
|
||||||
@@ -215,6 +226,7 @@ export const TOOL_DEFS = [
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'true=pin (default), false=unpin',
|
description: 'true=pin (default), false=unpin',
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['charts']
|
['charts']
|
||||||
),
|
),
|
||||||
@@ -232,6 +244,7 @@ export const TOOL_DEFS = [
|
|||||||
description: 'line|area|stacked|bar|multibar|pie',
|
description: 'line|area|stacked|bar|multibar|pie',
|
||||||
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['chart', 'mode']
|
['chart', 'mode']
|
||||||
),
|
),
|
||||||
@@ -248,6 +261,7 @@ export const TOOL_DEFS = [
|
|||||||
},
|
},
|
||||||
seconds: { type: 'number', description: 'Lookback seconds' },
|
seconds: { type: 'number', description: 'Lookback seconds' },
|
||||||
playing: { type: 'boolean', description: 'Live updates on/off' },
|
playing: { type: 'boolean', description: 'Live updates on/off' },
|
||||||
|
...NAVIGATE_PROP,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -258,6 +272,7 @@ export const TOOL_DEFS = [
|
|||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
q: { type: 'string', description: 'Filter query (empty clears)' },
|
q: { type: 'string', description: 'Filter query (empty clears)' },
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['q']
|
['q']
|
||||||
),
|
),
|
||||||
@@ -270,6 +285,7 @@ export const TOOL_DEFS = [
|
|||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
chart: { type: 'string', description: 'Seed chart id' },
|
chart: { type: 'string', description: 'Seed chart id' },
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['chart']
|
['chart']
|
||||||
),
|
),
|
||||||
@@ -279,11 +295,12 @@ export const TOOL_DEFS = [
|
|||||||
name: 'run_correlations',
|
name: 'run_correlations',
|
||||||
tier: 'deep',
|
tier: 'deep',
|
||||||
description:
|
description:
|
||||||
'Run Metric Correlations around a chart or timestamp and show results on the Charts wall.',
|
'Run Metric Correlations around a chart or timestamp on the Charts wall.',
|
||||||
parameters: params({
|
parameters: params({
|
||||||
chart: { type: 'string', description: 'Optional seed chart id' },
|
chart: { type: 'string', description: 'Optional seed chart id' },
|
||||||
ts: { type: 'number', description: 'Center timestamp ms (default now)' },
|
ts: { type: 'number', description: 'Center timestamp ms (default now)' },
|
||||||
window: { type: 'number', description: 'Highlight window seconds' },
|
window: { type: 'number', description: 'Highlight window seconds' },
|
||||||
|
...NAVIGATE_PROP,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -291,7 +308,7 @@ export const TOOL_DEFS = [
|
|||||||
name: 'open_view',
|
name: 'open_view',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description:
|
description:
|
||||||
'Navigate desktop to a view: overview|charts|dashboard|processes|alerts|logs|fleet|settings|qvac',
|
'Switch the user to a desktop view. Only when they asked to go there, or navigate=true / auto-navigate allows. Views: overview|charts|dashboard|processes|alerts|logs|fleet|settings|qvac',
|
||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
view: {
|
view: {
|
||||||
@@ -309,6 +326,7 @@ export const TOOL_DEFS = [
|
|||||||
'qvac',
|
'qvac',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['view']
|
['view']
|
||||||
),
|
),
|
||||||
@@ -327,7 +345,7 @@ export const TOOL_DEFS = [
|
|||||||
name: 'create_dashboard',
|
name: 'create_dashboard',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description:
|
description:
|
||||||
'Create a custom live dashboard and open it. Pass name + charts (comma ids). Use for "build a dashboard with CPU and RAM".',
|
'Create a custom live dashboard. Pass name + charts. Tab switch only with navigate=true or auto-navigate allow.',
|
||||||
parameters: params(
|
parameters: params(
|
||||||
{
|
{
|
||||||
name: { type: 'string', description: 'Dashboard name' },
|
name: { type: 'string', description: 'Dashboard name' },
|
||||||
@@ -341,6 +359,7 @@ export const TOOL_DEFS = [
|
|||||||
description: 'Default tile mode line|area|bar|…',
|
description: 'Default tile mode line|area|bar|…',
|
||||||
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['name']
|
['name']
|
||||||
),
|
),
|
||||||
@@ -390,6 +409,7 @@ export const TOOL_DEFS = [
|
|||||||
description: 'Tile chart type',
|
description: 'Tile chart type',
|
||||||
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
enum: ['line', 'area', 'stacked', 'bar', 'multibar', 'pie'],
|
||||||
},
|
},
|
||||||
|
...NAVIGATE_PROP,
|
||||||
},
|
},
|
||||||
['charts']
|
['charts']
|
||||||
),
|
),
|
||||||
@@ -414,9 +434,11 @@ export const TOOL_DEFS = [
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
name: 'open_dashboard',
|
name: 'open_dashboard',
|
||||||
tier: 'core',
|
tier: 'core',
|
||||||
description: 'Open the Dashboard tab and select a board by id (or active).',
|
description:
|
||||||
|
'Select a dashboard board. Switches to Dashboard tab only with navigate=true or auto-navigate allow (user asked to open it).',
|
||||||
parameters: params({
|
parameters: params({
|
||||||
id: { type: 'string', description: 'Dashboard id (optional)' },
|
id: { type: 'string', description: 'Dashboard id (optional)' },
|
||||||
|
...NAVIGATE_PROP,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -740,6 +762,53 @@ function wireDefs(defs) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve auto-navigate preference: off | ask | on
|
||||||
|
* @param {*} deps
|
||||||
|
*/
|
||||||
|
function autoNavMode(deps) {
|
||||||
|
const raw = deps.getAutoNavigate?.() ?? deps.autoNavigate ?? 'ask'
|
||||||
|
const s = String(raw || 'ask').toLowerCase()
|
||||||
|
if (s === 'on' || s === 'true' || s === 'always') return 'on'
|
||||||
|
if (s === 'off' || s === 'false' || s === 'never') return 'off'
|
||||||
|
return 'ask'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch desktop view only when allowed.
|
||||||
|
* @param {string} view
|
||||||
|
* @param {*} deps
|
||||||
|
* @param {object} [args]
|
||||||
|
* @returns {Promise<{ navigated: boolean, reason: string }>}
|
||||||
|
*/
|
||||||
|
async function maybeNavigate(view, deps, args = {}) {
|
||||||
|
const force =
|
||||||
|
args.navigate === true ||
|
||||||
|
args.navigate === 'true' ||
|
||||||
|
args.navigate === 1 ||
|
||||||
|
args.navigate === '1'
|
||||||
|
const mode = autoNavMode(deps)
|
||||||
|
const label = String(view || 'that view')
|
||||||
|
|
||||||
|
if (force || mode === 'on') {
|
||||||
|
deps.onOpenView?.(view)
|
||||||
|
return { navigated: true, reason: force ? 'navigate=true' : 'auto_on' }
|
||||||
|
}
|
||||||
|
if (mode === 'ask') {
|
||||||
|
const ok =
|
||||||
|
(await deps.confirmAction?.(
|
||||||
|
`QVAC wants to open the “${label}” view. Switch now?`
|
||||||
|
)) === true
|
||||||
|
if (ok) {
|
||||||
|
deps.onOpenView?.(view)
|
||||||
|
return { navigated: true, reason: 'user_confirmed' }
|
||||||
|
}
|
||||||
|
return { navigated: false, reason: 'user_declined' }
|
||||||
|
}
|
||||||
|
// off
|
||||||
|
return { navigated: false, reason: 'auto_off' }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
* manager: { request: (m: string, a?: object) => Promise<any>, active: any },
|
* manager: { request: (m: string, a?: object) => Promise<any>, active: any },
|
||||||
@@ -748,6 +817,8 @@ function wireDefs(defs) {
|
|||||||
* getCatalog?: () => Record<string, object>,
|
* getCatalog?: () => Record<string, object>,
|
||||||
* onOpenChart?: (chartId: string, ts?: number, opts?: object) => void,
|
* onOpenChart?: (chartId: string, ts?: number, opts?: object) => void,
|
||||||
* onOpenView?: (view: string) => void,
|
* onOpenView?: (view: string) => void,
|
||||||
|
* getAutoNavigate?: () => 'off'|'ask'|'on'|string,
|
||||||
|
* autoNavigate?: 'off'|'ask'|'on'|string,
|
||||||
* charts?: {
|
* charts?: {
|
||||||
* showCharts?: (opts: object) => object,
|
* showCharts?: (opts: object) => object,
|
||||||
* setPinned?: (ids: string|string[], opts?: object) => object,
|
* setPinned?: (ids: string|string[], opts?: object) => object,
|
||||||
@@ -921,9 +992,10 @@ export function createToolRunner(deps) {
|
|||||||
case 'open_chart': {
|
case 'open_chart': {
|
||||||
const chart = String(args.chart || args.id || '')
|
const chart = String(args.chart || args.id || '')
|
||||||
if (!chart) return { error: 'chart required' }
|
if (!chart) return { error: 'chart required' }
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
|
let result
|
||||||
if (chartsApi.showCharts) {
|
if (chartsApi.showCharts) {
|
||||||
return chartsApi.showCharts({
|
result = chartsApi.showCharts({
|
||||||
charts: [chart],
|
charts: [chart],
|
||||||
pin: args.pin !== false,
|
pin: args.pin !== false,
|
||||||
boardOnly: args.pin !== false,
|
boardOnly: args.pin !== false,
|
||||||
@@ -931,26 +1003,34 @@ export function createToolRunner(deps) {
|
|||||||
ts: args.ts,
|
ts: args.ts,
|
||||||
mode: args.mode,
|
mode: args.mode,
|
||||||
preset: args.preset,
|
preset: args.preset,
|
||||||
openFocus: true,
|
openFocus: nav.navigated,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
deps.onOpenChart?.(chart, args.ts, {
|
||||||
|
pin: args.pin !== false,
|
||||||
|
mode: args.mode,
|
||||||
|
preset: args.preset,
|
||||||
|
navigate: nav.navigated,
|
||||||
|
})
|
||||||
|
result = { ok: true, opened: chart }
|
||||||
}
|
}
|
||||||
deps.onOpenChart?.(chart, args.ts, {
|
return { ...result, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
pin: args.pin !== false,
|
|
||||||
mode: args.mode,
|
|
||||||
preset: args.preset,
|
|
||||||
})
|
|
||||||
return { ok: true, opened: chart }
|
|
||||||
}
|
}
|
||||||
case 'show_charts': {
|
case 'show_charts': {
|
||||||
const list = parseChartList(args.charts || args.chart || args.ids)
|
const list = parseChartList(args.charts || args.chart || args.ids)
|
||||||
if (!list.length) return { error: 'charts required' }
|
if (!list.length) return { error: 'charts required' }
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
if (!chartsApi.showCharts) {
|
if (!chartsApi.showCharts) {
|
||||||
// Fallback: open first chart only
|
if (nav.navigated) deps.onOpenChart?.(list[0], args.ts)
|
||||||
deps.onOpenChart?.(list[0], args.ts)
|
return {
|
||||||
return { ok: true, charts: list, partial: true }
|
ok: true,
|
||||||
|
charts: list,
|
||||||
|
partial: true,
|
||||||
|
navigated: nav.navigated,
|
||||||
|
navigateReason: nav.reason,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return chartsApi.showCharts({
|
const result = chartsApi.showCharts({
|
||||||
charts: list,
|
charts: list,
|
||||||
pin: args.pin !== false,
|
pin: args.pin !== false,
|
||||||
boardOnly: args.boardOnly != null ? Boolean(args.boardOnly) : args.pin !== false,
|
boardOnly: args.boardOnly != null ? Boolean(args.boardOnly) : args.pin !== false,
|
||||||
@@ -962,58 +1042,74 @@ export function createToolRunner(deps) {
|
|||||||
filter: args.filter,
|
filter: args.filter,
|
||||||
related: Boolean(args.related),
|
related: Boolean(args.related),
|
||||||
playing: args.playing,
|
playing: args.playing,
|
||||||
openFocus: list.length === 1,
|
openFocus: nav.navigated && list.length === 1,
|
||||||
})
|
})
|
||||||
|
return { ...result, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'pin_charts': {
|
case 'pin_charts': {
|
||||||
const list = parseChartList(args.charts || args.chart)
|
const list = parseChartList(args.charts || args.chart)
|
||||||
if (!list.length) return { error: 'charts required' }
|
if (!list.length) return { error: 'charts required' }
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
if (!chartsApi.setPinned) return { error: 'charts API unavailable' }
|
if (!chartsApi.setPinned) return { error: 'charts API unavailable' }
|
||||||
return chartsApi.setPinned(list, {
|
const result = chartsApi.setPinned(list, {
|
||||||
pin: args.pin !== false && args.pin !== 'false',
|
pin: args.pin !== false && args.pin !== 'false',
|
||||||
})
|
})
|
||||||
|
return { ...result, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'set_chart_type': {
|
case 'set_chart_type': {
|
||||||
const chart = String(args.chart || args.id || '')
|
const chart = String(args.chart || args.id || '')
|
||||||
const mode = String(args.mode || args.type || 'line')
|
const mode = String(args.mode || args.type || 'line')
|
||||||
if (!chart) return { error: 'chart required' }
|
if (!chart) return { error: 'chart required' }
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
if (!chartsApi.setChartMode) return { error: 'charts API unavailable' }
|
if (!chartsApi.setChartMode) return { error: 'charts API unavailable' }
|
||||||
return chartsApi.setChartMode(chart, mode)
|
const result = chartsApi.setChartMode(chart, mode)
|
||||||
|
return { ...result, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'set_time_window': {
|
case 'set_time_window': {
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
if (!chartsApi.setTimeWindow) return { error: 'charts API unavailable' }
|
if (!chartsApi.setTimeWindow) return { error: 'charts API unavailable' }
|
||||||
return chartsApi.setTimeWindow({
|
const result = chartsApi.setTimeWindow({
|
||||||
preset: args.preset,
|
preset: args.preset,
|
||||||
seconds: args.seconds ?? args.after,
|
seconds: args.seconds ?? args.after,
|
||||||
playing: args.playing,
|
playing: args.playing,
|
||||||
})
|
})
|
||||||
|
return { ...result, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'filter_charts': {
|
case 'filter_charts': {
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
if (!chartsApi.setFilter) return { error: 'charts API unavailable' }
|
if (!chartsApi.setFilter) return { error: 'charts API unavailable' }
|
||||||
return chartsApi.setFilter(args.q != null ? args.q : args.filter || '')
|
const result = chartsApi.setFilter(args.q != null ? args.q : args.filter || '')
|
||||||
|
return { ...result, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'show_related_ui': {
|
case 'show_related_ui': {
|
||||||
const chart = String(args.chart || args.id || '')
|
const chart = String(args.chart || args.id || '')
|
||||||
if (!chart) return { error: 'chart required' }
|
if (!chart) return { error: 'chart required' }
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
chartsApi.scrollToChart?.(chart)
|
chartsApi.scrollToChart?.(chart)
|
||||||
await chartsApi.showRelated?.(chart)
|
await chartsApi.showRelated?.(chart)
|
||||||
return { ok: true, chart, related: true }
|
return {
|
||||||
|
ok: true,
|
||||||
|
chart,
|
||||||
|
related: true,
|
||||||
|
navigated: nav.navigated,
|
||||||
|
navigateReason: nav.reason,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'run_correlations': {
|
case 'run_correlations': {
|
||||||
deps.onOpenView?.('charts')
|
const nav = await maybeNavigate('charts', deps, args)
|
||||||
const ts =
|
const ts = args.ts != null ? Number(args.ts) : Date.now()
|
||||||
args.ts != null ? Number(args.ts) : Date.now()
|
|
||||||
if (chartsApi.correlateAround) {
|
if (chartsApi.correlateAround) {
|
||||||
chartsApi.correlateAround(ts, {
|
chartsApi.correlateAround(ts, {
|
||||||
chart: args.chart,
|
chart: args.chart,
|
||||||
window: args.window,
|
window: args.window,
|
||||||
})
|
})
|
||||||
return { ok: true, ts, chart: args.chart || null }
|
return {
|
||||||
|
ok: true,
|
||||||
|
ts,
|
||||||
|
chart: args.chart || null,
|
||||||
|
navigated: nav.navigated,
|
||||||
|
navigateReason: nav.reason,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (chartsApi.runMetricCorrelations) {
|
if (chartsApi.runMetricCorrelations) {
|
||||||
const res = await chartsApi.runMetricCorrelations({
|
const res = await chartsApi.runMetricCorrelations({
|
||||||
@@ -1021,13 +1117,58 @@ export function createToolRunner(deps) {
|
|||||||
ts,
|
ts,
|
||||||
window: args.window,
|
window: args.window,
|
||||||
})
|
})
|
||||||
return { ok: true, ...(res || {}) }
|
return {
|
||||||
|
ok: true,
|
||||||
|
...(res || {}),
|
||||||
|
navigated: nav.navigated,
|
||||||
|
navigateReason: nav.reason,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { error: 'correlations API unavailable' }
|
return { error: 'correlations API unavailable' }
|
||||||
}
|
}
|
||||||
case 'open_view': {
|
case 'open_view': {
|
||||||
deps.onOpenView?.(String(args.view || 'overview'))
|
// Explicit navigation tool — still gated by auto-nav / navigate flag
|
||||||
return { ok: true, view: args.view }
|
const view = String(args.view || 'overview')
|
||||||
|
const nav = await maybeNavigate(view, deps, {
|
||||||
|
...args,
|
||||||
|
// open_view implies intent to navigate when auto is on/ask;
|
||||||
|
// with auto off, still require navigate=true
|
||||||
|
navigate: args.navigate === true || args.navigate === 'true' ? true : args.navigate,
|
||||||
|
})
|
||||||
|
// When mode is ask/on, maybeNavigate already handled. When off without force, no-op.
|
||||||
|
if (!nav.navigated && autoNavMode(deps) !== 'off') {
|
||||||
|
// ask declined already returned; on should have navigated
|
||||||
|
}
|
||||||
|
// For open_view with auto on, force path already navigated.
|
||||||
|
// With auto off, require navigate=true (maybeNavigate handles).
|
||||||
|
// With auto ask without prior force: maybeNavigate already asked.
|
||||||
|
// Special case: if auto is off and navigate not set, try one confirm as explicit open_view
|
||||||
|
if (!nav.navigated && autoNavMode(deps) === 'off' && args.navigate == null) {
|
||||||
|
const ok =
|
||||||
|
(await deps.confirmAction?.(
|
||||||
|
`Switch to the “${view}” view? (You can enable Auto-navigate in QVAC Settings.)`
|
||||||
|
)) === true
|
||||||
|
if (ok) {
|
||||||
|
deps.onOpenView?.(view)
|
||||||
|
return { ok: true, view, navigated: true, navigateReason: 'user_confirmed' }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
view,
|
||||||
|
navigated: false,
|
||||||
|
navigateReason: 'user_declined',
|
||||||
|
note: 'Stayed on current tab. Pass navigate=true when the user asks to open a view.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
view,
|
||||||
|
navigated: nav.navigated,
|
||||||
|
navigateReason: nav.reason,
|
||||||
|
note: nav.navigated
|
||||||
|
? undefined
|
||||||
|
: 'View not switched. User did not allow navigation.',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'list_dashboards': {
|
case 'list_dashboards': {
|
||||||
if (!deps.dashboards?.list) return { error: 'dashboards API unavailable' }
|
if (!deps.dashboards?.list) return { error: 'dashboards API unavailable' }
|
||||||
@@ -1042,8 +1183,8 @@ export function createToolRunner(deps) {
|
|||||||
mode: args.mode || 'area',
|
mode: args.mode || 'area',
|
||||||
tiles: args.tiles,
|
tiles: args.tiles,
|
||||||
})
|
})
|
||||||
deps.onOpenView?.('dashboard')
|
const nav = await maybeNavigate('dashboard', deps, args)
|
||||||
return r
|
return { ...r, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'update_dashboard': {
|
case 'update_dashboard': {
|
||||||
if (!deps.dashboards?.update) return { error: 'dashboards API unavailable' }
|
if (!deps.dashboards?.update) return { error: 'dashboards API unavailable' }
|
||||||
@@ -1068,8 +1209,8 @@ export function createToolRunner(deps) {
|
|||||||
mode: args.mode,
|
mode: args.mode,
|
||||||
replace: args.replace,
|
replace: args.replace,
|
||||||
})
|
})
|
||||||
deps.onOpenView?.('dashboard')
|
const nav = await maybeNavigate('dashboard', deps, args)
|
||||||
return r
|
return { ...r, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'remove_dashboard_charts': {
|
case 'remove_dashboard_charts': {
|
||||||
if (!deps.dashboards?.removeCharts) return { error: 'dashboards API unavailable' }
|
if (!deps.dashboards?.removeCharts) return { error: 'dashboards API unavailable' }
|
||||||
@@ -1080,10 +1221,29 @@ export function createToolRunner(deps) {
|
|||||||
}
|
}
|
||||||
case 'open_dashboard': {
|
case 'open_dashboard': {
|
||||||
if (!deps.dashboards?.open) {
|
if (!deps.dashboards?.open) {
|
||||||
deps.onOpenView?.('dashboard')
|
const nav = await maybeNavigate('dashboard', deps, {
|
||||||
return { ok: true, view: 'dashboard' }
|
...args,
|
||||||
|
navigate: args.navigate != null ? args.navigate : true,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
view: 'dashboard',
|
||||||
|
navigated: nav.navigated,
|
||||||
|
navigateReason: nav.reason,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return deps.dashboards.open({ id: args.id || args.dashboardId })
|
// Select board first, then maybe switch tab
|
||||||
|
const r = deps.dashboards.open({
|
||||||
|
id: args.id || args.dashboardId,
|
||||||
|
navigate: false,
|
||||||
|
})
|
||||||
|
const nav = await maybeNavigate('dashboard', deps, {
|
||||||
|
...args,
|
||||||
|
// open_dashboard is an open request: default navigate ask/on, but still confirm
|
||||||
|
navigate: args.navigate != null ? args.navigate : undefined,
|
||||||
|
})
|
||||||
|
// If open always showed view before, prevent double — dashboards.open in app still navigates
|
||||||
|
return { ...r, navigated: nav.navigated, navigateReason: nav.reason }
|
||||||
}
|
}
|
||||||
case 'silence_alert': {
|
case 'silence_alert': {
|
||||||
if (!args.confirmed) {
|
if (!args.confirmed) {
|
||||||
|
|||||||
Reference in New Issue
Block a user