@hachej/boring-ask-user 0.1.54 → 0.1.55

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/README.md CHANGED
@@ -23,7 +23,7 @@ cancels.
23
23
  | Panel | `ask-user.questions` ("Questions"), `placement: "center"`, chromeless |
24
24
  | Surface resolver | kind `questions` (`ASK_USER_SURFACE_KIND`) → opens the panel |
25
25
  | Agent tool | `ask_user` (blocking; resolves `answered` / `cancelled`) |
26
- | HTTP route | `POST /api/v1/questions/commands` (submit + cancel commands) |
26
+ | WorkspaceBridge ops | `ask-user.v1.request`, `ask-user.v1.answer`, `ask-user.v1.cancel`, `ask-user.v1.pending`, `ask-user.v1.transcript` |
27
27
  | Pi prompt | `pi.systemPrompt` nudges the agent to use `ask_user` over chat roleplay |
28
28
 
29
29
  ## How it's wired
@@ -74,6 +74,11 @@ The agent then calls `ask_user` with a `{ title, context?, schema }` payload:
74
74
  The pane opens, the user submits, and the tool resolves with
75
75
  `{ status: "answered", answer: { values: { env: "production" } } }`.
76
76
 
77
+ Submit/cancel/pending/transcript traffic goes through WorkspaceBridge
78
+ `ask-user.v1.*` operations. The old `questionsRoutes` helper for
79
+ `POST /api/v1/questions/commands` remains exported only for manual legacy
80
+ wiring; `createAskUserServerPlugin` does not register that route.
81
+
77
82
  ## Field types
78
83
 
79
84
  `text`, `textarea`, `select`, `multiselect`, `checkbox`, `radio`, `number`.
@@ -3,26 +3,20 @@
3
3
  // src/front/index.tsx
4
4
  import { Button, EmptyState, Notice, Pane, PaneBody, PaneFooter, PaneHeader, PaneTitle } from "@hachej/boring-ui-kit";
5
5
  import {
6
- UI_COMMAND_EVENT,
7
- events,
8
- useWorkspaceAttention,
9
- workspaceEvents
6
+ WORKSPACE_COMPOSER_STOP_EVENT as WORKSPACE_COMPOSER_STOP_EVENT2,
7
+ workspaceComposerStopAppliesToSession as workspaceComposerStopAppliesToSession2
10
8
  } from "@hachej/boring-workspace";
11
9
  import {
12
10
  definePlugin
13
11
  } from "@hachej/boring-workspace/plugin";
14
12
  import { HelpCircle, XCircle } from "lucide-react";
15
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useMemo as useMemo2, useSyncExternalStore, useState as useState2 } from "react";
13
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useSyncExternalStore, useState as useState2 } from "react";
16
14
 
17
15
  // src/shared/constants.ts
18
16
  var ASK_USER_PLUGIN_ID = "ask-user";
19
17
  var ASK_USER_PANEL_ID = "ask-user.questions";
20
18
  var ASK_USER_PANEL_TITLE = "Questions";
21
19
  var ASK_USER_SURFACE_KIND = "questions";
22
- var ASK_USER_COMMAND_KINDS = {
23
- SUBMIT: "questions.submit",
24
- CANCEL: "questions.cancel"
25
- };
26
20
  var ASK_USER_UI_STATE_SLOTS = {
27
21
  PENDING: "questions.pending"
28
22
  };
@@ -41,6 +35,15 @@ var ASK_USER_SCHEMA_LIMITS = {
41
35
  defaultTimeoutMs: 10 * 6e4
42
36
  };
43
37
 
38
+ // src/shared/bridge.ts
39
+ var ASK_USER_BRIDGE_OPS = {
40
+ request: "ask-user.v1.request",
41
+ answer: "ask-user.v1.answer",
42
+ cancel: "ask-user.v1.cancel",
43
+ pending: "ask-user.v1.pending",
44
+ transcript: "ask-user.v1.transcript"
45
+ };
46
+
44
47
  // src/shared/error-codes.ts
