This commit is contained in:
@@ -227,6 +227,7 @@ export function pickAgents(userText, maxAgents = 3) {
|
||||
* tools: { run: (name: string, args?: object) => Promise<any> },
|
||||
* query: string,
|
||||
* maxAgents?: number,
|
||||
* isAborted?: () => boolean,
|
||||
* onAgent?: (ev: {
|
||||
* id: string,
|
||||
* label: string,
|
||||
@@ -239,15 +240,36 @@ export function pickAgents(userText, maxAgents = 3) {
|
||||
export async function runSubAgents(opts) {
|
||||
const specs = pickAgents(opts.query, opts.maxAgents ?? 3)
|
||||
const tools = opts.tools
|
||||
const aborted = () => Boolean(opts.isAborted?.())
|
||||
|
||||
/** @type {Array<{ id: string, label: string, status: string, summary: string, result?: any, error?: string }>} */
|
||||
const agents = []
|
||||
|
||||
await Promise.all(
|
||||
specs.map(async (spec) => {
|
||||
if (aborted()) {
|
||||
agents.push({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
status: 'error',
|
||||
summary: 'stopped',
|
||||
error: 'stopped',
|
||||
})
|
||||
return
|
||||
}
|
||||
opts.onAgent?.({ id: spec.id, label: spec.label, status: 'start' })
|
||||
try {
|
||||
const result = await spec.run(tools)
|
||||
if (aborted()) {
|
||||
agents.push({
|
||||
id: spec.id,
|
||||
label: spec.label,
|
||||
status: 'error',
|
||||
summary: 'stopped',
|
||||
error: 'stopped',
|
||||
})
|
||||
return
|
||||
}
|
||||
const summary = spec.summarize(result)
|
||||
const row = {
|
||||
id: spec.id,
|
||||
|
||||
+208
-56
@@ -95,6 +95,47 @@ function getElectronIpc() {
|
||||
}
|
||||
|
||||
export function createQvacEngine(deps) {
|
||||
/** Soft cancel for current complete() / multi-round tool loop */
|
||||
let abortGeneration = false
|
||||
/** @type {string|null} */
|
||||
let activeRequestId = null
|
||||
|
||||
/**
|
||||
* Stop in-flight inference (and pending tool rounds).
|
||||
* Safe to call when idle.
|
||||
*/
|
||||
async function stop() {
|
||||
abortGeneration = true
|
||||
const ipc = getElectronIpc()
|
||||
try {
|
||||
if (ipc && sdkMode === 'main') {
|
||||
await ipc.invoke('peardata:qvac-cancel')
|
||||
} else if (sdk?.cancel) {
|
||||
if (activeRequestId) {
|
||||
try {
|
||||
await sdk.cancel({ requestId: activeRequestId })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (modelId) {
|
||||
try {
|
||||
await sdk.cancel({ modelId, kind: 'completion' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
deps.log?.(`QVAC stop: ${err?.message || err}`)
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
function isAborted() {
|
||||
return abortGeneration
|
||||
}
|
||||
|
||||
/** @type {any} */
|
||||
let sdk = null
|
||||
/** @type {'direct'|'main'|null} */
|
||||
@@ -442,6 +483,7 @@ export function createQvacEngine(deps) {
|
||||
*/
|
||||
async function complete(history, opts = {}) {
|
||||
touchActivity()
|
||||
abortGeneration = false
|
||||
const profile = getProfile(profileId || 'recommended')
|
||||
const prefs = deps.getPrefs?.() || {}
|
||||
const userLast = [...history].reverse().find((m) => m.role === 'user')
|
||||
@@ -471,6 +513,10 @@ export function createQvacEngine(deps) {
|
||||
...history.filter((m) => m.role !== 'system'),
|
||||
]
|
||||
|
||||
if (abortGeneration) {
|
||||
return { contentText: '_(Stopped.)_', toolCalls: [], mode: 'stopped', stopped: true }
|
||||
}
|
||||
|
||||
if (modelId && (sdkMode === 'main' || (sdk && sdk.completion))) {
|
||||
return completeWithSdk(fullHistory, profile, opts, prefs)
|
||||
}
|
||||
@@ -478,18 +524,40 @@ export function createQvacEngine(deps) {
|
||||
status = status === 'ready' ? status : 'fallback'
|
||||
// If multi-agent pack already ran, prefer its synthesis over naive fallback
|
||||
if (opts.agentFallbackText) {
|
||||
if (abortGeneration) {
|
||||
return { contentText: '_(Stopped.)_', toolCalls: [], mode: 'stopped', stopped: true }
|
||||
}
|
||||
const text = String(opts.agentFallbackText)
|
||||
if (opts.onToken) {
|
||||
const chunk = 24
|
||||
for (let i = 0; i < text.length; i += chunk) opts.onToken(text.slice(i, i + chunk))
|
||||
for (let i = 0; i < text.length; i += chunk) {
|
||||
if (abortGeneration) break
|
||||
opts.onToken(text.slice(i, i + chunk))
|
||||
}
|
||||
}
|
||||
opts.onEvent?.({ type: 'completionDone', stopReason: 'eos' })
|
||||
return { contentText: text, toolCalls: opts.agentToolCalls || [], mode: 'fallback' }
|
||||
opts.onEvent?.({ type: 'completionDone', stopReason: abortGeneration ? 'cancelled' : 'eos' })
|
||||
return {
|
||||
contentText: abortGeneration ? `${text}\n\n_(Stopped.)_` : text,
|
||||
toolCalls: opts.agentToolCalls || [],
|
||||
mode: abortGeneration ? 'stopped' : 'fallback',
|
||||
stopped: abortGeneration,
|
||||
}
|
||||
}
|
||||
if (abortGeneration) {
|
||||
return { contentText: '_(Stopped.)_', toolCalls: [], mode: 'stopped', stopped: true }
|
||||
}
|
||||
const result = await fallbackComplete(userLast?.content || '', deps.tools, {
|
||||
catalog: deps.getCatalog?.() || {},
|
||||
rag: prefs.rag !== false,
|
||||
})
|
||||
if (abortGeneration) {
|
||||
return {
|
||||
contentText: (result.contentText || '') + '\n\n_(Stopped.)_',
|
||||
toolCalls: result.toolCalls || [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
for (const c of result.toolCalls || []) {
|
||||
opts.onTool?.(c.name, c.args, c.result)
|
||||
}
|
||||
@@ -497,11 +565,16 @@ export function createQvacEngine(deps) {
|
||||
const text = result.contentText || ''
|
||||
const chunk = 24
|
||||
for (let i = 0; i < text.length; i += chunk) {
|
||||
if (abortGeneration) break
|
||||
opts.onToken(text.slice(i, i + chunk))
|
||||
}
|
||||
}
|
||||
opts.onEvent?.({ type: 'completionDone', stopReason: 'eos' })
|
||||
return result
|
||||
opts.onEvent?.({ type: 'completionDone', stopReason: abortGeneration ? 'cancelled' : 'eos' })
|
||||
return {
|
||||
...result,
|
||||
stopped: abortGeneration,
|
||||
mode: abortGeneration ? 'stopped' : result.mode,
|
||||
}
|
||||
}
|
||||
|
||||
async function completeWithSdk(history, profile, opts, prefs = {}) {
|
||||
@@ -523,6 +596,14 @@ export function createQvacEngine(deps) {
|
||||
)
|
||||
|
||||
for (let round = 0; round < 4; round++) {
|
||||
if (abortGeneration) {
|
||||
return {
|
||||
contentText: '_(Stopped.)_',
|
||||
toolCalls: [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
touchActivity()
|
||||
// Re-compact each tool round (payloads grow fast)
|
||||
messages = compactMessages(messages, {
|
||||
@@ -539,14 +620,32 @@ export function createQvacEngine(deps) {
|
||||
/** @type {(( _e: any, t: string) => void)|null} */
|
||||
let onThink = null
|
||||
try {
|
||||
onTok = (_e, t) => opts.onToken?.(t)
|
||||
onThink = (_e, t) => opts.onThinking?.(t)
|
||||
onTok = (_e, t) => {
|
||||
if (!abortGeneration) opts.onToken?.(t)
|
||||
}
|
||||
onThink = (_e, t) => {
|
||||
if (!abortGeneration) opts.onThinking?.(t)
|
||||
}
|
||||
ipc.on('peardata:qvac-token', onTok)
|
||||
ipc.on('peardata:qvac-thinking', onThink)
|
||||
const final = await ipc.invoke('peardata:qvac-complete', {
|
||||
history: messages,
|
||||
tools: toolDefs,
|
||||
})
|
||||
if (final?.stopped || abortGeneration) {
|
||||
let text = final?.contentText || ''
|
||||
if (final?.thinkingText && !/<think/i.test(text)) {
|
||||
text = `<think>\n${final.thinkingText}\n</think>\n${text}`
|
||||
}
|
||||
if (!text.trim()) text = '_(Stopped.)_'
|
||||
else if (!/stopped/i.test(text)) text += '\n\n_(Stopped.)_'
|
||||
return {
|
||||
contentText: text,
|
||||
toolCalls: [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
if (final?.error && !final.contentText) {
|
||||
if (isContextOverflowError(final.error) && overflowRetries < 3) {
|
||||
overflowRetries++
|
||||
@@ -602,6 +701,14 @@ export function createQvacEngine(deps) {
|
||||
}
|
||||
messages = [...messages, { role: 'assistant', content: text }]
|
||||
for (const tc of calls) {
|
||||
if (abortGeneration) {
|
||||
return {
|
||||
contentText: (text || '') + '\n\n_(Stopped.)_',
|
||||
toolCalls: [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
const name = tc.name || tc.function?.name
|
||||
let args = tc.arguments || tc.function?.arguments || {}
|
||||
if (typeof args === 'string') {
|
||||
@@ -647,65 +754,108 @@ export function createQvacEngine(deps) {
|
||||
tools: toolDefs,
|
||||
captureThinking: true,
|
||||
})
|
||||
activeRequestId = run.requestId || run.id || null
|
||||
|
||||
let content = ''
|
||||
/** @type {Array<{ name: string, arguments?: any, id?: string }>} */
|
||||
const toolCalls = []
|
||||
let streamStopped = false
|
||||
|
||||
if (run.events) {
|
||||
for await (const ev of run.events) {
|
||||
opts.onEvent?.(ev)
|
||||
if (ev.type === 'thinkingDelta' && (ev.text || ev.delta)) {
|
||||
opts.onThinking?.(ev.text || ev.delta)
|
||||
} else if (ev.type === 'contentDelta' && ev.text) {
|
||||
content += ev.text
|
||||
opts.onToken?.(ev.text)
|
||||
} else if (ev.type === 'toolCall') {
|
||||
const call = ev.call || ev.toolCall || ev
|
||||
toolCalls.push({
|
||||
name: call.name || ev.name,
|
||||
arguments: call.arguments || ev.arguments || {},
|
||||
id: call.id || ev.id,
|
||||
})
|
||||
try {
|
||||
if (run.events) {
|
||||
for await (const ev of run.events) {
|
||||
if (abortGeneration) {
|
||||
streamStopped = true
|
||||
break
|
||||
}
|
||||
opts.onEvent?.(ev)
|
||||
if (ev.type === 'thinkingDelta' && (ev.text || ev.delta)) {
|
||||
opts.onThinking?.(ev.text || ev.delta)
|
||||
} else if (ev.type === 'contentDelta' && ev.text) {
|
||||
content += ev.text
|
||||
opts.onToken?.(ev.text)
|
||||
} else if (ev.type === 'toolCall') {
|
||||
const call = ev.call || ev.toolCall || ev
|
||||
toolCalls.push({
|
||||
name: call.name || ev.name,
|
||||
arguments: call.arguments || ev.arguments || {},
|
||||
id: call.id || ev.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (run.tokenStream) {
|
||||
for await (const token of run.tokenStream) {
|
||||
if (abortGeneration) {
|
||||
streamStopped = true
|
||||
break
|
||||
}
|
||||
content += token
|
||||
opts.onToken?.(token)
|
||||
}
|
||||
}
|
||||
} else if (run.tokenStream) {
|
||||
for await (const token of run.tokenStream) {
|
||||
content += token
|
||||
opts.onToken?.(token)
|
||||
}
|
||||
}
|
||||
|
||||
const final = run.final ? await run.final : { contentText: content, toolCalls }
|
||||
|
||||
const calls = (final.toolCalls?.length ? final.toolCalls : toolCalls).filter(Boolean)
|
||||
if (!calls.length) {
|
||||
return {
|
||||
contentText: final.contentText || content,
|
||||
toolCalls: [],
|
||||
mode: 'qvac',
|
||||
stats: final.stats,
|
||||
}
|
||||
}
|
||||
|
||||
messages = [...messages, { role: 'assistant', content: final.contentText || content || '' }]
|
||||
for (const tc of calls) {
|
||||
const name = tc.name || tc.function?.name
|
||||
let args = tc.arguments || tc.function?.arguments || {}
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args)
|
||||
} catch {
|
||||
args = {}
|
||||
if (streamStopped || abortGeneration) {
|
||||
return {
|
||||
contentText: (content || '') + (content ? '\n\n' : '') + '_(Stopped.)_',
|
||||
toolCalls: [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
const result = await deps.tools.run(name, args)
|
||||
opts.onTool?.(name, args, result)
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: compactToolResult(name, result),
|
||||
name,
|
||||
})
|
||||
|
||||
const final = run.final ? await run.final : { contentText: content, toolCalls }
|
||||
|
||||
const calls = (final.toolCalls?.length ? final.toolCalls : toolCalls).filter(Boolean)
|
||||
if (!calls.length) {
|
||||
return {
|
||||
contentText: final.contentText || content,
|
||||
toolCalls: [],
|
||||
mode: 'qvac',
|
||||
stats: final.stats,
|
||||
}
|
||||
}
|
||||
|
||||
messages = [
|
||||
...messages,
|
||||
{ role: 'assistant', content: final.contentText || content || '' },
|
||||
]
|
||||
for (const tc of calls) {
|
||||
if (abortGeneration) {
|
||||
return {
|
||||
contentText: (final.contentText || content || '') + '\n\n_(Stopped.)_',
|
||||
toolCalls: [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
const name = tc.name || tc.function?.name
|
||||
let args = tc.arguments || tc.function?.arguments || {}
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args)
|
||||
} catch {
|
||||
args = {}
|
||||
}
|
||||
}
|
||||
const result = await deps.tools.run(name, args)
|
||||
opts.onTool?.(name, args, result)
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: compactToolResult(name, result),
|
||||
name,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
activeRequestId = null
|
||||
}
|
||||
}
|
||||
|
||||
if (abortGeneration) {
|
||||
return {
|
||||
contentText: '_(Stopped.)_',
|
||||
toolCalls: [],
|
||||
mode: 'stopped',
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +871,8 @@ export function createQvacEngine(deps) {
|
||||
loadProfile,
|
||||
unload,
|
||||
complete,
|
||||
stop,
|
||||
isAborted,
|
||||
getStatus,
|
||||
tryLoadSdk,
|
||||
touchActivity,
|
||||
|
||||
+99
-28
@@ -22,6 +22,7 @@ import { chartIdsFromToolLog, mountChartEmbeds } from './chart-embed.js'
|
||||
* messages: HTMLElement|null,
|
||||
* input: HTMLTextAreaElement|null,
|
||||
* sendBtn: HTMLElement|null,
|
||||
* stopBtn?: HTMLElement|null,
|
||||
* status: HTMLElement|null,
|
||||
* modelChip: HTMLElement|null,
|
||||
* samples: HTMLElement|null,
|
||||
@@ -792,13 +793,36 @@ export function createQvacView(opts) {
|
||||
}
|
||||
}
|
||||
|
||||
function setBusyUi(on) {
|
||||
busy = on
|
||||
const send = opts.els.sendBtn
|
||||
const stop = opts.els.stopBtn
|
||||
if (send) {
|
||||
send.disabled = on
|
||||
send.classList.toggle('hidden', on)
|
||||
}
|
||||
if (stop) {
|
||||
stop.classList.toggle('hidden', !on)
|
||||
stop.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stopInference() {
|
||||
if (!busy) return
|
||||
setStatus('Stopping…', 'busy')
|
||||
try {
|
||||
await engine.stop?.()
|
||||
} catch (err) {
|
||||
opts.log?.(`QVAC stop: ${err?.message || err}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const input = opts.els.input
|
||||
const text = (input?.value || '').trim()
|
||||
if (!text || busy) return
|
||||
if (input) input.value = ''
|
||||
busy = true
|
||||
opts.els.sendBtn && (opts.els.sendBtn.disabled = true)
|
||||
setBusyUi(true)
|
||||
appendMsg('user', text)
|
||||
const assistantUi = appendMsg('assistant', '…', { streaming: true })
|
||||
const streamBody = assistantUi?.body
|
||||
@@ -826,7 +850,11 @@ export function createQvacView(opts) {
|
||||
maxSubAgents: Number(settings().qvacMaxSubAgents) || 3,
|
||||
}
|
||||
|
||||
if (shouldUseSubAgents(text, prefs) && opts.isConnected?.()) {
|
||||
if (
|
||||
shouldUseSubAgents(text, prefs) &&
|
||||
opts.isConnected?.() &&
|
||||
!engine.isAborted?.()
|
||||
) {
|
||||
setStatus('Spinning up agents…', 'busy')
|
||||
if (streamBody) {
|
||||
streamBody.innerHTML = formatMdLite('_Dispatching multi-agent investigation…_')
|
||||
@@ -835,11 +863,19 @@ export function createQvacView(opts) {
|
||||
tools,
|
||||
query: text,
|
||||
maxAgents: prefs.maxSubAgents,
|
||||
isAborted: () => Boolean(engine.isAborted?.()),
|
||||
onAgent: (ev) => {
|
||||
if (engine.isAborted?.()) return
|
||||
paintAgentEvent(ev)
|
||||
setStatus(`Agent: ${ev.label}…`, 'busy')
|
||||
},
|
||||
})
|
||||
if (engine.isAborted?.()) {
|
||||
acc = '_(Stopped during multi-agent investigation.)_'
|
||||
paintAssistant(streamBody, acc, false)
|
||||
setStatus('Stopped', 'warn')
|
||||
return
|
||||
}
|
||||
completeOpts.agentBriefing = pack.contextText
|
||||
completeOpts.agentFallbackText = synthesizeFromAgents(text, pack)
|
||||
completeOpts.agentToolCalls = pack.agents.map((a) => ({
|
||||
@@ -862,6 +898,13 @@ export function createQvacView(opts) {
|
||||
setStatus('Synthesizing…', 'busy')
|
||||
}
|
||||
|
||||
if (engine.isAborted?.()) {
|
||||
acc = '_(Stopped.)_'
|
||||
paintAssistant(streamBody, acc, false)
|
||||
setStatus('Stopped', 'warn')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await engine.complete(hist, {
|
||||
...completeOpts,
|
||||
onToken: (t) => {
|
||||
@@ -876,6 +919,7 @@ export function createQvacView(opts) {
|
||||
paintAssistant(streamBody, raw, true)
|
||||
},
|
||||
onTool: (name, args, res) => {
|
||||
if (engine.isAborted?.()) return
|
||||
toolLog.push({ name, args, result: res })
|
||||
setStatus(`Tool: ${name}…`, 'busy')
|
||||
if (assistantUi?.div) {
|
||||
@@ -905,35 +949,49 @@ export function createQvacView(opts) {
|
||||
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(() => {})
|
||||
if (!result.stopped) {
|
||||
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(() => {})
|
||||
}
|
||||
}
|
||||
if (result.stopped || result.mode === 'stopped') {
|
||||
setStatus('Stopped', 'warn')
|
||||
} else {
|
||||
setStatus(result.mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
||||
}
|
||||
setStatus(result.mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
||||
} catch (err) {
|
||||
const msg = err?.message || String(err)
|
||||
if (streamBody) streamBody.innerHTML = formatMdLite(`Error: ${msg}`)
|
||||
setStatus(msg, 'error')
|
||||
if (/cancel|abort|stopped/i.test(msg)) {
|
||||
if (streamBody && !acc) {
|
||||
paintAssistant(streamBody, '_(Stopped.)_', false)
|
||||
} else if (streamBody && acc) {
|
||||
paintAssistant(streamBody, acc + '\n\n_(Stopped.)_', false)
|
||||
}
|
||||
setStatus('Stopped', 'warn')
|
||||
} else {
|
||||
if (streamBody) streamBody.innerHTML = formatMdLite(`Error: ${msg}`)
|
||||
setStatus(msg, 'error')
|
||||
}
|
||||
} finally {
|
||||
busy = false
|
||||
if (opts.els.sendBtn) opts.els.sendBtn.disabled = false
|
||||
setBusyUi(false)
|
||||
syncModelChip()
|
||||
}
|
||||
}
|
||||
@@ -1003,12 +1061,25 @@ export function createQvacView(opts) {
|
||||
|
||||
function bind() {
|
||||
opts.els.sendBtn?.addEventListener('click', () => send())
|
||||
opts.els.stopBtn?.addEventListener('click', () => stopInference())
|
||||
opts.els.input?.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape' && busy) {
|
||||
ev.preventDefault()
|
||||
stopInference()
|
||||
return
|
||||
}
|
||||
if (ev.key === 'Enter' && !ev.shiftKey) {
|
||||
ev.preventDefault()
|
||||
send()
|
||||
}
|
||||
})
|
||||
// Global Esc while QVAC tab is active
|
||||
opts.els.root?.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape' && busy) {
|
||||
ev.preventDefault()
|
||||
stopInference()
|
||||
}
|
||||
})
|
||||
opts.els.resetBtn?.addEventListener('click', () => {
|
||||
closeSettings()
|
||||
resetOnboarding()
|
||||
|
||||
Reference in New Issue
Block a user