@echomem/mcp 1.4.18 → 1.4.20

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,38 +199,45 @@ 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>' +
217
+ '<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip history import</button>' +
210
218
  '</div>' +
211
219
  '<p class="ctaMeta" id="exEta"></p>' +
212
220
  '</div>' +
213
221
  '<div class="readyFoot">' +
214
222
  '<details class="readyExplain" id="readyExplain"></details>' +
215
223
  '</div>' +
216
- '</section>';
224
+ '</section>' +
225
+ '<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
217
226
  document.getElementById("migrate").onclick = startMigrate;
218
227
  dashMounted = true;
219
228
  }
220
229
  document.getElementById("exBanner").innerHTML =
221
230
  (statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask Echo to extract anything unprocessed.</div>' : '') +
222
231
  (error ? '<div class="error">' + esc(error) + '</div>' : '');
223
- document.getElementById("exHeadline").textContent = headline;
232
+ document.getElementById("exHeadline").innerHTML = headlineHtml;
224
233
  document.getElementById("exSub").textContent = sub;
225
234
  var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
226
235
  var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
227
236
  var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
228
237
  // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
229
238
  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>';
239
+ var asset = id === "claude-desktop" ? "claude" : "codex";
240
+ 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
241
  };
232
242
  document.getElementById("exSources").innerHTML = !pendingTrusted
233
243
  ? '<span class="pfNote">Scanning local history&hellip;</span>'
@@ -251,20 +261,40 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
251
261
  '<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
252
262
  '<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
253
263
  '</div>';
264
+ renderSetupPlan(canExtract);
265
+ renderAccountChoice();
266
+ renderSessionSelection(canExtract);
267
+ if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
254
268
  // While still counting, hide the primary button entirely; the headline + source split
255
269
  // already say it is working, and the ready state should present one clear action.
256
270
  var migrateBtn = document.getElementById("migrate");
271
+ var skipHistoryBtn = document.getElementById("skipHistoryImport");
257
272
  if (pendingTrusted) {
258
273
  migrateBtn.style.display = "";
259
- migrateBtn.disabled = false;
260
- migrateBtn.textContent = canExtract ? extractLabel : "Finish setup";
274
+ migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
275
+ migrateBtn.innerHTML = '<span>' + esc(canExtract ? extractLabel : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
261
276
  migrateBtn.onclick = canExtract ? startMigrate : skip;
277
+ if (skipHistoryBtn) {
278
+ skipHistoryBtn.style.display = canExtract ? "" : "none";
279
+ skipHistoryBtn.onclick = skip;
280
+ }
262
281
  } else {
263
282
  migrateBtn.style.display = "none";
283
+ if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
264
284
  }
265
285
  }
266
286
  async function startMigrate() {
267
287
  if (decisionMade) return;
288
+ if (!setupPlanConfirmed()) {
289
+ setSetupPlanStatus("Choose Free, Pro, or Power before extraction starts.");
290
+ return;
291
+ }
292
+ var selectedKeys = selectedSessionKeyList();
293
+ if (!selectedKeys.length) {
294
+ renderSessionSelection(true, "Choose at least one conversation to extract.");
295
+ return;
296
+ }
297
+ stopBillingPoll();
268
298
  decisionMade = true;
269
299
  extractionEnded = false;
270
300
  setCityMode(false);
@@ -272,7 +302,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
272
302
  app.className = "";
273
303
  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
304
  try {
275
- var started = await postJson("/migrate", {});
305
+ var started = await postJson("/migrate", { conversationKeys: selectedKeys });
276
306
  extractMounted = false;
277
307
  renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
278
308
  await pollProgress();
@@ -282,10 +312,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
282
312
  }
283
313
  }
284
314
  async function pollProgress() {
315
+ var runId = ++progressPollRunId;
285
316
  for (;;) {
286
- if (extractionEnded) return;
317
+ if (extractionEnded || runId !== progressPollRunId) return;
287
318
  var progress = await getJson("/progress");
288
- if (extractionEnded) return;
319
+ if (extractionEnded || runId !== progressPollRunId) return;
289
320
  renderProgress(progress);
290
321
  if (progress.status === "completed" || progress.status === "failed") return;
291
322
  await sleep(1200);
@@ -293,6 +324,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
293
324
  }
294
325
  function renderProgress(progress) {
295
326
  if (extractionEnded) return;
327
+ lastExtractionProgress = progress;
296
328
  setCityMode(false);
297
329
  setExtractMode(true);
298
330
  var status = progress.status;
@@ -327,22 +359,25 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
327
359
  var savedMemoryLabel = extracted === 1 ? "memory saved" : "memories saved";
328
360
  var conversationNoun = completed === 1 ? "conversation" : "conversations";
329
361
  var timelineUrl = "https://yeahecho.com/memories/timeline";
330
- var stopDetail = String(progress.error) === "key-expired"
362
+ var stopDetail = String(progress.error) === "vault-locked"
331
363
  ? "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
364
  : esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
365
+ var completionNote = done && progress.latest && progress.latest !== "Import complete."
366
+ ? '<p class="exSub">' + esc(progress.latest) + '</p>'
367
+ : "";
333
368
  t.innerHTML =
334
369
  '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
335
- (done ? "" : '<p class="kicker-mono">Extraction stopped</p>') +
370
+ (done ? '<p class="utilityEyebrow">Memory extraction / complete</p>' : '<p class="utilityEyebrow">Memory extraction / stopped</p>') +
336
371
  '<div class="resultTop">' +
337
372
  '<div>' +
338
373
  (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>' +
374
+ ? '<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
375
+ : '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
341
376
  '<p class="exSub">' + stopDetail + '</p>' +
342
377
  '<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
343
378
  '</div>' +
344
379
  '</div>' +
345
- (done ? recallGateBlock() : "") +
380
+ (done ? tryItCard() : "") +
346
381
  (done ? "" :
347
382
  '<div class="memoryReceipt">' +
348
383
  '<p class="receiptK">What just happened</p>' +
@@ -353,7 +388,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
353
388
  '</div>') +
354
389
  '</div>' +
355
390
  (done ? hudTip() + doneCloseNote() : "");
356
- wireRecallGate();
391
+ wireTryButtons();
357
392
  return;
358
393
  }
359
394
  setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
@@ -379,9 +414,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
379
414
  countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
380
415
  }
381
416
  }
