@threadbase-sh/streamer 1.69.4 → 1.69.6

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.js CHANGED
@@ -9583,19 +9583,39 @@ var UnknownOptionError = class extends Error {
9583
9583
  question;
9584
9584
  value;
9585
9585
  };
9586
+ var UnsupportedPromptShapeError = class extends Error {
9587
+ constructor(detail) {
9588
+ super(`Unsupported prompt shape: ${detail}`);
9589
+ this.detail = detail;
9590
+ this.name = "UnsupportedPromptShapeError";
9591
+ }
9592
+ detail;
9593
+ };
9594
+ var IncompleteAnswerError = class extends Error {
9595
+ constructor(question) {
9596
+ super(`Missing answer for question "${question}"`);
9597
+ this.question = question;
9598
+ this.name = "IncompleteAnswerError";
9599
+ }
9600
+ question;
9601
+ };
9586
9602
  function answersToKeystrokes(questions, answers) {
9587
- let out = "";
9588
- for (const q of questions) {
9589
- const raw = answers[q.question];
9590
- if (raw === void 0) {
9591
- throw new Error(`Missing answer for question "${q.question}"`);
9592
- }
9593
- const label = Array.isArray(raw) ? raw[0] : raw;
9594
- const target = q.options.findIndex((o) => o.label === label);
9595
- if (target < 0) throw new UnknownOptionError(q.question, label);
9596
- out += DOWN.repeat(target) + ENTER3;
9603
+ if (questions.length !== 1) {
9604
+ throw new UnsupportedPromptShapeError(`${questions.length} questions`);
9597
9605
  }
9598
- return out;
9606
+ const q = questions[0];
9607
+ if (q.multiSelect) throw new UnsupportedPromptShapeError("multiSelect");
9608
+ const raw = answers[q.question];
9609
+ if (raw === void 0 || Array.isArray(raw) && raw.length === 0) {
9610
+ throw new IncompleteAnswerError(q.question);
9611
+ }
9612
+ if (Array.isArray(raw) && raw.length > 1) {
9613
+ throw new UnsupportedPromptShapeError(`${raw.length} labels for a single-select question`);
9614
+ }
9615
+ const label = Array.isArray(raw) ? raw[0] : raw;
9616
+ const target = q.options.findIndex((o) => o.label === label);
9617
+ if (target < 0) throw new UnknownOptionError(q.question, label);
9618
+ return DOWN.repeat(target) + ENTER3;
9599
9619
  }
9600
9620
 
9601
9621
  // src/services/questions/resolveAnswer.ts
@@ -9609,6 +9629,10 @@ function resolveAnswer(pending, body) {
9609
9629
  return { ok: true, keys: answersToKeystrokes(pending.questions, answers) };
9610
9630
  } catch (e) {
9611
9631
  if (e instanceof UnknownOptionError) return { ok: false, reason: "unknown_option" };
9632
+ if (e instanceof UnsupportedPromptShapeError) {
9633
+ return { ok: false, reason: "unsupported_prompt_shape" };
9634
+ }
9635
+ if (e instanceof IncompleteAnswerError) return { ok: false, reason: "incomplete_answer" };
9612
9636
  throw e;
9613
9637
  }
9614
9638
  }
@@ -9938,6 +9962,13 @@ var SessionHandlers = class {
9938
9962
  get sessionSubscribers() {
9939
9963
  return this.deps.sessionSubscribers;
9940
9964
  }
9965
+ // Prompt lifecycle events (question / permission and their cancellations)
9966
+ // carry prompt content and go ONLY to the session's subscribers — never to
9967
+ // every connected socket. A client that subscribes after a prompt opened
9968
+ // gets it from the subscribe replay (server-wiring), not from a broadcast.
9969
+ broadcastToSession(sessionId, message) {
9970
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], message);
9971
+ }
9941
9972
  get agentConfig() {
9942
9973
  return this.deps.agentConfig;
9943
9974
  }
@@ -10492,7 +10523,7 @@ var SessionHandlers = class {
10492
10523
  const toolUseId = `screen:${sessionId}:${key.length}`;
10493
10524
  this.pendingQuestions.set(sessionId, { toolUseId, questions, origin: "pty" });
10494
10525
  this.pendingQuestionKey.set(sessionId, key);
10495
- this.wsHub.broadcast({ type: "question", sessionId, toolUseId, questions });
10526
+ this.broadcastToSession(sessionId, { type: "question", sessionId, toolUseId, questions });
10496
10527
  }
