local-operator-ui 0.26.10 → 0.27.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.
- package/out/main/index.js +569 -58
- package/out/preload/index.js +15 -1
- package/out/renderer/assets/{_basePickBy-BdydKoij.js → _basePickBy-Bc8mqN6i.js} +1 -1
- package/out/renderer/assets/{_baseUniq-BpVbhhNL.js → _baseUniq-CJFnmQVQ.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DAXdL2AC.js → agent-details-page-DkshJZak.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-DX3woSzq.js → agent-hub-page-BNgJSDD-.js} +1 -1
- package/out/renderer/assets/{agents-page-Bi73jo2W.js → agents-page-CZhbc7M_.js} +2 -2
- package/out/renderer/assets/{arc-BuGUakcY.js → arc-_VNZ7Lk1.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-Dnbac4Ez.js → architectureDiagram-IEHRJDOE-DTpnyiCl.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-LQLL2aoo.js → blockDiagram-JOT3LUYC-WRMQhKcT.js} +1 -1
- package/out/renderer/assets/browser-page-DxvaF_od.js +1 -0
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-Dkwrfgoq.js → c4Diagram-VJAJSXHY-Dqr2t_SX.js} +1 -1
- package/out/renderer/assets/channel-DWyvkgwR.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-CoYUrJk7.js → chunk-4BMEZGHF-VrQLaWOX.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-HorAhh_g.js → chunk-A2AXSNBT-JbtRYOmY.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-DTzjT6Fa.js → chunk-AEK57VVT-D5ZhImUH.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-B7nQtFoz.js → chunk-D6G4REZN-DUc5G8Qu.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-D88vbt_N.js → chunk-RZ5BOZE2-torZvZ85.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-0EUd2ZoR.js → chunk-XZIHB7SX-CkRX9XJ3.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CjbYXD4Y.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CjbYXD4Y.js +1 -0
- package/out/renderer/assets/clone-BLuae6Q5.js +1 -0
- package/out/renderer/assets/{compact-pagination-C6rPEaEV.js → compact-pagination-C8j5ZOjD.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-BP1ipKDa.js → dagre-OKDRZEBW-BeSeozND.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-4RrMavMM.js → diagram-SSKATNLV-B1DwVFDf.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-1d8WVjL6.js → diagram-VNBRO52H-D-c30ym_.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-Bm8_w06I.js → erDiagram-Q7BY3M3F-DIssMOat.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-CldQlpnE.js → flowDiagram-4HSFHLVR-CdTNf2L5.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-DBoRJes4.js → ganttDiagram-APWFNJXF-DGl__V69.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-D7mryG21.js → gitGraphDiagram-7IBYFJ6S-CDEvouMR.js} +1 -1
- package/out/renderer/assets/{graph-D0QWVchm.js → graph-DcplOFs2.js} +1 -1
- package/out/renderer/assets/icon-DITfm2jL.css +1 -0
- package/out/renderer/assets/{index-DIcuvujQ.js → index-BRiWNQSn.js} +1 -1
- package/out/renderer/assets/{index-D2Bt0r9y.js → index-DDuEnqhv.js} +394 -387
- package/out/renderer/assets/{index-DG4o13Hg.js → index-DrNa2kYY.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-BkQngrDA.js → infoDiagram-PH2N3AL5-Fj0-QRyo.js} +1 -1
- package/out/renderer/assets/{installer-BTwDkxw3.js → installer-C5Cni-NB.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-C_W7LIeb.js → journeyDiagram-U35MCT3I-DuqPXQA0.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-Dk0byars.js → kanban-definition-NDS4AKOZ-BNm-Nc45.js} +1 -1
- package/out/renderer/assets/{layout-Dw37GQ8H.js → layout-Bf7tY41L.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-Dwhqro7r.js → legacy-agents-page-Zsh4wkfD.js} +1 -1
- package/out/renderer/assets/{mermaid.core-fF8rQxbR.js → mermaid.core-ovJ-Ftst.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-Bar5MZkh.js → mindmap-definition-ALO5MXBD-DeoEktqp.js} +1 -1
- package/out/renderer/assets/{page-header-BxEtfYZ4.js → page-header-dhdkzSOM.js} +1 -1
- package/out/renderer/assets/{parseISO-Ch7Ia-JL.js → parseISO-X5g1SrZ4.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-CKsztwrg.js → pieDiagram-IB7DONF6-DZVzWOqr.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-DfyPHpBg.js → quadrantDiagram-7GDLP6J5-C3XUFEBW.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-CTTPT_0V.js → radar-MK3ICKWK-BUR-aXwF.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-BOFrJt0t.js → radient-auth-buttons-BQxlFLQk.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-WGOccSbh.js → requirementDiagram-KVF5MWMF-BToLYBNy.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-BgA-fd7Q.js → sankeyDiagram-QLVOVGJD-DnLYPQYY.js} +1 -1
- package/out/renderer/assets/{schedules-page-Cx6KQ-L9.js → schedules-page-BzekBI69.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-SLQSmXRs.js → sequenceDiagram-X6HHIX6F--PfZO0A5.js} +1 -1
- package/out/renderer/assets/{settings-page-DLS-I3Yr.js → settings-page-A2CdEifL.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-BPGUu0oZ.js → stateDiagram-DGXRK772-D5HQXkE9.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Dy6Q4aId.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-DcoCR1Vk.js → timeline-definition-BDJGKUSR-BqRqVKHe.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-BWTNRQ9u.js → use-agent-like-mutation-CznWOMhh.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-BSep-6zq.js → xychartDiagram-VJFVF3MP-Co3C9GP-.js} +1 -1
- package/out/renderer/index.html +3 -3
- package/out/renderer/installer.html +3 -3
- package/package.json +3 -3
- package/out/renderer/assets/browser-page-CbGKjPow.js +0 -1
- package/out/renderer/assets/channel-C3VRqBwG.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CWF6V5Fp.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CWF6V5Fp.js +0 -1
- package/out/renderer/assets/clone-DXkbKicv.js +0 -1
- package/out/renderer/assets/icon-BVsYZnXF.css +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CajJa5pO.js +0 -1
- /package/out/renderer/assets/{icon-CyUpM0N4.js → icon-CIk6DTWl.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -1555,6 +1555,7 @@ function desktopEndpoint(request) {
|
|
|
1555
1555
|
}
|
|
1556
1556
|
}
|
|
1557
1557
|
const MAX_PROBE_PATHS = 64;
|
|
1558
|
+
const DIRECTORY_ENTRY_LIMIT = 200;
|
|
1558
1559
|
const MAX_FILE_READ_BYTES = 64 * 1024 * 1024;
|
|
1559
1560
|
const OPEN_SESSION_FLAG = "--open-session";
|
|
1560
1561
|
const OPEN_CATALOGUE_FLAG = "--open-catalogue";
|
|
@@ -4577,7 +4578,7 @@ function compareVersions(a, b) {
|
|
|
4577
4578
|
}
|
|
4578
4579
|
return 0;
|
|
4579
4580
|
}
|
|
4580
|
-
function compareCandidates(a, b, preferredPrefix) {
|
|
4581
|
+
function compareCandidates$1(a, b, preferredPrefix) {
|
|
4581
4582
|
const sameInstall = (candidate) => preferredPrefix !== null && preferredPrefix !== "" && candidate.record.prefix === preferredPrefix;
|
|
4582
4583
|
if (sameInstall(a) !== sameInstall(b)) return sameInstall(a) ? -1 : 1;
|
|
4583
4584
|
if (a.source !== b.source) return a.source === "record" ? -1 : 1;
|
|
@@ -4590,7 +4591,7 @@ function compareCandidates(a, b, preferredPrefix) {
|
|
|
4590
4591
|
}
|
|
4591
4592
|
function rankCandidates(candidates, preferredPrefix) {
|
|
4592
4593
|
return [...candidates].sort(
|
|
4593
|
-
(a, b) => compareCandidates(a, b, preferredPrefix)
|
|
4594
|
+
(a, b) => compareCandidates$1(a, b, preferredPrefix)
|
|
4594
4595
|
);
|
|
4595
4596
|
}
|
|
4596
4597
|
async function discoverDaemons(options = {}) {
|
|
@@ -10851,6 +10852,10 @@ class TabRegistry {
|
|
|
10851
10852
|
// silently reclaim the tab it owned in a previous app run.
|
|
10852
10853
|
owner: restored ? "user" : options.owner,
|
|
10853
10854
|
sessionId: restored ? null : options.sessionId ?? null,
|
|
10855
|
+
// The same value `sessionId` gets at creation, kept because a hand-over
|
|
10856
|
+
// overwrites `sessionId` and a revocation has to put the conversation back
|
|
10857
|
+
// (see the field's own note).
|
|
10858
|
+
homeSessionId: restored ? null : options.sessionId ?? null,
|
|
10854
10859
|
nonce: restored || options.owner === "user" ? null : mintNonce(),
|
|
10855
10860
|
handedTo: null,
|
|
10856
10861
|
restored,
|
|
@@ -10974,13 +10979,23 @@ class TabRegistry {
|
|
|
10974
10979
|
this.onChanged();
|
|
10975
10980
|
}
|
|
10976
10981
|
/** Revoke a hand-over: nulls the nonce, so the session's next action on that
|
|
10977
|
-
* handle gets the ordinary `tab_closed` refusal
|
|
10982
|
+
* handle gets the ordinary `tab_closed` refusal, and puts the tab back in the
|
|
10983
|
+
* conversation the user opened it in.
|
|
10984
|
+
*
|
|
10985
|
+
* THE ATTRIBUTION COMES BACK, NOT JUST THE OWNERSHIP (review round 1, Q2): the
|
|
10986
|
+
* revocation used to null `sessionId` outright, which took the tab out of the
|
|
10987
|
+
* conversation it was opened in (`tabsInScope` reads that field, and so does the
|
|
10988
|
+
* conversation's own tab count) — so revoking a hand-over silently lost the
|
|
10989
|
+
* conversation, and the design's §5.1 row 6 says a revoke is a true inverse. The
|
|
10990
|
+
* ownership and the capability still go, and `mayDrive` still refuses without the
|
|
10991
|
+
* nonce. A tab with no home (the route, a draft, a restored tab) still lands on
|
|
10992
|
+
* `null`, which is the behaviour it always had. */
|
|
10978
10993
|
revokeHandOver(tabId) {
|
|
10979
10994
|
const record = this.tabs.get(tabId);
|
|
10980
10995
|
if (!record) return;
|
|
10981
10996
|
record.nonce = null;
|
|
10982
10997
|
record.owner = "user";
|
|
10983
|
-
record.sessionId =
|
|
10998
|
+
record.sessionId = record.homeSessionId;
|
|
10984
10999
|
record.handedTo = null;
|
|
10985
11000
|
record.allocationId = "";
|
|
10986
11001
|
this.bumpEpoch(record.tabId);
|
|
@@ -11082,24 +11097,89 @@ class TabRegistry {
|
|
|
11082
11097
|
}
|
|
11083
11098
|
// ---- removal -------------------------------------------------------------
|
|
11084
11099
|
/**
|
|
11085
|
-
* Drop
|
|
11086
|
-
*
|
|
11087
|
-
*
|
|
11088
|
-
*
|
|
11089
|
-
*
|
|
11090
|
-
*
|
|
11100
|
+
* Drop one tab's record WITHOUT notifying: the registry-local half of every removal.
|
|
11101
|
+
*
|
|
11102
|
+
* EXTRACTED FROM `forget` FOR `destroyMany` (design R5), and the split is what makes a
|
|
11103
|
+
* batch one change rather than N: `destroyMany` drops every record first, then runs
|
|
11104
|
+
* ONE `applyLayout()` and ONE `onChanged()`, and the notify-free half is the only way
|
|
11105
|
+
* to do that without N full state builds, N IPC broadcasts and N `session.json`
|
|
11106
|
+
* writes — which is what N sequential closes cost today (`host.closeTab` →
|
|
11107
|
+
* `registry.destroy` → `onChanged` per tab).
|
|
11091
11108
|
*/
|
|
11092
|
-
|
|
11109
|
+
drop(tabId) {
|
|
11093
11110
|
const record = this.tabs.get(tabId);
|
|
11094
11111
|
if (!record) return void 0;
|
|
11095
11112
|
this.tabs.delete(tabId);
|
|
11096
11113
|
if (this.activeTabId === tabId) {
|
|
11097
11114
|
this.activeTabId = this.list().filter((candidate) => candidate.owner === "user").at(-1)?.tabId ?? null;
|
|
11098
11115
|
}
|
|
11116
|
+
return record;
|
|
11117
|
+
}
|
|
11118
|
+
/**
|
|
11119
|
+
* Drop a tab and hand its webContents id to `onRemove`, which is where the
|
|
11120
|
+
* debugger session, the log buffer and the child view are released.
|
|
11121
|
+
*
|
|
11122
|
+
* `forget` is the registry-local half PLUS the notification: `drop` above removes the
|
|
11123
|
+
* record, and this runs the one `applyLayout()`/`onChanged()` pair every single-tab
|
|
11124
|
+
* removal has always run. It is what `destroy` below and the fail-closed handle check
|
|
11125
|
+
* use (where the caller has no cleanup work to do — the webContents is already
|
|
11126
|
+
* destroyed), while `destroyMany` uses `drop` for the same reason one level down: it
|
|
11127
|
+
* wants N removals under ONE notification rather than N of each.
|
|
11128
|
+
*
|
|
11129
|
+
* WHAT IT IS NOT, because this file is where a reader learns which removal path
|
|
11130
|
+
* notifies and it used to say the opposite of `drop`'s note (review round 1, A8):
|
|
11131
|
+
* `forget` is NOT the registry-local half — `drop` above is — and a caller that wants
|
|
11132
|
+
* the removal without the notification wants `drop`.
|
|
11133
|
+
*/
|
|
11134
|
+
forget(tabId) {
|
|
11135
|
+
const record = this.drop(tabId);
|
|
11136
|
+
if (!record) return void 0;
|
|
11099
11137
|
this.applyLayout();
|
|
11100
11138
|
this.onChanged();
|
|
11101
11139
|
return record;
|
|
11102
11140
|
}
|
|
11141
|
+
/**
|
|
11142
|
+
* Close SEVERAL tabs as one change: the registry half of the renderer's bulk closes
|
|
11143
|
+
* (design R5).
|
|
11144
|
+
*
|
|
11145
|
+
* WHY ONE INTENT RATHER THAN N `destroy` CALLS, in the order the design weighs it:
|
|
11146
|
+
*
|
|
11147
|
+
* 1. "Close all tabs in this conversation" cannot be expressed as a list without
|
|
11148
|
+
* racing. A list computed from a projection the renderer read up to seconds ago
|
|
11149
|
+
* misses a tab an agent opened in that conversation in the meantime, and the user
|
|
11150
|
+
* pressed something that said ALL. (The renderer resolves that mode HERE, at
|
|
11151
|
+
* execution time, for exactly this reason.)
|
|
11152
|
+
* 2. N round trips are N full `chromeState()` builds, N IPC broadcasts and N
|
|
11153
|
+
* `session.json` writes, each of which re-renders the strip. A batch is one of
|
|
11154
|
+
* each.
|
|
11155
|
+
* 3. `destroy` is synchronous and the chrome's closes do not take the per-tab lane
|
|
11156
|
+
* (`registry.lane` is the dispatcher's), so there is no interleaving to reason
|
|
11157
|
+
* about within one intent — and N separate intents are N separate windows in which
|
|
11158
|
+
* an agent can create a tab. One intent, one window, one decision.
|
|
11159
|
+
*
|
|
11160
|
+
* IDS THAT ARE ALREADY GONE ARE SKIPPED RATHER THAN REFUSED: the user's intent was
|
|
11161
|
+
* that they be closed, and they are. Refusing the batch because one tab was closed
|
|
11162
|
+
* twice — by a second press, or by an agent's own `close` — would leave the other
|
|
11163
|
+
* tabs open, which is the one outcome nobody asked for.
|
|
11164
|
+
*
|
|
11165
|
+
* `onRemove` STILL RUNS PER RECORD and after the single notification, so the
|
|
11166
|
+
* webContents/debugger/log release path stays the single one it has always been
|
|
11167
|
+
* (design 11.1: with `WebContentsView`, nothing destroys the webContents for you).
|
|
11168
|
+
*/
|
|
11169
|
+
destroyMany(tabIds) {
|
|
11170
|
+
const removed = [];
|
|
11171
|
+
for (const tabId of tabIds) {
|
|
11172
|
+
const record = this.drop(tabId);
|
|
11173
|
+
if (record) removed.push(record);
|
|
11174
|
+
}
|
|
11175
|
+
if (removed.length === 0) return 0;
|
|
11176
|
+
this.applyLayout();
|
|
11177
|
+
this.onChanged();
|
|
11178
|
+
for (const record of removed) {
|
|
11179
|
+
this.onRemove(record.tabId, record.view.webContents.id);
|
|
11180
|
+
}
|
|
11181
|
+
return removed.length;
|
|
11182
|
+
}
|
|
11103
11183
|
/** Destroy a tab: forget it, then release its resources. `onRemove` is
|
|
11104
11184
|
* called for every removal path so a tab can never be forgotten while its
|
|
11105
11185
|
* webContents, debugger session or log buffer leak (design 11.1: with
|
|
@@ -11716,10 +11796,15 @@ class BrowserHost {
|
|
|
11716
11796
|
owner: entry.owner,
|
|
11717
11797
|
// WHICH CONVERSATION THIS TAB BELONGS TO, and the ONLY new field this
|
|
11718
11798
|
// change adds to the wire. `null` is a real and common value — a restored
|
|
11719
|
-
// tab is nobody's (`registry.ts:231`), a user tab
|
|
11720
|
-
//
|
|
11721
|
-
//
|
|
11722
|
-
// scope
|
|
11799
|
+
// tab is nobody's (`registry.ts:231`), a user tab never handed over and
|
|
11800
|
+
// opened from the route or a draft is nobody's — and it means "not any
|
|
11801
|
+
// conversation's", which is why a conversation's scope shows it under no
|
|
11802
|
+
// scope but `"all"` (spec 7.2).
|
|
11803
|
+
//
|
|
11804
|
+
// A TAB HANDED BACK KEEPS ITS CONVERSATION (review round 1, Q2):
|
|
11805
|
+
// revocation restores `homeSessionId` rather than writing `null`, so the
|
|
11806
|
+
// field is "which conversation this belongs to", not "which session is
|
|
11807
|
+
// driving it" — the second question is `handedOver` and `owner`.
|
|
11723
11808
|
sessionId: entry.sessionId,
|
|
11724
11809
|
active: entry.active,
|
|
11725
11810
|
restored: entry.restored,
|
|
@@ -11791,10 +11876,32 @@ class BrowserHost {
|
|
|
11791
11876
|
}))
|
|
11792
11877
|
};
|
|
11793
11878
|
}
|
|
11794
|
-
/**
|
|
11795
|
-
*
|
|
11796
|
-
|
|
11797
|
-
|
|
11879
|
+
/**
|
|
11880
|
+
* A user tab: created active, at about:blank, owned by the user and with no
|
|
11881
|
+
* capability until the user hands it over (design 6.1, 6.3).
|
|
11882
|
+
*
|
|
11883
|
+
* ATTRIBUTED TO THE CONVERSATION IT WAS OPENED FROM when it has one (design R1).
|
|
11884
|
+
* That is the whole of the main-process half and it takes no new mechanism:
|
|
11885
|
+
* `registry.create` already stores a non-restored tab's `sessionId`
|
|
11886
|
+
* (`registry.ts:231`) and already leaves every user tab's `nonce` null (`:232`).
|
|
11887
|
+
*
|
|
11888
|
+
* WHY THIS CHANGES NO CAPABILITY, and the test that pins it
|
|
11889
|
+
* (`scripts/browser-host.test.mjs`): `mayDrive` requires the session id AND a
|
|
11890
|
+
* nonce (`registry.ts:395-397`), and this tab has the first and not the second, so
|
|
11891
|
+
* an agent in that conversation still cannot drive a tab the user opened. The
|
|
11892
|
+
* `tabs` listing redacts its handle, `agentTabCount()` filters on `owner` and is
|
|
11893
|
+
* unmoved, the agent cap is unmoved, and hand-over stays the only authority
|
|
11894
|
+
* transfer.
|
|
11895
|
+
*
|
|
11896
|
+
* Before this, all three ways a user could open a tab produced one the pane's
|
|
11897
|
+
* "This conversation" scope would never show, because the scope filter reads this
|
|
11898
|
+
* one field.
|
|
11899
|
+
*/
|
|
11900
|
+
async newTab(sessionId2) {
|
|
11901
|
+
const record = this.registry.create({
|
|
11902
|
+
owner: "user",
|
|
11903
|
+
sessionId: sessionId2 ?? null
|
|
11904
|
+
});
|
|
11798
11905
|
await record.view.webContents.loadURL("about:blank");
|
|
11799
11906
|
await this.cdp.attach(record.view.webContents);
|
|
11800
11907
|
this.onChanged();
|
|
@@ -11807,6 +11914,32 @@ class BrowserHost {
|
|
|
11807
11914
|
this.onChanged();
|
|
11808
11915
|
return this.chromeState();
|
|
11809
11916
|
}
|
|
11917
|
+
/**
|
|
11918
|
+
* Close several tabs as ONE intent (design R5): resolve it, close them, notify once.
|
|
11919
|
+
*
|
|
11920
|
+
* TWO MODES RATHER THAN ONE LIST, and the difference is the race the design names:
|
|
11921
|
+
*
|
|
11922
|
+
* - `ids` is positional — "these tabs, the ones I could see" — and main closes
|
|
11923
|
+
* exactly those, skipping any that are already gone. A tab an agent opened after the
|
|
11924
|
+
* press is not in the list and survives, which the strip then shows honestly.
|
|
11925
|
+
* - `conversation` is resolved HERE, at execution time, against the live registry. A
|
|
11926
|
+
* list computed from a projection the renderer read seconds ago (the band stays open
|
|
11927
|
+
* while the user reads it) would miss a tab an agent opened in that conversation in
|
|
11928
|
+
* the meantime — and the user pressed something that said ALL.
|
|
11929
|
+
*
|
|
11930
|
+
* ONE `onChanged()` EITHER WAY, which is the whole point of `registry.destroyMany`: N
|
|
11931
|
+
* closes are N full `chromeState()` builds, N IPC broadcasts, N renderer re-reads and
|
|
11932
|
+
* N `session.json` writes.
|
|
11933
|
+
*/
|
|
11934
|
+
closeTabs(intent) {
|
|
11935
|
+
const tabIds = intent.mode === "ids" ? intent.tabIds : this.registry.list().filter((record) => record.sessionId === intent.sessionId).map((record) => record.tabId);
|
|
11936
|
+
for (const tabId of tabIds) {
|
|
11937
|
+
this.dropReceipt(tabId);
|
|
11938
|
+
this.loadFailures.delete(tabId);
|
|
11939
|
+
}
|
|
11940
|
+
this.registry.destroyMany(tabIds);
|
|
11941
|
+
return this.chromeState();
|
|
11942
|
+
}
|
|
11810
11943
|
/**
|
|
11811
11944
|
* Re-create the tabs a previous run recorded (design 7).
|
|
11812
11945
|
*
|
|
@@ -12094,6 +12227,7 @@ const BROWSER_IPC_CHANNELS = [
|
|
|
12094
12227
|
"browser-state",
|
|
12095
12228
|
"browser-new-tab",
|
|
12096
12229
|
"browser-close-tab",
|
|
12230
|
+
"browser-close-tabs",
|
|
12097
12231
|
"browser-activate-tab",
|
|
12098
12232
|
"browser-navigate",
|
|
12099
12233
|
"browser-reload",
|
|
@@ -12120,11 +12254,18 @@ function registerBrowserIpc(options) {
|
|
|
12120
12254
|
return host;
|
|
12121
12255
|
}
|
|
12122
12256
|
electron.ipcMain.handle("browser-state", (event) => authorize(event).chromeState());
|
|
12123
|
-
electron.ipcMain.handle("browser-new-tab", async (event) =>
|
|
12257
|
+
electron.ipcMain.handle("browser-new-tab", async (event, sessionId2) => {
|
|
12258
|
+
const host = authorize(event);
|
|
12259
|
+
return host.newTab(optionalConversationOrThrow(sessionId2));
|
|
12260
|
+
});
|
|
12124
12261
|
electron.ipcMain.handle("browser-close-tab", (event, tabId) => {
|
|
12125
12262
|
const host = authorize(event);
|
|
12126
12263
|
return host.closeTab(numberOrThrow(tabId, "tabId"));
|
|
12127
12264
|
});
|
|
12265
|
+
electron.ipcMain.handle("browser-close-tabs", (event, intent) => {
|
|
12266
|
+
const host = authorize(event);
|
|
12267
|
+
return host.closeTabs(closeTabsIntentOrThrow(intent));
|
|
12268
|
+
});
|
|
12128
12269
|
electron.ipcMain.handle("browser-activate-tab", (event, tabId) => {
|
|
12129
12270
|
const host = authorize(event);
|
|
12130
12271
|
return host.activateTab(numberOrThrow(tabId, "tabId"));
|
|
@@ -12220,6 +12361,38 @@ function numberOrThrow(value, name) {
|
|
|
12220
12361
|
}
|
|
12221
12362
|
return value;
|
|
12222
12363
|
}
|
|
12364
|
+
function closeTabsIntentOrThrow(value) {
|
|
12365
|
+
if (!value || typeof value !== "object") {
|
|
12366
|
+
throw new Error("A close request is required.");
|
|
12367
|
+
}
|
|
12368
|
+
const intent = value;
|
|
12369
|
+
if (intent.mode === "ids") {
|
|
12370
|
+
if (!Array.isArray(intent.tabIds) || intent.tabIds.length === 0) {
|
|
12371
|
+
throw new Error("Closing tabs by id needs at least one tab id.");
|
|
12372
|
+
}
|
|
12373
|
+
const tabIds = intent.tabIds.map(
|
|
12374
|
+
(tabId) => numberOrThrow(tabId, "each tab id")
|
|
12375
|
+
);
|
|
12376
|
+
return { mode: "ids", tabIds };
|
|
12377
|
+
}
|
|
12378
|
+
if (intent.mode === "conversation") {
|
|
12379
|
+
const sessionId2 = optionalConversationOrThrow(intent.sessionId);
|
|
12380
|
+
if (sessionId2 === null) {
|
|
12381
|
+
throw new Error("Closing a conversation's tabs needs a conversation id.");
|
|
12382
|
+
}
|
|
12383
|
+
return { mode: "conversation", sessionId: sessionId2 };
|
|
12384
|
+
}
|
|
12385
|
+
throw new Error("Unsupported close request.");
|
|
12386
|
+
}
|
|
12387
|
+
function optionalConversationOrThrow(value) {
|
|
12388
|
+
if (value === null || value === void 0) return null;
|
|
12389
|
+
if (typeof value !== "string") {
|
|
12390
|
+
throw new Error("A conversation id must be a string.");
|
|
12391
|
+
}
|
|
12392
|
+
const sessionId2 = value.trim();
|
|
12393
|
+
if (!sessionId2) throw new Error("A conversation id cannot be empty.");
|
|
12394
|
+
return sessionId2;
|
|
12395
|
+
}
|
|
12223
12396
|
function contentRectOrNull(value) {
|
|
12224
12397
|
if (value === null || value === void 0) return null;
|
|
12225
12398
|
if (typeof value !== "object")
|
|
@@ -15604,6 +15777,122 @@ function registerDevDriverIPC(options) {
|
|
|
15604
15777
|
};
|
|
15605
15778
|
});
|
|
15606
15779
|
}
|
|
15780
|
+
const PRUNE_NAMES = [
|
|
15781
|
+
"__pycache__",
|
|
15782
|
+
"node_modules",
|
|
15783
|
+
"dist",
|
|
15784
|
+
"build",
|
|
15785
|
+
".git",
|
|
15786
|
+
".venv"
|
|
15787
|
+
];
|
|
15788
|
+
function excluded(name) {
|
|
15789
|
+
if (name.startsWith(".")) return true;
|
|
15790
|
+
return PRUNE_NAMES.includes(name);
|
|
15791
|
+
}
|
|
15792
|
+
function resolveUserPath$1(filePath, cwd, home) {
|
|
15793
|
+
if (filePath === "~") return home;
|
|
15794
|
+
if (filePath.startsWith("~/")) return path$1.join(home, filePath.slice(2));
|
|
15795
|
+
if (cwd && !filePath.startsWith("/")) {
|
|
15796
|
+
const base = cwd === "~" || cwd.startsWith("~/") ? resolveUserPath$1(cwd, void 0, home) : cwd;
|
|
15797
|
+
return path$1.join(base, filePath);
|
|
15798
|
+
}
|
|
15799
|
+
return filePath;
|
|
15800
|
+
}
|
|
15801
|
+
const DIRECTORY_SCAN_LIMIT = 2e3;
|
|
15802
|
+
const collator = new Intl.Collator();
|
|
15803
|
+
function compareCandidates(a, b) {
|
|
15804
|
+
const display = collator.compare(a.name, b.name);
|
|
15805
|
+
if (display !== 0) return display;
|
|
15806
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
15807
|
+
}
|
|
15808
|
+
function offerCandidate(heap, candidate) {
|
|
15809
|
+
if (heap.length < DIRECTORY_SCAN_LIMIT) {
|
|
15810
|
+
heap.push(candidate);
|
|
15811
|
+
siftUp(heap, heap.length - 1);
|
|
15812
|
+
return true;
|
|
15813
|
+
}
|
|
15814
|
+
if (compareCandidates(candidate, heap[0]) >= 0) return false;
|
|
15815
|
+
heap[0] = candidate;
|
|
15816
|
+
siftDown(heap, 0);
|
|
15817
|
+
return true;
|
|
15818
|
+
}
|
|
15819
|
+
function siftUp(heap, from) {
|
|
15820
|
+
let index = from;
|
|
15821
|
+
while (index > 0) {
|
|
15822
|
+
const parent = index - 1 >> 1;
|
|
15823
|
+
if (compareCandidates(heap[index], heap[parent]) <= 0) return;
|
|
15824
|
+
[heap[parent], heap[index]] = [heap[index], heap[parent]];
|
|
15825
|
+
index = parent;
|
|
15826
|
+
}
|
|
15827
|
+
}
|
|
15828
|
+
function siftDown(heap, from) {
|
|
15829
|
+
let index = from;
|
|
15830
|
+
for (; ; ) {
|
|
15831
|
+
const left = index * 2 + 1;
|
|
15832
|
+
const right = left + 1;
|
|
15833
|
+
let largest = index;
|
|
15834
|
+
if (left < heap.length && compareCandidates(heap[left], heap[largest]) > 0)
|
|
15835
|
+
largest = left;
|
|
15836
|
+
if (right < heap.length && compareCandidates(heap[right], heap[largest]) > 0)
|
|
15837
|
+
largest = right;
|
|
15838
|
+
if (largest === index) return;
|
|
15839
|
+
[heap[largest], heap[index]] = [heap[index], heap[largest]];
|
|
15840
|
+
index = largest;
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15843
|
+
async function listDirectory(dir) {
|
|
15844
|
+
try {
|
|
15845
|
+
const candidates = [];
|
|
15846
|
+
let overflow = false;
|
|
15847
|
+
const stream = await promises.opendir(dir);
|
|
15848
|
+
for await (const entry of stream) {
|
|
15849
|
+
if (excluded(entry.name)) continue;
|
|
15850
|
+
if (!offerCandidate(candidates, {
|
|
15851
|
+
name: entry.name,
|
|
15852
|
+
directory: entry.isDirectory(),
|
|
15853
|
+
link: entry.isSymbolicLink()
|
|
15854
|
+
}))
|
|
15855
|
+
overflow = true;
|
|
15856
|
+
}
|
|
15857
|
+
candidates.sort(compareCandidates);
|
|
15858
|
+
const kept = candidates.slice(0, DIRECTORY_ENTRY_LIMIT);
|
|
15859
|
+
const entries = kept.map((candidate) => ({
|
|
15860
|
+
name: candidate.name,
|
|
15861
|
+
directory: candidate.link ? fs.statSync(path$1.join(dir, candidate.name), {
|
|
15862
|
+
throwIfNoEntry: false
|
|
15863
|
+
})?.isDirectory() ?? false : candidate.directory
|
|
15864
|
+
}));
|
|
15865
|
+
return {
|
|
15866
|
+
dir,
|
|
15867
|
+
entries,
|
|
15868
|
+
// Either bound withheld something. `overflow` is the scan cap, which
|
|
15869
|
+
// means the kept set is the smallest ALPHABETICAL
|
|
15870
|
+
// `DIRECTORY_SCAN_LIMIT` names of a directory nobody finished reading —
|
|
15871
|
+
// the same rows a full sort-then-truncate would have answered with — and
|
|
15872
|
+
// the second term is the answer's own `DIRECTORY_ENTRY_LIMIT`-row limit.
|
|
15873
|
+
truncated: overflow || candidates.length > DIRECTORY_ENTRY_LIMIT
|
|
15874
|
+
};
|
|
15875
|
+
} catch (error) {
|
|
15876
|
+
return {
|
|
15877
|
+
dir,
|
|
15878
|
+
entries: [],
|
|
15879
|
+
truncated: false,
|
|
15880
|
+
error: error instanceof Error ? error.message : String(error)
|
|
15881
|
+
};
|
|
15882
|
+
}
|
|
15883
|
+
}
|
|
15884
|
+
function realPathOrNull(path2) {
|
|
15885
|
+
try {
|
|
15886
|
+
return fs.realpathSync(path2);
|
|
15887
|
+
} catch {
|
|
15888
|
+
return null;
|
|
15889
|
+
}
|
|
15890
|
+
}
|
|
15891
|
+
function outsideWorkspace(realTarget, realRoot) {
|
|
15892
|
+
if (realRoot === null) return void 0;
|
|
15893
|
+
if (realTarget === null) return true;
|
|
15894
|
+
return realTarget !== realRoot && !realTarget.startsWith(`${realRoot}${path$1.sep}`);
|
|
15895
|
+
}
|
|
15607
15896
|
const LAUNCHER_MISSES_BEFORE_GONE = 2;
|
|
15608
15897
|
function createLauncherWatch(input) {
|
|
15609
15898
|
const threshold = input.missesBeforeGone ?? LAUNCHER_MISSES_BEFORE_GONE;
|
|
@@ -15790,6 +16079,25 @@ const READABLE_VERSION_PATTERN = /^v?\d+(\.\d+){0,3}([.-]?(?:a|b|c|rc|alpha|beta
|
|
|
15790
16079
|
const isReadableVersion = (value) => typeof value === "string" && READABLE_VERSION_PATTERN.test(value.trim());
|
|
15791
16080
|
const UP_TO_DATE_AFFIRMATION = "The application and server are up to date";
|
|
15792
16081
|
const updateCheckAffirmation = (channels) => channels.app === "current" && channels.server === "current" ? UP_TO_DATE_AFFIRMATION : null;
|
|
16082
|
+
const readable = (value) => isReadableVersion(value) ? value.trim() : null;
|
|
16083
|
+
const serverChannelVerdict = ({
|
|
16084
|
+
installVersion,
|
|
16085
|
+
runningVersion,
|
|
16086
|
+
publishedVersion,
|
|
16087
|
+
isNewer
|
|
16088
|
+
}) => {
|
|
16089
|
+
const published = readable(publishedVersion);
|
|
16090
|
+
const running = readable(runningVersion);
|
|
16091
|
+
if (!published || !running)
|
|
16092
|
+
return { status: "unavailable", state: "unreadable" };
|
|
16093
|
+
const install = readable(installVersion);
|
|
16094
|
+
const subject = install ?? running;
|
|
16095
|
+
if (isNewer(published, subject))
|
|
16096
|
+
return { status: "available", state: "install-behind" };
|
|
16097
|
+
if (install && install !== running && isNewer(install, running))
|
|
16098
|
+
return { status: "restart-required", state: "restart-required" };
|
|
16099
|
+
return { status: "current", state: "current" };
|
|
16100
|
+
};
|
|
15793
16101
|
const updateCheckVerdict = (channels) => ({
|
|
15794
16102
|
app: channels.app,
|
|
15795
16103
|
server: channels.server,
|
|
@@ -15859,6 +16167,37 @@ function stderrTail(stderr, maxLines = 4) {
|
|
|
15859
16167
|
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
15860
16168
|
return lines.slice(-maxLines).join("\n").slice(-600);
|
|
15861
16169
|
}
|
|
16170
|
+
function readingOfHealth(health) {
|
|
16171
|
+
const result = health.result;
|
|
16172
|
+
return {
|
|
16173
|
+
version: result?.version ?? null,
|
|
16174
|
+
prefix: result?.prefix ? result.prefix : null,
|
|
16175
|
+
installKind: result?.install_kind ? result.install_kind : null
|
|
16176
|
+
};
|
|
16177
|
+
}
|
|
16178
|
+
function appOwnedInstallRoots(input) {
|
|
16179
|
+
const support = managedSupportRoot(input.home);
|
|
16180
|
+
return [
|
|
16181
|
+
path$1.join(support, "managed-python"),
|
|
16182
|
+
...legacyVenvPaths(support),
|
|
16183
|
+
managedVenvPath(input),
|
|
16184
|
+
managedVenvPath({ ...input, packaged: !input.packaged })
|
|
16185
|
+
];
|
|
16186
|
+
}
|
|
16187
|
+
function appOwnsInstallRoot(input, prefix) {
|
|
16188
|
+
return isWithinAnyRoot(appOwnedInstallRoots(input), prefix);
|
|
16189
|
+
}
|
|
16190
|
+
function newerReading(left, right, isNewer) {
|
|
16191
|
+
if (typeof left !== "string" || !isReadableVersion(left)) return right;
|
|
16192
|
+
if (typeof right !== "string" || !isReadableVersion(right)) return left;
|
|
16193
|
+
return isNewer(left, right) ? left : right;
|
|
16194
|
+
}
|
|
16195
|
+
function isWithinAnyRoot(roots, candidate) {
|
|
16196
|
+
return roots.some((root) => {
|
|
16197
|
+
const relative = path__namespace.relative(root, candidate);
|
|
16198
|
+
return !relative.startsWith("..") && !path__namespace.isAbsolute(relative);
|
|
16199
|
+
});
|
|
16200
|
+
}
|
|
15862
16201
|
class UpdateService {
|
|
15863
16202
|
mainWindow = null;
|
|
15864
16203
|
isDevMode;
|
|
@@ -17724,10 +18063,24 @@ ${heal.removed.join("\n")}`,
|
|
|
17724
18063
|
return this.backendService?.getBackendUrl() ?? this.backendUrl;
|
|
17725
18064
|
}
|
|
17726
18065
|
/**
|
|
17727
|
-
*
|
|
17728
|
-
*
|
|
18066
|
+
* Read the server serving this app: its version, and the install it runs from.
|
|
18067
|
+
*
|
|
18068
|
+
* The install root travels with the version because the update check is a
|
|
18069
|
+
* statement about an INSTALL, and `/health` is the only thing that knows which
|
|
18070
|
+
* one is answering. The app used to classify the install it could name from the
|
|
18071
|
+
* `local-operator` shim on `PATH` and compare THAT against PyPI while reading
|
|
18072
|
+
* this version separately - so a machine serving the app from a three-release-old
|
|
18073
|
+
* environment was told "The application and server are up to date" beside a
|
|
18074
|
+
* Settings row naming the older build.
|
|
18075
|
+
*
|
|
18076
|
+
* @returns the reading, with a null version when nothing answered
|
|
17729
18077
|
*/
|
|
17730
|
-
async
|
|
18078
|
+
async readRunningBackend() {
|
|
18079
|
+
const notAnswered = {
|
|
18080
|
+
version: null,
|
|
18081
|
+
prefix: null,
|
|
18082
|
+
installKind: null
|
|
18083
|
+
};
|
|
17731
18084
|
try {
|
|
17732
18085
|
const backendUrl = this.liveBackendUrl();
|
|
17733
18086
|
logger.info(
|
|
@@ -17747,21 +18100,22 @@ ${heal.removed.join("\n")}`,
|
|
|
17747
18100
|
`Health API returned status ${response.status}`,
|
|
17748
18101
|
LogFileType.UPDATE_SERVICE
|
|
17749
18102
|
);
|
|
17750
|
-
return
|
|
18103
|
+
return notAnswered;
|
|
17751
18104
|
}
|
|
17752
18105
|
const healthData = await response.json();
|
|
17753
|
-
|
|
18106
|
+
const reading = readingOfHealth(healthData);
|
|
18107
|
+
if (reading.version) {
|
|
17754
18108
|
logger.info(
|
|
17755
|
-
`Backend version from health API: ${
|
|
18109
|
+
`Backend version from health API: ${reading.version} (install root ${reading.prefix ?? "not reported"}, install kind ${reading.installKind ?? "not reported"})`,
|
|
17756
18110
|
LogFileType.UPDATE_SERVICE
|
|
17757
18111
|
);
|
|
17758
|
-
return
|
|
18112
|
+
return reading;
|
|
17759
18113
|
}
|
|
17760
18114
|
logger.info(
|
|
17761
18115
|
"Backend version not available in health response",
|
|
17762
18116
|
LogFileType.UPDATE_SERVICE
|
|
17763
18117
|
);
|
|
17764
|
-
return "Unknown";
|
|
18118
|
+
return { ...reading, version: "Unknown" };
|
|
17765
18119
|
} catch (error) {
|
|
17766
18120
|
logger.error(
|
|
17767
18121
|
"Error getting backend version from health API:",
|
|
@@ -17789,15 +18143,16 @@ ${heal.removed.join("\n")}`,
|
|
|
17789
18143
|
`Alternative health API returned status ${response.status}`,
|
|
17790
18144
|
LogFileType.UPDATE_SERVICE
|
|
17791
18145
|
);
|
|
17792
|
-
return
|
|
18146
|
+
return notAnswered;
|
|
17793
18147
|
}
|
|
17794
18148
|
const healthData = await response.json();
|
|
17795
|
-
|
|
18149
|
+
const reading = readingOfHealth(healthData);
|
|
18150
|
+
if (reading.version) {
|
|
17796
18151
|
logger.info(
|
|
17797
|
-
`Backend version from alternative health API: ${
|
|
18152
|
+
`Backend version from alternative health API: ${reading.version} (install root ${reading.prefix ?? "not reported"}, install kind ${reading.installKind ?? "not reported"})`,
|
|
17798
18153
|
LogFileType.UPDATE_SERVICE
|
|
17799
18154
|
);
|
|
17800
|
-
return
|
|
18155
|
+
return reading;
|
|
17801
18156
|
}
|
|
17802
18157
|
} catch (altError) {
|
|
17803
18158
|
logger.error(
|
|
@@ -17822,7 +18177,7 @@ ${heal.removed.join("\n")}`,
|
|
|
17822
18177
|
`Backend version from pip: ${version}`,
|
|
17823
18178
|
LogFileType.UPDATE_SERVICE
|
|
17824
18179
|
);
|
|
17825
|
-
return version;
|
|
18180
|
+
return { version, prefix: null, installKind: null };
|
|
17826
18181
|
}
|
|
17827
18182
|
} catch (pipError) {
|
|
17828
18183
|
logger.error(
|
|
@@ -17832,9 +18187,19 @@ ${heal.removed.join("\n")}`,
|
|
|
17832
18187
|
);
|
|
17833
18188
|
}
|
|
17834
18189
|
}
|
|
17835
|
-
return
|
|
18190
|
+
return notAnswered;
|
|
17836
18191
|
}
|
|
17837
18192
|
}
|
|
18193
|
+
/**
|
|
18194
|
+
* The version the server serving this app reports, or null when it did not.
|
|
18195
|
+
*
|
|
18196
|
+
* Kept beside `readRunningBackend` rather than replaced by it: four callers ask
|
|
18197
|
+
* only this question (the skew notices, the by-hand panel), and none of them may
|
|
18198
|
+
* start judging an install because a second reading became available.
|
|
18199
|
+
*/
|
|
18200
|
+
async getInstalledBackendVersion() {
|
|
18201
|
+
return (await this.readRunningBackend()).version;
|
|
18202
|
+
}
|
|
17838
18203
|
/**
|
|
17839
18204
|
* Get the latest version from PyPI
|
|
17840
18205
|
* @returns Promise resolving to the latest version string or null if unable to fetch
|
|
@@ -17876,6 +18241,76 @@ ${heal.removed.join("\n")}`,
|
|
|
17876
18241
|
* @param silent - Whether to suppress notifications on no update
|
|
17877
18242
|
* @returns Promise resolving to update info or null if no update is available
|
|
17878
18243
|
*/
|
|
18244
|
+
/**
|
|
18245
|
+
* Read the install that is serving this app, from the root `/health` named.
|
|
18246
|
+
*
|
|
18247
|
+
* The version comes from the dist-info at that root
|
|
18248
|
+
* (`resolveDistributionMarkers`, the same read the global classification uses),
|
|
18249
|
+
* which is shell-free and needs no interpreter: `pip show` inside that
|
|
18250
|
+
* environment would spawn the very server's python, and a second process is not
|
|
18251
|
+
* worth a number that is already on disk beside the code.
|
|
18252
|
+
*
|
|
18253
|
+
* A root the server did not name answers with nulls, and the check treats that
|
|
18254
|
+
* as "the serving install could not be identified" rather than falling back to
|
|
18255
|
+
* an install the app can name - which is the reported defect.
|
|
18256
|
+
*/
|
|
18257
|
+
readServingInstall(reading) {
|
|
18258
|
+
const prefix = reading.prefix;
|
|
18259
|
+
if (!prefix) {
|
|
18260
|
+
return {
|
|
18261
|
+
prefix: null,
|
|
18262
|
+
version: null,
|
|
18263
|
+
installKind: reading.installKind,
|
|
18264
|
+
appOwned: false
|
|
18265
|
+
};
|
|
18266
|
+
}
|
|
18267
|
+
let version = null;
|
|
18268
|
+
try {
|
|
18269
|
+
const markers = resolveDistributionMarkers(prefix);
|
|
18270
|
+
version = (markers.editable || markers.sourceRef) && isReadableVersion(reading.version) ? reading.version : markers.version;
|
|
18271
|
+
} catch (error) {
|
|
18272
|
+
logger.warn(
|
|
18273
|
+
`Could not read the install at ${prefix}: ${error.message}`,
|
|
18274
|
+
LogFileType.UPDATE_SERVICE
|
|
18275
|
+
);
|
|
18276
|
+
}
|
|
18277
|
+
return {
|
|
18278
|
+
prefix,
|
|
18279
|
+
version,
|
|
18280
|
+
installKind: reading.installKind,
|
|
18281
|
+
appOwned: appOwnsInstallRoot(this.ownedInstallInput(), prefix)
|
|
18282
|
+
};
|
|
18283
|
+
}
|
|
18284
|
+
/**
|
|
18285
|
+
* This instance's own ownership triple.
|
|
18286
|
+
*
|
|
18287
|
+
* `packaged` is the build this process IS, not the environment it is asking
|
|
18288
|
+
* about: a packaged build pointed at a dev server still owns the environment
|
|
18289
|
+
* its bundle carries, and the sibling name is in the root list for the
|
|
18290
|
+
* question the other way round.
|
|
18291
|
+
*/
|
|
18292
|
+
ownedInstallInput() {
|
|
18293
|
+
return {
|
|
18294
|
+
platform: process.platform,
|
|
18295
|
+
home: electron.app.getPath("home"),
|
|
18296
|
+
appDataPath: electron.app.getPath("appData"),
|
|
18297
|
+
packaged: electron.app.isPackaged
|
|
18298
|
+
};
|
|
18299
|
+
}
|
|
18300
|
+
/**
|
|
18301
|
+
* The identity of the install serving this app, for the classification the plan
|
|
18302
|
+
* needs when that install is not one the app owns.
|
|
18303
|
+
*
|
|
18304
|
+
* Read from the console script INSIDE the reported prefix, so
|
|
18305
|
+
* `classifyGlobalInstall` sees the install that answered rather than whichever
|
|
18306
|
+
* one the shim on `PATH` happens to point at - the two differ exactly on the
|
|
18307
|
+
* machines this check exists for. Null when the prefix carries no console
|
|
18308
|
+
* script, which is "we could not classify it" rather than a guess.
|
|
18309
|
+
*/
|
|
18310
|
+
servingInstallIdentity(prefix) {
|
|
18311
|
+
const script = process.platform === "win32" ? path$1.join(prefix, "Scripts", "local-operator.exe") : path$1.join(prefix, "bin", "local-operator");
|
|
18312
|
+
return fs.existsSync(script) ? readInstallIdentity(script) : null;
|
|
18313
|
+
}
|
|
17879
18314
|
/**
|
|
17880
18315
|
* Decide what the backend update prompt may offer for a startup mode.
|
|
17881
18316
|
*
|
|
@@ -17889,13 +18324,34 @@ ${heal.removed.join("\n")}`,
|
|
|
17889
18324
|
* is the same question: a server the app attaches to but does not own was
|
|
17890
18325
|
* still being told to run pip, three lines below the code that had already
|
|
17891
18326
|
* learned better (reviews U4, D4).
|
|
18327
|
+
*
|
|
18328
|
+
* Both of those modes resolve the plan for ONE install: the install that is
|
|
18329
|
+
* actually serving this app, as `/health` reports it. Classifying a global
|
|
18330
|
+
* install by name while a different one serves the user is how the check came to
|
|
18331
|
+
* affirm a version the panel's own row contradicted.
|
|
17892
18332
|
*/
|
|
17893
|
-
async resolveBackendUpdatePlan(startupMode) {
|
|
18333
|
+
async resolveBackendUpdatePlan(startupMode, serving) {
|
|
17894
18334
|
if (startupMode === LocalOperatorStartupMode.EXISTING_SERVER || startupMode === LocalOperatorStartupMode.GLOBAL_INSTALL) {
|
|
17895
|
-
|
|
18335
|
+
if (serving.appOwned) {
|
|
18336
|
+
const detail = `The server serving this app runs from Local Operator's own managed environment at ${serving.prefix}, which the app owns rather than a package manager (the backend reports it as install kind "${serving.installKind || "not reported"}")${serving.version ? `, at version ${serving.version}` : " and its installed version could not be read"}.`;
|
|
18337
|
+
logger.info(
|
|
18338
|
+
`External backend install (${startupMode}) runs from the app's own managed environment at ${serving.prefix}; no installer owns it, so the app names no command`,
|
|
18339
|
+
LogFileType.UPDATE_SERVICE
|
|
18340
|
+
);
|
|
18341
|
+
return {
|
|
18342
|
+
canManageUpdate: false,
|
|
18343
|
+
updateCommand: "",
|
|
18344
|
+
remedy: "This server is running from Local Operator's own managed environment, which no package manager owns - so there is no terminal command that can update it correctly. The app can only update a server it started itself: stop this one, then start Local Operator again and let it start its own.",
|
|
18345
|
+
detail,
|
|
18346
|
+
sourceBuild: false,
|
|
18347
|
+
appOwned: true,
|
|
18348
|
+
installedInstallVersion: serving.version
|
|
18349
|
+
};
|
|
18350
|
+
}
|
|
18351
|
+
const identity = (serving.prefix ? this.servingInstallIdentity(serving.prefix) : null) ?? await this.resolveInstallIdentity();
|
|
17896
18352
|
const plan = resolveGlobalInstallPlan({ identity });
|
|
17897
18353
|
logger.info(
|
|
17898
|
-
`External backend install (${startupMode}): ${plan.detail}; remedy is \`${plan.updateCommand || "nothing the app can name"}\`; install version ${identity.version ?? "unreadable"}`,
|
|
18354
|
+
`External backend install (${startupMode}): ${plan.detail}; remedy is \`${plan.updateCommand || "nothing the app can name"}\`; install version ${serving.version ?? identity.version ?? "unreadable"}`,
|
|
17899
18355
|
LogFileType.UPDATE_SERVICE
|
|
17900
18356
|
);
|
|
17901
18357
|
return {
|
|
@@ -17904,7 +18360,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17904
18360
|
remedy: plan.remedy,
|
|
17905
18361
|
detail: plan.detail,
|
|
17906
18362
|
sourceBuild: plan.sourceBuild,
|
|
17907
|
-
|
|
18363
|
+
appOwned: false,
|
|
18364
|
+
installedInstallVersion: serving.version ?? identity.version ?? null
|
|
17908
18365
|
};
|
|
17909
18366
|
}
|
|
17910
18367
|
return {
|
|
@@ -17913,7 +18370,20 @@ ${heal.removed.join("\n")}`,
|
|
|
17913
18370
|
remedy: "Updating the server will improve AI functionality.",
|
|
17914
18371
|
detail: `The app started this server itself (${startupMode}).`,
|
|
17915
18372
|
sourceBuild: false,
|
|
17916
|
-
|
|
18373
|
+
/*
|
|
18374
|
+
* `false` although this IS the app's own environment: the flag answers
|
|
18375
|
+
* "may the manual panel tell the reader nothing here can move it", and on
|
|
18376
|
+
* this arm the app moves it itself with the button above. It is the
|
|
18377
|
+
* app-owned MANUAL arm that owns the sentence (review round 1, UX U1).
|
|
18378
|
+
*/
|
|
18379
|
+
appOwned: false,
|
|
18380
|
+
/*
|
|
18381
|
+
* The app-managed environment's own on-disk version when the server named
|
|
18382
|
+
* it, which is what an update moves: the check compares against the same
|
|
18383
|
+
* install it would rewrite, so a landed update with a daemon still on the
|
|
18384
|
+
* old build is visible as exactly that rather than as "nothing newer".
|
|
18385
|
+
*/
|
|
18386
|
+
installedInstallVersion: serving.version
|
|
17917
18387
|
};
|
|
17918
18388
|
}
|
|
17919
18389
|
/**
|
|
@@ -18026,11 +18496,17 @@ ${heal.removed.join("\n")}`,
|
|
|
18026
18496
|
);
|
|
18027
18497
|
return { status: "unavailable", info: null };
|
|
18028
18498
|
}
|
|
18029
|
-
const
|
|
18030
|
-
const runningVersion =
|
|
18499
|
+
const running = await this.readRunningBackend();
|
|
18500
|
+
const runningVersion = running.version;
|
|
18501
|
+
const serving = this.readServingInstall(running);
|
|
18502
|
+
const plan = await this.resolveBackendUpdatePlan(startupMode, serving);
|
|
18031
18503
|
const latestVersion = await this.getLatestPypiVersion();
|
|
18032
18504
|
if (latestVersion) this.lastPublishedBackendVersion = latestVersion;
|
|
18033
|
-
const installVersion = plan.installedInstallVersion
|
|
18505
|
+
const installVersion = serving.prefix ? plan.installedInstallVersion : newerReading(
|
|
18506
|
+
plan.installedInstallVersion,
|
|
18507
|
+
runningVersion,
|
|
18508
|
+
(candidate, subject) => this.isNewerVersion(candidate, subject)
|
|
18509
|
+
);
|
|
18034
18510
|
const installedVersion = installVersion && isReadableVersion(installVersion) ? installVersion : runningVersion;
|
|
18035
18511
|
if (!installedVersion || !latestVersion) {
|
|
18036
18512
|
logger.error(
|
|
@@ -18080,15 +18556,27 @@ ${heal.removed.join("\n")}`,
|
|
|
18080
18556
|
}
|
|
18081
18557
|
return { status: "unavailable", info: null };
|
|
18082
18558
|
}
|
|
18083
|
-
const
|
|
18559
|
+
const channel = serverChannelVerdict({
|
|
18560
|
+
installVersion,
|
|
18561
|
+
runningVersion,
|
|
18562
|
+
publishedVersion: latestVersion,
|
|
18563
|
+
isNewer: (candidate, subject) => this.isNewerVersion(candidate, subject)
|
|
18564
|
+
});
|
|
18565
|
+
const shouldUpdate = channel.status === "available";
|
|
18084
18566
|
logger.info(
|
|
18085
|
-
`Install on disk reports: ${installVersion ?? "no reading"}, running backend reports: ${runningVersion ?? "no reading"}, Latest: ${latestVersion}, Update needed: ${shouldUpdate}, Startup mode: ${startupMode}`,
|
|
18567
|
+
`Install on disk reports: ${installVersion ?? "no reading"} (at ${serving.prefix ?? "an install the server did not name"}), running backend reports: ${runningVersion ?? "no reading"}, Latest: ${latestVersion}, Update needed: ${shouldUpdate}, Server channel: ${channel.status} (${channel.state}), Startup mode: ${startupMode}`,
|
|
18086
18568
|
LogFileType.UPDATE_SERVICE
|
|
18087
18569
|
);
|
|
18088
18570
|
logger.info(
|
|
18089
18571
|
`Installed backend version: ${installedVersion}, Latest: ${latestVersion}, Update needed: ${shouldUpdate}, Startup mode: ${startupMode}`,
|
|
18090
18572
|
LogFileType.UPDATE_SERVICE
|
|
18091
18573
|
);
|
|
18574
|
+
if (channel.state === "restart-required") {
|
|
18575
|
+
logger.info(
|
|
18576
|
+
`The install is current (${installedVersion}) and the server serving this app runs ${runningVersion}; a restart of that server picks the installed build up.`,
|
|
18577
|
+
LogFileType.UPDATE_SERVICE
|
|
18578
|
+
);
|
|
18579
|
+
}
|
|
18092
18580
|
if (shouldUpdate) {
|
|
18093
18581
|
logger.info(
|
|
18094
18582
|
`New backend version available: ${latestVersion} (installed: ${installedVersion})`,
|
|
@@ -18100,6 +18588,7 @@ ${heal.removed.join("\n")}`,
|
|
|
18100
18588
|
runningVersion,
|
|
18101
18589
|
updateCommand: plan.updateCommand,
|
|
18102
18590
|
canManageUpdate: plan.canManageUpdate,
|
|
18591
|
+
appOwned: plan.appOwned,
|
|
18103
18592
|
startupMode,
|
|
18104
18593
|
remedy: plan.remedy,
|
|
18105
18594
|
detail: plan.detail,
|
|
@@ -18155,7 +18644,7 @@ ${heal.removed.join("\n")}`,
|
|
|
18155
18644
|
restartable: this.backendIsAppOwned()
|
|
18156
18645
|
});
|
|
18157
18646
|
}
|
|
18158
|
-
return { status:
|
|
18647
|
+
return { status: channel.status, info: null };
|
|
18159
18648
|
} catch (error) {
|
|
18160
18649
|
logger.error(
|
|
18161
18650
|
"Error checking for backend updates:",
|
|
@@ -18549,6 +19038,8 @@ ${tail}` : ""}`,
|
|
|
18549
19038
|
return false;
|
|
18550
19039
|
}
|
|
18551
19040
|
const startupMode = this.backendService.getStartupMode();
|
|
19041
|
+
const running = await this.readRunningBackend();
|
|
19042
|
+
const serving = this.readServingInstall(running);
|
|
18552
19043
|
switch (startupMode) {
|
|
18553
19044
|
case LocalOperatorStartupMode.NOT_STARTED:
|
|
18554
19045
|
logger.info(
|
|
@@ -18565,7 +19056,10 @@ ${tail}` : ""}`,
|
|
|
18565
19056
|
return false;
|
|
18566
19057
|
case LocalOperatorStartupMode.EXISTING_SERVER:
|
|
18567
19058
|
case LocalOperatorStartupMode.GLOBAL_INSTALL: {
|
|
18568
|
-
const plan = await this.resolveBackendUpdatePlan(
|
|
19059
|
+
const plan = await this.resolveBackendUpdatePlan(
|
|
19060
|
+
startupMode,
|
|
19061
|
+
serving
|
|
19062
|
+
);
|
|
18569
19063
|
const target2 = targetVersion ?? this.lastPublishedBackendVersion ?? null;
|
|
18570
19064
|
if (plan.canManageUpdate) {
|
|
18571
19065
|
return await this.updateGlobalInstall(
|
|
@@ -18578,7 +19072,7 @@ ${tail}` : ""}`,
|
|
|
18578
19072
|
`Cannot manage the update for ${startupMode}: ${plan.detail}`,
|
|
18579
19073
|
LogFileType.UPDATE_SERVICE
|
|
18580
19074
|
);
|
|
18581
|
-
const installed =
|
|
19075
|
+
const installed = running.version;
|
|
18582
19076
|
this.sendToRenderer("backend-update-manual-required", {
|
|
18583
19077
|
message: plan.remedy,
|
|
18584
19078
|
command: plan.updateCommand,
|
|
@@ -18587,7 +19081,14 @@ ${tail}` : ""}`,
|
|
|
18587
19081
|
currentVersion: installed && installed !== "Unknown" ? installed : null,
|
|
18588
19082
|
installVersion: plan.installedInstallVersion,
|
|
18589
19083
|
runningVersion: installed,
|
|
18590
|
-
sourceBuild: plan.sourceBuild
|
|
19084
|
+
sourceBuild: plan.sourceBuild,
|
|
19085
|
+
/*
|
|
19086
|
+
* The second door into the same state, and it needs the same reading: this
|
|
19087
|
+
* panel renders `ManualRemedyNote` too, and on an app-owned install the
|
|
19088
|
+
* closing line it would otherwise append asks for a press that cannot move
|
|
19089
|
+
* the environment (review round 1, UX U1).
|
|
19090
|
+
*/
|
|
19091
|
+
appOwned: plan.appOwned
|
|
18591
19092
|
});
|
|
18592
19093
|
return false;
|
|
18593
19094
|
}
|
|
@@ -19227,14 +19728,7 @@ class ViewerRecordPublisher {
|
|
|
19227
19728
|
}
|
|
19228
19729
|
}
|
|
19229
19730
|
const BASE64_FILE_EXTENSIONS = ["csv", "tsv", "xls", "xlsx", "ods"];
|
|
19230
|
-
const resolveUserPath = (filePath, cwd) =>
|
|
19231
|
-
if (filePath === "~") return electron.app.getPath("home");
|
|
19232
|
-
if (filePath.startsWith("~/"))
|
|
19233
|
-
return path$1.join(electron.app.getPath("home"), filePath.slice(2));
|
|
19234
|
-
if (cwd && !filePath.startsWith("/"))
|
|
19235
|
-
return path$1.join(cwd.startsWith("~/") ? resolveUserPath(cwd) : cwd, filePath);
|
|
19236
|
-
return filePath;
|
|
19237
|
-
};
|
|
19731
|
+
const resolveUserPath = (filePath, cwd) => resolveUserPath$1(filePath, cwd, electron.app.getPath("home"));
|
|
19238
19732
|
const windowLaunch = resolveWindowLaunchPlan({
|
|
19239
19733
|
env: launchEnv,
|
|
19240
19734
|
argv: process.argv,
|
|
@@ -20051,18 +20545,28 @@ electron.app.whenReady().then(async () => {
|
|
|
20051
20545
|
"probe-files",
|
|
20052
20546
|
async (_, paths, cwd) => {
|
|
20053
20547
|
const asked = Array.isArray(paths) ? paths.filter((path2) => typeof path2 === "string") : [];
|
|
20548
|
+
const realRoot = cwd ? realPathOrNull(resolveUserPath(cwd)) : null;
|
|
20054
20549
|
return asked.slice(0, MAX_PROBE_PATHS).map((input) => {
|
|
20055
20550
|
let resolved = input;
|
|
20056
20551
|
try {
|
|
20057
20552
|
resolved = resolveUserPath(input, cwd);
|
|
20058
20553
|
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
20554
|
+
const exists = stat !== void 0;
|
|
20059
20555
|
return {
|
|
20060
20556
|
input,
|
|
20061
20557
|
resolved,
|
|
20062
|
-
exists
|
|
20558
|
+
exists,
|
|
20063
20559
|
isFile: stat?.isFile() ?? false,
|
|
20064
20560
|
sizeBytes: stat?.isFile() ? stat.size : null,
|
|
20065
|
-
mtimeMs: stat?.isFile() ? stat.mtimeMs : null
|
|
20561
|
+
mtimeMs: stat?.isFile() ? stat.mtimeMs : null,
|
|
20562
|
+
/*
|
|
20563
|
+
* The containment verdict, and the reason it is asked ONLY of a path
|
|
20564
|
+
* that exists: the one caller that reads it (the composer's `@` chip)
|
|
20565
|
+
* asks the question about a candidate reference, so a path that is not
|
|
20566
|
+
* there costs no second syscall — and a miss is the common case on the
|
|
20567
|
+
* keystroke path this runs on.
|
|
20568
|
+
*/
|
|
20569
|
+
outsideWorkspace: exists ? outsideWorkspace(realPathOrNull(resolved), realRoot) : void 0
|
|
20066
20570
|
};
|
|
20067
20571
|
} catch (error) {
|
|
20068
20572
|
return {
|
|
@@ -20078,6 +20582,13 @@ electron.app.whenReady().then(async () => {
|
|
|
20078
20582
|
});
|
|
20079
20583
|
}
|
|
20080
20584
|
);
|
|
20585
|
+
electron.ipcMain.handle(
|
|
20586
|
+
"list-directory",
|
|
20587
|
+
async (_, dir, cwd) => {
|
|
20588
|
+
const target2 = typeof dir === "string" && dir.length > 0 ? dir : ".";
|
|
20589
|
+
return await listDirectory(resolveUserPath(target2, cwd));
|
|
20590
|
+
}
|
|
20591
|
+
);
|
|
20081
20592
|
electron.ipcMain.handle(
|
|
20082
20593
|
"read-file-bytes",
|
|
20083
20594
|
async (_, filePath, maxBytes = MAX_FILE_READ_BYTES) => {
|