@echomem/mcp 1.4.26 → 1.4.27

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.
@@ -48,30 +48,31 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
48
48
  return envelope;
49
49
  }
50
50
  function renderReportIssue(code, message) {
51
+ void message;
51
52
  report = null;
52
53
  reportEnvelope = null;
53
54
  resetReportSurface();
54
- setHead("Your local report could not be shown", "Needs attention");
55
- app.className = "loading-panel";
55
+ setHead("We couldn't finish this scan", "Needs attention");
56
+ app.className = "reportMessageStage";
56
57
  app.innerHTML =
57
- '<div style="padding:24px" data-report-state="failed">' +
58
- '<h2>No backup or sample data was shown</h2>' +
59
- '<p class="helper" style="margin-top:10px">' + esc(message || "The local scan did not produce a report that passed validation.") + '</p>' +
60
- '<div class="warning" style="margin-top:16px;margin-bottom:0"><strong>Error ' + esc(code || "REPORT_FAILED") + '</strong><br />Close this tab, check the terminal, and run <code>echomem-mcp init</code> again.</div>' +
61
- '</div>';
58
+ '<section class="reportMessage" data-report-state="failed">' +
59
+ '<h2>We couldn’t finish this scan.</h2>' +
60
+ '<p>Your coding history is unchanged.</p>' +
61
+ '<p>Close this tab and run <code>echomem-mcp init</code> again.</p>' +
62
+ '<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(code || "REPORT_FAILED") + '</code></details>' +
63
+ '</section>';
62
64
  }
63
65
  function renderEmptyReport() {
64
66
  report = null;
65
67
  reportEnvelope = null;
66
68
  resetReportSurface();
67
69
  setHead("No local coding history found", "Local scan complete");
68
- app.className = "loading-panel";
70
+ app.className = "reportMessageStage";
69
71
  app.innerHTML =
70
- '<div style="padding:24px" data-report-state="empty">' +
71
- '<h2>There is no local Codex or Claude Code history to analyze yet</h2>' +
72
- '<p class="helper" style="margin-top:10px">Echo checked this machine and found no supported sessions with token usage. Start a coding session, then run <code>echomem-mcp init</code> again.</p>' +
73
- '<p class="helper" style="margin-top:10px"><strong>No demo data, percentages, savings, persona, or city was substituted.</strong></p>' +
74
- '</div>';
72
+ '<section class="reportMessage" data-report-state="empty">' +
73
+ '<h2>No coding history found.</h2>' +
74
+ '<p>Start a Codex or Claude Code session, then run <code>echomem-mcp init</code> again.</p>' +
75
+ '</section>';
75
76
  }
76
77
  function renderBridgeIssue() {
77
78
  renderReportIssue("BRIDGE_UNREACHABLE", "The local bridge stopped answering before your report was ready. Your terminal may have closed or the process may have stopped.");
@@ -153,6 +154,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
153
154
  readyStage = "plan";
154
155
  app.className = "planGateStage";
155
156
  app.innerHTML =
157
+ setupJourney(3) +
156
158
  '<section class="planGateDecision">' +
157
159
  '<div class="setupPlanSlot" id="setupPlan"></div>' +
158
160
  '<aside class="planGateAccount" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></aside>' +
@@ -193,12 +195,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
193
195
  ? "Counting your <strong>new conversations…</strong>"
194
196
  : (knownDone
195
197
  ? "You are <strong>all caught up.</strong>"
196
- : "Echo found <strong>" + esc(number(pendN)) + " new coding session" + (pendN === 1 ? "" : "s") + ".</strong>");
198
+ : "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review.");
197
199
  var sub = !pendingTrusted
198
200
  ? "Echo is matching your local history against what is already in memory."
199
201
  : (knownDone
200
202
  ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
201
- : "Turn them into memory your agents can recall: decisions, fixes, and where you left off. No more re-explaining your project every session.");
203
+ : "Nothing uploads until you confirm.");
202
204
  var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start memory extraction" : "All caught up");
203
205
  // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
204
206
  // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
@@ -214,9 +216,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
214
216
  // one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
215
217
  // not a display-size headline.
216
218
  app.innerHTML =
219
+ setupJourney(4) +
217
220
  '<div id="exBanner"></div>' +
218
221
  '<section class="readyDecision extractionReady">' +
219
- '<p class="utilityEyebrow readyEyebrow">History import / review</p>' +
220
222
  '<div class="readyHero">' +
221
223
  '<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
222
224
  '<div class="exCopy">' +
@@ -225,10 +227,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
225
227
  '</div>' +
226
228
  '</div>' +
227
229
  '<div class="proofStrip" id="exSources"></div>' +
230
+ '<div class="billingCommitmentSlot" id="billingCommitment"></div>' +
228
231
  '<div class="readyGo">' +
229
232
  '<div class="actions">' +
230
233
  '<button id="migrate" class="primary"></button>' +
231
- '<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip for now</button>' +
232
234
  '</div>' +
233
235
  '<p class="ctaMeta" id="exEta"></p>' +
234
236
  '</div>' +
@@ -236,9 +238,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
236
238
  '<div class="readySetting readyAccountSetting"><span class="readySettingLabel">Account</span><div class="setupAccountSlot" id="setupAccount"></div></div>' +
237
239
  '<div class="readySetting readyPlanSetting"><span class="readySettingLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
238
240
  '</aside>' +
239
- '<div class="readyFoot">' +
240
- '<details class="readyExplain" id="readyExplain"></details>' +
241
- '</div>' +
242
241
  '</section>' +
243
242
  '<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
244
243
  document.getElementById("migrate").onclick = openSessionPicker;
@@ -251,7 +250,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
251
250
  document.getElementById("exSub").textContent = sub;
252
251
  var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
253
252
  var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
254
- var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
255
253
  // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
