local-operator-ui 0.34.56 → 0.35.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 (89) hide show
  1. package/out/main/index.js +570 -44
  2. package/out/preload/index.js +50 -1
  3. package/out/renderer/assets/{_basePickBy-BZpmSLU-.js → _basePickBy-DXcdlCne.js} +1 -1
  4. package/out/renderer/assets/{_baseUniq-5Fk4AsiY.js → _baseUniq-59s2A3Xi.js} +1 -1
  5. package/out/renderer/assets/{agent-details-page-BEoctGwH.js → agent-details-page-DzCMByBi.js} +1 -1
  6. package/out/renderer/assets/{agent-hub-page-DVtEYZne.js → agent-hub-page-DJCaMoAj.js} +1 -1
  7. package/out/renderer/assets/{agents-page-CqlFD0Ps.js → agents-page-jAx30gRr.js} +3 -3
  8. package/out/renderer/assets/{arc-Cgq0RekE.js → arc-DTGY_Lgi.js} +1 -1
  9. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-SKpw5bNL.js → architectureDiagram-IEHRJDOE-C1eZGb9i.js} +1 -1
  10. package/out/renderer/assets/{blockDiagram-JOT3LUYC-jQv0ffHr.js → blockDiagram-JOT3LUYC-DcFFqP3h.js} +1 -1
  11. package/out/renderer/assets/{book-open--60aKTv6.js → book-open-Bi2GxPj-.js} +1 -1
  12. package/out/renderer/assets/browser-page-Bnzl2Fck.js +1 -0
  13. package/out/renderer/assets/{browser-webauthn-prompt-PeVRs-qh.js → browser-webauthn-prompt-DcHbqaLn.js} +1 -1
  14. package/out/renderer/assets/{c4Diagram-VJAJSXHY-BlRyRK9C.js → c4Diagram-VJAJSXHY-CzUO0Dxd.js} +1 -1
  15. package/out/renderer/assets/channel-CBLzxDK-.js +1 -0
  16. package/out/renderer/assets/{chunk-4BMEZGHF-BBVAaHlr.js → chunk-4BMEZGHF-mCAjVMxO.js} +1 -1
  17. package/out/renderer/assets/{chunk-A2AXSNBT-sJR5i1wN.js → chunk-A2AXSNBT-D6-zo_Fr.js} +1 -1
  18. package/out/renderer/assets/{chunk-AEK57VVT-C8IWMgPw.js → chunk-AEK57VVT-B3ngikm8.js} +1 -1
  19. package/out/renderer/assets/{chunk-D6G4REZN-DTfUS03C.js → chunk-D6G4REZN-ChTXeHBQ.js} +1 -1
  20. package/out/renderer/assets/{chunk-RZ5BOZE2-DiGk8o7V.js → chunk-RZ5BOZE2-D2c3_Jya.js} +1 -1
  21. package/out/renderer/assets/{chunk-XZIHB7SX-Mamouuqj.js → chunk-XZIHB7SX-BTrxUgxT.js} +1 -1
  22. package/out/renderer/assets/classDiagram-GIVACNV2-BnE_q2SF.js +1 -0
  23. package/out/renderer/assets/classDiagram-v2-COTLJTTW-BnE_q2SF.js +1 -0
  24. package/out/renderer/assets/clone-Ctta4hua.js +1 -0
  25. package/out/renderer/assets/{console-mirror-Ch4jKiRK.js → console-mirror-Dc1bE9Vm.js} +1 -1
  26. package/out/renderer/assets/{consoleCapture-BYlfSgpd.js → consoleCapture-Wr6s4sLo.js} +1 -1
  27. package/out/renderer/assets/{conversation-input-sync-Cs9wr7F7.js → conversation-input-sync-C-0MXqyt.js} +140 -140
  28. package/out/renderer/assets/{dagre-OKDRZEBW-D5TY-UFL.js → dagre-OKDRZEBW-B9TiRiqo.js} +1 -1
  29. package/out/renderer/assets/{diagram-SSKATNLV-BTiHJkCz.js → diagram-SSKATNLV-1SfE1-5B.js} +1 -1
  30. package/out/renderer/assets/{diagram-VNBRO52H-CBxLprav.js → diagram-VNBRO52H-l4dzkACu.js} +1 -1
  31. package/out/renderer/assets/{erDiagram-Q7BY3M3F-9aF5V3AY.js → erDiagram-Q7BY3M3F-CUgpeoFK.js} +1 -1
  32. package/out/renderer/assets/{error-boundary-B5YLbOOu.js → error-boundary-C9PRNlmT.js} +2 -2
  33. package/out/renderer/assets/{flowDiagram-4HSFHLVR-BXlN8K06.js → flowDiagram-4HSFHLVR-T5JJ4Xtw.js} +1 -1
  34. package/out/renderer/assets/{ganttDiagram-APWFNJXF-1FcF-Cau.js → ganttDiagram-APWFNJXF-DQ2mySlQ.js} +1 -1
  35. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-CjCM20j_.js → gitGraphDiagram-7IBYFJ6S-C9DZfwGd.js} +1 -1
  36. package/out/renderer/assets/{graph-CGuSrplO.js → graph-TsL6qeA-.js} +1 -1
  37. package/out/renderer/assets/index-8yv29dzU.js +57 -0
  38. package/out/renderer/assets/index-Bk16DqRh.css +1 -0
  39. package/out/renderer/assets/{index-rnB6EiBX.js → index-D8u5NmIa.js} +177 -162
  40. package/out/renderer/assets/{index-CalJ1cZq.js → index-P4i-q0KF.js} +1 -1
  41. package/out/renderer/assets/{index-5brl9P13.js → index-rSXvFxM8.js} +1 -1
  42. package/out/renderer/assets/{infoDiagram-PH2N3AL5-Bqs15-nE.js → infoDiagram-PH2N3AL5-U47NbuL3.js} +1 -1
  43. package/out/renderer/assets/{installer-DZXz4Bgj.js → installer-fluCVssE.js} +1 -1
  44. package/out/renderer/assets/{journeyDiagram-U35MCT3I-BkSqHNi1.js → journeyDiagram-U35MCT3I-Bo7dozG5.js} +1 -1
  45. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-CGnCYoin.js → kanban-definition-NDS4AKOZ-Bn0hKA3l.js} +1 -1
  46. package/out/renderer/assets/{layout-BM7AzCJ1.js → layout-CnIEnr3t.js} +1 -1
  47. package/out/renderer/assets/{legacy-agents-page-B8f2tJOf.js → legacy-agents-page-pxEj7cjk.js} +1 -1
  48. package/out/renderer/assets/{mermaid.core-C14g_ZP7.js → mermaid.core-lUsLZpuU.js} +5 -5
  49. package/out/renderer/assets/{mesh-page-DCCummN5.js → mesh-page-vihuNvox.js} +1 -1
  50. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-D7Z-SM4X.js → mindmap-definition-ALO5MXBD-DtB-t5TL.js} +1 -1
  51. package/out/renderer/assets/{mini-CcJI0pQs.js → mini--p2d2BoB.js} +1 -1
  52. package/out/renderer/assets/{notebook-pen-Dj0km82T.js → notebook-pen-C-602S82.js} +1 -1
  53. package/out/renderer/assets/{org-access-D0eAQUWQ.js → org-access-DwKpT4sY.js} +1 -1
  54. package/out/renderer/assets/{org-surface-gate-Bgznt94a.js → org-surface-gate--inYykb1.js} +1 -1
  55. package/out/renderer/assets/{page-header-B3mzM2nC.js → page-header-Cz4WU8y6.js} +1 -1
  56. package/out/renderer/assets/{pieDiagram-IB7DONF6-3pVvYdop.js → pieDiagram-IB7DONF6-kHXX0Qj3.js} +1 -1
  57. package/out/renderer/assets/{projects-page-T2Yw-kRf.js → projects-page-B9Y4di7i.js} +1 -1
  58. package/out/renderer/assets/{publication-validation-BIJPIOf1.js → publication-validation-LQ7dKLdu.js} +1 -1
  59. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-Dttg4cdR.js → quadrantDiagram-7GDLP6J5-Da2X85Fu.js} +1 -1
  60. package/out/renderer/assets/{radar-MK3ICKWK-Bd7wnpF5.js → radar-MK3ICKWK-C0d75XBo.js} +1 -1
  61. package/out/renderer/assets/{radient-auth-buttons-MPoHRsQX.js → radient-auth-buttons-DQVWpG8o.js} +1 -1
  62. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-Jiaz4ehK.js → requirementDiagram-KVF5MWMF-CL-Kku_X.js} +1 -1
  63. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-C4tMl9ww.js → sankeyDiagram-QLVOVGJD-DWUg1eAE.js} +1 -1
  64. package/out/renderer/assets/{schedules-page-CR7bLqQt.js → schedules-page-B7tSQfbG.js} +1 -1
  65. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-DncET3lE.js → sequenceDiagram-X6HHIX6F-Bn0FlGHo.js} +1 -1
  66. package/out/renderer/assets/settings-page-Dty0ZTmK.js +292 -0
  67. package/out/renderer/assets/{stateDiagram-DGXRK772-BI1ziKRV.js → stateDiagram-DGXRK772-CkdY_hLZ.js} +1 -1
  68. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CWy168Js.js +1 -0
  69. package/out/renderer/assets/{timeline-definition-BDJGKUSR-DxR2rlJV.js → timeline-definition-BDJGKUSR-DodPShzl.js} +1 -1
  70. package/out/renderer/assets/ui-preferences-store-B_KwjcMZ.js +1 -0
  71. package/out/renderer/assets/{use-download-agent-mutation-C6TvAwyM.js → use-download-agent-mutation-CIiU8sRW.js} +1 -1
  72. package/out/renderer/assets/{use-memberships-query-AlVCuF9E.js → use-memberships-query-BMOJPwWD.js} +1 -1
  73. package/out/renderer/assets/{use-session-snapshot-XM0bTxHC.js → use-session-snapshot-B0aCKABJ.js} +1 -1
  74. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CJW_dOUR.js → xychartDiagram-VJFVF3MP-BRifY3vn.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`;
@@ -32318,6 +32457,8 @@ const enUiBrowser = {
32318
32457
  "ui.browser.browser_tab_strip.close": 'Close "',
32319
32458
  "ui.browser.browser_tab_strip.copy_url": "Copy URL",
32320
32459
  "ui.browser.browser_tab_strip.close_tab": "Close tab",
32460
+ "ui.browser.browser_tab_strip.mute_tab": "Mute tab",
32461
+ "ui.browser.browser_tab_strip.unmute_tab": "Unmute tab",
32321
32462
  "ui.browser.browser_tab_strip.close_tab_shortcut": "Close tab ({shortcut})",
32322
32463
  "ui.browser.browser_tab_strip.close_named_tab": "Close {title}",
32323
32464
  "ui.browser.browser_tab_strip.all_tabs_unshown": "All tabs — {count, plural, other {{count, number, ::group-off} not shown}}",
@@ -32330,6 +32471,7 @@ const enUiBrowser = {
32330
32471
  "ui.browser.browser_url_bar.forward_shortcut": "Forward ({shortcut})",
32331
32472
  "ui.browser.browser_url_bar.find_in_page": "Find in page",
32332
32473
  "ui.browser.browser_url_bar.find_in_page_shortcut": "Find in page ({shortcut})",
32474
+ "ui.browser.browser_url_bar.print_page": "Print page",
32333
32475
  "ui.browser.browser_url_bar.forward": "Forward",
32334
32476
  "ui.browser.browser_url_bar.reset_page_zoom_shortcut": "Reset page zoom ({shortcut})",
32335
32477
  "ui.browser.browser_url_bar.page_zoom_reset": "Page zoom {zoom, number, ::group-off}%, reset to 100%",
@@ -34320,12 +34462,12 @@ const enUiSettingsAppearance = {
34320
34462
  };
34321
34463
  const enUiSettingsAppearanceWindowGlass = {
34322
34464
  "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.",
34465
+ "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
34466
  "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.",
34467
+ "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
34468
  "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.",
34469
+ "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.",
34470
+ "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
34471
  "ui.settings.appearance.window_glass.coverage_label": "Glass applies to",
34330
34472
  "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
34473
  "ui.settings.appearance.window_glass.coverage_sidebars": "Sidebars only",
@@ -34342,7 +34484,11 @@ const enUiSettingsAppearanceWindowGlass = {
34342
34484
  "ui.settings.appearance.window_glass.material_tabbed": "Tabbed",
34343
34485
  "ui.settings.appearance.window_glass.intensity_start": "Frosted",
34344
34486
  "ui.settings.appearance.window_glass.intensity_end": "See-through",
34345
- "ui.settings.appearance.window_glass.intensity_value": "{value}% toward see-through"
34487
+ "ui.settings.appearance.window_glass.intensity_value": "{value}% toward see-through",
34488
+ "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.",
34489
+ "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.",
34490
+ "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.",
34491
+ "ui.settings.appearance.window_glass.unsupported_backdrop_filter": "Translucent window is unavailable because this renderer cannot draw a backdrop blur."
34346
34492
  };
34347
34493
  const enUiSettingsConfiguration = {
34348
34494
  "ui.settings.configuration.backend_setting_row.read_only": "Read-only",
@@ -34438,6 +34584,82 @@ const enUiSettingsConfiguration = {
34438
34584
  "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
34585
  "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
34586
  };
34587
+ const enUiSettingsImports = {
34588
+ "ui.settings.imports.title": "Import conversations",
34589
+ "ui.settings.imports.description": "Copy conversations and curated memory from Claude Code, Codex or omp into Local Operator. The source stores are read-only.",
34590
+ "ui.settings.imports.source_heading": "Sources",
34591
+ "ui.settings.imports.source_not_found": "Not found",
34592
+ "ui.settings.imports.source_sessions": "{count, plural, one {# session} other {# sessions}}",
34593
+ "ui.settings.imports.loading_sources": "Checking for source stores...",
34594
+ "ui.settings.imports.loading_sessions": "Reading {source} sessions...",
34595
+ "ui.settings.imports.loading_preview": "Building the preview...",
34596
+ "ui.settings.imports.empty_sources": "No Claude Code, Codex or omp stores were detected on this machine.",
34597
+ "ui.settings.imports.empty_sessions": "No sessions found for {source}.",
34598
+ "ui.settings.imports.empty_filter": "No sessions match this search.",
34599
+ "ui.settings.imports.error_sources": "Could not read the source stores. {reason}",
34600
+ "ui.settings.imports.error_sessions": "Could not read {source} sessions. {reason}",
34601
+ "ui.settings.imports.error_preview": "Could not build the preview. {reason}",
34602
+ "ui.settings.imports.error_start": "Could not start the import. {reason}",
34603
+ "ui.settings.imports.error_job": "Could not read import progress. {reason}",
34604
+ "ui.settings.imports.error_cancel": "Could not cancel the import. {reason}",
34605
+ "ui.settings.imports.retry": "Retry",
34606
+ "ui.settings.imports.refresh": "Refresh",
34607
+ "ui.settings.imports.search_placeholder": "Search conversations",
34608
+ "ui.settings.imports.select_all": "Select all",
34609
+ "ui.settings.imports.clear_all": "Clear selection",
34610
+ "ui.settings.imports.selected": "{count, plural, one {# selected} other {# selected}}",
34611
+ "ui.settings.imports.selection_limit": "Only {max, number} sessions can be selected at once. Select fewer sessions or import them in batches.",
34612
+ "ui.settings.imports.session_meta": "{messages, plural, one {# message} other {# messages}} · {compactions, plural, one {# compaction} other {# compactions}}",
34613
+ "ui.settings.imports.session_updated": "Updated {date}",
34614
+ "ui.settings.imports.session_index_only": "Index only — no conversation body",
34615
+ "ui.settings.imports.preview_heading": "Preview",
34616
+ "ui.settings.imports.preview_summary": "{sessions, plural, one {# session} other {# sessions}} · {messages, plural, one {# message} other {# messages}} · {compactions, plural, one {# compaction} other {# compactions}}",
34617
+ "ui.settings.imports.preview_memory": "{records, plural, one {# memory record} other {# memory records}} · ~{tokens} tokens",
34618
+ "ui.settings.imports.preview_confirm": "Review the selection above, then confirm the copy.",
34619
+ "ui.settings.imports.preview_required": "Select at least one session and build a preview first.",
34620
+ "ui.settings.imports.confirm_label": "I understand these conversations will be copied into Local Operator.",
34621
+ "ui.settings.imports.import_button": "{count, plural, one {Import # session} other {Import # sessions}}",
34622
+ "ui.settings.imports.importing": "Importing",
34623
+ "ui.settings.imports.progress_heading": "Importing conversations",
34624
+ "ui.settings.imports.progress_job": "Job {job} · {state}",
34625
+ "ui.settings.imports.progress_pending": "{count, plural, one {# session pending} other {# sessions pending}}",
34626
+ "ui.settings.imports.cancel": "Cancel import",
34627
+ "ui.settings.imports.cancelling": "Cancelling...",
34628
+ "ui.settings.imports.result_heading": "Import result",
34629
+ "ui.settings.imports.result_summary": "{state} · {sessions, plural, one {# session} other {# sessions}} · {messages, plural, one {# message} other {# messages}}",
34630
+ "ui.settings.imports.result_summary_cancelled": "{state} · {sessions, number} of {total, plural, one {# session} other {# sessions}} · {messages, plural, one {# message} other {# messages}}",
34631
+ "ui.settings.imports.result_target": "New session {target}",
34632
+ "ui.settings.imports.outcome_imported": "imported",
34633
+ "ui.settings.imports.outcome_resumed": "resumed",
34634
+ "ui.settings.imports.outcome_skipped": "already imported",
34635
+ "ui.settings.imports.outcome_metadata": "index only — nothing to import",
34636
+ "ui.settings.imports.outcome_failed": "failed",
34637
+ "ui.settings.imports.outcome_cancelled": "cancelled",
34638
+ "ui.settings.imports.warnings_heading": "{count, plural, one {# warning} other {# warnings}}",
34639
+ "ui.settings.imports.review_instructions": "Instructions saved for review",
34640
+ "ui.settings.imports.review_memory": "Unattached memory saved for review",
34641
+ "ui.settings.imports.copy_path": "Copy path",
34642
+ "ui.settings.imports.copied": "Copied",
34643
+ "ui.settings.imports.rerun_safe": "Re-running is safe: unchanged sources are skipped, grown sources resume, rewritten sources get a new copy.",
34644
+ "ui.settings.imports.import_another": "Import more conversations",
34645
+ "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.",
34646
+ "ui.settings.imports.state_queued": "queued",
34647
+ "ui.settings.imports.state_running": "running",
34648
+ "ui.settings.imports.state_done": "done",
34649
+ "ui.settings.imports.state_failed": "failed",
34650
+ "ui.settings.imports.state_cancelled": "cancelled",
34651
+ "ui.settings.imports.phase_queued": "queued",
34652
+ "ui.settings.imports.phase_selecting": "selecting",
34653
+ "ui.settings.imports.phase_importing": "importing",
34654
+ "ui.settings.imports.phase_memory": "reading memory",
34655
+ "ui.settings.imports.phase_done": "finishing",
34656
+ "ui.settings.imports.phase_cancelled": "cancelled",
34657
+ "ui.settings.imports.session_pending": "Pending",
34658
+ "ui.settings.imports.session_importing": "Importing",
34659
+ "ui.settings.imports.availability_full": "full",
34660
+ "ui.settings.imports.availability_metadata_only": "index only",
34661
+ "ui.settings.imports.availability_unsupported": "unsupported"
34662
+ };
34441
34663
  const enUiSettingsIntegrations = {
34442
34664
  "ui.settings.integrations.add_integration_form.add_integration": "Add integration",
34443
34665
  "ui.settings.integrations.add_integration_form.name": "Name",
@@ -36318,6 +36540,7 @@ const bundledEnglish = {
36318
36540
  ...enUiSettingsAppearance,
36319
36541
  ...enUiSettingsAppearanceWindowGlass,
36320
36542
  ...enUiSettingsConfiguration,
36543
+ ...enUiSettingsImports,
36321
36544
  ...enUiSettingsIntegrations,
36322
36545
  ...enUiSettings,
36323
36546
  ...enUiSettingsModelDefaultEffort,
@@ -39040,6 +39263,7 @@ const PAGE_ZOOM_STEPS = [
39040
39263
  4,
39041
39264
  5
39042
39265
  ];
39266
+ const PRINT_CANCELLED = /cancel/i;
39043
39267
  class PageControls {
39044
39268
  constructor(changed, resultTimeoutMs = 5e3) {
39045
39269
  this.changed = changed;
@@ -39174,6 +39398,36 @@ class PageControls {
39174
39398
  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
39399
  active.view.webContents.setZoomFactor?.(factor);
39176
39400
  }
39401
+ /** Print the presented page (LOH-13 slice C).
39402
+ *
39403
+ * HUMAN-ONLY, like everything in this class: it travels the sender-
39404
+ * authorized page-control IPC and the tab's serial lane, never the agent
39405
+ * protocol — an agent that needs a page as a document already has the
39406
+ * PDF/screenshot actions, and silently sending a page to a physical
39407
+ * printer is not a capability a tool call should hold.
39408
+ *
39409
+ * `silent: false` is the load-bearing option: Electron's silent mode
39410
+ * prints to the default printer with default settings and NO dialog,
39411
+ * which for a human press would be a side effect they never confirmed.
39412
+ * The dialog is the consent. A failure the dialog itself reports
39413
+ * (cancelled is the person changing their mind, not a failure) only
39414
+ * surfaces through `notice` when Electron names a real reason. */
39415
+ print(active) {
39416
+ if (!active.presented) return;
39417
+ const wc = active.view.webContents;
39418
+ if (wc.isDestroyed() || !wc.print) {
39419
+ this.notice = "Printing is not available for this page";
39420
+ this.noticeTabId = active.tabId;
39421
+ return;
39422
+ }
39423
+ this.notice = null;
39424
+ wc.print({ silent: false, printBackground: false }, (success, reason) => {
39425
+ if (success || !reason || PRINT_CANCELLED.test(reason)) return;
39426
+ this.notice = `Printing failed: ${reason}`;
39427
+ this.noticeTabId = active.tabId;
39428
+ this.changed();
39429
+ });
39430
+ }
39177
39431
  /** Call BEFORE a capability transfer. Isolated per-WebContents zoom means
39178
39432
  * resetting this tab cannot alter another tab at the same origin. */
39179
39433
  prepareAgent(record) {
@@ -39183,6 +39437,71 @@ class PageControls {
39183
39437
  if (changed) this.changed();
39184
39438
  }
39185
39439
  }
39440
+ class TabAudioTracker {
39441
+ constructor(changed) {
39442
+ this.changed = changed;
39443
+ }
39444
+ states = /* @__PURE__ */ new Map();
39445
+ listeners = /* @__PURE__ */ new Map();
39446
+ /** Start tracking one tab (idempotent) and return its current state. The
39447
+ * muted flag is re-read live on every call: `setAudioMuted` is also
39448
+ * reachable from the page's own media controls in principle, and a stale
39449
+ * cached flag would render a button that claims the opposite of the
39450
+ * page's actual state. */
39451
+ stateFor(tab) {
39452
+ this.track(tab);
39453
+ const state2 = this.states.get(tab.tabId) ?? {
39454
+ audible: false,
39455
+ muted: false
39456
+ };
39457
+ const wc = tab.view.webContents;
39458
+ if (!wc.isDestroyed()) state2.muted = wc.isAudioMuted?.() ?? state2.muted;
39459
+ return { ...state2 };
39460
+ }
39461
+ /** Mute or unmute one tab. Returns the resulting state, or null when the
39462
+ * webContents is already gone — the caller (a human press) then reports
39463
+ * the tab as closed rather than claiming a mute that never landed. */
39464
+ setMuted(tab, muted) {
39465
+ const wc = tab.view.webContents;
39466
+ if (wc.isDestroyed()) return null;
39467
+ this.track(tab);
39468
+ wc.setAudioMuted?.(muted);
39469
+ const state2 = this.stateFor(tab);
39470
+ state2.muted = wc.isAudioMuted?.() ?? muted;
39471
+ this.states.set(tab.tabId, state2);
39472
+ this.changed();
39473
+ return { ...state2 };
39474
+ }
39475
+ /** Drop tracking for every tab not in `liveTabIds`. Called from the
39476
+ * projection, which is the one place main already enumerates the live
39477
+ * set on every change. */
39478
+ prune(liveTabIds) {
39479
+ for (const [tabId, entry] of this.listeners) {
39480
+ if (liveTabIds.has(tabId)) continue;
39481
+ this.listeners.delete(tabId);
39482
+ this.states.delete(tabId);
39483
+ if (!entry.wc.isDestroyed())
39484
+ entry.wc.removeListener("audio-state-changed", entry.listener);
39485
+ }
39486
+ }
39487
+ track(tab) {
39488
+ if (this.listeners.has(tab.tabId)) return;
39489
+ const wc = tab.view.webContents;
39490
+ this.states.set(tab.tabId, {
39491
+ audible: wc.isDestroyed() ? false : wc.isCurrentlyAudible?.() ?? false,
39492
+ muted: wc.isDestroyed() ? false : wc.isAudioMuted?.() ?? false
39493
+ });
39494
+ const listener = (event) => {
39495
+ const current = this.states.get(tab.tabId);
39496
+ if (!current) return;
39497
+ current.audible = event?.audible ?? false;
39498
+ this.states.set(tab.tabId, current);
39499
+ this.changed();
39500
+ };
39501
+ this.listeners.set(tab.tabId, { wc, listener });
39502
+ if (!wc.isDestroyed()) wc.on("audio-state-changed", listener);
39503
+ }
39504
+ }
39186
39505
  const DUPLICATABLE_WEB_URL = /^https?:\/\//i;
39187
39506
  const DOCUMENT_SCOPED = /* @__PURE__ */ new Set([
39188
39507
  "read",
@@ -39272,6 +39591,10 @@ class BrowserHost {
39272
39591
  chromeTabOrder = new ChromeTabOrder();
39273
39592
  registry;
39274
39593
  pageControls;
39594
+ /** Per-tab audio for the strip's mute control (LOH-13 slice C). Main owns
39595
+ * it for the reason `tabLoading` is read here: a background tab's sound
39596
+ * is legible only through this projection. */
39597
+ tabAudio = new TabAudioTracker(() => this.onChanged());
39275
39598
  cdp;
39276
39599
  approvals;
39277
39600
  ownership;
@@ -39581,6 +39904,9 @@ class BrowserHost {
39581
39904
  const activeRecord = this.registry.activeTab;
39582
39905
  const active = activeRecord && !activeRecord.view.webContents.isDestroyed() ? activeRecord : null;
39583
39906
  const navigation = active?.view.webContents.navigationHistory;
39907
+ this.tabAudio.prune(
39908
+ new Set(this.registry.snapshot().map((entry) => entry.tabId))
39909
+ );
39584
39910
  return {
39585
39911
  pageControls: this.pageControls.state(active),
39586
39912
  tabs: this.chromeTabOrder.project(this.registry.snapshot()).map((entry, displayOrder) => ({
@@ -39616,7 +39942,20 @@ class BrowserHost {
39616
39942
  // was the whole of what the user saw of an agent's work. Read from the
39617
39943
  // record rather than the snapshot: `snapshot()` is the diagnostic
39618
39944
  // projection shared with `status`/`tabs`, and this field is chrome.
39619
- loading: this.tabLoading(entry.tabId)
39945
+ loading: this.tabLoading(entry.tabId),
39946
+ // PER TAB, projected with `loading` and for the same reason: a
39947
+ // background tab making sound has no other surface. `audible`
39948
+ // gates the mute control's visibility; `muted` is its state.
39949
+ // Read from the record (guarded like `tabLoading`) because the
39950
+ // snapshot carries no webContents.
39951
+ audible: (() => {
39952
+ const record = this.registry.get(entry.tabId);
39953
+ return record && !record.view.webContents.isDestroyed() ? this.tabAudio.stateFor(record).audible : false;
39954
+ })(),
39955
+ muted: (() => {
39956
+ const record = this.registry.get(entry.tabId);
39957
+ return record && !record.view.webContents.isDestroyed() ? this.tabAudio.stateFor(record).muted : false;
39958
+ })()
39620
39959
  })),
39621
39960
  activeTabId: activeRecord?.tabId ?? null,
39622
39961
  // The download surface's facts (§16.4): what is in flight, where it went and
@@ -39718,8 +40057,23 @@ class BrowserHost {
39718
40057
  error: { code: "tab_closed", message: t("ui.browser.chrome.tab_gone") }
39719
40058
  };
39720
40059
  let url = "";
39721
- let destinationId = action === "reload" ? tabId : null;
40060
+ let destinationId = action === "duplicate" ? null : tabId;
39722
40061
  try {
40062
+ if (action === "mute" || action === "unmute") {
40063
+ const result = this.tabAudio.setMuted(record, action === "mute");
40064
+ if (!result)
40065
+ return {
40066
+ status: "failed",
40067
+ action,
40068
+ tabId: null,
40069
+ error: {
40070
+ code: "tab_closed",
40071
+ message: t("ui.browser.chrome.tab_gone")
40072
+ }
40073
+ };
40074
+ this.onChanged();
40075
+ return { status: "completed", action, tabId };
40076
+ }
39723
40077
  url = record.view.webContents.getURL();
39724
40078
  if (action === "duplicate" && url !== "about:blank" && !DUPLICATABLE_WEB_URL.test(url))
39725
40079
  return {
@@ -39788,6 +40142,7 @@ class BrowserHost {
39788
40142
  if (!active || active.tabId !== tabId || !active.presented || active.view.webContents.isDestroyed())
39789
40143
  throw new Error("The browser tab is no longer presented.");
39790
40144
  if (action === "open") this.pageControls.open(active);
40145
+ else if (action === "print") this.pageControls.print(active);
39791
40146
  else if (action === "close") this.pageControls.clear();
39792
40147
  else if (action === "search")
39793
40148
  this.pageControls.search(active, query, forward, next);
@@ -40527,7 +40882,8 @@ class DownloadArmer {
40527
40882
  deadlineTimer: null,
40528
40883
  capTimer: null,
40529
40884
  finish: null,
40530
- settled: false
40885
+ settled: false,
40886
+ human: false
40531
40887
  };
40532
40888
  this.captures.set(tabId, capture);
40533
40889
  const deadline2 = setTimeout(() => this.settle(capture), timeoutMs);
@@ -40582,9 +40938,15 @@ class DownloadArmer {
40582
40938
  */
40583
40939
  decide(item, webContentsId) {
40584
40940
  const tabId = this.options.tabForWebContents(webContentsId);
40585
- const capture = tabId === null ? void 0 : this.captures.get(tabId);
40941
+ let capture = tabId === null ? void 0 : this.captures.get(tabId);
40586
40942
  if (!capture || capture.settled) {
40587
- return { cancel: true, reason: "no download call is armed on this tab" };
40943
+ capture = (tabId !== null ? this.humanCapture(tabId) : null) ?? void 0;
40944
+ if (!capture) {
40945
+ return {
40946
+ cancel: true,
40947
+ reason: "no download call is armed on this tab"
40948
+ };
40949
+ }
40588
40950
  }
40589
40951
  const raw = item.getFilename();
40590
40952
  const clean = safeName(raw);
@@ -40706,6 +41068,58 @@ class DownloadArmer {
40706
41068
  const dirs = this.notes.filter((note) => note.dir !== "").map((n) => n.dir);
40707
41069
  return [...this.captures.values()].at(-1)?.dir ?? dirs.at(-1) ?? null;
40708
41070
  }
41071
+ /** Build (or reuse) the human capture for one user-owned tab.
41072
+ *
41073
+ * Returns null — leaving the caller to cancel — when this host cannot
41074
+ * honestly call the download human-initiated: the tab is agent-owned or
41075
+ * handed to an agent (`ownerKindOf` reads the registry's CURRENT owner,
41076
+ * which hand-over flips), the armer was built without a human directory,
41077
+ * or that directory cannot be made. A settled human capture is replaced
41078
+ * rather than reused, so its file count bounds one burst (up to the
41079
+ * quiet window after the last write) and never the session.
41080
+ *
41081
+ * The directory is NOT chmodded, unlike `arm`: it is the person's own
41082
+ * downloads folder, which predates this app and whose mode is theirs.
41083
+ * The runtime-cap sampler IS started, for the reason `CAP_SAMPLE_MS`
41084
+ * states: an unknown-size human download has the same disk to fill as
41085
+ * an agent's. */
41086
+ humanCapture(tabId) {
41087
+ if (this.ownerKindOf(tabId) !== "user") return null;
41088
+ const dir = this.options.humanDownloadDir?.() ?? null;
41089
+ if (!dir) return null;
41090
+ try {
41091
+ fs.mkdirSync(dir, { recursive: true });
41092
+ } catch {
41093
+ return null;
41094
+ }
41095
+ const capture = {
41096
+ tabId,
41097
+ dir,
41098
+ timeoutMs: 0,
41099
+ startedAt: (this.options.now ?? Date.now)(),
41100
+ files: [],
41101
+ refusals: [],
41102
+ pending: 0,
41103
+ started: false,
41104
+ live: [],
41105
+ handedOut: /* @__PURE__ */ new Set(),
41106
+ active: null,
41107
+ lastProgressAt: 0,
41108
+ quietTimer: null,
41109
+ deadlineTimer: null,
41110
+ capTimer: null,
41111
+ finish: null,
41112
+ settled: false,
41113
+ human: true
41114
+ };
41115
+ capture.capTimer = setInterval(
41116
+ () => this.sampleCaps(capture),
41117
+ CAP_SAMPLE_MS
41118
+ );
41119
+ capture.capTimer.unref?.();
41120
+ this.captures.set(tabId, capture);
41121
+ return capture;
41122
+ }
40709
41123
  /** One upload's own line (review round 1, U3).
40710
41124
  *
40711
41125
  * WHY AN UPLOAD HAS A NOTE AT ALL, when §16.4 deliberately has no upload
@@ -40777,7 +41191,7 @@ class DownloadArmer {
40777
41191
  if (entry.weCancelled) {
40778
41192
  if (state2 !== "completed") discardPartial(savePath, this.options.log);
40779
41193
  } else if (state2 === "completed") {
40780
- restrictMode(savePath, this.options.log);
41194
+ if (!capture.human) restrictMode(savePath, this.options.log);
40781
41195
  capture.files.push(fileFact(item, savePath));
40782
41196
  this.note(capture, name, "saved", 1, "", null);
40783
41197
  } else {
@@ -40933,6 +41347,9 @@ class DownloadArmer {
40933
41347
  capture.settled = true;
40934
41348
  capture.active = null;
40935
41349
  this.clearTimers(capture);
41350
+ if (this.captures.get(capture.tabId) === capture) {
41351
+ this.captures.delete(capture.tabId);
41352
+ }
40936
41353
  capture.finish?.();
40937
41354
  this.options.onActivity?.();
40938
41355
  }
@@ -40963,6 +41380,7 @@ class DownloadArmer {
40963
41380
  capture.refusals.push(reason);
40964
41381
  this.note(capture, clean, "refused", 1, reason, refusal);
40965
41382
  this.options.log(`[browser] ${reason}`);
41383
+ this.options.onActivity?.();
40966
41384
  return { cancel: true, reason };
40967
41385
  }
40968
41386
  note(capture, name, outcome, count, reason, refusal) {
@@ -41215,7 +41633,7 @@ function registerBrowserIpc(options) {
41215
41633
  "browser-tab-action",
41216
41634
  (event, tabId, action) => {
41217
41635
  const host = authorize(event);
41218
- if (action !== "duplicate" && action !== "reload")
41636
+ if (action !== "duplicate" && action !== "reload" && action !== "mute" && action !== "unmute")
41219
41637
  throw new Error(t("ui.browser.chrome.invalid_action"));
41220
41638
  return host.actOnChromeTab(numberOrThrow(tabId, "tabId"), action);
41221
41639
  }
@@ -41243,7 +41661,9 @@ function registerBrowserIpc(options) {
41243
41661
  "browser-page-control",
41244
41662
  (event, tabId, action, query, forward, next) => {
41245
41663
  const host = authorize(event);
41246
- if (typeof action !== "string" || !["open", "close", "search", "in", "out", "reset"].includes(action))
41664
+ if (typeof action !== "string" || !["open", "close", "search", "print", "in", "out", "reset"].includes(
41665
+ action
41666
+ ))
41247
41667
  throw new Error("Invalid page control action.");
41248
41668
  if (typeof query !== "string" || query.length > 4096 || typeof forward !== "boolean" || typeof next !== "boolean")
41249
41669
  throw new Error("Invalid page search input.");
@@ -43623,6 +44043,17 @@ async function initializeBrowserHost(options, bindings) {
43623
44043
  // exists is a marker pointing at nothing. `null` when the lookup cannot answer
43624
44044
  // (a tab already gone), which the row renders the old way rather than guessing.
43625
44045
  ownerKindFor: (tabId) => registryForDownloads?.get(tabId)?.owner ?? null,
44046
+ // The human download destination (LOH-13 slice C): the person's
44047
+ // own downloads folder, read from the app at decision time. Main
44048
+ // composes it here for the same reason the reveal channel takes
44049
+ // no path: no renderer or page string ever becomes a directory.
44050
+ humanDownloadDir: () => electron.app.getPath("downloads"),
44051
+ // NO `humanDeadlineMs` WIRING (review round 2, MAJOR): the wall-clock
44052
+ // budget this used to pass settled — and cancelled, discarding the
44053
+ // partial — a legitimate in-flight human download at 120 s. A human
44054
+ // capture now ends only on a guard, the quiet window, or the tab.
44055
+ // `COMMAND_TIMEOUTS_S.download` remains the AGENT call's own budget,
44056
+ // passed by its caller and unchanged.
43626
44057
  log: log2,
43627
44058
  onActivity: () => {
43628
44059
  if (options.window.isDestroyed()) return;
@@ -45640,7 +46071,13 @@ function miniGlassPlan(input) {
45640
46071
  function miniDocumentState(state2, launchedGlassOn) {
45641
46072
  return {
45642
46073
  ...state2,
45643
- restartRequired: !state2.capability.liveToggle && state2.active !== launchedGlassOn
46074
+ /*
46075
+ * Same rule as the main window's `stateFor`: a restart is only pending for a
46076
+ * SUPPORTED change that cannot be applied live. Without the `supported` term a
46077
+ * capability drop (`mechanism: "none"`, also `liveToggle: false`) reads as a
46078
+ * restart-pending change in the popout as well.
46079
+ */
46080
+ restartRequired: state2.capability.supported && !state2.capability.liveToggle && state2.active !== launchedGlassOn
45644
46081
  };
45645
46082
  }
45646
46083
  function createMiniView(options) {
@@ -53901,21 +54338,46 @@ function attachWindowChrome(options) {
53901
54338
  function windowGlassFilePath(userDataDir) {
53902
54339
  return path$1.join(userDataDir, WINDOW_GLASS_FILE);
53903
54340
  }
54341
+ let gpuInfoReported = false;
54342
+ electron.app.on("gpu-info-update", () => {
54343
+ gpuInfoReported = true;
54344
+ });
54345
+ function readGlassEnvironmentFacts() {
54346
+ const facts = {};
54347
+ try {
54348
+ facts.accelerationDisabledByLaunch = electron.app.commandLine.hasSwitch("disable-gpu");
54349
+ } catch {
54350
+ facts.accelerationDisabledByLaunch = false;
54351
+ }
54352
+ try {
54353
+ facts.hardwareAcceleration = electron.app.isHardwareAccelerationEnabled();
54354
+ } catch {
54355
+ facts.hardwareAcceleration = null;
54356
+ }
54357
+ try {
54358
+ const status2 = gpuInfoReported ? electron.app.getGPUFeatureStatus() : null;
54359
+ facts.gpu = status2 ? {
54360
+ compositing: status2.gpu_compositing,
54361
+ rasterization: status2.rasterization
54362
+ } : null;
54363
+ } catch {
54364
+ facts.gpu = null;
54365
+ }
54366
+ return facts;
54367
+ }
53904
54368
  function readPersistedWindowGlass(userDataDir, capability) {
53905
54369
  const raw = readJson(
53906
54370
  windowGlassFilePath(userDataDir)
53907
54371
  );
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
- };
54372
+ return resolveStoredGlass(raw, capability);
53913
54373
  }
53914
54374
  function resolveLaunchWindowGlass(input) {
54375
+ const environment = input.environment ?? readGlassEnvironmentFacts();
53915
54376
  const capability = glassCapabilityFor({
53916
54377
  platform: input.platform ?? process.platform,
53917
54378
  osVersion: input.osVersion ?? process.getSystemVersion(),
53918
- reducedTransparency: input.reducedTransparency ?? electron.nativeTheme.prefersReducedTransparency
54379
+ reducedTransparency: input.reducedTransparency ?? electron.nativeTheme.prefersReducedTransparency,
54380
+ environment
53919
54381
  });
53920
54382
  let persisted = null;
53921
54383
  try {
@@ -53923,13 +54385,16 @@ function resolveLaunchWindowGlass(input) {
53923
54385
  } catch {
53924
54386
  persisted = null;
53925
54387
  }
53926
- const settings = persisted?.settings ?? DEFAULT_GLASS_SETTINGS;
54388
+ const settings = persisted?.settings ?? defaultGlassSettings(capability);
54389
+ const explicit = persisted?.explicit ?? [];
53927
54390
  const scheme = persisted?.scheme ?? "dark";
53928
54391
  return {
53929
54392
  capability,
53930
54393
  settings,
54394
+ explicit,
53931
54395
  scheme,
53932
- state: stateFor(settings, capability)
54396
+ state: stateFor(settings, capability),
54397
+ environment
53933
54398
  };
53934
54399
  }
53935
54400
  const stateFor = (settings, capability, launchedActive = glassIsActive(settings, capability)) => {
@@ -53938,7 +54403,16 @@ const stateFor = (settings, capability, launchedActive = glassIsActive(settings,
53938
54403
  capability,
53939
54404
  settings,
53940
54405
  active,
53941
- restartRequired: !capability.liveToggle && active !== launchedActive
54406
+ /*
54407
+ * A RESTART IS ONLY PENDING WHEN THE EFFECT IS AVAILABLE AND NOT LIVE.
54408
+ * `liveToggle: false` is also true of `mechanism: "none"`, so a capability
54409
+ * DROP (GPU software fallback, backdrop-filter report, Reduce transparency)
54410
+ * satisfied the old expression and told the user to restart while the window
54411
+ * had already fallen back to opaque. The unsupported state has its own
54412
+ * cause line; this notice is only for a supported change that needs a new
54413
+ * window (QA Q-1 / design D2).
54414
+ */
54415
+ restartRequired: capability.supported && !capability.liveToggle && active !== launchedActive
53942
54416
  };
53943
54417
  };
53944
54418
  function attachWindowGlass(options) {
@@ -53946,6 +54420,8 @@ function attachWindowGlass(options) {
53946
54420
  let capability = options.initial.capability;
53947
54421
  let settings = options.initial.settings;
53948
54422
  let scheme = options.initial.scheme;
54423
+ const explicit = new Set(options.initial.explicit);
54424
+ let environment = { ...options.initial.environment };
53949
54425
  const launchedActive = glassIsActive(settings, capability);
53950
54426
  let pinnedTheme = false;
53951
54427
  let appliedMechanism = launchedActive ? capability.mechanism : "none";
@@ -53989,7 +54465,17 @@ function attachWindowGlass(options) {
53989
54465
  }
53990
54466
  };
53991
54467
  const persist = () => {
53992
- if (!writeJsonAtomic(windowGlassFilePath(userDataDir), { settings, scheme }))
54468
+ if (!writeJsonAtomic(windowGlassFilePath(userDataDir), {
54469
+ /*
54470
+ * Versioned and per-field: the next launch fills only what this list does
54471
+ * not name from its own defaults. Writing the full `settings` object alone
54472
+ * is what made an untouched OFF indistinguishable from a chosen one.
54473
+ */
54474
+ version: GLASS_FILE_VERSION,
54475
+ settings,
54476
+ explicit: GLASS_SETTINGS_FIELDS.filter((field) => explicit.has(field)),
54477
+ scheme
54478
+ }))
53993
54479
  log2(
53994
54480
  "[window-glass] the setting could not be saved - it applies to this run and will not survive a restart"
53995
54481
  );
@@ -54010,6 +54496,11 @@ function attachWindowGlass(options) {
54010
54496
  (event, patch) => {
54011
54497
  if (!fromOwnWindow(event)) return null;
54012
54498
  settings = sanitizeGlassSettings(patch, capability, settings);
54499
+ if (patch !== null && typeof patch === "object") {
54500
+ for (const field of Object.keys(patch))
54501
+ if (GLASS_SETTINGS_FIELDS.includes(field))
54502
+ explicit.add(field);
54503
+ }
54013
54504
  applyToWindow();
54014
54505
  persist();
54015
54506
  push2();
@@ -54028,23 +54519,47 @@ function attachWindowGlass(options) {
54028
54519
  persist();
54029
54520
  return true;
54030
54521
  });
54031
- const onNativeThemeUpdated = () => {
54522
+ electron.ipcMain.handle(WINDOW_GLASS_RENDERER_CHANNEL, (event, report) => {
54523
+ if (!fromOwnWindow(event)) return null;
54524
+ if (report !== null && typeof report === "object") {
54525
+ const value = report.backdropFilter;
54526
+ if (typeof value === "boolean")
54527
+ environment = { ...environment, backdropFilter: value };
54528
+ }
54529
+ reevaluate();
54530
+ return current();
54531
+ });
54532
+ function reevaluate() {
54533
+ environment = {
54534
+ ...readGlassEnvironmentFacts(),
54535
+ ...environment.backdropFilter === void 0 ? {} : { backdropFilter: environment.backdropFilter }
54536
+ };
54032
54537
  const next = glassCapabilityFor({
54033
54538
  platform: process.platform,
54034
54539
  osVersion: process.getSystemVersion(),
54035
- reducedTransparency: electron.nativeTheme.prefersReducedTransparency
54540
+ reducedTransparency: electron.nativeTheme.prefersReducedTransparency,
54541
+ environment
54036
54542
  });
54037
- if (next.supported === capability.supported && next.reason === capability.reason)
54543
+ if (next.supported === capability.supported && next.reason === capability.reason && next.mechanism === capability.mechanism)
54038
54544
  return;
54545
+ const wasSupported = capability.supported;
54039
54546
  capability = next;
54040
54547
  settings = sanitizeGlassSettings(settings, capability, settings);
54548
+ if (next.supported && !wasSupported && !explicit.has("enabled"))
54549
+ settings.enabled = defaultGlassSettings(next).enabled;
54041
54550
  applyToWindow();
54042
54551
  push2();
54043
54552
  log2(
54044
54553
  `[window-glass] capability changed supported=${next.supported} reason=${next.reason}`
54045
54554
  );
54046
- };
54555
+ }
54556
+ const onNativeThemeUpdated = () => reevaluate();
54047
54557
  electron.nativeTheme.on("updated", onNativeThemeUpdated);
54558
+ electron.app.on("gpu-info-update", reevaluate);
54559
+ const onChildProcessGone = (_event, details) => {
54560
+ if (details?.type === "GPU") setTimeout(reevaluate, 0);
54561
+ };
54562
+ electron.app.on("child-process-gone", onChildProcessGone);
54048
54563
  if (glassIsActive(settings, capability)) {
54049
54564
  electron.nativeTheme.themeSource = scheme;
54050
54565
  pinnedTheme = true;
@@ -54055,7 +54570,10 @@ function attachWindowGlass(options) {
54055
54570
  electron.ipcMain.removeHandler(WINDOW_GLASS_GET_CHANNEL);
54056
54571
  electron.ipcMain.removeHandler(WINDOW_GLASS_SET_CHANNEL);
54057
54572
  electron.ipcMain.removeHandler(WINDOW_GLASS_SCHEME_CHANNEL);
54573
+ electron.ipcMain.removeHandler(WINDOW_GLASS_RENDERER_CHANNEL);
54058
54574
  electron.nativeTheme.removeListener("updated", onNativeThemeUpdated);
54575
+ electron.app.removeListener("gpu-info-update", reevaluate);
54576
+ electron.app.removeListener("child-process-gone", onChildProcessGone);
54059
54577
  if (pinnedTheme) electron.nativeTheme.themeSource = "system";
54060
54578
  }
54061
54579
  };
@@ -54808,6 +55326,7 @@ const authPopupWindowOptions = {
54808
55326
  disableDialogs: true
54809
55327
  }
54810
55328
  };
55329
+ let glassGateLogged = false;
54811
55330
  function createWindow(initialSession = null, openCatalogue = false, request = ownLaunchRequest()) {
54812
55331
  const chrome = resolveLaunchWindowChrome({
54813
55332
  argv: process.argv,
@@ -54822,6 +55341,13 @@ function createWindow(initialSession = null, openCatalogue = false, request = ow
54822
55341
  const glass = resolveLaunchWindowGlass({
54823
55342
  userDataDir: electron.app.getPath("userData")
54824
55343
  });
55344
+ if (!glassGateLogged) {
55345
+ glassGateLogged = true;
55346
+ logger.info(
55347
+ glassGateLine(glass.capability, glass.settings, process.platform),
55348
+ LogFileType.BACKEND
55349
+ );
55350
+ }
54825
55351
  const mainWindow2 = new electron.BrowserWindow({
54826
55352
  /*
54827
55353
  * The frame, per platform, from a pure function so the matrix is exercised on