@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.cjs CHANGED
@@ -9623,19 +9623,39 @@ var UnknownOptionError = class extends Error {
9623
9623
  question;
9624
9624
  value;
9625
9625
  };
9626
+ var UnsupportedPromptShapeError = class extends Error {
9627
+ constructor(detail) {
9628
+ super(`Unsupported prompt shape: ${detail}`);
9629
+ this.detail = detail;
9630
+ this.name = "UnsupportedPromptShapeError";
9631
+ }
9632
+ detail;
9633
+ };
9634
+ var IncompleteAnswerError = class extends Error {
9635
+ constructor(question) {
9636
+ super(`Missing answer for question "${question}"`);
9637
+ this.question = question;
9638
+ this.name = "IncompleteAnswerError";
9639
+ }
9640
+ question;
9641
+ };
9626
9642
  function answersToKeystrokes(questions, answers) {
9627
- let out = "";
9628
- for (const q of questions) {
9629
- const raw = answers[q.question];
9630
- if (raw === void 0) {
9631
- throw new Error(`Missing answer for question "${q.question}"`);
9632
- }
9633
- const label = Array.isArray(raw) ? raw[0] : raw;
9634
- const target = q.options.findIndex((o) => o.label === label);
9635
- if (target < 0) throw new UnknownOptionError(q.question, label);
9636
- out += DOWN.repeat(target) + ENTER3;
9643
+ if (questions.length !== 1) {
9644
+ throw new UnsupportedPromptShapeError(`${questions.length} questions`);
9637
9645
  }
9638
- return out;
9646
+ const q = questions[0];
9647
+ if (q.multiSelect) throw new UnsupportedPromptShapeError("multiSelect");
9648
+ const raw = answers[q.question];
9649
+ if (raw === void 0 || Array.isArray(raw) && raw.length === 0) {
9650
+ throw new IncompleteAnswerError(q.question);
9651
+ }
9652
+ if (Array.isArray(raw) && raw.length > 1) {
9653
+ throw new UnsupportedPromptShapeError(`${raw.length} labels for a single-select question`);
9654
+ }
9655
+ const label = Array.isArray(raw) ? raw[0] : raw;
9656
+ const target = q.options.findIndex((o) => o.label === label);
9657
+ if (target < 0) throw new UnknownOptionError(q.question, label);
9658
+ return DOWN.repeat(target) + ENTER3;
9639
9659
  }
9640
9660
 
9641
9661
  // src/services/questions/resolveAnswer.ts
@@ -9649,6 +9669,10 @@ function resolveAnswer(pending, body) {
9649
9669
  return { ok: true, keys: answersToKeystrokes(pending.questions, answers) };
9650
9670
  } catch (e) {
9651
9671
  if (e instanceof UnknownOptionError) return { ok: false, reason: "unknown_option" };
9672
+ if (e instanceof UnsupportedPromptShapeError) {
9673
+ return { ok: false, reason: "unsupported_prompt_shape" };
9674
+ }
9675
+ if (e instanceof IncompleteAnswerError) return { ok: false, reason: "incomplete_answer" };
9652
9676
  throw e;
9653
9677
  }
9654
9678
  }
@@ -9978,6 +10002,13 @@ var SessionHandlers = class {
9978
10002
  get sessionSubscribers() {
9979
10003
  return this.deps.sessionSubscribers;
9980
10004
  }
10005
+ // Prompt lifecycle events (question / permission and their cancellations)
10006
+ // carry prompt content and go ONLY to the session's subscribers — never to
10007
+ // every connected socket. A client that subscribes after a prompt opened
10008
+ // gets it from the subscribe replay (server-wiring), not from a broadcast.
10009
+ broadcastToSession(sessionId, message) {
10010
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], message);
10011
+ }
9981
10012
  get agentConfig() {
9982
10013
  return this.deps.agentConfig;
9983
10014
  }
@@ -10532,7 +10563,7 @@ var SessionHandlers = class {
10532
10563
  const toolUseId = `screen:${sessionId}:${key.length}`;
10533
10564
  this.pendingQuestions.set(sessionId, { toolUseId, questions, origin: "pty" });
10534
10565
  this.pendingQuestionKey.set(sessionId, key);
10535
- this.wsHub.broadcast({ type: "question", sessionId, toolUseId, questions });
10566
+ this.broadcastToSession(sessionId, { type: "question", sessionId, toolUseId, questions });
10536
10567
  }
