@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.0.1

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 (68) hide show
  1. package/README.md +176 -8
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +34 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/domain/grammar/ace.mjs +40 -4
  46. package/src/domain/grammar/lexicon-core.json +2 -1
  47. package/src/domain/grammar/lexicon.mjs +18 -0
  48. package/src/domain/reference-pack.mjs +31 -7
  49. package/src/domain/spider-fly-world.mjs +1 -1
  50. package/src/domain/sprite-templates.mjs +8 -6
  51. package/src/services/adventure-viz.mjs +130 -47
  52. package/src/services/adventure.mjs +440 -367
  53. package/src/services/chat-page-viz.mjs +37 -9
  54. package/src/services/chat.mjs +99 -18
  55. package/src/services/code-explorer-viz.mjs +52 -14
  56. package/src/services/extract-facts.mjs +4 -7
  57. package/src/services/ingest-viz.mjs +54 -26
  58. package/src/services/ledger-viz.mjs +105 -56
  59. package/src/services/memory-panel-viz.mjs +24 -0
  60. package/src/services/mud-viz.mjs +940 -21
  61. package/src/services/plan-viz.mjs +119 -61
  62. package/src/services/research-viz.mjs +151 -57
  63. package/src/services/spider-fly-turn.mjs +1 -1
  64. package/src/services/spider-fly-viz.mjs +67 -43
  65. package/src/services/sprite-catalog-viz.mjs +175 -51
  66. package/src/services/viz-theme.mjs +15 -0
  67. package/src/surfaces/web/memory-ask-browser.bundle.js +107 -107
  68. package/src/surfaces/web/mud-browser-entry.mjs +44 -2
@@ -288,7 +288,10 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
288
288
  const pageTitle = title || `tmct plan — ${actions.length} move${actions.length === 1 ? "" : "s"}`;
289
289
 
