diff --git a/src/firstRunExperience.test.mjs b/src/firstRunExperience.test.mjs index 81c1cfda5..87229e5c5 100644 --- a/src/firstRunExperience.test.mjs +++ b/src/firstRunExperience.test.mjs @@ -1030,10 +1030,11 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping // preserves the mission mapping and globe-rotation arguments. // S2 GhostMaps extends the generated layer enums; both feature contracts remain. // The layer alias hint leaves out aliases the layer id already spells. - assert.equal(block.length, 30901, 'serialized tool schema length drifted'); + // The Utilities panel adds one set_panel_open target. + assert.equal(block.length, 30919, 'serialized tool schema length drifted'); assert.equal( crypto.createHash('sha256').update(block).digest('hex'), - '9e1bd8fde648953c47718ba248758415a95c1484b0da6369df6b89b0505c0de2', + 'dbf9a45649179e626b2e3d938c43903f573c50ffa279b3245b18244dbe9a9284', 'the first-run missions must ride EXISTING tools: no schema edit, no cache bust', ); const instructions = fs.readFileSync( diff --git a/src/radioMarkup.test.mjs b/src/radioMarkup.test.mjs index 40acc6619..74a44e77d 100644 --- a/src/radioMarkup.test.mjs +++ b/src/radioMarkup.test.mjs @@ -152,7 +152,7 @@ test('the edited existing tools changed exactly as intended', () => { const panel = byName.get('set_panel_open'); assert.deepEqual( panel.parameters.properties.panelId.enum, - ['data-panel', 'location-bar', 'control-panel', 'cctv-panel', 'radio-panel', 'scene-panel', 'pp-toggles', 'global-context-panel', 'weather-panel', 'recent-imagery-panel', 'voice-settings', 'voice-preview', 'provider-settings'], + ['data-panel', 'location-bar', 'control-panel', 'cctv-panel', 'radio-panel', 'scene-panel', 'utilities-panel', 'pp-toggles', 'global-context-panel', 'weather-panel', 'recent-imagery-panel', 'voice-settings', 'voice-preview', 'provider-settings'], ); assert.deepEqual(panel.parameters.required, ['panelId', 'open']); diff --git a/src/voice/actionSchemas.js b/src/voice/actionSchemas.js index a27907666..9ec47028c 100644 --- a/src/voice/actionSchemas.js +++ b/src/voice/actionSchemas.js @@ -174,6 +174,7 @@ const schemas = [ 'cctv-panel', 'radio-panel', 'scene-panel', + 'utilities-panel', 'pp-toggles', 'global-context-panel', 'weather-panel', diff --git a/src/voice/actionSchemas.test.mjs b/src/voice/actionSchemas.test.mjs index 6f16f7606..d15a7c25e 100644 --- a/src/voice/actionSchemas.test.mjs +++ b/src/voice/actionSchemas.test.mjs @@ -43,12 +43,13 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea // Globe rotation adds one motion; visual keyhole fit and explicit // reset guidance/defaults update only move_camera and mode descriptions. // S2 GhostMaps extends the generated layer enums. + // The Utilities panel (Directions, Draw, Import) adds one panel target. // Panel adds named voice targets and timeout; shared Panel/Cockpit guidance // is consolidated without changing either feature's arguments. // Snow leaves the set_visual_style enum. // The Operator HUD layout leaves set_hud's layout enum. // The layer alias hint leaves out aliases the layer id already spells. - 'e65812db60a2702cebb496fca257cb04a28f1bbbf250cb9ab65a3db33005e0c8', + 'dc2bac1be93a503dc88d637beea2f1bc457e1f9167422bd91548b44756a8aca5', ); }); @@ -142,6 +143,7 @@ test('all legacy action arguments are byte-identical after removing the delibera 'voice-settings', 'voice-preview', 'provider-settings', + 'utilities-panel', ].includes(id), ); for (const tool of legacy) { diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 0ec5a0586..fbb6cc5ce 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -99,6 +99,12 @@ const PANEL_ALIASES = new Map([ ['global context', 'global-context-panel'], ['right context', 'global-context-panel'], ['context right panel', 'global-context-panel'], + ['utilities', 'utilities-panel'], + ['utilities panel', 'utilities-panel'], + ['draw', 'utilities-panel'], + ['drawing', 'utilities-panel'], + ['import', 'utilities-panel'], + ['import files', 'utilities-panel'], ['authoring', 'scene-panel'], ['scenes', 'scene-panel'], ['scene', 'scene-panel'], @@ -122,6 +128,7 @@ const PANEL_IDS = new Set([ 'cctv-panel', 'radio-panel', 'global-context-panel', + 'utilities-panel', 'scene-panel', 'pp-toggles', 'weather-panel', diff --git a/src/voice/gevActions.test.mjs b/src/voice/gevActions.test.mjs index 17fd43305..f6bccdfa9 100644 --- a/src/voice/gevActions.test.mjs +++ b/src/voice/gevActions.test.mjs @@ -754,9 +754,17 @@ test('showing a layer opens the panel that holds its row: Directions lives in Ut closest: (selector) => (selector === '[data-panel-id]' ? { id: panelId } : null), }); const rows = { directions: rowIn('utilities-panel'), flights: rowIn('data-panel') }; + // Each row is found only inside the list that really holds it. + const lists = { directions: '#utilities-toggles', flights: '#data-toggles' }; globalThis.CSS = { escape: (value) => value }; globalThis.document = { - querySelector: (selector) => rows[/data-layer-id="([^"]+)"/.exec(selector)?.[1]] || null, + querySelector: (selector) => { + for (const part of selector.split(',')) { + const [, list, id] = /^\s*(#[\w-]+) \[data-layer-id="([^"]+)"\]\s*$/.exec(part) || []; + if (id && lists[id] === list) return rows[id]; + } + return null; + }, }; try { const opened = []; @@ -4001,3 +4009,26 @@ test('set_panel_open accepts the Authoring name for the scene panel', async () = } assert.deepEqual(calls.at(-1), ['scene-panel', false, { explicit: true }]); }); + +test('set_panel_open reaches the Utilities panel by its name, Draw or Import', async () => { + const calls = []; + const runner = createGevActionRunner({ + viewer: { + clock: { onTick: { addEventListener: () => () => {} } }, + scene: { canvas: { addEventListener() {}, removeEventListener() {} } }, + camera: { + moveEnd: { addEventListener() {} }, + positionWC: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 1000), + }, + }, + styleManager: { + setPanelCollapsed: (...args) => calls.push(args), + }, + dataManager: { layers: new Map(), isEnabled: () => false, getAll: () => [] }, + }); + for (const panel of ['utilities-panel', 'utilities', 'drawing', 'import']) { + const result = await runner('set_panel_open', { panelId: panel, open: true }); + assert.equal(result.panelId, 'utilities-panel', panel); + } + assert.deepEqual(calls.at(-1), ['utilities-panel', false, { explicit: true }]); +});