react-os-shell 4.3.1 → 4.5.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.
Files changed (44) hide show
  1. package/dist/{Browser-674YMWMK.js → Browser-ISK7W7TB.js} +5 -5
  2. package/dist/{Browser-674YMWMK.js.map → Browser-ISK7W7TB.js.map} +1 -1
  3. package/dist/{Calculator-3ULKK5ZA.js → Calculator-GBC3FLAJ.js} +4 -4
  4. package/dist/{Calculator-3ULKK5ZA.js.map → Calculator-GBC3FLAJ.js.map} +1 -1
  5. package/dist/ConfirmDialog-R7NZVI2V.js +4 -0
  6. package/dist/{ConfirmDialog-XJP67AHR.js.map → ConfirmDialog-R7NZVI2V.js.map} +1 -1
  7. package/dist/{CurrencyConverter-JC2N7AY3.js → CurrencyConverter-URPZQBNV.js} +4 -4
  8. package/dist/{CurrencyConverter-JC2N7AY3.js.map → CurrencyConverter-URPZQBNV.js.map} +1 -1
  9. package/dist/{Documents-MJKEUOOL.js → Documents-KUAQ3EKC.js} +5 -5
  10. package/dist/{Documents-MJKEUOOL.js.map → Documents-KUAQ3EKC.js.map} +1 -1
  11. package/dist/{Files-BTZMPTL6.js → Files-OTPHB4BP.js} +5 -5
  12. package/dist/{Files-BTZMPTL6.js.map → Files-OTPHB4BP.js.map} +1 -1
  13. package/dist/{Notepad-NGLFEJLA.js → Notepad-3NVFGEO5.js} +5 -5
  14. package/dist/{Notepad-NGLFEJLA.js.map → Notepad-3NVFGEO5.js.map} +1 -1
  15. package/dist/{PomodoroTimer-RPCRTUKC.js → PomodoroTimer-QQOMBVP3.js} +5 -5
  16. package/dist/{PomodoroTimer-RPCRTUKC.js.map → PomodoroTimer-QQOMBVP3.js.map} +1 -1
  17. package/dist/{Preview-H4S427VB.js → Preview-7WQHKQ6O.js} +5 -5
  18. package/dist/{Preview-H4S427VB.js.map → Preview-7WQHKQ6O.js.map} +1 -1
  19. package/dist/{Spreadsheet-2YYUKKSY.js → Spreadsheet-YVL2IZA6.js} +5 -5
  20. package/dist/{Spreadsheet-2YYUKKSY.js.map → Spreadsheet-YVL2IZA6.js.map} +1 -1
  21. package/dist/{Stock-YTQ2OYQ7.js → Stock-33C4U7CR.js} +4 -4
  22. package/dist/{Stock-YTQ2OYQ7.js.map → Stock-33C4U7CR.js.map} +1 -1
  23. package/dist/{Weather-4I4EFUJO.js → Weather-G3XHTB75.js} +4 -4
  24. package/dist/{Weather-4I4EFUJO.js.map → Weather-G3XHTB75.js.map} +1 -1
  25. package/dist/{WorldClock-7PZI7VRE.js → WorldClock-ZUVI3LBN.js} +4 -4
  26. package/dist/{WorldClock-7PZI7VRE.js.map → WorldClock-ZUVI3LBN.js.map} +1 -1
  27. package/dist/apps/index.js +15 -15
  28. package/dist/{chunk-PMPJLVZV.js → chunk-5HCDJPX7.js} +3 -3
  29. package/dist/{chunk-PMPJLVZV.js.map → chunk-5HCDJPX7.js.map} +1 -1
  30. package/dist/chunk-DLVSSR77.js +7 -0
  31. package/dist/{chunk-K6MB36UJ.js.map → chunk-DLVSSR77.js.map} +1 -1
  32. package/dist/{chunk-VA6M46NL.js → chunk-EF5W3EXL.js} +47 -10
  33. package/dist/chunk-EF5W3EXL.js.map +1 -0
  34. package/dist/{chunk-YF2XNVGN.js → chunk-LSFZJZTQ.js} +3 -3
  35. package/dist/{chunk-YF2XNVGN.js.map → chunk-LSFZJZTQ.js.map} +1 -1
  36. package/dist/{chunk-ASRXGCSQ.js → chunk-NIOJDCAU.js} +4 -4
  37. package/dist/{chunk-ASRXGCSQ.js.map → chunk-NIOJDCAU.js.map} +1 -1
  38. package/dist/index.d.ts +411 -155
  39. package/dist/index.js +357 -81
  40. package/dist/index.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/ConfirmDialog-XJP67AHR.js +0 -4
  43. package/dist/chunk-K6MB36UJ.js +0 -7
  44. package/dist/chunk-VA6M46NL.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { subscribePomo, getPomoSnapshot } from './chunk-YF2XNVGN.js';
2
- export { setShellTodoProvider } from './chunk-YF2XNVGN.js';
1
+ import { subscribePomo, getPomoSnapshot } from './chunk-LSFZJZTQ.js';
2
+ export { setShellTodoProvider } from './chunk-LSFZJZTQ.js';
3
3
  export { ImageAnnotator_default as ImageAnnotator } from './chunk-QU5L5ZYM.js';
4
4
  import { PREVIEW_OPENED_EVENT, openPreviewFile } from './chunk-DINNWYVB.js';
