1. engine.beginGeneration() — clears the abort latch at the start of each send 2. Stop unlocks the UI immediately — increments a runId, shows Send again, status: “Stopped — ready for a new message” 3. In-flight turn is invalidated — late stream/tool updates ignore a superseded run 4. New chat — stops any busy turn and resets the same way 5. Cancel timeout — IPC cancel won’t hang Stop for more than ~2.5s
This commit is contained in:
+15
-1
@@ -103,13 +103,18 @@ export function createQvacEngine(deps) {
|
|||||||
/**
|
/**
|
||||||
* Stop in-flight inference (and pending tool rounds).
|
* Stop in-flight inference (and pending tool rounds).
|
||||||
* Safe to call when idle.
|
* Safe to call when idle.
|
||||||
|
* Leaves abort latch set until {@link beginGeneration} (start of next send).
|
||||||
*/
|
*/
|
||||||
async function stop() {
|
async function stop() {
|
||||||
abortGeneration = true
|
abortGeneration = true
|
||||||
const ipc = getElectronIpc()
|
const ipc = getElectronIpc()
|
||||||
try {
|
try {
|
||||||
if (ipc && sdkMode === 'main') {
|
if (ipc && sdkMode === 'main') {
|
||||||
await ipc.invoke('peardata:qvac-cancel')
|
// Don't hang the UI if cancel is slow — race a short timeout
|
||||||
|
await Promise.race([
|
||||||
|
ipc.invoke('peardata:qvac-cancel'),
|
||||||
|
new Promise((r) => setTimeout(r, 2500)),
|
||||||
|
])
|
||||||
} else if (sdk?.cancel) {
|
} else if (sdk?.cancel) {
|
||||||
if (activeRequestId) {
|
if (activeRequestId) {
|
||||||
try {
|
try {
|
||||||
@@ -136,6 +141,12 @@ export function createQvacEngine(deps) {
|
|||||||
return abortGeneration
|
return abortGeneration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clear stop latch — call at the start of each user send. */
|
||||||
|
function beginGeneration() {
|
||||||
|
abortGeneration = false
|
||||||
|
activeRequestId = null
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
let sdk = null
|
let sdk = null
|
||||||
/** @type {'direct'|'main'|null} */
|
/** @type {'direct'|'main'|null} */
|
||||||
@@ -483,6 +494,8 @@ export function createQvacEngine(deps) {
|
|||||||
*/
|
*/
|
||||||
async function complete(history, opts = {}) {
|
async function complete(history, opts = {}) {
|
||||||
touchActivity()
|
touchActivity()
|
||||||
|
// Fresh generation for this complete() call (stop during prior turns is cleared).
|
||||||
|
// UI also calls beginGeneration() at send start so multi-agent can run before this.
|
||||||
abortGeneration = false
|
abortGeneration = false
|
||||||
const profile = getProfile(profileId || 'recommended')
|
const profile = getProfile(profileId || 'recommended')
|
||||||
const prefs = deps.getPrefs?.() || {}
|
const prefs = deps.getPrefs?.() || {}
|
||||||
@@ -873,6 +886,7 @@ export function createQvacEngine(deps) {
|
|||||||
complete,
|
complete,
|
||||||
stop,
|
stop,
|
||||||
isAborted,
|
isAborted,
|
||||||
|
beginGeneration,
|
||||||
getStatus,
|
getStatus,
|
||||||
tryLoadSdk,
|
tryLoadSdk,
|
||||||
touchActivity,
|
touchActivity,
|
||||||
|
|||||||
+55
-14
@@ -112,6 +112,8 @@ export function createQvacView(opts) {
|
|||||||
/** @type {Array<{ role: string, content: string, tools?: any[] }>} */
|
/** @type {Array<{ role: string, content: string, tools?: any[] }>} */
|
||||||
let messages = []
|
let messages = []
|
||||||
let busy = false
|
let busy = false
|
||||||
|
/** Invalidates in-flight send when Stop is pressed so UI unlocks immediately. */
|
||||||
|
let runId = 0
|
||||||
let wizardStep = 0
|
let wizardStep = 0
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
let selectedProfile = 'recommended'
|
let selectedProfile = 'recommended'
|
||||||
@@ -809,6 +811,17 @@ export function createQvacView(opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock composer after Stop even if the in-flight send is still winding down.
|
||||||
|
* Also clears the abort latch only after a new send begins (see beginGeneration).
|
||||||
|
*/
|
||||||
|
function finishStoppedUi() {
|
||||||
|
runId += 1
|
||||||
|
setBusyUi(false)
|
||||||
|
setStatus('Stopped — ready for a new message', 'warn')
|
||||||
|
syncModelChip()
|
||||||
|
}
|
||||||
|
|
||||||
async function stopInference() {
|
async function stopInference() {
|
||||||
if (!busy) return
|
if (!busy) return
|
||||||
setStatus('Stopping…', 'busy')
|
setStatus('Stopping…', 'busy')
|
||||||
@@ -818,6 +831,8 @@ export function createQvacView(opts) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
opts.log?.(`QVAC stop: ${err?.message || err}`)
|
opts.log?.(`QVAC stop: ${err?.message || err}`)
|
||||||
}
|
}
|
||||||
|
// Always re-enable chat immediately — do not wait for hung tools / stream
|
||||||
|
finishStoppedUi()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function send() {
|
async function send() {
|
||||||
@@ -825,6 +840,9 @@ export function createQvacView(opts) {
|
|||||||
const text = (input?.value || '').trim()
|
const text = (input?.value || '').trim()
|
||||||
if (!text || busy) return
|
if (!text || busy) return
|
||||||
if (input) input.value = ''
|
if (input) input.value = ''
|
||||||
|
// Clear previous Stop latch so this turn can run
|
||||||
|
engine.beginGeneration?.()
|
||||||
|
const myRun = ++runId
|
||||||
setBusyUi(true)
|
setBusyUi(true)
|
||||||
appendMsg('user', text)
|
appendMsg('user', text)
|
||||||
const assistantUi = appendMsg('assistant', '…', { streaming: true })
|
const assistantUi = appendMsg('assistant', '…', { streaming: true })
|
||||||
@@ -837,6 +855,7 @@ export function createQvacView(opts) {
|
|||||||
// Hide legacy sticky bar if still in DOM
|
// Hide legacy sticky bar if still in DOM
|
||||||
opts.els.agentBar?.classList.add('hidden')
|
opts.els.agentBar?.classList.add('hidden')
|
||||||
opts.els.agentBar && (opts.els.agentBar.innerHTML = '')
|
opts.els.agentBar && (opts.els.agentBar.innerHTML = '')
|
||||||
|
const stillThisRun = () => myRun === runId
|
||||||
try {
|
try {
|
||||||
// History for the model: clean answers only (no think tags)
|
// History for the model: clean answers only (no think tags)
|
||||||
const hist = messages
|
const hist = messages
|
||||||
@@ -894,11 +913,13 @@ export function createQvacView(opts) {
|
|||||||
followMessagesScroll()
|
followMessagesScroll()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (engine.isAborted?.()) {
|
if (engine.isAborted?.() || !stillThisRun()) {
|
||||||
swarm.fail('Stopped')
|
if (stillThisRun()) {
|
||||||
acc = '_(Stopped during multi-agent investigation.)_'
|
swarm.fail('Stopped')
|
||||||
paintAssistant(streamBody, acc, false)
|
acc = '_(Stopped during multi-agent investigation.)_'
|
||||||
setStatus('Stopped', 'warn')
|
paintAssistant(streamBody, acc, false)
|
||||||
|
setStatus('Stopped — ready for a new message', 'warn')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
swarm.complete('All agents done — synthesizing answer…')
|
swarm.complete('All agents done — synthesizing answer…')
|
||||||
@@ -929,20 +950,24 @@ export function createQvacView(opts) {
|
|||||||
followMessagesScroll({ force: true })
|
followMessagesScroll({ force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (engine.isAborted?.()) {
|
if (engine.isAborted?.() || !stillThisRun()) {
|
||||||
acc = '_(Stopped.)_'
|
if (stillThisRun()) {
|
||||||
paintAssistant(streamBody, acc, false)
|
acc = '_(Stopped.)_'
|
||||||
setStatus('Stopped', 'warn')
|
paintAssistant(streamBody, acc, false)
|
||||||
|
setStatus('Stopped — ready for a new message', 'warn')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await engine.complete(hist, {
|
const result = await engine.complete(hist, {
|
||||||
...completeOpts,
|
...completeOpts,
|
||||||
onToken: (t) => {
|
onToken: (t) => {
|
||||||
|
if (!stillThisRun()) return
|
||||||
acc += t
|
acc += t
|
||||||
paintAssistant(streamBody, mergeThinkStream(thinkingAcc, acc), true)
|
paintAssistant(streamBody, mergeThinkStream(thinkingAcc, acc), true)
|
||||||
},
|
},
|
||||||
onThinking: (t) => {
|
onThinking: (t) => {
|
||||||
|
if (!stillThisRun()) return
|
||||||
thinkingAcc += t
|
thinkingAcc += t
|
||||||
const raw = thinkingAcc
|
const raw = thinkingAcc
|
||||||
? `<think>\n${thinkingAcc}\n</think>\n${acc}`
|
? `<think>\n${thinkingAcc}\n</think>\n${acc}`
|
||||||
@@ -950,7 +975,7 @@ export function createQvacView(opts) {
|
|||||||
paintAssistant(streamBody, raw, true)
|
paintAssistant(streamBody, raw, true)
|
||||||
},
|
},
|
||||||
onTool: (name, args, res) => {
|
onTool: (name, args, res) => {
|
||||||
if (engine.isAborted?.()) return
|
if (engine.isAborted?.() || !stillThisRun()) return
|
||||||
toolLog.push({ name, args, result: res })
|
toolLog.push({ name, args, result: res })
|
||||||
setStatus(`Tool: ${name}…`, 'busy')
|
setStatus(`Tool: ${name}…`, 'busy')
|
||||||
if (assistantUi?.div) {
|
if (assistantUi?.div) {
|
||||||
@@ -961,6 +986,7 @@ export function createQvacView(opts) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if (!stillThisRun()) return
|
||||||
acc = result.contentText || acc
|
acc = result.contentText || acc
|
||||||
if (thinkingAcc && !/<think/i.test(acc)) {
|
if (thinkingAcc && !/<think/i.test(acc)) {
|
||||||
acc = `<think>\n${thinkingAcc}\n</think>\n${acc}`
|
acc = `<think>\n${thinkingAcc}\n</think>\n${acc}`
|
||||||
@@ -1004,11 +1030,12 @@ export function createQvacView(opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.stopped || result.mode === 'stopped') {
|
if (result.stopped || result.mode === 'stopped') {
|
||||||
setStatus('Stopped', 'warn')
|
setStatus('Stopped — ready for a new message', 'warn')
|
||||||
} else {
|
} else {
|
||||||
setStatus(result.mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
setStatus(result.mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!stillThisRun()) return
|
||||||
const msg = err?.message || String(err)
|
const msg = err?.message || String(err)
|
||||||
if (/cancel|abort|stopped/i.test(msg)) {
|
if (/cancel|abort|stopped/i.test(msg)) {
|
||||||
if (streamBody && !acc) {
|
if (streamBody && !acc) {
|
||||||
@@ -1016,14 +1043,17 @@ export function createQvacView(opts) {
|
|||||||
} else if (streamBody && acc) {
|
} else if (streamBody && acc) {
|
||||||
paintAssistant(streamBody, acc + '\n\n_(Stopped.)_', false)
|
paintAssistant(streamBody, acc + '\n\n_(Stopped.)_', false)
|
||||||
}
|
}
|
||||||
setStatus('Stopped', 'warn')
|
setStatus('Stopped — ready for a new message', 'warn')
|
||||||
} else {
|
} else {
|
||||||
if (streamBody) streamBody.innerHTML = formatMdLite(`Error: ${msg}`)
|
if (streamBody) streamBody.innerHTML = formatMdLite(`Error: ${msg}`)
|
||||||
setStatus(msg, 'error')
|
setStatus(msg, 'error')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setBusyUi(false)
|
// Only the active run owns the composer; Stop already unlocked via finishStoppedUi
|
||||||
syncModelChip()
|
if (stillThisRun()) {
|
||||||
|
setBusyUi(false)
|
||||||
|
syncModelChip()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,6 +1066,14 @@ export function createQvacView(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function newChat() {
|
function newChat() {
|
||||||
|
// Cancel any in-flight turn and unlock composer
|
||||||
|
if (busy) {
|
||||||
|
engine.stop?.().catch(() => {})
|
||||||
|
finishStoppedUi()
|
||||||
|
} else {
|
||||||
|
engine.beginGeneration?.()
|
||||||
|
runId += 1
|
||||||
|
}
|
||||||
releaseLiveSwarm()
|
releaseLiveSwarm()
|
||||||
messages = []
|
messages = []
|
||||||
if (opts.els.messages) opts.els.messages.innerHTML = ''
|
if (opts.els.messages) opts.els.messages.innerHTML = ''
|
||||||
@@ -1045,6 +1083,9 @@ export function createQvacView(opts) {
|
|||||||
'assistant',
|
'assistant',
|
||||||
'New chat. Ask about host health, metrics, anomalies, or processes.'
|
'New chat. Ask about host health, metrics, anomalies, or processes.'
|
||||||
)
|
)
|
||||||
|
const mode = settings().qvacMode
|
||||||
|
setStatus(mode === 'qvac' ? 'Ready' : mode === 'fallback' ? 'Ready (tools-only)' : 'Ready', 'ok')
|
||||||
|
syncModelChip()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetOnboarding() {
|
function resetOnboarding() {
|
||||||
|
|||||||
Reference in New Issue
Block a user