382
- t.innerHTML =
417
+ var recoveryNotice = extractionRecoveryMessage
418
+ ? '<div class="error">' + esc(extractionRecoveryMessage) + '</div>'
419
+ : "";
420
+ t.innerHTML = recoveryNotice +
383
421
  '<div class="workbench">' +
384
- '<h2>' + (preparing ? "Setting up the import." : "Building your memory.") + '</h2>' +
422
+ '<p class="utilityEyebrow">Memory extraction / live</p>' +
423
+ '<h2 class="siteHeadline">' + (preparing ? "Setting up the <strong>import.</strong>" : "Building your <strong>memory.</strong>") + '</h2>' +
385
424
  '<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
425
  '<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
387
426
  '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
@@ -399,15 +438,75 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
399
438
  if (m >= 60) { var h = Math.floor(m / 60); return h + "h " + (m % 60) + "m"; }
400
439
  return m > 0 ? m + "m " + s + "s" : s + "s";
401
440
  }
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.";
441
+ function cleanRecallTopic(value) {
442
+ var topic = String(value || "")
443
+ .replace(/\s+/g, " ")
444
+ .replace(/\s+[·|]\s+\d+$/, "")
445
+ .trim();
446
+ if (!topic || topic.length < 3) return "";
447
+ if (topic.length > 56) topic = topic.slice(0, 53).trim() + "...";
448
+ var generic = {
449
+ "no project detected": true,
450
+ "untitled coding session": true,
451
+ "project": true,
452
+ "projects": true,
453
+ "coding session": true,
454
+ "coding sessions": true
455
+ };
456
+ return generic[topic.toLowerCase()] ? "" : topic;
457
+ }
458
+ function recallTopics() {
459
+ var selected = Object.create(null);
460
+ selectedSessionKeyList().forEach(function (key) { selected[key] = true; });
461
+ var sessions = candidateSessions().filter(function (session) {
462
+ return !!selected[String(session.key || "")];
463
+ });
464
+ if (!sessions.length) sessions = candidateSessions();
465
+
466
+ var scored = Object.create(null);
467
+ function addTopic(value, base, recency) {
468
+ var topic = cleanRecallTopic(value);
469
+ if (!topic) return;
470
+ var key = topic.toLowerCase();
471
+ if (!scored[key]) scored[key] = { topic: topic, score: 0 };
472
+ scored[key].score += base + recency;
473
+ }
474
+ sessions.forEach(function (session, index) {
475
+ var recency = Math.max(1, sessions.length - index);
476
+ addTopic(session.project, 100, recency);
477
+ addTopic(session.title, 20, recency);
478
+ });
479
+ return Object.keys(scored)
480
+ .map(function (key) { return scored[key]; })
481
+ .sort(function (a, b) { return b.score - a.score || a.topic.localeCompare(b.topic); })
482
+ .slice(0, 3)
483
+ .map(function (entry) { return entry.topic; });
484
+ }
485
+ function recallQuestions() {
486
+ var topics = recallTopics();
487
+ var templates = [
488
+ "What do I know about {x}?",
489
+ "Catch me up on {x}.",
490
+ "Where did I leave off with {x}?"
491
+ ];
492
+ var questions = topics.map(function (topic, index) {
493
+ return templates[index].replace("{x}", topic);
494
+ });
495
+ var fallbacks = [
496
+ "What have I been working on recently?",
497
+ "What decisions and next steps should I pick up?",
498
+ "What from my recent work needs attention next?"
499
+ ];
500
+ fallbacks.forEach(function (question) {
501
+ if (questions.length < 3) questions.push(question);
502
+ });
503
+ return questions.slice(0, 3);
404
504
  }
405
505
  function setLaunchStatus(message) {
406
506
  var node = document.getElementById("launchStatus");
407
507
  if (node) node.textContent = message || "";
408
508
  }