5
5
  export { Breadcrumbs } from './chunk-DINNWYVB.js';
@@ -15,10 +15,10 @@ export { setShellAuthBridge } from './chunk-3NZSR3ZG.js';
15
15
  import { EditableGrid } from './chunk-GP4Y3VCB.js';
16
16
  export { EditableGrid } from './chunk-GP4Y3VCB.js';
17
17
  import './chunk-4SQU5YV6.js';
18
- import { APP_VERSION, VERSION } from './chunk-K6MB36UJ.js';
19
- export { VERSION } from './chunk-K6MB36UJ.js';
20
- import { registerModalEscapeInterceptor, useIsMobile, useWindowManager, PopupMenu, PopupMenuLabel, PopupMenuDivider, PopupMenuItem, WINDOW_REGISTRY, isPageEntry, Modal, useShellPrefs, SIDEBAR_STRIP_W, forgetMaximizedWindowBoxes, ModalActions, useModalActive, client_default, LoadingSpinner, isShellApiClientConfigured, useUndo, CancelButton, setWindowPosition } from './chunk-VA6M46NL.js';
21
- export { CancelButton, ConfirmProvider, CopyButton, DocFavStar, Modal, ModalActions, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, UndoProvider, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, commitExposeHighlight, confirm, confirmDestructive, exitExposeMode, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, prompt, registerModalEscapeInterceptor, setExposeHighlight, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useIsActiveWindow, useLocalStoragePrefs, useModalActive, useShellPrefs, useUndo, useUndoable, useUndoableState, useWidgetSettings, useWindowManager, useWindowMenuItem, useWindowTitle } from './chunk-VA6M46NL.js';
18
+ import { APP_VERSION, VERSION } from './chunk-DLVSSR77.js';
19
+ export { VERSION } from './chunk-DLVSSR77.js';
20
+ import { registerModalEscapeInterceptor, useIsMobile, useWindowManager, PopupMenu, PopupMenuLabel, PopupMenuDivider, PopupMenuItem, WINDOW_REGISTRY, isPageEntry, Modal, setPerfCollecting, drainPerfEvents, useShellPrefs, markMenuOpen, SIDEBAR_STRIP_W, forgetMaximizedWindowBoxes, ModalActions, useModalActive, client_default, LoadingSpinner, isShellApiClientConfigured, useUndo, CancelButton, setWindowPosition } from './chunk-EF5W3EXL.js';
21
+ export { CancelButton, ConfirmProvider, CopyButton, DocFavStar, Modal, ModalActions, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, UndoProvider, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, beginWindowGesture, commitExposeHighlight, confirm, confirmDestructive, exitExposeMode, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, markMenuOpen, prompt, registerModalEscapeInterceptor, setExposeHighlight, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useIsActiveWindow, useLocalStoragePrefs, useModalActive, useShellPrefs, useUndo, useUndoable, useUndoableState, useWidgetSettings, useWindowManager, useWindowMenuItem, useWindowTitle } from './chunk-EF5W3EXL.js';
22
22
  import { glassStyle, startMenuCategories, navSections, isSection, GLASS_INPUT_BG, navIcons, isReachable, sectionIcons, useShellAuth, visibleChildren } from './chunk-DKDD5KXG.js';
23
23
  export { GLASS_DIVIDER, GLASS_INPUT_BG, ShellAuthProvider, glassStyle, setShellNavIcons, useShellAuth } from './chunk-DKDD5KXG.js';
24
24
  import { createContext, lazy, forwardRef, useCallback, useState, useRef, useId, useEffect, useLayoutEffect, useMemo, useContext, isValidElement, cloneElement, Suspense, Fragment as Fragment$1, Children, useSyncExternalStore } from 'react';
@@ -466,9 +466,9 @@ function HelpCenter({
466
466
  (a, b) => (rank.get(a.key) ?? Infinity) - (rank.get(b.key) ?? Infinity)
467
467
  );
468
468
  }, [docs, categoryOrder]);
469
- const matches = (doc) => !q || doc.title.toLowerCase().includes(q) || doc.body.toLowerCase().includes(q) || doc.category_label.toLowerCase().includes(q);
469
+ const matches2 = (doc) => !q || doc.title.toLowerCase().includes(q) || doc.body.toLowerCase().includes(q) || doc.category_label.toLowerCase().includes(q);
470
470
  const visibleGroups = useMemo(
471
- () => groups.map((g) => ({ ...g, docs: g.docs.filter(matches) })).filter((g) => g.docs.length > 0),
471
+ () => groups.map((g) => ({ ...g, docs: g.docs.filter(matches2) })).filter((g) => g.docs.length > 0),
472
472
  [groups, q]
473
473
  );
474
474
  const visibleDocs = useMemo(() => visibleGroups.flatMap((g) => g.docs), [visibleGroups]);
@@ -1428,15 +1428,28 @@ function classify(reading) {
1428
1428
  }
1429
1429
 
1430
1430
  // src/shell/perfLog.ts
1431
+ var ACTIVITY_ORDER = [
1432
+ { kind: "submenu", test: (r) => r.submenus > 0 },
1433
+ { kind: "menu", test: (r) => r.menus > 0 },
1434
+ { kind: "resize", test: (r) => r.resizeMs > 0 },
1435
+ { kind: "move", test: (r) => r.moveMs > 0 },
1436
+ { kind: "scroll", test: (r) => r.scrolls > 0 },
1437
+ { kind: "type", test: (r) => r.keys > 0 },
1438
+ { kind: "click", test: (r) => r.clicks > 0 || r.dragMs > 0 }
1439
+ ];
1440
+ function classifyActivity(r) {
1441
+ return ACTIVITY_ORDER.find((entry) => entry.test(r))?.kind ?? "idle";
1442
+ }
1431
1443
  var LOG_CAP = 2400;