290
290
  return `<!doctype html>
291
- <html lang="en">
291
+ <!-- data-theme is pinned to dark on purpose: this page reads as a track/
292
+ timeline console, and that console stays dark under a light OS or site
293
+ theme too, the same way a DAW's own window does. -->
294
+ <html lang="en" data-theme="dark">
292
295
  <head>
293
296
  <meta charset="utf-8">
294
297
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -309,97 +312,144 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
309
312
  -->
310
313
  <style>
311
314
  ${THEME_TOKENS_CSS}
312
- html { background: var(--bg); }
313
- body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; }
314
- main { max-width: 880px; margin: 0 auto; padding: 1.4rem 1rem 3rem; }
315
- .head { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: .4rem; }
316
- h1 { font-size: 1.15rem; margin: 0 0 .8rem; }
317
- .chip { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; padding: .12rem .55rem; }
318
- .stage { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: 1rem; }
315
+ /* This console stays dark under either site theme (the html tag pins
316
+ data-theme="dark" above), so these extra surfaces are plain literals
317
+ rather than theme-reactive tokens a track console has one lighting
318
+ rig, not two. --taught/--corpus/--entail/--alert still come from
319
+ THEME_TOKENS_CSS above and still carry the site's own provenance
320
+ language: taught (green) marks the actual current position, entail
321
+ (amber) marks tmct's own inferred step goal, corpus (blue) marks a
322
+ session parameter, alert (red) marks a failed solve. */
323
+ :root {
324
+ --rail: #101317; --well: #0A0C0E; --strip: #1A1F25; --strip2: #20262D;
325
+ --taught-glow: rgba(95, 190, 139, .45); --entail-glow: rgba(217, 165, 84, .4);
326
+ }
327
+ html { background: var(--rail); }
328
+ body { margin: 0; background: var(--rail); color: var(--ink); font-family: ${MONO_STACK}; font-size: 14px; }
329
+ main { max-width: 1040px; margin: 0 auto; padding: 1.3rem 1rem 3rem; }
330
+ button { font: inherit; }
331
+
332
+ .rack { border: 1px solid var(--line); background: var(--card); }
333
+
334
+ /* ---- head strip: the nameplate ---- */
335
+ .headStrip { display: flex; justify-content: space-between; align-items: flex-end; gap: .6rem; flex-wrap: wrap; padding: .9rem 1.1rem .75rem; background: linear-gradient(180deg, var(--strip2), var(--strip)); border-bottom: 1px solid var(--line); border-radius: 9px 9px 0 0; }
336
+ .eyebrow { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); margin: 0 0 .35rem; }
337
+ h1 { font-family: ${SERIF_STACK}; font-size: 1.2rem; font-weight: 600; margin: 0; color: var(--ink); }
338
+ .chip { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .02em; color: var(--muted); border: 1px solid var(--line); border-radius: 3px; padding: .22rem .6rem; background: var(--well); text-align: right; }
339
+
340
+ /* ---- session strip: a channel-strip row of solve parameters ---- */
341
+ .sessionStrip { display: flex; align-items: stretch; flex-wrap: wrap; background: var(--strip); border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .74rem; }
342
+ .sessionCell { display: flex; flex-direction: column; justify-content: center; gap: .3rem; padding: .55rem .95rem; border-right: 1px solid var(--line); }
343
+ .paramLabel { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
344
+ .sessionStrip input[type="number"] { width: 3.6rem; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--well); color: var(--taught); border: 1px solid var(--line); border-radius: 4px; padding: .22rem .4rem; font-variant-numeric: tabular-nums; }
345
+ .sessionAction { justify-content: center; }
346
+ .sessionAction button { font-family: ${MONO_STACK}; font-size: .74rem; padding: .4rem .85rem; border-radius: 5px; border: 1px solid var(--line); background: var(--well); color: var(--ink); cursor: pointer; }
347
+ .sessionAction button:hover:not(:disabled) { border-color: var(--corpus); color: var(--corpus); }
348
+ .sessionAction button:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
349
+ .sessionAction button:disabled { opacity: .4; cursor: default; }
350
+ .sessionStatus { display: flex; align-items: center; margin-left: auto; padding: 0 .95rem; color: var(--muted); }
351
+ .sessionStatus.isError { color: var(--alert); }
352
+
353
+ /* ---- transport bar ---- */
354
+ .transport { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; padding: .7rem 1.1rem; background: var(--rail); border-bottom: 1px solid var(--line); }
355
+ .transportButtons { display: flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
356
+ .transportButtons button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .48rem .8rem; border: none; border-right: 1px solid var(--line); background: var(--strip); color: var(--ink); cursor: pointer; }
357
+ .transportButtons button:last-child { border-right: none; }
358
+ .transportButtons button:hover:not(:disabled) { background: var(--strip2); }
359
+ .transportButtons button:focus-visible { outline: 2px solid var(--taught); outline-offset: -2px; }
360
+ .transportButtons button[disabled] { opacity: .35; cursor: default; }
361
+ .transportButtons #play.isPlaying { background: var(--taught-soft); color: var(--taught); }
362
+ .lcd { margin-left: auto; background: var(--well); border: 1px solid var(--line); border-radius: 6px; padding: .45rem .75rem; min-width: 10rem; }
363
+ #stepLabel { display: block; font-family: ${MONO_STACK}; font-size: 1rem; letter-spacing: .03em; color: var(--taught); text-shadow: 0 0 8px var(--taught-glow); font-variant-numeric: tabular-nums; white-space: nowrap; }
364
+ .lcdMeter { margin-top: .3rem; height: 3px; background: var(--line); border-radius: 2px; overflow: hidden; }
365
+ .lcdMeterFill { height: 100%; width: 0%; background: var(--taught); box-shadow: 0 0 6px var(--taught-glow); transition: width .18s ease; }
366
+
367
+ /* ---- the deck: board on the left, arrangement view on the right ---- */
368
+ .stage { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 0; }
319
369
  /* grid items refuse to shrink below their content's min-width by default, so
320
370
  the 640px board would stretch the track and the whole page with it on a
321
371
  phone — let the items shrink and the boardwrap scroll instead. */
322
372
  .stage > * { min-width: 0; }
323
- @media (max-width: 660px) { .stage { grid-template-columns: 1fr; } }
373
+ .deck { padding: 1rem 1.1rem 1.1rem; border-right: 1px solid var(--line); }
374
+ @media (max-width: 660px) { .stage { grid-template-columns: 1fr; } .deck { border-right: none; border-bottom: 1px solid var(--line); } }
324
375
  .boardwrap { overflow-x: auto; }
325
- .board { position: relative; width: ${BOARD_W}px; height: ${BOARD_H}px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; }
376
+ .board { position: relative; width: ${BOARD_W}px; height: ${BOARD_H}px; background: var(--well); border: 1px solid var(--line); border-radius: 6px; }
326
377
  .base { position: absolute; left: 4%; right: 4%; bottom: 22px; height: 6px; background: var(--line); border-radius: 3px; }
327
378
  .post { position: absolute; bottom: 28px; width: 6px; height: 150px; background: var(--line); border-radius: 3px 3px 0 0; }
328
- .anchorlabel { position: absolute; bottom: 2px; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); }
329
- .block { position: absolute; height: ${BLOCK_H}px; border-radius: 6px; color: #fff; display: flex; align-items: center; justify-content: center; font-family: ${MONO_STACK}; font-size: .66rem; box-shadow: 0 1px 2px rgba(0,0,0,.25); }
330
- .block.moving { z-index: 3; box-shadow: 0 4px 10px rgba(0,0,0,.3); }
379
+ .anchorlabel { position: absolute; bottom: 2px; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
380
+ .block { position: absolute; height: ${BLOCK_H}px; border-radius: 4px; color: #fff; display: flex; align-items: center; justify-content: center; font-family: ${MONO_STACK}; font-size: .64rem; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 2px 4px rgba(0,0,0,.4); }
381
+ .block.moving { z-index: 3; box-shadow: 0 0 0 2px var(--taught), 0 6px 14px rgba(0,0,0,.5); }
331
382
  .circle { position: absolute; width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--muted); transform: translate(-50%, -100%); }
332
383
  .circlelabel { position: absolute; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .6rem; color: var(--muted); }
333
- .controls { display: flex; gap: .45rem; align-items: center; margin-top: .7rem; flex-wrap: wrap; }
334
- .controls button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border-radius: 6px; border: 1px solid var(--line); background: var(--card); color: var(--ink); cursor: pointer; }
335
- .controls button:hover { border-color: var(--taught); }
336
- .controls button:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
337
- .controls button[disabled] { opacity: .4; cursor: default; }
338
- .controls .step { margin-left: auto; font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
339
- .goalline { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .76rem; color: var(--taught); background: var(--taught-soft); border-radius: 6px; padding: .45rem .65rem; }
340
- .facts { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); border-top: 1px dashed var(--line); padding-top: .55rem; line-height: 1.7; }
384
+ .goalline { margin-top: .8rem; font-family: ${MONO_STACK}; font-size: .76rem; color: var(--entail); background: var(--entail-soft); border: 1px solid var(--entail); border-radius: 6px; padding: .5rem .75rem; }
385
+ .facts { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border-top: 1px dashed var(--line); padding-top: .55rem; line-height: 1.7; }
341
386
  .facts b { color: var(--ink); }
342
- .movelist { list-style: none; margin: 0; padding: 0; font-family: ${MONO_STACK}; font-size: .74rem; align-self: start; }
343
- .movelist li { padding: .28rem .55rem; border-left: 3px solid transparent; color: var(--muted); cursor: pointer; border-radius: 0 5px 5px 0; font-variant-numeric: tabular-nums; }
344
- .movelist li:hover { color: var(--ink); }
345
- .movelist li.done { color: var(--ink); }
346
- .movelist li.current { border-left-color: var(--taught); color: var(--ink); background: var(--taught-soft); font-weight: 700; }
347
- .movelist .phasehead { font-size: .64rem; letter-spacing: .06em; text-transform: uppercase; border-left: 3px solid var(--taught); margin-top: .5rem; }
387
+ .movelist { list-style: none; margin: 0; padding: .5rem; font-family: ${MONO_STACK}; font-size: .72rem; align-self: start; }
388
+ .movelist .phasehead { display: block; background: var(--strip); border: 1px solid var(--line); border-left: 3px solid var(--corpus); border-radius: 4px; padding: .35rem .55rem; margin: .55rem 0 .3rem; font-size: .62rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); cursor: pointer; }
348
389
  .movelist .phasehead:first-child { margin-top: 0; }
349
- .movelist .phasehead:hover { color: var(--taught); background: var(--taught-soft); }
390
+ .movelist .phasehead:hover { color: var(--ink); border-left-color: var(--taught); }
350
391
  .movelist .phasehead:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
351
- @media (prefers-reduced-motion: reduce) { .block { transition: none !important; } }
352
- .liveControls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; padding: .55rem .7rem; border: 1px solid var(--line); border-radius: 8px; background: var(--card); font-family: ${MONO_STACK}; font-size: .74rem; }
353
- .liveControls label { display: flex; align-items: center; gap: .35rem; color: var(--muted); }
354
- .liveControls input[type="number"] { width: 3.6rem; font-family: ${MONO_STACK}; font-size: .74rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 5px; padding: .2rem .4rem; }
355
- .liveControls button { font-family: ${MONO_STACK}; font-size: .74rem; padding: .3rem .7rem; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--ink); cursor: pointer; }
356
- .liveControls button:hover:not(:disabled) { border-color: var(--taught); }
357
- .liveControls button:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
358
- .liveControls button:disabled { opacity: .4; cursor: default; }
359
- .liveControls .livestatus { color: var(--muted); margin-left: auto; }
360
- .lower { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem; margin-top: 1.2rem; }
392
+ .movelist li:not(.phasehead) { padding: .38rem .55rem; margin: .12rem 0; border: 1px solid transparent; border-radius: 4px; color: var(--muted); cursor: pointer; font-variant-numeric: tabular-nums; opacity: .55; }
393
+ .movelist li:not(.phasehead):hover { color: var(--ink); border-color: var(--line); opacity: 1; }
394
+ .movelist li.done { color: var(--ink); background: var(--strip); opacity: 1; }
395
+ .movelist li.current { color: var(--ink); background: var(--taught-soft); border-color: var(--taught); box-shadow: inset 3px 0 0 var(--taught); font-weight: 700; opacity: 1; }
396
+ @media (prefers-reduced-motion: reduce) { .block, .lcdMeterFill { transition: none !important; } }
397
+
398
+ /* ---- lower rack: two channel strips, teach dock and PDDL artifact ---- */
399
+ .lower { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0; border-top: 1px solid var(--line); }
361
400
  @media (max-width: 780px) { .lower { grid-template-columns: 1fr; } }
362
- .chat, .pddlpanel { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .6rem .75rem; min-width: 0; }
363
- .chat h2, .pddlpanel h2 { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .5rem; }
401
+ .chat, .pddlpanel { background: var(--card); padding: .65rem .8rem; min-width: 0; }
402
+ .chat { border-right: 1px solid var(--line); }
403
+ @media (max-width: 780px) { .chat { border-right: none; border-bottom: 1px solid var(--line); } }
404
+ .chat h2, .pddlpanel h2 { font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .5rem; }
364
405
  .chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 180px; overflow-y: auto; margin-bottom: .5rem; }
365
406
  .chatlog:empty { display: none; margin-bottom: 0; }
366
407
  .chatlog .u { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
367
408
  .chatlog .u::before { content: "tmct> "; color: var(--taught); }
368
- .chatlog .a { font-size: .85rem; line-height: 1.4; white-space: pre-wrap; }
409
+ .chatlog .a { font-family: ${SERIF_STACK}; font-size: .87rem; line-height: 1.45; white-space: pre-wrap; color: var(--ink); }
369
410
  .chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--line); padding-top: .5rem; }
370
411
  .chatlog:empty + .chatask { border-top: none; padding-top: 0; }
371
412
  .chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
372
- .chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
413
+ .chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--well); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: .34rem .55rem; min-width: 0; }
373
414
  .chatask input:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
374
415
  .chatpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; }
375
- .pill { font-family: ${MONO_STACK}; font-size: .68rem; padding: .2rem .6rem; border: 1px solid var(--line); border-radius: 99px; background: var(--bg); color: var(--ink); cursor: pointer; white-space: nowrap; }
376
- .pill:hover { border-color: var(--taught); }
416
+ .pill { font-family: ${MONO_STACK}; font-size: .66rem; padding: .22rem .6rem; border: 1px solid var(--line); border-radius: 99px; background: var(--well); color: var(--ink); cursor: pointer; white-space: nowrap; }
417
+ .pill:hover { border-color: var(--taught); color: var(--taught); }
377
418
  .pill:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
378
- .pddlpanel pre { margin: 0; font-family: ${MONO_STACK}; font-size: .68rem; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 420px; overflow-y: auto; color: var(--ink); }
419
+ .pddlpanel pre { margin: 0; font-family: ${MONO_STACK}; font-size: .67rem; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 420px; overflow-y: auto; color: var(--ink); background: var(--well); border: 1px solid var(--line); border-radius: 4px; padding: .6rem .7rem; }
379
420
  </style>
380
421
  </head>
381
422
  <body>
382
423
  <main>
383
- <div class="head">
384
- <h1 id="pageTitle">${escapeHtml(pageTitle)}</h1>
424
+ <div class="rack">
425
+ <div class="headStrip">
426
+ <div>
427
+ <div class="eyebrow">tmct &middot; plan</div>
428
+ <h1 id="pageTitle">${escapeHtml(pageTitle)}</h1>
429
+ </div>
385
430
  <span class="chip">blocks archetype · ${pageData.layouts.length} snapshots · plan: findActionPath</span>
386
431
  </div>
387
- <div class="liveControls" id="liveControls">
388
- <label for="diskCount">disks <input id="diskCount" type="number" min="1" max="7" value="3"></label>
389
- <label for="maxDepth">max search depth <input id="maxDepth" type="number" min="1" max="999" value="300"></label>
390
- <button id="resolveBtn" type="button">solve a fresh puzzle</button>
391
- <span class="livestatus" id="liveStatus"></span>
432
+ <div class="sessionStrip" id="liveControls">
433
+ <label class="sessionCell" for="diskCount"><span class="paramLabel">disks</span><input id="diskCount" type="number" min="1" max="7" value="3"></label>
434
+ <label class="sessionCell" for="maxDepth"><span class="paramLabel">max depth</span><input id="maxDepth" type="number" min="1" max="999" value="300"></label>
435
+ <div class="sessionCell sessionAction"><button id="resolveBtn" type="button">solve a fresh puzzle</button></div>
436
+ <span class="sessionStatus" id="liveStatus"></span>
437
+ </div>
438
+ <div class="transport">
439
+ <div class="transportButtons">
440
+ <button id="reset" aria-label="Reset to start">⏮ reset</button>
441
+ <button id="back" aria-label="Step back">◀ back</button>
442
+ <button id="play" aria-label="Play">▶ play</button>
443
+ <button id="next" aria-label="Step forward">step ▶</button>
444
+ </div>
445
+ <div class="lcd">
446
+ <span id="stepLabel"></span>
447
+ <div class="lcdMeter"><div class="lcdMeterFill" id="stepMeter"></div></div>
448
+ </div>
392
449
  </div>
393
450
  <div class="stage">
394
- <div>
451
+ <div class="deck">
395
452
  <div class="boardwrap"><div class="board" id="board" aria-label="plan board"></div></div>
396
- <div class="controls">
397
- <button id="reset" aria-label="Reset to start">⏮ reset</button>
398
- <button id="back" aria-label="Step back">◀ back</button>
399
- <button id="play" aria-label="Play">▶ play</button>
400
- <button id="next" aria-label="Step forward">step ▶</button>
401
- <span class="step" id="stepLabel"></span>
402
- </div>
403
453
  <div class="goalline" id="goalline" hidden></div>
404
454
  <div class="facts" id="facts"></div>
405
455
  </div>
@@ -423,6 +473,7 @@ h1 { font-size: 1.15rem; margin: 0 0 .8rem; }
423
473
  <pre id="pddlOut">${escapeHtml(pddlText)}</pre>
424
474
  </div>
425
475
  </div>
476
+ </div>
426
477
  </main>
427
478
  <script>
428
479
  const PLAN = ${embedded};
@@ -431,6 +482,7 @@ const PLAN = ${embedded};
431
482
  <script>
432
483
  (function () {
433
484
  "use strict";
485
+ const escapeHtml = ${escapeHtml.toString()};
434
486
  // Best-effort: a copy of this page opened without the sibling worker file
435
487
  // (a tmct --render plan --output file, a file:// open) just swallows the
436
488
  // registration failure and works exactly as before.
@@ -438,6 +490,7 @@ const PLAN = ${embedded};
438
490
  const pageTitleEl = document.getElementById("pageTitle");
439
491
  const board = document.getElementById("board");
440
492
  const stepLabel = document.getElementById("stepLabel");
493
+ const stepMeterEl = document.getElementById("stepMeter");
441
494
  const goalline = document.getElementById("goalline");
442
495
  const factsEl = document.getElementById("facts");
443
496
  const movelist = document.getElementById("movelist");
@@ -511,6 +564,7 @@ const PLAN = ${embedded};
511
564
  function render() {
512
565
  const phase = phaseFor(step);
513
566
  stepLabel.textContent = "step " + step + " / " + N + (phase ? " · " + phase : "");
567
+ if (stepMeterEl) stepMeterEl.style.width = (N ? Math.min(step, N) / N * 100 : 0) + "%";
514
568
  if (plan.stepGoals) {
515
569
  goalline.hidden = false;
516
570
  goalline.textContent = step < N
@@ -520,7 +574,7 @@ const PLAN = ${embedded};
520
574
  goalline.hidden = true;
521
575
  }
522
576
  factsEl.innerHTML = "<b>board@step" + step + "</b> — " +
523
- plan.facts[step].map((f) => f.replace(/&/g, "&amp;").replace(/</g, "&lt;")).join(" · ") +
577
+ plan.facts[step].map((f) => escapeHtml(f)).join(" · ") +
524
578
  ' <span style="opacity:.7">(plan: findActionPath)</span>';
525
579
  [...movelist.querySelectorAll("li:not(.phasehead)")].forEach((li, i) => {
526
580
  li.classList.toggle("done", i < step);
@@ -529,6 +583,7 @@ const PLAN = ${embedded};
529
583
  btn.back.disabled = step === 0 || animating;
530
584
  btn.next.disabled = step === N || animating;
531
585
  btn.play.textContent = playing ? "⏸ pause" : (step === N ? "▶ replay" : "▶ play");
586
+ btn.play.classList.toggle("isPlaying", playing);
532
587
  }
533
588
  async function forward() {
534
589
  if (animating || step >= N) return;
@@ -625,6 +680,7 @@ const PLAN = ${embedded};
625
680
  const liveAvailable = typeof tmctPlan !== "undefined" && typeof tmctPlan.createPlanSession === "function";
626
681
  if (!liveAvailable) {
627
682
  liveStatusEl.textContent = "live re-solve unavailable here — showing the baked-in replay only.";
683
+ liveStatusEl.classList.add("isError");
628
684
  resolveBtn.disabled = true;
629
685
  } else {
630
686
  let session = null;
@@ -698,6 +754,7 @@ const PLAN = ${embedded};
698
754
  const d = Math.max(1, parseInt(maxDepthEl.value, 10) || 300);
699
755
  diskCountEl.value = n; maxDepthEl.value = d;
700
756
  liveStatusEl.textContent = "solving a " + n + "-disk puzzle…";
757
+ liveStatusEl.classList.remove("isError");
701
758
  await tryLoadWink();
702
759
  session = await tmctPlan.createPlanSession({ diskCount: n, maxDepth: d });
703
760
  if (session.plan) {
@@ -705,6 +762,7 @@ const PLAN = ${embedded};
705
762
  liveStatusEl.textContent = "live — " + n + " disk" + (n === 1 ? "" : "s") + ", max depth " + d + ".";
706
763
  } else {
707
764
  liveStatusEl.textContent = "no plan found within " + d + " moves — raise max search depth and try again.";
765
+ liveStatusEl.classList.add("isError");
708
766
  }
709
767
  resolveBtn.disabled = false;
710
768
  }));