@echomem/mcp 1.4.18 → 1.4.19

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.
@@ -102,6 +102,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
102
102
  try {
103
103
  var cfg = await getJson("/config");
104
104
  authUrl = cfg.authUrl || authUrl;
105
+ switchAccountUrl = cfg.switchAccountUrl || switchAccountUrl || authUrl;
105
106
  if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
106
107
  } catch (_) {}
107
108
  await sleep(900);
@@ -172,9 +173,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
172
173
  var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
173
174
  var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
174
175
  setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
175
- var headline = !pendingTrusted
176
- ? "Counting your new conversations"
177
- : (knownDone ? "You are all caught up." : "Echo found " + number(pendN) + " new coding session" + (pendN === 1 ? "" : "s") + ".");
176
+ var headlineHtml = !pendingTrusted
177
+ ? "Counting your <strong>new conversations…</strong>"
178
+ : (knownDone
179
+ ? "You are <strong>all caught up.</strong>"
180
+ : "Echo found <strong>" + esc(number(pendN)) + " new coding session" + (pendN === 1 ? "" : "s") + ".</strong>");
178
181
  var sub = !pendingTrusted
179
182
  ? "Echo is matching your local history against what is already in memory."
180
183
  : (knownDone
@@ -196,14 +199,18 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
196
199
  app.innerHTML =
197
200
  '<div id="exBanner"></div>' +
198
201
  '<section class="readyDecision">' +
202
+ '<p class="utilityEyebrow readyEyebrow">History import / review</p>' +
199
203
  '<div class="readyHero">' +
200
204
  '<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
201
205
  '<div class="exCopy">' +
202
- '<h2 id="exHeadline"></h2>' +
206
+ '<h2 class="siteHeadline" id="exHeadline"></h2>' +
203
207
  '<p class="exSub" id="exSub"></p>' +
204
208
  '</div>' +
205
209
  '</div>' +
206
210
  '<div class="proofStrip" id="exSources"></div>' +
211
+ '<div class="setupAccountSlot" id="setupAccount"></div>' +
212
+ '<div class="setupPlanSlot" id="setupPlan"></div>' +
213
+ '<div class="sessionSelectionSlot" id="sessionSelection"></div>' +
207
214
  '<div class="readyGo">' +
208
215
  '<div class="actions">' +
209
216
  '<button id="migrate" class="primary"></button>' +
@@ -213,21 +220,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
213
220
  '<div class="readyFoot">' +
214
221
  '<details class="readyExplain" id="readyExplain"></details>' +
215
222
  '</div>' +
216
- '</section>';
223
+ '</section>' +
224
+ '<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
217
225
  document.getElementById("migrate").onclick = startMigrate;
218
226
  dashMounted = true;
219
227
  }
220
228
  document.getElementById("exBanner").innerHTML =