1432
1444
  var MIN_GROUP_SAMPLES = 4;
1445
+ var MIN_EVENT_SAMPLES = 2;
1433
1446
  function appendRecord(log, record, cap = LOG_CAP) {
1434
1447
  const next = log.length >= cap ? log.slice(log.length - cap + 1) : log.slice();
1435
1448
  next.push(record);
1436
1449
  return next;
1437
1450
  }
1438
1451
  function isInteracting(r) {
1439
- return r.clicks > 0 || r.keys > 0 || r.scrolls > 0 || r.dragMs > 0;
1452
+ return classifyActivity(r) !== "idle";
1440
1453
  }
1441
1454
  function median(values) {
1442
1455
  if (!values.length) return 0;
@@ -1445,7 +1458,27 @@ function median(values) {
1445
1458
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1446
1459
  }
1447
1460
  function group(records) {
1448
- return { samples: records.length, medianFps: median(records.map((r) => r.fps)) };
1461
+ const measured = records.filter((r) => r.fps > 0);
1462
+ return {
1463
+ samples: records.length,
1464
+ medianFps: median(measured.map((r) => r.fps)),
1465
+ worstMs: records.reduce((worst, r) => r.worstMs > worst ? r.worstMs : worst, 0),
1466
+ stalls: records.length - measured.length
1467
+ };
1468
+ }
1469
+ function bucket(records, keyOf) {
1470
+ const map = /* @__PURE__ */ new Map();
1471
+ for (const r of records) {
1472
+ const key = keyOf(r);
1473
+ if (key === null) continue;
1474
+ const existing = map.get(key);
1475
+ if (existing) existing.push(r);
1476
+ else map.set(key, [r]);
1477
+ }
1478
+ return map;
1479
+ }
1480
+ function rankWorstFirst(groups) {
1481
+ return groups.sort((a, b) => a.medianFps - b.medianFps);
1449
1482
  }
1450
1483
  function summariseLog(log) {
1451
1484
  const empty = {
@@ -1456,37 +1489,34 @@ function summariseLog(log) {
1456
1489
  verdictShare: { smooth: 0, gpu: 0, cpu: 0, unknown: 0 },
1457
1490
  interacting: null,
1458
1491
  idle: null,
1492
+ byActivity: [],
1459
1493
  byWindowCount: [],
1460
- worstWindows: []
1494
+ worstWindows: [],
1495
+ worstMenus: []
1461
1496
  };
1462
1497
  if (!log.length) return empty;
1463
- const measured = log.filter((r) => r.fps > 0);
1464
1498
  const verdictShare = { ...empty.verdictShare };
1465
1499
  for (const r of log) verdictShare[r.verdict] += 1 / log.length;
1466
- const interacting = measured.filter(isInteracting);
1467
- const idle = measured.filter((r) => !isInteracting(r));
1468
- const byWindows = /* @__PURE__ */ new Map();
1469
- const byKey = /* @__PURE__ */ new Map();
1470
- for (const r of measured) {
1471
- const bucket = byWindows.get(r.windows);
1472
- if (bucket) bucket.push(r);
1473
- else byWindows.set(r.windows, [r]);
1474
- if (r.active) {
1475
- const keyed = byKey.get(r.active);
1476
- if (keyed) keyed.push(r);
1477
- else byKey.set(r.active, [r]);
1478
- }
1479
- }
1500
+ const interacting = log.filter(isInteracting);
1501
+ const idle = log.filter((r) => !isInteracting(r));
1480
1502
  return {
1481
1503
  samples: log.length,
1482
1504
  durationMs: log[log.length - 1].t - log[0].t,
1483
- medianFps: median(measured.map((r) => r.fps)),
1505
+ medianFps: median(log.filter((r) => r.fps > 0).map((r) => r.fps)),
1484
1506
  worstFrameMs: log.reduce((worst, r) => r.worstMs > worst ? r.worstMs : worst, 0),
1485
1507
  verdictShare,
1486
1508
  interacting: interacting.length >= MIN_GROUP_SAMPLES ? group(interacting) : null,
1487
1509
  idle: idle.length >= MIN_GROUP_SAMPLES ? group(idle) : null,
1488
- byWindowCount: [...byWindows.entries()].filter(([, rs]) => rs.length >= MIN_GROUP_SAMPLES).map(([windows, rs]) => ({ windows, ...group(rs) })).sort((a, b) => a.windows - b.windows),
1489
- worstWindows: [...byKey.entries()].filter(([, rs]) => rs.length >= MIN_GROUP_SAMPLES).map(([key, rs]) => ({ key, ...group(rs) })).sort((a, b) => a.medianFps - b.medianFps)
1510
+ byActivity: rankWorstFirst(
1511
+ [...bucket(log, classifyActivity).entries()].filter(([, rs]) => rs.length >= MIN_EVENT_SAMPLES).map(([kind, rs]) => ({ kind, ...group(rs) }))
1512
+ ),
1513
+ byWindowCount: [...bucket(log, (r) => r.windows).entries()].filter(([, rs]) => rs.length >= MIN_GROUP_SAMPLES).map(([windows, rs]) => ({ windows, ...group(rs) })).sort((a, b) => a.windows - b.windows),
1514
+ worstWindows: rankWorstFirst(
1515
+ [...bucket(log, (r) => r.active).entries()].filter(([, rs]) => rs.length >= MIN_GROUP_SAMPLES).map(([key, rs]) => ({ key, ...group(rs) }))
1516
+ ),
1517
+ worstMenus: rankWorstFirst(
1518
+ [...bucket(log, (r) => r.menuKey).entries()].filter(([, rs]) => rs.length >= MIN_EVENT_SAMPLES).map(([key, rs]) => ({ key, ...group(rs) }))
1519
+ )
1490
1520
  };
1491
1521
  }
1492
1522
  var CSV_COLUMNS = [
@@ -1502,7 +1532,12 @@ var CSV_COLUMNS = [
1502
1532
  "clicks",
1503
1533
  "keys",
1504
1534
  "scrolls",
1505
- "dragMs"
1535
+ "dragMs",
1536
+ "menus",
1537
+ "submenus",
1538
+ "menuKey",
1539
+ "moveMs",
1540
+ "resizeMs"
1506
1541
  ];
1507
1542
  function toCsv(log) {
1508
1543
  const cell = (value) => {
@@ -1515,11 +1550,144 @@ function toCsv(log) {
1515
1550
  ...log.map((r) => CSV_COLUMNS.map((c) => cell(r[c])).join(","))
1516
1551
  ].join("\n");
1517
1552
  }
1553
+
1554
+ // src/shell/perfEnvironment.ts
1555
+ function pickBrand(brands) {
1556
+ if (!brands?.length) return null;
1557
+ const real = brands.filter((b) => !/not[^a-z]*a[^a-z]*brand/i.test(b.brand));
1558
+ const named = real.find((b) => b.brand !== "Chromium") ?? real[0];
1559
+ return named ? `${named.brand} ${named.version}` : null;
1560
+ }
1561
+ function asString(value) {
1562
+ return typeof value === "string" && value ? value : null;
1563
+ }
1564
+ async function requestAsyncEnvironment() {
1565
+ return { hints: await readHints(), battery: await readBattery() };
1566
+ }
1567
+ async function readHints() {
1568
+ const uaData = navigator.userAgentData;
1569
+ if (!uaData) return null;
1570
+ const base = {
1571
+ platform: uaData.platform ?? null,
1572
+ platformVersion: null,
1573
+ architecture: null,
1574
+ model: null,
1575
+ browser: pickBrand(uaData.brands)
1576
+ };
1577
+ if (!uaData.getHighEntropyValues) return base;
1578
+ try {
1579
+ const high = await uaData.getHighEntropyValues([
1580
+ "platform",
1581
+ "platformVersion",
1582
+ "architecture",
1583
+ "model",
1584
+ "fullVersionList"
1585
+ ]);
1586
+ const full = high.fullVersionList;
1587
+ return {
1588
+ platform: asString(high.platform) ?? base.platform,
1589
+ platformVersion: asString(high.platformVersion),
1590
+ architecture: asString(high.architecture),
1591
+ model: asString(high.model),
1592
+ // The full list carries the build number; the low-entropy brands only
1593
+ // carry the major, which is not enough to match against a known bug.
1594
+ browser: pickBrand(full) ?? base.browser
1595
+ };
1596
+ } catch {
1597
+ return base;
1598
+ }
1599
+ }
1600
+ async function readBattery() {
1601
+ const getBattery = navigator.getBattery;
1602
+ if (!getBattery) return null;
1603
+ try {
1604
+ const battery = await getBattery.call(navigator);
1605
+ return { charging: battery.charging, level: battery.level };
1606
+ } catch {
1607
+ return null;
1608
+ }
1609
+ }
1610
+ function readGpu() {
1611
+ if (typeof document === "undefined") return { renderer: null, vendor: null, available: false };
1612
+ try {
1613
+ const gl = document.createElement("canvas").getContext("webgl");
1614
+ if (!gl) return { renderer: null, vendor: null, available: false };
1615
+ const debug = gl.getExtension("WEBGL_debug_renderer_info");
1616
+ const renderer = debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
1617
+ const vendor = debug ? gl.getParameter(debug.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
1618
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
1619
+ return { renderer: asString(renderer), vendor: asString(vendor), available: true };
1620
+ } catch {
1621
+ return { renderer: null, vendor: null, available: false };
1622
+ }
1623
+ }
1624
+ function matches(query) {
1625
+ try {
1626
+ return window.matchMedia(query).matches;
1627
+ } catch {
1628
+ return false;
1629
+ }
1630
+ }
1631
+ function readEnvironment(async_) {
1632
+ const nav = navigator;
1633
+ const memory = performance.memory;
1634
+ const hints = async_?.hints ?? null;
1635
+ const gpu = readGpu();
1636
+ const conn = nav.connection;
1637
+ return {
1638
+ userAgent: nav.userAgent,
1639
+ browser: hints?.browser ?? null,
1640
+ platform: hints?.platform ?? null,
1641
+ platformVersion: hints?.platformVersion ?? null,
1642
+ architecture: hints?.architecture ?? null,
1643
+ model: hints?.model ?? null,
1644
+ cpuCores: typeof nav.hardwareConcurrency === "number" ? nav.hardwareConcurrency : null,
1645
+ deviceMemoryGB: typeof nav.deviceMemory === "number" ? nav.deviceMemory : null,
1646
+ gpu: gpu.renderer,
1647
+ gpuVendor: gpu.vendor,
1648
+ webglAvailable: gpu.available,
1649
+ screen: {
1650
+ width: screen.width,
1651
+ height: screen.height,
1652
+ dpr: window.devicePixelRatio,
1653
+ colorDepth: typeof screen.colorDepth === "number" ? screen.colorDepth : null
1654
+ },
1655
+ viewport: { width: window.innerWidth, height: window.innerHeight },
1656
+ heapLimitMB: memory ? Math.round(memory.jsHeapSizeLimit / 1048576) : null,
1657
+ network: conn ? {
1658
+ effectiveType: asString(conn.effectiveType),
1659
+ downlinkMbps: typeof conn.downlink === "number" ? conn.downlink : null,
1660
+ rttMs: typeof conn.rtt === "number" ? conn.rtt : null,
1661
+ saveData: conn.saveData === true
1662
+ } : null,
1663
+ battery: async_?.battery ?? null,
1664
+ prefersReducedMotion: matches("(prefers-reduced-motion: reduce)"),
1665
+ forcedColors: matches("(forced-colors: active)"),
1666
+ reducedTransparency: document.documentElement.classList.contains("rosh-reduce-transparency"),
1667
+ touchPoints: typeof nav.maxTouchPoints === "number" ? nav.maxTouchPoints : null
1668
+ };
1669
+ }
1670
+ function describeMachine(env) {
1671
+ const parts = [];
1672
+ if (env.browser) parts.push(env.browser);
1673
+ const os = [env.platform, env.platformVersion].filter(Boolean).join(" ");
1674
+ if (os) parts.push(env.architecture ? `${os} (${env.architecture})` : os);
1675
+ if (env.gpu) parts.push(env.gpu);
1676
+ else if (!env.webglAvailable) parts.push("no WebGL (software rendering)");
1677
+ if (env.cpuCores) parts.push(`${env.cpuCores} cores`);
1678
+ if (env.deviceMemoryGB) parts.push(`${env.deviceMemoryGB}GB+ RAM`);
1679
+ parts.push(`${env.screen.width}\xD7${env.screen.height} @${env.screen.dpr}x`);
1680
+ if (env.battery && !env.battery.charging) parts.push(`on battery ${Math.round(env.battery.level * 100)}%`);
1681
+ if (env.reducedTransparency) parts.push("reduce transparency on");
1682
+ if (env.prefersReducedMotion) parts.push("reduced motion");
1683
+ return parts.join(" \xB7 ");
1684
+ }
1518
1685
  var FLUSH_MS = 500;
1519
1686
  var WINDOW_MS = 1e3;
1520
1687
  var HISTORY = 40;
1521
1688
  var PERSIST_EVERY_MS = 1e4;
1522
1689
  var LOG_STORAGE_KEY = "react-os-shell:perf-log";
1690
+ var MAX_DRAG_GAP_MS = 100;
1523
1691
  var VERDICT_STYLES = {
1524
1692
  smooth: { dot: "bg-emerald-400", text: "text-emerald-300" },
1525
1693
  gpu: { dot: "bg-amber-400", text: "text-amber-300" },
@@ -1541,7 +1709,7 @@ function readWindows() {
1541
1709
  });
1542
1710
  return { windows: panels.length, active };
1543
1711
  }
1544
- function PerfStats({ onClose, className = "" }) {
1712
+ function PerfStats({ onClose, onSubmit, className = "" }) {
1545
1713
  const framesRef = useRef([]);
1546
1714
  const blockedMsRef = useRef(0);
1547
1715
  const activityRef = useRef({ clicks: 0, keys: 0, scrolls: 0, dragMs: 0 });
@@ -1549,11 +1717,30 @@ function PerfStats({ onClose, className = "" }) {
1549
1717
  const logRef = useRef([]);
1550
1718
  const startedAtRef = useRef(0);
1551
1719
  const persistedAtRef = useRef(0);
1720
+ const snapshotRef = useRef(null);
1721
+ const envRef = useRef(null);
1552
1722
  const [longTaskSupported, setLongTaskSupported] = useState(false);
1553
1723
  const [reading, setReading] = useState({ fps: 0, frameMs: 0, worstMs: 0, blockedPct: null });
1554
1724
  const [history, setHistory] = useState([]);
1555
1725
  const [heap, setHeap] = useState(null);
1556
1726
  const [logSize, setLogSize] = useState(0);
1727
+ const [composing, setComposing] = useState(false);
1728
+ const [message, setMessage] = useState("");
1729
+ const [sending, setSending] = useState(false);
1730
+ const [sendError, setSendError] = useState(null);
1731
+ useEffect(() => {
1732
+ setPerfCollecting(true);
1733
+ return () => setPerfCollecting(false);
1734
+ }, []);
1735
+ useEffect(() => {
1736
+ let live = true;
1737
+ requestAsyncEnvironment().then((env) => {
1738
+ if (live) envRef.current = env;
1739
+ });
1740
+ return () => {
1741
+ live = false;
1742
+ };
1743
+ }, []);
1557
1744
  useEffect(() => {
1558
1745
  let raf = 0;
1559
1746
  const tick = (now) => {
@@ -1588,8 +1775,9 @@ function PerfStats({ onClose, className = "" }) {
1588
1775
  const onPointerMove = () => {
1589
1776
  if (!dragRef.current.down) return;
1590
1777
  const now = performance.now();
1591
- activityRef.current.dragMs += now - dragRef.current.since;
1778
+ const gap = now - dragRef.current.since;
1592
1779
  dragRef.current.since = now;
1780
+ if (gap <= MAX_DRAG_GAP_MS) activityRef.current.dragMs += gap;
1593
1781
  };
1594
1782
  const onPointerUp = () => {
1595
1783
  dragRef.current.down = false;
@@ -1643,6 +1831,7 @@ function PerfStats({ onClose, className = "" }) {
1643
1831
  const heapMB = memory ? memory.usedJSHeapSize / 1048576 : null;
1644
1832
  const nextReading = { ...frames, blockedPct };
1645
1833
  const activity = activityRef.current;
1834
+ const events = drainPerfEvents();
1646
1835
  const { windows, active } = readWindows();
1647
1836
  logRef.current = appendRecord(logRef.current, {
1648
1837
  t: Math.round(now - startedAtRef.current),
@@ -1655,7 +1844,12 @@ function PerfStats({ onClose, className = "" }) {
1655
1844
  clicks: activity.clicks,
1656
1845
  keys: activity.keys,
1657
1846
  scrolls: activity.scrolls,
1658
- dragMs: Math.round(activity.dragMs)
1847
+ dragMs: Math.round(activity.dragMs),
1848
+ menus: events.menus,
1849
+ submenus: events.submenus,
1850
+ menuKey: events.menuKey,
1851
+ moveMs: Math.round(events.moveMs),
1852
+ resizeMs: Math.round(events.resizeMs)
1659
1853
  });
1660
1854
  activity.clicks = 0;
1661
1855
  activity.keys = 0;
@@ -1672,38 +1866,66 @@ function PerfStats({ onClose, className = "" }) {
1672
1866
  }, FLUSH_MS);
1673
1867
  return () => clearInterval(id);
1674
1868
  }, [longTaskSupported, persist]);
1675
- const download = useCallback((body, type, extension) => {
1676
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1677
- const url = URL.createObjectURL(new Blob([body], { type }));
1678
- const link = document.createElement("a");
1679
- link.href = url;
1680
- link.download = `perf-log-${stamp}.${extension}`;
1681
- link.click();
1682
- URL.revokeObjectURL(url);
1869
+ const beginReport = useCallback(() => {
1870
+ snapshotRef.current = logRef.current.slice();
1871
+ setSendError(null);
1872
+ setComposing(true);
1683
1873
  }, []);
1684
- const exportJson = useCallback(() => {
1685
- download(
1686
- JSON.stringify(
1874
+ const verdict = classify(reading);
1875
+ const buildReport = useCallback(() => {
1876
+ const records = snapshotRef.current ?? logRef.current;
1877
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1878
+ const summary = summariseLog(records);
1879
+ const environment = readEnvironment(envRef.current);
1880
+ return {
1881
+ message,
1882
+ filename: `perf-log-${stamp}.json`,
1883
+ summary,
1884
+ environment,
1885
+ verdict: verdict.label,
1886
+ json: JSON.stringify(
1687
1887
  {
1688
1888
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1689
- userAgent: navigator.userAgent,
1690
- screen: { width: window.screen.width, height: window.screen.height, dpr: window.devicePixelRatio },
1691
- reducedTransparency: document.documentElement.classList.contains("rosh-reduce-transparency"),
1692
- summary: summariseLog(logRef.current),
1693
- records: logRef.current
1889
+ message,
1890
+ verdict: verdict.label,
1891
+ environment,
1892
+ summary,
1893
+ records
1694
1894
  },
1695
1895
  null,
1696
1896
  2
1697
- ),
1698
- "application/json",
1699
- "json"
1700
- );
1701
- }, [download]);
1702
- const exportCsv = useCallback(() => download(toCsv(logRef.current), "text/csv", "csv"), [download]);
1703
- const verdict = classify(reading);
1897
+ )
1898
+ };
1899
+ }, [message, verdict.label]);
1900
+ const downloadReport = useCallback((report) => {
1901
+ const url = URL.createObjectURL(new Blob([report.json], { type: "application/json" }));
1902
+ const link = document.createElement("a");
1903
+ link.href = url;
1904
+ link.download = report.filename;
1905
+ link.click();
1906
+ URL.revokeObjectURL(url);
1907
+ }, []);
1908
+ const send = useCallback(async () => {
1909
+ const report = buildReport();
1910
+ setSending(true);
1911
+ setSendError(null);
1912
+ try {
1913
+ if (onSubmit) await onSubmit(report);
1914
+ else downloadReport(report);
1915
+ snapshotRef.current = null;
1916
+ setMessage("");
1917
+ setComposing(false);
1918
+ } catch (err) {
1919
+ setSendError(err instanceof Error && err.message ? err.message : "Could not send. Try again.");
1920
+ } finally {
1921
+ setSending(false);
1922
+ }
1923
+ }, [buildReport, downloadReport, onSubmit]);
1704
1924
  const styles = VERDICT_STYLES[verdict.kind];
1705
1925
  const peak = Math.max(60, ...history);
1706
1926
  const loggedSeconds = Math.round(logSize * FLUSH_MS / 1e3);
1927
+ const sendLabel = onSubmit ? "Send" : "Download";
1928
+ const reportSamples = snapshotRef.current?.length ?? logSize;
1707
1929
  return /* @__PURE__ */ jsxs(
1708
1930
  "div",
1709
1931
  {
@@ -1766,34 +1988,73 @@ function PerfStats({ onClose, className = "" }) {
1766
1988
  ] }),
1767
1989
  /* @__PURE__ */ jsx("p", { className: "mt-0.5 leading-snug text-white/45", children: verdict.detail })
1768
1990
  ] }),
1769
- /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-between border-t border-white/10 pt-1.5", children: [
1991
+ /* @__PURE__ */ jsx("div", { className: "mt-2 border-t border-white/10 pt-1.5", children: composing ? (
1992
+ /* Composer. `select-text` because the panel as a whole is
1993
+ `select-none` — without it the user cannot correct their own
1994
+ sentence. */
1995
+ /* @__PURE__ */ jsxs("div", { className: "select-text space-y-1.5", children: [
1996
+ /* @__PURE__ */ jsx("label", { className: "block text-white/40", htmlFor: "perf-report-message", children: "What were you doing?" }),
1997
+ /* @__PURE__ */ jsx(
1998
+ "textarea",
1999
+ {
2000
+ id: "perf-report-message",
2001
+ value: message,
2002
+ onChange: (e) => setMessage(e.target.value),
2003
+ rows: 3,
2004
+ autoFocus: true,
2005
+ placeholder: "e.g. opening the second-level menu stutters",
2006
+ className: "w-full resize-none rounded border border-white/15 bg-black/40 px-1.5 py-1 font-mono text-[10px] text-white/90 placeholder:text-white/25 focus:border-white/40 focus:outline-none"
2007
+ }
2008
+ ),
2009
+ sendError && /* @__PURE__ */ jsx("p", { className: "leading-snug text-rose-300", children: sendError }),
2010
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2011
+ /* @__PURE__ */ jsxs("span", { className: "text-white/35", children: [
2012
+ reportSamples,
2013
+ " samples"
2014
+ ] }),
2015
+ /* @__PURE__ */ jsxs("span", { className: "flex gap-1", children: [
2016
+ /* @__PURE__ */ jsx(
2017
+ "button",
2018
+ {
2019
+ onClick: () => {
2020
+ setComposing(false);
2021
+ snapshotRef.current = null;
2022
+ setSendError(null);
2023
+ },
2024
+ disabled: sending,
2025
+ className: "rounded border border-white/15 px-1.5 py-0.5 text-white/50 transition-colors hover:border-white/30 hover:text-white disabled:opacity-40",
2026
+ children: "Cancel"
2027
+ }
2028
+ ),
2029
+ /* @__PURE__ */ jsx(
2030
+ "button",
2031
+ {
2032
+ onClick: send,
2033
+ disabled: sending,
2034
+ className: "rounded border border-white/30 bg-white/10 px-1.5 py-0.5 text-white/90 transition-colors hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-40",
2035
+ children: sending ? "Sending\u2026" : sendLabel
2036
+ }
2037
+ )
2038
+ ] })
2039
+ ] })
2040
+ ] })
2041
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1770
2042
  /* @__PURE__ */ jsxs("span", { className: "text-white/35", children: [
1771
2043
  "log ",
1772
2044
  logSize,
1773
2045
  " \xB7 ",
1774
2046
  loggedSeconds < 60 ? `${loggedSeconds}s` : `${Math.round(loggedSeconds / 60)}m`
1775
2047
  ] }),
1776
- /* @__PURE__ */ jsxs("span", { className: "flex gap-1", children: [
1777
- /* @__PURE__ */ jsx(
1778
- "button",
1779
- {
1780
- onClick: exportJson,
1781
- disabled: !logSize,
1782
- className: "rounded border border-white/15 px-1.5 py-0.5 text-white/60 transition-colors hover:border-white/30 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
1783
- children: "JSON"
1784
- }
1785
- ),
1786
- /* @__PURE__ */ jsx(
1787
- "button",
1788
- {
1789
- onClick: exportCsv,
1790
- disabled: !logSize,
1791
- className: "rounded border border-white/15 px-1.5 py-0.5 text-white/60 transition-colors hover:border-white/30 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
1792
- children: "CSV"
1793
- }
1794
- )
1795
- ] })
1796
- ] })
2048
+ /* @__PURE__ */ jsx(
2049
+ "button",
2050
+ {
2051
+ onClick: beginReport,
2052
+ disabled: !logSize,
2053
+ className: "rounded border border-white/15 px-1.5 py-0.5 text-white/60 transition-colors hover:border-white/30 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
2054
+ children: onSubmit ? "Report this" : "Save report"
2055
+ }
2056
+ )
2057
+ ] }) })
1797
2058
  ]
1798
2059
  }
