File size: 41,816 Bytes
ad474c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
"""The WebUI: a CRT terminal, the diagnostics, the visualisations, the assistant.



Served by ``http.server`` and nothing else — no framework, no bundler, no build

step.  The page is one string; the data comes from JSON endpoints that call the

same objects the CLI does, so the browser cannot show something the machine

does not actually do.



Endpoints:



    GET  /                 the page (terminal + panels)

    GET  /api/state        boot screen, terminal cells, kernel status

    POST /api/exec         {"line": "run"} -> console output

    POST /api/chat         {"text": "..."} -> assistant reply + executed command

    GET  /api/autonomy     idle/readahead state: graded accuracy, lookahead, cache

    POST /api/autonomy     {"enabled": true} -> toggle the autonomous pass

    GET  /api/os           the webOS frame, windows, memory map

    POST /api/os/key       {"key": "F5"} -> one named key to the window manager

    POST /api/os/text      {"text": "stat"} -> typing into the focused window

    POST /api/os/click     {"x": 12, "y": 4} -> focus the window at that cell

    POST /api/os/launch    {"app": "ide"} -> open an app

    POST /api/boot         re-run POST + boot sector

    GET  /api/diag         every layer's measured report

    GET  /api/viz/<name>   one visualisation as ASCII

    GET  /api/pipeline     the stages, with their real counters



It binds ``127.0.0.1`` by default and holds a single kernel in memory: this is a

local instrument panel, not a multi-tenant service.  There is no authentication

because there is no remote exposure.

"""

from __future__ import annotations

import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from .assistant import INTENTS, Assistant
from .autonomy import AutonomousMode
from .boot import BootSequence
from .hypervec import DEFAULT_DIM
from .os import Desktop
from .semantic import load as load_semantic
from .terminal import Kernel
from .x86 import Boot

