mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(terminal): add WebGL renderer toggle in settings
Adds a 'WebGL Renderer' toggle to Settings > Appearance (desktop). WebGL stays on by default; users can turn it off to force the DOM renderer when they hit GPU glitches, without needing the ?nowebgl URL param. Explicit opt-in (or ?webgl=force) clears a stale auto-fallback marker. Mobile skip and the long-task auto-fallback safety net are unchanged. The device/param/sticky/pref interaction is factored into a pure, unit-tested shouldSkipWebGL() helper in constants.js.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"aicodeman": patch
|
||||
---
|
||||
|
||||
Add a **WebGL Renderer** toggle to Settings → Appearance (desktop). WebGL stays on by default; turning it off forces the DOM renderer for users who hit GPU glitches, without needing the `?nowebgl` URL param. Turning it back on (or `?webgl=force`) clears any stale auto-fallback marker. The existing mobile skip and long-task auto-fallback safety net are unchanged. The skip decision is factored into a pure, unit-tested `shouldSkipWebGL()` helper.
|
||||
@@ -111,6 +111,31 @@ function evaluateWebGLLongTaskTrip(recent, entries, now, config = WEBGL_FALLBACK
|
||||
return recent.length >= config.LONGTASK_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure decision for whether to skip the WebGL renderer at terminal init, and
|
||||
* whether to clear the auto-fallback sticky marker. Keeps the interaction
|
||||
* between device type, URL params, the sticky marker, and the user's settings
|
||||
* toggle in one testable place (terminal-ui.js calls this).
|
||||
*
|
||||
* Precedence (desktop only — mobile always skips):
|
||||
* 1. user toggle OFF -> skip (one-shot opt-out, sticky untouched)
|
||||
* 2. ?nowebgl -> skip (one-shot opt-out, sticky untouched)
|
||||
* 3. user toggle ON / ?webgl=force -> enable + clear stale sticky marker
|
||||
* 4. untouched (default on) -> respect the auto-fallback sticky marker
|
||||
*
|
||||
* @param {{deviceType?: string, noWebglParam?: boolean, forceParam?: boolean,
|
||||
* stickyDisabled?: boolean, userPrefEnabled?: (boolean|undefined)}} [input]
|
||||
* @returns {{skip: boolean, clearSticky: boolean}}
|
||||
*/
|
||||
function shouldSkipWebGL(input = {}) {
|
||||
if (input.deviceType !== 'desktop') return { skip: true, clearSticky: false };
|
||||
const pref = input.userPrefEnabled; // true | false | undefined (default on)
|
||||
if (pref === false) return { skip: true, clearSticky: false };
|
||||
if (input.noWebglParam) return { skip: true, clearSticky: false };
|
||||
if (pref === true || input.forceParam) return { skip: false, clearSticky: true };
|
||||
return { skip: !!input.stickyDisabled, clearSticky: false };
|
||||
}
|
||||
|
||||
// Expose for tests. `const` declarations at the top of a non-module script
|
||||
// are global lexical bindings but not `window` properties, so explicit
|
||||
// assignment is the test-visible API surface.
|
||||
@@ -129,6 +154,7 @@ function shouldAutoWrapTabs(input) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.WEBGL_FALLBACK = WEBGL_FALLBACK;
|
||||
window.evaluateWebGLLongTaskTrip = evaluateWebGLLongTaskTrip;
|
||||
window.shouldSkipWebGL = shouldSkipWebGL;
|
||||
window.CodemanTabOverflow = {
|
||||
shouldAutoWrapTabs,
|
||||
};
|
||||
|
||||
@@ -939,6 +939,13 @@
|
||||
<option value="og">OG Codeman</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-item" id="appSettingsWebglRendererItem" title="Use the GPU-accelerated WebGL terminal renderer (desktop only). Turn off to force the DOM renderer if you hit GPU glitches. Codeman also auto-falls-back to the DOM renderer after repeated GPU stalls.">
|
||||
<span class="settings-item-label">WebGL Renderer</span>
|
||||
<label class="switch switch-sm">
|
||||
<input type="checkbox" id="appSettingsWebglRenderer">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- Input Section -->
|
||||
<div class="settings-section-header">Input</div>
|
||||
<div class="settings-item settings-item-multiline" title="Shows typed characters instantly via overlay while forwarding keystrokes to the server in the background. Enables Tab completion, preserves input across tab switches, and protects against session crashes. Recommended for mobile and high-latency connections.">
|
||||
|
||||
@@ -309,6 +309,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsShowResponseViewer').checked = settings.showResponseViewer ?? defaults.showResponseViewer ?? false;
|
||||
document.getElementById('appSettingsShowAttachmentsButton').checked = settings.showAttachmentsButton ?? defaults.showAttachmentsButton ?? false;
|
||||
document.getElementById('appSettingsSkin').value = settings.skin ?? defaults.skin ?? 'daylight-blue';
|
||||
// WebGL renderer (desktop only — mobile always uses the DOM renderer, so hide
|
||||
// the toggle there so it can't promise something that won't apply).
|
||||
document.getElementById('appSettingsWebglRenderer').checked = settings.webglRendererEnabled ?? defaults.webglRendererEnabled ?? true;
|
||||
const webglItem = document.getElementById('appSettingsWebglRendererItem');
|
||||
if (webglItem) webglItem.style.display = MobileDetection.getDeviceType() === 'desktop' ? '' : 'none';
|
||||
document.getElementById('appSettingsShowMonitor').checked = settings.showMonitor ?? defaults.showMonitor ?? false;
|
||||
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? defaults.showProjectInsights ?? false;
|
||||
document.getElementById('appSettingsShowFileBrowser').checked = settings.showFileBrowser ?? defaults.showFileBrowser ?? false;
|
||||
@@ -1377,6 +1382,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
tunnelEnabled: document.getElementById('appSettingsTunnelEnabled').checked,
|
||||
localEchoEnabled: document.getElementById('appSettingsLocalEcho').checked,
|
||||
cjkInputEnabled: document.getElementById('appSettingsCjkInput').checked,
|
||||
webglRendererEnabled: document.getElementById('appSettingsWebglRenderer').checked,
|
||||
extendedKeyboardBar: document.getElementById('appSettingsExtendedKeyboardBar').checked,
|
||||
tabTwoRows: document.getElementById('appSettingsTabTwoRows').checked,
|
||||
skin: document.getElementById('appSettingsSkin').value,
|
||||
@@ -1694,6 +1700,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
ralphTrackerEnabled: false,
|
||||
tabTwoRows: false,
|
||||
cjkInputEnabled: false,
|
||||
webglRendererEnabled: false, // mobile always uses the DOM renderer
|
||||
skin: 'daylight-blue',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,9 +246,6 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Lazy-loaded: script downloaded only on desktop (saves 244KB on mobile).
|
||||
this._webglAddon = null;
|
||||
const _params = new URLSearchParams(location.search);
|
||||
if (_params.get('webgl') === 'force') {
|
||||
try { localStorage.removeItem('codeman-webgl-disabled'); } catch {}
|
||||
}
|
||||
const _stickyDisabled = (() => {
|
||||
try {
|
||||
const raw = localStorage.getItem('codeman-webgl-disabled');
|
||||
@@ -263,11 +260,23 @@ Object.assign(CodemanApp.prototype, {
|
||||
return true;
|
||||
} catch { return false; }
|
||||
})();
|
||||
const skipWebGL =
|
||||
MobileDetection.getDeviceType() !== 'desktop' ||
|
||||
_params.has('nowebgl') ||
|
||||
_stickyDisabled;
|
||||
if (_stickyDisabled) {
|
||||
// User's "WebGL Renderer" toggle (Settings > Appearance). undefined = untouched
|
||||
// (desktop default on); false = explicit opt-out; true = explicit opt-in.
|
||||
const _webglSettings = this.loadAppSettingsFromStorage();
|
||||
const _webglDefaults = this.getDefaultSettings();
|
||||
const _webglPref = _webglSettings.webglRendererEnabled ?? _webglDefaults.webglRendererEnabled;
|
||||
const { skip: skipWebGL, clearSticky: _clearWebglSticky } = shouldSkipWebGL({
|
||||
deviceType: MobileDetection.getDeviceType(),
|
||||
noWebglParam: _params.has('nowebgl'),
|
||||
forceParam: _params.get('webgl') === 'force',
|
||||
stickyDisabled: _stickyDisabled,
|
||||
userPrefEnabled: _webglPref,
|
||||
});
|
||||
// Explicit opt-in (toggle ON) or ?webgl=force retires a stale auto-fallback marker.
|
||||
if (_clearWebglSticky) {
|
||||
try { localStorage.removeItem('codeman-webgl-disabled'); } catch {}
|
||||
}
|
||||
if (skipWebGL && _stickyDisabled) {
|
||||
console.log('[CRASH-DIAG] WebGL sticky-disabled from prior stalls — DOM renderer in use. Re-enable: ?webgl=force');
|
||||
}
|
||||
if (!skipWebGL) {
|
||||
|
||||
@@ -215,4 +215,80 @@ describe('WebGL longtask auto-fallback', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldSkipWebGL — renderer toggle + fallback precedence', () => {
|
||||
type Input = {
|
||||
deviceType?: string;
|
||||
noWebglParam?: boolean;
|
||||
forceParam?: boolean;
|
||||
stickyDisabled?: boolean;
|
||||
userPrefEnabled?: boolean;
|
||||
};
|
||||
const run = async (input: Input): Promise<{ skip: boolean; clearSticky: boolean }> =>
|
||||
page.evaluate(
|
||||
(i) =>
|
||||
(
|
||||
window as unknown as {
|
||||
shouldSkipWebGL: (x: unknown) => { skip: boolean; clearSticky: boolean };
|
||||
}
|
||||
).shouldSkipWebGL(i),
|
||||
input
|
||||
);
|
||||
|
||||
it('exposes shouldSkipWebGL on window', async () => {
|
||||
const t = await page.evaluate(
|
||||
() => typeof (window as unknown as { shouldSkipWebGL?: unknown }).shouldSkipWebGL
|
||||
);
|
||||
expect(t).toBe('function');
|
||||
});
|
||||
|
||||
it('mobile always skips, even when the user opted in', async () => {
|
||||
expect(await run({ deviceType: 'mobile', userPrefEnabled: true })).toEqual({
|
||||
skip: true,
|
||||
clearSticky: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('explicit opt-out skips without touching the sticky marker', async () => {
|
||||
expect(await run({ deviceType: 'desktop', userPrefEnabled: false, stickyDisabled: false })).toEqual({
|
||||
skip: true,
|
||||
clearSticky: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('?nowebgl is a one-shot opt-out (sticky untouched)', async () => {
|
||||
expect(await run({ deviceType: 'desktop', noWebglParam: true })).toEqual({
|
||||
skip: true,
|
||||
clearSticky: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('explicit opt-in enables and clears a stale sticky marker', async () => {
|
||||
expect(await run({ deviceType: 'desktop', userPrefEnabled: true, stickyDisabled: true })).toEqual({
|
||||
skip: false,
|
||||
clearSticky: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('?webgl=force enables and clears the sticky marker', async () => {
|
||||
expect(await run({ deviceType: 'desktop', forceParam: true, stickyDisabled: true })).toEqual({
|
||||
skip: false,
|
||||
clearSticky: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('untouched default respects an active sticky marker', async () => {
|
||||
expect(await run({ deviceType: 'desktop', stickyDisabled: true })).toEqual({
|
||||
skip: true,
|
||||
clearSticky: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('untouched default enables WebGL when nothing is disabling it', async () => {
|
||||
expect(await run({ deviceType: 'desktop', stickyDisabled: false })).toEqual({
|
||||
skip: false,
|
||||
clearSticky: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user