@hachej/boring-ask-user 0.1.63 → 0.1.65

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.
@@ -1,7 +1,17 @@
1
+ import * as _hachej_boring_workspace_plugin from '@hachej/boring-workspace/plugin';
1
2
  import { BoringFrontFactoryWithId } from '@hachej/boring-workspace/plugin';
3
+ import { WorkspaceAttentionBlocker } from '@hachej/boring-workspace';
2
4
 
5
+ declare const INBOX_DEMO_SESSION_ID = "showcase";
6
+ declare function createInboxDemoBlockers(now?: number): WorkspaceAttentionBlocker[];
7
+ declare const inboxDemoPlugin: _hachej_boring_workspace_plugin.BoringFrontFactoryWithId;
8
+
9
+ interface CreateAskUserPluginOptions {
10
+ /** Register the workspace Inbox button/overlay in the app-left rail. */
11
+ appLeftInbox?: boolean;
12
+ }
3
13
  /**
4
- * `BoringFrontFactoryWithId` for the ask-user plugin. Registers
14
+ * Creates a `BoringFrontFactoryWithId` for the ask-user plugin. Registers
5
15
  * (1) a provider that owns the per-app questions runtime (apiBaseUrl,
6
16
  * auth headers, in-memory pending-question store), (2) a "Questions"
7
17
  * panel rendering the pending question form, and (3) a surface
@@ -10,8 +20,10 @@ import { BoringFrontFactoryWithId } from '@hachej/boring-workspace/plugin';
10
20
  * Pass directly to `WorkspaceProvider.plugins`.
11
21
  *
12
22
  * The panel is opened via the surface resolver (kind: ASK_USER_SURFACE_KIND),
13
- * which is how the server-side agent tool triggers it.
23
+ * which is how the server-side agent tool triggers it. The app-left Inbox
24
+ * button is opt-in via `appLeftInbox`.
14
25
  */
26
+ declare function createAskUserPlugin(options?: CreateAskUserPluginOptions): BoringFrontFactoryWithId;
15
27
  declare const askUserPlugin: BoringFrontFactoryWithId;
16
28
 
17
- export { askUserPlugin, askUserPlugin as default };
29
+ export { type CreateAskUserPluginOptions, INBOX_DEMO_SESSION_ID, askUserPlugin, createAskUserPlugin, createInboxDemoBlockers, askUserPlugin as default, inboxDemoPlugin };
@@ -4,13 +4,15 @@
4
4
  import { Button, EmptyState, Notice, Pane, PaneBody, PaneFooter, PaneHeader, PaneTitle } from "@hachej/boring-ui-kit";
5
5
  import {
6
6
  WORKSPACE_COMPOSER_STOP_EVENT as WORKSPACE_COMPOSER_STOP_EVENT2,
7
+ useWorkspaceAttention as useWorkspaceAttention4,
8
+ useWorkspaceContext,
7
9
  workspaceComposerStopAppliesToSession as workspaceComposerStopAppliesToSession2
8
10
  } from "@hachej/boring-workspace";
9
11
  import {
10
- definePlugin
12
+ definePlugin as definePlugin2
11
13
  } from "@hachej/boring-workspace/plugin";
12
- import { HelpCircle, XCircle } from "lucide-react";
13
- import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useSyncExternalStore, useState as useState2 } from "react";
14
+ import { HelpCircle, Inbox as Inbox2, XCircle } from "lucide-react";
15
+ import { useEffect as useEffect5, useMemo as useMemo4, useRef as useRef3, useSyncExternalStore, useState as useState3 } from "react";
14
16
 
15
17
  // src/shared/constants.ts
16
18
  var ASK_USER_PLUGIN_ID = "ask-user";
@@ -573,9 +575,18 @@ function useAskUserAttentionBlockers(runtime, pendingSnapshot) {
573
575
  reason: "ask-user.question",
574
576
  surfaceKind: ASK_USER_SURFACE_KIND,
575
577
  target: hint.questionId,
576
- label: "Answer the question in Questions to continue",
578
+ label: hydrated?.title ?? "Answer the question in Questions to continue",
577
579
  sessionId: hint.sessionId,
578
580
  sessionBadge: { kind: "question", label: "question", tone: "attention", priority: 10 },
581
+ pruneWhenSessionMissing: true,
582
+ focus: { closeWorkbenchLeftPane: true },
583
+ inbox: {
584
+ kind: "question",
585
+ sourceLabel: "question",
586
+ createdAt: hydrated?.createdAt,
587
+ updatedAt: hydrated?.updatedAt ?? hydrated?.createdAt,
588
+ priority: 10
589
+ },
579
590
  actions
580
591
  });
581
592
  }
