local-operator-ui 0.34.56 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/out/main/index.js +604 -46
  2. package/out/preload/index.js +50 -1
  3. package/out/renderer/assets/{_basePickBy-BZpmSLU-.js → _basePickBy-C724Y6y1.js} +1 -1
  4. package/out/renderer/assets/{_baseUniq-5Fk4AsiY.js → _baseUniq-CTHBzlaK.js} +1 -1
  5. package/out/renderer/assets/{agent-details-page-BEoctGwH.js → agent-details-page-C6VzLw1m.js} +1 -1
  6. package/out/renderer/assets/{agent-hub-page-DVtEYZne.js → agent-hub-page-C4o6Aqru.js} +1 -1
  7. package/out/renderer/assets/{agents-page-CqlFD0Ps.js → agents-page-FP1_RvOY.js} +3 -3
  8. package/out/renderer/assets/{arc-Cgq0RekE.js → arc-Cv2GJOa_.js} +1 -1
  9. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-SKpw5bNL.js → architectureDiagram-IEHRJDOE-BZyhfzWz.js} +1 -1
  10. package/out/renderer/assets/{blockDiagram-JOT3LUYC-jQv0ffHr.js → blockDiagram-JOT3LUYC-DtHvlbei.js} +1 -1
  11. package/out/renderer/assets/{book-open--60aKTv6.js → book-open-C65LfbfB.js} +1 -1
  12. package/out/renderer/assets/browser-page-Cgwj8_cP.js +1 -0
  13. package/out/renderer/assets/{browser-webauthn-prompt-PeVRs-qh.js → browser-webauthn-prompt-CjbHvbI7.js} +1 -1
  14. package/out/renderer/assets/{c4Diagram-VJAJSXHY-BlRyRK9C.js → c4Diagram-VJAJSXHY-DKV5t3tX.js} +1 -1
  15. package/out/renderer/assets/channel-Bzt6SfZF.js +1 -0
  16. package/out/renderer/assets/{chunk-4BMEZGHF-BBVAaHlr.js → chunk-4BMEZGHF-D1J0hVNB.js} +1 -1
  17. package/out/renderer/assets/{chunk-A2AXSNBT-sJR5i1wN.js → chunk-A2AXSNBT-msLg_v_T.js} +1 -1
  18. package/out/renderer/assets/{chunk-AEK57VVT-C8IWMgPw.js → chunk-AEK57VVT-Dk25anb1.js} +1 -1
  19. package/out/renderer/assets/{chunk-D6G4REZN-DTfUS03C.js → chunk-D6G4REZN-DhfHQLiP.js} +1 -1
  20. package/out/renderer/assets/{chunk-RZ5BOZE2-DiGk8o7V.js → chunk-RZ5BOZE2-D83jc5YN.js} +1 -1
  21. package/out/renderer/assets/{chunk-XZIHB7SX-Mamouuqj.js → chunk-XZIHB7SX-DCYwl-3m.js} +1 -1
  22. package/out/renderer/assets/classDiagram-GIVACNV2-BAnolaTk.js +1 -0
  23. package/out/renderer/assets/classDiagram-v2-COTLJTTW-BAnolaTk.js +1 -0
  24. package/out/renderer/assets/clone-QOUemT9c.js +1 -0
  25. package/out/renderer/assets/{console-mirror-Ch4jKiRK.js → console-mirror-M7tcIGks.js} +1 -1
  26. package/out/renderer/assets/{consoleCapture-BYlfSgpd.js → consoleCapture-DN8X9Ugb.js} +1 -1
  27. package/out/renderer/assets/{conversation-input-sync-Cs9wr7F7.js → conversation-input-sync-CO6CiDf8.js} +264 -259
  28. package/out/renderer/assets/{dagre-OKDRZEBW-D5TY-UFL.js → dagre-OKDRZEBW-D8jHPlQ1.js} +1 -1
  29. package/out/renderer/assets/{diagram-SSKATNLV-BTiHJkCz.js → diagram-SSKATNLV-DBWQdBIr.js} +1 -1
  30. package/out/renderer/assets/{diagram-VNBRO52H-CBxLprav.js → diagram-VNBRO52H-DT_32LVX.js} +1 -1
  31. package/out/renderer/assets/{erDiagram-Q7BY3M3F-9aF5V3AY.js → erDiagram-Q7BY3M3F-B-GEywmS.js} +1 -1
  32. package/out/renderer/assets/{error-boundary-B5YLbOOu.js → error-boundary-Ba0RLAEb.js} +2 -2
  33. package/out/renderer/assets/{flowDiagram-4HSFHLVR-BXlN8K06.js → flowDiagram-4HSFHLVR-DWqU7r-A.js} +1 -1
  34. package/out/renderer/assets/{ganttDiagram-APWFNJXF-1FcF-Cau.js → ganttDiagram-APWFNJXF-DGenG4u6.js} +1 -1
  35. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-CjCM20j_.js → gitGraphDiagram-7IBYFJ6S-BPA8urey.js} +1 -1
  36. package/out/renderer/assets/{graph-CGuSrplO.js → graph-GUG88wOs.js} +1 -1
  37. package/out/renderer/assets/index-BadTTC0t.js +57 -0
  38. package/out/renderer/assets/{index-CalJ1cZq.js → index-DYdfRls6.js} +1 -1
  39. package/out/renderer/assets/{index-rnB6EiBX.js → index-DZH3HC4h.js} +216 -201
  40. package/out/renderer/assets/index-DstdCAdq.css +1 -0
  41. package/out/renderer/assets/{index-5brl9P13.js → index-FR873jzR.js} +1 -1
  42. package/out/renderer/assets/{infoDiagram-PH2N3AL5-Bqs15-nE.js → infoDiagram-PH2N3AL5-BcZyO1LQ.js} +1 -1
  43. package/out/renderer/assets/{installer-DZXz4Bgj.js → installer-DArqAYep.js} +1 -1
  44. package/out/renderer/assets/{journeyDiagram-U35MCT3I-BkSqHNi1.js → journeyDiagram-U35MCT3I-Mo89YhbY.js} +1 -1
  45. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-CGnCYoin.js → kanban-definition-NDS4AKOZ-ZnBrWmw4.js} +1 -1
  46. package/out/renderer/assets/{layout-BM7AzCJ1.js → layout-PAHQYOvY.js} +1 -1
  47. package/out/renderer/assets/{legacy-agents-page-B8f2tJOf.js → legacy-agents-page-Czc5Wbjr.js} +1 -1
  48. package/out/renderer/assets/{mermaid.core-C14g_ZP7.js → mermaid.core-C9gG5Wli.js} +5 -5
  49. package/out/renderer/assets/{mesh-page-DCCummN5.js → mesh-page-BxR50_4c.js} +1 -1
  50. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-D7Z-SM4X.js → mindmap-definition-ALO5MXBD-CCb-zon0.js} +1 -1
  51. package/out/renderer/assets/{mini-CcJI0pQs.js → mini-DqIb7MPh.js} +1 -1
  52. package/out/renderer/assets/{notebook-pen-Dj0km82T.js → notebook-pen-D8RjZqVC.js} +1 -1
  53. package/out/renderer/assets/{org-access-D0eAQUWQ.js → org-access-BK8MV-Og.js} +1 -1
  54. package/out/renderer/assets/{org-surface-gate-Bgznt94a.js → org-surface-gate-BG7zY2lU.js} +1 -1
  55. package/out/renderer/assets/{page-header-B3mzM2nC.js → page-header-Ch4j7oI6.js} +1 -1
  56. package/out/renderer/assets/{pieDiagram-IB7DONF6-3pVvYdop.js → pieDiagram-IB7DONF6-BXTvjCLw.js} +1 -1
  57. package/out/renderer/assets/{projects-page-T2Yw-kRf.js → projects-page-si3RlRLo.js} +1 -1
  58. package/out/renderer/assets/{publication-validation-BIJPIOf1.js → publication-validation-CxIO7FGl.js} +1 -1
  59. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-Dttg4cdR.js → quadrantDiagram-7GDLP6J5-BMU76de4.js} +1 -1
  60. package/out/renderer/assets/{radar-MK3ICKWK-Bd7wnpF5.js → radar-MK3ICKWK-C1pTiS7q.js} +1 -1
  61. package/out/renderer/assets/{radient-auth-buttons-MPoHRsQX.js → radient-auth-buttons-DrefDDYq.js} +1 -1
  62. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-Jiaz4ehK.js → requirementDiagram-KVF5MWMF-DsVqu3pR.js} +1 -1
  63. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-C4tMl9ww.js → sankeyDiagram-QLVOVGJD-CprOe8fX.js} +1 -1
  64. package/out/renderer/assets/{schedules-page-CR7bLqQt.js → schedules-page-CzOtqfGC.js} +1 -1
  65. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-DncET3lE.js → sequenceDiagram-X6HHIX6F-DZ5TPMR1.js} +1 -1
  66. package/out/renderer/assets/settings-page-7uhG4VZK.js +287 -0
  67. package/out/renderer/assets/{stateDiagram-DGXRK772-BI1ziKRV.js → stateDiagram-DGXRK772-_sPBuqdd.js} +1 -1
  68. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-B-4nbbOf.js +1 -0
  69. package/out/renderer/assets/{timeline-definition-BDJGKUSR-DxR2rlJV.js → timeline-definition-BDJGKUSR-DWABwm6j.js} +1 -1
  70. package/out/renderer/assets/ui-preferences-store-wu8ubztI.js +1 -0
  71. package/out/renderer/assets/{use-download-agent-mutation-C6TvAwyM.js → use-download-agent-mutation-ZIDdnF_f.js} +1 -1
  72. package/out/renderer/assets/{use-memberships-query-AlVCuF9E.js → use-memberships-query-BZiimzLs.js} +1 -1
  73. package/out/renderer/assets/{use-session-snapshot-XM0bTxHC.js → use-session-snapshot-BiWZ99PX.js} +1 -1
  74. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CJW_dOUR.js → xychartDiagram-VJFVF3MP-BBSAaYhx.js} +1 -1
  75. package/out/renderer/console-capture.html +5 -5
  76. package/out/renderer/index.html +7 -7
  77. package/out/renderer/installer.html +4 -4
  78. package/out/renderer/mini.html +7 -7
  79. package/package.json +2 -2
  80. package/out/renderer/assets/browser-page-jc3r193W.js +0 -1
  81. package/out/renderer/assets/channel-BpN7da5J.js +0 -1
  82. package/out/renderer/assets/classDiagram-GIVACNV2-IcRW-YOl.js +0 -1
  83. package/out/renderer/assets/classDiagram-v2-COTLJTTW-IcRW-YOl.js +0 -1
  84. package/out/renderer/assets/clone-Dri7yO5y.js +0 -1
  85. package/out/renderer/assets/index-BUuvY2SU.css +0 -1
  86. package/out/renderer/assets/index-DbzmFwlT.js +0 -57
  87. package/out/renderer/assets/settings-page-B_D-wGm4.js +0 -292
  88. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Bqeb-TkR.js +0 -1
  89. package/out/renderer/assets/ui-preferences-store-D1IJShox.js +0 -1
