local-operator-ui 0.31.27 → 0.31.29
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.
- package/out/main/index.js +1031 -63
- package/out/preload/index.js +37 -0
- package/out/renderer/assets/{_basePickBy-oM-LmypH.js → _basePickBy-DL33QdW4.js} +1 -1
- package/out/renderer/assets/{_baseUniq-CfyEpPZd.js → _baseUniq-CBRLNfCa.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DIBks4X5.js → agent-details-page-JNorVbUk.js} +2 -2
- package/out/renderer/assets/{agent-hub-page-oV4gEqC4.js → agent-hub-page-B53zgVHZ.js} +3 -3
- package/out/renderer/assets/agents-page-D5VssooM.js +7 -0
- package/out/renderer/assets/arc-C1D2LaN2.js +1 -0
- package/out/renderer/assets/architectureDiagram-IEHRJDOE-Dm1oPlGu.js +36 -0
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-Cn1dvDZU.js → blockDiagram-JOT3LUYC-CeNuHJ3J.js} +2 -2
- package/out/renderer/assets/{book-open-P6KBj9Du.js → book-open-C2GJZCxM.js} +1 -1
- package/out/renderer/assets/browser-page-DHwPmeL7.js +1 -0
- package/out/renderer/assets/browser-webauthn-prompt-Cy_dHOdv.js +1 -0
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-B8F1XGHT.js → c4Diagram-VJAJSXHY-Cieumjrn.js} +4 -4
- package/out/renderer/assets/channel-BipwWMab.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-CVoXO2u6.js → chunk-4BMEZGHF-CeIWDTcE.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-LFo0s6P7.js → chunk-A2AXSNBT-nkZ-ZEU8.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-B8s36YJo.js → chunk-AEK57VVT-D3HkgezL.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-BKkRpyOx.js → chunk-D6G4REZN-BZjnLSCT.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-qoB87AlE.js → chunk-RZ5BOZE2-RMzqoQiC.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-BACgoEDe.js → chunk-XZIHB7SX-DJl2yrB3.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-C7VPQGdP.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-C7VPQGdP.js +1 -0
- package/out/renderer/assets/clone-fWuYSDG8.js +1 -0
- package/out/renderer/assets/{console-mirror-fXJ5fMxl.js → console-mirror-BhuN6Ypv.js} +1 -1
- package/out/renderer/assets/{consoleCapture-Df4uIOqu.js → consoleCapture-BNipqRoj.js} +1 -1
- package/out/renderer/assets/conversation-input-sync-BoRhB7VS.js +918 -0
- package/out/renderer/assets/conversation-input-sync-CIGg7Rga.css +1 -0
- package/out/renderer/assets/dagre-OKDRZEBW-DbaX-Vfd.js +4 -0
- package/out/renderer/assets/diagram-SSKATNLV-BP9pThlk.js +43 -0
- package/out/renderer/assets/diagram-VNBRO52H-BM5HUD5o.js +24 -0
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-CGjlhqbg.js → erDiagram-Q7BY3M3F-CUqUCymj.js} +2 -2
- package/out/renderer/assets/error-boundary-CAyz2bjY.js +126 -0
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-rb1I18B8.js → flowDiagram-4HSFHLVR-BPz65dD6.js} +2 -2
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-D-bSpERZ.js → ganttDiagram-APWFNJXF-ccu__hit.js} +2 -2
- package/out/renderer/assets/gitGraphDiagram-7IBYFJ6S-Ct9B7LDY.js +65 -0
- package/out/renderer/assets/{graph-BkNT14na.js → graph-DIsum2dA.js} +1 -1
- package/out/renderer/assets/index-BWjn66XI.js +1 -0
- package/out/renderer/assets/index-Bgxehgfc.js +784 -0
- package/out/renderer/assets/index-CCRonkWc.css +1 -0
- package/out/renderer/assets/{index-_dbvHdgc.js → index-CTnUXmyg.js} +1 -1
- package/out/renderer/assets/index-DHKnfVbX.css +1 -0
- package/out/renderer/assets/index-FX4vEaoW.js +7 -0
- package/out/renderer/assets/infoDiagram-PH2N3AL5-Bey34vow.js +2 -0
- package/out/renderer/assets/installer-Bu8Z9JPe.js +1 -0
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-B5bji5kb.js → journeyDiagram-U35MCT3I-jn9ZwYXW.js} +6 -6
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BGi8PMEB.js → kanban-definition-NDS4AKOZ-C-wz8kvA.js} +4 -4
- package/out/renderer/assets/{layout-D3rwe0Wt.js → layout-BREzqltz.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-DnpWTuf8.js → legacy-agents-page-CO-TGqR5.js} +4 -4
- package/out/renderer/assets/{mermaid.core-CHiYins7.js → mermaid.core-8-ikrzww.js} +5 -5
- package/out/renderer/assets/mesh-page-0CSX8EG-.js +1 -0
- package/out/renderer/assets/mindmap-definition-ALO5MXBD-CJzxvyVl.js +95 -0
- package/out/renderer/assets/mini-DIRe7eu8.js +1 -0
- package/out/renderer/assets/org-surface-gate-C1t_jjzP.js +1 -0
- package/out/renderer/assets/{page-header-B3kbzqCp.js → page-header-CKzo9EwH.js} +1 -1
- package/out/renderer/assets/pieDiagram-IB7DONF6-B38MVZvk.js +30 -0
- package/out/renderer/assets/projects-page-4aBbeqHa.js +55 -0
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-BZkLgvoE.js → quadrantDiagram-7GDLP6J5-CGwbmj-X.js} +4 -4
- package/out/renderer/assets/{radar-MK3ICKWK-QB0E4nFC.js → radar-MK3ICKWK-0J95rurP.js} +1 -1
- package/out/renderer/assets/radient-auth-buttons-DlO_vXxS.js +1 -0
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-Cb1rqfe3.js → requirementDiagram-KVF5MWMF-Csqc9PAd.js} +3 -3
- package/out/renderer/assets/sankeyDiagram-QLVOVGJD-Lm7B9S0v.js +10 -0
- package/out/renderer/assets/{schedules-page-DnHKOQij.js → schedules-page-BI6mduTY.js} +12 -12
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Bh-yCDC0.js → sequenceDiagram-X6HHIX6F-Dw8RM08f.js} +2 -2
- package/out/renderer/assets/settings-page-BsIAdS9g.js +292 -0
- package/out/renderer/assets/{settings-section-C9etJXA9.js → settings-section-DlTh8xfK.js} +1 -1
- package/out/renderer/assets/stateDiagram-DGXRK772-DHKpJy8t.js +1 -0
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CFuWzCss.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-pgK_gxyk.js → timeline-definition-BDJGKUSR-BxyX64Ja.js} +2 -2
- package/out/renderer/assets/ui-preferences-store-C5PakFPc.js +1 -0
- package/out/renderer/assets/{use-download-agent-mutation-CXw_g4Rv.js → use-download-agent-mutation-CeaoIrQs.js} +3 -3
- package/out/renderer/assets/use-memberships-query-D95x-vrx.js +1 -0
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CYYxIZAM.js → xychartDiagram-VJFVF3MP-PNx7e-Lx.js} +6 -6
- package/out/renderer/console-capture.html +5 -5
- package/out/renderer/index.html +9 -9
- package/out/renderer/installer.html +4 -5
- package/out/renderer/mini.html +7 -6
- package/package.json +9 -3
- package/out/renderer/assets/agents-page-CZrNPorc.js +0 -7
- package/out/renderer/assets/aida-control-CQx1CGx-.js +0 -50
- package/out/renderer/assets/arc-DXJiD7SC.js +0 -1
- package/out/renderer/assets/architectureDiagram-IEHRJDOE-VHM5BLyo.js +0 -36
- package/out/renderer/assets/browser-page-D6eiItUp.js +0 -1
- package/out/renderer/assets/browser-webauthn-prompt-CByfVm08.js +0 -1
- package/out/renderer/assets/channel-DZQWNfux.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-jw9F6ZK-.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-jw9F6ZK-.js +0 -1
- package/out/renderer/assets/clone-DzWAH3PE.js +0 -1
- package/out/renderer/assets/dagre-OKDRZEBW-CLMw1D7m.js +0 -4
- package/out/renderer/assets/diagram-SSKATNLV-plMydQ5N.js +0 -43
- package/out/renderer/assets/diagram-VNBRO52H-BptPse7H.js +0 -24
- package/out/renderer/assets/error-boundary-D3Um1PIl.js +0 -6
- package/out/renderer/assets/gitGraphDiagram-7IBYFJ6S-DDJQIXgd.js +0 -65
- package/out/renderer/assets/index-2qmFWi_F.css +0 -1
- package/out/renderer/assets/index-BXamXVGw.js +0 -7
- package/out/renderer/assets/index-D3ICBbhi.js +0 -1642
- package/out/renderer/assets/index-DHcQTmYa.js +0 -1
- package/out/renderer/assets/index-DuUnXH9L.css +0 -1
- package/out/renderer/assets/infoDiagram-PH2N3AL5-Bg2dnJEw.js +0 -2
- package/out/renderer/assets/installer-D-IQpqXR.js +0 -1
- package/out/renderer/assets/mesh-page-B2_ZtHTQ.js +0 -1
- package/out/renderer/assets/mindmap-definition-ALO5MXBD-BwO55Ck2.js +0 -95
- package/out/renderer/assets/mini-D7VYY0xP.js +0 -1
- package/out/renderer/assets/org-surface-gate-Da1VmZbG.js +0 -1
- package/out/renderer/assets/pieDiagram-IB7DONF6-j0i7dQ2D.js +0 -30
- package/out/renderer/assets/projects-page-f-tkHojn.js +0 -35
- package/out/renderer/assets/radient-auth-buttons-2wnsv7f6.js +0 -1
- package/out/renderer/assets/sankeyDiagram-QLVOVGJD-ST_5m9Zy.js +0 -10
- package/out/renderer/assets/settings-page-D1dF4GZE.js +0 -292
- package/out/renderer/assets/stateDiagram-DGXRK772-D6MGh56o.js +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Dvv5Bcoz.js +0 -1
- package/out/renderer/assets/textarea-DeXGhzIK.js +0 -121
- package/out/renderer/assets/ui-preferences-store-BLiDaCIL.js +0 -1
- package/out/renderer/assets/use-memberships-query-BGPQ6jDl.js +0 -1
package/out/main/index.js
CHANGED
|
@@ -62,12 +62,14 @@ function pairingHasRemedy(cause) {
|
|
|
62
62
|
const id$1 = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
63
63
|
const settingKey = zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/);
|
|
64
64
|
const secret = zod.z.string().min(1).max(32768);
|
|
65
|
-
const
|
|
65
|
+
const sessionIdPattern = /^[a-f0-9]{12}$/;
|
|
66
|
+
const sessionId = zod.z.string().regex(sessionIdPattern);
|
|
66
67
|
const skillCwd = zod.z.string().min(1).max(4096);
|
|
67
68
|
const mcpServerName = zod.z.string().regex(/^[A-Za-z0-9_.:-]{1,100}$/);
|
|
68
69
|
const mcpSecretReference = zod.z.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/);
|
|
69
70
|
const mcpOperationId = zod.z.string().regex(/^[a-f0-9]{32}$/);
|
|
70
|
-
const
|
|
71
|
+
const mcpCatalogCwdPattern = /^(\/|[A-Za-z]:[\\/])/;
|
|
72
|
+
const mcpCatalogCwd = zod.z.string().min(1).max(4096).regex(mcpCatalogCwdPattern);
|
|
71
73
|
const SUBSCRIPTION_ID_PATTERN = /^[a-f0-9]{32}$/;
|
|
72
74
|
const requestId = zod.z.string().regex(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/);
|
|
73
75
|
const meshId = zod.z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
|
|
@@ -100,7 +102,17 @@ const profileFields = zod.z.object({
|
|
|
100
102
|
instructions: zod.z.string().max(8e3).optional(),
|
|
101
103
|
tools: zod.z.array(zod.z.string().min(1).max(128)).max(256).optional(),
|
|
102
104
|
effort: zod.z.string().max(64).optional(),
|
|
103
|
-
delegate: zod.z.boolean().optional()
|
|
105
|
+
delegate: zod.z.boolean().optional(),
|
|
106
|
+
/*
|
|
107
|
+
* The action class (`reactive` | `proactive`), the field the Class control
|
|
108
|
+
* writes. An ENUM rather than a free string, because the route takes a
|
|
109
|
+
* `Literal` and a spelling this schema let through would be a 422 the
|
|
110
|
+
* caller could not tell from a bug — the same reasoning `scheduleUnit`
|
|
111
|
+
* above states. Optional like every other field here: an omitted key means
|
|
112
|
+
* "leave the class alone" on an update, which is what lets the control send
|
|
113
|
+
* the class and nothing else.
|
|
114
|
+
*/
|
|
115
|
+
action_class: zod.z.enum(["reactive", "proactive"]).optional()
|
|
104
116
|
}).strict();
|
|
105
117
|
const teamFields = zod.z.object({
|
|
106
118
|
name: zod.z.string().min(1).max(64).optional(),
|
|
@@ -266,6 +278,25 @@ const PROJECT_MILESTONE_NAME_MAX_CHARS = 80;
|
|
|
266
278
|
const hubItemKind = zod.z.enum(["agent", "team"]);
|
|
267
279
|
const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
268
280
|
zod.z.object({ op: zod.z.literal("capabilities") }).strict(),
|
|
281
|
+
/*
|
|
282
|
+
* THE MACHINE'S RUNTIME ROSTER, read for the straggler census a completed
|
|
283
|
+
* server update reports (`GET /v1/desktop/runtimes`). A GET with a fixed
|
|
284
|
+
* shape, so the whole request is the op.
|
|
285
|
+
*
|
|
286
|
+
* It reads the INVENTORY (`probe=false` in the path composer): the census
|
|
287
|
+
* asks every row what build it booted with, and the route's per-row loopback
|
|
288
|
+
* connects answer a different question ("did it answer?") that nothing here
|
|
289
|
+
* acts on. Skipping them also keeps the response inside the control budget
|
|
290
|
+
* without depending on the probe pool's own deadline logic.
|
|
291
|
+
*
|
|
292
|
+
* THE CONTROL BUDGET IS THE RIGHT ONE, and it is a bound rather than a hope:
|
|
293
|
+
* the route's own ceiling is the two external reads in front of the
|
|
294
|
+
* composition (5 s process table + 3 s socket table, stated in `roster.py`),
|
|
295
|
+
* and `probe=false` skips the per-row connects entirely - so the worst case
|
|
296
|
+
* is ~8 s, well under the 20 s control deadline. It is NOT on the long-read
|
|
297
|
+
* list: nothing here fans out to a provider or walks a ledger.
|
|
298
|
+
*/
|
|
299
|
+
zod.z.object({ op: zod.z.literal("runtimes.list") }).strict(),
|
|
269
300
|
zod.z.object({ op: zod.z.literal("profiles.list") }).strict(),
|
|
270
301
|
zod.z.object({ op: zod.z.literal("profiles.get"), name: profileName }).strict(),
|
|
271
302
|
zod.z.object({ op: zod.z.literal("profiles.install"), name: profileName, requestId }).strict(),
|
|
@@ -755,11 +786,38 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
755
786
|
zod.z.object({
|
|
756
787
|
op: zod.z.literal("sessions.answer"),
|
|
757
788
|
sessionId,
|
|
758
|
-
|
|
759
|
-
|
|
789
|
+
/**
|
|
790
|
+
* The owner epoch, for the two GATE shapes. Optional here because a queued
|
|
791
|
+
* ask has no epoch to give — `ask_id` is the other selector and the checks
|
|
792
|
+
* below require exactly one of the pair.
|
|
793
|
+
*/
|
|
794
|
+
epoch: id$1.optional(),
|
|
795
|
+
requestId: id$1.optional(),
|
|
760
796
|
value: zod.z.string().max(32768).optional(),
|
|
761
797
|
approved: zod.z.boolean().optional(),
|
|
762
|
-
questionIndex: zod.z.number().int().min(0).optional()
|
|
798
|
+
questionIndex: zod.z.number().int().min(0).optional(),
|
|
799
|
+
/**
|
|
800
|
+
* The queued ask's id (`a-3f9c`). Its presence selects the THIRD body shape —
|
|
801
|
+
* `{ask_id, answers}` or `{ask_id, decline}` — and, per the backend's own
|
|
802
|
+
* contract, makes `epoch` irrelevant rather than optional-but-checked: an ask
|
|
803
|
+
* outlives the owner that queued it, so requiring an epoch would refuse
|
|
804
|
+
* exactly the case the feature exists for (a cold session whose asks are
|
|
805
|
+
* still open). The single-winner rule that the epoch check used to provide
|
|
806
|
+
* now lives on the ask log, where the first `answered` event wins.
|
|
807
|
+
*/
|
|
808
|
+
askId: id$1.optional(),
|
|
809
|
+
/**
|
|
810
|
+
* The WHOLE ask's answers, keyed by question id, each a list because a
|
|
811
|
+
* question may be multi-select.
|
|
812
|
+
*
|
|
813
|
+
* One atomic body rather than a per-question stream: the blocking path
|
|
814
|
+
* answered one question at a time over the wire, and a client that died
|
|
815
|
+
* part-way left an ask half-settled — the exact state the atomic submit
|
|
816
|
+
* exists to make unrepresentable.
|
|
817
|
+
*/
|
|
818
|
+
answers: zod.z.record(zod.z.string(), zod.z.array(zod.z.string().max(32768))).optional(),
|
|
819
|
+
/** "No answer — decide yourself", the explicit form of today's Esc. */
|
|
820
|
+
decline: zod.z.boolean().optional()
|
|
763
821
|
}).strict(),
|
|
764
822
|
zod.z.object({
|
|
765
823
|
op: zod.z.literal("sessions.seen"),
|
|
@@ -1234,7 +1292,25 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
1234
1292
|
sessionId,
|
|
1235
1293
|
requestId,
|
|
1236
1294
|
message: zod.z.string().max(2e5).optional(),
|
|
1237
|
-
|
|
1295
|
+
/*
|
|
1296
|
+
* WHERE THE COPY STOPS. `next_safe` is the historical form and the default
|
|
1297
|
+
* (the whole committed conversation, waiting for a turn boundary);
|
|
1298
|
+
* `at_entry` cuts through `entryId` and needs no boundary, because a named
|
|
1299
|
+
* point is already committed.
|
|
1300
|
+
*
|
|
1301
|
+
* `entryId` is bounded rather than patterned for the route's reason -
|
|
1302
|
+
* entry ids are minted in more than one shape and existence in the
|
|
1303
|
+
* CONVERSATION is what decides whether one is real (the route answers an
|
|
1304
|
+
* unknown or foreign id with a refusal, never a fork of the wrong
|
|
1305
|
+
* history) - so this client validates the shape it can and lets the
|
|
1306
|
+
* backend adjudicate the rest.
|
|
1307
|
+
*
|
|
1308
|
+
* BOTH ARE OPTIONAL AND ABSENT IS THE OLD CALL, key for key: a typed
|
|
1309
|
+
* `/fork` and the palette's fork send neither, and the body builder below
|
|
1310
|
+
* omits `entry_id` entirely rather than sending it as null.
|
|
1311
|
+
*/
|
|
1312
|
+
boundary: zod.z.enum(["next_safe", "at_entry"]).optional(),
|
|
1313
|
+
entryId: zod.z.string().max(128).optional()
|
|
1238
1314
|
}).strict(),
|
|
1239
1315
|
zod.z.object({
|
|
1240
1316
|
op: zod.z.literal("sessions.stop"),
|
|
@@ -1758,6 +1834,20 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
1758
1834
|
key: projectKey,
|
|
1759
1835
|
name: zod.z.string().min(1).max(PROJECT_MILESTONE_NAME_MAX_CHARS)
|
|
1760
1836
|
}).strict(),
|
|
1837
|
+
/*
|
|
1838
|
+
* The check-in fan-out (`POST /v1/desktop/projects/{project}/request-update`):
|
|
1839
|
+
* ask every linked session to post a progress update. APPENDED to the union
|
|
1840
|
+
* like its siblings, and gated by its OWN capability key
|
|
1841
|
+
* (`features.projects_request_update`) rather than a bump of `projects`: the
|
|
1842
|
+
* tab renders perfectly well against a backend that cannot ask its sessions
|
|
1843
|
+
* for anything, so the version would hide a working surface behind an update
|
|
1844
|
+
* it does not need (the `session_search` rule above). The key is the row's
|
|
1845
|
+
* address (id or name) - the same shape every other projects op takes.
|
|
1846
|
+
*/
|
|
1847
|
+
zod.z.object({
|
|
1848
|
+
op: zod.z.literal("projects.request_update"),
|
|
1849
|
+
key: projectKey
|
|
1850
|
+
}).strict(),
|
|
1761
1851
|
/*
|
|
1762
1852
|
* AIDA'S CONTROL PLANE: one read and one control op on the same route
|
|
1763
1853
|
* (`/v1/desktop/aida`), because the rail's row and the composer's `/aida`
|
|
@@ -1785,6 +1875,48 @@ const desktopRequestUnion = zod.z.discriminatedUnion("op", [
|
|
|
1785
1875
|
]);
|
|
1786
1876
|
const desktopRequestSchema = desktopRequestUnion.superRefine(
|
|
1787
1877
|
(request, ctx) => {
|
|
1878
|
+
if (request.op === "sessions.answer") {
|
|
1879
|
+
if (request.askId !== void 0) {
|
|
1880
|
+
if (!request.askId)
|
|
1881
|
+
ctx.addIssue({
|
|
1882
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1883
|
+
message: "ask_id must be a non-empty string",
|
|
1884
|
+
path: ["askId"]
|
|
1885
|
+
});
|
|
1886
|
+
if (request.decline !== true && !request.answers)
|
|
1887
|
+
ctx.addIssue({
|
|
1888
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1889
|
+
message: "A queued-ask answer needs answers or decline",
|
|
1890
|
+
path: ["answers"]
|
|
1891
|
+
});
|
|
1892
|
+
else if (request.decline === true && request.answers !== void 0)
|
|
1893
|
+
ctx.addIssue({
|
|
1894
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1895
|
+
message: "Supply either answers or decline",
|
|
1896
|
+
path: ["decline"]
|
|
1897
|
+
});
|
|
1898
|
+
else if (request.answers !== void 0 && Object.keys(request.answers).length === 0)
|
|
1899
|
+
ctx.addIssue({
|
|
1900
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1901
|
+
message: "answers must name at least one question",
|
|
1902
|
+
path: ["answers"]
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (!request.epoch)
|
|
1907
|
+
ctx.addIssue({
|
|
1908
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1909
|
+
message: "An epoch is required to answer a gate",
|
|
1910
|
+
path: ["epoch"]
|
|
1911
|
+
});
|
|
1912
|
+
if (!request.requestId)
|
|
1913
|
+
ctx.addIssue({
|
|
1914
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1915
|
+
message: "A request id is required to answer a gate",
|
|
1916
|
+
path: ["requestId"]
|
|
1917
|
+
});
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1788
1920
|
if (request.op === "sessions.create" && request.purpose === void 0) {
|
|
1789
1921
|
if (request.cwd === void 0) {
|
|
1790
1922
|
ctx.addIssue({
|
|
@@ -1980,6 +2112,9 @@ const READ_ONLY_OPS = /* @__PURE__ */ new Set([
|
|
|
1980
2112
|
"profiles.get",
|
|
1981
2113
|
"profiles.list",
|
|
1982
2114
|
"providers.list",
|
|
2115
|
+
// A reader that changes nothing (the route's own docstring): the straggler
|
|
2116
|
+
// census, and the app re-reads it rather than caching a stale count.
|
|
2117
|
+
"runtimes.list",
|
|
1983
2118
|
"sessions.aside.get",
|
|
1984
2119
|
"sessions.checkpoints",
|
|
1985
2120
|
"sessions.failovers",
|
|
@@ -2071,6 +2206,8 @@ function desktopEndpoint(request) {
|
|
|
2071
2206
|
switch (request.op) {
|
|
2072
2207
|
case "capabilities":
|
|
2073
2208
|
return { path: "/v1/capabilities", method: "GET" };
|
|
2209
|
+
case "runtimes.list":
|
|
2210
|
+
return { path: "/v1/desktop/runtimes?probe=false", method: "GET" };
|
|
2074
2211
|
/*
|
|
2075
2212
|
* The mesh reads. Both are plain GETs with no parameters at all - the backend
|
|
2076
2213
|
* reads THIS device's own relay, so there is nothing for the client to scope it
|
|
@@ -2404,6 +2541,16 @@ function desktopEndpoint(request) {
|
|
|
2404
2541
|
}
|
|
2405
2542
|
};
|
|
2406
2543
|
case "sessions.answer":
|
|
2544
|
+
if (request.askId !== void 0)
|
|
2545
|
+
return {
|
|
2546
|
+
path: `/v1/desktop/sessions/${request.sessionId}/answers`,
|
|
2547
|
+
method: "POST",
|
|
2548
|
+
body: {
|
|
2549
|
+
ask_id: request.askId,
|
|
2550
|
+
answers: request.answers,
|
|
2551
|
+
decline: request.decline
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2407
2554
|
return {
|
|
2408
2555
|
path: `/v1/desktop/sessions/${request.sessionId}/answers`,
|
|
2409
2556
|
method: "POST",
|
|
@@ -2852,7 +2999,17 @@ function desktopEndpoint(request) {
|
|
|
2852
2999
|
body: {
|
|
2853
3000
|
request_id: request.requestId,
|
|
2854
3001
|
message: request.message,
|
|
2855
|
-
boundary: request.boundary
|
|
3002
|
+
boundary: request.boundary,
|
|
3003
|
+
/*
|
|
3004
|
+
* The route validates the two TOGETHER: `entry_id` with `next_safe`
|
|
3005
|
+
* is a 422, and `at_entry` without one is a 422. This builder does
|
|
3006
|
+
* not police that pairing (the schema above is what a renderer may
|
|
3007
|
+
* send; the route is what adjudicates it), but it does keep the
|
|
3008
|
+
* absent case byte-identical to the call that shipped before the
|
|
3009
|
+
* cut existed - `undefined` is dropped by `JSON.stringify`, so a
|
|
3010
|
+
* `next_safe` fork sends no `entry_id` key at all.
|
|
3011
|
+
*/
|
|
3012
|
+
entry_id: request.entryId
|
|
2856
3013
|
}
|
|
2857
3014
|
};
|
|
2858
3015
|
case "sessions.stop":
|
|
@@ -3086,6 +3243,11 @@ function desktopEndpoint(request) {
|
|
|
3086
3243
|
path: `/v1/desktop/projects/${encodeURIComponent(request.key)}/milestones/${encodeURIComponent(request.name)}`,
|
|
3087
3244
|
method: "DELETE"
|
|
3088
3245
|
};
|
|
3246
|
+
case "projects.request_update":
|
|
3247
|
+
return {
|
|
3248
|
+
path: `/v1/desktop/projects/${encodeURIComponent(request.key)}/request-update`,
|
|
3249
|
+
method: "POST"
|
|
3250
|
+
};
|
|
3089
3251
|
case "aida.status":
|
|
3090
3252
|
return { path: "/v1/desktop/aida", method: "GET" };
|
|
3091
3253
|
case "aida.control":
|
|
@@ -3108,6 +3270,9 @@ const MINI_VIEW_SUMMONED = "mini-view:summoned";
|
|
|
3108
3270
|
const MINI_VIEW_DISMISS = "mini-view:dismiss";
|
|
3109
3271
|
const MINI_VIEW_REGISTRATION = "mini-view:registration";
|
|
3110
3272
|
const MINI_VIEW_REGISTRATION_GET = "mini-view:registration-get";
|
|
3273
|
+
const MINI_VIEW_RESIZE = "mini-view:resize";
|
|
3274
|
+
const MINI_VIEW_DIALOG = "mini-view:dialog";
|
|
3275
|
+
const MINI_VIEW_PAINTED = "mini-view:painted";
|
|
3111
3276
|
const MINI_VIEW_DISMISS_REASONS = [
|
|
3112
3277
|
"escape",
|
|
3113
3278
|
"blur",
|
|
@@ -3119,6 +3284,10 @@ function isMiniViewDismissReason(value) {
|
|
|
3119
3284
|
}
|
|
3120
3285
|
const MINI_VIEW_WIDTH = 640;
|
|
3121
3286
|
const MINI_VIEW_HEIGHT = 168;
|
|
3287
|
+
const MINI_VIEW_MAX_HEIGHT = 560;
|
|
3288
|
+
function isMiniViewResizePayload(value) {
|
|
3289
|
+
return value !== null && typeof value === "object" && typeof value.height === "number" && Number.isFinite(value.height);
|
|
3290
|
+
}
|
|
3122
3291
|
const DEFAULT_QUICK_SEND_VALUE = "primary+alt+shift+space";
|
|
3123
3292
|
const FUNCTION_KEY_TOKEN = /^f([1-9]|1[0-9]|2[0-4])$/;
|
|
3124
3293
|
function isFunctionKeyToken(token) {
|
|
@@ -5724,9 +5893,12 @@ function resolveGlobalInstallPlan(input) {
|
|
|
5724
5893
|
* no cost at all before the click (review U3). It named the restart as its
|
|
5725
5894
|
* cost for the same reason - it was the one destructive thing this path
|
|
5726
5895
|
* did. It no longer is: on the generation layout the install lands beside
|
|
5727
|
-
* the running build and the server
|
|
5728
|
-
*
|
|
5729
|
-
*
|
|
5896
|
+
* the running build and the server MOVES ONTO IT as part of the update -
|
|
5897
|
+
* the in-place serve reload, verified by the serving process's own record
|
|
5898
|
+
* (a restart remains only the fallback for a reload that cannot be proven
|
|
5899
|
+
* to land, and even that cuts no turn) - so the cost is the install and
|
|
5900
|
+
* the wait, and no sentence here may describe a turn being dropped or
|
|
5901
|
+
* defer the move to some later idle.
|
|
5730
5902
|
*
|
|
5731
5903
|
* Source build - the same disclosure with the one difference that matters:
|
|
5732
5904
|
* this route DOES rewrite a tree a live runtime is reading, so its sentence
|
|
@@ -5745,7 +5917,7 @@ function resolveGlobalInstallPlan(input) {
|
|
|
5745
5917
|
* panels, and in the by-hand panel the next line was the version sentence,
|
|
5746
5918
|
* so the promise landed on the wrong line (review D5).
|
|
5747
5919
|
*/
|
|
5748
|
-
remedy: managed ? "The app installs the new build beside the one in use and
|
|
5920
|
+
remedy: managed ? "The app installs the new build beside the one in use and moves the server onto it immediately, so nothing in flight is cut off." : sourceRebuildRoute ? "Rebuilds this checkout with `lop-update`. The app waits for the turns running on this machine to finish first, and the rebuild then reinstalls this install in place - so a turn started while it runs can still be interrupted - and it can take up to half an hour. This install keeps reporting the checkout's version, not the release the app offered." : "This install predates the non-disruptive installer, so update it once from your terminal; that updater rewrites the shared environment in place, which can interrupt sessions mid-turn. The app manages updates after that.",
|
|
5749
5921
|
detail: `${detail}${managed ? provenance : sourceRebuildRoute ? " source build of this machine's checkout; an in-place rebuild." : provenance}`,
|
|
5750
5922
|
sourceBuild,
|
|
5751
5923
|
managedRoute: managed ? "entry-point" : sourceRebuildRoute ? "source-build" : null,
|
|
@@ -5850,8 +6022,10 @@ function serveRecord(pid, dir) {
|
|
|
5850
6022
|
}
|
|
5851
6023
|
function servingInstallReadings(record) {
|
|
5852
6024
|
const version = record?.version?.trim() ?? "";
|
|
6025
|
+
const instanceId = record?.instance_id?.trim() ?? "";
|
|
5853
6026
|
return {
|
|
5854
6027
|
bootVersion: version === "" ? null : version,
|
|
6028
|
+
instanceId: instanceId === "" ? null : instanceId,
|
|
5855
6029
|
prefix: record?.prefix?.trim() ?? "",
|
|
5856
6030
|
installKind: record?.install_kind?.trim() ?? "",
|
|
5857
6031
|
startedByApp: record?.desktop === true && (record.claim_key ?? "") === ""
|
|
@@ -10762,13 +10936,14 @@ class BackendServiceManager {
|
|
|
10762
10936
|
* Stop only the captured serve generation, including failed startups. A port
|
|
10763
10937
|
* or process name is not ownership. Unconfirmed exit blocks replacement.
|
|
10764
10938
|
*/
|
|
10765
|
-
async stop(isRestart = false) {
|
|
10939
|
+
async stop(isRestart = false, options = {}) {
|
|
10766
10940
|
this.startEpoch++;
|
|
10767
10941
|
if (!isRestart) this.isAppClosing = true;
|
|
10768
10942
|
this.disposeStreamRelay();
|
|
10769
10943
|
this.stopHealthCheck();
|
|
10770
10944
|
const generation = this.ownedServe;
|
|
10771
|
-
if (generation)
|
|
10945
|
+
if (generation)
|
|
10946
|
+
await this.stopGeneration(generation, isRestart, options.graceMs);
|
|
10772
10947
|
await this.startPromise;
|
|
10773
10948
|
}
|
|
10774
10949
|
captureServe(child) {
|
|
@@ -10826,8 +11001,17 @@ class BackendServiceManager {
|
|
|
10826
11001
|
* "we signalled it and moved on" would authorise exactly the double-serve
|
|
10827
11002
|
* this class exists to prevent. An unconfirmed exit keeps ownership, and the
|
|
10828
11003
|
* caller reports failure instead of proceeding.
|
|
11004
|
+
*
|
|
11005
|
+
* `graceMs` OVERRIDES THE RESTART GRACE FOR ONE CALL (2026-09-30), added for
|
|
11006
|
+
* the update fallback: a serve being stopped because its reload could not be
|
|
11007
|
+
* proven drains for as long as uvicorn waits for its connections (the
|
|
11008
|
+
* server's own wait has no deadline), and the caller that knows the process
|
|
11009
|
+
* is about to be replaced may bound that wait. The ESCALATION is unchanged -
|
|
11010
|
+
* SIGTERM, the (possibly shorter) grace, SIGKILL, the force wait - so an
|
|
11011
|
+
* unresponsive process still fails closed, and a caller that passes nothing
|
|
11012
|
+
* gets the same grace it always got.
|
|
10829
11013
|
*/
|
|
10830
|
-
stopGeneration(generation, isRestart) {
|
|
11014
|
+
stopGeneration(generation, isRestart, graceMs) {
|
|
10831
11015
|
if (generation.stop) return generation.stop;
|
|
10832
11016
|
generation.stop = (async () => {
|
|
10833
11017
|
const waitForExit = (ms) => new Promise((resolve) => {
|
|
@@ -10847,7 +11031,7 @@ class BackendServiceManager {
|
|
|
10847
11031
|
});
|
|
10848
11032
|
});
|
|
10849
11033
|
if (this.canSignal(generation)) generation.child.kill("SIGTERM");
|
|
10850
|
-
const grace = isRestart ? this.shutdownTimeoutMs.restart : this.shutdownTimeoutMs.normal;
|
|
11034
|
+
const grace = graceMs ?? (isRestart ? this.shutdownTimeoutMs.restart : this.shutdownTimeoutMs.normal);
|
|
10851
11035
|
if (!await waitForExit(grace)) {
|
|
10852
11036
|
if (this.canSignal(generation)) generation.child.kill("SIGKILL");
|
|
10853
11037
|
if (!await waitForExit(this.shutdownTimeoutMs.force)) {
|
|
@@ -11789,13 +11973,18 @@ class BackendServiceManager {
|
|
|
11789
11973
|
* Restart the backend service
|
|
11790
11974
|
* This method properly handles the restart process to ensure that any pending
|
|
11791
11975
|
* timeouts from the stop operation don't affect the newly started process
|
|
11976
|
+
*
|
|
11977
|
+
* `options.stopGraceMs` bounds ONE restart's SIGTERM grace (see
|
|
11978
|
+
* `stopGeneration`); it exists for the update fallback, whose process is
|
|
11979
|
+
* already known to be replaced, and callers that pass nothing keep the
|
|
11980
|
+
* default grace exactly as before.
|
|
11792
11981
|
* @returns Promise resolving to true if the restart was successful, false otherwise
|
|
11793
11982
|
*/
|
|
11794
|
-
restart() {
|
|
11983
|
+
restart(options = {}) {
|
|
11795
11984
|
if (this.restartPromise) return this.restartPromise;
|
|
11796
11985
|
this.restartPromise = (async () => {
|
|
11797
11986
|
try {
|
|
11798
|
-
await this.stop(true);
|
|
11987
|
+
await this.stop(true, { graceMs: options.stopGraceMs });
|
|
11799
11988
|
if (this.isAppClosing) return false;
|
|
11800
11989
|
return await this.start();
|
|
11801
11990
|
} catch (error) {
|
|
@@ -13625,6 +13814,14 @@ const METHODS$1 = [
|
|
|
13625
13814
|
"tabs",
|
|
13626
13815
|
"scroll",
|
|
13627
13816
|
"logs",
|
|
13817
|
+
// The structured read actions (`styles`, `hit_test`, `ancestors`), in
|
|
13818
|
+
// `protocol.py`'s tuple order: page-geometry reads answered by fixed
|
|
13819
|
+
// self-contained page functions (the vendored `driver/geometry-read.ts`),
|
|
13820
|
+
// capped and rounded in the page. This array is the wire's closed gate, so
|
|
13821
|
+
// it must stay byte-comparable with the Python source it mirrors.
|
|
13822
|
+
"styles",
|
|
13823
|
+
"hit_test",
|
|
13824
|
+
"ancestors",
|
|
13628
13825
|
"request_access",
|
|
13629
13826
|
"await_access",
|
|
13630
13827
|
"cancel_access",
|
|
@@ -13683,7 +13880,13 @@ const ERROR_CODES = [
|
|
|
13683
13880
|
// a command that hangs rather than one that fails.
|
|
13684
13881
|
...CONSOLE_ERROR_CODES
|
|
13685
13882
|
];
|
|
13686
|
-
const HOST_CAPABILITIES = [
|
|
13883
|
+
const HOST_CAPABILITIES = [
|
|
13884
|
+
"styles",
|
|
13885
|
+
"hit_test",
|
|
13886
|
+
"ancestors",
|
|
13887
|
+
"download",
|
|
13888
|
+
"upload"
|
|
13889
|
+
];
|
|
13687
13890
|
const RUN_DIRNAME = path$1.join("run", "ui-browser");
|
|
13688
13891
|
const STATE_FILENAME = "host.json";
|
|
13689
13892
|
const STATE_DIR_MODE = 448;
|
|
@@ -19305,6 +19508,135 @@ function clampTimeout(requested) {
|
|
|
19305
19508
|
if (!Number.isFinite(wanted) || wanted <= 0) return CAPS.downloadTimeoutS;
|
|
19306
19509
|
return Math.min(wanted, CAPS.downloadTimeoutMaxS);
|
|
19307
19510
|
}
|
|
19511
|
+
const READ_STYLES_SOURCE = '(selector, properties) => {\n const DEFAULTS = [\n "display",\n "position",\n "visibility",\n "opacity",\n "z-index",\n "transform",\n "transform-origin",\n "top",\n "right",\n "bottom",\n "left",\n "width",\n "height",\n "contain",\n "filter",\n "will-change",\n "overflow",\n "overflow-x",\n "overflow-y",\n "pointer-events"\n ];\n const MAX_MATCHES = 5;\n const MAX_STYLES = 30;\n const MAX_INLINE = 30;\n const MAX_IDENTITY = 120;\n const MAX_VALUE = 200;\n const all = document.querySelectorAll(selector);\n if (!all || all.length === 0) return null;\n const names = DEFAULTS.slice();\n if (Array.isArray(properties)) {\n for (let i = 0; i < properties.length && names.length < MAX_STYLES; i++) {\n const name = String(properties[i] ?? "").trim();\n if (name && names.indexOf(name) === -1) names.push(name);\n }\n }\n const round2 = (value) => {\n const rounded = Math.round(value * 100) / 100;\n return rounded === 0 ? 0 : rounded;\n };\n const clip = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + "\\u2026" : value;\n const rectOf = (el) => {\n const r = el.getBoundingClientRect();\n return {\n x: round2(r.x),\n y: round2(r.y),\n top: round2(r.top),\n right: round2(r.right),\n bottom: round2(r.bottom),\n width: round2(r.width),\n height: round2(r.height)\n };\n };\n const attrOf = (el, name) => clip(el.getAttribute(name) || "", MAX_IDENTITY);\n const matches = [];\n const first = Array.from(all).slice(0, MAX_MATCHES);\n for (const el of first) {\n const computed = getComputedStyle(el);\n const styles = {};\n for (const name of names) {\n styles[name] = clip(String(computed.getPropertyValue(name) || "").trim(), MAX_VALUE);\n }\n const inline = {};\n const declaration = el.style;\n let inlineCount = 0;\n for (let j = 0; j < declaration.length && inlineCount < MAX_INLINE; j++) {\n const name = declaration.item(j);\n if (name && name.indexOf("--") === 0) {\n const key = clip(name, MAX_IDENTITY);\n if (!(key in inline)) {\n inline[key] = clip(\n String(declaration.getPropertyValue(name) || "").trim(),\n MAX_VALUE\n );\n inlineCount += 1;\n }\n }\n }\n matches.push({\n tag: clip(el.tagName.toLowerCase(), MAX_IDENTITY),\n id: attrOf(el, "id"),\n role: attrOf(el, "role"),\n className: attrOf(el, "class"),\n rect: rectOf(el),\n styles,\n inline\n });\n }\n return { count: matches.length, matches, truncated: all.length > matches.length };\n}';
|
|
19512
|
+
const HIT_TEST_SOURCE = '(x, y) => {\n const PROPS = ["display", "position", "visibility", "opacity", "z-index", "pointer-events"];\n const MAX_ELEMENTS = 8;\n const MAX_IDENTITY = 120;\n const MAX_VALUE = 200;\n const px = Number(x);\n const py = Number(y);\n if (!Number.isFinite(px) || !Number.isFinite(py)) return null;\n const stack = document.elementsFromPoint(px, py);\n if (!stack || stack.length === 0) return null;\n const round2 = (value) => {\n const rounded = Math.round(value * 100) / 100;\n return rounded === 0 ? 0 : rounded;\n };\n const clip = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + "\\u2026" : value;\n const attrOf = (el, name) => clip(el.getAttribute(name) || "", MAX_IDENTITY);\n const elements = [];\n for (const el of stack.slice(0, MAX_ELEMENTS)) {\n const computed = getComputedStyle(el);\n const styles = {};\n for (const name of PROPS) {\n styles[name] = clip(String(computed.getPropertyValue(name) || "").trim(), MAX_VALUE);\n }\n const r = el.getBoundingClientRect();\n elements.push({\n tag: clip(el.tagName.toLowerCase(), MAX_IDENTITY),\n id: attrOf(el, "id"),\n role: attrOf(el, "role"),\n className: attrOf(el, "class"),\n rect: {\n x: round2(r.x),\n y: round2(r.y),\n top: round2(r.top),\n right: round2(r.right),\n bottom: round2(r.bottom),\n width: round2(r.width),\n height: round2(r.height)\n },\n styles\n });\n }\n return { count: elements.length, elements };\n}';
|
|
19513
|
+
const ANCESTORS_SOURCE = '(selector, depth) => {\n const PROPS = [\n "display",\n "position",\n "visibility",\n "overflow",\n "overflow-x",\n "overflow-y",\n "transform",\n "transform-origin",\n "contain",\n "filter",\n "will-change",\n "z-index",\n "isolation",\n "top",\n "left"\n ];\n const DEFAULT_DEPTH = 12;\n const MAX_DEPTH = 16;\n const MAX_IDENTITY = 120;\n const MAX_VALUE = 200;\n const el = document.querySelector(selector);\n if (!el) return null;\n const wanted = typeof depth === "number" && Number.isFinite(depth) ? Math.floor(depth) : DEFAULT_DEPTH;\n const bound = Math.max(1, Math.min(MAX_DEPTH, wanted));\n const round2 = (value) => {\n const rounded = Math.round(value * 100) / 100;\n return rounded === 0 ? 0 : rounded;\n };\n const clip = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + "\\u2026" : value;\n const attrOf = (el2, name) => clip(el2.getAttribute(name) || "", MAX_IDENTITY);\n const chain = [];\n let node = el;\n while (node && chain.length < bound) {\n const computed = getComputedStyle(node);\n const styles = {};\n for (const name of PROPS) {\n styles[name] = clip(String(computed.getPropertyValue(name) || "").trim(), MAX_VALUE);\n }\n const r = node.getBoundingClientRect();\n chain.push({\n tag: clip(node.tagName.toLowerCase(), MAX_IDENTITY),\n id: attrOf(node, "id"),\n role: attrOf(node, "role"),\n className: attrOf(node, "class"),\n rect: {\n x: round2(r.x),\n y: round2(r.y),\n top: round2(r.top),\n right: round2(r.right),\n bottom: round2(r.bottom),\n width: round2(r.width),\n height: round2(r.height)\n },\n styles\n });\n if (node === document.documentElement) break;\n node = node.parentElement;\n }\n return { count: chain.length, chain };\n}';
|
|
19514
|
+
const MAX_REQUESTED_PROPERTIES = 20;
|
|
19515
|
+
const DEFAULT_DEPTH = 12;
|
|
19516
|
+
const MAX_DEPTH = 16;
|
|
19517
|
+
async function runFixed(record, source, args, label) {
|
|
19518
|
+
const code = `(${source})(${args.map((arg) => JSON.stringify(arg)).join(", ")})`;
|
|
19519
|
+
return await deadline(
|
|
19520
|
+
record.view.webContents.executeJavaScriptInIsolatedWorld(
|
|
19521
|
+
ISOLATED_WORLD_ID,
|
|
19522
|
+
[{ code }]
|
|
19523
|
+
),
|
|
19524
|
+
SCRIPTING_DEADLINE_MS,
|
|
19525
|
+
label
|
|
19526
|
+
);
|
|
19527
|
+
}
|
|
19528
|
+
function selectorError(selector, error) {
|
|
19529
|
+
if (error instanceof Error && INVALID_SELECTOR.test(error.message)) {
|
|
19530
|
+
return new BridgeCommandError(
|
|
19531
|
+
"element_not_found",
|
|
19532
|
+
`selector ${selector} is not valid`
|
|
19533
|
+
);
|
|
19534
|
+
}
|
|
19535
|
+
return error;
|
|
19536
|
+
}
|
|
19537
|
+
function structuredResult(name, result) {
|
|
19538
|
+
if (typeof result !== "object" || result === null) {
|
|
19539
|
+
throw new BridgeCommandError(
|
|
19540
|
+
"internal",
|
|
19541
|
+
`the page returned no structured ${name} result`
|
|
19542
|
+
);
|
|
19543
|
+
}
|
|
19544
|
+
return result;
|
|
19545
|
+
}
|
|
19546
|
+
function requiredSelector(params) {
|
|
19547
|
+
const selector = stringParam(params, "selector");
|
|
19548
|
+
if (!selector) {
|
|
19549
|
+
throw new BridgeCommandError("element_not_found", "selector is required");
|
|
19550
|
+
}
|
|
19551
|
+
return selector;
|
|
19552
|
+
}
|
|
19553
|
+
function requestedProperties(params) {
|
|
19554
|
+
if (!Array.isArray(params.properties)) return [];
|
|
19555
|
+
return params.properties.filter((entry) => typeof entry === "string").slice(0, MAX_REQUESTED_PROPERTIES);
|
|
19556
|
+
}
|
|
19557
|
+
async function styles(ctx, params) {
|
|
19558
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19559
|
+
const selector = requiredSelector(params);
|
|
19560
|
+
const properties = requestedProperties(params);
|
|
19561
|
+
let result;
|
|
19562
|
+
try {
|
|
19563
|
+
result = await runFixed(
|
|
19564
|
+
record,
|
|
19565
|
+
READ_STYLES_SOURCE,
|
|
19566
|
+
[selector, properties],
|
|
19567
|
+
`styles(${selector})`
|
|
19568
|
+
);
|
|
19569
|
+
} catch (error) {
|
|
19570
|
+
throw selectorError(selector, error);
|
|
19571
|
+
}
|
|
19572
|
+
if (result === null || result === void 0) {
|
|
19573
|
+
throw new BridgeCommandError(
|
|
19574
|
+
"element_not_found",
|
|
19575
|
+
`selector ${selector} matched nothing`
|
|
19576
|
+
);
|
|
19577
|
+
}
|
|
19578
|
+
ctx.registry.touch(record);
|
|
19579
|
+
return { ...structuredResult("styles", result), ...pageOf(record.view) };
|
|
19580
|
+
}
|
|
19581
|
+
async function hitTest(ctx, params) {
|
|
19582
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19583
|
+
const x = numberParam(params, "x");
|
|
19584
|
+
const y = numberParam(params, "y");
|
|
19585
|
+
if (x === void 0 || y === void 0) {
|
|
19586
|
+
throw new BridgeCommandError(
|
|
19587
|
+
"internal",
|
|
19588
|
+
"'hit_test' needs numeric x and y (viewport coordinates)"
|
|
19589
|
+
);
|
|
19590
|
+
}
|
|
19591
|
+
const result = await runFixed(
|
|
19592
|
+
record,
|
|
19593
|
+
HIT_TEST_SOURCE,
|
|
19594
|
+
[x, y],
|
|
19595
|
+
`hit_test(${x}, ${y})`
|
|
19596
|
+
);
|
|
19597
|
+
if (result === null || result === void 0) {
|
|
19598
|
+
throw new BridgeCommandError(
|
|
19599
|
+
"element_not_found",
|
|
19600
|
+
`no element at point (${x}, ${y})`
|
|
19601
|
+
);
|
|
19602
|
+
}
|
|
19603
|
+
ctx.registry.touch(record);
|
|
19604
|
+
return {
|
|
19605
|
+
...structuredResult("hit_test", result),
|
|
19606
|
+
...pageOf(record.view)
|
|
19607
|
+
};
|
|
19608
|
+
}
|
|
19609
|
+
async function ancestors(ctx, params) {
|
|
19610
|
+
const record = ctx.registry.requireSurface(params.tab);
|
|
19611
|
+
const selector = requiredSelector(params);
|
|
19612
|
+
const raw = numberParam(params, "depth");
|
|
19613
|
+
const depth = Math.max(
|
|
19614
|
+
1,
|
|
19615
|
+
Math.min(MAX_DEPTH, raw === void 0 ? DEFAULT_DEPTH : Math.floor(raw))
|
|
19616
|
+
);
|
|
19617
|
+
let result;
|
|
19618
|
+
try {
|
|
19619
|
+
result = await runFixed(
|
|
19620
|
+
record,
|
|
19621
|
+
ANCESTORS_SOURCE,
|
|
19622
|
+
[selector, depth],
|
|
19623
|
+
`ancestors(${selector})`
|
|
19624
|
+
);
|
|
19625
|
+
} catch (error) {
|
|
19626
|
+
throw selectorError(selector, error);
|
|
19627
|
+
}
|
|
19628
|
+
if (result === null || result === void 0) {
|
|
19629
|
+
throw new BridgeCommandError(
|
|
19630
|
+
"element_not_found",
|
|
19631
|
+
`selector ${selector} matched nothing`
|
|
19632
|
+
);
|
|
19633
|
+
}
|
|
19634
|
+
ctx.registry.touch(record);
|
|
19635
|
+
return {
|
|
19636
|
+
...structuredResult("ancestors", result),
|
|
19637
|
+
...pageOf(record.view)
|
|
19638
|
+
};
|
|
19639
|
+
}
|
|
19308
19640
|
const MAX_AGENT_TABS = 8;
|
|
19309
19641
|
const BACKGROUND_VIEWPORT = {
|
|
19310
19642
|
x: 0,
|
|
@@ -20269,6 +20601,12 @@ function factOf(path2) {
|
|
|
20269
20601
|
}
|
|
20270
20602
|
const DOCUMENT_SCOPED = /* @__PURE__ */ new Set([
|
|
20271
20603
|
"read",
|
|
20604
|
+
// The structured reads (`styles`, `hit_test`, `ancestors`) describe the
|
|
20605
|
+
// CURRENT document exactly as `read` does, so a result that raced a
|
|
20606
|
+
// navigation it did not cause must be discarded the same way.
|
|
20607
|
+
"styles",
|
|
20608
|
+
"hit_test",
|
|
20609
|
+
"ancestors",
|
|
20272
20610
|
"snapshot",
|
|
20273
20611
|
"screenshot",
|
|
20274
20612
|
"click",
|
|
@@ -20296,6 +20634,9 @@ const NAVIGATION_ACTIONS = /* @__PURE__ */ new Set(["click", "type"]);
|
|
|
20296
20634
|
const TAB_SCOPED = /* @__PURE__ */ new Set([
|
|
20297
20635
|
"goto",
|
|
20298
20636
|
"read",
|
|
20637
|
+
"styles",
|
|
20638
|
+
"hit_test",
|
|
20639
|
+
"ancestors",
|
|
20299
20640
|
"snapshot",
|
|
20300
20641
|
"screenshot",
|
|
20301
20642
|
"click",
|
|
@@ -20473,6 +20814,12 @@ class BrowserHost {
|
|
|
20473
20814
|
return retitle();
|
|
20474
20815
|
case "read":
|
|
20475
20816
|
return read(this, params);
|
|
20817
|
+
case "styles":
|
|
20818
|
+
return styles(this, params);
|
|
20819
|
+
case "hit_test":
|
|
20820
|
+
return hitTest(this, params);
|
|
20821
|
+
case "ancestors":
|
|
20822
|
+
return ancestors(this, params);
|
|
20476
20823
|
case "snapshot":
|
|
20477
20824
|
return snapshot(this, params);
|
|
20478
20825
|
case "scroll":
|
|
@@ -24229,6 +24576,7 @@ async function startBrowserHost(options) {
|
|
|
24229
24576
|
}
|
|
24230
24577
|
});
|
|
24231
24578
|
window.contentView.addChildView(view);
|
|
24579
|
+
view.setBounds(BACKGROUND_VIEWPORT);
|
|
24232
24580
|
view.setVisible(false);
|
|
24233
24581
|
return view;
|
|
24234
24582
|
}
|
|
@@ -33246,17 +33594,43 @@ const MINI_DOCUMENT = "mini.html";
|
|
|
33246
33594
|
function miniViewUrlFor(rendererUrl) {
|
|
33247
33595
|
return INDEX_DOCUMENT_SUFFIX.test(rendererUrl) ? rendererUrl.replace(INDEX_DOCUMENT_SUFFIX, `/${MINI_DOCUMENT}`) : `${rendererUrl.replace(TRAILING_SLASH, "")}/${MINI_DOCUMENT}`;
|
|
33248
33596
|
}
|
|
33597
|
+
const MINI_VIEW_GROUND = "#22201c";
|
|
33598
|
+
const MINI_VIEW_PAINT_TIMEOUT_MS = 1500;
|
|
33599
|
+
function miniViewPresentationFor(input) {
|
|
33600
|
+
if (input.show !== "focus") return "present";
|
|
33601
|
+
if (input.painted) return "present";
|
|
33602
|
+
if (input.timedOut) return "error-surface";
|
|
33603
|
+
return "wait";
|
|
33604
|
+
}
|
|
33605
|
+
function miniViewErrorSurfaceHtml() {
|
|
33606
|
+
return `<!doctype html>
|
|
33607
|
+
<html><head><meta charset="utf-8" /><title>Quick send</title>
|
|
33608
|
+
<style>
|
|
33609
|
+
:root { color-scheme: dark; }
|
|
33610
|
+
html, body { margin: 0; height: 100%; background: ${MINI_VIEW_GROUND}; }
|
|
33611
|
+
body { display: flex; align-items: center; padding: 0 16px;
|
|
33612
|
+
font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
33613
|
+
color: #c2bcb0; }
|
|
33614
|
+
p { margin: 0; }
|
|
33615
|
+
strong { font-weight: 600; color: #ef8078; }
|
|
33616
|
+
</style></head>
|
|
33617
|
+
<body><p><strong>Quick send could not open.</strong> Try the hotkey again, or restart Local Operator if it keeps failing.</p></body></html>`;
|
|
33618
|
+
}
|
|
33249
33619
|
function createMiniView(options) {
|
|
33250
33620
|
const window = new electron.BrowserWindow({
|
|
33251
33621
|
width: MINI_VIEW_WIDTH,
|
|
33252
33622
|
height: MINI_VIEW_HEIGHT,
|
|
33253
33623
|
/*
|
|
33254
|
-
* Sized in content, not window, terms
|
|
33255
|
-
*
|
|
33256
|
-
*
|
|
33624
|
+
* Sized in content, not window, terms, and the content size is now the
|
|
33625
|
+
* BASE of a measured range rather than the whole story: the restyle's
|
|
33626
|
+
* chrome (previews, the readings strip, the picker sheet) asks for more
|
|
33627
|
+
* through `MINI_VIEW_RESIZE` below, and this call is what makes the
|
|
33628
|
+
* page's viewport the number every frame is captured at.
|
|
33257
33629
|
*/
|
|
33258
33630
|
useContentSize: true,
|
|
33259
33631
|
show: false,
|
|
33632
|
+
/* The dark ground under an as-yet-unpainted document; see MINI_VIEW_GROUND. */
|
|
33633
|
+
backgroundColor: MINI_VIEW_GROUND,
|
|
33260
33634
|
// A composer that floats over other apps carries no frame of its own:
|
|
33261
33635
|
// the app's own rows are the entire surface, and the one sanctioned
|
|
33262
33636
|
// elevation step is the card's ground.
|
|
@@ -33304,7 +33678,91 @@ function createMiniView(options) {
|
|
|
33304
33678
|
if (process.platform === "darwin") {
|
|
33305
33679
|
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
33306
33680
|
}
|
|
33681
|
+
let paintedForLoad = false;
|
|
33682
|
+
let paintTimer = null;
|
|
33683
|
+
let paintWaiters = [];
|
|
33684
|
+
const settlePaint = (ready) => {
|
|
33685
|
+
if (paintTimer !== null) {
|
|
33686
|
+
clearTimeout(paintTimer);
|
|
33687
|
+
paintTimer = null;
|
|
33688
|
+
}
|
|
33689
|
+
paintedForLoad = ready;
|
|
33690
|
+
const waiting = paintWaiters;
|
|
33691
|
+
paintWaiters = [];
|
|
33692
|
+
for (const resolve of waiting) resolve();
|
|
33693
|
+
};
|
|
33694
|
+
const showErrorSurface = () => {
|
|
33695
|
+
if (disposed || window.isDestroyed()) return;
|
|
33696
|
+
settlePaint(false);
|
|
33697
|
+
void window.loadURL(
|
|
33698
|
+
`data:text/html;charset=utf-8,${encodeURIComponent(miniViewErrorSurfaceHtml())}`
|
|
33699
|
+
);
|
|
33700
|
+
window.webContents.once("did-finish-load", () => {
|
|
33701
|
+
if (disposed || window.isDestroyed()) return;
|
|
33702
|
+
positionForCursorDisplay();
|
|
33703
|
+
presentMiniView(window, options.show, {
|
|
33704
|
+
trigger: "mini-view",
|
|
33705
|
+
report: options.report
|
|
33706
|
+
});
|
|
33707
|
+
if (options.show === "focus") {
|
|
33708
|
+
window.webContents.send(MINI_VIEW_SUMMONED, {
|
|
33709
|
+
at: Date.now()
|
|
33710
|
+
});
|
|
33711
|
+
}
|
|
33712
|
+
});
|
|
33713
|
+
};
|
|
33714
|
+
window.webContents.on("did-start-loading", () => {
|
|
33715
|
+
paintedForLoad = false;
|
|
33716
|
+
});
|
|
33717
|
+
window.webContents.on(
|
|
33718
|
+
"did-fail-load",
|
|
33719
|
+
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
33720
|
+
if (!isMainFrame) return;
|
|
33721
|
+
options.report?.(
|
|
33722
|
+
`[mini-view] load failed (${errorCode} ${errorDescription}) for ${validatedURL}; showing the error card`
|
|
33723
|
+
);
|
|
33724
|
+
showErrorSurface();
|
|
33725
|
+
}
|
|
33726
|
+
);
|
|
33727
|
+
window.webContents.on("render-process-gone", (_event, details) => {
|
|
33728
|
+
options.report?.(
|
|
33729
|
+
`[mini-view] renderer gone (${details.reason}); showing the error card`
|
|
33730
|
+
);
|
|
33731
|
+
showErrorSurface();
|
|
33732
|
+
});
|
|
33307
33733
|
void window.loadURL(options.url);
|
|
33734
|
+
const onResize = (event, payload) => {
|
|
33735
|
+
if (window.isDestroyed() || event.sender !== window.webContents || event.senderFrame !== window.webContents.mainFrame) {
|
|
33736
|
+
return;
|
|
33737
|
+
}
|
|
33738
|
+
if (!isMiniViewResizePayload(payload)) return;
|
|
33739
|
+
const area = electron.screen.getDisplayMatching(window.getBounds()).workArea;
|
|
33740
|
+
const ceiling = Math.max(
|
|
33741
|
+
MINI_VIEW_HEIGHT,
|
|
33742
|
+
Math.min(MINI_VIEW_MAX_HEIGHT, area.height - 64)
|
|
33743
|
+
);
|
|
33744
|
+
const height = Math.max(
|
|
33745
|
+
MINI_VIEW_HEIGHT,
|
|
33746
|
+
Math.min(Math.round(payload.height), ceiling)
|
|
33747
|
+
);
|
|
33748
|
+
const bounds = window.getContentBounds();
|
|
33749
|
+
if (bounds.height === height) return;
|
|
33750
|
+
window.setContentSize(MINI_VIEW_WIDTH, height);
|
|
33751
|
+
const bottom = bounds.y + height;
|
|
33752
|
+
if (bottom > area.y + area.height) {
|
|
33753
|
+
window.setPosition(
|
|
33754
|
+
bounds.x,
|
|
33755
|
+
Math.max(area.y, area.y + area.height - height)
|
|
33756
|
+
);
|
|
33757
|
+
}
|
|
33758
|
+
};
|
|
33759
|
+
electron.ipcMain.on(MINI_VIEW_RESIZE, onResize);
|
|
33760
|
+
const onPainted = (event) => {
|
|
33761
|
+
if (window.isDestroyed() || event.sender !== window.webContents || event.senderFrame !== window.webContents.mainFrame)
|
|
33762
|
+
return;
|
|
33763
|
+
settlePaint(true);
|
|
33764
|
+
};
|
|
33765
|
+
electron.ipcMain.on(MINI_VIEW_PAINTED, onPainted);
|
|
33308
33766
|
let disposed = false;
|
|
33309
33767
|
function hide() {
|
|
33310
33768
|
if (disposed || window.isDestroyed()) return;
|
|
@@ -33334,42 +33792,86 @@ function createMiniView(options) {
|
|
|
33334
33792
|
}
|
|
33335
33793
|
hide();
|
|
33336
33794
|
});
|
|
33337
|
-
function
|
|
33338
|
-
if (disposed) return;
|
|
33339
|
-
|
|
33340
|
-
|
|
33795
|
+
function presentWhenReady() {
|
|
33796
|
+
if (disposed || window.isDestroyed()) return;
|
|
33797
|
+
const decision = miniViewPresentationFor({
|
|
33798
|
+
show: options.show,
|
|
33799
|
+
painted: paintedForLoad,
|
|
33800
|
+
timedOut: false
|
|
33801
|
+
});
|
|
33802
|
+
if (decision === "error-surface") {
|
|
33803
|
+
showErrorSurface();
|
|
33341
33804
|
return;
|
|
33342
33805
|
}
|
|
33806
|
+
if (decision === "present") {
|
|
33807
|
+
presentNow();
|
|
33808
|
+
return;
|
|
33809
|
+
}
|
|
33810
|
+
paintWaiters.push(() => {
|
|
33811
|
+
if (miniViewPresentationFor({
|
|
33812
|
+
show: options.show,
|
|
33813
|
+
painted: paintedForLoad,
|
|
33814
|
+
timedOut: false
|
|
33815
|
+
}) === "present")
|
|
33816
|
+
presentNow();
|
|
33817
|
+
});
|
|
33818
|
+
if (paintTimer === null) {
|
|
33819
|
+
paintTimer = setTimeout(() => {
|
|
33820
|
+
paintTimer = null;
|
|
33821
|
+
if (miniViewPresentationFor({
|
|
33822
|
+
show: options.show,
|
|
33823
|
+
painted: paintedForLoad,
|
|
33824
|
+
timedOut: true
|
|
33825
|
+
}) === "error-surface")
|
|
33826
|
+
showErrorSurface();
|
|
33827
|
+
}, MINI_VIEW_PAINT_TIMEOUT_MS);
|
|
33828
|
+
}
|
|
33829
|
+
}
|
|
33830
|
+
function presentNow() {
|
|
33831
|
+
if (disposed || window.isDestroyed()) return;
|
|
33343
33832
|
positionForCursorDisplay();
|
|
33344
33833
|
presentMiniView(window, options.show, {
|
|
33345
33834
|
trigger: "mini-view",
|
|
33346
33835
|
report: options.report
|
|
33347
33836
|
});
|
|
33348
|
-
if (options.show === "focus"
|
|
33349
|
-
|
|
33350
|
-
|
|
33837
|
+
if (options.show === "focus") {
|
|
33838
|
+
window.webContents.send(MINI_VIEW_SUMMONED, {
|
|
33839
|
+
at: Date.now()
|
|
33840
|
+
});
|
|
33351
33841
|
}
|
|
33352
33842
|
}
|
|
33843
|
+
function toggle() {
|
|
33844
|
+
if (disposed) return;
|
|
33845
|
+
if (!window.isDestroyed() && window.isVisible()) {
|
|
33846
|
+
hide();
|
|
33847
|
+
return;
|
|
33848
|
+
}
|
|
33849
|
+
presentWhenReady();
|
|
33850
|
+
}
|
|
33353
33851
|
function positionForCursorDisplay() {
|
|
33354
33852
|
if (window.isDestroyed()) return;
|
|
33355
33853
|
const display = electron.screen.getDisplayNearestPoint(
|
|
33356
33854
|
electron.screen.getCursorScreenPoint()
|
|
33357
33855
|
);
|
|
33358
33856
|
const area = display.workArea;
|
|
33857
|
+
const height = window.isDestroyed() ? MINI_VIEW_HEIGHT : window.getContentBounds().height;
|
|
33359
33858
|
const x = Math.round(area.x + (area.width - MINI_VIEW_WIDTH) / 2);
|
|
33360
|
-
const desiredY = area.y + area.height / 3 -
|
|
33859
|
+
const desiredY = area.y + area.height / 3 - height / 2;
|
|
33361
33860
|
const y = Math.round(
|
|
33362
|
-
Math.min(
|
|
33363
|
-
Math.max(desiredY, area.y),
|
|
33364
|
-
area.y + area.height - MINI_VIEW_HEIGHT
|
|
33365
|
-
)
|
|
33861
|
+
Math.min(Math.max(desiredY, area.y), area.y + area.height - height)
|
|
33366
33862
|
);
|
|
33367
33863
|
window.setPosition(x, y);
|
|
33368
33864
|
}
|
|
33369
33865
|
function dispose() {
|
|
33370
33866
|
if (disposed) return;
|
|
33371
33867
|
disposed = true;
|
|
33868
|
+
electron.ipcMain.removeListener(MINI_VIEW_RESIZE, onResize);
|
|
33869
|
+
electron.ipcMain.removeListener(MINI_VIEW_PAINTED, onPainted);
|
|
33372
33870
|
electron.ipcMain.removeHandler(MINI_VIEW_DISMISS);
|
|
33871
|
+
if (paintTimer !== null) {
|
|
33872
|
+
clearTimeout(paintTimer);
|
|
33873
|
+
paintTimer = null;
|
|
33874
|
+
}
|
|
33373
33875
|
if (!window.isDestroyed()) window.destroy();
|
|
33374
33876
|
}
|
|
33375
33877
|
return { window, toggle, hide, dispose };
|
|
@@ -33436,6 +33938,123 @@ function titlebarOptions(platform, chrome) {
|
|
|
33436
33938
|
}
|
|
33437
33939
|
};
|
|
33438
33940
|
}
|
|
33941
|
+
const UPDATE_REPORT_KEY = "update_report";
|
|
33942
|
+
const UPDATE_REPORT_SCHEMA_PREFIX = "update.report.";
|
|
33943
|
+
const DAEMON_STATUSES = /* @__PURE__ */ new Set([
|
|
33944
|
+
"refreshed",
|
|
33945
|
+
"already",
|
|
33946
|
+
"failed",
|
|
33947
|
+
"unsupervised"
|
|
33948
|
+
]);
|
|
33949
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33950
|
+
function parseUpdateReport(stdout) {
|
|
33951
|
+
let found = null;
|
|
33952
|
+
for (const line of stdout.split("\n")) {
|
|
33953
|
+
const trimmed = line.trim();
|
|
33954
|
+
if (!trimmed.startsWith("{") || !trimmed.includes(UPDATE_REPORT_KEY))
|
|
33955
|
+
continue;
|
|
33956
|
+
let payload;
|
|
33957
|
+
try {
|
|
33958
|
+
payload = JSON.parse(trimmed);
|
|
33959
|
+
} catch {
|
|
33960
|
+
continue;
|
|
33961
|
+
}
|
|
33962
|
+
const report = reportFromPayload(payload);
|
|
33963
|
+
if (report) found = report;
|
|
33964
|
+
}
|
|
33965
|
+
return found;
|
|
33966
|
+
}
|
|
33967
|
+
function reportFromPayload(payload) {
|
|
33968
|
+
if (!isRecord(payload)) return null;
|
|
33969
|
+
const raw = payload[UPDATE_REPORT_KEY];
|
|
33970
|
+
if (!isRecord(raw)) return null;
|
|
33971
|
+
const schema2 = raw.schema;
|
|
33972
|
+
if (typeof schema2 !== "string" || !schema2.startsWith(UPDATE_REPORT_SCHEMA_PREFIX))
|
|
33973
|
+
return null;
|
|
33974
|
+
const installVersion = raw.install_version;
|
|
33975
|
+
const target2 = raw.target;
|
|
33976
|
+
if (typeof installVersion !== "string" || typeof target2 !== "string")
|
|
33977
|
+
return null;
|
|
33978
|
+
const serve = raw.serve;
|
|
33979
|
+
const daemons = raw.daemons;
|
|
33980
|
+
if (!Array.isArray(serve) || !Array.isArray(daemons)) return null;
|
|
33981
|
+
const serveEntries = [];
|
|
33982
|
+
for (const entry of serve) {
|
|
33983
|
+
if (!isRecord(entry)) return null;
|
|
33984
|
+
const { pid, from, to, moved, instance } = entry;
|
|
33985
|
+
if (typeof pid !== "number" || typeof from !== "string" || typeof to !== "string" || typeof moved !== "boolean" || typeof instance !== "string")
|
|
33986
|
+
return null;
|
|
33987
|
+
serveEntries.push({ pid, from, to, moved, instance });
|
|
33988
|
+
}
|
|
33989
|
+
const daemonEntries = [];
|
|
33990
|
+
for (const entry of daemons) {
|
|
33991
|
+
if (!isRecord(entry)) return null;
|
|
33992
|
+
const { name, status: status2, version } = entry;
|
|
33993
|
+
if (typeof name !== "string" || typeof status2 !== "string" || !DAEMON_STATUSES.has(status2) || typeof version !== "string")
|
|
33994
|
+
return null;
|
|
33995
|
+
daemonEntries.push({
|
|
33996
|
+
name,
|
|
33997
|
+
status: status2,
|
|
33998
|
+
version
|
|
33999
|
+
});
|
|
34000
|
+
}
|
|
34001
|
+
return {
|
|
34002
|
+
schema: schema2,
|
|
34003
|
+
install_version: installVersion,
|
|
34004
|
+
target: target2,
|
|
34005
|
+
serve: serveEntries,
|
|
34006
|
+
daemons: daemonEntries
|
|
34007
|
+
};
|
|
34008
|
+
}
|
|
34009
|
+
function serveMovedOntoBuild(input) {
|
|
34010
|
+
if (!input.healthOk) return false;
|
|
34011
|
+
const before = input.instanceBefore?.trim() ?? "";
|
|
34012
|
+
if (before === "") return false;
|
|
34013
|
+
const after = input.recordAfter.instanceId?.trim() || input.reportServe?.instance.trim() || "";
|
|
34014
|
+
if (after === "" || after === before) return false;
|
|
34015
|
+
return serveOnTarget(
|
|
34016
|
+
input.recordAfter.bootVersion,
|
|
34017
|
+
input.installVersion,
|
|
34018
|
+
input.target
|
|
34019
|
+
);
|
|
34020
|
+
}
|
|
34021
|
+
function serveOnTarget(bootVersion, installVersion, target2) {
|
|
34022
|
+
const boot = bootVersion?.trim() ?? "";
|
|
34023
|
+
if (boot === "") return false;
|
|
34024
|
+
const wanted = target2?.trim() ?? "";
|
|
34025
|
+
if (wanted !== "") {
|
|
34026
|
+
const order2 = compareVersions(boot, wanted);
|
|
34027
|
+
return order2 !== null && order2 >= 0;
|
|
34028
|
+
}
|
|
34029
|
+
const installed = installVersion?.trim() ?? "";
|
|
34030
|
+
if (installed === "") return false;
|
|
34031
|
+
const order = compareVersions(boot, installed);
|
|
34032
|
+
return order === null ? boot === installed : order >= 0;
|
|
34033
|
+
}
|
|
34034
|
+
function runtimeStragglerCount(body, current) {
|
|
34035
|
+
const currentBuild = current?.trim() ?? "";
|
|
34036
|
+
if (currentBuild === "") return null;
|
|
34037
|
+
if (!isRecord(body)) return null;
|
|
34038
|
+
const result = body.result;
|
|
34039
|
+
if (!isRecord(result)) return null;
|
|
34040
|
+
const runtimes = result.runtimes;
|
|
34041
|
+
if (!Array.isArray(runtimes)) return null;
|
|
34042
|
+
let count = 0;
|
|
34043
|
+
for (const row of runtimes) {
|
|
34044
|
+
if (!isRecord(row)) continue;
|
|
34045
|
+
const build = typeof row.build_version === "string" ? row.build_version.trim() : "";
|
|
34046
|
+
if (build === "") continue;
|
|
34047
|
+
if (build !== currentBuild) count += 1;
|
|
34048
|
+
}
|
|
34049
|
+
return count;
|
|
34050
|
+
}
|
|
34051
|
+
function daemonMoveOutcome(report) {
|
|
34052
|
+
if (report === null) return null;
|
|
34053
|
+
const failed = report.daemons.filter(
|
|
34054
|
+
(daemon) => daemon.status === "failed" || daemon.status === "unsupervised"
|
|
34055
|
+
).map((daemon) => daemon.name);
|
|
34056
|
+
return { source: "report", moved: failed.length === 0, failed };
|
|
34057
|
+
}
|
|
33439
34058
|
const CHROMIUM_TRANSPORT_CODES = [
|
|
33440
34059
|
"ERR_INTERNET_DISCONNECTED",
|
|
33441
34060
|
"ERR_NETWORK_CHANGED",
|
|
@@ -34225,6 +34844,7 @@ const BETA_VERSION_REGEX = /v\d+\.\d+\.\d+\.beta\.\d+/;
|
|
|
34225
34844
|
const ARCHITECTURE_SEPARATOR_REGEX = /\s+/;
|
|
34226
34845
|
const GLOBAL_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
34227
34846
|
const SOURCE_REBUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
34847
|
+
const UPDATE_RESTART_STOP_GRACE_MS = 5e3;
|
|
34228
34848
|
const BUNDLE_ID_REGEX = /^[\w.-]+$/;
|
|
34229
34849
|
const INSTALL_IN_FLIGHT_RECHECK_MS = 1e4;
|
|
34230
34850
|
const LAUNCHCTL_NOT_LOADED_REGEX = /could not find|no such process/i;
|
|
@@ -38714,6 +39334,219 @@ ${heal.removed.join("\n")}`,
|
|
|
38714
39334
|
async readFleetSnapshot(backend) {
|
|
38715
39335
|
return backend.servingSessionFleet();
|
|
38716
39336
|
}
|
|
39337
|
+
/**
|
|
39338
|
+
* The serving process's own record, both fields from ONE read.
|
|
39339
|
+
*
|
|
39340
|
+
* The post-swap move proof compares an instance token against the pre-swap
|
|
39341
|
+
* one, and a pair assembled from two reads can straddle a republish (a new
|
|
39342
|
+
* instance under the OLD version string, or the reverse) - so the two fields
|
|
39343
|
+
* come out of a single `servingInstall()` call, which reads the record file
|
|
39344
|
+
* once. Absence is both-fields-null, never a papered-over reading: a record
|
|
39345
|
+
* that could not be read proves nothing and the verdict says so.
|
|
39346
|
+
*/
|
|
39347
|
+
servingRecordReadings() {
|
|
39348
|
+
const readings = this.backendService?.servingInstall().readings ?? servingInstallReadings(null);
|
|
39349
|
+
return {
|
|
39350
|
+
bootVersion: readings.bootVersion,
|
|
39351
|
+
instanceId: readings.instanceId
|
|
39352
|
+
};
|
|
39353
|
+
}
|
|
39354
|
+
/**
|
|
39355
|
+
* The pre-swap rows still resident in a fresh read, or null when unreadable.
|
|
39356
|
+
*
|
|
39357
|
+
* The liveness count's own arithmetic, in one place because two callers need
|
|
39358
|
+
* it in different shapes (the restart fallback counts from this, the moved
|
|
39359
|
+
* path's fallback counts THROUGH it). A read that could not be taken is null
|
|
39360
|
+
* rather than an empty set: "nothing resident" would be a measured zero, and
|
|
39361
|
+
* an unanswered roster is not a measurement. A cold row (an empty
|
|
39362
|
+
* `liveState`, the catalogue's spelling for a session with no runtime) is not
|
|
39363
|
+
* resident - `isLiveRow` owns that rule, the same one the engagement diff
|
|
39364
|
+
* uses.
|
|
39365
|
+
*/
|
|
39366
|
+
stillResidentRows(before, after) {
|
|
39367
|
+
if (after === null) return null;
|
|
39368
|
+
const liveNow = new Set(
|
|
39369
|
+
after.filter((row) => isLiveRow(row)).map((row) => row.sessionId)
|
|
39370
|
+
);
|
|
39371
|
+
return before.filter((row) => isLiveRow(row) && liveNow.has(row.sessionId));
|
|
39372
|
+
}
|
|
39373
|
+
/**
|
|
39374
|
+
* The BUILD-accurate straggler count: runtimes not on the build the server
|
|
39375
|
+
* is on, from the machine's runtime roster, or null when it did not answer.
|
|
39376
|
+
*
|
|
39377
|
+
* THIS IS THE PRIMARY CENSUS since 2026-09-30 (design §5): `GET
|
|
39378
|
+
* /v1/desktop/runtimes?probe=false` returns one row per live runtime with the
|
|
39379
|
+
* `build_version` the runtime's own record (or boot record) carries, so the
|
|
39380
|
+
* count answers the question the notice's sentence makes - "still on the old
|
|
39381
|
+
* version" - from a build reading rather than from a liveness proxy. An empty
|
|
39382
|
+
* `build_version` is UNKNOWN and is not counted (the roster journal's own
|
|
39383
|
+
* rule), and a row whose build equals the current one has already rolled.
|
|
39384
|
+
*
|
|
39385
|
+
* Null (unreadable body, non-200, a transport failure, or no current build to
|
|
39386
|
+
* compare against) sends the caller to the liveness fallback; the failure is
|
|
39387
|
+
* logged here, where the transport error is still in hand. The read is
|
|
39388
|
+
* deliberately per-call rather than cached: it is the one reading whose whole
|
|
39389
|
+
* point is that it moves as runtimes roll, so every completion asks the
|
|
39390
|
+
* machine again. A notice-reopen re-read is NOT wired (review round 1's A1):
|
|
39391
|
+
* no renderer->main census channel exists yet, so the count travels with the
|
|
39392
|
+
* completion it was measured for - recorded as deferred on the PR rather
|
|
39393
|
+
* than claimed here.
|
|
39394
|
+
*/
|
|
39395
|
+
async readRuntimeCensus(backend, current) {
|
|
39396
|
+
try {
|
|
39397
|
+
const response = await backend.requestDesktop({ op: "runtimes.list" });
|
|
39398
|
+
if (response.status !== 200) {
|
|
39399
|
+
logger.info(
|
|
39400
|
+
`Runtime roster did not answer (${response.status}); the straggler count falls back to the liveness reading`,
|
|
39401
|
+
LogFileType.UPDATE_SERVICE
|
|
39402
|
+
);
|
|
39403
|
+
return null;
|
|
39404
|
+
}
|
|
39405
|
+
const count = runtimeStragglerCount(response.body, current);
|
|
39406
|
+
if (count === null) {
|
|
39407
|
+
logger.info(
|
|
39408
|
+
"Runtime roster answered with no usable rows; the straggler count falls back to the liveness reading",
|
|
39409
|
+
LogFileType.UPDATE_SERVICE
|
|
39410
|
+
);
|
|
39411
|
+
}
|
|
39412
|
+
return count;
|
|
39413
|
+
} catch (error) {
|
|
39414
|
+
logger.info(
|
|
39415
|
+
`Runtime roster read failed (${error instanceof Error ? error.message : String(error)}); the straggler count falls back to the liveness reading`,
|
|
39416
|
+
LogFileType.UPDATE_SERVICE
|
|
39417
|
+
);
|
|
39418
|
+
return null;
|
|
39419
|
+
}
|
|
39420
|
+
}
|
|
39421
|
+
/**
|
|
39422
|
+
* The straggler count and where it came from: build first, liveness second.
|
|
39423
|
+
*
|
|
39424
|
+
* The build count (`readRuntimeCensus`) is the primary. When the roster does
|
|
39425
|
+
* not answer, the fallback is the EXISTING liveness reading, in whichever
|
|
39426
|
+
* shape the path already has it: the restart fallback passes the rows its own
|
|
39427
|
+
* post-restart read found resident (`resident`), while the moved path passes
|
|
39428
|
+
* the pre-update snapshot (`before`) and this takes one fresh fleet read to
|
|
39429
|
+
* count it. A path with neither - no snapshot, or a fleet read that failed -
|
|
39430
|
+
* answers null/null, which the notice renders as the numberless sentence: a
|
|
39431
|
+
* count that cannot be measured is not a zero.
|
|
39432
|
+
*
|
|
39433
|
+
* The source travels with the count for the RECORD, and for any later reader
|
|
39434
|
+
* that needs to know how it was measured - it does not choose the notice's
|
|
39435
|
+
* words. The notice keeps ONE session-family sentence for both sources
|
|
39436
|
+
* (designer copy consult, 2026-09-30: the producer's vocabulary belongs in
|
|
39437
|
+
* the payload field, never in the reader's sentence), so a build count is not
|
|
39438
|
+
* printed as "N runtimes" and the liveness reading's documented over-count
|
|
39439
|
+
* rides the field beside the count instead of changing the sentence.
|
|
39440
|
+
*/
|
|
39441
|
+
async readStragglerCensus(input) {
|
|
39442
|
+
const build = await this.readRuntimeCensus(input.backend, input.current);
|
|
39443
|
+
if (build !== null) return { count: build, source: "build" };
|
|
39444
|
+
let resident;
|
|
39445
|
+
if (input.resident !== void 0) {
|
|
39446
|
+
resident = input.resident;
|
|
39447
|
+
} else if (input.before != null) {
|
|
39448
|
+
resident = this.stillResidentRows(
|
|
39449
|
+
input.before,
|
|
39450
|
+
await this.readFleetSnapshot(input.backend)
|
|
39451
|
+
);
|
|
39452
|
+
} else {
|
|
39453
|
+
resident = null;
|
|
39454
|
+
}
|
|
39455
|
+
if (resident === null) return { count: null, source: null };
|
|
39456
|
+
logger.info(
|
|
39457
|
+
`Straggler count from the liveness reading: ${resident.length} pre-swap session(s) still resident`,
|
|
39458
|
+
LogFileType.UPDATE_SERVICE
|
|
39459
|
+
);
|
|
39460
|
+
return { count: resident.length, source: "liveness" };
|
|
39461
|
+
}
|
|
39462
|
+
/**
|
|
39463
|
+
* The restart fallback's tail for a generation install: ONE read, the
|
|
39464
|
+
* engage, and the resident set - no retire wait (2026-09-30).
|
|
39465
|
+
*
|
|
39466
|
+
* WHY THERE IS NO WAIT HERE. The rebuild route's `reengageDisplacedSessions`
|
|
39467
|
+
* waits for the pre-swap set to retire because a rebuild rewrites the tree a
|
|
39468
|
+
* runtime is reading, so runtimes really do die under it and the wave takes
|
|
39469
|
+
* ~35 s to pass. A serve restart displaces nothing by itself - session
|
|
39470
|
+
* runtimes are detached and survived the bounce (they retire on their own
|
|
39471
|
+
* build check, whenever their next idle is) - and the measured ordinary shape
|
|
39472
|
+
* was an idle-gated fleet that never moved, so the wait burned its full 60 s
|
|
39473
|
+
* grace for an engage it never performed. The re-engage opportunity it
|
|
39474
|
+
* represented is idle's job now (the 2026-09-29 directive), and the honest
|
|
39475
|
+
* statement replaces the wait: engage only the sessions THIS read shows
|
|
39476
|
+
* without a runtime, and count the rest as still resident.
|
|
39477
|
+
*
|
|
39478
|
+
* IDEMPOTENT BY THE SAME READ: the engage set is the difference the one read
|
|
39479
|
+
* shows, so a runtime that came back on its own is never spawned twice.
|
|
39480
|
+
* Returns null when the read could not be taken - not measured, which the
|
|
39481
|
+
* census and the caller's log both carry as such.
|
|
39482
|
+
*/
|
|
39483
|
+
async engageDisplacedAfterFallbackRestart(backend, snapshot2) {
|
|
39484
|
+
if (snapshot2 === null) return null;
|
|
39485
|
+
if (snapshot2.length === 0) return [];
|
|
39486
|
+
const after = await this.readFleetSnapshot(backend);
|
|
39487
|
+
const displaced = displacedSessions(snapshot2, after);
|
|
39488
|
+
const engaged = [];
|
|
39489
|
+
const failed = [];
|
|
39490
|
+
for (const row of displaced) {
|
|
39491
|
+
try {
|
|
39492
|
+
if (await this.engageSessionRuntime(backend, row))
|
|
39493
|
+
engaged.push(row.sessionId);
|
|
39494
|
+
else
|
|
39495
|
+
failed.push({
|
|
39496
|
+
sessionId: row.sessionId,
|
|
39497
|
+
reason: "the server did not take the engage"
|
|
39498
|
+
});
|
|
39499
|
+
} catch (error) {
|
|
39500
|
+
failed.push({
|
|
39501
|
+
sessionId: row.sessionId,
|
|
39502
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
39503
|
+
});
|
|
39504
|
+
}
|
|
39505
|
+
}
|
|
39506
|
+
logger.info(
|
|
39507
|
+
`Re-engaged ${engaged.length} of ${displaced.length} session(s) displaced by the restart${failed.length > 0 ? `; ${failed.length} did not answer (${failed.map((row) => row.sessionId).join(", ")})` : ""}`,
|
|
39508
|
+
LogFileType.UPDATE_SERVICE
|
|
39509
|
+
);
|
|
39510
|
+
if (after === null) return null;
|
|
39511
|
+
return this.stillResidentRows(snapshot2, after);
|
|
39512
|
+
}
|
|
39513
|
+
/**
|
|
39514
|
+
* Say the rollover's evidence, once, where a post-mortem reads it.
|
|
39515
|
+
*
|
|
39516
|
+
* THE HONEST DEGRADE IS THE POINT (task §3): with the updater's structured
|
|
39517
|
+
* report absent - any core build before `update.report.v1` - serve is proven
|
|
39518
|
+
* by its own record and the daemons are BEST-EFFORT. That is said in exactly
|
|
39519
|
+
* those words rather than faked from a line that is not there, because the
|
|
39520
|
+
* alternative (treating an absent report as "daemons fine") is how a
|
|
39521
|
+
* user-visible claim gets made with no evidence behind it. A report that NAMES a
|
|
39522
|
+
* failed daemon is surfaced as a warning beside the same line.
|
|
39523
|
+
*
|
|
39524
|
+
* Names the three inputs the verdict read - the instance pair, the boot
|
|
39525
|
+
* reading, the `/health` answer - so a wrong verdict can be reconstructed
|
|
39526
|
+
* from the log without re-running anything.
|
|
39527
|
+
*/
|
|
39528
|
+
logRolloverEvidence(input) {
|
|
39529
|
+
const verdict = input.moved ? "proven moved" : "not proven moved";
|
|
39530
|
+
const facts = `instance ${input.instanceBefore ?? "no reading"} -> ${input.recordAfter.instanceId ?? "no reading"}, boot ${input.recordAfter.bootVersion ?? "no reading"}, /health ${input.healthOk ? "answered" : "did not answer"}`;
|
|
39531
|
+
const daemons = daemonMoveOutcome(input.report);
|
|
39532
|
+
if (daemons === null) {
|
|
39533
|
+
logger.info(
|
|
39534
|
+
`Rollover evidence: serve ${verdict} (${facts}); the updater printed no update.report.v1 line, so the daemons are best-effort with no evidence either way`,
|
|
39535
|
+
LogFileType.UPDATE_SERVICE
|
|
39536
|
+
);
|
|
39537
|
+
return;
|
|
39538
|
+
}
|
|
39539
|
+
logger.info(
|
|
39540
|
+
`Rollover evidence: serve ${verdict} (${facts}); the updater's report says ${daemons.moved ? "every supervised daemon is refreshed or already current" : `these daemons did not refresh: ${daemons.failed.join(", ")}`}`,
|
|
39541
|
+
LogFileType.UPDATE_SERVICE
|
|
39542
|
+
);
|
|
39543
|
+
if (!daemons.moved) {
|
|
39544
|
+
logger.warn(
|
|
39545
|
+
`Daemon refresh incomplete after the update: ${daemons.failed.join(", ")} (named by the update.report.v1 line; serve's own move is a separate reading)`,
|
|
39546
|
+
LogFileType.UPDATE_SERVICE
|
|
39547
|
+
);
|
|
39548
|
+
}
|
|
39549
|
+
}
|
|
38717
39550
|
/**
|
|
38718
39551
|
* The build the daemon serving this app BOOTED with, or null when its record
|
|
38719
39552
|
* carries no reading.
|
|
@@ -38735,7 +39568,7 @@ ${heal.removed.join("\n")}`,
|
|
|
38735
39568
|
* the same pair rather than about two different questions.
|
|
38736
39569
|
*/
|
|
38737
39570
|
servingBootVersion() {
|
|
38738
|
-
return this.
|
|
39571
|
+
return this.servingRecordReadings().bootVersion;
|
|
38739
39572
|
}
|
|
38740
39573
|
/**
|
|
38741
39574
|
* The running reading an update event carries to the renderer.
|
|
@@ -39106,6 +39939,7 @@ ${heal.removed.join("\n")}`,
|
|
|
39106
39939
|
sourceRebuild: rebuildPath
|
|
39107
39940
|
});
|
|
39108
39941
|
const rebuildRoute = freshPlan.managedRoute === "source-build" && rebuildPath !== null;
|
|
39942
|
+
const generationInstall = freshPlan.managedRoute === "entry-point";
|
|
39109
39943
|
if (!consolePath && !rebuildRoute) {
|
|
39110
39944
|
this.sendToRenderer("backend-update-error", {
|
|
39111
39945
|
message: "The global install could not be found on this machine, so the update did not run. See the update service log.",
|
|
@@ -39118,6 +39952,8 @@ ${heal.removed.join("\n")}`,
|
|
|
39118
39952
|
const before = this.readInstallVersionAt(installPath) ?? plan.installedInstallVersion;
|
|
39119
39953
|
const markerBefore = rebuildRoute ? this.readRebuildMarkerState(installPath) : null;
|
|
39120
39954
|
const fleetBeforeInstall = rebuildRoute ? await this.readFleetSnapshot(backend) : null;
|
|
39955
|
+
const serveInstanceBefore = generationInstall ? this.servingRecordReadings().instanceId : null;
|
|
39956
|
+
const fleetBeforeUpdate = generationInstall ? await this.readFleetSnapshot(backend) : null;
|
|
39121
39957
|
if (rebuildRoute && !await this.drainFleetForUpdate({
|
|
39122
39958
|
backend,
|
|
39123
39959
|
command: freshPlan.updateCommand ?? null
|
|
@@ -39216,9 +40052,63 @@ ${heal.removed.join("\n")}`,
|
|
|
39216
40052
|
`Global install updated: ${before ?? "unknown"} -> ${after ?? "unknown"}`,
|
|
39217
40053
|
LogFileType.UPDATE_SERVICE
|
|
39218
40054
|
);
|
|
39219
|
-
const
|
|
39220
|
-
|
|
39221
|
-
|
|
40055
|
+
const updateReport = generationInstall ? parseUpdateReport(run.stdout) : null;
|
|
40056
|
+
let serveRecordAfter = { bootVersion: null, instanceId: null };
|
|
40057
|
+
let serveHealthOk = null;
|
|
40058
|
+
let serveMovedWithoutRestart = false;
|
|
40059
|
+
if (generationInstall) {
|
|
40060
|
+
serveRecordAfter = this.servingRecordReadings();
|
|
40061
|
+
serveHealthOk = await this.checkBackendHealth();
|
|
40062
|
+
serveMovedWithoutRestart = serveMovedOntoBuild({
|
|
40063
|
+
instanceBefore: serveInstanceBefore,
|
|
40064
|
+
recordAfter: serveRecordAfter,
|
|
40065
|
+
installVersion: after,
|
|
40066
|
+
target: target2,
|
|
40067
|
+
reportServe: updateReport?.serve[0] ?? null,
|
|
40068
|
+
healthOk: serveHealthOk
|
|
40069
|
+
});
|
|
40070
|
+
this.logRolloverEvidence({
|
|
40071
|
+
report: updateReport,
|
|
40072
|
+
moved: serveMovedWithoutRestart,
|
|
40073
|
+
instanceBefore: serveInstanceBefore,
|
|
40074
|
+
recordAfter: serveRecordAfter,
|
|
40075
|
+
healthOk: serveHealthOk
|
|
40076
|
+
});
|
|
40077
|
+
}
|
|
40078
|
+
if (serveMovedWithoutRestart) {
|
|
40079
|
+
const census = await this.readStragglerCensus({
|
|
40080
|
+
backend,
|
|
40081
|
+
current: serveRecordAfter.bootVersion ?? after ?? target2,
|
|
40082
|
+
before: fleetBeforeUpdate
|
|
40083
|
+
});
|
|
40084
|
+
logger.info(
|
|
40085
|
+
`The server serving this app moved onto ${serveRecordAfter.bootVersion ?? "the new build"} without a restart (instance changed, /health answered, ${census.count ?? "no"} straggler(s) counted from ${census.source ?? "no"} reading); completing without bouncing it`,
|
|
40086
|
+
LogFileType.UPDATE_SERVICE
|
|
40087
|
+
);
|
|
40088
|
+
this.sendToRenderer("backend-update-completed", {
|
|
40089
|
+
installVersion: after,
|
|
40090
|
+
/*
|
|
40091
|
+
* THE RECORD's reading, not `/health`'s (round 2, T1): the panel
|
|
40092
|
+
* compares these two strings, and only the process's own record
|
|
40093
|
+
* names the build it actually loaded. The moved verdict requires
|
|
40094
|
+
* a boot reading, so this arm cannot send a null it proved.
|
|
40095
|
+
*/
|
|
40096
|
+
runningVersion: serveRecordAfter.bootVersion,
|
|
40097
|
+
moved: true,
|
|
40098
|
+
restarted: false,
|
|
40099
|
+
restartable: this.backendIsAppOwned(),
|
|
40100
|
+
/*
|
|
40101
|
+
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
40102
|
+
* the global install's own updater, so the environment it moved is never
|
|
40103
|
+
* the app's managed one (design D5).
|
|
40104
|
+
*/
|
|
40105
|
+
appOwnedEnvironment: false,
|
|
40106
|
+
sessionsOnOldBuild: census.count,
|
|
40107
|
+
sessionsOnOldBuildSource: census.source
|
|
40108
|
+
});
|
|
40109
|
+
return true;
|
|
40110
|
+
}
|
|
40111
|
+
if (backend.isUsingExternalBackend()) {
|
|
39222
40112
|
const health = await this.getInstalledBackendVersion();
|
|
39223
40113
|
const running2 = this.eventRunningVersion(health);
|
|
39224
40114
|
logger.info(
|
|
@@ -39229,6 +40119,7 @@ ${heal.removed.join("\n")}`,
|
|
|
39229
40119
|
installVersion: after,
|
|
39230
40120
|
runningVersion: running2,
|
|
39231
40121
|
restarted: false,
|
|
40122
|
+
moved: false,
|
|
39232
40123
|
restartable: this.backendIsAppOwned(),
|
|
39233
40124
|
/*
|
|
39234
40125
|
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
@@ -39239,13 +40130,21 @@ ${heal.removed.join("\n")}`,
|
|
|
39239
40130
|
});
|
|
39240
40131
|
return true;
|
|
39241
40132
|
}
|
|
40133
|
+
if (generationInstall) {
|
|
40134
|
+
logger.info(
|
|
40135
|
+
`Install moved to ${after ?? "unknown"}; the serving process's own record does not prove it moved onto the new build (instance ${serveInstanceBefore ?? "no reading"} -> ${serveRecordAfter.instanceId ?? "no reading"}, boot ${serveRecordAfter.bootVersion ?? "no reading"}, /health ${serveHealthOk ? "answered" : "did not answer"}); restarting the daemon this app owns onto it`,
|
|
40136
|
+
LogFileType.UPDATE_SERVICE
|
|
40137
|
+
);
|
|
40138
|
+
}
|
|
39242
40139
|
logger.info(
|
|
39243
40140
|
"Restarting backend service onto the updated install...",
|
|
39244
40141
|
LogFileType.UPDATE_SERVICE
|
|
39245
40142
|
);
|
|
39246
40143
|
this.sendToRenderer("backend-update-progress", { phase: "restarting" });
|
|
39247
40144
|
const fleetBeforeRestart = await this.readFleetSnapshot(backend);
|
|
39248
|
-
const restartSuccess = await backend.restart(
|
|
40145
|
+
const restartSuccess = await backend.restart({
|
|
40146
|
+
stopGraceMs: UPDATE_RESTART_STOP_GRACE_MS
|
|
40147
|
+
});
|
|
39249
40148
|
if (!restartSuccess) {
|
|
39250
40149
|
logger.error(
|
|
39251
40150
|
"Backend service restart failed after the global install update",
|
|
@@ -39302,12 +40201,43 @@ ${heal.removed.join("\n")}`,
|
|
|
39302
40201
|
fleetBeforeInstall,
|
|
39303
40202
|
fleetBeforeRestart
|
|
39304
40203
|
);
|
|
39305
|
-
|
|
40204
|
+
let sessionsOnOldBuild;
|
|
40205
|
+
let sessionsOnOldBuildSource;
|
|
40206
|
+
if (rebuildRoute) {
|
|
40207
|
+
const reengage = reengageSnapshot !== null ? await this.reengageFleetAfterRestart(backend, reengageSnapshot) : null;
|
|
40208
|
+
const census = await this.readStragglerCensus({
|
|
40209
|
+
backend,
|
|
40210
|
+
current: after ?? target2,
|
|
40211
|
+
resident: reengage !== null ? reengage.stillResident : null
|
|
40212
|
+
});
|
|
40213
|
+
sessionsOnOldBuild = census.count;
|
|
40214
|
+
sessionsOnOldBuildSource = census.source;
|
|
40215
|
+
} else {
|
|
40216
|
+
const resident = await this.engageDisplacedAfterFallbackRestart(
|
|
40217
|
+
backend,
|
|
40218
|
+
reengageSnapshot
|
|
40219
|
+
);
|
|
40220
|
+
const census = await this.readStragglerCensus({
|
|
40221
|
+
backend,
|
|
40222
|
+
current: reported ?? after ?? target2,
|
|
40223
|
+
resident
|
|
40224
|
+
});
|
|
40225
|
+
sessionsOnOldBuild = census.count;
|
|
40226
|
+
sessionsOnOldBuildSource = census.source;
|
|
40227
|
+
}
|
|
39306
40228
|
this.sendToRenderer("backend-update-completed", {
|
|
39307
40229
|
installVersion: after,
|
|
39308
40230
|
runningVersion: reported,
|
|
39309
40231
|
restarted: true,
|
|
39310
|
-
|
|
40232
|
+
/*
|
|
40233
|
+
* The app moved this daemon onto the new build with the restart, and the
|
|
40234
|
+
* distinction is stated where it was proven (`reported` was held
|
|
40235
|
+
* at-or-past the target above). Absent on the rebuild route, which keeps
|
|
40236
|
+
* its version and its own sentence.
|
|
40237
|
+
*/
|
|
40238
|
+
...rebuildRoute ? {} : { moved: true },
|
|
40239
|
+
sessionsOnOldBuild,
|
|
40240
|
+
sessionsOnOldBuildSource
|
|
39311
40241
|
});
|
|
39312
40242
|
return true;
|
|
39313
40243
|
}
|
|
@@ -40516,9 +41446,15 @@ if (process.platform === "darwin" && electron.app.dock) {
|
|
|
40516
41446
|
if (windowLaunch.hideDock) {
|
|
40517
41447
|
electron.app.dock.hide();
|
|
40518
41448
|
} else {
|
|
41449
|
+
electron.app.dock.show();
|
|
40519
41450
|
electron.app.dock.setIcon(image);
|
|
40520
41451
|
}
|
|
40521
41452
|
}
|
|
41453
|
+
function reassertDockParticipation() {
|
|
41454
|
+
if (process.platform !== "darwin" || !electron.app.dock) return;
|
|
41455
|
+
if (windowLaunch.hideDock) return;
|
|
41456
|
+
electron.app.dock.show();
|
|
41457
|
+
}
|
|
40522
41458
|
const telemetryLaunch = resolveTelemetryLaunch({
|
|
40523
41459
|
env: launchEnv,
|
|
40524
41460
|
projectKey: backendConfig.VITE_PUBLIC_POSTHOG_KEY
|
|
@@ -41706,24 +42642,37 @@ electron.app.whenReady().then(async () => {
|
|
|
41706
42642
|
return false;
|
|
41707
42643
|
}
|
|
41708
42644
|
});
|
|
41709
|
-
electron.ipcMain.handle("show-open-dialog", async (
|
|
41710
|
-
|
|
42645
|
+
electron.ipcMain.handle("show-open-dialog", async (event, options) => {
|
|
42646
|
+
const parent = electron.BrowserWindow.fromWebContents(event.sender) ?? mainWindow;
|
|
42647
|
+
if (!parent) {
|
|
41711
42648
|
logger.error(
|
|
41712
|
-
"Cannot show open dialog:
|
|
42649
|
+
"Cannot show open dialog: no window can own it.",
|
|
41713
42650
|
LogFileType.BACKEND
|
|
41714
42651
|
);
|
|
41715
42652
|
return { canceled: true, filePaths: [] };
|
|
41716
42653
|
}
|
|
41717
|
-
const
|
|
41718
|
-
|
|
41719
|
-
|
|
41720
|
-
|
|
41721
|
-
|
|
41722
|
-
|
|
41723
|
-
|
|
41724
|
-
|
|
41725
|
-
|
|
41726
|
-
|
|
42654
|
+
const mini = miniView;
|
|
42655
|
+
const fromMini = mini !== null && !mini.window.isDestroyed() && event.sender === mini.window.webContents;
|
|
42656
|
+
const dialogPayload = { open: true };
|
|
42657
|
+
if (fromMini)
|
|
42658
|
+
mini.window.webContents.send(MINI_VIEW_DIALOG, dialogPayload);
|
|
42659
|
+
try {
|
|
42660
|
+
const result = await electron.dialog.showOpenDialog(
|
|
42661
|
+
parent,
|
|
42662
|
+
withRememberedDirectory(
|
|
42663
|
+
"open-file",
|
|
42664
|
+
options,
|
|
42665
|
+
pickerFallbackDirectory()
|
|
42666
|
+
)
|
|
42667
|
+
);
|
|
42668
|
+
rememberPickedDirectory("open-file", result.filePaths);
|
|
42669
|
+
return result;
|
|
42670
|
+
} finally {
|
|
42671
|
+
if (fromMini && mini !== null && !mini.window.isDestroyed())
|
|
42672
|
+
mini.window.webContents.send(MINI_VIEW_DIALOG, {
|
|
42673
|
+
open: false
|
|
42674
|
+
});
|
|
42675
|
+
}
|
|
41727
42676
|
});
|
|
41728
42677
|
electron.ipcMain.handle("select-directory", async () => {
|
|
41729
42678
|
if (!mainWindow) {
|
|
@@ -42132,6 +43081,7 @@ electron.app.whenReady().then(async () => {
|
|
|
42132
43081
|
};
|
|
42133
43082
|
setupMainWindowWithUpdateService(launchSession, launchCatalogue);
|
|
42134
43083
|
electron.app.on("activate", () => {
|
|
43084
|
+
reassertDockParticipation();
|
|
42135
43085
|
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
|
42136
43086
|
if (quitState.isQuitting()) {
|
|
42137
43087
|
reportSkippedWhileQuitting(
|
|
@@ -42171,6 +43121,13 @@ electron.app.on("window-all-closed", () => {
|
|
|
42171
43121
|
electron.app.quit();
|
|
42172
43122
|
return;
|
|
42173
43123
|
}
|
|
43124
|
+
if (quitState.isQuitting()) {
|
|
43125
|
+
logger.info(
|
|
43126
|
+
"All windows closed as part of a quit in progress; will-quit owns the completion",
|
|
43127
|
+
LogFileType.BACKEND
|
|
43128
|
+
);
|
|
43129
|
+
return;
|
|
43130
|
+
}
|
|
42174
43131
|
logger.info(
|
|
42175
43132
|
"All windows closed, but keeping app active (macOS platform)",
|
|
42176
43133
|
LogFileType.BACKEND
|
|
@@ -42210,7 +43167,7 @@ electron.app.on("will-quit", (event) => {
|
|
|
42210
43167
|
failsafe.unref();
|
|
42211
43168
|
void backendService.stop(false).then(() => {
|
|
42212
43169
|
clearTimeout(failsafe);
|
|
42213
|
-
electron.app.
|
|
43170
|
+
electron.app.exit(0);
|
|
42214
43171
|
}).catch((error) => {
|
|
42215
43172
|
clearTimeout(failsafe);
|
|
42216
43173
|
logger.error(
|
|
@@ -42229,22 +43186,33 @@ const holdQuitForSessionCookieSnapshot = createSessionCookieQuitHold({
|
|
|
42229
43186
|
});
|
|
42230
43187
|
electron.app.on("before-quit", async (event) => {
|
|
42231
43188
|
quitState.begin();
|
|
43189
|
+
electron.globalShortcut.unregisterAll();
|
|
43190
|
+
miniViewRegistrar?.dispose();
|
|
43191
|
+
quickSendWatcher?.stop();
|
|
43192
|
+
miniView?.dispose();
|
|
43193
|
+
if (windowLaunch.mode === "headless") {
|
|
43194
|
+
launcherWatch?.stop();
|
|
43195
|
+
armHeadlessExitDeadline("the app is quitting");
|
|
43196
|
+
}
|
|
42232
43197
|
if (await holdQuitForSessionCookieSnapshot(event)) return;
|
|
42233
43198
|
logger.info("App is about to quit", LogFileType.BACKEND);
|
|
43199
|
+
if (!backendService.isOwnedCleanupComplete()) {
|
|
43200
|
+
try {
|
|
43201
|
+
await backendService.stop(false);
|
|
43202
|
+
} catch (error) {
|
|
43203
|
+
logger.error(
|
|
43204
|
+
"Owned backend stop rejected during quit; continuing the shutdown",
|
|
43205
|
+
LogFileType.BACKEND,
|
|
43206
|
+
error
|
|
43207
|
+
);
|
|
43208
|
+
}
|
|
43209
|
+
}
|
|
42234
43210
|
if (parkedLaunches.length > 0) {
|
|
42235
43211
|
reportParksAtQuit(
|
|
42236
43212
|
parkedLaunches.map((parked) => parked.session),
|
|
42237
43213
|
reportRaise
|
|
42238
43214
|
);
|
|
42239
43215
|
}
|
|
42240
|
-
if (windowLaunch.mode === "headless") {
|
|
42241
|
-
launcherWatch?.stop();
|
|
42242
|
-
armHeadlessExitDeadline("the app is quitting");
|
|
42243
|
-
}
|
|
42244
|
-
electron.globalShortcut.unregisterAll();
|
|
42245
|
-
miniViewRegistrar?.dispose();
|
|
42246
|
-
quickSendWatcher?.stop();
|
|
42247
|
-
miniView?.dispose();
|
|
42248
43216
|
activeUpdateService?.quitForInFlightInstall("app quit");
|
|
42249
43217
|
void stopBrowserHost();
|
|
42250
43218
|
});
|