From 9e7a290a79ce3e58b63b202def88b299167f0070 Mon Sep 17 00:00:00 2001 From: Nerual Dreming Date: Wed, 14 Oct 2026 10:54:10 +0300 Subject: [PATCH 01/30] The report link opens the issue forms with this computer filled in Settings, About, "Report issues" led to the list of issues; it opens GitHub's choice of forms now, with the studio's version, system, graphics card and model set passed to the fields of the same id. --- app/components/SettingsModal.tsx | 5 ++++- app/services/issueReport.ts | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 app/services/issueReport.ts diff --git a/app/components/SettingsModal.tsx b/app/components/SettingsModal.tsx index 5820a11b..92a04bd8 100644 --- a/app/components/SettingsModal.tsx +++ b/app/components/SettingsModal.tsx @@ -13,6 +13,8 @@ import { AgentPanel } from './AgentPanel'; import { HubSettings } from './HubSettings'; import { STUDIO, STUDIO_REPO_URL } from '../studio'; import { isDesktop } from '../services/externalLinks'; +import { issueReportUrl, type ReportFacts } from '../services/issueReport'; +import { useSetupStatus } from '../services/studioQueries'; import { onSidebarExtras, setSidebarExtras, sidebarExtras, type SidebarExtras } from '../services/playerPanels'; /** @@ -46,6 +48,7 @@ export const SettingsModal: React.FC = ({ isOpen, initialSec const { user, setDisplayName } = useAuth(); const { t, language, setLanguage } = useI18n(); const [section, setSection] = useState('account'); + const reportFacts = useSetupStatus().data; /// A part of the page to open at, after a colon: `models:assistant`. const [focus, setFocus] = useState(null); // Opened from somewhere with a page in mind - the profile line, say. @@ -262,7 +265,7 @@ export const SettingsModal: React.FC = ({ isOpen, initialSec {STUDIO.name} - {t('reportIssues')} + {t('reportIssues')} diff --git a/app/services/issueReport.ts b/app/services/issueReport.ts new file mode 100644 index 00000000..9f92d010 --- /dev/null +++ b/app/services/issueReport.ts @@ -0,0 +1,34 @@ +/** What the bug form on GitHub asks about this computer, as far as the studio knows it. */ +export interface ReportFacts { + hardware?: { gpuName?: string; totalVramGb?: number; totalRamGb?: number }; + selected_profile_id?: string | null; + portable?: boolean; +} + +function system(portable: boolean | undefined): string { + const agent = navigator.userAgent; + const name = /Windows/.test(agent) ? 'Windows' : /Mac OS X|Macintosh/.test(agent) ? 'macOS' : /Linux/.test(agent) ? 'Linux' : ''; + return portable ? `${name} (portable)`.trim() : name; +} + +/** + * GitHub's choice of forms (bug, idea, question) with what the studio knows + * passed along. The keys are the `id`s of the text fields in + * `.github/ISSUE_TEMPLATE/*.yml`; the chosen form fills a field from the query + * parameter of the same name. + */ +export function issueReportUrl(repositoryUrl: string, version: string, facts?: ReportFacts): string { + const query = new URLSearchParams({ version }); + const os = system(facts?.portable); + if (os) query.set('os', os); + const hardware = facts?.hardware; + if (hardware?.gpuName) { + const memory = [ + hardware.totalVramGb ? `${Math.round(hardware.totalVramGb)} GB` : '', + hardware.totalRamGb ? `RAM ${Math.round(hardware.totalRamGb)} GB` : '', + ].filter(Boolean).join(', '); + query.set('gpu', memory ? `${hardware.gpuName}, ${memory}` : hardware.gpuName); + } + if (facts?.selected_profile_id) query.set('models', facts.selected_profile_id); + return `${repositoryUrl}/issues/new/choose?${query.toString()}`; +} From aabe8b5df1d1f782328005add6b50ed2d9089180 Mon Sep 17 00:00:00 2001 From: Nerual Dreming Date: Wed, 14 Oct 2026 13:15:02 +0300 Subject: [PATCH 02/30] Statistics: every call to the studio's API by route, model downloads ready or failed, and song speed when the engine had the song to itself --- Cargo.lock | 2 +- app/i18n/notices.ts | 10 +++---- crates/music-server/Cargo.toml | 2 +- crates/music-server/src/lib.rs | 36 ++++++++++++++++++++---- crates/music-server/src/model_manager.rs | 15 ++++++++++ 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b73d037..ee487b5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3053,7 +3053,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "studio-hub-client" version = "0.1.0" -source = "git+https://github.com/timoncool/studio-hub-client?rev=531327a64882580bd44f8e173908be0a046c7adc#531327a64882580bd44f8e173908be0a046c7adc" +source = "git+https://github.com/timoncool/studio-hub-client?rev=0d158def13c07455c99fe5347738c623cca090dd#0d158def13c07455c99fe5347738c623cca090dd" dependencies = [ "axum", "chrono", diff --git a/app/i18n/notices.ts b/app/i18n/notices.ts index 01f9a473..73f024a8 100644 --- a/app/i18n/notices.ts +++ b/app/i18n/notices.ts @@ -6,7 +6,7 @@ const en = { hubAd: 'Ad', hubTelemetryTitle: 'Anonymous statistics', hubTelemetryCheckbox: 'Send anonymous usage statistics', - hubTelemetryWhy: 'How many people use the studio and what for helps decide what to work on next. Nothing personal: no lyrics, prompts, audio or file names — only counts, the model sets used, the version, the graphics card class and why something failed, with paths, names and quoted text cut out on this computer before it is sent.', + hubTelemetryWhy: 'How many people use the studio and what for helps decide what to work on next. Nothing personal: no lyrics, prompts, audio or file names — only counts, which of the studio's functions were used (by name, never with what), the model sets used, the version, the graphics card class and why something failed, with paths, names and quoted text cut out on this computer before it is sent.', hubTelemetryWhat: 'What is sent', hubTelemetryEnabled: 'Statistics are on', hubTelemetryDisabled: 'Statistics are off', @@ -27,7 +27,7 @@ const ru: Strings = { hubAd: 'Реклама', hubTelemetryTitle: 'Анонимная статистика', hubTelemetryCheckbox: 'Отправлять анонимную статистику использования', - hubTelemetryWhy: 'Сколько людей пользуется студией и для чего — так понятно, что делать дальше. Ничего личного: ни текстов, ни промптов, ни звука, ни имён файлов — только счётчики, наборы моделей, версия, класс видеокарты и причина сбоя, из которой пути, имена и всё в кавычках вырезаются на этом компьютере до отправки.', + hubTelemetryWhy: 'Сколько людей пользуется студией и для чего — так понятно, что делать дальше. Ничего личного: ни текстов, ни промптов, ни звука, ни имён файлов — только счётчики, какие функции студии использовались (по названию, без содержимого), наборы моделей, версия, класс видеокарты и причина сбоя, из которой пути, имена и всё в кавычках вырезаются на этом компьютере до отправки.', hubTelemetryWhat: 'Что отправляется', hubTelemetryEnabled: 'Статистика включена', hubTelemetryDisabled: 'Статистика выключена', @@ -46,7 +46,7 @@ const zh: Strings = { hubAd: '广告', hubTelemetryTitle: '匿名统计', hubTelemetryCheckbox: '发送匿名使用统计', - hubTelemetryWhy: '有多少人在用、用来做什么,决定接下来做什么。不含任何个人信息:没有歌词、提示词、音频或文件名——只有计数、所用模型组合、版本、显卡档次和失败原因;原因中的路径、名称和引号内文字会在发送前于本机删除。', + hubTelemetryWhy: '有多少人在用、用来做什么,决定接下来做什么。不含任何个人信息:没有歌词、提示词、音频或文件名——只有计数、用过哪些功能(仅功能名,不含内容)、所用模型组合、版本、显卡档次和失败原因;原因中的路径、名称和引号内文字会在发送前于本机删除。', hubTelemetryWhat: '发送的内容', hubTelemetryEnabled: '统计已开启', hubTelemetryDisabled: '统计已关闭', @@ -65,7 +65,7 @@ const ja: Strings = { hubAd: '広告', hubTelemetryTitle: '匿名の統計', hubTelemetryCheckbox: '匿名の利用統計を送信する', - hubTelemetryWhy: '何人がどんな用途で使っているかで、次に取り組むことを決めます。個人的な情報は一切なし:歌詞、プロンプト、音声、ファイル名は送らず、回数、使ったモデル構成、バージョン、GPU のクラス、失敗の理由だけです。理由に含まれるパス、名前、引用符内の文字は送信前にこの PC で取り除きます。', + hubTelemetryWhy: '何人がどんな用途で使っているかで、次に取り組むことを決めます。個人的な情報は一切なし:歌詞、プロンプト、音声、ファイル名は送らず、回数、使った機能(機能名のみ、中身は送りません)、使ったモデル構成、バージョン、GPU のクラス、失敗の理由だけです。理由に含まれるパス、名前、引用符内の文字は送信前にこの PC で取り除きます。', hubTelemetryWhat: '送信される内容', hubTelemetryEnabled: '統計はオンです', hubTelemetryDisabled: '統計はオフです', @@ -84,7 +84,7 @@ const ko: Strings = { hubAd: '광고', hubTelemetryTitle: '익명 통계', hubTelemetryCheckbox: '익명 사용 통계 보내기', - hubTelemetryWhy: '몇 명이 어떤 용도로 쓰는지로 다음에 할 일을 정합니다. 개인 정보는 없습니다: 가사, 프롬프트, 오디오, 파일 이름은 보내지 않고 횟수, 사용한 모델 구성, 버전, 그래픽 카드 등급, 실패 이유만 보냅니다. 이유 속 경로, 이름, 따옴표 안 글은 보내기 전에 이 컴퓨터에서 지웁니다.', + hubTelemetryWhy: '몇 명이 어떤 용도로 쓰는지로 다음에 할 일을 정합니다. 개인 정보는 없습니다: 가사, 프롬프트, 오디오, 파일 이름은 보내지 않고 횟수, 사용한 기능(기능 이름만, 내용 없음), 사용한 모델 구성, 버전, 그래픽 카드 등급, 실패 이유만 보냅니다. 이유 속 경로, 이름, 따옴표 안 글은 보내기 전에 이 컴퓨터에서 지웁니다.', hubTelemetryWhat: '보내는 내용', hubTelemetryEnabled: '통계가 켜져 있습니다', hubTelemetryDisabled: '통계가 꺼져 있습니다', diff --git a/crates/music-server/Cargo.toml b/crates/music-server/Cargo.toml index a4e2b6de..02f06a9b 100644 --- a/crates/music-server/Cargo.toml +++ b/crates/music-server/Cargo.toml @@ -44,7 +44,7 @@ sha2.workspace = true symphonia.workspace = true sysinfo.workspace = true # Studio Hub: anonymous statistics and the news feed (telemetry only after the start screen showed its choice). -studio-hub-client = { git = "https://github.com/timoncool/studio-hub-client", rev = "531327a64882580bd44f8e173908be0a046c7adc" } +studio-hub-client = { git = "https://github.com/timoncool/studio-hub-client", rev = "0d158def13c07455c99fe5347738c623cca090dd" } ort.workspace = true realfft = "3.5" regex = "1" diff --git a/crates/music-server/src/lib.rs b/crates/music-server/src/lib.rs index 11434ce9..9ebeb02e 100644 --- a/crates/music-server/src/lib.rs +++ b/crates/music-server/src/lib.rs @@ -873,6 +873,9 @@ pub async fn serve() -> anyhow::Result<()> { pin_placeholders(state).await; }); } + if let Some(hub) = &state.hub { + state.model_manager.report_to(hub.clone()); + } let app = Router::new() .route("/health", get(health)) @@ -1039,6 +1042,11 @@ pub async fn serve() -> anyhow::Result<()> { // two-megabyte cap rejected a generated cover by dropping the // connection, which reaches the interface as "Failed to fetch". .layer(DefaultBodyLimit::max(256 * 1024 * 1024)); + // counted before the MCP tools take the router, so an agent's calls count as the features they use + let app = match &state.hub { + Some(hub) => hub.track(app), + None => app, + }; // the MCP tools call the same routes, inside the process mcp::install(app.clone()); let app = app @@ -7365,10 +7373,15 @@ async fn run_ace_job(state: AppState, run: AceRun) { let job_id = run.job_id.clone(); // the parts as they were when the song started: the person may switch sets while it is made let parts_at_start = state.selected_component_ids.read().await.clone().unwrap_or_default(); + // timed only when nothing else held the engine: a song waiting its turn would read as a slow card + let alone = !another_job_under_way(&state, &job_id).await; + let started = std::time::Instant::now(); let outcome = { let _using = state.engine_use.read().await; ace_job(&state, run).await }; + let worked = started.elapsed(); + let work = (alone && !another_job_under_way(&state, &job_id).await).then_some(worked); let cancelled = job_cancelled(&state, &job_id).await; // The engine died under the song - a refused or dropped connection, or a // restarted engine that no longer knows the job: its own log says why @@ -7422,13 +7435,16 @@ async fn run_ace_job(state: AppState, run: AceRun) { // however it ended, it is no longer one the studio's closing could cut off let ended = state.jobs.read().await.get(&job_id).map(|job| { let profiles: Vec = job.songs.iter().filter_map(|made| made.song.profile_id.clone()).collect(); - (job_status_name(&job.status), job.message.clone(), job.songs.len(), profiles) + (job_status_name(&job.status), job.message.clone(), job.songs.len(), profiles, songs_seconds(&job.songs)) }); if let Some(hub) = &state.hub { match ended.as_ref() { - Some(("completed", _, songs, profiles)) if *songs > 0 => { + Some(("completed", _, songs, profiles, seconds)) if *songs > 0 => { hub.count("songs", *songs as u64); hub.count("song_done", 1); + if let Some(work) = work { + hub.worked("song", *seconds, work); + } for profile in profiles { hub.used_model(profile); } @@ -7440,15 +7456,15 @@ async fn run_ace_job(state: AppState, run: AceRun) { }; hub.used_models(set.as_deref(), &parts, *songs as u64); } - Some(("failed", message, _, _)) => { + Some(("failed", message, ..)) => { hub.count("song_failed", 1); hub.failed("song", message); } - Some(("cancelled", _, _, _)) => hub.count("song_cancelled", 1), + Some(("cancelled", ..)) => hub.count("song_cancelled", 1), _ => {} } } - if let Some((status, message, _, _)) = ended { + if let Some((status, message, ..)) = ended { // a song that reached the library needs no record of its request any more let kept = if status == "completed" { state.library.forget_music_job(&job_id) } else { state.library.set_music_job_status(&job_id, status, &message) }; if let Err(error) = kept { @@ -7457,6 +7473,16 @@ async fn run_ace_job(state: AppState, run: AceRun) { } } +/// Whether another song is queued or being made besides this one. +async fn another_job_under_way(state: &AppState, job_id: &str) -> bool { + state.jobs.read().await.iter().any(|(id, job)| id != job_id && matches!(job.status, MusicJobStatus::Queued | MusicJobStatus::Running)) +} + +/// The length of the songs a job made, as the library measured them. +fn songs_seconds(songs: &[CompletedSong]) -> f64 { + songs.iter().filter_map(|made| made.song.metadata.get("duration_seconds").and_then(Value::as_f64)).sum() +} + #[derive(Debug, Deserialize)] struct ComfyExportQuery { /// Strength folded into the file; 1 when left out. diff --git a/crates/music-server/src/model_manager.rs b/crates/music-server/src/model_manager.rs index 0c10d1b8..544450b5 100644 --- a/crates/music-server/src/model_manager.rs +++ b/crates/music-server/src/model_manager.rs @@ -41,6 +41,8 @@ pub struct ModelManager { http: reqwest::Client, state: Arc>, cancelled: Arc, + /// Where a download's outcome is counted, once the hub is up. + hub: Arc>, } #[derive(Debug, Clone, Serialize)] @@ -168,6 +170,7 @@ impl ModelManager { http: crate::net::client(), state: Arc::new(RwLock::new(state)), cancelled: Arc::new(AtomicBool::new(false)), + hub: Arc::default(), }) } @@ -297,6 +300,11 @@ impl ModelManager { self.state.read().await.active.as_ref().filter(|job| job.id == id).cloned() } + /// From now on every download that ends is counted in the hub: models ready, or failed with its reason. + pub fn report_to(&self, hub: studio_hub_client::Hub) { + let _ = self.hub.set(hub); + } + pub fn installed_profile_files(&self, profile_id: &str) -> Result { let selection = resolve_install(InstallRequest { profile_id: Some(profile_id.into()), component_ids: vec![] })?; self.installed_files_from_selection(selection, &format!("selected profile '{profile_id}'")) @@ -353,6 +361,13 @@ impl ModelManager { job.error = Some(error.to_string()); } } + if let Some(hub) = self.hub.get() { + match job.status { + DownloadStatus::Completed => hub.setup_ready(), + DownloadStatus::Failed => hub.setup_failed(job.error.as_deref().unwrap_or_default()), + _ => {} + } + } let _ = self.persist_locked(&state); } } From ca58d7378681c9390d79c1d8d222bc8d2890db04 Mon Sep 17 00:00:00 2001 From: Nerual Dreming Date: Wed, 14 Oct 2026 13:20:12 +0300 Subject: [PATCH 03/30] Statistics text: no apostrophe inside the quoted string --- app/i18n/notices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/notices.ts b/app/i18n/notices.ts index 73f024a8..468a3726 100644 --- a/app/i18n/notices.ts +++ b/app/i18n/notices.ts @@ -6,7 +6,7 @@ const en = { hubAd: 'Ad', hubTelemetryTitle: 'Anonymous statistics', hubTelemetryCheckbox: 'Send anonymous usage statistics', - hubTelemetryWhy: 'How many people use the studio and what for helps decide what to work on next. Nothing personal: no lyrics, prompts, audio or file names — only counts, which of the studio's functions were used (by name, never with what), the model sets used, the version, the graphics card class and why something failed, with paths, names and quoted text cut out on this computer before it is sent.', + hubTelemetryWhy: 'How many people use the studio and what for helps decide what to work on next. Nothing personal: no lyrics, prompts, audio or file names — only counts, which studio functions were used (by name, never with what), the model sets used, the version, the graphics card class and why something failed, with paths, names and quoted text cut out on this computer before it is sent.', hubTelemetryWhat: 'What is sent', hubTelemetryEnabled: 'Statistics are on', hubTelemetryDisabled: 'Statistics are off', From a2c37702fa2b41f8c0b843fd25bb62d42343b141 Mon Sep 17 00:00:00 2001 From: wuyue Date: Wed, 14 Oct 2026 22:25:10 +0800 Subject: [PATCH 04/30] Engine settings take any extra launch flag and a hand-built engine executable; a restart applies what the page shows - extra_args: one line of extra launch flags, split like a shell would and appended last, so it can also override a studio flag; --host/--port/ --models/--adapters stay with the studio and an unmatched quote is refused - engine_bin: an ace-server built by hand, picked in the native dialog, checked at save; STUDIO_ENGINE_BIN still wins, and a custom build no longer needs the bundled engine folder to start - /engine/options says which executable runs and which one a restart would start; the restart button saves what is on the page first, waits for a stopped engine's port to clear and never reuses a leftover process --- app/components/EngineSettings.tsx | 105 +++++++++++++++- app/i18n/en.ts | 10 ++ app/i18n/ja.ts | 10 ++ app/i18n/ko.ts | 10 ++ app/i18n/ru.ts | 10 ++ app/i18n/zh.ts | 10 ++ crates/music-engine/src/server.rs | 122 ++++++++++++++++++- crates/music-server/src/lib.rs | 192 ++++++++++++++++++++++++++++-- crates/music-server/src/mcp.rs | 2 +- 9 files changed, 450 insertions(+), 21 deletions(-) diff --git a/app/components/EngineSettings.tsx b/app/components/EngineSettings.tsx index 6a6e4a76..d0490754 100644 --- a/app/components/EngineSettings.tsx +++ b/app/components/EngineSettings.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { AlertTriangle, Check, Cpu, Loader2, RotateCw , Terminal } from 'lucide-react'; +import { AlertTriangle, Check, Cpu, FolderOpen, Loader2, RotateCw , Terminal } from 'lucide-react'; import { useI18n } from '../context/I18nContext'; /** @@ -22,6 +22,10 @@ interface EngineOptions { clamp_fp16: boolean; /** The NVIDIA card by nvidia-smi index; null: the first. Taken at the studio's start. */ gpu?: number | null; + /** Any other launch flag, as one line. It reaches the engine after the flags above. */ + extra_args: string; + /** A hand-built engine (acestep.cpp) to run instead of the bundled one; null: the bundled engine. */ + engine_bin?: string | null; } const DEFAULTS: EngineOptions = { @@ -32,6 +36,8 @@ const DEFAULTS: EngineOptions = { split_cfg_forwards: false, clamp_fp16: false, gpu: null, + extra_args: '', + engine_bin: null, }; const CONTROL = @@ -56,17 +62,48 @@ export const EngineSettings: React.FC = () => { void fetch('/v1/system/gpus').then(response => (response.ok ? response.json() : null)).then(setCards).catch(() => undefined); }, []); const [saved, setSaved] = useState(DEFAULTS); + // Which engine executable a start takes and which one is running, so the + // page answers "did my change apply?" without opening the engine log. + const [runtime, setRuntime] = useState<{ executable: string | null; executable_source: string; running_executable: string | null } | null>(null); const [busy, setBusy] = useState(false); + const [picking, setPicking] = useState(false); const [notice, setNotice] = useState(null); const [error, setError] = useState(null); + // The native file picker belongs to the studio process: this window is a + // browser and cannot open one. Nothing is changed until the user saves, + // the same as every other field on this page. + const browseEngine = async () => { + setPicking(true); + setError(null); + try { + const response = await fetch('/engine/pick-executable', { method: 'POST' }); + const body = await response.json().catch(() => null); + if (!response.ok) throw new Error(body?.error || `Opening the file picker failed (${response.status})`); + if (body?.picked && typeof body.path === 'string') { + setOptions(current => ({ ...current, engine_bin: body.path })); + } + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'Opening the file picker failed.'); + } finally { + setPicking(false); + } + }; + const load = useCallback(async () => { try { const response = await fetch('/engine/options'); if (!response.ok) throw new Error(`Engine options are unavailable (${response.status})`); - const body: { options: EngineOptions } = await response.json(); - setOptions(body.options); - setSaved(body.options); + const body: { options: EngineOptions; executable?: string | null; executable_source?: string; running_executable?: string | null } = await response.json(); + // A settings file written before a flag existed arrives without it; + // the default keeps the comparison below honest. + setOptions({ ...DEFAULTS, ...body.options }); + setSaved({ ...DEFAULTS, ...body.options }); + setRuntime({ + executable: body.executable ?? null, + executable_source: body.executable_source ?? 'bundled', + running_executable: body.running_executable ?? null, + }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Could not read the engine options.'); } @@ -92,6 +129,8 @@ export const EngineSettings: React.FC = () => { setNotice(body.engine_restarted ? t('engineOptionsSavedRestarted') : t('engineOptionsSavedLater')); + // The restart, or the lack of one, changed which engine is running. + void load(); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Saving the engine options failed.'); } finally { @@ -123,6 +162,7 @@ export const EngineSettings: React.FC = () => { const body = await response.json().catch(() => null); if (!response.ok) throw new Error(body?.error || `Restarting the engine failed (${response.status})`); setNotice(t('engineRestarted')); + void load(); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Restarting the engine failed.'); } finally { @@ -172,6 +212,44 @@ export const EngineSettings: React.FC = () => { )} +
+ {t('engineBinLabel')} + setOptions(current => ({ ...current, engine_bin: event.target.value === '' ? null : event.target.value }))} + spellCheck={false} + placeholder={t('engineBinBundled')} + className={`${CONTROL} mt-2 font-mono text-xs`} + /> +
+ + +
+ {t('engineBinHint')} + {runtime && ( + + {runtime.running_executable ? `${t('engineBinRunning')}: ${runtime.running_executable}` : t('engineBinNotRunning')} + {runtime.executable && runtime.executable !== runtime.running_executable ? ` · ${t('engineBinNextStart')}: ${runtime.executable}` : ''} + {runtime.executable_source === 'environment' ? ` · ${t('engineBinEnvOverride')}` : ''} + + )} +
+ { +
+ {t('extraArgsLabel')} +
+