@@ -655,9 +666,8 @@ function useAskUserPendingRefresh(runtime, options) {
655
666
  const sessionsToHydrate = /* @__PURE__ */ new Set();
656
667
  if (activeSessionId) sessionsToHydrate.add(activeSessionId);
657
668
  for (const hint of hints) {
658
- if (isSessionOpen(runtime, hint.sessionId)) sessionsToHydrate.add(hint.sessionId);
669
+ sessionsToHydrate.add(hint.sessionId);
659
670
  }
660
- if (sessionsToHydrate.size === 0 && hints[0]) sessionsToHydrate.add(hints[0].sessionId);
661
671
  await Promise.all([...sessionsToHydrate].map(async (sessionId) => {
662
672
  try {
663
673
  await runtime.refreshPending(sessionId);
@@ -707,10 +717,454 @@ function hasPendingStateSlot(state) {
707
717
  return !!state && Object.prototype.hasOwnProperty.call(state, ASK_USER_UI_STATE_SLOTS.PENDING);
708
718
  }
709
719
 
710
- // src/front/index.tsx
720
+ // src/front/inbox/InboxOverlay.tsx
721
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo3, useState as useState2 } from "react";
722
+ import { Inbox, MailOpen, X } from "lucide-react";
723
+ import { IconButton } from "@hachej/boring-ui-kit";
724
+ import { emitWorkspaceAttentionAction, useWorkspaceAttention as useWorkspaceAttention2, useAppLeftOverlayChrome, cn as cn3 } from "@hachej/boring-workspace";
725
+
726
+ // src/front/inbox/attentionBlockerAdapter.ts
727
+ import { workspaceAttentionSessionBadgeForBlocker } from "@hachej/boring-workspace";
728
+ var FALLBACK_TIMESTAMP = "1970-01-01T00:00:00.000Z";
729
+ function dateValue(value) {
730
+ if (value === void 0 || value === null) return null;
731
+ const date = value instanceof Date ? value : new Date(value);
732
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
733
+ }
734
+ function blockerTimestamp(blocker) {
735
+ return dateValue(blocker.inbox?.updatedAt) ?? dateValue(blocker.inbox?.createdAt) ?? FALLBACK_TIMESTAMP;
736
+ }
737
+ function blockerKind(blocker) {
738
+ if (blocker.inbox?.kind) return blocker.inbox.kind;
739
+ const badge = workspaceAttentionSessionBadgeForBlocker(blocker);
740
+ if (badge?.kind === "question" || badge?.kind === "review" || badge?.kind === "approval" || badge?.kind === "notice") return badge.kind;
741
+ return "notice";
742
+ }
743
+ function blockerTitle(blocker) {
744
+ return blocker.label || blocker.reason || "Workspace item";
745
+ }
746
+ function blockerSourceLabel(blocker) {
747
+ return blocker.inbox?.sourceLabel ?? workspaceAttentionSessionBadgeForBlocker(blocker)?.label ?? "workspace";
748
+ }
749
+ function isInboxAttentionBlocker(blocker) {
750
+ return !!blocker.inbox;
751
+ }
752
+ function attentionBlockerToInboxItem(blocker) {
753
+ const updatedAt = blockerTimestamp(blocker);
754
+ return {
755
+ id: blocker.id,
756
+ kind: blockerKind(blocker),
757
+ status: "open",
758
+ title: blockerTitle(blocker),
759
+ description: blocker.reason,
760
+ source: { type: "plugin", pluginId: blocker.reason, label: blockerSourceLabel(blocker) },
761
+ sessionId: blocker.sessionId ?? null,
762
+ chatAvailable: blocker.pruneWhenSessionMissing === true && !!blocker.sessionId,
763
+ targetLabel: blocker.target ?? "",
764
+ artifact: blocker.surfaceKind ? {
765
+ type: "surface",
766
+ surfaceKind: blocker.surfaceKind,
767
+ target: blocker.target,
768
+ params: blocker.sessionId ? { sessionId: blocker.sessionId } : void 0
769
+ } : null,
770
+ createdAt: dateValue(blocker.inbox?.createdAt) ?? updatedAt,
771
+ updatedAt,
772
+ priority: blocker.inbox?.priority ?? workspaceAttentionSessionBadgeForBlocker(blocker)?.priority ?? 0,
773
+ actions: blocker.actions?.map((action) => ({ id: action.id, label: action.label })) ?? []
774
+ };
775
+ }
776
+
777
+ // src/front/inbox/InboxFilterBar.tsx
778
+ import { cn } from "@hachej/boring-workspace";
711
779
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
780
+ function InboxFilterBar({
781
+ filter,
782
+ counts,
783
+ onFilterChange
784
+ }) {
785
+ return /* @__PURE__ */ jsx2("div", { className: "flex shrink-0 gap-1 border-b border-border/60 bg-[color:oklch(from_var(--background)_calc(l-0.012)_c_h)] px-4 py-2", children: [
786
+ ["all", "All", counts.all],
787
+ ["questions", "Questions", counts.questions],
788
+ ["reviews", "Reviews", counts.reviews]
789
+ ].map(([id, label2, count]) => /* @__PURE__ */ jsxs2(
790
+ "button",
791
+ {
792
+ type: "button",
793
+ onClick: () => onFilterChange(id),
794
+ className: cn(
795
+ "rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
796
+ filter === id ? "bg-[color:oklch(from_var(--accent)_l_c_h/0.14)] text-[color:var(--accent)]" : "bg-transparent text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground"
797
+ ),
798
+ children: [
799
+ label2,
800
+ " ",
801
+ /* @__PURE__ */ jsx2("span", { className: "ml-1 opacity-70", children: count })
802
+ ]
803
+ },
804
+ id
805
+ )) });
806
+ }
807
+
808
+ // src/front/inbox/InboxSection.tsx
809
+ import { Pin } from "lucide-react";
810
+
811
+ // src/front/inbox/InboxRow.tsx
812
+ import { MessageSquare, Star } from "lucide-react";
813
+ import { cn as cn2 } from "@hachej/boring-workspace";
814
+
815
+ // src/front/inbox/inboxItemModel.ts
816
+ function inboxItemDate(item) {
817
+ const date = new Date(item.updatedAt || item.createdAt);
818
+ return Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date(0) : date;
819
+ }
820
+ function formatInboxTime(item, now = Date.now()) {
821
+ const date = inboxItemDate(item);
822
+ const diff = now - date.getTime();
823
+ if (diff < 6e4) return "now";
824
+ const minutes = Math.floor(diff / 6e4);
825
+ if (minutes < 60) return `${minutes}m`;
826
+ const hours = Math.floor(minutes / 60);
827
+ if (hours < 24) return `${hours}h`;
828
+ const days = Math.floor(hours / 24);
829
+ if (days < 7) return `${days}d`;
830
+ return date.toLocaleDateString(void 0, { month: "short", day: "numeric" });
831
+ }
832
+ function inboxItemSender(item) {
833
+ return item.source.label || item.source.type;
834
+ }
835
+ function filterInboxItems(items, filter) {
836
+ if (filter === "all") return [...items];
837
+ return items.filter((item) => filter === "questions" ? item.kind === "question" : item.kind === "review");
838
+ }
839
+ function sortInboxItems(items) {
840
+ return [...items].sort((a, b) => {
841
+ const byTime = inboxItemDate(b).getTime() - inboxItemDate(a).getTime();
842
+ if (byTime !== 0) return byTime;
843
+ const byPriority = b.priority - a.priority;
844
+ if (byPriority !== 0) return byPriority;
845
+ return a.title.localeCompare(b.title);
846
+ });
847
+ }
848
+ function mergeInboxPinnedState(items, pinnedIds) {
849
+ return items.map((item) => ({ ...item, pinned: pinnedIds.has(item.id) }));
850
+ }
851
+
852
+ // src/front/inbox/InboxRow.tsx
853
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
854
+ function badgeTone(kind) {
855
+ switch (kind) {
856
+ case "review":
857
+ return "bg-amber-500/12 text-amber-700 dark:text-amber-300";
858
+ case "approval":
859
+ return "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300";
860
+ case "notice":
861
+ return "bg-foreground/[0.07] text-muted-foreground";
862
+ default:
863
+ return "bg-[color:var(--accent)]/12 text-[color:var(--accent)]";
864
+ }
865
+ }
866
+ function InboxRow({
867
+ item,
868
+ onTogglePinned,
869
+ onOpenArtifact,
870
+ onOpenChat
871
+ }) {
872
+ const subtitle = [item.sessionId ? `Session ${item.sessionId}` : null, item.targetLabel || null].filter(Boolean).join(" \xB7 ");
873
+ return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3(
874
+ "div",
875
+ {
876
+ role: "button",
877
+ tabIndex: 0,
878
+ className: "group flex h-11 w-full items-center gap-2 overflow-hidden px-4 text-left text-[12px] transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
879
+ onClick: () => onOpenArtifact(item),
880
+ onKeyDown: (event) => {
881
+ if (event.currentTarget !== event.target) return;
882
+ if (event.key !== "Enter" && event.key !== " ") return;
883
+ event.preventDefault();
884
+ onOpenArtifact(item);
885
+ },
886
+ children: [
887
+ /* @__PURE__ */ jsx3("span", { className: "size-2 shrink-0 rounded-full bg-[color:var(--accent)]" }),
888
+ /* @__PURE__ */ jsx3("span", { className: "max-w-28 shrink-0 truncate font-semibold text-foreground", children: inboxItemSender(item) }),
889
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-hidden whitespace-nowrap text-foreground", children: [
890
+ /* @__PURE__ */ jsx3("span", { className: "truncate font-medium", children: item.title }),
891
+ /* @__PURE__ */ jsx3("span", { className: "shrink-0 text-muted-foreground", children: "\u2014" }),
892
+ /* @__PURE__ */ jsx3("span", { className: "truncate text-muted-foreground", children: subtitle || item.description })
893
+ ] }),
894
+ /* @__PURE__ */ jsxs3("span", { className: "flex shrink-0 items-center gap-1.5", children: [
895
+ /* @__PURE__ */ jsx3("span", { className: cn2("rounded-full px-2 py-0.5 text-[10px] font-medium", badgeTone(item.kind)), children: item.kind }),
896
+ /* @__PURE__ */ jsx3("span", { className: "text-[11px] font-medium text-muted-foreground", title: inboxItemDate(item).toLocaleString(), children: formatInboxTime(item) })
897
+ ] }),
898
+ item.sessionId && item.chatAvailable ? /* @__PURE__ */ jsx3(
899
+ "button",
900
+ {
901
+ type: "button",
902
+ "aria-label": `Open chat session ${item.sessionId}`,
903
+ title: "Open chat session",
904
+ className: "grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:bg-foreground/[0.06] hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 group-hover:opacity-100 group-focus-visible:opacity-100",
905
+ onClick: (event) => {
906
+ event.stopPropagation();
907
+ onOpenChat(item);
908
+ },
909
+ children: /* @__PURE__ */ jsx3(MessageSquare, { className: "size-3.5", strokeWidth: 1.75 })
910
+ }
911
+ ) : null,
912
+ /* @__PURE__ */ jsx3(
913
+ "button",
914
+ {
915
+ type: "button",
916
+ "aria-label": `${item.pinned ? "Unpin" : "Pin"} ${item.title}`,
917
+ title: item.pinned ? "Unpin" : "Pin",
918
+ className: cn2(
919
+ "grid size-6 shrink-0 place-items-center rounded-md transition-colors hover:bg-foreground/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
920
+ item.pinned ? "text-[color:var(--accent)] opacity-100" : "text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100"
921
+ ),
922
+ onClick: (event) => {
923
+ event.stopPropagation();
924
+ onTogglePinned(item.id);
925
+ },
926
+ children: /* @__PURE__ */ jsx3(Star, { className: cn2("size-3.5", item.pinned && "fill-current"), strokeWidth: 1.75 })
927
+ }
928
+ )
929
+ ]
930
+ }
931
+ ) });
932
+ }
933
+
934
+ // src/front/inbox/InboxSection.tsx
935
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
936
+ function InboxSection({
937
+ title,
938
+ items,
939
+ onTogglePinned,
940
+ onOpenArtifact,
941
+ onOpenChat
942
+ }) {
943
+ if (items.length === 0) return null;
944
+ return /* @__PURE__ */ jsxs4("section", { className: "py-1", children: [
945
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1 px-4 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/70", children: [
946
+ title === "Pinned" ? /* @__PURE__ */ jsx4(Pin, { className: "size-3", strokeWidth: 1.75 }) : null,
947
+ title
948
+ ] }),
949
+ /* @__PURE__ */ jsx4("ul", { role: "list", className: "divide-y divide-border/60 bg-card/80", children: items.map((item) => /* @__PURE__ */ jsx4(
950
+ InboxRow,
951
+ {
952
+ item,
953
+ onTogglePinned,
954
+ onOpenArtifact,
955
+ onOpenChat
956
+ },
957
+ item.id
958
+ )) })
959
+ ] });
960
+ }
961
+
962
+ // src/front/inbox/WorkspaceInboxShellContext.tsx
963
+ import { useMemo as useMemo2 } from "react";
964
+ import { useWorkspaceShellCapabilities } from "@hachej/boring-workspace";
965
+ function useWorkspaceInboxShell() {
966
+ const shell = useWorkspaceShellCapabilities();
967
+ return useMemo2(() => ({
968
+ openInboxArtifact: (item) => shell.openArtifact(item.artifact, {
969
+ // Row clicks open the workspace artifact/question only. The explicit chat
970
+ // icon owns chat opening; otherwise ask-user inbox rows jump to chat
971
+ // instead of showing the Questions form.
972
+ sessionId: null,
973
+ title: item.title,
974
+ instanceId: item.id
975
+ }),
976
+ openDetachedChat: shell.openDetachedChat
977
+ }), [shell]);
978
+ }
979
+
980
+ // src/front/inbox/InboxOverlay.tsx
981
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
982
+ function readPinnedIds(storageKey) {
983
+ if (!storageKey) return /* @__PURE__ */ new Set();
984
+ try {
985
+ const raw = globalThis.localStorage?.getItem(storageKey);
986
+ const parsed = raw ? JSON.parse(raw) : null;
987
+ return new Set(Array.isArray(parsed?.ids) ? parsed.ids.filter((id) => typeof id === "string" && id.length > 0) : []);
988
+ } catch {
989
+ return /* @__PURE__ */ new Set();
990
+ }
991
+ }
992
+ function writePinnedIds(storageKey, ids) {
993
+ if (!storageKey) return;
994
+ try {
995
+ const values = Array.from(ids);
996
+ if (values.length === 0) globalThis.localStorage?.removeItem(storageKey);
997
+ else globalThis.localStorage?.setItem(storageKey, JSON.stringify({ ids: values }));
998
+ } catch {
999
+ }
1000
+ }
1001
+ function InboxOverlay({ onClose, pinStorageKey }) {
1002
+ const { headerInsetStart, headerInsetEnd } = useAppLeftOverlayChrome();
1003
+ const { blockers } = useWorkspaceAttention2();
1004
+ const shell = useWorkspaceInboxShell();
1005
+ const [filter, setFilter] = useState2("all");
1006
+ const [shellError, setShellError] = useState2(null);
1007
+ const [selectedItemId, setSelectedItemId] = useState2(null);
1008
+ const [pinnedIds, setPinnedIds] = useState2(() => readPinnedIds(pinStorageKey));
1009
+ useEffect3(() => {
1010
+ setPinnedIds(readPinnedIds(pinStorageKey));
1011
+ }, [pinStorageKey]);
1012
+ const sorted = useMemo3(() => sortInboxItems(blockers.filter(isInboxAttentionBlocker).map(attentionBlockerToInboxItem)), [blockers]);
1013
+ const filtered = useMemo3(() => filterInboxItems(sorted, filter), [filter, sorted]);
1014
+ const items = useMemo3(() => mergeInboxPinnedState(filtered, pinnedIds), [filtered, pinnedIds]);
1015
+ const pinnedItems = useMemo3(() => items.filter((item) => item.pinned), [items]);
1016
+ const unpinnedItems = useMemo3(() => items.filter((item) => !item.pinned), [items]);
1017
+ const selectedItem = useMemo3(() => items.find((item) => item.id === selectedItemId) ?? null, [items, selectedItemId]);
1018
+ const selectedBlocker = useMemo3(() => selectedItem ? blockers.find((blocker) => blocker.id === selectedItem.id) : void 0, [blockers, selectedItem]);
1019
+ const counts = useMemo3(() => ({
1020
+ all: sorted.length,
1021
+ questions: filterInboxItems(sorted, "questions").length,
1022
+ reviews: filterInboxItems(sorted, "reviews").length
1023
+ }), [sorted]);
1024
+ const togglePinned = useCallback2((id) => {
1025
+ setPinnedIds((current) => {
1026
+ const next = new Set(current);
1027
+ if (next.has(id)) next.delete(id);
1028
+ else next.add(id);
1029
+ writePinnedIds(pinStorageKey, next);
1030
+ return next;
1031
+ });
1032
+ }, [pinStorageKey]);
1033
+ const handleShellResult = useCallback2((result) => {
1034
+ setShellError(result.success ? null : result.message);
1035
+ }, []);
1036
+ const openArtifact = useCallback2((item) => {
1037
+ if (!item.artifact) {
1038
+ setSelectedItemId(item.id);
1039
+ setShellError(null);
1040
+ return;
1041
+ }
1042
+ const result = shell.openInboxArtifact(item);
1043
+ handleShellResult(result);
1044
+ setSelectedItemId(result.success ? null : item.id);
1045
+ }, [handleShellResult, shell]);
1046
+ const openChat = useCallback2((item) => {
1047
+ if (!item.sessionId) return;
1048
+ handleShellResult(shell.openDetachedChat(item.sessionId, { title: item.title }));
1049
+ }, [handleShellResult, shell]);
1050
+ const handleDetailAction = useCallback2((actionId) => {
1051
+ if (!selectedItem || !selectedBlocker) return;
1052
+ emitWorkspaceAttentionAction({ blockerId: selectedBlocker.id, actionId, blocker: selectedBlocker, sessionId: selectedItem.sessionId ?? void 0 });
1053
+ if (actionId === "open") {
1054
+ if (selectedItem.artifact) openArtifact(selectedItem);
1055
+ else setShellError("This inbox item has no artifact target.");
1056
+ }
1057
+ }, [openArtifact, selectedBlocker, selectedItem]);
1058
+ return /* @__PURE__ */ jsxs5("div", { "data-boring-workspace-part": "inbox-overlay", className: "flex h-full min-h-0 flex-col bg-background", children: [
1059
+ /* @__PURE__ */ jsxs5("header", { className: cn3(
1060
+ "flex h-12 shrink-0 items-center justify-between border-b border-border/60",
1061
+ headerInsetStart ? "pl-12" : "pl-4",
1062
+ headerInsetEnd ? "pr-16" : "pr-4"
1063
+ ), children: [
1064
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1065
+ /* @__PURE__ */ jsx5("span", { className: "grid size-7 place-items-center rounded-lg bg-[color:oklch(from_var(--accent)_l_c_h/0.14)] text-[color:var(--accent)]", children: /* @__PURE__ */ jsx5(Inbox, { className: "h-4 w-4", strokeWidth: 1.75, "aria-hidden": "true" }) }),
1066
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1067
+ /* @__PURE__ */ jsx5("h2", { className: "truncate text-sm font-semibold tracking-tight text-foreground", children: "Inbox" }),
1068
+ /* @__PURE__ */ jsx5("p", { className: "truncate text-xs text-muted-foreground", children: "Questions, reviews, and owner decisions" })
1069
+ ] })
1070
+ ] }),
1071
+ /* @__PURE__ */ jsx5("div", { className: "flex shrink-0 items-center gap-0.5", children: /* @__PURE__ */ jsx5(IconButton, { type: "button", variant: "ghost", size: "icon-xs", onClick: onClose, "aria-label": "Close inbox", title: "Close", className: "text-muted-foreground hover:text-foreground", children: /* @__PURE__ */ jsx5(X, { className: "size-3", strokeWidth: 1.75 }) }) })
1072
+ ] }),
1073
+ /* @__PURE__ */ jsx5(InboxFilterBar, { filter, counts, onFilterChange: setFilter }),
1074
+ shellError ? /* @__PURE__ */ jsx5("div", { className: "border-b border-border/60 bg-destructive/10 px-4 py-2 text-xs text-destructive", children: shellError }) : null,
1075
+ selectedItem ? /* @__PURE__ */ jsxs5("div", { className: "border-b border-border/60 bg-background px-4 py-3 text-xs", children: [
1076
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-start justify-between gap-3", children: [
1077
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1078
+ /* @__PURE__ */ jsx5("div", { className: "truncate font-medium text-foreground", children: selectedItem.title }),
1079
+ /* @__PURE__ */ jsx5("div", { className: "mt-1 line-clamp-2 text-muted-foreground", children: selectedItem.description })
1080
+ ] }),
1081
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "shrink-0 text-muted-foreground hover:text-foreground", onClick: () => setSelectedItemId(null), children: "Close" })
1082
+ ] }),
1083
+ selectedItem.actions.length > 0 && selectedBlocker ? /* @__PURE__ */ jsx5("div", { className: "mt-3 flex flex-wrap gap-1.5", children: selectedItem.actions.map((action) => /* @__PURE__ */ jsx5(
1084
+ "button",
1085
+ {
1086
+ type: "button",
1087
+ className: "rounded-md border border-border/80 bg-background px-2 py-1 text-[11px] font-medium hover:bg-muted/60",
1088
+ onClick: () => handleDetailAction(action.id),
1089
+ children: action.label
1090
+ },
1091
+ action.id
1092
+ )) }) : null
1093
+ ] }) : null,
1094
+ /* @__PURE__ */ jsx5("div", { className: "boring-scrollbar-discreet min-h-0 flex-1 overflow-y-auto bg-[color:oklch(from_var(--background)_calc(l-0.012)_c_h)] py-2", "aria-live": "polite", children: items.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "flex h-full min-h-[180px] items-center justify-center px-6 text-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsxs5("div", { children: [
1095
+ /* @__PURE__ */ jsx5(MailOpen, { className: "mx-auto mb-3 size-8 text-muted-foreground/70", strokeWidth: 1.75 }),
1096
+ /* @__PURE__ */ jsx5("div", { className: "font-medium text-foreground/80", children: "Inbox zero" }),
1097
+ /* @__PURE__ */ jsx5("p", { className: "mt-1 max-w-xs", children: "When plugins or external harnesses ask for a decision, it appears here." })
1098
+ ] }) }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1099
+ /* @__PURE__ */ jsx5(InboxSection, { title: "Pinned", items: pinnedItems, onTogglePinned: togglePinned, onOpenArtifact: openArtifact, onOpenChat: openChat }),
1100
+ /* @__PURE__ */ jsx5(InboxSection, { title: "Inbox", items: unpinnedItems, onTogglePinned: togglePinned, onOpenArtifact: openArtifact, onOpenChat: openChat })
1101
+ ] }) })
1102
+ ] });
1103
+ }
1104
+
1105
+ // src/front/inbox/examples/inboxDemoPlugin.tsx
1106
+ import { useEffect as useEffect4 } from "react";
1107
+ import { WORKSPACE_OPEN_PATH_SURFACE_KIND, useWorkspaceAttention as useWorkspaceAttention3 } from "@hachej/boring-workspace";
1108
+ import { definePlugin } from "@hachej/boring-workspace/plugin";
1109
+ var INBOX_DEMO_SESSION_ID = "showcase";
1110
+ function createInboxDemoBlockers(now = Date.now()) {
1111
+ return [
1112
+ {
1113
+ id: "demo-question-deploy",
1114
+ reason: "ask-user.question",
1115
+ label: "Pick the deploy target for the release smoke",
1116
+ sessionId: INBOX_DEMO_SESSION_ID,
1117
+ pruneWhenSessionMissing: true,
1118
+ target: "README.md",
1119
+ surfaceKind: WORKSPACE_OPEN_PATH_SURFACE_KIND,
1120
+ inbox: {
1121
+ kind: "question",
1122
+ sourceLabel: "question",
1123
+ createdAt: now - 4 * 6e4,
1124
+ priority: 10
1125
+ },
1126
+ sessionBadge: { kind: "question", label: "question", tone: "attention", priority: 10 },
1127
+ actions: [{ id: "answer", label: "Answer" }, { id: "dismiss", label: "Dismiss" }]
1128
+ },
1129
+ {
1130
+ id: "demo-review-ui",
1131
+ reason: "external-review.request",
1132
+ label: "Review Codex notes on workspace inbox flow",
1133
+ sessionId: "codex-42",
1134
+ target: "analysis.py",
1135
+ surfaceKind: WORKSPACE_OPEN_PATH_SURFACE_KIND,
1136
+ inbox: {
1137
+ kind: "review",
1138
+ sourceLabel: "review",
1139
+ createdAt: now - 2 * 60 * 6e4,
1140
+ priority: 8
1141
+ },
1142
+ sessionBadge: { kind: "review", label: "review", tone: "warning", priority: 8 },
1143
+ actions: [{ id: "open", label: "Open review" }]
1144
+ }
1145
+ ];
1146
+ }
1147
+ function InboxDemoAttentionSeed() {
1148
+ const { addBlocker, removeBlocker } = useWorkspaceAttention3();
1149
+ useEffect4(() => {
1150
+ const blockers = createInboxDemoBlockers();
1151
+ for (const blocker of blockers) addBlocker(blocker);
1152
+ return () => blockers.forEach((blocker) => removeBlocker(blocker.id));
1153
+ }, [addBlocker, removeBlocker]);
1154
+ return null;
1155
+ }
1156
+ var inboxDemoPlugin = definePlugin({
1157
+ id: "inbox-demo-playground",
1158
+ label: "Inbox Demo Playground",
1159
+ setup(api) {
1160
+ api.registerBinding({ id: "inbox-demo-attention-seed", component: InboxDemoAttentionSeed });
1161
+ }
1162
+ });
1163
+
1164
+ // src/front/index.tsx
1165
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
712
1166
  function AskUserProvider({ apiBaseUrl, authHeaders, activeSessionId, openSessionIds, children }) {
713
- const runtime = useMemo2(() => ({
1167
+ const runtime = useMemo4(() => ({
714
1168
  ...sharedQuestionsStore,
715
1169
  apiBaseUrl,
716
1170
  authHeaders,
@@ -728,11 +1182,14 @@ function AskUserProvider({ apiBaseUrl, authHeaders, activeSessionId, openSession
728
1182
  useAskUserAttentionActions(runtime);
729
1183
  useAskUserComposerStopCancel(runtime);
730
1184
  useAskUserPendingRefresh(runtime, { activeSessionId, apiBaseUrl, authHeaders });
731
- return /* @__PURE__ */ jsx2(QuestionsRuntimeContext.Provider, { value: runtime, children });
1185
+ return /* @__PURE__ */ jsx6(QuestionsRuntimeContext.Provider, { value: runtime, children });
732
1186
  }
733
1187
  function paneQuestionSessionId(runtime, params) {
734
1188
  const activeSessionId = runtime.activeSessionId ?? null;
735
1189
  if (activeSessionId && isPaneSessionVisible(runtime, activeSessionId) && hasReadyQuestion(runtime, activeSessionId)) return activeSessionId;
1190
+ if (params?.sessionId && hasReadyQuestion(runtime, params.sessionId)) return params.sessionId;
1191
+ const hintedSessionId = runtime.getPendingHints().find((hint) => !hint.status || hint.status === "ready")?.sessionId;
1192
+ if (hintedSessionId) return hintedSessionId;
736
1193
  if (params?.sessionId && isPaneSessionVisible(runtime, params.sessionId)) return params.sessionId;
737
1194
  return activeSessionId && isPaneSessionVisible(runtime, activeSessionId) ? activeSessionId : null;
738
1195
  }
@@ -747,23 +1204,46 @@ function hasReadyQuestion(runtime, sessionId) {
747
1204
  if (pending?.status === "ready") return true;
748
1205
  return runtime.getPendingHints().some((hint) => hint.sessionId === sessionId && (!hint.status || hint.status === "ready"));
749
1206
  }
1207
+ function InboxCountBadge() {
1208
+ const { blockers } = useWorkspaceAttention4();
1209
+ const count = blockers.filter(isInboxAttentionBlocker).length;
1210
+ if (count === 0) return null;
1211
+ const label2 = count > 99 ? "99+" : String(count);
1212
+ return /* @__PURE__ */ jsx6(
1213
+ "span",
1214
+ {
1215
+ "data-boring-workspace-part": "app-left-inbox-count",
1216
+ "aria-label": `${count} inbox item${count === 1 ? "" : "s"}`,
1217
+ className: "inline-flex min-w-5 items-center justify-center rounded-full bg-[color:var(--accent)] px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white shadow-sm",
1218
+ children: label2
1219
+ }
1220
+ );
1221
+ }
1222
+ function AskUserInboxOverlay({ onClose }) {
1223
+ const { workspaceId } = useWorkspaceContext();
1224
+ return /* @__PURE__ */ jsx6(InboxOverlay, { onClose, pinStorageKey: `boring-workspace:inbox-pins:${workspaceId ?? "workspace"}` });
1225
+ }
750
1226
  function QuestionsPane({ api, params, className }) {
751
1227
  const runtime = useQuestionsRuntime();
1228
+ useSyncExternalStore(runtime.subscribe, () => pendingQuestionSnapshot(runtime), () => "none");
752
1229
  const paneSessionId = paneQuestionSessionId(runtime, params);
753
1230
  const pending = useSyncExternalStore(runtime.subscribe, () => runtime.getPending(paneSessionId), () => runtime.getPending(paneSessionId));
754
- const [closedQuestionId, setClosedQuestionId] = useState2(null);
1231
+ const [closedQuestionId, setClosedQuestionId] = useState3(null);
755
1232
  const retargetRefreshRef = useRef3(null);
756
- const [error, setError] = useState2(null);
757
- const [submitting, setSubmitting] = useState2(false);
1233
+ const [error, setError] = useState3(null);
1234
+ const [submitting, setSubmitting] = useState3(false);
758
1235
  const question = pending?.questionId === closedQuestionId ? null : pending;
759
- const client = useMemo2(() => createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }), [runtime.apiBaseUrl, runtime.authHeaders]);
760
- useEffect3(() => {
1236
+ const client = useMemo4(() => createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }), [runtime.apiBaseUrl, runtime.authHeaders]);
1237
+ useEffect5(() => {
761
1238
  if (!params?.sessionId || !isPaneSessionKnownHidden(runtime, params.sessionId)) return;
1239
+ if (hasReadyQuestion(runtime, params.sessionId)) return;
1240
+ const hints = runtime.getPendingHints();
1241
+ if (hints.length === 0) return;
762
1242
  const activeSessionId = runtime.activeSessionId ?? null;
763
1243
  const canShowActiveQuestion = activeSessionId && isPaneSessionVisible(runtime, activeSessionId) && hasReadyQuestion(runtime, activeSessionId);
764
1244
  if (!canShowActiveQuestion) api.close();
765
1245
  }, [api, params?.sessionId, runtime]);
766
- useEffect3(() => {
1246
+ useEffect5(() => {
767
1247
  const onStop = (event) => {
768
1248
  const detail = event.detail;
769
1249
  if (!question || !workspaceComposerStopAppliesToSession2(detail, question.sessionId)) return;
@@ -774,7 +1254,7 @@ function QuestionsPane({ api, params, className }) {
774
1254
  window.addEventListener(WORKSPACE_COMPOSER_STOP_EVENT2, onStop);
775
1255
  return () => window.removeEventListener(WORKSPACE_COMPOSER_STOP_EVENT2, onStop);
776
1256
  }, [api, question, runtime]);
777
- useEffect3(() => {
1257
+ useEffect5(() => {
778
1258
  if (!paneSessionId) return;
779
1259
  const targetQuestionId = params?.questionId;
780
1260
  if (!pending) {
@@ -791,13 +1271,13 @@ function QuestionsPane({ api, params, className }) {
791
1271
  retargetRefreshRef.current = refreshKey;
792
1272
  void runtime.refreshPending(paneSessionId).catch(() => void 0);
793
1273
  }, [paneSessionId, params?.questionId, pending, runtime]);
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: [
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: [
796
- /* @__PURE__ */ jsx2(HelpCircle, { className: "h-4 w-4 text-muted-foreground" }),
1274
+ return /* @__PURE__ */ jsx6("div", { className: className ? `${className} min-h-0 overflow-hidden` : "h-full min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxs6(Pane, { className: "h-full min-h-0 overflow-hidden border-0 bg-background text-sm", children: [
1275
+ /* @__PURE__ */ jsx6(PaneHeader, { className: "border-b bg-background/95", children: /* @__PURE__ */ jsx6("div", { children: /* @__PURE__ */ jsxs6(PaneTitle, { className: "flex items-center gap-2", children: [
1276
+ /* @__PURE__ */ jsx6(HelpCircle, { className: "h-4 w-4 text-muted-foreground" }),
797
1277
  " Agent needs input"
798
1278
  ] }) }) }),
799
- !question ? /* @__PURE__ */ jsx2(PaneBody, { className: "overflow-auto p-4", children: /* @__PURE__ */ jsx2(EmptyState, { icon: /* @__PURE__ */ jsx2(HelpCircle, { className: "h-5 w-5" }), title: "No pending questions", description: "When the agent needs a decision, the form will appear here.", className: "border border-dashed bg-muted/20" }) }) : null,
800
- question?.status === "ready" && question.schema ? /* @__PURE__ */ jsx2(QuestionFormProvider, { schema: question.schema, submitting, onSubmit: async (values) => {
1279
+ !question ? /* @__PURE__ */ jsx6(PaneBody, { className: "overflow-auto p-4", children: /* @__PURE__ */ jsx6(EmptyState, { icon: /* @__PURE__ */ jsx6(HelpCircle, { className: "h-5 w-5" }), title: "No pending questions", description: "When the agent needs a decision, the form will appear here.", className: "border border-dashed bg-muted/20" }) }) : null,
1280
+ question?.status === "ready" && question.schema ? /* @__PURE__ */ jsx6(QuestionFormProvider, { schema: question.schema, submitting, onSubmit: async (values) => {
801
1281
  setSubmitting(true);
802
1282
  setError(null);
803
1283
  try {
@@ -825,72 +1305,89 @@ function QuestionsPane({ api, params, className }) {
825
1305
  } finally {
826
1306
  setSubmitting(false);
827
1307
  }
828
- }, children: /* @__PURE__ */ jsxs2(QuestionForm, { className: "flex min-h-0 flex-1 flex-col", children: [
829
- /* @__PURE__ */ jsx2(PaneBody, { className: "overflow-auto p-4", children: /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
830
- /* @__PURE__ */ jsxs2("section", { className: "rounded-md border border-border/60 bg-muted/30 p-4", children: [
831
- /* @__PURE__ */ jsx2("div", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Waiting for answer" }),
832
- /* @__PURE__ */ jsx2("h2", { className: "mt-2 text-balance text-sm font-semibold leading-5 text-foreground", children: question.title ?? "Question" }),
833
- question.context ? /* @__PURE__ */ jsx2("p", { className: "mt-2 max-w-prose text-sm leading-6 text-muted-foreground", children: question.context }) : null
1308
+ }, children: /* @__PURE__ */ jsxs6(QuestionForm, { className: "flex min-h-0 flex-1 flex-col", children: [
1309
+ /* @__PURE__ */ jsx6(PaneBody, { className: "overflow-auto p-4", children: /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
1310
+ /* @__PURE__ */ jsxs6("section", { className: "rounded-md border border-border/60 bg-muted/30 p-4", children: [
1311
+ /* @__PURE__ */ jsx6("div", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Waiting for answer" }),
1312
+ /* @__PURE__ */ jsx6("h2", { className: "mt-2 text-balance text-sm font-semibold leading-5 text-foreground", children: question.title ?? "Question" }),
1313
+ question.context ? /* @__PURE__ */ jsx6("p", { className: "mt-2 max-w-prose text-sm leading-6 text-muted-foreground", children: question.context }) : null
834
1314
  ] }),
835
- /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: /* @__PURE__ */ jsx2(QuestionFields, {}) }),
836
- error ? /* @__PURE__ */ jsx2(Notice, { tone: "destructive", role: "alert", children: error }) : null
1315
+ /* @__PURE__ */ jsx6("div", { className: "space-y-4", children: /* @__PURE__ */ jsx6(QuestionFields, {}) }),
1316
+ error ? /* @__PURE__ */ jsx6(Notice, { tone: "destructive", role: "alert", children: error }) : null
837
1317
  ] }) }),
838
- /* @__PURE__ */ jsxs2(PaneFooter, { className: "justify-between border-t bg-background px-4 py-3", children: [
839
- /* @__PURE__ */ jsx2("p", { className: "min-w-0 text-xs text-muted-foreground", children: "Sends answers and closes the pane." }),
840
- /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
841
- /* @__PURE__ */ jsx2(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsx2(QuestionCancelButton, { children: "Cancel" }) }),
842
- /* @__PURE__ */ jsx2(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsx2(QuestionSubmitButton, { children: question.schema.submitLabel ?? "Send answers" }) })
1318
+ /* @__PURE__ */ jsxs6(PaneFooter, { className: "justify-between border-t bg-background px-4 py-3", children: [
1319
+ /* @__PURE__ */ jsx6("p", { className: "min-w-0 text-xs text-muted-foreground", children: "Sends answers and closes the pane." }),
1320
+ /* @__PURE__ */ jsxs6("div", { className: "flex gap-2", children: [
1321
+ /* @__PURE__ */ jsx6(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsx6(QuestionCancelButton, { children: "Cancel" }) }),
1322
+ /* @__PURE__ */ jsx6(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsx6(QuestionSubmitButton, { children: question.schema.submitLabel ?? "Send answers" }) })
843
1323
  ] })
844
1324
  ] })
845
1325
  ] }) }, question.questionId) : null,
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: [
847
- /* @__PURE__ */ jsx2(XCircle, { className: "h-4 w-4 text-muted-foreground" }),
1326
+ question && question.status !== "ready" ? /* @__PURE__ */ jsx6(PaneBody, { className: "p-5", children: /* @__PURE__ */ jsx6(Notice, { children: /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2", children: [
1327
+ /* @__PURE__ */ jsx6(XCircle, { className: "h-4 w-4 text-muted-foreground" }),
848
1328
  "Question ",
849
1329
  question.status
850
1330
  ] }) }) }) : null
851
1331
  ] }) });
852
1332
  }
853
- var askUserPlugin = definePlugin({
854
- id: ASK_USER_PLUGIN_ID,
855
- label: ASK_USER_PANEL_TITLE,
856
- providers: [
857
- {
858
- id: `${ASK_USER_PLUGIN_ID}.provider`,
859
- component: AskUserProvider
860
- }
861
- ],
862
- panels: [
863
- {
864
- id: ASK_USER_PANEL_ID,
865
- label: ASK_USER_PANEL_TITLE,
866
- icon: HelpCircle,
867
- component: QuestionsPane,
868
- placement: "center",
869
- source: "builtin",
870
- chromeless: true
871
- }
872
- ],
873
- surfaceResolvers: [
874
- {
875
- id: `${ASK_USER_PLUGIN_ID}.surface`,
876
- kind: ASK_USER_SURFACE_KIND,
877
- source: "builtin",
878
- // No inner kind guard — the workspace's surface registry already
879
- // pre-filters by the top-level `kind` field before calling resolve.
880
- resolve(request) {
881
- const sessionId = typeof request.meta === "object" && request.meta && typeof request.meta.sessionId === "string" ? request.meta.sessionId : void 0;
882
- return {
883
- component: ASK_USER_PANEL_ID,
884
- id: ASK_USER_PANEL_ID,
885
- title: ASK_USER_PANEL_TITLE,
886
- params: { questionId: request.target, sessionId }
887
- };
1333
+ function createAskUserPlugin(options = {}) {
1334
+ return definePlugin2({
1335
+ id: ASK_USER_PLUGIN_ID,
1336
+ label: ASK_USER_PANEL_TITLE,
1337
+ providers: [
1338
+ {
1339
+ id: `${ASK_USER_PLUGIN_ID}.provider`,
1340
+ component: AskUserProvider
888
1341
  }
889
- }
890
- ]
891
- });
1342
+ ],
1343
+ panels: [
1344
+ {
1345
+ id: ASK_USER_PANEL_ID,
1346
+ label: ASK_USER_PANEL_TITLE,
1347
+ icon: HelpCircle,
1348
+ component: QuestionsPane,
1349
+ placement: "center",
1350
+ source: "builtin",
1351
+ chromeless: true
1352
+ }
1353
+ ],
1354
+ appLeftActions: options.appLeftInbox ? [
1355
+ {
1356
+ id: "inbox",
1357
+ label: "Inbox",
1358
+ icon: Inbox2,
1359
+ trailing: InboxCountBadge,
1360
+ overlay: AskUserInboxOverlay,
1361
+ order: 10
1362
+ }
1363
+ ] : [],
1364
+ surfaceResolvers: [
1365
+ {
1366
+ id: `${ASK_USER_PLUGIN_ID}.surface`,
1367
+ kind: ASK_USER_SURFACE_KIND,
1368
+ source: "builtin",
1369
+ // No inner kind guard — the workspace's surface registry already
1370
+ // pre-filters by the top-level `kind` field before calling resolve.
1371
+ resolve(request) {
1372
+ const sessionId = typeof request.meta === "object" && request.meta && typeof request.meta.sessionId === "string" ? request.meta.sessionId : void 0;
1373
+ return {
1374
+ component: ASK_USER_PANEL_ID,
1375
+ id: ASK_USER_PANEL_ID,
1376
+ title: ASK_USER_PANEL_TITLE,
1377
+ params: { questionId: request.target, sessionId }
1378
+ };
1379
+ }
1380
+ }
1381
+ ]
1382
+ });
1383
+ }
1384
+ var askUserPlugin = createAskUserPlugin();
892
1385
  var front_default = askUserPlugin;
893
1386
  export {
1387
+ INBOX_DEMO_SESSION_ID,
894
1388
  askUserPlugin,
895
- front_default as default
1389
+ createAskUserPlugin,
1390
+ createInboxDemoBlockers,
1391
+ front_default as default,
1392
+ inboxDemoPlugin
896
1393
  };
@@ -358,6 +358,7 @@ var AskUserRuntime = class {
358
358
  }
359
359
  async ask(request, signal) {
360
360
  const ownerPrincipalId = request.ownerPrincipalId ?? this.ownerPrincipalId;
361
+ await this.abandonOrphanedPending([request.sessionId]);
361
362
  this.assertAllowed(request.sessionId, ownerPrincipalId);
362
363
  const question = this.createQuestion({ ...request, ownerPrincipalId });
363
364
  const parsed = AskUserFormSchemaSchema.safeParse(request.schema);
@@ -2961,13 +2961,13 @@ declare const AskUserAnswerSchema: z.ZodObject<{
2961
2961
  values: z.ZodRecord<z.ZodEffects<z.ZodString, string, string>, z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">, z.ZodBoolean, z.ZodNumber, z.ZodNull]>>;
2962
2962
  submittedAt: z.ZodString;
2963
2963
  }, "strict", z.ZodTypeAny, {
2964
- values: Record<string, string | number | boolean | string[] | null>;
2965
2964
  sessionId: string;
2965
+ values: Record<string, string | number | boolean | string[] | null>;
2966
2966
  questionId: string;
2967
2967
  submittedAt: string;
2968
2968
  }, {
2969
- values: Record<string, string | number | boolean | string[] | null>;
2970
2969
  sessionId: string;
2970
+ values: Record<string, string | number | boolean | string[] | null>;
2971
2971
  questionId: string;
2972
2972
  submittedAt: string;
2973
2973
  }>;
@@ -2979,28 +2979,28 @@ declare const QuestionsSubmitCommandSchema: z.ZodObject<{
2979
2979
  questionId: z.ZodString;
2980
2980
  sessionId: z.ZodString;
2981
2981
  }, "strict", z.ZodTypeAny, {
2982
- values: Record<string, string | number | boolean | string[] | null>;
2983
2982
  sessionId: string;
2983
+ values: Record<string, string | number | boolean | string[] | null>;
2984
2984
  questionId: string;
2985
2985
  answerToken: string;
2986
2986
  }, {
2987
- values: Record<string, string | number | boolean | string[] | null>;
2988
2987
  sessionId: string;
2988
+ values: Record<string, string | number | boolean | string[] | null>;
2989
2989
  questionId: string;
2990
2990
  answerToken: string;
2991
2991
  }>;
2992
2992
  }, "strict", z.ZodTypeAny, {
2993
2993
  params: {
2994
- values: Record<string, string | number | boolean | string[] | null>;
2995
2994
  sessionId: string;
2995
+ values: Record<string, string | number | boolean | string[] | null>;
2996
2996
  questionId: string;
2997
2997
  answerToken: string;
2998
2998
  };
2999
2999
  kind: "questions.submit";
3000
3000
  }, {
3001
3001
  params: {
3002
- values: Record<string, string | number | boolean | string[] | null>;
3003
3002
  sessionId: string;
3003
+ values: Record<string, string | number | boolean | string[] | null>;
3004
3004
  questionId: string;
3005
3005
  answerToken: string;
3006
3006
  };
@@ -3044,28 +3044,28 @@ declare const QuestionsCommandSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObje
3044
3044
  questionId: z.ZodString;
3045
3045
  sessionId: z.ZodString;
3046
3046
  }, "strict", z.ZodTypeAny, {
3047
- values: Record<string, string | number | boolean | string[] | null>;
3048
3047
  sessionId: string;
3048
+ values: Record<string, string | number | boolean | string[] | null>;
3049
3049
  questionId: string;
3050
3050
  answerToken: string;
3051
3051
  }, {
3052
- values: Record<string, string | number | boolean | string[] | null>;
3053
3052
  sessionId: string;
3053
+ values: Record<string, string | number | boolean | string[] | null>;
3054
3054
  questionId: string;
3055
3055
  answerToken: string;
3056
3056
  }>;
3057
3057
  }, "strict", z.ZodTypeAny, {
3058
3058
  params: {
3059
- values: Record<string, string | number | boolean | string[] | null>;
3060
3059
  sessionId: string;
3060
+ values: Record<string, string | number | boolean | string[] | null>;
3061
3061
  questionId: string;
3062
3062
  answerToken: string;
3063
3063
  };
3064
3064
  kind: "questions.submit";
3065
3065
  }, {
3066
3066
  params: {
3067
- values: Record<string, string | number | boolean | string[] | null>;
3068
3067
  sessionId: string;
3068
+ values: Record<string, string | number | boolean | string[] | null>;
3069
3069
  questionId: string;
3070
3070
  answerToken: string;
3071
3071
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-ask-user",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -46,28 +46,28 @@
46
46
  "fastify": "^5.3.3",
47
47
  "react": "^18.0.0 || ^19.0.0",
48
48
  "react-dom": "^18.0.0 || ^19.0.0",
49
- "@hachej/boring-workspace": "0.1.63"
49
+ "@hachej/boring-workspace": "0.1.65"
50
50
  },
51
51
  "dependencies": {
52
- "lucide-react": "^1.8.0",
52
+ "lucide-react": "^1.21.0",
53
53
  "zod": "^3.23.0",
54
- "@hachej/boring-ui-kit": "0.1.63"
54
+ "@hachej/boring-ui-kit": "0.1.65"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@testing-library/jest-dom": "^6.9.1",
58
58
  "@testing-library/react": "^16.3.0",
59
- "@types/node": "^22.0.0",
60
- "@types/react": "^19.0.0",
59
+ "@types/node": "^22.20.0",
60
+ "@types/react": "^19.2.17",
61
61
  "@types/react-dom": "^19.0.0",
62
62
  "@vitejs/plugin-react": "^4.0.0",
63
- "fastify": "^5.3.3",
64
- "jsdom": "^29.0.2",
65
- "react": "^19.0.0",
66
- "react-dom": "^19.0.0",
63
+ "fastify": "^5.9.0",
64
+ "jsdom": "^29.1.1",
65
+ "react": "^19.2.7",
66
+ "react-dom": "^19.2.7",
67
67
  "tsup": "^8.4.0",
68
- "typescript": "~5.9.3",
69
- "vitest": "^3.2.6",
70
- "@hachej/boring-workspace": "0.1.63"
68
+ "typescript": "~6.0.3",
69
+ "vitest": "^4.1.9",
70
+ "@hachej/boring-workspace": "0.1.65"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "fastify": {