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