10537
10568
  // Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the
10538
10569
  // additive `permission` / `permission_cancelled` events. Mobile answers by
@@ -10542,7 +10573,7 @@ var SessionHandlers = class {
10542
10573
  if (!this.pendingPermission.has(sessionId)) return;
10543
10574
  this.pendingPermission.delete(sessionId);
10544
10575
  this.pendingPermissionKey.delete(sessionId);
10545
- this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
10576
+ this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
10546
10577
  return;
10547
10578
  }
10548
10579
  const key = permissionContentKey(gate);
@@ -10556,7 +10587,7 @@ var SessionHandlers = class {
10556
10587
  `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
10557
10588
  { event: "ws.broadcast_permission", sessionId, subscriberCount }
10558
10589
  );
10559
- this.wsHub.broadcast({
10590
+ this.broadcastToSession(sessionId, {
10560
10591
  type: "permission",
10561
10592
  sessionId,
10562
10593
  ...gate.prompt ? { prompt: gate.prompt } : {},
@@ -10604,7 +10635,7 @@ var SessionHandlers = class {
10604
10635
  const gateClosed = () => {
10605
10636
  this.pendingPermission.delete(sessionId);
10606
10637
  this.pendingPermissionKey.delete(sessionId);
10607
- this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
10638
+ this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
10608
10639
  json(res, 409, { ok: false, reason: "gate_closed" });
10609
10640
  };
10610
10641
  const gate = this.pendingPermission.get(sessionId);
@@ -10676,14 +10707,21 @@ var SessionHandlers = class {
10676
10707
  const pending = this.pendingQuestions.get(sessionId);
10677
10708
  const resolution = resolveAnswer(pending, body);
10678
10709
  if (!resolution.ok) {
10679
- json(res, 400, { ok: false, reason: resolution.reason });
10710
+ const unanswerable = resolution.reason === "unsupported_prompt_shape" || resolution.reason === "incomplete_answer";
10711
+ json(res, 400, {
10712
+ ok: false,
10713
+ reason: resolution.reason,
10714
+ ...unanswerable ? {
10715
+ error: "This prompt needs an answer the app cannot give yet; answer it in the terminal"
10716
+ } : {}
10717
+ });
10680
10718
  return;
10681
10719
  }
10682
10720
  const toolUseId = pending?.toolUseId ?? "";
10683
10721
  if (!await this.questionMenuStillOpen(sessionId)) {
10684
10722
  this.pendingQuestions.delete(sessionId);
10685
10723
  this.pendingQuestionKey.delete(sessionId);
10686
- this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId });
10724
+ this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
10687
10725
  json(res, 409, { ok: false, reason: "question_gone" });
10688
10726
  return;
10689
10727
  }
@@ -10695,7 +10733,7 @@ var SessionHandlers = class {
10695
10733
  return;
10696
10734
  }
10697
10735
  this.pendingQuestions.delete(sessionId);
10698
- this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId });
10736
+ this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
10699
10737
  json(res, 200, { ok: true });
10700
10738
  }
10701
10739
  // Best-effort: a session we don't own a PTY for, or one that raced away
@@ -17805,7 +17843,7 @@ var StreamerServer = class {
17805
17843
  priorToolUseId
17806
17844
  });
17807
17845
  this.pendingQuestionKey.set(sessionId, key);
17808
- if (broadcast) this.wsHub.broadcast(m);
17846
+ if (broadcast) this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], m);
17809
17847
  }
17810
17848
  }
17811
17849
  cancelPendingQuestion(sessionId) {
@@ -17813,7 +17851,11 @@ var StreamerServer = class {
17813
17851
  if (!pq) return;
17814
17852
  this.pendingQuestions.delete(sessionId);
17815
17853
  this.pendingQuestionKey.delete(sessionId);
17816
- this.wsHub.broadcast({ type: "question_cancelled", sessionId, toolUseId: pq.toolUseId });
17854
+ this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
17855
+ type: "question_cancelled",
17856
+ sessionId,
17857
+ toolUseId: pq.toolUseId
17858
+ });
17817
17859
  }
17818
17860
  async handleBrowse(url, res) {
17819
17861
  if (!this.browseRoot) {