45
48
  var ASK_USER_ERROR_CODES = {
46
49
  PENDING_EXISTS: "ASK_USER_PENDING_EXISTS",
@@ -303,50 +306,219 @@ var QuestionsClientError = class extends Error {
303
306
  code;
304
307
  statusCode;
305
308
  };
306
- function readPendingQuestionFromState(state) {
309
+ function readPendingQuestionHintsFromState(state) {
307
310
  const slot = state?.[ASK_USER_UI_STATE_SLOTS.PENDING];
308
- if (!slot || typeof slot !== "object") return null;
309
- const question = slot.question;
310
- return question && typeof question === "object" ? question : null;
311
+ if (!slot || typeof slot !== "object") return [];
312
+ const hints = /* @__PURE__ */ new Map();
313
+ const rawSlot = slot;
314
+ const current = readHint(rawSlot.hint) ?? readHint(rawSlot.question);
315
+ if (current) hints.set(current.sessionId, current);
316
+ if (rawSlot.hintsBySession && typeof rawSlot.hintsBySession === "object" && !Array.isArray(rawSlot.hintsBySession)) {
317
+ for (const [sessionId, candidate] of Object.entries(rawSlot.hintsBySession)) {
318
+ const hint = readHint(candidate);
319
+ if (hint && hint.sessionId === sessionId) hints.set(sessionId, hint);
320
+ }
321
+ }
322
+ return [...hints.values()];
323
+ }
324
+ function readHint(value) {
325
+ if (!value || typeof value !== "object") return null;
326
+ const raw = value;
327
+ if (typeof raw.questionId !== "string" || typeof raw.sessionId !== "string") return null;
328
+ const status = normalizeQuestionStatus(raw.status);
329
+ return { questionId: raw.questionId, sessionId: raw.sessionId, ...status === "abandoned" && raw.status === void 0 ? {} : { status } };
311
330
  }
312
331
  function createQuestionsClient(options = {}) {
313
- async function dispatch(command) {
314
- const response = await fetch(`${options.apiBaseUrl ?? ""}/api/v1/questions/commands`, {
332
+ async function callBridge(op, input, sessionId, idempotencyKey) {
333
+ const response = await fetch(`${options.apiBaseUrl ?? ""}/api/v1/workspace-bridge/call`, {
315
334
  method: "POST",
316
- headers: { "Content-Type": "application/json", ...options.headers ?? {} },
317
- body: JSON.stringify(command)
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ ...options.headers ?? {},
338
+ ...sessionId ? { "x-boring-session-id": sessionId } : {}
339
+ },
340
+ body: JSON.stringify({ op, input, ...idempotencyKey ? { idempotencyKey } : {} })
318
341
  });
319
342
  const payload = await response.json().catch(() => ({}));
320
- if (!response.ok) throw new QuestionsClientError(payload.error ?? ASK_USER_ERROR_CODES.UI_UNAVAILABLE, payload.message ?? "Question command failed", response.status);
321
- return payload;
343
+ if (!response.ok) {
344
+ const error = "error" in payload ? payload.error : void 0;
345
+ throw new QuestionsClientError(
346
+ error?.code ?? ASK_USER_ERROR_CODES.UI_UNAVAILABLE,
347
+ error?.message ?? "Question bridge call failed",
348
+ response.status
349
+ );
350
+ }
351
+ if (!payload.ok) {
352
+ throw new QuestionsClientError(
353
+ payload.error?.code ?? ASK_USER_ERROR_CODES.UI_UNAVAILABLE,
354
+ payload.error?.message ?? "Question bridge call failed",
355
+ response.status
356
+ );
357
+ }
358
+ return payload.output;
322
359
  }
323
360
  return {
324
- dispatch,
325
- cancel(question) {
326
- return dispatch({ kind: ASK_USER_COMMAND_KINDS.CANCEL, params: { questionId: question.questionId, sessionId: question.sessionId, answerToken: question.answerToken } });
361
+ async pending(sessionId) {
362
+ const output = await callBridge(
363
+ ASK_USER_BRIDGE_OPS.pending,
364
+ { sessionId },
365
+ sessionId
366
+ );
367
+ return normalizeQuestion(output.pending);
368
+ },
369
+ async cancel(question) {
370
+ ensureAnswerToken(question);
371
+ return await callBridge(
372
+ ASK_USER_BRIDGE_OPS.cancel,
373
+ {
374
+ questionId: question.questionId,
375
+ sessionId: question.sessionId,
376
+ answerToken: question.answerToken
377
+ },
378
+ question.sessionId,
379
+ await deriveIdempotencyKey(ASK_USER_BRIDGE_OPS.cancel, {
380
+ questionId: question.questionId,
381
+ sessionId: question.sessionId,
382
+ answerToken: question.answerToken
383
+ })
384
+ );
327
385
  },
328
- submit(question, values) {
386
+ async submit(question, values) {
387
+ ensureAnswerToken(question);
329
388
  if (!question.schema) throw new QuestionsClientError(ASK_USER_ERROR_CODES.QUESTION_NOT_READY, "Question is not ready");
330
389
  const validation = validateQuestionValues(question.schema, values);
331
390
  if (!validation.valid) throw new QuestionsClientError(ASK_USER_ERROR_CODES.ANSWER_INVALID, firstValidationMessage(validation));
332
- return dispatch({ kind: ASK_USER_COMMAND_KINDS.SUBMIT, params: { questionId: question.questionId, sessionId: question.sessionId, answerToken: question.answerToken, values } });
391
+ return await callBridge(
392
+ ASK_USER_BRIDGE_OPS.answer,
393
+ {
394
+ questionId: question.questionId,
395
+ sessionId: question.sessionId,
396
+ answerToken: question.answerToken,
397
+ values
398
+ },
399
+ question.sessionId,
400
+ await deriveIdempotencyKey(ASK_USER_BRIDGE_OPS.answer, {
401
+ questionId: question.questionId,
402
+ sessionId: question.sessionId,
403
+ answerToken: question.answerToken,
404
+ values
405
+ })
406
+ );
333
407
  }
334
408
  };
335
409
  }
410
+ function normalizeQuestion(value) {
411
+ if (!value || typeof value !== "object") return null;
412
+ const raw = value;
413
+ if (typeof raw.questionId !== "string" || typeof raw.sessionId !== "string") return null;
414
+ const schema = isAskUserFormSchema(raw.schema) ? raw.schema : void 0;
415
+ return {
416
+ questionId: raw.questionId,
417
+ sessionId: raw.sessionId,
418
+ ownerPrincipalId: typeof raw.ownerPrincipalId === "string" ? raw.ownerPrincipalId : "anonymous",
419
+ status: normalizeQuestionStatus(raw.status),
420
+ title: typeof raw.title === "string" ? raw.title : void 0,
421
+ context: typeof raw.context === "string" ? raw.context : void 0,
422
+ schema,
423
+ answerToken: typeof raw.answerToken === "string" ? raw.answerToken : "",
424
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date(0)).toISOString(),
425
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString()
426
+ };
427
+ }
428
+ async function deriveIdempotencyKey(op, inputValue) {
429
+ const canonical = `${op}:${stableStringify(inputValue)}`;
430
+ const subtle = globalThis.crypto?.subtle;
431
+ if (subtle && typeof subtle.digest === "function") {
432
+ const input = new TextEncoder().encode(canonical);
433
+ const digest = await subtle.digest("SHA-256", input);
434
+ return `ask-user-idem:${Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")}`;
435
+ }
436
+ return `ask-user-idem:${deterministicHashHex(canonical)}`;
437
+ }
438
+ function deterministicHashHex(value) {
439
+ let h1 = 3735928559 ^ value.length;
440
+ let h2 = 1103547991 ^ value.length;
441
+ let h3 = 3235826430 ^ value.length;
442
+ let h4 = 2654435769 ^ value.length;
443
+ for (let i = 0; i < value.length; i += 1) {
444
+ const code = value.charCodeAt(i);
445
+ h1 = Math.imul(h1 ^ code, 2654435761);
446
+ h2 = Math.imul(h2 ^ code, 1597334677);
447
+ h3 = Math.imul(h3 ^ code, 2246822507);
448
+ h4 = Math.imul(h4 ^ code, 3266489909);
449
+ }
450
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
451
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h3 ^ h3 >>> 13, 3266489909);
452
+ h3 = Math.imul(h3 ^ h3 >>> 16, 2246822507) ^ Math.imul(h4 ^ h4 >>> 13, 3266489909);
453
+ h4 = Math.imul(h4 ^ h4 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
454
+ return [h1, h2, h3, h4].map((part) => (part >>> 0).toString(16).padStart(8, "0")).join("");
455
+ }
456
+ function stableStringify(value) {
457
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
458
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
459
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
460
+ }
461
+ function normalizeQuestionStatus(status) {
462
+ if (status === "pending" || status === "ready") return "ready";
463
+ if (status === "answered" || status === "cancelled" || status === "abandoned") return status;
464
+ return "abandoned";
465
+ }
466
+ function ensureAnswerToken(question) {
467
+ if (!question.answerToken) throw new QuestionsClientError(ASK_USER_ERROR_CODES.QUESTION_NOT_READY, "Question answer token is missing");
468
+ }
336
469
  function firstValidationMessage(validation) {
337
470
  return Object.values(validation.errors)[0] ?? "Invalid answer";
338
471
  }
472
+ function isAskUserFormSchema(value) {
473
+ return !!value && typeof value === "object" && value.wireVersion === 1 && Array.isArray(value.fields);
474
+ }
339
475
 
340
- // src/front/index.tsx
341
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
476
+ // src/front/runtime.ts
477
+ import { createContext as createContext2, useContext as useContext2 } from "react";
342
478
  function createQuestionsStore() {
343
479
  const listeners = /* @__PURE__ */ new Set();
344
- let pending = null;
480
+ const pendingBySession = /* @__PURE__ */ new Map();
481
+ const hintsBySession = /* @__PURE__ */ new Map();
482
+ const emit = () => {
483
+ for (const listener of [...listeners]) listener();
484
+ };
345
485
  return {
346
- getPending: () => pending,
347
- setPending(question) {
348
- pending = question;
349
- for (const listener of [...listeners]) listener();
486
+ getPending(sessionId) {
487
+ return sessionId ? pendingBySession.get(sessionId) ?? null : null;
488
+ },
489
+ setPending(question, sessionId) {
490
+ if (question) {
491
+ pendingBySession.set(question.sessionId, question);
492
+ hintsBySession.set(question.sessionId, { questionId: question.questionId, sessionId: question.sessionId, status: question.status });
493
+ } else if (sessionId) {
494
+ pendingBySession.delete(sessionId);
495
+ hintsBySession.delete(sessionId);
496
+ } else {
497
+ pendingBySession.clear();
498
+ hintsBySession.clear();
499
+ }
500
+ emit();
501
+ },
502
+ getPendingHints() {
503
+ return [...hintsBySession.values()];
504
+ },
505
+ getHydratedPendingKeys() {
506
+ return [...pendingBySession.values()].map((question) => `${question.sessionId}:${question.questionId}:${question.status}`);
507
+ },
508
+ setPendingHints(hints) {
509
+ hintsBySession.clear();
510
+ const authoritativeHints = /* @__PURE__ */ new Map();
511
+ for (const hint of hints) {
512
+ hintsBySession.set(hint.sessionId, hint);
513
+ authoritativeHints.set(hint.sessionId, hint);
514
+ }
515
+ for (const [sessionId, question] of [...pendingBySession.entries()]) {
516
+ const hint = authoritativeHints.get(sessionId);
517
+ if (!hint || hint.questionId !== question.questionId || hint.status && hint.status !== question.status) {
518
+ pendingBySession.delete(sessionId);
519
+ }
520
+ }
521
+ emit();
350
522
  },
351
523
  subscribe(listener) {
352
524
  listeners.add(listener);
@@ -356,60 +528,143 @@ function createQuestionsStore() {
356
528
  }
357
529
  var sharedQuestionsStore = createQuestionsStore();
358
530
  var QuestionsRuntimeContext = createContext2(null);
359
- function sessionScopedBlockerId(sessionId) {
360
- return sessionId === "default" || sessionId === "anonymous" ? void 0 : sessionId;
361
- }
362
- function pendingQuestionSnapshot(store) {
363
- const pending = store.getPending();
364
- return pending ? `${pending.sessionId}:${pending.questionId}:${pending.status}` : "none";
365
- }
366
531
  function useQuestionsRuntime() {
367
532
  const ctx = useContext2(QuestionsRuntimeContext);
368
533
  if (!ctx) throw new Error("askUserPlugin QuestionsPane must be rendered under AskUserProvider");
369
534
  return ctx;
370
535
  }
371
- function AskUserProvider({ apiBaseUrl, authHeaders, children }) {
536
+ function pendingQuestionSnapshot(store) {
537
+ const hints = store.getPendingHints().map((hint) => `${hint.sessionId}:${hint.questionId}:${hint.status ?? "ready"}`).sort();
538
+ const hydrated = store.getHydratedPendingKeys().sort();
539
+ return `${hints.length ? hints.join("|") : "none"}#hydrated=${hydrated.join("|")}`;
540
+ }
541
+ function isSessionOpen(runtime, sessionId) {
542
+ if (runtime.openSessionIds) return runtime.openSessionIds.includes(sessionId);
543
+ return runtime.activeSessionId === sessionId;
544
+ }
545
+
546
+ // src/front/providerHooks.ts
547
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
548
+ import {
549
+ UI_COMMAND_EVENT,
550
+ WORKSPACE_ATTENTION_ACTION_EVENT,
551
+ WORKSPACE_COMPOSER_STOP_EVENT,
552
+ WORKSPACE_SURFACE_OPEN_SKIPPED_EVENT,
553
+ events,
554
+ postUiCommand,
555
+ useWorkspaceAttention,
556
+ workspaceComposerStopAppliesToSession,
557
+ workspaceComposerStopTargetSessionId,
558
+ workspaceEvents
559
+ } from "@hachej/boring-workspace";
560
+ function useAskUserAttentionBlockers(runtime, pendingSnapshot) {
372
561
  const { addBlocker, removeBlocker } = useWorkspaceAttention();
373
- const runtime = useMemo2(() => ({ ...sharedQuestionsStore, apiBaseUrl, authHeaders }), [apiBaseUrl, authHeaders]);
374
- const pendingSnapshot = useSyncExternalStore(runtime.subscribe, () => pendingQuestionSnapshot(runtime), () => "none");
375
562
  useEffect2(() => {
376
- const pending = runtime.getPending();
377
- const blockerId = pending ? `${ASK_USER_PLUGIN_ID}:${pending.sessionId}:${pending.questionId}` : null;
378
- if (pending?.status === "ready" && blockerId) {
563
+ const blockerIds = [];
564
+ for (const hint of runtime.getPendingHints()) {
565
+ if (hint.status && hint.status !== "ready") continue;
566
+ const blockerId = `${ASK_USER_PLUGIN_ID}:${hint.sessionId}:${hint.questionId}`;
567
+ blockerIds.push(blockerId);
568
+ const hydrated = runtime.getPending(hint.sessionId);
569
+ const isActiveHint = runtime.activeSessionId === hint.sessionId && isSessionOpen(runtime, hint.sessionId);
570
+ const actions = hydrated ? [{ id: "open", label: "Open Questions" }, { id: "cancel", label: "Cancel question" }] : isActiveHint ? [{ id: "open", label: "Open Questions" }] : void 0;
379
571
  addBlocker({
380
572
  id: blockerId,
381
- reason: "waiting_for_user_input",
573
+ reason: "ask-user.question",
382
574
  surfaceKind: ASK_USER_SURFACE_KIND,
383
- target: pending.questionId,
575
+ target: hint.questionId,
384
576
  label: "Answer the question in Questions to continue",
385
- sessionId: sessionScopedBlockerId(pending.sessionId),
386
- actions: [{ id: "open", label: "Open Questions" }, { id: "cancel", label: "Cancel question" }]
577
+ sessionId: hint.sessionId,
578
+ sessionBadge: { kind: "question", label: "question", tone: "attention", priority: 10 },
579
+ actions
387
580
  });
388
581
  }
389
582
  return () => {
390
- if (blockerId) removeBlocker(blockerId);
583
+ for (const blockerId of blockerIds) removeBlocker(blockerId);
391
584
  };
392
585
  }, [addBlocker, removeBlocker, runtime, pendingSnapshot]);
586
+ }
587
+ function useAskUserAutoOpen(runtime, activeSessionId, pendingSnapshot) {
588
+ const autoOpenedQuestionsRef = useRef2(/* @__PURE__ */ new Set());
589
+ useEffect2(() => {
590
+ for (const hint2 of runtime.getPendingHints()) {
591
+ if (!isSessionOpen(runtime, hint2.sessionId)) autoOpenedQuestionsRef.current.delete(`${hint2.sessionId}:${hint2.questionId}`);
592
+ }
593
+ if (!activeSessionId || !isSessionOpen(runtime, activeSessionId)) return;
594
+ const hint = runtime.getPendingHints().find((candidate) => candidate.sessionId === activeSessionId);
595
+ if (!hint || hint.status && hint.status !== "ready") return;
596
+ const hydrated = runtime.getPending(activeSessionId);
597
+ if (!hydrated || hydrated.questionId !== hint.questionId || hydrated.status !== "ready") return;
598
+ const key = `${hint.sessionId}:${hint.questionId}`;
599
+ if (autoOpenedQuestionsRef.current.has(key)) return;
600
+ autoOpenedQuestionsRef.current.add(key);
601
+ postUiCommand({
602
+ kind: "openSurface",
603
+ params: {
604
+ kind: ASK_USER_SURFACE_KIND,
605
+ target: hint.questionId,
606
+ meta: { sessionId: hint.sessionId, openOnlyWhenSessionOpen: true }
607
+ }
608
+ });
609
+ }, [activeSessionId, runtime, pendingSnapshot]);
610
+ }
611
+ function useAskUserAttentionActions(runtime) {
612
+ useEffect2(() => {
613
+ const onAction = (event) => {
614
+ const detail = event.detail;
615
+ if (!detail || detail.actionId !== "cancel" || detail.blocker.reason !== "ask-user.question") return;
616
+ const sessionId = detail.blocker.sessionId ?? detail.sessionId ?? runtime.activeSessionId;
617
+ const pending = runtime.getPending(sessionId);
618
+ if (!pending || detail.blocker.target && pending.questionId !== detail.blocker.target) return;
619
+ runtime.setPending(null, pending.sessionId);
620
+ void createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }).cancel(pending).catch(() => void 0);
621
+ };
622
+ window.addEventListener(WORKSPACE_ATTENTION_ACTION_EVENT, onAction);
623
+ return () => window.removeEventListener(WORKSPACE_ATTENTION_ACTION_EVENT, onAction);
624
+ }, [runtime]);
625
+ }
626
+ function useAskUserComposerStopCancel(runtime) {
393
627
  useEffect2(() => {
394
628
  const onStop = (event) => {
395
- const sessionId = event.detail?.sessionId;
396
- const pending = runtime.getPending();
397
- if (!pending || sessionId && sessionScopedBlockerId(pending.sessionId) && sessionId !== pending.sessionId) return;
398
- runtime.setPending(null);
629
+ const detail = event.detail;
630
+ const sessionId = workspaceComposerStopTargetSessionId(detail, runtime.activeSessionId);
631
+ const pending = runtime.getPending(sessionId);
632
+ if (!pending || !workspaceComposerStopAppliesToSession(detail, pending.sessionId, {
633
+ fallbackSessionId: runtime.activeSessionId
634
+ })) return;
635
+ runtime.setPending(null, pending.sessionId);
399
636
  void createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }).cancel(pending).catch(() => void 0);
400
637
  };
401
- window.addEventListener("boring:workspace-composer-stop", onStop);
402
- return () => window.removeEventListener("boring:workspace-composer-stop", onStop);
638
+ window.addEventListener(WORKSPACE_COMPOSER_STOP_EVENT, onStop);
639
+ return () => window.removeEventListener(WORKSPACE_COMPOSER_STOP_EVENT, onStop);
403
640
  }, [runtime]);
641
+ }
642
+ function useAskUserPendingRefresh(runtime, options) {
643
+ const { activeSessionId, apiBaseUrl, authHeaders } = options;
404
644
  useEffect2(() => {
405
645
  let stopped = false;
406
646
  async function refreshPending() {
647
+ let hints = [];
407
648
  try {
408
649
  const response = await fetch(`${apiBaseUrl}/api/v1/ui/state`, { headers: authHeaders });
409
650
  const state = await response.json().catch(() => null);
410
- if (!stopped) runtime.setPending(readPendingQuestionFromState(state));
651
+ hints = readPendingQuestionHintsFromState(state);
652
+ if (!stopped && hasPendingStateSlot(state)) runtime.setPendingHints(hints);
411
653
  } catch {
412
654
  }
655
+ const sessionsToHydrate = /* @__PURE__ */ new Set();
656
+ if (activeSessionId) sessionsToHydrate.add(activeSessionId);
657
+ for (const hint of hints) {
658
+ if (isSessionOpen(runtime, hint.sessionId)) sessionsToHydrate.add(hint.sessionId);
659
+ }
660
+ if (sessionsToHydrate.size === 0 && hints[0]) sessionsToHydrate.add(hints[0].sessionId);
661
+ await Promise.all([...sessionsToHydrate].map(async (sessionId) => {
662
+ try {
663
+ await runtime.refreshPending(sessionId);
664
+ } catch {
665
+ if (!stopped) runtime.setPending(null, sessionId);
666
+ }
667
+ }));
413
668
  }
414
669
  const onVisibility = () => {
415
670
  if (document.visibilityState === "visible") void refreshPending();
@@ -417,6 +672,10 @@ function AskUserProvider({ apiBaseUrl, authHeaders, children }) {
417
672
  const onUiCommand = () => {
418
673
  void refreshPending();
419
674
  };
675
+ const onSurfaceOpenSkipped = (event) => {
676
+ const detail = event.detail;
677
+ if (detail?.kind === ASK_USER_SURFACE_KIND) void refreshPending();
678
+ };
420
679
  let agentDataTimer = null;
421
680
  const onAgentData = () => {
422
681
  if (agentDataTimer) return;
@@ -426,44 +685,112 @@ function AskUserProvider({ apiBaseUrl, authHeaders, children }) {
426
685
  }, 1200);
427
686
  };
428
687
  const offAgentData = events.on(workspaceEvents.agentData, onAgentData);
688
+ const offUiCommand = events.on(workspaceEvents.uiCommand, onUiCommand);
429
689
  void refreshPending();
430
690
  window.addEventListener("focus", refreshPending);
431
691
  document.addEventListener("visibilitychange", onVisibility);
432
692
  window.addEventListener(UI_COMMAND_EVENT, onUiCommand);
693
+ window.addEventListener(WORKSPACE_SURFACE_OPEN_SKIPPED_EVENT, onSurfaceOpenSkipped);
433
694
  return () => {
434
695
  stopped = true;
435
696
  if (agentDataTimer) clearTimeout(agentDataTimer);
436
697
  offAgentData();
698
+ offUiCommand();
437
699
  window.removeEventListener("focus", refreshPending);
438
700
  document.removeEventListener("visibilitychange", onVisibility);
439
701
  window.removeEventListener(UI_COMMAND_EVENT, onUiCommand);
702
+ window.removeEventListener(WORKSPACE_SURFACE_OPEN_SKIPPED_EVENT, onSurfaceOpenSkipped);
440
703
  };
441
- }, [apiBaseUrl, authHeaders, runtime]);
704
+ }, [activeSessionId, apiBaseUrl, authHeaders, runtime]);
705
+ }
706
+ function hasPendingStateSlot(state) {
707
+ return !!state && Object.prototype.hasOwnProperty.call(state, ASK_USER_UI_STATE_SLOTS.PENDING);
708
+ }
709
+
710
+ // src/front/index.tsx
711
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
712
+ function AskUserProvider({ apiBaseUrl, authHeaders, activeSessionId, openSessionIds, children }) {
713
+ const runtime = useMemo2(() => ({
714
+ ...sharedQuestionsStore,
715
+ apiBaseUrl,
716
+ authHeaders,
717
+ activeSessionId,
718
+ openSessionIds,
719
+ async refreshPending(sessionId) {
720
+ const pending = await createQuestionsClient({ apiBaseUrl, headers: authHeaders }).pending(sessionId);
721
+ sharedQuestionsStore.setPending(pending, sessionId);
722
+ return pending;
723
+ }
724
+ }), [activeSessionId, apiBaseUrl, authHeaders, openSessionIds]);
725
+ const pendingSnapshot = useSyncExternalStore(runtime.subscribe, () => pendingQuestionSnapshot(runtime), () => "none");
726
+ useAskUserAttentionBlockers(runtime, pendingSnapshot);
727
+ useAskUserAutoOpen(runtime, activeSessionId, pendingSnapshot);
728
+ useAskUserAttentionActions(runtime);
729
+ useAskUserComposerStopCancel(runtime);
730
+ useAskUserPendingRefresh(runtime, { activeSessionId, apiBaseUrl, authHeaders });
442
731
  return /* @__PURE__ */ jsx2(QuestionsRuntimeContext.Provider, { value: runtime, children });
443
732
  }
733
+ function paneQuestionSessionId(runtime, params) {
734
+ const activeSessionId = runtime.activeSessionId ?? null;
735
+ if (activeSessionId && isPaneSessionVisible(runtime, activeSessionId) && hasReadyQuestion(runtime, activeSessionId)) return activeSessionId;
736
+ if (params?.sessionId && isPaneSessionVisible(runtime, params.sessionId)) return params.sessionId;
737
+ return activeSessionId && isPaneSessionVisible(runtime, activeSessionId) ? activeSessionId : null;
738
+ }
739
+ function isPaneSessionVisible(runtime, sessionId) {
740
+ return !runtime.openSessionIds || isSessionOpen(runtime, sessionId);
741
+ }
742
+ function isPaneSessionKnownHidden(runtime, sessionId) {
743
+ return !!runtime.openSessionIds && !isSessionOpen(runtime, sessionId);
744
+ }
745
+ function hasReadyQuestion(runtime, sessionId) {
746
+ const pending = runtime.getPending(sessionId);
747
+ if (pending?.status === "ready") return true;
748
+ return runtime.getPendingHints().some((hint) => hint.sessionId === sessionId && (!hint.status || hint.status === "ready"));
749
+ }
444
750
  function QuestionsPane({ api, params, className }) {
445
751
  const runtime = useQuestionsRuntime();
446
- const pending = useSyncExternalStore(runtime.subscribe, runtime.getPending, runtime.getPending);
752
+ const paneSessionId = paneQuestionSessionId(runtime, params);
753
+ const pending = useSyncExternalStore(runtime.subscribe, () => runtime.getPending(paneSessionId), () => runtime.getPending(paneSessionId));
447
754
  const [closedQuestionId, setClosedQuestionId] = useState2(null);
755
+ const retargetRefreshRef = useRef3(null);
448
756
  const [error, setError] = useState2(null);
449
757
  const [submitting, setSubmitting] = useState2(false);
450
- const paramQuestion = params?.question;
451
- const question = pending ?? (paramQuestion?.questionId === closedQuestionId ? null : paramQuestion) ?? null;
758
+ const question = pending?.questionId === closedQuestionId ? null : pending;
452
759
  const client = useMemo2(() => createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }), [runtime.apiBaseUrl, runtime.authHeaders]);
453
- useEffect2(() => {
760
+ useEffect3(() => {
761
+ if (!params?.sessionId || !isPaneSessionKnownHidden(runtime, params.sessionId)) return;
762
+ const activeSessionId = runtime.activeSessionId ?? null;
763
+ const canShowActiveQuestion = activeSessionId && isPaneSessionVisible(runtime, activeSessionId) && hasReadyQuestion(runtime, activeSessionId);
764
+ if (!canShowActiveQuestion) api.close();
765
+ }, [api, params?.sessionId, runtime]);
766
+ useEffect3(() => {
454
767
  const onStop = (event) => {
455
- const sessionId = event.detail?.sessionId;
456
- if (!question || sessionId && sessionScopedBlockerId(question.sessionId) && sessionId !== question.sessionId) return;
768
+ const detail = event.detail;
769
+ if (!question || !workspaceComposerStopAppliesToSession2(detail, question.sessionId)) return;
457
770
  setClosedQuestionId(question.questionId);
458
- runtime.setPending(null);
771
+ runtime.setPending(null, question.sessionId);
459
772
  api.close();
460
773
  };
461
- window.addEventListener("boring:workspace-composer-stop", onStop);
462
- return () => window.removeEventListener("boring:workspace-composer-stop", onStop);
774
+ window.addEventListener(WORKSPACE_COMPOSER_STOP_EVENT2, onStop);
775
+ return () => window.removeEventListener(WORKSPACE_COMPOSER_STOP_EVENT2, onStop);
463
776
  }, [api, question, runtime]);
464
- useEffect2(() => {
465
- if (question && pending === null && !paramQuestion) api.close();
466
- }, [api, pending, paramQuestion, question]);
777
+ useEffect3(() => {
778
+ if (!paneSessionId) return;
779
+ const targetQuestionId = params?.questionId;
780
+ if (!pending) {
781
+ retargetRefreshRef.current = null;
782
+ void runtime.refreshPending(paneSessionId).catch(() => void 0);
783
+ return;
784
+ }
785
+ if (!targetQuestionId || pending.questionId === targetQuestionId) {
786
+ retargetRefreshRef.current = null;
787
+ return;
788
+ }
789
+ const refreshKey = `${paneSessionId}:${targetQuestionId}`;
790
+ if (retargetRefreshRef.current === refreshKey) return;
791
+ retargetRefreshRef.current = refreshKey;
792
+ void runtime.refreshPending(paneSessionId).catch(() => void 0);
793
+ }, [paneSessionId, params?.questionId, pending, runtime]);
467
794
  return /* @__PURE__ */ jsx2("div", { className: className ? `${className} min-h-0 overflow-hidden` : "h-full min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxs2(Pane, { className: "h-full min-h-0 overflow-hidden border-0 bg-background text-sm", children: [
468
795
  /* @__PURE__ */ jsx2(PaneHeader, { className: "border-b bg-background/95", children: /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsxs2(PaneTitle, { className: "flex items-center gap-2", children: [
469
796
  /* @__PURE__ */ jsx2(HelpCircle, { className: "h-4 w-4 text-muted-foreground" }),
@@ -476,7 +803,7 @@ function QuestionsPane({ api, params, className }) {
476
803
  try {
477
804
  await client.submit(question, values);
478
805
  setClosedQuestionId(question.questionId);
479
- runtime.setPending(null);
806
+ runtime.setPending(null, question.sessionId);
480
807
  api.close();
481
808
  params?.__closeWorkbenchOnDone?.();
482
809
  } catch (err) {
@@ -490,7 +817,7 @@ function QuestionsPane({ api, params, className }) {
490
817
  try {
491
818
  await client.cancel(question);
492
819
  setClosedQuestionId(question.questionId);
493
- runtime.setPending(null);
820
+ runtime.setPending(null, question.sessionId);
494
821
  api.close();
495
822
  params?.__closeWorkbenchOnDone?.();
496
823
  } catch (err) {
@@ -515,7 +842,7 @@ function QuestionsPane({ api, params, className }) {
515
842
  /* @__PURE__ */ jsx2(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsx2(QuestionSubmitButton, { children: question.schema.submitLabel ?? "Send answers" }) })
516
843
  ] })
517
844
  ] })
518
- ] }) }) : null,
845
+ ] }) }, question.questionId) : null,
519
846
  question && question.status !== "ready" ? /* @__PURE__ */ jsx2(PaneBody, { className: "p-5", children: /* @__PURE__ */ jsx2(Notice, { children: /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-2", children: [
520
847
  /* @__PURE__ */ jsx2(XCircle, { className: "h-4 w-4 text-muted-foreground" }),
521
848
  "Question ",
@@ -551,12 +878,12 @@ var askUserPlugin = definePlugin({
551
878
  // No inner kind guard — the workspace's surface registry already
552
879
  // pre-filters by the top-level `kind` field before calling resolve.
553
880
  resolve(request) {
554
- const metaQuestion = typeof request.meta === "object" && request.meta && "question" in request.meta ? request.meta.question : void 0;
881
+ const sessionId = typeof request.meta === "object" && request.meta && typeof request.meta.sessionId === "string" ? request.meta.sessionId : void 0;
555
882
  return {
556
883
  component: ASK_USER_PANEL_ID,
557
884
  id: ASK_USER_PANEL_ID,
558
885
  title: ASK_USER_PANEL_TITLE,
559
- params: { questionId: request.target, question: metaQuestion }
886
+ params: { questionId: request.target, sessionId }
560
887
  };
561
888
  }
562
889
  }