@sanqianx/ai-coding-event-bridge-console 0.5.0 → 0.5.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.
@@ -495,6 +495,7 @@ function fetchTurns(options) {
495
495
  if (!append) { state.turns = []; state.cursor = null; }
496
496
  renderLive();
497
497
  var query = '/api/turns?project=' + encodeURIComponent(state.project.id)
498
+ + '&order=desc'
498
499
  + (state.cursor ? '&cursor=' + encodeURIComponent(state.cursor) : '');
499
500
  return api(query).then(function (payload) {
500
501
  if (!state.project || payload.project !== state.project.id) return; // 切换已发生,丢弃过期响应
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/ai-coding-event-bridge-console",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local console for the AI coding event bridge: project import with per-project journal storage, REST query API and a built-in conversation explorer page.",
5
5
  "license": "MIT",
6
6
  "private": false,
package/src/server.js CHANGED
@@ -60,6 +60,16 @@ function decodeCursor(raw) {
60
60
  return null;
61
61
  }
62
62
 
63
+ function decodeSeqCursor(raw) {
64
+ try {
65
+ const decoded = JSON.parse(Buffer.from(String(raw), 'base64url').toString('utf8'));
66
+ if (decoded && decoded.v === 1 && typeof decoded.seq === 'number') return decoded;
67
+ } catch (_) {
68
+ // fall through
69
+ }
70
+ return null;
71
+ }
72
+
63
73
  function pickFolderEnabled() {
64
74
  return process.env.BRIDGE_CONSOLE_PICK_FOLDER !== '0';
65
75
  }
@@ -294,7 +304,7 @@ function createConsoleServer({ home }) {
294
304
  * project journal after its last commit boundary. Everything earlier is
295
305
  * archived under <store>/commits and served by the commit-doc endpoints.
296
306
  */
297
- async function currentTurns(project, { limit, cursorRaw }) {
307
+ async function currentTurns(project, { limit, cursorRaw, order }) {
298
308
  const dir = projectJournalDir(project);
299
309
  const journal = existingJournal(dir);
300
310
  if (!journal) {
@@ -307,24 +317,43 @@ function createConsoleServer({ home }) {
307
317
  const records = await journal.readEvents({ fromSequence: lastBoundary + 1 });
308
318
  const turns = queryAt(dir)._projectTurns(records.filter(isConversationEvent));
309
319
  turns.sort((a, b) => a.startSequence - b.startSequence);
310
- let start = 0;
311
- if (cursorRaw) {
312
- let decoded = null;
313
- try {
314
- decoded = JSON.parse(Buffer.from(String(cursorRaw), 'base64url').toString('utf8'));
315
- } catch (_) {
316
- decoded = null;
320
+ let pageAsc;
321
+ let nextCursor = null;
322
+ if (order === 'desc') {
323
+ // Newest on top: page 1 holds the newest turns; each cursor step pages
324
+ // further into the past.
325
+ let start;
326
+ if (cursorRaw) {
327
+ const decoded = decodeSeqCursor(cursorRaw);
328
+ if (!decoded || (decoded.o && decoded.o !== 'desc')) return null;
329
+ start = turns.findIndex((turn) => turn.startSequence < decoded.seq);
330
+ if (start === -1) start = turns.length;
331
+ } else {
332
+ start = Math.max(0, turns.length - limit);
333
+ }
334
+ pageAsc = turns.slice(start, start + limit);
335
+ if (start > 0) {
336
+ nextCursor = encodeCursor({ v: 1, seq: turns[start].startSequence, o: 'desc' });
337
+ }
338
+ } else {
339
+ let start = 0;
340
+ if (cursorRaw) {
341
+ const decoded = decodeSeqCursor(cursorRaw);
342
+ if (!decoded || (decoded.o && decoded.o !== 'asc')) return null;
343
+ start = turns.findIndex((turn) => turn.startSequence > decoded.seq);
344
+ if (start === -1) start = turns.length;
345
+ }
346
+ pageAsc = turns.slice(start, start + limit);
347
+ if (start + limit < turns.length && pageAsc.length) {
348
+ nextCursor = encodeCursor({ v: 1, seq: pageAsc[pageAsc.length - 1].startSequence, o: 'asc' });
317
349
  }
318
- if (!decoded || decoded.v !== 1 || typeof decoded.seq !== 'number') return null;
319
- start = turns.findIndex((turn) => turn.startSequence > decoded.seq);
320
- if (start === -1) start = turns.length;
321
350
  }
322
- const page = turns.slice(start, start + limit);
351
+ const page = order === 'desc' ? pageAsc.slice().reverse() : pageAsc;
323
352
  return {
324
353
  project: project.id,
325
354
  date: null,
326
355
  turns: page,
327
- nextCursor: start + limit < turns.length && page.length ? encodeCursor({ v: 1, seq: page[page.length - 1].startSequence }) : null,
356
+ nextCursor,
328
357
  totalTurns: turns.length,
329
358
  annotations: {}
330
359
  };
@@ -618,11 +647,12 @@ function createConsoleServer({ home }) {
618
647
 
619
648
  if (route === 'GET /api/turns') {
620
649
  const limit = Math.max(1, Math.min(Number(url.searchParams.get('limit')) || 50, 200));
650
+ const order = url.searchParams.get('order') === 'desc' ? 'desc' : 'asc';
621
651
  if (!project.auto) {
622
652
  // Registered projects: the journal is a conveyor — serve only the
623
653
  // current (uncommitted) conversation and keep it maintained.
624
654
  await maintainProject(project);
625
- const current = await currentTurns(project, { limit, cursorRaw: url.searchParams.get('cursor') });
655
+ const current = await currentTurns(project, { limit, cursorRaw: url.searchParams.get('cursor'), order });
626
656
  if (!current) {
627
657
  sendJson(res, 400, { error: { code: 'CURSOR_INVALID', message: 'cursor is invalid; restart the query' } });
628
658
  return;
@@ -633,22 +663,49 @@ function createConsoleServer({ home }) {
633
663
  const date = url.searchParams.get('date') || null;
634
664
  const merged = await mergedTurns(project, { date });
635
665
  const cursor = url.searchParams.get('cursor');
636
- let start = 0;
637
- if (cursor) {
638
- const decoded = decodeCursor(cursor);
639
- if (!decoded) {
640
- sendJson(res, 400, { error: { code: 'CURSOR_INVALID', message: 'cursor is invalid; restart the query' } });
641
- return;
666
+ let pageAsc;
667
+ let nextCursor = null;
668
+ if (order === 'desc') {
669
+ // Newest on top: page 1 holds the newest turns; each cursor step
670
+ // pages further into the past.
671
+ let start;
672
+ if (cursor) {
673
+ const decoded = decodeCursor(cursor);
674
+ if (!decoded || (decoded.o && decoded.o !== 'desc')) {
675
+ sendJson(res, 400, { error: { code: 'CURSOR_INVALID', message: 'cursor is invalid; restart the query' } });
676
+ return;
677
+ }
678
+ start = merged.findIndex((turn) => turnSortKey(turn) < decoded.key);
679
+ if (start === -1) start = merged.length;
680
+ } else {
681
+ start = Math.max(0, merged.length - limit);
682
+ }
683
+ pageAsc = merged.slice(start, start + limit);
684
+ if (start > 0) {
685
+ nextCursor = encodeCursor({ v: 1, key: turnSortKey(merged[start]), o: 'desc' });
686
+ }
687
+ } else {
688
+ let start = 0;
689
+ if (cursor) {
690
+ const decoded = decodeCursor(cursor);
691
+ if (!decoded || (decoded.o && decoded.o !== 'asc')) {
692
+ sendJson(res, 400, { error: { code: 'CURSOR_INVALID', message: 'cursor is invalid; restart the query' } });
693
+ return;
694
+ }
695
+ start = merged.findIndex((turn) => turnSortKey(turn) > decoded.key);
696
+ if (start === -1) start = merged.length;
697
+ }
698
+ pageAsc = merged.slice(start, start + limit);
699
+ if (start + limit < merged.length && pageAsc.length) {
700
+ nextCursor = encodeCursor({ v: 1, key: turnSortKey(pageAsc[pageAsc.length - 1]), o: 'asc' });
642
701
  }
643
- start = merged.findIndex((turn) => turnSortKey(turn) > decoded.key);
644
- if (start === -1) start = merged.length;
645
702
  }
646
- const page = merged.slice(start, start + limit);
703
+ const page = order === 'desc' ? pageAsc.slice().reverse() : pageAsc;
647
704
  sendJson(res, 200, {
648
705
  project: project.id,
649
706
  date,
650
707
  turns: page,
651
- nextCursor: start + limit < merged.length ? encodeCursor({ v: 1, key: turnSortKey(merged[start + limit - 1]) }) : null,
708
+ nextCursor,
652
709
  totalTurns: merged.length,
653
710
  annotations: await annotationsFor(project, page)
654
711
  });