pi-context-engine 0.7.0 → 0.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-context-engine",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Semantic code context for pi: tree-sitter index + hybrid vector/BM25 retrieval + cross-session memory, all local. Port of code-context-engine (MIT, elara-labs) as a native pi extension.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -97,8 +97,10 @@ export async function savingsPayload(eng: ContextEngine) {
97
97
  };
98
98
  }
99
99
 
100
- /** Indexed files with chunk counts, largest first. */
101
- export function filesPayload(eng: ContextEngine, limit = 500) {
100
+ /** Indexed files with chunk counts, largest first. `offset` + `limit`
101
+ * window into the sorted list — the Files tab pages through with a
102
+ * "show more" control instead of a hard cap. */
103
+ export function filesPayload(eng: ContextEngine, limit = 500, offset = 0) {
102
104
  let counts: Record<string, number> = {};
103
105
  try {
104
106
  counts = eng.backend.fileChunkCounts();
@@ -108,10 +110,13 @@ export function filesPayload(eng: ContextEngine, limit = 500) {
108
110
  const rows = Object.entries(counts)
109
111
  .map(([filePath, chunks]) => ({ filePath, chunks }))
110
112
  .sort((a, b) => b.chunks - a.chunks || a.filePath.localeCompare(b.filePath));
113
+ const start = Math.max(0, Math.min(Math.floor(offset) || 0, rows.length));
114
+ const size = Math.max(1, Math.min(Math.floor(limit) || 500, 5000));
111
115
  return {
112
116
  total: rows.length,
113
117
  chunks: rows.reduce((acc, r) => acc + r.chunks, 0),
114
- files: rows.slice(0, Math.max(1, Math.min(limit, 5000))),
118
+ offset: start,
119
+ files: rows.slice(start, start + size),
115
120
  };
116
121
  }
117
122
 
@@ -142,14 +147,21 @@ export function decisionsPayload(eng: ContextEngine, limit = 100) {
142
147
  };
143
148
  }
144
149
 
145
- /** Everything, for /api/export. */
150
+ /** Everything, for /api/export. Session timelines are included so the
151
+ * download really is "all of the above" (review finding on #24). */
146
152
  export async function exportPayload(eng: ContextEngine, ext: ExtDashboardState) {
153
+ const sessions = sessionsPayload(eng);
154
+ const timelines: Awaited<ReturnType<typeof timelinePayload>>[] = [];
155
+ for (const s of sessions.sessions) {
156
+ timelines.push(timelinePayload(eng, s.id));
157
+ }
147
158
  return {
148
159
  generatedAt: new Date().toISOString(),
149
160
  status: statusPayload(eng, ext),
150
161
  savings: await savingsPayload(eng),
151
- files: filesPayload(eng),
152
- sessions: sessionsPayload(eng),
162
+ files: filesPayload(eng, 5000),
163
+ sessions,
164
+ timelines,
153
165
  decisions: decisionsPayload(eng),
154
166
  };
155
167
  }
@@ -107,8 +107,11 @@ export const PAGE_HTML = `<!doctype html>
107
107
  <script>
108
108
  'use strict';
109
109
  var TABS = ['overview', 'files', 'sessions', 'memory', 'savings'];
110
- var state = { tab: 'overview', sessionId: null, status: null };
111
- var pollTimer = null;
110
+ var state = { tab: 'overview', sessionId: null, status: null, projectDir: null, filesOffset: 0, filesMax: 0 };
111
+ // Monotonic request generation: every load bumps it, responses from a
112
+ // superseded load (tab switch, project-switch refetch, away-and-back) are
113
+ // dropped. A tab-name check alone misses same-tab supersession.
114
+ var loadToken = 0;
112
115
 
113
116
  function $(id) { return document.getElementById(id); }
114
117
  function esc(s) {
@@ -162,6 +165,15 @@ function renderStatus(st) {
162
165
  else if (st.chunks > 0) { badge.className = 'badge live'; $('state-text').textContent = 'live'; }
163
166
  else { badge.className = 'badge'; $('state-text').textContent = 'no index'; }
164
167
  $('counts').textContent = fmt(st.chunks) + ' chunks · ' + fmt(st.files) + ' files';
168
+ // Project switch (live engine replaced): compare full paths — basenames
169
+ // collide (/a/repo vs /b/repo). Drop session selection and refetch the
170
+ // ACTIVE tab, or project A's rows stay on screen under project B's label.
171
+ var projectChanged = state.projectDir !== null && state.projectDir !== st.projectDir;
172
+ state.projectDir = st.projectDir;
173
+ if (projectChanged) {
174
+ state.sessionId = null;
175
+ if (state.tab !== 'overview') { switchTab(state.tab); return; }
176
+ }
165
177
  if (state.tab === 'overview') renderOverview(st);
166
178
  }
167
179
 
@@ -248,23 +260,69 @@ function card(k, v, s) {
248
260
  function cfg(k, v) { return '<div>' + esc(k) + ': <b>' + v + '</b></div>'; }
249
261
 
250
262
  /* ── files ────────────────────────────────────────────────────────── */
263
+ var FILES_PAGE = 500;
264
+ function filesTableHtml(d) {
265
+ // Bar widths must compare across the WHOLE table: the list is sorted by
266
+ // chunk count, so page 1 row 1 is the global max. Per-page maxima would
267
+ // give each appended page's top file a full-width bar.
268
+ var max = state.filesMax;
269
+ return d.files.map(function (f) {
270
+ return '<tr><td class="path">' + esc(f.filePath) + '</td>' +
271
+ '<td class="num" style="width:70px">' + fmt(f.chunks) + '</td>' +
272
+ '<td style="width:180px">' + pctBar(f.chunks, max) + '</td></tr>';
273
+ }).join('');
274
+ }
251
275
  function loadFiles() {
252
- api('/api/files').then(function (d) {
276
+ var gen = ++loadToken;
277
+ state.filesOffset = 0;
278
+ api('/api/files?limit=' + FILES_PAGE).then(function (d) {
279
+ if (gen !== loadToken) return; // superseded by a newer load
253
280
  if (d.files.length === 0) { setMain('<div class="empty">No files indexed yet.</div>'); return; }
254
- var max = d.files[0].chunks;
255
- var rows = d.files.map(function (f) {
256
- return '<tr><td class="path">' + esc(f.filePath) + '</td>' +
257
- '<td class="num" style="width:70px">' + fmt(f.chunks) + '</td>' +
258
- '<td style="width:180px">' + pctBar(f.chunks, max) + '</td></tr>';
259
- }).join('');
281
+ state.filesMax = d.files[0].chunks;
260
282
  setMain('<div class="panel"><h2>' + fmt(d.total) + ' files · ' + fmt(d.chunks) + ' chunks</h2>' +
261
- '<table><tr><th>File</th><th class="num">Chunks</th><th></th></tr>' + rows + '</table></div>');
262
- }).catch(errPanel);
283
+ '<table><tr><th>File</th><th class="num">Chunks</th><th></th></tr><tbody id="files-body">' +
284
+ filesTableHtml(d) + '</tbody></table>' + moreFilesHtml(d) + '</div>');
285
+ state.filesOffset = d.offset + d.files.length;
286
+ var more = $('files-more');
287
+ if (more) more.onclick = appendFiles;
288
+ }).catch(function (e) { if (gen === loadToken) errPanel(e); });
289
+ }
290
+ function moreFilesHtml(d) {
291
+ var remaining = d.total - (d.offset + d.files.length);
292
+ if (remaining <= 0) return '';
293
+ var label = 'Show ' + fmt(remaining) + ' more files';
294
+ return '<div style="margin-top:12px"><button class="btn" id="files-more" data-label="' + label + '">' +
295
+ label + '</button></div>';
296
+ }
297
+ function appendFiles() {
298
+ var gen = ++loadToken;
299
+ var btn = $('files-more');
300
+ if (btn) { btn.disabled = true; btn.textContent = 'Loading…'; } // double-click must not re-request the same offset
301
+ api('/api/files?limit=' + FILES_PAGE + '&offset=' + state.filesOffset).then(function (d) {
302
+ if (gen !== loadToken) return;
303
+ var body = $('files-body');
304
+ if (!body) return;
305
+ body.insertAdjacentHTML('beforeend', filesTableHtml(d));
306
+ state.filesOffset = d.offset + d.files.length;
307
+ var more = $('files-more');
308
+ if (more) more.parentNode.remove();
309
+ var panel = body.closest('.panel');
310
+ if (panel) panel.insertAdjacentHTML('beforeend', moreFilesHtml(d));
311
+ var next = $('files-more');
312
+ if (next) next.onclick = appendFiles;
313
+ }).catch(function (e) {
314
+ if (gen !== loadToken) return;
315
+ var failed = $('files-more');
316
+ if (failed) { failed.disabled = false; failed.textContent = failed.getAttribute('data-label') || 'Show more'; }
317
+ toast(e && e.message ? e.message : String(e));
318
+ });
263
319
  }
264
320
 
265
321
  /* ── sessions ─────────────────────────────────────────────────────── */
266
322
  function loadSessions() {
323
+ var gen = ++loadToken;
267
324
  api('/api/sessions').then(function (d) {
325
+ if (gen !== loadToken) return; // superseded by a newer load
268
326
  if (d.sessions.length === 0) { setMain('<div class="empty">No sessions recorded yet.</div>'); return; }
269
327
  var items = d.sessions.map(function (s) {
270
328
  var active = s.id === d.activeSessionId;
@@ -279,9 +337,11 @@ function loadSessions() {
279
337
  Array.prototype.forEach.call(document.querySelectorAll('.sess-item'), function (el) {
280
338
  el.onclick = function () { openSession(el.getAttribute('data-id')); };
281
339
  });
282
- if (!state.sessionId) openSession(d.sessions[0].id, true);
283
- else openSession(state.sessionId, true);
284
- }).catch(errPanel);
340
+ // Keep the selection when it is still in the list; after a project switch
341
+ // the old session id is gone, so fall back to the newest.
342
+ var keep = state.sessionId && d.sessions.some(function (s) { return s.id === state.sessionId; });
343
+ openSession(keep ? state.sessionId : d.sessions[0].id, true);
344
+ }).catch(function (e) { if (gen === loadToken) errPanel(e); });
285
345
  }
286
346
 
287
347
  function openSession(id, silent) {
@@ -291,7 +351,7 @@ function openSession(id, silent) {
291
351
  });
292
352
  api('/api/sessions/' + encodeURIComponent(id)).then(function (d) {
293
353
  var box = $('sess-detail');
294
- if (!box) return;
354
+ if (!box || state.sessionId !== id) return; // selection moved on before the response landed
295
355
  var html = '';
296
356
  if (d.session) {
297
357
  html += '<h2>' + esc(d.session.project) + ' · ' + esc(d.session.status) + '</h2>' +
@@ -323,7 +383,9 @@ function openSession(id, silent) {
323
383
 
324
384
  /* ── memory ───────────────────────────────────────────────────────── */
325
385
  function loadMemory() {
386
+ var gen = ++loadToken;
326
387
  api('/api/decisions').then(function (d) {
388
+ if (gen !== loadToken) return; // superseded by a newer load
327
389
  var html = '<div class="grid two-col"><div class="panel"><h2>Decisions</h2>';
328
390
  if (d.decisions.length === 0 && d.consolidated.length === 0) {
329
391
  html += '<div class="empty">No recorded decisions yet. The agent saves them with record_decision.</div>';
@@ -349,12 +411,14 @@ function loadMemory() {
349
411
  }
350
412
  html += '</div></div>';
351
413
  setMain(html);
352
- }).catch(errPanel);
414
+ }).catch(function (e) { if (gen === loadToken) errPanel(e); });
353
415
  }
354
416
 
355
417
  /* ── savings ──────────────────────────────────────────────────────── */
356
418
  function loadSavings() {
419
+ var gen = ++loadToken;
357
420
  api('/api/savings').then(function (d) {
421
+ if (gen !== loadToken) return; // superseded by a newer load
358
422
  var t = d.totals;
359
423
  var html = '<div class="grid cards" style="margin-bottom:14px">' +
360
424
  card('Saved', fmtK(t.savedTokens), 'full-file baseline') +
@@ -387,7 +451,7 @@ function loadSavings() {
387
451
  html = '<div class="empty">No savings recorded yet — they accumulate as the agent uses context_search.</div>';
388
452
  }
389
453
  setMain(html);
390
- }).catch(errPanel);
454
+ }).catch(function (e) { if (gen === loadToken) errPanel(e); });
391
455
  }
392
456
 
393
457
  function errPanel(e) {
@@ -162,7 +162,7 @@ export class DashboardServer {
162
162
  sendJson(res, 200, await savingsPayload(eng));
163
163
  return;
164
164
  case "/api/files":
165
- sendJson(res, 200, filesPayload(eng, clampLimit(url.searchParams.get("limit"), 500, 5000)));
165
+ sendJson(res, 200, filesPayload(eng, clampLimit(url.searchParams.get("limit"), 500, 5000), clampLimit(url.searchParams.get("offset"), 0, 1e9)));
166
166
  return;
167
167
  case "/api/sessions":
168
168
  sendJson(res, 200, sessionsPayload(eng, clampLimit(url.searchParams.get("limit"), 50, 500)));