eklavya 1.27.1 → 1.28.0

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.
@@ -358,6 +358,66 @@
358
358
  summary.qa__meta::before { content:'▸'; color:var(--faint); }
359
359
  details[open] > summary.qa__meta::before { content:'▾'; color:var(--spot); }
360
360
  summary.qa__meta:focus-visible { outline:none; box-shadow:var(--ring); }
361
+
362
+ /* ---------- memory timeline ----------
363
+ One line per entry on a vertical rule. A row is a <details>: the title is
364
+ the summary, the snippet and the link to the full entry open under it, so
365
+ the list stays scannable and detail costs one click. */
366
+ .counts { margin:var(--space-2) 0 0; font-family:var(--font-mono); font-size:12px; color:var(--faint); }
367
+ .counts .warn { color:var(--warning); }
368
+ .tl { list-style:none; margin:var(--space-4) 0 0; padding:0; position:relative; }
369
+ .tl::before { content:''; position:absolute; left:67px; top:8px; bottom:8px; width:1px; background:var(--line-2); }
370
+ .tl-day { display:flex; align-items:baseline; gap:var(--space-3); padding:var(--space-5) 0 var(--space-2) 88px; position:relative; }
371
+ .tl-day:first-child { padding-top:0; }
372
+ .tl-day::before { content:''; position:absolute; left:62px; top:calc(var(--space-5) + 6px); width:11px; height:11px; background:var(--line-2); }
373
+ .tl-day:first-child::before { top:6px; }
374
+ .tl-day h2 { margin:0; font-family:var(--font-disp); font-size:var(--text-lg); font-weight:var(--weight-bold); letter-spacing:-.02em; }
375
+ .tl-day span { font-family:var(--font-mono); font-size:10.5px; letter-spacing:var(--tracking-caps); text-transform:uppercase; color:var(--spot); }
376
+ .tl-gap { padding:var(--space-2) 0 var(--space-2) 88px; font-family:var(--font-mono); font-size:11.5px; color:var(--faint); }
377
+ .tl-item > summary { list-style:none; cursor:pointer; display:grid; grid-template-columns:52px minmax(0,1fr) auto;
378
+ align-items:center; column-gap:36px; min-height:44px; padding-right:var(--space-3); position:relative; }
379
+ .tl-item > summary::-webkit-details-marker { display:none; }
380
+ .tl-item > summary::before { content:''; position:absolute; left:62px; top:50%; width:11px; height:11px; margin-top:-5.5px;
381
+ box-sizing:border-box; border-radius:50%; border:2px solid var(--dim); background:var(--bg); }
382
+ .tl-item > summary:hover { background:var(--panel); }
383
+ .tl-item > summary:focus-visible { outline:none; box-shadow:var(--ring); }
384
+ .tl-item[open] > summary::before { background:var(--spot); border-color:var(--spot); }
385
+ .tl-item[open] { background:var(--panel); }
386
+ .tl-time { font-family:var(--font-mono); font-size:12px; color:var(--faint); text-align:right; font-variant-numeric:tabular-nums; }
387
+ .tl-title { font-size:15px; font-weight:var(--weight-medium); color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
388
+ .tl-item[open] > summary .tl-title { font-weight:var(--weight-semibold); }
389
+ .tl-type { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.12em; text-transform:uppercase; color:var(--faint); white-space:nowrap; }
390
+ .tl-body { padding:0 var(--space-4) var(--space-4) 88px; display:grid; gap:var(--space-3); }
391
+ .tl-body p { margin:0; color:var(--dim); font-size:13.5px; max-width:72ch; overflow-wrap:anywhere; }
392
+ .tl-meta { display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-2) var(--space-4);
393
+ font-family:var(--font-mono); font-size:11.5px; color:var(--faint); }
394
+ .tl-meta .link { margin-left:auto; font-family:var(--font-body); font-size:13.5px; font-weight:var(--weight-semibold); }
395
+ .tl-item.gone .tl-title { text-decoration:line-through; color:var(--faint); }
396
+ /* A session is a larger ring; its observations fold under it. */
397
+ .tl-session > summary::before { width:15px; height:15px; margin-top:-7.5px; left:60px; border-color:var(--spot); }
398
+ .tl-session[open] > summary::before { background:var(--bg); }
399
+ .tl-session[open] { background:none; }
400
+ .tl-session > summary .tl-title { font-weight:var(--weight-medium); }
401
+ .tl-session > summary .tl-type { color:var(--spot); }
402
+ .tl-kids { list-style:none; margin:0; padding:0; }
403
+ .tl-kids .tl-item > summary::before { left:64px; width:7px; height:7px; margin-top:-3.5px; border-color:var(--faint-2); }
404
+ .tl-kids .tl-item[open] > summary::before { border-color:var(--spot); }
405
+ .tl-kids .tl-title { font-size:14px; padding-left:var(--space-6); }
406
+ .tl-kids .tl-body { padding-left:112px; }
407
+ .tl-kids .tl-gap { padding-left:112px; }
408
+ #mwhen, #mtype, #mtag { max-width:100%; min-width:0; }
409
+ /* Narrow: the title gets the width and two lines; the type drops under it. */
410
+ @media (max-width:560px) {
411
+ .tl-item > summary { grid-template-columns:52px minmax(0,1fr); padding:var(--space-2) var(--space-2) var(--space-2) 0; }
412
+ .tl-item > summary::before { top:calc(var(--space-2) + 11px); margin-top:0; }
413
+ .tl-session > summary::before { top:calc(var(--space-2) + 9px); }
414
+ .tl-kids .tl-item > summary::before { top:calc(var(--space-2) + 13px); }
415
+ .tl-time { align-self:start; padding-top:3px; }
416
+ .tl-title { white-space:normal; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
417
+ .tl-type { grid-column:2; margin-top:2px; }
418
+ .tl-kids .tl-type { padding-left:var(--space-6); }
419
+ .tl-body, .tl-kids .tl-body, .tl-kids .tl-gap { padding-left:88px; }
420
+ }
361
421
  .raw { font-family:var(--font-mono); font-size:12px; line-height:1.55; white-space:pre-wrap;
362
422
  word-break:break-word; color:var(--dim); background:var(--mass); padding:var(--space-3);
363
423
  max-height:320px; overflow:auto; margin:var(--space-3) 0 0; }
@@ -1728,24 +1788,72 @@ function memoryQuery(type) {
1728
1788
  return p.toString();
1729
1789
  }
1730
1790
 
1791
+ const dayName = (d) => d.toLocaleDateString(undefined, { weekday:'short', day:'numeric', month:'short' });
1792
+ const clock = (d) => d.toLocaleTimeString(undefined, { hour:'2-digit', minute:'2-digit' });
1793
+
1794
+ /** One entry on the timeline: a line that opens to its snippet and a link to the full entry. */
1795
+ function tlEntry(e) {
1796
+ const d = parse(e.occurred_at);
1797
+ const more = e.narrative_length > String(e.snippet ?? '').length;
1798
+ const gone = e.deleted_at || e.superseded_by;
1799
+ return `<li><details class="tl-item${gone ? ' gone' : ''}" data-entry="${esc(e.id)}">
1800
+ <summary><span class="tl-time">${d ? esc(clock(d)) : '—'}</span>
1801
+ <span class="tl-title">${esc(e.title)}</span><span class="tl-type">${esc(e.type ?? e.kind)}</span></summary>
1802
+ <div class="tl-body">
1803
+ ${e.snippet ? `<p>${esc(e.snippet)}${more ? '…' : ''}</p>` : ''}
1804
+ <div class="tl-meta"><span>${esc(pname(e.project))}</span><span>${plural(e.event_count, 'evidence event')}</span>
1805
+ ${entryMark(e)}<a class="link" data-go="${esc(MH('entry', e.id))}">Open entry →</a></div>
1806
+ </div></details></li>`;
1807
+ }
1808
+
1809
+ /**
1810
+ * The page's entries as a timeline: grouped by local day, and an entry that
1811
+ * shares a session with a session summary on this page folds under that
1812
+ * summary. Grouping is per page — a session whose summary sits on the next page
1813
+ * shows its observations as ordinary lines here — because the server pages
1814
+ * entries, not sessions.
1815
+ */
1731
1816
  function drawMemory(d) {
1732
1817
  T.memory.page = d.page; // the server clamps a page number a filter shrank
1733
- const rows = d.rows.map((e) => {
1734
- const gone = e.deleted_at || e.superseded_by;
1735
- const more = e.narrative_length > String(e.snippet ?? '').length;
1736
- return `<tr class="row${gone ? ' gone' : ''}" data-go="${esc(MH('entry', e.id))}">
1737
- <td class="slug" style="white-space:nowrap">${esc(stamp(e.occurred_at))}</td>
1738
- <td style="min-width:280px"><span class="title">${esc(e.title)}</span>
1739
- <div class="ctx">${esc(e.snippet ?? '')}${more ? '…' : ''}</div></td>
1740
- <td><span class="pill">${esc(e.type ?? e.kind)}</span></td>
1741
- <td style="min-width:120px"><div class="tags">${tagPills(e.tags)}</div></td>
1742
- <td class="slug">${esc(pname(e.project))}</td>
1743
- <td class="num">${e.event_count}</td>
1744
- <td>${entryMark(e)}</td>
1745
- </tr>`;
1746
- });
1747
- return table([{t:'When'},{t:'Observation'},{t:'Type'},{t:'Tags'},{t:'Project'},{t:'Evidence',n:1},{t:''}], rows)
1748
- + pager('memory', d.total, d.page, d.per);
1818
+ if (!d.rows.length) return '<p class="empty">Nothing in memory matches these filters.</p>';
1819
+ const summaries = new Map(d.rows.filter((e) => e.kind === 'session_summary' && e.session_id).map((e) => [e.session_id, e]));
1820
+ const kids = new Map();
1821
+ for (const e of d.rows) {
1822
+ if (e.kind === 'session_summary' || !summaries.has(e.session_id)) continue;
1823
+ if (!kids.has(e.session_id)) kids.set(e.session_id, []);
1824
+ kids.get(e.session_id).push(e);
1825
+ }
1826
+ const now = new Date();
1827
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
1828
+ const out = [];
1829
+ let last = null;
1830
+ for (const e of d.rows) {
1831
+ if (e.kind !== 'session_summary' && summaries.has(e.session_id)) continue; // drawn under its summary
1832
+ const at = parse(e.occurred_at);
1833
+ const day = at ? new Date(at.getFullYear(), at.getMonth(), at.getDate()) : null;
1834
+ if (day && (!last || day.getTime() !== last.getTime())) {
1835
+ if (last) {
1836
+ const gap = Math.round((last - day) / DAY_MS) - 1;
1837
+ const after = new Date(day.getFullYear(), day.getMonth(), day.getDate() + 1);
1838
+ const before = new Date(last.getFullYear(), last.getMonth(), last.getDate() - 1);
1839
+ if (gap === 1) out.push(`<li class="tl-gap">Nothing on ${esc(dayName(after))}</li>`);
1840
+ if (gap > 1) out.push(`<li class="tl-gap">Nothing from ${esc(dayName(after))} to ${esc(dayName(before))}</li>`);
1841
+ }
1842
+ const note = day.getTime() === today ? 'Today' : Math.round((today - day.getTime()) / DAY_MS) === 1 ? 'Yesterday' : '';
1843
+ out.push(`<li class="tl-day"><h2>${esc(dayName(day))}</h2>${note ? `<span>${note}</span>` : ''}</li>`);
1844
+ last = day;
1845
+ }
1846
+ const inner = kids.get(e.session_id);
1847
+ if (e.kind === 'session_summary' && inner) {
1848
+ out.push(`<li><details class="tl-item tl-session">
1849
+ <summary><span class="tl-time">${at ? esc(clock(at)) : '—'}</span>
1850
+ <span class="tl-title">${esc(e.title.replace(/^Session:\s*/, ''))}</span>
1851
+ <span class="tl-type">Session · ${plural(inner.length, 'observation')}</span></summary>
1852
+ <ol class="tl-kids">${inner.map(tlEntry).join('')}
1853
+ <li class="tl-gap"><a class="link" data-go="${esc(MH('entry', e.id))}">The session summary →</a></li></ol></details></li>`);
1854
+ } else out.push(tlEntry(e));
1855
+ }
1856
+ return `<ol class="tl" aria-label="Timeline">${out.join('')}</ol>` + pager('memory', d.total, d.page, d.per);
1749
1857
  }
1750
1858
 
1751
1859
  /**
@@ -1754,25 +1862,41 @@ function drawMemory(d) {
1754
1862
  * selector; indexing, reuse, delivery and assessment are only aggregated for
1755
1863
  * the whole database, and say so rather than pretend to be this project's.
1756
1864
  */
1757
- function memTiles() {
1865
+ function memCounts() {
1758
1866
  const M = S.memory;
1759
1867
  const scoped = PROJECT !== null;
1760
1868
  const inv = projectsInScope();
1761
1869
  const sum = (f) => inv.reduce((a, p) => a + f(p), 0);
1762
- const events = scoped ? sum((p) => p.memory.events) : M.captured;
1763
- const pending = scoped ? sum((p) => p.memory.pending) : M.pending;
1764
- const entries = scoped ? sum((p) => p.memory.entries) : M.entries;
1870
+ return {
1871
+ scoped,
1872
+ events: scoped ? sum((p) => p.memory.events) : M.captured,
1873
+ pending: scoped ? sum((p) => p.memory.pending) : M.pending,
1874
+ entries: scoped ? sum((p) => p.memory.entries) : M.entries,
1875
+ sessions: scoped ? sum((p) => p.memory.sessions) : INV.projects.reduce((a, p) => a + p.memory.sessions, 0),
1876
+ };
1877
+ }
1878
+
1879
+ function memTiles() {
1880
+ const M = S.memory;
1881
+ const { scoped, events, pending, entries, sessions } = memCounts();
1765
1882
  const everywhere = scoped ? 'all projects · ' : '';
1766
1883
  return tiles([
1767
1884
  ['Observations', entries, scoped ? `live here · ${M.indexed} indexed across all projects`
1768
1885
  : `${M.entries_total} written · ${M.superseded} superseded · ${M.deleted} deleted · ${M.indexed} indexed`, '', MH('timeline')],
1769
1886
  ['Evidence', events, `${events - pending} distilled · ${pending} still pending`, pending ? 'warn' : '', MH('health')],
1770
- ['Sessions', scoped ? sum((p) => p.memory.sessions) : INV.projects.reduce((a, p) => a + p.memory.sessions, 0), 'that captured or remembered', '', MH('sessions')],
1887
+ ['Sessions', sessions, 'that captured or remembered', '', MH('sessions')],
1771
1888
  ['Reused', M.reused, `${everywhere}${M.exposed} actually delivered`, '', MH('reuse')],
1772
1889
  ['Assessed', M.assessed, everywhere + 'evidence concepts you were asked about', '', LH('concepts')],
1773
1890
  ]);
1774
1891
  }
1775
1892
 
1893
+ /** `YYYY-MM-DD` for n days before today, local — what a date input holds. */
1894
+ const daysAgo = (n) => {
1895
+ const d = new Date(); d.setDate(d.getDate() - n);
1896
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
1897
+ };
1898
+ const WHEN = { today:0, '7d':6, '30d':29 };
1899
+
1776
1900
  function viewMemory(param) {
1777
1901
  const t = T.memory;
1778
1902
  const M = S.memory;
@@ -1785,35 +1909,41 @@ function viewMemory(param) {
1785
1909
  t.until = CUR.query.get('to') || '';
1786
1910
  fill('mem-rows', '/api/memory?' + memoryQuery(type), drawMemory);
1787
1911
 
1788
- const chip = (k, label, n) =>
1789
- `<button class="chip" data-mtype="${esc(k)}" aria-pressed="${type === k}">${esc(label)}${n === undefined ? '' : ' ' + n}</button>`;
1790
- const typeChips = types.map((x) => chip(x.type, x.type, x.n)).join('')
1791
- + (type !== 'all' && !types.some((x) => x.type === type) ? chip(type, type, 0) : '');
1912
+ // A preset is only a `from` date, so the URL stays the one date format and a
1913
+ // shared link reads the same; anything else is a custom range.
1914
+ const when = !t.since && !t.until ? 'any'
1915
+ : !t.until && Object.keys(WHEN).find((k) => daysAgo(WHEN[k]) === t.since) || 'custom';
1916
+ const opt = (v, label, on) => `<option value="${esc(v)}"${on ? ' selected' : ''}>${esc(label)}</option>`;
1917
+ const typeOpts = opt('all', 'All types', type === 'all')
1918
+ + types.map((x) => opt(x.type, `${x.type} (${x.n})`, type === x.type)).join('')
1919
+ + (type !== 'all' && !types.some((x) => x.type === type) ? opt(type, `${type} (0)`, true) : '');
1920
+ const { entries, sessions, pending } = memCounts();
1792
1921
 
1793
1922
  return [
1794
1923
  `<div>
1795
1924
  <h1 class="page__title">Timeline</h1>
1796
- <p class="page__sub">What your sessions actually did, distilled into observations and kept with the raw
1797
- evidence behind each one. Corrections supersede rather than overwrite and deletions are marked, so what
1798
- you see is the audit trail and not a tidied version of it.</p>
1925
+ <p class="counts">${plural(entries, 'observation')} · ${plural(sessions, 'session')}${pending
1926
+ ? ` · <a class="link warn" data-go="${esc(MH('health'))}">${pending} evidence pending</a>` : ''}</p>
1799
1927
  </div>`,
1800
1928
 
1801
- memTiles(),
1802
-
1803
- card(null, null,
1804
- `<div class="filters">
1805
- <div class="grow"><input type="search" id="mq" value="${esc(t.q)}" placeholder="Search titles, narrative, facts and files…" aria-label="Search memory"></div>
1929
+ `<div>
1930
+ <div class="filters">
1931
+ <div class="grow"><input type="search" id="mq" value="${esc(t.q)}" placeholder="Search memory" aria-label="Search memory"></div>
1932
+ <select id="mtype" aria-label="Type">${typeOpts}</select>
1806
1933
  <select id="mtag" aria-label="Tag">
1807
1934
  <option value="all">All tags</option>
1808
- ${(M.tags ?? []).map((x) => `<option value="${esc(x.tag)}"${t.tag === x.tag ? ' selected' : ''}>${esc(x.tag)} (${x.n})</option>`).join('')}
1935
+ ${(M.tags ?? []).map((x) => opt(x.tag, `${x.tag} (${x.n})`, t.tag === x.tag)).join('')}
1809
1936
  </select>
1810
- <input type="date" id="mfrom" value="${esc(t.since)}" aria-label="From date">
1811
- <input type="date" id="mto" value="${esc(t.until)}" aria-label="To date">
1812
- </div>
1813
- <div class="chips" id="mtypes">
1814
- ${chip('all', 'All')}${typeChips}
1937
+ <select id="mwhen" aria-label="When">
1938
+ ${opt('any', 'Any time', when === 'any')}${opt('today', 'Today', when === 'today')}
1939
+ ${opt('7d', 'Last 7 days', when === '7d')}${opt('30d', 'Last 30 days', when === '30d')}
1940
+ ${opt('custom', 'Custom range', when === 'custom')}
1941
+ </select>
1942
+ ${when === 'custom' ? `<input type="date" id="mfrom" value="${esc(t.since)}" aria-label="From date">
1943
+ <input type="date" id="mto" value="${esc(t.until)}" aria-label="To date">` : ''}
1815
1944
  </div>
1816
- <div style="margin-top:var(--space-4)" id="mem-rows"><p class="empty">Loading the timeline…</p></div>`),
1945
+ <div id="mem-rows"><p class="empty">Loading the timeline…</p></div>
1946
+ </div>`,
1817
1947
  ].join('');
1818
1948
  }
1819
1949
 
@@ -2782,13 +2912,6 @@ function onActivate(e) {
2782
2912
  }
2783
2913
  const tab = e.target.closest('[data-tab]');
2784
2914
  if (tab) { T.review.page = 1; return goTo(LH('review', tab.dataset.tab === 'due' ? null : tab.dataset.tab)); }
2785
- const mtype = e.target.closest('[data-mtype]');
2786
- if (mtype) {
2787
- T.memory.page = 1;
2788
- const q = CUR.query;
2789
- return goTo(MH('timeline', mtype.dataset.mtype === 'all' ? null : mtype.dataset.mtype,
2790
- { tag: q.get('tag'), from: q.get('from'), to: q.get('to') }));
2791
- }
2792
2915
  const go = e.target.closest('[data-go]');
2793
2916
  if (go) return goTo(go.dataset.go);
2794
2917
  }
@@ -2798,7 +2921,7 @@ $('view').addEventListener('click', onActivate);
2798
2921
  // are divs and table rows, so Enter and Space have to be wired by hand.
2799
2922
  $('view').addEventListener('keydown', (e) => {
2800
2923
  if (e.key !== 'Enter' && e.key !== ' ') return;
2801
- if (!e.target.closest('[data-go],[data-state],[data-tab],[data-page],[data-mtype]')) return;
2924
+ if (!e.target.closest('[data-go],[data-state],[data-tab],[data-page]')) return;
2802
2925
  e.preventDefault();
2803
2926
  onActivate(e);
2804
2927
  });
@@ -2825,6 +2948,19 @@ $('view').addEventListener('change', (e) => {
2825
2948
  if (e.target.id === 'dom') { T.concepts.page = 1; setQuery({ domain: e.target.value }); }
2826
2949
  if (e.target.id === 'sort') { T.concepts.sort = e.target.value; T.concepts.page = 1; render(); }
2827
2950
  if (e.target.id === 'mtag') { T.memory.page = 1; setQuery({ tag: e.target.value }); }
2951
+ if (e.target.id === 'mtype') {
2952
+ T.memory.page = 1;
2953
+ const q = CUR.query;
2954
+ goTo(MH('timeline', e.target.value === 'all' ? null : e.target.value,
2955
+ { tag: q.get('tag'), from: q.get('from'), to: q.get('to') }));
2956
+ }
2957
+ if (e.target.id === 'mwhen') {
2958
+ T.memory.page = 1;
2959
+ const v = e.target.value;
2960
+ // Custom keeps the range's start, closes it at today and reveals the date boxes.
2961
+ if (v === 'custom') setQuery({ from: CUR.query.get('from') || daysAgo(6), to: daysAgo(0) });
2962
+ else setQuery({ from: v === 'any' ? null : daysAgo(WHEN[v]), to: null });
2963
+ }
2828
2964
  if (e.target.id === 'mfrom') { T.memory.page = 1; setQuery({ from: e.target.value }); }
2829
2965
  if (e.target.id === 'mto') { T.memory.page = 1; setQuery({ to: e.target.value }); }
2830
2966
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eklavya",
3
3
  "displayName": "Eklavya",
4
- "version": "1.27.1",
4
+ "version": "1.28.0",
5
5
  "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
6
6
  "author": {
7
7
  "name": "Ajay Kumar"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eklavya",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "Learn while your agent works — local knowledge graph, spaced repetition, and quiz gating for agent-assisted development.",
5
5
  "license": "MIT",
6
6
  "repository": {