package/out/main/index.js CHANGED
@@ -185,6 +185,26 @@ const profileName = zod.z.string().min(1).max(128).refine(
185
185
  )
186
186
  );
187
187
  const target = zod.z.object({ kind: zod.z.enum(["agent", "team"]), name: profileName }).strict();
188
+ const importSourceName = zod.z.enum(["claude", "codex", "omp"]);
189
+ const importSessionIds = zod.z.record(
190
+ zod.z.string(),
191
+ zod.z.array(zod.z.string().min(1).max(512)).max(5e3)
192
+ );
193
+ const importRequestFields = {
194
+ sources: zod.z.array(importSourceName).max(3).optional(),
195
+ sessionIds: importSessionIds.optional(),
196
+ /*
197
+ * Kept on the wire for the CLI/TUI and future callers, but the Settings
198
+ * surface never sets it: no-default-all is enforced in the UI by requiring
199
+ * an explicit per-session selection before preview and again before import.
200
+ */
201
+ all: zod.z.boolean().optional(),
202
+ includeSubagents: zod.z.boolean().optional(),
203
+ includeClearedSegments: zod.z.boolean().optional(),
204
+ includeMemory: zod.z.boolean().optional(),
205
+ includeInstructions: zod.z.boolean().optional(),
206
+ maxSessions: zod.z.number().int().min(1).max(5e3).optional()
207
+ };
188
208
  const modelSelection = zod.z.object({
189
209
  provider: zod.z.string().min(1).max(128),
190
210
  model_id: zod.z.string().min(1).max(512),
@@ -419,6 +439,27 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
419
439
  zod.z.object({ op: zod.z.literal("runtimes.list") }).strict(),
420
440
  zod.z.object({ op: zod.z.literal("profiles.list") }).strict(),
421
441
  zod.z.object({ op: zod.z.literal("profiles.get"), name: profileName }).strict(),
442
+ /*
443
+ * The harness import plane. Reads are fixed-shape GETs; preview and start
444
+ * carry the same explicit request fields, and neither is allowed to infer a
445
+ * selection: the UI paints an empty selection and makes the user choose.
446
+ */
447
+ zod.z.object({ op: zod.z.literal("imports.sources") }).strict(),
448
+ zod.z.object({
449
+ op: zod.z.literal("imports.sessions"),
450
+ source: importSourceName,
451
+ includeSubagents: zod.z.boolean().optional()
452
+ }).strict(),
453
+ zod.z.object({ op: zod.z.literal("imports.preview"), ...importRequestFields }).strict(),
454
+ zod.z.object({ op: zod.z.literal("imports.start"), ...importRequestFields }).strict(),
455
+ zod.z.object({
456
+ op: zod.z.literal("imports.status"),
457
+ jobId: zod.z.string().min(1).max(128)
458
+ }).strict(),
459
+ zod.z.object({
460
+ op: zod.z.literal("imports.cancel"),
461
+ jobId: zod.z.string().min(1).max(128)
462
+ }).strict(),
422
463
  /*
423
464
  * Definition CAS (proposed additive wire, b-wire-receipt-definition-cas-and-adopt).
424
465
  * `expectedDefinitionRevision` is an opaque echo of the `definition_revision` the
@@ -2780,6 +2821,10 @@ zod.z.object({
2780
2821
  const DESKTOP_MESSAGE_BYTE_BUDGET = 88e4;
2781
2822
  const DESKTOP_CONTROL_BYTE_BUDGET = 262144;
2782
2823
  const DESKTOP_SYSTEM_PROMPT_BYTE_BUDGET = 11e5;
2824
+ const IMPORT_OPS = /* @__PURE__ */ new Set([
2825
+ "imports.preview",
2826
+ "imports.start"
2827
+ ]);
2783
2828
  const MESSAGE_OPS = /* @__PURE__ */ new Set([
2784
2829
  "sessions.message",
2785
2830
  "sessions.command",
@@ -2792,7 +2837,7 @@ const MESSAGE_OPS = /* @__PURE__ */ new Set([
2792
2837
  function desktopRequestByteBudget(op) {
2793
2838
  if (op === "legacy.agent.systemPrompt.update")
2794
2839
  return DESKTOP_SYSTEM_PROMPT_BYTE_BUDGET;
2795
- return MESSAGE_OPS.has(op) ? DESKTOP_MESSAGE_BYTE_BUDGET : DESKTOP_CONTROL_BYTE_BUDGET;
2840
+ return MESSAGE_OPS.has(op) || IMPORT_OPS.has(op) ? DESKTOP_MESSAGE_BYTE_BUDGET : DESKTOP_CONTROL_BYTE_BUDGET;
2796
2841
  }
2797
2842
  const DESKTOP_CONTROL_DEADLINE_MS = 2e4;
2798
2843
  const DESKTOP_LONG_READ_DEADLINE_MS = 9e4;
@@ -3054,6 +3099,18 @@ function assertPairedPublicationTarget(request) {
3054
3099
  );
3055
3100
  }
3056
3101
  }
3102
+ function harnessImportBody(request) {
3103
+ return {
3104
+ sources: request.sources ?? null,
3105
+ session_ids: request.sessionIds ?? {},
3106
+ all: request.all ?? false,
3107
+ include_subagents: request.includeSubagents ?? false,
3108
+ include_cleared_segments: request.includeClearedSegments ?? false,
3109
+ include_memory: request.includeMemory ?? true,
3110
+ include_instructions: request.includeInstructions ?? true,
3111
+ ...request.maxSessions === void 0 ? {} : { max_sessions: request.maxSessions }
3112
+ };
3113
+ }
3057
3114
  function desktopEndpoint(request) {
3058
3115
  switch (request.op) {
3059
3116
  case "capabilities":
@@ -3149,6 +3206,38 @@ function desktopEndpoint(request) {
3149
3206
  method: "POST",
3150
3207
  body: { request_id: request.requestId, name: request.name }
3151
3208
  };
3209
+ case "imports.sources":
3210
+ return { path: "/v1/desktop/imports/sources", method: "GET" };
3211
+ case "imports.sessions": {
3212
+ const query = new URLSearchParams({ source: request.source });
3213
+ if (request.includeSubagents) query.set("include_subagents", "true");
3214
+ return {
3215
+ path: `/v1/desktop/imports/sessions?${query.toString()}`,
3216
+ method: "GET"
3217
+ };
3218
+ }
3219
+ case "imports.preview":
3220
+ return {
3221
+ path: "/v1/desktop/imports/preview",
3222
+ method: "POST",
3223
+ body: harnessImportBody(request)
3224
+ };
3225
+ case "imports.start":
3226
+ return {
3227
+ path: "/v1/desktop/imports/jobs",
3228
+ method: "POST",
3229
+ body: harnessImportBody(request)
3230
+ };
3231
+ case "imports.status":
3232
+ return {
3233
+ path: `/v1/desktop/imports/jobs/${encodeURIComponent(request.jobId)}`,
3234
+ method: "GET"
3235
+ };
3236
+ case "imports.cancel":
3237
+ return {
3238
+ path: `/v1/desktop/imports/jobs/${encodeURIComponent(request.jobId)}`,
3239
+ method: "DELETE"
3240
+ };
3152
3241
  case "profiles.create":
3153
3242
  return {
3154
3243
  path: "/v1/desktop/profiles",
@@ -4659,27 +4748,42 @@ function resolveWindowChromeForLaunch(input) {
4659
4748
  const GLASS_COVERAGES = ["sidebars", "full"];
4660
4749
  const GLASS_MAC_MATERIALS = ["window", "menu", "popover"];
4661
4750
  const GLASS_WIN_MATERIALS = ["mica", "acrylic", "tabbed"];
4751
+ const DEFAULT_GLASS_INTENSITY = 80;
4662
4752
  const DEFAULT_GLASS_SETTINGS = {
4663
4753
  enabled: false,
4664
4754
  coverage: "full",
4665
- intensity: 40,
4755
+ intensity: DEFAULT_GLASS_INTENSITY,
4666
4756
  material: "window"
4667
4757
  };
4668
4758
  const WIN_BACKGROUND_MATERIAL_MIN_BUILD = 22621;
4669
4759
  const parseVersion = (version) => version.split(".").map((part) => Number.parseInt(part, 10)).map((n) => Number.isFinite(n) ? n : 0);
4760
+ const gpuFeatureUsable = (status2) => typeof status2 === "string" && status2.startsWith("enabled");
4761
+ function glassEnvironmentObjection(environment) {
4762
+ if (!environment) return null;
4763
+ if (environment.accelerationDisabledByLaunch || environment.hardwareAcceleration === false)
4764
+ return "hardware-acceleration-disabled";
4765
+ if (environment.gpu && (!gpuFeatureUsable(environment.gpu.compositing) || !gpuFeatureUsable(environment.gpu.rasterization)))
4766
+ return "gpu-unavailable";
4767
+ if (environment.backdropFilter === false)
4768
+ return "backdrop-filter-unavailable";
4769
+ return null;
4770
+ }
4670
4771
  function glassCapabilityFor(input) {
4671
- const none = (reason) => ({
4772
+ const none = (reason, defaultMaterial = DEFAULT_GLASS_SETTINGS.material) => ({
4672
4773
  supported: false,
4673
4774
  mechanism: "none",
4674
4775
  reason,
4675
4776
  materials: [],
4676
- defaultMaterial: DEFAULT_GLASS_SETTINGS.material,
4777
+ defaultMaterial,
4677
4778
  liquid: false,
4779
+ defaultEnabled: false,
4678
4780
  liveToggle: false
4679
4781
  });
4680
4782
  const version = parseVersion(input.osVersion);
4681
4783
  if (input.platform === "darwin") {
4682
4784
  if (input.reducedTransparency) return none("reduced-transparency");
4785
+ const objection = glassEnvironmentObjection(input.environment);
4786
+ if (objection) return none(objection);
4683
4787
  return {
4684
4788
  supported: true,
4685
4789
  mechanism: "vibrancy",
@@ -4687,26 +4791,26 @@ function glassCapabilityFor(input) {
4687
4791
  materials: GLASS_MAC_MATERIALS,
4688
4792
  defaultMaterial: "window",
4689
4793
  liquid: (version[0] ?? 0) >= 26,
4794
+ defaultEnabled: true,
4690
4795
  liveToggle: true
4691
4796
  };
4692
4797
  }
4693
4798
  if (input.platform === "win32") {
4694
4799
  if ((version[2] ?? 0) < WIN_BACKGROUND_MATERIAL_MIN_BUILD)
4695
- return none("windows-too-old");
4696
- if (input.reducedTransparency) return none("reduced-transparency");
4697
- return {
4698
- supported: true,
4699
- mechanism: "background-material",
4700
- reason: "ok",
4701
- materials: GLASS_WIN_MATERIALS,
4702
- defaultMaterial: "mica",
4703
- liquid: false,
4704
- liveToggle: false
4705
- };
4800
+ return none("windows-too-old", "mica");
4801
+ return none("windows-unverified", "mica");
4706
4802
  }
4707
- if (input.platform === "linux") return none("linux-no-native-blur");
4803
+ if (input.platform === "linux") return none("linux-compositor-dependent");
4708
4804
  return none("unsupported-platform");
4709
4805
  }
4806
+ function defaultGlassSettings(capability) {
4807
+ return {
4808
+ enabled: capability.defaultEnabled,
4809
+ coverage: "full",
4810
+ intensity: DEFAULT_GLASS_INTENSITY,
4811
+ material: capability.defaultMaterial
4812
+ };
4813
+ }
4710
4814
  const isCoverage = (value) => GLASS_COVERAGES.includes(value);
4711
4815
  const migrateCoverage = (value) => value === "chrome" ? "sidebars" : value;
4712
4816
  function sanitizeGlassSettings(raw, capability, base = DEFAULT_GLASS_SETTINGS) {
@@ -4726,6 +4830,40 @@ function sanitizeGlassSettings(raw, capability, base = DEFAULT_GLASS_SETTINGS) {
4726
4830
  material
4727
4831
  };
4728
4832
  }
4833
+ const GLASS_FILE_VERSION = 2;
4834
+ const GLASS_SETTINGS_FIELDS = [
4835
+ "enabled",
4836
+ "coverage",
4837
+ "intensity",
4838
+ "material"
4839
+ ];
4840
+ function resolveStoredGlass(raw, capability) {
4841
+ if (raw === null || typeof raw !== "object") return null;
4842
+ const record = raw;
4843
+ if (record.settings === null || typeof record.settings !== "object")
4844
+ return null;
4845
+ const defaults = defaultGlassSettings(capability);
4846
+ const stored = sanitizeGlassSettings(
4847
+ record.settings,
4848
+ capability,
4849
+ DEFAULT_GLASS_SETTINGS
4850
+ );
4851
+ const legacy = record.version !== GLASS_FILE_VERSION || !Array.isArray(record.explicit);
4852
+ const explicit = legacy ? [...GLASS_SETTINGS_FIELDS] : GLASS_SETTINGS_FIELDS.filter(
4853
+ (field) => record.explicit.includes(field)
4854
+ );
4855
+ const settings = { ...defaults };
4856
+ for (const field of explicit)
4857
+ settings[field] = stored[field];
4858
+ return {
4859
+ settings,
4860
+ explicit,
4861
+ scheme: record.scheme === "light" ? "light" : "dark"
4862
+ };
4863
+ }
4864
+ function glassGateLine(capability, settings, platform) {
4865
+ return `[window-glass] launch gate platform=${platform} supported=${capability.supported} default=${capability.defaultEnabled} enabled=${settings.enabled} intensity=${settings.intensity} reason=${capability.reason} mechanism=${capability.mechanism}`;
4866
+ }
4729
4867
  const glassIsActive = (settings, capability) => settings.enabled && capability.supported;
4730
4868
  const GLASS_TRANSPARENT = "#00000000";
4731
4869
  function glassWindowOptions(settings, capability, platform) {
@@ -4763,6 +4901,7 @@ const WINDOW_GLASS_GET_CHANNEL = "window-glass:get";
4763
4901
  const WINDOW_GLASS_SET_CHANNEL = "window-glass:set";
4764
4902
  const WINDOW_GLASS_SCHEME_CHANNEL = "window-glass:scheme";
4765
4903
  const WINDOW_GLASS_STATE_CHANNEL = "window-glass:state";
4904
+ const WINDOW_GLASS_RENDERER_CHANNEL = "window-glass:renderer";
4766
4905
  const WINDOW_GLASS_FILE = "window-glass.json";
4767
4906
  function windowGlassArgument(state2, scheme) {
4768
4907
  if (!state2.active) return `${WINDOW_GLASS_ARG}=off`;
@@ -8012,6 +8151,19 @@ const mediaRequestSchema = zod.z.discriminatedUnion("op", [
8012
8151
  childId: zod.z.string().regex(/^[a-f0-9]{12}$/),
8013
8152
  digest: zod.z.string().regex(/^[a-f0-9]{32}$/)
8014
8153
  }).strict(),
8154
+ // A generated sound's PRE-RENDER peaks sidecar (`<digest>.peaks.json`, 240
8155
+ // bars). It rides this relay because it is content IN the session store,
8156
+ // served beside the audio it describes by the same attachment route family -
8157
+ // the renderer asks the store one question ("what did this session keep under
8158
+ // this digest?") rather than learning the store's layout - and because the
8159
+ // route needs the bearer, which this relay attaches and the JSON transport's
8160
+ // callers would have to re-plumb. Same shape constraints as the attachment op
8161
+ // above for the same reason: main builds the path.
8162
+ zod.z.object({
8163
+ op: zod.z.literal("sessions.attachment.waveform"),
8164
+ sessionId: zod.z.string().regex(/^[a-f0-9]{12}$/),
8165
+ digest: zod.z.string().regex(/^[a-f0-9]{32}$/)
8166
+ }).strict(),
8015
8167
  // One supplement component's assembled HTML document (turn-supplements
8016
8168
  // memo §2.7), as `{ html }` JSON. Both identifiers are shape-constrained
8017
8169
  // here for the same reason the attachment ops' are: the path is built in
@@ -8046,6 +8198,11 @@ function endpoint(request) {
8046
8198
  path: `/v1/desktop/sessions/${request.sessionId}/children/${request.childId}/attachments/${request.digest}`,
8047
8199
  method: "GET"
8048
8200
  };
8201
+ case "sessions.attachment.waveform":
8202
+ return {
8203
+ path: `/v1/desktop/sessions/${request.sessionId}/attachments/${request.digest}/waveform`,
8204
+ method: "GET"
8205
+ };
8049
8206
  case "sessions.supplementDocument":
8050
8207
  return {
8051
8208
  path: `/v1/desktop/sessions/${request.sessionId}/supplements/${request.digest}/document`,
@@ -8090,7 +8247,7 @@ async function requestDesktopMediaOutcome(input, bytes, backendUrl, token) {
8090
8247
  const target2 = endpoint(request);
8091
8248
  let body;
8092
8249
  let contentType;
8093
- if (request.op === "agent.export" || request.op === "sessions.attachment" || request.op === "subagents.attachment" || request.op === "sessions.supplementDocument") {
8250
+ if (request.op === "agent.export" || request.op === "sessions.attachment" || request.op === "subagents.attachment" || request.op === "sessions.attachment.waveform" || request.op === "sessions.supplementDocument") {
8094
8251
  body = void 0;
8095
8252
  } else if (request.op === "speech.create" || request.op === "speech.agent") {
8096
8253
  body = JSON.stringify(request.request);
@@ -32318,6 +32475,8 @@ const enUiBrowser = {
32318
32475
  "ui.browser.browser_tab_strip.close": 'Close "',
32319
32476
  "ui.browser.browser_tab_strip.copy_url": "Copy URL",
32320
32477
  "ui.browser.browser_tab_strip.close_tab": "Close tab",
32478
+ "ui.browser.browser_tab_strip.mute_tab": "Mute tab",
32479
+ "ui.browser.browser_tab_strip.unmute_tab": "Unmute tab",
32321
32480
  "ui.browser.browser_tab_strip.close_tab_shortcut": "Close tab ({shortcut})",
32322
32481
  "ui.browser.browser_tab_strip.close_named_tab": "Close {title}",
32323
32482
  "ui.browser.browser_tab_strip.all_tabs_unshown": "All tabs — {count, plural, other {{count, number, ::group-off} not shown}}",
@@ -32330,6 +32489,7 @@ const enUiBrowser = {
32330
32489
  "ui.browser.browser_url_bar.forward_shortcut": "Forward ({shortcut})",
32331
32490
  "ui.browser.browser_url_bar.find_in_page": "Find in page",
32332
32491
  "ui.browser.browser_url_bar.find_in_page_shortcut": "Find in page ({shortcut})",
32492
+ "ui.browser.browser_url_bar.print_page": "Print page",
32333
32493
  "ui.browser.browser_url_bar.forward": "Forward",
32334
32494
  "ui.browser.browser_url_bar.reset_page_zoom_shortcut": "Reset page zoom ({shortcut})",
32335
32495
  "ui.browser.browser_url_bar.page_zoom_reset": "Page zoom {zoom, number, ::group-off}%, reset to 100%",
@@ -32396,7 +32556,7 @@ const enUiCanvas = {
32396
32556
  "ui.canvas.files.documents": "Documents",
32397
32557
  "ui.canvas.files.spreadsheets": "Spreadsheets",
32398
32558
  "ui.canvas.files.other": "Other",
32399
- "ui.canvas.files.audio": "Audio",
32559
+ "ui.canvas.files.audio": "Sounds",
32400
32560
  "ui.canvas.files.code": "Code",
32401
32561
  "ui.canvas.files.presentations": "Presentations",
32402
32562
  "ui.canvas.files.archives": "Archives",
@@ -34320,12 +34480,12 @@ const enUiSettingsAppearance = {
34320
34480
  };
34321
34481
  const enUiSettingsAppearanceWindowGlass = {
34322
34482
  "ui.settings.appearance.window_glass.label": "Translucent window",
34323
- "ui.settings.appearance.window_glass.description": "Lets your desktop show through the app window, blurred by the system. Off by default. Text stays readable on every theme: the slider stops before any theme's text gets hard to read.",
34483
+ "ui.settings.appearance.window_glass.description": "Lets your desktop show through the app window, blurred by the system. On by default on macOS where the system supports it, and off elsewhere. Text stays readable on every theme: the slider stops before any theme's text gets hard to read.",
34324
34484
  "ui.settings.appearance.window_glass.restart_needed": "Restart Local Operator to apply this change. The window keeps its current look until then.",
34325
- "ui.settings.appearance.window_glass.unsupported_reduced_transparency": "Translucent window is unavailable because Reduce transparency is on in your system accessibility settings. Turn it off there and this control will appear.",
34485
+ "ui.settings.appearance.window_glass.unsupported_reduced_transparency": "Translucent window is unavailable because Reduce transparency is on in your system accessibility settings. Turn it off there to enable this control.",
34326
34486
  "ui.settings.appearance.window_glass.unsupported_windows_too_old": "Translucent window needs Windows 11 version 22H2 or later.",
34327
- "ui.settings.appearance.window_glass.unsupported_linux": "Translucent window is not available on Linux: there is no system blur an app can ask for, and a see-through window without one would show the desktop's text through the app's.",
34328
- "ui.settings.appearance.window_glass.unsupported_other": "Translucent window is not available on this system.",
34487
+ "ui.settings.appearance.window_glass.unsupported_linux": "Translucent window is not available on Linux: there is no native window blur an app can request. Desktop translucency there depends on the compositor (X11 or Wayland) and can fall back to software blur, which would show the desktop's text through the app's.",
34488
+ "ui.settings.appearance.window_glass.unsupported_other": "Translucent window is not available on this platform: only macOS is verified to composite a blurred window, and the Windows 11 material is not verified on a real machine yet.",
34329
34489
  "ui.settings.appearance.window_glass.coverage_label": "Glass applies to",
34330
34490
  "ui.settings.appearance.window_glass.coverage_description": "Which parts of the window go translucent. Sidebars only leaves the conversation and pages solid. Full app also covers them and the side panels; web pages, documents and images shown inside stay solid.",
34331
34491
  "ui.settings.appearance.window_glass.coverage_sidebars": "Sidebars only",
@@ -34342,7 +34502,11 @@ const enUiSettingsAppearanceWindowGlass = {
34342
34502
  "ui.settings.appearance.window_glass.material_tabbed": "Tabbed",
34343
34503
  "ui.settings.appearance.window_glass.intensity_start": "Frosted",
34344
34504
  "ui.settings.appearance.window_glass.intensity_end": "See-through",
34345
- "ui.settings.appearance.window_glass.intensity_value": "{value}% toward see-through"
34505
+ "ui.settings.appearance.window_glass.intensity_value": "{value}% toward see-through",
34506
+ "ui.settings.appearance.window_glass.unsupported_windows_unverified": "Translucent window is not offered on Windows yet: Mica and Acrylic are implemented but have not been tested on a real Windows machine, so the control stays off until that testing lands.",
34507
+ "ui.settings.appearance.window_glass.unsupported_gpu": "Translucent window is unavailable because the GPU cannot composite it right now: compositing or rasterisation is disabled or running in software. A driver update or leaving a remote session may bring the control back without a restart.",
34508
+ "ui.settings.appearance.window_glass.unsupported_acceleration": "Translucent window is unavailable because hardware acceleration is off for this launch (--disable-gpu or a crash fallback). Relaunch with acceleration on to use it.",
34509
+ "ui.settings.appearance.window_glass.unsupported_backdrop_filter": "Translucent window is unavailable because this renderer cannot draw a backdrop blur."
34346
34510
  };
34347
34511
  const enUiSettingsConfiguration = {
34348
34512
  "ui.settings.configuration.backend_setting_row.read_only": "Read-only",
@@ -34438,6 +34602,82 @@ const enUiSettingsConfiguration = {
34438
34602
  "ui.settings.configuration.settings_filter_bar.advanced_shown": "{count, plural, one {{count, number, ::group-off} advanced setting shown — Expand all to see it} other {{count, number, ::group-off} advanced settings shown — Expand all to see them}}",
34439
34603
  "ui.settings.configuration.settings_filter_bar.core_and_advanced": "{coreCount, plural, one {{coreCount, number, ::group-off} core setting} other {{coreCount, number, ::group-off} core settings}}, {advancedCount, plural, one {{advancedCount, number, ::group-off} advanced} other {{advancedCount, number, ::group-off} advanced}}"
34440
34604
  };
34605
+ const enUiSettingsImports = {
34606
+ "ui.settings.imports.title": "Import conversations",
34607
+ "ui.settings.imports.description": "Copy conversations and curated memory from Claude Code, Codex or omp into Local Operator. The source stores are read-only.",
34608
+ "ui.settings.imports.source_heading": "Sources",
34609
+ "ui.settings.imports.source_not_found": "Not found",
34610
+ "ui.settings.imports.source_sessions": "{count, plural, one {# session} other {# sessions}}",
34611
+ "ui.settings.imports.loading_sources": "Checking for source stores...",
34612
+ "ui.settings.imports.loading_sessions": "Reading {source} sessions...",
34613
+ "ui.settings.imports.loading_preview": "Building the preview...",
34614
+ "ui.settings.imports.empty_sources": "No Claude Code, Codex or omp stores were detected on this machine.",
34615
+ "ui.settings.imports.empty_sessions": "No sessions found for {source}.",
34616
+ "ui.settings.imports.empty_filter": "No sessions match this search.",
34617
+ "ui.settings.imports.error_sources": "Could not read the source stores. {reason}",
34618
+ "ui.settings.imports.error_sessions": "Could not read {source} sessions. {reason}",
34619
+ "ui.settings.imports.error_preview": "Could not build the preview. {reason}",
34620
+ "ui.settings.imports.error_start": "Could not start the import. {reason}",
34621
+ "ui.settings.imports.error_job": "Could not read import progress. {reason}",
34622
+ "ui.settings.imports.error_cancel": "Could not cancel the import. {reason}",
34623
+ "ui.settings.imports.retry": "Retry",
34624
+ "ui.settings.imports.refresh": "Refresh",
34625
+ "ui.settings.imports.search_placeholder": "Search conversations",
34626
+ "ui.settings.imports.select_all": "Select all",
34627
+ "ui.settings.imports.clear_all": "Clear selection",
34628
+ "ui.settings.imports.selected": "{count, plural, one {# selected} other {# selected}}",
34629
+ "ui.settings.imports.selection_limit": "Only {max, number} sessions can be selected at once. Select fewer sessions or import them in batches.",
34630
+ "ui.settings.imports.session_meta": "{messages, plural, one {# message} other {# messages}} · {compactions, plural, one {# compaction} other {# compactions}}",
34631
+ "ui.settings.imports.session_updated": "Updated {date}",
34632
+ "ui.settings.imports.session_index_only": "Index only — no conversation body",
34633
+ "ui.settings.imports.preview_heading": "Preview",
34634
+ "ui.settings.imports.preview_summary": "{sessions, plural, one {# session} other {# sessions}} · {messages, plural, one {# message} other {# messages}} · {compactions, plural, one {# compaction} other {# compactions}}",
34635
+ "ui.settings.imports.preview_memory": "{records, plural, one {# memory record} other {# memory records}} · ~{tokens} tokens",
34636
+ "ui.settings.imports.preview_confirm": "Review the selection above, then confirm the copy.",
34637
+ "ui.settings.imports.preview_required": "Select at least one session and build a preview first.",
34638
+ "ui.settings.imports.confirm_label": "I understand these conversations will be copied into Local Operator.",
34639
+ "ui.settings.imports.import_button": "{count, plural, one {Import # session} other {Import # sessions}}",
34640
+ "ui.settings.imports.importing": "Importing",
34641
+ "ui.settings.imports.progress_heading": "Importing conversations",
34642
+ "ui.settings.imports.progress_job": "Job {job} · {state}",
34643
+ "ui.settings.imports.progress_pending": "{count, plural, one {# session pending} other {# sessions pending}}",
34644
+ "ui.settings.imports.cancel": "Cancel import",
34645
+ "ui.settings.imports.cancelling": "Cancelling...",
34646
+ "ui.settings.imports.result_heading": "Import result",
34647
+ "ui.settings.imports.result_summary": "{state} · {sessions, plural, one {# session} other {# sessions}} · {messages, plural, one {# message} other {# messages}}",
34648
+ "ui.settings.imports.result_summary_cancelled": "{state} · {sessions, number} of {total, plural, one {# session} other {# sessions}} · {messages, plural, one {# message} other {# messages}}",
34649
+ "ui.settings.imports.result_target": "New session {target}",
34650
+ "ui.settings.imports.outcome_imported": "imported",
34651
+ "ui.settings.imports.outcome_resumed": "resumed",
34652
+ "ui.settings.imports.outcome_skipped": "already imported",
34653
+ "ui.settings.imports.outcome_metadata": "index only — nothing to import",
34654
+ "ui.settings.imports.outcome_failed": "failed",
34655
+ "ui.settings.imports.outcome_cancelled": "cancelled",
34656
+ "ui.settings.imports.warnings_heading": "{count, plural, one {# warning} other {# warnings}}",
34657
+ "ui.settings.imports.review_instructions": "Instructions saved for review",
34658
+ "ui.settings.imports.review_memory": "Unattached memory saved for review",
34659
+ "ui.settings.imports.copy_path": "Copy path",
34660
+ "ui.settings.imports.copied": "Copied",
34661
+ "ui.settings.imports.rerun_safe": "Re-running is safe: unchanged sources are skipped, grown sources resume, rewritten sources get a new copy.",
34662
+ "ui.settings.imports.import_another": "Import more conversations",
34663
+ "ui.settings.imports.metadata_only_explain": "The source index has an entry but no conversation body; nothing can be imported. The harness may have removed the rollout.",
34664
+ "ui.settings.imports.state_queued": "queued",
34665
+ "ui.settings.imports.state_running": "running",
34666
+ "ui.settings.imports.state_done": "done",
34667
+ "ui.settings.imports.state_failed": "failed",
34668
+ "ui.settings.imports.state_cancelled": "cancelled",
34669
+ "ui.settings.imports.phase_queued": "queued",
34670
+ "ui.settings.imports.phase_selecting": "selecting",
34671
+ "ui.settings.imports.phase_importing": "importing",
34672
+ "ui.settings.imports.phase_memory": "reading memory",
34673
+ "ui.settings.imports.phase_done": "finishing",
34674
+ "ui.settings.imports.phase_cancelled": "cancelled",
34675
+ "ui.settings.imports.session_pending": "Pending",
34676
+ "ui.settings.imports.session_importing": "Importing",
34677
+ "ui.settings.imports.availability_full": "full",
34678
+ "ui.settings.imports.availability_metadata_only": "index only",
34679
+ "ui.settings.imports.availability_unsupported": "unsupported"
34680
+ };
34441
34681
  const enUiSettingsIntegrations = {
34442
34682
  "ui.settings.integrations.add_integration_form.add_integration": "Add integration",
34443
34683
  "ui.settings.integrations.add_integration_form.name": "Name",
@@ -36318,6 +36558,7 @@ const bundledEnglish = {
36318
36558
  ...enUiSettingsAppearance,
36319
36559
  ...enUiSettingsAppearanceWindowGlass,
36320
36560
  ...enUiSettingsConfiguration,
36561
+ ...enUiSettingsImports,
36321
36562
  ...enUiSettingsIntegrations,
36322
36563
  ...enUiSettings,
36323
36564
  ...enUiSettingsModelDefaultEffort,
@@ -39040,6 +39281,7 @@ const PAGE_ZOOM_STEPS = [
39040
39281
  4,
39041
39282
  5
39042
39283
  ];
39284
+ const PRINT_CANCELLED = /cancel/i;
39043
39285
  class PageControls {
39044
39286
  constructor(changed, resultTimeoutMs = 5e3) {
39045
39287
  this.changed = changed;
@@ -39174,6 +39416,36 @@ class PageControls {
39174
39416
  const factor = command === "reset" ? 1 : command === "in" ? PAGE_ZOOM_STEPS.find((step) => step > current + 1e-3) ?? 5 : [...PAGE_ZOOM_STEPS].reverse().find((step) => step < current - 1e-3) ?? 0.25;
39175
39417
  active.view.webContents.setZoomFactor?.(factor);
39176
39418
  }
39419
+ /** Print the presented page (LOH-13 slice C).
39420
+ *
39421
+ * HUMAN-ONLY, like everything in this class: it travels the sender-
39422
+ * authorized page-control IPC and the tab's serial lane, never the agent
39423
+ * protocol — an agent that needs a page as a document already has the
39424
+ * PDF/screenshot actions, and silently sending a page to a physical
39425
+ * printer is not a capability a tool call should hold.
39426
+ *
39427
+ * `silent: false` is the load-bearing option: Electron's silent mode
39428
+ * prints to the default printer with default settings and NO dialog,
39429
+ * which for a human press would be a side effect they never confirmed.
39430
+ * The dialog is the consent. A failure the dialog itself reports
39431
+ * (cancelled is the person changing their mind, not a failure) only
39432
+ * surfaces through `notice` when Electron names a real reason. */
39433
+ print(active) {
39434
+ if (!active.presented) return;
39435
+ const wc = active.view.webContents;
39436
+ if (wc.isDestroyed() || !wc.print) {
39437
+ this.notice = "Printing is not available for this page";
39438
+ this.noticeTabId = active.tabId;
39439
+ return;
39440
+ }
39441
+ this.notice = null;
39442
+ wc.print({ silent: false, printBackground: false }, (success, reason) => {
39443
+ if (success || !reason || PRINT_CANCELLED.test(reason)) return;
39444
+ this.notice = `Printing failed: ${reason}`;
39445
+ this.noticeTabId = active.tabId;
39446
+ this.changed();
39447
+ });
39448
+ }
39177
39449
  /** Call BEFORE a capability transfer. Isolated per-WebContents zoom means
39178
39450
  * resetting this tab cannot alter another tab at the same origin. */
39179
39451
  prepareAgent(record) {
@@ -39183,6 +39455,71 @@ class PageControls {
39183
39455
  if (changed) this.changed();
39184
39456
  }
39185
39457
  }
39458
+ class TabAudioTracker {
39459
+ constructor(changed) {
39460
+ this.changed = changed;
39461
+ }
39462
+ states = /* @__PURE__ */ new Map();
39463
+ listeners = /* @__PURE__ */ new Map();
39464
+ /** Start tracking one tab (idempotent) and return its current state. The
39465
+ * muted flag is re-read live on every call: `setAudioMuted` is also
39466
+ * reachable from the page's own media controls in principle, and a stale
39467
+ * cached flag would render a button that claims the opposite of the
39468
+ * page's actual state. */
39469
+ stateFor(tab) {
39470
+ this.track(tab);
39471
+ const state2 = this.states.get(tab.tabId) ?? {
39472
+ audible: false,
39473
+ muted: false
39474
+ };
39475
+ const wc = tab.view.webContents;
39476
+ if (!wc.isDestroyed()) state2.muted = wc.isAudioMuted?.() ?? state2.muted;
39477
+ return { ...state2 };
39478
+ }
39479
+ /** Mute or unmute one tab. Returns the resulting state, or null when the
39480
+ * webContents is already gone — the caller (a human press) then reports
39481
+ * the tab as closed rather than claiming a mute that never landed. */
39482
+ setMuted(tab, muted) {
39483
+ const wc = tab.view.webContents;
39484
+ if (wc.isDestroyed()) return null;
39485
+ this.track(tab);
39486
+ wc.setAudioMuted?.(muted);
39487
+ const state2 = this.stateFor(tab);
39488
+ state2.muted = wc.isAudioMuted?.() ?? muted;
39489
+ this.states.set(tab.tabId, state2);
39490
+ this.changed();
39491
+ return { ...state2 };
39492
+ }
39493
+ /** Drop tracking for every tab not in `liveTabIds`. Called from the
39494
+ * projection, which is the one place main already enumerates the live
39495
+ * set on every change. */
39496
+ prune(liveTabIds) {
39497
+ for (const [tabId, entry] of this.listeners) {
39498
+ if (liveTabIds.has(tabId)) continue;
39499
+ this.listeners.delete(tabId);
39500
+ this.states.delete(tabId);
39501
+ if (!entry.wc.isDestroyed())
39502
+ entry.wc.removeListener("audio-state-changed", entry.listener);
39503
+ }
39504
+ }
39505
+ track(tab) {
39506
+ if (this.listeners.has(tab.tabId)) return;
39507
+ const wc = tab.view.webContents;
39508
+ this.states.set(tab.tabId, {
39509
+ audible: wc.isDestroyed() ? false : wc.isCurrentlyAudible?.() ?? false,
39510
+ muted: wc.isDestroyed() ? false : wc.isAudioMuted?.() ?? false
39511
+ });
39512
+ const listener = (event) => {
39513
+ const current = this.states.get(tab.tabId);
39514
+ if (!current) return;
39515
+ current.audible = event?.audible ?? false;
39516
+ this.states.set(tab.tabId, current);
39517
+ this.changed();
39518
+ };
39519
+ this.listeners.set(tab.tabId, { wc, listener });
39520
+ if (!wc.isDestroyed()) wc.on("audio-state-changed", listener);
39521
+ }
39522
+ }
39186
39523
  const DUPLICATABLE_WEB_URL = /^https?:\/\//i;
39187
39524
  const DOCUMENT_SCOPED = /* @__PURE__ */ new Set([
39188
39525
  "read",
@@ -39272,6 +39609,23 @@ class BrowserHost {
39272
39609
  chromeTabOrder = new ChromeTabOrder();
39273
39610
  registry;
39274
39611
  pageControls;
39612
+ /** Per-tab audio for the strip's mute control (LOH-13 slice C). Main owns
39613
+ * it for the reason `tabLoading` is read here: a background tab's sound
39614
+ * is legible only through this projection.
39615
+ *
39616
+ * ASSIGNED IN THE CONSTRUCTOR, NOT INITIALISED HERE. The shipping main
39617
+ * bundle is compiled to V8 bytecode by electron-vite, and its `vite:bytecode`
39618
+ * transform refuses an arrow function inside a class PROPERTY: "Unable to
39619
+ * transform arrow inside class property". This initialiser was that arrow,
39620
+ * and it failed the v0.35.0 platform builds identically on every OS, so no
39621
+ * update assets shipped. A constructor assignment is inside the transform's
39622
+ * supported shape, keeps the field `readonly`, and preserves the callback's
39623
+ * laziness (the tracker only stores it). Do not move it back to a field
39624
+ * initialiser. Keep arrows out of every non-static class-field initialiser,
39625
+ * not only the direct form: a callback passed to a constructor
39626
+ * (`new X(() => ...)`) or sitting inside an object/array literal value is
39627
+ * refused by the same rule (2026-10-18). */
39628
+ tabAudio;
39275
39629
  cdp;
39276
39630
  approvals;
39277
39631
  ownership;
@@ -39303,6 +39657,7 @@ class BrowserHost {
39303
39657
  this.downloads = options.downloads;
39304
39658
  this.log = options.log;
39305
39659
  this.onChanged = options.onChanged;
39660
+ this.tabAudio = new TabAudioTracker(() => this.onChanged());
39306
39661
  this.facts = options.facts;
39307
39662
  this.forgetSiteData = options.forgetSiteData;
39308
39663
  }
@@ -39581,6 +39936,9 @@ class BrowserHost {
39581
39936
  const activeRecord = this.registry.activeTab;
39582
39937
  const active = activeRecord && !activeRecord.view.webContents.isDestroyed() ? activeRecord : null;
39583
39938
  const navigation = active?.view.webContents.navigationHistory;
39939
+ this.tabAudio.prune(
39940
+ new Set(this.registry.snapshot().map((entry) => entry.tabId))
39941
+ );
39584
39942
  return {
39585
39943
  pageControls: this.pageControls.state(active),
39586
39944
  tabs: this.chromeTabOrder.project(this.registry.snapshot()).map((entry, displayOrder) => ({
@@ -39616,7 +39974,20 @@ class BrowserHost {
39616
39974
  // was the whole of what the user saw of an agent's work. Read from the
39617
39975
  // record rather than the snapshot: `snapshot()` is the diagnostic
39618
39976
  // projection shared with `status`/`tabs`, and this field is chrome.
39619
- loading: this.tabLoading(entry.tabId)
39977
+ loading: this.tabLoading(entry.tabId),
39978
+ // PER TAB, projected with `loading` and for the same reason: a
39979
+ // background tab making sound has no other surface. `audible`
39980
+ // gates the mute control's visibility; `muted` is its state.
39981
+ // Read from the record (guarded like `tabLoading`) because the
39982
+ // snapshot carries no webContents.
39983
+ audible: (() => {
39984
+ const record = this.registry.get(entry.tabId);
39985
+ return record && !record.view.webContents.isDestroyed() ? this.tabAudio.stateFor(record).audible : false;
39986
+ })(),
39987
+ muted: (() => {
39988
+ const record = this.registry.get(entry.tabId);
39989
+ return record && !record.view.webContents.isDestroyed() ? this.tabAudio.stateFor(record).muted : false;
39990
+ })()
39620
39991
  })),
39621
39992
  activeTabId: activeRecord?.tabId ?? null,
39622
39993
  // The download surface's facts (§16.4): what is in flight, where it went and
@@ -39718,8 +40089,23 @@ class BrowserHost {
39718
40089
  error: { code: "tab_closed", message: t("ui.browser.chrome.tab_gone") }
39719
40090
  };
39720
40091
  let url = "";
39721
- let destinationId = action === "reload" ? tabId : null;
40092
+ let destinationId = action === "duplicate" ? null : tabId;
39722
40093
  try {
40094
+ if (action === "mute" || action === "unmute") {
40095
+ const result = this.tabAudio.setMuted(record, action === "mute");
40096
+ if (!result)
40097
+ return {
40098
+ status: "failed",
40099
+ action,
40100
+ tabId: null,
40101
+ error: {
40102
+ code: "tab_closed",
40103
+ message: t("ui.browser.chrome.tab_gone")
40104
+ }
40105
+ };
40106
+ this.onChanged();
40107
+ return { status: "completed", action, tabId };
40108
+ }
39723
40109
  url = record.view.webContents.getURL();
39724
40110
  if (action === "duplicate" && url !== "about:blank" && !DUPLICATABLE_WEB_URL.test(url))
39725
40111
  return {
@@ -39788,6 +40174,7 @@ class BrowserHost {
39788
40174
  if (!active || active.tabId !== tabId || !active.presented || active.view.webContents.isDestroyed())
39789
40175
  throw new Error("The browser tab is no longer presented.");
39790
40176
  if (action === "open") this.pageControls.open(active);
40177
+ else if (action === "print") this.pageControls.print(active);
39791
40178
  else if (action === "close") this.pageControls.clear();
39792
40179
  else if (action === "search")
39793
40180
  this.pageControls.search(active, query, forward, next);
@@ -40527,7 +40914,8 @@ class DownloadArmer {
40527
40914
  deadlineTimer: null,
40528
40915
  capTimer: null,
40529
40916
  finish: null,
40530
- settled: false
40917
+ settled: false,
40918
+ human: false
40531
40919
  };
40532
40920
  this.captures.set(tabId, capture);
40533
40921
  const deadline2 = setTimeout(() => this.settle(capture), timeoutMs);
@@ -40582,9 +40970,15 @@ class DownloadArmer {
40582
40970
  */
40583
40971
  decide(item, webContentsId) {
40584
40972
  const tabId = this.options.tabForWebContents(webContentsId);
40585
- const capture = tabId === null ? void 0 : this.captures.get(tabId);
40973
+ let capture = tabId === null ? void 0 : this.captures.get(tabId);
40586
40974
  if (!capture || capture.settled) {
40587
- return { cancel: true, reason: "no download call is armed on this tab" };
40975
+ capture = (tabId !== null ? this.humanCapture(tabId) : null) ?? void 0;
40976
+ if (!capture) {
40977
+ return {
40978
+ cancel: true,
40979
+ reason: "no download call is armed on this tab"
40980
+ };
40981
+ }
40588
40982
  }
40589
40983
  const raw = item.getFilename();
40590
40984
  const clean = safeName(raw);
@@ -40706,6 +41100,58 @@ class DownloadArmer {
40706
41100
  const dirs = this.notes.filter((note) => note.dir !== "").map((n) => n.dir);
40707
41101
  return [...this.captures.values()].at(-1)?.dir ?? dirs.at(-1) ?? null;
40708
41102
  }
41103
+ /** Build (or reuse) the human capture for one user-owned tab.
41104
+ *
41105
+ * Returns null — leaving the caller to cancel — when this host cannot
41106
+ * honestly call the download human-initiated: the tab is agent-owned or
41107
+ * handed to an agent (`ownerKindOf` reads the registry's CURRENT owner,
41108
+ * which hand-over flips), the armer was built without a human directory,
41109
+ * or that directory cannot be made. A settled human capture is replaced
41110
+ * rather than reused, so its file count bounds one burst (up to the
41111
+ * quiet window after the last write) and never the session.
41112
+ *
41113
+ * The directory is NOT chmodded, unlike `arm`: it is the person's own
41114
+ * downloads folder, which predates this app and whose mode is theirs.
41115
+ * The runtime-cap sampler IS started, for the reason `CAP_SAMPLE_MS`
41116
+ * states: an unknown-size human download has the same disk to fill as
41117
+ * an agent's. */
41118
+ humanCapture(tabId) {
41119
+ if (this.ownerKindOf(tabId) !== "user") return null;
41120
+ const dir = this.options.humanDownloadDir?.() ?? null;
41121
+ if (!dir) return null;
41122
+ try {
41123
+ fs.mkdirSync(dir, { recursive: true });
41124
+ } catch {
41125
+ return null;
41126
+ }
41127
+ const capture = {
41128
+ tabId,
41129
+ dir,
41130
+ timeoutMs: 0,
41131
+ startedAt: (this.options.now ?? Date.now)(),
41132
+ files: [],
41133
+ refusals: [],
41134
+ pending: 0,
41135
+ started: false,
41136
+ live: [],
41137
+ handedOut: /* @__PURE__ */ new Set(),
41138
+ active: null,
41139
+ lastProgressAt: 0,
41140
+ quietTimer: null,
41141
+ deadlineTimer: null,
41142
+ capTimer: null,
41143
+ finish: null,
41144
+ settled: false,
41145
+ human: true
41146
+ };
41147
+ capture.capTimer = setInterval(
41148
+ () => this.sampleCaps(capture),
41149
+ CAP_SAMPLE_MS
41150
+ );
41151
+ capture.capTimer.unref?.();
41152
+ this.captures.set(tabId, capture);
41153
+ return capture;
41154
+ }
40709
41155
  /** One upload's own line (review round 1, U3).
40710
41156
  *
40711
41157
  * WHY AN UPLOAD HAS A NOTE AT ALL, when §16.4 deliberately has no upload
@@ -40777,7 +41223,7 @@ class DownloadArmer {
40777
41223
  if (entry.weCancelled) {
40778
41224
  if (state2 !== "completed") discardPartial(savePath, this.options.log);
40779
41225
  } else if (state2 === "completed") {
40780
- restrictMode(savePath, this.options.log);
41226
+ if (!capture.human) restrictMode(savePath, this.options.log);
40781
41227
  capture.files.push(fileFact(item, savePath));
40782
41228
  this.note(capture, name, "saved", 1, "", null);
40783
41229
  } else {
@@ -40933,6 +41379,9 @@ class DownloadArmer {
40933
41379
  capture.settled = true;
40934
41380
  capture.active = null;
40935
41381
  this.clearTimers(capture);
41382
+ if (this.captures.get(capture.tabId) === capture) {
41383
+ this.captures.delete(capture.tabId);
41384
+ }
40936
41385
  capture.finish?.();
40937
41386
  this.options.onActivity?.();
40938
41387
  }
@@ -40963,6 +41412,7 @@ class DownloadArmer {
40963
41412
  capture.refusals.push(reason);
40964
41413
  this.note(capture, clean, "refused", 1, reason, refusal);
40965
41414
  this.options.log(`[browser] ${reason}`);
41415
+ this.options.onActivity?.();
40966
41416
  return { cancel: true, reason };
40967
41417
  }
40968
41418
  note(capture, name, outcome, count, reason, refusal) {
@@ -41215,7 +41665,7 @@ function registerBrowserIpc(options) {
41215
41665
  "browser-tab-action",
41216
41666
  (event, tabId, action) => {
41217
41667
  const host = authorize(event);
41218
- if (action !== "duplicate" && action !== "reload")
41668
+ if (action !== "duplicate" && action !== "reload" && action !== "mute" && action !== "unmute")
41219
41669
  throw new Error(t("ui.browser.chrome.invalid_action"));
41220
41670
  return host.actOnChromeTab(numberOrThrow(tabId, "tabId"), action);
41221
41671
  }
@@ -41243,7 +41693,9 @@ function registerBrowserIpc(options) {
41243
41693
  "browser-page-control",
41244
41694
  (event, tabId, action, query, forward, next) => {
41245
41695
  const host = authorize(event);
41246
- if (typeof action !== "string" || !["open", "close", "search", "in", "out", "reset"].includes(action))
41696
+ if (typeof action !== "string" || !["open", "close", "search", "print", "in", "out", "reset"].includes(
41697
+ action
41698
+ ))
41247
41699
  throw new Error("Invalid page control action.");
41248
41700
  if (typeof query !== "string" || query.length > 4096 || typeof forward !== "boolean" || typeof next !== "boolean")
41249
41701
  throw new Error("Invalid page search input.");
@@ -43623,6 +44075,17 @@ async function initializeBrowserHost(options, bindings) {
43623
44075
  // exists is a marker pointing at nothing. `null` when the lookup cannot answer
43624
44076
  // (a tab already gone), which the row renders the old way rather than guessing.
43625
44077
  ownerKindFor: (tabId) => registryForDownloads?.get(tabId)?.owner ?? null,
44078
+ // The human download destination (LOH-13 slice C): the person's
44079
+ // own downloads folder, read from the app at decision time. Main
44080
+ // composes it here for the same reason the reveal channel takes
44081
+ // no path: no renderer or page string ever becomes a directory.
44082
+ humanDownloadDir: () => electron.app.getPath("downloads"),
44083
+ // NO `humanDeadlineMs` WIRING (review round 2, MAJOR): the wall-clock
44084
+ // budget this used to pass settled — and cancelled, discarding the
44085
+ // partial — a legitimate in-flight human download at 120 s. A human
44086
+ // capture now ends only on a guard, the quiet window, or the tab.
44087
+ // `COMMAND_TIMEOUTS_S.download` remains the AGENT call's own budget,
44088
+ // passed by its caller and unchanged.
43626
44089
  log: log2,
43627
44090
  onActivity: () => {
43628
44091
  if (options.window.isDestroyed()) return;
@@ -45640,7 +46103,13 @@ function miniGlassPlan(input) {
45640
46103
  function miniDocumentState(state2, launchedGlassOn) {
45641
46104
  return {
45642
46105
  ...state2,
45643
- restartRequired: !state2.capability.liveToggle && state2.active !== launchedGlassOn
46106
+ /*
46107
+ * Same rule as the main window's `stateFor`: a restart is only pending for a
46108
+ * SUPPORTED change that cannot be applied live. Without the `supported` term a
46109
+ * capability drop (`mechanism: "none"`, also `liveToggle: false`) reads as a
46110
+ * restart-pending change in the popout as well.
46111
+ */
46112
+ restartRequired: state2.capability.supported && !state2.capability.liveToggle && state2.active !== launchedGlassOn
45644
46113
  };
45645
46114
  }
45646
46115
  function createMiniView(options) {
@@ -53901,21 +54370,46 @@ function attachWindowChrome(options) {
53901
54370
  function windowGlassFilePath(userDataDir) {
53902
54371
  return path$1.join(userDataDir, WINDOW_GLASS_FILE);
53903
54372
  }
54373
+ let gpuInfoReported = false;
54374
+ electron.app.on("gpu-info-update", () => {
54375
+ gpuInfoReported = true;
54376
+ });
54377
+ function readGlassEnvironmentFacts() {
54378
+ const facts = {};
54379
+ try {
54380
+ facts.accelerationDisabledByLaunch = electron.app.commandLine.hasSwitch("disable-gpu");
54381
+ } catch {
54382
+ facts.accelerationDisabledByLaunch = false;
54383
+ }
54384
+ try {
54385
+ facts.hardwareAcceleration = electron.app.isHardwareAccelerationEnabled();
54386
+ } catch {
54387
+ facts.hardwareAcceleration = null;
54388
+ }
54389
+ try {
54390
+ const status2 = gpuInfoReported ? electron.app.getGPUFeatureStatus() : null;
54391
+ facts.gpu = status2 ? {
54392
+ compositing: status2.gpu_compositing,
54393
+ rasterization: status2.rasterization
54394
+ } : null;
54395
+ } catch {
54396
+ facts.gpu = null;
54397
+ }
54398
+ return facts;
54399
+ }
53904
54400
  function readPersistedWindowGlass(userDataDir, capability) {
53905
54401
  const raw = readJson(
53906
54402
  windowGlassFilePath(userDataDir)
53907
54403
  );
53908
- if (raw === null || typeof raw !== "object") return null;
53909
- return {
53910
- settings: sanitizeGlassSettings(raw.settings, capability),
53911
- scheme: raw.scheme === "light" ? "light" : "dark"
53912
- };
54404
+ return resolveStoredGlass(raw, capability);
53913
54405
  }
53914
54406
  function resolveLaunchWindowGlass(input) {
54407
+ const environment = input.environment ?? readGlassEnvironmentFacts();
53915
54408
  const capability = glassCapabilityFor({
53916
54409
  platform: input.platform ?? process.platform,
53917
54410
  osVersion: input.osVersion ?? process.getSystemVersion(),
53918
- reducedTransparency: input.reducedTransparency ?? electron.nativeTheme.prefersReducedTransparency
54411
+ reducedTransparency: input.reducedTransparency ?? electron.nativeTheme.prefersReducedTransparency,
54412
+ environment
53919
54413
  });
53920
54414
  let persisted = null;
53921
54415
  try {
@@ -53923,13 +54417,16 @@ function resolveLaunchWindowGlass(input) {
53923
54417
  } catch {
53924
54418
  persisted = null;
53925
54419
  }
53926
- const settings = persisted?.settings ?? DEFAULT_GLASS_SETTINGS;
54420
+ const settings = persisted?.settings ?? defaultGlassSettings(capability);
54421
+ const explicit = persisted?.explicit ?? [];
53927
54422
  const scheme = persisted?.scheme ?? "dark";
53928
54423
  return {
53929
54424
  capability,
53930
54425
  settings,
54426
+ explicit,
53931
54427
  scheme,
53932
- state: stateFor(settings, capability)
54428
+ state: stateFor(settings, capability),
54429
+ environment
53933
54430
  };
53934
54431
  }
53935
54432
  const stateFor = (settings, capability, launchedActive = glassIsActive(settings, capability)) => {
@@ -53938,7 +54435,16 @@ const stateFor = (settings, capability, launchedActive = glassIsActive(settings,
53938
54435
  capability,
53939
54436
  settings,
53940
54437
  active,
53941
- restartRequired: !capability.liveToggle && active !== launchedActive
54438
+ /*
54439
+ * A RESTART IS ONLY PENDING WHEN THE EFFECT IS AVAILABLE AND NOT LIVE.
54440
+ * `liveToggle: false` is also true of `mechanism: "none"`, so a capability
54441
+ * DROP (GPU software fallback, backdrop-filter report, Reduce transparency)
54442
+ * satisfied the old expression and told the user to restart while the window
54443
+ * had already fallen back to opaque. The unsupported state has its own
54444
+ * cause line; this notice is only for a supported change that needs a new
54445
+ * window (QA Q-1 / design D2).
54446
+ */
54447
+ restartRequired: capability.supported && !capability.liveToggle && active !== launchedActive
53942
54448
  };
53943
54449
  };
53944
54450
  function attachWindowGlass(options) {
@@ -53946,6 +54452,8 @@ function attachWindowGlass(options) {
53946
54452
  let capability = options.initial.capability;
53947
54453
  let settings = options.initial.settings;
53948
54454
  let scheme = options.initial.scheme;
54455
+ const explicit = new Set(options.initial.explicit);
54456
+ let environment = { ...options.initial.environment };
53949
54457
  const launchedActive = glassIsActive(settings, capability);
53950
54458
  let pinnedTheme = false;
53951
54459
  let appliedMechanism = launchedActive ? capability.mechanism : "none";
@@ -53989,7 +54497,17 @@ function attachWindowGlass(options) {
53989
54497
  }
53990
54498
  };
53991
54499
  const persist = () => {
53992
- if (!writeJsonAtomic(windowGlassFilePath(userDataDir), { settings, scheme }))
54500
+ if (!writeJsonAtomic(windowGlassFilePath(userDataDir), {
54501
+ /*
54502
+ * Versioned and per-field: the next launch fills only what this list does
54503
+ * not name from its own defaults. Writing the full `settings` object alone
54504
+ * is what made an untouched OFF indistinguishable from a chosen one.
54505
+ */
54506
+ version: GLASS_FILE_VERSION,
54507
+ settings,
54508
+ explicit: GLASS_SETTINGS_FIELDS.filter((field) => explicit.has(field)),
54509
+ scheme
54510
+ }))
53993
54511
  log2(
53994
54512
  "[window-glass] the setting could not be saved - it applies to this run and will not survive a restart"
53995
54513
  );
@@ -54010,6 +54528,11 @@ function attachWindowGlass(options) {
54010
54528
  (event, patch) => {
54011
54529
  if (!fromOwnWindow(event)) return null;
54012
54530
  settings = sanitizeGlassSettings(patch, capability, settings);
54531
+ if (patch !== null && typeof patch === "object") {
54532
+ for (const field of Object.keys(patch))
54533
+ if (GLASS_SETTINGS_FIELDS.includes(field))
54534
+ explicit.add(field);
54535
+ }
54013
54536
  applyToWindow();
54014
54537
  persist();
54015
54538
  push2();
@@ -54028,23 +54551,47 @@ function attachWindowGlass(options) {
54028
54551
  persist();
54029
54552
  return true;
54030
54553
  });
54031
- const onNativeThemeUpdated = () => {
54554
+ electron.ipcMain.handle(WINDOW_GLASS_RENDERER_CHANNEL, (event, report) => {
54555
+ if (!fromOwnWindow(event)) return null;
54556
+ if (report !== null && typeof report === "object") {
54557
+ const value = report.backdropFilter;
54558
+ if (typeof value === "boolean")
54559
+ environment = { ...environment, backdropFilter: value };
54560
+ }
54561
+ reevaluate();
54562
+ return current();
54563
+ });
54564
+ function reevaluate() {
54565
+ environment = {
54566
+ ...readGlassEnvironmentFacts(),
54567
+ ...environment.backdropFilter === void 0 ? {} : { backdropFilter: environment.backdropFilter }
54568
+ };
54032
54569
  const next = glassCapabilityFor({
54033
54570
  platform: process.platform,
54034
54571
  osVersion: process.getSystemVersion(),
54035
- reducedTransparency: electron.nativeTheme.prefersReducedTransparency
54572
+ reducedTransparency: electron.nativeTheme.prefersReducedTransparency,
54573
+ environment
54036
54574
  });
54037
- if (next.supported === capability.supported && next.reason === capability.reason)
54575
+ if (next.supported === capability.supported && next.reason === capability.reason && next.mechanism === capability.mechanism)
54038
54576
  return;
54577
+ const wasSupported = capability.supported;
54039
54578
  capability = next;
54040
54579
  settings = sanitizeGlassSettings(settings, capability, settings);
54580
+ if (next.supported && !wasSupported && !explicit.has("enabled"))
54581
+ settings.enabled = defaultGlassSettings(next).enabled;
54041
54582
  applyToWindow();
54042
54583
  push2();
54043
54584
  log2(
54044
54585
  `[window-glass] capability changed supported=${next.supported} reason=${next.reason}`
54045
54586
  );
54046
- };
54587
+ }
54588
+ const onNativeThemeUpdated = () => reevaluate();
54047
54589
  electron.nativeTheme.on("updated", onNativeThemeUpdated);
54590
+ electron.app.on("gpu-info-update", reevaluate);
54591
+ const onChildProcessGone = (_event, details) => {
54592
+ if (details?.type === "GPU") setTimeout(reevaluate, 0);
54593
+ };
54594
+ electron.app.on("child-process-gone", onChildProcessGone);
54048
54595
  if (glassIsActive(settings, capability)) {
54049
54596
  electron.nativeTheme.themeSource = scheme;
54050
54597
  pinnedTheme = true;
@@ -54055,7 +54602,10 @@ function attachWindowGlass(options) {
54055
54602
  electron.ipcMain.removeHandler(WINDOW_GLASS_GET_CHANNEL);
54056
54603
  electron.ipcMain.removeHandler(WINDOW_GLASS_SET_CHANNEL);
54057
54604
  electron.ipcMain.removeHandler(WINDOW_GLASS_SCHEME_CHANNEL);
54605
+ electron.ipcMain.removeHandler(WINDOW_GLASS_RENDERER_CHANNEL);
54058
54606
  electron.nativeTheme.removeListener("updated", onNativeThemeUpdated);
54607
+ electron.app.removeListener("gpu-info-update", reevaluate);
54608
+ electron.app.removeListener("child-process-gone", onChildProcessGone);
54059
54609
  if (pinnedTheme) electron.nativeTheme.themeSource = "system";
54060
54610
  }
54061
54611
  };
@@ -54808,6 +55358,7 @@ const authPopupWindowOptions = {
54808
55358
  disableDialogs: true
54809
55359
  }
54810
55360
  };
55361
+ let glassGateLogged = false;
54811
55362
  function createWindow(initialSession = null, openCatalogue = false, request = ownLaunchRequest()) {
54812
55363
  const chrome = resolveLaunchWindowChrome({
54813
55364
  argv: process.argv,
@@ -54822,6 +55373,13 @@ function createWindow(initialSession = null, openCatalogue = false, request = ow
54822
55373
  const glass = resolveLaunchWindowGlass({
54823
55374
  userDataDir: electron.app.getPath("userData")
54824
55375
  });
55376
+ if (!glassGateLogged) {
55377
+ glassGateLogged = true;
55378
+ logger.info(
55379
+ glassGateLine(glass.capability, glass.settings, process.platform),
55380
+ LogFileType.BACKEND
55381
+ );
55382
+ }
54825
55383
  const mainWindow2 = new electron.BrowserWindow({
54826
55384
  /*
54827
55385
  * The frame, per platform, from a pure function so the matrix is exercised on