PAGE = """<!DOCTYPE html>

<html lang="en"><head><meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>HDCSTORAGE</title>

<style>

:root{--ph:#7dffb0;--dim:#2f7a52;--amber:#ffbf5e;--bg:#04140c;--panel:#061c12}

*{box-sizing:border-box}

html,body{margin:0;background:var(--bg);color:var(--ph);

  font:14px/1.35 "Cascadia Mono","Consolas","DejaVu Sans Mono",monospace}

body{padding:clamp(6px,1.1vw,12px)}

#wrap{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,430px);

  gap:clamp(6px,0.9vw,12px);align-items:start;max-width:1900px;margin:0 auto}

.panel{background:var(--panel);border:1px solid var(--dim);border-radius:10px;padding:10px;

  box-shadow:0 0 24px rgba(0,255,140,.07) inset,0 0 12px rgba(0,255,140,.05);min-width:0}

h2{margin:0 0 8px;font-size:12px;letter-spacing:.18em;color:var(--dim);font-weight:600;

  text-transform:uppercase;display:flex;align-items:center;gap:8px;flex-wrap:wrap}

h2 span.title{flex:1;min-width:0}

#fontctl{display:flex;gap:4px;letter-spacing:0}

#fontctl button{padding:2px 7px;font-size:11px;line-height:1.2;border-radius:5px}

/* The CRT is a fixed 80-column character grid and must never reflow, so the font

   size is measured in fitCrt() from the real monospace column width *and* from

   the box height instead of guessed in CSS.  #screen has a definite height (not

   a min/max pair) so that measurement is stable, and the whole grid -- menu bar

   to taskbar -- is always on screen: nothing needs scrolling.  overflow:auto is

   only the fallback for A+ zoom, which is an explicit request to go bigger than

   the box. */

#screen{position:relative;border-radius:8px;overflow:auto;overscroll-behavior:contain;

  background:radial-gradient(ellipse at center,#08210f 0%,#020a06 100%);

  padding:clamp(6px,0.7vw,12px);height:min(76vh,720px);min-height:290px}

#screen:after{content:"";position:absolute;inset:0;pointer-events:none;border-radius:8px;

  background:repeating-linear-gradient(0deg,rgba(0,0,0,.34) 0 1px,rgba(0,0,0,0) 1px 3px),

             radial-gradient(ellipse at center,rgba(0,0,0,0) 55%,rgba(0,0,0,.6) 100%);}

#crt{white-space:pre;margin:0;color:var(--ph);position:relative;z-index:1;

  width:max-content;min-width:100%;font-size:var(--crt-size,13px);line-height:1.3;

  text-shadow:0 0 6px rgba(80,255,170,.5),0 0 18px rgba(40,255,140,.2)}

form#row{display:flex;gap:8px;margin:8px 0 0}

input,button{font:inherit;background:#03170e;color:var(--ph);border:1px solid var(--dim);

  border-radius:6px;padding:7px 10px;min-width:0}

input{flex:1}

button{cursor:pointer}

button:hover{background:#072a19;border-color:var(--ph)}

button:disabled{opacity:.5;cursor:progress}

#tabs{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap}

#tabs button.active{background:#0b3320;border-color:var(--ph);color:#c9ffdf}

#view{max-height:70vh;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12.5px}

.msg{border-left:2px solid var(--dim);padding:6px 8px;margin:6px 0;background:#05190f}

.msg b{color:#c9ffdf}

.msg.miss{border-color:var(--amber)}

.msg.err{border-color:#ff6b6b}

.small{color:var(--dim);font-size:11.5px}

b.k{color:#c9ffdf}

.ahead{display:flex;gap:6px;flex-wrap:wrap;margin:6px 0}

.ahead span{border:1px solid var(--dim);border-radius:999px;padding:2px 9px;font-size:11.5px}

.ahead span.on{border-color:var(--ph);color:#c9ffdf}

#status{font-size:11.5px;color:var(--dim);margin-top:6px}

/* One column as soon as the split would squeeze the 80-column grid: a 1024-wide

   screen at 125-150% browser zoom is really ~800px, and the side panel would

   leave the terminal too narrow to be legible. */

@media (max-width:1180px){#wrap{grid-template-columns:minmax(0,1fr)}

  #screen{height:min(58vh,560px)}#view{max-height:52vh}}

@media (max-height:640px){#screen{height:46vh}}

@media (max-height:480px){#screen{height:40vh}#view{max-height:40vh}}

/* desktop mode: the CRT is the screen, so the console prompt steps aside, the

   grid takes the window instead of a share of it, and the pointer becomes a

   cursor over the window grid.  calc() subtracts the panel header and status

   line so the whole grid fits the viewport without the terminal scrolling; the

   min-height is what 30 rows need at the 8px legibility floor, so a tiny window

   scrolls the page rather than cutting the grid. */

body.desktop form#row{display:none}

body.desktop #crt{cursor:crosshair}

body.desktop #screen{height:calc(100vh - 116px);min-height:344px}

@media (max-width:1180px){body.desktop #screen{height:calc(100vh - 112px)}}

@media (max-height:640px){body.desktop #screen{height:calc(100vh - 104px)}}

@media (max-height:480px){body.desktop #screen{height:calc(100vh - 96px)}}

</style></head><body>

<div id="wrap">

  <div class="panel">

    <h2><span class="title">HDCSTORAGE &mdash; CRTC mono, 80x25</span>

      <span id="fontctl">

        <button type="button" data-font="-1" title="smaller text">A-</button>

        <button type="button" data-font="0" title="fit the grid to the width">FIT</button>

        <button type="button" data-font="1" title="larger text">A+</button>

      </span></h2>

    <div id="screen"><pre id="crt"></pre></div>

    <form id="row" onsubmit="send(); return false;">

      <input id="cmd" autocomplete="off" spellcheck="false" enterkeyhint="send"

        placeholder="help | asm | run | step | regs | shader | ring | state ...">

      <button type="submit">EXEC</button>

    </form>

    <div id="status">connecting</div>

  </div>

  <div class="panel">

    <div id="tabs">

      <button type="button" data-tab="chat" class="active">ASSISTANT</button>

      <button type="button" data-tab="os">DESKTOP</button>

      <button type="button" data-tab="pipeline">PIPELINE</button>

      <button type="button" data-tab="viz">VISUALISE</button>

      <button type="button" data-tab="diag">DIAGNOSTICS</button>

    </div>

    <div id="view"></div>

  </div>

</div>

<script>

const scr=document.getElementById('screen'), crt=document.getElementById('crt'),

      view=document.getElementById('view'), status=document.getElementById('status');

let tab='chat', lastScreen=[], booted=false, busy=false;



function esc(s){return String(s==null?'':s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}

function paint(lines){

  crt.innerHTML=(lines||[]).map(esc).join('\\n');

  // A taller grid (25-row kernel screen, 30-row desktop) needs a smaller font to

  // stay whole in the same box, so re-fit when the row count changes.

  const rows=gridHeight((lines||[]).length);

  if(rows!==gridRows){ gridRows=rows; fitCrt(); }

  scr.scrollTop=scr.scrollHeight;   // the grid scrolls inside #screen, not #crt

}

function note(text){status.textContent=text;}



// The CRT is exactly 80 columns, so a font picked in px cannot suit every

// display: on a low-resolution panel or at a large font size the grid either

// overflows or turns unreadable.  Measure the real column width and derive the

// font size from the space that is actually available -- width *and* height.

// A- / A+ scale around that fit and the choice is remembered across reloads.

const probe=document.createElement('span');

probe.setAttribute('aria-hidden','true');

probe.style.cssText='position:absolute;visibility:hidden;left:-9999px;top:0;'+

  'white-space:pre;font:100px/1 "Cascadia Mono","Consolas","DejaVu Sans Mono",monospace';

document.body.appendChild(probe);

let zoom=1;

try{ zoom=parseFloat(localStorage.getItem('crtZoom'))||1; }catch(e){ zoom=1; }

if(!(zoom>0) || !isFinite(zoom)) zoom=1;



// The grid the machine draws: 80 columns always, and at least the 25 rows of the

// CRTC text screen so the boot typewriter does not shrink the font line by line.

const GRID_COLS=80, GRID_ROWS=25, LINE=1.3;

let gridRows=GRID_ROWS;

function gridHeight(rows){ return Math.max(GRID_ROWS, Math.min(200, rows||0)); }



function fitCrt(){

  probe.textContent='0'.repeat(GRID_COLS);

  const perCol=probe.getBoundingClientRect().width/GRID_COLS;   // px per column at 100px

  if(!perCol) return;

  // clientWidth/Height exclude the scrollbar and border but include the padding,

  // which the grid cannot use, so subtract it rather than guessing a constant.

  const cs=getComputedStyle(scr);

  const padX=parseFloat(cs.paddingLeft)+parseFloat(cs.paddingRight);

  const padY=parseFloat(cs.paddingTop)+parseFloat(cs.paddingBottom);

  const availW=Math.max(120, scr.clientWidth-padX);

  const availH=Math.max(80, scr.clientHeight-padY);

  const byWidth=availW/(perCol*GRID_COLS)*100*zoom;   // every column visible

  const byHeight=availH/(gridRows*LINE)*zoom;         // every row visible

  // Both constraints at once, which is what removes the scrollbar: fitting width

  // alone let a 30-row frame run past a 76vh box, and paint() then scrolled the

  // desktop down to its taskbar and hid the menu bar.  8px is the legibility

  // floor for a genuinely tiny window; A- still goes below the fit on request.

  const size=Math.max(8, Math.min(44, Math.min(byWidth, byHeight)));

  crt.style.fontSize=size.toFixed(2)+'px';

}

document.querySelectorAll('#fontctl button').forEach(btn=>{

  btn.onclick=()=>{

    const k=Number(btn.dataset.font);

    zoom = k===0 ? 1 : Math.max(0.55, Math.min(2.6, zoom*(k>0?1.15:1/1.15)));

    try{ localStorage.setItem('crtZoom', String(zoom)); }catch(e){}

    fitCrt(); note('terminal text '+Math.round(zoom*100)+'% of fit');

  };

});

let resizeTimer=0;

window.addEventListener('resize', ()=>{

  clearTimeout(resizeTimer);

  resizeTimer=setTimeout(fitCrt, 120);

});



async function api(path, body){

  const opt = body===undefined ? {} : {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)};

  const r = await fetch(path, opt);

  const text = await r.text();

  if(!r.ok) throw new Error(path+' -> HTTP '+r.status+' '+text.slice(0,180));

  try { return JSON.parse(text); } catch(e){ throw new Error(path+' -> non-JSON response'); }

}



async function refreshState(){

  try{

    const s = await api('/api/state');

    if(!booted && s.boot){ // type the POST log out, one line at a time

      booted=true; lastScreen=[];

      for(const line of s.boot){

        lastScreen.push(line); paint(lastScreen);

        await new Promise(r=>setTimeout(r, line.trim()?90:30));

      }

    }

    lastScreen = s.screen; paint(lastScreen); note(s.status);

  }catch(e){ note('offline: '+e.message); }

}



// EXEC: send exactly what was typed and show the kernel's own screen back.

// The kernel echoes the line and reprints the prompt, so the response is the

// authoritative 80x25 grid; an optimistic echo would only add a 26th row and

// then vanish.  An empty box and a failed request both say so out loud instead

// of doing nothing, and a failure hands the line back rather than eating it.

async function send(){

  const box=document.getElementById('cmd');

  const line=box.value.trim();

  if(!line){ note('nothing to run \u2014 type a command'); box.focus(); return; }

  if(busy){ note('busy: still running the previous line'); return; }

  box.value=''; box.disabled=true; busy=true;

  note('running: '+line);

  try{

    const r = await api('/api/exec', {line});

    lastScreen = r.screen; paint(lastScreen); note('ready');

  }catch(e){

    box.value=line;

    lastScreen = lastScreen.concat(['[error] '+e.message]); paint(lastScreen); note('error');

  }finally{ busy=false; box.disabled=false; box.focus(); }

  if(tab==='pipeline') showPipeline();

}



function setTab(name){

  tab=name;

  document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('active', b.dataset.tab===name));

  document.body.classList.toggle('desktop', name==='os');

  if(name==='chat') showChat();

  else if(name==='os'){ if(bootPlayed) showOs(); else playBoot(); }

  else if(name==='pipeline') showPipeline();

  else if(name==='viz') showViz();

  else if(name==='diag') showDiag();

}



// ---------------------------------------------------------------- the webOS

// The desktop is rendered by the machine and arrives as 80 character rows, so

// the browser's job is only to show them and post keys back.  Keys are captured

// globally while the tab is active and sent one at a time; clicks are converted

// from pixels to a character cell and sent as coordinates.

async function showOs(){

  try{

    const s = await api('/api/os');

    paint(s.frame);

    note('webOS | '+s.windows.length+' windows | '+s.frame_ms+' ms/frame | '+s.status);

  }catch(e){ note('os: '+e.message); }

}

// The boot chain the machine recorded at startup: POST, shader splash,

// AUTOEXEC.  It is replayed at its own delays the first time the desktop is

// opened, and any key skips it — the frames are a recording of work that

// already happened, so skipping loses nothing.

let bootPlayed = false, bootSkipped = false, bootPlaying = false;

async function playBoot(){

  if(bootPlayed) return false;

  bootPlayed = true; bootPlaying = true; bootSkipped = false;

  let frames;

  try{ frames = (await api('/api/boot')).frames || []; }

  catch(e){ bootPlaying=false; return false; }

  document.body.classList.add('desktop');

  for(const f of frames){

    if(bootSkipped) break;

    paint(f.lines);

    note('BOOT | '+f.phase+' | '+f.status+'  (any key skips)');

    await new Promise(r=>setTimeout(r, f.delay || 120));

  }

  bootPlaying = false;

  await showOs();

  return true;

}

async function osCall(path, body){

  try{

    const s = await api(path, body);

    if(s.frame){ paint(s.frame); }

    note('webOS | '+s.frame_ms+' ms/frame | '+s.status);

    return s;

  }catch(e){ note('os: '+e.message); return null; }

}

document.addEventListener('keydown', e=>{

  if(tab!=='os') return;

  if(bootPlaying){ e.preventDefault(); bootSkipped=true; return; }  // any key skips the boot

  if(e.target && e.target.tagName==='INPUT') return;   // the exec box keeps its keys

  const named={'Tab':1,'Enter':1,'Backspace':1,'Escape':1,'ArrowUp':1,'ArrowDown':1,

               'ArrowLeft':1,'ArrowRight':1,'F1':1,'F2':1,'F3':1,'F4':1,'F5':1,'F6':1};

  if(e.ctrlKey && (e.key==='w'||e.key==='W')){ e.preventDefault(); osCall('/api/os/key',{key:'Ctrl+W'}); return; }

  if(e.ctrlKey && e.key==='Tab'){ e.preventDefault(); osCall('/api/os/key',{key:'Ctrl+Tab'}); return; }

  if(e.ctrlKey && ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)){

    e.preventDefault(); osCall('/api/os/key',{key:'Ctrl+'+e.key.replace('Arrow','')}); return;

  }

  if(e.key===' '){ e.preventDefault(); osCall('/api/os/key',{key:' '}); return; }

  if(named[e.key]){ e.preventDefault(); osCall('/api/os/key',{key:e.key}); return; }

  if(e.key.length===1){ e.preventDefault(); osCall('/api/os/text',{text:e.key}); }

});

	// Click a cell: the desktop focuses whatever window is under it.  #crt's own

// rect already accounts for the panel padding and any scroll, so the pixel to

// cell conversion is measured against it rather than against the panel.

crt.addEventListener('click', e=>{

  if(tab!=='os') return;

  if(bootPlaying){ bootSkipped=true; return; }

  const rect=crt.getBoundingClientRect();

  const size=parseFloat(getComputedStyle(crt).fontSize)||13;

  probe.textContent='0'.repeat(100);

  const charW=probe.getBoundingClientRect().width/100;

  const cellH=size*1.3;

  const x=Math.floor((e.clientX-rect.left)/charW);

  const y=Math.floor((e.clientY-rect.top)/cellH);

  if(x>=0&&y>=0) osCall('/api/os/click',{x,y});

});

document.querySelectorAll('#tabs button').forEach(b=>{ b.onclick=()=>setTab(b.dataset.tab); });



// The idle/readahead strip: what the machine expects next, how often that has

// been right, and whether the autonomous pass may pre-run it.  The numbers are

// the same ones `python -m hdc autonomy` prints, so the panel cannot claim

// anything the layer does not.

function autonomyBlock(a){

  if(!a || !a.stats) return '';

  const s=a.stats;

  const chips=(s.readahead||[]).map(p=>

    '<span class="'+(p.cached?'on':'')+'">'+esc(p.intent)+' '+

    Number(p.confidence||0).toFixed(2)+'</span>').join('')

    || '<span class="small">no lookahead yet</span>';

  return '<div class="msg"><b class="k">IDLE / READAHEAD</b>'+

    '<div class="small">'+(s.enabled?'autonomous mode on':'readahead only')+

      ' &middot; graded '+s.graded+', correct '+s.hits+

      ' ('+Math.round(100*(s.accuracy||0))+'%, chance '+s.chance+')'+

      ' &middot; ticks '+s.ticks+' &middot; transitions '+s.transitions+

      ' &middot; pre-ran '+s.autonomous_actions+', served '+s.served_from_readahead+

      (s.ms_saved?(' ('+s.ms_saved+' ms saved)'):'')+'</div>'+

    '<div class="ahead">'+chips+'</div>'+

    '<div><button type="button" onclick="toggleAutonomy()">'+

      (s.enabled?'DISABLE':'ENABLE')+' AUTONOMY</button>'+

      ' <span class="small">read-only commands only</span></div></div>';

}

async function toggleAutonomy(){

  try{

    const a = await api('/api/autonomy');

    await api('/api/autonomy',{enabled: !(a.stats||{}).enabled});

    await showChat();

  }catch(e){ note('error: '+e.message); }

}



async function showChat(){

  let ahead=null;

  try{ ahead = await api('/api/autonomy'); }catch(e){ ahead=null; }

  try{

    const h = await api('/api/chat/history');

    view.innerHTML = autonomyBlock(ahead) +

      '<div class="small">TextRing encoder &rarr; prototype classifier &rarr; kernel command. '+

      'Confidence is a raw similarity, not a probability.</div>' +

      h.map(m=>`<div class="msg ${m.confidence<0.42?'miss':''}">

        <b>${esc(m.input)}</b>

        <div class="small">intent=${esc(m.intent||'(none)')} conf=${m.confidence} ${m.executed?('&rarr; '+esc(m.executed)):''}</div>

        <div>${esc(m.reply||'')}</div>

        ${m.output?`<pre class="small">${esc(String(m.output).slice(0,1400))}</pre>`:''}

      </div>`).join('') +

      '<form id="chatrow" onsubmit="ask(); return false;" style="display:flex;gap:8px;margin-top:8px">'+

      '<input id="chat" placeholder="say: run the program" autocomplete="off">'+

      '<button type="submit">SEND</button></form>';

    document.getElementById('chat').focus();

  }catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }

}

async function ask(){

  const box=document.getElementById('chat'); const text=box.value.trim();

  if(!text) return;

  box.value='';

  view.innerHTML='<div class="small">thinking &mdash; encoding and classifying...</div>';

  try{

    const r = await api('/api/chat',{text});

    lastScreen = r.screen; paint(lastScreen); note('ready');

  }catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; return; }

  await showChat();

}



async function showPipeline(){

  view.innerHTML='<div class="small">reading pipeline...</div>';

  try{

    const p = await api('/api/pipeline');

    view.innerHTML = p.stages.map(s=>`<div class="msg"><b class="k">${esc(s.name)}</b>

      <div class="small">${esc(s.detail)}</div>

      <div class="small">${esc(s.metric)}</div></div>`).join('');

  }catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }

}



async function showViz(){

  view.innerHTML='<div class="small">listing visualisations...</div>';

  try{

    const names = await api('/api/viz');

    view.innerHTML = names.list.map(n=>`<div class="msg"><b class="k">${esc(n)}</b>

      <div><button type="button" data-viz="${esc(n)}">render</button></div>

      <pre class="small" id="v-${esc(n)}"></pre></div>`).join('');

    view.querySelectorAll('button[data-viz]').forEach(b=>{

      b.onclick=()=>loadViz(b.dataset.viz, b);

    });

  }catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }

}

async function loadViz(name, button){

  const target=document.getElementById('v-'+name);

  if(target) target.textContent='rendering '+name+' ...';

  if(button){ button.disabled=true; }

  try{

    const v = await api('/api/viz/'+name);

    if(target) target.textContent = v.body;

  }catch(e){ if(target) target.textContent='[error] '+e.message; }

  finally{ if(button){ button.disabled=false; } }

}



async function showDiag(){

  view.innerHTML='<div class="small">listing diagnostics...</div>';

  try{

    const d = await api('/api/diag');

    view.innerHTML = d.sections.map(k=>`<div class="msg"><b class="k">${esc(k)}</b>

      <div><button type="button" data-diag="${esc(k)}">load</button></div>

      <pre class="small" id="d-${esc(k)}"></pre></div>`).join('');

    view.querySelectorAll('button[data-diag]').forEach(b=>{

      b.onclick=()=>loadDiag(b.dataset.diag, b);

    });

  }catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }

}

async function loadDiag(name, button){

  const target=document.getElementById('d-'+name);

  if(target) target.textContent='loading '+name+' ...';

  if(button){ button.disabled=true; }

  try{

    const d = await api('/api/diag/'+encodeURIComponent(name));

    if(target) target.textContent = d.body + (d.cached?'\\n\\n[cached]':'');

  }catch(e){ if(target) target.textContent='[error] '+e.message; }

  finally{ if(button){ button.disabled=false; } }

}



(async function(){

  fitCrt();

  await refreshState();

  fitCrt();

  // #os opens straight onto the desktop, so the webOS can be linked to

  if((location.hash||'').replace('#','')==='os'){ setTab('os'); }

  else { await showChat(); }

  setInterval(async ()=>{ try{ const s=await api('/api/state'); note(s.status); }catch(e){} }, 20000);

})();

</script></body></html>

"""


