App: one card tells a person with an older engine that a newer one is available, with one button (Update engine); #4848

After an app or Control update the engine is never touched, and the app said nothing. Strata Control keeps what the last update or check found newer
(update_available of 'strata components engine', Qwen engine and DeepSeek engine; read without the network); the host passes it to the app as
engine.updateAvailable and the Models screen shows one card in plain words with the existing Update engine job as its button (disabled while a job
runs or a model is loaded). A DeepSeek engine update is reported as an engine update, not as 'Strata is updated'. JavaScript only; the Control side is
on f-0291-keepcurrent.
This commit is contained in:
Niko1221 committed 2026-11-01 00:34:13 +01:00
1 parent 0dfa2d9b19
commit 27de07ceff
15 files changed
+107 -12

No files matched your search

+2 -2
View File
@@ -125,7 +125,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
const models=(list.models??[]).filter(m=>m.component_status!=='invalid'&&m.source_config).map(m=>({...(disk[m.id]??{}),model:m.id,config:basename(m.source_config),configPath:m.source_config,model_name:m.name,context:m.context_tokens??0,backend:m.backend,ready:m.component_status==='present'&&!(disk[m.id]?.partsMissing),missing:missingNames(m.missing,disk[m.id]?.missingFiles),images:'off',port:undefined,last_used:null}));
const cores=os.cpus().length;
return {summary:engine.usable?`Strata Control is ready; engine ${engine.engine.version} (${engine.engine.platform}-${engine.engine.backend}).`:(engine.notice??'No engine for this PC was found.'),
installDir:list.install_dir,engine:{usable:engine.usable,notice:engine.notice,wrongMake:engine.wrongMake===true,version:engine.engine?.version??null,backend:engine.backend,componentsDir:engine.components_dir,previous:engine.previous??null,pinned:engine.pinned??null,updateNotice:engine.update_notice??null},
installDir:list.install_dir,engine:{usable:engine.usable,notice:engine.notice,wrongMake:engine.wrongMake===true,version:engine.engine?.version??null,backend:engine.backend,componentsDir:engine.components_dir,previous:engine.previous??null,pinned:engine.pinned??null,updateNotice:engine.update_notice??null,updateAvailable:engine.update_available??null},
installed:{strata_folder:list.install_dir,data_folder:null,engine:engine.engine?{version:engine.engine.version,backend:engine.engine.backend,folder:engine.engine.root}:{folder:null,version:null},models,usage:installer.installed?.usage??null},
hardware:installer.hardware?{...installer.hardware,cpu:{...(installer.hardware.cpu??{}),cores:installer.hardware.cpu?.cores??cores},gpus:installer.hardware.gpus??[]}:{cpu:{cores},ram_gb:Math.round(os.totalmem()/1024**3),gpus:[]},
recommendation:installer.recommendation??null,installerError:installer.error??null,install:installer.install??null,running:null};
@@ -446,7 +446,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
}
return {summary:notes.filter(Boolean).join(' '),result:done};
}
function updateSummary(done){const r=(done.results??[]).filter(x=>x.status==='updated').map(x=>x.component==='engine'?'The engine is updated and ready.':'Strata is updated. It takes effect the next time you open the app.');return r.length?r.join(' '):'Everything is already up to date.';}
function updateSummary(done){const r=(done.results??[]).filter(x=>x.status==='updated').map(x=>x.component==='engine'||x.component==='dsv41'?'The engine is updated and ready.':'Strata is updated. It takes effect the next time you open the app.');return r.length?r.join(' '):'Everything is already up to date.';}
function execute(cmd,args,cwd){return new Promise((resolve,reject)=>{let output='';const process=spawnProcess(cmd,args,{cwd,stdio:['ignore','pipe','pipe']});process.stdout.on('data',c=>{output+=c;if(output.length>100000)process.kill();});process.stderr.on('data',c=>append('update',c.toString()));process.on('error',reject);process.on('exit',code=>code===0?resolve(output):reject(new Error('Git could not update this installation. Open diagnostics.')));});}
/* Every write of a model's run config keeps the person's options: those they wrote into "args" or "user_args" since the file was last written are read from the file itself, go over what the caller built, and "args_written" is refreshed. */
function writeModelConfig(path,next){const mine=mergeUserArgs(captureUserArgs(readJson(path)),next[USER_KEY]??[]);next=stampedUserArgs(next,mine);copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);}
@@ -0,0 +1,40 @@
import { useState } from 'react';
import { tr } from '../lib/i18n';
import { Btn, Icon } from './ui';
import { nativeAction, type NativeState } from '../lib/native';
/** Strata Control's last check found a newer engine than the one installed (the Qwen engine, the DeepSeek engine, or both): its numbers come from the
* check kept on this PC (`update_available` of `strata components engine`), so showing the card reads no network. After an app or Control update this is how a
* person with an older engine is told (#4848). */
export const engineUpdateOffered = (native: NativeState | null | undefined) => {
const offer = native?.engine?.updateAvailable;
return !!(offer && (offer.engine || offer.dsv41));
};
/** One card, plain words, one button. The update itself is the existing "Update engine" job (its progress shows where every job's does); models are not touched. */
export function EngineUpdateCard({ native, busy }: { native: NativeState | null | undefined; busy: boolean }) {
const [pending, setPending] = useState(false);
const [error, setError] = useState('');
if (!engineUpdateOffered(native)) return null;
const running = (native?.instances.length ?? 0) > 0;
async function update() {
setPending(true);
setError('');
try {
await nativeAction('operations', { kind: 'update-engine' });
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setPending(false);
}
}
return <div className="engine-warning engine-update" role="status">
<Icon name="info" size={18}/>
<div>
<p>{tr("A newer engine is available for this computer. Your models and settings stay as they are.")}</p>
{running ? <small>{tr("Unload your running models first.")}</small> : null}
{error ? <small className="inline-error" role="alert">{error}</small> : null}
</div>
<Btn size="sm" variant="primary" disabled={busy || pending || running} onClick={() => void update()}>{tr("Update engine")}</Btn>
</div>;
}
@@ -12,6 +12,7 @@ import { installProgress, installStage, stageText, useDownloadEta } from '../lib
import { useKept } from '../lib/kept-state';
import { DownloadParts } from './DownloadParts';
import { EngineWarning } from './EngineWarning';
import { EngineUpdateCard } from './EngineUpdateCard';
import { sizeGb } from '../lib/format';
import { NativeDeleteDialog } from './NativeDelete';
import { plainFailureText } from '../lib/plain-errors';
@@ -205,6 +206,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
{tr(selectedSize?.images ? <><div className="native-image-reading"><label><input type="checkbox" checked={vision !== 'no'} disabled={!selectedSize?.images || !!active} onChange={e => setVision(e.target.checked ? 'yes' : 'no')}/><span><strong>{tr("Read pictures ")}<small>{tr("On by default")}</small></strong><small>{tr("Understand photos, screenshots and charts. The picture files download automatically. Turn it off to save memory.")}</small></span></label>{tr(expert && vision !== 'no' ? <Select ariaLabel={tr("Image encoder device")} value={vision === 'yes' ? 'yes' : vision} options={[{ value: 'yes', label: 'Automatic' }, { value: 'gpu', label: 'GPU' }, { value: 'cpu', label: 'CPU' }]} onChange={setVision}/> : null)}</div>{tr(vision !== 'no' && installed?.ready && installed.visionPrepared === false ? <div className="native-vision-missing" role="status"><span>{tr("Picture reading needs a few more files that are not on this computer yet.")}</span><Btn size="sm" disabled={pending || !!active} onClick={() => void download()}>{tr("Download picture-reading files")}</Btn></div> : null)}</> : null)}
</> : null)}
<EngineWarning native={native}/>
<EngineUpdateCard native={native} busy={!!active}/>
{tr(active ? <NativeProgress job={active} label={jobLabel(active)}/> : null)}
{tr(job?.state === 'error' ? <ErrorNotice message={job.error ?? job.summary} action={tr("Retry")} onRetry={() => void load()}/> : null)}
{tr(error ? <ErrorNotice message={error}/> : null)}
+1 -1
View File
@@ -11,7 +11,7 @@ export interface NativeInstance {id:string;apiModelId:string;model:string;label:
export interface NativeState {
/** The picture reader (strata-vision) is installed with the engine, so models marked for pictures can read them here. */
/** Strata Control's view of the engine for this computer (the app): usable false = none found or recognised; `notice` says why in plain words. */
engine?:{usable:boolean;notice?:string|null;wrongMake?:boolean;version?:string|null};
engine?:{usable:boolean;notice?:string|null;wrongMake?:boolean;version?:string|null;/** What the last update check found newer (kept on this PC, read without the network): the Qwen engine, the DeepSeek engine, the app. */updateAvailable?:{engine?:string|null;dsv41?:string|null;app?:string|null}|null};
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;/** Free bytes on the drive that holds the models folder. */downloadFreeBytes?:number|null;downloadTotalBytes?:number|null;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';/** The Drafting choice saved with the model: auto | mtp | dflash2 | off. */drafting?:string;/** DeepSeek: the most context its pack reads (max_position_embeddings in the pack's ds4.txt); absent when the pack does not say. */contextMax?:number;dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;/** SMs x clock: only the order between cards matters (strata_control/gpu_roles.py). */speed?:number;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{/** The engine serves DFlash2 (Control says): the Drafting choice then offers it. */dflash2?:boolean;responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
let state:NativeState|null=null;
const listeners=new Set<()=>void>();
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "Schnellere Antworten auf NVIDIA-Karten mit 12 GB",
"fx83": "Bevorzugt die Teile des Modells, die schon auf der Grafikkarte liegen. Das macht Antworten auf Karten mit 12 GB schneller. Standardmäßig aus: In langen Agent-Sitzungen kann sich das Modell wiederholen und schwächere Ergebnisse liefern. Schalten Sie die Option ein, wenn Sie vor allem chatten und die Geschwindigkeit wollen.",
"fx84": "Die Datei wurde heruntergeladen. „Weiter“ versucht die Installation erneut.",
"fx85": "Unter „Details“ steht der Grund."
"fx85": "Unter „Details“ steht der Grund.",
"fxec1": "Für diesen Computer gibt es eine neuere Engine. Ihre Modelle und Einstellungen bleiben unverändert.",
"fxec2": "Beenden Sie zuerst Ihre laufenden Modelle."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "Faster answers on 12 GB NVIDIA cards",
"fx83": "Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.",
"fx84": "The file was downloaded. Continue tries the install again.",
"fx85": "Open Details to see why."
"fx85": "Open Details to see why.",
"fxec1": "A newer engine is available for this computer. Your models and settings stay as they are.",
"fxec2": "Unload your running models first."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "Respuestas más rápidas en tarjetas NVIDIA de 12 GB",
"fx83": "Prefiere las partes del modelo que ya están en la tarjeta gráfica, lo que hace las respuestas más rápidas en tarjetas de 12 GB. Desactivado por defecto: en sesiones largas de agente puede repetirse y dar resultados más débiles. Actívalo si sobre todo chateas y quieres la velocidad.",
"fx84": "El archivo se descargó. «Continuar» vuelve a intentar la instalación.",
"fx85": "Abre «Detalles» para ver el motivo."
"fx85": "Abre «Detalles» para ver el motivo.",
"fxec1": "Hay un motor más nuevo para este ordenador. Sus modelos y ajustes se conservan.",
"fxec2": "Descargue primero los modelos en ejecución."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "Réponses plus rapides sur les cartes NVIDIA de 12 Go",
"fx83": "Privilégie les parties du modèle déjà présentes sur la carte graphique, ce qui accélère les réponses sur les cartes de 12 Go. Désactivé par défaut : dans de longues sessions d’agent, le modèle peut se répéter et donner de moins bons résultats. Activez-le si vous discutez surtout et voulez la vitesse.",
"fx84": "Le fichier est téléchargé. « Continuer » relance l’installation.",
"fx85": "Ouvrez « Détails » pour voir la raison."
"fx85": "Ouvrez « Détails » pour voir la raison.",
"fxec1": "Un moteur plus récent est disponible pour cet ordinateur. Vos modèles et vos réglages ne changent pas.",
"fxec2": "Déchargez d’abord vos modèles en cours d’exécution."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "12 GB の NVIDIA カードでの高速な応答",
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答が速くなります。初期設定ではオフです。長いエージェント作業では、同じ内容を繰り返したり、結果が弱くなったりすることがあります。主にチャットで使い、速度が欲しい場合はオンにしてください。",
"fx84": "ファイルはダウンロードされました。「続ける」を押すと、もう一度インストールを試します。",
"fx85": "理由は「詳細」で確認できます。"
"fx85": "理由は「詳細」で確認できます。",
"fxec1": "このコンピューターで使える新しいエンジンがあります。モデルと設定はそのままです。",
"fxec2": "先に実行中のモデルをアンロードしてください。"
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "12GB NVIDIA 카드에서 더 빠른 응답",
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 더 빨라집니다. 기본값은 꺼짐입니다. 긴 에이전트 세션에서는 같은 말을 반복하거나 결과가 나빠질 수 있습니다. 주로 대화에 쓰고 속도를 원하면 켜세요.",
"fx84": "파일을 내려받았습니다. 「계속」을 누르면 설치를 다시 시도합니다.",
"fx85": "이유는 「자세히」에서 볼 수 있습니다."
"fx85": "이유는 「자세히」에서 볼 수 있습니다.",
"fxec1": "이 컴퓨터에 사용할 수 있는 새 엔진이 있습니다. 모델과 설정은 그대로 유지됩니다.",
"fxec2": "먼저 실행 중인 모델을 언로드하세요."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "Respostas mais rápidas em placas NVIDIA de 12 GB",
"fx83": "Prefere as partes do modelo que já estão na placa de vídeo, o que deixa as respostas mais rápidas em placas de 12 GB. Desativado por padrão: em sessões longas de agente ele pode se repetir e dar resultados mais fracos. Ative se você usa principalmente o chat e quer a velocidade.",
"fx84": "O arquivo foi baixado. «Continuar» tenta a instalação de novo.",
"fx85": "Abra «Detalhes» para ver o motivo."
"fx85": "Abra «Detalhes» para ver o motivo.",
"fxec1": "Há um motor mais recente para este computador. Seus modelos e configurações permanecem como estão.",
"fxec2": "Descarregue primeiro os modelos em execução."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "Более быстрые ответы на видеокартах NVIDIA с 12 ГБ",
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся быстрее. По умолчанию выключено: в длинных сессиях агента модель может повторяться и давать более слабые результаты. Включите, если вы в основном общаетесь в чате и хотите скорость.",
"fx84": "Файл скачан. Кнопка «Продолжить» повторит установку.",
"fx85": "Причину можно посмотреть в «Подробностях»."
"fx85": "Причину можно посмотреть в «Подробностях».",
"fxec1": "Для этого компьютера доступен более новый движок. Ваши модели и настройки останутся без изменений.",
"fxec2": "Сначала выгрузите запущенные модели."
}
+3 -1
View File
@@ -83,5 +83,7 @@
"fx82": "12 GB NVIDIA 显卡上更快的回答",
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答更快。默认关闭:在长时间的智能体会话中,它可能重复内容并给出较弱的结果。如果你主要用来聊天并想要速度,请开启。",
"fx84": "文件已下载。点击“继续”将再次尝试安装。",
"fx85": "打开“详细信息”查看原因。"
"fx85": "打开“详细信息”查看原因。",
"fxec1": "此电脑有更新的引擎可用。您的模型和设置保持不变。",
"fxec2": "请先卸载正在运行的模型。"
}
+2
View File
@@ -255,3 +255,5 @@
.engine-warning p { margin: 0; }
.engine-warning svg { flex: none; margin-top: 1px; }
.engine-warning small { display: block; margin-top: 4px; }
.engine-update { align-items: center; }
.engine-update > div { flex: 1; }
@@ -0,0 +1,33 @@
import test from 'node:test';
import { readFileSync as read } from 'node:fs';
import assert from 'node:assert/strict';
const src = name => read(new URL(`../${name}`, import.meta.url), 'utf8');
test('the host passes the kept update check to the app without asking the network', () => {
const host = src('server/native-control.mjs');
assert.match(host, /updateAvailable:engine\.update_available\?\?null/, 'the snapshot carries what `components engine` kept');
assert.match(src('src/lib/native.ts'), /updateAvailable\?:\{engine\?:string\|null;dsv41\?:string\|null;app\?:string\|null\}\|null/);
});
test('one card, one button, only when an engine (Qwen or DeepSeek) is offered, and it runs the existing Update engine job', () => {
const card = src('src/components/EngineUpdateCard.tsx');
assert.match(card, /offer && \(offer\.engine \|\| offer\.dsv41\)/);
assert.match(card, /nativeAction\('operations', \{ kind: 'update-engine' \}\)/);
assert.equal((card.match(/<Btn /g) ?? []).length, 1, 'one button');
assert.doesNotMatch(card, /fetch\(|api\.get|api\.post|XMLHttpRequest/, 'showing the card reads no network: only the kept state');
assert.match(card, /disabled=\{busy \|\| pending \|\| running\}/, 'not while a job runs or a model is loaded');
assert.doesNotMatch(card, /process\.env|STRATA_|environment variable/i, 'no environment variables in the words');
});
test('the card is on the Models screen once and nowhere else', () => {
const models = src('src/components/NativeModels.tsx');
assert.equal((models.match(/<EngineUpdateCard /g) ?? []).length, 1);
assert.match(models, /<EngineUpdateCard native=\{native\} busy=\{!!active\}\/>/);
assert.doesNotMatch(src('src/components/GuidedSetup.tsx'), /EngineUpdateCard/);
});
test('an updated DeepSeek engine is reported as an engine, not as "Strata is updated"', () => {
const host = src('server/native-control.mjs');
assert.match(host, /x\.component==='engine'\|\|x\.component==='dsv41'\?'The engine is updated and ready\.'/);
});