diff --git a/desktop/main/smoke-tasks/t18b.ts b/desktop/main/smoke-tasks/t18b.ts index 93eef0c98..d9e38a585 100644 --- a/desktop/main/smoke-tasks/t18b.ts +++ b/desktop/main/smoke-tasks/t18b.ts @@ -142,7 +142,12 @@ async function composerPull(js: Js, check: Check, w: BrowserWindow): Promise("!!(DL.ready && DL.ready.id)")) break; + } const landed = await js>(`(() => { const k = document.getElementById('wiz-key'); const u = document.getElementById('wiz-url'); return {open: SEL.open, phase: WIZ.phase, pop: !!document.querySelector('#overlays .selpop'), diff --git a/desktop/renderer/analytics-acts.js b/desktop/renderer/analytics-acts.js index aa5bdb505..62854dda9 100644 --- a/desktop/renderer/analytics-acts.js +++ b/desktop/renderer/analytics-acts.js @@ -50,7 +50,7 @@ const ANX_ACTS = (() => { tts:{auto:L}, ftoggle:L, fnew:L, frename:L, fdel:L, fpick:L, fput:L, fautosort:L, more:{chats:L, tasks:L}, task:L, toastx:L, 'turn':{continue:L}, sessmodel:{apply:L}, copy:{reply:L, session:L}, agent:{restart:L, update:L}, jump:{appr:L}, diag:{llmlogs:L, check:L, startModel:L}, - dl:{cancel:L}, dlc:{cloud:L, dismiss:L, fold:L, keep:L, retry:L, switch:L, unfold:L}, + dl:{cancel:L}, dlc:{cloud:L, dismiss:L, fold:L, keep:L, retry:L, switch:L, unfold:L, visionLater:L, visionRestart:L}, appr:{y:L, n:L, esc:L, s:L, a:L}, selector:{model:L}, cards:{expand:L, collapse:L}, room:{chat:L, tasks:L, skills:L}, insp:{steps:L, reasoning:L, world:L}, console:{agent:L, llm:L}, toggle:{sidebar:L, inspector:L, console:L, files:L}, diff --git a/desktop/renderer/renderer.js b/desktop/renderer/renderer.js index 15ea8c005..0185f7fd5 100644 --- a/desktop/renderer/renderer.js +++ b/desktop/renderer/renderer.js @@ -787,6 +787,12 @@ const DL = { const SPOT = {gate: false, ids: null, asked: false, dismissed: null}; /* Backlog 18 — the download card's own state (renderDlcard). Declared up here for the same reason as DL: the first top-level render() reads it. */ +/* ATO-1373: a vision projector landed for the model that is serving now. The + projector is read only when a model starts, so the running one still has no + vision. The download card offers a Restart model instead of restarting it + unasked: a restart would end a reply in progress (llmRestartModel refuses + under a running turn). {id, pid, busy, error} or null. */ +const VISION = {restart: null}; const DLC = { /** Folded to the round badge. A fold lasts one run of downloads: the next run, after the card has been empty, starts open (DownloadPanel). */ @@ -13249,6 +13255,11 @@ function dlCardRows() { } // F8: a vision projector waiting for its turn (dlProjectorPark). Its × takes it off. DL.projectorQueue.forEach((p) => rows.push({src:'dl', kind:'projector', id: p.id, state:'queued', cancel:'dlc:drop:projector:' + p.id})); + // ATO-1373: the serving model's projector landed; a restart makes it read images. + const vr = VISION.restart; + if (vr && !rows.some((r) => r.kind === 'weights' && r.id === vr.id)) { + rows.push({src:'restart', kind:'weights', id: vr.id, state: vr.busy ? 'restarting' : 'restart', error: vr.error}); + } // Backlog 18: landed while the agent was on another model; it waits for a Switch. if (DL.ready && !rows.some((r) => r.kind === 'weights' && r.id === DL.ready.id)) { rows.push({src:'ready', kind:'weights', id: DL.ready.id, state:'ready'}); @@ -13313,6 +13324,8 @@ function dlCardPercent(r) { function dlCardLine(r) { if (r.state === 'failed') return r.error; if (r.state === 'ready') return 'Downloaded. The agent stays on its current model until you switch.'; + if (r.state === 'restart') return r.error || 'Restart the model to read images.'; + if (r.state === 'restarting') return 'Restarting the model…'; const lead = r.kind === 'projector' ? 'Vision projector · ' : ''; if (r.state === 'queued') return lead + 'Queued'; if (r.state === 'cancelling') return 'Cancelling…'; @@ -13327,6 +13340,17 @@ function dlCardLine(r) { function dlCardRowHTML(r, i) { const name = dlCardName(r); + if (r.state === 'restart' || r.state === 'restarting') { + // ATO-1373: no bar, the files are on disk. The restart is the person's to press. + const busy = r.state === 'restarting'; + return '
  • ' + + '
    ' + esc('Vision files for ' + name + ' are ready') + '' + + '' + + (busy ? '' : '') + '
    ' + + '

    ' + esc(dlCardLine(r)) + '

  • '; + } if (r.state === 'ready') { // No bar: there is nothing left to move. Switch is the start it did not get by itself. return '
  • ' @@ -13352,17 +13376,22 @@ function dlCardRowHTML(r, i) { + '
  • '; } +/** A row whose bytes are still coming: not failed, not landed, not a restart offer (ATO-1373). */ +function dlRowMoving(r) { + return r.state !== 'failed' && r.state !== 'ready' && r.src !== 'restart'; +} + /** Open: a header with the count and the fold, the rows, the cloud offer. */ function dlCardHTML(rows) { - const moving = rows.some((r) => r.state !== 'failed' && r.state !== 'ready'); + const moving = rows.some((r) => dlRowMoving(r)); const failed = rows.some((r) => r.state === 'failed'); - const ready = rows.some((r) => r.state === 'ready'); + const ready = rows.some((r) => r.state === 'ready' || r.src === 'restart'); const n = rows.length; /* The setup screen's "set up a cloud model meanwhile" offer moved here with the download it was about. Not for an embedding model: nothing waits on it. Not beside a ready row alone either: that model landed while the agent was already on another one. */ - const offer = (moving || failed) && rows.some((r) => r.kind !== 'embedding' && r.state !== 'ready'); + const offer = (moving || failed) && rows.some((r) => r.kind !== 'embedding' && r.state !== 'ready' && r.src !== 'restart'); const title = moving ? 'Downloading' : failed && ready ? 'Downloads' : failed ? 'Download failed' : 'Download complete'; const glyph = moving ? ic('download') : failed ? ic('alert') : ic('check'); return '
    ' @@ -13381,7 +13410,7 @@ function dlCardHTML(rows) { /** Folded: a round button that is still a live count. Red when one failed. */ function dlCardBadgeHTML(rows) { const failed = rows.some((r) => r.state === 'failed'); - const moving = rows.some((r) => r.state !== 'failed' && r.state !== 'ready'); + const moving = rows.some((r) => dlRowMoving(r)); return '