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 CHANGED
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  // src/main.ts
4
- var import_electron = require("electron");
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 import_node_path19 = require("node:path");
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
- await (0, import_promises3.writeFile)(tmp, JSON.stringify(record, null, 2) + "\n");
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 === ";" || c === "&" && cmd[i + 1] === "&" || c === "|" && cmd[i + 1] === "|") {
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 !== ";") i++;
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)).map((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
- return stages[0].replace(REDIRECT_RE, "").trim();
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
- for (const s of samples.filter((x) => x.kind === kind).sort((a, b) => b.at - a.at)) {
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 import_node_path18 = require("node:path");
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, import_node_path18.join)(this.opts.dataDir, "usage-calibration.json");
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, import_node_path18.join)(this.opts.home, ".codex", "sessions"), "codex", true);
3362
- watchDir((0, import_node_path18.join)(this.opts.home, ".claude", "projects"), "claude-code", true);
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 import_electron.Menu.buildFromTemplate(toElectron(buildMenuModel(recentSessions(state, 8), settings, available)));
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 import_electron.Notification({ title, body });
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: settings.modes[target],
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
- 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.");
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 import_electron.Notification({
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
- import_electron.nativeTheme.themeSource = a.theme;
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 (import_electron.app.isPackaged) import_electron.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
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 import_electron.shell.openExternal("https://cursor.com/dashboard?tab=usage");
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) import_electron.clipboard.writeText(d.prompt);
3944
+ if (d) import_electron2.clipboard.writeText(d.prompt);
3656
3945
  break;
3657
3946
  }
3658
3947
  case "quit":
3659
- import_electron.app.quit();
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 import_electron.Notification({ title: a.title, body: latest && target ? a.body : a.body.replace(/ ?Click to hand off.*$/, "") });
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 import_electron.app.getPath("userData");
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, import_node_path19.basename)(raw.trim()).replace(/\.jsonl?$/, "");
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, import_node_path19.join)(dir, bundle);
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 import_electron.nativeImage.createThumbnailFromPath(path, { width: 64, height: 64 });
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 import_electron.BrowserWindow({
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, import_node_path19.join)(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false }
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", hidePopover);
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, import_node_path19.join)(__dirname, "..", "renderer", "popover.html"));
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 = import_electron.screen.getDisplayNearestPoint({ x: t.x, y: t.y }).workArea;
3819
- const userArea = where === "user" ? import_electron.screen.getDisplayNearestPoint(import_electron.screen.getCursorScreenPoint()).workArea : iconArea;
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
- import_electron.app.focus({ steal: true });
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
- import_electron.ipcMain.on("harlay:ready", (e) => {
4146
+ import_electron2.ipcMain.on("harlay:ready", (e) => {
3856
4147
  if (fromPopover(e)) refreshUi();
3857
4148
  });
3858
- import_electron.ipcMain.on("harlay:hide", (e) => {
4149
+ import_electron2.ipcMain.on("harlay:hide", (e) => {
3859
4150
  if (fromPopover(e)) hidePopover();
3860
4151
  });
3861
- import_electron.ipcMain.on("harlay:resize", (e, height) => {
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 = import_electron.screen.getDisplayMatching(popover.getBounds()).workArea;
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
- import_electron.ipcMain.handle("harlay:addChat", async (e, req) => {
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
- import_electron.ipcMain.on("harlay:action", (e, action) => {
4162
+ import_electron2.ipcMain.on("harlay:action", (e, action) => {
3872
4163
  if (!fromPopover(e) || !isMenuAction(action)) return;
3873
- if (action.type === "handoff" || action.type === "freshStart" || action.type === "resendHandoff" || action.type === "openCursorUsage") hidePopover();
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 (!import_electron.app.requestSingleInstanceLock()) {
3878
- import_electron.app.quit();
4171
+ if (!import_electron2.app.requestSingleInstanceLock()) {
4172
+ import_electron2.app.quit();
3879
4173
  } else {
3880
- import_electron.app.on("window-all-closed", () => {
4174
+ import_electron2.app.on("window-all-closed", () => {
3881
4175
  });
3882
- import_electron.app.on("activate", () => {
4176
+ import_electron2.app.on("activate", () => {
3883
4177
  debug("activate", { alertKey });
3884
4178
  revealPopover();
3885
4179
  });
3886
- import_electron.app.on("second-instance", () => revealPopover());
3887
- import_electron.app.whenReady().then(async () => {
3888
- import_electron.app.dock?.hide();
3889
- settingsFile = (0, import_node_path19.join)(import_electron.app.getPath("userData"), "settings.json");
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
- import_electron.nativeTheme.themeSource = settings.theme;
3892
- if (import_electron.app.isPackaged) import_electron.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
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, import_node_path19.join)(__dirname, "..", "assets", "trayTemplate.png");
3896
- const icon = import_electron.nativeImage.createFromPath(iconPath);
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 import_electron.Tray(icon);
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 (!import_electron.globalShortcut.register(HOTKEY.accelerator, onHotkey)) console.error(`[harlay] could not register ${HOTKEY.accelerator}`);
3931
- if (!import_electron.app.getLoginItemSettings().wasOpenedAtLogin) revealPopover();
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(() => import_electron.app.quit(), 1500);
4229
+ setTimeout(() => import_electron2.app.quit(), 1500);
3936
4230
  });
3937
- import_electron.app.on("will-quit", () => {
3938
- import_electron.globalShortcut.unregisterAll();
4231
+ import_electron2.app.on("will-quit", () => {
4232
+ import_electron2.globalShortcut.unregisterAll();
3939
4233
  watcher?.stop();
3940
4234
  usage?.stop();
3941
4235
  });