1799
2060
  );
@@ -2986,7 +3247,13 @@ function Desktop({ profile }) {
2986
3247
  const taskbarAtBottom = prefs.taskbar_position !== "top" && prefs.taskbar_position !== "left" && prefs.taskbar_position !== "right";
2987
3248
  const side = prefs.taskbar_position === "right" ? "left-3" : "right-3";
2988
3249
  const bottom = taskbarAtBottom ? versionShowing ? "bottom-24" : "bottom-16" : versionShowing ? "bottom-8" : "bottom-3";
2989
- return /* @__PURE__ */ jsx("div", { className: `absolute z-[240] ${side} ${bottom}`, children: /* @__PURE__ */ jsx(PerfStats, { onClose: () => saveShellPrefs({ show_desktop_stats: false }) }) });
3250
+ return /* @__PURE__ */ jsx("div", { className: `absolute z-[240] ${side} ${bottom}`, children: /* @__PURE__ */ jsx(
3251
+ PerfStats,
3252
+ {
3253
+ onClose: () => saveShellPrefs({ show_desktop_stats: false }),
3254
+ onSubmit: host.onSubmitPerfReport
3255
+ }
3256
+ ) });
2990
3257
  })(),
2991
3258
  prefs.show_desktop_version === true && (host.productVersion ?? APP_VERSION) && /* @__PURE__ */ jsx(
2992
3259
  "button",
@@ -3325,6 +3592,15 @@ function StartMenu({
3325
3592
  useEffect(() => {
3326
3593
  setHoveredChild(null);
3327
3594
  }, [hoveredSection]);
3595
+ useEffect(() => {
3596
+ if (open) markMenuOpen("menu", "start");
3597
+ }, [open]);
3598
+ useEffect(() => {
3599
+ if (hoveredSection) markMenuOpen("submenu", hoveredSection);
3600
+ }, [hoveredSection]);
3601
+ useEffect(() => {
3602
+ if (hoveredChild) markMenuOpen("submenu", hoveredChild);
3603
+ }, [hoveredChild]);
3328
3604
  useLayoutEffect(() => {
3329
3605
  if (!flyoutRef.current || !hoveredSection || search.length >= 2) return;
3330
3606
  const h = flyoutRef.current.offsetHeight;
@@ -4840,7 +5116,7 @@ function Customization({ omit, section } = {}) {
4840
5116
  /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-700", children: [
4841
5117
  "Show performance stats",
4842
5118
  /* @__PURE__ */ jsx("span", { className: "block text-xs text-gray-400", children: "Overlays frame rate, worst frame time and a bottleneck reading on the desktop, beside the version number. Turn it on to check whether Reduce transparency actually helps on this machine \u2014 or whether the slowness is coming from somewhere else." }),
4843
- /* @__PURE__ */ jsx("span", { className: "block text-xs text-gray-400 mt-1", children: "While it is on, each reading is recorded with what was happening at the time \u2014 how many windows were open, which one was on top, and counts of clicks, keystrokes, scrolls and window drags. No page content or keystroke text is recorded. The log stays on this device and is only shared if you export it with the JSON or CSV button on the overlay." })
5119
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-gray-400 mt-1", children: "While it is on, each reading is recorded with what was happening at the time \u2014 how many windows were open, which one was on top, which menu or submenu you opened, how long you spent moving or resizing a window, and counts of clicks, keystrokes and scrolls. No page content or keystroke text is recorded. The log stays on this device unless you use the overlay's Report button, which sends it \u2014 with whatever you type in the box, and a description of this computer (browser, operating system, processor cores, memory, graphics card, screen, connection speed and whether you are on battery) \u2014 to the team as a bug report." })
4844
5120
  ] })
4845
5121
  ] })