409
- async function copyRecallPrompt() {
410
- var prompt = recallPrompt();
509
+ async function copyRecallPrompt(prompt) {
411
510
  try {
412
511
  await navigator.clipboard.writeText(prompt);
413
512
  return true;
@@ -415,162 +514,477 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
415
514
  return false;
416
515
  }
417
516
  }
418
- async function copyPromptOnly() {
419
- var copied = await copyRecallPrompt();
420
- var copyBtn = document.getElementById("tryCopy");
517
+ async function copyPromptOnly(prompt, copyBtn) {
518
+ var copied = await copyRecallPrompt(prompt);
421
519
  if (copyBtn && copied) {
520
+ var original = copyBtn.innerHTML;
422
521
  copyBtn.textContent = "Copied";
423
- window.setTimeout(function () { copyBtn.textContent = "Copy prompt"; }, 1600);
522
+ window.setTimeout(function () { copyBtn.innerHTML = original; }, 1600);
424
523
  }
425
- setLaunchStatus(copied ? "Prompt copied." : "Could not copy automatically. Select the prompt and copy it.");
524
+ setLaunchStatus(copied ? "Question copied." : "Could not copy the question automatically.");
426
525
  }
427
526
  function codexDeepLink(prompt) {
428
527
  var q = "prompt=" + encodeURIComponent(prompt);
429
528
  if (workspacePath) q += "&path=" + encodeURIComponent(workspacePath);
430
529
  return "codex://new?" + q;
431
530
  }
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.");
531
+ async function tryInCodex(prompt) {
532
+ var copied = await copyRecallPrompt(prompt);
533
+ setLaunchStatus(copied ? "Copied question. Opening Codex..." : "Opening Codex with your question...");
436
534
  window.location.href = codexDeepLink(prompt);
437
535
  }
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.");
536
+ async function openClaudeDesktop(prompt) {
537
+ var copied = await copyRecallPrompt(prompt);
538
+ setLaunchStatus(copied ? "Copied question. Opening Claude Desktop..." : "Opening Claude Desktop...");
441
539
  try {
442
540
  var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
443
- setLaunchStatus(res && res.message ? res.message : "Prompt copied. Paste it into Claude Desktop.");
541
+ setLaunchStatus(res && res.message ? res.message : "Question copied. Paste it into Claude Desktop.");
444
542
  } catch (error) {
445
543
  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.";
544
+ if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The question is copied - open Claude Desktop and paste it.";
447
545
  setLaunchStatus(msg);
448
546
  }
449
547
  }
548
+ function candidateSessions() {
549
+ return stats && Array.isArray(stats.candidateSessions) ? stats.candidateSessions : [];
550
+ }
551
+ function historicalSelectionLimit() {
552
+ if (!setupPlanConfirmed()) return 0;
553
+ var quota = billingStatus && billingStatus.historicalConversationQuota;
554
+ if (quota && typeof quota.remaining === "number" && isFinite(quota.remaining)) {
555
+ return Math.max(0, Math.floor(quota.remaining));
556
+ }
557
+ var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
558
+ if (plan === "power" || plan === "enterprise") return 500;
559
+ if (paidRecallPlan(plan)) return 250;
560
+ return 100;
561
+ }
562
+ function selectedSessionKeyList() {
563
+ var valid = Object.create(null);
564
+ candidateSessions().forEach(function (session) { valid[String(session.key || "")] = true; });
565
+ return Object.keys(selectedSessionKeys).filter(function (key) { return selectedSessionKeys[key] && valid[key]; });
566
+ }
567
+ function ensureSessionSelection() {
568
+ var candidates = candidateSessions();
569
+ var valid = Object.create(null);
570
+ candidates.forEach(function (session) { valid[String(session.key || "")] = true; });
571
+ Object.keys(selectedSessionKeys).forEach(function (key) {
572
+ if (!valid[key]) delete selectedSessionKeys[key];
573
+ });
574
+ if (!setupPlanConfirmed()) return;
575
+ var limit = Math.min(candidates.length, historicalSelectionLimit());
576
+ if (!sessionSelectionTouched) {
577
+ selectedSessionKeys = Object.create(null);
578
+ candidates.slice(0, limit).forEach(function (session) { selectedSessionKeys[String(session.key)] = true; });
579
+ return;
580
+ }
581
+ var selected = selectedSessionKeyList();
582
+ if (selected.length > limit) {
583
+ var keep = Object.create(null);
584
+ var selectedSet = Object.create(null);
585
+ selected.forEach(function (key) { selectedSet[key] = true; });
586
+ candidates.forEach(function (session) {
587
+ var key = String(session.key || "");
588
+ if (selectedSet[key] && Object.keys(keep).length < limit) keep[key] = true;
589
+ });
590
+ selectedSessionKeys = keep;
591
+ }
592
+ }
593
+ function sessionSourceLabel(source) {
594
+ return source === "claude-code" ? "Claude Code" : "Codex";
595
+ }
596
+ function sessionDateLabel(value) {
597
+ if (!value) return "Date unavailable";
598
+ var date = new Date(value);
599
+ if (!isFinite(date.getTime())) return "Date unavailable";
600
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
601
+ }
602
+ function renderSessionSelection(canExtract, message) {
603
+ var slot = document.getElementById("sessionSelection");
604
+ if (!slot) return;
605
+ if (!canExtract || !setupPlanConfirmed()) {
606
+ slot.innerHTML = "";
607
+ slot.classList.add("is-hidden");
608
+ return;
609
+ }
610
+ slot.classList.remove("is-hidden");
611
+ var candidates = candidateSessions();
612
+ if (!candidates.length) {
613
+ 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>';
614
+ return;
615
+ }
616
+ ensureSessionSelection();
617
+ var selected = selectedSessionKeyList();
618
+ var limit = Math.min(candidates.length, historicalSelectionLimit());
619
+ var planLimited = candidates.length > limit;
620
+ var summary = planLimited
621
+ ? "Your plan can import " + number(limit) + " of " + number(candidates.length) + " available conversations."
622
+ : "All " + number(candidates.length) + " available conversations are included by default.";
623
+ slot.innerHTML =
624
+ '<section class="sessionSelectionCard" aria-labelledby="sessionSelectionTitle">' +
625
+ '<span class="sessionSelectionIcon" aria-hidden="true">' + setupIcon("list-check") + '</span>' +
626
+ '<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>' +
627
+ '<button type="button" class="secondary sessionEditButton" id="editSessions">Review &amp; edit</button>' +
628
+ '</section>';
629
+ var edit = document.getElementById("editSessions");
630
+ if (edit) edit.onclick = openSessionPicker;
631
+ var migrateBtn = document.getElementById("migrate");
632
+ if (migrateBtn) migrateBtn.disabled = selected.length === 0 || !setupPlanConfirmed();
633
+ }
634
+ function updateSessionPickerSelectionUi(dialog, input, limit) {
635
+ var selectedCount = selectedSessionKeyList().length;
636
+ hideSessionPickerLimitNotice();
637
+ var row = input && input.closest ? input.closest(".sessionPickerRow") : null;
638
+ if (row) row.classList.toggle("is-selected", !!input.checked);
639
+ var count = dialog.querySelector(".sessionPickerActions strong");
640
+ if (count) count.textContent = number(selectedCount) + " / " + number(limit) + " selected";
641
+ var status = dialog.querySelector("#sessionPickerStatus");
642
+ if (status) status.textContent = "";
643
+ var confirm = dialog.querySelector("#confirmSessions");
644
+ if (confirm) {
645
+ confirm.disabled = selectedCount === 0;
646
+ confirm.textContent = "Use " + number(selectedCount) + " selected";
647
+ }
648
+ }
649
+ function sessionPickerLimitNotice(canDismiss) {
650
+ var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
651
+ var linkLabel = plan === "power" || plan === "team" || plan === "enterprise"
652
+ ? "View plan options"
653
+ : "Upgrade to import more";
654
+ return '<div class="sessionPickerLimitCard">' +
655
+ '<span class="sessionPickerLimitEyebrow">Plan limit</span>' +
656
+ '<h3>Plan limit reached.</h3>' +
657
+ '<p>Upgrade your plan to select and import more conversations.</p>' +
658
+ '<div class="sessionPickerLimitActions"><a id="upgradeSessionPlan" class="button primary" href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">' + linkLabel + '</a>' +
659
+ (canDismiss ? '<button type="button" class="textButton" id="dismissSessionLimit">Keep editing</button>' : "") + '</div>' +
660
+ '</div>';
661
+ }
662
+ function wireSessionPickerUpgrade() {
663
+ var upgrade = document.getElementById("upgradeSessionPlan");
664
+ if (upgrade) upgrade.onclick = function () { startBillingPoll(); };
665
+ var dismiss = document.getElementById("dismissSessionLimit");
666
+ if (dismiss) dismiss.onclick = hideSessionPickerLimitNotice;
667
+ }
668
+ function showSessionPickerLimitNotice(canDismiss) {
669
+ var wrap = document.getElementById("sessionPickerListWrap");
670
+ var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
671
+ var overlay = document.getElementById("sessionPickerLimitOverlay");
672
+ var status = document.getElementById("sessionPickerStatus");
673
+ if (status) status.textContent = "Plan limit reached. Upgrade to import more.";
674
+ if (!wrap || !overlay) return;
675
+ overlay.innerHTML = sessionPickerLimitNotice(canDismiss);
676
+ overlay.hidden = false;
677
+ if (list) list.inert = true;
678
+ wrap.classList.add("is-limit-blocked");
679
+ wireSessionPickerUpgrade();
680
+ }
681
+ function hideSessionPickerLimitNotice() {
682
+ var wrap = document.getElementById("sessionPickerListWrap");
683
+ var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
684
+ var overlay = document.getElementById("sessionPickerLimitOverlay");
685
+ var status = document.getElementById("sessionPickerStatus");
686
+ if (status) status.textContent = "";
687
+ if (overlay) { overlay.hidden = true; overlay.innerHTML = ""; }
688
+ if (list) list.inert = false;
689
+ if (wrap) wrap.classList.remove("is-limit-blocked");
690
+ }
691
+ function renderSessionPicker() {
692
+ var dialog = document.getElementById("sessionPicker");
693
+ if (!dialog) return;
694
+ ensureSessionSelection();
695
+ var all = candidateSessions();
696
+ var selected = selectedSessionKeyList();
697
+ var limit = Math.min(all.length, historicalSelectionLimit());
698
+ var query = sessionPickerQuery.trim().toLowerCase();
699
+ var filtered = all.filter(function (session) {
700
+ if (sessionPickerSource !== "all" && session.source !== sessionPickerSource) return false;
701
+ if (!query) return true;
702
+ return [session.title, session.project, session.source, session.date].join(" ").toLowerCase().indexOf(query) >= 0;
703
+ });
704
+ var visible = filtered.slice(0, 250);
705
+ var rows = visible.map(function (session) {
706
+ var key = String(session.key || "");
707
+ var encodedKey = encodeURIComponent(key);
708
+ var checked = !!selectedSessionKeys[key];
709
+ var sourceIcon = session.source === "claude-code" ? "claude" : "codex";
710
+ var sourceFallback = session.source === "claude-code" ? "CL" : "CX";
711
+ return '<label class="sessionPickerRow' + (checked ? " is-selected" : "") + '">' +
712
+ '<input type="checkbox" data-session-key="' + esc(encodedKey) + '"' + (checked ? " checked" : "") + ' />' +
713
+ '<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>' +
714
+ '<span class="sessionPickerMain"><strong>' + esc(session.title || "Untitled coding session") + '</strong><small>' + esc(session.project || "No project detected") + ' · ' + esc(sessionDateLabel(session.date)) + '</small></span>' +
715
+ '<span class="sessionPickerMeta">' + esc(compact(Number(session.approxInputTokens) || 0)) + ' tok</span>' +
716
+ '</label>';
717
+ }).join("");
718
+ var overflow = filtered.length > visible.length
719
+ ? '<p class="sessionPickerOverflow">Showing the first 250 matches. Narrow the search to find older conversations.</p>'
720
+ : "";
721
+ dialog.innerHTML =
722
+ '<div class="sessionPickerShell">' +
723
+ '<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>' +
724
+ '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
725
+ '<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>' +
726
+ '</div>' +
727
+ '<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" class="sessionPickerStatus" role="status" aria-live="polite">' + (limit === 0 ? "Plan limit reached. Upgrade to import more." : "") + '</span><button type="button" class="textButton" id="selectNewestSessions">Select newest</button><button type="button" class="textButton" id="clearSessions">Clear</button></div>' +
728
+ '<div class="sessionPickerListWrap' + (limit === 0 ? " is-limit-blocked" : "") + '" id="sessionPickerListWrap"><div class="sessionPickerList"' + (limit === 0 ? " inert" : "") + '>' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + overflow + '</div><div class="sessionPickerLimitOverlay" id="sessionPickerLimitOverlay" role="alert"' + (limit === 0 ? "" : " hidden") + '>' + (limit === 0 ? sessionPickerLimitNotice(false) : "") + '</div></div>' +
729
+ '<footer class="sessionPickerFoot"><p>Only selected conversations will be sent for extraction.</p><div class="sessionPickerFootActions"><button type="button" class="secondary" id="skipSessionImport">Skip history import</button><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Use ' + esc(number(selected.length)) + ' selected</button></div></footer>' +
730
+ '</div>';
731
+ var close = document.getElementById("closeSessionPicker");
732
+ var confirm = document.getElementById("confirmSessions");
733
+ var skipImport = document.getElementById("skipSessionImport");
734
+ if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
735
+ if (confirm) confirm.onclick = function () { dialog.close(); renderSessionSelection(true); };
736
+ if (skipImport) skipImport.onclick = skip;
737
+ wireSessionPickerUpgrade();
738
+ var search = document.getElementById("sessionPickerSearch");
739
+ if (search) search.oninput = function () {
740
+ sessionPickerQuery = search.value || "";
741
+ renderSessionPicker();
742
+ var next = document.getElementById("sessionPickerSearch");
743
+ if (next) { next.focus(); next.setSelectionRange(next.value.length, next.value.length); }
744
+ };
745
+ var source = document.getElementById("sessionPickerSource");
746
+ if (source) source.onchange = function () { sessionPickerSource = source.value || "all"; renderSessionPicker(); };
747
+ Array.prototype.forEach.call(dialog.querySelectorAll("[data-session-key]"), function (input) {
748
+ input.onchange = function () {
749
+ var key = decodeURIComponent(input.getAttribute("data-session-key") || "");
750
+ var count = selectedSessionKeyList().length;
751
+ if (input.checked && !selectedSessionKeys[key] && count >= limit) {
752
+ input.checked = false;
753
+ showSessionPickerLimitNotice(limit > 0);
754
+ return;
755
+ }
756
+ sessionSelectionTouched = true;
757
+ if (input.checked) selectedSessionKeys[key] = true;
758
+ else delete selectedSessionKeys[key];
759
+ // Keep the existing list DOM so selecting a row does not reset its scroll position.
760
+ updateSessionPickerSelectionUi(dialog, input, limit);
761
+ };
762
+ });
763
+ var newest = document.getElementById("selectNewestSessions");
764
+ if (newest) newest.onclick = function () {
765
+ sessionSelectionTouched = true;
766
+ selectedSessionKeys = Object.create(null);
767
+ all.slice(0, limit).forEach(function (session) { selectedSessionKeys[String(session.key)] = true; });
768
+ renderSessionPicker();
769
+ };
770
+ var clear = document.getElementById("clearSessions");
771
+ if (clear) clear.onclick = function () { sessionSelectionTouched = true; selectedSessionKeys = Object.create(null); renderSessionPicker(); };
772
+ }
773
+ function openSessionPicker() {
774
+ var dialog = document.getElementById("sessionPicker");
775
+ if (!dialog) return;
776
+ renderSessionPicker();
777
+ if (typeof dialog.showModal === "function") dialog.showModal();
778
+ else dialog.setAttribute("open", "");
779
+ }
780
+ function accountInitials(account) {
781
+ var label = String(account && (account.displayName || account.email) || "EchoMem").trim();
782
+ return label.split(/\s+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
783
+ }
784
+ function renderAccountChoice() {
785
+ var slot = document.getElementById("setupAccount");
786
+ if (!slot) return;
787
+ var account = billingStatus && billingStatus.account;
788
+ var name = account && account.displayName ? account.displayName : "Connected EchoMem account";
789
+ var email = account && account.email ? account.email : "This import will be saved to the account connected on this device.";
790
+ var avatarUrl = account && account.avatarUrl ? String(account.avatarUrl) : "";
791
+ var avatar = '<span class="setupAccountAvatar" aria-hidden="true">' +
792
+ '<span>' + esc(accountInitials(account)) + '</span>' +
793
+ (avatarUrl ? '<img src="' + esc(avatarUrl) + '" alt="" onerror="this.remove()" />' : '') +
794
+ '</span>';
795
+ slot.innerHTML =
796
+ '<section class="setupAccountCard" aria-label="Connected EchoMem account">' +
797
+ avatar +
798
+ '<span class="setupAccountCopy"><small>Saving to</small><strong>' + esc(name) + '</strong><span>' + esc(email) + '</span></span>' +
799
+ '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Change account</button>' +
800
+ '</section>';
801
+ var change = document.getElementById("changeSetupAccount");
802
+ if (change) change.onclick = switchSetupAccount;
803
+ }
804
+ async function switchSetupAccount() {
805
+ var button = document.getElementById("changeSetupAccount");
806
+ if (button) { button.disabled = true; button.textContent = "Opening account chooser…"; }
807
+ try {
808
+ await postJson("/logout", {}, 12000);
809
+ stats = null;
810
+ billingStatus = null;
811
+ connected = false;
812
+ selectedSessionKeys = Object.create(null);
813
+ sessionSelectionTouched = false;
814
+ if (!openAuthWindow(switchAccountUrl || authUrl)) {
815
+ if (button) { button.disabled = false; button.textContent = "Change account"; }
816
+ showPopupFallback("Your browser blocked the account chooser. Open it below to continue.", switchAccountUrl || authUrl);
817
+ return;
818
+ }
819
+ startConnectionPoll();
820
+ } catch (error) {
821
+ if (button) { button.disabled = false; button.textContent = "Change account"; }
822
+ setSetupPlanStatus(error && error.message ? error.message : "Could not open the account chooser.");
823
+ }
824
+ }
450
825
  function paidRecallPlan(plan) {
451
826
  return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
452
827
  }
453
- function recallPricingUrl() {
828
+ function setupPricingUrl(plan, startTrial) {
829
+ var paidPlan = plan === "power" ? "power" : "pro";
454
830
  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>';
831
+ ? billingStatus.pricingUrl + (startTrial ? "&start=" + paidPlan + "&trial=" + (billingStatus.trialAvailable === false ? "0" : "1") : "")
832
+ : "https://echoknows.com/pricing?source=mcp_onboarding" + (startTrial ? "&start=" + paidPlan + "&trial=1" : "");
833
+ }
834
+ function setupPlanConfirmed() {
835
+ return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
485
836
  }
486
- function setRecallGateStatus(message) {
487
- var node = document.getElementById("recallGateStatus");
837
+ function setSetupPlanStatus(message) {
838
+ var node = document.getElementById("setupPlanStatus");
488
839
  if (node) node.textContent = message || "";
489
840
  }
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.";
841
+ function renderSetupPlan(canExtract) {
842
+ var slot = document.getElementById("setupPlan");
843
+ if (!slot) return;
844
+ if (!canExtract) {
845
+ slot.innerHTML = "";
846
+ slot.classList.add("is-hidden");
847
+ return;
848
+ }
849
+ slot.classList.remove("is-hidden");
850
+ var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
851
+ var paid = paidRecallPlan(plan);
852
+ var migrateBtn = document.getElementById("migrate");
853
+ if (paid) {
854
+ setupPlanChoice = "paid";
855
+ var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
856
+ var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
857
+ var paidUsage = paidHistory && typeof paidHistory.used === "number" && typeof paidHistory.limit === "number"
858
+ ? paidHistory.used + " / " + paidHistory.limit + " bulk history imports used."
859
+ : "Your bulk history import and weekly new-chat allowance are ready.";
860
+ 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>';
861
+ renderSessionSelection(true);
862
+ stopBillingPoll();
863
+ return;
864
+ }
865
+ if (setupPlanChoice === "free") {
866
+ 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>';
867
+ var changeBtn = document.getElementById("changeSetupPlan");
868
+ if (changeBtn) changeBtn.onclick = function () { setupPlanChoice = ""; renderSetupPlan(true); renderSessionSelection(true); };
869
+ renderSessionSelection(true);
870
+ return;
871
+ }
872
+ var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
873
+ slot.innerHTML =
874
+ '<section class="setupPlanChoice" aria-labelledby="setupPlanHeading">' +
875
+ '<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>' +
876
+ '<div class="setupPlanGrid">' +
877
+ '<article class="setupPlanCard">' +
878
+ '<div class="setupPlanHead"><strong>Free</strong><span>$0 forever</span></div>' +
879
+ '<p>Start with the essentials.</p>' +
880
+ '<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>' +
881
+ '<button type="button" class="secondary" id="chooseFreePlan">Continue with Free</button>' +
882
+ '</article>' +
883
+ '<article class="setupPlanCard is-pro">' +
884
+ '<div class="setupPlanHead"><strong>Pro</strong><span>$40 / month</span></div>' +
885
+ '<p>More recall for serious work.</p>' +
886
+ '<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>' +
887
+ '<button type="button" class="primary" id="chooseProPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Pro") + '</button>' +
888
+ '<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
889
+ '</article>' +
890
+ '<article class="setupPlanCard">' +
891
+ '<div class="setupPlanHead"><strong>Power</strong><span>$100 / month</span></div>' +
892
+ '<p>Higher limits for people who use Echo every day.</p>' +
893
+ // Future Power feature: API and automation access.
894
+ '<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>' +
895
+ '<button type="button" class="secondary" id="choosePowerPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Power") + '</button>' +
896
+ '<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
897
+ '</article>' +
898
+ '</div>' +
899
+ '<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>' +
900
+ '</section>';
901
+ var freeBtn = document.getElementById("chooseFreePlan");
902
+ if (freeBtn) freeBtn.onclick = function () { setupPlanChoice = "free"; renderSetupPlan(true); renderSessionSelection(true); };
903
+ var proBtn = document.getElementById("chooseProPlan");
904
+ if (proBtn) proBtn.onclick = function () { openPaidPricing("pro"); };
905
+ var powerBtn = document.getElementById("choosePowerPlan");
906
+ if (powerBtn) powerBtn.onclick = function () { openPaidPricing("power"); };
907
+ if (migrateBtn) migrateBtn.disabled = true;
908
+ renderSessionSelection(true);
513
909
  }
514
910
  async function refreshBillingStatus() {
911
+ if (billingStatusLoading) return;
912
+ billingStatusLoading = true;
515
913
  try {
516
914
  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
- }
915
+ renderSetupPlan(true);
916
+ renderAccountChoice();
917
+ renderSessionSelection(true);
918
+ if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Free still works.");
521
919
  } catch (_) {
522
- setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
920
+ setSetupPlanStatus("Plan check unavailable. Free still works.");
921
+ } finally {
922
+ billingStatusLoading = false;
523
923
  }
524
924
  }
525
- function openRecallPricing() {
526
- var url = recallPricingUrl();
527
- setRecallGateStatus("Opening pricing in a new tab...");
925
+ function refreshBillingOnFocus() { void refreshBillingStatus(); }
926
+ function startBillingPoll() {
927
+ if (billingPollTimer) return;
928
+ window.addEventListener("focus", refreshBillingOnFocus);
929
+ billingPollTimer = window.setInterval(function () { void refreshBillingStatus(); }, 2500);
930
+ }
931
+ function stopBillingPoll() {
932
+ if (billingPollTimer) window.clearInterval(billingPollTimer);
933
+ billingPollTimer = null;
934
+ window.removeEventListener("focus", refreshBillingOnFocus);
935
+ }
936
+ function openPaidPricing(plan) {
937
+ var url = setupPricingUrl(plan, true);
938
+ setSetupPlanStatus("Opening secure checkout…");
528
939
  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.");
940
+ var opened = window.open(url, "_blank");
941
+ if (!opened) {
942
+ setSetupPlanStatus("Your browser blocked checkout. Allow pop-ups, then try again.");
943
+ return;
944
+ }
945
+ try { opened.opener = null; } catch (_) {}
946
+ startBillingPoll();
531
947
  } catch (_) {
532
- setRecallGateStatus("Could not open a new tab. Allow pop-ups for this page, then click Compare plans again.");
948
+ setSetupPlanStatus("Could not open checkout. Allow pop-ups, then try again.");
533
949
  }
534
950
  }
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
951
  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>' +
952
+ var questions = recallQuestions();
953
+ var prompts = questions.map(function (question, index) {
954
+ var numberLabel = String(index + 1).padStart(2, "0");
955
+ return '<article class="recallPrompt" data-recall-prompt="' + esc(question) + '">' +
956
+ '<div class="recallPromptHead">' +
957
+ '<span>Suggested prompt ' + numberLabel + '</span>' +
958
+ '<button type="button" class="recallCopy" data-recall-copy aria-label="Copy suggested prompt ' + (index + 1) + '">' + setupIcon("copy") + '<span>Copy</span></button>' +
554
959
  '</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>' +
960
+ '<p class="recallPromptText">' + esc(question) + '</p>' +
961
+ '<div class="recallPromptActions" aria-label="Use suggested prompt ' + (index + 1) + '">' +
962
+ '<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>' +
963
+ '<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>' +
964
+ '</div>' +
965
+ '</article>';
966
+ }).join("");
967
+ return '<div class="tryCard">' +
968
+ '<p class="tryK">First recall</p>' +
969
+ '<h3 class="tryTitle">Try your new memory</h3>' +
970
+ '<p class="tryLead">These prompts are ready to use. Copy one, or ask it directly in your coding agent.</p>' +
971
+ '<div class="recallPromptList">' + prompts + '</div>' +
561
972
  '<p class="launchStatus" id="launchStatus"></p>' +
562
973
  '</div>';
563
974
  }
564
975
  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);
976
+ Array.prototype.forEach.call(document.querySelectorAll("[data-recall-prompt]"), function (row) {
977
+ var prompt = row.getAttribute("data-recall-prompt") || "";
978
+ var copyBtn = row.querySelector("[data-recall-copy]");
979
+ var codexBtn = row.querySelector("[data-recall-codex]");
980
+ var claudeBtn = row.querySelector("[data-recall-claude]");
981
+ if (copyBtn) copyBtn.addEventListener("click", function () { void copyPromptOnly(prompt, copyBtn); });
982
+ if (codexBtn) codexBtn.addEventListener("click", function () { void tryInCodex(prompt); });
983
+ if (claudeBtn) claudeBtn.addEventListener("click", function () { void openClaudeDesktop(prompt); });
984
+ });
571
985
  }
572
986
  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>';
987
+ 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
988
  }
