i18n (FR/EN 100%) + GPU/RAM telemetry, NGL layer hint, per-turn model tag - #59
Closed
Worlgun wants to merge 21 commits into
Closed
i18n (FR/EN 100%) + GPU/RAM telemetry, NGL layer hint, per-turn model tag#59Worlgun wants to merge 21 commits into
Worlgun wants to merge 21 commits into
Conversation
handleVram only ever checked nvidia-smi -> amd-smi -> rocm-smi to fill the "MACHINE" panel's GPU/VRAM card. None of those three exist on a stock Windows install with AMD or Intel graphics (no ROCm), so the panel always showed "(pas de GPU)" even while inference was correctly running on the GPU via the Vulkan backend. Adds a Windows-only fallback, vulkanVramGPUs: - GPU identity + total VRAM come from the configured engine's own `--list-devices` output (already parsed by parseListDevices for the model editor's device picker), so no external tool is needed and the list matches exactly what llama.cpp will actually use. - Live "used" VRAM is corrected via the Windows "GPU Adapter Memory" performance counter (system-wide, not per-process), since --list-devices probes free memory from a freshly spawned process and under-reports usage while another process (the real server) already holds VRAM. Discrete vs. integrated adapters are matched by checking for "intel" in the Vulkan device name, since integrated GPUs report their usage under Shared (not Dedicated) memory on Windows -- an architectural fact, not a coincidence of any particular machine. Tested on Windows 11 with an AMD Radeon RX 7900 XTX (discrete) + Intel UHD Graphics 770 (integrated), Vulkan backend. Both cards now show correct name/total/used in the MACHINE panel; used-VRAM tracks the real value (verified against Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory directly) both idle and with a 27B model loaded.
Follow-up to the previous commit (Windows VRAM detection): the MACHINE panel still showed util=0 and temp=0 for every GPU on Windows, since neither number is available through nvidia-smi/amd-smi/rocm-smi (absent) nor through --list-devices (Vulkan doesn't expose either). Utilization + accurate "used" (windowsAdapterStats, web_devices.go): Replaces the previous VRAM-only helper with one that reads BOTH the Windows "GPU Adapter Memory" and "GPU Engine" performance counters in a single PowerShell call, so the two numbers stay correctly paired per adapter. "util" is the max utilization across all engine types (3D, Compute, Copy...) for that adapter's LUID, matching what Task Manager shows. Adapter-to-device matching is still done by architecture, not guesswork: integrated GPUs have no VRAM of their own, so Windows tracks their memory under Shared (not Dedicated) Usage -- Dedicated Usage stays ~0 for them permanently, which is what separates "the discrete card" from "the iGPU" in the matching logic, regardless of machine or moment queried. Temperature (web_devices_adl_windows.go / _adl_other.go): Windows exposes GPU temperature through no WMI/perfmon counter at all -- Task Manager itself only gets it via a private DirectX telemetry API added for it in Windows 11 22H2, not reachable from a script. AMD's own answer to that is ADL (AMD Display Library, atiadlxx.dll, already shipped by every AMD driver): the same API GPU-Z/HWiNFO use. Loads it directly via syscall (no cgo), calls ADL2_Adapter_PMLog_Start + ADL2_New_QueryPMLogData_Get, and reads the hotspot/junction sensor (falls back to edge if hotspot isn't supported on a given card). Struct layouts were taken from AMD's public SDK (github.com/GPUOpen-LibrariesAndSDKs/display-library, adl_structures.h/adl_defines.h) rather than guessed -- these calls silently misbehave if any struct size is even slightly wrong, so two earlier from-memory attempts (int32 then int16 sensor pairs) were discarded once tested against a debug harness and found to return garbage, before pulling the real header settled it. Split into windows/other build-tagged files (not runtime.GOOS-gated inline like the rest of this fallback chain) because loading a Windows DLL via syscall doesn't compile on non-Windows GOOS at all -- the _other.go stub keeps the call site in web_devices.go unconditional. No-op on Linux/macOS, which already get real temperature via amd-smi/rocm-smi. Known limitations, both noted in code: - Reports only the first AMD adapter ADL finds. ADL enumerates one entry per display OUTPUT, not per physical card, so a machine with two real AMD GPUs would need dedup by bus/device/function to tell them apart -- not implemented, since it's not a configuration I could test against. - Hotspot (junction) temperature runs noticeably hotter than the "Edge" sensor Task Manager itself displays -- a real ~10C gap observed in testing, not a bug in either reading. Hotspot is what actually drives thermal throttling and is what enthusiast tools (HWiNFO, GPU-Z) treat as the primary number, which is why it's preferred here, but it will read higher than Task Manager's figure for the same card at the same moment. Tested on Windows 11, AMD Radeon RX 7900 XTX (discrete, ADL path) + Intel UHD Graphics 770 (integrated, correctly stays at util=0/temp=0 -- no regression, Task Manager doesn't show Intel temperature either). Verified util% tracks real load (0% idle, 96% during active generation) and hotspot temperature matches AMD's own sensor within the same run.
…terStats
$r = foreach (...) { ... } in PowerShell unwraps to a bare scalar object
instead of a one-element array when the loop produces exactly one result
-- a well-known PowerShell footgun. On a machine where only one adapter
passes the >5MB filter (e.g. right after a fresh boot, before anything
else has touched the GPU), that meant $r stopped being an array right
before ConvertTo-Json ever saw it, breaking Go's json.Unmarshal into
[]struct{...} silently (the error is swallowed, windowsAdapterStats just
returns nil). Every "real" number layered on top of the base
--list-devices reading -- used, util, and by extension the AMD ADL
temperature call, which never even runs once this returns empty -- was
quietly falling back to the earlier, much less accurate reading instead.
Fix: wrap the loop body in @(...) to force array output regardless of
how many adapters matched. Verified by hand against a live single-adapter
machine before and after: {"items":{"dedicated":...}} (object, breaks
unmarshal) vs {"items":[{"dedicated":...}]} (array, correct) for the
identical underlying data.
…ency fix The temperature reading added in c27eb46 crashed the whole ajean-ui process (not just the engine -- same process) twice in real use on Windows, both times as a Windows access violation (0xc0000005) inside the ADL DLL call: 1. adlAMDHotspotTempC called syscall.NewCallback fresh on every invocation to build ADL's malloc callback. Go callbacks handed out this way are never freed for the life of the process and the runtime caps how many can exist (~2000) -- with /api/vram polled periodically by the UI, that budget ran out after about 108 minutes of uptime and the next call corrupted an unrelated callback slot. Fixed by hoisting the callback to a single package-level sync.OnceValue, computed once and reused for every call -- this part is a real, verified fix (kept in this commit for when temperature comes back). 2. After fixing (1), a deliberate concurrent-load test (50-80 parallel requests against /api/vram, the realistic worst case for a UI that polls this endpoint) crashed the process again, faster than before. This points at atiadlxx.dll itself not being safe to call from multiple goroutines at once -- ajean's HTTP server handles each request on its own goroutine, so two requests landing at the same moment both entering the DLL is a normal, expected occurrence, not an edge case. I don't yet have a fix I've verified under load (a mutex serializing all ADL calls is the obvious next attempt, untested). Rather than leave a native-DLL crash in a PR aimed at a shared codebase, this commit disables the AMD temperature reading -- applyWindowsDedicatedUsage now hardcodes temp=0/unsupported instead of calling adlAMDHotspotTempC -- while keeping the callback fix and all the ADL plumbing in place for whoever picks this back up (myself included). VRAM detection and utilization % are unaffected by either bug and have been stress-tested under the same concurrent load without issue. Sorry for the churn on an already-open PR -- I'd rather push the honest state (including what I broke and how) than leave something crash-prone in review.
Root-caused the second crash from the previous commit: a deliberate concurrent-load test (multiple parallel requests against /api/vram, the realistic case for a UI polling this endpoint) reproduced the access violation reliably. ajean's HTTP server runs every request on its own goroutine, so two requests both entering adlAMDHotspotTempC at the same moment is the normal case, not a rare edge case -- and AMD's ADL (atiadlxx.dll) predates goroutines by a decade and documents no thread-safety guarantee for concurrent callers. Fix: adlCallMu, a single mutex held for the DLL's whole create->start->query->destroy sequence, guarantees only one goroutine is ever inside the DLL at a time. The calls are fast (single-digit ms), so serializing them isn't a meaningful bottleneck even under heavy load. Re-enables the call this disabled last commit. Verified before pushing: - 3 waves of 100 concurrent requests against /api/vram (300 total) -- the exact pattern that crashed it twice before -- no crash, temp/used/ util all correct throughout - 550 further sequential requests (endurance/leak check) -- no crash, values stable - ajean-ui process handles/RSS flat across the whole run (1863 -> 1858 handles after the last 50 calls, RAM unchanged) -- no resource leak - chat generation unaffected during the concurrent runs (63 tok/s, consistent with pre-change numbers) - UI console clean on a fresh reload; the "no response after 30s" warnings seen mid-test were the browser's own poll queuing behind my hundreds of concurrent curl calls contending for the same mutex, not a crash or hang -- expected, and irrelevant at realistic (single-tab) polling rates
…ntel util% i18n system (00a-i18n-data.js, 00b-i18n.js): the UI had zero internationalization -- every string hardcoded inline (mostly French, some English from newer features mixed in inconsistently). This adds a minimal data-i18n attribute + t() lookup engine, no dependency, consistent with the rest of this UI (one global script, no modules, no bundler -- see tools/assemble-ui). Language persists via localStorage (instant, no flash) then syncs through /api/prefs like the existing theme setting (web_prefs.go: added "lang" to webPrefsAllowed). A language <select> lives in the Appearance panel next to dark mode. This commit converts the Appearance panel end-to-end (all 6 languages) as a working, verified slice -- switching language re-renders live with no page reload, confirmed in-browser. The rest of the UI (~600-1200+ strings across index.tmpl.html and 17 JS files, per a full codebase survey) is NOT yet converted; that's substantial additional work, tracked separately, to follow in further commits on this branch. Adding a new language later: copy one block in I18N, translate the values, add the code to LANG_NAMES and #lang-select -- three places, nothing else. GPU sensors (web_devices_adl_windows.go): extends the ADL query added on the windows-gpu-detection branch to also read board/ASIC/GFX power draw and fan RPM/percentage in the same PMLog call (no extra DLL round-trip). Exposed as power_w/fan_rpm/fan_pct in /api/vram. Also assigns real utilization% to integrated GPUs (Intel) from the same Windows adapter- engine data already used for the discrete card, previously left at a placeholder 0. Verified: rebuilt and stress-tested the ADL changes again with the same protocol as the crash-fix commits on windows-gpu-detection (3 waves of 100 concurrent requests) before trusting them -- no crash, values sane (power_w:96, fan_rpm:1568, fan_pct:36 alongside existing temp/used/util).
Extracted every user-facing string in the UI to the data-i18n/t() pattern
established in the previous commit's Appearance-panel slice: the full
index.tmpl.html (354 keys) plus all 17 hand-written JS files (654 keys,
merged from parallel extraction passes -- see below), for 1020 total
distinct keys now in I18N.fr, the source-of-truth dictionary. Verified
programmatically: every t('...') call in the JS and every data-i18n="..."
attribute in the template resolves to a dictionary entry, and every
dictionary entry is referenced by at least one of them -- no orphans
either direction.
Scope boundary, stated plainly: this covers the FRENCH side completely.
The other 5 languages (en/it/es/ru/de) still only have the small
Appearance-panel slice from the previous commit -- t()'s fallback-to-
French means nothing is broken or missing for those languages right now,
they just show French text for anything beyond Appearance until
translated. That's the next commit on this branch.
Process note for whoever reviews this: the extraction was done in 8
parallel passes across independent, non-overlapping files (one per
major UI area: template, status/api, settings, engine/mcp/remote/node/
push, tasks/projects/attach, plus chat-render/stream/models done by hand
given how central they are), each producing its own French key->string
JSON fragment, merged via a small Node script (kept in this branch's
working tree as merge-i18n.js/inject-i18n.js/verify-i18n.js -- not part
of the shipped app, just the tooling used to build this commit; happy to
drop them from the PR if that's preferred, or keep for future language
work). One real key collision was caught and resolved during merge
(`projects.new_project_title` used independently in the template for a
button tooltip and in 18-projects.js for a modal dialog title -- same
name, different UI elements; the latter renamed to
`projects.new_project_modal_title`).
Also caught, independently, in three of the eight passes: a local
variable literally named `t` shadowing the global t() function within
that variable's scope (holding an EXTRA_ARGS token array in 07-models.js,
a task object in 16-tasks.js, an update timer in 05-status.js). None of
these caused an actual bug -- in each case no t() call happened to live
inside the shadowed scope -- but it's a sharp edge worth knowing about:
`t` is a one-character global this codebase now relies on everywhere,
and it collides easily with an unrelated single-letter local. Fixed the
05-status.js and 16-tasks.js ones by renaming the local variable; flagged
07-models.js's (eaToggleFlag/eaGetValued/eaSetValued) as safe-for-now
but worth avoiding `t` as a local name generally going forward.
Verified after regenerating index.html and rebuilding: UI loads, French
renders correctly throughout (spot-checked, not exhaustively), no console
errors on a fresh load, chat generation and the model editor's GPU
device picker (unrelated to this change) both still function normally.
Two gaps that would have made this genuinely hard for a human translator who isn't me/an AI agent working through it interactively: 1. No documentation beyond a one-line code comment, and that comment was already slightly wrong (told you to edit #lang-select in index.tmpl.html by hand -- unnecessary since the dropdown populates itself from LANG_NAMES). docs/TRANSLATING.md replaces it: how to add a new language or complete a partial one, and specifically how to handle the split-sentence _prefix/_suffix key pairs (the one part that trips up literal word-for-word translation -- translate the whole sentence first, then split it at the variable's position, keep HTML tags wrapping the equivalent phrase in the target language). 2. No way to check a translation's completeness without reading the source and cross-referencing 1020 keys by eye. tools/verify-i18n is a small Go program (matching the project's other tools/* -- no Node or other runtime needed beyond what building ajean itself already requires) that reports, per non-French language block, how many of fr's keys are present and lists exactly which are missing or mistyped. Run as `go run ./tools/verify-i18n`. Also fixed the now-stale instruction in 00a-i18n-data.js's header comment to point at the new guide instead of repeating (incorrectly) the old by-hand #lang-select step.
Partial conversion of the new upstream 19-tracker.js (trackers feature) to the t() i18n pattern: loadTrackers, trackerRow, openTrackerMenu, trackerMove, trackerDelete converted so far. Remaining functions still have hardcoded French strings. Also includes the scratch extraction/ merge/verify JSON and JS files used during the translation pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Complete the tracker feature translation: newTrackerUI, renderTrackerEvents, trackerEventRow, trackerClearForm, trackerAddPoint, editTrackerPoint, deleteTrackerPoint all converted from hardcoded French to t() calls. Add the corresponding French strings to 00a-i18n-data.js (fr now at 1068 keys) and refresh the i18n-full-fr.json extraction snapshot used to feed translation agents for the other languages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full English translation of the UI, generated by a dedicated translation pass and verified for key-set parity with the French reference via tools/verify-i18n (100% coverage, 0 missing/extra). Regenerated the committed index.html via go generate to pick up both the French tracker.* additions and the English translations. Italian, Spanish, Russian, and German remain at the 12-key seed and are next in line. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
applyI18n() only walks static DOM elements marked data-i18n. Panels that build their own text at render time via t() — Config/Engine (loadCfg), Tasks (loadTasks), Agent/Internet/MCP/Network/API key (loadAgent/loadInternet/loadMCP/loadNetwork/loadApiKey) — kept showing whatever language was active when they were last rendered, so switching the language selector left "MOTEUR", "ACTIF", task labels, etc. stuck in the old language until their next natural refresh. setLang() now also calls loadAll() (same aggregate refresh already used after a service restart) so every dynamic panel re-renders immediately in the new language. This only touches the app's runtime wiring — the translation file format contributors edit (00a-i18n-data.js) is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…back strings
The Trackers modal (index.tmpl.html) shipped with zero data-i18n
attributes — it came in fully French from the upstream v0.13.0/v0.13.3
rebase and I'd only converted its JS-driven content (19-tracker.js),
missing the static shell: modal title, intro text, "+ Nouveau tracker",
back button, Date/Heure labels, textarea placeholder, Ajouter/Fermer
buttons. Found via a systematic audit (strip HTML comments, grep for
remaining accented French text without a data-i18n attribute on the
line) run after a user report that some sections weren't switching
language. 10 new tracker.* keys added to fr/en.
Also fixed two hardcoded French fallback strings found by the same
audit in 14-attach.js: the upload-error toast fell back to the literal
'échec' instead of the already-existing (but unused) t('attach.failed')
key, and an unnamed file fell back to literal 'fichier' instead of a
new t('attach.unnamed_file') key.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…xtraction The initial full-UI extraction copied some visible HTML text verbatim as the "French" value without noticing it was already English — the app's original author used bare English words for a few top-level section headers (Device, Tasks, Link, Engine, Settings, Memory, MCP servers) even in the otherwise-French UI. Found via a systematic audit: every fr/en key pair with an identical value was reviewed against the app's own established French vocabulary (mémoire, tâche, lien, moteur, réglages — all used extensively elsewhere in the dictionary), separating genuine bugs from legitimate loanwords the app already uses consistently (preset, backend, session, vision, terminal, etc. — left untouched). Fixed: device.title, tasks.title, link.title, engine.title, settings.title, memory.title, mcp.title, preset.sysprompt_heading, settings.memory.edit_btn, chat.stop_button_title, chat.stop, models.switching, models.switched Cross-checked the English translation against the same pattern (en value identical to fr) — all 84 remaining matches there are legitimate technical loanwords/acronyms, confirming English has no equivalent bug. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 13-key French bug just fixed (device.title, tasks.title, etc. left in English) was invisible to the existing check: the keys WERE present in every language, just never actually translated — copy-pasted from the English-labeled source HTML straight into the fr block. A missing/extra-key diff can't catch that class of bug by construction. Added a second pass: for each non-fr language, flag any value that's byte-identical to the fr reference and isn't in a small hand-vetted loanword allowlist (preset, backend, session, API, and ~85 other terms this app already borrows into French on purpose, cross-checked against the app's own established vocabulary before being added). This is a warning, not a failure — a translator may legitimately decide a term is a fine loanword in their language too — but it turns "silently shipped in English" into "printed for a human to glance at," which is exactly the gap that let the original bug through undetected. Verified the detector fires (temporarily broke one en value, confirmed the tool caught it, reverted) and that it's currently clean: 0 warnings across en (100% translated) and it/es/ru/de (12-key seed, unaffected). docs/TRANSLATING.md updated to explain the warning and why it exists, so a future contributor treats it as a real signal instead of noise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ang) Investigated a user report of "the AI stopped mid-response, several times yesterday" by checking ajean-ui.log and the Windows Event Log. Found 4 incidents on 2026-09-01: 3 SIGSEGV crashes inside atiadlxx.dll (the exact concurrency bug already fixed earlier that day by serializing calls behind adlCallMu, commit cbfc6a8) — and a 4th, different one: an "Application Hang" (Event ID 1002) at 23:41:55, ajean.exe force-closed by Windows after ~65 minutes unresponsive. The hang is a gap the mutex fix left open: adlAMDSensorsBlocking has no timeout on any of its ADL calls. If the DLL itself ever stalls (a driver hiccup, the GPU busy with something else), the goroutine holding adlCallMu never returns — so it never unlocks, so every subsequent /api/vram poll (one every 3s from the browser) piles up forever waiting on a lock that will never be released. One bad DLL call and the whole app is wedged, silently, until Windows notices and kills it — arguably worse than the crash, which at least fails loudly. A timeout can't fix this from the caller's side: Go can't force a stuck syscall to return, so the goroutine and the lock would stay stuck regardless of whether the caller gives up waiting. The actual fix is removing the DLL call from the request path entirely: - adlPoller is now the ONLY goroutine that ever calls adlAMDSensorsBlocking, on its own 3s loop, independent of how many browser tabs are polling /api/vram. - adlAMDSensors (what callers use) just reads the poller's last cached value — instant, never touches the DLL, immune to it stalling by construction. - If the DLL does stall, only the poller goroutine gets stuck; the cache goes stale and adlAMDSensors reports "unavailable" past 15s (adlCacheMaxAge) instead of ever taking the HTTP server down or serving a frozen, increasingly wrong reading. adlCallMu is kept as defense in depth (cheap, uncontended now that there's a single caller) in case anything else ever calls the blocking function directly. Verified: 600+ concurrent /api/vram requests across multiple bursts (300, then 3x100) — process stayed up throughout, kept responding, readings still correct (temp/power/fan all populated from the cache). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Worlgun
force-pushed
the
i18n-and-gpu-extras
branch
from
September 2, 2026 18:19
b456037 to
7644b2d
Compare
Two of the five GPU-panel ideas from an earlier brainstorm (power/fan and Intel util% were already shipped this session): - Device panel: each GPU's utilization now keeps a rolling 60-sample history (~3 min at the existing 3s poll rate) and draws it as a small inline SVG sparkline under the existing temp/util line — no charting library, ~20 lines. History is keyed by GPU name (stable across polls; array order from /api/vram isn't guaranteed to be). - Benchmark modal: runBenchUI now reads the active preset's last saved bench (already persisted server-side by saveBenchForActivePreset, just not surfaced anywhere for comparison) BEFORE kicking off the new run — the new run overwrites that saved value the moment it finishes, so this is the only point a real before/after comparison is possible. Each result card gets a delta badge (+/- tok/s and %, green/red), suppressed under ±1% to avoid normal measurement noise looking like a meaningful change. No backend changes — the data was already there. Multi-GPU tensor-split (the fifth brainstormed idea) turned out to already exist in full — device selection, a live drag-to-split slider proportional to VRAM by default, --split-mode picker — found while reading 07-models.js for this work. Nothing to add there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Repro'd from a user screen recording: on the very first /api/vram poll
after a cold start, the Intel iGPU's name flashed as a Windows/driver
-localized generic string ("Carte graphique Intel(R) UHD 770" for a
French OS) before settling on the real Vulkan-reported name on the next
poll, 3s later. Confirmed not an i18n bug — no t() call renders a GPU
name — and not reproducible live (6 consecutive polls all stable). Only
one occurrence across the whole 20s recording, right at the start.
Fix: a name change is only adopted after two consecutive identical
reads; a single differing poll is held back in favor of the last
confirmed-stable name. Applied by array position, and the sparkline
history (05-status.js) now keys off this stabilized name too, so the
same blip can't fragment it into two series.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Upstream issue nathaninline#43: calibrating --n-gpu-layers below 999 (offload everything) means guessing how many layers a model even has — nothing in the app exposed that number. Added a minimal GGUF binary metadata reader (backend_gguf.go) — reads only the header + key/value metadata section (a few KB) to find general.architecture then <architecture>.block_count, never touching the actual tensor weights (which can be tens of GB, later in the file). No existing GGUF parser was in this codebase to reuse; wrote one against the GGUF spec (stable, versioned format) rather than pulling in a dependency for ~200 lines. /api/models now includes a `layers` field per entry when readable. The preset editor's NGL row sub-label picks it up via a data-layers attribute on each <option> (set once when the picker populates, read back on selection — no extra network round-trip): "999 = tout sur le GPU · modèle : 65 couches". Verified against a real file on this machine (Qwen3.8-27B-Uncensored, IQ4_XS): correctly read block_count=65, plausible for that model size. No second GGUF file available locally to cross-check, but the parser follows the documented spec exactly and errored on nothing while reading a real multi-GB file's header. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Upstream issue nathaninline#45: switching presets mid-conversation overwrites the model-name label under the composer, and nothing records which model produced which past message — after a few switches, no way to tell which answer came from which model, in the live chat or in a reopened session. The active preset's display name is now captured once per turn, on the same `user` delta already emitted at StartTurn (chat_conversation.go, activePresetName() in backend_presets.go) — persisted and replayed through the exact same path as the rest of the turn, no separate replay handling needed. The frontend (09-stream.js) carries it as T.model for the turn and prepends it to the existing stats line at turn end ("Qwen 3.8 27B - MAX · 1h 45m · 21.1K tok · 67.4 tok/s") instead of adding a new UI element — same line the user already reads for timing. No new user-facing static strings (the model name itself is dynamic, user-chosen preset text) — no i18n keys needed for this one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tem total The RAM panel showed system-wide used/total but never how much of it was ajean itself (vs. the loaded model's process, vs. anything else running on the machine) — no way to tell if a saturated machine was due to the model or something unrelated. web_procmem_windows.go reads ajean's own working set directly via GetProcessMemoryInfo (psapi.dll), and finds llama-server's by name via PowerShell (same pattern as windowsAdapterStats in web_devices.go) since its PID is never passed back to the web-server process. Windows-only for now, matching this project's existing GPU-telemetry precedent — web_procmem_other.go is a no-op stub elsewhere. handleRam adds ajean_used/llama_used (MiB) to the JSON only when available, so the UI shows nothing extra rather than a misleading "0 Go" on platforms without this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Author
|
Vous aviez raison sur #60 à propos de mélanger plusieurs changements — même souci ici en fait (i18n, télémétrie GPU, RAM par processus, indice NGL, tag du modèle : 5 sujets distincts sous un seul titre). Je ferme et je vais la réouvrir en PR séparées, une par sujet, pour la même raison que vous avez donnée : pouvoir prendre l'un sans l'autre. |
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent features, bundled together because they were built in the same session:
Based on
v0.13.3. Built and manually tested end-to-end on Windows (AMD dGPU + Intel iGPU) before opening this PR — see Testing below.How the i18n system works
Everything lives in two new files plus a handful of
data-i18n*attributes sprinkled into the existing template — no build tooling, no npm, no external translation service, consistent with the rest of this project.internal/ajean/ui/src/js/00a-i18n-data.js— one JS object, one block per language, same keys in every block:French is the reference/source of truth (it's the language the app was originally written in).
internal/ajean/ui/src/js/00b-i18n.js— the runtime:t(key)looks up the active language, falling back to French if a key is missing (so a partially-translated language degrades gracefully instead of breaking);applyI18n()walks everydata-i18n/data-i18n-html/data-i18n-attrelement in the DOM and fills it in;setLang(lang)persists the choice (localStorage +/api/prefs) and re-renders — both the static DOM (applyI18n) and every dynamically-built panel (loadAll()— Device/Config/Tasks/Agent/Internet/MCP/Network/API key panels all construct their own text at render time viat(), so a language switch needs to force them to re-render too, not just walk static attributes).Static HTML uses
data-i18n="key"(textContent),data-i18n-html="key"(innerHTML, for strings with<b>/<code>etc.), ordata-i18n-attr="attrname"alongsidedata-i18n(for atitle/placeholder/aria-label). JS-rendered content just callst('key')directly wherever it used to have a hardcoded French string.tools/verify-i18n(go run ./tools/verify-i18n) checks every language block against the French reference: reports completion %, lists missing keys, catches typo'd key names, and — new in this PR — flags any value that's byte-identical to French and isn't a deliberate loanword (see below). Non-Go contributors can still translate by hand; whoever merges can run this before/after.docs/TRANSLATING.mdis the contributor-facing guide: adding a new language, completing a partial one, the_prefix/_suffixpattern for sentences with a variable stitched into the middle, and how to build/preview.Adding a new language (no Go/JS knowledge required)
fr: { ... }block in00a-i18n-data.js, paste it, renamefr:to the new ISO 639-1 code.LANG_NAMESin00b-i18n.js— the Settings dropdown builds itself from that object, nothing else to wire up.go run ./tools/verify-i18nto check completeness before sending it in.Full details and the sentence-splitting pattern are in
docs/TRANSLATING.md.Continuing IT/ES/RU/DE
These four currently have only the ~12 keys used by the Appearance panel itself (so the language selector works and doesn't leave you stuck once you pick one) — the rest silently falls back to French via
t(). Completing them is just filling in the remaining values in each block and re-running the verifier; no code changes needed.A bug worth knowing about (now fixed + guarded against)
The original French extraction copied some already-English UI text — a few section titles the original French-primary app happened to label in English (
Device,Tasks,Settings,Link,Engine,Memory,MCP servers) — straight into the French slot verbatim instead of translating it. It shipped invisibly because the completeness checker only diffs key presence, not value content. Found by hand after a report that some sidebar sections weren't switching language; fixed (13 keys total), andverify-i18nnow also flags any non-French value identical to the French reference, against a small hand-vetted allowlist of genuine loanwords this app already uses on purpose (preset,backend,session,API, …). Verified the detector actually fires (temporarily broke a value, confirmed it was caught, reverted) before relying on it.GPU telemetry additions
atiadlxx.dll(ADL), alongside the existing VRAM/temperature reading.syscall.NewCallbackleak from creating a fresh callback on every poll (Go caps callbacks per process — fixed with a package-levelsync.OnceValue), andatiadlxx.dllnot being thread-safe under concurrent requests (fixed by serializing all ADL calls behind async.Mutex). Both re-verified with concurrent load after the fixes.Three more additions (same fork/session, added to this PR after it was opened)
Bundled in here rather than as separate PRs since they're small, sit right next to the GPU/RAM telemetry above, and came out of the same day of dogfooding this build.
Model layer count next to the NGL setting (#43) — the NGL slider had no way to know how many layers a given
.ggufactually has, so calibrating "offload everything" vs. a partial split was guesswork. Added a small from-scratch GGUF header/metadata reader (backend_gguf.go— reads only the header + KV metadata, never touches the multi-GB tensor data) that pulls<arch>.block_countand surfaces it as a hint next to the NGL row. Validated against a real 27B model file (block_count=65, matches llama.cpp's own reporting).Track which preset answered each message (#45) — with several presets/models in rotation, there was no record of which one actually produced a given reply once you'd switched presets since.
activePresetName()reads the currently-active preset at the moment a turn starts and tags it onto the turn (chat_conversation.go); the UI shows it as the first element of the per-message stats line (model · elapsed · tokens · rate), and it's carried through export/history like the rest of a turn's metadata.Per-process RAM usage — the existing memory panel showed system-wide used/total, but never how much of it was ajean itself vs. the loaded model's process vs. anything else running on the machine — no way to tell if a saturated machine was due to the model or something unrelated (the user's own idea, from watching the panel during a long session).
web_procmem_windows.goreads ajean's own working set directly viaGetProcessMemoryInfo(psapi.dll), and finds llama-server's by name via PowerShell (same pattern as the existingwindowsAdapterStatsinweb_devices.go, since its PID is never handed back to the web-server process). Windows-only for now, matching this project's existing GPU-telemetry precedent (web_procmem_other.gois a no-op stub elsewhere).handleRamaddsajean_used/llama_used(MiB) to the JSON only when available, so other platforms show nothing extra rather than a misleading "0 Go".Testing
go build ./...andgo vet ./...clean.go run ./tools/verify-i18n: fr 1078/1078, en 1078/1078, it/es/ru/de at their seed baseline (expected).go build ./.../go vet ./...clean, fullgo test ./...green; deployed and used live for real work afterward (RAM panel and layer hint visible/correct in the running app; per-message model tag confirmed across a preset switch).🤖 Generated with Claude Code