class App:
    """Kernel + assistant + boot state, shared by every request."""

    def __init__(self, d: int = DEFAULT_DIM):
        self.d = d
        # Encoding is capped at 2000 bits regardless of the machine's d: the
        # assistant projects ~330 words at startup, and at d=10000 that alone
        # took 21 s before the first response. 2000 keeps it under a second and
        # is the codec's documented floor.
        self.enc_d = min(d, 2000)
        self.lock = threading.Lock()
        self.encoder = load_semantic(self.enc_d)
        self.kernel = Kernel(d=d)
        self.assistant = Assistant(self.kernel, d=self.enc_d, encoder=self.encoder)
        # idle + learning + readahead over the same assistant; off until asked
        self.autonomy = AutonomousMode(self.assistant, d=self.enc_d, enabled=False)
        # the webOS shares this kernel's storage, so the desktop's filesystem and
        # memory map describe the machine the terminal is running on
        self.desktop = Desktop(d=d, kernel=self.kernel, autonomy=self.autonomy)
        # POST -> shader splash -> AUTOEXEC, run once; the AUTOEXEC lines are the
        # ones that launch the windows, so the desktop handed over is the result
        self.boot_sequence = BootSequence(self.desktop, d=d)
        self.boot_frames = self.boot_sequence.run()
        self.boot = Boot()
        self.boot_log: list[str] = []
        self._diag_cache: dict[str, str] = {}
        self._cold_boot()

    def _cold_boot(self) -> None:
        self.boot = Boot()
        self.boot.post()
        result = self.boot.boot()
        self.boot_log = [line for line in result["screen"] if line.strip()]
        self.kernel.boot(self.boot)
        self.boot_result = result

    # ---------------------------------------------------------------- views
    def state(self) -> dict:
        return {
            "boot": self.boot_log,
            "screen": self.kernel.term.screen(),
            "status": "kernel running | d=%d | cells=%d | pc=%d" % (
                self.d, len(self.kernel.storage), self.kernel.machine.pc),
            "booted": self.kernel.booted,
        }

    def exec(self, line: str) -> dict:
        with self.lock:
            output = self.kernel.execute(line)
        return {"output": output, "screen": self.kernel.term.screen()}

    def chat(self, text: str) -> dict:
        with self.lock:
            # through the autonomy layer: every turn is graded against the
            # prediction made for it, and a readahead hit is served from cache
            record = self.autonomy.chat(text)
        return dict(record, screen=self.kernel.term.screen())

    def chat_history(self) -> list[dict]:
        return self.assistant.history[-12:]

    def autonomy_state(self) -> dict:
        return {"stats": self.autonomy.stats(), "report": self.autonomy.report()}

    # ------------------------------------------------------------ the webOS
    def os_state(self) -> dict:
        with self.lock:
            return self.desktop.state()

    def os_key(self, key: str) -> dict:
        with self.lock:
            self.desktop.key(str(key))
            return self.desktop.state()

    def os_text(self, text: str) -> dict:
        with self.lock:
            self.desktop.text(str(text))
            return self.desktop.state()

    def os_click(self, x: int, y: int) -> dict:
        with self.lock:
            self.desktop.click(int(x), int(y))
            return self.desktop.state()

    def os_launch(self, app: str) -> dict:
        with self.lock:
            self.desktop.launch(str(app))
            return self.desktop.state()

    def boot_state(self) -> dict:
        return self.boot_frames

    def set_autonomy(self, enabled: bool) -> dict:
        with self.lock:
            self.autonomy.enabled = bool(enabled)
            if enabled:
                self.autonomy.idle(2)      # two ticks: learn, then predict
        return self.autonomy_state()

    def pipeline(self) -> dict:
        kernel = self.kernel
        stages = [
            ("1 BIOS / POST", "16-bit real mode, IVT installed, text buffer at B800",
             "stage=%s, %d instructions" % (self.boot_result["stage"],
                                            self.boot_result["instructions"])),
            ("2 Boot sector", "hand-assembled 512 bytes; int 10h teletype, int 21h handoff",
             "signature 55 AA verified, DS=07C0"),
            ("3 Kernel", "HDC interpreter took over at the handoff",
             "cells=%d pc=%d" % (len(kernel.storage), kernel.machine.pc)),
            ("4 Assembler", "text -> hypervector instruction image, verified by decoding back",
             "program=%s" % ("loaded" if kernel.program else "none")),
            ("5 Machine", "fetch from storage, decode by cleanup, execute",
             "steps=%d status=%s" % (kernel.machine.steps, kernel.machine.status)),
            ("6 Tools", "MCP tool records resolved by similarity",
             "%d registered" % len(kernel.tools.list_tools())),
            ("7 Shader", "coded-image operators: dim, edges, normals, depth",
             "fabric d=%d" % self.d),
            ("8 State", "predictive transitions, beam planner, surprise",
             "planner horizon 3"),
            ("9 Ring", "historic ledger, PAST->PRESENT->FUTURE, energy budget",
             "ledger capacity 16"),
            ("10 Text / semantic", "TextRing encoder, optional static embeddings",
             "encoder=%s" % ("semantic (potion-base-8M)" if self.encoder else "character n-gram")),
            ("11 Assistant", "prototype classification over encoded utterances",
             "%d intents, %d trained" % (len(INTENTS), self.assistant.trained)),
        ]
        return {"stages": [{"name": n, "detail": d_, "metric": m} for n, d_, m in stages]}

    DIAG = ("help", "isa", "stat", "net", "shader", "memory modes",
            "state", "ring", "text", "semantic", "topology", "hub", "autonomy")

    def diagnostics(self, name: str) -> dict:
        """One diagnostics section, cached.



        Computing all eleven at once took 9 s at d=10000 and the browser just

        sat there.  One section at a time keeps every click responsive, and

        the cache makes a repeat click instant.

        """
        if name in self._diag_cache:
            return {"name": name, "body": self._diag_cache[name], "cached": True}
        body = self._compute_diagnostic(name)
        self._diag_cache[name] = body
        return {"name": name, "body": body, "cached": False}

    def _compute_diagnostic(self, name: str) -> str:
        from .isa import opcode_table_text
        from .memory_modes import demo_report as memory_report
        from .net import selfcheck
        from .ring import report as ring_report
        from .semantic import report as semantic_report
        from .shader import demo_report as shader_report
        from .state import report as state_report
        from .terminal import HELP
        from .text import report as text_report
        from .topology import report as topology_report

        if name == "hub":
            # offline: the cache from `python -m hdc hub` is used, never the
            # network — a diagnostics click must not wait on the Hub
            from .hub import report as hub_report

            return hub_report(offline=True)
        if name == "autonomy":
            from .autonomy import report as autonomy_report

            return autonomy_report(d=self.enc_d)

        if name == "help":
            return HELP
        if name == "isa":
            return opcode_table_text()
        if name == "stat":
            return self.kernel.execute("stat")
        if name == "net":
            return "\n".join("%-22s %s" % (k, v) for k, v in selfcheck().items())
        # the remaining reports run at the codec floor: they are measurements
        # of the algorithms, not of this machine's dimension
        if name == "shader":
            return shader_report(d=self.enc_d)
        if name == "memory modes":
            return memory_report(d=self.enc_d)
        if name == "state":
            return state_report(d=self.enc_d)
        if name == "ring":
            return ring_report(d=self.enc_d)
        if name == "text":
            return text_report(d=self.enc_d)
        if name == "semantic":
            return semantic_report(d=self.enc_d)
        if name == "topology":
            return topology_report(d=self.enc_d)
        raise KeyError(name)

    # ------------------------------------------------------------ visuals
    VIZ = ("shader2d", "shader3d", "shader4d", "fractal", "crystal",
           "manifold", "semantic", "ring", "state")

    def visualisation(self, name: str) -> dict:
        from .shader import ShaderFabric, step_image
        from .shader3d import Volume, cube_volume
        from .shader4d import sweep_volume

        d = self.d
        fabric = ShaderFabric(d=d)
        if name == "shader2d":
            image = step_image(fabric=fabric)
            body = "\n".join([
                "source:", image.ascii(),
                "", "edges (exact |a-b|):", image.edges().ascii(),
                "", "dim(1) — exact halving:", image.dim(1).ascii(),
                "", "dilate(2) then bleed(2):", image.dilate(2).bleed(2).ascii(),
            ])
            return {"title": "2D shader operators", "body": body}
        if name == "shader3d":
            volume = cube_volume(5, 180, fabric)
            return {"title": "3D volume: shaded+projected, curvature",
                    "body": "shaded projection:\n" + volume.ascii_render()
                            + "\n\nslice z=2:\n" + volume.ascii_slice(2)
                            + "\n\nedges:\n" + volume.edges().ascii_slice(2)}
        if name == "shader4d":
            vol = sweep_volume(step=6, size=5, value=200, fabric=fabric)
            entropy = vol.entropy_along_u(8)
            return {"title": "4D entropic stack",
                    "body": "slice u=0:\n" + vol.ascii_u(0)
                            + "\n\nslice u=3:\n" + vol.ascii_u(3)
                            + "\n\nentropy along u (z=2):\n" + entropy.ascii_slice(2)
                            + "\n\ndrift %.4f" % vol.drift()}
        if name == "fractal":
            from .fractal import fbm, fern, sierpinski

            return {"title": "fractal: Sierpinski, fBm, fern",
                    "body": "sierpinski(5):\n" + sierpinski(5, fabric=fabric).ascii()
                            + "\n\nfBm 32x32:\n" + fbm(32, 32, 4, 8, 7, fabric).ascii()
                            + "\n\nfern(3000):\n" + fern(3000, 34, 36, fabric).ascii()}
        if name == "crystal":
            from .crystal import grain_boundaries, lattice_shade, report

            volume = cube_volume(5, 180, fabric)
            return {"title": "crystal: grains + lattice banding",
                    "body": report(volume) + "\n\nlattice banding z=2:\n"
                            + lattice_shade(volume, spacing=2).ascii_slice(2)}
        if name == "manifold":
            from .manifold import curvature, geodesic_field, report

            rows = [[[180 if (x in (0, 4) or y in (0, 4) or z in (0, 4)) else 0
                      for x in range(5)] for y in range(5)] for z in range(5)]
            volume = Volume.from_levels(rows, fabric)
            return {"title": "manifold: geodesic + curvature",
                    "body": report(volume, seed=(0, 0, 0))
                            + "\n\ngeodesic from (0,0,0) at z=0:\n"
                            + geodesic_field(volume, (0, 0, 0)).ascii_slice(0)}
        if name == "semantic":
            encoder = self.encoder
            if encoder is None:
                return {"title": "semantic", "body": "model not downloaded — see hdc/semantic.py"}
            words = ["car", "automobile", "doctor", "physician", "banana", "tectonics"]
            header = "%-11s" % "" + " ".join("%-10s" % w[:10] for w in words)
            rows = [header]
            for a in words:
                cells = ["%+.3f" % encoder.similarity(a, b) for b in words]
                rows.append("%-11s" % a[:11] + " ".join("%-10s" % c for c in cells))
            return {"title": "semantic similarity (model cosine)",
                    "body": "\n".join(rows)}
        if name == "ring":
            from .ring import report

            return {"title": "ring controller", "body": report(d=d)}
        if name == "state":
            from .state import report

            return {"title": "predictive state", "body": report(d=d)}
        raise KeyError(name)


