aboutsummaryrefslogtreecommitdiffstats
path: root/claude_logbook/template.html
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-08-17 15:14:11 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-08-17 15:14:11 -0300
commitffe36e782b064a31b7a695ac1372f1329ac79153 (patch)
tree0a40ea9ade098e8e9ff21028d4a6442c133cba15 /claude_logbook/template.html
parent0d1fe286ac6ea30b91c15b44c0872b57a864ea7f (diff)
downloadclaude-logbook-ffe36e782b064a31b7a695ac1372f1329ac79153.tar.gz
claude-logbook-ffe36e782b064a31b7a695ac1372f1329ac79153.zip
Renombrar el proyecto a claude-logbook
El nombre en español no se encontraba, y la zona de "claude + sessions" ya está poblada por cosas distintas con las que se confundía: gapmiss/claude-sessions plugin de Obsidian kyupid/claude-sessions monitor de sesiones en curso (PyPI) claude-session-manager wrapper que preserva contexto (PyPI) LearnedGeek/claude-recall búsqueda con SQLite FTS5 (PyPI) "logbook" cubre las dos mitades — las conversaciones y las memorias — sin caer en "session manager", y encaja con lo que el proyecto produce: una bitácora que se abre en cualquier lado. claude_sesiones/ → claude_logbook/ y todas las referencias ~/.cache/claude-sesiones → ~/.cache/claude-logbook el resumen de PyPI pasa a inglés, como el README principal El comando viejo sigue funcionando: pyproject declara claude-sesiones como segundo entry point del mismo main, para no rehacer la memoria muscular. La interfaz sigue en español; solo cambia cómo se llama e instala el paquete. De paso, memory.py faltaba en la tabla de módulos de los dos READMEs. Claude-Session: https://claude.ai/code/session_01RmtZ9qBemrc9TncwVTG6ED
Diffstat (limited to 'claude_logbook/template.html')
-rw-r--r--claude_logbook/template.html1394
1 files changed, 1394 insertions, 0 deletions
diff --git a/claude_logbook/template.html b/claude_logbook/template.html
new file mode 100644
index 0000000..7c8ba76
--- /dev/null
+++ b/claude_logbook/template.html
@@ -0,0 +1,1394 @@
+<!doctype html>
+<html lang="es">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="color-scheme" content="light dark">
+<meta name="robots" content="noindex, nofollow">
+<title>Sesiones de Claude Code</title>
+
+<script>
+ // Corre antes del primer pintado para que el tema elegido no parpadee:
+ // primero la preferencia guardada, si no la del sistema.
+ (function () {
+ var saved = null;
+ try { saved = localStorage.getItem('cs-tema'); } catch (e) {}
+ var dark = saved ? saved === 'dark'
+ : matchMedia('(prefers-color-scheme: dark)').matches;
+ document.documentElement.dataset.theme = dark ? 'dark' : 'light';
+ })();
+</script>
+
+<style>
+ /* Una paleta por tema y nada más: el data-theme lo pone el script de arriba
+ antes de pintar, así que no hace falta repetir los valores dentro de una
+ @media. El botón del toolbar cambia ese mismo atributo. */
+
+ :root, :root[data-theme="light"] {
+ --ground: #EDEEF2;
+ --surface: #FFFFFF;
+ --raised: #F6F7F9;
+ --ink: #14161C;
+ --ink-soft: #3D4250;
+ --muted: #6A6F7E;
+ --faint: #9AA0AE;
+ --line: #DCDEE5;
+ --line-soft:#E7E9EE;
+ --amber: #A85A16;
+ --amber-lo: #C98A47;
+ --amber-bg: rgba(168, 90, 22, 0.09);
+ --scrim: rgba(20, 22, 28, .38);
+ --shadow: 0 1px 2px rgba(20, 22, 28, .06), 0 8px 24px -12px rgba(20, 22, 28, .18);
+ --shadow-lg:0 24px 70px -20px rgba(20, 22, 28, .45);
+ --focus: #2C6FD1;
+
+ --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ }
+
+ :root[data-theme="dark"] {
+ --ground: #0E1015;
+ --surface: #171A21;
+ --raised: #1D212A;
+ --ink: #DFE2EA;
+ --ink-soft: #B4BAC7;
+ --muted: #878DA0;
+ --faint: #666C7D;
+ --line: #262A34;
+ --line-soft:#1F232C;
+ --amber: #E29A4A;
+ --amber-lo: #8A6435;
+ --amber-bg: rgba(226, 154, 74, 0.11);
+ --scrim: rgba(0, 0, 0, .62);
+ --shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.7);
+ --shadow-lg:0 24px 70px -20px rgba(0,0,0,.85);
+ --focus: #6BA5F0;
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ background: var(--ground);
+ color: var(--ink);
+ font-family: var(--sans);
+ font-size: 15px;
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ }
+ body.locked { overflow: hidden; }
+
+ .wrap { max-width: 1180px; margin: 0 auto; padding: 40px 24px 96px; }
+
+ /* ---------- masthead ---------- */
+
+ .masthead { display: flex; flex-direction: column; gap: 8px; margin-bottom: 28px; }
+
+ .eyebrow {
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: .14em;
+ text-transform: uppercase;
+ color: var(--amber);
+ }
+
+ h1 {
+ margin: 0;
+ font-size: clamp(28px, 4.5vw, 40px);
+ line-height: 1.1;
+ letter-spacing: -.022em;
+ font-weight: 620;
+ text-wrap: balance;
+ }
+
+ .lede { margin: 0; max-width: 64ch; color: var(--muted); font-size: 15px; }
+ .lede code, .foot code {
+ font-family: var(--mono);
+ font-size: .88em;
+ background: var(--amber-bg);
+ color: var(--amber);
+ padding: 1px 5px;
+ border-radius: 4px;
+ }
+
+ /* ---------- stats ---------- */
+
+ .stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
+ gap: 1px;
+ background: var(--line);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ overflow: hidden;
+ margin-bottom: 24px;
+ }
+ .stat { background: var(--surface); padding: 14px 16px 16px; }
+ .stat-k {
+ display: block;
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: .13em;
+ text-transform: uppercase;
+ color: var(--faint);
+ margin-bottom: 5px;
+ }
+ .stat-v {
+ font-family: var(--mono);
+ font-size: 22px;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: -.02em;
+ color: var(--ink);
+ }
+ .stat-v small { font-size: 12px; color: var(--muted); letter-spacing: 0; }
+
+ /* ---------- toolbar ---------- */
+
+ .toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+ padding: 12px 0;
+ background: linear-gradient(var(--ground) 76%, transparent);
+ }
+
+ .field { position: relative; flex: 1 1 240px; min-width: 180px; }
+ .field svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--faint); pointer-events: none; }
+
+ input[type="search"], select {
+ width: 100%;
+ font: inherit;
+ font-size: 14px;
+ color: var(--ink);
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 9px 12px;
+ appearance: none;
+ }
+ input[type="search"] { padding-left: 34px; font-family: var(--mono); font-size: 13px; }
+ input[type="search"]::-webkit-search-cancel-button { filter: grayscale(1) opacity(.5); }
+
+ select {
+ flex: 0 1 auto;
+ width: auto;
+ max-width: 300px;
+ padding-right: 30px;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
+ background-position: calc(100% - 16px) calc(50% + 1px), calc(100% - 11px) calc(50% + 1px);
+ background-size: 5px 5px, 5px 5px;
+ background-repeat: no-repeat;
+ }
+
+ .toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--muted);
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 9px 12px;
+ cursor: pointer;
+ user-select: none;
+ white-space: nowrap;
+ }
+ .toggle input { accent-color: var(--amber); margin: 0; }
+ .toggle:hover { color: var(--ink); }
+ .toggle:has(input:checked) { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ .tbtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--muted);
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 9px 11px;
+ cursor: pointer;
+ flex-shrink: 0;
+ }
+ .tbtn:hover { color: var(--amber); border-color: var(--amber-lo); }
+ .tbtn .sun { display: none; }
+ :root[data-theme="dark"] .tbtn .sun { display: block; }
+ :root[data-theme="dark"] .tbtn .moon { display: none; }
+
+ .noscript {
+ margin: 0 0 24px;
+ padding: 14px 16px;
+ border: 1px solid var(--amber-lo);
+ background: var(--amber-bg);
+ border-radius: 10px;
+ color: var(--ink);
+ font-size: 14px;
+ }
+
+ :is(input, select, button, .toggle, .row[tabindex]):focus-visible {
+ outline: 2px solid var(--focus);
+ outline-offset: 1px;
+ }
+
+ /* ---------- tabla ---------- */
+
+ .grid { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--surface); box-shadow: var(--shadow); }
+
+ .row {
+ display: grid;
+ grid-template-columns: 3px 1fr 108px 58px 66px 128px;
+ align-items: center;
+ gap: 0 14px;
+ border-bottom: 1px solid var(--line-soft);
+ }
+ .row:last-child { border-bottom: 0; }
+ .row.clickable { cursor: pointer; }
+ .row.clickable:hover { background: var(--raised); }
+
+ .head {
+ position: sticky;
+ top: 62px;
+ z-index: 10;
+ background: var(--raised);
+ border-bottom: 1px solid var(--line);
+ }
+ .head span, .head button {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ color: var(--faint);
+ padding: 9px 0;
+ background: none;
+ border: 0;
+ text-align: left;
+ cursor: default;
+ }
+ .head button { cursor: pointer; display: inline-flex; align-items: center; gap: 4px; }
+ .head button:hover { color: var(--ink); }
+ .head button[aria-sort] { color: var(--amber); }
+ .head button .car { opacity: 0; }
+ .head button[aria-sort] .car { opacity: 1; }
+ .head .num { justify-content: flex-end; text-align: right; }
+ .head .pad-l { padding-left: 14px; }
+ .head .pad-r { padding-right: 16px; }
+
+ .r-body { padding: 11px 0 12px; min-width: 0; }
+
+ .stripe { align-self: stretch; background: var(--stripe, transparent); }
+
+ .title {
+ margin: 0 0 3px;
+ font-size: 14px;
+ line-height: 1.42;
+ color: var(--ink);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+ mark { background: var(--amber-bg); color: var(--amber); border-radius: 3px; padding: 0 1px; }
+
+ .meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px 8px;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--muted);
+ min-width: 0;
+ }
+ .path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
+
+ .chip {
+ font-size: 10px;
+ letter-spacing: .06em;
+ text-transform: uppercase;
+ padding: 1px 5px;
+ border-radius: 3px;
+ border: 1px solid var(--line);
+ color: var(--faint);
+ white-space: nowrap;
+ }
+ .chip-branch { color: var(--muted); }
+ .chip-empty, .chip-auto { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ .snippet {
+ margin: 5px 0 0;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ line-height: 1.5;
+ color: var(--muted);
+ border-left: 2px solid var(--amber-lo);
+ padding-left: 9px;
+ }
+
+ .cell-n {
+ font-family: var(--mono);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ color: var(--ink-soft);
+ text-align: right;
+ padding: 11px 0 12px;
+ }
+ .cell-date { font-family: var(--mono); font-size: 12px; font-variant-numeric: tabular-nums; color: var(--ink-soft); padding: 11px 0 12px; }
+ .cell-date small { display: block; color: var(--faint); font-size: 10.5px; letter-spacing: .04em; }
+ .dim { color: var(--faint); }
+
+ .cell-act { padding: 9px 16px 9px 0; display: flex; justify-content: flex-end; }
+
+ .copy {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--muted);
+ background: var(--raised);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ padding: 6px 9px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ white-space: nowrap;
+ transition: color .12s, border-color .12s, background .12s;
+ }
+ .copy:hover, .copy.done { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ .empty-row .title { color: var(--muted); font-style: italic; }
+
+ .none { padding: 48px 20px; text-align: center; color: var(--muted); font-family: var(--mono); font-size: 13px; }
+ .count { font-family: var(--mono); font-size: 11.5px; color: var(--faint); padding: 10px 2px 0; letter-spacing: .04em; }
+
+ /* ---------- lector ---------- */
+
+ .scrim {
+ position: fixed;
+ inset: 0;
+ background: var(--scrim);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity .2s;
+ z-index: 40;
+ }
+ .scrim.open { opacity: 1; pointer-events: auto; }
+
+ .reader {
+ position: fixed;
+ top: 0;
+ right: 0;
+ height: 100%;
+ width: min(920px, 100%);
+ display: flex;
+ flex-direction: column;
+ background: var(--ground);
+ border-left: 1px solid var(--line);
+ box-shadow: var(--shadow-lg);
+ transform: translateX(100%);
+ transition: transform .24s cubic-bezier(.4, 0, .2, 1);
+ z-index: 41;
+ visibility: hidden;
+ }
+ .reader.open { transform: none; visibility: visible; }
+
+ .reader-head {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ padding: 18px 20px 15px;
+ border-bottom: 1px solid var(--line);
+ background: var(--surface);
+ }
+ .reader-head h2 {
+ margin: 0 0 6px;
+ font-size: 17px;
+ line-height: 1.3;
+ letter-spacing: -.012em;
+ font-weight: 600;
+ text-wrap: balance;
+ }
+ .rh-text { flex: 1; min-width: 0; }
+ .rh-acts { display: flex; gap: 7px; flex-shrink: 0; }
+
+ .xbtn {
+ background: var(--raised);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ color: var(--muted);
+ cursor: pointer;
+ padding: 7px 9px;
+ display: inline-flex;
+ align-items: center;
+ transition: color .12s, border-color .12s, background .12s;
+ }
+ .xbtn:hover { color: var(--ink); }
+ .xbtn.done { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ .transcript {
+ flex: 1;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding: 20px 20px 64px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ }
+
+ .turn { display: flex; flex-direction: column; gap: 5px; }
+ .who {
+ font-family: var(--mono);
+ font-size: 10px;
+ letter-spacing: .13em;
+ text-transform: uppercase;
+ color: var(--faint);
+ }
+ .turn-u .who { color: var(--amber); }
+
+ .bubble {
+ border-radius: 8px;
+ padding: 11px 14px;
+ font-size: 14.5px;
+ line-height: 1.62;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ border: 1px solid var(--line-soft);
+ background: var(--surface);
+ color: var(--ink-soft);
+ }
+ .turn-u .bubble { background: var(--amber-bg); border-color: var(--amber-lo); color: var(--ink); }
+
+ .bubble pre {
+ margin: 9px 0;
+ padding: 10px 12px;
+ background: var(--ground);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ overflow-x: auto;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ line-height: 1.55;
+ white-space: pre;
+ color: var(--ink);
+ }
+ .bubble code {
+ font-family: var(--mono);
+ font-size: .87em;
+ background: var(--raised);
+ border: 1px solid var(--line-soft);
+ border-radius: 4px;
+ padding: 1px 4px;
+ }
+ .bubble strong { color: var(--ink); font-weight: 600; }
+ .bubble .h {
+ display: block;
+ margin: 4px 0 -2px;
+ font-size: .95em;
+ font-weight: 620;
+ letter-spacing: -.008em;
+ color: var(--ink);
+ }
+ .bubble pre code { background: none; border: 0; padding: 0; font-size: inherit; }
+
+ .tool {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--muted);
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ padding: 3px 0 3px 12px;
+ border-left: 2px solid var(--line);
+ overflow-wrap: anywhere;
+ }
+ .tool b { color: var(--amber); font-weight: 500; flex-shrink: 0; }
+
+ .reader-empty { color: var(--muted); font-family: var(--mono); font-size: 13px; text-align: center; padding: 60px 20px; }
+
+ /* ---------- toast ---------- */
+
+ .toast {
+ position: fixed;
+ left: 50%;
+ bottom: 26px;
+ transform: translate(-50%, 14px);
+ background: var(--ink);
+ color: var(--ground);
+ font-family: var(--mono);
+ font-size: 12px;
+ padding: 9px 14px;
+ border-radius: 7px;
+ box-shadow: var(--shadow);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity .16s, transform .16s;
+ z-index: 60;
+ max-width: min(90vw, 560px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .toast.show { opacity: 1; transform: translate(-50%, 0); }
+
+ .legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 16px;
+ align-items: center;
+ margin-top: 18px;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--faint);
+ }
+ .legend i { display: inline-block; width: 16px; height: 3px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
+
+ .foot {
+ margin-top: 26px;
+ padding-top: 18px;
+ border-top: 1px solid var(--line);
+ color: var(--muted);
+ font-size: 13px;
+ max-width: 68ch;
+ }
+ .foot p { margin: 0 0 8px; }
+
+ @media (max-width: 860px) {
+ .wrap { padding: 28px 14px 72px; }
+ .row { grid-template-columns: 3px 1fr auto; gap: 0 12px; }
+ .head { display: none; }
+ .r-body { padding: 12px 0 4px; grid-column: 2 / -1; }
+ .cell-date { grid-column: 2; padding: 0 0 10px; }
+ .cell-n { display: none; }
+ .cell-act { grid-column: 3; grid-row: 2; padding: 0 12px 10px 0; }
+ select { max-width: 100%; flex: 1 1 100%; }
+ .reader { width: 100%; border-left: 0; }
+ }
+
+ /* ---------- pestañas y memorias ---------- */
+
+ .tabs {
+ display: flex;
+ gap: 2px;
+ margin-bottom: 18px;
+ border-bottom: 1px solid var(--line);
+ }
+ .tab {
+ font: inherit;
+ font-size: 13.5px;
+ color: var(--muted);
+ background: none;
+ border: 0;
+ border-bottom: 2px solid transparent;
+ padding: 8px 14px;
+ margin-bottom: -1px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ }
+ .tab:hover { color: var(--ink); }
+ .tab[aria-selected="true"] { color: var(--amber); border-bottom-color: var(--amber); }
+ .tab .n {
+ font-family: var(--mono);
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ color: var(--faint);
+ background: var(--raised);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 0 6px;
+ }
+ .tab[aria-selected="true"] .n { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ body[data-mode="s"] .only-m,
+ body[data-mode="m"] .only-s { display: none; }
+
+ .row.mem { grid-template-columns: 3px 1fr 104px 210px 108px; }
+
+ .mem-name { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
+ .mem-name .nm { font-size: 14px; color: var(--ink); font-family: var(--mono); }
+ .mem-desc {
+ margin: 3px 0 0;
+ font-size: 12.5px;
+ line-height: 1.45;
+ color: var(--muted);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .ty {
+ font-family: var(--mono);
+ font-size: 10.5px;
+ letter-spacing: .05em;
+ padding: 1px 6px;
+ border-radius: 3px;
+ border: 1px solid var(--line);
+ color: var(--muted);
+ white-space: nowrap;
+ }
+ .ty-project { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+ .ty-user { color: var(--focus); border-color: var(--focus); background: transparent; }
+ .ty-feedback { color: var(--copper, var(--amber)); border-color: var(--amber-lo); }
+ .ty-reference { color: var(--muted); }
+
+ /* Una memoria fuera de MEMORY.md sigue en disco pero no se carga en contexto. */
+ .chip-unlisted { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ .mem-body {
+ font-size: 14.5px;
+ line-height: 1.68;
+ color: var(--ink-soft);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+ .mem-body pre {
+ margin: 10px 0;
+ padding: 10px 12px;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ overflow-x: auto;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ white-space: pre;
+ color: var(--ink);
+ }
+ .mem-body code {
+ font-family: var(--mono);
+ font-size: .87em;
+ background: var(--surface);
+ border: 1px solid var(--line-soft);
+ border-radius: 4px;
+ padding: 1px 4px;
+ }
+ .mem-body pre code { background: none; border: 0; padding: 0; font-size: inherit; }
+ .mem-body strong { color: var(--ink); font-weight: 600; }
+ .mem-body .h { display: block; margin: 10px 0 -2px; font-weight: 620; color: var(--ink); }
+
+ .wl {
+ font-family: var(--mono);
+ font-size: .9em;
+ color: var(--amber);
+ background: var(--amber-bg);
+ border: 1px solid var(--amber-lo);
+ border-radius: 4px;
+ padding: 0 4px;
+ cursor: pointer;
+ }
+ .wl.dead { color: var(--faint); border-color: var(--line); background: none; cursor: default; }
+
+ .mem-foot {
+ margin-top: 22px;
+ padding-top: 14px;
+ border-top: 1px solid var(--line);
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--muted);
+ }
+ .mem-foot .lbl { color: var(--faint); margin-right: 6px; }
+
+ .jump {
+ align-self: flex-start;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--muted);
+ background: var(--raised);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ padding: 6px 10px;
+ cursor: pointer;
+ }
+ .jump:hover { color: var(--amber); border-color: var(--amber-lo); background: var(--amber-bg); }
+
+ @media (max-width: 860px) {
+ .row.mem { grid-template-columns: 3px 1fr auto; }
+ .row.mem .cell-ty { grid-column: 2; padding-bottom: 10px; }
+ .row.mem .cell-proj { display: none; }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ * { transition: none !important; }
+ }
+</style>
+</head>
+
+<body>
+<div class="wrap">
+ <header class="masthead">
+ <span class="eyebrow">~/.claude/projects</span>
+ <h1>Tus sesiones de Claude Code</h1>
+ <p class="lede">Cada conversación queda en un <code>.jsonl</code> local. Hacé clic en cualquier fila para leerla, o copiá el comando para retomarla — el <code>--resume</code> solo encuentra la sesión desde su directorio original, así que el botón copia el <code>cd</code> también.</p>
+ </header>
+
+ <noscript>
+ <p class="noscript">Esta página arma la lista con JavaScript. Con JavaScript
+ desactivado no se ve nada — los datos igual están adentro del archivo, en el
+ <code>&lt;script id="payload"&gt;</code>.</p>
+ </noscript>
+
+ <div class="tabs" role="tablist" aria-label="Qué mirar">
+ <button type="button" class="tab" role="tab" data-mode="s" aria-selected="true">
+ Sesiones <span class="n" id="nS"></span>
+ </button>
+ <button type="button" class="tab" role="tab" data-mode="m" aria-selected="false">
+ Memorias <span class="n" id="nM"></span>
+ </button>
+ </div>
+
+ <div class="stats" id="stats"></div>
+
+ <div class="toolbar">
+ <div class="field">
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14 14"/></svg>
+ <input type="search" id="q" placeholder="buscar título, ruta o uuid… (/)" autocomplete="off" spellcheck="false" aria-label="Buscar sesiones">
+ </div>
+ <select id="proj" aria-label="Filtrar por proyecto"></select>
+ <label class="toggle only-s"><input type="checkbox" id="deep"> buscar dentro del chat</label>
+ <label class="toggle only-s"><input type="checkbox" id="hideEmpty"> ocultar vacías</label>
+ <select id="mtype" class="only-m" aria-label="Filtrar por tipo de memoria"></select>
+ <label class="toggle only-m"><input type="checkbox" id="unlisted"> solo fuera del índice</label>
+ <button type="button" class="tbtn" id="theme" title="Cambiar entre claro y oscuro" aria-label="Cambiar tema">
+ <svg class="moon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M13.5 9.6A5.8 5.8 0 0 1 6.4 2.5a5.8 5.8 0 1 0 7.1 7.1Z"/></svg>
+ <svg class="sun" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="3.1"/><path d="M8 1v1.6M8 13.4V15M1 8h1.6M13.4 8H15M3.1 3.1l1.1 1.1M11.8 11.8l1.1 1.1M12.9 3.1l-1.1 1.1M4.2 11.8l-1.1 1.1"/></svg>
+ </button>
+ </div>
+
+ <div class="grid" role="table" aria-label="Sesiones de Claude Code">
+ <div class="row head only-s" role="row">
+ <span></span>
+ <button type="button" data-sort="t" class="pad-l">Sesión <span class="car">▾</span></button>
+ <button type="button" data-sort="l">Fecha <span class="car">▾</span></button>
+ <button type="button" data-sort="u" class="num">Msgs <span class="car">▾</span></button>
+ <button type="button" data-sort="d" class="num">Dur. <span class="car">▾</span></button>
+ <span class="pad-r" style="text-align:right">Reanudar</span>
+ </div>
+ <div class="row head mem only-m" role="row">
+ <span></span>
+ <button type="button" data-msort="name" class="pad-l">Memoria <span class="car">▾</span></button>
+ <button type="button" data-msort="ty">Tipo <span class="car">▾</span></button>
+ <button type="button" data-msort="p">Proyecto <span class="car">▾</span></button>
+ <button type="button" data-msort="l">Fecha <span class="car">▾</span></button>
+ </div>
+ <div id="rows"></div>
+ </div>
+
+ <p class="count" id="count"></p>
+
+ <div class="legend only-s">
+ <span>Franja izquierda = antigüedad:</span>
+ <span><i style="background:var(--amber)"></i>hoy / ayer</span>
+ <span><i style="background:var(--amber);opacity:.55"></i>última semana</span>
+ <span><i style="background:var(--amber);opacity:.26"></i>dos semanas</span>
+ <span><i style="background:var(--line)"></i>más viejo</span>
+ </div>
+
+ <div class="legend only-m">
+ <span><span class="chip chip-unlisted">fuera del índice</span> = está en disco pero no figura en MEMORY.md, así que no se carga en contexto</span>
+ </div>
+
+ <div class="foot only-m">
+ <p>Cada proyecto guarda sus recuerdos en <code>&lt;proyecto&gt;/memory/</code>: un <code>.md</code> por memoria y un <code>MEMORY.md</code> que los indexa. El índice es lo único que se lee al arrancar una sesión.</p>
+ <p>Los tipos los define Claude al escribirlas: <b>project</b> es trabajo en curso, <b>user</b> quién sos y cómo trabajás, <b>feedback</b> correcciones tuyas, <b>reference</b> punteros a recursos externos.</p>
+ </div>
+
+ <div class="foot only-s">
+ <p>Los títulos los genera Claude durante la sesión. Cuando falta, cae al primer mensaje tuyo — y ahí se nota, porque arranca en minúscula o suena a pregunta suelta.</p>
+ <p><b>Vacía</b> es una sesión que se abrió pero nunca recibió un mensaje: un <code>/resume</code> cancelado, un <code>/login</code>. <b>No interactiva</b> es un <code>claude -p</code> con algo piped por stdin — típicamente un <code>git diff</code> para redactar el mensaje de commit, no un chat.</p>
+ <p>La transcripción trae el texto de la conversación y una línea por herramienta usada, pero no lo que esas herramientas devolvieron: son el 95 % del volumen en disco.</p>
+ <p id="snap"></p>
+ </div>
+</div>
+
+<div class="scrim" id="scrim"></div>
+
+<aside class="reader" id="reader" role="dialog" aria-modal="true" aria-labelledby="rTitle" tabindex="-1">
+ <div class="reader-head">
+ <div class="rh-text">
+ <h2 id="rTitle"></h2>
+ <div class="meta" id="rMeta"></div>
+ </div>
+ <div class="rh-acts">
+ <button type="button" class="xbtn" id="rCopy" title="Copiar comando para reanudar" aria-label="Copiar comando para reanudar">
+ <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5.5" y="5.5" width="8" height="9" rx="1.5"/><path d="M10.5 3.5H3a.5.5 0 0 0-.5.5v8"/></svg>
+ </button>
+ <button type="button" class="xbtn" id="rClose" title="Cerrar (Esc)" aria-label="Cerrar">
+ <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="m4 4 8 8M12 4l-8 8"/></svg>
+ </button>
+ </div>
+ </div>
+ <div class="transcript" id="rBody"></div>
+</aside>
+
+<div class="toast" id="toast" role="status" aria-live="polite"></div>
+
+<script id="payload" type="application/json">__DATA__</script>
+<script>
+const PAYLOAD = JSON.parse(document.getElementById('payload').textContent);
+const DATA = PAYLOAD.s || [];
+const MEM = PAYLOAD.m || [];
+// Instantánea = la actividad más reciente de los datos, para que "hoy" siga siendo
+// cierto respecto de cuándo se leyeron los logs y no del reloj de quien mira.
+const NOW = new Date(Math.max(
+ ...DATA.map(d => +new Date(d.l)), ...MEM.map(m => +new Date(m.l))));
+const BY_ID = new Map(DATA.map(d => [d.id, d]));
+const BY_NAME = new Map(MEM.map(m => [m.name, m]));
+// Los enlaces [[...]] pueden apuntar al nombre declarado o al del archivo.
+MEM.forEach(m => { const f = m.file.replace(/\.md$/, ''); if (!BY_NAME.has(f)) BY_NAME.set(f, m); });
+
+const $ = s => document.querySelector(s);
+const rowsEl = $('#rows'), qEl = $('#q'), projEl = $('#proj'), deepEl = $('#deep'),
+ emptyEl = $('#hideEmpty'), countEl = $('#count'), toastEl = $('#toast'),
+ readerEl = $('#reader'), scrimEl = $('#scrim'), bodyEl = $('#rBody'),
+ typeEl = $('#mtype'), unlistedEl = $('#unlisted');
+
+let sortKey = 'l', sortDir = -1, lastFocus = null;
+let mSortKey = 'l', mSortDir = -1;
+// Qué hay abierto en el lector: {kind: 's'|'m', key} o null.
+let open_ = null;
+// Sin memorias la pestaña no aporta nada, así que arrancamos siempre en sesiones.
+let mode = 's';
+
+/* ---------- formato ---------- */
+
+const DAY = 864e5;
+const MES = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
+const pad = n => String(n).padStart(2, '0');
+
+const daysAgo = iso => (NOW - new Date(iso)) / DAY;
+const fmtDate = iso => { const d = new Date(iso); return `${pad(d.getDate())} ${MES[d.getMonth()]}`; };
+const fmtTime = iso => { const d = new Date(iso); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; };
+
+function fmtRel(iso) {
+ const n = daysAgo(iso);
+ if (n < 1) return 'hoy';
+ if (n < 2) return 'ayer';
+ if (n < 7) return `hace ${Math.floor(n)} d`;
+ if (n < 30) return `hace ${Math.floor(n / 7)} sem`;
+ return `hace ${Math.floor(n / 30)} mes`;
+}
+function fmtDur(m) {
+ if (m == null) return '—';
+ if (m < 1) return '<1m';
+ if (m < 60) return m + 'm';
+ const h = Math.floor(m / 60), r = m % 60;
+ return r ? `${h}h${pad(r)}` : `${h}h`;
+}
+function stripeFor(iso) {
+ const n = daysAgo(iso);
+ if (n < 2) return 'var(--amber)';
+ if (n < 7) return 'color-mix(in srgb, var(--amber) 55%, transparent)';
+ if (n < 14) return 'color-mix(in srgb, var(--amber) 26%, transparent)';
+ return 'var(--line)';
+}
+
+const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
+
+// Resalta la primera coincidencia. La aguja se escapa igual que el pajar: si
+// no, buscar "&" o "<" nunca encontraría nada, porque en `s` ya son entidades.
+function mark(text, q) {
+ const s = esc(text);
+ if (!q) return s;
+ const needle = esc(q);
+ const i = s.toLowerCase().indexOf(needle.toLowerCase());
+ if (i < 0) return s;
+ return s.slice(0, i) + '<mark>' + s.slice(i, i + needle.length) + '</mark>'
+ + s.slice(i + needle.length);
+}
+
+// Markdown mínimo: código, negritas y títulos. Los saltos los resuelve pre-wrap.
+// El código se guarda aparte antes de tocar negritas, para que un ** dentro de
+// un bloque de código quede tal cual.
+function renderBody(text, wikilinks) {
+ const stash = [];
+ // Tras esc() no queda ningún "<" crudo en el texto: todos son &lt;. Por eso un
+ // marcador con "<" no puede colisionar con el contenido del mensaje.
+ const keep = html => `<K${stash.push(html) - 1}>`;
+
+ let out = esc(text);
+ out = out.replace(/```[a-zA-Z0-9_+-]*\n?([\s\S]*?)```/g,
+ (_, code) => keep(`<pre>${code.replace(/\n+$/, '')}</pre>`));
+ out = out.replace(/`([^`\n]+)`/g, (_, code) => keep(`<code>${code}</code>`));
+ out = out.replace(/^#{1,6}\s+(.+)$/gm, '<b class="h">$1</b>');
+ out = out.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
+ if (wikilinks) {
+ // Va acá adentro, con el código ya guardado aparte: un [[...]] dentro de un
+ // bloque de código tiene que quedar como texto.
+ out = out.replace(/\[\[([^\]\n]+)\]\]/g, (_, name) => {
+ const hit = BY_NAME.get(name);
+ return hit
+ ? `<button type="button" class="wl" data-wl="${esc(hit.name)}">${esc(name)}</button>`
+ : `<span class="wl dead" title="todavía no existe una memoria con ese nombre">${esc(name)}</span>`;
+ });
+ }
+
+ return out.replace(/<K(\d+)>/g, (_, i) => stash[i]);
+}
+
+/* ---------- cabecera ---------- */
+
+(function stats() {
+ const msgs = DATA.reduce((a, d) => a + d.u, 0);
+ const mb = DATA.reduce((a, d) => a + d.k, 0) / 1024;
+ const ds = DATA.map(d => new Date(d.l)).sort((a, b) => a - b);
+ const f = ds[0], l = ds[ds.length - 1];
+ $('#stats').innerHTML = [
+ ['sesiones', DATA.length, ''],
+ ['proyectos', new Set(DATA.map(d => d.p)).size, ''],
+ ['tus mensajes', msgs, ''],
+ ['en disco', mb.toFixed(1), ' MB'],
+ ['período', `${pad(f.getDate())} ${MES[f.getMonth()]} – ${pad(l.getDate())} ${MES[l.getMonth()]}`, ''],
+ ['memorias', MEM.length, ''],
+ ].map(([k, v, s]) => `<div class="stat"><span class="stat-k">${k}</span><span class="stat-v">${v}${s ? `<small>${s}</small>` : ''}</span></div>`).join('');
+})();
+
+function fillProjects() {
+ const src = mode === 'm' ? MEM : DATA;
+ const counts = {};
+ src.forEach(d => counts[d.p] = (counts[d.p] || 0) + 1);
+ const label = mode === 'm' ? 'memorias' : 'sesiones';
+ projEl.innerHTML = `<option value="">todos los proyectos (${src.length} ${label})</option>` +
+ Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
+ .map(([p, n]) => `<option value="${esc(p)}">${esc(p)} · ${n}</option>