575
989
  function doneCloseNote() {
576
990
  return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
@@ -578,34 +992,39 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
578
992
  function renderEnding() {
579
993
  setCityMode(false);
580
994
  setExtractMode(true);
995
+ // This view replaces the live extraction shell. Mark it unmounted so a failed
996
+ // pause can rebuild the progress page instead of trying to patch missing DOM.
997
+ extractMounted = false;
581
998
  setHead("Ending extraction", "Ending");
582
999
  app.className = "";
583
1000
  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>' +
1001
+ '<div class="miniLoad">' +
1002
+ '<p class="utilityEyebrow">Memory extraction / closing</p>' +
1003
+ '<h2 class="siteHeadline">Stopping the <strong>remaining work</strong></h2>' +
1004
+ '<p>Echo is canceling every queued conversation and releasing its unused reservations. Anything already saved stays saved.</p>' +
587
1005
  '<div class="miniBar"><i></i></div>' +
588
1006
  '</div>';
589
1007
  }
590
1008
  function renderSkipped() {
591
1009
  setCityMode(false);
592
1010
  setExtractMode(true);
593
- setHead("Setup paused", "Skipped");
1011
+ setHead("Extraction ended for now", "Ended");
594
1012
  app.className = "";
595
1013
  app.innerHTML =
596
1014
  '<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() +
1015
+ '<div class="pauseStatus" role="status"><span aria-hidden="true">' + setupIcon("pause") + '</span><small>Extraction ended for now</small></div>' +
1016
+ '<h2 class="siteHeadline">You can come back <strong>anytime.</strong></h2>' +
1017
+ '<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>' +
1018
+ tryItCard() +
601
1019
  hudTip() +
602
1020
  '</div>';
603
- wireRecallGate();
1021
+ wireTryButtons();
604
1022
  }
605
1023
  async function endExtraction() {
606
1024
  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.");
1025
+ 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
1026
  if (!ok) return;
1027
+ extractionRecoveryMessage = "";
609
1028
  extractionEnded = true;
610
1029
  renderEnding();
611
1030
  try {
@@ -614,18 +1033,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
614
1033
  renderSkipped();
615
1034
  } catch (error) {
616
1035
  // 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.
1036
+ // conversations will be scheduled. Restore the last live extraction state immediately;
1037
+ // do not depend on another bridge request before leaving the ending screen.
618
1038
  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
- }
1039
+ var message = error && error.message ? error.message : "Could not end extraction. It may still be running.";
1040
+ extractionRecoveryMessage = message;
1041
+ extractMounted = false;
1042
+ renderProgress(lastExtractionProgress || {
1043
+ status: "running",
1044
+ total: 0,
1045
+ completed: 0,
1046
+ failed: 0,
1047
+ extracted: 0,
1048
+ latest: "Extraction is still running."
1049
+ });
1050
+ // Rejoin live updates. If the bridge is temporarily unavailable, the restored page and
1051
+ // End for now button remain usable instead of being replaced by another blocking state.
1052
+ void pollProgress().catch(function () {});
629
1053
  }
630
1054
  }
631
1055
  async function skip() {