harlay 0.2.1 → 0.3.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/app/dist/main.cjs +356 -62
- package/app/dist/preload.cjs +5 -1
- package/app/renderer/overlay.css +76 -0
- package/app/renderer/overlay.html +13 -0
- package/app/renderer/overlay.js +192 -0
- package/app/renderer/popover.css +43 -0
- package/app/renderer/popover.html +3 -1
- package/app/renderer/popover.js +128 -6
- package/dist/harlay.mjs +44 -7
- package/package.json +1 -1
package/app/dist/main.cjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
var
|
|
4
|
+
var import_electron2 = require("electron");
|
|
5
5
|
var import_promises19 = require("node:fs/promises");
|
|
6
6
|
var import_node_os3 = require("node:os");
|
|
7
|
-
var
|
|
7
|
+
var import_node_path20 = require("node:path");
|
|
8
8
|
|
|
9
9
|
// ../../packages/core/src/types.ts
|
|
10
10
|
var TOOLS = ["claude-code", "codex", "cursor"];
|
|
@@ -594,14 +594,15 @@ async function write(d, o, dir) {
|
|
|
594
594
|
const r = new Redactor();
|
|
595
595
|
const prompt = launchPrompt(path);
|
|
596
596
|
const record = buildRecord(d, id, o.previousId, { target: o.target, mode: o.mode, prompt }, r);
|
|
597
|
-
|
|
597
|
+
const text = JSON.stringify(record, null, 2) + "\n";
|
|
598
|
+
await (0, import_promises3.writeFile)(tmp, text);
|
|
598
599
|
try {
|
|
599
600
|
await (0, import_promises3.link)(tmp, path);
|
|
600
601
|
} catch (e) {
|
|
601
602
|
if (e.code === "EEXIST") continue;
|
|
602
603
|
throw e;
|
|
603
604
|
}
|
|
604
|
-
return { id, path, prompt, redactions: addCounts(d.redactions ?? {}, r.counts) };
|
|
605
|
+
return { id, path, prompt, redactions: addCounts(d.redactions ?? {}, r.counts), chars: text.length };
|
|
605
606
|
}
|
|
606
607
|
} finally {
|
|
607
608
|
await (0, import_promises3.rm)(tmp, { force: true });
|
|
@@ -1455,10 +1456,12 @@ function splitShell(cmd, pipes = false) {
|
|
|
1455
1456
|
if (c === '"' || c === "'") {
|
|
1456
1457
|
quote = c;
|
|
1457
1458
|
cur += c;
|
|
1458
|
-
} else if (c === "
|
|
1459
|
+
} else if (c === "\\" && cmd[i + 1] === "\n") {
|
|
1460
|
+
i++;
|
|
1461
|
+
} else if (c === ";" || c === "\n" || c === "&" && cmd[i + 1] === "&" || c === "|" && cmd[i + 1] === "|") {
|
|
1459
1462
|
out.push(cur);
|
|
1460
1463
|
cur = "";
|
|
1461
|
-
if (c
|
|
1464
|
+
if (c === "&" || c === "|") i++;
|
|
1462
1465
|
} else if (c === "|" && pipes) {
|
|
1463
1466
|
out.push(cur);
|
|
1464
1467
|
cur = "";
|
|
@@ -1499,13 +1502,29 @@ function findLastFailure(cmds) {
|
|
|
1499
1502
|
}
|
|
1500
1503
|
var RESULTS_MAX = 12;
|
|
1501
1504
|
var REFERENCES_MAX = 15;
|
|
1505
|
+
var HEREDOC_RE = /(?<!<)<<(?!<)(-?)\s*(['"]?)([A-Za-z_][\w.-]*)\2/g;
|
|
1506
|
+
function stripHeredocs(cmd) {
|
|
1507
|
+
if (!cmd.includes("<<")) return cmd;
|
|
1508
|
+
const lines2 = cmd.split("\n");
|
|
1509
|
+
const out = [];
|
|
1510
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
1511
|
+
out.push(lines2[i]);
|
|
1512
|
+
for (const m of lines2[i].matchAll(HEREDOC_RE)) {
|
|
1513
|
+
const tabs = m[1] === "-";
|
|
1514
|
+
const end = lines2.findIndex((l, j) => j > i && (tabs ? l.replace(/^\t+/, "") : l) === m[3]);
|
|
1515
|
+
if (end !== -1) i = end;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return out.join("\n");
|
|
1519
|
+
}
|
|
1502
1520
|
var REDIRECT_RE = /\s+(?:\d?>>?|&>)\s*(?:&\d|\/dev\/null)/g;
|
|
1503
1521
|
function checkOf(cmd) {
|
|
1504
1522
|
let piped = false;
|
|
1505
|
-
const parts = splitShell(cmd).filter((s) => !isCd(s) && !isProbeCommand(s)).
|
|
1523
|
+
const parts = splitShell(stripHeredocs(cmd)).filter((s) => !isCd(s) && !isProbeCommand(s)).flatMap((s) => {
|
|
1506
1524
|
const stages = splitShell(s, true);
|
|
1507
1525
|
if (stages.length > 1) piped = true;
|
|
1508
|
-
|
|
1526
|
+
const first = stages[0]?.replace(REDIRECT_RE, "").trim();
|
|
1527
|
+
return first ? [first] : [];
|
|
1509
1528
|
});
|
|
1510
1529
|
return parts.length ? { key: parts.join(" && "), piped } : null;
|
|
1511
1530
|
}
|
|
@@ -1768,7 +1787,14 @@ async function runHandoff(o) {
|
|
|
1768
1787
|
const git2 = await takeGitSnapshot(cwd);
|
|
1769
1788
|
const previous = await readPreviousHandoff(o.store, git2?.root ?? cwd);
|
|
1770
1789
|
const data = extractHandoff(session, git2, { now: o.now ?? /* @__PURE__ */ new Date(), reason: o.reason, previous });
|
|
1790
|
+
o.onProgress?.({
|
|
1791
|
+
stage: "extracted",
|
|
1792
|
+
messages: session.timeline.filter((t) => t.kind === "user" || t.kind === "assistant").length,
|
|
1793
|
+
filesChanged: git2 ? git2.files.length : data.filesTouched.length,
|
|
1794
|
+
chars: sessionChars(session)
|
|
1795
|
+
});
|
|
1771
1796
|
const written = await writeHandoff(data, { target: o.target, mode: o.mode, previousId: data.carriedFrom, store: o.store });
|
|
1797
|
+
o.onProgress?.({ stage: "written", path: written.path, chars: written.chars });
|
|
1772
1798
|
if (o.launch === false) return { written, plan: null, data };
|
|
1773
1799
|
let plan;
|
|
1774
1800
|
try {
|
|
@@ -1779,6 +1805,16 @@ async function runHandoff(o) {
|
|
|
1779
1805
|
}
|
|
1780
1806
|
return { written, plan, data };
|
|
1781
1807
|
}
|
|
1808
|
+
function sessionChars(s) {
|
|
1809
|
+
let n = 0;
|
|
1810
|
+
for (const t of s.timeline) {
|
|
1811
|
+
if (t.kind === "user" || t.kind === "assistant") n += t.text.length;
|
|
1812
|
+
else if (t.kind === "command") n += t.run.cmd.length + t.run.output.length;
|
|
1813
|
+
else if (t.kind === "edit") n += t.edit.path.length + (t.edit.before?.length ?? 0) + (t.edit.after?.length ?? 0);
|
|
1814
|
+
else n += t.path.length;
|
|
1815
|
+
}
|
|
1816
|
+
return n;
|
|
1817
|
+
}
|
|
1782
1818
|
|
|
1783
1819
|
// ../../packages/core/src/delivery.ts
|
|
1784
1820
|
var SLACK_MS = 2e3;
|
|
@@ -2162,6 +2198,8 @@ var import_node_path13 = require("node:path");
|
|
|
2162
2198
|
var HOUR = 36e5;
|
|
2163
2199
|
var SAMPLE_MAX_AGE_MS = 30 * 24 * HOUR;
|
|
2164
2200
|
var DISPROVE_FACTOR = 1.15;
|
|
2201
|
+
var LIMIT_TRUST_SHARE = 0.5;
|
|
2202
|
+
var LIMIT_TRUST_MS = 7 * 24 * HOUR;
|
|
2165
2203
|
var MIN_CALIBRATION_PCT = 5;
|
|
2166
2204
|
var PACE_MS2 = 30 * 6e4;
|
|
2167
2205
|
function replyUnits(usage2, model) {
|
|
@@ -2326,7 +2364,9 @@ function mergeSamples(stored, fresh, now) {
|
|
|
2326
2364
|
}
|
|
2327
2365
|
function pickCapacity(kind, samples, entries, hits, now) {
|
|
2328
2366
|
const len = windowMs(kind);
|
|
2329
|
-
|
|
2367
|
+
const mine = samples.filter((x) => x.kind === kind).sort((a, b) => b.at - a.at);
|
|
2368
|
+
const doubtful = (s) => s.source === "limit" && mine.some((e) => e.source === "exact" && e.at <= s.at && s.at - e.at <= LIMIT_TRUST_MS && s.capacity < e.capacity * LIMIT_TRUST_SHARE);
|
|
2369
|
+
for (const s of mine.filter((x) => !doubtful(x))) {
|
|
2330
2370
|
let disproved = false;
|
|
2331
2371
|
for (let start = s.at; start < now.getTime() && !disproved; start += len / 5) {
|
|
2332
2372
|
const end = Math.min(start + len, now.getTime());
|
|
@@ -3294,6 +3334,186 @@ function panelPosition(icon, iconArea, userArea, width) {
|
|
|
3294
3334
|
return { x, y: icon.y + icon.height };
|
|
3295
3335
|
}
|
|
3296
3336
|
|
|
3337
|
+
// src/overlay.ts
|
|
3338
|
+
var import_electron = require("electron");
|
|
3339
|
+
var import_node_path18 = require("node:path");
|
|
3340
|
+
var MAX_SHOW_MS = 16e3;
|
|
3341
|
+
var Overlay = class {
|
|
3342
|
+
win = null;
|
|
3343
|
+
playing = null;
|
|
3344
|
+
timer = null;
|
|
3345
|
+
/** Plays it on the display with the target's window (or the pointer's); here `from` and `window` are screen coordinates. */
|
|
3346
|
+
play(p) {
|
|
3347
|
+
const display = p.window ? import_electron.screen.getDisplayMatching(p.window) : import_electron.screen.getDisplayNearestPoint(import_electron.screen.getCursorScreenPoint());
|
|
3348
|
+
const b = display.bounds;
|
|
3349
|
+
const fromHere = p.from.x >= b.x && p.from.x < b.x + b.width && p.from.y >= b.y && p.from.y < b.y + b.height;
|
|
3350
|
+
const from = fromHere ? { x: p.from.x - b.x, y: p.from.y - b.y } : { x: b.width - 80, y: 12 };
|
|
3351
|
+
const window = p.window ? { x: p.window.x - b.x, y: p.window.y - b.y, width: p.window.width, height: p.window.height } : null;
|
|
3352
|
+
const win = this.ensure();
|
|
3353
|
+
win.setBounds(b);
|
|
3354
|
+
this.playing = p.key;
|
|
3355
|
+
const send = () => win.webContents.send("harlay:overlay", { ...p, from, window, menuBar: display.workArea.y - b.y });
|
|
3356
|
+
if (win.webContents.isLoading()) win.webContents.once("did-finish-load", send);
|
|
3357
|
+
else send();
|
|
3358
|
+
win.showInactive();
|
|
3359
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3360
|
+
this.timer = setTimeout(() => this.done(), MAX_SHOW_MS);
|
|
3361
|
+
}
|
|
3362
|
+
/** The agent picked the handoff up: the widget says so before it goes. */
|
|
3363
|
+
read(key) {
|
|
3364
|
+
if (this.playing === key && this.win?.isVisible()) this.win.webContents.send("harlay:overlay", { read: true });
|
|
3365
|
+
}
|
|
3366
|
+
/** The page finished (or MAX_SHOW_MS passed). */
|
|
3367
|
+
done() {
|
|
3368
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3369
|
+
this.timer = null;
|
|
3370
|
+
this.playing = null;
|
|
3371
|
+
this.win?.hide();
|
|
3372
|
+
}
|
|
3373
|
+
isPage(wc) {
|
|
3374
|
+
return !!this.win && this.win.webContents === wc;
|
|
3375
|
+
}
|
|
3376
|
+
ensure() {
|
|
3377
|
+
if (this.win && !this.win.isDestroyed()) return this.win;
|
|
3378
|
+
const win = new import_electron.BrowserWindow({
|
|
3379
|
+
show: false,
|
|
3380
|
+
frame: false,
|
|
3381
|
+
transparent: true,
|
|
3382
|
+
backgroundColor: "#00000000",
|
|
3383
|
+
hasShadow: false,
|
|
3384
|
+
resizable: false,
|
|
3385
|
+
movable: false,
|
|
3386
|
+
minimizable: false,
|
|
3387
|
+
maximizable: false,
|
|
3388
|
+
fullscreenable: false,
|
|
3389
|
+
focusable: false,
|
|
3390
|
+
skipTaskbar: true,
|
|
3391
|
+
// A non-activating panel over the menu bar too, so the light can leave from the icon.
|
|
3392
|
+
type: "panel",
|
|
3393
|
+
enableLargerThanScreen: true,
|
|
3394
|
+
alwaysOnTop: true,
|
|
3395
|
+
webPreferences: { preload: (0, import_node_path18.join)(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false }
|
|
3396
|
+
});
|
|
3397
|
+
win.setAlwaysOnTop(true, "screen-saver");
|
|
3398
|
+
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
3399
|
+
win.setIgnoreMouseEvents(true);
|
|
3400
|
+
win.webContents.on("will-navigate", (e) => e.preventDefault());
|
|
3401
|
+
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
3402
|
+
void win.loadFile((0, import_node_path18.join)(__dirname, "..", "renderer", "overlay.html"));
|
|
3403
|
+
this.win = win;
|
|
3404
|
+
return win;
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
// src/show.ts
|
|
3409
|
+
var import_node_child_process4 = require("node:child_process");
|
|
3410
|
+
var tokensIn = (chars) => Math.max(1, Math.round(chars / 4));
|
|
3411
|
+
var plural = (n, one) => `${n.toLocaleString("en-US")} ${one}${n === 1 ? "" : "s"}`;
|
|
3412
|
+
function startView(o) {
|
|
3413
|
+
const fresh = o.from === o.to;
|
|
3414
|
+
const stopped = o.status === "limit";
|
|
3415
|
+
const why = stopped || o.status === "warning" ? o.detail.replace(/^Limit hit/, "usage limit").replace(/ · /g, ", ") : fresh ? "new chat, same task" : "moved by you";
|
|
3416
|
+
return {
|
|
3417
|
+
key: o.key,
|
|
3418
|
+
from: { tool: o.from, label: TOOL_LABEL[o.from] },
|
|
3419
|
+
to: { tool: o.to, label: fresh ? "New chat" : TOOL_LABEL[o.to], mode: o.mode },
|
|
3420
|
+
title: o.title,
|
|
3421
|
+
tag: stopped ? "stopped" : "moving",
|
|
3422
|
+
steps: [
|
|
3423
|
+
{ name: "Detect", state: "run", detail: why.charAt(0).toLowerCase() + why.slice(1) },
|
|
3424
|
+
{ name: "Extract", state: "wait", detail: "reading the chat and git" },
|
|
3425
|
+
{ name: "Distill", state: "wait", detail: "into one .harlay file" },
|
|
3426
|
+
{ name: "Rehydrate", state: "wait", detail: rehydrateDetail(o.to, o.mode, fresh, false) }
|
|
3427
|
+
],
|
|
3428
|
+
count: null,
|
|
3429
|
+
error: null,
|
|
3430
|
+
leaving: false
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
function rehydrateDetail(to, mode, fresh, done) {
|
|
3434
|
+
const where = `${fresh ? `new ${TOOL_LABEL[to]} chat` : TOOL_LABEL[to]}${mode === "terminal" ? " in Terminal" : ""}`;
|
|
3435
|
+
if (!done) return `opening ${where}`;
|
|
3436
|
+
return mode === "app" ? `${where} \xB7 press \u23CE to send` : `${where} \xB7 picking it up`;
|
|
3437
|
+
}
|
|
3438
|
+
function extractDetail(messages, filesChanged) {
|
|
3439
|
+
return filesChanged ? `${plural(messages, "message")} \xB7 ${plural(filesChanged, "file")} changed` : plural(messages, "message");
|
|
3440
|
+
}
|
|
3441
|
+
function withStep(v, name, state2, detail) {
|
|
3442
|
+
return { ...v, steps: v.steps.map((s) => s.name === name ? { ...s, state: state2, detail: detail ?? s.detail } : s) };
|
|
3443
|
+
}
|
|
3444
|
+
function failView(v, error) {
|
|
3445
|
+
return { ...v, tag: "failed", error, steps: v.steps.map((s) => s.state === "run" ? { ...s, state: "fail" } : s) };
|
|
3446
|
+
}
|
|
3447
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3448
|
+
var Pacer = class {
|
|
3449
|
+
constructor(clock3 = { now: Date.now, sleep }) {
|
|
3450
|
+
this.clock = clock3;
|
|
3451
|
+
this.last = clock3.now();
|
|
3452
|
+
}
|
|
3453
|
+
clock;
|
|
3454
|
+
chain = Promise.resolve();
|
|
3455
|
+
last;
|
|
3456
|
+
/** `update` runs at least `minMs` after the one before it; resolves once it has run. One that throws doesn't stop the rest. */
|
|
3457
|
+
next(minMs, update) {
|
|
3458
|
+
this.chain = this.chain.then(async () => {
|
|
3459
|
+
const wait = this.last + minMs - this.clock.now();
|
|
3460
|
+
if (wait > 0) await this.clock.sleep(wait);
|
|
3461
|
+
try {
|
|
3462
|
+
update();
|
|
3463
|
+
} catch (e) {
|
|
3464
|
+
console.error("[harlay] handoff view update failed", e);
|
|
3465
|
+
}
|
|
3466
|
+
this.last = this.clock.now();
|
|
3467
|
+
});
|
|
3468
|
+
return this.chain;
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
var LIST_WINDOWS = `ObjC.import('CoreGraphics');
|
|
3472
|
+
var l = ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements, $.kCGNullWindowID))) || [];
|
|
3473
|
+
JSON.stringify(l.filter(function (w) { return w.kCGWindowLayer === 0 && w.kCGWindowAlpha !== 0; }).slice(0, 16).map(function (w) {
|
|
3474
|
+
var b = w.kCGWindowBounds; return { id: w.kCGWindowNumber, app: w.kCGWindowOwnerName, x: b.X, y: b.Y, width: b.Width, height: b.Height };
|
|
3475
|
+
}));`;
|
|
3476
|
+
function listWindows() {
|
|
3477
|
+
return new Promise((resolve) => {
|
|
3478
|
+
(0, import_node_child_process4.execFile)("osascript", ["-l", "JavaScript", "-e", LIST_WINDOWS], { timeout: 2e3 }, (err, out) => {
|
|
3479
|
+
if (err) return resolve([]);
|
|
3480
|
+
try {
|
|
3481
|
+
const list = JSON.parse(out);
|
|
3482
|
+
resolve(Array.isArray(list) ? list : []);
|
|
3483
|
+
} catch {
|
|
3484
|
+
resolve([]);
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
function windowOwners(target, mode, terminalApp) {
|
|
3490
|
+
return mode === "terminal" ? [terminalApp] : APP_BUNDLES[target].map((b) => b.replace(/\.app$/, ""));
|
|
3491
|
+
}
|
|
3492
|
+
function ownedBy(app2, owners) {
|
|
3493
|
+
const a = app2.toLowerCase();
|
|
3494
|
+
return owners.some((o) => {
|
|
3495
|
+
const b = o.toLowerCase();
|
|
3496
|
+
return a === b || a.startsWith(b);
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
var isMain = (w) => w.width >= 320 && w.height >= 240;
|
|
3500
|
+
async function windowIds() {
|
|
3501
|
+
return new Set((await listWindows()).map((w) => w.id));
|
|
3502
|
+
}
|
|
3503
|
+
async function findTargetWindow(owners, o) {
|
|
3504
|
+
const before = o.newSince ? await o.newSince : null;
|
|
3505
|
+
const end = Date.now() + o.timeoutMs;
|
|
3506
|
+
for (; ; ) {
|
|
3507
|
+
const all = (await listWindows()).filter(isMain);
|
|
3508
|
+
const mine = all.filter((w) => ownedBy(w.app, owners));
|
|
3509
|
+
const found = before ? mine.find((w) => !before.has(w.id)) : all[0] && ownedBy(all[0].app, owners) ? all[0] : void 0;
|
|
3510
|
+
if (found) return rect(found);
|
|
3511
|
+
if (Date.now() >= end) return mine[0] ? rect(mine[0]) : null;
|
|
3512
|
+
await sleep(250);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
var rect = (w) => ({ x: w.x, y: w.y, width: w.width, height: w.height });
|
|
3516
|
+
|
|
3297
3517
|
// src/targets.ts
|
|
3298
3518
|
function pickTargets(source, defaultTarget, available2, modes, limitedUntil = {}, now = /* @__PURE__ */ new Date()) {
|
|
3299
3519
|
const order = [defaultTarget, ...TOOLS.filter((t) => t !== defaultTarget)];
|
|
@@ -3304,7 +3524,7 @@ function pickTargets(source, defaultTarget, available2, modes, limitedUntil = {}
|
|
|
3304
3524
|
var import_node_events2 = require("node:events");
|
|
3305
3525
|
var import_node_fs4 = require("node:fs");
|
|
3306
3526
|
var import_promises18 = require("node:fs/promises");
|
|
3307
|
-
var
|
|
3527
|
+
var import_node_path19 = require("node:path");
|
|
3308
3528
|
var CODEX_STALE_MS = 30 * 6e4;
|
|
3309
3529
|
var CODEX_QUERY_GAP_MS = 30 * 6e4;
|
|
3310
3530
|
var DEBOUNCE_MS = 2e3;
|
|
@@ -3335,7 +3555,7 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3335
3555
|
return claudeStatusFile(this.opts.dataDir);
|
|
3336
3556
|
}
|
|
3337
3557
|
get samplesFile() {
|
|
3338
|
-
return (0,
|
|
3558
|
+
return (0, import_node_path19.join)(this.opts.dataDir, "usage-calibration.json");
|
|
3339
3559
|
}
|
|
3340
3560
|
current() {
|
|
3341
3561
|
return this.snapshots;
|
|
@@ -3358,8 +3578,8 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3358
3578
|
} catch {
|
|
3359
3579
|
}
|
|
3360
3580
|
};
|
|
3361
|
-
watchDir((0,
|
|
3362
|
-
watchDir((0,
|
|
3581
|
+
watchDir((0, import_node_path19.join)(this.opts.home, ".codex", "sessions"), "codex", true);
|
|
3582
|
+
watchDir((0, import_node_path19.join)(this.opts.home, ".claude", "projects"), "claude-code", true);
|
|
3363
3583
|
watchDir(this.opts.dataDir, "claude-code", false);
|
|
3364
3584
|
await Promise.all([this.update("codex"), this.update("claude-code")]);
|
|
3365
3585
|
void this.topUpCodex();
|
|
@@ -3461,6 +3681,7 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3461
3681
|
var MAX_LIVE_NOTIFICATIONS = 50;
|
|
3462
3682
|
var AUTO_HANDOFF_COOLDOWN_MS = 2 * 6e4;
|
|
3463
3683
|
var home = process.env.HARLAY_HOME ?? (0, import_node_os3.homedir)();
|
|
3684
|
+
if (process.env.HARLAY_HOME) import_electron2.app.setPath("userData", (0, import_node_path20.join)(home, "Library", "Application Support", "Harlay"));
|
|
3464
3685
|
var readers = createReaders({ home });
|
|
3465
3686
|
var state = createState();
|
|
3466
3687
|
var liveNotifications = /* @__PURE__ */ new Set();
|
|
@@ -3477,6 +3698,8 @@ var claudeBridge = "off";
|
|
|
3477
3698
|
var lastAutoHandoffAt = 0;
|
|
3478
3699
|
var agentIcons = {};
|
|
3479
3700
|
var health = new HealthMonitor();
|
|
3701
|
+
var overlay = new Overlay();
|
|
3702
|
+
var showing = null;
|
|
3480
3703
|
var errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
3481
3704
|
function toElectron(nodes) {
|
|
3482
3705
|
return nodes.map((n) => {
|
|
@@ -3502,7 +3725,7 @@ function refreshUi() {
|
|
|
3502
3725
|
popover.webContents.send("harlay:model", model);
|
|
3503
3726
|
}
|
|
3504
3727
|
function nativeMenu() {
|
|
3505
|
-
return
|
|
3728
|
+
return import_electron2.Menu.buildFromTemplate(toElectron(buildMenuModel(recentSessions(state, 8), settings, available)));
|
|
3506
3729
|
}
|
|
3507
3730
|
function keep(n) {
|
|
3508
3731
|
liveNotifications.add(n);
|
|
@@ -3517,7 +3740,7 @@ function keep(n) {
|
|
|
3517
3740
|
n.on("action", release);
|
|
3518
3741
|
}
|
|
3519
3742
|
function notify(title, body) {
|
|
3520
|
-
const n = new
|
|
3743
|
+
const n = new import_electron2.Notification({ title, body });
|
|
3521
3744
|
keep(n);
|
|
3522
3745
|
n.show();
|
|
3523
3746
|
}
|
|
@@ -3527,28 +3750,93 @@ async function handoff(ref, target) {
|
|
|
3527
3750
|
handoffsInFlight.add(key);
|
|
3528
3751
|
refreshUi();
|
|
3529
3752
|
const since = /* @__PURE__ */ new Date();
|
|
3753
|
+
const mode = settings.modes[target];
|
|
3754
|
+
const show = presentHandoff(ref, target, mode);
|
|
3530
3755
|
try {
|
|
3531
3756
|
const res = await runHandoff({
|
|
3532
3757
|
session: ref,
|
|
3533
3758
|
target,
|
|
3534
|
-
mode
|
|
3759
|
+
mode,
|
|
3535
3760
|
readers,
|
|
3536
3761
|
store: handoffStore(home),
|
|
3537
3762
|
terminalApp: settings.terminalApp,
|
|
3538
|
-
resolve: defaultResolveOptions()
|
|
3763
|
+
resolve: defaultResolveOptions(),
|
|
3764
|
+
onProgress: show.progress
|
|
3539
3765
|
});
|
|
3540
|
-
const mode = settings.modes[target];
|
|
3541
3766
|
const { id, path, prompt } = res.written;
|
|
3542
3767
|
markHandedOff(state, key, target, /* @__PURE__ */ new Date(), { id, path, prompt, mode, cwd: res.data.workdir, since });
|
|
3543
3768
|
closeResolvedAlert();
|
|
3544
|
-
|
|
3769
|
+
void show.launched();
|
|
3545
3770
|
} catch (e) {
|
|
3771
|
+
show.failed(errorMessage(e));
|
|
3546
3772
|
notify("Harlay could not hand off", errorMessage(e));
|
|
3547
3773
|
} finally {
|
|
3548
3774
|
handoffsInFlight.delete(key);
|
|
3549
3775
|
}
|
|
3550
3776
|
refreshUi();
|
|
3551
3777
|
}
|
|
3778
|
+
function presentHandoff(ref, target, mode) {
|
|
3779
|
+
const key = sessionKey(ref);
|
|
3780
|
+
const t = state.sessions.get(key);
|
|
3781
|
+
let view = startView({ key, from: ref.tool, to: target, mode, title: t ? sessionTitle(t) : "This chat", status: t?.status ?? "idle", detail: t?.detail ?? "" });
|
|
3782
|
+
const set = (v) => {
|
|
3783
|
+
view = v;
|
|
3784
|
+
if (showing === key && popover && !popover.isDestroyed()) popover.webContents.send("harlay:handoff", v);
|
|
3785
|
+
};
|
|
3786
|
+
showing = key;
|
|
3787
|
+
alertKey = null;
|
|
3788
|
+
if (popover && !popover.isVisible()) showPopover(null, false, "user");
|
|
3789
|
+
set(view);
|
|
3790
|
+
const pace = new Pacer();
|
|
3791
|
+
void pace.next(380, () => set(withStep(withStep(view, "Detect", "done"), "Extract", "run")));
|
|
3792
|
+
const before = mode === "terminal" ? windowIds() : null;
|
|
3793
|
+
let fromTokens = health.get(key)?.context?.tokens ?? null;
|
|
3794
|
+
let window = Promise.resolve(null);
|
|
3795
|
+
return {
|
|
3796
|
+
progress: (p) => {
|
|
3797
|
+
if (p.stage === "extracted") {
|
|
3798
|
+
fromTokens ??= tokensIn(p.chars);
|
|
3799
|
+
void pace.next(520, () => set(withStep(withStep(view, "Extract", "done", extractDetail(p.messages, p.filesChanged)), "Distill", "run")));
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
const to = tokensIn(p.chars);
|
|
3803
|
+
const from = fromTokens ?? to;
|
|
3804
|
+
if (from > to) void pace.next(0, () => set({ ...view, count: { from, to } }));
|
|
3805
|
+
const size = from > to ? void 0 : `${to.toLocaleString("en-US")} tokens in one .harlay file`;
|
|
3806
|
+
void pace.next(from > to ? 950 : 500, () => set(withStep(withStep(view, "Distill", "done", size), "Rehydrate", "run")));
|
|
3807
|
+
window = findTargetWindow(windowOwners(target, mode, settings.terminalApp), { newSince: before, timeoutMs: 4e3 });
|
|
3808
|
+
},
|
|
3809
|
+
launched: async () => {
|
|
3810
|
+
await pace.next(450, () => set({ ...withStep(view, "Rehydrate", "done", rehydrateDetail(target, mode, ref.tool === target, true)), tag: "done" }));
|
|
3811
|
+
await pace.next(800, () => set({ ...view, leaving: true }));
|
|
3812
|
+
await pace.next(200, () => void 0);
|
|
3813
|
+
if (showing !== key) {
|
|
3814
|
+
notify(`Continuing in ${TOOL_LABEL[target]}`, mode === "app" ? "The prompt is typed in: press Enter there to send it." : "It picks up where this chat left off.");
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
showing = null;
|
|
3818
|
+
hidePopover();
|
|
3819
|
+
const found = await window;
|
|
3820
|
+
const tb = tray?.getBounds();
|
|
3821
|
+
const delivered = state.sessions.get(key)?.delivery?.state;
|
|
3822
|
+
overlay.play({
|
|
3823
|
+
key,
|
|
3824
|
+
from: tb ? { x: tb.x + tb.width / 2, y: tb.y + tb.height / 2 } : { x: 0, y: 0 },
|
|
3825
|
+
window: found,
|
|
3826
|
+
to: { tool: target, label: TOOL_LABEL[target], icon: agentIcons[target] ?? null, mode },
|
|
3827
|
+
fromLabel: TOOL_LABEL[ref.tool],
|
|
3828
|
+
// The agent may have picked it up already (a terminal starts at once).
|
|
3829
|
+
picked: delivered === "sent" || delivered === "read"
|
|
3830
|
+
});
|
|
3831
|
+
},
|
|
3832
|
+
failed: (error) => {
|
|
3833
|
+
void pace.next(0, () => {
|
|
3834
|
+
set(failView(view, error));
|
|
3835
|
+
if (showing === key) showing = null;
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3552
3840
|
var DELIVERY_CHECK_MS = 3e3;
|
|
3553
3841
|
var checkingDeliveries = false;
|
|
3554
3842
|
async function checkDeliveries() {
|
|
@@ -3560,8 +3848,9 @@ async function checkDeliveries() {
|
|
|
3560
3848
|
const got = await checkDelivery(readers, { id: d.id, path: d.path, target: d.target, since: d.since }).catch(() => null);
|
|
3561
3849
|
const { changed, becameMissed } = applyDelivery(t, got, /* @__PURE__ */ new Date());
|
|
3562
3850
|
if (changed) refreshUi();
|
|
3851
|
+
if (changed && (d.state === "sent" || d.state === "read")) overlay.read(sessionKey(t.ref));
|
|
3563
3852
|
if (becameMissed) {
|
|
3564
|
-
const n = new
|
|
3853
|
+
const n = new import_electron2.Notification({
|
|
3565
3854
|
title: `${TOOL_LABEL[d.target]} hasn't picked up the handoff`,
|
|
3566
3855
|
body: d.mode === "app" ? `Press Enter where the prompt is typed in ${TOOL_LABEL[d.target]}, or open Harlay to send it again.` : "Open Harlay to send it again."
|
|
3567
3856
|
});
|
|
@@ -3600,7 +3889,7 @@ async function onAction(a) {
|
|
|
3600
3889
|
}
|
|
3601
3890
|
case "setTheme":
|
|
3602
3891
|
settings.theme = a.theme;
|
|
3603
|
-
|
|
3892
|
+
import_electron2.nativeTheme.themeSource = a.theme;
|
|
3604
3893
|
await persist();
|
|
3605
3894
|
break;
|
|
3606
3895
|
case "setDefault":
|
|
@@ -3617,7 +3906,7 @@ async function onAction(a) {
|
|
|
3617
3906
|
break;
|
|
3618
3907
|
case "toggleLogin":
|
|
3619
3908
|
settings.openAtLogin = !settings.openAtLogin;
|
|
3620
|
-
if (
|
|
3909
|
+
if (import_electron2.app.isPackaged) import_electron2.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
|
|
3621
3910
|
await persist();
|
|
3622
3911
|
break;
|
|
3623
3912
|
case "toggleClaudeBridge":
|
|
@@ -3636,7 +3925,7 @@ async function onAction(a) {
|
|
|
3636
3925
|
break;
|
|
3637
3926
|
}
|
|
3638
3927
|
case "openCursorUsage":
|
|
3639
|
-
await
|
|
3928
|
+
await import_electron2.shell.openExternal("https://cursor.com/dashboard?tab=usage");
|
|
3640
3929
|
break;
|
|
3641
3930
|
case "unpinChat":
|
|
3642
3931
|
settings.pinned = settings.pinned.filter((k) => k !== a.key);
|
|
@@ -3652,11 +3941,11 @@ async function onAction(a) {
|
|
|
3652
3941
|
break;
|
|
3653
3942
|
case "copyPrompt": {
|
|
3654
3943
|
const d = state.sessions.get(a.key)?.delivery;
|
|
3655
|
-
if (d)
|
|
3944
|
+
if (d) import_electron2.clipboard.writeText(d.prompt);
|
|
3656
3945
|
break;
|
|
3657
3946
|
}
|
|
3658
3947
|
case "quit":
|
|
3659
|
-
|
|
3948
|
+
import_electron2.app.quit();
|
|
3660
3949
|
break;
|
|
3661
3950
|
}
|
|
3662
3951
|
} catch (e) {
|
|
@@ -3684,7 +3973,7 @@ function onWatchEvent(ev) {
|
|
|
3684
3973
|
function onUsageAlert(a) {
|
|
3685
3974
|
const latest = recentSessions(state, 50).find((t) => t.ref.tool === a.tool && t.status !== "handed_off");
|
|
3686
3975
|
const [target] = latest ? pickTargets(a.tool, settings.defaultTarget, available, settings.modes, state.limitedUntil, /* @__PURE__ */ new Date()) : [];
|
|
3687
|
-
const n = new
|
|
3976
|
+
const n = new import_electron2.Notification({ title: a.title, body: latest && target ? a.body : a.body.replace(/ ?Click to hand off.*$/, "") });
|
|
3688
3977
|
keep(n);
|
|
3689
3978
|
n.on("click", () => {
|
|
3690
3979
|
if (latest && target) void handoff(latest.ref, target);
|
|
@@ -3692,7 +3981,7 @@ function onUsageAlert(a) {
|
|
|
3692
3981
|
n.show();
|
|
3693
3982
|
}
|
|
3694
3983
|
function dataDir() {
|
|
3695
|
-
return
|
|
3984
|
+
return import_electron2.app.getPath("userData");
|
|
3696
3985
|
}
|
|
3697
3986
|
function onHotkey() {
|
|
3698
3987
|
if (popover?.isVisible()) {
|
|
@@ -3712,7 +4001,7 @@ async function trackPinned() {
|
|
|
3712
4001
|
seedSessions(state, refs.filter((r) => r !== null));
|
|
3713
4002
|
}
|
|
3714
4003
|
async function addChat(tool, raw) {
|
|
3715
|
-
const id = (0,
|
|
4004
|
+
const id = (0, import_node_path20.basename)(raw.trim()).replace(/\.jsonl?$/, "");
|
|
3716
4005
|
if (!/^[\w.-]{4,200}$/.test(id)) return { ok: false, error: "That doesn\u2019t look like a chat ID." };
|
|
3717
4006
|
const ref = await findSessionById(readers, tool, id).catch(() => null);
|
|
3718
4007
|
if (!ref) return { ok: false, error: `No ${TOOL_LABEL[tool]} chat with that ID on this Mac.` };
|
|
@@ -3730,10 +4019,10 @@ async function loadAgentIcons() {
|
|
|
3730
4019
|
const dir = defaultResolveOptions().applicationsDir;
|
|
3731
4020
|
for (const tool of TOOLS) {
|
|
3732
4021
|
for (const bundle of APP_BUNDLES[tool]) {
|
|
3733
|
-
const path = (0,
|
|
4022
|
+
const path = (0, import_node_path20.join)(dir, bundle);
|
|
3734
4023
|
try {
|
|
3735
4024
|
await (0, import_promises19.access)(path);
|
|
3736
|
-
const thumb = await
|
|
4025
|
+
const thumb = await import_electron2.nativeImage.createThumbnailFromPath(path, { width: 64, height: 64 });
|
|
3737
4026
|
agentIcons[tool] = thumb.resize({ width: 36, height: 36, quality: "best" }).toDataURL();
|
|
3738
4027
|
break;
|
|
3739
4028
|
} catch {
|
|
@@ -3768,7 +4057,7 @@ async function startFresh(ref) {
|
|
|
3768
4057
|
await handoff(ref, tool);
|
|
3769
4058
|
}
|
|
3770
4059
|
function createPopover() {
|
|
3771
|
-
const win = new
|
|
4060
|
+
const win = new import_electron2.BrowserWindow({
|
|
3772
4061
|
width: POPOVER_WIDTH,
|
|
3773
4062
|
height: 420,
|
|
3774
4063
|
show: false,
|
|
@@ -3786,13 +4075,15 @@ function createPopover() {
|
|
|
3786
4075
|
acceptFirstMouse: true,
|
|
3787
4076
|
// backgroundThrottling off: the limit panel opens while another app is in front, and a throttled page would leave
|
|
3788
4077
|
// its fade-in (and so the whole panel) invisible.
|
|
3789
|
-
webPreferences: { preload: (0,
|
|
4078
|
+
webPreferences: { preload: (0, import_node_path20.join)(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false }
|
|
3790
4079
|
});
|
|
3791
4080
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
3792
|
-
win.on("blur",
|
|
4081
|
+
win.on("blur", () => {
|
|
4082
|
+
if (!showing) hidePopover();
|
|
4083
|
+
});
|
|
3793
4084
|
win.webContents.on("will-navigate", (e) => e.preventDefault());
|
|
3794
4085
|
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
3795
|
-
void win.loadFile((0,
|
|
4086
|
+
void win.loadFile((0, import_node_path20.join)(__dirname, "..", "renderer", "popover.html"));
|
|
3796
4087
|
return win;
|
|
3797
4088
|
}
|
|
3798
4089
|
var debug = process.env.HARLAY_DEBUG ? (...a) => console.log("[harlay]", ...a) : () => void 0;
|
|
@@ -3815,8 +4106,8 @@ function closeResolvedAlert() {
|
|
|
3815
4106
|
function showPopover(route = null, focus = true, where = "icon") {
|
|
3816
4107
|
if (!popover || !tray) return;
|
|
3817
4108
|
const t = tray.getBounds();
|
|
3818
|
-
const iconArea =
|
|
3819
|
-
const userArea = where === "user" ?
|
|
4109
|
+
const iconArea = import_electron2.screen.getDisplayNearestPoint({ x: t.x, y: t.y }).workArea;
|
|
4110
|
+
const userArea = where === "user" ? import_electron2.screen.getDisplayNearestPoint(import_electron2.screen.getCursorScreenPoint()).workArea : iconArea;
|
|
3820
4111
|
const { x, y } = panelPosition(t, iconArea, userArea, POPOVER_WIDTH);
|
|
3821
4112
|
popover.setPosition(Math.round(x), Math.round(y));
|
|
3822
4113
|
refreshUi();
|
|
@@ -3832,7 +4123,7 @@ function showPopover(route = null, focus = true, where = "icon") {
|
|
|
3832
4123
|
}
|
|
3833
4124
|
function revealPopover(route = null, where = "icon") {
|
|
3834
4125
|
if (!popover || !tray) return;
|
|
3835
|
-
|
|
4126
|
+
import_electron2.app.focus({ steal: true });
|
|
3836
4127
|
if (popover.webContents.isLoading()) popover.webContents.once("did-finish-load", () => showPopover(route, true, where));
|
|
3837
4128
|
else showPopover(route, true, where);
|
|
3838
4129
|
}
|
|
@@ -3852,50 +4143,53 @@ function togglePopover() {
|
|
|
3852
4143
|
}
|
|
3853
4144
|
function listenToPopover() {
|
|
3854
4145
|
const fromPopover = (e) => !!popover && e.sender === popover.webContents;
|
|
3855
|
-
|
|
4146
|
+
import_electron2.ipcMain.on("harlay:ready", (e) => {
|
|
3856
4147
|
if (fromPopover(e)) refreshUi();
|
|
3857
4148
|
});
|
|
3858
|
-
|
|
4149
|
+
import_electron2.ipcMain.on("harlay:hide", (e) => {
|
|
3859
4150
|
if (fromPopover(e)) hidePopover();
|
|
3860
4151
|
});
|
|
3861
|
-
|
|
4152
|
+
import_electron2.ipcMain.on("harlay:resize", (e, height) => {
|
|
3862
4153
|
if (!fromPopover(e) || !popover || typeof height !== "number" || !Number.isFinite(height)) return;
|
|
3863
|
-
const area =
|
|
4154
|
+
const area = import_electron2.screen.getDisplayMatching(popover.getBounds()).workArea;
|
|
3864
4155
|
popover.setSize(POPOVER_WIDTH, Math.round(Math.min(Math.max(height, 120), area.height - 24)));
|
|
3865
4156
|
});
|
|
3866
|
-
|
|
4157
|
+
import_electron2.ipcMain.handle("harlay:addChat", async (e, req) => {
|
|
3867
4158
|
const r = typeof req === "object" && req !== null ? req : {};
|
|
3868
4159
|
if (!fromPopover(e) || !isTool(r.tool) || typeof r.id !== "string") return { ok: false, error: "Paste a chat ID." };
|
|
3869
4160
|
return addChat(r.tool, r.id);
|
|
3870
4161
|
});
|
|
3871
|
-
|
|
4162
|
+
import_electron2.ipcMain.on("harlay:action", (e, action) => {
|
|
3872
4163
|
if (!fromPopover(e) || !isMenuAction(action)) return;
|
|
3873
|
-
if (action.type === "
|
|
4164
|
+
if (action.type === "resendHandoff" || action.type === "openCursorUsage") hidePopover();
|
|
3874
4165
|
void onAction(action);
|
|
3875
4166
|
});
|
|
4167
|
+
import_electron2.ipcMain.on("harlay:overlayDone", (e) => {
|
|
4168
|
+
if (overlay.isPage(e.sender)) overlay.done();
|
|
4169
|
+
});
|
|
3876
4170
|
}
|
|
3877
|
-
if (!
|
|
3878
|
-
|
|
4171
|
+
if (!import_electron2.app.requestSingleInstanceLock()) {
|
|
4172
|
+
import_electron2.app.quit();
|
|
3879
4173
|
} else {
|
|
3880
|
-
|
|
4174
|
+
import_electron2.app.on("window-all-closed", () => {
|
|
3881
4175
|
});
|
|
3882
|
-
|
|
4176
|
+
import_electron2.app.on("activate", () => {
|
|
3883
4177
|
debug("activate", { alertKey });
|
|
3884
4178
|
revealPopover();
|
|
3885
4179
|
});
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
settingsFile = (0,
|
|
4180
|
+
import_electron2.app.on("second-instance", () => revealPopover());
|
|
4181
|
+
import_electron2.app.whenReady().then(async () => {
|
|
4182
|
+
import_electron2.app.dock?.hide();
|
|
4183
|
+
settingsFile = (0, import_node_path20.join)(import_electron2.app.getPath("userData"), "settings.json");
|
|
3890
4184
|
settings = await loadSettings(settingsFile);
|
|
3891
|
-
|
|
3892
|
-
if (
|
|
4185
|
+
import_electron2.nativeTheme.themeSource = settings.theme;
|
|
4186
|
+
if (import_electron2.app.isPackaged) import_electron2.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
|
|
3893
4187
|
available = await availableTargets(defaultResolveOptions());
|
|
3894
4188
|
await loadAgentIcons();
|
|
3895
|
-
const iconPath = (0,
|
|
3896
|
-
const icon =
|
|
4189
|
+
const iconPath = (0, import_node_path20.join)(__dirname, "..", "assets", "trayTemplate.png");
|
|
4190
|
+
const icon = import_electron2.nativeImage.createFromPath(iconPath);
|
|
3897
4191
|
icon.setTemplateImage(true);
|
|
3898
|
-
tray = new
|
|
4192
|
+
tray = new import_electron2.Tray(icon);
|
|
3899
4193
|
tray.setToolTip("Harlay");
|
|
3900
4194
|
if (icon.isEmpty()) {
|
|
3901
4195
|
console.error("[harlay] tray icon missing at", iconPath);
|
|
@@ -3927,15 +4221,15 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
3927
4221
|
claudeBridge = await bridgeState(home);
|
|
3928
4222
|
if (claudeBridge === "on") await installBridge(home, dataDir()).catch((e) => console.error("[harlay] status line refresh failed", e));
|
|
3929
4223
|
await usage.start().catch((e) => console.error("[harlay] usage monitor failed to start", e));
|
|
3930
|
-
if (!
|
|
3931
|
-
if (!
|
|
4224
|
+
if (!import_electron2.globalShortcut.register(HOTKEY.accelerator, onHotkey)) console.error(`[harlay] could not register ${HOTKEY.accelerator}`);
|
|
4225
|
+
if (!import_electron2.app.getLoginItemSettings().wasOpenedAtLogin) revealPopover();
|
|
3932
4226
|
}).catch((e) => {
|
|
3933
4227
|
console.error("[harlay] startup failed", e);
|
|
3934
4228
|
notify("Harlay failed to start", errorMessage(e));
|
|
3935
|
-
setTimeout(() =>
|
|
4229
|
+
setTimeout(() => import_electron2.app.quit(), 1500);
|
|
3936
4230
|
});
|
|
3937
|
-
|
|
3938
|
-
|
|
4231
|
+
import_electron2.app.on("will-quit", () => {
|
|
4232
|
+
import_electron2.globalShortcut.unregisterAll();
|
|
3939
4233
|
watcher?.stop();
|
|
3940
4234
|
usage?.stop();
|
|
3941
4235
|
});
|