mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
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:
1 parent
0dfa2d9b19
commit
27de07ceff
15 files changed
+107
-12
No files matched your search
@@ -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)}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -83,5 +83,7 @@
|
||||
"fx82": "12 GB の NVIDIA カードでの高速な応答",
|
||||
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答が速くなります。初期設定ではオフです。長いエージェント作業では、同じ内容を繰り返したり、結果が弱くなったりすることがあります。主にチャットで使い、速度が欲しい場合はオンにしてください。",
|
||||
"fx84": "ファイルはダウンロードされました。「続ける」を押すと、もう一度インストールを試します。",
|
||||
"fx85": "理由は「詳細」で確認できます。"
|
||||
"fx85": "理由は「詳細」で確認できます。",
|
||||
"fxec1": "このコンピューターで使える新しいエンジンがあります。モデルと設定はそのままです。",
|
||||
"fxec2": "先に実行中のモデルをアンロードしてください。"
|
||||
}
|
||||
@@ -83,5 +83,7 @@
|
||||
"fx82": "12GB NVIDIA 카드에서 더 빠른 응답",
|
||||
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 더 빨라집니다. 기본값은 꺼짐입니다. 긴 에이전트 세션에서는 같은 말을 반복하거나 결과가 나빠질 수 있습니다. 주로 대화에 쓰고 속도를 원하면 켜세요.",
|
||||
"fx84": "파일을 내려받았습니다. 「계속」을 누르면 설치를 다시 시도합니다.",
|
||||
"fx85": "이유는 「자세히」에서 볼 수 있습니다."
|
||||
"fx85": "이유는 「자세히」에서 볼 수 있습니다.",
|
||||
"fxec1": "이 컴퓨터에 사용할 수 있는 새 엔진이 있습니다. 모델과 설정은 그대로 유지됩니다.",
|
||||
"fxec2": "먼저 실행 중인 모델을 언로드하세요."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -83,5 +83,7 @@
|
||||
"fx82": "Более быстрые ответы на видеокартах NVIDIA с 12 ГБ",
|
||||
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся быстрее. По умолчанию выключено: в длинных сессиях агента модель может повторяться и давать более слабые результаты. Включите, если вы в основном общаетесь в чате и хотите скорость.",
|
||||
"fx84": "Файл скачан. Кнопка «Продолжить» повторит установку.",
|
||||
"fx85": "Причину можно посмотреть в «Подробностях»."
|
||||
"fx85": "Причину можно посмотреть в «Подробностях».",
|
||||
"fxec1": "Для этого компьютера доступен более новый движок. Ваши модели и настройки останутся без изменений.",
|
||||
"fxec2": "Сначала выгрузите запущенные модели."
|
||||
}
|
||||
@@ -83,5 +83,7 @@
|
||||
"fx82": "12 GB NVIDIA 显卡上更快的回答",
|
||||
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答更快。默认关闭:在长时间的智能体会话中,它可能重复内容并给出较弱的结果。如果你主要用来聊天并想要速度,请开启。",
|
||||
"fx84": "文件已下载。点击“继续”将再次尝试安装。",
|
||||
"fx85": "打开“详细信息”查看原因。"
|
||||
"fx85": "打开“详细信息”查看原因。",
|
||||
"fxec1": "此电脑有更新的引擎可用。您的模型和设置保持不变。",
|
||||
"fxec2": "请先卸载正在运行的模型。"
|
||||
}
|
||||
@@ -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\.'/);
|
||||
});
|
||||
Reference in new issue
Block a user