@bongos/core 1.24.81 → 1.24.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.bongos-core.json +85 -70
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +5 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/api/openapi.json +86 -2
  8. package/docs/api-reference.md +4 -2
  9. package/docs/copy-inventory.md +38 -32
  10. package/docs/copy-registry.json +107 -52
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-inventory.json +1 -0
  13. package/docs/page-readings.json +202 -196
  14. package/modules/builder-settings/hall-view-prefs.js +50 -0
  15. package/modules/builder-settings/routes/settings.js +32 -0
  16. package/modules/hall-ui/public/ai-studio.html +3 -3
  17. package/modules/hall-ui/public/atlas.html +3 -3
  18. package/modules/hall-ui/public/blockers.html +3 -3
  19. package/modules/hall-ui/public/board-room.html +3 -3
  20. package/modules/hall-ui/public/city.html +4 -4
  21. package/modules/hall-ui/public/collab.html +3 -3
  22. package/modules/hall-ui/public/commands.html +3 -3
  23. package/modules/hall-ui/public/copy-desk.html +3 -3
  24. package/modules/hall-ui/public/deploy.html +3 -3
  25. package/modules/hall-ui/public/goal-map.html +4 -4
  26. package/modules/hall-ui/public/goals.html +3 -3
  27. package/modules/hall-ui/public/government.html +3 -3
  28. package/modules/hall-ui/public/idea.html +3 -3
  29. package/modules/hall-ui/public/ideas.html +3 -3
  30. package/modules/hall-ui/public/index.html +5 -5
  31. package/modules/hall-ui/public/market-watch-item.html +3 -3
  32. package/modules/hall-ui/public/market-watch-rules.html +3 -3
  33. package/modules/hall-ui/public/market-watch.html +3 -3
  34. package/modules/hall-ui/public/modules.html +3 -3
  35. package/modules/hall-ui/public/money.html +3 -3
  36. package/modules/hall-ui/public/my-agents.html +3 -3
  37. package/modules/hall-ui/public/options-menu.js +2 -1
  38. package/modules/hall-ui/public/primer.html +3 -3
  39. package/modules/hall-ui/public/profile.html +3 -3
  40. package/modules/hall-ui/public/project-settings.html +3 -3
  41. package/modules/hall-ui/public/roster.html +3 -3
  42. package/modules/hall-ui/public/settings-view.js +75 -0
  43. package/modules/hall-ui/public/settings.html +17 -4
  44. package/modules/hall-ui/public/shell-game.css +10 -0
  45. package/modules/hall-ui/public/shell-game.js +98 -33
  46. package/modules/hall-ui/public/shell.js +4 -3
  47. package/modules/hall-ui/public/street.html +3 -3
  48. package/modules/hall-ui/public/studio.html +3 -3
  49. package/modules/hall-ui/public/style.css +2 -0
  50. package/modules/hall-ui/public/task.html +3 -3
  51. package/modules/hall-ui/public/thinking.html +4 -4
  52. package/modules/hall-ui/public/tweak-editor.html +3 -3
  53. package/modules/hall-ui/public/visor.js +7 -1
  54. package/modules/hall-ui/public/watch.html +3 -3
  55. package/modules/hall-ui/public/work.html +3 -3
  56. package/modules/hall-ui/records/game-mode.md +5 -4
  57. package/modules/hall-ui/records/start-screen.md +4 -0
  58. package/package-lock.json +2 -2
  59. package/package.json +1 -1
  60. package/release-notes.json +6 -0
  61. package/scripts/hall-preview/pref-writes.js +8 -1
  62. package/src/module-api.js +1 -1
  63. package/tests/hall_fullscreen.mjs +42 -21
  64. package/tests/hall_start_screen.mjs +38 -3
  65. package/tests/hall_view_prefs.mjs +42 -0
  66. package/tests/hall_visor.mjs +3 -2
  67. package/tests/nav_permission_atoms.mjs +1 -1
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
11
11
  <link rel="preconnect" href="https://fonts.googleapis.com">
12
12
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm">
13
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-view2">
14
14
  <link rel="stylesheet" href="/builders/tweak-editor.css?v=2026-09-29-tw27-aqfix">
15
15
  </head>
