koneck 2.92.0 → 2.94.0

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.
@@ -4446,7 +4446,15 @@ $('tfontdown').onclick = () => { term.fontPx = Math.max(9, term.fontPx - 1); app
4446
4446
  * is dispatched as a real mouse event at the same point, so what happens is what would happen to a
4447
4447
  * person clicking there.
4448
4448
  */
4449
- const preview = { shot: null, width: 0 };
4449
+ /*
4450
+ * Nothing here: this held { shot, width } and was read by nothing at all.
4451
+ *
4452
+ * Left over from a version where the panel kept the picture itself. The name is now the panel's own
4453
+ * elements, a few lines down, and two declarations of it stopped the whole client script from
4454
+ * parsing — which takes every handler on the page with it, so the view menu, the composer and the
4455
+ * panel all stop responding at once. Worth knowing that is the symptom: not one broken control but
4456
+ * all of them.
4457
+ */
4450
4458
 
4451
4459
  /*
4452
4460
  * Typing into the page.
@@ -4487,6 +4495,19 @@ function flushTyping() {
4487
4495
  async function pageKey(ev) {
4488
4496
  // Left alone: the browser's own shortcuts, and anything with a modifier that is not Shift.
4489
4497
  if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
4498
+ /*
4499
+ * Scrolling first, and handled here rather than sent to the page as a keypress.
4500
+ *
4501
+ * Pressing PageDown in the page would work too, but only when something scrollable has focus
4502
+ * there — click a text field first and PageDown moves the caret instead, which is the page
4503
+ * behaving correctly and the panel appearing not to. Scrolling the window is unambiguous.
4504
+ */
4505
+ const scroll = SCROLL_KEYS[ev.key];
4506
+ if (scroll) {
4507
+ ev.preventDefault();
4508
+ scroll();
4509
+ return;
4510
+ }
4490
4511
  const special = SPECIAL_KEYS[ev.key];
4491
4512
  if (special) {
4492
4513
  ev.preventDefault();
@@ -4516,92 +4537,215 @@ function pageCoords(img, ev) {
4516
4537
  };
4517
4538
  }
4518
4539
 
4519
- function drawPreview(data) {
4540
+ /**
4541
+ * The preview's own elements, built once and kept.
4542
+ *
4543
+ * It used to empty the panel and build a fresh <img> on every update — so every scroll, click and
4544
+ * refresh blanked the picture, decoded a new one, and put it back. That is the blinking: not a slow
4545
+ * page but a panel demolishing itself thirty times a minute. Reported as "no smooth scrolling, like
4546
+ * opening sections", which is exactly what a sequence of rebuilt stills looks like.
4547
+ *
4548
+ * Held here rather than found by id each time, because the handlers are wired once and must read
4549
+ * whatever is current rather than closing over one draw's data.
4550
+ */
4551
+ const preview = { stage: null, img: null, cursor: null, hint: null, chips: null,
4552
+ controls: [], swapping: false, next: null };
4553
+
4554
+ /**
4555
+ * The pointer, where something just happened.
4556
+ *
4557
+ * Asked for as "see the mouse pointer like a human when operating" — and it is not decoration: a
4558
+ * click whose target you cannot see is a click you cannot check. It moves to the point, presses,
4559
+ * and fades, so watching the panel tells you where the agent went and whether it hit the thing it
4560
+ * meant to.
4561
+ */
4562
+ function showPointer(pageX, pageY, kind) {
4563
+ const img = preview.img;
4564
+ const cursor = preview.cursor;
4565
+ if (!img || !cursor || !img.naturalWidth) return;
4566
+ const box = img.getBoundingClientRect();
4567
+ const stage = preview.stage.getBoundingClientRect();
4568
+ const scale = box.width / img.naturalWidth;
4569
+ cursor.style.left = Math.round(box.left - stage.left + pageX * scale) + 'px';
4570
+ cursor.style.top = Math.round(box.top - stage.top + pageY * scale) + 'px';
4571
+ cursor.hidden = false;
4572
+ // Restarted rather than added to: a second click during the first animation should look like a
4573
+ // second click, not like nothing.
4574
+ cursor.classList.remove('press', 'move');
4575
+ void cursor.offsetWidth;
4576
+ cursor.classList.add(kind === 'press' ? 'press' : 'move');
4577
+ }
4578
+
4579
+ /** Where a control is on the page, from the last read, so the agent's clicks can be pointed at. */
4580
+ function boxOfControl(want) {
4581
+ const found = (preview.controls || []).find((c) =>
4582
+ (typeof want.ref === 'number' && c.ref === want.ref)
4583
+ || (want.selector && c.selector === want.selector)
4584
+ || (want.label && String(c.label || '').toLowerCase() === String(want.label).toLowerCase()));
4585
+ if (!found || !found.box) return null;
4586
+ return { x: found.box.x + found.box.w / 2, y: found.box.y + found.box.h / 2 };
4587
+ }
4588
+
4589
+ /**
4590
+ * Swaps the picture without a blank frame in between.
4591
+ *
4592
+ * decode() waits for the new image to be ready before it is shown, so the old one stays up until
4593
+ * the new one can replace it in a single paint. Without it the src change clears the element and
4594
+ * the panel flashes white between every frame, which is the whole of the flicker.
4595
+ *
4596
+ * Coalesced, because pictures can arrive faster than they decode: while one is being prepared the
4597
+ * newest arrival is held, and anything older than it is dropped — showing a stale frame after a
4598
+ * fresh one is worse than skipping it.
4599
+ */
4600
+ async function swapShot(src) {
4601
+ if (preview.swapping) { preview.next = src; return; }
4602
+ preview.swapping = true;
4603
+ try {
4604
+ for (let src2 = src; src2; src2 = preview.next, preview.next = null) {
4605
+ const loading = new Image();
4606
+ loading.src = src2;
4607
+ try { if (loading.decode) await loading.decode(); } catch (e) { /* show it regardless */ }
4608
+ if (preview.img) preview.img.src = src2;
4609
+ }
4610
+ } finally {
4611
+ preview.swapping = false;
4612
+ }
4613
+ }
4614
+
4615
+ /** The panel's furniture, wired once. */
4616
+ function previewShell() {
4617
+ if (preview.stage && preview.stage.parentNode) return;
4520
4618
  const host = $('pvbody');
4521
4619
  host.innerHTML = '';
4522
- if (!data || data.running === false) {
4523
- host.appendChild(el('div', 'pvnone', (data && data.note)
4524
- || 'Nothing open yet.'));
4525
- $('pvinfo').textContent = '';
4526
- return;
4527
- }
4528
- $('pvurl').value = data.url || $('pvurl').value;
4529
- $('pvinfo').textContent = data.title || '';
4530
- // Shown as unusable rather than silently doing nothing, which is the difference between a
4531
- // control that is off and one that is broken.
4532
- $('pvback').disabled = !(data.nav && data.nav.back);
4533
- $('pvfwd').disabled = !(data.nav && data.nav.forward);
4534
- if (!data.shot) {
4535
- host.appendChild(el('div', 'pvnone', 'The browser is open but produced no picture.'));
4536
- return;
4537
- }
4620
+
4621
+ const stage = el('div', 'pvstage');
4538
4622
  const img = document.createElement('img');
4539
4623
  img.className = 'pvshot';
4540
- img.src = data.shot;
4541
- img.alt = data.title || 'the page';
4624
+ img.alt = 'the page';
4625
+ /*
4626
+ * Focusable, so the keyboard has somewhere to go.
4627
+ *
4628
+ * An image cannot receive key events without this, which was the mechanical reason typing did
4629
+ * nothing: there was no element for the keys to arrive at.
4630
+ */
4631
+ img.tabIndex = 0;
4632
+ const cursor = el('div', 'pvcursor');
4633
+ cursor.hidden = true;
4634
+ stage.appendChild(img);
4635
+ stage.appendChild(cursor);
4636
+ host.appendChild(stage);
4637
+
4638
+ const hint = el('div', 'pvhint');
4639
+ host.appendChild(hint);
4640
+ const chips = el('div', 'pvctl');
4641
+ host.appendChild(chips);
4642
+
4643
+ /*
4644
+ * Buttons for the same thing the keys do.
4645
+ *
4646
+ * They moved a flat 600 pixels, which is most of a page on a laptop and a third of one on a tall
4647
+ * window — so "scroll down" twice could skip a band of the page or barely move. A page is the
4648
+ * picture's height, whatever that is.
4649
+ */
4650
+ const scroller = el('div', 'pvhint');
4651
+ for (const [label, go] of [['↑ page', () => scrollByPages(-1)], ['↓ page', () => scrollByPages(1)],
4652
+ ['top', () => SCROLL_KEYS.Home()], ['end', () => SCROLL_KEYS.End()]]) {
4653
+ const b = el('button', 'pvbtn', label);
4654
+ b.style.margin = '0 4px';
4655
+ b.onclick = () => { if (preview.img) preview.img.focus(); go(); };
4656
+ scroller.appendChild(b);
4657
+ }
4658
+ scroller.appendChild(el('span', 'dim', ' \u2014 or the wheel, PageUp/PageDown, Home/End'));
4659
+ host.appendChild(scroller);
4660
+
4542
4661
  /*
4543
4662
  * The cursor answers the question a browser answers: is there something here to click?
4544
4663
  *
4545
- * It was a crosshair over the whole page, which is the language of a screenshot tool. The
4546
- * controls carry their boxes, so hovering one shows a hand and its label, and everywhere else
4547
- * shows an arrow — the same two states a real page has, from information the panel already had
4548
- * and was discarding.
4664
+ * A crosshair over the whole page is the language of a screenshot tool. The controls carry their
4665
+ * boxes, so hovering one shows a hand and its label and everywhere else shows an arrow — the same
4666
+ * two states a real page has, from information the panel already had.
4549
4667
  */
4550
- const controls = (data.controls || []).filter((c) => c.box);
4551
4668
  img.onmousemove = (ev) => {
4552
4669
  const at = pageCoords(img, ev);
4553
- const over = controls.find((c) =>
4670
+ const over = (preview.controls || []).filter((c) => c.box).find((c) =>
4554
4671
  at.x >= c.box.x && at.x <= c.box.x + c.box.w &&
4555
4672
  at.y >= c.box.y && at.y <= c.box.y + c.box.h);
4556
4673
  img.style.cursor = over ? 'pointer' : 'default';
4557
4674
  img.title = over ? (over.label || over.tag) : '';
4558
4675
  };
4559
4676
  img.onmouseleave = () => { img.style.cursor = 'default'; img.title = ''; };
4560
- /*
4561
- * Focusable, so the keyboard has somewhere to go.
4562
- *
4563
- * An image cannot receive key events without this, which is the mechanical reason typing did
4564
- * nothing: there was no element for the keys to arrive at. Clicking the page focuses it, so
4565
- * clicking a field and then typing works the way it reads.
4566
- */
4567
- img.tabIndex = 0;
4568
4677
  img.onkeydown = (ev) => { void pageKey(ev); };
4569
4678
  img.onblur = () => { void flushTyping(); };
4570
4679
 
4571
- // The picture is 1280 wide whatever it is displayed at, so a click has to be scaled back into
4572
- // page coordinates or it lands somewhere else entirely.
4680
+ // The picture is 1280 wide whatever it is displayed at, so a click is scaled back into page
4681
+ // coordinates or it lands somewhere else entirely.
4573
4682
  img.onclick = async (ev) => {
4574
4683
  img.focus();
4575
4684
  const at = pageCoords(img, ev);
4685
+ showPointer(at.x, at.y, 'press');
4576
4686
  $('pvinfo').textContent = 'clicking ' + at.x + ',' + at.y + '…';
4577
4687
  await browserDo({ action: 'click', x: at.x, y: at.y });
4578
4688
  };
4579
- host.appendChild(img);
4580
- const hint = el('div', 'pvhint',
4581
- 'Click the page to click it for real, then type. ' + (data.controls || []).length
4582
- + ' interactive elements.');
4583
- host.appendChild(hint);
4584
- // The same numbered elements the agent is given, clickable — so a person can drive the flow the
4585
- // agent is stuck on without hunting for the pixel.
4586
- if ((data.controls || []).length) {
4587
- const row = el('div', 'pvctl');
4588
- for (const c of data.controls.slice(0, 18)) {
4689
+
4690
+ preview.stage = stage;
4691
+ preview.img = img;
4692
+ preview.cursor = cursor;
4693
+ preview.hint = hint;
4694
+ preview.chips = chips;
4695
+ }
4696
+
4697
+ function drawPreview(data) {
4698
+ const host = $('pvbody');
4699
+ if (!data || data.running === false) {
4700
+ host.innerHTML = '';
4701
+ preview.stage = null;
4702
+ host.appendChild(el('div', 'pvnone', (data && data.note) || 'Nothing open yet.'));
4703
+ $('pvinfo').textContent = '';
4704
+ return;
4705
+ }
4706
+ $('pvurl').value = data.url || $('pvurl').value;
4707
+ $('pvinfo').textContent = data.title || '';
4708
+ // Shown as unusable rather than silently doing nothing, which is the difference between a
4709
+ // control that is off and one that is broken.
4710
+ $('pvback').disabled = !(data.nav && data.nav.back);
4711
+ $('pvfwd').disabled = !(data.nav && data.nav.forward);
4712
+ if (!data.shot) {
4713
+ host.innerHTML = '';
4714
+ preview.stage = null;
4715
+ host.appendChild(el('div', 'pvnone', 'The browser is open but produced no picture.'));
4716
+ return;
4717
+ }
4718
+
4719
+ previewShell();
4720
+ preview.controls = data.controls || [];
4721
+ void swapShot(data.shot);
4722
+ preview.hint.textContent = 'Click the page to click it for real, then type. '
4723
+ + preview.controls.length + ' interactive elements.';
4724
+
4725
+ /*
4726
+ * The chips, rebuilt only when they have changed.
4727
+ *
4728
+ * They are the one part that genuinely differs between pages, and rebuilding them on every scroll
4729
+ * — when the controls are identical — was throwing away the element under somebody's pointer
4730
+ * mid-click.
4731
+ */
4732
+ const want = preview.controls.slice(0, 18)
4733
+ .map((c) => c.ref + '. ' + (c.label || c.tag)).join('|');
4734
+ if (preview.chips.dataset.shape !== want) {
4735
+ preview.chips.dataset.shape = want;
4736
+ preview.chips.innerHTML = '';
4737
+ for (const c of preview.controls.slice(0, 18)) {
4589
4738
  const b = el('button', 'pvchip', c.ref + '. ' + (c.label || c.tag));
4590
4739
  b.type = 'button';
4591
4740
  b.title = c.selector;
4592
- b.onclick = () => void browserDo({ action: 'click', ref: c.ref });
4593
- row.appendChild(b);
4741
+ b.onclick = () => {
4742
+ const at = boxOfControl({ ref: c.ref });
4743
+ if (at) showPointer(at.x, at.y, 'press');
4744
+ void browserDo({ action: 'click', ref: c.ref });
4745
+ };
4746
+ preview.chips.appendChild(b);
4594
4747
  }
4595
- host.appendChild(row);
4596
- }
4597
- const row = el('div', 'pvhint');
4598
- for (const [label, by] of [['scroll up', -600], ['scroll down', 600]]) {
4599
- const b = el('button', 'pvbtn', label);
4600
- b.style.margin = '0 4px';
4601
- b.onclick = () => void browserDo({ action: 'scroll', by: by });
4602
- row.appendChild(b);
4603
4748
  }
4604
- host.appendChild(row);
4605
4749
  }
4606
4750
 
4607
4751
  /*
@@ -4622,16 +4766,69 @@ function drawPreview(data) {
4622
4766
  $('pvbody').addEventListener('wheel', (ev) => {
4623
4767
  if (!document.querySelector('#pvbody .pvshot')) return; // nothing open to scroll
4624
4768
  ev.preventDefault();
4625
- scrolling.by += ev.deltaY;
4626
- if (scrolling.timer) return;
4627
- scrolling.timer = setTimeout(() => {
4628
- const by = Math.round(scrolling.by);
4629
- scrolling.by = 0;
4630
- scrolling.timer = null;
4631
- if (by !== 0) void browserDo({ action: 'scroll', by: by });
4632
- }, SCROLL_BATCH_MS);
4769
+ queueScroll(ev.deltaY);
4633
4770
  }, { passive: false });
4634
4771
 
4772
+ /*
4773
+ * A click anywhere in the panel gives the page the keyboard.
4774
+ *
4775
+ * Only the picture is focusable, so Page Down after clicking the space beside it did nothing —
4776
+ * focus was on the document body and the panel's keys never fired. A browser viewport does not work
4777
+ * that way: clicking near the page is clicking the page. Buttons and fields are left alone, or
4778
+ * pressing one would take the focus straight back off it again.
4779
+ */
4780
+ $('pvbody').addEventListener('mousedown', (ev) => {
4781
+ if (!preview.img) return;
4782
+ if (ev.target.closest('button, input, select, textarea, a')) return;
4783
+ preview.img.focus();
4784
+ });
4785
+
4786
+ /** Gathers a burst, sends it as one movement. */
4787
+ function queueScroll(by) {
4788
+ scrolling.by += by;
4789
+ if (scrolling.timer) return;
4790
+ scrolling.timer = setTimeout(flushScroll, SCROLL_BATCH_MS);
4791
+ }
4792
+
4793
+ function flushScroll() {
4794
+ const by = Math.round(scrolling.by);
4795
+ scrolling.by = 0;
4796
+ if (scrolling.timer) clearTimeout(scrolling.timer);
4797
+ scrolling.timer = null;
4798
+ if (by !== 0) void browserDo({ action: 'scroll', by: by });
4799
+ }
4800
+
4801
+ /*
4802
+ * Page Down, and the rest of the keys that move a page rather than a few lines.
4803
+ *
4804
+ * A wheel is for reading something already on screen. Getting to the bottom of a long form with one
4805
+ * is thirty gestures, each costing a screenshot, and the panel is not the place to do that. Page Up
4806
+ * and Page Down move a viewport at a time; Home and End go to the ends, expressed as a scroll far
4807
+ * larger than any page because scrollBy clamps and asking for a million pixels lands at the edge.
4808
+ *
4809
+ * A viewport is the picture's own height: the shot is the viewport and nothing beyond it, so its
4810
+ * natural height is exactly how far a page goes. Less a small overlap, so the line you stopped
4811
+ * reading on is still there after the jump — the reason every document reader does this.
4812
+ *
4813
+ * Sent immediately rather than batched. A held Page Down is a handful of events, not fifty, and
4814
+ * waiting on a deliberate keypress reads as the key not working.
4815
+ */
4816
+ const PAGE_OVERLAP = 60;
4817
+
4818
+ function scrollByPages(pages) {
4819
+ const img = preview.img;
4820
+ const viewport = img && img.naturalHeight ? img.naturalHeight : 720;
4821
+ scrolling.by += pages * Math.max(120, viewport - PAGE_OVERLAP);
4822
+ flushScroll();
4823
+ }
4824
+
4825
+ const SCROLL_KEYS = {
4826
+ PageDown: () => scrollByPages(1),
4827
+ PageUp: () => scrollByPages(-1),
4828
+ Home: () => { scrolling.by -= 1e7; flushScroll(); },
4829
+ End: () => { scrolling.by += 1e7; flushScroll(); },
4830
+ };
4831
+
4635
4832
  async function browserDo(body) {
4636
4833
  $('pvinfo').textContent = 'working…';
4637
4834
  let out;
@@ -5037,6 +5234,28 @@ function watchTool(e, done) {
5037
5234
  if (e.name === 'browser') {
5038
5235
  $('watch').hidden = false;
5039
5236
  if (t.url) $('pvurl').value = t.url;
5237
+ /*
5238
+ * The pointer goes where the agent is acting, before the picture catches up.
5239
+ *
5240
+ * Watching a screenshot change is watching the aftermath; watching the pointer arrive and press
5241
+ * is watching the work. The coordinates come either straight from the call or from the box of
5242
+ * the control it named in the last read, which is how a click by ref or label can be pointed at
5243
+ * rather than merely reported.
5244
+ */
5245
+ if (!done) {
5246
+ let asked = null;
5247
+ try { asked = e.args ? JSON.parse(e.args) : null; } catch (err) { asked = null; }
5248
+ if (asked && /^(click|type|fill)$/.test(String(asked.action ?? ''))) {
5249
+ const at = typeof asked.x === 'number' && typeof asked.y === 'number'
5250
+ ? { x: asked.x, y: asked.y }
5251
+ : boxOfControl({ ref: asked.ref, label: asked.label, selector: asked.selector })
5252
+ // A fill names its fields rather than one target: point at the first it can place.
5253
+ ?? (Array.isArray(asked.fields)
5254
+ ? asked.fields.map((f) => boxOfControl(f)).find(Boolean) ?? null
5255
+ : null);
5256
+ if (at) showPointer(at.x, at.y, asked.action === 'click' ? 'press' : 'move');
5257
+ }
5258
+ }
5040
5259
  watchHead(verb, t.url || 'the page');
5041
5260
  trailPush({ label: t.url ? shortUrl(t.url) : 'page', verb, kind: 'page',
5042
5261
  url: t.url || '', ok: done ? e.ok !== false : undefined });
@@ -5581,7 +5800,9 @@ addEventListener('keydown', (ev) => {
5581
5800
  * already in use — and since the box also filtered, 476 models were immediately narrowed to the
5582
5801
  * one you were on. It looked exactly like a text field with no choice in it, because it was.
5583
5802
  */
5584
- const catalog = { provider: null, entries: [], listed: false, note: '', chosen: '' };
5803
+ const catalog = { provider: null, entries: [], listed: false, note: '', chosen: '',
5804
+ /* Only the newest ask may land. See loadModels. */ seq: 0,
5805
+ /* What each provider last said, so re-selecting one is instant. */ seen: {} };
5585
5806
 
5586
5807
  /** Fills the provider dropdown from whatever the registry holds, declared entries included. */
5587
5808
  /** The providers as last listed, so the key row knows what the chosen one needs. */
@@ -5738,7 +5959,7 @@ $('keydrop').onclick = async () => {
5738
5959
  $('keysaid').style.color = 'var(--muted)';
5739
5960
  $('keysaid').textContent = 'Forgotten. ' + name + ' will ask for a key again.';
5740
5961
  await fillProviders(name);
5741
- catalog.provider = null;
5962
+ forgetCatalogs();
5742
5963
  };
5743
5964
 
5744
5965
  $('keygo').onclick = async () => {
@@ -5769,7 +5990,7 @@ $('keygo').onclick = async () => {
5769
5990
  // The listing now reports it present, so the row goes away and the models can be asked again.
5770
5991
  keyReplacing = '';
5771
5992
  await fillProviders($('mprov').value);
5772
- catalog.provider = null;
5993
+ forgetCatalogs();
5773
5994
  void loadModels($('mprov').value, state.model === '-' ? '' : state.model);
5774
5995
  };
5775
5996
 
@@ -5800,8 +6021,7 @@ function drawModels() {
5800
6021
  catalog.chosen = '';
5801
6022
  return;
5802
6023
  }
5803
- $('mnote').textContent = (shown.length === total ? total + ' models' : shown.length + ' of ' + total)
5804
- + (catalog.chosen ? ' · using ' + catalog.chosen : ' · click one to choose');
6024
+ modelNote(shown.length, total);
5805
6025
 
5806
6026
  // Grouped by vendor, which is what turns a flat several-hundred-entry list into something you
5807
6027
  // can actually read.
@@ -5813,31 +6033,142 @@ function drawModels() {
5813
6033
  }
5814
6034
  const on = m.id === catalog.chosen;
5815
6035
  const row = el('div', 'mrow' + (on ? ' on' : ''));
6036
+ row.dataset.id = m.id;
5816
6037
  row.appendChild(el('div', 'mid', m.id));
5817
6038
  row.appendChild(el('div', 'mdet', m.detail));
5818
- row.onclick = () => { catalog.chosen = m.id; drawModels(); };
6039
+ row.onclick = () => chooseModel(m.id);
5819
6040
  host.appendChild(row);
5820
6041
  if (on && !first) first = row;
5821
6042
  }
5822
6043
  if (shown.length > 400) {
5823
6044
  host.appendChild(el('div', 'mgroup', 'and ' + (shown.length - 400) + ' more — keep typing'));
5824
6045
  }
5825
- // The model in use should be in view when the dialog opens, not hundreds of rows down.
6046
+ // The model in use should be in view when the dialog opens, not hundreds of rows down. Only on a
6047
+ // redraw: doing it when somebody picks a row scrolls the list out from under the click.
5826
6048
  if (first) first.scrollIntoView({ block: 'center' });
5827
6049
  }
5828
6050
 
5829
- /** Asks the selected provider what it serves. */
5830
- async function loadModels(provider, preselect) {
5831
- if (catalog.provider === provider) { drawModels(); return; }
6051
+ /*
6052
+ * How many are listed, which one Switch will use, and how to ask again.
6053
+ *
6054
+ * The list is held for ten minutes, which is right for a provider whose catalogue changes weekly
6055
+ * and wrong for the machine under the desk: pull a model into Ollama and it would not appear until
6056
+ * the cache expired, with nothing on screen to suggest otherwise. So the count line carries the way
6057
+ * to re-ask — and it goes past both the dialog's memory and the server's.
6058
+ */
6059
+ function modelNote(showing, total) {
6060
+ const note = $('mnote');
6061
+ note.textContent = (showing === total ? total + ' models' : showing + ' of ' + total)
6062
+ + (catalog.chosen ? ' · using ' + catalog.chosen : ' · click one to choose') + ' · ';
6063
+ const again = el('button', 'linkish', 'ask again');
6064
+ again.type = 'button';
6065
+ again.title = 'Ask ' + catalog.provider + ' what it serves now, ignoring what it said before';
6066
+ again.onclick = () => {
6067
+ const who = $('mprov').value;
6068
+ note.textContent = 'Asking ' + who + ' what it serves…';
6069
+ void loadModels(who, catalog.chosen, { fresh: true });
6070
+ };
6071
+ note.appendChild(again);
6072
+ }
6073
+
6074
+ /*
6075
+ * Picking one, without redrawing the list.
6076
+ *
6077
+ * This used to set the choice and call drawModels, which empties the list and builds it again:
6078
+ * measured at 400 rows, 50ms of rebuilding to change which row is highlighted — and then
6079
+ * scrollIntoView on the newly built row moved the list by 8,304 pixels, so the list jumped out from
6080
+ * under the click that had just been made. Nothing about a selection needs any of that.
6081
+ *
6082
+ * One class off, one class on, and the count line updated.
6083
+ */
6084
+ function chooseModel(id) {
6085
+ catalog.chosen = id;
6086
+ const host = $('mlist');
6087
+ const was = host.querySelector('.mrow.on');
6088
+ if (was) was.classList.remove('on');
6089
+ const now = host.querySelector('.mrow[data-id="' + cssEscape(id) + '"]');
6090
+ if (now) now.classList.add('on');
6091
+ const showing = host.querySelectorAll('.mrow').length;
6092
+ modelNote(showing, catalog.entries.length);
6093
+ }
6094
+
6095
+ /*
6096
+ * A model id inside an attribute selector.
6097
+ *
6098
+ * Ids carry slashes, dots and colons — "anthropic/claude-sonnet-4.5" — and a dot in a selector is a
6099
+ * class. CSS.escape where it exists, which is everywhere current, and a conservative fallback
6100
+ * rather than an unescaped selector that would throw.
6101
+ */
6102
+ function cssEscape(value) {
6103
+ if (window.CSS && CSS.escape) return CSS.escape(value);
6104
+ return String(value).replace(/["\\]/g, '\\$&');
6105
+ }
6106
+
6107
+ /*
6108
+ * Throws away what the providers said, so the next ask is live.
6109
+ *
6110
+ * Every "re-ask" used to be catalog.provider = null on its own. That still reads as re-asking, and
6111
+ * once answers were kept per provider it silently stopped being true: the kept answer would be
6112
+ * served instead, so entering a key was followed by the same empty list the provider gave before it
6113
+ * had one — indistinguishable from the key not working.
6114
+ */
6115
+ function forgetCatalogs() {
6116
+ catalog.provider = null;
6117
+ catalog.seen = {};
6118
+ }
6119
+
6120
+ /*
6121
+ * Asks the selected provider what it serves.
6122
+ *
6123
+ * ## Only the newest ask may land
6124
+ *
6125
+ * Two of these can be in flight at once — pick a provider, then pick another before the first has
6126
+ * answered — and there was nothing to stop the slower one arriving last and overwriting the faster.
6127
+ * Measured with a provider taking 2.6 seconds and one taking 120ms: selecting the slow one and then
6128
+ * the quick one left the menu reading "fastco" above a list of the *other* provider's models. Click
6129
+ * one of those rows and you have chosen a model the selected provider does not serve.
6130
+ *
6131
+ * Worse, it wedged: catalog.provider was then the provider you were no longer on, so selecting it
6132
+ * again matched the "already loaded" check and did nothing at all. Closing the dialog and opening
6133
+ * it again cleared that, which is precisely what people were doing to get out of it.
6134
+ *
6135
+ * A sequence number, checked after every await. A stale answer is dropped rather than drawn.
6136
+ *
6137
+ * ## Asked once per provider
6138
+ *
6139
+ * Answers are kept per provider for as long as the dialog session lasts, so going back to one you
6140
+ * have already looked at is immediate rather than another round trip. The server holds them for ten
6141
+ * minutes as well, so even a fresh page load usually answers from memory.
6142
+ */
6143
+ async function loadModels(provider, preselect, opts) {
6144
+ const seq = ++catalog.seq;
6145
+ const fresh = !!(opts && opts.fresh);
6146
+ const settle = (out) => {
6147
+ catalog.provider = provider;
6148
+ catalog.entries = out.entries || [];
6149
+ catalog.listed = !!out.listed;
6150
+ catalog.note = out.note || '';
6151
+ // Preselect only if the provider actually serves it: carrying a name across providers is how a
6152
+ // session ends up asking Anthropic for qwen3-coder.
6153
+ catalog.chosen = preselect && catalog.entries.some((m) => m.id === preselect) ? preselect : '';
6154
+ if (!catalog.listed && preselect) $('mmodel').value = preselect;
6155
+ drawModels();
6156
+ };
6157
+
6158
+ const known = fresh ? null : catalog.seen[provider];
6159
+ if (known) { settle(known); return; }
6160
+
5832
6161
  $('mlist').innerHTML = '';
5833
6162
  $('mnote').textContent = 'Asking ' + provider + ' what it serves…';
5834
6163
  let out;
5835
6164
  // Named, so a key typed into this session can be used to ask what the provider serves.
5836
6165
  try {
5837
6166
  out = await api('/api/models?provider=' + encodeURIComponent(provider)
5838
- + (state.session ? '&session=' + encodeURIComponent(state.session) : ''));
6167
+ + (state.session ? '&session=' + encodeURIComponent(state.session) : '')
6168
+ + (fresh ? '&fresh=1' : ''));
5839
6169
  }
5840
6170
  catch (e) {
6171
+ if (seq !== catalog.seq) return; // a newer selection has been made
5841
6172
  catalog.provider = provider; catalog.entries = []; catalog.listed = false;
5842
6173
  catalog.note = e.message;
5843
6174
  catalog.chosen = '';
@@ -5846,15 +6177,11 @@ async function loadModels(provider, preselect) {
5846
6177
  drawModels();
5847
6178
  return;
5848
6179
  }
5849
- catalog.provider = provider;
5850
- catalog.entries = out.entries || [];
5851
- catalog.listed = !!out.listed;
5852
- catalog.note = out.note || '';
5853
- // Preselect only if the provider actually serves it: carrying a name across providers is how a
5854
- // session ends up asking Anthropic for qwen3-coder.
5855
- catalog.chosen = preselect && catalog.entries.some((m) => m.id === preselect) ? preselect : '';
5856
- if (!catalog.listed && preselect) $('mmodel').value = preselect;
5857
- drawModels();
6180
+ // Remembered even if it is no longer wanted: it is a true answer about that provider, and the
6181
+ // person may well select it next.
6182
+ catalog.seen[provider] = out;
6183
+ if (seq !== catalog.seq) return;
6184
+ settle(out);
5858
6185
  }
5859
6186
 
5860
6187
  $('pmodel').onclick = async () => {
@@ -5872,7 +6199,7 @@ $('pmodel').onclick = async () => {
5872
6199
  * anything.
5873
6200
  */
5874
6201
  $('mmodel').value = '';
5875
- catalog.provider = null; // re-ask: a key may have been set since last time
6202
+ forgetCatalogs(); // re-ask: a key may have been set since last time
5876
6203
  $('mdlg').showModal();
5877
6204
 
5878
6205
  const info = await fillProviders(state.provider);
@@ -5987,7 +6314,7 @@ async function afterProviderSaved(out) {
5987
6314
  $('provkey').placeholder = keyEnvPlaceholder('');
5988
6315
  await fillProviders(out.name);
5989
6316
  $('mmodel').value = out.defaultModel === 'default' ? '' : out.defaultModel;
5990
- catalog.provider = null;
6317
+ forgetCatalogs();
5991
6318
  void loadModels(out.name);
5992
6319
  }
5993
6320
 
@@ -1 +1 @@
1
- {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAg9LlB,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuxMlB,CAAC;AACF,CAAC"}