@echomem/mcp 1.4.19 → 1.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/dist/codex-session-files.js +7 -1
- package/dist/hud/cli.js +0 -0
- package/dist/index.js +9 -10
- package/dist/keystore.js +9 -19
- package/dist/migrate.js +50 -32
- package/dist/setup-page/client-extraction.js +60 -8
- package/dist/setup-page/client-report-city.js +14 -5
- package/dist/setup-page/styles-extraction.js +75 -6
- package/dist/setup-page/styles-website-alignment.js +183 -0
- package/dist/setup-preview.js +16 -2
- package/dist/setup.js +166 -14
- package/package.json +1 -1
- package/templates/codex-skills/echomem-forget/SKILL.md +16 -0
- package/templates/codex-skills/echomem-forget/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-login/SKILL.md +27 -0
- package/templates/codex-skills/echomem-login/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-save/SKILL.md +14 -0
- package/templates/codex-skills/echomem-save/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-search/SKILL.md +14 -0
- package/templates/codex-skills/echomem-search/agents/openai.yaml +6 -0
- package/dist/context-analysis/claude-canonical-adapter.js +0 -315
- package/dist/hud/efficiency.js +0 -447
package/README.md
CHANGED
|
@@ -25,8 +25,11 @@ For **zero-knowledge accounts**, the key never leaves your machine. The bridge h
|
|
|
25
25
|
- **writes:** the key is handed to your own backend transiently in the `X-Encryption-Key` header so
|
|
26
26
|
memories are encrypted at rest (the server processes plaintext for the request only — never stores
|
|
27
27
|
the key).
|
|
28
|
-
- **
|
|
29
|
-
|
|
28
|
+
- **trusted-device lifecycle:** after a verified login/unlock, the MCP key remains available until
|
|
29
|
+
the user runs `echomem-mcp lock`, logs out, or removes the local credentials. If the key is absent,
|
|
30
|
+
tools tell the user to run `echomem-mcp unlock` in their own terminal. The passphrase prompt is
|
|
31
|
+
visible while typed characters stay hidden, and the current agent session can retry immediately
|
|
32
|
+
after unlock — **ciphertext is never handed to the model**.
|
|
30
33
|
|
|
31
34
|
Unencrypted accounts are unaffected. `search_memories` returns retrieved memories by default so
|
|
32
35
|
your MCP client does the final answer generation; pass `includeAnswer: true` only if you need the
|
|
@@ -47,8 +50,11 @@ to the global install, but installing globally avoids the issue entirely). The c
|
|
|
47
50
|
launch-at-login also runs from the installed path. `init` is the flagship one-liner — it wraps `setup --all --with-hud`: it writes each
|
|
48
51
|
installed agent's MCP config (with **no secret** in it — credentials live in
|
|
49
52
|
`~/.echomem/credentials.json`, mode 0600), adds the EchoMem memory guidance to their global
|
|
50
|
-
`AGENTS.md` / `CLAUDE.md`,
|
|
51
|
-
|
|
53
|
+
`AGENTS.md` / `CLAUDE.md`, installs first-party `echomem-search`, `echomem-save`,
|
|
54
|
+
`echomem-forget`, and `echomem-login` skills for Codex, opens the browser to approve the device
|
|
55
|
+
(and unlock the vault for encrypted accounts), then launches the context HUD. EchoMem updates only
|
|
56
|
+
its own skill folders; other memory-provider skills are detected and reported but never modified.
|
|
57
|
+
Reload your editors and you're done.
|
|
52
58
|
|
|
53
59
|
Prefer to keep it minimal? `echomem-mcp setup` configures only the auto-detected editor and skips the
|
|
54
60
|
HUD; the granular commands below still work.
|
|
@@ -64,7 +70,8 @@ HUD; the granular commands below still work.
|
|
|
64
70
|
| `npx -y @echomem/mcp@latest update --client codex` | Update one client only |
|
|
65
71
|
| `echomem-mcp setup --with-hud [--client codex]` | Write client config + log in + launch the EchoMem context HUD |
|
|
66
72
|
| `echomem-mcp login` | Approve device in browser (or use `--token` / `--passphrase`) |
|
|
67
|
-
| `echomem-mcp unlock` |
|
|
73
|
+
| `echomem-mcp unlock` | Privately unlock the vault on this trusted device |
|
|
74
|
+
| `echomem-mcp lock` | Remove the local vault key while keeping the device login |
|
|
68
75
|
| `echomem-mcp status` | Show token / key / detected clients, configured bridge versions, and update guidance |
|
|
69
76
|
| `echomem-mcp doctor [--no-network]` | Diagnose configured client bridge versions |
|
|
70
77
|
| `echomem-mcp logout` | Remove stored credentials |
|
|
@@ -35,6 +35,12 @@ function normalizedSessionId(value) {
|
|
|
35
35
|
const trimmed = value.trim();
|
|
36
36
|
return new RegExp(`^${UUID_RE.source}$`, "i").test(trimmed) ? trimmed.toLowerCase() : trimmed;
|
|
37
37
|
}
|
|
38
|
+
function isAutomaticCodexSource(source) {
|
|
39
|
+
if (typeof source !== "string")
|
|
40
|
+
return false;
|
|
41
|
+
const normalized = source.trim().toLowerCase().replace(/-/g, "_");
|
|
42
|
+
return normalized === "auto_review" || normalized === "guardian";
|
|
43
|
+
}
|
|
38
44
|
function metadataFromFile(file) {
|
|
39
45
|
try {
|
|
40
46
|
for (const line of initialJsonLines(file)) {
|
|
@@ -63,7 +69,7 @@ function metadataFromFile(file) {
|
|
|
63
69
|
return {
|
|
64
70
|
id: rawId || null,
|
|
65
71
|
startedAtMs: Number.isFinite(timestamp) ? timestamp : null,
|
|
66
|
-
userInitiated: !isSubagent && !hasParent,
|
|
72
|
+
userInitiated: !isSubagent && !hasParent && !isAutomaticCodexSource(source),
|
|
67
73
|
};
|
|
68
74
|
}
|
|
69
75
|
return { id: null, startedAtMs: null, userInitiated: null };
|
package/dist/hud/cli.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -22,11 +22,6 @@ class NoTokenError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
/** Thrown when an encrypted account has no usable key — the model gets an unlock nudge, never ciphertext. */
|
|
24
24
|
class LockedError extends Error {
|
|
25
|
-
expired;
|
|
26
|
-
constructor(expired) {
|
|
27
|
-
super("EchoMem vault locked");
|
|
28
|
-
this.expired = expired;
|
|
29
|
-
}
|
|
30
25
|
}
|
|
31
26
|
function stringifyErrorValue(value) {
|
|
32
27
|
if (value == null) {
|
|
@@ -126,7 +121,7 @@ function formatUpgradeRequiredResult(error) {
|
|
|
126
121
|
pricingUrl,
|
|
127
122
|
});
|
|
128
123
|
return [
|
|
129
|
-
"ACTION REQUIRED: tell the user to start a
|
|
124
|
+
"ACTION REQUIRED: tell the user to start a 7-day Pro trial or choose a subscription at this exact URL:",
|
|
130
125
|
pricingUrl,
|
|
131
126
|
"When replying, include the exact URL above. Do not reply only with \"connect\" or \"upgrade\".",
|
|
132
127
|
"",
|
|
@@ -591,7 +586,7 @@ class EchoMemApiClient {
|
|
|
591
586
|
// No usable key: consult the server to tell "unencrypted" apart from "encrypted but locked/expired".
|
|
592
587
|
const cfg = await this.getEncryptionConfig();
|
|
593
588
|
if (cfg.enabled)
|
|
594
|
-
throw new LockedError(
|
|
589
|
+
throw new LockedError("EchoMem vault locked");
|
|
595
590
|
return { enabled: false };
|
|
596
591
|
}
|
|
597
592
|
pruneDeleteConfirmations(now = Date.now()) {
|
|
@@ -1221,9 +1216,13 @@ class EchoMemMCPServer {
|
|
|
1221
1216
|
content: [
|
|
1222
1217
|
{
|
|
1223
1218
|
type: "text",
|
|
1224
|
-
text:
|
|
1225
|
-
|
|
1226
|
-
|
|
1219
|
+
text: [
|
|
1220
|
+
"🔒 EchoMem vault is locked.",
|
|
1221
|
+
"This encrypted account has no usable local decryption key. Once unlocked, this trusted device stays unlocked until you explicitly lock it or log out.",
|
|
1222
|
+
"Action required from the user: open Terminal and run `echomem-mcp unlock` yourself. Do not have the agent run this interactive command and do not send your passphrase in chat.",
|
|
1223
|
+
"At `Vault passphrase (typing is hidden):`, type the passphrase and press Return. No characters will appear while you type; that is expected.",
|
|
1224
|
+
"After the success message, retry this EchoMem action in the current session — no editor restart is needed.",
|
|
1225
|
+
].join("\n"),
|
|
1227
1226
|
},
|
|
1228
1227
|
],
|
|
1229
1228
|
};
|
package/dist/keystore.js
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* Storage posture: a 0600 file in the user's home — the same on-disk trust level as the extension's
|
|
11
11
|
* `chrome.storage.local`. The OS keychain (spec §8's ideal) is a drop-in for `read`/`write` later;
|
|
12
12
|
* it is deliberately deferred because `keytar` is a native build that complicates `npx` distribution.
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
13
|
+
* The MCP bridge uses an explicit trusted-device lifecycle: a verified encryption key remains
|
|
14
|
+
* available until the user runs `lock`/`logout` or removes the local credentials. Interactive app
|
|
15
|
+
* session TTLs are intentionally separate from this device-bound developer workflow.
|
|
15
16
|
*/
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
import path from "node:path";
|
|
19
|
-
export const DEFAULT_KEY_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
20
|
/** The bridge's local config dir (`~/.echomem` or `$ECHO_CONFIG_DIR`) — home for credentials + telemetry. */
|
|
21
21
|
export function echoConfigDir() {
|
|
22
22
|
return process.env.ECHO_CONFIG_DIR || path.join(os.homedir(), ".echomem");
|
|
@@ -55,29 +55,19 @@ export class KeyStore {
|
|
|
55
55
|
getToken() {
|
|
56
56
|
return process.env.ECHO_API_TOKEN || readFileCreds().token;
|
|
57
57
|
}
|
|
58
|
-
/** Encryption key (base64), or undefined if absent
|
|
58
|
+
/** Encryption key (base64), or undefined if absent. Env wins. */
|
|
59
59
|
getKey() {
|
|
60
60
|
if (process.env.ECHO_ENCRYPTION_KEY)
|
|
61
61
|
return process.env.ECHO_ENCRYPTION_KEY;
|
|
62
|
-
|
|
63
|
-
if (!creds.key)
|
|
64
|
-
return undefined;
|
|
65
|
-
if (creds.keyExpiresAt && Date.now() > creds.keyExpiresAt)
|
|
66
|
-
return undefined;
|
|
67
|
-
return creds.key;
|
|
68
|
-
}
|
|
69
|
-
/** True when a key is present but past its TTL — used to tell "locked (expired)" from "never set". */
|
|
70
|
-
isKeyExpired() {
|
|
71
|
-
if (process.env.ECHO_ENCRYPTION_KEY)
|
|
72
|
-
return false;
|
|
73
|
-
const creds = readFileCreds();
|
|
74
|
-
return !!(creds.key && creds.keyExpiresAt && Date.now() > creds.keyExpiresAt);
|
|
62
|
+
return readFileCreds().key;
|
|
75
63
|
}
|
|
76
64
|
saveToken(token) {
|
|
77
65
|
writeFileCreds({ ...readFileCreds(), token });
|
|
78
66
|
}
|
|
79
|
-
saveKey(keyBase64
|
|
80
|
-
|
|
67
|
+
saveKey(keyBase64) {
|
|
68
|
+
const creds = readFileCreds();
|
|
69
|
+
delete creds.keyExpiresAt;
|
|
70
|
+
writeFileCreds({ ...creds, key: keyBase64 });
|
|
81
71
|
}
|
|
82
72
|
clearKey() {
|
|
83
73
|
const creds = readFileCreds();
|
package/dist/migrate.js
CHANGED
|
@@ -25,7 +25,8 @@ import readline from "node:readline";
|
|
|
25
25
|
import axios from "axios";
|
|
26
26
|
import { KeyStore, echoConfigDir } from "./keystore.js";
|
|
27
27
|
import { fetchEncryptionConfig } from "./encryption.js";
|
|
28
|
-
import {
|
|
28
|
+
import { discoverCodexSessionFiles } from "./codex-session-files.js";
|
|
29
|
+
import { resolveClaudeProjectsDir } from "./local-data-paths.js";
|
|
29
30
|
import { walk, eachLine } from "./report.js";
|
|
30
31
|
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
31
32
|
const RATE_MAX = 28; // stay under the import-jobs /run limit of 30 / 60s
|
|
@@ -184,20 +185,29 @@ export function normalizeCwd(cwd) {
|
|
|
184
185
|
const m = cwd.match(/worktrees\/[^/]+\/(.+)$/);
|
|
185
186
|
return m ? m[1] : cwd;
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
function userCreatedCodexFiles(codexRoot) {
|
|
189
|
+
return discoverCodexSessionFiles({
|
|
190
|
+
...(codexRoot
|
|
191
|
+
? { roots: [{ kind: "active", path: codexRoot, priority: 0 }] }
|
|
192
|
+
: {}),
|
|
193
|
+
includeArchived: false,
|
|
194
|
+
userInitiatedOnly: true,
|
|
195
|
+
}).files.map((file) => file.path);
|
|
196
|
+
}
|
|
197
|
+
function userCreatedClaudeFiles(claudeRoot) {
|
|
198
|
+
return walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
|
|
199
|
+
}
|
|
200
|
+
/** Discover every user-created local session, newest first (by first-turn timestamp). */
|
|
201
|
+
export function discoverSessions(opts = {}) {
|
|
189
202
|
const out = [];
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (s)
|
|
195
|
-
out.push(s);
|
|
196
|
-
}
|
|
203
|
+
for (const f of userCreatedCodexFiles(opts.codexRoot)) {
|
|
204
|
+
const s = assembleCodex(f);
|
|
205
|
+
if (s)
|
|
206
|
+
out.push(s);
|
|
197
207
|
}
|
|
198
|
-
const claudeRoot = resolveClaudeProjectsDir();
|
|
208
|
+
const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
|
|
199
209
|
if (claudeRoot) {
|
|
200
|
-
for (const f of
|
|
210
|
+
for (const f of userCreatedClaudeFiles(claudeRoot)) {
|
|
201
211
|
const s = assembleClaude(f);
|
|
202
212
|
if (s)
|
|
203
213
|
out.push(s);
|
|
@@ -250,6 +260,20 @@ function hasClaudeText(content) {
|
|
|
250
260
|
return false;
|
|
251
261
|
return content.some((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
|
|
252
262
|
}
|
|
263
|
+
function isUserCreatedClaudeSessionFile(file) {
|
|
264
|
+
return initialJsonObjects(file).some((obj) => {
|
|
265
|
+
if (!isRecord(obj) || obj.type !== "user")
|
|
266
|
+
return false;
|
|
267
|
+
if (obj.userType !== "external" || obj.isSidechain !== false || obj.parentUuid !== null)
|
|
268
|
+
return false;
|
|
269
|
+
if (obj.isMeta === true || obj.isCompactSummary === true)
|
|
270
|
+
return false;
|
|
271
|
+
if (typeof obj.agentId === "string" && obj.agentId.trim())
|
|
272
|
+
return false;
|
|
273
|
+
const message = isRecord(obj.message) ? obj.message : {};
|
|
274
|
+
return hasClaudeText(message.content);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
253
277
|
function fastSessionInfo(file, source) {
|
|
254
278
|
let conversationKey = `${source === "codex" ? "codex" : "claude-code"}:${sha16(file)}`;
|
|
255
279
|
let hasTextTurn = false;
|
|
@@ -287,21 +311,18 @@ function fastSessionInfo(file, source) {
|
|
|
287
311
|
}
|
|
288
312
|
function fastSessionEntries(opts = {}) {
|
|
289
313
|
const out = [];
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (info.hasTextTurn || info.hasRealKey)
|
|
299
|
-
out.push({ filePath, source: "codex", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
300
|
-
}
|
|
314
|
+
for (const filePath of userCreatedCodexFiles(opts.codexRoot)) {
|
|
315
|
+
const stat = statSafe(filePath);
|
|
316
|
+
const info = fastSessionInfo(filePath, "codex");
|
|
317
|
+
// Include if we found text OR a real session id (big sessions can have their first text turn beyond
|
|
318
|
+
// the 1MB probe window — gating only on text dropped them entirely; exact discovery refines later).
|
|
319
|
+
// We require a real key so the fast/exact conversationKey match (no sha16 fallback mismatch).
|
|
320
|
+
if (info.hasTextTurn || info.hasRealKey)
|
|
321
|
+
out.push({ filePath, source: "codex", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
301
322
|
}
|
|
302
323
|
const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
|
|
303
324
|
if (claudeRoot) {
|
|
304
|
-
for (const filePath of
|
|
325
|
+
for (const filePath of userCreatedClaudeFiles(claudeRoot)) {
|
|
305
326
|
const stat = statSafe(filePath);
|
|
306
327
|
const info = fastSessionInfo(filePath, "claude-code");
|
|
307
328
|
// Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
|
|
@@ -761,7 +782,7 @@ function authedClient(token) {
|
|
|
761
782
|
});
|
|
762
783
|
}
|
|
763
784
|
export function discoverMigratableSessions(opts = {}) {
|
|
764
|
-
let sessions = discoverSessions();
|
|
785
|
+
let sessions = discoverSessions(opts);
|
|
765
786
|
const since = opts.since;
|
|
766
787
|
if (since)
|
|
767
788
|
sessions = sessions.filter((s) => (s.firstTs || "").slice(0, 10) >= since);
|
|
@@ -1076,7 +1097,7 @@ async function runJobs(args) {
|
|
|
1076
1097
|
const message = responseMessage(e);
|
|
1077
1098
|
const durationMs = Date.now() - jobStartedAt;
|
|
1078
1099
|
if (status === 422 && errCode === "ENCRYPTION_KEY_REQUIRED") {
|
|
1079
|
-
stoppedReason = "
|
|
1100
|
+
stoppedReason = "vault-locked"; // no usable local vault key — signal workers to stop pulling
|
|
1080
1101
|
done++;
|
|
1081
1102
|
recordMetric(buildMigrationMetric({
|
|
1082
1103
|
runId: args.runId, importSessionId: args.session.id, jobId: job.id, index: done, total,
|
|
@@ -1257,8 +1278,8 @@ export async function cmdMigrate(flags) {
|
|
|
1257
1278
|
console.log(c.dim(`Import session ${h.sessionId} — ${h.jobCount} jobs queued (the web dashboard can watch this live).`));
|
|
1258
1279
|
console.log(c.dim(`Metrics: ${h.metricsFile}`));
|
|
1259
1280
|
const r = await h.done;
|
|
1260
|
-
if (r.stoppedReason === "
|
|
1261
|
-
console.error(c.yellow(`\n⚠ Vault locked mid-run.
|
|
1281
|
+
if (r.stoppedReason === "vault-locked") {
|
|
1282
|
+
console.error(c.yellow(`\n⚠ Vault locked mid-run. Open Terminal, run \`echomem-mcp unlock\`, and re-run migrate to resume (${r.migrated} done so far).`));
|
|
1262
1283
|
process.exitCode = 1;
|
|
1263
1284
|
}
|
|
1264
1285
|
console.log("");
|
|
@@ -1275,10 +1296,7 @@ export async function cmdMigrate(flags) {
|
|
|
1275
1296
|
if (code === "NOT_LOGGED_IN")
|
|
1276
1297
|
console.error("Not logged in. Run `echomem-mcp login` first, then re-run migrate.");
|
|
1277
1298
|
else if (code === "VAULT_LOCKED") {
|
|
1278
|
-
|
|
1279
|
-
console.error(store.isKeyExpired()
|
|
1280
|
-
? "Vault key expired. Run `echomem-mcp unlock`, then re-run migrate."
|
|
1281
|
-
: "This account is ENCRYPTED but the vault is locked. Run `echomem-mcp unlock`, then re-run migrate.");
|
|
1299
|
+
console.error("This account is encrypted but the local vault is locked. Open Terminal, run `echomem-mcp unlock`, then re-run migrate.");
|
|
1282
1300
|
}
|
|
1283
1301
|
else if (code === "FORBIDDEN_SCOPE") {
|
|
1284
1302
|
console.error("This device token cannot import history. Re-connect this device with `echomem-mcp login`.");
|
|
@@ -214,6 +214,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
214
214
|
'<div class="readyGo">' +
|
|
215
215
|
'<div class="actions">' +
|
|
216
216
|
'<button id="migrate" class="primary"></button>' +
|
|
217
|
+
'<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip history import</button>' +
|
|
217
218
|
'</div>' +
|
|
218
219
|
'<p class="ctaMeta" id="exEta"></p>' +
|
|
219
220
|
'</div>' +
|
|
@@ -267,13 +268,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
267
268
|
// While still counting, hide the primary button entirely; the headline + source split
|
|
268
269
|
// already say it is working, and the ready state should present one clear action.
|
|
269
270
|
var migrateBtn = document.getElementById("migrate");
|
|
271
|
+
var skipHistoryBtn = document.getElementById("skipHistoryImport");
|
|
270
272
|
if (pendingTrusted) {
|
|
271
273
|
migrateBtn.style.display = "";
|
|
272
274
|
migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
|
|
273
275
|
migrateBtn.innerHTML = '<span>' + esc(canExtract ? extractLabel : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
|
|
274
276
|
migrateBtn.onclick = canExtract ? startMigrate : skip;
|
|
277
|
+
if (skipHistoryBtn) {
|
|
278
|
+
skipHistoryBtn.style.display = canExtract ? "" : "none";
|
|
279
|
+
skipHistoryBtn.onclick = skip;
|
|
280
|
+
}
|
|
275
281
|
} else {
|
|
276
282
|
migrateBtn.style.display = "none";
|
|
283
|
+
if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
|
|
277
284
|
}
|
|
278
285
|
}
|
|
279
286
|
async function startMigrate() {
|
|
@@ -352,7 +359,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
352
359
|
var savedMemoryLabel = extracted === 1 ? "memory saved" : "memories saved";
|
|
353
360
|
var conversationNoun = completed === 1 ? "conversation" : "conversations";
|
|
354
361
|
var timelineUrl = "https://yeahecho.com/memories/timeline";
|
|
355
|
-
var stopDetail = String(progress.error) === "
|
|
362
|
+
var stopDetail = String(progress.error) === "vault-locked"
|
|
356
363
|
? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
|
|
357
364
|
: esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
|
|
358
365
|
var completionNote = done && progress.latest && progress.latest !== "Import complete."
|
|
@@ -626,6 +633,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
626
633
|
}
|
|
627
634
|
function updateSessionPickerSelectionUi(dialog, input, limit) {
|
|
628
635
|
var selectedCount = selectedSessionKeyList().length;
|
|
636
|
+
hideSessionPickerLimitNotice();
|
|
629
637
|
var row = input && input.closest ? input.closest(".sessionPickerRow") : null;
|
|
630
638
|
if (row) row.classList.toggle("is-selected", !!input.checked);
|
|
631
639
|
var count = dialog.querySelector(".sessionPickerActions strong");
|
|
@@ -638,6 +646,48 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
638
646
|
confirm.textContent = "Use " + number(selectedCount) + " selected";
|
|
639
647
|
}
|
|
640
648
|
}
|
|
649
|
+
function sessionPickerLimitNotice(canDismiss) {
|
|
650
|
+
var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
|
|
651
|
+
var linkLabel = plan === "power" || plan === "team" || plan === "enterprise"
|
|
652
|
+
? "View plan options"
|
|
653
|
+
: "Upgrade to import more";
|
|
654
|
+
return '<div class="sessionPickerLimitCard">' +
|
|
655
|
+
'<span class="sessionPickerLimitEyebrow">Plan limit</span>' +
|
|
656
|
+
'<h3>Plan limit reached.</h3>' +
|
|
657
|
+
'<p>Upgrade your plan to select and import more conversations.</p>' +
|
|
658
|
+
'<div class="sessionPickerLimitActions"><a id="upgradeSessionPlan" class="button primary" href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">' + linkLabel + '</a>' +
|
|
659
|
+
(canDismiss ? '<button type="button" class="textButton" id="dismissSessionLimit">Keep editing</button>' : "") + '</div>' +
|
|
660
|
+
'</div>';
|
|
661
|
+
}
|
|
662
|
+
function wireSessionPickerUpgrade() {
|
|
663
|
+
var upgrade = document.getElementById("upgradeSessionPlan");
|
|
664
|
+
if (upgrade) upgrade.onclick = function () { startBillingPoll(); };
|
|
665
|
+
var dismiss = document.getElementById("dismissSessionLimit");
|
|
666
|
+
if (dismiss) dismiss.onclick = hideSessionPickerLimitNotice;
|
|
667
|
+
}
|
|
668
|
+
function showSessionPickerLimitNotice(canDismiss) {
|
|
669
|
+
var wrap = document.getElementById("sessionPickerListWrap");
|
|
670
|
+
var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
|
|
671
|
+
var overlay = document.getElementById("sessionPickerLimitOverlay");
|
|
672
|
+
var status = document.getElementById("sessionPickerStatus");
|
|
673
|
+
if (status) status.textContent = "Plan limit reached. Upgrade to import more.";
|
|
674
|
+
if (!wrap || !overlay) return;
|
|
675
|
+
overlay.innerHTML = sessionPickerLimitNotice(canDismiss);
|
|
676
|
+
overlay.hidden = false;
|
|
677
|
+
if (list) list.inert = true;
|
|
678
|
+
wrap.classList.add("is-limit-blocked");
|
|
679
|
+
wireSessionPickerUpgrade();
|
|
680
|
+
}
|
|
681
|
+
function hideSessionPickerLimitNotice() {
|
|
682
|
+
var wrap = document.getElementById("sessionPickerListWrap");
|
|
683
|
+
var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
|
|
684
|
+
var overlay = document.getElementById("sessionPickerLimitOverlay");
|
|
685
|
+
var status = document.getElementById("sessionPickerStatus");
|
|
686
|
+
if (status) status.textContent = "";
|
|
687
|
+
if (overlay) { overlay.hidden = true; overlay.innerHTML = ""; }
|
|
688
|
+
if (list) list.inert = false;
|
|
689
|
+
if (wrap) wrap.classList.remove("is-limit-blocked");
|
|
690
|
+
}
|
|
641
691
|
function renderSessionPicker() {
|
|
642
692
|
var dialog = document.getElementById("sessionPicker");
|
|
643
693
|
if (!dialog) return;
|
|
@@ -674,14 +724,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
674
724
|
'<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
|
|
675
725
|
'<label class="sessionSourceFilter"><span>Source</span><select id="sessionPickerSource"><option value="all"' + (sessionPickerSource === "all" ? " selected" : "") + '>All sources</option><option value="codex"' + (sessionPickerSource === "codex" ? " selected" : "") + '>Codex</option><option value="claude-code"' + (sessionPickerSource === "claude-code" ? " selected" : "") + '>Claude Code</option></select></label>' +
|
|
676
726
|
'</div>' +
|
|
677
|
-
'<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" role="status"
|
|
678
|
-
'<div class="sessionPickerList">' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + '</div>' +
|
|
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>' +
|
|
727
|
+
'<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" class="sessionPickerStatus" role="status" aria-live="polite">' + (limit === 0 ? "Plan limit reached. Upgrade to import more." : "") + '</span><button type="button" class="textButton" id="selectNewestSessions">Select newest</button><button type="button" class="textButton" id="clearSessions">Clear</button></div>' +
|
|
728
|
+
'<div class="sessionPickerListWrap' + (limit === 0 ? " is-limit-blocked" : "") + '" id="sessionPickerListWrap"><div class="sessionPickerList"' + (limit === 0 ? " inert" : "") + '>' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + overflow + '</div><div class="sessionPickerLimitOverlay" id="sessionPickerLimitOverlay" role="alert"' + (limit === 0 ? "" : " hidden") + '>' + (limit === 0 ? sessionPickerLimitNotice(false) : "") + '</div></div>' +
|
|
729
|
+
'<footer class="sessionPickerFoot"><p>Only selected conversations will be sent for extraction.</p><div class="sessionPickerFootActions"><button type="button" class="secondary" id="skipSessionImport">Skip history import</button><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Use ' + esc(number(selected.length)) + ' selected</button></div></footer>' +
|
|
680
730
|
'</div>';
|
|
681
731
|
var close = document.getElementById("closeSessionPicker");
|
|
682
732
|
var confirm = document.getElementById("confirmSessions");
|
|
733
|
+
var skipImport = document.getElementById("skipSessionImport");
|
|
683
734
|
if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
|
|
684
735
|
if (confirm) confirm.onclick = function () { dialog.close(); renderSessionSelection(true); };
|
|
736
|
+
if (skipImport) skipImport.onclick = skip;
|
|
737
|
+
wireSessionPickerUpgrade();
|
|
685
738
|
var search = document.getElementById("sessionPickerSearch");
|
|
686
739
|
if (search) search.oninput = function () {
|
|
687
740
|
sessionPickerQuery = search.value || "";
|
|
@@ -697,8 +750,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
697
750
|
var count = selectedSessionKeyList().length;
|
|
698
751
|
if (input.checked && !selectedSessionKeys[key] && count >= limit) {
|
|
699
752
|
input.checked = false;
|
|
700
|
-
|
|
701
|
-
if (status) status.textContent = "Plan limit reached";
|
|
753
|
+
showSessionPickerLimitNotice(limit > 0);
|
|
702
754
|
return;
|
|
703
755
|
}
|
|
704
756
|
sessionSelectionTouched = true;
|
|
@@ -832,7 +884,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
832
884
|
'<div class="setupPlanHead"><strong>Pro</strong><span>$40 / month</span></div>' +
|
|
833
885
|
'<p>More recall for serious work.</p>' +
|
|
834
886
|
'<ul><li>' + setupIcon("check") + '<span>250 bulk history imports</span></li><li>' + setupIcon("check") + '<span>200K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>100 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
|
|
835
|
-
'<button type="button" class="primary" id="chooseProPlan">' + (trialAvailable ? "Start
|
|
887
|
+
'<button type="button" class="primary" id="chooseProPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Pro") + '</button>' +
|
|
836
888
|
'<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
|
|
837
889
|
'</article>' +
|
|
838
890
|
'<article class="setupPlanCard">' +
|
|
@@ -840,7 +892,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
840
892
|
'<p>Higher limits for people who use Echo every day.</p>' +
|
|
841
893
|
// Future Power feature: API and automation access.
|
|
842
894
|
'<ul><li>' + setupIcon("check") + '<span>500 bulk history imports</span></li><li>' + setupIcon("check") + '<span>750K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>250 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
|
|
843
|
-
'<button type="button" class="secondary" id="choosePowerPlan">' + (trialAvailable ? "Start
|
|
895
|
+
'<button type="button" class="secondary" id="choosePowerPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Power") + '</button>' +
|
|
844
896
|
'<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
|
|
845
897
|
'</article>' +
|
|
846
898
|
'</div>' +
|
|
@@ -192,12 +192,21 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
|
|
|
192
192
|
app.className = "scanLoading";
|
|
193
193
|
app.innerHTML =
|
|
194
194
|
'<div class="scanLoader" aria-live="polite">' +
|
|
195
|
-
'<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
'<div class="scanLoaderLead">' +
|
|
196
|
+
'<span class="scanMascot" aria-hidden="true"><img src="/hud-assets/echo-face-cutout.png" alt="" /></span>' +
|
|
197
|
+
'<div class="scanLoaderCopy">' +
|
|
198
|
+
'<p class="scanEyebrow">Local scan</p>' +
|
|
199
|
+
'<h2 class="siteHeadline">Reading your <strong>local coding history</strong></h2>' +
|
|
200
|
+
'<p class="scanIntro">Echo is organizing your Codex and Claude Code sessions on this Mac.</p>' +
|
|
201
|
+
'</div>' +
|
|
202
|
+
'</div>' +
|
|
203
|
+
'<div class="scanProgressCard">' +
|
|
204
|
+
'<div class="scanProgress" role="progressbar" aria-label="Local scan progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + pct + '">' +
|
|
205
|
+
'<i id="scan-load-fill" class="scanProgressFill" style="width:' + pct + '%"></i>' +
|
|
206
|
+
'</div>' +
|
|
207
|
+
'<p id="scan-load-caption" class="scanCaption">' + esc(caption) + '</p>' +
|
|
199
208
|
'</div>' +
|
|
200
|
-
'<p
|
|
209
|
+
'<p class="scanPrivacy"><span aria-hidden="true"></span>Transcripts stay on this device during the scan.</p>' +
|
|
201
210
|
'</div>';
|
|
202
211
|
}
|
|
203
212
|
|
|
@@ -271,6 +271,11 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
271
271
|
border-radius: 14px;
|
|
272
272
|
box-shadow: 0 18px 34px -20px rgba(26,58,143,0.62);
|
|
273
273
|
}
|
|
274
|
+
.readyDecision .actions .skipHistoryButton {
|
|
275
|
+
min-height: 58px;
|
|
276
|
+
padding: 0 22px;
|
|
277
|
+
border-radius: 14px;
|
|
278
|
+
}
|
|
274
279
|
.readyDecision .actions .primary:not(:disabled):hover { box-shadow: 0 22px 38px -18px rgba(26,58,143,0.66); }
|
|
275
280
|
.readyDecision .actions .primary .setupGlyph { width: 19px; height: 19px; }
|
|
276
281
|
/* Proof strip: one quiet line of evidence (source split), not stat cards. Centered. */
|
|
@@ -1383,6 +1388,7 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1383
1388
|
|
|
1384
1389
|
.sessionPicker {
|
|
1385
1390
|
width: min(860px, calc(100vw - 32px));
|
|
1391
|
+
height: min(780px, calc(100vh - 32px));
|
|
1386
1392
|
max-height: min(780px, calc(100vh - 32px));
|
|
1387
1393
|
margin: auto;
|
|
1388
1394
|
border: 0;
|
|
@@ -1393,7 +1399,7 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1393
1399
|
box-shadow: 0 34px 90px -30px rgba(10,18,46,0.68);
|
|
1394
1400
|
}
|
|
1395
1401
|
.sessionPicker::backdrop { background: rgba(13,21,43,0.62); backdrop-filter: blur(4px); }
|
|
1396
|
-
.sessionPickerShell { display: grid; grid-template-rows: auto auto auto minmax(
|
|
1402
|
+
.sessionPickerShell { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr) auto; height: 100%; min-height: 0; max-height: min(780px, calc(100vh - 32px)); overflow: hidden; background: var(--echo-paper-white); }
|
|
1397
1403
|
.sessionPickerHead {
|
|
1398
1404
|
display: flex;
|
|
1399
1405
|
align-items: flex-start;
|
|
@@ -1465,9 +1471,60 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1465
1471
|
padding: 9px 24px;
|
|
1466
1472
|
}
|
|
1467
1473
|
.sessionPickerActions > strong { margin-right: auto; color: var(--echo-ink-primary); font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
1468
|
-
.
|
|
1474
|
+
.sessionPickerStatus { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
|
1469
1475
|
.sessionPickerActions .textButton { min-height: 32px; color: var(--echo-ink-primary); }
|
|
1470
|
-
.
|
|
1476
|
+
.sessionPickerListWrap { position: relative; min-height: 0; overflow: hidden; background: var(--echo-paper-white); }
|
|
1477
|
+
.sessionPickerList { height: 100%; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; background: var(--echo-paper-white); padding: 6px 0; }
|
|
1478
|
+
.sessionPickerListWrap.is-limit-blocked .sessionPickerList {
|
|
1479
|
+
filter: blur(4px);
|
|
1480
|
+
opacity: 0.36;
|
|
1481
|
+
pointer-events: none;
|
|
1482
|
+
transform: scale(1.012);
|
|
1483
|
+
transition: filter 180ms ease, opacity 180ms ease, transform 180ms ease;
|
|
1484
|
+
user-select: none;
|
|
1485
|
+
}
|
|
1486
|
+
.sessionPickerLimitOverlay {
|
|
1487
|
+
position: absolute;
|
|
1488
|
+
inset: 0;
|
|
1489
|
+
z-index: 3;
|
|
1490
|
+
display: grid;
|
|
1491
|
+
place-items: center;
|
|
1492
|
+
overflow-y: auto;
|
|
1493
|
+
background: rgba(245,247,252,0.38);
|
|
1494
|
+
padding: 20px;
|
|
1495
|
+
backdrop-filter: blur(2px);
|
|
1496
|
+
}
|
|
1497
|
+
.sessionPickerLimitOverlay[hidden] { display: none; }
|
|
1498
|
+
.sessionPickerLimitCard {
|
|
1499
|
+
width: min(470px, 100%);
|
|
1500
|
+
border: 1px solid rgba(26,58,143,0.18);
|
|
1501
|
+
border-radius: 18px;
|
|
1502
|
+
background: rgba(255,255,255,0.92);
|
|
1503
|
+
padding: clamp(24px, 5vw, 36px);
|
|
1504
|
+
text-align: center;
|
|
1505
|
+
box-shadow: 0 24px 56px -28px rgba(20,36,84,0.46), 0 6px 18px rgba(20,24,34,0.10);
|
|
1506
|
+
}
|
|
1507
|
+
.sessionPickerLimitEyebrow {
|
|
1508
|
+
display: inline-block;
|
|
1509
|
+
color: #9b6612;
|
|
1510
|
+
font-family: var(--echo-font-mono);
|
|
1511
|
+
font-size: 10px;
|
|
1512
|
+
font-weight: 900;
|
|
1513
|
+
letter-spacing: 0.1em;
|
|
1514
|
+
text-transform: uppercase;
|
|
1515
|
+
}
|
|
1516
|
+
.sessionPickerLimitCard h3 {
|
|
1517
|
+
margin: 8px 0 0;
|
|
1518
|
+
color: var(--echo-ink-text);
|
|
1519
|
+
font-family: var(--echo-font-brand);
|
|
1520
|
+
font-size: clamp(28px, 5vw, 40px);
|
|
1521
|
+
font-weight: 900;
|
|
1522
|
+
line-height: 1;
|
|
1523
|
+
}
|
|
1524
|
+
.sessionPickerLimitCard p { margin: 13px auto 0; max-width: 360px; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.5; }
|
|
1525
|
+
.sessionPickerLimitActions { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 22px; }
|
|
1526
|
+
.sessionPickerLimitActions .button { min-height: 46px; padding: 0 20px; }
|
|
1527
|
+
.sessionPickerLimitActions .textButton { color: var(--echo-ink-primary); }
|
|
1471
1528
|
.sessionPickerRow {
|
|
1472
1529
|
display: grid;
|
|
1473
1530
|
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
|
@@ -1521,6 +1578,7 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1521
1578
|
padding: 14px 24px;
|
|
1522
1579
|
}
|
|
1523
1580
|
.sessionPickerFoot p { margin: 0; color: var(--echo-ink-mute); font-size: 12px; }
|
|
1581
|
+
.sessionPickerFootActions { display: flex; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
|
|
1524
1582
|
.sessionPickerFoot button { min-width: 170px; }
|
|
1525
1583
|
|
|
1526
1584
|
.pauseStatus {
|
|
@@ -1740,6 +1798,10 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1740
1798
|
border-radius: 4px;
|
|
1741
1799
|
box-shadow: none;
|
|
1742
1800
|
}
|
|
1801
|
+
.readyDecision .actions .skipHistoryButton {
|
|
1802
|
+
min-height: 52px;
|
|
1803
|
+
border-radius: 4px;
|
|
1804
|
+
}
|
|
1743
1805
|
.readyDecision .actions .primary:not(:disabled):hover { box-shadow: none; }
|
|
1744
1806
|
.ctaMeta { justify-content: flex-start; }
|
|
1745
1807
|
.readyFoot {
|
|
@@ -1948,7 +2010,8 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1948
2010
|
.setupPlanConfirmed { grid-template-columns: auto minmax(0, 1fr); }
|
|
1949
2011
|
.setupPlanConfirmed .textButton { grid-column: 2; width: fit-content; }
|
|
1950
2012
|
.readyDecision .actions,
|
|
1951
|
-
.readyDecision .actions .primary
|
|
2013
|
+
.readyDecision .actions .primary,
|
|
2014
|
+
.readyDecision .actions .skipHistoryButton { width: 100%; }
|
|
1952
2015
|
.readyDecision .actions .primary { min-width: 0; }
|
|
1953
2016
|
.readyGo,
|
|
1954
2017
|
.readyFoot,
|
|
@@ -1965,15 +2028,21 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1965
2028
|
.recallPromptActions { display: grid; grid-template-columns: 1fr; }
|
|
1966
2029
|
.recallAgent { width: 100%; }
|
|
1967
2030
|
.skipStage { width: calc(100vw - 28px); padding: 26px 20px; }
|
|
1968
|
-
.sessionPicker { width: 100vw; max-height: 100vh; border-radius: 0; border-left-width: 4px; }
|
|
1969
|
-
.sessionPickerShell { max-height: 100vh; min-height:
|
|
2031
|
+
.sessionPicker { width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; border-left-width: 4px; }
|
|
2032
|
+
.sessionPickerShell { height: 100%; max-height: 100vh; min-height: 0; }
|
|
1970
2033
|
.sessionPickerHead { padding: 18px 16px 15px; }
|
|
1971
2034
|
.sessionPickerToolbar { grid-template-columns: 1fr; padding: 12px 16px; }
|
|
1972
2035
|
.sessionPickerActions { flex-wrap: wrap; padding: 8px 16px; }
|
|
1973
2036
|
.sessionPickerActions > strong { width: 100%; }
|
|
1974
2037
|
.sessionPickerRow { grid-template-columns: auto auto minmax(0, 1fr); margin: 0 4px; padding: 10px 8px; }
|
|
1975
2038
|
.sessionPickerMeta { display: none; }
|
|
2039
|
+
.sessionPickerLimitOverlay { padding: 14px; }
|
|
2040
|
+
.sessionPickerLimitCard { border-radius: 15px; padding: 24px 18px; }
|
|
2041
|
+
.sessionPickerLimitActions { display: grid; }
|
|
2042
|
+
.sessionPickerLimitActions .button,
|
|
2043
|
+
.sessionPickerLimitActions button { width: 100%; }
|
|
1976
2044
|
.sessionPickerFoot { align-items: stretch; flex-direction: column; padding: 12px 16px; }
|
|
2045
|
+
.sessionPickerFootActions { display: grid; }
|
|
1977
2046
|
.sessionPickerFoot button { width: 100%; }
|
|
1978
2047
|
}
|
|
1979
2048
|
|