16
16
  <!-- THE TWEAK EDITOR (task 1004321 / BV2.TW10, goal 1000095; ADR 0341 D4 and
@@ -143,10 +143,10 @@
143
143
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
144
144
 
145
145
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
146
- <script src="/builders/shell-game.js?v=2026-11-11-helm"></script>
146
+ <script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
147
147
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
148
148
  <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
149
- <script src="/builders/shell.js?v=2026-11-11-helm"></script>
149
+ <script src="/builders/shell.js?v=2026-11-11-view2"></script>
150
150
  <script src="/builders/hall-dialog.js"></script>
151
151
  <script src="/builders/palette.js" defer></script>
152
152
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -69,7 +69,13 @@
69
69
  var mode = typeof C.current === 'function' ? C.current() : null;
70
70
  return ACTIONS[mode] ? mode : null;
71
71
  }
72
- function actionsFor(id) { return ACTIONS[id] ? ACTIONS[id].slice() : []; }
72
+ // Game mode reaches only the game's places (task 1005200): an action that opens a classic
73
+ // page is off the glass until that page has a game version. A tool is always this page's.
74
+ function inGame(a) {
75
+ var G = window.OTBShellGame;
76
+ return !a.href || !G || typeof G.isPlace !== 'function' || G.isPlace(a.href);
77
+ }
78
+ function actionsFor(id) { return ACTIONS[id] ? ACTIONS[id].filter(inGame) : []; }
73
79
  function credits() {
74
80
  var C = craft();
75
81
  var me = C && typeof C.me === 'function' ? C.me() : null;
@@ -11,7 +11,7 @@
11
11
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm">
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-view2">
15
15
  <!-- The oversight family's sheet (task 1003469): the figure band, the group
16
16
  head, the row, the form. Then the page's own: severities, the report
17
17
  reader, the grade trend. The attention tiles + tabs come from style.css's
@@ -269,10 +269,10 @@
269
269
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
270
270
  chrome exists before the page script runs. -->
271
271
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
272
- <script src="/builders/shell-game.js?v=2026-11-11-helm"></script>
272
+ <script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
273
273
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
274
274
  <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
275
- <script src="/builders/shell.js?v=2026-11-11-helm"></script>
275
+ <script src="/builders/shell.js?v=2026-11-11-view2"></script>
276
276
  <script src="/builders/hall-dialog.js"></script>
277
277
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
278
278
  <script src="/builders/palette.js" defer></script>
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-view2">
12
12
  <!-- Page-local styles (task 2170) — extracted from the old inline <style>
13
13
  block per the hall placement rules; shared pager/count-pill styles now
14
14
  live in style.css's hall-kit section. -->
@@ -172,10 +172,10 @@
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
174
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
175
- <script src="/builders/shell-game.js?v=2026-11-11-helm"></script>
175
+ <script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
176
176
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
177
177
  <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
178
- <script src="/builders/shell.js?v=2026-11-11-helm"></script>
178
+ <script src="/builders/shell.js?v=2026-11-11-view2"></script>
179
179
  <script src="/builders/hall-dialog.js"></script>
180
180
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
181
181
  <script src="/builders/palette.js" defer></script>
@@ -7,14 +7,15 @@ Covers: every shelled hall page (`shell-game.js`, `shell.js`). Spec: `docs/specs
7
7
  - **Game mode is the existing full-screen switch, made hall-wide. There is no second full-screen mode.** The switch was already on every page (task 1004430) but remembered per page, so a reader had to turn it on 33 times. Game mode is one `localStorage` key, `hall-game`; while it is `on`, `shell.js` `storedFullscreen()` answers yes for every page, whatever that page remembers. What hides, the corner button and the Escape hand-off are the shell's, unchanged.
8
8
  - **`shell-game.js` holds the mode and nothing else**: `window.OTBShellGame` = `on()`, `set(bool)`, `hint()`. It stamps `html[data-game="on"]` for a sheet or script that wants to read the mode. `shell.js` reads it through a fallback object, so a page without the file is never in game mode. It is its own file because `shell.js` sits on the 1,500-line ratchet; the start screen and options menu (R03, R04) are expected to grow here, not in `shell.js`.
9
9
  - **Load order**: `shell-game.js` before `shell-trail.js` / `craft-mode.js` / `shell.js`, loaded the same way (deferred or not) as the shell on that page. Test-pinned.
10
- - **Leaving is for this page load only.** In game mode the corner button brings the bars back (Escape did too until task 1005144; it opens the options menu now), and nothing is written: the next page opens without them again, and a page's own `hall-fullscreen:<page>` memory is never overwritten. Turning game mode off (`set(false)`) hands each page back what it remembered. `OTBShell.setFullscreen(null)` is the "re-read what should show" call `set()` uses.
10
+ - **Game or classic, never both (task 1005200, owner 2026-11-11).** Inside game mode there is no way back to the bars: the corner button is hidden (`style.css`) and does nothing if pressed, Escape opens the options menu, and the "how to leave" line is never shown. The only switch is Options → Preferences → View (`settings-view.js`), plus the chooser at every sign-in (`records/start-screen.md`). A page's own `hall-fullscreen:<page>` memory is still never overwritten, so switching to classic hands each page back what it remembered.
11
+ - **Game mode reaches only the game's places** (`shell-game.js` `PLACES`: home, city, goal-map, thinking, atlas, settings, profile). Any other hall page `location.replace`s to the city on load, and so does switching to game on one. A screen embedded in the options menu is exempt. The visor drops an action whose `href` is not a place (`visor.js` `inGame`). A page joins `PLACES` when it has a game version.
12
+ - **The choice is kept on the account**: `GET/PATCH /api/bongos/me/hall-view` (`modules/builder-settings/hall-view-prefs.js`, key `hall_view` in `behavior_prefs`, null = never chosen). `hall-game` in localStorage is only the copy a page paints from; `sync()` reads the account on every page load and switches if another browser changed it.
11
13
  - **No page may opt out.** `<body data-fullscreen-switch="off">` would keep that page's sidebar in game mode; the test fails on one. A new hall page needs `#app-topbar` and the `shell-game.js` tag, like the rest.
12
- - **The "how to leave" line shows once per tab in game mode** (`sessionStorage 'hall-game-hint'`), not on every page load. Outside game mode the shell's rule is unchanged.
13
- - **How it is turned on today**: `?game=on` / `?game=off` on any hall URL, or `OTBShellGame.set()`. Nothing in the hall's interface sets it yet; the start screen (R03) is where that lands.
14
+ - **`?game=on` / `?game=off` are gone** (task 1005200): the address no longer switches the view.
14
15
 
15
16
  ## For the tasks after this one
16
17
 
17
18
  - A HUD or options menu that wants Escape must call `preventDefault()` on the press. `shell.js` `onFullscreenKey` waits one tick before leaving so a listener added after it still wins.
18
- - Since R04 (task 1005144, ADR 0401) the last Escape in game mode opens the options menu (`shell-game.js` `onEscape`), and on the menu it closes it again. The corner button is still the way back to the bars.
19
+ - Since R04 (task 1005144, ADR 0401) the last Escape in game mode opens the options menu (`shell-game.js` `onEscape`), and on the menu it closes it again. Since task 1005200 there is no way back to the bars inside game mode.
19
20
 
20
21
  Tests: `tests/hall_fullscreen.mjs` (the eight `game mode:` cases run the real `shell-game.js` + `shell.js`), and the script lists in `tests/hall_city.mjs`, `tests/hall_street.mjs`, `tests/hall_my_agents.mjs`, `tests/goal_map_page.mjs`.
@@ -23,3 +23,7 @@ Covers: Home (`shell-game.js` `start()`, `shell-game.css`, the hand-off in `welc
23
23
  - New art for the start screen (the city in the clouds) is task 1005196; `welcome-boot.js` still hands in the welcome's painting.
24
24
 
25
25
  Tests: `tests/hall_start_screen.mjs` (the hand-off and pure parts in a vm; both screens, the panel's place at desk and phone widths, the segments, Quit, the no-city and reduced-motion paths in a real browser).
26
+
27
+ ## The view chooser (task 1005200, owner 2026-11-11)
28
+
29
+ Every sign-in now opens on a chooser before the start screen: **Game** or **Classic**, each with one line saying what it is. The view chosen last time (the browser's `hall-game` copy) is marked "Last time", lit with the accent on its edge, and focused, so Enter keeps it. Game goes on to the start screen as before; Classic closes the screen and the hall is as it always was. Either choice is saved to the account (`PATCH /api/bongos/me/hall-view`). The Extras list lost The docs: it is not a game place. Tests: `tests/hall_start_screen.mjs`.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.81",
3
+ "version": "1.24.82",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.81",
9
+ "version": "1.24.82",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.81",
3
+ "version": "1.24.82",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11589,5 +11589,11 @@
11589
11589
  "id": "1005192",
11590
11590
  "text": "The hall now wears the new Helm look: deep night panels, ice-blue text, wide capital titles and cut-corner buttons, all from one shared design system. The start and load screens are rebuilt to match, and the hall is dark only."
11591
11591
  }
11592
+ ],
11593
+ "1.24.82": [
11594
+ {
11595
+ "id": "1005200",
11596
+ "text": "The hall now has two separate views, Game and Classic, and never mixes them. You choose one each time you sign in and can switch only in Options. Game mode stays full screen and only opens the game's places."
11597
+ }
11592
11598
  ]
11593
11599
  }
@@ -14,7 +14,7 @@
14
14
  //
15
15
  // What it is NOT: storage. Nothing is written to disk and a restart forgets
16
16
  // everything; each recorded write is logged so the harness console shows what the
17
- // page sent. Only these five routes are recorded — every other write is refused
17
+ // page sent. Only these routes are recorded — every other write is refused
18
18
  // exactly as before.
19
19
  'use strict';
20
20
 
@@ -27,6 +27,7 @@ const wandering = lib('wandering-prefs');
27
27
  const models = lib('model-alloc-prefs');
28
28
  const render = lib('render-prefs');
29
29
  const skills = lib('skill-prefs');
30
+ const hallView = lib('hall-view-prefs'); // task 1005200: game or classic
30
31
 
31
32
  // apiPath → the /me slice it answers for (null: the skill prefs, not on /me)
32
33
  const ROUTES = {
@@ -35,6 +36,7 @@ const ROUTES = {
35
36
  'me/model-allocation': 'model_allocation',
36
37
  'me/render': 'render',
37
38
  'me/skill-prefs': null,
39
+ 'me/hall-view': 'hall_view',
38
40
  };
39
41
 
40
42
  function createPrefRecorder({ log = console.log } = {}) {
@@ -63,6 +65,7 @@ function createPrefRecorder({ log = console.log } = {}) {
63
65
  if (slice === 'wandering') return wandering.describeForApi(behavior, rank);
64
66
  if (slice === 'model_allocation') return models.describeForApi(behavior, instanceModels);
65
67
  if (slice === 'render') return render.describeForApi(behavior);
68
+ if (slice === 'hall_view') return hallView.describeForApi(behavior);
66
69
  return {
67
70
  prefs: skillPrefs, effective: skills.listEffective(skillPrefs), defaults: skills.SKILL_DEFAULTS,
68
71
  known_skills: skills.KNOWN_SKILLS, allowed_models: skills.ALLOWED_MODELS,
@@ -96,6 +99,10 @@ function createPrefRecorder({ log = console.log } = {}) {
96
99
  v = render.validatePatch(b);
97
100
  if (!v.ok) return invalid(v.errors);
98
101
  behavior = render.applyPatch(behavior, v.patch);
102
+ } else if (slice === 'hall_view') {
103
+ v = hallView.validatePatch(b);
104
+ if (!v.ok) return invalid(v.errors);
105
+ behavior = hallView.applyView(behavior, v.view);
99
106
  } else {
100
107
  v = skills.validatePatch(b);
101
108
  if (!v.ok) return invalid(v.errors);
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.81'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.82'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -94,7 +94,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
94
94
  get cookie() { return ''; }, set cookie(_v) {},
95
95
  };
96
96
  const assigned = []; // task 1005144: where the page sent the browser (game mode's Escape opens the menu)
97
- const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign(u) { assigned.push(u); }, replace() {} };
97
+ const replaced = []; // task 1005200: a classic page in game mode sends the browser to the city
98
+ const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign(u) { assigned.push(u); }, replace(u) { replaced.push(u); } };
98
99
  // `session` is the TAB's sessionStorage: pass one object to several boots and
99
100
  // they are page loads in one tab (the trail, task 1004476).
100
101
  const sessionStorage = {
@@ -135,6 +136,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
135
136
  return {
136
137
  store,
137
138
  assigned,
139
+ replaced,
138
140
  game: () => windowObj.OTBShellGame,
139
141
  gameStamp: () => root.dataset.game === 'on',
140
142
  // The "how to leave" line: showing = the shell created it and it is not hidden.
@@ -517,17 +519,36 @@ test('game mode: every hall page carries the switch — none opts out, and each
517
519
  }
518
520
  });
519
521
 
520
- test('game mode: leaving brings the bars back for THIS page only, and never rewrites a page\'s own memory', () => {
522
+ // task 1005200 (owner, 2026-11-11): game OR classic, never both. Inside game mode there is
523
+ // no way back to the bars — the corner button is hidden (style.css) and, pressed
524
+ // anyway, does nothing; Escape is the options menu's, where the view is switched.
525
+ test('game mode: there is no way back to the bars inside it, and a page\'s own memory is never rewritten', () => {
521
526
  const store = { 'hall-game': 'on', 'hall-fullscreen:thinking': 'on' };
522
- const goals = boot({ page: 'goals', optIn: false, store });
523
- goals.button().click();
524
- assert.equal(goals.on(), false, 'the corner button still leaves (Escape opens the options menu, below)');
525
- assert.equal(store['hall-fullscreen:goals'], undefined, 'nothing remembered for the page');
526
- assert.equal(boot({ page: 'goals', optIn: false, store }).on(), true, 'the next load opens without bars again');
527
+ const profile = boot({ page: 'profile', optIn: false, store });
528
+ profile.button().click();
529
+ assert.equal(profile.on(), true, 'the corner button does not leave game mode');
530
+ assert.equal(store['hall-fullscreen:profile'], undefined, 'nothing remembered for the page');
527
531
  const sky = boot({ page: 'thinking', store });
528
532
  sky.button().click();
529
- assert.equal(sky.on(), false, 'the corner button leaves too');
533
+ sky.press('Escape');
534
+ assert.equal(sky.on(), true, 'nor does Escape');
530
535
  assert.equal(store['hall-fullscreen:thinking'], 'on', 'the sky\'s own choice survives game mode');
536
+ assert.match(read('style.css'), /html\[data-game="on"\] \.fullscreen-toggle \{ display: none; \}/, 'and the button is not drawn');
537
+ });
538
+
539
+ test('game mode: only the game\'s places are there — a classic page sends the browser to the city', () => {
540
+ const store = { 'hall-game': 'on' };
541
+ assert.deepEqual(boot({ page: 'goals', optIn: false, store }).replaced, ['/'], 'Goals is a classic page');
542
+ assert.deepEqual(boot({ page: 'work', optIn: false, store, path: '/builders/work' }).replaced, ['/builders/'], 'on the apex too');
543
+ for (const page of ['home', 'city', 'goal-map', 'thinking', 'atlas', 'settings', 'profile']) {
544
+ assert.deepEqual(boot({ page, optIn: false, store }).replaced, [], `${page} is a game place`);
545
+ }
546
+ assert.deepEqual(boot({ page: 'goals', optIn: false, store: {} }).replaced, [], 'classic reaches every page');
547
+ const G = boot({ page: 'city', store }).game();
548
+ assert.equal(G.isPlace('/city'), true);
549
+ assert.equal(G.isPlace('/builders/'), true);
550
+ assert.equal(G.isPlace('/work'), false);
551
+ assert.equal(G.isPlace('/blockers?x=1'), false);
531
552
  });
532
553
 
533
554
  // task 1005144 (BONGOS-V3 R04, ADR 0399 §2.4 row 6): in game mode the last Escape is the
@@ -562,35 +583,35 @@ test('game mode: the Escape hand-off is unchanged — a press the page used, or
562
583
 
563
584
  test('game mode: set() switches the page live, and turning it off hands each page back its own memory', () => {
564
585
  const store = { 'hall-fullscreen:thinking': 'on' };
565
- const goals = boot({ page: 'goals', optIn: false, store });
586
+ const goals = boot({ page: 'profile', optIn: false, store });
566
587
  goals.game().set(true);
567
588
  assert.equal(store['hall-game'], 'on');
568
589
  assert.equal(goals.on(), true, 'on at once, no reload');
569
590
  assert.ok(goals.resizes() >= 1, 'a page that sizes to its box is told to re-measure');
570
591
  goals.game().set(false);
571
- assert.equal(goals.on(), false, 'goals remembered nothing, so its bars return');
592
+ assert.equal(goals.on(), false, 'the profile remembered nothing, so its bars return');
572
593
  assert.equal(goals.gameStamp(), false);
573
- assert.equal(store['hall-fullscreen:goals'], undefined, 'and switching the mode wrote nothing for the page');
594
+ assert.equal(store['hall-fullscreen:profile'], undefined, 'and switching the mode wrote nothing for the page');
595
+ const work = boot({ page: 'work', optIn: false, store: {} });
596
+ work.game().set(true);
597
+ assert.deepEqual(work.replaced, ['/'], 'switched to game on a classic page: to the city');
574
598
  const sky = boot({ page: 'thinking', store: Object.assign(store, { 'hall-game': 'on' }) });
575
599
  sky.game().set(false);
576
600
  assert.equal(sky.on(), true, 'the sky had chosen full screen itself, and keeps it');
577
601
  });
578
602
 
579
- test('game mode: ?game=on / ?game=off set it from the URL; anything else is ignored', () => {
603
+ test('game mode: the address does not switch it — Options and the sign-in chooser do (task 1005200)', () => {
580
604
  const store = {};
581
- assert.equal(boot({ page: 'work', optIn: false, store, search: '?game=on' }).on(), true);
582
- assert.equal(store['hall-game'], 'on');
583
- assert.equal(boot({ page: 'goals', optIn: false, store }).on(), true, 'and it holds on the next page');
584
- assert.equal(boot({ page: 'work', optIn: false, store, search: '?game=off' }).on(), false);
585
- boot({ page: 'work', optIn: false, store, search: '?game=%22%3E%3Cimg%3E' });
586
- assert.equal(store['hall-game'], 'off', 'a value that is not on/off is not stored');
605
+ assert.equal(boot({ page: 'profile', optIn: false, store, search: '?game=on' }).on(), false);
606
+ assert.equal(store['hall-game'], undefined, 'nothing stored from the URL');
607
+ assert.doesNotMatch(read('shell-game.js'), /get\('game'\)/, 'the ?game= reader is gone');
587
608
  });
588
609
 
589
- test('game mode: how to leave is said once per tab, not on every page; outside it the shell\'s rule stands', () => {
610
+ test('game mode never says how to leave full screen (there is no leaving); outside it the shell\'s rule stands', () => {
590
611
  const store = { 'hall-game': 'on' };
591
612
  const session = {};
592
- assert.equal(boot({ page: 'goals', optIn: false, store, session }).hintShowing(), true, 'the first page says it');
593
- assert.equal(boot({ page: 'work', optIn: false, store, session }).hintShowing(), false, 'the next does not repeat it');
613
+ assert.equal(boot({ page: 'profile', optIn: false, store, session }).hintShowing(), false, 'no line in game mode');
614
+ assert.equal(boot({ page: 'city', store, session }).hintShowing(), false);
594
615
  const own = { 'hall-fullscreen:thinking': 'on' };
595
616
  assert.equal(boot({ page: 'thinking', store: own, session: {} }).hintShowing(), true);
596
617
  assert.equal(boot({ page: 'thinking', store: own, session: {} }).hintShowing(), true, 'a remembered page says it on every open, as before');
@@ -188,12 +188,21 @@ test('a sign-in passes through the start screen and the load screen before the w
188
188
  const { page, errors } = await signedIn(ctx);
189
189
  await page.waitForSelector('#game-start');
190
190
  assert.deepEqual(await state(page), { start: true, welcome: false, picker: false, game: null, full: null },
191
- 'the start screen, not the welcome and not the picker; game mode is not on until Start');
191
+ 'the start screen, not the welcome and not the picker; game mode is not on until it is chosen');
192
+ // task 1005200: first the chooser — game or classic — then, for game, the start screen.
193
+ const views = await page.evaluate(() => ({
194
+ items: Array.from(document.querySelectorAll('.gs__items:not([hidden]) .gs__item')).map((e) => e.textContent),
195
+ focus: document.activeElement.textContent,
196
+ }));
197
+ assert.deepEqual(views, { items: ['GameThe city, full screen', 'ClassicThe hall with its sidebar'], focus: 'GameThe city, full screen' });
198
+ await page.keyboard.press('Enter');
199
+ await page.waitForSelector('#gs-items:not([hidden])');
200
+ assert.equal((await state(page)).game, 'on', 'choosing Game turns game mode on');
192
201
  const menu = await page.evaluate(() => ({
193
202
  title: document.getElementById('gs-title').textContent,
194
- items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).map((e) => e.textContent),
203
+ items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras):not([hidden]) .gs__item')).map((e) => e.textContent),
195
204
  focus: document.activeElement.textContent,
196
- small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
205
+ small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras):not([hidden]) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
197
206
  }));
198
207
  assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
199
208
 
@@ -253,6 +262,29 @@ test('a sign-in passes through the start screen and the load screen before the w
253
262
  await ctx.close();
254
263
  });
255
264
 
265
+ // task 1005200: Classic at the chooser — the screen goes, game mode is off, and the account
266
+ // is told. The view chosen last time is the one marked and focused.
267
+ test('choosing Classic at sign-in closes the screen, leaves game mode off, and saves it to the account', { skip }, async () => {
268
+ const ctx = await hall();
269
+ const saved = [];
270
+ await ctx.route(ORIGIN + '/api/bongos/me/hall-view', (route) => {
271
+ if (route.request().method() === 'PATCH') saved.push(JSON.parse(route.request().postData()));
272
+ return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ view: null }) });
273
+ });
274
+ const page = await ctx.newPage();
275
+ await page.addInitScript(() => { try { localStorage.setItem('hall-welcome-signin', '1'); localStorage.setItem('hall-game', 'off'); } catch (_) { /* no storage */ } });
276
+ await page.goto(ORIGIN + '/');
277
+ await page.waitForFunction(() => !!window.OTBShell && !!window.OTBWelcome && !!window.OTBShellGame);
278
+ await page.evaluate(() => { const me = { authed: true, rank: 'metic', builder: { id: 7, display_name: 'Ada', github_login: 'ada' } }; window.OTBShell.applyAccess(me); window.OTBShell.applyAccess(me); });
279
+ await page.waitForSelector('#game-start');
280
+ assert.equal(await page.evaluate(() => document.activeElement.textContent), 'ClassicLast timeThe hall with its sidebar', 'the view chosen last time is focused and marked');
281
+ await page.keyboard.press('Enter');
282
+ await page.waitForSelector('#game-start', { state: 'detached' });
283
+ assert.equal((await state(page)).game, null);
284
+ assert.deepEqual(saved, [{ view: 'classic' }]);
285
+ await ctx.close();
286
+ });
287
+
256
288
  test('on a phone the panel is the whole width, nothing scrolls sideways and every choice clears the floor (task 1005192)', { skip }, async () => {
257
289
  for (const [w, h] of [[390, 844], [320, 700]]) {
258
290
  const ctx = await hall({ viewport: { width: w, height: h } });
@@ -266,6 +298,7 @@ test('on a phone the panel is the whole width, nothing scrolls sideways and ever
266
298
  };
267
299
  });
268
300
  assert.deepEqual(m, { x: 0, w, sideways: 0, small: 0 }, `at ${w}px`);
301
+ await page.click('[data-gs="view-game"]');
269
302
  await page.click('[data-gs="start"]');
270
303
  await page.waitForSelector('#gs-load:not([hidden])');
271
304
  assert.equal(await page.evaluate(() => { const gs = document.getElementById('game-start'); return gs.scrollWidth - gs.clientWidth; }), 0, `the load screen fits ${w}px`);
@@ -279,6 +312,7 @@ test('Quit closes the tab, and says so when the browser keeps it open', { skip }
279
312
  const { page } = await signedIn(ctx);
280
313
  await page.waitForSelector('#game-start');
281
314
  await page.evaluate(() => { window.__closed = 0; window.close = () => { window.__closed += 1; }; });
315
+ await page.click('[data-gs="view-game"]');
282
316
  await page.click('[data-gs="quit"]');
283
317
  await page.waitForSelector('#gs-note:not([hidden])');
284
318
  assert.equal(await page.evaluate(() => window.__closed), 1);
@@ -291,6 +325,7 @@ test('with no city to draw, or with reduced motion, the still and the tip stand
291
325
  const ctx = await hall(opts);
292
326
  const { page, errors } = await signedIn(ctx);
293
327
  await page.waitForSelector('#game-start');
328
+ await page.click('[data-gs="view-game"]');
294
329
  await page.click('[data-gs="start"]');
295
330
  await page.waitForSelector('#gs-load:not([hidden])');
296
331
  assert.match(await page.textContent('#gs-tip'), /\.$/);
@@ -0,0 +1,42 @@
1
+ // tests/hall_view_prefs.mjs — the game-or-classic choice kept on the account
2
+ // (task 1005200, modules/builder-settings/hall-view-prefs.js), and the two routes
3
+ // that read and write it.
4
+ import { test } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { createRequire } from 'node:module';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const ROOT = fileURLToPath(new URL('..', import.meta.url));
13
+ const prefs = require('../modules/builder-settings/hall-view-prefs.js');
14
+
15
+ test('never chosen is null, not a default view: the sign-in chooser asks', () => {
16
+ assert.equal(prefs.storedView({}), null);
17
+ assert.equal(prefs.storedView(null), null);
18
+ assert.equal(prefs.storedView({ hall_view: 'fullscreen' }), null, 'an unknown value is not a choice');
19
+ assert.deepEqual(prefs.describeForApi({ hall_view: 'game' }), { view: 'game', views: ['game', 'classic'] });
20
+ });
21
+
22
+ test('a PATCH takes exactly { view: game | classic }', () => {
23
+ assert.deepEqual(prefs.validatePatch({ view: 'classic' }), { ok: true, view: 'classic' });
24
+ assert.equal(prefs.validatePatch({}).ok, false, 'no view');
25
+ assert.equal(prefs.validatePatch({ view: 'both' }).ok, false);
26
+ assert.equal(prefs.validatePatch({ view: 'game', extra: 1 }).ok, false, 'unknown keys refused');
27
+ assert.equal(prefs.validatePatch([]).ok, false);
28
+ });
29
+
30
+ test('saving the view keeps every other behaviour preference', () => {
31
+ const before = { wandering: 2, widgets: 'rich', interaction: { verbosity: 'concise' } };
32
+ const after = prefs.applyView(before, 'game');
33
+ assert.deepEqual(after, { ...before, hall_view: 'game' });
34
+ assert.equal(before.hall_view, undefined, 'pure: the input is not changed');
35
+ });
36
+
37
+ test('the routes: own builder only, GET and PATCH on /me/hall-view', () => {
38
+ const src = fs.readFileSync(path.join(ROOT, 'modules/builder-settings/routes/settings.js'), 'utf8');
39
+ assert.match(src, /router\.get\('\/me\/hall-view', api\.requireBuilder,/);
40
+ assert.match(src, /router\.patch\('\/me\/hall-view', api\.requireBuilder,/);
41
+ assert.match(src, /hallViewPrefs\.applyView\(existing, v\.view\)/, 'writes through the pure shaper');
42
+ });
@@ -151,7 +151,7 @@ test('in game mode the glass shows the place, the main action, the sprite and th
151
151
  }));
152
152
  assert.equal(g.shown, 'block');
153
153
  assert.equal(g.place, 'Test city');
154
- assert.match(g.main, /^Claim a task/);
154
+ assert.match(g.main, /^Goal sky/, 'Claim a task opens a classic page, so it is off the glass in game mode (task 1005200)');
155
155
  assert.equal(g.who, 'Artisan');
156
156
  assert.equal(g.credits, '56,021 credits');
157
157
  assert.equal(g.sprite, '/builders/characters/artisan.png');
@@ -189,7 +189,8 @@ test('the sheet lists the character\'s actions, then this place, and runs a scen
189
189
  const { ctx, page } = await open();
190
190
  await page.click('[data-visor-sheet]');
191
191
  const rows = await page.$$eval('.visor__sheet .visor__row', (r) => r.map((x) => [...x.children].map((c) => c.textContent).join(' ')));
192
- assert.deepEqual(rows, ['Claim a task C', 'Blockers B', 'Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Options Esc']);
192
+ // task 1005200: Claim a task and Blockers open classic pages, so they are off the glass.
193
+ assert.deepEqual(rows, ['Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Options Esc']);
193
194
  await page.click('[data-visor-scene="0"]');
194
195
  assert.deepEqual(await page.evaluate(() => ({ centred: window.centred, open: OTBVisor.isOpen() })), { centred: true, open: false });
195
196
  await ctx.close();
@@ -103,7 +103,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
103
103
  // task 1005070: the remembered nav (the rail paints before /me).
104
104
  // task 1005088: the sidebar mark is drawn by reference to the one brand mark.
105
105
  // task 1005144: My agents, Project settings, Permissions and Modules left for the options menu.
106
- const SHELL_SHA = 'a4d41b30b41e49f4';
106
+ const SHELL_SHA = '89f6f166ed22a8ee';
107
107
 
108
108
  test('shell.js has not changed without its cache-bust being bumped', () => {
109
109
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);