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 +1 -1
- package/src/dashboard/api.ts +18 -6
- package/src/dashboard/page.ts +81 -17
- package/src/dashboard/server.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-engine",
|
|
3
|
-
"version": "0.7.
|
|
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",
|
package/src/dashboard/api.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
162
|
+
files: filesPayload(eng, 5000),
|
|
163
|
+
sessions,
|
|
164
|
+
timelines,
|
|
153
165
|
decisions: decisionsPayload(eng),
|
|
154
166
|
};
|
|
155
167
|
}
|
package/src/dashboard/page.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>' +
|
|
262
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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) {
|
package/src/dashboard/server.ts
CHANGED
|
@@ -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)));
|