256
254
  var srcIcon = function (id, fallback) {
257
255
  var asset = id === "claude-desktop" ? "claude" : "codex";
@@ -268,59 +266,54 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
268
266
  // Reassurances live at the moment of commitment — right under the button.
269
267
  document.getElementById("exEta").innerHTML = pendingTrusted
270
268
  ? (canExtract
271
- ? '<span class="cm">&asymp; ' + esc(etaLabel) + '</span><span class="cmDot">&middot;</span><span class="cm">runs locally</span>' + (alreadyN ? '<span class="cmDot">&middot;</span><span class="cm">' + esc(number(alreadyN)) + ' already-saved skipped</span>' : '')
269
+ ? '<span class="cm">&asymp; ' + esc(etaLabel) + '</span>'
272
270
  : 'Nothing new to extract right now.')
273
271
  : 'Echo is checking local history against saved memory&hellip;';
274
- document.getElementById("readyExplain").innerHTML =
275
- '<summary>What exactly happens?</summary>' +
276
- '<p>A coding session is a local Codex or Claude Code transcript from this Mac. Echo extracts compact memories from it, not a full chat replay.</p>' +
277
- '<div class="readySteps">' +
278
- '<div class="readyStep"><strong>Read locally</strong><span>Echo scans session logs on this device.</span></div>' +
279
- '<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
280
- '<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
281
- '</div>';
282
272
  renderSetupPlan(canExtract);
283
273
  renderAccountChoice();
284
274
  if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
285
275
  // While still counting, hide the primary button entirely; the headline + source split
286
276
  // already say it is working, and the ready state should present one clear action.
287
277
  var migrateBtn = document.getElementById("migrate");
288
- var skipHistoryBtn = document.getElementById("skipHistoryImport");
289
278
  if (pendingTrusted) {
290
279
  migrateBtn.style.display = "";
291
280
  if (canExtract) ensureSessionSelection();
292
281
  migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
293
282
  migrateBtn.innerHTML = '<span>' + esc(canExtract ? "Review " + number(pendN) + " sessions" : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
294
283
  migrateBtn.onclick = canExtract ? openSessionPicker : skip;
295
- if (skipHistoryBtn) {
296
- skipHistoryBtn.style.display = canExtract ? "" : "none";
297
- skipHistoryBtn.onclick = skip;
298
- }
299
284
  } else {
300
285
  migrateBtn.style.display = "none";
301
- if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
302
286
  }
303
287
  }
288
+ // The picker's confirm button checks this BEFORE closing the dialog. Closing first turned a
289
+ // blocked start into a silent bounce back to the dashboard — or all the way to the plan gate,
290
+ // because a billing refresh can flip setupPlanConfirmed() while the picker is open.
291
+ function migrateBlockedReason() {
292
+ if (!setupPlanConfirmed()) return "Choose Original, Pro, or Power to bring Echo home.";
293
+ if (!selectedSessionKeyList().length) return "Choose at least one conversation to extract.";
294
+ return "";
295
+ }
296
+ function setSessionPickerStatus(message) {
297
+ var status = document.getElementById("sessionPickerStatus");
298
+ if (status) status.textContent = message;
299
+ }
304
300
  async function startMigrate() {
305
301
  if (decisionMade) return;
306
- if (!setupPlanConfirmed()) {
307
- setSetupPlanStatus("Choose Original, Pro, or Power to bring Echo home.");
302
+ var blocked = migrateBlockedReason();
303
+ if (blocked) {
304
+ if (!setupPlanConfirmed()) setSetupPlanStatus(blocked);
305
+ else openSessionPicker();
306
+ setSessionPickerStatus(blocked);
308
307
  return;
309
308
  }
310
309
  var selectedKeys = selectedSessionKeyList();
311
- if (!selectedKeys.length) {
312
- openSessionPicker();
313
- var pickerStatus = document.getElementById("sessionPickerStatus");
314
- if (pickerStatus) pickerStatus.textContent = "Choose at least one conversation to extract.";
315
- return;
316
- }
317
310
  stopBillingPoll();
318
311
  decisionMade = true;
319
312
  extractionEnded = false;
320
313
  setCityMode(false);
321
314
  setHead("Extracting", "Starting");
322
315
  app.className = "";
323
- 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>';
316
+ app.innerHTML = '<div class="miniLoad"><h2>Starting extraction…</h2><div class="miniBar"><i></i></div></div>';
324
317
  var started;
325
318
  try {
326
319
  started = await postJson("/migrate", { conversationKeys: selectedKeys });
@@ -414,11 +407,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
414
407
  // each poll only rebuilds the text column.
415
408
  if (!extractMounted) {
416
409
  app.innerHTML =
410
+ '<div id="setupJourneySlot">' + setupJourney(4) + '</div>' +
417
411
  '<section class="exHero"><div class="exCopy" id="exText"></div>' +
418
412
  plateBlock() +
419
413
  '</section>';
420
414
  extractMounted = true;
421
415
  }
416
+ var journeySlot = document.getElementById("setupJourneySlot");
417
+ if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 5 : 4);
422
418
  var shell = app.querySelector(".exHero");
423
419
  if (shell) {
424
420
  shell.classList.toggle("exHeroResult", done || stopped);
@@ -439,27 +435,18 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
439
435
  : "";
440
436
  t.innerHTML =
441
437
  '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
442
- (done ? '<p class="utilityEyebrow">Memory extraction / complete</p>' : '<p class="utilityEyebrow">Memory extraction / stopped</p>') +
443
438
  '<div class="resultTop">' +
444
439
  '<div>' +
445
440
  (done
446
441
  ? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
447
442
  : '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
448
- '<p class="exSub">' + stopDetail + '</p>' +
449
- '<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
443
+ '<p class="exSub">' + stopDetail + '</p>') +
450
444
  '</div>' +
451
445
  '</div>' +
452
446
  (done ? tryItCard() : "") +
453
- (done ? "" :
454
- '<div class="memoryReceipt">' +
455
- '<p class="receiptK">What just happened</p>' +
456
- '<div class="finishStats">' +
457
- '<div class="finishStat"><strong>' + esc(number(completed)) + '</strong><span>' + esc(conversationNoun) + ' extracted</span></div>' +
458
- '<div class="finishStat"><strong>' + esc(number(extracted)) + '</strong><span>memories ready</span></div>' +
459
- '</div>' +
460
- '</div>') +
447
+ (done ? "" : '<p class="resultSummary">' + esc(number(extracted)) + ' ' + esc(memoryNoun) + ' saved · ' + esc(number(completed)) + ' ' + esc(conversationNoun) + ' processed</p>') +
461
448
  '</div>' +
462
- (done ? hudTip() + doneCloseNote() : "");
449
+ "";
463
450
  wireTryButtons();
464
451
  return;
465
452
  }
@@ -480,10 +467,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
480
467
  if (completed >= 5 && elapsed > 20) remainSec = (total - completed) / (completed / elapsed);
481
468
  else remainSec = Math.max(0, (exDeadline - now) / 1000);
482
469
  if (now > exDeadline) {
483
- overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected. Still working.</strong>' +
484
- 'Everything already processed is saved. Ending now is safe; the next run resumes from the remaining conversations.</div>';
470
+ overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected.</strong> Saved work is safe.</div>';
485
471
  } else {
486
- countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
472
+ countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes</p>';
487
473
  }
488
474
  }
489
475
  var recoveryNotice = extractionRecoveryMessage
@@ -491,15 +477,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
491
477
  : "";
492
478
  t.innerHTML = recoveryNotice +
493
479
  '<div class="workbench">' +
494
- '<p class="utilityEyebrow">Memory extraction / live</p>' +
495
480
  '<h2 class="siteHeadline">' + (preparing ? "Setting up the <strong>import.</strong>" : "Building your <strong>memory.</strong>") + '</h2>' +
496
481
  '<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>' +
497
482
  '<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
498
- '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
499
- '<div class="exCurrent"><i></i><div><span>Now handling</span><strong>' + esc(statusLine) + '</strong></div></div>' +
483
+ '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory saved" : "memories saved") + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
484
+ (String(statusLine).toLowerCase().indexOf("reconnecting") >= 0 ? '<p class="exActivity">' + esc(statusLine) + '</p>' : '') +
500
485
  countdownHtml + overdueHtml +
501
- '<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button><span>Ends cleanly; next time continues from saved work.</span></div>' +
502
- '<p class="exHint">You can step away from this page. Echo saves each finished conversation as it goes.</p>' +
486
+ '<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button></div>' +
503
487
  '</div>';
504
488
  var endBtn = document.getElementById("endExtraction");
505
489
  if (endBtn) endBtn.onclick = endExtraction;
@@ -602,21 +586,50 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
602
586
  }
603
587
  async function tryInCodex(prompt) {
604
588
  var copied = await copyRecallPrompt(prompt);
605
- setLaunchStatus(copied ? "Copied question. Opening Codex..." : "Opening Codex with your question...");
589
+ setLaunchStatus(copied ? "Opening Codex with your first recall…" : "Opening Codex with your first recall…");
590
+ startFirstRecallWatch();
606
591
  window.location.href = codexDeepLink(prompt);
607
592
  }
608
593
  async function openClaudeDesktop(prompt) {
609
594
  var copied = await copyRecallPrompt(prompt);
610
- setLaunchStatus(copied ? "Copied question. Opening Claude Desktop..." : "Opening Claude Desktop...");
595
+ setLaunchStatus(copied ? "Question copied. Opening Claude Desktop for your first recall…" : "Opening Claude Desktop for your first recall…");
596
+ startFirstRecallWatch();
611
597
  try {
612
598
  var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
613
- setLaunchStatus(res && res.message ? res.message : "Question copied. Paste it into Claude Desktop.");
599
+ setLaunchStatus(res && res.message ? res.message + " Send it there; this page will confirm when Echo remembers." : "Question copied. Send it in Claude Desktop; this page will confirm when Echo remembers.");
614
600
  } catch (error) {
615
601
  var msg = error && error.message ? error.message : "Could not auto-open Claude.";
616
602
  if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The question is copied - open Claude Desktop and paste it.";
617
603
  setLaunchStatus(msg);
618
604
  }
619
605
  }
606
+ function stopFirstRecallWatch() {
607
+ if (firstRecallPollTimer) window.clearInterval(firstRecallPollTimer);
608
+ firstRecallPollTimer = null;
609
+ }
610
+ async function checkFirstRecallActivation() {
611
+ if (!firstRecallPollStartedAt || Date.now() - firstRecallPollStartedAt > 120000) {
612
+ stopFirstRecallWatch();
613
+ return;
614
+ }
615
+ try {
616
+ var latest = await getJson("/billing-status");
617
+ if (latest && latest.activation && latest.activation.hasSearchedMemory === true) {
618
+ stopFirstRecallWatch();
619
+ document.title = "Echo remembered it — setup complete";
620
+ var card = document.querySelector(".tryCard");
621
+ if (card) card.classList.add("is-recalled");
622
+ setLaunchStatus("Echo remembered it. Setup complete.");
623
+ }
624
+ } catch (_) {}
625
+ }
626
+ function startFirstRecallWatch() {
627
+ firstRecallPollStartedAt = Date.now();
628
+ stopFirstRecallWatch();
629
+ firstRecallPollTimer = window.setInterval(function () {
630
+ void checkFirstRecallActivation();
631
+ }, 2500);
632
+ }
620
633
  function candidateSessions() {
621
634
  return stats && Array.isArray(stats.candidateSessions) ? stats.candidateSessions : [];
622
635
  }
@@ -792,20 +805,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
792
805
  : "";
793
806
  dialog.innerHTML =
794
807
  '<div class="sessionPickerShell">' +
795
- '<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>' +
808
+ '<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p>Up to ' + esc(number(limit)) + ' · newest selected first</p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
796
809
  '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
797
810
  '<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>' +
798
811
  '</div>' +
799
812
  '<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>' +
800
813
  '<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>' +
801
- '<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") + '>Start memory extraction</button></div></footer>' +
814
+ '<footer class="sessionPickerFoot"><div class="sessionPickerFootActions"><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Start memory extraction</button></div></footer>' +
802
815
  '</div>';
803
816
  var close = document.getElementById("closeSessionPicker");
804
817
  var confirm = document.getElementById("confirmSessions");
805
- var skipImport = document.getElementById("skipSessionImport");
806
818
  if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
807
- if (confirm) confirm.onclick = function () { dialog.close(); void startMigrate(); };
808
- if (skipImport) skipImport.onclick = skip;
819
+ if (confirm) confirm.onclick = function () {
820
+ var blocked = migrateBlockedReason();
821
+ if (blocked) { setSessionPickerStatus(blocked); return; }
822
+ dialog.close();
823
+ void startMigrate();
824
+ };
809
825
  wireSessionPickerUpgrade();
810
826
  var search = document.getElementById("sessionPickerSearch");
811
827
  if (search) search.oninput = function () {
@@ -872,13 +888,42 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
872
888
  '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Switch</button>' +
873
889
  '</section>';
874
890
  var change = document.getElementById("changeSetupAccount");
875
- if (change) change.onclick = switchSetupAccount;
891
+ if (change) change.onclick = renderAccountSwitchConfirm;
892
+ }
893
+ function renderAccountSwitchConfirm() {
894
+ var account = billingStatus && billingStatus.account;
895
+ var accountLabel = account && (account.email || account.displayName)
896
+ ? (account.email || account.displayName)
897
+ : "the connected account";
898
+ accountSwitchPending = true;
899
+ setCityMode(false);
900
+ setExtractMode(false);
901
+ setReadyMode(true);
902
+ setHead("Switch EchoMem account", "Confirm");
903
+ app.className = "accountSwitchStage";
904
+ app.innerHTML =
905
+ setupJourney(4) +
906
+ '<section class="accountSwitchConfirm" aria-labelledby="accountSwitchTitle">' +
907
+ '<button type="button" class="flowBack" id="cancelAccountSwitch">← Back</button>' +
908
+ '<h2 id="accountSwitchTitle">Switch account?</h2>' +
909
+ '<p><strong>' + esc(accountLabel) + '</strong> stays connected until you continue.</p>' +
910
+ '<button type="button" class="primary" id="confirmAccountSwitch">Sign out &amp; switch</button>' +
911
+ '<p class="localAuthStatus" id="localAuthStatus" role="status" aria-live="polite"></p>' +
912
+ '</section>';
913
+ var cancel = document.getElementById("cancelAccountSwitch");
914
+ if (cancel) cancel.onclick = function () {
915
+ accountSwitchPending = false;
916
+ renderDashboard();
917
+ };
918
+ var confirm = document.getElementById("confirmAccountSwitch");
919
+ if (confirm) confirm.onclick = function () { void switchSetupAccount(); };
876
920
  }
877
921
  async function switchSetupAccount() {
878
- var button = document.getElementById("changeSetupAccount");
922
+ var button = document.getElementById("confirmAccountSwitch");
879
923
  if (button) { button.disabled = true; button.textContent = "Signing out..."; }
880
924
  try {
881
925
  await postJson("/logout", {}, 12000);
926
+ accountSwitchPending = false;
882
927
  stats = null;
883
928
  billingStatus = null;
884
929
  connected = false;
@@ -889,13 +934,89 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
889
934
  localAuthAgeConfirmed = false;
890
935
  renderLocalLogin("");
891
936
  } catch (error) {
892
- if (button) { button.disabled = false; button.textContent = "Change account"; }
893
- setSetupPlanStatus(error && error.message ? error.message : "Could not switch accounts.");
937
+ if (button) { button.disabled = false; button.textContent = "Sign out & switch"; }
938
+ setLocalAuthStatus(error && error.message ? error.message : "Could not switch accounts.", "error");
894
939
  }
895
940
  }
896
941
  function paidRecallPlan(plan) {
897
942
  return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
898
943
  }
944
+ function setupPlanPrice(plan) {
945
+ return plan === "power" ? "$100" : "$20";
946
+ }
947
+ function formatBillingDate(value) {
948
+ if (typeof value !== "string" || !value) return "";
949
+ var parsed = new Date(value);
950
+ if (!isFinite(parsed.getTime())) return "";
951
+ try {
952
+ return new Intl.DateTimeFormat(undefined, {
953
+ year: "numeric",
954
+ month: "long",
955
+ day: "numeric"
956
+ }).format(parsed);
957
+ } catch (_) {
958
+ return parsed.toISOString().slice(0, 10);
959
+ }
960
+ }
961
+ function trialChargeDateText() {
962
+ var chargeAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
963
+ try {
964
+ return new Intl.DateTimeFormat(undefined, {
965
+ year: "numeric",
966
+ month: "long",
967
+ day: "numeric"
968
+ }).format(chargeAt);
969
+ } catch (_) {
970
+ return chargeAt.toISOString().slice(0, 10);
971
+ }
972
+ }
973
+ function paidPlanLifecycleText(plan) {
974
+ var state = String(billingStatus && billingStatus.subscriptionStatus || "").toLowerCase();
975
+ var trialEnd = formatBillingDate(billingStatus && billingStatus.trialEndedAt);
976
+ var periodEnd = formatBillingDate(billingStatus && billingStatus.currentPeriodEnd);
977
+ if (billingStatus && billingStatus.cancelAtPeriodEnd === true) {
978
+ var accessEnd = state === "trialing" ? (trialEnd || periodEnd) : periodEnd;
979
+ return accessEnd
980
+ ? "Canceled — active until " + accessEnd + ". It will not renew."
981
+ : "Canceled — active until the end of this billing period. It will not renew.";
982
+ }
983
+ if (state === "trialing") {
984
+ return trialEnd
985
+ ? "Trial active until " + trialEnd + ". Then " + setupPlanPrice(plan) + "/month unless canceled."
986
+ : "Trial active. Then " + setupPlanPrice(plan) + "/month unless canceled.";
987
+ }
988
+ if (state === "active" && periodEnd) return "Active — renews on " + periodEnd + ".";
989
+ return "";
990
+ }
991
+ function renderBillingCommitment(plan) {
992
+ var slot = document.getElementById("billingCommitment");
993
+ if (!slot) return;
994
+ if (!paidRecallPlan(plan)) {
995
+ slot.innerHTML = "";
996
+ slot.classList.remove("is-visible");
997
+ return;
998
+ }
999
+ var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
1000
+ var state = String(billingStatus && billingStatus.subscriptionStatus || "").toLowerCase();
1001
+ var canceled = !!(billingStatus && billingStatus.cancelAtPeriodEnd === true);
1002
+ var statusLabel = canceled ? "Canceled" : (state === "trialing" ? "Trial active" : "Active");
1003
+ var lifecycle = paidPlanLifecycleText(plan);
1004
+ var quota = billingStatus && billingStatus.historicalConversationQuota;
1005
+ var limit = quota && typeof quota.limit === "number" ? quota.limit : (plan === "power" ? 2000 : 500);
1006
+ var moneyFact = canceled
1007
+ ? "No future charge"
1008
+ : (state === "trialing" ? setupPlanPrice(plan) + "/month after trial" : setupPlanPrice(plan) + "/month");
1009
+ slot.classList.add("is-visible");
1010
+ slot.innerHTML =
1011
+ '<section class="billingCommitment' + (canceled ? ' is-canceled' : '') + '" aria-label="Current plan commitment">' +
1012
+ '<div class="billingCommitmentIdentity"><span>' + esc(statusLabel) + '</span><strong>' + esc(planLabel) + '</strong></div>' +
1013
+ '<div class="billingCommitmentFacts">' +
1014
+ '<span><b>' + esc(number(limit)) + '</b> coding-session imports</span>' +
1015
+ '<span><b>' + esc(moneyFact) + '</b></span>' +
1016
+ '</div>' +
1017
+ '<p>' + esc(lifecycle || "Your paid plan is active.") + '</p>' +
1018
+ '</section>';
1019
+ }
899
1020
  function setupPricingUrl(plan, startTrial) {
900
1021
  var paidPlan = plan === "power" ? "power" : "pro";
901
1022
  return (billingStatus && billingStatus.pricingUrl)
@@ -906,16 +1027,21 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
906
1027
  return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
907
1028
  }
908
1029
  function setupPlanDefinition(plan) {
1030
+ var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
909
1031
  if (plan === "power") return {
910
1032
  id: "power",
911
1033
  name: "Power",
912
1034
  homeName: "Power",
913
1035
  promise: "More room. More agents. Much more Echo.",
914
1036
  price: "$100",
915
- cadence: "per month · 14-day trial",
1037
+ cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
916
1038
  sticker: "/hud-assets/echo-pricing-power-sticker.png",
917
1039
  stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
918
- features: ["2,000 past chats", "agent-heavy memory", "2,000 context returns / week"]
1040
+ features: [
1041
+ "2,000 coding sessions",
1042
+ "agent-heavy memory",
1043
+ "2,000 memory recalls / week"
1044
+ ]
919
1045
  };
920
1046
  if (plan === "pro") return {
921
1047
  id: "pro",
@@ -923,11 +1049,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
923
1049
  homeName: "Pro",
924
1050
  promise: "A room for the work you return to every day.",
925
1051
  price: "$20",
926
- cadence: "per month · 14-day trial",
1052
+ cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
927
1053
  sticker: "/hud-assets/echo-pricing-pro-sticker.png",
928
1054
  stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
929
1055
  popular: true,
930
- features: ["500 past chats", "daily memory updates", "500 context returns / week"]
1056
+ features: [
1057
+ "500 coding sessions",
1058
+ "daily memory updates",
1059
+ "500 memory recalls / week"
1060
+ ]
931
1061
  };
932
1062
  return {
933
1063
  id: "free",
@@ -938,19 +1068,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
938
1068
  cadence: "free forever · no card needed",
939
1069
  sticker: "/hud-assets/echo-pricing-free-sticker.png",
940
1070
  stickerAlt: "Original Echo hugs one simple memory card.",
941
- features: ["100 past chats", "a few new chats / week", "100 context returns / week"]
1071
+ features: ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"]
942
1072
  };
943
1073
  }
944
- function setupDiscoveredSessionCount() {
945
- var pending = stats && stats.migratable && stats.migratable.pending;
946
- if (typeof pending === "number" && isFinite(pending)) return Math.max(0, pending);
947
- var total = stats && stats.sessions && stats.sessions.total;
948
- return typeof total === "number" && isFinite(total) ? Math.max(0, total) : 0;
949
- }
950
1074
  function renderPlanHabitat() {
951
1075
  setupPlanPreview = setupPlanPreview === "pro" || setupPlanPreview === "power" ? setupPlanPreview : "free";
952
1076
  var plan = setupPlanDefinition(setupPlanPreview);
953
- var discovered = setupDiscoveredSessionCount();
954
1077
  var tabs = ["free", "pro", "power"].map(function (id) {
955
1078
  var tab = setupPlanDefinition(id);
956
1079
  var selected = id === plan.id;
@@ -960,12 +1083,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
960
1083
  return '<li>' + setupIcon("check") + '<span>' + esc(feature) + '</span></li>';
961
1084
  }).join("");
962
1085
  var chooseId = plan.id === "free" ? "chooseFreePlan" : (plan.id === "power" ? "choosePowerPlan" : "chooseProPlan");
963
- var chooseLabel = plan.id === "free" ? "Take " + plan.homeName + " home" : "Start 14-day trial";
964
- var discovery = discovered > 0
965
- ? '<p class="mvpPricingDiscovery">Echo found <strong>' + esc(number(discovered)) + '</strong> new coding session' + (discovered === 1 ? "" : "s") + ' on this Mac.</p>'
966
- : "";
1086
+ var chooseLabel = plan.id === "free"
1087
+ ? "Continue with " + plan.homeName + " — Free"
1088
+ : ((!billingStatus || billingStatus.trialAvailable !== false)
1089
+ ? "Start " + plan.homeName + " trial — $0 today"
1090
+ : "Subscribe to " + plan.homeName);
967
1091
  return '<section class="mvpPricing" aria-labelledby="setupPlanHeading">' +
968
- '<header class="mvpPricingIntro"><span>Plans</span><h2 id="setupPlanHeading">Choose your Echo.</h2>' + discovery + '</header>' +
1092
+ '<header class="mvpPricingIntro"><h2 id="setupPlanHeading">Choose your Echo.</h2></header>' +
969
1093
  '<div class="mvpPlanTabs" role="tablist" aria-label="Choose an Echo plan">' + tabs + '</div>' +
970
1094
  '<article class="mvpPlanHabitat" id="activePlanHabitat" role="tabpanel" aria-labelledby="planTab-' + plan.id + '" data-plan="' + plan.id + '">' +
971
1095
  '<div class="mvpHabitatScene">' +
@@ -977,9 +1101,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
977
1101
  '<div class="mvpHabitatDetails">' +
978
1102
  '<div class="mvpPlanIdentity"><span>' + esc(plan.name) + '</span>' + (plan.popular ? "<b>Echo&rsquo;s pick</b>" : "") + '</div>' +
979
1103
  '<h3>' + esc(plan.homeName) + ' Echo</h3>' +
980
- '<p class="mvpPlanPromise">' + esc(plan.promise) + '</p>' +
981
1104
  '<div class="mvpPlanPrice"><strong>' + esc(plan.price) + '</strong><span>' + esc(plan.cadence) + '</span></div>' +
982
- '<p class="mvpPacksLabel">Echo packs</p>' +
983
1105
  '<ul class="mvpPlanFacts">' + facts + '</ul>' +
984
1106
  '<button type="button" class="primary mvpPlanCta" id="' + chooseId + '">' + esc(chooseLabel) + ' <span aria-hidden="true">→</span></button>' +
985
1107
  '</div>' +
@@ -1024,6 +1146,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1024
1146
  slot.innerHTML = "";
1025
1147
  slot.classList.add("is-hidden");
1026
1148
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1149
+ renderBillingCommitment("");
1027
1150
  return;
1028
1151
  }
1029
1152
  slot.classList.remove("is-hidden");
@@ -1042,12 +1165,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1042
1165
  var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
1043
1166
  var checkoutJustConfirmed = !!billingActivationPendingPlan;
1044
1167
  billingActivationPendingPlan = "";
1168
+ rememberCheckoutSessionId("");
1169
+ billingSyncAttempts = 0;
1045
1170
  var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
1046
1171
  var paidUsage = paidHistory && typeof paidHistory.used === "number" && typeof paidHistory.limit === "number"
1047
- ? paidHistory.used + " / " + paidHistory.limit + " bulk history imports used."
1048
- : "Your bulk history import and weekly new-chat allowance are ready.";
1172
+ ? paidHistory.used + " / " + paidHistory.limit + " coding sessions imported."
1173
+ : "Your coding-session import and weekly memory allowance are ready.";
1174
+ var lifecycleText = paidPlanLifecycleText(plan);
1175
+ var paidDetail = lifecycleText
1176
+ ? lifecycleText + (checkoutJustConfirmed ? "" : " " + paidUsage)
1177
+ : (checkoutJustConfirmed ? "Continue below." : paidUsage);
1049
1178
  if (checkoutJustConfirmed) document.title = "Echo plan active — continue setup";
1050
- slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Your Echo is ready. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(checkoutJustConfirmed ? "Continue below." : paidUsage) + '</span></span><button type="button" class="textButton" id="changeActivePlan">Change</button></div>';
1179
+ slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + (lifecycleText ? ' has-lifecycle' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Your Echo is ready. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(paidDetail) + '</span></span><button type="button" class="textButton" id="changeActivePlan">Change</button></div>';
1180
+ renderBillingCommitment(plan);
1051
1181
  var changeActivePlan = document.getElementById("changeActivePlan");
1052
1182
  if (changeActivePlan) changeActivePlan.onclick = openHostedPlanOptions;
1053
1183
  renderSessionSelection(true);
@@ -1056,7 +1186,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1056
1186
  }
1057
1187
  if (setupPlanChoice === "free") {
1058
1188
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1059
- slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 100 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1189
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 coding sessions and 100 memory recalls each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1190
+ renderBillingCommitment("");
1060
1191
  var changeBtn = document.getElementById("changeSetupPlan");
1061
1192
  if (changeBtn) changeBtn.onclick = function () {
1062
1193
  rememberSetupPlanChoice("");
@@ -1069,17 +1200,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1069
1200
  ? "power"
1070
1201
  : (setupPlanChoice === "pro" || billingActivationPendingPlan === "pro" ? "pro" : "");
1071
1202
  if (pendingPaidPlan) {
1203
+ renderBillingCommitment("");
1072
1204
  if (readySettings) {
1073
1205
  readySettings.classList.remove("is-plan-open");
1074
1206
  readySettings.classList.add("is-checkout-pending");
1075
1207
  }
1076
1208
  var selectedPaidPlan = pendingPaidPlan === "power" ? "Power" : "Pro";
1209
+ var pendingHasTrial = !billingStatus || billingStatus.trialAvailable !== false;
1210
+ var pendingPrice = pendingPaidPlan === "power" ? "$100" : "$20";
1211
+ var pendingLimit = pendingPaidPlan === "power" ? "2,000" : "500";
1212
+ var pendingStatus = billingCheckMessage || (billingLastCheckedAt
1213
+ ? "Checking automatically every few seconds · Last checked just now"
1214
+ : "Checking automatically every few seconds");
1077
1215
  slot.innerHTML =
1078
1216
  '<section class="setupPlanChoice setupPlanCheckout" aria-labelledby="setupPlanHeading">' +
1079
- '<div class="setupPlanIntro"><h3 id="setupPlanHeading">Start your 14-day trial.</h3><p>Finish in the secure Stripe tab, then return here. Echo will update automatically.</p></div>' +
1080
- '<button type="button" class="primary" id="continueSelectedPaidPlan">Reopen secure checkout</button>' +
1081
- '<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another Echo</button><span id="setupPlanStatus" role="status"></span></div>' +
1217
+ '<div class="setupPlanIntro"><h3 id="setupPlanHeading">' + esc(pendingHasTrial ? selectedPaidPlan + " trial — $0 today" : selectedPaidPlan + " subscription") + '</h3><p>Finish in Stripe. This page updates automatically.</p></div>' +
1218
+ '<dl class="checkoutCommitment">' +
1219
+ '<div><dt>Today</dt><dd>' + esc(pendingHasTrial ? "$0" : pendingPrice) + '</dd></div>' +
1220
+ '<div><dt>History import</dt><dd>' + esc(pendingLimit) + ' sessions</dd></div>' +
1221
+ '<div><dt>' + esc(pendingHasTrial ? "First charge" : "Billing") + '</dt><dd>' + esc(pendingHasTrial ? pendingPrice + " on " + trialChargeDateText() : pendingPrice + "/month now") + '</dd></div>' +
1222
+ '</dl>' +
1223
+ '<div class="checkoutActions"><button type="button" class="primary" id="continueSelectedPaidPlan">Reopen Stripe checkout</button><button type="button" class="secondary" id="checkPaymentStatus">Check payment status</button></div>' +
1224
+ '<p class="checkoutPollStatus" id="setupPlanStatus" role="status" aria-live="polite">' + esc(pendingStatus) + '</p>' +
1225
+ '<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another plan</button></div>' +
1082
1226
  '</section>';
1227
+ var checkPaid = document.getElementById("checkPaymentStatus");
1228
+ if (checkPaid) checkPaid.onclick = function () {
1229
+ billingCheckMessage = "Checking payment status…";
1230
+ renderSetupPlan(true);
1231
+ void refreshBillingStatus();
1232
+ };
1083
1233
  var continuePaid = document.getElementById("continueSelectedPaidPlan");
1084
1234
  if (continuePaid) continuePaid.onclick = function () { openPaidPricing(pendingPaidPlan); };
1085
1235
  var changePaid = document.getElementById("changeSelectedPaidPlan");
@@ -1099,19 +1249,43 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1099
1249
  readySettings.classList.add("is-plan-open");
1100
1250
  }
1101
1251
  slot.innerHTML = renderPlanHabitat();
1252
+ renderBillingCommitment("");
1102
1253
  bindPlanHabitat();
1103
1254
  if (migrateBtn) migrateBtn.disabled = true;
1104
1255
  }
1256
+ async function maybeSyncPendingCheckout() {
1257
+ if (!pendingCheckoutSessionId || billingSyncAttempts >= 12) return;
1258
+ var now = Date.now();
1259
+ if (billingSyncLastAttemptAt && now - billingSyncLastAttemptAt < 5000) return;
1260
+ billingSyncLastAttemptAt = now;
1261
+ billingSyncAttempts++;
1262
+ try {
1263
+ var result = await postJson(
1264
+ "/billing-checkout-sync",
1265
+ { sessionId: pendingCheckoutSessionId },
1266
+ 18000
1267
+ );
1268
+ if (result && result.ok === true) rememberCheckoutSessionId("");
1269
+ } catch (_) {
1270
+ // A 409 is expected while the hosted Checkout session is still open.
1271
+ // Webhooks remain primary; this bounded fallback repairs delayed delivery.
1272
+ }
1273
+ }
1105
1274
  async function refreshBillingStatus() {
1106
1275
  if (billingStatusLoading) return;
1107
1276
  billingStatusLoading = true;
1108
1277
  try {
1278
+ await maybeSyncPendingCheckout();
1109
1279
  billingStatus = await getJson("/billing-status");
1280
+ billingLastCheckedAt = Date.now();
1281
+ billingCheckMessage = "";
1110
1282
  renderSetupPlan(true);
1111
1283
  renderAccountChoice();
1112
1284
  renderSessionSelection(true);
1113
1285
  if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Original still works.");
1114
1286
  } catch (_) {
1287
+ billingLastCheckedAt = Date.now();
1288
+ billingCheckMessage = "Could not check payment yet. Echo will keep trying automatically.";
1115
1289
  setSetupPlanStatus("Plan check unavailable. Original still works.");
1116
1290
  } finally {
1117
1291
  billingStatusLoading = false;
@@ -1147,23 +1321,28 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1147
1321
  opened.location.href = response.url;
1148
1322
  setSetupPlanStatus(openedMessage);
1149
1323
  startBillingPoll();
1150
- return true;
1324
+ return response;
1151
1325
  } catch (error) {
1152
1326
  try { if (opened && !opened.closed) opened.close(); } catch (_) {}
1153
1327
  setSetupPlanStatus(error && error.message ? error.message : "Could not open secure Echo billing.");
1154
- return false;
1328
+ return null;
1155
1329
  }
1156
1330
  }
1157
1331
  async function openPaidPricing(plan) {
1158
1332
  setSetupPlanStatus("Opening secure checkout…");
1159
- var opened = await openHostedBilling(
1333
+ var checkout = await openHostedBilling(
1160
1334
  "/billing-checkout",
1161
1335
  { plan: plan, trial: !billingStatus || billingStatus.trialAvailable !== false },
1162
1336
  "Creating your secure checkout…",
1163
1337
  "Checkout opened in a secure tab. Keep this local page open; Echo will confirm here automatically."
1164
1338
  );
1165
- if (opened) {
1339
+ if (checkout) {
1166
1340
  billingActivationPendingPlan = plan;
1341
+ if (typeof checkout.sessionId === "string") {
1342
+ rememberCheckoutSessionId(checkout.sessionId);
1343
+ billingSyncAttempts = 0;
1344
+ billingSyncLastAttemptAt = 0;
1345
+ }
1167
1346
  document.title = "Finish checkout — Echo setup is waiting";
1168
1347
  renderSetupPlan(true);
1169
1348
  }
@@ -1178,25 +1357,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1178
1357
  }
1179
1358
  function tryItCard() {
1180
1359
  var questions = recallQuestions();
1181
- var prompts = questions.map(function (question, index) {
1182
- var numberLabel = String(index + 1).padStart(2, "0");
1183
- return '<article class="recallPrompt" data-recall-prompt="' + esc(question) + '">' +
1184
- '<div class="recallPromptHead">' +
1185
- '<span>Suggested prompt ' + numberLabel + '</span>' +
1186
- '<button type="button" class="recallCopy" data-recall-copy aria-label="Copy suggested prompt ' + (index + 1) + '">' + setupIcon("copy") + '<span>Copy</span></button>' +
1187
- '</div>' +
1188
- '<p class="recallPromptText">' + esc(question) + '</p>' +
1189
- '<div class="recallPromptActions" aria-label="Use suggested prompt ' + (index + 1) + '">' +
1190
- '<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>' +
1191
- '<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>' +
1192
- '</div>' +
1193
- '</article>';
1194
- }).join("");
1360
+ var primary = questions[0];
1195
1361
  return '<div class="tryCard">' +
1196
- '<p class="tryK">First recall</p>' +
1197
- '<h3 class="tryTitle">Try your new memory</h3>' +
1198
- '<p class="tryLead">These prompts are ready to use. Copy one, or ask it directly in your coding agent.</p>' +
1199
- '<div class="recallPromptList">' + prompts + '</div>' +
1362
+ '<h3 class="tryTitle">Let Echo prove it remembers</h3>' +
1363
+ '<article class="recallPrompt recallPromptPrimary" data-recall-prompt="' + esc(primary) + '">' +
1364
+ '<div class="recallPromptHead"><span>Your question</span><button type="button" class="recallCopy" data-recall-copy aria-label="Copy your first recall question">' + setupIcon("copy") + '<span>Copy</span></button></div>' +
1365
+ '<p class="recallPromptText">' + esc(primary) + '</p>' +
1366
+ '<div class="recallPromptActions" aria-label="Try your first memory recall">' +
1367
+ '<button type="button" class="recallAgent recallAgentPrimary" data-recall-codex><span class="recallAgentIcon"><img src="/hud-assets/codex.svg" alt="" /></span><span>Ask in Codex</span></button>' +
1368
+ '<button type="button" class="recallAgent is-claude" data-recall-claude><span class="recallAgentIcon"><img src="/hud-assets/claude.svg" alt="" /></span><span>Use Claude instead</span></button>' +
1369
+ '</div>' +
1370
+ '</article>' +
1200
1371
  '<p class="launchStatus" id="launchStatus"></p>' +
1201
1372
  '</div>';
1202
1373
  }
@@ -1211,12 +1382,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1211
1382
  if (claudeBtn) claudeBtn.addEventListener("click", function () { void openClaudeDesktop(prompt); });
1212
1383
  });
1213
1384
  }
1214
- function hudTip() {
1215
- 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>';
1216
- }
1217
- function doneCloseNote() {
1218
- return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
1219
- }
1220
1385
  function renderEnding() {
1221
1386
  setCityMode(false);
1222
1387
  setExtractMode(true);
@@ -1226,27 +1391,30 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1226
1391
  setHead("Ending extraction", "Ending");
1227
1392
  app.className = "";
1228
1393
  app.innerHTML =
1229
- '<div class="miniLoad">' +
1230
- '<p class="utilityEyebrow">Memory extraction / closing</p>' +
1231
- '<h2 class="siteHeadline">Stopping the <strong>remaining work</strong></h2>' +
1232
- '<p>Echo is canceling every queued conversation and releasing its unused reservations. Anything already saved stays saved.</p>' +
1233
- '<div class="miniBar"><i></i></div>' +
1234
- '</div>';
1394
+ setupJourney(4) +
1395
+ '<section class="lifecycleState lifecyclePending">' +
1396
+ '<h2>Stopping the remaining queued work…</h2>' +
1397
+ '<p>Saved memories stay intact.</p>' +
1398
+ '<div class="miniBar" aria-label="Ending extraction"><i></i></div>' +
1399
+ '</section>';
1235
1400
  }
1236
1401
  function renderSkipped() {
1237
1402
  setCityMode(false);
1238
1403
  setExtractMode(true);
1239
1404
  setHead("Extraction ended for now", "Ended");
1240
1405
  app.className = "";
1406
+ var savedCount = Math.max(0, Number(lastExtractionProgress && lastExtractionProgress.extracted || 0));
1241
1407
  app.innerHTML =
1242
- '<div class="skipStage">' +
1243
- '<div class="pauseStatus" role="status"><span aria-hidden="true">' + setupIcon("pause") + '</span><small>Extraction ended for now</small></div>' +
1244
- '<h2 class="siteHeadline">You can come back <strong>anytime.</strong></h2>' +
1245
- '<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>' +
1246
- tryItCard() +
1247
- hudTip() +
1248
- '</div>';
1249
- wireTryButtons();
1408
+ setupJourney(savedCount > 0 ? 5 : 4) +
1409
+ '<section class="lifecycleState lifecyclePaused">' +
1410
+ '<header class="lifecycleIntro">' +
1411
+ '<h2>' + (savedCount > 0 ? esc(number(savedCount)) + ' memories are ready to use.' : 'Import ended safely.') + '</h2>' +
1412
+ '<p>The remaining queue was canceled. Saved memories were not removed.</p>' +
1413
+ '</header>' +
1414
+ (savedCount > 0 ? tryItCard() : '') +
1415
+ '<p class="lifecycleResume">Continue later with <code>echomem-mcp init</code>.</p>' +
1416
+ '</section>';
1417
+ if (savedCount > 0) wireTryButtons();
1250
1418
  }
1251
1419
  async function endExtraction() {
1252
1420
  if (extractionEnded) return;