def make_handler(app: App):
    class Handler(BaseHTTPRequestHandler):
        server_version = "HDCSTORAGE/1.0"

        def log_message(self, fmt, *args):   # quiet: this is a local panel
            pass

        def _send(self, payload: bytes, content_type: str, code: int = 200) -> None:
            self.send_response(code)
            self.send_header("Content-Type", content_type)
            self.send_header("Content-Length", str(len(payload)))
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(payload)

        def _json(self, data, code: int = 200) -> None:
            self._send(json.dumps(data).encode("utf-8"), "application/json", code)

        def _body(self) -> dict:
            length = int(self.headers.get("Content-Length") or 0)
            if not length:
                return {}
            try:
                return json.loads(self.rfile.read(length).decode("utf-8"))
            except json.JSONDecodeError:
                return {}

        def do_GET(self) -> None:                       # noqa: N802
            path = self.path.split("?")[0]
            if path in ("/", "/index.html"):
                self._send(PAGE.encode("utf-8"), "text/html; charset=utf-8")
            elif path == "/api/state":
                self._json(app.state())
            elif path == "/api/chat/history":
                self._json(app.chat_history())
            elif path == "/api/autonomy":
                self._json(app.autonomy_state())
            elif path == "/api/os":
                self._json(app.os_state())
            elif path == "/api/boot":
                self._json(app.boot_state())
            elif path == "/api/diag":
                self._json({"sections": list(App.DIAG)})
            elif path.startswith("/api/diag/"):
                try:
                    self._json(app.diagnostics(path.rsplit("/", 1)[1]))
                except KeyError:
                    self._json({"error": "unknown section"}, 404)
            elif path == "/api/pipeline":
                self._json(app.pipeline())
            elif path == "/api/viz":
                self._json({"list": list(App.VIZ)})
            elif path.startswith("/api/viz/"):
                try:
                    self._json(app.visualisation(path.rsplit("/", 1)[1]))
                except KeyError:
                    self._json({"error": "unknown visualisation"}, 404)
            else:
                self._json({"error": "not found"}, 404)

        def do_POST(self) -> None:                      # noqa: N802
            path = self.path.split("?")[0]
            body = self._body()
            if path == "/api/exec":
                self._json(app.exec(str(body.get("line", ""))))
            elif path == "/api/chat":
                self._json(app.chat(str(body.get("text", ""))))
            elif path == "/api/autonomy":
                self._json(app.set_autonomy(bool(body.get("enabled"))))
            elif path == "/api/os/key":
                self._json(app.os_key(str(body.get("key", ""))))
            elif path == "/api/os/text":
                self._json(app.os_text(str(body.get("text", ""))))
            elif path == "/api/os/click":
                self._json(app.os_click(int(body.get("x", 0)), int(body.get("y", 0))))
            elif path == "/api/os/launch":
                self._json(app.os_launch(str(body.get("app", "terminal"))))
            elif path == "/api/boot":
                with app.lock:
                    app._cold_boot()
                self._json(app.state())
            else:
                self._json({"error": "not found"}, 404)

    return Handler