221
229
  (statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask Echo to extract anything unprocessed.</div>' : '') +
222
230
  (error ? '<div class="error">' + esc(error) + '</div>' : '');
223
- document.getElementById("exHeadline").textContent = headline;
231
+ document.getElementById("exHeadline").innerHTML = headlineHtml;
224
232
  document.getElementById("exSub").textContent = sub;
225
233
  var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
226
234
  var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
227
235
  var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
228
236
  // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
229
237
  var srcIcon = function (id, fallback) {
230
- return '<span class="pfIcon"><img src="/agent-icons/' + id + '.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="pfFallback">' + fallback + '</span></span>';
238
+ var asset = id === "claude-desktop" ? "claude" : "codex";
239
+ return '<span class="pfIcon"><img src="/hud-assets/' + asset + '.svg" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="pfFallback">' + fallback + '</span></span>';
231
240
  };
232
241
  document.getElementById("exSources").innerHTML = !pendingTrusted
233
242
  ? '<span class="pfNote">Scanning local history&hellip;</span>'
@@ -251,13 +260,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
251
260
  '<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
252
261
  '<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
253
262
  '</div>';
263
+ renderSetupPlan(canExtract);
264
+ renderAccountChoice();
265
+ renderSessionSelection(canExtract);
266
+ if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
254
267
  // While still counting, hide the primary button entirely; the headline + source split
255
268
  // already say it is working, and the ready state should present one clear action.
256
269
  var migrateBtn = document.getElementById("migrate");
257
270
  if (pendingTrusted) {
258
271
  migrateBtn.style.display = "";
259
- migrateBtn.disabled = false;
260
- migrateBtn.textContent = canExtract ? extractLabel : "Finish setup";
272
+ migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
273
+ migrateBtn.innerHTML = '<span>' + esc(canExtract ? extractLabel : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
261
274
  migrateBtn.onclick = canExtract ? startMigrate : skip;
262
275
  } else {
263
276
  migrateBtn.style.display = "none";
@@ -265,6 +278,16 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
265
278
  }
266
279
  async function startMigrate() {
267
280
  if (decisionMade) return;
281
+ if (!setupPlanConfirmed()) {
282
+ setSetupPlanStatus("Choose Free, Pro, or Power before extraction starts.");
283
+ return;
284
+ }
285
+ var selectedKeys = selectedSessionKeyList();
286
+ if (!selectedKeys.length) {
287
+ renderSessionSelection(true, "Choose at least one conversation to extract.");
288
+ return;
289
+ }
290
+ stopBillingPoll();
268
291
  decisionMade = true;
269
292
  extractionEnded = false;
270
293
  setCityMode(false);
@@ -272,7 +295,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
272
295
  app.className = "";
273
296
  app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically. Finished work is saved as it goes.</p><div class="miniBar"><i></i></div></div>';
274
297
  try {
275
- var started = await postJson("/migrate", {});
298
+ var started = await postJson("/migrate", { conversationKeys: selectedKeys });
276
299
  extractMounted = false;
277
300
  renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
278
301
  await pollProgress();
@@ -282,10 +305,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
282
305
  }
283
306
  }
284
307
  async function pollProgress() {
308
+ var runId = ++progressPollRunId;
285
309
  for (;;) {
286
- if (extractionEnded) return;
310
+ if (extractionEnded || runId !== progressPollRunId) return;
287
311
  var progress = await getJson("/progress");
288
- if (extractionEnded) return;
312
+ if (extractionEnded || runId !== progressPollRunId) return;
289
313
  renderProgress(progress);
290
314
  if (progress.status === "completed" || progress.status === "failed") return;
291
315
  await sleep(1200);
@@ -293,6 +317,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
293
317
  }
294
318
  function renderProgress(progress) {
295
319
  if (extractionEnded) return;
320
+ lastExtractionProgress = progress;
296
321
  setCityMode(false);
297
322
  setExtractMode(true);
298
323
  var status = progress.status;
@@ -330,19 +355,22 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
330
355
  var stopDetail = String(progress.error) === "key-expired"
331
356
  ? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
332
357
  : esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
358
+ var completionNote = done && progress.latest && progress.latest !== "Import complete."
359
+ ? '<p class="exSub">' + esc(progress.latest) + '</p>'
360
+ : "";
333
361
  t.innerHTML =
334
362
  '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
335
- (done ? "" : '<p class="kicker-mono">Extraction stopped</p>') +
363
+ (done ? '<p class="utilityEyebrow">Memory extraction / complete</p>' : '<p class="utilityEyebrow">Memory extraction / stopped</p>') +
336
364
  '<div class="resultTop">' +
337
365
  '<div>' +
338
366
  (done
339
- ? '<h2 class="savedHeading"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span><a class="viewEchoLink" href="' + timelineUrl + '" target="_blank" rel="noopener noreferrer">(view in yeahecho)</a></h2>'
340
- : '<h2>Some conversations need another pass.</h2>' +
367
+ ? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span><a class="viewEchoLink" href="' + timelineUrl + '" target="_blank" rel="noopener noreferrer">(view in yeahecho)</a></h2>' + completionNote
368
+ : '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
341
369
  '<p class="exSub">' + stopDetail + '</p>' +
342
370
  '<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
343
371
  '</div>' +
344
372
  '</div>' +
345
- (done ? recallGateBlock() : "") +
373
+ (done ? tryItCard() : "") +
346
374
  (done ? "" :
347
375
  '<div class="memoryReceipt">' +
348
376
  '<p class="receiptK">What just happened</p>' +
@@ -353,7 +381,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
353
381
  '</div>') +
354
382
  '</div>' +
355
383
  (done ? hudTip() + doneCloseNote() : "");
356
- wireRecallGate();
384
+ wireTryButtons();
357
385
  return;
358
386
  }
359
387
  setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
@@ -379,9 +407,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
379
407
  countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
380
408
  }
381
409
  }
382
- t.innerHTML =
410
+ var recoveryNotice = extractionRecoveryMessage
411
+ ? '<div class="error">' + esc(extractionRecoveryMessage) + '</div>'
412
+ : "";
413
+ t.innerHTML = recoveryNotice +
383
414
  '<div class="workbench">' +
384
- '<h2>' + (preparing ? "Setting up the import." : "Building your memory.") + '</h2>' +
415
+ '<p class="utilityEyebrow">Memory extraction / live</p>' +
416
+ '<h2 class="siteHeadline">' + (preparing ? "Setting up the <strong>import.</strong>" : "Building your <strong>memory.</strong>") + '</h2>' +
385
417
  '<div class="exMeter"><strong>' + (preparing ? "Local" : esc(number(pct)) + "%") + '</strong><span>' + (preparing ? "sizing sessions" : (total ? esc(number(completed)) + " of " + esc(number(total)) + " conversations" : "starting")) + '</span></div>' +
386
418
  '<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
387
419
  '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
@@ -399,15 +431,75 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
399
431
  if (m >= 60) { var h = Math.floor(m / 60); return h + "h " + (m % 60) + "m"; }
400
432
  return m > 0 ? m + "m " + s + "s" : s + "s";
401
433
  }
402
- function recallPrompt() {
403
- return "Warm up from EchoMem: call search_memories for what I have been working on recently, then give me a 5-line recap of my current projects and where each one stands, so we can pick up right where I left off. If this is Codex or Claude Code, open the EchoMem context HUD by running echomem-hud app. Use the HUD's context-health score to tell me when to renew into a fresh session and carry a checkpoint forward. As we work, when we make a decision, finish a fix, or hit a milestone, save it with save_conversation. Use this prompt in Codex, Claude Code, or Claude Desktop; if you are in another chat surface, tell me EchoMem tools may not be available here.";
434
+ function cleanRecallTopic(value) {
435
+ var topic = String(value || "")
436
+ .replace(/\s+/g, " ")
437
+ .replace(/\s+[·|]\s+\d+$/, "")
438
+ .trim();
439
+ if (!topic || topic.length < 3) return "";
440
+ if (topic.length > 56) topic = topic.slice(0, 53).trim() + "...";
441
+ var generic = {
442
+ "no project detected": true,
443
+ "untitled coding session": true,
444
+ "project": true,
445
+ "projects": true,
446
+ "coding session": true,
447
+ "coding sessions": true
448
+ };
449
+ return generic[topic.toLowerCase()] ? "" : topic;
450
+ }
451
+ function recallTopics() {
452
+ var selected = Object.create(null);
453
+ selectedSessionKeyList().forEach(function (key) { selected[key] = true; });
454
+ var sessions = candidateSessions().filter(function (session) {
455
+ return !!selected[String(session.key || "")];
456
+ });
457
+ if (!sessions.length) sessions = candidateSessions();
458
+
459
+ var scored = Object.create(null);
460
+ function addTopic(value, base, recency) {
461
+ var topic = cleanRecallTopic(value);
462
+ if (!topic) return;
463
+ var key = topic.toLowerCase();
464
+ if (!scored[key]) scored[key] = { topic: topic, score: 0 };
465
+ scored[key].score += base + recency;
466
+ }
467
+ sessions.forEach(function (session, index) {
468
+ var recency = Math.max(1, sessions.length - index);
469
+ addTopic(session.project, 100, recency);
470
+ addTopic(session.title, 20, recency);
471
+ });
472
+ return Object.keys(scored)
473
+ .map(function (key) { return scored[key]; })
474
+ .sort(function (a, b) { return b.score - a.score || a.topic.localeCompare(b.topic); })
475
+ .slice(0, 3)
476
+ .map(function (entry) { return entry.topic; });
477
+ }
478
+ function recallQuestions() {
479
+ var topics = recallTopics();
480
+ var templates = [
481
+ "What do I know about {x}?",
482
+ "Catch me up on {x}.",
483
+ "Where did I leave off with {x}?"
484
+ ];
485
+ var questions = topics.map(function (topic, index) {
486
+ return templates[index].replace("{x}", topic);
487
+ });
488
+ var fallbacks = [
489
+ "What have I been working on recently?",
490
+ "What decisions and next steps should I pick up?",
491
+ "What from my recent work needs attention next?"
492
+ ];
493
+ fallbacks.forEach(function (question) {
494
+ if (questions.length < 3) questions.push(question);
495
+ });
496
+ return questions.slice(0, 3);
404
497
  }
405
498
  function setLaunchStatus(message) {
406
499
  var node = document.getElementById("launchStatus");
407
500
  if (node) node.textContent = message || "";
408
501
  }
409
- async function copyRecallPrompt() {
410
- var prompt = recallPrompt();
502
+ async function copyRecallPrompt(prompt) {
411
503
  try {
412
504
  await navigator.clipboard.writeText(prompt);
413
505
  return true;
@@ -415,162 +507,432 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
415
507
  return false;
416
508
  }
417
509
  }
418
- async function copyPromptOnly() {
419
- var copied = await copyRecallPrompt();
420
- var copyBtn = document.getElementById("tryCopy");
510
+ async function copyPromptOnly(prompt, copyBtn) {
511
+ var copied = await copyRecallPrompt(prompt);
421
512
  if (copyBtn && copied) {
513
+ var original = copyBtn.innerHTML;
422
514
  copyBtn.textContent = "Copied";
423
- window.setTimeout(function () { copyBtn.textContent = "Copy prompt"; }, 1600);
515
+ window.setTimeout(function () { copyBtn.innerHTML = original; }, 1600);
424
516
  }
425
- setLaunchStatus(copied ? "Prompt copied." : "Could not copy automatically. Select the prompt and copy it.");
517
+ setLaunchStatus(copied ? "Question copied." : "Could not copy the question automatically.");
426
518
  }
427
519
  function codexDeepLink(prompt) {
428
520
  var q = "prompt=" + encodeURIComponent(prompt);
429
521
  if (workspacePath) q += "&path=" + encodeURIComponent(workspacePath);
430
522
  return "codex://new?" + q;
431
523
  }
432
- async function tryInCodex() {
433
- var prompt = recallPrompt();
434
- var copied = await copyRecallPrompt();
435
- setLaunchStatus(copied ? "Copied prompt. Opening Codex..." : "Opening Codex. If the prompt is empty, copy it manually.");
524
+ async function tryInCodex(prompt) {
525
+ var copied = await copyRecallPrompt(prompt);
526
+ setLaunchStatus(copied ? "Copied question. Opening Codex..." : "Opening Codex with your question...");
436
527
  window.location.href = codexDeepLink(prompt);
437
528
  }
438
- async function openClaudeDesktop() {
439
- var copied = await copyRecallPrompt();
440
- setLaunchStatus(copied ? "Copied prompt. Opening Claude Desktop..." : "Opening Claude Desktop. If the prompt is empty, copy it manually.");
529
+ async function openClaudeDesktop(prompt) {
530
+ var copied = await copyRecallPrompt(prompt);
531
+ setLaunchStatus(copied ? "Copied question. Opening Claude Desktop..." : "Opening Claude Desktop...");
441
532
  try {
442
533
  var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
443
- setLaunchStatus(res && res.message ? res.message : "Prompt copied. Paste it into Claude Desktop.");
534
+ setLaunchStatus(res && res.message ? res.message : "Question copied. Paste it into Claude Desktop.");
444
535
  } catch (error) {
445
536
  var msg = error && error.message ? error.message : "Could not auto-open Claude.";
446
- if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The prompt is copied - open Claude Desktop and paste it.";
537
+ if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The question is copied - open Claude Desktop and paste it.";
447
538
  setLaunchStatus(msg);
448
539
  }
449
540
  }
541
+ function candidateSessions() {
542
+ return stats && Array.isArray(stats.candidateSessions) ? stats.candidateSessions : [];
543
+ }
544
+ function historicalSelectionLimit() {
545
+ if (!setupPlanConfirmed()) return 0;
546
+ var quota = billingStatus && billingStatus.historicalConversationQuota;
547
+ if (quota && typeof quota.remaining === "number" && isFinite(quota.remaining)) {
548
+ return Math.max(0, Math.floor(quota.remaining));
549
+ }
550
+ var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
551
+ if (plan === "power" || plan === "enterprise") return 500;
552
+ if (paidRecallPlan(plan)) return 250;
553
+ return 100;
554
+ }
555
+ function selectedSessionKeyList() {
556
+ var valid = Object.create(null);
557
+ candidateSessions().forEach(function (session) { valid[String(session.key || "")] = true; });
558
+ return Object.keys(selectedSessionKeys).filter(function (key) { return selectedSessionKeys[key] && valid[key]; });
559
+ }
560
+ function ensureSessionSelection() {
561
+ var candidates = candidateSessions();
562
+ var valid = Object.create(null);
563
+ candidates.forEach(function (session) { valid[String(session.key || "")] = true; });
564
+ Object.keys(selectedSessionKeys).forEach(function (key) {
565
+ if (!valid[key]) delete selectedSessionKeys[key];
566
+ });
567
+ if (!setupPlanConfirmed()) return;
568
+ var limit = Math.min(candidates.length, historicalSelectionLimit());
569
+ if (!sessionSelectionTouched) {
570
+ selectedSessionKeys = Object.create(null);
571
+ candidates.slice(0, limit).forEach(function (session) { selectedSessionKeys[String(session.key)] = true; });
572
+ return;
573
+ }
574
+ var selected = selectedSessionKeyList();
575
+ if (selected.length > limit) {
576
+ var keep = Object.create(null);
577
+ var selectedSet = Object.create(null);
578
+ selected.forEach(function (key) { selectedSet[key] = true; });
579
+ candidates.forEach(function (session) {
580
+ var key = String(session.key || "");
581
+ if (selectedSet[key] && Object.keys(keep).length < limit) keep[key] = true;
582
+ });
583
+ selectedSessionKeys = keep;
584
+ }
585
+ }
586
+ function sessionSourceLabel(source) {
587
+ return source === "claude-code" ? "Claude Code" : "Codex";
588
+ }
589
+ function sessionDateLabel(value) {
590
+ if (!value) return "Date unavailable";
591
+ var date = new Date(value);
592
+ if (!isFinite(date.getTime())) return "Date unavailable";
593
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
594
+ }
595
+ function renderSessionSelection(canExtract, message) {
596
+ var slot = document.getElementById("sessionSelection");
597
+ if (!slot) return;
598
+ if (!canExtract || !setupPlanConfirmed()) {
599
+ slot.innerHTML = "";
600
+ slot.classList.add("is-hidden");
601
+ return;
602
+ }
603
+ slot.classList.remove("is-hidden");
604
+ var candidates = candidateSessions();
605
+ if (!candidates.length) {
606
+ slot.innerHTML = '<div class="sessionSelectionCard is-loading"><span class="sessionSelectionIcon" aria-hidden="true">' + setupIcon("list-check") + '</span><div><strong>Preparing your conversation list…</strong><p>Echo is reading titles and project names locally so you can review the selection.</p></div></div>';
607
+ return;
608
+ }
609
+ ensureSessionSelection();
610
+ var selected = selectedSessionKeyList();
611
+ var limit = Math.min(candidates.length, historicalSelectionLimit());
612
+ var planLimited = candidates.length > limit;
613
+ var summary = planLimited
614
+ ? "Your plan can import " + number(limit) + " of " + number(candidates.length) + " available conversations."
615
+ : "All " + number(candidates.length) + " available conversations are included by default.";
616
+ slot.innerHTML =
617
+ '<section class="sessionSelectionCard" aria-labelledby="sessionSelectionTitle">' +
618
+ '<span class="sessionSelectionIcon" aria-hidden="true">' + setupIcon("list-check") + '</span>' +
619
+ '<div class="sessionSelectionCopy"><span class="sessionSelectionEyebrow">Conversations to import</span><strong id="sessionSelectionTitle">' + esc(number(selected.length)) + ' selected</strong><p>' + esc(message || summary) + '</p></div>' +
620
+ '<button type="button" class="secondary sessionEditButton" id="editSessions">Review &amp; edit</button>' +
621
+ '</section>';
622
+ var edit = document.getElementById("editSessions");
623
+ if (edit) edit.onclick = openSessionPicker;
624
+ var migrateBtn = document.getElementById("migrate");
625
+ if (migrateBtn) migrateBtn.disabled = selected.length === 0 || !setupPlanConfirmed();
626
+ }
627
+ function updateSessionPickerSelectionUi(dialog, input, limit) {
628
+ var selectedCount = selectedSessionKeyList().length;
629
+ var row = input && input.closest ? input.closest(".sessionPickerRow") : null;
630
+ if (row) row.classList.toggle("is-selected", !!input.checked);
631
+ var count = dialog.querySelector(".sessionPickerActions strong");
632
+ if (count) count.textContent = number(selectedCount) + " / " + number(limit) + " selected";
633
+ var status = dialog.querySelector("#sessionPickerStatus");
634
+ if (status) status.textContent = "";
635
+ var confirm = dialog.querySelector("#confirmSessions");
636
+ if (confirm) {
637
+ confirm.disabled = selectedCount === 0;
638
+ confirm.textContent = "Use " + number(selectedCount) + " selected";
639
+ }
640
+ }
641
+ function renderSessionPicker() {
642
+ var dialog = document.getElementById("sessionPicker");
643
+ if (!dialog) return;
644
+ ensureSessionSelection();
645
+ var all = candidateSessions();
646
+ var selected = selectedSessionKeyList();
647
+ var limit = Math.min(all.length, historicalSelectionLimit());
648
+ var query = sessionPickerQuery.trim().toLowerCase();
649
+ var filtered = all.filter(function (session) {
650
+ if (sessionPickerSource !== "all" && session.source !== sessionPickerSource) return false;
651
+ if (!query) return true;
652
+ return [session.title, session.project, session.source, session.date].join(" ").toLowerCase().indexOf(query) >= 0;
653
+ });
654
+ var visible = filtered.slice(0, 250);
655
+ var rows = visible.map(function (session) {
656
+ var key = String(session.key || "");
657
+ var encodedKey = encodeURIComponent(key);
658
+ var checked = !!selectedSessionKeys[key];
659
+ var sourceIcon = session.source === "claude-code" ? "claude" : "codex";
660
+ var sourceFallback = session.source === "claude-code" ? "CL" : "CX";
661
+ return '<label class="sessionPickerRow' + (checked ? " is-selected" : "") + '">' +
662
+ '<input type="checkbox" data-session-key="' + esc(encodedKey) + '"' + (checked ? " checked" : "") + ' />' +
663
+ '<span class="sessionPickerSource ' + (session.source === "claude-code" ? "is-claude" : "is-codex") + '"><img src="/hud-assets/' + sourceIcon + '.svg" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="sessionPickerSourceFallback">' + sourceFallback + '</span></span>' +
664
+ '<span class="sessionPickerMain"><strong>' + esc(session.title || "Untitled coding session") + '</strong><small>' + esc(session.project || "No project detected") + ' · ' + esc(sessionDateLabel(session.date)) + '</small></span>' +
665
+ '<span class="sessionPickerMeta">' + esc(compact(Number(session.approxInputTokens) || 0)) + ' tok</span>' +
666
+ '</label>';
667
+ }).join("");
668
+ var overflow = filtered.length > visible.length
669
+ ? '<p class="sessionPickerOverflow">Showing the first 250 matches. Narrow the search to find older conversations.</p>'
670
+ : "";
671
+ dialog.innerHTML =
672
+ '<div class="sessionPickerShell">' +
673
+ '<header class="sessionPickerHead"><div><span>Bulk history import</span><h2 id="sessionPickerTitle">Choose conversations</h2><p>Select up to ' + esc(number(limit)) + '. Newest conversations are selected first.</p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
674
+ '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
675
+ '<label class="sessionSourceFilter"><span>Source</span><select id="sessionPickerSource"><option value="all"' + (sessionPickerSource === "all" ? " selected" : "") + '>All sources</option><option value="codex"' + (sessionPickerSource === "codex" ? " selected" : "") + '>Codex</option><option value="claude-code"' + (sessionPickerSource === "claude-code" ? " selected" : "") + '>Claude Code</option></select></label>' +
676
+ '</div>' +
677
+ '<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" role="status"></span><button type="button" class="textButton" id="selectNewestSessions">Select newest</button><button type="button" class="textButton" id="clearSessions">Clear</button></div>' +
678
+ '<div class="sessionPickerList">' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + '</div>' + overflow +
679
+ '<footer class="sessionPickerFoot"><p>Only selected conversations will be sent for extraction.</p><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Use ' + esc(number(selected.length)) + ' selected</button></footer>' +
680
+ '</div>';
681
+ var close = document.getElementById("closeSessionPicker");
682
+ var confirm = document.getElementById("confirmSessions");
683
+ if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
684
+ if (confirm) confirm.onclick = function () { dialog.close(); renderSessionSelection(true); };
685
+ var search = document.getElementById("sessionPickerSearch");
686
+ if (search) search.oninput = function () {
687
+ sessionPickerQuery = search.value || "";
688
+ renderSessionPicker();
689
+ var next = document.getElementById("sessionPickerSearch");
690
+ if (next) { next.focus(); next.setSelectionRange(next.value.length, next.value.length); }
691
+ };
692
+ var source = document.getElementById("sessionPickerSource");
693
+ if (source) source.onchange = function () { sessionPickerSource = source.value || "all"; renderSessionPicker(); };
694
+ Array.prototype.forEach.call(dialog.querySelectorAll("[data-session-key]"), function (input) {
695
+ input.onchange = function () {
696
+ var key = decodeURIComponent(input.getAttribute("data-session-key") || "");
697
+ var count = selectedSessionKeyList().length;
698
+ if (input.checked && !selectedSessionKeys[key] && count >= limit) {
699
+ input.checked = false;
700
+ var status = document.getElementById("sessionPickerStatus");
701
+ if (status) status.textContent = "Plan limit reached";
702
+ return;
703
+ }
704
+ sessionSelectionTouched = true;
705
+ if (input.checked) selectedSessionKeys[key] = true;
706
+ else delete selectedSessionKeys[key];
707
+ // Keep the existing list DOM so selecting a row does not reset its scroll position.
708
+ updateSessionPickerSelectionUi(dialog, input, limit);
709
+ };
710
+ });
711
+ var newest = document.getElementById("selectNewestSessions");
712
+ if (newest) newest.onclick = function () {
713
+ sessionSelectionTouched = true;
714
+ selectedSessionKeys = Object.create(null);
715
+ all.slice(0, limit).forEach(function (session) { selectedSessionKeys[String(session.key)] = true; });
716
+ renderSessionPicker();
717
+ };
718
+ var clear = document.getElementById("clearSessions");
719
+ if (clear) clear.onclick = function () { sessionSelectionTouched = true; selectedSessionKeys = Object.create(null); renderSessionPicker(); };
720
+ }
721
+ function openSessionPicker() {
722
+ var dialog = document.getElementById("sessionPicker");
723
+ if (!dialog) return;
724
+ renderSessionPicker();
725
+ if (typeof dialog.showModal === "function") dialog.showModal();
726
+ else dialog.setAttribute("open", "");
727
+ }
728
+ function accountInitials(account) {
729
+ var label = String(account && (account.displayName || account.email) || "EchoMem").trim();
730
+ return label.split(/\s+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
731
+ }
732
+ function renderAccountChoice() {
733
+ var slot = document.getElementById("setupAccount");
734
+ if (!slot) return;
735
+ var account = billingStatus && billingStatus.account;
736
+ var name = account && account.displayName ? account.displayName : "Connected EchoMem account";
737
+ var email = account && account.email ? account.email : "This import will be saved to the account connected on this device.";
738
+ var avatarUrl = account && account.avatarUrl ? String(account.avatarUrl) : "";
739
+ var avatar = '<span class="setupAccountAvatar" aria-hidden="true">' +
740
+ '<span>' + esc(accountInitials(account)) + '</span>' +
741
+ (avatarUrl ? '<img src="' + esc(avatarUrl) + '" alt="" onerror="this.remove()" />' : '') +
742
+ '</span>';
743
+ slot.innerHTML =
744
+ '<section class="setupAccountCard" aria-label="Connected EchoMem account">' +
745
+ avatar +
746
+ '<span class="setupAccountCopy"><small>Saving to</small><strong>' + esc(name) + '</strong><span>' + esc(email) + '</span></span>' +
747
+ '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Change account</button>' +
748
+ '</section>';
749
+ var change = document.getElementById("changeSetupAccount");
750
+ if (change) change.onclick = switchSetupAccount;
751
+ }
752
+ async function switchSetupAccount() {
753
+ var button = document.getElementById("changeSetupAccount");
754
+ if (button) { button.disabled = true; button.textContent = "Opening account chooser…"; }
755
+ try {
756
+ await postJson("/logout", {}, 12000);
757
+ stats = null;
758
+ billingStatus = null;
759
+ connected = false;
760
+ selectedSessionKeys = Object.create(null);
761
+ sessionSelectionTouched = false;
762
+ if (!openAuthWindow(switchAccountUrl || authUrl)) {
763
+ if (button) { button.disabled = false; button.textContent = "Change account"; }
764
+ showPopupFallback("Your browser blocked the account chooser. Open it below to continue.", switchAccountUrl || authUrl);
765
+ return;
766
+ }
767
+ startConnectionPoll();
768
+ } catch (error) {
769
+ if (button) { button.disabled = false; button.textContent = "Change account"; }
770
+ setSetupPlanStatus(error && error.message ? error.message : "Could not open the account chooser.");
771
+ }
772
+ }
450
773
  function paidRecallPlan(plan) {
451
774
  return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
452
775
  }
453
- function recallPricingUrl() {
776
+ function setupPricingUrl(plan, startTrial) {
777
+ var paidPlan = plan === "power" ? "power" : "pro";
454
778
  return (billingStatus && billingStatus.pricingUrl)
455
- || "https://echoknows.com/pricing?source=mcp_onboarding";
456
- }
457
- function recallGateUnlocked() {
458
- return recallGateSkipped || paidRecallPlan(billingStatus && billingStatus.plan);
459
- }
460
- function recallTrialAvailable() {
461
- return !billingStatus || billingStatus.trialAvailable !== false;
462
- }
463
- function recallGateBlock() {
464
- var unlocked = recallGateUnlocked();
465
- var trialAvailable = recallTrialAvailable();
466
- return '<div class="recallGateBlock">' +
467
- '<section class="recallGate' + (unlocked ? " is-hidden" : "") + '" id="recallGate">' +
468
- '<p class="recallEyebrow"><i></i>Premium recall</p>' +
469
- '<h3 id="recallGateTitle">' + (trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?") + '</h3>' +
470
- '<p id="recallGateBody">' + (trialAvailable ? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way." : "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.") + '</p>' +
471
- '<div class="recallTrial' + (trialAvailable ? "" : " is-hidden") + '" id="recallTrialBadge" aria-label="14 days free"><strong>14</strong><span>days free<br>then choose what fits</span></div>' +
472
- '<p class="recallUsedNote' + (trialAvailable ? " is-hidden" : "") + '" id="recallUsedNote">Your trial was already used. You can still unlock recall by choosing Pro or Power.</p>' +
473
- '<div class="recallGateActions">' +
474
- '<button type="button" class="primary" id="recallStartTrial">' + (trialAvailable ? "Compare Pro and Power" : "See pricing") + '</button>' +
475
- '<button type="button" class="textButton" id="recallSkip">Skip recall for now</button>' +
476
- '</div>' +
477
- '<p class="recallFine" id="recallFine">' + (trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.") + '</p>' +
478
- '<p class="launchStatus" id="recallGateStatus"></p>' +
479
- '</section>' +
480
- '<div id="recallTryWrap" class="recallTryWrap' + (unlocked ? "" : " is-hidden") + '">' +
481
- '<p id="recallSkipNote" class="recallSkipNote' + (recallGateSkipped ? "" : " is-hidden") + '">Saving will keep working. Search and recall require Pro or Power when this prompt calls <code>search_memories</code>.</p>' +
482
- tryItCard() +
483
- '</div>' +
484
- '</div>';
779
+ ? billingStatus.pricingUrl + (startTrial ? "&start=" + paidPlan + "&trial=" + (billingStatus.trialAvailable === false ? "0" : "1") : "")
780
+ : "https://echoknows.com/pricing?source=mcp_onboarding" + (startTrial ? "&start=" + paidPlan + "&trial=1" : "");
485
781
  }
486
- function setRecallGateStatus(message) {
487
- var node = document.getElementById("recallGateStatus");
782
+ function setupPlanConfirmed() {
783
+ return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
784
+ }
785
+ function setSetupPlanStatus(message) {
786
+ var node = document.getElementById("setupPlanStatus");
488
787
  if (node) node.textContent = message || "";
489
788
  }
490
- function applyRecallGate() {
491
- var gate = document.getElementById("recallGate");
492
- var wrap = document.getElementById("recallTryWrap");
493
- var skipNote = document.getElementById("recallSkipNote");
494
- var unlocked = recallGateUnlocked();
495
- if (gate) gate.classList.toggle("is-hidden", unlocked);
496
- if (wrap) wrap.classList.toggle("is-hidden", !unlocked);
497
- if (skipNote) skipNote.classList.toggle("is-hidden", !recallGateSkipped);
498
- var trialAvailable = recallTrialAvailable();
499
- var badge = document.getElementById("recallTrialBadge");
500
- if (badge) badge.classList.toggle("is-hidden", !trialAvailable);
501
- var usedNote = document.getElementById("recallUsedNote");
502
- if (usedNote) usedNote.classList.toggle("is-hidden", trialAvailable);
503
- var title = document.getElementById("recallGateTitle");
504
- if (title) title.textContent = trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?";
505
- var body = document.getElementById("recallGateBody");
506
- if (body) body.textContent = trialAvailable
507
- ? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way."
508
- : "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.";
509
- var cta = document.getElementById("recallStartTrial");
510
- if (cta) cta.textContent = trialAvailable ? "Compare Pro and Power" : "See pricing";
511
- var fine = document.getElementById("recallFine");
512
- if (fine) fine.textContent = trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.";
789
+ function renderSetupPlan(canExtract) {
790
+ var slot = document.getElementById("setupPlan");
791
+ if (!slot) return;
792
+ if (!canExtract) {
793
+ slot.innerHTML = "";
794
+ slot.classList.add("is-hidden");
795
+ return;
796
+ }
797
+ slot.classList.remove("is-hidden");
798
+ var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
799
+ var paid = paidRecallPlan(plan);
800
+ var migrateBtn = document.getElementById("migrate");
801
+ if (paid) {
802
+ setupPlanChoice = "paid";
803
+ var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
804
+ var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
805
+ var paidUsage = paidHistory && typeof paidHistory.used === "number" && typeof paidHistory.limit === "number"
806
+ ? paidHistory.used + " / " + paidHistory.limit + " bulk history imports used."
807
+ : "Your bulk history import and weekly new-chat allowance are ready.";
808
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + esc(planLabel) + ' is active.</strong><span>' + esc(paidUsage) + '</span></span></div>';
809
+ renderSessionSelection(true);
810
+ stopBillingPoll();
811
+ return;
812
+ }
813
+ if (setupPlanChoice === "free") {
814
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Free selected.</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 10 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
815
+ var changeBtn = document.getElementById("changeSetupPlan");
816
+ if (changeBtn) changeBtn.onclick = function () { setupPlanChoice = ""; renderSetupPlan(true); renderSessionSelection(true); };
817
+ renderSessionSelection(true);
818
+ return;
819
+ }
820
+ var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
821
+ slot.innerHTML =
822
+ '<section class="setupPlanChoice" aria-labelledby="setupPlanHeading">' +
823
+ '<div class="setupPlanIntro"><h3 id="setupPlanHeading">Choose a plan before extraction starts.</h3><p>Plans set how much history you can import and how much new conversation text Echo processes each week. There is no memory-count limit.</p></div>' +
824
+ '<div class="setupPlanGrid">' +
825
+ '<article class="setupPlanCard">' +
826
+ '<div class="setupPlanHead"><strong>Free</strong><span>$0 forever</span></div>' +
827
+ '<p>Start with the essentials.</p>' +
828
+ '<ul><li>' + setupIcon("check") + '<span>100 bulk history imports</span></li><li>' + setupIcon("check") + '<span>25K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>10 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
829
+ '<button type="button" class="secondary" id="chooseFreePlan">Continue with Free</button>' +
830
+ '</article>' +
831
+ '<article class="setupPlanCard is-pro">' +
832
+ '<div class="setupPlanHead"><strong>Pro</strong><span>$40 / month</span></div>' +
833
+ '<p>More recall for serious work.</p>' +
834
+ '<ul><li>' + setupIcon("check") + '<span>250 bulk history imports</span></li><li>' + setupIcon("check") + '<span>200K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>100 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
835
+ '<button type="button" class="primary" id="chooseProPlan">' + (trialAvailable ? "Start 14-day trial" : "Choose Pro") + '</button>' +
836
+ '<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
837
+ '</article>' +
838
+ '<article class="setupPlanCard">' +
839
+ '<div class="setupPlanHead"><strong>Power</strong><span>$100 / month</span></div>' +
840
+ '<p>Higher limits for people who use Echo every day.</p>' +
841
+ // Future Power feature: API and automation access.
842
+ '<ul><li>' + setupIcon("check") + '<span>500 bulk history imports</span></li><li>' + setupIcon("check") + '<span>750K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>250 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
843
+ '<button type="button" class="secondary" id="choosePowerPlan">' + (trialAvailable ? "Start 14-day trial" : "Choose Power") + '</button>' +
844
+ '<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
845
+ '</article>' +
846
+ '</div>' +
847
+ '<div class="setupPlanFoot"><a href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">See all plan details</a><span id="setupPlanStatus" role="status"></span></div>' +
848
+ '</section>';
849
+ var freeBtn = document.getElementById("chooseFreePlan");
850
+ if (freeBtn) freeBtn.onclick = function () { setupPlanChoice = "free"; renderSetupPlan(true); renderSessionSelection(true); };
851
+ var proBtn = document.getElementById("chooseProPlan");
852
+ if (proBtn) proBtn.onclick = function () { openPaidPricing("pro"); };
853
+ var powerBtn = document.getElementById("choosePowerPlan");
854
+ if (powerBtn) powerBtn.onclick = function () { openPaidPricing("power"); };
855
+ if (migrateBtn) migrateBtn.disabled = true;
856
+ renderSessionSelection(true);
513
857
  }
514
858
  async function refreshBillingStatus() {
859
+ if (billingStatusLoading) return;
860
+ billingStatusLoading = true;
515
861
  try {
516
862
  billingStatus = await getJson("/billing-status");
517
- applyRecallGate();
518
- if (!recallGateUnlocked() && billingStatus && billingStatus.plan === "unknown") {
519
- setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
520
- }
863
+ renderSetupPlan(true);
864
+ renderAccountChoice();
865
+ renderSessionSelection(true);
866
+ if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Free still works.");
521
867
  } catch (_) {
522
- setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
868
+ setSetupPlanStatus("Plan check unavailable. Free still works.");
869
+ } finally {
870
+ billingStatusLoading = false;
523
871
  }
524
872
  }
525
- function openRecallPricing() {
526
- var url = recallPricingUrl();
527
- setRecallGateStatus("Opening pricing in a new tab...");
873
+ function refreshBillingOnFocus() { void refreshBillingStatus(); }
874
+ function startBillingPoll() {
875
+ if (billingPollTimer) return;
876
+ window.addEventListener("focus", refreshBillingOnFocus);
877
+ billingPollTimer = window.setInterval(function () { void refreshBillingStatus(); }, 2500);
878
+ }
879
+ function stopBillingPoll() {
880
+ if (billingPollTimer) window.clearInterval(billingPollTimer);
881
+ billingPollTimer = null;
882
+ window.removeEventListener("focus", refreshBillingOnFocus);
883
+ }
884
+ function openPaidPricing(plan) {
885
+ var url = setupPricingUrl(plan, true);
886
+ setSetupPlanStatus("Opening secure checkout…");
528
887
  try {
529
- var opened = window.open(url, "_blank", "noopener,noreferrer");
530
- if (!opened) setRecallGateStatus("Your browser blocked the pricing tab. Allow pop-ups for this page, then click Compare plans again.");
888
+ var opened = window.open(url, "_blank");
889
+ if (!opened) {
890
+ setSetupPlanStatus("Your browser blocked checkout. Allow pop-ups, then try again.");
891
+ return;
892
+ }
893
+ try { opened.opener = null; } catch (_) {}
894
+ startBillingPoll();
531
895
  } catch (_) {
532
- setRecallGateStatus("Could not open a new tab. Allow pop-ups for this page, then click Compare plans again.");
896
+ setSetupPlanStatus("Could not open checkout. Allow pop-ups, then try again.");
533
897
  }
534
898
  }
535
- function skipRecallGate() {
536
- recallGateSkipped = true;
537
- applyRecallGate();
538
- }
539
- function wireRecallGate() {
540
- var trialBtn = document.getElementById("recallStartTrial");
541
- if (trialBtn) trialBtn.addEventListener("click", openRecallPricing);
542
- var skipBtn = document.getElementById("recallSkip");
543
- if (skipBtn) skipBtn.addEventListener("click", skipRecallGate);
544
- wireTryButtons();
545
- void refreshBillingStatus();
546
- }
547
899
  function tryItCard() {
548
- var prompt = recallPrompt();
549
- return '<div class="tryCard">' +
550
- '<h3 class="tryTitle">Echo MCP now available to use</h3>' +
551
- '<div class="promptShell">' +
552
- '<div class="promptControls">' +
553
- '<button type="button" id="tryCopy" class="promptCopyLabel">Copy prompt</button>' +
900
+ var questions = recallQuestions();
901
+ var prompts = questions.map(function (question, index) {
902
+ var numberLabel = String(index + 1).padStart(2, "0");
903
+ return '<article class="recallPrompt" data-recall-prompt="' + esc(question) + '">' +
904
+ '<div class="recallPromptHead">' +
905
+ '<span>Suggested prompt ' + numberLabel + '</span>' +
906
+ '<button type="button" class="recallCopy" data-recall-copy aria-label="Copy suggested prompt ' + (index + 1) + '">' + setupIcon("copy") + '<span>Copy</span></button>' +
554
907
  '</div>' +
555
- '<pre id="tryPrompt">' + esc(prompt) + '</pre>' +
556
- '</div>' +
557
- '<div class="agentLaunch" aria-label="Open in a coding agent">' +
558
- '<button type="button" id="tryCodex"><span class="agentIcon"><img src="/agent-icons/codex.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="agentFallback">CX</span></span><span class="agentText"><strong>Try in Codex</strong><small>copy prompt + open session</small></span></button>' +
559
- '<button type="button" id="tryClaudeDesktop"><span class="agentIcon"><img src="/agent-icons/claude-desktop.png" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="agentFallback">CL</span></span><span class="agentText"><strong>Open Claude Desktop</strong><small>copy prompt + open app</small></span></button>' +
560
- '</div>' +
908
+ '<p class="recallPromptText">' + esc(question) + '</p>' +
909
+ '<div class="recallPromptActions" aria-label="Use suggested prompt ' + (index + 1) + '">' +
910
+ '<button type="button" class="recallAgent" data-recall-codex><span class="recallAgentIcon"><img src="/hud-assets/codex.svg" alt="" /></span><span>Ask in Codex</span></button>' +
911
+ '<button type="button" class="recallAgent is-claude" data-recall-claude><span class="recallAgentIcon"><img src="/hud-assets/claude.svg" alt="" /></span><span>Ask in Claude</span></button>' +
912
+ '</div>' +
913
+ '</article>';
914
+ }).join("");
915
+ return '<div class="tryCard">' +
916
+ '<p class="tryK">First recall</p>' +
917
+ '<h3 class="tryTitle">Try your new memory</h3>' +
918
+ '<p class="tryLead">These prompts are ready to use. Copy one, or ask it directly in your coding agent.</p>' +
919
+ '<div class="recallPromptList">' + prompts + '</div>' +
561
920
  '<p class="launchStatus" id="launchStatus"></p>' +
562
921
  '</div>';
563
922
  }
564
923
  function wireTryButtons() {
565
- var copyBtn = document.getElementById("tryCopy");
566
- if (copyBtn) copyBtn.addEventListener("click", copyPromptOnly);
567
- var codexBtn = document.getElementById("tryCodex");
568
- if (codexBtn) codexBtn.addEventListener("click", tryInCodex);
569
- var claudeBtn = document.getElementById("tryClaudeDesktop");
570
- if (claudeBtn) claudeBtn.addEventListener("click", openClaudeDesktop);
924
+ Array.prototype.forEach.call(document.querySelectorAll("[data-recall-prompt]"), function (row) {
925
+ var prompt = row.getAttribute("data-recall-prompt") || "";
926
+ var copyBtn = row.querySelector("[data-recall-copy]");
927
+ var codexBtn = row.querySelector("[data-recall-codex]");
928
+ var claudeBtn = row.querySelector("[data-recall-claude]");
929
+ if (copyBtn) copyBtn.addEventListener("click", function () { void copyPromptOnly(prompt, copyBtn); });
930
+ if (codexBtn) codexBtn.addEventListener("click", function () { void tryInCodex(prompt); });
931
+ if (claudeBtn) claudeBtn.addEventListener("click", function () { void openClaudeDesktop(prompt); });
932
+ });
571
933
  }
572
934
  function hudTip() {
573
- return '<p class="hudTip"><img src="/hud-assets/echo-face-cutout.png" alt="" /><span><strong>Live HUD:</strong> Echo watches the context window while you work. When the score drifts low, start a fresh session and carry the checkpoint forward. In Codex or Claude Code, ask &ldquo;<strong>open the EchoMem HUD</strong>&rdquo; or run <code>echomem-hud app</code>.</span></p>';
935
+ return '<p class="hudTip"><img src="/hud-assets/echo-face-cutout.png" alt="" /><span><strong>Live HUD:</strong> Echo watches context quality while you work. When it drops, <strong>Start new session with context</strong> opens a fresh Codex or Claude Code session and carries the useful work forward. Open it with <code>echomem-hud app</code>.</span></p>';
574
936
  }
575
937
  function doneCloseNote() {
576
938
  return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
@@ -578,34 +940,39 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
578
940
  function renderEnding() {
579
941
  setCityMode(false);
580
942
  setExtractMode(true);
943
+ // This view replaces the live extraction shell. Mark it unmounted so a failed
944
+ // pause can rebuild the progress page instead of trying to patch missing DOM.
945
+ extractMounted = false;
581
946
  setHead("Ending extraction", "Ending");
582
947
  app.className = "";
583
948
  app.innerHTML =
584
- '<div class="miniLoad" style="margin:0 auto;text-align:center;place-items:center">' +
585
- '<h2>Ending cleanly</h2>' +
586
- '<p>Give Echo a moment to mark this setup as finished for now. Everything already captured stays saved.</p>' +
949
+ '<div class="miniLoad">' +
950
+ '<p class="utilityEyebrow">Memory extraction / closing</p>' +
951
+ '<h2 class="siteHeadline">Stopping the <strong>remaining work</strong></h2>' +
952
+ '<p>Echo is canceling every queued conversation and releasing its unused reservations. Anything already saved stays saved.</p>' +
587
953
  '<div class="miniBar"><i></i></div>' +
588
954
  '</div>';
589
955
  }
590
956
  function renderSkipped() {
591
957
  setCityMode(false);
592
958
  setExtractMode(true);
593
- setHead("Setup paused", "Skipped");
959
+ setHead("Extraction ended for now", "Ended");
594
960
  app.className = "";
595
961
  app.innerHTML =
596
962
  '<div class="skipStage">' +
597
- '<span class="seal">Skipped for now</span>' +
598
- '<h2>You can come back anytime.</h2>' +
599
- '<p>Echo saved what already finished. When you are ready, run <code>echomem-mcp init</code> again — it picks up right where it left off.</p>' +
600
- recallGateBlock() +
963
+ '<div class="pauseStatus" role="status"><span aria-hidden="true">' + setupIcon("pause") + '</span><small>Extraction ended for now</small></div>' +
964
+ '<h2 class="siteHeadline">You can come back <strong>anytime.</strong></h2>' +
965
+ '<p>All remaining queued work was canceled and its unused reservations were released. When you are ready, run <code>echomem-mcp init</code> again — it rebuilds the remaining list from your local sessions.</p>' +
966
+ tryItCard() +
601
967
  hudTip() +
602
968
  '</div>';
603
- wireRecallGate();
969
+ wireTryButtons();
604
970
  }
605
971
  async function endExtraction() {
606
972
  if (extractionEnded) return;
607
- var ok = window.confirm("End extraction for now? You can come back anytime. Everything already captured stays saved, and the next run continues from what remains.");
973
+ var ok = window.confirm("End extraction for now? Echo will cancel all remaining queued work and release its unused reservations. Everything already captured stays saved.");
608
974
  if (!ok) return;
975
+ extractionRecoveryMessage = "";
609
976
  extractionEnded = true;
610
977
  renderEnding();
611
978
  try {
@@ -614,18 +981,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
614
981
  renderSkipped();
615
982
  } catch (error) {
616
983
  // A paused screen is a promise: do not show it until the bridge confirms that no more
617
- // conversations will be scheduled. Keep the active view visible when pausing fails.
984
+ // conversations will be scheduled. Restore the last live extraction state immediately;
985
+ // do not depend on another bridge request before leaving the ending screen.
618
986
  extractionEnded = false;
619
- var message = error && error.message ? error.message : "Could not pause extraction. It is still running.";
620
- try {
621
- renderProgress(await getJson("/progress"));
622
- var notice = document.getElementById("exText");
623
- if (notice) notice.insertAdjacentHTML("afterbegin", '<div class="error">' + esc(message) + '</div>');
624
- } catch (_) {
625
- setHead("Could not pause extraction", "Still extracting");
626
- app.className = "notice";
627
- app.innerHTML = '<h2>Extraction may still be running.</h2><p>' + esc(message) + '</p><p>Keep this page open and try ending it again in a moment.</p>';
628
- }
987
+ var message = error && error.message ? error.message : "Could not end extraction. It may still be running.";
988
+ extractionRecoveryMessage = message;
989
+ extractMounted = false;
990
+ renderProgress(lastExtractionProgress || {
991
+ status: "running",
992
+ total: 0,
993
+ completed: 0,
994
+ failed: 0,
995
+ extracted: 0,
996
+ latest: "Extraction is still running."
997
+ });
998
+ // Rejoin live updates. If the bridge is temporarily unavailable, the restored page and
999
+ // End for now button remain usable instead of being replaced by another blocking state.
1000
+ void pollProgress().catch(function () {});
629
1001
  }
630
1002
  }
631
1003
  async function skip() {