@echomem/mcp 1.4.26 → 1.4.28

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,34 +48,61 @@ 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.");
78
79
  }
80
+ function renderDiscoveryCheck(slow) {
81
+ void slow;
82
+ setCityMode(false);
83
+ setExtractMode(false);
84
+ setReadyMode(true);
85
+ setHead("Loading", "Working");
86
+ app.className = "discoveryLoader";
87
+ app.innerHTML = '<div class="discoverySpinner" role="status" aria-label="Loading"><span aria-hidden="true"></span></div>';
88
+ }
89
+ function renderDiscoveryCheckIssue(error) {
90
+ setCityMode(false);
91
+ setExtractMode(false);
92
+ setReadyMode(true);
93
+ setHead("Session refresh paused", "Needs attention");
94
+ app.className = "loading-panel";
95
+ app.innerHTML =
96
+ '<div style="padding:24px"><h2>Echo could not refresh session status</h2>' +
97
+ '<p class="helper">Keep the terminal open and try the local check again.</p>' +
98
+ (error ? '<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(error) + '</code></details>' : '') +
99
+ '<div class="actions"><button id="retryDiscoveryCheck" class="primary">Try again</button></div></div>';
100
+ var retry = document.getElementById("retryDiscoveryCheck");
101
+ if (retry) retry.onclick = function () {
102
+ statsPollStarted = false;
103
+ void waitForStats();
104
+ };
105
+ }
79
106
  function renderLogoutPending() {
80
107
  setCityMode(false);
81
108
  setHead("Signing out locally", "Working");
@@ -95,30 +122,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
95
122
  }
96
123
  async function waitForStats() {
97
124
  if (statsPollStarted) return;
125
+ var statsEpoch = accountStateEpoch;
98
126
  statsPollStarted = true;
99
127
  setCityMode(false);
100
128
  setExtractMode(false);
101
129
  setReadyMode(true);
102
- setHead("Checking extraction", "Working");
103
- app.className = "loading-panel";
104
- app.innerHTML = '<div style="padding:24px"><h2>Checking for active work</h2><p class="helper">Echo is reconnecting to any extraction already running on this machine.</p></div>';
130
+ renderDiscoveryCheck(false);
105
131
  try {
106
132
  if (await resumeExistingExtraction()) return;
133
+ if (statsEpoch !== accountStateEpoch || !connected) return;
107
134
  var started = Date.now();
108
135
  var lastError = "";
109
136
  for (;;) {
110
- if (decisionMade) return;
137
+ if (decisionMade || statsEpoch !== accountStateEpoch || !connected) return;
111
138
  try {
112
139
  var res = await fetchWithTimeout("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 3000);
140
+ if (statsEpoch !== accountStateEpoch || !connected) return;
113
141
  if (res.status === 200) {
114
- stats = await res.json();
115
- if (decisionMade) return;
142
+ var nextStats = await res.json();
143
+ if (decisionMade || statsEpoch !== accountStateEpoch || !connected) return;
144
+ stats = nextStats;
116
145
  if (connected && !billingStatus && !billingStatusLoading) {
117
146
  try { await refreshBillingStatus(); } catch (_) {}
118
- if (decisionMade) return;
147
+ if (decisionMade || statsEpoch !== accountStateEpoch || !connected) return;
119
148
  }
120
149
  renderDashboard();
121
- if (stats && stats.partial) { await sleep(1200); continue; }
150
+ if (stats && stats.partial) {
151
+ await sleep(1200);
152
+ if (statsEpoch !== accountStateEpoch || !connected) return;
153
+ continue;
154
+ }
122
155
  return;
123
156
  }
124
157
  if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
@@ -153,6 +186,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
153
186
  readyStage = "plan";
154
187
  app.className = "planGateStage";
155
188
  app.innerHTML =
189
+ setupJourney(3) +
156
190
  '<section class="planGateDecision">' +
157
191
  '<div class="setupPlanSlot" id="setupPlan"></div>' +
158
192
  '<aside class="planGateAccount" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></aside>' +
@@ -170,7 +204,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
170
204
  var discovery = stats && stats.discovery ? stats.discovery : {};
171
205
  var isPartial = !!(stats && stats.partial);
172
206
  var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
173
- var eta = migratable && migratable.eta ? migratable.eta : {};
174
207
  var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
175
208
  var sessions = stats && stats.sessions ? stats.sessions : {};
176
209
  // The "quick" phase counts only this device's local ledger; it does not know what the account
@@ -181,25 +214,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
181
214
  var knownDone = pendingTrusted && pending === 0;
182
215
  var canExtract = pendingTrusted && pending > 0;
183
216
  var pendN = hasPendingCount ? pending : 0;
184
- var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
185
217
  var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
186
218
  var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
187
219
  if (connected && !billingStatusLoading && !setupPlanConfirmed()) {
188
220
  renderPlanGate();
189
221
  return;
190
222
  }
223
+ var importableNow = canExtract ? Math.min(pendN, historicalSelectionLimit()) : 0;
224
+ var planLimited = canExtract && importableNow < pendN;
225
+ var deferredByPlan = planLimited ? pendN - importableNow : 0;
226
+ var currentPlan = String(billingStatus && billingStatus.plan || "").toLowerCase();
227
+ var currentPlanLabel = paidRecallPlan(currentPlan)
228
+ ? currentPlan.charAt(0).toUpperCase() + currentPlan.slice(1) + " Echo"
229
+ : "Original Echo";
191
230
  setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
192
231
  var headlineHtml = !pendingTrusted
193
232
  ? "Counting your <strong>new conversations…</strong>"
194
233
  : (knownDone
195
234
  ? "You are <strong>all caught up.</strong>"
196
- : "Echo found <strong>" + esc(number(pendN)) + " new coding session" + (pendN === 1 ? "" : "s") + ".</strong>");
235
+ : (planLimited
236
+ ? "<strong>" + esc(number(pendN)) + " sessions</strong> found."
237
+ : "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review."));
197
238
  var sub = !pendingTrusted
198
239
  ? "Echo is matching your local history against what is already in memory."
199
- : (knownDone
240
+ : (knownDone
200
241
  ? (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.");
202
- var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start memory extraction" : "All caught up");
242
+ : (planLimited
243
+ ? (importableNow > 0
244
+ ? currentPlanLabel + " can import " + number(importableNow) + " of them. Choose which ones to bring into memory; the other " + number(deferredByPlan) + " stay on this Mac."
245
+ : currentPlanLabel + " has no coding-session imports remaining. These sessions stay on this Mac.")
246
+ : ""));
203
247
  // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
204
248
  // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
205
249
  // Later polls only patch the text/counts in place; the iframe stays mounted.
@@ -214,9 +258,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
214
258
  // one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
215
259
  // not a display-size headline.
216
260
  app.innerHTML =
261
+ setupJourney(4) +
262
+ '<aside class="readyUtilityNav" aria-label="Account and plan settings">' +
263
+ '<div class="planGateAccount readyAccountNav" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></div>' +
264
+ '<div class="readyPlanNav" aria-label="Current plan"><span class="readyPlanNavLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
265
+ '</aside>' +
217
266
  '<div id="exBanner"></div>' +
218
267
  '<section class="readyDecision extractionReady">' +
219
- '<p class="utilityEyebrow readyEyebrow">History import / review</p>' +
220
268
  '<div class="readyHero">' +
221
269
  '<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
222
270
  '<div class="exCopy">' +
@@ -225,20 +273,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
225
273
  '</div>' +
226
274
  '</div>' +
227
275
  '<div class="proofStrip" id="exSources"></div>' +
276
+ '<div class="billingCommitmentSlot" id="billingCommitment"></div>' +
228
277
  '<div class="readyGo">' +
229
278
  '<div class="actions">' +
230
279
  '<button id="migrate" class="primary"></button>' +
231
- '<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip for now</button>' +
232
280
  '</div>' +
233
281
  '<p class="ctaMeta" id="exEta"></p>' +
234
282
  '</div>' +
235
- '<aside class="readySettings" aria-label="Account and plan settings">' +
236
- '<div class="readySetting readyAccountSetting"><span class="readySettingLabel">Account</span><div class="setupAccountSlot" id="setupAccount"></div></div>' +
237
- '<div class="readySetting readyPlanSetting"><span class="readySettingLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
238
- '</aside>' +
239
- '<div class="readyFoot">' +
240
- '<details class="readyExplain" id="readyExplain"></details>' +
241
- '</div>' +
242
283
  '</section>' +
243
284
  '<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
244
285
  document.getElementById("migrate").onclick = openSessionPicker;
@@ -251,7 +292,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
251
292
  document.getElementById("exSub").textContent = sub;
252
293
  var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
253
294
  var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
254
- var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
255
295
  // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
256
296
  var srcIcon = function (id, fallback) {
257
297
  var asset = id === "claude-desktop" ? "claude" : "codex";
@@ -268,59 +308,75 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
268
308
  // Reassurances live at the moment of commitment — right under the button.
269
309
  document.getElementById("exEta").innerHTML = pendingTrusted
270
310
  ? (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>' : '')
311
+ ? ''
272
312
  : 'Nothing new to extract right now.')
273
313
  : '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
314
  renderSetupPlan(canExtract);
283
315
  renderAccountChoice();
284
316
  if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
285
317
  // While still counting, hide the primary button entirely; the headline + source split
286
318
  // already say it is working, and the ready state should present one clear action.
287
319
  var migrateBtn = document.getElementById("migrate");
288
- var skipHistoryBtn = document.getElementById("skipHistoryImport");
289
320
  if (pendingTrusted) {
290
321
  migrateBtn.style.display = "";
291
- if (canExtract) ensureSessionSelection();
292
- migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
293
- migrateBtn.innerHTML = '<span>' + esc(canExtract ? "Review " + number(pendN) + " sessions" : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
322
+ var candidatesReady = !canExtract || candidateSessions().length > 0;
323
+ if (canExtract && candidatesReady) ensureSessionSelection();
324
+ // This button opens the picker; zero selected belongs to the picker's Start button.
325
+ migrateBtn.disabled = canExtract && (
326
+ !setupPlanConfirmed() ||
327
+ !candidatesReady ||
328
+ historicalSelectionLimit() === 0
329
+ );
330
+ migrateBtn.innerHTML = '<span>' + esc(
331
+ canExtract
332
+ ? (candidatesReady
333
+ ? (importableNow === 0
334
+ ? "No imports available"
335
+ : (planLimited
336
+ ? "Choose " + number(importableNow) + " of " + number(pendN) + " sessions"
337
+ : "Review " + number(pendN) + " sessions"))
338
+ : "Preparing session list…")
339
+ : "Finish setup"
340
+ ) + '</span>' + (canExtract && candidatesReady ? setupIcon("arrow-right") : "");
294
341
  migrateBtn.onclick = canExtract ? openSessionPicker : skip;
295
- if (skipHistoryBtn) {
296
- skipHistoryBtn.style.display = canExtract ? "" : "none";
297
- skipHistoryBtn.onclick = skip;
298
- }
299
342
  } else {
300
343
  migrateBtn.style.display = "none";
301
- if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
302
344
  }
303
345
  }
346
+ // The picker's confirm button checks this BEFORE closing the dialog. Closing first turned a
347
+ // blocked start into a silent bounce back to the dashboard — or all the way to the plan gate,
348
+ // because a billing refresh can flip setupPlanConfirmed() while the picker is open.
349
+ function migrateBlockedReason() {
350
+ if (!setupPlanConfirmed()) return "Choose Original, Pro, or Power to bring Echo home.";
351
+ if (!selectedSessionKeyList().length) return "Choose at least one conversation to extract.";
352
+ return "";
353
+ }
354
+ function setSessionPickerStatus(message) {
355
+ var status = document.getElementById("sessionPickerStatus");
356
+ if (status) status.textContent = message;
357
+ }
304
358
  async function startMigrate() {
305
359
  if (decisionMade) return;
306
- if (!setupPlanConfirmed()) {
307
- setSetupPlanStatus("Choose Original, Pro, or Power to bring Echo home.");
360
+ var blocked = migrateBlockedReason();
361
+ if (blocked) {
362
+ if (!setupPlanConfirmed()) setSetupPlanStatus(blocked);
363
+ else openSessionPicker();
364
+ setSessionPickerStatus(blocked);
308
365
  return;
309
366
  }
310
367
  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
368
  stopBillingPoll();
318
369
  decisionMade = true;
319
370
  extractionEnded = false;
320
371
  setCityMode(false);
372
+ setScanMode(false);
373
+ setReadyMode(false);
374
+ setExtractMode(true);
321
375
  setHead("Extracting", "Starting");
322
- 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>';
376
+ app.className = "extractionStartStage";
377
+ app.innerHTML =
378
+ setupJourney(4) +
379
+ '<div class="discoverySpinner" role="status" aria-label="Starting import"><span aria-hidden="true"></span></div>';
324
380
  var started;
325
381
  try {
326
382
  started = await postJson("/migrate", { conversationKeys: selectedKeys });
@@ -334,8 +390,34 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
334
390
  await pollProgress();
335
391
  }
336
392
  async function resumeExistingExtraction() {
393
+ var resumeEpoch = accountStateEpoch;
394
+ var existing = null;
395
+ var lastError = "";
396
+ for (var attempt = 0; attempt < 3; attempt++) {
397
+ try {
398
+ var progressResponse = await fetchWithTimeout(
399
+ "/progress?nonce=" + encodeURIComponent(nonce),
400
+ { credentials: "omit", cache: "no-store" },
401
+ 3000
402
+ );
403
+ if (!progressResponse.ok) throw new Error(await progressResponse.text() || ("HTTP " + progressResponse.status));
404
+ existing = await progressResponse.json();
405
+ break;
406
+ } catch (error) {
407
+ if (resumeEpoch !== accountStateEpoch || !connected) return false;
408
+ lastError = error && error.message ? error.message : String(error);
409
+ if (attempt < 2) {
410
+ renderDiscoveryCheck(true);
411
+ await sleep(500 * (attempt + 1));
412
+ }
413
+ }
414
+ }
415
+ if (!existing) {
416
+ renderDiscoveryCheckIssue(lastError);
417
+ return true;
418
+ }
337
419
  try {
338
- var existing = await getJson("/progress");
420
+ if (resumeEpoch !== accountStateEpoch || !connected) return false;
339
421
  if (!existing || existing.status === "idle") return false;
340
422
  decisionMade = true;
341
423
  extractionEnded = false;
@@ -345,21 +427,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
345
427
  void pollProgress();
346
428
  }
347
429
  return true;
348
- } catch (_) {
349
- // Stats cannot prove that no import is running. Stay in the recovery view until
350
- // the authoritative progress endpoint answers instead of exposing a second start.
351
- decisionMade = true;
352
- extractionEnded = false;
353
- extractMounted = false;
354
- renderProgress({
355
- status: "running",
356
- total: 0,
357
- completed: 0,
358
- failed: 0,
359
- extracted: 0,
360
- latest: "Reconnecting to the active extraction..."
361
- });
362
- void pollProgress();
430
+ } catch (error) {
431
+ renderDiscoveryCheckIssue(error && error.message ? error.message : String(error));
363
432
  return true;
364
433
  }
365
434
  }
@@ -414,11 +483,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
414
483
  // each poll only rebuilds the text column.
415
484
  if (!extractMounted) {
416
485
  app.innerHTML =
486
+ '<div id="setupJourneySlot">' + setupJourney(4) + '</div>' +
417
487
  '<section class="exHero"><div class="exCopy" id="exText"></div>' +
418
488
  plateBlock() +
419
489
  '</section>';
420
490
  extractMounted = true;
421
491
  }
492
+ var journeySlot = document.getElementById("setupJourneySlot");
493
+ if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 5 : 4);
422
494
  var shell = app.querySelector(".exHero");
423
495
  if (shell) {
424
496
  shell.classList.toggle("exHeroResult", done || stopped);
@@ -438,28 +510,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
438
510
  ? '<p class="exSub">' + esc(progress.latest) + '</p>'
439
511
  : "";
440
512
  t.innerHTML =
441
- '<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
442
- (done ? '<p class="utilityEyebrow">Memory extraction / complete</p>' : '<p class="utilityEyebrow">Memory extraction / stopped</p>') +
513
+ '<div class="resultStage ' + (done ? "firstRecallStage ok" : "warn") + '">' +
443
514
  '<div class="resultTop">' +
444
515
  '<div>' +
445
516
  (done
446
- ? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
517
+ ? '<h2 class="savedHeading"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
447
518
  : '<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>') +
519
+ '<p class="exSub">' + stopDetail + '</p>') +
450
520
  '</div>' +
451
521
  '</div>' +
452
522
  (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>') +
523
+ (done ? "" : '<p class="resultSummary">' + esc(number(extracted)) + ' ' + esc(memoryNoun) + ' saved · ' + esc(number(completed)) + ' ' + esc(conversationNoun) + ' processed</p>') +
461
524
  '</div>' +
462
- (done ? hudTip() + doneCloseNote() : "");
525
+ "";
463
526
  wireTryButtons();
464
527
  return;
465
528
  }
@@ -480,10 +543,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
480
543
  if (completed >= 5 && elapsed > 20) remainSec = (total - completed) / (completed / elapsed);
481
544
  else remainSec = Math.max(0, (exDeadline - now) / 1000);
482
545
  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>';
546
+ overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected.</strong> Saved work is safe.</div>';
485
547
  } else {
486
- countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
548
+ countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes</p>';
487
549
  }
488
550
  }
489
551
  var recoveryNotice = extractionRecoveryMessage
@@ -491,15 +553,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
491
553
  : "";
492
554
  t.innerHTML = recoveryNotice +
493
555
  '<div class="workbench">' +
494
- '<p class="utilityEyebrow">Memory extraction / live</p>' +
495
556
  '<h2 class="siteHeadline">' + (preparing ? "Setting up the <strong>import.</strong>" : "Building your <strong>memory.</strong>") + '</h2>' +
496
557
  '<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
558
  '<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>' +
559
+ '<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory saved" : "memories saved") + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
560
+ (String(statusLine).toLowerCase().indexOf("reconnecting") >= 0 ? '<p class="exActivity">' + esc(statusLine) + '</p>' : '') +
500
561
  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>' +
562
+ '<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button></div>' +
503
563
  '</div>';
504
564
  var endBtn = document.getElementById("endExtraction");
505
565
  if (endBtn) endBtn.onclick = endExtraction;
@@ -602,21 +662,50 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
602
662
  }
603
663
  async function tryInCodex(prompt) {
604
664
  var copied = await copyRecallPrompt(prompt);
605
- setLaunchStatus(copied ? "Copied question. Opening Codex..." : "Opening Codex with your question...");
665
+ setLaunchStatus(copied ? "Opening Codex with your first recall…" : "Opening Codex with your first recall…");
666
+ startFirstRecallWatch();
606
667
  window.location.href = codexDeepLink(prompt);
607
668
  }
608
669
  async function openClaudeDesktop(prompt) {
609
670
  var copied = await copyRecallPrompt(prompt);
610
- setLaunchStatus(copied ? "Copied question. Opening Claude Desktop..." : "Opening Claude Desktop...");
671
+ setLaunchStatus(copied ? "Question copied. Opening Claude Desktop for your first recall…" : "Opening Claude Desktop for your first recall…");
672
+ startFirstRecallWatch();
611
673
  try {
612
674
  var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
613
- setLaunchStatus(res && res.message ? res.message : "Question copied. Paste it into Claude Desktop.");
675
+ 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
676
  } catch (error) {
615
677
  var msg = error && error.message ? error.message : "Could not auto-open Claude.";
616
678
  if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The question is copied - open Claude Desktop and paste it.";
617
679
  setLaunchStatus(msg);
618
680
  }
619
681
  }
682
+ function stopFirstRecallWatch() {
683
+ if (firstRecallPollTimer) window.clearInterval(firstRecallPollTimer);
684
+ firstRecallPollTimer = null;
685
+ }
686
+ async function checkFirstRecallActivation() {
687
+ if (!firstRecallPollStartedAt || Date.now() - firstRecallPollStartedAt > 120000) {
688
+ stopFirstRecallWatch();
689
+ return;
690
+ }
691
+ try {
692
+ var latest = await getJson("/billing-status");
693
+ if (latest && latest.activation && latest.activation.hasSearchedMemory === true) {
694
+ stopFirstRecallWatch();
695
+ document.title = "Echo remembered it — setup complete";
696
+ var card = document.querySelector(".tryCard");
697
+ if (card) card.classList.add("is-recalled");
698
+ setLaunchStatus("Echo remembered it. Setup complete.");
699
+ }
700
+ } catch (_) {}
701
+ }
702
+ function startFirstRecallWatch() {
703
+ firstRecallPollStartedAt = Date.now();
704
+ stopFirstRecallWatch();
705
+ firstRecallPollTimer = window.setInterval(function () {
706
+ void checkFirstRecallActivation();
707
+ }, 2500);
708
+ }
620
709
  function candidateSessions() {
621
710
  return stats && Array.isArray(stats.candidateSessions) ? stats.candidateSessions : [];
622
711
  }
@@ -701,7 +790,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
701
790
  var edit = document.getElementById("editSessions");
702
791
  if (edit) edit.onclick = openSessionPicker;
703
792
  var migrateBtn = document.getElementById("migrate");
704
- if (migrateBtn) migrateBtn.disabled = selected.length === 0 || !setupPlanConfirmed();
793
+ if (migrateBtn) migrateBtn.disabled =
794
+ candidates.length === 0 ||
795
+ !setupPlanConfirmed() ||
796
+ historicalSelectionLimit() === 0;
705
797
  }
706
798
  function updateSessionPickerSelectionUi(dialog, input, limit) {
707
799
  var selectedCount = selectedSessionKeyList().length;
@@ -727,13 +819,46 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
727
819
  '<span class="sessionPickerLimitEyebrow">Plan limit</span>' +
728
820
  '<h3>Plan limit reached.</h3>' +
729
821
  '<p>Upgrade your plan to select and import more conversations.</p>' +
730
- '<div class="sessionPickerLimitActions"><a id="upgradeSessionPlan" class="button primary" href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">' + linkLabel + '</a>' +
822
+ '<div class="sessionPickerLimitActions"><button type="button" id="upgradeSessionPlan" class="button primary">' + linkLabel + '</button>' +
731
823
  (canDismiss ? '<button type="button" class="textButton" id="dismissSessionLimit">Keep editing</button>' : "") + '</div>' +
732
824
  '</div>';
733
825
  }
826
+ async function openSessionPickerPlanAction() {
827
+ var currentPlan = String(billingStatus && billingStatus.plan || "").toLowerCase();
828
+ if (currentPlan === "power" || currentPlan === "team" || currentPlan === "enterprise") {
829
+ setSessionPickerStatus("Opening secure plan options…");
830
+ var portal = await openHostedPlanOptions();
831
+ setSessionPickerStatus(portal
832
+ ? "Plan options opened in Stripe. This page will update automatically."
833
+ : "Could not open plan options. Try again.");
834
+ return;
835
+ }
836
+ var targetPlan = currentPlan === "pro" ? "power" : "pro";
837
+ setSessionPickerStatus("Opening secure Stripe checkout…");
838
+ var checkout = await openHostedBilling(
839
+ "/billing-checkout",
840
+ { plan: targetPlan, trial: !billingStatus || billingStatus.trialAvailable !== false },
841
+ "Creating your secure checkout…",
842
+ "Checkout opened in Stripe. Keep this local page open; Echo will confirm here automatically."
843
+ );
844
+ if (!checkout) {
845
+ setSessionPickerStatus("Could not open Stripe checkout. Try again.");
846
+ return;
847
+ }
848
+ billingActivationPendingPlan = targetPlan;
849
+ if (typeof checkout.sessionId === "string") {
850
+ rememberCheckoutSessionId(checkout.sessionId);
851
+ billingSyncAttempts = 0;
852
+ billingSyncLastAttemptAt = 0;
853
+ }
854
+ document.title = "Finish checkout — Echo setup is waiting";
855
+ setSessionPickerStatus("Stripe checkout opened. Return here when you finish; this page updates automatically.");
856
+ }
734
857
  function wireSessionPickerUpgrade() {
735
- var upgrade = document.getElementById("upgradeSessionPlan");
736
- if (upgrade) upgrade.onclick = function () { startBillingPoll(); };
858
+ ["upgradeSessionPlan", "upgradeSessionPlanInline"].forEach(function (id) {
859
+ var upgrade = document.getElementById(id);
860
+ if (upgrade) upgrade.onclick = function () { void openSessionPickerPlanAction(); };
861
+ });
737
862
  var dismiss = document.getElementById("dismissSessionLimit");
738
863
  if (dismiss) dismiss.onclick = hideSessionPickerLimitNotice;
739
864
  }
@@ -767,6 +892,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
767
892
  var all = candidateSessions();
768
893
  var selected = selectedSessionKeyList();
769
894
  var limit = Math.min(all.length, historicalSelectionLimit());
895
+ var planLimited = all.length > limit;
896
+ var inlineUpgrade = planLimited && limit > 0
897
+ ? '<button type="button" id="upgradeSessionPlanInline" class="sessionPickerUpgrade">Upgrade</button>'
898
+ : "";
770
899
  var query = sessionPickerQuery.trim().toLowerCase();
771
900
  var filtered = all.filter(function (session) {
772
901
  if (sessionPickerSource !== "all" && session.source !== sessionPickerSource) return false;
@@ -792,20 +921,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
792
921
  : "";
793
922
  dialog.innerHTML =
794
923
  '<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>' +
924
+ '<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p class="sessionPickerSummary"><span class="sessionPickerQuota">Up to <strong>' + esc(number(limit)) + '</strong></span>' + inlineUpgrade + '<span class="sessionPickerOrder">&middot; newest selected first</span></p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
796
925
  '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
797
926
  '<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
927
  '</div>' +
799
928
  '<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
929
  '<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>' +
930
+ '<footer class="sessionPickerFoot"><div class="sessionPickerFootActions"><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Start memory extraction</button></div></footer>' +
802
931
  '</div>';
803
932
  var close = document.getElementById("closeSessionPicker");
804
933
  var confirm = document.getElementById("confirmSessions");
805
- var skipImport = document.getElementById("skipSessionImport");
806
934
  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;
935
+ if (confirm) confirm.onclick = function () {
936
+ var blocked = migrateBlockedReason();
937
+ if (blocked) { setSessionPickerStatus(blocked); return; }
938
+ dialog.close();
939
+ void startMigrate();
940
+ };
809
941
  wireSessionPickerUpgrade();
810
942
  var search = document.getElementById("sessionPickerSearch");
811
943
  if (search) search.oninput = function () {
@@ -850,16 +982,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
850
982
  else dialog.setAttribute("open", "");
851
983
  }
852
984
  function accountInitials(account) {
853
- var label = String(account && (account.displayName || account.email) || "EchoMem").trim();
854
- return label.split(/\s+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
985
+ var email = String(account && account.email || "").trim();
986
+ var label = email ? email.split("@")[0] : "EchoMem";
987
+ return label.split(/[\s._-]+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
855
988
  }
856
989
  function renderAccountChoice() {
857
990
  var slot = document.getElementById("setupAccount");
858
991
  if (!slot) return;
859
992
  var account = billingStatus && billingStatus.account;
860
- var name = account && account.displayName ? account.displayName : "Connected EchoMem account";
861
- var email = account && account.email ? account.email : "This import will be saved to the account connected on this device.";
862
- var accountLabel = account && account.email ? account.email : name;
993
+ var accountLabel = account && account.email ? account.email : "Connected account";
863
994
  var avatarUrl = account && account.avatarUrl ? String(account.avatarUrl) : "";
864
995
  var avatar = '<span class="setupAccountAvatar" aria-hidden="true">' +
865
996
  '<span>' + esc(accountInitials(account)) + '</span>' +
@@ -868,54 +999,168 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
868
999
  slot.innerHTML =
869
1000
  '<section class="setupAccountCard" aria-label="Connected EchoMem account">' +
870
1001
  avatar +
871
- '<span class="setupAccountCopy"><small>Logged in as</small><strong>' + esc(accountLabel) + '</strong><span>' + esc(email) + '</span></span>' +
1002
+ '<span class="setupAccountCopy"><small>Logged in as</small><strong>' + esc(accountLabel) + '</strong></span>' +
872
1003
  '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Switch</button>' +
873
1004
  '</section>';
874
1005
  var change = document.getElementById("changeSetupAccount");
875
- if (change) change.onclick = switchSetupAccount;
1006
+ if (change) change.onclick = renderAccountSwitchConfirm;
1007
+ }
1008
+ function renderAccountSwitchConfirm() {
1009
+ var account = billingStatus && billingStatus.account;
1010
+ var accountLabel = account && account.email
1011
+ ? account.email
1012
+ : "the connected account";
1013
+ accountSwitchPending = true;
1014
+ setCityMode(false);
1015
+ setExtractMode(false);
1016
+ setReadyMode(true);
1017
+ setHead("Switch EchoMem account", "Confirm");
1018
+ app.className = "accountSwitchStage";
1019
+ app.innerHTML =
1020
+ setupJourney(4) +
1021
+ '<section class="accountSwitchConfirm" aria-labelledby="accountSwitchTitle">' +
1022
+ '<button type="button" class="flowBack" id="cancelAccountSwitch">← Back</button>' +
1023
+ '<h2 id="accountSwitchTitle">Switch account?</h2>' +
1024
+ '<p><strong>' + esc(accountLabel) + '</strong> stays connected until you continue.</p>' +
1025
+ '<button type="button" class="primary" id="confirmAccountSwitch">Sign out &amp; switch</button>' +
1026
+ '<p class="localAuthStatus" id="localAuthStatus" role="status" aria-live="polite"></p>' +
1027
+ '</section>';
1028
+ var cancel = document.getElementById("cancelAccountSwitch");
1029
+ if (cancel) cancel.onclick = function () {
1030
+ accountSwitchPending = false;
1031
+ renderDashboard();
1032
+ };
1033
+ var confirm = document.getElementById("confirmAccountSwitch");
1034
+ if (confirm) confirm.onclick = function () { void switchSetupAccount(); };
876
1035
  }
877
1036
  async function switchSetupAccount() {
878
- var button = document.getElementById("changeSetupAccount");
1037
+ var button = document.getElementById("confirmAccountSwitch");
879
1038
  if (button) { button.disabled = true; button.textContent = "Signing out..."; }
1039
+ accountStateEpoch++;
1040
+ stopBillingPoll();
1041
+ progressPollRunId++;
880
1042
  try {
881
- await postJson("/logout", {}, 12000);
1043
+ var logoutResult = await postJson("/logout", {}, 12000);
1044
+ if (!logoutResult || logoutResult.connected !== false) {
1045
+ throw new Error("Echo did not confirm that this device signed out.");
1046
+ }
1047
+ accountSwitchPending = false;
882
1048
  stats = null;
1049
+ statsSlow = false;
883
1050
  billingStatus = null;
1051
+ billingStatusLoading = false;
884
1052
  connected = false;
1053
+ rememberSetupPlanChoice("");
885
1054
  selectedSessionKeys = Object.create(null);
886
1055
  sessionSelectionTouched = false;
887
1056
  localAuthEmail = "";
888
1057
  localAuthTermsAccepted = false;
889
1058
  localAuthAgeConfirmed = false;
890
- renderLocalLogin("");
1059
+ renderLocalLogin("Signed out. Enter the email for the account you want to use.");
891
1060
  } catch (error) {
892
- if (button) { button.disabled = false; button.textContent = "Change account"; }
893
- setSetupPlanStatus(error && error.message ? error.message : "Could not switch accounts.");
1061
+ if (connected) startBillingPoll();
1062
+ if (button) { button.disabled = false; button.textContent = "Sign out & switch"; }
1063
+ setLocalAuthStatus(error && error.message ? error.message : "Could not switch accounts.", "error");
894
1064
  }
895
1065
  }
896
1066
  function paidRecallPlan(plan) {
897
1067
  return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
898
1068
  }
899
- function setupPricingUrl(plan, startTrial) {
900
- var paidPlan = plan === "power" ? "power" : "pro";
901
- return (billingStatus && billingStatus.pricingUrl)
902
- ? billingStatus.pricingUrl + (startTrial ? "&start=" + paidPlan + "&trial=" + (billingStatus.trialAvailable === false ? "0" : "1") : "")
903
- : "https://echoknows.com/account?source=mcp_onboarding" + (startTrial ? "&start=" + paidPlan + "&trial=1" : "");
1069
+ function setupPlanPrice(plan) {
1070
+ return plan === "power" ? "$100" : "$20";
1071
+ }
1072
+ function formatBillingDate(value) {
1073
+ if (typeof value !== "string" || !value) return "";
1074
+ var parsed = new Date(value);
1075
+ if (!isFinite(parsed.getTime())) return "";
1076
+ try {
1077
+ return new Intl.DateTimeFormat(undefined, {
1078
+ year: "numeric",
1079
+ month: "long",
1080
+ day: "numeric"
1081
+ }).format(parsed);
1082
+ } catch (_) {
1083
+ return parsed.toISOString().slice(0, 10);
1084
+ }
1085
+ }
1086
+ function trialChargeDateText() {
1087
+ var chargeAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
1088
+ try {
1089
+ return new Intl.DateTimeFormat(undefined, {
1090
+ year: "numeric",
1091
+ month: "long",
1092
+ day: "numeric"
1093
+ }).format(chargeAt);
1094
+ } catch (_) {
1095
+ return chargeAt.toISOString().slice(0, 10);
1096
+ }
1097
+ }
1098
+ function paidPlanLifecycleText(plan) {
1099
+ var state = String(billingStatus && billingStatus.subscriptionStatus || "").toLowerCase();
1100
+ var trialEnd = formatBillingDate(billingStatus && billingStatus.trialEndedAt);
1101
+ var periodEnd = formatBillingDate(billingStatus && billingStatus.currentPeriodEnd);
1102
+ if (billingStatus && billingStatus.cancelAtPeriodEnd === true) {
1103
+ var accessEnd = state === "trialing" ? (trialEnd || periodEnd) : periodEnd;
1104
+ return accessEnd
1105
+ ? "Canceled — active until " + accessEnd + ". It will not renew."
1106
+ : "Canceled — active until the end of this billing period. It will not renew.";
1107
+ }
1108
+ if (state === "trialing") {
1109
+ return trialEnd
1110
+ ? "Trial active until " + trialEnd + ". Then " + setupPlanPrice(plan) + "/month unless canceled."
1111
+ : "Trial active. Then " + setupPlanPrice(plan) + "/month unless canceled.";
1112
+ }
1113
+ if (state === "active" && periodEnd) return "Active — renews on " + periodEnd + ".";
1114
+ return "";
1115
+ }
1116
+ function renderBillingCommitment(plan) {
1117
+ var slot = document.getElementById("billingCommitment");
1118
+ if (!slot) return;
1119
+ if (!paidRecallPlan(plan)) {
1120
+ slot.innerHTML = "";
1121
+ slot.classList.remove("is-visible");
1122
+ return;
1123
+ }
1124
+ var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
1125
+ var state = String(billingStatus && billingStatus.subscriptionStatus || "").toLowerCase();
1126
+ var canceled = !!(billingStatus && billingStatus.cancelAtPeriodEnd === true);
1127
+ var statusLabel = canceled ? "Canceled" : (state === "trialing" ? "Trial active" : "Active");
1128
+ var lifecycle = paidPlanLifecycleText(plan);
1129
+ var quota = billingStatus && billingStatus.historicalConversationQuota;
1130
+ var limit = quota && typeof quota.limit === "number" ? quota.limit : (plan === "power" ? 2000 : 500);
1131
+ var moneyFact = canceled
1132
+ ? "No future charge"
1133
+ : (state === "trialing" ? setupPlanPrice(plan) + "/month after trial" : setupPlanPrice(plan) + "/month");
1134
+ slot.classList.add("is-visible");
1135
+ slot.innerHTML =
1136
+ '<section class="billingCommitment' + (canceled ? ' is-canceled' : '') + '" aria-label="Current plan commitment">' +
1137
+ '<div class="billingCommitmentIdentity"><span>' + esc(statusLabel) + '</span><strong>' + esc(planLabel) + '</strong></div>' +
1138
+ '<div class="billingCommitmentFacts">' +
1139
+ '<span><b>' + esc(number(limit)) + '</b> coding-session imports</span>' +
1140
+ '<span><b>' + esc(moneyFact) + '</b></span>' +
1141
+ '</div>' +
1142
+ '<p>' + esc(lifecycle || "Your paid plan is active.") + '</p>' +
1143
+ '</section>';
904
1144
  }
905
1145
  function setupPlanConfirmed() {
906
1146
  return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
907
1147
  }
908
1148
  function setupPlanDefinition(plan) {
1149
+ var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
909
1150
  if (plan === "power") return {
910
1151
  id: "power",
911
1152
  name: "Power",
912
1153
  homeName: "Power",
913
1154
  promise: "More room. More agents. Much more Echo.",
914
1155
  price: "$100",
915
- cadence: "per month · 14-day trial",
1156
+ cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
916
1157
  sticker: "/hud-assets/echo-pricing-power-sticker.png",
917
1158
  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"]
1159
+ features: [
1160
+ "2,000 past chats",
1161
+ "agent-heavy memory",
1162
+ "2,000 memory recalls / week"
1163
+ ]
919
1164
  };
920
1165
  if (plan === "pro") return {
921
1166
  id: "pro",
@@ -923,11 +1168,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
923
1168
  homeName: "Pro",
924
1169
  promise: "A room for the work you return to every day.",
925
1170
  price: "$20",
926
- cadence: "per month · 14-day trial",
1171
+ cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
927
1172
  sticker: "/hud-assets/echo-pricing-pro-sticker.png",
928
1173
  stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
929
1174
  popular: true,
930
- features: ["500 past chats", "daily memory updates", "500 context returns / week"]
1175
+ features: [
1176
+ "500 past chats",
1177
+ "daily memory updates",
1178
+ "500 memory recalls / week"
1179
+ ]
931
1180
  };
932
1181
  return {
933
1182
  id: "free",
@@ -938,19 +1187,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
938
1187
  cadence: "free forever · no card needed",
939
1188
  sticker: "/hud-assets/echo-pricing-free-sticker.png",
940
1189
  stickerAlt: "Original Echo hugs one simple memory card.",
941
- features: ["100 past chats", "a few new chats / week", "100 context returns / week"]
1190
+ features: ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"]
942
1191
  };
943
1192
  }
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
1193
  function renderPlanHabitat() {
951
1194
  setupPlanPreview = setupPlanPreview === "pro" || setupPlanPreview === "power" ? setupPlanPreview : "free";
952
1195
  var plan = setupPlanDefinition(setupPlanPreview);
953
- var discovered = setupDiscoveredSessionCount();
954
1196
  var tabs = ["free", "pro", "power"].map(function (id) {
955
1197
  var tab = setupPlanDefinition(id);
956
1198
  var selected = id === plan.id;
@@ -960,12 +1202,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
960
1202
  return '<li>' + setupIcon("check") + '<span>' + esc(feature) + '</span></li>';
961
1203
  }).join("");
962
1204
  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
- : "";
1205
+ var chooseLabel = plan.id === "free"
1206
+ ? "Continue with " + plan.homeName + " — Free"
1207
+ : ((!billingStatus || billingStatus.trialAvailable !== false)
1208
+ ? "Start " + plan.homeName + " trial — $0 today"
1209
+ : "Subscribe to " + plan.homeName);
967
1210
  return '<section class="mvpPricing" aria-labelledby="setupPlanHeading">' +
968
- '<header class="mvpPricingIntro"><span>Plans</span><h2 id="setupPlanHeading">Choose your Echo.</h2>' + discovery + '</header>' +
1211
+ '<header class="mvpPricingIntro"><h2 id="setupPlanHeading">Choose your Echo.</h2></header>' +
969
1212
  '<div class="mvpPlanTabs" role="tablist" aria-label="Choose an Echo plan">' + tabs + '</div>' +
970
1213
  '<article class="mvpPlanHabitat" id="activePlanHabitat" role="tabpanel" aria-labelledby="planTab-' + plan.id + '" data-plan="' + plan.id + '">' +
971
1214
  '<div class="mvpHabitatScene">' +
@@ -977,9 +1220,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
977
1220
  '<div class="mvpHabitatDetails">' +
978
1221
  '<div class="mvpPlanIdentity"><span>' + esc(plan.name) + '</span>' + (plan.popular ? "<b>Echo&rsquo;s pick</b>" : "") + '</div>' +
979
1222
  '<h3>' + esc(plan.homeName) + ' Echo</h3>' +
980
- '<p class="mvpPlanPromise">' + esc(plan.promise) + '</p>' +
981
1223
  '<div class="mvpPlanPrice"><strong>' + esc(plan.price) + '</strong><span>' + esc(plan.cadence) + '</span></div>' +
982
- '<p class="mvpPacksLabel">Echo packs</p>' +
983
1224
  '<ul class="mvpPlanFacts">' + facts + '</ul>' +
984
1225
  '<button type="button" class="primary mvpPlanCta" id="' + chooseId + '">' + esc(chooseLabel) + ' <span aria-hidden="true">→</span></button>' +
985
1226
  '</div>' +
@@ -1024,6 +1265,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1024
1265
  slot.innerHTML = "";
1025
1266
  slot.classList.add("is-hidden");
1026
1267
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1268
+ renderBillingCommitment("");
1027
1269
  return;
1028
1270
  }
1029
1271
  slot.classList.remove("is-hidden");
@@ -1042,12 +1284,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1042
1284
  var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
1043
1285
  var checkoutJustConfirmed = !!billingActivationPendingPlan;
1044
1286
  billingActivationPendingPlan = "";
1287
+ rememberCheckoutSessionId("");
1288
+ billingSyncAttempts = 0;
1045
1289
  var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
1046
1290
  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.";
1291
+ ? paidHistory.used + " / " + paidHistory.limit + " coding sessions imported."
1292
+ : "Your coding-session import and weekly memory allowance are ready.";
1293
+ var lifecycleText = paidPlanLifecycleText(plan);
1294
+ var paidDetail = lifecycleText
1295
+ ? lifecycleText + (checkoutJustConfirmed ? "" : " " + paidUsage)
1296
+ : (checkoutJustConfirmed ? "Continue below." : paidUsage);
1049
1297
  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>';
1298
+ 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>';
1299
+ renderBillingCommitment(plan);
1051
1300
  var changeActivePlan = document.getElementById("changeActivePlan");
1052
1301
  if (changeActivePlan) changeActivePlan.onclick = openHostedPlanOptions;
1053
1302
  renderSessionSelection(true);
@@ -1056,7 +1305,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1056
1305
  }
1057
1306
  if (setupPlanChoice === "free") {
1058
1307
  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>';
1308
+ 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>';
1309
+ renderBillingCommitment("");
1060
1310
  var changeBtn = document.getElementById("changeSetupPlan");
1061
1311
  if (changeBtn) changeBtn.onclick = function () {
1062
1312
  rememberSetupPlanChoice("");
@@ -1069,17 +1319,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1069
1319
  ? "power"
1070
1320
  : (setupPlanChoice === "pro" || billingActivationPendingPlan === "pro" ? "pro" : "");
1071
1321
  if (pendingPaidPlan) {
1322
+ renderBillingCommitment("");
1072
1323
  if (readySettings) {
1073
1324
  readySettings.classList.remove("is-plan-open");
1074
1325
  readySettings.classList.add("is-checkout-pending");
1075
1326
  }
1076
1327
  var selectedPaidPlan = pendingPaidPlan === "power" ? "Power" : "Pro";
1328
+ var pendingHasTrial = !billingStatus || billingStatus.trialAvailable !== false;
1329
+ var pendingPrice = pendingPaidPlan === "power" ? "$100" : "$20";
1330
+ var pendingLimit = pendingPaidPlan === "power" ? "2,000" : "500";
1331
+ var pendingStatus = billingCheckMessage || (billingLastCheckedAt
1332
+ ? "Checking automatically every few seconds · Last checked just now"
1333
+ : "Checking automatically every few seconds");
1077
1334
  slot.innerHTML =
1078
1335
  '<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>' +
1336
+ '<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>' +
1337
+ '<dl class="checkoutCommitment">' +
1338
+ '<div><dt>Today</dt><dd>' + esc(pendingHasTrial ? "$0" : pendingPrice) + '</dd></div>' +
1339
+ '<div><dt>History import</dt><dd>' + esc(pendingLimit) + ' sessions</dd></div>' +
1340
+ '<div><dt>' + esc(pendingHasTrial ? "First charge" : "Billing") + '</dt><dd>' + esc(pendingHasTrial ? pendingPrice + " on " + trialChargeDateText() : pendingPrice + "/month now") + '</dd></div>' +
1341
+ '</dl>' +
1342
+ '<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>' +
1343
+ '<p class="checkoutPollStatus" id="setupPlanStatus" role="status" aria-live="polite">' + esc(pendingStatus) + '</p>' +
1344
+ '<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another plan</button></div>' +
1082
1345
  '</section>';
1346
+ var checkPaid = document.getElementById("checkPaymentStatus");
1347
+ if (checkPaid) checkPaid.onclick = function () {
1348
+ billingCheckMessage = "Checking payment status…";
1349
+ renderSetupPlan(true);
1350
+ void refreshBillingStatus();
1351
+ };
1083
1352
  var continuePaid = document.getElementById("continueSelectedPaidPlan");
1084
1353
  if (continuePaid) continuePaid.onclick = function () { openPaidPricing(pendingPaidPlan); };
1085
1354
  var changePaid = document.getElementById("changeSelectedPaidPlan");
@@ -1099,22 +1368,60 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1099
1368
  readySettings.classList.add("is-plan-open");
1100
1369
  }
1101
1370
  slot.innerHTML = renderPlanHabitat();
1371
+ renderBillingCommitment("");
1102
1372
  bindPlanHabitat();
1103
1373
  if (migrateBtn) migrateBtn.disabled = true;
1104
1374
  }
1375
+ async function maybeSyncPendingCheckout() {
1376
+ if (!pendingCheckoutSessionId || billingSyncAttempts >= 12) return;
1377
+ var now = Date.now();
1378
+ if (billingSyncLastAttemptAt && now - billingSyncLastAttemptAt < 5000) return;
1379
+ billingSyncLastAttemptAt = now;
1380
+ billingSyncAttempts++;
1381
+ try {
1382
+ var result = await postJson(
1383
+ "/billing-checkout-sync",
1384
+ { sessionId: pendingCheckoutSessionId },
1385
+ 18000
1386
+ );
1387
+ if (result && result.ok === true) rememberCheckoutSessionId("");
1388
+ } catch (_) {
1389
+ // A 409 is expected while the hosted Checkout session is still open.
1390
+ // Webhooks remain primary; this bounded fallback repairs delayed delivery.
1391
+ }
1392
+ }
1105
1393
  async function refreshBillingStatus() {
1106
- if (billingStatusLoading) return;
1394
+ if (billingStatusLoading || !connected) return;
1395
+ var billingEpoch = accountStateEpoch;
1107
1396
  billingStatusLoading = true;
1108
1397
  try {
1109
- billingStatus = await getJson("/billing-status");
1110
- renderSetupPlan(true);
1111
- renderAccountChoice();
1112
- renderSessionSelection(true);
1398
+ await maybeSyncPendingCheckout();
1399
+ if (billingEpoch !== accountStateEpoch || !connected) return;
1400
+ var nextBillingStatus = await getJson("/billing-status");
1401
+ if (billingEpoch !== accountStateEpoch || !connected) return;
1402
+ billingStatus = nextBillingStatus;
1403
+ billingLastCheckedAt = Date.now();
1404
+ billingCheckMessage = "";
1405
+ if (readyStage === "sessions" && setupPlanConfirmed()) {
1406
+ // Keep the visible promise in sync with the authoritative plan/quota response.
1407
+ // renderDashboard patches the mounted shell without recreating the page. If the
1408
+ // picker is already open after an upgrade, refresh its quota/action in place too.
1409
+ renderDashboard();
1410
+ var openPicker = document.getElementById("sessionPicker");
1411
+ if (openPicker && openPicker.open) renderSessionPicker();
1412
+ } else {
1413
+ renderSetupPlan(true);
1414
+ renderAccountChoice();
1415
+ renderSessionSelection(true);
1416
+ }
1113
1417
  if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Original still works.");
1114
1418
  } catch (_) {
1419
+ if (billingEpoch !== accountStateEpoch || !connected) return;
1420
+ billingLastCheckedAt = Date.now();
1421
+ billingCheckMessage = "Could not check payment yet. Echo will keep trying automatically.";
1115
1422
  setSetupPlanStatus("Plan check unavailable. Original still works.");
1116
1423
  } finally {
1117
- billingStatusLoading = false;
1424
+ if (billingEpoch === accountStateEpoch) billingStatusLoading = false;
1118
1425
  }
1119
1426
  }
1120
1427
  function refreshBillingOnFocus() { void refreshBillingStatus(); }
@@ -1147,29 +1454,34 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1147
1454
  opened.location.href = response.url;
1148
1455
  setSetupPlanStatus(openedMessage);
1149
1456
  startBillingPoll();
1150
- return true;
1457
+ return response;
1151
1458
  } catch (error) {
1152
1459
  try { if (opened && !opened.closed) opened.close(); } catch (_) {}
1153
1460
  setSetupPlanStatus(error && error.message ? error.message : "Could not open secure Echo billing.");
1154
- return false;
1461
+ return null;
1155
1462
  }
1156
1463
  }
1157
1464
  async function openPaidPricing(plan) {
1158
1465
  setSetupPlanStatus("Opening secure checkout…");
1159
- var opened = await openHostedBilling(
1466
+ var checkout = await openHostedBilling(
1160
1467
  "/billing-checkout",
1161
1468
  { plan: plan, trial: !billingStatus || billingStatus.trialAvailable !== false },
1162
1469
  "Creating your secure checkout…",
1163
1470
  "Checkout opened in a secure tab. Keep this local page open; Echo will confirm here automatically."
1164
1471
  );
1165
- if (opened) {
1472
+ if (checkout) {
1166
1473
  billingActivationPendingPlan = plan;
1474
+ if (typeof checkout.sessionId === "string") {
1475
+ rememberCheckoutSessionId(checkout.sessionId);
1476
+ billingSyncAttempts = 0;
1477
+ billingSyncLastAttemptAt = 0;
1478
+ }
1167
1479
  document.title = "Finish checkout — Echo setup is waiting";
1168
1480
  renderSetupPlan(true);
1169
1481
  }
1170
1482
  }
1171
1483
  async function openHostedPlanOptions() {
1172
- await openHostedBilling(
1484
+ return await openHostedBilling(
1173
1485
  "/billing-portal",
1174
1486
  {},
1175
1487
  "Opening secure plan management…",
@@ -1178,25 +1490,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1178
1490
  }
1179
1491
  function tryItCard() {
1180
1492
  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("");
1493
+ var primary = questions[0];
1195
1494
  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>' +
1495
+ '<h3 class="tryTitle">Take it into a fresh session.</h3>' +
1496
+ '<article class="recallPrompt recallPromptPrimary" data-recall-prompt="' + esc(primary) + '">' +
1497
+ '<div class="recallPromptHead"><span>Try this</span><button type="button" class="recallCopy" data-recall-copy aria-label="Copy your first recall question">' + setupIcon("copy") + '<span>Copy</span></button></div>' +
1498
+ '<p class="recallPromptText">' + esc(primary) + '</p>' +
1499
+ '<div class="recallPromptActions" aria-label="Try your first memory recall">' +
1500
+ '<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>' +
1501
+ '<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>' +
1502
+ '</div>' +
1503
+ '</article>' +
1200
1504
  '<p class="launchStatus" id="launchStatus"></p>' +
1201
1505
  '</div>';
1202
1506
  }
@@ -1211,12 +1515,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1211
1515
  if (claudeBtn) claudeBtn.addEventListener("click", function () { void openClaudeDesktop(prompt); });
1212
1516
  });
1213
1517
  }
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
1518
  function renderEnding() {
1221
1519
  setCityMode(false);
1222
1520
  setExtractMode(true);
@@ -1226,27 +1524,30 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1226
1524
  setHead("Ending extraction", "Ending");
1227
1525
  app.className = "";
1228
1526
  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>';
1527
+ setupJourney(4) +
1528
+ '<section class="lifecycleState lifecyclePending">' +
1529
+ '<h2>Stopping the remaining queued work…</h2>' +
1530
+ '<p>Saved memories stay intact.</p>' +
1531
+ '<div class="miniBar" aria-label="Ending extraction"><i></i></div>' +
1532
+ '</section>';
1235
1533
  }
1236
1534
  function renderSkipped() {
1237
1535
  setCityMode(false);
1238
1536
  setExtractMode(true);
1239
1537
  setHead("Extraction ended for now", "Ended");
1240
1538
  app.className = "";
1539
+ var savedCount = Math.max(0, Number(lastExtractionProgress && lastExtractionProgress.extracted || 0));
1241
1540
  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();
1541
+ setupJourney(savedCount > 0 ? 5 : 4) +
1542
+ '<section class="lifecycleState lifecyclePaused">' +
1543
+ '<header class="lifecycleIntro">' +
1544
+ '<h2>' + (savedCount > 0 ? esc(number(savedCount)) + ' memories are ready to use.' : 'Import ended safely.') + '</h2>' +
1545
+ '<p>The remaining queue was canceled. Saved memories were not removed.</p>' +
1546
+ '</header>' +
1547
+ (savedCount > 0 ? tryItCard() : '') +
1548
+ '<p class="lifecycleResume">Continue later with <code>echomem-mcp init</code>.</p>' +
1549
+ '</section>';
1550
+ if (savedCount > 0) wireTryButtons();
1250
1551
  }
1251
1552
  async function endExtraction() {
1252
1553
  if (extractionEnded) return;