def main(argv: list[str] | None = None) -> int:
    """``python -m hdc web [--port 8765] [--d 2000] [--open]``."""
    import sys
    import webbrowser

    argv = list(sys.argv[1:] if argv is None else argv)
    port = 8765
    host = "127.0.0.1"
    # 2000 is the codec floor and keeps every endpoint sub-second; the CLI's
    # 10 000-bit default made assembling a demo take seconds in the UI
    d = 2000
    open_browser = False
    i = 0
    while i < len(argv):
        if argv[i] == "--port" and i + 1 < len(argv):
            port = int(argv[i + 1]); i += 2
        elif argv[i] == "--d" and i + 1 < len(argv):
            d = int(argv[i + 1]); i += 2
        elif argv[i] == "--host" and i + 1 < len(argv):
            host = argv[i + 1]; i += 2
        elif argv[i] == "--open":
            open_browser = True; i += 1
        else:
            i += 1
    app = App(d=d)
    server = ThreadingHTTPServer((host, port), make_handler(app))
    url = "http://%s:%d/" % (host, port)
    print("HDCSTORAGE WebUI on %s  (d=%d, encoder=%s)"
          % (url, d, "semantic" if app.encoder else "n-gram"))
    print("  ctrl-c to stop")
    if open_browser:
        webbrowser.open(url)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nstopped")
    finally:
        server.server_close()
    return 0


if __name__ == "__main__":  # pragma: no cover - manual run
    raise SystemExit(main())