4846
5122
  ] })
@@ -10504,6 +10780,6 @@ function useEditHotkey(callback) {
10504
10780
  }, [callback, isActive]);
10505
10781
  }
10506
10782
 
10507
- export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, BLOCKED_PCT_CPU, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, DateRangePicker, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, LOG_CAP, Label, Layout, ListFooter, ListLoadError, LoadingSpinner2 as LoadingSpinner, MIN_GROUP_SAMPLES, MOD, Markdown, MediaUploadField, MediaUploadGrid, MetricBar, MilestoneTimeline, NativeSelect, NotificationBell, PageHeader, Pagination, PdfActionButton, PerfStats, Radio_default as Radio, ResizableTable, SHIFT, SMOOTH_FPS, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, UndoControls, WidgetManager, appendRecord as appendPerfRecord, applyDevTitle, classify as classifyPerf, createWindowRegistry, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isInteracting, isMac, isSeverityTone, mediaFileName, mergeBulkItems, toCsv as perfLogToCsv, severityOf, summariseFrames, summariseLog as summarisePerfLog, toISODate2 as toISODate, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
10783
+ export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, BLOCKED_PCT_CPU, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, DateRangePicker, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, LOG_CAP, Label, Layout, ListFooter, ListLoadError, LoadingSpinner2 as LoadingSpinner, MIN_EVENT_SAMPLES, MIN_GROUP_SAMPLES, MOD, Markdown, MediaUploadField, MediaUploadGrid, MetricBar, MilestoneTimeline, NativeSelect, NotificationBell, PageHeader, Pagination, PdfActionButton, PerfStats, Radio_default as Radio, ResizableTable, SHIFT, SMOOTH_FPS, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, UndoControls, WidgetManager, appendRecord as appendPerfRecord, applyDevTitle, classifyActivity, classify as classifyPerf, createWindowRegistry, describeMachine, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isInteracting, isMac, isSeverityTone, mediaFileName, mergeBulkItems, toCsv as perfLogToCsv, readEnvironment as readPerfEnvironment, requestAsyncEnvironment, severityOf, summariseFrames, summariseLog as summarisePerfLog, toISODate2 as toISODate, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
10508
10784
  //# sourceMappingURL=index.js.map
10509
10785
  //# sourceMappingURL=index.js.map