@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.
- package/dist/codex-session-files.js +20 -0
- package/dist/context-analysis/vendored-canonical.js +54 -11
- package/dist/forensics.js +24 -4
- package/dist/hud/cli.js +18 -1
- package/dist/hud/hooks.js +59 -0
- package/dist/index.js +111 -16
- package/dist/migrate.js +24 -11
- package/dist/package-metadata.js +5 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +87 -15
- package/dist/setup-page/client-extraction.js +508 -207
- package/dist/setup-page/client-lifecycle.js +10 -11
- package/dist/setup-page/client-report-audit.js +3 -2
- package/dist/setup-page/client-report-city.js +168 -38
- package/dist/setup-page/styles-extraction.js +37 -0
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +751 -151
- package/dist/setup-page/styles-website-alignment.js +8 -3
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +402 -44
- package/dist/v1-contract.js +39 -6
- package/package.json +3 -2
- package/templates/codex-skills/echomem-save/SKILL.md +10 -0
- package/templates/echomem-recall.md +10 -2
|
@@ -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("
|
|
55
|
-
app.className = "
|
|
55
|
+
setHead("We couldn't finish this scan", "Needs attention");
|
|
56
|
+
app.className = "reportMessageStage";
|
|
56
57
|
app.innerHTML =
|
|
57
|
-
'<
|
|
58
|
-
'<h2>
|
|
59
|
-
'<p
|
|
60
|
-
'<
|
|
61
|
-
|
|
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 = "
|
|
70
|
+
app.className = "reportMessageStage";
|
|
69
71
|
app.innerHTML =
|
|
70
|
-
'<
|
|
71
|
-
'<h2>
|
|
72
|
-
'<p
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
:
|
|
202
|
-
|
|
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
|
-
? '
|
|
311
|
+
? ''
|
|
272
312
|
: 'Nothing new to extract right now.')
|
|
273
313
|
: 'Echo is checking local history against saved memory…';
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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") +
|
|
499
|
-
'<
|
|
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
|
|
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 ? "
|
|
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 ? "
|
|
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.
|
|
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 =
|
|
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"><
|
|
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
|
-
|
|
736
|
-
|
|
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><
|
|
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">· 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"><
|
|
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 () {
|
|
808
|
-
|
|
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
|
|
854
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 & 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("
|
|
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 (
|
|
893
|
-
|
|
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
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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: [
|
|
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: [
|
|
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
|
|
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"
|
|
964
|
-
|
|
965
|
-
|
|
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"><
|
|
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’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 + "
|
|
1048
|
-
: "Your
|
|
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(
|
|
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
|
|
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">
|
|
1080
|
-
'<
|
|
1081
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
|
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
|
|
1461
|
+
return null;
|
|
1155
1462
|
}
|
|
1156
1463
|
}
|
|
1157
1464
|
async function openPaidPricing(plan) {
|
|
1158
1465
|
setSetupPlanStatus("Opening secure checkout…");
|
|
1159
|
-
var
|
|
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 (
|
|
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
|
|
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
|
-
'<
|
|
1197
|
-
'<
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
'<h2
|
|
1232
|
-
'<p>
|
|
1233
|
-
'<div class="miniBar"><i></i></div>' +
|
|
1234
|
-
'</
|
|
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
|
-
|
|
1243
|
-
|
|
1244
|
-
'<
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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;
|