diff options
Diffstat (limited to 'claude_sesiones/template.html')
| -rw-r--r-- | claude_sesiones/template.html | 1394 |
1 files changed, 0 insertions, 1394 deletions
diff --git a/claude_sesiones/template.html b/claude_sesiones/template.html deleted file mode 100644 index e56cc30..0000000 --- a/claude_sesiones/template.html +++ /dev/null @@ -1,1394 +0,0 @@ -<!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><script id="payload"></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><proyecto>/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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 <. 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>`).join(''); -} - -(function types() { - const counts = {}; - MEM.forEach(m => counts[m.ty] = (counts[m.ty] || 0) + 1); - typeEl.innerHTML = `<option value="">todos los tipos (${MEM.length})</option>` + - Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) - .map(([t, n]) => `<option value="${esc(t)}">${esc(t)} · ${n}</option>`).join(''); -})(); - -$('#nS').textContent = DATA.length; -$('#nM').textContent = MEM.length; - -/* ---------- lista ---------- */ - -function convoText(d) { - if (d._flat === undefined) d._flat = d.c.map(m => m.x).join('\n').toLowerCase(); - return d._flat; -} - -function snippetFor(d, q) { - for (const m of d.c) { - const i = m.x.toLowerCase().indexOf(q); - if (i < 0) continue; - const from = Math.max(0, i - 55); - const raw = m.x.slice(from, i + q.length + 75).replace(/\s+/g, ' '); - return (from ? '…' : '') + raw + '…'; - } - return null; -} - -function current() { - const q = qEl.value.trim().toLowerCase(); - const proj = projEl.value; - const deep = deepEl.checked; - - const list = DATA.filter(d => { - if (proj && d.p !== proj) return false; - if (emptyEl.checked && d.e) return false; - if (!q) return true; - if ((d.t || '').toLowerCase().includes(q) || d.p.toLowerCase().includes(q) - || d.id.toLowerCase().includes(q) || (d.b || '').toLowerCase().includes(q)) return true; - return deep && convoText(d).includes(q); - }); - - const k = sortKey; - list.sort((a, b) => { - if (k === 't') { - const av = (a.t || '~').toLowerCase(), bv = (b.t || '~').toLowerCase(); - return av < bv ? -sortDir : av > bv ? sortDir : 0; - } - const av = k === 'l' ? new Date(a.l) : (a[k] ?? -1); - cons |