10497
10528
  // Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the
10498
10529
  // additive `permission` / `permission_cancelled` events. Mobile answers by
@@ -10502,7 +10533,7 @@ var SessionHandlers = class {
10502
10533
  if (!this.pendingPermission.has(sessionId)) return;
10503
10534
  this.pendingPermission.delete(sessionId);
10504
10535
  this.pendingPermissionKey.delete(sessionId);
10505
- this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
10536
+ this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
10506
10537
  return;
10507
10538
  }
10508
10539
  const key = permissionContentKey(gate);
@@ -10516,7 +10547,7 @@ var SessionHandlers = class {
10516
10547
  `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
10517
10548
  { event: "ws.broadcast_permission", sessionId, subscriberCount }
10518
10549
  );
10519
- this.wsHub.broadcast({
10550
+ this.broadcastToSession(sessionId, {
10520
10551
  type: "permission",
10521
10552
  sessionId,
10522
10553
  ...gate.prompt ? { prompt: gate.prompt } : {},
@@ -10564,7 +10595,7 @@ var SessionHandlers = class {
10564
10595
  const gateClosed = () => {
10565
10596
  this.pendingPermission.delete(sessionId);
10566
10597
  this.pendingPermissionKey.delete(sessionId);
10567
- this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
10598
+ this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
10568
10599
  json(res, 409, { ok: false, reason: "gate_closed" });
10569
10600
  };
10570
10601
  const gate = this.pendingPermission.get(sessionId);
@@ -10636,14 +10667,21 @@ var SessionHandlers = class {
10636
10667
  const pending = this.pendingQuestions.get(sessionId);
10637
10668
  const resolution = resolveAnswer(pending, body);
10638
10669
  if (!resolution.ok) {
10639
- json(res, 400, { ok: false, reason: resolution.reason });
10670
+ const unanswerable = resolution.reason === "unsupported_prompt_shape" || resolution.reason === "incomplete_answer";
10671
+ json(res, 400, {
10672
+ ok: false,
10673
+ reason: resolution.reason,
10674
+ ...unanswerable ? {
10675
+ error: "This prompt needs an answer the app cannot give yet; answer it in the terminal"
10676
+ } : {}
10677
+ });
10640
10678
  return;
10641
10679
  }
10642
10680
  const toolUseId = pending?.toolUseId ?? "";
10643
10681
  if (!await this.questionMenuStillOpen(sessionId)) {
10644
10682
  this.pendingQuestions.delete(sessionId);
10645
10683
  this.pendingQuestionKey.delete(sessionId);
10646
- this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId });
10684
+ this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
10647
10685
  json(res, 409, { ok: false, reason: "question_gone" });
10648
10686
  return;
10649
10687
  }
@@ -10655,7 +10693,7 @@ var SessionHandlers = class {
10655
10693
  return;
10656
10694
  }
10657
10695
  this.pendingQuestions.delete(sessionId);
10658
- this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId });
10696
+ this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
10659
10697
  json(res, 200, { ok: true });
10660
10698
  }
10661
10699
  // Best-effort: a session we don't own a PTY for, or one that raced away
@@ -17776,7 +17814,7 @@ var StreamerServer = class {
17776
17814
  priorToolUseId
17777
17815
  });
17778
17816
  this.pendingQuestionKey.set(sessionId, key);
17779
- if (broadcast) this.wsHub.broadcast(m);
17817
+ if (broadcast) this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], m);
17780
17818
  }
17781
17819
  }
17782
17820
  cancelPendingQuestion(sessionId) {
@@ -17784,7 +17822,11 @@ var StreamerServer = class {
17784
17822
  if (!pq) return;
17785
17823
  this.pendingQuestions.delete(sessionId);
17786
17824
  this.pendingQuestionKey.delete(sessionId);
17787
- this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId: pq.toolUseId });
17825
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
17826
+ type: "question_cancelled",
17827
+ sessionId,
17828
+ toolUseId: pq.toolUseId
17829
+ });
17788
17830
  }
17789
17831
  async handleBrowse(url, res) {
17790
17832
  if (!this.browseRoot) {