mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
fix(mobile): gate the phone overview's run picker on CLI availability
MOBILE_OVERVIEW_RUN_MODES / _buildMobileOverviewRunMenu is a separate, hardcoded duplicate of the toolbar's #runModeMenu (mobile-overview.js is a newer feature that mirrors the toolbar menu's look/behavior rather than reusing its render), so it never picked up #201's isCliAvailable() gating and offered every backend regardless of what the server actually has installed. Gate it the same way: skip an entry unless isCliAvailable(mode), shell always exempt. Added functional + static regression tests mirroring the toolbar menu's own test pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e2a644997e
commit
1ea39de650
@@ -486,6 +486,11 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
* The Run picker: the same backends as the toolbar's run-mode menu, plus saved
|
* The Run picker: the same backends as the toolbar's run-mode menu, plus saved
|
||||||
* web tabs. Deliberately no "Recent Sessions" block, unlike the toolbar menu:
|
* web tabs. Deliberately no "Recent Sessions" block, unlike the toolbar menu:
|
||||||
* past conversations have their own section further down this screen.
|
* past conversations have their own section further down this screen.
|
||||||
|
*
|
||||||
|
* Gated the same way as the toolbar's #runModeMenu (isCliAvailable(), shell
|
||||||
|
* exempt) — this list is a separate, hardcoded duplicate of the toolbar's menu
|
||||||
|
* rather than a shared render, so it never picked up #201's gating and offered
|
||||||
|
* every backend regardless of what's actually installed.
|
||||||
*/
|
*/
|
||||||
_buildMobileOverviewRunMenu() {
|
_buildMobileOverviewRunMenu() {
|
||||||
const menu = document.createElement('div');
|
const menu = document.createElement('div');
|
||||||
@@ -493,6 +498,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const current = this.runMode || 'claude';
|
const current = this.runMode || 'claude';
|
||||||
|
|
||||||
for (const entry of MOBILE_OVERVIEW_RUN_MODES) {
|
for (const entry of MOBILE_OVERVIEW_RUN_MODES) {
|
||||||
|
if (entry.mode !== 'shell' && !this.isCliAvailable(entry.mode)) continue;
|
||||||
const option = document.createElement('button');
|
const option = document.createElement('button');
|
||||||
option.type = 'button';
|
option.type = 'button';
|
||||||
option.className = 'mobile-overview-run-option' + (entry.mode === current ? ' selected' : '');
|
option.className = 'mobile-overview-run-option' + (entry.mode === current ? ' selected' : '');
|
||||||
|
|||||||
@@ -12,13 +12,34 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
||||||
|
|
||||||
|
/** Minimal fake DOM node — enough surface for mobile-overview.js's programmatic builders. */
|
||||||
|
function fakeElement(): any {
|
||||||
|
const el: any = {
|
||||||
|
className: '',
|
||||||
|
type: '',
|
||||||
|
dataset: {},
|
||||||
|
style: {},
|
||||||
|
children: [] as any[],
|
||||||
|
setAttribute() {},
|
||||||
|
appendChild(child: any) {
|
||||||
|
el.children.push(child);
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function loadOverviewApp(overrides: Record<string, any> = {}) {
|
function loadOverviewApp(overrides: Record<string, any> = {}) {
|
||||||
const CodemanApp = function CodemanApp(this: any) {};
|
const CodemanApp = function CodemanApp(this: any) {};
|
||||||
const context = vm.createContext({
|
const context = vm.createContext({
|
||||||
CodemanApp,
|
CodemanApp,
|
||||||
console,
|
console,
|
||||||
window: {},
|
window: {},
|
||||||
document: { getElementById: () => null },
|
document: {
|
||||||
|
getElementById: () => null,
|
||||||
|
createElement: () => fakeElement(),
|
||||||
|
createElementNS: () => fakeElement(),
|
||||||
|
},
|
||||||
MobileDetection: { getDeviceType: () => 'mobile' },
|
MobileDetection: { getDeviceType: () => 'mobile' },
|
||||||
});
|
});
|
||||||
vm.runInContext(readFileSync(resolve(PUBLIC, 'mobile-overview.js'), 'utf8'), context, {
|
vm.runInContext(readFileSync(resolve(PUBLIC, 'mobile-overview.js'), 'utf8'), context, {
|
||||||
@@ -272,3 +293,47 @@ describe('mobile overview wiring', () => {
|
|||||||
expect(hardcoded).toEqual([]);
|
expect(hardcoded).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('mobile overview run picker (CLI availability gating)', () => {
|
||||||
|
function modeButtons(menu: any): string[] {
|
||||||
|
return menu.children.filter((c: any) => c.dataset.moAction === 'run-mode').map((c: any) => c.dataset.moMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #201 gated the toolbar's #runModeMenu on isCliAvailable(); this phone-only
|
||||||
|
// picker (MOBILE_OVERVIEW_RUN_MODES / _buildMobileOverviewRunMenu) is a
|
||||||
|
// separate, hardcoded duplicate of that menu rather than a shared render, so
|
||||||
|
// it silently offered every backend regardless of what the server reported.
|
||||||
|
it('hides run modes the server reports as unavailable, keeps shell always', () => {
|
||||||
|
const app = loadOverviewApp({
|
||||||
|
runMode: 'claude',
|
||||||
|
isCliAvailable: (tool: string) => tool === 'claude',
|
||||||
|
});
|
||||||
|
const menu = app._buildMobileOverviewRunMenu();
|
||||||
|
expect(modeButtons(menu)).toEqual(['claude', 'shell']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows every mode when every CLI is available', () => {
|
||||||
|
const app = loadOverviewApp({
|
||||||
|
runMode: 'claude',
|
||||||
|
isCliAvailable: () => true,
|
||||||
|
});
|
||||||
|
const menu = app._buildMobileOverviewRunMenu();
|
||||||
|
expect(modeButtons(menu)).toEqual(['claude', 'opencode', 'codex', 'gemini', 'antigravity', 'shell']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gates every mode the picker actually offers', () => {
|
||||||
|
// Catches a new backend being added to MOBILE_OVERVIEW_RUN_MODES without
|
||||||
|
// being gated — the same class of bug that let this list drift from the
|
||||||
|
// toolbar menu's gating in the first place.
|
||||||
|
const src = readFileSync(resolve(PUBLIC, 'mobile-overview.js'), 'utf8');
|
||||||
|
const modesBlock = src.slice(
|
||||||
|
src.indexOf('const MOBILE_OVERVIEW_RUN_MODES'),
|
||||||
|
src.indexOf('];', src.indexOf('const MOBILE_OVERVIEW_RUN_MODES')) + 2
|
||||||
|
);
|
||||||
|
const offered = [...modesBlock.matchAll(/mode: '([^']+)'/g)].map((m) => m[1]);
|
||||||
|
expect(offered).toContain('antigravity');
|
||||||
|
const fn = src.slice(src.indexOf('_buildMobileOverviewRunMenu() {'));
|
||||||
|
const gate = fn.slice(0, fn.indexOf('const header'));
|
||||||
|
expect(gate).toContain('isCliAvailable');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user