@threadbase-sh/streamer 1.44.4 → 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/cli.cjs CHANGED
@@ -114168,14 +114168,19 @@ function scrapePermissionGate(lines) {
114168
114168
  const options = [];
114169
114169
  let cursor;
114170
114170
  let firstOptionLine = -1;
114171
- for (let i = 0; i < lines.length; i++) {
114172
- const m2 = OPTION_RE.exec(stripGutter(lines[i]));
114173
- if (!m2) continue;
114174
- const index = Number.parseInt(m2[2], 10);
114175
- if (!Number.isFinite(index)) continue;
114176
- if (firstOptionLine === -1) firstOptionLine = i;
114177
- if (m2[1]) cursor = index;
114178
- options.push({ index, label: m2[3] });
114171
+ for (let i = lines.length - 1; i >= 0; i--) {
114172
+ const stripped = stripGutter(lines[i]);
114173
+ const m2 = OPTION_RE.exec(stripped);
114174
+ if (m2) {
114175
+ const index = Number.parseInt(m2[2], 10);
114176
+ if (!Number.isFinite(index)) continue;
114177
+ firstOptionLine = i;
114178
+ if (m2[1]) cursor = index;
114179
+ options.unshift({ index, label: m2[3] });
114180
+ continue;
114181
+ }
114182
+ if (options.length === 0) continue;
114183
+ if (stripped.trim().length === 0 || BOX_ONLY_RE.test(lines[i].trim())) break;
114179
114184
  }
114180
114185
  if (options.length === 0) return null;
114181
114186
  let prompt;
@@ -114214,7 +114219,15 @@ function scrapeDetail(lines, promptLine) {
114214
114219
  }
114215
114220
  return collected.length > 0 ? collected.reverse().join("\n") : void 0;
114216
114221
  }
114217
- var OSC_777_PERMISSION_RE, OSC_777_WAITING_RE, OPTION_RE, FOOTER_RE, BOX_ONLY_RE, PROMPT_ARROW_RE, MAX_DETAIL_LINES;
114222
+ function detectGateScreen(lines) {
114223
+ if (lines.some((l) => ASK_MENU_FOOTER_RE.test(l))) return null;
114224
+ if (!lines.some((l) => GATE_FOOTER_RE.test(l))) return null;
114225
+ const gate = scrapePermissionGate(lines);
114226
+ if (!gate || gate.options.length < 2) return null;
114227
+ if (!gate.options.some((o) => YES_NO_LABEL_RE.test(o.label))) return null;
114228
+ return gate;
114229
+ }
114230
+ var OSC_777_PERMISSION_RE, OSC_777_WAITING_RE, OPTION_RE, FOOTER_RE, BOX_ONLY_RE, PROMPT_ARROW_RE, MAX_DETAIL_LINES, GATE_FOOTER_RE, ASK_MENU_FOOTER_RE, YES_NO_LABEL_RE;
114218
114231
  var init_detectPermissionGate = __esm({
114219
114232
  "src/services/questions/detectPermissionGate.ts"() {
114220
114233
  "use strict";
@@ -114225,6 +114238,9 @@ var init_detectPermissionGate = __esm({
114225
114238
  BOX_ONLY_RE = /^[\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;
114226
114239
  PROMPT_ARROW_RE = /^[\s]*[❯›>]\s*$/;
114227
114240
  MAX_DETAIL_LINES = 6;
114241
+ GATE_FOOTER_RE = /esc to cancel/i;
114242
+ ASK_MENU_FOOTER_RE = /Enter to select/i;
114243
+ YES_NO_LABEL_RE = /^(yes|no)\b/i;
114228
114244
  }
114229
114245
  });
114230
114246
 
@@ -114448,7 +114464,7 @@ function toPublicSession2(s3) {
114448
114464
  function stripAnsi2(str) {
114449
114465
  return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
114450
114466
  }
114451
- var import_headless2, import_crypto9, import_fs22, import_path22, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, PTY_COLS2, PTY_ROWS2, SCREEN_SCROLLBACK2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, pty2, PTYManager;
114467
+ var import_headless2, import_crypto9, import_fs22, import_path22, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, PTY_COLS2, PTY_ROWS2, SCREEN_SCROLLBACK2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, pty2, PTYManager;
114452
114468
  var init_pty_manager = __esm({
114453
114469
  "src/pty-manager.ts"() {
114454
114470
  "use strict";
@@ -114472,6 +114488,8 @@ var init_pty_manager = __esm({
114472
114488
  SCREEN_SCROLLBACK2 = 1e3;
114473
114489
  CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
114474
114490
  QUIET_DETECT_MS2 = 500;
114491
+ OSC_TAIL_CHARS = 128;
114492
+ SCRAPE_THROTTLE_MS = 300;
114475
114493
  CLAUDE_READY_FALLBACK_MS = 8e3;
114476
114494
  SUBMIT_BYTES2 = "\r";
114477
114495
  SUBMIT_DELAY_MS = 16;
@@ -114490,6 +114508,12 @@ var init_pty_manager = __esm({
114490
114508
  // the next prompt-ready without a fresh 777 (gate closed). Prevents
114491
114509
  // re-broadcasting open/close on every chunk.
114492
114510
  permissionOpen = /* @__PURE__ */ new Set();
114511
+ // Last chunk's raw tail per session — prepended to the next chunk before
114512
+ // the OSC regex test so a split escape still matches. Consumed on match.
114513
+ oscTail = /* @__PURE__ */ new Map();
114514
+ // When the last full detection pass ran per session (any trigger) — the
114515
+ // clock the SCRAPE_THROTTLE_MS ceiling is measured against.
114516
+ lastDetectAt = /* @__PURE__ */ new Map();
114493
114517
  // Content key of the last AskUserQuestion broadcast from the rendered screen,
114494
114518
  // per session — de-dupes the same menu firing on consecutive repaints.
114495
114519
  lastScreenQuestionKey = /* @__PURE__ */ new Map();
@@ -114498,6 +114522,13 @@ var init_pty_manager = __esm({
114498
114522
  // on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled
114499
114523
  // on permissionOpen but keyed by content (a shell prompt has no OSC trigger).
114500
114524
  shellPromptOpen = /* @__PURE__ */ new Map();
114525
+ // Content key (permissionContentKey, cursor excluded — see arm 3) of the
114526
+ // gate that was still painted on screen when arm 2 closed it. Suppresses
114527
+ // arm 3 re-claiming that same box from a later trigger-less/throttled pass
114528
+ // before Claude erases it — otherwise the paint-time claim reopens the
114529
+ // card it just closed. Cleared once detectGateScreen sees the box is gone,
114530
+ // so a genuinely new (even content-identical) gate is claimed normally.
114531
+ closedGateKey = /* @__PURE__ */ new Map();
114501
114532
  // Tracks sessions (both fresh and resume) whose PTY has spawned but Claude
114502
114533
  // hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.
114503
114534
  pendingReady = /* @__PURE__ */ new Set();
@@ -114851,8 +114882,11 @@ var init_pty_manager = __esm({
114851
114882
  this.queuedInputs.delete(sessionId);
114852
114883
  this.firstChunkAt.delete(sessionId);
114853
114884
  this.permissionOpen.delete(sessionId);
114885
+ this.oscTail.delete(sessionId);
114886
+ this.lastDetectAt.delete(sessionId);
114854
114887
  this.lastScreenQuestionKey.delete(sessionId);
114855
114888
  this.shellPromptOpen.delete(sessionId);
114889
+ this.closedGateKey.delete(sessionId);
114856
114890
  this.quietCheckers.get(sessionId)?.cancel();
114857
114891
  this.quietCheckers.delete(sessionId);
114858
114892
  this.clearReadyFallback(sessionId);
@@ -114947,8 +114981,11 @@ var init_pty_manager = __esm({
114947
114981
  for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);
114948
114982
  this.readyFallbackTimers.clear();
114949
114983
  this.permissionOpen.clear();
114984
+ this.oscTail.clear();
114985
+ this.lastDetectAt.clear();
114950
114986
  this.lastScreenQuestionKey.clear();
114951
114987
  this.shellPromptOpen.clear();
114988
+ this.closedGateKey.clear();
114952
114989
  }
114953
114990
  handleOutput(sessionId, data) {
114954
114991
  const session = this.sessions.get(sessionId);
@@ -115012,16 +115049,24 @@ var init_pty_manager = __esm({
115012
115049
  async detectLivePrompts(sessionId, rawData, stripped) {
115013
115050
  const session = this.sessions.get(sessionId);
115014
115051
  if (!session) return;
115015
- const oscPermission = hasPermissionOsc(rawData);
115016
- const oscWaitingForInput = hasWaitingForInputOsc(rawData);
115052
+ const oscWindow = (this.oscTail.get(sessionId) ?? "") + rawData;
115053
+ const oscPermission = hasPermissionOsc(oscWindow);
115054
+ const oscWaitingForInput = hasWaitingForInputOsc(oscWindow);
115055
+ if (rawData !== "") {
115056
+ if (oscPermission || oscWaitingForInput) this.oscTail.delete(sessionId);
115057
+ else this.oscTail.set(sessionId, oscWindow.slice(-OSC_TAIL_CHARS));
115058
+ }
115017
115059
  const hasAskFooter = /Enter to select/i.test(stripped);
115018
115060
  const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m2) => stripped.includes(m2));
115019
115061
  const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
115020
115062
  stripped
115021
115063
  );
115022
- if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
115064
+ const nowMs = Date.now();
115065
+ const scrapeDue = nowMs - (this.lastDetectAt.get(sessionId) ?? 0) >= SCRAPE_THROTTLE_MS;
115066
+ if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !scrapeDue && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
115023
115067
  return;
115024
115068
  }
115069
+ this.lastDetectAt.set(sessionId, nowMs);
115025
115070
  const lines = await this.getOutputLines(sessionId, 60);
115026
115071
  const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
115027
115072
  if (oscPermission || hasAskFooter || askFooterOnScreen) {
@@ -115041,13 +115086,30 @@ var init_pty_manager = __esm({
115041
115086
  this.permissionOpen.add(sessionId);
115042
115087
  this.onPermissionChange?.(sessionId, gate ?? { options: [] });
115043
115088
  } else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
115044
- const gate = scrapePermissionGate(lines);
115045
- if (oscWaitingForInput || !gate && hasPromptMarker) {
115089
+ const gate = detectGateScreen(lines);
115090
+ const stillPainted = gate !== null || scrapePermissionGate(lines) !== null;
115091
+ if (oscWaitingForInput || !stillPainted && hasPromptMarker) {
115046
115092
  this.permissionOpen.delete(sessionId);
115047
115093
  this.onPermissionChange?.(sessionId, null);
115094
+ if (gate) {
115095
+ this.closedGateKey.set(sessionId, permissionContentKey({ ...gate, cursor: void 0 }));
115096
+ } else {
115097
+ this.closedGateKey.delete(sessionId);
115098
+ }
115048
115099
  } else if (gate) {
115049
115100
  this.onPermissionChange?.(sessionId, gate);
115050
115101
  }
115102
+ } else if (!askFooterOnScreen && !oscWaitingForInput) {
115103
+ const gate = detectGateScreen(lines);
115104
+ if (gate) {
115105
+ const key = permissionContentKey({ ...gate, cursor: void 0 });
115106
+ if (this.closedGateKey.get(sessionId) !== key) {
115107
+ this.permissionOpen.add(sessionId);
115108
+ this.onPermissionChange?.(sessionId, gate);
115109
+ }
115110
+ } else {
115111
+ this.closedGateKey.delete(sessionId);
115112
+ }
115051
115113
  }
115052
115114
  if (askFooterOnScreen) {
115053
115115
  const detected = detectQuestionFromScreen(lines);
@@ -115190,8 +115252,11 @@ var init_pty_manager = __esm({
115190
115252
  this.queuedInputs.delete(sessionId);
115191
115253
  this.firstChunkAt.delete(sessionId);
115192
115254
  this.permissionOpen.delete(sessionId);
115255
+ this.oscTail.delete(sessionId);
115256
+ this.lastDetectAt.delete(sessionId);
115193
115257
  this.lastScreenQuestionKey.delete(sessionId);
115194
115258
  this.shellPromptOpen.delete(sessionId);
115259
+ this.closedGateKey.delete(sessionId);
115195
115260
  this.quietCheckers.get(sessionId)?.cancel();
115196
115261
  this.quietCheckers.delete(sessionId);
115197
115262
  this.clearReadyFallback(sessionId);
@@ -142318,7 +142383,10 @@ var createMiscRoutes = (deps) => {
142318
142383
  // Same contract: this server serves GET /api/config/feature-flags. Lives
142319
142384
  // here rather than behind /api/config (admin-only) so a read-only client
142320
142385
  // still learns the server supports flags even if it can't read values.
142321
- featureFlags: true
142386
+ featureFlags: true,
142387
+ // Same contract: this server serves GET /api/projects/summary, which the
142388
+ // Hub's grouped views need before they can draw a tree.
142389
+ projectSummary: true
142322
142390
  });
142323
142391
  });
142324
142392
  app.get("/api/profiles", (c) => c.json([]));
@@ -142593,6 +142661,11 @@ var createProjectRoutes = (deps) => {
142593
142661
  deps.handleGetPopularProjects(url2, c.env.outgoing);
142594
142662
  return alreadyHandled4();
142595
142663
  });
142664
+ app.get("/summary", (c) => {
142665
+ const url2 = new URL(c.req.url);
142666
+ deps.handleGetProjectSummaries(url2, c.env.outgoing);
142667
+ return alreadyHandled4();
142668
+ });
142596
142669
  return app;
142597
142670
  };
142598
142671
 
@@ -143468,6 +143541,21 @@ var ConversationCache = class _ConversationCache {
143468
143541
  ORDER BY cnt DESC
143469
143542
  LIMIT ?`
143470
143543
  ),
143544
+ // Same table, same rows and same NULL filter /api/conversations lists
143545
+ // from, so a group's count/last-activity can never disagree with the
143546
+ // page it opens. Bare project_name is the one from the MAX(last_activity)
143547
+ // row (SQLite's documented min/max-aggregate bare-column rule).
143548
+ projectSummaries: db.prepare(
143549
+ `SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest
143550
+ FROM conversation_meta
143551
+ WHERE project_path IS NOT NULL
143552
+ GROUP BY project_path
143553
+ ORDER BY latest DESC, project_path ASC
143554
+ LIMIT ? OFFSET ?`
143555
+ ),
143556
+ projectSummaryCount: db.prepare(
143557
+ "SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE project_path IS NOT NULL"
143558
+ ),
143471
143559
  getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
143472
143560
  upsertFileState: db.prepare(
143473
143561
  `INSERT INTO conversation_file_state
@@ -143843,6 +143931,23 @@ var ConversationCache = class _ConversationCache {
143843
143931
  sessionCount: r.cnt
143844
143932
  }));
143845
143933
  }
143934
+ /** Every project with at least one cached conversation, most recently active
143935
+ * first. Paths are the raw `project_path` values, which is what
143936
+ * /api/conversations?project= matches on exactly — so a summary row is
143937
+ * always joinable against the page it describes. */
143938
+ listProjectSummaries(opts) {
143939
+ const total = this.stmts.projectSummaryCount.get().n;
143940
+ const rows = opts.limit === 0 ? [] : this.stmts.projectSummaries.all(opts.limit, opts.offset);
143941
+ return {
143942
+ total,
143943
+ projects: rows.map((r) => ({
143944
+ path: r.project_path,
143945
+ name: r.project_name ?? r.project_path.split(/[/\\]/).pop() ?? r.project_path,
143946
+ conversationCount: r.cnt,
143947
+ lastActivity: new Date(r.latest ?? 0).toISOString()
143948
+ }))
143949
+ };
143950
+ }
143846
143951
  ensureFileIndex() {
143847
143952
  if (this.fileIndexLoaded) return;
143848
143953
  const rows = this.stmts.allFilePaths.all();
@@ -148568,6 +148673,7 @@ var StreamerServer = class {
148568
148673
  handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
148569
148674
  handleListProjects: (url2, res) => handleListProjects(url2, res),
148570
148675
  handleGetPopularProjects: (url2, res) => this.handleGetPopularProjects(url2, res),
148676
+ handleGetProjectSummaries: (url2, res) => this.handleGetProjectSummaries(url2, res),
148571
148677
  handlePairStart: (res) => this.handlePairStart(res),
148572
148678
  handlePairExchange: (req, res) => this.handlePairExchange(req, res),
148573
148679
  handleBrowse: (url2, res) => this.handleBrowse(url2, res),
@@ -150133,6 +150239,20 @@ var StreamerServer = class {
150133
150239
  const projects = this.cache.getPopularProjects(limit);
150134
150240
  json2(res, 200, { projects, total: projects.length });
150135
150241
  }
150242
+ handleGetProjectSummaries(url2, res) {
150243
+ if (this.rejectIfWarmingUp(res)) return;
150244
+ const limit = intParam(url2, "limit", 200);
150245
+ const offset = intParam(url2, "offset", 0);
150246
+ if (!this.cache) {
150247
+ json2(res, 503, {
150248
+ error: "Conversation cache unavailable",
150249
+ code: "CACHE_UNAVAILABLE"
150250
+ });
150251
+ return;
150252
+ }
150253
+ const { projects, total } = this.cache.listProjectSummaries({ limit, offset });
150254
+ json2(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });
150255
+ }
150136
150256
  buildStatCache(previousScanner) {
150137
150257
  if (!this.cache) return void 0;
150138
150258
  if (!previousScanner) {