@threadbase-sh/streamer 1.45.0 → 1.46.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.
package/dist/index.cjs CHANGED
@@ -5345,7 +5345,10 @@ var createMiscRoutes = (deps) => {
5345
5345
  // Same contract: this server serves GET /api/config/feature-flags. Lives
5346
5346
  // here rather than behind /api/config (admin-only) so a read-only client
5347
5347
  // still learns the server supports flags even if it can't read values.
5348
- featureFlags: true
5348
+ featureFlags: true,
5349
+ // Same contract: this server serves GET /api/projects/summary, which the
5350
+ // Hub's grouped views need before they can draw a tree.
5351
+ projectSummary: true
5349
5352
  });
5350
5353
  });
5351
5354
  app.get("/api/profiles", (c) => c.json([]));
@@ -5492,6 +5495,11 @@ var createProjectRoutes = (deps) => {
5492
5495
  deps.handleGetPopularProjects(url, c.env.outgoing);
5493
5496
  return alreadyHandled4();
5494
5497
  });
5498
+ app.get("/summary", (c) => {
5499
+ const url = new URL(c.req.url);
5500
+ deps.handleGetProjectSummaries(url, c.env.outgoing);
5501
+ return alreadyHandled4();
5502
+ });
5495
5503
  return app;
5496
5504
  };
5497
5505
 
@@ -6357,6 +6365,21 @@ var ConversationCache = class _ConversationCache {
6357
6365
  ORDER BY cnt DESC
6358
6366
  LIMIT ?`
6359
6367
  ),
6368
+ // Same table, same rows and same NULL filter /api/conversations lists
6369
+ // from, so a group's count/last-activity can never disagree with the
6370
+ // page it opens. Bare project_name is the one from the MAX(last_activity)
6371
+ // row (SQLite's documented min/max-aggregate bare-column rule).
6372
+ projectSummaries: db.prepare(
6373
+ `SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest
6374
+ FROM conversation_meta
6375
+ WHERE project_path IS NOT NULL
6376
+ GROUP BY project_path
6377
+ ORDER BY latest DESC, project_path ASC
6378
+ LIMIT ? OFFSET ?`
6379
+ ),
6380
+ projectSummaryCount: db.prepare(
6381
+ "SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE project_path IS NOT NULL"
6382
+ ),
6360
6383
  getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
6361
6384
  upsertFileState: db.prepare(
6362
6385
  `INSERT INTO conversation_file_state
@@ -6732,6 +6755,23 @@ var ConversationCache = class _ConversationCache {
6732
6755
  sessionCount: r.cnt
6733
6756
  }));
6734
6757
  }
6758
+ /** Every project with at least one cached conversation, most recently active
6759
+ * first. Paths are the raw `project_path` values, which is what
6760
+ * /api/conversations?project= matches on exactly — so a summary row is
6761
+ * always joinable against the page it describes. */
6762
+ listProjectSummaries(opts) {
6763
+ const total = this.stmts.projectSummaryCount.get().n;
6764
+ const rows = opts.limit === 0 ? [] : this.stmts.projectSummaries.all(opts.limit, opts.offset);
6765
+ return {
6766
+ total,
6767
+ projects: rows.map((r) => ({
6768
+ path: r.project_path,
6769
+ name: r.project_name ?? r.project_path.split(/[/\\]/).pop() ?? r.project_path,
6770
+ conversationCount: r.cnt,
6771
+ lastActivity: new Date(r.latest ?? 0).toISOString()
6772
+ }))
6773
+ };
6774
+ }
6735
6775
  ensureFileIndex() {
6736
6776
  if (this.fileIndexLoaded) return;
6737
6777
  const rows = this.stmts.allFilePaths.all();
@@ -10873,6 +10913,7 @@ var StreamerServer = class {
10873
10913
  handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
10874
10914
  handleListProjects: (url, res) => handleListProjects(url, res),
10875
10915
  handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
10916
+ handleGetProjectSummaries: (url, res) => this.handleGetProjectSummaries(url, res),
10876
10917
  handlePairStart: (res) => this.handlePairStart(res),
10877
10918
  handlePairExchange: (req, res) => this.handlePairExchange(req, res),
10878
10919
  handleBrowse: (url, res) => this.handleBrowse(url, res),
@@ -12438,6 +12479,20 @@ var StreamerServer = class {
12438
12479
  const projects = this.cache.getPopularProjects(limit);
12439
12480
  json(res, 200, { projects, total: projects.length });
12440
12481
  }
12482
+ handleGetProjectSummaries(url, res) {
12483
+ if (this.rejectIfWarmingUp(res)) return;
12484
+ const limit = intParam(url, "limit", 200);
12485
+ const offset = intParam(url, "offset", 0);
12486
+ if (!this.cache) {
12487
+ json(res, 503, {
12488
+ error: "Conversation cache unavailable",
12489
+ code: "CACHE_UNAVAILABLE"
12490
+ });
12491
+ return;
12492
+ }
12493
+ const { projects, total } = this.cache.listProjectSummaries({ limit, offset });
12494
+ json(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });
12495
+ }
12441
12496
  buildStatCache(previousScanner) {
12442
12497
  if (!this.cache) return void 0;
12443
12498
  if (!previousScanner) {